IOT-OS之RT-Thread(十三)--- 網絡分層結構 + netdev/SAL原理

後面會重點介紹WLAN與網絡協議部分的實現原理與應用開發,比先前介紹的設備驅動層級更多、更復雜。前一篇博客已經介紹了驅動分層與主從分離思想,這篇博客介紹網絡分層結構,以便更清晰的瞭解WLAN驅動與網絡協議的層級調用關係。

後面使用的網絡協議是LwIP,之前已經寫了一系列的博客專門介紹LwIP網絡協議棧的實現原理,有興趣瞭解TCP/IP協議的可以去看看。後面介紹其與WLAN的適配和網絡應用開發,只介紹LwIP與設備相關的網絡接口層和與應用開發相關的Socket API層。

一、網絡分層結構

在介紹TCP/IP網絡協議在操作系統(比如Linux或RT-Thread)中的層級關係前,先看下TCP/IP網絡協議棧本身的分層結構(圖片取自博客:Linux 網絡棧剖析):
TCP/IP網絡分層結構
我們單獨介紹網絡協議棧時,經常使用上面的層級結構,方便了解每一層的作用及實現原理。但當我們在操作系統中使用網絡協議棧(比如LwIP協議棧)時,常把其看作一個整體,只關心協議棧對下層硬件與上層應用的接口,並不關心協議棧內部具體的實現過程。我們先看下Linux網絡子系統的核心分層架構:
Linux核心網絡棧架構

  • 系統調用接口層:爲應用程序提供訪問內核網絡子系統的方法,主要指socket系統調用,比如BSD Socket API;
  • 協議無關接口層:實現一組基於socket的通用函數來訪問各種不同的協議,比如RT-Thread提供的SAL套接字抽象層,當更換網絡協議棧時,只需要更改該層的代碼,而不需要對上層應用做任何更改;
  • 網絡協議層:實現各種具體的網絡協議,比如LwIP協議;
  • 設備無關接口層:將協議與各種網絡設備驅動連接在一起,並提供一組通用函數供底層網絡設備驅動程序使用,使它們可以操作高層協議棧,比如RT-Thread提供的netdev網絡接口設備層;
  • 設備驅動層:負責管理物理網絡設備的設備驅動程序,比如以太網卡enc28j60驅動、WiFi無線網卡esp8266驅動等。

下面以Pandora開發板上基於ENC28J60以太網卡移植LwIP協議棧的程序代碼爲例,展示RT-Thread網絡分層架構中各層接口的實現與調用關係。

二、RT-Thread網絡分層結構

2.1 ENC28J60設備驅動層

ENC28J60以太網卡與STM32L475芯片間是通過SPI2總線進行通信的,網卡驅動底層也就是SPI2驅動(SPI驅動在前一篇博客:驅動分層與主從分離思想中介紹過了),所以可以把ENC28J60看作一個SPI外設,它繼承自rt_spi_device。

但ENC28J60不僅僅是一個SPI外設,它還是一個以太網卡,還應該繼承網絡接口設備的通用屬性,也即包含eth_device類(LwIP協議提供的以太網接口設備描述)或netdev類(netdev設備無關接口層提供的網絡接口描述),RT-Thread給出的ENC28J60驅動包含了eth_device結構,ENC28J60的設備描述結構如下:

// .\rt-thread\components\drivers\spi\enc28j60.h

struct net_device
{
    /* inherit from ethernet device */
    struct eth_device parent;

    /* interface address info. */
    rt_uint8_t  dev_addr[MAX_ADDR_LEN]; /* hw address   */

    rt_uint8_t emac_rev;
    rt_uint8_t phy_rev;
    rt_uint8_t phy_pn;
    rt_uint32_t phy_id;

    /* spi device */
    struct rt_spi_device *spi_device;
    struct rt_mutex lock;
};
  • ENC28J60作爲SPI外設

ENC28J60既然是SPI外設,自然需要配置SPI2的 I/O 引腳,並使能相關的宏定義(該過程詳見博客:LwIP協議棧移植)。ENC28J60作爲SPI2外設的片選引腳是PD.5,將其綁定到SPI2總線上的過程如下:

// applications\enc28j60_port.c

#define PIN_NRF_CS    GET_PIN(D, 5)        // PD5 :  NRF_CS       --> WIRELESS

int enc28j60_init(void)
{
    __HAL_RCC_GPIOD_CLK_ENABLE();
    rt_hw_spi_device_attach("spi2", "spi21", GPIOD, GPIO_PIN_5);
    ......
}
  • ENC28J60作爲以太網卡設備

ENC28J60作爲以太網卡設備,我們先看看其繼承的父類以太網設備的描述結構:

// rt-thread\components\net\lwip-1.4.1\src\include\netif\ethernetif.h

struct eth_device
{
    /* inherit from rt_device */
    struct rt_device parent;

    /* network interface for lwip */
    struct netif *netif;
    struct rt_semaphore tx_ack;

    rt_uint16_t flags;
    rt_uint8_t  link_changed;
    rt_uint8_t  link_status;

    /* eth device interface */
    struct pbuf* (*eth_rx)(rt_device_t dev);
    rt_err_t (*eth_tx)(rt_device_t dev, struct pbuf* p);
};

eth_device結構也繼承自RT-Thread的設備基類rt_device,同時包含了LwIP協議棧網絡接口設備netif。既然eth_device是一個網絡設備對象,就需要將其註冊到 I/O 設備管理器中(需要藉助SPI設備訪問接口,實現上層要求的rt_device_ops訪問接口和自身需要的eth_rx / eth_tx訪問接口),註冊過程如下:

// rt-thread\components\drivers\spi\enc28j60.c

#ifdef RT_USING_DEVICE_OPS
const static struct rt_device_ops enc28j60_ops = 
{
    enc28j60_init,
    enc28j60_open,
    enc28j60_close,
    enc28j60_read,
    enc28j60_write,
    enc28j60_control
};
#endif

rt_err_t enc28j60_attach(const char *spi_device_name)
{
    struct rt_spi_device *spi_device;

    spi_device = (struct rt_spi_device *)rt_device_find(spi_device_name);
    ......
    /* config spi */
    ......
    enc28j60_dev.spi_device = spi_device;

    /* detect device */
    ......
    /* init rt-thread device struct */
    enc28j60_dev.parent.parent.type    = RT_Device_Class_NetIf;
#ifdef RT_USING_DEVICE_OPS
    enc28j60_dev.parent.parent.ops     = &enc28j60_ops;
#else
    ......
#endif

    /* init rt-thread ethernet device struct */
    enc28j60_dev.parent.eth_rx  = enc28j60_rx;
    enc28j60_dev.parent.eth_tx  = enc28j60_tx;

    rt_mutex_init(&enc28j60_dev.lock, "enc28j60", RT_IPC_FLAG_FIFO);

    eth_device_init(&(enc28j60_dev.parent), "e0");

    return RT_EOK;
}

// rt-thread\components\net\lwip-2.1.0\src\netif\ethernetif.c

rt_err_t eth_device_init(struct eth_device * dev, const char *name)
{
    ......
    return eth_device_init_with_flag(dev, name, flags);
}

