1 准备
- 硬件准备:STM32F407ZG、ST-Link、USB 转串口模块(有板载的串口通信接口也行)
- 软件准备:RT-Thread Studio
2 流程
整个项目流程都是在 RT-Thread Studio 上完成的,RT-Thread Studio 的图形化配置对于使用者来说会非常方便,前提是项目使用的是 RT-Thread 标准版或者 Smart 版,如果使用的是 Nano 版,那将无法使用组件包。
3 创建项目
创建项目的方法非常简单,只需要在新建项目时选择对应的芯片型号即可,我使用的是 STM32F407ZG 芯片,默认情况下 RT-Thread Studio 中是没有 STM32F4 系列的芯片包支持的,所以需要手动下载。
3.1 下载 STM32F4 系列芯片包
步骤 1:点击左上角的「文件」,选择「新建」-->「RT-Thread 项目」。
步骤 2:在新出现的窗口中找到「系列:」,点击后面的下拉框选择「添加更多」。
步骤 3:接下来找到「STM32F4」选项,勾选并点击安装。大概率是安装不成功的,因为下载这个资源的时候需要认证,防止恶意访问(我猜的),所以我们只能使用手动下载的方法。
步骤 4:按照下载时在下载窗口下面看到的下载网址输入到浏览器地址栏(可以用 QQ 截图的图片转文字功能),按步骤进行下载。
步骤 5:将下载后的资源解压,并将解压后的文件夹名称改为「STM32F4」,然后将文件夹复制到 RT-Thread Studio 安装目录下的 repo\Extract\Chip_Support_Packages\RealThread 目录下,如:D:\RT-ThreadStudio\repo\Extract\Chip_Support_Packages\RealThread。(注意:解压后的文件夹是文件,而不是另一个同名文件夹)
步骤 6:完成,现在到 RT-Thread Studio 中创建项目时应该会出现 STM32F4 系列的芯片包。
步骤 1 的截图 步骤 2 的截图 步骤 3 的截图 步骤 4 的截图 步骤 5 的截图
3.2 创建项目
点击左上角的「文件」,选择「新建」-->「RT-Thread 项目」,输入项目名称并选择相应的芯片以及要使用的调试器,点击完成。
4 配置 SPI
步骤 1 :双击打开「RT-Thread Sertting」,找到 SPI 组件,点击打开并使用 CTRL+S
按键保存。
步骤 2 :双击打开「CubeMX Settings」,进行时钟、引脚复用功能等配置。具体如何进行配置,可以自行搜索或者参考文章最后的「附」。
步骤 3 :配置完成后点击「编译」按钮编译,保证目前代码的正确性(左上角的小锤子就是编译按钮,选中项目的时候才能点,不然编译器不知道要编译哪个项目)。
5 配置 W5500 软件包
步骤 1 :使用 RT-Thread Settings 添加 W5500 软件包。点击「RT-Thread Studio」-->「添加软件包」,在新窗口中搜索 W5500
并点击「添加」按钮。
步骤 2 :添加完成后可以看到 RT-Thread Settings 的页面上多出了一个 WIZnet
图标。
步骤 3 :右击 WIZnet
图标,选择「配置」,然后点击「WIZnet device configure」下拉框,配置 SPI 的属性,配置完成后 CTRL+S
按键保存,如果此时编译,编译会有很多警告。
此处配置时,主要配置的是
WIZnet device configure
选项下的 SPI 设备名和引脚编号。
引脚编号在 drivers/drv_gpio.c 文件中可以查看。
步骤 4 :挂载 SPI 设备(之前做的 SPI 配置只是向内核注册了 SPI 总线,设备还需要手动挂载,此处的设备为 W5500)。由于 之前保存了 WIZnet
的配置,所以 RT-Thread Studio 会在项目目录下生成一个 packages 目录。打开 packages/src/wiz.c 文件,在文件中找到 int wiz_init(void)
函数,在函数中有一句注释「/* I think you can attach w5500 into spi bus at here. You can use this function to realize.*/」,取消注释的下一行关于代码的注释,并且按照这个函数声明来挂载 SPI 设备。
步骤 5 :在 packagew/src/wiz.c 文件最上面添加 #include <board.h>
来增加 GPIO_Typedef 类型的定义,否则编译时会找不到 GPIO_Typedef 的定义,最后就可以编译下载程序看是否成功挂载设备。
步骤 1 的截图 步骤 2 的截图 步骤 3 的截图 步骤 4 的截图 步骤 5 的截图
6 测试 W5500
下载后可以通过 finish shell 测试 W5500 驱动是否能够正常使用。通过 ifconfig
命令可以查看是否获取到 IP 地址(测试 DHCP 是否起作用),通过 ping
命令可以测试能否正常连接网络。
此时的 MAC 地址是默认的「00-E0-81-DC-53-1A」,如果想要修改可以调用「int wiz_set_mac(const char *mac);」函数来修改。
7 正式使用
客户端、HTTP 客户端、select 实现的客户端、TCP 服务端、UDP 服务端,这些在官方的 API 参考手册中都有相应的例程,比我写的更好,可以参考:here。
下面的例程是我基于自己的理解写的,仅作为参考,实际以官方为准。
7.1 实现一个简单的 TCP 客户端
我简单画了一个TCP 客户端与服务端建立通信的流程,基于这个流程再简单细化一下就可以简单建立 TCP 通信。
开始的等待 DHCP 获取 IP 地址是最简单但也是最容易忽视的地方,如果还没有获取到 Ip 地址就开始创建套接字,那必然创建失败。
-
等待 DHCP 获取 IP 等待 DHCP 获取 IP 的问题在于如何知道 IP 地址是否已经获取到,一个简单高效的方法就是反复读取网卡的 IP,能读取到即认为 DHCP 获取成功。不过,采用这种方法前,还需要获取一下网卡设备的对象,这样便于我们读取网卡的 IP。 获取网卡设备的对象使用
struct netdev *net_dev = netdev_get_by_name("W5500");
来实现。获取到网卡对象后,可以使用每 1s 读取一次net_dev->ip_addr.addr
,如果没有获取到则数值为 0,读取到则数值不为 0。 -
创建 socket 套接字 创建套接字的方法很简单,使用
int socketfd = socket(AF_INET, SOCK_STREAM, 0);
即可创建一个套接字。 -
客户端绑定 socket 套接字 客户端想要绑定套接字,首先需要决定使用哪个协议(IPV4/IPV6)、哪个网卡(IP 地址)以及哪个端口来通信,将这些数据赋值到
sockaddr_in
结构体中,然后调用bind(socketfd, (struct sockaddr *)&client_addr, sizeof(struct sockaddr));
这个函数就可以绑定套接字。 -
客户端连接服务端 与客户端绑定套接字类似,需要知道想要连接的服务器的具体信息,然后通过
connect(socketfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr))
函数即可连接服务器。 -
客户端向服务端发送数据 发送数据使用
int send_size = send(socketfd, msg, strlen(msg), 0);
即可发送,参数分别为:套接字描述符,发送字符串的头指针,字符串长度,0。 -
等待接收服务端数据 等待接收服务器数据也较为简单,使用
recv_size = recv(socketfd, recv_buffer, 1024, 0);
即可以阻塞的方式等待服务器的一个数据,参数分别为:套接字描述符,接收数据的数组,接收数据的最大长度,0。
具体代码如下:(直接看不好看的可以看图片:代码)
/*
* Date Author Notes
* 2022-03-13 徐浚策 简单的 Socket 使用 Demo
*/
#include <stdio.h> // 在这个示例中没用到
#include <string.h> // 提供 strlen() 函数
#include <rtthread.h>
#define DBG_TAG "main"
#define DBG_LVL DBG_LOG
#include <rtdbg.h> // 提供 LOG_D() 等函数,没用到
#include <board.h> // 提供一些设备的定义,没用到
#include <sys/socket.h> // 提供 socket 的标准函数,如:bind()、connect()等
#include <netdb.h> // 提供了获取 host 的方法,没有到
#include <netdev.h> // 提供了网卡相关的定义和函数
#define SERVER_HOST "192.168.31.36" // 服务器 IP 地址
#define SERVER_PORT 15370 // 服务器使用的端口
int main(void)
{
struct sockaddr_in client_addr; // 客户端套接字地址
struct sockaddr_in server_addr; // 服务端套接字地址
struct netdev *net_dev = RT_NULL; // netdev 网卡设备
int socketfd = -1; // 套接字文件描述符
/* 通过网卡名称获取 netdev 网卡对象 */
net_dev = netdev_get_by_name("W5500");
if (net_dev == RT_NULL){
rt_kprintf("get network interface device(%s) failed!\n", "W5500");
return -RT_ERROR;
}rt_kprintf("get network interface device(%s) success!\n", "W5500");
/* 等待 DHCP 获取 IP 地址成功,超时时间 20s。如果没有 IP 地址,Socket 无法创建。 */
int count = 20;
while(count-- && !(net_dev->ip_addr.addr)){
rt_thread_mdelay(1000);
}
if(!count){
rt_kprintf("get ip failed!\n");
return -RT_ERROR;
}
/* 创建一个套接字 */
if((socketfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
rt_kprintf("create socket failed!\n");
return -RT_ERROR;
}rt_kprintf("create socket success!\n");
/* 初始化需要绑定的客户端地址 */
client_addr.sin_family = AF_INET; // IPV4
client_addr.sin_port = htons(8080); // 客户端使用的端口
client_addr.sin_addr.s_addr = net_dev->ip_addr.addr; // 获取网卡对象中的 IP 地址,这个地址是由 DHCP 获取的,没有地址时为 0
rt_memset(&(client_addr.sin_zero), 0, sizeof(client_addr.sin_zero)); // 这个字段只起填充作用,使得 sockaddr_in 与 sockaddr 长度一样
/* 客户端绑定套接字 */
if (bind(socketfd, (struct sockaddr *)&client_addr, sizeof(struct sockaddr)) < 0){
rt_kprintf("socket bind failed!\n");
closesocket(socketfd);
return -RT_ERROR;
}rt_kprintf("socket bind network interface device(%s) success!\n", net_dev->name);
/* 初始化预连接的服务端地址(配置服务端地址) */
server_addr.sin_family = AF_INET; // IPV4
server_addr.sin_port = htons(SERVER_PORT); // 服务端使用的端口
server_addr.sin_addr.s_addr = inet_addr(SERVER_HOST); // 服务器的 IP 地址
rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero)); // 无实际作用,填充长度
/* 连接到服务端 */
if (connect(socketfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) < 0){
rt_kprintf("socket connect failed!\n");
closesocket(socketfd);
return -RT_ERROR;
}rt_kprintf("socket connect success!\n");
/* 向服务端发送数据 */
int send_size = 0; // 发送的数据长度
static const char *msg = "Hello, i am stm32f407zg.\n"; // 准备向服务端发送的数据
if ((send_size = send(socketfd, msg, strlen(msg), 0)) <= 0) {
rt_kprintf("send msg failed!\n", send_size);
}rt_kprintf("send msg success, send_size:%d.\n", send_size);
/* 接收服务器发送的数据,阻塞等待服务端数据,如果想要持续通信,使用 while 循环即可 */
int recv_size = -1; // 接收到的数据长度
char *recv_buffer = rt_calloc(1, 1024); // 接收数据的缓冲
if((recv_size = recv(socketfd, recv_buffer, 1024, 0)) < 0){
rt_kprintf("receive data failed!\n");
}rt_kprintf("receive data success, recv_size:%d.\n", recv_size);
rt_kprintf("the data is:%s\n", recv_buffer);
return RT_EOK;
}
7.2 待更新... ...
附
CubeMX Settings 配置 SPI
此时 SPI 已经配置完成,但是还没有添加到设备列表中(设备还没有向设备管理器注册),可以在 drivers 目录下的 board.h 中打开
#define BSP_USING_SPI2
的注释,这将会向内核注册 SPI 总线。
想要知道是否已经注册可以通过 finsh shell 的 list_device 命令查看。