netdev 组件主要作用是解决设备多网卡连接时网络连接问题,用于统一管理各个网卡信息与网络连接状态,并且提供统一的网卡调试命令接口。 其主要功能特点如下所示:
网卡概念:
网卡概念介绍之前先了解协议栈相关概念,协议栈是指网络中各层协议的总和,每种协议栈反映了不同的网络数据交互方式,RT-Thread 系统中目前支持三种协议栈类型: lwIP 协议栈、AT Socket 协议栈、WIZnet TCP/IP硬件协议栈。每种协议栈对应一种协议簇类型(family),上述协议栈分别对应的协议簇类型为:AF_INET、AF_AT、AF_WIZ。
网卡的初始化和注册建立在协议簇类型上,所以每种网卡对应唯一的协议簇类型。 Socket 套接字描述符的创建建立在 netdev 网卡基础上,所以每个创建的 Socket 对应唯一的网卡。协议簇、网卡和 socket 之间关系如下图所示:
每个网卡对应唯一的网卡结构体对象,其中包含该网卡的主要信息和实时状态,用于后面网卡信息的获取和设置。
网卡状态:
/* The list of network interface device */
struct netdev *netdev_list;
/* The default network interface device */
struct netdev *netdev_default;
为了方便网卡的管理和控制,netdev 组件中提供网卡列表用于统一管理各个网卡设备,系统中每个网卡在初始化是会创建和注册网卡对象到 netdev 组件网卡列表中。
网卡列表中有且只有一个默认网卡,一般为系统中第一个注册的网卡,可以通过 netdev_set_default() 函数设置默认网卡,默认网卡的主要作用是确定优先使用的进行网络通讯的网卡类型,方便网卡的切换和网卡信息的获取。
int netdev_register(struct netdev *netdev, const char *name, void *user_data);
参数 | 描述 |
---|---|
netdev | 网卡对象 |
name | 网卡名称 |
user_data | 用户使用数据 |
返回 | —— |
0 | 网卡注册成功 |
-1 | 网卡注册失败 |
将网卡挂载到网卡列表(*netdev_list)和默认网卡(*netdev_default)。
该函数不需要在用户层调用,一般为网卡驱动初始化完成之后自动调用,如 esp8266 网卡的注册在 esp8266 设备网络初始化之后自动完成。
该函数可以在网卡使用时,注销网卡的注册,即从网卡列表中删除对应网卡,注销网卡的接口如下所示:
int netdev_unregister(struct netdev *netdev);
struct netdev *netdev_get_first_by_flags(uint16_t flags);
struct netdev *netdev_get_by_family(int family);
struct netdev *netdev_get_by_ipaddr(ip_addr_t *ip_addr);
该函数主要用于 bind 函数绑定指定 IP 地址时获取网卡状态信息的情况。
struct netdev *netdev_get_by_name(const char *name);
void netdev_set_default(struct netdev *netdev);
int netdev_set_up(struct netdev *netdev);
int netdev_set_down(struct netdev *netdev);
int netdev_dhcp_enabled(struct netdev *netdev, rt_bool_t is_enabled);
/* 设置网卡 IP 地址 */
int netdev_set_ipaddr(struct netdev *netdev, const ip_addr_t *ipaddr);
/* 设置网卡网关地址 */
int netdev_set_gw(struct netdev *netdev, const ip_addr_t *gw);
/* 设置网卡子网掩码地址 */
int netdev_set_netmask(struct netdev *netdev, const ip_addr_t *netmask);
/* 设置网卡 DNS 服务器地址,主要用于网卡域名解析功能 */
int netdev_set_dns_server(struct netdev *netdev, uint8_t dns_num, const ip_addr_t *dns_server);
ypedef void (*netdev_callback_fn )(struct netdev *netdev, enum netdev_cb_type type);
void netdev_set_status_callback(struct netdev *netdev, netdev_callback_fn status_callback);
#define netdev_is_up(netdev)
#define netdev_is_link_up(netdev)
#define netdev_is_internet_up(netdev)
#define netdev_is_dhcp_enable(netdev)
单网卡模式下,开启和关闭默认网卡自动切换功能无明显效果。
多网卡模式下,如果开启默认网卡自动切换功能,当前默认网卡状态改变为 down 或 link_down 时,默认网卡会切换到网卡列表中第一个状态为 up 和 link_up 的网卡。这样可以使一个网卡断开后快速切换到另一个可用网卡,简化用户应用层网卡切换操作。如果未开启该功能,则不会自动切换默认网卡。
服务器使用流程:
在上面网络客户端操作过程中,当进行 recv 操作时,如果对应的通道数据没有准备好,那系统就会让当前任务进入阻塞状态,当前任务不能再进行其他的操作。
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()等接口时,会调用如下相应接口:
const struct dfs_file_ops _net_fops =
{
NULL, /* open */
dfs_net_close,
dfs_net_ioctl,
dfs_net_read,
dfs_net_write,
NULL,
NULL, /* lseek */
NULL, /* getdents */
dfs_net_poll,
};
创建套接字接口:
int socket(int domain, int type, int protocol);
socket调用的流程大致如下:socket->sal_socket->at_socket/lwip_socket.
上述为标准 BSD Socket API 中 socket 创建函数的定义,domain 表示协议域又称为协议簇(family),用于判断使用哪种协议栈或网络实现,AT Socket 协议栈使用的簇类型为 AF_AT,lwIP 协议栈使用协议簇类型有 AF_INET等,WIZnet 协议栈使用的协议簇类型为 AF_WIZ。
对于不同的软件包,socket 传入的协议簇类型可能是固定的,不会随着 SAL 组件接入方式的不同而改变。为了动态适配不同协议栈或网络实现的接入,SAL 组件中对于每个协议栈或者网络实现提供两种协议簇类型匹配方式:主协议簇类型和次协议簇类型。socket 创建时先判断传入协议簇类型是否存在已经支持的主协议类型,如果是则使用对应协议栈或网络实现,如果不是判断次协议簇类型是否支持。目前系统支持协议簇类型如下:
链接服务器接口:
int connect(int s, const struct sockaddr *name, socklen_t namelen)
connect调用的流程大致如下:connect->sal_connect->at_connect/lwip_connect.
static struct sal_socket_table socket_table;
初始化sal套接字:
int 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)进行赋值。
int sal_socket(int domain, int type, int protocol)
int sal_bind(int socket, const struct sockaddr *name, socklen_t namelen)
int sal_connect(int socket, const struct sockaddr *name, socklen_t namelen)
其他接口:
int sal_accept(int socket, struct sockaddr *addr, socklen_t *addrlen)
int sal_shutdown(int socket, int how)
int sal_getpeername (int socket, struct sockaddr *name, socklen_t *namelen);
int sal_getsockname (int socket, struct sockaddr *name, socklen_t *namelen);
int sal_getsockopt (int socket, int level, int optname, void *optval, socklen_t *optlen);
int sal_setsockopt (int socket, int level, int optname, const void *optval, socklen_t optlen);
int sal_listen(int socket, int backlog);
int sal_recvfrom(int socket, void *mem, size_t len, int flags,
struct sockaddr *from, socklen_t *fromlen);
int sal_sendto(int socket, const void *dataptr, size_t size, int flags,
const struct sockaddr *to, socklen_t tolen);
int sal_socket(int domain, int type, int protocol);
int sal_closesocket(int socket);
int sal_ioctlsocket(int socket, long cmd, void *arg);
创建套接字(socket)
int socket(int domain, int type, int protocol);
绑定套接字(bind)
int bind(int s, const struct sockaddr *name, socklen_t namelen);
建立连接(connect)
int connect(int s, const struct sockaddr *name, socklen_t namelen)sal_connect
监听套接字(listen)
int listen(int s, int backlog)
接收连接(accept)
int accept(int s, struct sockaddr *addr, socklen_t *addrlen)
TCP 数据发送(send)
int send(int s, const void *dataptr, size_t size, int flags)
TCP 数据接收(recv)
int recv(int s, void *mem, size_t len, int flags)
UDP 数据发送(sendto)
int sendto(int s, const void *dataptr, size_t size, int flags, const struct sockaddr *to, socklen_t tolen)
UDP 数据接收(recvfrom)
int recvfrom(int s, void *mem, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen)
关闭套接字(closesocket)
int closesocket(int s)
按设置关闭套接字(shutdown)
int shutdown(int s, int how)
设置套接字选项(setsockopt)
int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen)
获取套接字选项(getsockopt)
int getsockopt(int s, int level, int optname, void *optval, socklen_t *optlen)
获取远端地址信息(getpeername)
int getpeername(int s, struct sockaddr *name, socklen_t *namelen)
获取本地地址信息(getsockname)
int getsockname(int s, struct sockaddr *name, socklen_t *namelen)
配置套接字参数(ioctlsocket)
int ioctlsocket(int s, long cmd, void *arg)
AT 命令集是一种应用于 AT 服务器(AT Server)与 AT 客户端(AT Client)间的设备连接与数据通信的方式。 其基本结构如下图所示:
AT 命令由三个部分组成,分别是:前缀、主体和结束符。其中前缀由字符 AT 构成;主体由命令、参数和可能用到的数据组成;结束符一般为 ("\r\n")。
响应数据: AT Client 发送命令之后收到的 AT Server 响应状态和信息。
URC 数据: AT Server 主动发送给 AT Client 的数据,一般出现在一些特殊的情况,比如 WIFI 连接断开、TCP 接收数据等,这些情况往往需要用户做出相应操作。
AT 组件是基于 RT-Thread 系统的 AT Server 和 AT Client 的实现,组件完成 AT 命令的发送、命令格式及参数判断、命令的响应、响应数据的接收、响应数据的解析、URC 数据处理等整个 AT 命令数据交互流程。
通过 AT 组件,设备可以作为 AT Client 使用串口连接其他设备发送并接收解析数据,可以作为 AT Server 让其他设备甚至电脑端连接完成发送数据的响应,也可以在本地 shell 启动 CLI 模式使设备同时支持 AT Server 和 AT Client 功能,该模式多用于设备开发调试。
AT Server 主要功能特点:
AT Client 主要功能是发送 AT 命令、接收数据并解析数据。
AT Client列表:
static struct at_client at_client_table[AT_CLIENT_NUM_MAX] = { 0 };
AT 客户端都挂载在at_client_table里。
AT Client数据结构:
创建AT客户端对象,初始化客户端对象参数。
int 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 数据的处理。
创建响应结构体:
at_response_t at_create_resp(rt_size_t buf_size, rt_size_t line_num, rt_int32_t timeout);
删除响应结构体:
void at_delete_resp(at_response_t resp);
设置响应结构体参数:
at_response_t at_resp_set_info(at_response_t resp, rt_size_t buf_size, rt_size_t line_num, rt_int32_t timeout);
发送命令并接收响应:
rt_err_t at_exec_cmd(at_response_t resp, const char *cmd_expr, ...);
获取指定行号的响应数据:
该函数用于在 AT Server 响应数据中获取指定行号的一行数据。
const char *at_resp_get_line(at_response_t resp, rt_size_t resp_line);
获取指定关键字的响应数据:
该函数用于在 AT Server 响应数据中通过关键字获取对应的一行数据。
const char *at_resp_get_line_by_kw(at_response_t resp, const char *keyword);
解析指定行号的响应数据:
该函数用于在 AT Server 响应数据中获取指定行号的一行数据, 并解析该行数据中的参数。
int at_resp_parse_line_args(at_response_t resp, rt_size_t resp_line, const char *resp_expr, ...);
发送命令并解析接收响应例程:
/*
* 程序清单:AT Client 发送命令并解析接收响应例程
*/
int user_at_client_send(int argc, char**argv)
{
at_response_t resp = RT_NULL;
char ip[20];
char mac[20];
char uartdata[20];
if (argc != 2)
{
LOG_E("at_cli_send [command] - AT client send commands to AT server.");
return -RT_ERROR;
}
/* 创建响应结构体,设置最大支持响应数据长度为 512 字节,响应数据行数无限制,超时时间为 5 秒 */
resp = at_create_resp(512, 0, rt_tick_from_millisecond(5000));
if (!resp)
{
LOG_E("No memory for response structure!");
return -RT_ENOMEM;
}
/* 发送 AT 命令并接收 AT Server 响应数据,数据及信息存放在 resp 结构体中 */
if (at_exec_cmd(resp, argv[1]) != RT_EOK)
{
LOG_E("AT client send commands failed, response error or timeout !");
return -1;
}
/* 命令发送成功 */
rt_kprintf("AT Client send commands to AT Server success!\n");
if(at_resp_get_line_by_kw(resp,"UART")!= NULL)
{
/* 解析获取串口配置信息AT+UART?,1 表示解析响应数据第一行 */
at_resp_parse_line_args(resp, 1,"+UART:%s", uartdata);
rt_kprintf("+UART:%s\n",uartdata);
}
/* 删除响应结构体 */
at_delete_resp(resp);
return RT_EOK;
}
/* 输出 at_Client_send 函数到 msh 中 */
MSH_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 数据列表初始化:
void at_set_urc_table(const struct at_urc *table, rt_size_t size);
AT Client 移植具体示例:
static void urc_conn_func(const char *data, rt_size_t size)
{
/* WIFI 连接成功信息 */
LOG_D("AT Server device WIFI connect success!");
}
static void urc_recv_func(const char *data, rt_size_t size)
{
/* 接收到服务器发送数据 */
LOG_D("AT Client receive AT Server data!");
}
static void urc_func(const char *data, rt_size_t size)
{
/* 设备启动信息 */
LOG_D("AT Server device startup!");
}
static struct at_urc urc_table[] = {
{"WIFI CONNECTED", "\r\n", urc_conn_func},
{"+RECV", ":", urc_recv_func},
{"RDY", "\r\n", urc_func},
};
int at_client_port_init(void)
{
/* 添加多种 URC 数据至 URC 列表中,当接收到同时匹配 URC 前缀和后缀的数据,执行 URC 函数 */
at_set_urc_table(urc_table, sizeof(urc_table) / sizeof(urc_table[0]));
return RT_EOK;
}
发送指定长度数据:
rt_size_t at_client_send(const char *buf, rt_size_t size);
接收指定长度数据:
rt_size_t at_client_recv(char *buf, rt_size_t size,rt_int32_t timeout);
设置接收数据的行结束符:
void at_set_end_sign(char ch);
等待模块初始化完成:
int at_client_wait_connect(rt_uint32_t timeout);
网卡的初始化和注册建立在协议簇类型上,所以每种网卡对应唯一的协议簇类型。 每种协议栈对应一种协议簇类型(family),AT协议簇对应的协议栈是AT Socket 协议栈,每种AT设备都对应唯一的AT Socket 协议栈。
AT 设备列表:
/* The global list of at device */
static rt_slist_t at_device_list = RT_SLIST_OBJECT_INIT(at_device_list);
/* The global list of at device class */
static 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设备数据结构:
int at_device_register(struct at_device *device, const char *device_name,
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 功能的使用依赖于如下几个组件:
int at_socket(int domain, int type, int protocol)
int at_bind(int socket, const struct sockaddr *name, socklen_t namelen)
int at_connect(int socket, const struct sockaddr *name, socklen_t namelen)
int at_sendto(int socket, const void *data, size_t size, int flags, const struct sockaddr *to, socklen_t tolen)
其他API
int at_closesocket(int socket)
int at_recvfrom(int socket, void *mem, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen)
int at_getsockopt(int socket, int level, int optname, void *optval, socklen_t *optlen)
int at_setsockopt(int socket, int level, int optname, const void *optval, socklen_t optlen)
int at_shutdown(int socket, int how)
static int esp8266_device_class_register(void)
注册esp8266设备操作函数:
static const struct at_device_ops esp8266_device_ops =
{
esp8266_init,
esp8266_deinit,
esp8266_control,
};
class->device_ops = &esp8266_device_ops
注册esp8266_at_socket操作接口:
static const struct at_socket_ops esp8266_socket_ops =
{
esp8266_socket_connect,
esp8266_socket_close,
esp8266_socket_send,
esp8266_domain_resolve,
esp8266_socket_set_event_cb,
};
#define ESP8266_SAMPLE_DEIVCE_NAME "esp0"
static struct at_device_esp8266 esp0 =
{
ESP8266_SAMPLE_DEIVCE_NAME,
ESP8266_SAMPLE_CLIENT_NAME,
ESP8266_SAMPLE_WIFI_SSID,
ESP8266_SAMPLE_WIFI_PASSWORD,
ESP8266_SAMPLE_RECV_BUFF_LEN,
};
struct at_device_esp8266 *esp8266 = &esp0;
return at_device_register(&(esp8266->device),
esp8266->device_name,
esp8266->client_name,
AT_DEVICE_CLASS_ESP8266,
(void *) esp8266);
static int esp8266_init(struct at_device *device)
创建esp_net线程,链接无线网络后自动销毁
static void esp8266_init_thread_entry(void *parameter)
注册ESP8266设备操作接口:
static const struct netdev_ops esp8266_netdev_ops =
{
esp8266_netdev_set_up,
esp8266_netdev_set_down,
esp8266_netdev_set_addr_info,
esp8266_netdev_set_dns_server,
esp8266_netdev_set_dhcp,
#ifdef NETDEV_USING_PING
esp8266_netdev_ping,
#endif
#ifdef NETDEV_USING_NETSTAT
esp8266_netdev_netstat,
#endif
};
netdev->ops = &esp8266_netdev_ops
static int esp8266_net_init(struct at_device *device)
注册urc_table
static const struct at_urc urc_table[] =
{
{"busy p", "\r\n", urc_busy_p_func},
{"busy s", "\r\n", urc_busy_s_func},
{"WIFI CONNECTED", "\r\n", urc_func},
{"WIFI DISCONNECT", "\r\n", urc_func},
};
static const struct at_urc urc_table[] =
{
{"SEND OK", "\r\n", urc_send_func},
{"SEND FAIL", "\r\n", urc_send_func},
{"Recv", "bytes\r\n", urc_send_bfsz_func},
{"", ",CLOSED\r\n", urc_close_func},
{"+IPD", ":", urc_recv_func},
};
在 RT-Thread Setting 文件中借助图形化配置工具打开软件 lwip 的组件,保存更新。
移植网络设备层和LAN8720驱动:
本例中使用的是 stm32f429-fire-challenger开发板,所以需要下载 BSP的LWIP驱动,将下载的LWIP驱动源码 drv_etc.c 和 drv_etc.h 文件添加到自己工程驱动文件所在的路径。
将drv_etc.c代码做一下更动:
添加ETH外设配置:
打开stm32f429-fire-challenger的BSP,在board目录下找到stm32f4xx_hal_msp.c文件,移植到工程中。
然后改动stm32f4xx_hal_msp.c里的代码:
void HAL_ETH_MspInit(ETH_HandleTypeDef* heth)
和void HAL_ETH_MspDeInit(ETH_HandleTypeDef* heth)
RT-Thread 的 lwIP 移植在原版的基础上,添加了网络设备层以替换原来的驱动层。和原来的驱动层不同的是,对于以太网数据的收发采用了独立的双线程结构,erx 线程和 etx 线程在正常情况下,两者的优先级设置成相同,用户可以根据自身实际要求进行微调以侧重接收或发送。
数据接收流程:
当以太网硬件设备收到网络报文产生中断时,接收到的数据会被存放到接收缓冲区,然后以太网中断程序会发送邮件来唤醒 erx 线程,erx 线程会按照接收到的数据长度来申请 pbuf,并将数据放入 pbuf 的 payload 中,然后将 pbuf 通过邮件发送给 去处理。
数据发送流程:
当有数据需要发送时,LwIP 会将数据通过邮件发送给 etx 线程,然后永久等待在 tx_ack 信号量上。etx 线程接收到邮件后,通过调用驱动中的 rt_stm32_eth_tx() 函数发送数据,发送完成之后再发送一次 tx_ack 信号量唤醒 LwIP
网络设备介绍:
RT-Thread 网络设备继承了标准设备,由 eth_device 结构体定义:
struct eth_device
{
/* 标准设备 */
struct rt_device parent;
/* lwIP 网络接口 */
struct netif *netif;
/* 发送应答信号量 */
struct rt_semaphore tx_ack;
/* 网络状态标志 */
rt_uint16_t flags;
rt_uint8_t link_changed;
rt_uint8_t link_status;
/* 数据包收发接口 */
struct pbuf* (*eth_rx)(rt_device_t dev);
rt_err_t (*eth_tx)(rt_device_t dev, struct pbuf* p);
};
实现数据包收发接口,对应了 eth_device 结构体中的 eth_rx 及 eth_tx 元素:
rt_err_t rt_stm32_eth_tx(rt_device_t dev, struct pbuf* p);
struct pbuf *rt_stm32_eth_rx(rt_device_t dev);
注册以太网设备,初始化以太网硬件,配置 MAC 地址:
rt_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的结构定义:
struct rt_stm32_eth
{
/* inherit from ethernet device */
struct eth_device parent;
rt_timer_t poll_link_timer;
/* interface address info, hw address */
rt_uint8_t dev_addr[MAX_ADDR_LEN];
/* ETH_Speed */
uint32_t ETH_Speed;
/* ETH_Duplex_Mode */
uint32_t ETH_Mode;
};
实现rt_device设备的接口:
static rt_err_t rt_stm32_eth_init(rt_device_t dev);
static rt_err_t rt_stm32_eth_open(rt_device_t dev, rt_uint16_t oflag);
static rt_err_t rt_stm32_eth_close(rt_device_t dev);
static rt_size_t rt_stm32_eth_read(rt_device_t dev, rt_off_t pos, void* buffer, rt_size_t size);
static rt_size_t rt_stm32_eth_write (rt_device_t dev, rt_off_t pos, const void* buffer, rt_size_t size);
static 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 地址。如果需要,也可以通过增加控制字的方式来扩展其他控制功能。
实现驱动层的数据包收发接口:
rt_stm32_eth_rx()
rt_stm32_eth_rx 会去读取接收缓冲区中的数据,并放入 pbuf(lwIP 中利用结构体 pbuf 来管理数据包 )中,并返回 pbuf 指针。
网络设备层的“erx” 接收线程会阻塞在获取 eth_rx_thread_mb 邮箱上,当它接收到邮件时,会调用 rt_stm32_eth_rx 去接收数据。
rt_stm32_eth_tx()
rt_stm32_eth_tx 会将要发送的数据放入发送缓冲区,等待 DMA 来发送数据。
网络设备层的“etx” 发送线程会阻塞在获取 eth_tx_thread_mb 邮箱上, 当它接收到邮件时,会调用 rt_stm32_eth_tx 来发送数据。
ETH 设备初始化:
static int rt_hw_stm32_eth_init(void)
INIT_DEVICE_EXPORT(rt_hw_stm32_eth_init);
由系统自动初始化机制调用。
lwip协议栈初始化:
int lwip_system_init(void)
INIT_PREV_EXPORT(lwip_system_init)
由系统自动初始化机制调用。
下面示例完成通过传入的网卡名称绑定该网卡 IP 地址并和服务器进行连接通信的过程:
static int bing_test(int argc, char **argv)
{
struct sockaddr_in client_addr;
struct sockaddr_in server_addr;
struct netdev *netdev = RT_NULL;
int sockfd = -1;
int AF = -1;
uint8_t send_buf[]= "This is a TCP Client test...\n";
uint8_t read_buf[10];
if (argc != 2)
{
rt_kprintf("bind_test [netdev_name] --bind network interface device by name.\n");
return -RT_ERROR;
}
if(rt_strcmp(argv[1], "esp0") == 0)
{
AF = AF_AT;
}else if(rt_strcmp(argv[1], "e0") == 0){
AF = AF_INET;
}else{
return -RT_ERROR;
}
/* 通过名称获取 netdev 网卡对象 */
netdev = netdev_get_by_name(argv[1]);
if (netdev == RT_NULL)
{
rt_kprintf("get network interface device(%s) failed.\n", argv[1]);
return -RT_ERROR;
}
/* 设置默认网卡对象 */
netdev_set_default(netdev);
if ((sockfd = socket(AF, SOCK_STREAM, 0)) < 0)
{
rt_kprintf("Socket create failed.\n");
return -RT_ERROR;
}
/* 初始化需要绑定的客户端地址 */
client_addr.sin_family = AF;
client_addr.sin_port = htons(8080);
/* 获取网卡对象中 IP 地址信息 */
client_addr.sin_addr.s_addr = netdev->ip_addr.addr;
rt_memset(&(client_addr.sin_zero), 0, sizeof(client_addr.sin_zero));
if (bind(sockfd, (struct sockaddr *)&client_addr, sizeof(struct sockaddr)) < 0)
{
rt_kprintf("socket bind failed.\n");
closesocket(sockfd);
return -RT_ERROR;
}
rt_kprintf("socket bind network interface device(%s) success!\n", netdev->name);
/* 初始化预连接的服务端地址 */
server_addr.sin_family = AF;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = inet_addr(SERVER_HOST);
rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));
/* 连接到服务端 */
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) < 0)
{
rt_kprintf("socket connect failed!\n");
closesocket(sockfd);
return -RT_ERROR;
}
else
{
rt_kprintf("socket connect success!\n");
}
write(sockfd,send_buf,sizeof(send_buf));
read(sockfd,read_buf,sizeof(read_buf));
rt_kprintf("%s\n",read_buf);
/* 关闭连接 */
closesocket(sockfd);
return RT_EOK;
}
MSH_CMD_EXPORT(bing_test, bind network interface device test);