/* Keep old drivers compatible in RT-Thread */
rt_err_t eth_device_init_with_flag(struct eth_device *dev, const char *name, rt_uint16_t flags)
{
    struct netif* netif;

    netif = (struct netif*) rt_malloc (sizeof(struct netif));
    ......
    /* set netif */
    dev->netif = netif;
    /* device flags, which will be set to netif flags when initializing */
    dev->flags = flags;
    /* link changed status of device */
    dev->link_changed = 0x00;
    dev->parent.type = RT_Device_Class_NetIf;
    /* register to RT-Thread device manager */
    rt_device_register(&(dev->parent), name, RT_DEVICE_FLAG_RDWR);
    ......

	/* netif config */
	......
    /* if tcp thread has been started up, we add this netif to the system */
    if (rt_thread_find("tcpip") != RT_NULL)
    {
		......
        netifapi_netif_add(netif, &ipaddr, &netmask, &gw, dev, eth_netif_device_init, tcpip_input);
    }
	......
    return RT_EOK;
}

上面的代碼在enc28j60驅動程序中已經實現了,我們只需要調用函數enc28j60_attach即可將以太網設備對象註冊到RT-Thread I/O 設備管理器。

如果tcpip進程已經啓動,則會調用netifapi_netif_add函數,完成LwIP協議內部的網絡接口設備netif的初始化和註冊,LwIP協議棧就可以訪問enc28j60以太網卡了。

  • ENC28J60的中斷服務

網卡並不是一個被動響應設備,不能只等着主機來讀取數據,當網卡接收到數據時,應能及時通知主機來讀取並處理接收到的數據。這就需要網卡具有中斷響應的能力,一般網卡設備都有IRQ中斷引腳,ENC28J60也不例外。我們想要讓主機及時響應網卡的中斷信號,就要編寫相應的中斷處理程序,並將其綁定到網卡中斷引腳上,當有中斷信號觸發時,自動執行我們編寫的中斷處理程序,完成網絡數據的接收與處理。ENC28J60驅動提供的中斷處理程序及其對網絡數據的接收處理過程如下:

// rt-thread\components\drivers\spi\enc28j60.c

void enc28j60_isr(void)
{
    eth_device_ready(&enc28j60_dev.parent);
    NET_DEBUG("enc28j60_isr\r\n");
}

// rt-thread\components\net\lwip-2.1.0\src\netif\ethernetif.c

#ifndef LWIP_NO_RX_THREAD
rt_err_t eth_device_ready(struct eth_device* dev)
{
    if (dev->netif)
        /* post message to Ethernet thread */
        return rt_mb_send(&eth_rx_thread_mb, (rt_uint32_t)dev);
    else
        return ERR_OK; /* netif is not initialized yet, just return. */
}
......

#ifndef LWIP_NO_RX_THREAD
/* Ethernet Rx Thread */
static void eth_rx_thread_entry(void* parameter)
{
    struct eth_device* device;

    while (1)
    {
        if (rt_mb_recv(&eth_rx_thread_mb, (rt_ubase_t *)&device, RT_WAITING_FOREVER) == RT_EOK)
        {
            ......
            /* receive all of buffer */
            while (1)
            {   
                p = device->eth_rx(&(device->parent));
                if (p != RT_NULL)
                {
                    /* notify to upper layer */
                    if( device->netif->input(p, device->netif) != ERR_OK )
                    ......
                }
                else break;
            }
        }
        ......
    }
}
#endif

以太網卡的數據接收線程eth_rx_thread_entry阻塞在郵箱eth_rx_thread_mb上,當其中斷處理程序enc28j60_isr被調用時,會向郵箱eth_rx_thread_mb發送觸發中斷的網卡設備對象基地址,通知接收線程eth_rx_thread_entry網卡device有接收數據需要處理。接收線程eth_rx_thread_entry收到郵箱eth_rx_thread_mb的郵件後,開始在指定網卡設備device上接收數據,並將其提交給上層LwIP協議棧進行處理。

我們在註冊完enc28j60網絡設備對象後,使用pin設備將上面的中斷處理程序enc28j60_isr綁定到中斷引腳PIN_NRF_IRQ上,設置中斷觸發模式並使能中斷即可,完整的enc28j60網卡設備註冊過程如下:

// applications\enc28j60_port.c

#define PIN_NRF_IRQ   GET_PIN(D, 3)        // PD3 :  NRF_IRQ      --> WIRELESS
#define PIN_NRF_CE    GET_PIN(D, 4)        // PD4 :  NRF_CE       --> WIRELESS
#define PIN_NRF_CS    GET_PIN(D, 5)        // PD5 :  NRF_CS       --> WIRELESS

int enc28j60_init(void)
{
    __HAL_RCC_GPIOD_CLK_ENABLE();
    rt_hw_spi_device_attach("spi2", "spi21", GPIOD, GPIO_PIN_5);

    /* attach enc28j60 to spi. spi21 cs - PD6 */
    enc28j60_attach("spi21");

    /* init interrupt pin */
    rt_pin_mode(PIN_NRF_IRQ, PIN_MODE_INPUT_PULLUP);
    rt_pin_attach_irq(PIN_NRF_IRQ, PIN_IRQ_MODE_FALLING, (void(*)(void*))enc28j60_isr, RT_NULL);
    rt_pin_irq_enable(PIN_NRF_IRQ, PIN_IRQ_ENABLE);

    return 0;
}
INIT_COMPONENT_EXPORT(enc28j60_init);

2.2 設備無關接口層netdev

netdev(network interface device),即網絡接口設備,又稱網卡。每一個用於網絡連接的設備都可以註冊成網卡,爲了適配更多的種類的網卡,避免系統中對單一網卡的依賴,RT-Thread 系統提供了 netdev 組件用於網卡管理和控制。

netdev 組件主要作用是解決設備多網卡連接時網絡連接問題,用於統一管理各個網卡信息與網絡連接狀態,並且提供統一的網卡調試命令接口。 其主要功能特點如下所示:

  • 抽象網卡概念,每個網絡連接設備可註冊唯一網卡;
  • 提供多種網絡連接信息查詢,方便用戶實時獲取當前網卡網絡狀態;
  • 建立網卡列表和默認網卡,可用於網絡連接的切換;
  • 提供多種網卡操作接口(設置 IP、DNS 服務器地址,設置網卡狀態等);
  • 統一管理網卡調試命令(ping、ifconfig、netstat、dns 等命令)。

每個網卡對應唯一的網卡結構體對象,其中包含該網卡的主要信息和實時狀態,用於後面網卡信息的獲取和設置,RT-Thread提供的netdev組件對網卡結構體對象的描述如下:

// rt-thread\components\net\netdev\include\netdev.h

/* network interface device object */
struct netdev
{
    rt_slist_t list; 
    
    char name[RT_NAME_MAX];                            /* network interface device name */
    ip_addr_t ip_addr;                                 /* IP address */
    ip_addr_t netmask;                                 /* subnet mask */
    ip_addr_t gw;                                      /* gateway */
    ip_addr_t dns_servers[NETDEV_DNS_SERVERS_NUM];     /* DNS server */
    uint8_t hwaddr_len;                                /* hardware address length */
    uint8_t hwaddr[NETDEV_HWADDR_MAX_LEN];             /* hardware address */
    
