专栏首页AIoT开源项目分享RT-Thread进阶之网络框架

RT-Thread进阶之网络框架

1、网卡组件(netdev)

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 之间关系如下图所示:

1.1 netdev数据结构

每个网卡对应唯一的网卡结构体对象,其中包含该网卡的主要信息和实时状态,用于后面网卡信息的获取和设置。

网卡状态:

  • up/down:底层网卡初始化完成之后置为 up 状态,用于判断网卡开启还是禁用。
  • link_up/link_down:用于判断网卡设备是否具有有效的链路连接,连接后可以与其他网络设备进行通信。该状态一般由网卡底层驱动设置。
  • internet_up/internet_down:用于判断设备是否连接到因特网,接入后可以与外网设备进行通信。
  • dhcp_enable/dhcp_disable:用于判断当前网卡设备是否开启 DHCP 功能支持。

1.2 网卡列表和默认网卡

/* The list of network interface device */
struct netdev *netdev_list;
/* The default network interface device */
struct netdev *netdev_default;

为了方便网卡的管理和控制,netdev 组件中提供网卡列表用于统一管理各个网卡设备,系统中每个网卡在初始化时会创建和注册网卡对象到 netdev 组件网卡列表中。 网卡列表中有且只有一个默认网卡,一般为系统中第一个注册的网卡,可以通过 netdev_set_default() 函数设置默认网卡,默认网卡的主要作用是确定优先使用的进行网络通讯的网卡类型,方便网卡的切换和网卡信息的获取。

1.3 网卡注册

int netdev_register(struct netdev *netdev, const char *name, void *user_data);

参数

描述

netdev

网卡对象

name

网卡名称

user_data

用户使用数据

返回

——

0

网卡注册成功

-1

网卡注册失败

将网卡挂载到网卡列表(netdev_list)和默认网卡(netdev_default)。 该函数不需要在用户层调用,一般为网卡驱动初始化完成之后自动调用,如 esp8266 网卡的注册在 esp8266 设备网络初始化之后自动完成。

1.2 注销网卡

该函数可以在网卡使用时,注销网卡的注册,即从网卡列表中删除对应网卡,注销网卡的接口如下所示:

int netdev_unregister(struct netdev *netdev);

1.3 获取网卡对象

  • 通过状态获取第一个匹配的网卡对象
struct netdev *netdev_get_first_by_flags(uint16_t flags);
  • 获取第一个指定协议簇类型的网卡对象
struct netdev *netdev_get_by_family(int family);
  • 通过 IP 地址获取网卡对象
struct netdev *netdev_get_by_ipaddr(ip_addr_t *ip_addr);

该函数主要用于 bind 函数绑定指定 IP 地址时获取网卡状态信息的情况。

  • 通过名称获取网卡对象
struct netdev *netdev_get_by_name(const char *name);

1.4 设置网卡信息

  • 设置默认网卡
void netdev_set_default(struct netdev *netdev);
  • 设置网卡 up/down 状态
int netdev_set_up(struct netdev *netdev);
int netdev_set_down(struct netdev *netdev);
  • 设置网卡 DHCP 功能状态 DHCP 即动态主机配置协议,如果开启该网卡 DHCP 功能将无法设置该网卡 IP 、网关和子网掩码地址等信息,如果关闭该功能则可以设置上述信息。
int netdev_dhcp_enabled(struct netdev *netdev, rt_bool_t is_enabled);
  • 设置网卡地址信息 设置指定网卡地址 IP 、网关和子网掩码地址,需要在网卡关闭 DHCP 功能状态使用。
