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)

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

6 socket 多線程非阻塞網絡編程

socket 編程簡介:
在 RT-Thread 使用 socket 網絡編程時,當一個任務調用 socket的 recv()函數接收數據時,如果 socket 上並沒有接收到數據,這個任務將阻塞在這個 recv() 函數裏。這個時候,這個任務想要處理一些其他事情,例如進行一些數據採集,發送一些額外數據到網絡上等,將變得不可能了。與此同時,其他線程也需要將數據上傳同一個服務器,如果直接多個線程共同使用一個 socket 操作,這將會破壞底層 lwip 的消息事件模型。
socket 編程模型如下圖所示:

客戶端使用流程:

  • socket() 創建一個 socket,返回套接字的描述符,併爲其分配系統資源。
  • connect() 向服務器發出連接請求。
  • send()/recv() 與服務器進行通信。
  • closesocket() 關閉 socket,回收資源。

服務器使用流程:

  • socket() 創建一個 socket,返回套接字的描述符,併爲其分配系統資源。
  • bind() 將套接字綁定到一個本地地址和端口上。
  • listen() 將套接字設爲監聽模式並設置監聽數量,準備接收客戶端請求。
  • accept() 等待監聽的客戶端發起連接,並返回已接受連接的新套接字描述符。
  • recv()/send() 用新套接字與客戶端進行通信。
  • closesocket() 關閉 socket,回收資源。

在上面網絡客戶端操作過程中,當進行 recv 操作時,如果對應的通道數據沒有準備好,那系統就會讓當前任務進入阻塞狀態,當前任務不能再進行其他的操作。

非阻塞 socket 編程簡介

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章