    uint16_t flags;                                    /* network interface device status flag */
    uint16_t mtu;                                      /* maximum transfer unit (in bytes) */
    const struct netdev_ops *ops;                      /* network interface device operations */
    
    netdev_callback_fn status_callback;                /* network interface device flags change callback */
    netdev_callback_fn addr_callback;                  /* network interface device address information change callback */

#ifdef RT_USING_SAL
    void *sal_user_data;                               /* user-specific data for SAL */
#endif /* RT_USING_SAL */
    void *user_data;                                   /* user-specific data */
};

/* whether the network interface device is 'up' (set by the network interface driver or application) */
#define NETDEV_FLAG_UP                 0x01U
/* if set, the network interface device has an active link (set by the network interface driver) */
#define NETDEV_FLAG_LINK_UP            0x04U
/* if set, the network interface device connected to internet successfully (set by the network interface driver) */
#define NETDEV_FLAG_INTERNET_UP        0x80U
/* if set, the network interface device has DHCP capability (set by the network interface device driver or application) */
#define NETDEV_FLAG_DHCP               0x100U

netdev組件中的網卡設備對象並沒有繼承自設備基類rt_device,而是將多個網卡設備對象組織成一個單向鏈表進行統一管理,系統中每個網卡在初始化時會創建和註冊網卡設備對象到該網卡鏈表中。

netdev 組件還通過flags成員提供對網卡網絡狀態的管理和控制,其類型主要包括下面四種:

  • up/down:底層網卡初始化完成之後置爲 up 狀態,用於判斷網卡開啓還是禁用;
  • link_up/link_down:用於判斷網卡設備是否具有有效的鏈路連接,連接後可以與其他網絡設備進行通信,該狀態一般由網卡底層驅動設置;
  • internet_up/internet_down:用於判斷設備是否連接到因特網,接入後可以與外網設備進行通信;
  • dhcp_enable/dhcp_disable:用於判斷當前網卡設備是否開啓 DHCP 功能支持。

netdev組件爲我們提供了一系列訪問網卡設備的接口,這些接口最終是通過調用netdev_ops接口函數實現的。我們要想使用netdev組件爲我們提供的接口,需要創建網卡設備對象netdev後,實現netdev_ops接口函數集合,並將其註冊到網卡鏈表中。我們先看下netdev_ops包含哪些接口函數:

// rt-thread\components\net\netdev\include\netdev.h

/* The network interface device operations */
struct netdev_ops
{
    /* set network interface device hardware status operations */
    int (*set_up)(struct netdev *netdev);
    int (*set_down)(struct netdev *netdev);

    /* set network interface device address information operations */
    int (*set_addr_info)(struct netdev *netdev, ip_addr_t *ip_addr, ip_addr_t *netmask, ip_addr_t *gw);
    int (*set_dns_server)(struct netdev *netdev, uint8_t dns_num, ip_addr_t *dns_server);
    int (*set_dhcp)(struct netdev *netdev, rt_bool_t is_enabled);

    /* set network interface device common network interface device operations */
    int (*ping)(struct netdev *netdev, const char *host, size_t data_len, uint32_t timeout, struct netdev_ping_resp *ping_resp);
    void (*netstat)(struct netdev *netdev);

};

網卡設備netdev的創建和註冊過程如下:

// rt-thread\components\net\lwip-2.1.0\src\netif\ethernetif.c

const struct netdev_ops lwip_netdev_ops =
{
    lwip_netdev_set_up,
    lwip_netdev_set_down,

    lwip_netdev_set_addr_info,
#ifdef RT_LWIP_DNS
    lwip_netdev_set_dns_server,
#else 
    NULL,
#endif /* RT_LWIP_DNS */

#ifdef RT_LWIP_DHCP
    lwip_netdev_set_dhcp,
#else
    NULL,
#endif /* RT_LWIP_DHCP */

#ifdef RT_LWIP_USING_PING
    lwip_netdev_ping,
#else
    NULL,
#endif /* RT_LWIP_USING_PING */

#if defined (RT_LWIP_TCP) || defined (RT_LWIP_UDP)
    lwip_netdev_netstat,
#endif /* RT_LWIP_TCP || RT_LWIP_UDP */
};

static int netdev_add(struct netif *lwip_netif)
{
#define LWIP_NETIF_NAME_LEN 2
    int result = 0;
    struct netdev *netdev = RT_NULL;
    char name[LWIP_NETIF_NAME_LEN + 1] = {0};

    RT_ASSERT(lwip_netif);

    netdev = (struct netdev *)rt_calloc(1, sizeof(struct netdev));
    if (netdev == RT_NULL)
    {
        return -ERR_IF;
    }

    netdev->flags = lwip_netif->flags;
    netdev->ops = &lwip_netdev_ops;
    netdev->hwaddr_len =  lwip_netif->hwaddr_len;
    rt_memcpy(netdev->hwaddr, lwip_netif->hwaddr, lwip_netif->hwaddr_len);
    
#ifdef SAL_USING_LWIP
    extern int sal_lwip_netdev_set_pf_info(struct netdev *netdev);
    /* set the lwIP network interface device protocol family information */
    sal_lwip_netdev_set_pf_info(netdev);
#endif /* SAL_USING_LWIP */

    rt_strncpy(name, lwip_netif->name, LWIP_NETIF_NAME_LEN);
    result = netdev_register(netdev, name, (void *)lwip_netif);

#ifdef RT_LWIP_DHCP
    netdev_low_level_set_dhcp_status(netdev, RT_TRUE);
#endif

    return result;
}

// rt-thread\components\net\netdev\src\netdev.c

int netdev_register(struct netdev *netdev, const char *name, void *user_data)
{
    ......
    /* clean network interface device */
    flags_mask = NETDEV_FLAG_UP | NETDEV_FLAG_LINK_UP | NETDEV_FLAG_INTERNET_UP | NETDEV_FLAG_DHCP;
    netdev->flags &= ~flags_mask;
    ......
    /* fill network interface device */
    rt_strncpy(netdev->name, name, rt_strlen(name));
    netdev->user_data = user_data;

    /* initialize current network interface device single list */
    rt_slist_init(&(netdev->list));

    level = rt_hw_interrupt_disable();

    if (netdev_list == RT_NULL)
    {
        netdev_list = netdev;
        netdev_default = netdev;
    }
    else
    {
        /* tail insertion */
        rt_slist_append(&(netdev_list->list), &(netdev->list));
    }

    rt_hw_interrupt_enable(level);

    return RT_EOK;    
}

訪問接口函數集合lwip_netdev_ops的實現,最終是通過調用LwIP網絡接口層netif_xxx來實現的,相當於對netif_xxx接口又封裝了一層。網卡註冊函數netdev_register實際上就是在完成網卡設備netdev對象的初始化後將其插入到網卡鏈表中,網卡註銷函數則是將其從網卡鏈表中移除。

網卡設備對象的創建和註冊函數netdev_add何時被誰調用呢?我們在工程文件中全局搜索關鍵詞netdev_add,發現其只被函數eth_netif_device_init調用,該函數是以太網卡初始化函數,被netifapi_netif_add作爲網卡初始化函數指針傳入,最終被netif_add函數調用執行。也就是說,當我們完成以太網卡的綁定(調用enc28j60_attach),網卡設備初始化函數eth_netif_device_init便會被LwIP協議內部調用執行,eth_netif_device_init函數內部調用netdev_add的部分代碼如下:

