lwip系列一之数据的收发
lwip宏观的
经过一段时间的反复折磨,也看了许多资料,做一下学习总结,同时希望通过向他人表述来加深对内容的理解。驱动程序是参照野火的,但是我觉得这里面有点小小的疑问没有解决。
我不知道大家曾经是否有和我一样的疑问,学完计算机网络后,对计算机网络的各个层次的原理有所了解,但是有个疑问就是如何将这个协议用起来,为了能更好的说明数据收发过程,我们暂时将这个协议当成一个黑盒子,观察其如何在计算机中立起来。
参考下面的图
将lwip看成是看成是黑盒子,他能接收数据,然后对其进行处理,怎么处理暂时不管,然后递交给应用端;
反之,应用端发送数据,经过协议处理后,递交给外设,然后发送出去。
所以说这个lwip可以认为是一个消息处理器,对接收的消息进行处理,对发送的消息进行处理。
所以说可以直接将这个协议栈打包成一个高优先级的线程,干什么呢,不断的去读取邮箱中的需要处理的数据(包含发送和接收)谁先来,谁先处理。这样就将协议“立”起来了。
实际中就有一个线程,叫做tcpip_thread
,有个邮箱tcpip_mbox
来存放等待处理的消息,这个线程在干什么事呢?
不断的尝试从邮箱中取数据,取到呢,就进行消息处理
取不到呢?判断一下有没有超时事件,
没有超时事件,那就一直阻塞,任务进入阻塞态,让其他任务运行
有超时事件,获取下次超时的时间,然后阻塞这段时间,然后进行超时检查。大致的逻辑是这样的。具体的代码细节,暂时不管,这里主要是方便我的记忆。
这样就在整体上能对lwip有个认识。
然后呢,关注数据的收发了,这里呢只描述上图左边,靠近底层的数据收发的实现过程。就是在那种软件、硬件交叉的地方。
数据的接收过程
我们先来看一张逻辑框图:
对于接收过程:我们传输的电平信号通过网线的接口进入到外部PHY中,然后再被MAC所接收,当然这个MAC具有地址过滤的机制们也就是说能根据MAC地址能进行过滤,还会进行CRC校验进行帧的接收,这些是可配置的,配置完后,硬件会自动帮你做的,还会硬件帮你过滤掉以太网帧的前导符,帧首符,还有CRC校验字段。
MAC完成到它的功能后,就会将接收到的数据放入2k字节的接收FIFO中,然后呢,通过DMA将数据传送至物理内存,直白一点说就是传送至你定义数组啊之类的能存放数据的空间。
对于发送过程:就是上述的逆过程了。
数据包装过程
我们知道,tcp/ip是不同层次的,如果是层与层之间递交数据时,要发生数据的拷贝,那么整个lwip内核的运行效率就会十分低下。
所以说为了避免数据产生层层拷贝,引入了pbuf
结构体,通过一个指向该结构体的指针来访问全部数据。
该结构体的原理图如下:
此时我们大致来说说数据的流向:
数据被以太网外设接收后,再通过DMA传输至物理内存,这里的物理内存呢实际上就是数组(也称为缓存),也就是说数据经过DMA后,就到一个数组中,等着你来用。你不能直接占据这个空间来使用,因为这个空间还需要接收其他数据,所以数据来了后要马上把这个空间的数据搬出来,把这个空间释放出来以接收其他以太网数据,因为以太网一直不断的在接收数据,你不出来,后续数据不就没有空间可以放了吗。
所以说,一旦有数据之后呢,就要立刻去将数据清出来,放入上面提及的pbuf
中。
为了实现上面这句话,在lwip中呢,创建了一个计数信号量和一个接收线程来实现任务同步的功能,为什么是计数信号量而不是二值信号量呢,二值不适合频繁发生中断的场合。
在以太网外设初始化时,初始化为接收完成中断,然后在接收中断完成的回调函数中释放信号量,信号量的释放导致接收线程的运行,接收线程干什么事呢?核心就是将缓存中的数据清出来,然后包装成pbuf
,然后对pbuf再进行简单的包装变成消息,然后投递给前面提到的tcpip_mbox
,然后线程tcpip_thread
就运行起来了,就可以开始处理消息了。
到这里为止,我们涉及到关键的二个线程,一个邮箱,一个计数信号量。
一个tcp/ip处理线程tcpip_thread
:不断地尝试去读邮箱的消息,然后进行消息处理
一个先进先出的邮箱tcpip_mbox
:不管是接收的还是需要发送的,最终都会在邮箱中排队,等待处理
一个计数信号量s_xSemaphore
:用于在接收完成时,触发中断,在中断回调中释放信号量,触发接收线程的运行
一个接收线程ethernetif_input
:核心是将缓存的数据清出来,包装成pbuf
,再包装成消息,发到邮箱tcpip_mbox
上述呢,基本上将整个lwip的运行,以及数据的流向大致有个印象。当然要通过一篇文章将所有方方面面讲清楚是不可能的,最终要搞清楚什么的,必须啃代码,这里只是整体有个印象,能将整个过程联通起来。
再细节一点
前面提到数据的流向个过程是
缓存——>pbuf——>消息。前面认为缓存是一个数组,确实是一个数组,每个缓存呢对应有一个描述符来管理,这个描述符是个结构体,按道理说是软件来管理的,但是实际上确是软件定义了这个结构体,但是里面一些状态字段的更新之类的却是硬件帮你做的,搞得有点像寄存器,这是我个人就觉得是最抽象的地方,你说你软件搞得,看看程序就行,硬件搞得,看看原理就行,对吧,这里呢,这个描述符搞得又与软件相关,又与硬件相关。先看看几个定义:
ETH_DMADescTypeDef DMARxDscrTab[ETH_RXBUFNB] ;/*接收描述符 */
ETH_DMADescTypeDef DMATxDscrTab[ETH_TXBUFNB] ;/* 发送描述符 */
uint8_t Rx_Buff[ETH_RXBUFNB][ETH_RX_BUF_SIZE] ; /* 接收缓冲区 */
uint8_t Tx_Buff[ETH_TXBUFNB][ETH_TX_BUF_SIZE] ; /* 发送缓冲区 */
从上面可以看到,描述符是就是特定类型的结构,当然结构体内部成员字段代表什么含义就去看参考手册,这里不说,定义的是一个结构体数组,每个描述符对应一个缓存,缓存就是数组,来源于下面的Rx_Buff
缓冲区(本质上就是一个二维数组)
这里区别一下缓存与缓冲区的关系,缓存是缓冲区的一部分,像野火的驱动设计中就设计缓存为1/8的缓冲区大小。缓存就是缓冲区的一个子集。
我们接收到的数据呢,就存放在缓存里,我们发送数据呢,就将数据放入发送的缓冲区中,并将发送描述符第一个成员字段的OWN
置位,就把数据发送出去了。
我们通过下面一张图来描述缓存,描述符的关系。
写过一点程序的就知道,光定义一个结构体,内部是空的,所以要建立上图中的这个样子,需要在初始化时调用一个函数
HAL_ETH_DMATxDescListInit(&heth, DMATxDscrTab, &Tx_Buff[0][0], ETH_TXBUFNB);
来建立起上图的关系。这个函数会在后续进行注释,可以看看其是怎么样的过程。
好了,到此,基本上呢原理性的东西基本上说的差不多了。下面能就对关键的代码语句进行说明与注释,当然有些东西在一篇文章中没法说的特别详细,大致看看,同时加深自己的印象。
数据接收过程关键性代码阅读
1)前面提到过,在接收中断中的服务函数中接收完成回调函数中释放信号量:来触发接收线程的运行,代码如下:
extern xSemaphoreHandle s_xSemaphore;
void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth)
{
LED2_TOGGLE;
portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR( s_xSemaphore, &xHigherPriorityTaskWoken );//释放信号量
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
2)释放信号量后,线程ethernetif_input
运行,这个线程的代码如下:核心逻辑还是如文章上面所说,具体涉及的重要函数的注释放在文章后面,感兴趣的可以看看
void ethernetif_input(void *pParams)
{
struct netif *netif;
struct pbuf *p = NULL;
netif = (struct netif*) pParams;
while(1)
{
if(xSemaphoreTake( s_xSemaphore, portMAX_DELAY ) == pdTRUE)//获取信号量
{
taskENTER_CRITICAL();
TRY_GET_NEXT_FRAGMENT:
p = low_level_input(netif);//将缓存数据取出,并打包成pbuf
taskEXIT_CRITICAL();
if(p != NULL)
{
taskENTER_CRITICAL();
if (netif->input(p, netif) != ERR_OK)//将pbuf打包成消息,并发送至邮箱中这个函数是
//tcpip_input(struct pbuf *p, struct netif *inp)
{
LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_input: IP input error\n