FreeRTOS+Lwip+STM32 网卡驱动函数运行原理(网卡+wifi)

塞2564196

项目背景

主板是野火的STM32H743,例程也是野火附带的例程,有需要的可以去野火大学堂下载。因为野火只是给出了以太网和wifi的单独例程,而项目需要两者结合在一个工程,所以需要弄清楚两者驱动函数都是如何编写的,方便合并。

Lwip网络接口函数

在Lwip当中,源码作者用netif结构体来表示一个网卡接口,内含网卡的IP地址、数据接收函数、数据发送函数等。然后通过netif_add函数添加到网卡链表当中去。

在源码当中存在一个ethernetif.c文件,这个文件存在5个函数的框架,包括函数名、函数参数、函数内容等。这就是5个网卡驱动函数的框架,我们需要在这里面添加实际使用的网卡特性。

  1. static err_t low_level_output( struct netif *netif, struct pbuf *p )
  2. static struct pubf*low_level_input(strcut netif *netif)
  3. static void ethernetif_input(struvt netif *netif)
  4. static void low_level_init(struct netif *netif )
  5. err_t ethernetif_init(  struct netif *netif )

low_level_output函数为网卡的发送函数,它主要将内核的数据包发送出去,数据包采用 pbuf 数 据结构进行描述,该数据结构是一个比较复杂的数据结构。

low_level_input函数为网卡的数据接收函数,该函数会接收一个数据包,为了内核易于对数据 包的管理,该函数必须将接收的数据封装成 pbuf 的形式。

ethernetif_input函数的主要作用就是调用 low_level_input函数从网卡中读取一个数据包,然后解析该数据包的类型是属于 ARP 数据包还是 IP 数据包,再将包递交给上层。

 low_level_init函数为网卡初始化底层函数,它主要完成网卡的复位及参数初始化,根据实际的网卡属性进行配置 netif 中与网卡相关的字段,例如网卡的 MAC 地址、长度,最大发送单元等。

ethernetif_init函数,网卡初始化函数。

可以看到5个函数当中四个都带了static关键字,有关于这个关键字的作用如下所示。

在函数的返回类型前加上关键字static,函数就被定义成为静态函数。
函数的定义和声明默认情况下是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
定义静态函数的好处:
<1> 其他文件中可以定义相同名字的函数,不会发生冲突
<2> 静态函数不能被其他文件所用。 

前4个函数都只是在ethernetif.c文件内部调用,并不会被其他文件所引用,而ethernetif_init函数是会被netif_add函数调用,所以不带static关键词。

他们几个函数之间的关系如下图所示:

以太网例程

 以太网例程直接使用了这个源文件,函数名字都没有更改。 下面来看看具体函数都做了些什么?(以下截取部分代码)

1.ethernetif_init函数

对netif结构体的成员进行赋值。

output字段指向一个函数,这个函数将网络层即IP层的数据包往下传,也就是往链路层传递,具体TCP/IP分层见下层。当IP层有信息需要对外发送的时候,Lwip就会调用该字段指向的函数,把数据包封装一下,然后往下传。这个函数源码作者已经帮忙写好了,就是来自etharp.c的库函数,不需要自己改动。

linkoutput字段也是指向一个函数,但这个函数更底层,主要被ARP模块调用,ARP模块也属于网络层的协议之一。我们知道,平时转发消息我们都会以IP地址来转发和接收,但是实际上网卡底层是不认识IP这个东西的,它只认识MAC地址,所以需要有个协议来转换IP地址和MAC地址,这个协议就是ARP协议。linkoutput所指向的函数主要是承接IP层传递下来的数据包,整合转换一下,编程以太网数据帧,然后通过网卡发送出去。但是不同网卡发送出去的步骤和流程是不一样的,所以linkoutput需要自己根据网卡特性进行改写。而它所指向的low_level_output函数就是上面提到五大函数之一。

最后调用了low_level_init函数,这个函数也是上面提到的五大函数之一。

2. low_level_init函数

这个函数就是用HAL库经典的初始化结构体,调用HAL_ETH_Init函数完成以STM32太网硬件的初始化。

 

下面sys_thread_new是FreeRTOS创建任务的函数,这里建立了一个ethernetif_input任务 ,其实就是上面提到了五大函数之一。

在最后,它海湾城了LAN8720A芯片的初始化,这是个以太网芯片。

3.ethernetif_input函数

因为在初始化函数创建了关于这个函数的任务,所以其实ethernetif_input函数是一个死循环函数,或者叫任务。它就是在不断地等待数据的来临,作为主板的第一站哨兵。接下来看看哨兵是如何识别数据的到来。这个函数也是需要根据网卡的特性进行编写的。

在死循环的第一句,是一个读取信号量的FreeRTOS函数,portMAX_DELAY代表如果读到信号,就往下运行;如果读不到,那就一直在这等着。关于这个函数具体内容可以看关于FreeRTOS的信号量的说明。

那这个等待的信号到底是哪里发出来的?

在ethrnetif.c文件183行可以看到,这个定义了这个信号,而且是全局变量。

 在LAN8720a.c,也就是以太网芯片相关的文件,我们可以找到这个信号量s_xSemaphore。在前面有关键词extern,告诉编译器这个变量不在此文件,要去其他文件找,也就要去ethrnetif.c文件找。