// rt-thread\components\net\lwip-2.1.0\src\netif\ethernetif.c

static err_t eth_netif_device_init(struct netif *netif)
{
    struct eth_device *ethif;

    ethif = (struct eth_device*)netif->state;
    if (ethif != RT_NULL)
    {
        rt_device_t device;

#ifdef RT_USING_NETDEV
    /* network interface device register */
    netdev_add(netif);
#endif /* RT_USING_NETDEV */

        /* get device object */
        device = (rt_device_t) ethif;
        if (rt_device_init(device) != RT_EOK)
        {
            return ERR_IF;
        }
		......
        /* set default netif */
        if (netif_default == RT_NULL)
            netif_set_default(ethif->netif);
		......
        /* set interface up */
        netif_set_up(ethif->netif);
        ......
        return ERR_OK;
    }
    return ERR_IF;
}

如果想使用netdev組件,只需要定義宏RT_USING_NETDEV,RT-Thread已經幫我們實現了網卡設備對象netdev與訪問接口netdev_ops的創建和註冊,我們可以直接使用netdev提供的接口函數和finsh命令,下面列舉幾個常用的接口函數及finsh命令:

// rt-thread\components\net\netdev\include\netdev.h

/* Set default network interface device in list */
void netdev_set_default(struct netdev *netdev);

/*  Set network interface device status */
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);

/* Set network interface device address */
int netdev_set_ipaddr(struct netdev *netdev, const ip_addr_t *ipaddr);
int netdev_set_netmask(struct netdev *netdev, const ip_addr_t *netmask);
int netdev_set_gw(struct netdev *netdev, const ip_addr_t *gw);
int netdev_set_dns_server(struct netdev *netdev, uint8_t dns_num, const ip_addr_t *dns_server);

/* Set network interface device callback, it can be called when the status or address changed */
void netdev_set_status_callback(struct netdev *netdev, netdev_callback_fn status_callback);

/* Set network interface device status and address, this function can only be called in the network interface device driver */
void netdev_low_level_set_ipaddr(struct netdev *netdev, const ip_addr_t *ipaddr);
void netdev_low_level_set_netmask(struct netdev *netdev, const ip_addr_t *netmask);
void netdev_low_level_set_gw(struct netdev *netdev, const ip_addr_t *gw);
void netdev_low_level_set_dns_server(struct netdev *netdev, uint8_t dns_num, const ip_addr_t *dns_server);
void netdev_low_level_set_status(struct netdev *netdev, rt_bool_t is_up);
void netdev_low_level_set_link_status(struct netdev *netdev, rt_bool_t is_up);
void netdev_low_level_set_dhcp_status(struct netdev *netdev, rt_bool_t is_enable);// rt-thread\components\net\netdev\src\netdev.c

FINSH_FUNCTION_EXPORT_ALIAS(netdev_ifconfig, __cmd_ifconfig, list the information of all network interfaces);
FINSH_FUNCTION_EXPORT_ALIAS(netdev_ping, __cmd_ping, ping network host);
FINSH_FUNCTION_EXPORT_ALIAS(netdev_dns, __cmd_dns, list and set the information of dns);
FINSH_FUNCTION_EXPORT_ALIAS(netdev_netstat, __cmd_netstat, list the information of TCP / IP);

2.3 網絡協議層LwIP

LwIP協議棧內部的實現原理可以參考系列博客:TCP/IP協議之LwIP,這裏只介紹LwIP的網絡接口層與socket API層。

  • 網絡接口層netif

  • netif網卡初始化與註冊

LwIP協議棧網絡接口層主要完成netif對象的創建、初始化和註冊,在前面介紹ENC28J60設備驅動層時提到,以太網卡設備註冊時,會通過調用netifapi_netif_add完成netif對象的創建和初始化,函數調用過程大致如下:

enc28j60_init(void) ---> 
enc28j60_attach("spi21") ---> 
eth_device_init(&(enc28j60_dev.parent), "e0") ---> 
eth_device_init_with_flag(dev, name, flags) ---> 
netifapi_netif_add(netif, &ipaddr, &netmask, &gw, dev, eth_netif_device_init, tcpip_input) ---> do_netifapi_netif_add(&msg) ---> 
netif_add(msg->netif, msg->msg.add.state, msg->msg.add.init, msg->msg.add.input) --->
init(netif) --(equal to)--> eth_netif_device_init(netif)

網絡接口層除了完成網卡設備netif的初始化工作外,還需要設定網卡發射、接收數據的函數指針。從UDP數據報的發送、接收過程圖可以看出,LwIP協議棧接收、發送數據,都是通過調用netif->input和netif->output來實現的:
UDP數據報收發過程

  • netif->linkoutput網卡發送接口

先看下netif->output函數指針的配置與調用過程,netif配置了兩個數據輸出函數指針,分別是netif->output和netif->linkoutput,前者是將數據包交由ARP層發送,經過ARP協議處理最終還是通過netif->linkoutput函數經網卡發送出去,我們重點關注跟網卡直接相關的netif->linkoutput函數指針的配置和調用過程:

// .\rt-thread\components\net\lwip-2.1.0\src\netif\ethernetif.c

/* Keep old drivers compatible in RT-Thread */
rt_err_t eth_device_init_with_flag(struct eth_device *dev, const char *name, rt_uint16_t flags)
{
    ......
    /* set linkoutput */
    netif->linkoutput   = ethernetif_linkoutput;
    ......
    netifapi_netif_add(netif, &ipaddr, &netmask, &gw, dev, eth_netif_device_init, tcpip_input);
    ......
}

static err_t ethernetif_linkoutput(struct netif *netif, struct pbuf *p)
{
......
    struct eth_device* enetif;

    enetif = (struct eth_device*)netif->state;

    if (enetif->eth_tx(&(enetif->parent), p) != RT_EOK)
    {
        return ERR_IF;
    }
......
}

當LwIP協議棧上層需要發送數據時,會調用netif->linkoutput接口函數,我們在網卡初始化過程中已經將函數指針ethernetif_linkoutput賦值給netif->linkoutput。負責從網卡發出數據的函數ethernetif_linkoutput最終是通過調用enc28j60網卡的enetif->eth_tx接口實現的,該接口函數在enc28j60的驅動程序中藉助SPI驅動接口實現。

  • netif->input網卡接收接口

接下來看netif->input函數指針的配置與調用過程,前面在以太網卡初始化函數eth_device_init_with_flag中調用了netifapi_netif_add,並傳入了網卡接收函數指針tcpip_input。在博客:Sequetia API編程中介紹過,tcpip_input會將接收到的數據包通過郵箱投遞給LwIP協議棧內核線程tcpip_thread,該過程如下圖示:
數據包接收過程
netif->input函數指針何時被調用呢?再回看下前面介紹enc28j60中斷處理程序enc28j60_isr,當收到中斷觸發信號後,會向網卡數據接收線程eth_rx_thread_entry發送一個郵件。eth_rx_thread_entry接收到郵件信號後會調用device->eth_rx接收數據包,該接口函數也是在enc28j60網卡驅動中藉助SPI接口函數實現。

