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程序,让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>Congrats!</title></head>\
                                <body><h1>Welcome to LwIP 2.1.0 HTTP server!</h1></body></html>";
static char Sensor_Data[] ="<html><head><title>Congrats!</title></head>\
                                <center><p>The current temperature is: %3d.%d C, humidity is: %3d.%d %.\
                                </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

更多文章:

发布了65 篇原创文章 · 获赞 35 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/m0_37621078/article/details/104836942