184行是一个关于以太网的回调函数,这个回调函数是读取中断会进入的函数。在函数内容,xSemaphoreGiveFromISR是FreeRTOS在中断当中释放信号量的函数。由此我们就可以得到信号传输的过程,当以太网接收到数据,就会进入到读取回调函数,释放信号。而ethernetif_input任务一直在等待着这个信号量,所以信号量一释放,这边就读取到这个信号量,继续运行。

 在读取到信号量,会调用low_level_input函数,也就是前面的五大函数之一。得到p,这个p就是Lwip专门拿来装数据的数据结构pbuf。

 拿到数据之后,调用netif的input字段进行处理,这个input字段也是个函数,后续会讲解。它主要是作用就是,哨兵说:“lwip线程大佬,以太网模块刚告诉我来数据了,我收到数据了,已经封装成你喜欢的pbuf数据包模式,你看看如何处理”。至于lwip线程如何处理,且看后续分析。

4.low_level_input函数

这个函数是被ethernetif_input函数所调用,可以说是其打下手的小弟。这个小弟干的任务主要是调用HAL_ETH_GetRxDataBuffer函数,这个是HAL库函数。从名字就很容易得知,就是读取数据的函数。然后标号2,主要是把标号1函数读取到的数据,一顿操作,整理成pbuf形式。为什么要有这个过程?因为不同网卡有不同的数据形式,而Lwip作为通用的一个协议栈,就需要自己一套数据结构,而不需要随着网卡的不同而改变。所以就需要有数据结构转变这一步骤。

5.low_level_output函数

有接收函数,必然就有输出函数。这个函数被挂在netif的linkoutput字段,所以去检索这个函数会发现没人调用它,大家都调用netif->linkoutput,但其实一样的,调用linkoutput就相当于调用了此函数。

 这个函数前面一顿操作,最后调用了这个HAL_ETH_Transmit函数。看名字就知道,这是HAL库函数,以太网专门拿来发送数据的。

6.函数定义与声明

因为之前编程,只要是在c文件出现函数定义,对应h文件就必然存在一个函数声明。学过C语言的都知道,这是防止前面的函数调用了后面的函数,提前声明避免这种问题。当然也可以为了让其他c文件调用,这样只需要包含h文件就行了。用多了之后,以为这是一种必须的操作。

 所以看到ethernetif.h文件的函数声明就有点懵,五大函数,怎么就声明了2个函数。后面查了资料确认下,才想起声明不是必须的。首先其他三大函数并不会被其他c文件所调用,只会被本文件所调用,所以没有在h文件声明的必要。然后只要保证先定义后调用,就可以直接不用声明。

ethernetif_init函数需要声明,是因为其他c文件的netif_add函数需要调用它。

ethernetif_input函数需要声明,是因为本文件的low_level_init函数需要调用它新建一个任务,而该函数在文件最开始的位置。

7.netif_add函数

前面讲到五大函数,其中三大函数都是被自己人调用,而ethernetif_input函数就是一个单独的任务,一直在等信号,不会被其他函数调用。只有ethernetif_init函数需要被调用。

 在sys_arch.c文件当中的TCPIP_Init函数,我们可以看到第一步先完成了Lwip线程的初始化,然后就用了netif_add函数进行网络接口初始化。该函数参数:第一个是定义好的网络接口,目前里面是空空如也。后面三个分别是IP地址、子网掩码、网关地址。

 后面两个参数,分别代表网络接口的初始化函数和网络接口向IP层提交数据包的函数。init函数就是前面我们讲的五大函数之一,input函数会挂在neitf的input字段。

 前面我们讲到,哨兵收到数据并整理成pbuf会告诉Lwip线程。那Lwip线程是如何接收数据并且去做处理的,答案就是这个netif数据结构的input所指向的函数。但是我们在初始化函数,对output字段和linkoutput字段都挂载了函数,并没有对input挂载函数。而netif初始化时又是空空如也。究竟在哪里对input进行了赋值。

就是netif_add函数最后一个参数,这个参数是tcpip_input函数,这个函数是Lwip作者已经定义好的库函数,直接用就好,它会对数据包进行判断和流转。netif_add函数在内部就会将tcpip_input函数挂载在input字段。

接下来看看netif-add函数,这个源码作者已经写好的库函数内部在干些什么。

在这一部分,左边的input是netif的字段,右边的input是函数传进来的参数,也就是刚才的 tcpip_input函数。

这一部分就进行了网卡的初始化工作,虽然这是个判断语句,但是init函数已经执行了一次。init函数是函数参数之一,也就是ethernetif_init函数。

所以只要我们调用netif_add函数,初始化函数便会在内部执行。

wifi例程

wifi例程大部分内容与以太网例程相同,不作同样的解释。

wifi例程并没有使用原作者的文件名字,不是ethernetif.c文件,而是wwd_network.c文件。

在五大函数上,两个初始化函数名字不变,low_level_output数据发送函数名字不变。而两个接收函数则变成了一个函数,命名为 host_network_process_ethernet_data函数。

之前我们讲过low_level_input函数就是小弟,为ethernetif_input函数做一些辅助性工作。而wifi例程不需要小弟,自己完成了数据接收的任务。所以两个例程比对一下,就能弄懂大致的逻辑。

host_network_process_ethernet_data函数也是同样的逻辑,从wifi模块接收到数据,会通过Lwip线程处理数据,即netif->input字段。这里input同样指向了tcpip_input函数(tcpic.c)。

 

  • 10
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值