// .\rt-thread\components\net\lwip-2.1.0\src\netif\ethernetif.c

/* Ethernet Rx Thread */
static void eth_rx_thread_entry(void* parameter)
{
    struct eth_device* device;

    while (1)
    {
        if (rt_mb_recv(&eth_rx_thread_mb, (rt_ubase_t *)&device, RT_WAITING_FOREVER) == RT_EOK)
        {
            ......
            /* receive all of buffer */
            while (1)
            {
                if(device->eth_rx == RT_NULL) break;
                
                p = device->eth_rx(&(device->parent));
                if (p != RT_NULL)
                {
                    /* notify to upper layer */
                    if( device->netif->input(p, device->netif) != ERR_OK )
					......
                }
                else break;
            }
        }
        ......
    }
}

從網卡接受完數據包後,會調用device->netif->input接口,將數據包投遞給LwIP協議棧內核線程tcpip_thread進行處理。

  • LwIP協議棧初始化

LwIP協議棧如果想正常工作,也需要調用初始化程序,完成協議棧運行所需資源的分配,LwIP協議棧是在哪被初始化的呢?全局搜索lwip_init,向前追溯不難發現,RT-Thread通過自動初始化命令INIT_PREV_EXPORT(lwip_system_init)完成LwIP協議棧的初始化工作。我們定義宏RT_USING_LWIP,RT-Thread就可以幫我們自動完成LwIP協議棧的初始化工作。

// .\rt-thread\components\net\lwip-2.1.0\src\arch\sys_arch.c

int lwip_system_init(void)
{
    ......
    tcpip_init(tcpip_init_done_callback, (void *)&done_sem);
	......
}
INIT_PREV_EXPORT(lwip_system_init);
  • socket API層

LwIP socket API層的實現原理可以參考博客:Socket API編程,socket API主要是供上層協議無關接口層或應用程序調用的,上面兩層接口的實現最終也是通過調用這些socket API完成的,相當於對socket API的再封裝。這裏只展示下常見的socket API接口:

// .\rt-thread\components\net\lwip-2.1.0\src\include\lwip\sockets.h

int lwip_accept(int s, struct sockaddr *addr, socklen_t *addrlen);
int lwip_bind(int s, const struct sockaddr *name, socklen_t namelen);
int lwip_shutdown(int s, int how);
int lwip_getpeername (int s, struct sockaddr *name, socklen_t *namelen);
int lwip_getsockname (int s, struct sockaddr *name, socklen_t *namelen);
int lwip_getsockopt (int s, int level, int optname, void *optval, socklen_t *optlen);
int lwip_setsockopt (int s, int level, int optname, const void *optval, socklen_t optlen);
 int lwip_close(int s);
int lwip_connect(int s, const struct sockaddr *name, socklen_t namelen);
int lwip_listen(int s, int backlog);
ssize_t lwip_recv(int s, void *mem, size_t len, int flags);
ssize_t lwip_read(int s, void *mem, size_t len);
ssize_t lwip_readv(int s, const struct iovec *iov, int iovcnt);
ssize_t lwip_recvfrom(int s, void *mem, size_t len, int flags,
      struct sockaddr *from, socklen_t *fromlen);
ssize_t lwip_recvmsg(int s, struct msghdr *message, int flags);
ssize_t lwip_send(int s, const void *dataptr, size_t size, int flags);
ssize_t lwip_sendmsg(int s, const struct msghdr *message, int flags);
ssize_t lwip_sendto(int s, const void *dataptr, size_t size, int flags,
    const struct sockaddr *to, socklen_t tolen);
int lwip_socket(int domain, int type, int protocol);
ssize_t lwip_write(int s, const void *dataptr, size_t size);
ssize_t lwip_writev(int s, const struct iovec *iov, int iovcnt);
#if LWIP_SOCKET_SELECT
int lwip_select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,
                struct timeval *timeout);
#endif
#if LWIP_SOCKET_POLL
int lwip_poll(struct pollfd *fds, nfds_t nfds, int timeout);
#endif
int lwip_ioctl(int s, long cmd, void *argp);
int lwip_fcntl(int s, int cmd, int val);
const char *lwip_inet_ntop(int af, const void *src, char *dst, socklen_t size);
int lwip_inet_pton(int af, const char *src, void *dst);

2.4 協議無關接口層SAL

爲了適配更多的網絡協議棧類型,避免系統對單一網絡協議棧的依賴,RT-Thread 系統提供了一套 SAL(套接字抽象層)組件,該組件完成對不同網絡協議棧或網絡實現接口的抽象並對上層提供一組標準的 BSD Socket API,這樣開發者只需要關心和使用網絡應用層提供的網絡接口,而無需關心底層具體網絡協議棧類型和實現,極大的提高了系統的兼容性,方便開發者完成協議棧的適配和網絡相關的開發。SAL 組件主要功能特點:

  • 抽象、統一多種網絡協議棧接口;
  • 提供 Socket 層面的 TLS 加密傳輸特性;
  • 支持標準 BSD Socket API;
  • 統一的 FD 管理,便於使用 read/write poll/select 來操作網絡功能。

先看下SAL層對socket的數據結構描述:

// .\rt-thread\components\net\sal_socket\include\sal.h

struct sal_socket
{
    uint32_t magic;                    /* SAL socket magic word */

    int socket;                        /* SAL socket descriptor */
    int domain;
    int type;
    int protocol;

    struct netdev *netdev;             /* SAL network interface device */

    void *user_data;                   /* user-specific data */
#ifdef SAL_USING_TLS
    void *user_data_tls;               /* user-specific TLS data */
#endif
};

// .\rt-thread\components\net\sal_socket\src\sal_socket.c

/* the socket table used to dynamic allocate sockets */
struct sal_socket_table
{
    uint32_t max_socket;
    struct sal_socket **sockets;
};

/* The global socket table */
static struct sal_socket_table socket_table;

可以看到sal_socket結構體包含netdev成員,還記得前面介紹的netdev結構體包含了sal_user_data成員嗎?由此可見,協議無關接口層SAL與設備無關接口層netdev相互依賴,netdev可以單獨使用,如果要使用SAL則需連同netdev一同啓用。

再來看需要向SAL層註冊的接口函數集合:

// .\rt-thread\components\net\sal_socket\include\sal.h

/* network interface socket opreations */
struct sal_socket_ops
{
    int (*socket)     (int domain, int type, int protocol);
    int (*closesocket)(int s);
    int (*bind)       (int s, const struct sockaddr *name, socklen_t namelen);
    int (*listen)     (int s, int backlog);
    int (*connect)    (int s, const struct sockaddr *name, socklen_t namelen);
    int (*accept)     (int s, struct sockaddr *addr, socklen_t *addrlen);
    int (*sendto)     (int s, const void *data, size_t size, int flags, const struct sockaddr *to, socklen_t tolen);
    int (*recvfrom)   (int s, void *mem, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);
    int (*getsockopt) (int s, int level, int optname, void *optval, socklen_t *optlen);
    int (*setsockopt) (int s, int level, int optname, const void *optval, socklen_t optlen);
    int (*shutdown)   (int s, int how);
    int (*getpeername)(int s, struct sockaddr *name, socklen_t *namelen);
    int (*getsockname)(int s, struct sockaddr *name, socklen_t *namelen);
    int (*ioctlsocket)(int s, long cmd, void *arg);
#ifdef SAL_USING_POSIX
    int (*poll)       (struct dfs_fd *file, struct rt_pollreq *req);
#endif
};

