lwip移植
1、无操作系统移植
1.1、先是lwip源码移植
(1)api

(2)core

(3)core\ipv4

(4)netif

1.2、用户配置文件移植

lwipopts.h 就是用于配置 LwIP 的相关参数的, 一般来说 LwIP 默认会有参数的配置, 存放在 opt.h 文件中, 如果用户没有在lwipopts.h 文件进行配置,那么 LwIP 就会使用 opt.h 默认的参数,注意,在移植的时候出现定义某些参数是非常重要的,这对我们 LwIP 的性能至关重要,甚至在配置错误的时候能直接导致 LwIP 的运行崩溃。
cc.h 文件中包含处理器相关的变量类型、数据结构及字节对齐的相关宏。LwIP 中使用的基本变量类型均以位数进行命名,为抽象的变量类型定义,开发者需要根据所用处理器及编译器特性进行定义,一般我们直接将变量直接定义为 C 语言的基本类型,如 unsigned char、 int 等,这样子可以保证 LwIP 协议栈就与平台无关了。除此之外我们还可以定义大小端模式,输出调试的宏等。
perf.h 文件是与系统统计与测量相关的头文件,我们暂时无需使用任何统计与测量功能,因此该头文件的量宏定义直接为空即可。
ethernetif.c文件时底层驱动相关的文件。在前面说了关于底层驱动的函数, 这些函数在网卡中至关重要, 而 ethernetif.c 文件就是存放这些函数的, LwIP 的 contrib 包中就包含这个文件的模板,我们直接拿过来修改即可,该文件的路径为“contrib-2.1.0\examples\ethernetif”,然后我们拷贝到 arch 文件夹下,并且创建一个 ethernetif.h 文件,一同添加到我们的工程中即可。
(1)lwip时基
LwIP 也是一个内核, 与操作系统一样, 也是由时基驱动的, LwIP 作者为了能让内核正常运行, 也引入了一个时钟来驱动,这样子可以处理内核中各种定时事件,如 ARP 定时、TCP 定时等, LwIP 已经实现处理超时(定时)事件的函数 sys_check_timeouts(),具体怎么处理的就无需用户关心。由于时钟的来源是由用户提供的,这就需要用户实现一个sys_now()函数来获取系统的时钟,以毫秒为单位, LwIP 通过两次获取的时间就能判断是否有超时,从而让内核去处理对应的事件。
我们在 STM32 中,一般采用 SysTick 作为 LwIP 的时基定时器,将 SysTick 产生中断的频率设置为 1000HZ,也就是 1ms 触发一次中断,每次产生中断的时候,系统变量就会加 1,当然,在 HAL 库中已经实现了获取系统时间的函数 HAL_GetTick(),那么很简单,我们在 sys_now()函数中直接返回 HAL_GetTick()函数得到的值即可,具体见。但是有个问题,如果 SysTick 的频率不是 1000HZ,那就需要将 HAL_GetTick()函数得到的值转换为时间(ms),这就由用户自己实现即可,这也是很简单的,当我们使用操作系统的时候,就直接可以转换了使用操作系统的宏进行 tick 与 ms 的转换了,在后续讲解。
u32_t sys_now(void) { return HAL_GetTick(); } void SysTick_Handler(void) { HAL_IncTick(); }
(2)协议栈初始化
想要使用 LwIP,那就必须先将协议栈初始化,我们就创建一个函数,在函数中初始化协议栈,注册网卡,设置主机的 IP 地址、子网掩码、网关地址等,比如作者个人电脑的 IP地址是 192.163.1.181,那么我们在开发板上设置的 IP 地址必须是与路由器处于同一子网的,我就设置为 192.168.1.122,因为这个地址必须是路由器承认的合法地址,否则路由器不会对这个 IP 地址的数据包进行转发,网关就写对应的网关(路由器 IP 地址) 192.168.1.1 即可, 255.255.255.0 为整个局域网的子网掩码。
然后挂载我们的网卡,挂载网卡的函数我们也讲解过了,就是 netif_add()函数,如果我们了解了前面章节的内容,移植起来是一点都不费劲的。
这里主要讲解一下ethernet_input()函数,这个函数在 ethernet.c 文件中(在以前的版本如 1.4.1,这个函数在etharp.c 文件) ,主要是用于无操作系统时候 LwIP 去处理接收到的数据,接收网卡的数据然后往上层递交, 对于不同的数据包进行不同的处理,如果是 ARP 包,则调用etharp_input()函数交给 ARP 去处理,更新 ARP 缓存表;如果是 IP 包,则调用 ip4_input()函数递交给 IP 层去处理。
(3)获取数据包(查询和中断)
通过上面的步骤,我们能使用开发板获取网络的数据包了,但是获取数据包的方式有两种,一种是查询方式,另一种是中断方式。查询方式通过主函数的 while 循环进行周期性处理,去获取网卡中是否接收到数据包,然后递交给上层协议去处理,而中单方式则不一样,在网卡接收到一个数据包的时候,就触发中断,通知 CPU 去处理,这样子效率就会高很多,特别是在操作系统环境下,我们都采用中断方式去获取数据包。当然,查询方式与中断方式的网卡底层初始化是不一样的,主要是通过网卡接收方式的配置进行初始化,在初始化的时候,如果网卡接收模式被配置为 ETH_RXINTERRUPT_MODE, 则表示使用中断方式获取数据包, 而如果网卡接收模式被配置为 ETH_RXPOLLING_MODE 则表明用查询方式获取数据包。
提示:网卡底层初始化函数是 Bsp_Eth_Init(),在 bsp_eth.c 文件中,用户可以自行修改。
1)查询方式
使用查询方式获取数据包的时候,我们只需要在程序中周期性调用网卡接收函数即可,具体见下面代码
int main(void) { //板级外设初始化 BSP_Init(); //LwIP 协议栈初始化 LwIP_Init(); while (1) { //调用网卡接收函数 ethernetif_input(&gnetif); //处理 LwIP 中定时事件 sys_check_timeouts(); } }
2)中断方式
采用查询的方式虽然可行,但是这种方式的效率不高,因为查询就要 CPU 去看看有没有数据,就像一个人在房子中等朋友过来,但是不知道朋友什么时候来,那主人就要每隔一段时间去看看朋友有没有过来,这样子就占用了大量的资源,主人也没能做其他事情,而如果在门口装一个门铃,朋友来的时候就按下门铃,主人就知道朋友来了,就出去迎接,这样子就不会占用主人的时间,主人可以做其他事情。同样的,我们可以使用中断方式来接收数据,当接收完成的时候,就通知 CPU 来处理即可, 当然,还需要我们编写对应的中断服务函数 ETH_IRQHandler(), 具体见代码清单 。
int flag = 0; int main(void) { //板级外设初始化 BSP_Init(); //LwIP 协议栈初始化 LwIP_Init(); while (1) { if (flag) { flag = 0; //调用网卡接收函数 ethernetif_input(&gnetif); } //处理 LwIP 中定时事件 sys_check_timeouts(); } } void ETH_IRQHandler(void) { HAL_ETH_IRQHandler(&heth); } void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth) { flag = 1; // LWIP_Process(); }
2、操作系统移植
LwIP 不仅能在裸机上运行,也能在操作系统环境下运行,而且在操作系统环境下,用户能使用 NETCONN API 与 Socket API 编程,相比 RAW API 编程会更加简便。 操作系统环境下,这意味着多线程环境,一般来说 LwIP 作为一个独立的处理线程运行,用户程序也独立为一个/多个线程,这样子在操作系统中就相互独立开,并且借助操作系统的 IPC 通信机制,更好地实现功能的需求。
LwIP 在设计之初, 设计者无法预测 LwIP 运行的环境是怎么样的, 而且世界上操作系统那么多, 根本没法统一, 而如果 LwIP 要运行在操作系统环境中, 那么就必须产生依赖,即 LwIP 需要依赖操作系统自身的通信机制, 如信号量、 互斥量、 消息队列(邮箱) 等,所以 LwIP 设计者在设计的时候就提供一套与操作系统相关的接口,由用户根据操作系统的不同进行移植,这样子就能降低耦合度,让 LwIP 内核不受其运行的环境影响,因为往往用户并不能完全了解内核的运作,所以只需要用户在移植的时候对 LwIP 提供的接口根据不同操作系统进行完善即可。
2.1、源码移植
源码移植和无操作系统一样
2.2、用户配置文件移植

lwipopts.h 就是用于配置 LwIP 的相关参数的, 一般来说 LwIP 默认会有参数的配置, 存放在 opt.h 文件中, 如果用户没有在lwipopts.h 文件进行配置,那么 LwIP 就会使用 opt.h 默认的参数,注意,在移植的时候出现定义某些参数是非常重要的,这对我们 LwIP 的性能至关重要,甚至在配置错误的时候能直接导致 LwIP 的运行崩溃。
cc.h 文件中包含处理器相关的变量类型、数据结构及字节对齐的相关宏。LwIP 中使用的基本变量类型均以位数进行命名,为抽象的变量类型定义,开发者需要根据所用处理器及编译器特性进行定义,一般我们直接将变量直接定义为 C 语言的基本类型,如 unsigned char、 int 等,这样子可以保证 LwIP 协议栈就与平台无关了。除此之外我们还可以定义大小端模式,输出调试的宏等。
perf.h 文件是与系统统计与测量相关的头文件,我们暂时无需使用任何统计与测量功能,因此该头文件的量宏定义直接为空即可。
ethernetif.c文件是底层驱动相关的文件。在前面说了关于底层驱动的函数, 这些函数在网卡中至关重要, 而 ethernetif.c 文件就是存放这些函数的, LwIP 的 contrib 包中就包含这个文件的模板,我们直接拿过来修改即可,该文件的路径为“contrib-2.1.0\examples\ethernetif”,然后我们拷贝到 arch 文件夹下,并且创建一个 ethernetif.h 文件,一同添加到我们的工程中即可。
sys_arch.c/h是带操作系统时需要的,如果 lwIP 使用操作系统,那么 sys_arch.c/h 这两个文件是必要的,因为它们是 lwIP 内核与操作系统交互的接口文件。在实验中使用 FreeRTOS 操作系统时, sys_arch.c 文件中包含的邮箱、信号量以及互斥锁等 IPC 策略都是由 FreeRTOS 操作系统提供的。需要注意的是,FreeRTOS 操作系统没有邮箱的概念,可以使用消息队列来替代邮箱机制。
(1)修改 stm32f10x_it.c
SysTick 中断服务函数是一个非常重要的函数, FreeRTOS 所有跟时间相关的事情都在里面处理, SysTick 就是 FreeRTOS 的一个心跳时钟,驱动着 FreeRTOS 的运行,就像人的心跳一样,假如没有心跳,我们就相当于“死了”,同样的, FreeRTOS 没有了心跳,那么它就会卡死在某个地方,不能进行任务调度,不能运行任何的东西,因此我们需要实现一个 FreeRTOS 的心跳时钟, FreeRTOS 帮我们实现了 SysTick 的启动的配置:在 port.c 文件中已经实现 vPortSetupTimerInterrupt()函数,并且 FreeRTOS 通用的 SysTick 中断服务函数也实现了:在 port.c 文件中已经实现 xPortSysTickHandler()函数,所以移植的时候只需要我们在 stm32f4xx_it.c 文件中实现我们对应(STM32)平台上的 SysTick_Handler()函数即可。
FreeRTOS 为开发者考虑得特别多, PendSV_Handler()与 SVC_Handler()这两个很重要的函数都帮我们实现了,在在 port.c 文件中已经实现 xPortPendSVHandler()与 vPortSVCHandler()函数,防止我们自己实现不了,那么在 stm32f4xx_it.c 中就需要我们注释掉或者删除掉PendSV_Handler()与 SVC_Handler()这两个函数了。
(2)lwipopts.h 文件需要加入的配置
在前面的章节也说了 lwipopts.h 文件的作用,而此刻在操作系统中移植,我们首先要将添加了操作系统的工程拿过来,把 lwipopts.h 文件修改一下, 该文件最重要的宏定义就是 NO_SYS, 我们把它定义为 0 就表示使用操作系统,当然,在使用操作系统的时候我们一般都会使用 NETCONN API 与 Socket API 编程,那么就需要将宏 LWIP_NETCONN 与LWIP_SOCKET 定义为 1,表示使能这两种 API 编程, lwipopts.h 简单修改一下即可,然后再添加一下线程运行的一些宏定义,
(3) sys_arch.c/h 文件的编写
操作系统环境下, LwIP 移植的核心就是编写与操作系统相关的接口文件 sys_arch.c 和sys_arch.h,这两个文件可以自己创建也可以从 contrib 包中获取,路径分别为“contrib-2.1.0\ports\freertos”与“contrib-2.1.0\ports\freertos\include\arch” ,用户在移植的时候必须根据操作系统的功能为协议栈提供相应的接口,如邮箱(因为本次移植以 FreeRTOS 为例子, FreeRTOS 中没有邮箱这种概念,但是可以使用消息队列替代,为了迎合 LwIP 中的命名,下文统一采用邮箱表示)、信号量、互斥量等,这些 IPC 通信机制是保证内核与上层API 接口通信的基本保障,也是内核实现管理的继承,同时在 sys.h 文件中声明了用户需要实现的所有函数框架,这些函数具体见表格 8-2。

看到那么多函数, 是不是头都大了, 其实这些函数的实现都是很简单的, 首先讲解一下邮箱函数的实现。在 LwIP 中,用户代码与协议栈内部之间是通过邮箱进行数据的交互的,邮箱本质上就是一个指向数据的指针, API 将指针传递给内核,内核通过这个指针访问数据,然后去处理,反之内核将数据传递给用户代码也是通过邮箱将一个指针进行传递。
在操作系统环境下, LwIP 会作为一个线程运行,线程的名字叫 tcpip_thread,在初始化 LwIP 的时候,内核就会自动创建这个线程,并且在线程运行的时候阻塞在邮箱上,等待数据进行处理,这个邮箱数据的来源可能在底层网卡接收到的数据或者上层应用程序的数据,总之, tcpip_thread 线程在获取到邮箱中的数据时候,就会退出阻塞态,去处理数据,在处理完毕数据后又进入阻塞态中等待数据的到来, 如此反复。
信号量与互斥量的实现为内核提供同步与互斥的机制,比如当用户想要发送一个数据的时候,就会调用上层 API 接口, API 接口就会去先发送一个数据给内核去处理,然后尝试获取一个信号量,因为此时是没有信号量的,所以就会阻塞用户线程;内核在知道用户想要发送数据后,就会调用对应的网卡去发送数据,当数据发送完成后就释放一个信号量告知用户线程发送完成,这样子用户线程就得以继续执行。

浙公网安备 33010602011771号