netdev 组件主要作用是解决设备多网卡连接时网络连接问题,用于统一管理各个网卡信息与网络连接状态,并且提供统一的网卡调试命令接口。其主要功能特点如下所示:
抽象网卡概念,每个网络连接设备可注册唯一网卡。
提供多种网络连接信息查询,方便用户实时获取当前网卡网络状态;
建立网卡列表和默认网卡,可用于网络连接的切换;
提供多种网卡操作接口(设置 IP、DNS 服务器地址,设置网卡状态等);
统一管理网卡调试命令(ping、ifconfig、netstat、dns 等命令);
网卡概念:
网卡概念介绍之前先了解协议栈相关概念,协议栈是指网络中各层协议的总和,每种协议栈反映了不同的网络数据交互方式,RT-Thread 系统中目前支持三种协议栈类型:lwIP 协议栈、AT Socket 协议栈、WIZnet TCP/IP硬件协议栈。每种协议栈对应一种协议簇类型(family),上述协议栈分别对应的协议簇类型为:AF_INET、AF_AT、AF_WIZ。
网卡的初始化和注册建立在协议簇类型上,所以每种网卡对应唯一的协议簇类型。Socket 套接字描述符的创建建立在 netdev 网卡基础上,所以每个创建的 Socket 对应唯一的网卡。协议簇、网卡和 socket 之间关系如下图所示:
每个网卡对应唯一的网卡结构体对象,其中包含该网卡的主要信息和实时状态,用于后面网卡信息的获取和设置。
up/down:底层网卡初始化完成之后置为 up 状态,用于判断网卡开启还是禁用。
link_up/link_down:用于判断网卡设备是否具有有效的链路连接,连接后可以与其他网络设备进行通信。该状态一般由网卡底层驱动设置。
internet_up/internet_down:用于判断设备是否连接到因特网,接入后可以与外网设备进行通信。
dhcp_enable/dhcp_disable:用于判断当前网卡设备是否开启 DHCP 功能支持。
1/* The list of network interface device */
2struct netdev *netdev_list;
3/* The default network interface device */
4struct netdev *netdev_default;
为了方便网卡的管理和控制,netdev 组件中提供网卡列表用于统一管理各个网卡设备,系统中每个网卡在初始化时会创建和注册网卡对象到 netdev 组件网卡列表中。
网卡列表中有且只有一个默认网卡,一般为系统中第一个注册的网卡,可以通过 netdev_set_default() 函数设置默认网卡,默认网卡的主要作用是确定优先使用的进行网络通讯的网卡类型,方便网卡的切换和网卡信息的获取。
1int netdev_register(struct netdev *netdev, const char *name, void *user_data);
参数 | 描述 |
---|---|
netdev | 网卡对象 |
name | 网卡名称 |
user_data | 用户使用数据 |
返回 | —— |
0 | 网卡注册成功 |
-1 | 网卡注册失败 |
将网卡挂载到网卡列表(netdev_list)和默认网卡(netdev_default)。
该函数不需要在用户层调用,一般为网卡驱动初始化完成之后自动调用,如 esp8266 网卡的注册在 esp8266 设备网络初始化之后自动完成。
该函数可以在网卡使用时,注销网卡的注册,即从网卡列表中删除对应网卡,注销网卡的接口如下所示:
1int netdev_unregister(struct netdev *netdev);
通过状态获取第一个匹配的网卡对象
1struct netdev *netdev_get_first_by_flags(uint16_t flags);
获取第一个指定协议簇类型的网卡对象
1struct netdev *netdev_get_by_family(int family);
通过 IP 地址获取网卡对象
1struct netdev *netdev_get_by_ipaddr(ip_addr_t *ip_addr);
该函数主要用于 bind 函数绑定指定 IP 地址时获取网卡状态信息的情况。
通过名称获取网卡对象
1struct netdev *netdev_get_by_name(const char *name);
设置默认网卡
1void netdev_set_default(struct netdev *netdev);
设置网卡 up/down 状态
1int netdev_set_up(struct netdev *netdev);
2int netdev_set_down(struct netdev *netdev);
设置网卡 DHCP 功能状态
DHCP 即动态主机配置协议,如果开启该网卡 DHCP 功能将无法设置该网卡 IP 、网关和子网掩码地址等信息,如果关闭该功能则可以设置上述信息。
1int netdev_dhcp_enabled(struct netdev *netdev, rt_bool_t is_enabled);
设置网卡地址信息
设置指定网卡地址 IP 、网关和子网掩码地址,需要在网卡关闭 DHCP 功能状态使用。
1/* 设置网卡 IP 地址 */
2int netdev_set_ipaddr(struct netdev *netdev, const ip_addr_t *ipaddr);
3/* 设置网卡网关地址 */
4int netdev_set_gw(struct netdev *netdev, const ip_addr_t *gw);
5/* 设置网卡子网掩码地址 */
6int netdev_set_netmask(struct netdev *netdev, const ip_addr_t *netmask);
7/* 设置网卡 DNS 服务器地址,主要用于网卡域名解析功能 */
8int netdev_set_dns_server(struct netdev *netdev, uint8_t dns_num, const ip_addr_t *dns_server);
设置网卡回调函数
可以用于设备网卡状态改变时调用的回调函数,状态的改变包括:up/down、 link_up/link_down、internet_up/internet_down、dhcp_enable/dhcp_disable 等。
1ypedef void (*netdev_callback_fn )(struct netdev *netdev, enum netdev_cb_type type);
2void netdev_set_status_callback(struct netdev *netdev, netdev_callback_fn status_callback);
判断网卡是否为 up 状态
1#define netdev_is_up(netdev)
判断网卡是否为 link_up 状态
1#define netdev_is_link_up(netdev)
判断网卡是否为 internet_up 状态
1#define netdev_is_internet_up(netdev)
判断网卡 DHCP 功能是否开启
1#define netdev_is_dhcp_enable(netdev)
单网卡模式下,开启和关闭默认网卡自动切换功能无明显效果。
多网卡模式下,如果开启默认网卡自动切换功能,当前默认网卡状态改变为 down 或 link_down 时,默认网卡会切换到网卡列表中第一个状态为 up 和 link_up 的网卡。这样可以使一个网卡断开后快速切换到另一个可用网卡,简化用户应用层网卡切换操作。如果未开启该功能,则不会自动切换默认网卡。
socket 编程模型如下图所示:
socket() 创建一个 socket,返回套接字的描述符,并为其分配系统资源。
connect() 向服务器发出连接请求。
send()/recv() 与服务器进行通信。
closesocket() 关闭 socket,回收资源。
服务器使用流程:
socket() 创建一个 socket,返回套接字的描述符,并为其分配系统资源。
bind() 将套接字绑定到一个本地地址和端口上。
listen() 将套接字设为监听模式并设置监听数量,准备接收客户端请求。
accept() 等待监听的客户端发起连接,并返回已接受连接的新套接字描述符。
recv()/send() 用新套接字与客户端进行通信。
closesocket() 关闭 socket,回收资源。
SAL 组件主要功能特点:
抽象、统一多种网络协议栈接口;
提供 Socket 层面的 TLS 加密传输特性;
支持标准 BSD Socket API;
统一的 FD 管理,便于使用 read/write poll/select 来操作网络功能;
SAL 网络框架:
多协议栈接入与接口函数统一抽象功能:
对于不同的协议栈或网络功能实现,网络接口的名称可能各不相同,以 connect 连接函数为例,lwIP 协议栈中接口名称为 lwip_connect ,而 AT Socket 网络实现中接口名称为 at_connect。SAL 组件提供对不同协议栈或网络实现接口的抽象和统一,组件在 socket 创建时通过判断传入的协议簇(domain)类型来判断使用的协议栈或网络功能,完成 RT-Thread 系统中多协议的接入与使用。
目前 SAL 组件支持的协议栈或网络实现类型有:lwIP 协议栈、AT Socket 协议栈、WIZnet 硬件 TCP/IP 协议栈。
在 Socket 中,它使用一个套接字来记录网络的一个连接,套接字是一个整数,就像我们操作文件一样,利用一个文件描述符,可以对它打开、读、写、关闭等操作,类似的,在网络中,我们也可以对 Socket 套接字进行这样子的操作,比如开启一个网络的连接、读取连接主机发送来的数据、向连接的主机发送数据、终止连接等操作。
socket文件描述符的操作接口如下所示,在创建套接字的时候进行初始化,当使用虚拟文件系统的接口write(),read(),close()等接口时,会调用如下相应接口:
1const struct dfs_file_ops _net_fops =
2{
3 NULL, /* open */
4 dfs_net_close,
5 dfs_net_ioctl,
6 dfs_net_read,
7 dfs_net_write,
8 NULL,
9 NULL, /* lseek */
10 NULL, /* getdents */
11 dfs_net_poll,
12};
创建套接字接口:
1int socket(int domain, int type, int protocol);
socket调用的流程大致如下:socket->sal_socket->at_socket/lwip_socket.
创建一个BSD套接字
分配一个fd文件描述符
初始化fd文件描述符
创建套接字,然后将其放入dfs_fd
上述为标准 BSD Socket API 中 socket 创建函数的定义,domain 表示协议域又称为协议簇(family),用于判断使用哪种协议栈或网络实现,AT Socket 协议栈使用的簇类型为 AF_AT,lwIP 协议栈使用协议簇类型有 AF_INET等,WIZnet 协议栈使用的协议簇类型为 AF_WIZ。
对于不同的软件包,socket 传入的协议簇类型可能是固定的,不会随着 SAL 组件接入方式的不同而改变。为了动态适配不同协议栈或网络实现的接入,SAL 组件中对于每个协议栈或者网络实现提供两种协议簇类型匹配方式:主协议簇类型和次协议簇类型。socket 创建时先判断传入协议簇类型是否存在已经支持的主协议类型,如果是则使用对应协议栈或网络实现,如果不是判断次协议簇类型是否支持。目前系统支持协议簇类型如下:
lwIP 协议栈:family = AF_INET、sec_family = AF_INET
AT Socket 协议栈:family = AF_AT、sec_family = AF_INET WIZnet
硬件 TCP/IP 协议栈:family = AF_WIZ、sec_family = AF_INET
链接服务器接口:
1int connect(int s, const struct sockaddr *name, socklen_t namelen)
2
connect调用的流程大致如下:connect->sal_connect->at_connect/lwip_connect.
connect:SAL 组件对外提供的抽象的 BSD Socket API,用于统一 fd 管理;
sal_connect:SAL 组件中 connect 实现函数,用于调用底层协议栈注册的 operation 函数。
at_connect/lwip_connect:底层协议栈提供的层 connect 连接函数,在网卡初始化完成时注册到 SAL 组件中,最终调用的操作函数。
网络接口设备协议簇数据结构:
1static struct sal_socket_table socket_table;
初始化sal套接字:
1int sal_init(void);
该初始化函数主要是对 SAL 组件进行初始化,动态申请socket_table对象。支持组件重复初始化判断,完成对组件中使用的互斥锁等资源的初始化。
如果AT组件使用了SAL 套接字,则在sal_at_netdev_set_pf_info(netdev)函数对网络接口设备协议族信息(struct sal_proto_family)进行赋值。
如果LWIP组件使用了SAL 套接字,则在sal_lwip_netdev_set_pf_info(struct netdev *netdev)函数对网络接口设备协议族信息(struct sal_proto_family)进行赋值。
1int sal_socket(int domain, int type, int protocol)
在套接字表中分配一个新的套接字和注册的套接字选项
通过套接字描述符获取sal套接字对象
初始化sal套接字对象
打开有效的网络接口套接字(at_socket/lwip_socket)
1int sal_bind(int socket, const struct sockaddr *name, socklen_t namelen)
通过套接字描述符获取套接字对象
检查输入ipaddr是否是默认的netdev ipaddr,如果不是根据ip地址获取新的网卡设备
通过网络接口设备检查和获取协议族
调用对应驱动的bind接口(at_bind/lwip_bind)
1int sal_connect(int socket, const struct sockaddr *name, socklen_t namelen)
通过套接字描述符获取套接字对象
调用对应驱动的connect接口(at_connect/lwip_connect)
其他接口:
1int sal_accept(int socket, struct sockaddr *addr, socklen_t *addrlen)
2int sal_shutdown(int socket, int how)
3int sal_getpeername (int socket, struct sockaddr *name, socklen_t *namelen);
4int sal_getsockname (int socket, struct sockaddr *name, socklen_t *namelen);
5int sal_getsockopt (int socket, int level, int optname, void *optval, socklen_t *optlen);
6int sal_setsockopt (int socket, int level, int optname, const void *optval, socklen_t optlen);
7int sal_listen(int socket, int backlog);
8int sal_recvfrom(int socket, void *mem, size_t len, int flags,
9 struct sockaddr *from, socklen_t *fromlen);
10int sal_sendto(int socket, const void *dataptr, size_t size, int flags,
11 const struct sockaddr *to, socklen_t tolen);
12int sal_socket(int domain, int type, int protocol);
13int sal_closesocket(int socket);
14int sal_ioctlsocket(int socket, long cmd, void *arg);
创建套接字(socket)
1int socket(int domain, int type, int protocol);
创建一个BSD套接字
分配一个fd文件描述符
通过sal_socket()接口创建套接字
初始化fd文件描述符,然后将套接字socket放入dfs_fd
绑定套接字(bind)
1int bind(int s, const struct sockaddr *name, socklen_t namelen);
调用sal_bind()
建立连接(connect)
1int connect(int s, const struct sockaddr *name, socklen_t namelen)sal_connect
调用sal_connect()
监听套接字(listen)
1int listen(int s, int backlog)
接收连接(accept)
1int accept(int s, struct sockaddr *addr, socklen_t *addrlen)
TCP 数据发送(send)
1int send(int s, const void *dataptr, size_t size, int flags)
TCP 数据接收(recv)
1int recv(int s, void *mem, size_t len, int flags)
UDP 数据发送(sendto)
1int sendto(int s, const void *dataptr, size_t size, int flags, const struct sockaddr *to, socklen_t tolen)
UDP 数据接收(recvfrom)
1int recvfrom(int s, void *mem, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen)
关闭套接字(closesocket)
1int closesocket(int s)
按设置关闭套接字(shutdown)
1int shutdown(int s, int how)
设置套接字选项(setsockopt)
1int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen)
获取套接字选项(getsockopt)
1int getsockopt(int s, int level, int optname, void *optval, socklen_t *optlen)
获取远端地址信息(getpeername)
1int getpeername(int s, struct sockaddr *name, socklen_t *namelen)
获取本地地址信息(getsockname)
1int getsockname(int s, struct sockaddr *name, socklen_t *namelen)
配置套接字参数(ioctlsocket)
1int ioctlsocket(int s, long cmd, void *arg)
AT 命令集是一种应用于 AT 服务器(AT Server)与 AT 客户端(AT Client)间的设备连接与数据通信的方式。其基本结构如下图所示:
AT 组件是基于 RT-Thread 系统的 AT Server 和 AT Client 的实现,组件完成 AT 命令的发送、命令格式及参数判断、命令的响应、响应数据的接收、响应数据的解析、URC 数据处理等整个 AT 命令数据交互流程。
通过 AT 组件,设备可以作为 AT Client 使用串口连接其他设备发送并接收解析数据,可以作为 AT Server 让其他设备甚至电脑端连接完成发送数据的响应,也可以在本地 shell 启动 CLI 模式使设备同时支持 AT Server 和 AT Client 功能,该模式多用于设备开发调试。
AT Server 主要功能特点:
基础命令:实现多种通用基础命令(ATE、ATZ 等);
命令兼容:命令支持忽略大小写,提高命令兼容性;
命令检测:命令支持自定义参数表达式,并实现对接收的命令参数自检测功能;
命令注册:提供简单的用户自定义命令添加方式,类似于 finsh/msh 命令添加方式;
调试模式:提供 AT Server CLI 命令行交互模式,主要用于设备调试。
AT Client 主要功能特点:
URC 数据处理:完备的 URC 数据的处理方式;
数据解析:支持自定义响应数据的解析方式,方便获取响应数据中相关信息;
调试模式:提供 AT Client CLI 命令行交互模式,主要用于设备调试。
AT Socket:作为 AT Client 功能的延伸,使用 AT 命令收发作为基础,实现标准的 BSD Socket API,完成数据的收发功能,使用户通过 AT 命令完成设备连网和数据通讯。
多客户端支持:AT 组件目前支持多客户端同时运行
AT Client 主要功能是发送 AT 命令、接收数据并解析数据。
AT Client列表:
1static struct at_client at_client_table[AT_CLIENT_NUM_MAX] = { 0 };
AT 客户端都挂载在at_client_table里。
AT Client数据结构:
创建AT客户端对象,初始化客户端对象参数。
1int at_client_init(const char *dev_name, rt_size_t recv_bufsz);
at_client_init() 函数完成对 AT Client 设备初始化、AT Client 移植函数的初始化、AT Client 使用的信号量、互斥锁等资源初始化,并创建 at_client 线程用于 AT Client 中数据的接收的解析以及对 URC 数据的处理。
创建响应结构体:
1at_response_t at_create_resp(rt_size_t buf_size, rt_size_t line_num, rt_int32_t timeout);
删除响应结构体:
1void at_delete_resp(at_response_t resp);
设置响应结构体参数:
1at_response_t at_resp_set_info(at_response_t resp, rt_size_t buf_size, rt_size_t line_num, rt_int32_t timeout);
发送命令并接收响应:
1 rt_err_t at_exec_cmd(at_response_t resp, const char *cmd_expr, ...);
获取指定行号的响应数据:
该函数用于在 AT Server 响应数据中获取指定行号的一行数据。
1const char *at_resp_get_line(at_response_t resp, rt_size_t resp_line);
获取指定关键字的响应数据:
该函数用于在 AT Server 响应数据中通过关键字获取对应的一行数据。
1const char *at_resp_get_line_by_kw(at_response_t resp, const char *keyword);
解析指定行号的响应数据:
该函数用于在 AT Server 响应数据中获取指定行号的一行数据, 并解析该行数据中的参数。
1int at_resp_parse_line_args(at_response_t resp, rt_size_t resp_line, const char *resp_expr, ...);
发送命令并解析接收响应例程:
1/*
2 * 程序清单:AT Client 发送命令并解析接收响应例程
3 */
4
5int user_at_client_send(int argc, char**argv)
6{
7 at_response_t resp = RT_NULL;
8 char ip[20];
9 char mac[20];
10 char uartdata[20];
11 if (argc != 2)
12 {
13 LOG_E("at_cli_send [command] - AT client send commands to AT server.");
14 return -RT_ERROR;
15 }
16
17 /* 创建响应结构体,设置最大支持响应数据长度为 512 字节,响应数据行数无限制,超时时间为 5 秒 */
18 resp = at_create_resp(512, 0, rt_tick_from_millisecond(5000));
19 if (!resp)
20 {
21 LOG_E("No memory for response structure!");
22 return -RT_ENOMEM;
23 }
24
25 /* 发送 AT 命令并接收 AT Server 响应数据,数据及信息存放在 resp 结构体中 */
26 if (at_exec_cmd(resp, argv[1]) != RT_EOK)
27 {
28 LOG_E("AT client send commands failed, response error or timeout !");
29 return -1;
30 }
31
32 /* 命令发送成功 */
33 rt_kprintf("AT Client send commands to AT Server success!\n");
34 if(at_resp_get_line_by_kw(resp,"UART")!= NULL)
35 {
36 /* 解析获取串口配置信息AT+UART?,1 表示解析响应数据第一行 */
37 at_resp_parse_line_args(resp, 1,"+UART:%s", uartdata);
38 rt_kprintf("+UART:%s\n",uartdata);
39 }
40 /* 删除响应结构体 */
41 at_delete_resp(resp);
42
43 return RT_EOK;
44}
45/* 输出 at_Client_send 函数到 msh 中 */
46MSH_CMD_EXPORT(user_at_client_send, AT Client send commands to AT Server and get response data);
URC 数据的处理是 AT Client 另一个重要功能,URC 数据为服务器主动下发的数据,不能通过上述数据发送接收函数接收,并且对于不同设备 URC 数据格式和功能不一样,所以 URC 数据处理的方式也是需要用户自定义实现的。
每种 URC 数据都有一个结构体控制块,用于定义判断 URC 数据的前缀和后缀,以及 URC 数据的执行函数。一段数据只有完全匹配 URC 的前缀和后缀才能定义为 URC 数据,获取到匹配的 URC 数据后会立刻执行 URC 数据执行函数。所以开发者添加一个 URC 数据需要自定义匹配的前缀、后缀和执行函数。
URC 数据列表初始化:
1void at_set_urc_table(const struct at_urc *table, rt_size_t size);
AT Client 移植具体示例:
1static void urc_conn_func(const char *data, rt_size_t size)
2{
3 /* WIFI 连接成功信息 */
4 LOG_D("AT Server device WIFI connect success!");
5}
6
7static void urc_recv_func(const char *data, rt_size_t size)
8{
9 /* 接收到服务器发送数据 */
10 LOG_D("AT Client receive AT Server data!");
11}
12
13static void urc_func(const char *data, rt_size_t size)
14{
15 /* 设备启动信息 */
16 LOG_D("AT Server device startup!");
17}
18
19static struct at_urc urc_table[] = {
20 {"WIFI CONNECTED", "\r\n", urc_conn_func},
21 {"+RECV", ":", urc_recv_func},
22 {"RDY", "\r\n", urc_func},
23};
24
25int at_client_port_init(void)
26{
27 /* 添加多种 URC 数据至 URC 列表中,当接收到同时匹配 URC 前缀和后缀的数据,执行 URC 函数 */
28 at_set_urc_table(urc_table, sizeof(urc_table) / sizeof(urc_table[0]));
29 return RT_EOK;
30}
发送指定长度数据:
1rt_size_t at_client_send(const char *buf, rt_size_t size);
接收指定长度数据:
1rt_size_t at_client_recv(char *buf, rt_size_t size,rt_int32_t timeout);
设置接收数据的行结束符:
1void at_set_end_sign(char ch);
等待模块初始化完成:
1int at_client_wait_connect(rt_uint32_t timeout);
网卡的初始化和注册建立在协议簇类型上,所以每种网卡对应唯一的协议簇类型。每种协议栈对应一种协议簇类型(family),AT协议簇对应的协议栈是AT Socket 协议栈,每种AT设备都对应唯一的AT Socket 协议栈。
AT 设备列表:
1/* The global list of at device */
2static rt_slist_t at_device_list = RT_SLIST_OBJECT_INIT(at_device_list);
3/* The global list of at device class */
4static rt_slist_t at_device_class_list = RT_SLIST_OBJECT_INIT(at_device_class_list);
at设备的具体网卡对象,例如(esp8266网卡、esp32网卡等)注册到at_device_class_list 列表,对at_device_class_list 创建的网卡对象进行填充。网卡注册在驱动层进行。
at设备对象注册到at_device_list列表,对at设备的具体网卡对象进行统一管理。AT设备注册在应用层进行。
AT设备数据结构:
1int at_device_register(struct at_device *device, const char *device_name,
2 const char *at_client_name, uint16_t class_id, void *user_data)
应用层运行AT设备注册接口之前,需要先在外设驱动相关的自动初始化机制INIT_DEVICE_EXPORT(fn) 申明注册AT类的网卡设备,然后应用层注册AT设备的时候才能在at_device_class_list 列表里通过AT设备ID找到具体的网卡驱动。
AT Socket 是AT Client 功能的延伸,使用 AT 命令收发作为基础功能,提供 ping 或者 ifconfig等命令用于测试设备网络连接环境,ping 命令原理是通过 AT 命令发送请求到服务器,服务器响应数据,客户端解析 ping 数据并显示。ifocnfig 命令可以查看当前设备网络状态和 AT 设备生成的网卡基本信息。
AT Socket 功能的使用依赖于如下几个组件:
AT 组件:AT Socket 功能基于 AT Client 功能的实现;
SAL 组件:SAL 组件主要是 AT Socket 接口的抽象,实现标准 BSD Socket API;
netdev 组件:用于抽象和管理 AT 设备生成的网卡设备相关信息,提供 ping、ifconfig、netstat 等网络命令;
AT Device 软件包:针对不同设备的 AT Socket 移植和示例文件,以软件包的形式给出;
AT Socket 数据结构:
1int at_socket(int domain, int type, int protocol)
通过协议族AF_AT获取第一个指定协议簇类型的网卡对象
通过网卡对象的名字获得AT设备的对象
通过AT设备的对象分配并初始化一个新的AT套接字
1int at_bind(int socket, const struct sockaddr *name, socklen_t namelen)
获取当前设备ip地址
从sockaddr结构中选择ip地址和端口
如果输入的ip地址不同于设备的ip地址,则根据输入的ip分配新的套接字,否则返回。
1int at_connect(int socket, const struct sockaddr *name, socklen_t namelen)
socketaddr结构获取IP地址和端口
调用对应AT网卡驱动的_socket_connect()链接服务器
设置套接字接收数据回调函数
1int at_sendto(int socket, const void *data, size_t size, int flags, const struct sockaddr *to, socklen_t tolen)
调用对应AT网卡驱动的_socket_send()发送数据
其他API
1int at_closesocket(int socket)
2
1int at_recvfrom(int socket, void *mem, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen)
2
1int at_getsockopt(int socket, int level, int optname, void *optval, socklen_t *optlen)
2
1int at_setsockopt(int socket, int level, int optname, const void *optval, socklen_t optlen)
2
1int at_shutdown(int socket, int how)
2
使用AT Socket 功能的框架:
1static int esp8266_device_class_register(void)
2
创建并初始化ESP8266 device class对象
在at_device_class_list列表注册AT_DEVICE_CLASS_ESP8266客户端ID
注册esp8266设备操作函数:
1static const struct at_device_ops esp8266_device_ops =
2{
3 esp8266_init,
4 esp8266_deinit,
5 esp8266_control,
6};
7class->device_ops = &esp8266_device_ops
8
注册esp8266_at_socket操作接口:
1static const struct at_socket_ops esp8266_socket_ops =
2{
3 esp8266_socket_connect,
4 esp8266_socket_close,
5 esp8266_socket_send,
6 esp8266_domain_resolve,
7 esp8266_socket_set_event_cb,
8};
1#define ESP8266_SAMPLE_DEIVCE_NAME "esp0"
2static struct at_device_esp8266 esp0 =
3{
4 ESP8266_SAMPLE_DEIVCE_NAME,
5 ESP8266_SAMPLE_CLIENT_NAME,
6
7 ESP8266_SAMPLE_WIFI_SSID,
8 ESP8266_SAMPLE_WIFI_PASSWORD,
9 ESP8266_SAMPLE_RECV_BUFF_LEN,
10};
11
12struct at_device_esp8266 *esp8266 = &esp0;
13
14return at_device_register(&(esp8266->device),
15 esp8266->device_name,
16 esp8266->client_name,
17 AT_DEVICE_CLASS_ESP8266,
18 (void *) esp8266);
从at_device_class_list列表通过客户端ID获取ESP8266设备类对象
创建并初始化AT device class对象
在at_device_list列表注册AT设备
调用ESP8266设备类对象的初始化驱动接口
1static int esp8266_init(struct at_device *device)
创建esp_net线程,链接无线网络后自动销毁
1static void esp8266_init_thread_entry(void *parameter)
注册ESP8266设备操作接口:
1static const struct netdev_ops esp8266_netdev_ops =
2{
3 esp8266_netdev_set_up,
4 esp8266_netdev_set_down,
5
6 esp8266_netdev_set_addr_info,
7 esp8266_netdev_set_dns_server,
8 esp8266_netdev_set_dhcp,
9
10#ifdef NETDEV_USING_PING
11 esp8266_netdev_ping,
12#endif
13#ifdef NETDEV_USING_NETSTAT
14 esp8266_netdev_netstat,
15#endif
16};
17netdev->ops = &esp8266_netdev_ops
1static int esp8266_net_init(struct at_device *device)
2
注册urc_table
1static const struct at_urc urc_table[] =
2{
3 {"busy p", "\r\n", urc_busy_p_func},
4 {"busy s", "\r\n", urc_busy_s_func},
5 {"WIFI CONNECTED", "\r\n", urc_func},
6 {"WIFI DISCONNECT", "\r\n", urc_func},
7};
1static const struct at_urc urc_table[] =
2{
3 {"SEND OK", "\r\n", urc_send_func},
4 {"SEND FAIL", "\r\n", urc_send_func},
5 {"Recv", "bytes\r\n", urc_send_bfsz_func},
6 {"", ",CLOSED\r\n", urc_close_func},
7 {"+IPD", ":", urc_recv_func},
8};
驱动架构图:
在 RT-Thread Setting 文件中借助图形化配置工具打开软件 lwip 的组件,保存更新。
移植网络设备层和LAN8720驱动:
本例中使用的是 stm32f429-fire-challenger开发板,所以需要下载 BSP的LWIP驱动,将下载的LWIP驱动源码 drv_etc.c 和 drv_etc.h 文件添加到自己工程驱动文件所在的路径。
将#include
删除extern void phy_reset(void);和 phy_reset();
添加ETH外设配置:
打开stm32f429-fire-challenger的BSP,在board目录下找到stm32f4xx_hal_msp.c文件,移植到工程中。
然后改动stm32f4xx_hal_msp.c里的代码:
把#include "main.h"改为#include "board.h"
删除多余的配置,只保留void HAL_ETH_MspInit(ETH_HandleTypeDef* heth)
和void HAL_ETH_MspDeInit(ETH_HandleTypeDef* heth)
打开include "board.h",添加#define PHY_USING_LAN8720A
移植完成,编译。
RT-Thread 的 lwIP 移植在原版的基础上,添加了网络设备层以替换原来的驱动层。和原来的驱动层不同的是,对于以太网数据的收发采用了独立的双线程结构,erx 线程和 etx 线程在正常情况下,两者的优先级设置成相同,用户可以根据自身实际要求进行微调以侧重接收或发送。
数据接收流程:
1struct eth_device
2{
3 /* 标准设备 */
4 struct rt_device parent;
5
6 /* lwIP 网络接口 */
7 struct netif *netif;
8 /* 发送应答信号量 */
9 struct rt_semaphore tx_ack;
10
11 /* 网络状态标志 */
12 rt_uint16_t flags;
13 rt_uint8_t link_changed;
14 rt_uint8_t link_status;
15
16 /* 数据包收发接口 */
17 struct pbuf* (*eth_rx)(rt_device_t dev);
18 rt_err_t (*eth_tx)(rt_device_t dev, struct pbuf* p);
19};
实现数据包收发接口,对应了 eth_device 结构体中的 eth_rx 及 eth_tx 元素:
1rt_err_t rt_stm32_eth_tx(rt_device_t dev, struct pbuf* p);
2struct pbuf *rt_stm32_eth_rx(rt_device_t dev);
注册以太网设备,初始化以太网硬件,配置 MAC 地址:
1rt_err_t eth_device_init_with_flag(struct eth_device *dev, const char *name, rt_uint16_t flags)
此函数由LAN8720的驱动rt_hw_stm32_eth_init()调用。
LAN8720网卡对象stm32_eth_device由rt_stm32_eth类创建,rt_stm32_eth类继承自eth_device类。
rt_stm32_eth的结构定义:
1struct rt_stm32_eth
2{
3 /* inherit from ethernet device */
4 struct eth_device parent;
5 rt_timer_t poll_link_timer;
6 /* interface address info, hw address */
7 rt_uint8_t dev_addr[MAX_ADDR_LEN];
8 /* ETH_Speed */
9 uint32_t ETH_Speed;
10 /* ETH_Duplex_Mode */
11 uint32_t ETH_Mode;
12};
实现rt_device设备的接口:
1static rt_err_t rt_stm32_eth_init(rt_device_t dev);
2static rt_err_t rt_stm32_eth_open(rt_device_t dev, rt_uint16_t oflag);
3static rt_err_t rt_stm32_eth_close(rt_device_t dev);
4static rt_size_t rt_stm32_eth_read(rt_device_t dev, rt_off_t pos, void* buffer, rt_size_t size);
5static rt_size_t rt_stm32_eth_write (rt_device_t dev, rt_off_t pos, const void* buffer, rt_size_t size);
6static rt_err_t rt_stm32_eth_control(rt_device_t dev, int cmd, void *args);
rt_stm32_eth_init 用于初始化 DMA 和 MAC 控制器。
rt_stm32_eth_open 用于上层应用打开网络设备,目前未使用到,直接返回 RT_EOK。
rt_stm32_eth_close 用于上层应用关闭网络设备,目前未使用到,直接返回 RT_EOK。
rt_stm32_eth_read 用于上层应用向底层设备进行直接读写的情况,对于网络设备,每个报文都有固定的格式,所以这个接口目前并未使用,直接返回 0 值。
rt_stm32_eth_write 用于上层应用向底层设备进行直接读写的情况,对于网络设备,每个报文都有固定的格式,所以这个接口目前并未使用,直接返回 0 值。
rt_stm32_eth_control 用于控制以太网接口设备,目前用于获取以太网接口的 mac 地址。如果需要,也可以通过增加控制字的方式来扩展其他控制功能。
实现驱动层的数据包收发接口:
1rt_stm32_eth_rx()
rt_stm32_eth_rx 会去读取接收缓冲区中的数据,并放入 pbuf(lwIP 中利用结构体 pbuf 来管理数据包 )中,并返回 pbuf 指针。
网络设备层的“erx” 接收线程会阻塞在获取 eth_rx_thread_mb 邮箱上,当它接收到邮件时,会调用 rt_stm32_eth_rx 去接收数据。
1rt_stm32_eth_tx()
rt_stm32_eth_tx 会将要发送的数据放入发送缓冲区,等待 DMA 来发送数据。
网络设备层的“etx” 发送线程会阻塞在获取 eth_tx_thread_mb 邮箱上, 当它接收到邮件时,会调用 rt_stm32_eth_tx 来发送数据。
ETH 设备初始化:
1static int rt_hw_stm32_eth_init(void)
2INIT_DEVICE_EXPORT(rt_hw_stm32_eth_init);
由系统自动初始化机制调用。
lwip协议栈初始化:
1int lwip_system_init(void)
2INIT_PREV_EXPORT(lwip_system_init)
由系统自动初始化机制调用。
下面示例完成通过传入的网卡名称绑定该网卡 IP 地址并和服务器进行连接通信的过程:
1static int bing_test(int argc, char **argv)
2{
3 struct sockaddr_in client_addr;
4 struct sockaddr_in server_addr;
5 struct netdev *netdev = RT_NULL;
6 int sockfd = -1;
7 int AF = -1;
8 uint8_t send_buf[]= "This is a TCP Client test...\n";
9 uint8_t read_buf[10];
10 if (argc != 2)
11 {
12 rt_kprintf("bind_test [netdev_name] --bind network interface device by name.\n");
13 return -RT_ERROR;
14 }
15 if(rt_strcmp(argv[1], "esp0") == 0)
16 {
17 AF = AF_AT;
18 }else if(rt_strcmp(argv[1], "e0") == 0){
19 AF = AF_INET;
20 }else{
21 return -RT_ERROR;
22 }
23 /* 通过名称获取 netdev 网卡对象 */
24 netdev = netdev_get_by_name(argv[1]);
25 if (netdev == RT_NULL)
26 {
27 rt_kprintf("get network interface device(%s) failed.\n", argv[1]);
28 return -RT_ERROR;
29 }
30 /* 设置默认网卡对象 */
31 netdev_set_default(netdev);
32 if ((sockfd = socket(AF, SOCK_STREAM, 0)) < 0)
33 {
34 rt_kprintf("Socket create failed.\n");
35 return -RT_ERROR;
36 }
37
38 /* 初始化需要绑定的客户端地址 */
39 client_addr.sin_family = AF;
40 client_addr.sin_port = htons(8080);
41 /* 获取网卡对象中 IP 地址信息 */
42 client_addr.sin_addr.s_addr = netdev->ip_addr.addr;
43 rt_memset(&(client_addr.sin_zero), 0, sizeof(client_addr.sin_zero));
44
45 if (bind(sockfd, (struct sockaddr *)&client_addr, sizeof(struct sockaddr)) < 0)
46 {
47 rt_kprintf("socket bind failed.\n");
48 closesocket(sockfd);
49 return -RT_ERROR;
50 }
51 rt_kprintf("socket bind network interface device(%s) success!\n", netdev->name);
52
53 /* 初始化预连接的服务端地址 */
54 server_addr.sin_family = AF;
55 server_addr.sin_port = htons(SERVER_PORT);
56 server_addr.sin_addr.s_addr = inet_addr(SERVER_HOST);
57 rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));
58
59 /* 连接到服务端 */
60 if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) < 0)
61 {
62 rt_kprintf("socket connect failed!\n");
63 closesocket(sockfd);
64 return -RT_ERROR;
65 }
66 else
67 {
68 rt_kprintf("socket connect success!\n");
69 }
70 write(sockfd,send_buf,sizeof(send_buf));
71 read(sockfd,read_buf,sizeof(read_buf));
72 rt_kprintf("%s\n",read_buf);
73 /* 关闭连接 */
74 closesocket(sockfd);
75 return RT_EOK;
76}
77MSH_CMD_EXPORT(bing_test, bind network interface device test);
“在看”的小可爱永远十八岁!
RT-Thread
让物联网终端的开发变得简单、快速,芯片的价值得到最大化发挥。Apache2.0协议,可免费在商业产品中使用,不需要公布源码,无潜在商业风险。
长按二维码,关注我们
点击阅读原文,进入RT-Thread 官网
你点的每个“在看”,我都认真当成了喜欢