/* sal network database name resolving */
struct sal_netdb_ops
{
    struct hostent* (*gethostbyname)  (const char *name);
    int             (*gethostbyname_r)(const char *name, struct hostent *ret, char *buf, size_t buflen, struct hostent **result, int *h_errnop);
    int             (*getaddrinfo)    (const char *nodename, const char *servname, const struct addrinfo *hints, struct addrinfo **res);
    void            (*freeaddrinfo)   (struct addrinfo *ai);
};

struct sal_proto_family
{
    int family;                                  /* primary protocol families type */
    int sec_family;                              /* secondary protocol families type */
    const struct sal_socket_ops *skt_ops;        /* socket opreations */
    const struct sal_netdb_ops *netdb_ops;       /* network database opreations */
};

需要向SAL層註冊的接口有兩組,分別是sal_socket_ops和sal_netdb_ops,後者是用於DNS域名解析的接口,兩組接口都被包含在協議簇結構體sal_proto_family中。我們要想使用SAL層接口,需要完成sal_socket_table和sal_proto_family的初始化與註冊。

先看sal_socket_table資源的分配和初始化過程:

// .\rt-thread\components\net\sal_socket\src\sal_socket.c

int sal_init(void)
{
    ......
    /* init sal socket table */
    cn = SOCKET_TABLE_STEP_LEN < SAL_SOCKETS_NUM ? SOCKET_TABLE_STEP_LEN : SAL_SOCKETS_NUM;
    socket_table.max_socket = cn;
    socket_table.sockets = rt_calloc(1, cn * sizeof(struct sal_socket *));
    ......
    /* create sal socket lock */
    rt_mutex_init(&sal_core_lock, "sal_lock", RT_IPC_FLAG_FIFO);
	......
}
INIT_COMPONENT_EXPORT(sal_init);

RT-Thread使用自動初始化組件完成sal_socket_table資源的分配與初始化,我們只需要定義相應的宏就可以了,比較省心。

接下來看sal_proto_family的初始化與註冊過程:

// .\rt-thread\components\net\sal_socket\impl\af_inet_lwip.c

static const struct sal_socket_ops lwip_socket_ops =
{
    inet_socket,
    lwip_close,
    lwip_bind,
    lwip_listen,
    lwip_connect,
    inet_accept,
    (int (*)(int, const void *, size_t, int, const struct sockaddr *, socklen_t))lwip_sendto,
    (int (*)(int, void *, size_t, int, struct sockaddr *, socklen_t *))lwip_recvfrom,
    lwip_getsockopt,
    //TODO fix on 1.4.1
    lwip_setsockopt,
    lwip_shutdown,
    lwip_getpeername,
    inet_getsockname,
    inet_ioctlsocket,
#ifdef SAL_USING_POSIX
    inet_poll,
#endif
};

static const struct sal_netdb_ops lwip_netdb_ops =
{
    lwip_gethostbyname,
    lwip_gethostbyname_r,
    lwip_getaddrinfo,
    lwip_freeaddrinfo,
};

static const struct sal_proto_family lwip_inet_family =
{
    AF_INET,
    AF_INET,
    &lwip_socket_ops,
    &lwip_netdb_ops,
};

/* Set lwIP network interface device protocol family information */
int sal_lwip_netdev_set_pf_info(struct netdev *netdev)
{
    RT_ASSERT(netdev);
    
    netdev->sal_user_data = (void *) &lwip_inet_family;
    return 0;
}

接口函數集合lwip_socket_ops與lwip_netdb_ops最終是通過調用lwip協議棧的socket API實現的,最後將初始化後的lwip_inet_family對象賦值給netdev->sal_user_data。通過netdev對象就可以訪問lwip_inet_family內實現的所有接口,通過sal_socket也可以訪問netdev,間接訪問到lwip_inet_family內實現的所有接口。

要想完成lwip_inet_family的註冊,需要調用函數sal_lwip_netdev_set_pf_info(netdev),該函數在哪被調用呢?我們繼續全局搜索關鍵詞,發現是在函數netdev_add中被調用的。在前面介紹netdev組件時提到,netdev_add函數是在以太網卡設備初始化函數eth_netif_device_init中被調用的,也就是在網卡硬件初始化過程中就已經完成了lwip_inet_family的註冊(當然需要開啓相應的宏定義)。

  • TLS接口註冊

TLS(Transport Layer Security)安全傳輸層協議主要用於通信數據的加密,並不影響SAL向上提供的接口。RT-Thread使用的TLS組件時mbedtls(一個由 ARM 公司使用 C 語言實現和維護的 SSL/TLS 算法庫),如果啓用了TLS組件,SAL層的實現函數中會自動調用mbedtls的接口函數,實現數據的加密傳輸。

TLS協議的數據結構描述與需要向其註冊的接口函數集合如下:

// .\rt-thread\components\net\sal_socket\include\sal_tls.h

struct sal_proto_tls
{
    char name[RT_NAME_MAX];                      /* TLS protocol name */
    const struct sal_proto_tls_ops *ops;         /* SAL TLS protocol options */
};

struct sal_proto_tls_ops
{
    int (*init)(void);
    void* (*socket)(int socket);
    int (*connect)(void *sock);
    int (*send)(void *sock, const void *data, size_t size);
    int (*recv)(void *sock, void *mem, size_t len);
    int (*closesocket)(void *sock);

    int (*set_cret_list)(void *sock, const void *cert, size_t size);              /* Set TLS credentials */
    int (*set_ciphersurite)(void *sock, const void* ciphersurite, size_t size);   /* Set select ciphersuites */
    int (*set_peer_verify)(void *sock, const void* peer_verify, size_t size);     /* Set peer verification */
    int (*set_dtls_role)(void *sock, const void *dtls_role, size_t size);         /* Set role for DTLS */
};

sal_proto_tls對象的初始化和註冊過程如下:

// .\rt-thread\components\net\sal_socket\impl\proto_mbedtls.c

static const struct sal_proto_tls_ops mbedtls_proto_ops= 
{
    RT_NULL,
    mebdtls_socket,
    mbedtls_connect,
    (int (*)(void *sock, const void *data, size_t size)) mbedtls_client_write,
    (int (*)(void *sock, void *mem, size_t len)) mbedtls_client_read,
    mbedtls_closesocket,
};

static const struct sal_proto_tls mbedtls_proto =
{
    "mbedtls",
    &mbedtls_proto_ops,
};

int sal_mbedtls_proto_init(void)
{
    /* register MbedTLS protocol options to SAL */
    sal_proto_tls_register(&mbedtls_proto);

    return 0;
}
INIT_COMPONENT_EXPORT(sal_mbedtls_proto_init);

// .\rt-thread\components\net\sal_socket\src\sal_socket.c

