文章目录
后面会重点介绍WLAN与网络协议部分的实现原理与应用开发,比先前介绍的设备驱动层级更多、更复杂。前一篇博客已经介绍了驱动分层与主从分离思想,这篇博客介绍网络分层结构,以便更清晰的了解WLAN驱动与网络协议的层级调用关系。
后面使用的网络协议是LwIP,之前已经写了一系列的博客专门介绍LwIP网络协议栈的实现原理,有兴趣了解TCP/IP协议的可以去看看。后面介绍其与WLAN的适配和网络应用开发,只介绍LwIP与设备相关的网络接口层和与应用开发相关的Socket API层。
一、网络分层结构
在介绍TCP/IP网络协议在操作系统(比如Linux或RT-Thread)中的层级关系前,先看下TCP/IP网络协议栈本身的分层结构(图片取自博客:Linux 网络栈剖析):
我们单独介绍网络协议栈时,经常使用上面的层级结构,方便了解每一层的作用及实现原理。但当我们在操作系统中使用网络协议栈(比如LwIP协议栈)时,常把其看作一个整体,只关心协议栈对下层硬件与上层应用的接口,并不关心协议栈内部具体的实现过程。我们先看下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(ð_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(ð_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)(stru