/* 设置网卡 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);
  • 设置网卡回调函数 可以用于设备网卡状态改变时调用的回调函数,状态的改变包括:up/down、 link_up/link_down、internet_up/internet_down、dhcp_enable/dhcp_disable 等。
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);

1.5 获取网卡信息

  • 判断网卡是否为 up 状态
#define netdev_is_up(netdev)
  • 判断网卡是否为 link_up 状态
#define netdev_is_link_up(netdev)
  • 判断网卡是否为 internet_up 状态
#define netdev_is_internet_up(netdev)
  • 判断网卡 DHCP 功能是否开启
#define netdev_is_dhcp_enable(netdev)

1.6 默认网卡自动切换

单网卡模式下,开启和关闭默认网卡自动切换功能无明显效果。 多网卡模式下,如果开启默认网卡自动切换功能,当前默认网卡状态改变为 down 或 link_down 时,默认网卡会切换到网卡列表中第一个状态为 up 和 link_up 的网卡。这样可以使一个网卡断开后快速切换到另一个可用网卡,简化用户应用层网卡切换操作。如果未开启该功能,则不会自动切换默认网卡。

1.7 FinSH 命令

2、套接字组件(SAL)

socket 编程模型如下图所示:

客户端使用流程:

  • socket() 创建一个 socket,返回套接字的描述符,并为其分配系统资源。
  • connect() 向服务器发出连接请求。
  • send()/recv() 与服务器进行通信。
  • closesocket() 关闭 socket,回收资源。

服务器使用流程:

  • socket() 创建一个 socket,返回套接字的描述符,并为其分配系统资源。
  • bind() 将套接字绑定到一个本地地址和端口上。
  • listen() 将套接字设为监听模式并设置监听数量,准备接收客户端请求。
  • accept() 等待监听的客户端发起连接,并返回已接受连接的新套接字描述符。
  • recv()/send() 用新套接字与客户端进行通信。
  • closesocket() 关闭 socket,回收资源。

2.1 SAL 简介

SAL 组件主要功能特点:

  • 抽象、统一多种网络协议栈接口;
  • 提供 Socket 层面的 TLS 加密传输特性;
  • 支持标准 BSD Socket API;
  • 统一的 FD 管理,便于使用 read/write poll/select 来操作网络功能;

SAL 网络框架:

2.2 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套接字
  • 分配一个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

链接服务器接口:

int connect(int s, const struct sockaddr *name, socklen_t namelen)

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 组件中,最终调用的操作函数。

2.3 数据结构

网络接口设备协议簇数据结构:

SAL 套接字表数据结构:

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)进行赋值。

2.4 SAL Socket API 介绍

int sal_socket(int domain, int type, int protocol)
  • 在套接字表中分配一个新的套接字和注册的套接字选项
  • 通过套接字描述符获取sal套接字对象
  • 初始化sal套接字对象
  • 打开有效的网络接口套接字(at_socket/lwip_socket)
int sal_bind(int socket, const struct sockaddr *name, socklen_t namelen)
  • 通过套接字描述符获取套接字对象
  • 检查输入ipaddr是否是默认的netdev ipaddr,如果不是根据ip地址获取新的网卡设备
  • 通过网络接口设备检查和获取协议族
  • 调用对应驱动的bind接口(at_bind/lwip_bind)
int sal_connect(int socket, const struct sockaddr *name, socklen_t namelen)
  • 通过套接字描述符获取套接字对象
  • 调用对应驱动的connect接口(at_connect/lwip_connect)

其他接口:

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);

2.5 BSD Socket API 介绍

创建套接字(socket)

int socket(int domain, int type, int protocol);
  • 创建一个BSD套接字
  • 分配一个fd文件描述符
  • 通过sal_socket()接口创建套接字
  • 初始化fd文件描述符,然后将套接字socket放入dfs_fd

绑定套接字(bind)

int bind(int s, const struct sockaddr *name, socklen_t namelen);
  • 调用sal_bind()

建立连接(connect)

int connect(int s, const struct sockaddr *name, socklen_t namelen)sal_connect
  • 调用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)

3、AT组件

AT 命令集是一种应用于 AT 服务器(AT Server)与 AT 客户端(AT Client)间的设备连接与数据通信的方式。其基本结构如下图所示:

AT 命令由三个部分组成,分别是:前缀、主体和结束符。其中前缀由字符 AT 构成;主体由命令、参数和可能用到的数据组成;结束符一般为("\r\n")。 响应数据: AT Client 发送命令之后收到的 AT Server 响应状态和信息。 URC 数据:AT Server 主动发送给 AT Client 的数据,一般出现在一些特殊的情况,比如 WIFI 连接断开、TCP 接收数据等,这些情况往往需要用户做出相应操作。

3.1 AT 组件简介

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 组件目前支持多客户端同时运行

3.2 AT Client

AT Client 主要功能是发送 AT 命令、接收数据并解析数据。 AT Client列表:

static struct at_client at_client_table[AT_CLIENT_NUM_MAX] = { 0 };

AT 客户端都挂载在at_client_table里。 AT Client数据结构:

3.2.1 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 数据的处理。

3.2.2 AT Client 数据收发方式

创建响应结构体:

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, ...);

3.2.3 AT Client 数据解析方式

获取指定行号的响应数据: 该函数用于在 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);

3.2.4 AT Client URC数据处理

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;
}

3.2.5 AT Client其他接口

发送指定长度数据:

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);

3.3 AT 协议簇

3.3.1 AT 设备框架

网卡的初始化和注册建立在协议簇类型上,所以每种网卡对应唯一的协议簇类型。每种协议栈对应一种协议簇类型(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设备数据结构:

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找到具体的网卡驱动。

3.3.2 AT Socket

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 数据结构:

3.3.2.1 AT Socket API介绍

int at_socket(int domain, int type, int protocol)
  • 通过协议族AF_AT获取第一个指定协议簇类型的网卡对象
  • 通过网卡对象的名字获得AT设备的对象
  • 通过AT设备的对象分配并初始化一个新的AT套接字
int at_bind(int socket, const struct sockaddr *name, socklen_t namelen)
  • 获取当前设备ip地址
  • 从sockaddr结构中选择ip地址和端口
  • 如果输入的ip地址不同于设备的ip地址,则根据输入的ip分配新的套接字,否则返回。
int at_connect(int socket, const struct sockaddr *name, socklen_t namelen)
  • socketaddr结构获取IP地址和端口
  • 调用对应AT网卡驱动的_socket_connect()链接服务器
  • 设置套接字接收数据回调函数
int at_sendto(int socket, const void *data, size_t size, int flags, const struct sockaddr *to, socklen_t tolen)
  • 调用对应AT网卡驱动的_socket_send()发送数据

其他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)

4、应用实例

4.1 使用at_device软件包的ESP8266模组

使用AT Socket 功能的框架:

启动流程:

4.1.1 注册ESP8266设备驱动

static int esp8266_device_class_register(void)

  • 创建并初始化ESP8266 device class对象
  • 在at_device_class_list列表注册AT_DEVICE_CLASS_ESP8266客户端ID

注册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,
};

4.1.2 初始化ESP8266设备,链接上无线网络

#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);
  • 从at_device_class_list列表通过客户端ID获取ESP8266设备类对象
  • 创建并初始化AT device class对象
  • 在at_device_list列表注册AT设备
  • 调用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},
};

4.2 lwip网络协议栈驱动移植

驱动架构图:

4.2.1 添加lwip协议栈软件包

在 RT-Thread Setting 文件中借助图形化配置工具打开软件 lwip 的组件,保存更新。

4.2.2 移植网络设备层和LAN8720驱动移植

移植网络设备层和LAN8720驱动: 本例中使用的是 stm32f429-fire-challenger开发板,所以需要下载 BSP的LWIP驱动,将下载的LWIP驱动源码 drv_etc.c 和 drv_etc.h 文件添加到自己工程驱动文件所在的路径。

将drv_etc.c代码做一下更动:

  • 将#include <drv_log.h>改为#include <rtdbg.h>
  • 删除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

移植完成,编译。

4.2.3 网络设备层和LAN8720驱动解析

4.2.3.1 网络设备层解析

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()调用。

4.2.3.2 LAN8720驱动解析:

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)

由系统自动初始化机制调用。

5 使用网卡设备链接服务器

下面示例完成通过传入的网卡名称绑定该网卡 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);

本文分享自微信公众号 - AIoT开源项目分享(Aladdin-Wang),作者:AIoTkk

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-06-23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 制作STM32F429的SPI FLASH下载算法

    AIoT-KK
  • 【DIY数字仪表】STM32F429移植TouchGFX到RT-Thread系统(1)

    硬件: 野火挑战者STM32F429 V1开发版,5寸屏 软件:最新版本的STM32CubeF4固件库,TouchGFXDesigner v4.13和 ST...

    AIoT-KK
  • STM32通用低功耗组件——PM

    嵌入式系统低功耗管理的目的在于满足用户对性能需求的前提下,尽可能降低系统能耗以延长设备待机时间。高性能与有限的电池能量在嵌入式系统中矛盾最为突出,硬件低功耗设计...

    AIoT-KK
  • PAT 1011 World Cup Betting

    1011. World Cup Betting (20) 时间限制 400 ms 内存限制 65536 kB 代码长度限制 16000 B...

    ShenduCC
  • Nginx配置文件功能注释

    最近在学配置Nginx做均衡负载,首先最重要的是Nginx的配置文件,参考网上查到的资料,并结合实践, 将该配置文件的详细功能注释在这里做下笔记,方便今后查阅。...

    joshua317
  • openresty (nginx)

    以谁为师
  • 记一次RabbitMQ(3.8版本)在liunx下安装

    吴文周
  • python基础项目实战:将python文件打包成EXE应用程序

    相信大家都想把自己完成的项目打包成EXE应用文件,然后就可以放在桌面随时都能运行了,下面来分享利用pytinstaller这个第三方库来打包程序,既简单又快捷,...

    一墨编程学习
  • 最新世界大学排名:计算机专业哪家强?

    泰晤士高等教育世界大学排名、QS 世界大学排名、USNews 世界大学排名、上海软科世界大学学术排名是公认的四大权威世界大学排名。

    GitHubDaily
  • 实战 | Python爬取B站柯南弹幕+Gephi梳理主线剧情

    利用Chrome浏览器抓包可知,B站的弹幕文件以XML文档式进行储存,如下所示(共三千条实时弹幕)

    朱小五

扫码关注云+社区

领取腾讯云代金券