#ifdef SAL_USING_TLS
/* The global TLS protocol options */
static struct sal_proto_tls *proto_tls;
#endif

#ifdef SAL_USING_TLS
int sal_proto_tls_register(const struct sal_proto_tls *pt)
{
    RT_ASSERT(pt);
    proto_tls = (struct sal_proto_tls *) pt;

    return 0;
}
#endif

變量proto_tls在文件sal_socket.c中屬於全局變量(被static修飾,僅限於本文件內),該文件是SAL組件對外訪問接口函數的實現文件。也就是說,我們啓用TLS協議後,SAL組件對外提供的訪問接口函數實現代碼中就會調用TLS接口,完成數據的加密傳輸,而且該組件的註冊是被自動初始化的,比較省心。

  • SAL對外提供的訪問接口

SAL組件初始化並註冊成功後,我們就可以使用SAL提供的接口進行應用開發了,先看看SAL組件提供了哪些訪問接口:

// .\rt-thread\components\net\sal_socket\include\sal_socket.h

int sal_accept(int socket, struct sockaddr *addr, socklen_t *addrlen);
int sal_bind(int socket, const struct sockaddr *name, socklen_t namelen);
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_connect(int socket, const struct sockaddr *name, socklen_t namelen);
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);

// .\rt-thread\components\net\sal_socket\include\sal_netdb.h

struct hostent *sal_gethostbyname(const char *name);

int sal_gethostbyname_r(const char *name, struct hostent *ret, char *buf,
                size_t buflen, struct hostent **result, int *h_errnop);
void sal_freeaddrinfo(struct addrinfo *ai);
int sal_getaddrinfo(const char *nodename,
       const char *servname,
       const struct addrinfo *hints,
       struct addrinfo **res);

如果不習慣使用SAL層提供的sal_xxx形式的接口,SAL還爲我們進行了再次封裝,將其封裝爲比較通用的BSD Socket API,封裝後的接口如下:

// .\rt-thread\components\net\sal_socket\include\socket\sys_socket\sys\socket.h

#ifdef SAL_USING_POSIX
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
int bind(int s, const struct sockaddr *name, socklen_t namelen);
int shutdown(int s, int how);
int getpeername(int s, struct sockaddr *name, socklen_t *namelen);
int getsockname(int s, struct sockaddr *name, socklen_t *namelen);
int getsockopt(int s, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);
int connect(int s, const struct sockaddr *name, socklen_t namelen);
int listen(int s, int backlog);
int recv(int s, void *mem, size_t len, int flags);
int recvfrom(int s, void *mem, size_t len, int flags,
      struct sockaddr *from, socklen_t *fromlen);
int send(int s, const void *dataptr, size_t size, int flags);
int sendto(int s, const void *dataptr, size_t size, int flags,
    const struct sockaddr *to, socklen_t tolen);
int socket(int domain, int type, int protocol);
int closesocket(int s);
int ioctlsocket(int s, long cmd, void *arg);
#else
#define accept(s, addr, addrlen)                           sal_accept(s, addr, addrlen)
#define bind(s, name, namelen)                             sal_bind(s, name, namelen)
#define shutdown(s, how)                                   sal_shutdown(s, how)
#define getpeername(s, name, namelen)                      sal_getpeername(s, name, namelen)
#define getsockname(s, name, namelen)                      sal_getsockname(s, name, namelen)
#define getsockopt(s, level, optname, optval, optlen)      sal_getsockopt(s, level, optname, optval, optlen)
#define setsockopt(s, level, optname, optval, optlen)      sal_setsockopt(s, level, optname, optval, optlen)
#define connect(s, name, namelen)                          sal_connect(s, name, namelen)
#define listen(s, backlog)                                 sal_listen(s, backlog)
#define recv(s, mem, len, flags)                           sal_recvfrom(s, mem, len, flags, NULL, NULL)
#define recvfrom(s, mem, len, flags, from, fromlen)        sal_recvfrom(s, mem, len, flags, from, fromlen)
#define send(s, dataptr, size, flags)                      sal_sendto(s, dataptr, size, flags, NULL, NULL)
#define sendto(s, dataptr, size, flags, to, tolen)         sal_sendto(s, dataptr, size, flags, to, tolen)
#define socket(domain, type, protocol)                     sal_socket(domain, type, protocol)
#define closesocket(s)                                     sal_closesocket(s)
#define ioctlsocket(s, cmd, arg)                           sal_ioctlsocket(s, cmd, arg)
#endif /* SAL_USING_POSIX */

// .\rt-thread\components\net\sal_socket\include\socket\netdb.h

struct hostent *gethostbyname(const char *name);

int gethostbyname_r(const char *name, struct hostent *ret, char *buf,
                size_t buflen, struct hostent **result, int *h_errnop);
void freeaddrinfo(struct addrinfo *ai);
int getaddrinfo(const char *nodename,
       const char *servname,
       const struct addrinfo *hints,
       struct addrinfo **res);

BSD Socket API也是我們進行網絡應用開發時最常使用的接口,如果要使用這些接口,除了啓用相應的宏定義,還需要包含這些接口所在的兩個頭文件。

2.5 系統調用接口層

系統調用接口層一般是用戶空間應用程序向內核空間請求服務的接口,爲了方便應用程序使用內核服務,同時隔離用戶空間與內核空間,內核空間嚮應用程序提供了一套訪問接口,即API(Application Programming Interface)。內核空間爲用戶空間系統的系統調用接口大致可分爲線程控制與線程間通信(比如pthread API)、文件訪問與文件系統操作、內存管理、網絡管理(比如BSD Socket)、系統控制與用戶管理等,前面介紹過的BSD Socket API 就屬於一類系統調用接口,下面我們使用BSD Socket API 編寫一個網絡示例程序。

三、HTTP服務應用示例

我們在LwIP協議棧移植博文的基礎上繼續開發,此時已經移植好了ENC28J60以太網卡驅動與LwIP V2.1協議棧,我們想使用SAL層提供的BSD Socket API 編寫應用程序,只需要在menuconfig中使能netdev組件與SAL組件即可。在LwIP協議棧中,我們爲了使用ping命令驗證協議棧移植的效果,已經使能了netdev組件,這裏只需要再使能SAL組件即可,配置界面如下:
啓用SAL組件
我們試着編寫一個HTTP server程序(不熟悉HTTP協議與HTML語法的可參考博客:Web三大技術要素),讓Pandora作爲HTTP服務器,當有Client連接請求時,向客戶端發送一個網頁。再LwIP協議棧移植博文最後,實現了一個使用網頁遠程控制開發板LED等亮滅的HTTP server應用程序,這裏稍微換點花樣,將開發板採集到的溫溼度數據通過網頁返回給用戶Client。

要將溫溼度數據返回給網頁,首先需要開發板能獲取到溫溼度數據,這就需要我們能讀取開發板上溫溼度傳感器的數據,因此需要配置溫溼度傳感器的驅動程序。Pandora開發板上使用的溫溼度傳感器型號爲AHT10,Pandora的驅動程序已經爲我們預先配置好了AHT10,我們只需要啓用就可以了,跟前面啓用SAL組件類似,只需要在menuconfig中啓用AHT10即可,配置界面如下:
啓用AHT10傳感器
STM32L475 使用 I2C 總線協議與 AHT10傳感器進行通信,從AHT10上讀取溫溼度信息的方法在博文:IIC設備對象管理與Sensor管理框架中有過詳細介紹,這裏就不贅述了,下面重點看使用BSD Socket API 編寫TCP Server 程序的方法。

使用BSD Socket API 編寫 HTTP Server 程序,實現將開發板上採集的的溫溼度數據返回給網頁請求者的示例程序如下:

// applications\sockapi_http_demo.c

#include <rtthread.h>
#include <sys\socket.h>		/* 使用BSD socket,需要包含socket.h頭文件 */
#include <string.h>
#include <sensor.h>

#define DBG_TAG               "Socket"
#define DBG_LVL               DBG_INFO
#include <rtdbg.h>

/* defined received buffer size */
#define BUFSZ       512
/* defined the number of times aht10 sensor data is sent */
#define SENDCNT     10
/* defined aht10 sensor name */
#define SENSOR_TEMP_NAME    "temp_aht10"
#define SENSOR_HUMI_NAME    "humi_aht10"

static rt_thread_t tid = RT_NULL;

const static char http_html_hdr[] = "HTTP/1.1 200 OK\r\nContent-type: text/html\r\n\r\n";
const static char http_index_html[] = "<html><head><title>Sensor-AHT10</title></head>\
                              		  <body><h1>Welcome to LwIP 2.1.0 HTTP server!</h1></body></html>";
static char Sensor_Data[] ="<html><head><title>Sensor-AHT10</title></head>\
                           <body><center><p>The current temperature is: %3d.%d C, humidity is: %3d.%d %.\
                           </p></center></body></html>";

/** Serve one HTTP connection accepted in the http thread */
static void httpserver_serve(int sock)
{
    /* 用於接收的指針,後面會做一次動態分配以請求可用內存 */
    char *buffer;
    int bytes_received, cnt = SENDCNT;
    /* sensor設備對象與sensor數據類型 */
    rt_device_t sensor_temp, sensor_humi;
    struct rt_sensor_data temp_data, humi_data;

    /* 分配接收用的數據緩衝 */
    buffer = rt_malloc(BUFSZ+1);
    if(buffer == RT_NULL)
    {
      LOG_E("No memory\n");
      return;
    }

    /* 從connected socket中接收數據,接收buffer是512大小 */
    bytes_received = recv(sock, buffer, BUFSZ, 0);
    if (bytes_received > 0)
    {
        /* 有接收到數據,在末端添加字符串結束符 */
        buffer[bytes_received] = '\0';

        /* 若是GET請求,則向網頁返回html數據 */
        if(strncmp(buffer, "GET", 3) == 0)
        {
            /* 向網頁返回固定數據 */
            send(sock, http_html_hdr, strlen(http_html_hdr), 0);
            send(sock, http_index_html, strlen(http_index_html), 0);

            /* 發現並打開溫溼度傳感器設備 */
            sensor_temp = rt_device_find(SENSOR_TEMP_NAME);
            rt_device_open(sensor_temp, RT_DEVICE_FLAG_RDONLY);

            sensor_humi = rt_device_find(SENSOR_HUMI_NAME);
            rt_device_open(sensor_humi, RT_DEVICE_FLAG_RDONLY);

            do
            {
                /* 讀取溫溼度數據,並將其填入Sensor_Data字符串 */
                rt_device_read(sensor_temp, 0, &temp_data, 1);
                rt_device_read(sensor_humi, 0, &humi_data, 1);
                rt_sprintf(buffer, Sensor_Data, 
                        temp_data.data.temp / 10, temp_data.data.temp % 10,
                        humi_data.data.humi / 10, humi_data.data.humi % 10);
                
                /* 向網頁週期性發送溫溼度數據 */
                send(sock, buffer, strlen(buffer), 0);
                rt_thread_mdelay(6000);
            } while (cnt--);

            rt_device_close(sensor_temp);
            rt_device_close(sensor_humi);
        }
    }
    rt_free(buffer);
}

/** The main function, never returns! */
static void httpserver_thread(void *arg)
{
    socklen_t sin_size;
    int sock, connected, ret;
    struct sockaddr_in server_addr, client_addr;

    /* 一個socket在使用前,需要預先創建出來,指定SOCK_STREAM爲TCP的socket */
    sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
    if(sock == -1)
    {
      LOG_E("Socket error\n"); 
      return;
    }

    /* 初始化服務端地址,HTTP端口號爲80 */
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(80);
    server_addr.sin_addr.s_addr = INADDR_ANY;
    rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));

    /* 綁定socket到服務端地址 */
    ret = bind(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr));
    if(ret == -1)
    {
      LOG_E("Unable to bind\n");   
      return;
    }

    /* 在socket上進行監聽 */
    if(listen(sock, 5) == -1)
    {
      LOG_E("Listen error\n"); 
      return;
    }

    LOG_I("\nTCPServer Waiting for client on port 80...\n");
    
    do {
        sin_size = sizeof(struct sockaddr_in);
        /* 接受一個客戶端連接socket的請求,這個函數調用是阻塞式的 */
        connected = accept(sock, (struct sockaddr *)&client_addr, &sin_size);
        if (connected >= 0)
        {
            /* 接受返回的client_addr指向了客戶端的地址信息 */
            LOG_I("I got a connection from (%s , %d)\n",
                   inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
            /* 客戶端連接的處理 */
            httpserver_serve(connected);
            closesocket(connected);
        }
    } while(connected >= 0);
    
    closesocket(sock);
    return;
}

/** Initialize the HTTP server (start its thread) */
static void httpserver_init()
{
    /* 創建並啓動http server線程 */
	tid = rt_thread_create("http_server_socket", 
  							httpserver_thread, NULL, 
							RT_LWIP_TCPTHREAD_STACKSIZE * 2, 
                            RT_LWIP_TCPTHREAD_PRIORITY + 1, 10);
	if(tid != RT_NULL)
	{
		rt_thread_startup(tid);
		LOG_I("Startup a tcp web server.\n");
	}
}
MSH_CMD_EXPORT_ALIAS(httpserver_init, sockapi_web, socket api httpserver init);

使用scons命令創建MDK工程,並使用MDK打開工程文件,編譯無報錯,將其燒錄到開發板中,Pandora開發板被分配的網卡IP如下:
pandora被分配IP
從上圖可以看出,Pandora上插的ENC28J60正常啓用,分配的IP爲192.168.0.3,可以正常訪問網絡,向外提供HTTP服務,我們運行前面編寫的sockapi_web程序,提示“[I/Socket] Startup a tcp web server.”,表示開發板已經啓動了一個HTTP server。我們只需要在瀏覽器中輸入前面查詢到的開發板網卡地址192.168.0.3,就可以在網頁上看到開發板返回的溫溼度數據了,結果如下:
pandora返回的溫溼度數據
程序設置的每6秒返回一組溫溼度數據,共返回11組,跟預期一致,程序編寫無明顯Bug,上面的攝氏度符合℃不是ASCII字符,這裏使用C替代了。

本示例工程源碼下載地址:https://github.com/StreamAI/LwIP_Projects/tree/master/stm32l475-pandora-lwip

更多文章:

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