lwIP 开发指南(上)

正点原子视频教程:https://www.bilibili.com/video/BV1R84y1r7Up?p=1&vd_source=cc0e43b449de7e8663ca1f89dd5fea7d
教程文档:http://www.openedv.com/docs/book-videos/zdyzshipin/4free/newlwIP.html
开发指南文档:最新探索者资料A盘

目录

lwIP 初探

本章,先介绍计算机网络相关知识,然后对lwIP软件库进行概述,接着介绍 MAC 内核的基本知识,最后探讨 LAN8720A 和 YT8512C 以太网 PHY 层芯片。

TCP/IP 协议栈是什么

TCP/IP 协议栈是一系列网络协议的总和,是构成网络通信的核心骨架,它定义了电子设备如何连入因特网,以及数据如何在它们之间进行传输。

TCP/IP 协议采用4层结构,分别是应用层、传输层、网络层和网络接口层,每一层都呼叫它的下一层所提供的协议来完成自己的需求。由于我们大部分时间都工作在应用层,下层的事情不用我们操心;其次网络协议体系本身就很复杂庞大,入门门槛高,因此很难搞清楚TCP/IP 的工作原理。如果读者想深入了解TCP/IP 协议栈的工作原理,可阅读《计算机网络书籍》。

TCP/IP 协议栈架构

网络协议有很多,如 MQTT、TCP、UDP、IP 等协议,这些协议组成了TCP/IP 协议栈,同时,这些协议具有层次性,它们分布在应用层,传输层和网络层。

TCP/IP 协议栈的分层结构和网络协议得对应关系如下图所示:

在这里插入图片描述

由于OSI 模型和协议比较复杂,所以并没有得到广泛的应用。而TCP/IP 模型因其开放性和易用性在实践中得到了广泛的应用,它也成为互联网的主流协议。

注意:网络技术的发展并不是遵循严格的OSI分层概念。实际上现在的互联网使用的是TCP/IP 体系结构有时已经演变成为图1.1.1.2所示那样,即某些应用程序可以直接使用 IP 层,或甚至直接使用最下面的网络接口层。

在这里插入图片描述

无论哪种表示方法,TCP/IP 模型各个层次都分别对应于不同的协议。TCP/IP 协议栈负责确保网络设备之间能够通信。它是一组规则,规定了信息如何在网络中传输。

这些协议都分布在应用层,传输层和网络层,网络接口层是由硬件来实现。

如Windows 操作系统包含了CBISC 协议栈,该协议栈就是实现了TCP/IP 协议栈的应用层,传输层和网络层的功能;网络接口层由网卡实现,所以 CBISC 协议栈和网卡构建了网络通信的核心骨架。因此,无论哪一款以太网产品,都必须符合TCP/IP 体系结构,才能实现网络通信。

注意:路由器和交换机等相关网络设备只实现网络层和网络接口层的功能。

TCP/IP 协议栈的封包和拆包

TCP/IP 协议栈的封包和拆包也是一个非常重要的知识,如以太网设备发送数据和接收数据的处理流程是怎么样的?这个问题涉及到TCP/IP 协议栈对数据处理的流程,该流程称之为“封包”和“拆包”。“封包”是对发送数据处理的流程,而“拆包”是对接收数据处理的流程,如下图:

在这里插入图片描述

上图中,发送端发送的数据自顶向下依次传递。各层协议依次在数据前添加本层的首部且设置本层首部的信息,最终将处理后的MAC帧递交给物理层转成光电模拟信号发送至网络,这个流程称之为封包流程。

在这里插入图片描述

上图中,当帧数据到达目的主机时,将沿着协议栈自底向上依次传递。各层协议依次根据帧中本层负责的头部信息以获取所需数据,最终将处理后的帧交给应用层,这个流程称之为拆包的过程。

lwIP 简介(lwIP实现应用层、传输层和网络层,网络接口层由硬件实现)

lwIP 与TCP/IP 体系结构的对应关系

在这里插入图片描述

从上图可以看出,lwIP 软件库只实现了TCP/IP 体系结构的应用层、传输层和网络层的功能,但网络接口层不能使用软件的方式实现,因为网络接口层是把数据包转成光电模拟信号,并转发至网络,所以网络接口层只能由硬件来实现

网络接口层又分为数据链路层和物理层,数据链路层就是STM32内嵌的MAC核,物理层就是外置的PHY芯片。

lwIP 是Light Weight(轻型)IP 协议,有无操作系统的支持都可以运行。lwIP 实现的重点是在保持TCP/IP 协议主要功能的基础上减少对 RAM 的占用,它只需十几 KB 的 RAM 和 40K 左右的ROM 就可以运行,这使 lwIP 协议栈适合在低端的嵌入式系统中使用。lwIP 的设计理念下,既可以无操作系统使用,也可以带操作系统使用;既可以支持多线程,也可以无线程。它可以运行在 8 位以及32 位的微处理器上,同时支持大端、小端系统。

lwIP 的各项特性,如下表所示:

在这里插入图片描述

在这里插入图片描述

lwIP 源码下载

lwIP 的开发托管在Savannah 上,Savannah 是软件开发、维护和分发。每个人都可以通过使用Savannah 的界面、Git 和邮件列表下载lwIP 源码包。lwIP 的项目主页:

http://savannah.nongnu.org/projects/lwip/。
在这个主页上,读者需要关注“project homepage”和“download area”这两个链接地址。

打开lwIP 项目主页之后,往下找到“Quick Overview”选项,如下图所示:

在这里插入图片描述

点击上图中 Project Homepage 链接地址,读者可以看到官方对于lwIP 的说明文档,包括lwIP 更新日记、常见误解、已发现的BUG、多线程、优化提示和相关文件中的函数描述等内容。

点击上图中的 Domnload Area 链接地址,读者可以看到 lwIP 源码和contrib 包的下载网页,如下图所示那样。由于lwIP 版本居多,因此本教程选择目前最新的 lwIP 版本(2.1.3)。下图中的contrib 包是提供用户lwIP 移植文件和lwIP 相关demo 例程。

注:contrib 包不属于lwIP内核的一部分,它只是为我们提供移植文件和学习实例。

在这里插入图片描述

点击上图中的lwip-2.1.3.zip 和contrib-2.1.0.zip 链接,下载完成之后在本地上可以看到这两个压缩包。

lwIP 文件说明

根据上一个小节的操作,我们已经下载了lwip-2.1.3.zipcontrib-2.1.0.zip 这两个压缩包。

接下来,笔者带大家认识一下lwip-2.1.3 和contrib-2.1.0 文件夹内的文件。

➢ lwIP 源码包文件说明

打开 lwip-2.1.3 文件夹,如下图所示:

在这里插入图片描述

上图中,这个文件夹包含的文件和文件夹非常多,这些文件与文件夹描述如下表所示。

在这里插入图片描述

上表中,src 文件夹是 lwIP 源码包中最重要的,它是 lwIP 的内核文件,也是我们移植到工程中的重要文件。接下来,笔者重点讲解 src 文件夹下的文件与文件夹,如下表所示。

在这里插入图片描述

  • api 文件夹下的文件是实现应用层与传输层递交数据的接口实现;
  • apps 文件夹下的文件实现了多种应用层协议;
  • core 文件夹下的文件是构建 lwIP 内核的源文件,对应了TCP/IP 体系架构的传输层、网络层;
  • include 文件夹包含了 lwIP 软件库的全部头文件;
  • netif 文件夹下的文件实现了网络层与数据链路层交互接口,以及管理不同类型的网卡——需要用户根据实际网卡自行实现。

打开core 文件夹,我们会发现,lwIP 是由一系列的模块组合而成,这些模块包括:
TCP/IP 协议栈的各种协议、内存管理、数据包管理、网卡管理、网卡接口、基础功能和API接口模块等,每一个模块是由几个源文件和一个头文件集合,这些头文件全部放在 include 文件夹下,而源文件都是放在 core 文件夹下。这些模块描述如下:

在这里插入图片描述
在这里插入图片描述

➢ lwIP 的 contrib 包文件说明

contrib 包提供了 lwIP 移植文件和 lwIP 相关 demo(应用实例),如下图所示:

在这里插入图片描述

上图中,ports 文件夹提供了lwIP 基于FreeRTOS 操作系统的移植文件;examples 和 apps文件夹提供读者学习 lwIP 的应用实例。至此,lwIP 源码库和contrib 包介绍完毕。

MAC 内核简介(STM32 内置) —— 数据链路层

STM32 内置了一个MAC 内核,它实现了TCP/IP 体系架构的数据链路层功能。STM32 内置以太网架构如下所示:

在这里插入图片描述

上图,绿色框的RX FIFO 和TX FIFO 都是2KB 的物理存储器,它们分别存储网络层递交的以太网数据和接收的以太网数据。以太网DMA 是网络层和数据链路层的中间桥梁,是利用存储器到存储器方式传输;

红色框框的内容可分为两个部分讲解,RMII 与 MII 是 MAC内核(数据链路层)与 PHY 芯片(物理层)的数据交互通道,用来传输以太网数据。

MDC 和 MDIO 是 MAC 内核对PHY 芯片的管理和配置,是站管理接口(SMI)所需的通信引脚。站管理接口(SMI)允许应用程序通过2 条线:时钟(MDC)和数据线(MDIO)访问任意PHY 寄存器,和IIC一样,该接口支持访问多达32 个PHY(管理多个PHY设备)。应用程序可以从32 个PHY 中选择一个PHY,然后从任意PHY 包含的32 个寄存器中选择一个寄存器,发送控制数据或接收状态信息。

任意给定时间内只能对一个PHY 中的一个寄存器进行寻址。在MAC 对PHY 进行读写操作的时候,应用程序不能修改MII 的地址寄存器和MII 的数据寄存器。在此期间对MII 地址寄存器或MII 数据寄存器执行的写操作将会被忽略。例如关于SMI 接口的详细介绍大家可以参考STM32F4xx 中文参考手册的824 页。

在这里插入图片描述

➢ 介质独立接口:MII

MII 用于MAC 层与PHY 层进行数据传输。MCU 通过MII 与PHY 层芯片的连接图如下。

图1.3.2 MCU 与PHY 层芯片连接

从图中可以看出,MII 介质接口使用的引脚数量是非常多的,这也反映出引脚紧缺的 MCU 不适合使用 MII 介质接口来实现以太网数据传输,MII 接口引脚的作用如下所示。

MII_TX_CLK:连续时钟信号。该信号提供进行TX 数据传输时的参考时序。标称频率为:速率为10 Mbit/s 时为2.5 MHz;速率为100 Mbit/s 时为25 MHz。
MII_RX_CLK:连续时钟信号。该信号提供进行RX 数据传输时的参考时序。标称频率为:速率为10 Mbit/s 时为2.5 MHz;速率为100 Mbit/s 时为25 MHz。
MII_TX_EN:发送使能信号。
MII_TXD[3:0]:数据发送信号。该信号是4 个一组的数据信号(4位位宽)。
MII_CRS:载波侦听信号。
MII_COL:冲突检测信号。
MII_RXD[3:0]:数据接收信号。该信号是4 个一组的数据信号(4位位宽)。
MII_RX_DV:接收数据有效信号。
MII_RX_ER:接收错误信号。该信号必须保持一个或多个周期(MII_RX_CLK),从而向MAC 子层指示在帧的某处检测到错误。

➢ 精简介质独立接口:RMII

精简介质独立接口(RMII)规范降低 10/100Mbit/s 下微控制器以太网外设与外部PHY 间的引脚数。根据IEEE 802.3u 标准,MII 包括16 个数据和控制信号的引脚,而RMII 规范将引脚数减少为7 个。

MCU 通过RMII 接口与PHY 层芯片的连接图如下图所示。因为RMII 相比MII,其发送和接收都少了两条线。因此要达到10Mbit/s 的速度,其时钟频率应为5MHZ,同理要达到100Mbit/s 的速度其时钟频率应为50MHz。正点原子开发板就是采用此接口连接PHY 芯片。

在这里插入图片描述

可以看出,REF_CLK 引脚需要提供50MHz 时钟频率,它分别提供 MAC 内核和 PHY 芯片,确保它们时钟同步。

在这里插入图片描述
在这里插入图片描述

PHY 芯片介绍(以太网芯片 外置) —— 物理层

PHY 芯片在TCP/IP 体系架构中扮演着物理层的角色,它把数据转换成光电模拟信号传输至网络当中。本小节介绍正点原子常用的 PHY 芯片,它们分别为LAN8720A 和YT8512C,这两款 PHY 芯片都是支持10/100BASE-T 百兆以太网传输速率。

在这里插入图片描述

YT8512C 简介

YT8512C 是低功耗单端口 10/100Mbps 以太网 PHY 芯片。它通过两条标准双绞线电缆收发器发送和接收数据所需的所有物理层功能。另外,YT8512C 通过 标准MIIRMII 接口连接到 mcu MAC 核。

YT8512C 功能结构图如下图所示:

在这里插入图片描述

上图是YT8512C 芯片的内部总架构示意图,从图中我们大概可以看出,它通过 LED0\LED1 引脚的电平来设置 PHY 地址,由 XTAL,Clock 引脚提供 PHY 内部时钟,同时 TXP\TXN\RXP\RXN 引脚连接到RJ45(网口)

➢ PHY 地址设置

MAC 层通过 SMI 总线对 PHY 芯片进行读写操作,SMI 可以控制32 个PHY 芯片,通过PHY 地址的不同来配置对应的PHY 芯片。YT8512C 芯片的PHY 地址设置如下表所示:

在这里插入图片描述

上表中,我们可通过YT8512C 芯片的 LED0/PHYADD0 和LED1/PHYADD1 引脚电平来设置PHY 地址。由于正点原子板载的PHY 芯片是把这两个引脚拉低,所以它的PHY 地址为0x00。打开HAL 配置文件或者打开PHY 配置文件,我们在此文件下配置PHY 地址,这些文件如下表所示:

在这里插入图片描述

可以看到,探索者和DMF407 开发板的PHY 地址在stm32f4xx_hal_conf.h 文件下设置的,而阿波罗和北极星开发板的PHY 地址在ethernet_chip.h 文件下设置的。因为探索者与DMF407 使用的HAL 库版本比阿波罗与北极星开发板所使用的HAL 库版本旧,所以它们的移植流程存在巨大的差异。这里笔者暂不讲解这部分的内容。

在这里插入图片描述

➢ YT8521C 的 RMII 接口介绍

YT8521C 的RMII 接口提供了两种RMII 模式,这两种模式分别为:

  • RMII1 模式:这个模式下YT8521C 的TXC 引脚不会输出50MHz 时钟。该模式的连接示意图如下图1.4.1.2 所示。
  • RMII2 模式:这个模式下YT8521C 的TXC 引脚会输出50MHz 时钟。该模式的连接示意图如下图1.4.1.3 所示。

在这里插入图片描述

对于RMII 接口而言,外部必须提供50MHz 的时钟驱动PHY 与MAC 内核,该时钟为了使PHY 芯片与MAC 内核保持时钟同步操作,它可以来自PHY 芯片、有源晶振或者STM32的MCO 引脚。如果我们的电路采用RMII1 模式的话,那么PHY 芯片由25MHz 晶振经过内部PLL 倍频达到50MHz,但是MAC 内核没有被提供50MHz 与PHY 芯片保持时钟同步,所以我们必须在此基础上使用MCO 或外部接入50MHz 晶振提供时钟给MAC 内核,以保持时钟同步。

在这里插入图片描述

如果电路使用上图模式连接的话,那么PHY 芯片经过外接晶振25MHz 和内部PLL 倍频操作,最终PHY 芯片内部的时钟为50MHz。接着PHY 芯片外围引脚TXC 会输出50MHz 时钟频率,该时钟频率可输入到MAC 内核保持时钟同步,这样我们无需外接晶振或者MCO 提供MAC 内核时钟。

注:RMII1 模式和RMII2 模式的选择是由YT8521C的 RX_DV(8)和RXD3(12)引脚决定,具体如何选择,请读者参考“YT8512C.PDF”手册的17 到18 页的内容。

在这里插入图片描述

➢ YT8521C 的寄存器介绍

在这里插入图片描述

PHY 是由IEEE 802.3 定义的,一般通过SMI 对PHY 进行管理和控制,也就是读写PHY内部寄存器。PHY 寄存器的地址空间为5 位,可以定义0 ~ 31 共32 个寄存器,但是,随着PHY 芯片功能的增加,很多PHY 芯片采用分页技术来扩展地址空间,定义更多的寄存器,在这里笔者不讨论这种情况,IEEE 802.3 定义了0~ 15 这16 个寄存器的功能,而16~31 寄存器由芯片制造商自由定义的。

在YT8521C 中有很多寄存器,这里笔者只介绍几个用到的寄存器(包括寄存器地址,此处使用十进制表示):BCR(0),BSR(1),PHY 特殊功能寄存器(17)这三个寄存器。

首先我们来看一 BCR(0)寄存器,BCR 寄存器各位介绍如下表所示。

在这里插入图片描述
在这里插入图片描述

我们设置以太网 速率和双工,其实就是配置PHY 芯片的BCR 寄存器。在HAL 配置文件或者ethernet_chip.h 文件定义了BCR 和BSR 寄存器,代码如下:
探索者、DMF407 开发板(HAL 配置文件下):

#define PHY_BCR ((uint16_t)0x0000)
#define PHY_BSR ((uint16_t)0x0001)

阿波罗、北极星开发板(PHY 配置文件下):

#define ETH_CHIP_BCR ((uint16_t)0x0000U)
#define ETH_CHIP_BSR ((uint16_t)0x0001U)

由于探索者及DMF407 开发板的例程是使用V1.26 版本的HAL 库,所以这两个寄存器并不需要读者来操作,原因就是我们调用HAL_ETH_Init 函数以后系统就会根据我们输入的参数配置YT8521C 的相应寄存器。但是,阿波罗及北极星开发板的例程使用目前最新的HAL 版本,它要求读者手动操作BCR 寄存器,例如自动协商、软复位等操作。

BSR 寄存器各个位介绍如下表所示:

在这里插入图片描述
在这里插入图片描述

BSR 寄存器为YT8521C 的状态寄存器,通过读取该寄存器的值我们可以得到当前的连接速度、双工状态和连接状态等信息。

接下来,笔者介绍的是YT8521C 特殊功能寄存器,此寄存器的各位如下表所示:

在这里插入图片描述
在这里插入图片描述

在特殊功能寄存器中我们关心的是bit13~bit15 这三位,因为系统通过读取这3 位的值来设置BCR 寄存器的bit8 和bit13。由于特殊功能寄存器不属于IEEE802.3 规定的前16 个寄存器,所以每个厂家的可能不同,这个需要用户根据自己实际使用的PHY 芯片去修改。

ST 提供的以太网驱动文件有三个配置项值得读者注意的,它们分别为PHY_SR、PHY_SPEED_STATUS 和PHY_DUPLEX_STATUS 配置项,这些配置项用来描述PHY 特殊功能寄存器,根据该寄存器的值设置BCR 寄存器的第8 位和第13 位,即 双工和网速

探索者、DMF407 开发板:

/* 网卡PHY地址设置*/
#define ETHERNET_PHY_ADDRESS 0x00
/* 选择PHY芯片*/
#define LAN8720 0
#define SR8201F 1
#define YT8512C 2
#define RTL8201 3
#define PHY_TYPE YT8512C

#if(PHY_TYPE == LAN8720)
#define PHY_SR ((uint16_t)0x1F) /* PHY状态寄存器地址*/
#define PHY_SPEED_STATUS ((uint16_t)0x0004) /* PHY速度状态*/
#define PHY_DUPLEX_STATUS ((uint16_t)0x0010) /* PHY双工状态*/

#elif(PHY_TYPE == SR8201F)
#define PHY_SR ((uint16_t)0x00) /* PHY状态寄存器地址*/
#define PHY_SPEED_STATUS ((uint16_t)0x2020) /* PHY速度状态*/
#define PHY_DUPLEX_STATUS ((uint16_t)0x0100) /* PHY双工状态*/

#elif(PHY_TYPE == YT8512C)
#define PHY_SR ((uint16_t)0x11) /* PHY状态寄存器地址*/
#define PHY_SPEED_STATUS ((uint16_t)0x4010) /* PHY速度状态*/
#define PHY_DUPLEX_STATUS ((uint16_t)0x2000) /* PHY双工状态*/

#elif(PHY_TYPE == RTL8201)
#define PHY_SR ((uint16_t)0x10) /* PHY状态寄存器地址*/
#define PHY_SPEED_STATUS ((uint16_t)0x0022) /* PHY速度状态*/
#define PHY_DUPLEX_STATUS ((uint16_t)0x0004) /* PHY双工状态*/

阿波罗、北极星开发板:

/* PHY地址*/
#define ETH_CHIP_ADDR ((uint16_t)0x0000U)
/* 选择PHY芯片*/
#define LAN8720 0
#define SR8201F 1
#define YT8512C 2
#define RTL8201 3
#define PHY_TYPE YT8512C

#if(PHY_TYPE == LAN8720)
#define ETH_CHIP_PHYSCSR ((uint16_t)0x1F)
#define ETH_CHIP_SPEED_STATUS ((uint16_t)0x0004)
#define ETH_CHIP_DUPLEX_STATUS ((uint16_t)0x0010)

#elif(PHY_TYPE == SR8201F)
#define ETH_CHIP_PHYSCSR ((uint16_t)0x00)
#define ETH_CHIP_SPEED_STATUS ((uint16_t)0x2020)
#define ETH_CHIP_DUPLEX_STATUS ((uint16_t)0x0100)

#elif(PHY_TYPE == YT8512C)
#define ETH_CHIP_PHYSCSR ((uint16_t)0x11)
#define ETH_CHIP_SPEED_STATUS ((uint16_t)0x4010)
#define ETH_CHIP_DUPLEX_STATUS ((uint16_t)0x2000)

#elif(PHY_TYPE == RTL8201)
#define ETH_CHIP_PHYSCSR ((uint16_t)0x10)
#define ETH_CHIP_SPEED_STATUS ((uint16_t)0x0022)
#define ETH_CHIP_DUPLEX_STATUS ((uint16_t)0x0004)
#endif /* PHY_TYPE */

笔者已经适配了多款PHY 芯片,根据PHY_TYPE 配置项来选择PHY_SR、PHY_SPEED_STATUS 和PHY_DUPLEX_STATUS 配置项的数值。

LAN8720A 简介

LAN8720A 是一款低功耗的10/100M 以太网 PHY 层芯片,它通过RMII/MII 介质接口与以太网MAC 层通信,内置10-BASE-T/100BASE-TX 全双工传输模块,支持10Mbps 和100Mbps。LAN8720A 主要特点如下:

高性能的10/100M 以太网传输模块。
支持RMII 接口以减少引脚数。
支持全双工和半双工模式。
两个状态LED 输出。
可以使用 25M 晶振以降低成本。
支持自协商模式。
支持HP Auto-MDIX 自动翻转功能。
支持SMI 串行管理接口。
支持MAC 接口。

LAN8720A 功能框图如下图所示:

在这里插入图片描述

➢ LAN8720A 中断管理
LAN8720A 的器件管理接口支持非IEEE 802.3 规范的中断功能。当一个中断事件发生并且相应事件的中断位使能,LAN8720A 就会在nINT(14 脚)产生一个低电平有效的中断信号。LAN8720A 的中断系统提供两种中断模式:主中断模式和复用中断模式。主中断模式是默认中断模式,LAN8720A 上电或复位后就工作在主中断模式,当模式控制/状态寄存器(十进制地址为17)的ALTINT 为0 是LAN8720 工作在主模式,当ALTINT 为1 时工作在复用中断模式。正点原子的STM32 系列开发板并未用到中断功能,关于中断的具体用法可以参考LAN8720A 数据手册的29,30 页。

➢ PHY 地址设置
MAC 层通过SMI 总线对PHY 进行读写操作,SMI 可以控制32 个PHY 芯片,通过不同的PHY 芯片地址来对不同的PHY 操作。LAN8720A 通过设置RXER/PHYAD0 引脚来设置其PHY 地址,默认情况下为0,其地址设置如下表所示。正点原子的STM32 系列开发板使用的是默认地址,也就是0X00。

在这里插入图片描述
在这里插入图片描述

➢ nINT/REFCLKO 配置

nINTSEL 引脚(2 号引脚)用于设置nINT/REFCLKO 引脚(14 号引脚)的功能。
nINTSEL 配置如下表所示。

在这里插入图片描述

当工作在REF_CLK In 模式时,50MHz 的外部时钟信号(左下角)同时接到LAN8720 的XTAL1/CKIN引脚(5 号引脚)和STM32 的RMII_REF_CLK(PA1)引脚上,如下图所示。

在这里插入图片描述

为了降低成本,LAN8720A 可以从外部的 25MHz 的晶振(右下角)中产生REF_CLK 时钟。到要使用此功能时应工作在REF_CLK Out 模式。当工作在REF_CLO Out 模式时REF_CLK 的时钟源如下图所示。

在这里插入图片描述

➢ LAN8720A 内部寄存器
PHY 是由IEEE 802.3 定义的,一般通过SMI 对PHY 进行管理和控制,也就是读写PHY 内部寄存器。PHY 寄存器的地址空间为5 位,可以定义0~ 31 共32 个寄存器,但是随之PHY 芯片功能的增加,很多PHY 芯片采用分页技术来扩展地址空间,定义更多的寄存器,在这里我们不讨论这种情况。IEEE 802.3 定义了0~ 15 这16 个寄存器的功能,16~31 寄存器由芯片制造商自由定义。在LAN8720A 有很多寄存器,笔者重点讲解BCR(0),BSR(1),PHY 特殊功能寄存器(31)这三个寄存器,前面两个寄存器笔者已经在1.6.1 小节讲解了,这里笔者无需重复讲解。接下来介绍的是LAN8720A 特殊功能寄存器,此寄存器的各个位如下表所示:

在这里插入图片描述

在特殊功能寄存器中我们关心的是bit2~bit4 这三位,因为系统通过读取这3 位的值来设置BCR 寄存器的bit8 和bit13。

两种以太网接入 MCU 方案

以太网接入方案一般分为两种,它们分别为全硬件TCP/IP 协议栈软件TCP/IP 协议栈,其中,软件TCP/IP 协议栈用途非常广泛,如电脑、交换机等网络设备,而全硬件TCP/IP 协议栈是近年来比较新型的以太网接入方案。下面分别来讲解这两种接入方案的差异和优缺点。

软件TCP/IP 协议栈以太网接入方案

这种方案由 lwIP + MAC 内核 + PHY 层芯片实现以太网物理连接,如正点原子的探索者、阿波罗、北极星以及电机开发板都是采用这类型的以太网接入方案,该方案的连接示意图如下图所示:

在这里插入图片描述

上图中,MCU 要求内置 MAC 内核,该内核相当TCP/IP 体系结构的数据链路层,而lwIP软件库用来实现TCP/IP 体系结构的应用层、传输层和网络层,同时,板载PHY 层芯片用来实现TCP/IP 体系结构的物理层。因此,lwIP、MAC 内核和PHY 层芯片构建了网络通信核心骨架。

优点:
移植性:可在不同平台、不同编译环境的程序代码经过修改转移到自己的系统中运行。
可造性:可在TCP/IP协议栈的基础上添加和删除相关功能。
可扩展性:可扩展到其他领域的应用及开发。

缺点:
内存方面分析:传统的TCP/IP 方案是移植一个lwIP 的TCP/IP 协议(RAM 50K+,ROM 80K+),造成主控可用内存减小。
从代码量分析:移植lwIP可能需要的代码量超过40KB,对于有些主控芯片内存匮乏来说无疑是一个严重的问题。
从运行性能方面分析:由于软件TCP/IP协议栈方案在通信时候是不断地访问中断机制,造成线程无法运行,如果多线程运行,会使MCU的工作效率大大降低。
从安全性方面分析:软件协议栈会很容易遭受网络攻击,造成单片机瘫痪。

硬件TCP/IP 协议栈以太网接入方案

所谓全硬件TCP/IP 协议栈是将传统的软件协议TCP/IP 协议栈用硬件化的逻辑门电路来实现。芯片内部完成TCP、UDP、ICMP 等多种网络协议,并且实现了物理层以太网控制(MAC+PHY)、内存管理等功能,完成了一整套硬件化的以太网解决方案。

该方案的连接示意图如下图所示:

在这里插入图片描述

上图中,MCU 通过串口或者SPI 进行网络通讯,无需移植协议库,极大地减少程序的代码量,甚至弥补了网络协议安全性不足的短板。硬件TCP/IP 协议栈的优缺点,如下所示:

优点:
从代码量方面来看:相比于传统的接入已经大大减少了代码量。
从运行方面来看:极大的减少了中断次数,让单片机更好的完成其他线程的工作。
从安全性方面来看:硬件化的逻辑门电路来处理TCP/IP协议是不可被攻击的,也就是说网络攻击和病毒对它无效,这也充分弥补了网络协议安全性不足的短板。

缺点:
从可扩展性来看:虽然该芯片内部使用逻辑门电路来实现应用层和物理层协议,但是它具有功能局限性,例如给TCP/IP协议栈添加一个协议,这样它无法快速添加了。
从收发速率来看:全硬件TCP/IP协议栈芯片都是采用并口、SPI以及IIC等通讯接口来收发数据,这些数据会受通信接口的速率而影响。

总的来说:全硬件TCP / IP 协议栈简化传统的软件TCP / IP 协议栈,卸载了MCU 用于处理TCP / IP 这部分的线程,节约MCU 内部ROM 等硬件资源,工程师只需进行简单的套接字编程和少量的寄存器操作即可方便地进行嵌入式以太网上层应用开发,减少产品开发周期,降低开发成本。

lwIP 无操作系统移植

本教程适用于正点原子所有支持lwIP 的STM32开发板,各个开发板移植lwIP 的不同之处,将会在教程中以表格或者红色字体的形式指出。

lwIP 前期准备

在移植之前我们需要一个基础工程,这里笔者使用裸机例程中的内存管理实验作为基础工程。

首先我们把内存管理实验重命名为“lwIP 例程1 lwIP 裸机移植”工程,接着在该工程的Middlewares 文件夹下创建lwip 文件夹,并在此文件夹下创建arch、lwip_app 文件夹。arch 文件夹主要存放lwIP 系统配置文件,而lwip_app 文件夹主要存放用户文件,lwip 文件夹下的文件结构如下图所示:

在这里插入图片描述

以太网DMA 描述符

在移植lwIP 之前,读者必须了解以太网DMA 描述符相关的知识,下面笔者使用F4_V1.26.0 版本HAL 库和F4_V1.27.0(F7/H7 相同)版本的HAL 库来讲解以太网DMA 描述符的知识,如果它们存在不一样的以太网知识,则笔者会重点标注。

ST 的以太网DMA 描述符简介

ST 以太网模块中的接收/发送FIFO 和内存之间的以太网数据包传输是以太网DMA 描述符完成的。它们一共有两个描述符列表:一个用于接收,一个用于发送,这两个列表的基地址分别写入DMARDLAR 寄存器和DMATDLAR 寄存器,当然这两个描述符列表支持两种链接方式如下图所示:

在这里插入图片描述

图2.2.1.1 展示了DMA 描述符的两种结构:环形结构和链接结构,在ST 提供的以太网驱动库stm32f4/f7/h7xx_hal_eth.c 中初始化发送和接收描述符列表就使用的是链接结构,DMA 描述符连接结构的具体描述如下图2.2.1.2 和2.2.1.3 所示。

在这里插入图片描述

上图的DMA 描述符链接结构,由以下函数实现如下表所示:

在这里插入图片描述

这些函数应注意以下几点:
一个以太网数据包可以跨越一个或多个DMA 描述符。
一个DMA 描述符只能用于一个以太网数据包。
DMA 描述符列表中的最后一个描述符指向第一个,形成循环链式结构。
在ST 的以太网驱动库stm32f4/f7/h7xx_hal_eth.h 中有个结构体ETH_DMADescTypeDef,这个结构体定义了发送及接收DMA 描述符,ETH_DMADescTypeDef 结构体代码如下。
F4_V1.26.0 版本的HAL 库(探索者、DMF407):

typedef struct
{
    __IO uint32_t Status;         /* 状态*/
    uint32_t ControlBufferSize;   /* 控制和buffer1,buffer2的长度*/
    uint32_t Buffer1Addr;         /* buffer1地址*/
    uint32_t Buffer2NextDescAddr; /* buffer2地址或下一个描述符地址*/
    uint32_t ExtendedStatus;      /* 增强描述符状态*/
    uint32_t Reserved1;           /* 保留*/
    uint32_t TimeStampLow;        /* 时间戳低位*/
    uint32_t TimeStampHigh;       /* 时间戳高位*/
} ETH_DMADescTypeDef;

F4_V1.27.0/F7_V1.17/H7_V1.10 版本的HAL 库(阿波罗、北极星):

typedef struct
{
    __IO uint32_t DESC0;
    __IO uint32_t DESC1;
    __IO uint32_t DESC2;
    __IO uint32_t DESC3;
    __IO uint32_t DESC4;
    __IO uint32_t DESC5;
    __IO uint32_t DESC6;
    __IO uint32_t DESC7;
    uint32_t BackupAddr0; /* used to store rx buffer 1 address */
    uint32_t BackupAddr1; /* used to store rx buffer 2 address */
} ETH_DMADescTypeDef;

可以看出,虽然F4_V1.26.0 的ETH_DMADescTypeDef 结构体与F4_V1.27.0/F7_V1.17/H7_V1.10 的ETH_DMADescTypeDef 结构体成员变量名称不同,但是它们实现的功能是一致的,例如DESC0 和Status 都是描述以太网DMA 描述符的状态,其他成员变量也是一一对应。

DMA 描述符的类型

DMA 描述符分为增强描述符和常规描述符,本教程中我们使用的是常规描述符,如果使用常规描述符的话就只使用ETH_DMADescTypeDef 结构体的前四个成员变量,有关常规描述符和增强描述符的区别大家可以参考STM32F4/F7/H7xx 中文参考手册的以太网章节。
我们知道STM32F4/F7/H7 的描述符分为发送描述符和接收描述符,发送描述符和接收描述符都用ETH_DMADescTypeDef 这个结构体来定义,这里笔者只以发送描述符(常规Tx 的DMA 描述符)为例讲解一下,接收描述符(常规Rx 的DMA 描述符)与其类似。常规Tx 的DMA 描述符可以用下图来表示。

在这里插入图片描述

我们可以从图2.2.2.1 中看到常规Tx DMA 描述符有4 个“寄存器”:TDES0、TDES1、TDES2 和TDES3,如果是增强描述符的话就会有8 个“寄存器”。这里一定要注意这4 个“寄存器”并不是STM32F4/F7/H7 真实存在的,我们在STM32F4/F7/H7 的寄存器列表里面是找不到的,这4 个“寄存器”是由ETH_DMADescTypeDef 这个结构体描述,笔者将它们称之为“软件寄存器”,这些“寄存器”也叫做描述符字,描述符字和ETH_DMADescTypeDef 这个结构体的关系如下表所示(此处以Tx DMA 描述符为例)。

在这里插入图片描述

以上表2.2.2.1 所列的是常规Tx 的DMA 描述符,常规Rx 的DMA 描述符与之类似。和我们操作寄存器一样,描述符字的每个位代表不同的状态或控制,关于Tx 的DMA 描述符中描述符字各个位的含意大家可以参考STM32F4/F7/H7xx 中文参考手册的以太网章节(Rx DMA描述符)。

前面了解到,ST 的官方以太网库stm32f4/f7/h7xx_hal_eth.c 中使用链接结构的DMA 描述符,那么在以太网描述符结构体ETH_DMADescTypeDef 中Buffer1Addr(DESC2)就是保存缓冲区1 的地址,Buffer2NextDescAddr(DESC3)就是保存了下一个描述
符的地址,如下图所示:
F4_V1.26.0 版本的HAL 库(探索者、DMF407):

在这里插入图片描述

F4_V1.27.0/F7_V1.17/H7_V1.10 版本的HAL 库(阿波罗、北极星):

在这里插入图片描述

正如图2.2.2.2 所示那样,笔者需要定义两个DMA 描述符数组,分别用于发送和接收

DMA,在例程中,我们会在例程中的ethernet.c 文件中定义两个指向ETH_DMADescTypeDef的指针g_eth_dma_rx_dscr_tab 和g_eth_dma_tx_dscr_tab,一个用于DMA 接收,一个用于DAM 发送,代码如下:
F4_V1.26.0 版本的HAL 库(探索者、DMF407):

ETH_DMADescTypeDef *g_eth_dma_rx_dscr_tab; /* 以太网DMA接收描述符数据结构体指针*/
ETH_DMADescTypeDef *g_eth_dma_tx_dscr_tab; /* 以太网DMA发送描述符数据结构体指针*/

F4_V1.27.0/F7_V1.17/H7_V1.10 版本的HAL 库(阿波罗、北极星):

ETH_DMADescTypeDef g_eth_dma_rx_dscr_tab[ETH_RX_DESC_CNT];
ETH_DMADescTypeDef g_eth_dma_tx_dscr_tab[ETH_TX_DESC_CNT];

我们可以用内存分配函数给上面这两个指针分配内存,如果有读者不想用动态内存的话也可以直接定义两个数组。前面了解到,ST 以太网驱动库stm32f4/f7/h7xx_hal_eth.c 中采用的是DMA 链接结构,但是g_eth_dma_rx_dscr_tab 和g_eth_dma_tx_dscr_tab 是两个指针(或数组),那么我们必然要将这两个指针(或数组)改为链接结构。在stm32f4/f7/h7xx_hal_eth.c 文件中有两个函数可以把两个指针(或数组)变为链接结构,它们分别为:
F4_V1.26.0 版本的HAL 库(探索者、DMF407):
HAL_ETH_DMATxDescListInit 和HAL_ETH_DMARxDescListInit。
F4_V1.27.0/F7_V1.17/H7_V1.10 版本的HAL 库(阿波罗、北极星):
ETH_DMATxDescListInit 和ETH_DMARxDescListInit。
我们通过这两个函数我们就可以将上面两个指针(或数组)变为链接结构了。

如何追踪DMA 描述符

为了追踪Rx/Tx 的DMA 描述符,在下图结构体中定义了两个非常重要的指针变量,如下图所示。
F4_V1.26.0 版本的HAL 库(探索者、DMF407):

在这里插入图片描述

F4_V1.27.0/F7_V1.17/H7_V1.10 版本的HAL 库(阿波罗、北极星):

在这里插入图片描述

这两个指针变量指向ETH_DMADescTypeDef 结构体,在使用过程中它们两个分别指向下一个要发送或者接收的描述符,如下图所示那样:

在这里插入图片描述

至此STM32F4/F7/H7 以太网模块中的DMA 描述符讲解完成了,关于DMA 描述符的详细内容读者可以参考STM32 F4/F7/H7xx 中文参考手册。

lwIP 移植流程(探索者、DMF407)

在这里插入图片描述
在这里插入图片描述

添加网卡驱动程序(ethernet.c/h)到BSP目录

注意:探索者及DMF407 开发板的移植lwIP 流程是一致的,它们唯一不同的地方只有三处,稍后会指出区别的部分。

打开正点原子提供的“lwIP 例程1 lwIP 裸机移植”实验并在此实验中进入Drivers\BSP 文件夹,在这里面有一个ETHERNET 文件夹,在这个文件夹中有ethernet.c 和ethernet.h 这两个文件,这两个文件里面包含以太网驱动初始化和 MAC 的驱动程序,大家在移植的时候将ethernet.c/h 文件拷贝到自己工程的Drivers\BSP\ETHERNET 文件夹下,并且将ethernet.c 添加到工程中的Drivers/BSP 分组,如下图所示那样:

在这里插入图片描述

图2.3.1.1 添加ethernet.c 后的Drivers/BSP 分组

下面介绍ethernet.c 定义了哪些函数,如下表所示:

在这里插入图片描述

表2.3.1.1 ethernet.c 文件函数

首先我们来看一下ethernet_init 函数。ethernet_init 函数代码如下所示:

/**
 * @brief 以太网芯片初始化
 * @param 无
 * @retval 0,成功
 * 1,失败
 */
uint8_t ethernet_init(void)
{
    uint8_t macaddress[6];
    macaddress[0] = lwipdev.mac[0];
    macaddress[1] = lwipdev.mac[1];
    macaddress[2] = lwipdev.mac[2];
    macaddress[3] = lwipdev.mac[3];
    macaddress[4] = lwipdev.mac[4];
    macaddress[5] = lwipdev.mac[5];
    
    g_eth_handler.Instance = ETH;
    g_eth_handler.Init.AutoNegotiation = ETH_AUTONEGOTIATION_ENABLE;/* 使能自协商模式*/
    g_eth_handler.Init.Speed = ETH_SPEED_100M;						/* 速度100M,如果开启了自协商模式,此配置就无效*/
    g_eth_handler.Init.DuplexMode = ETH_MODE_FULLDUPLEX;    		/* 全双工模式,如果开启了自协商模式,此配置就无效*/
    g_eth_handler.Init.PhyAddress = ETHERNET_PHY_ADDRESS;    		/* 以太网芯片的地址*/
    g_eth_handler.Init.MACAddr = macaddress;    					/* MAC地址*/
    g_eth_handler.Init.RxMode = ETH_RXINTERRUPT_MODE;    			/* 中断接收模式*/
    g_eth_handler.Init.ChecksumMode = ETH_CHECKSUM_BY_HARDWARE;    	/* 硬件帧校验*/
    g_eth_handler.Init.MediaInterface = ETH_MEDIA_INTERFACE_RMII;   /* RMII接口*/
    
    if (HAL_ETH_Init(&g_eth_handler) == HAL_OK)
    {
        return 0; /* 成功*/
    }
    else
    {
        return 1; /* 失败*/
    }
}

在这个函数中,我们首先设置 MAC 地址以及相关的ETH 配置,接着调用 HAL_ETH_Init 函数初始化以太网。

HAL_ETH_MspInit 函数会被 HAL_ETH_Init 函数调用,该函数用来初始化以太网IO、使能时钟、配置中断等,该函数代码如下。

/**
 * @brief ETH底层驱动,时钟使能,引脚配置
 * @note 此函数会被HAL_ETH_Init()调用
 * @param heth:以太网句柄
 * @retval 无
 */
void HAL_ETH_MspInit(ETH_HandleTypeDef *heth)
{
    GPIO_InitTypeDef gpio_init_struct;
    ETH_CLK_GPIO_CLK_ENABLE();   /* 开启ETH_CLK时钟*/
    ETH_MDIO_GPIO_CLK_ENABLE();  /* 开启ETH_MDIO时钟*/
    ETH_CRS_GPIO_CLK_ENABLE();   /* 开启ETH_CRS时钟*/
    ETH_MDC_GPIO_CLK_ENABLE();   /* 开启ETH_MDC时钟*/
    ETH_RXD0_GPIO_CLK_ENABLE();  /* 开启ETH_RXD0时钟*/
    ETH_RXD1_GPIO_CLK_ENABLE();  /* 开启ETH_RXD1时钟*/
    ETH_TX_EN_GPIO_CLK_ENABLE(); /* 开启ETH_TX_EN时钟*/
    ETH_TXD0_GPIO_CLK_ENABLE();  /* 开启ETH_TXD0时钟*/
    ETH_TXD1_GPIO_CLK_ENABLE();  /* 开启ETH_TXD1时钟*/
    ETH_RESET_GPIO_CLK_ENABLE(); /* 开启ETH_RESET时钟*/
    __HAL_RCC_ETH_CLK_ENABLE();  /* 开启ETH时钟*/
    /* 网络引脚设置RMII接口
     * ETH_MDIO -------------------------> PA2
     * ETH_MDC --------------------------> PC1
     * ETH_RMII_REF_CLK------------------> PA1
     * ETH_RMII_CRS_DV ------------------> PA7
     * ETH_RMII_RXD0 --------------------> PC4
     * ETH_RMII_RXD1 --------------------> PC5
     * ETH_RMII_TX_EN -------------------> PG11
     * ETH_RMII_TXD0 --------------------> PG13
     * ETH_RMII_TXD1 --------------------> PG14
     * ETH_RESET-------------------------> PD3
     */
    /* PA1,2,7 */
    gpio_init_struct.Pin = ETH_CLK_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_AF_PP;    /* 推挽复用*/
    gpio_init_struct.Pull = GPIO_NOPULL;        /* 不带上下拉*/
    gpio_init_struct.Speed = GPIO_SPEED_HIGH;   /* 高速*/
    gpio_init_struct.Alternate = GPIO_AF11_ETH; /* 复用为ETH功能*/
    HAL_GPIO_Init(ETH_CLK_GPIO_PORT, &gpio_init_struct);
    gpio_init_struct.Pin = ETH_MDIO_GPIO_PIN;
    HAL_GPIO_Init(ETH_MDIO_GPIO_PORT, &gpio_init_struct);
    gpio_init_struct.Pin = ETH_CRS_GPIO_PIN;
    HAL_GPIO_Init(ETH_CRS_GPIO_PORT, &gpio_init_struct);
    /* PC1 */
    gpio_init_struct.Pin = ETH_MDC_GPIO_PIN;
    HAL_GPIO_Init(ETH_MDC_GPIO_PORT, &gpio_init_struct); /* ETH_MDC初始化*/
    /* PC4 */
    gpio_init_struct.Pin = ETH_RXD0_GPIO_PIN;
    HAL_GPIO_Init(ETH_RXD0_GPIO_PORT, &gpio_init_struct); /* ETH_RXD0初始化*/
    /* PC5 */
    gpio_init_struct.Pin = ETH_RXD1_GPIO_PIN;
    HAL_GPIO_Init(ETH_RXD1_GPIO_PORT, &gpio_init_struct); /* ETH_RXD1初始化*/
    /* PG11,13,14 */
    gpio_init_struct.Pin = ETH_TX_EN_GPIO_PIN;
    HAL_GPIO_Init(ETH_TX_EN_GPIO_PORT, &gpio_init_struct); /* ETH_TX_EN初始化*/
    gpio_init_struct.Pin = ETH_TXD0_GPIO_PIN;
    HAL_GPIO_Init(ETH_TXD0_GPIO_PORT, &gpio_init_struct); /* ETH_TXD0初始化*/
    gpio_init_struct.Pin = ETH_TXD1_GPIO_PIN;
    HAL_GPIO_Init(ETH_TXD1_GPIO_PORT, &gpio_init_struct); /* ETH_TXD1初始化*/
    /* 复位引脚*/
    gpio_init_struct.Pin = ETH_RESET_GPIO_PIN;   /* ETH_RESET初始化*/
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出*/
    gpio_init_struct.Pull = GPIO_NOPULL;         /* 无上下拉*/
    gpio_init_struct.Speed = GPIO_SPEED_HIGH;    /* 高速*/
    HAL_GPIO_Init(ETH_RESET_GPIO_PORT, &gpio_init_struct);

    ETHERNET_RST(0); 						/* 硬件复位PHY芯片,非常重要*/
    delay_ms(50);
    ETHERNET_RST(1);                      	/* 复位结束*/

    HAL_NVIC_SetPriority(ETH_IRQn, 6, 0); 	/* 网络中断优先级应该高一点*/
    HAL_NVIC_EnableIRQ(ETH_IRQn);
}

此函数非常简单,用来初始化以太网的引脚和设置以太网中断的优先级,最后硬件复位一下PHY 芯片。注:DMF407 电机开发板使用PI8 作为以太网芯片复位引脚,而探索者使用PD3 来作为以太网芯片复位引脚,如下表所示:

在这里插入图片描述

注意:这里是探索者与DMF407 开发板第一处区别。

接下来我们来介绍ethernet_read_phy 和ethernet_write_phy 这两个函数,这两个函数用来读取和配置PHY 芯片内部寄存器的,其实它们就是对HAL_ETH_ReadPHYRegister 和HAL_ETH_ReadPHYRegister 做了一个简单的封装。

ethernet_chip_get_speed 函数用来获取网络的速度和双工状态,该函数代码如下:

/**
* @breif 获得网络芯片的速度模式
* @param 无
* @retval 1:获取100M成功
0:失败
*/
uint8_t ethernet_chip_get_speed(void)
{
    uint8_t speed;
#if (PHY_TYPE == LAN8720) /* 从LAN8720的31号寄存器中读取网络速度和双工模式*/
    speed = ((ethernet_read_phy(PHY_SR) & PHY_SPEED_STATUS) >> 4);
#elif (PHY_TYPE == SR8201F) /* 从SR8201F的0号寄存器中读取网络速度和双工模式*/
    speed = ((ethernet_read_phy(PHY_SR) & PHY_SPEED_STATUS) >> 13);
#elif (PHY_TYPE == YT8512C) /* 从YT8512C的17号寄存器中读取网络速度和双工模式*/
    speed = ((ethernet_read_phy(PHY_SR) & PHY_SPEED_STATUS) >> 14);
#elif (PHY_TYPE == RTL8201) /* 从RTL8201的16号寄存器中读取网络速度和双工模式*/
    speed = ((ethernet_read_phy(PHY_SR) & PHY_SPEED_STATUS) >> 1);
#endif
    return speed;
}

由于YT8512C、SR8201F 和RTL8201 这三款PHY 芯片都是兼容一个原理图的,所以我们根据PHY_TYPE 宏定义来选择PHY 芯片。

ETH_IRQHandler 函数为以太网 DMA 接收中断服务函数,中断服务函数代码如下:

extern void lwip_pkt_handle(void); /* 在lwip_comm.c里面定义*/
/**
 * @breif 中断服务函数
 * @param 无
 * @retval 无
 */
void ETH_IRQHandler(void)
{
    if (ethernet_get_eth_rx_size(g_eth_handler.RxDesc))
    {
        lwip_pkt_handle(); /* ====== 处理以太网数据,将数据提交给 LWIP ====== */
    }
    /* 清除DMA中断标志位*/
    __HAL_ETH_DMA_CLEAR_IT(&g_eth_handler, ETH_DMA_IT_NIS);
    /* 清除DMA接收中断标志位*/
    __HAL_ETH_DMA_CLEAR_IT(&g_eth_handler, ETH_DMA_IT_R);
}

在中断服务函数中,我们通过判断接收到的数据包长度是否为0,当接收的数据包长度不为0 时,程序就调用lwip_pkt_handle 函数处理接收到的数据包,这个函数我们稍后会做讲解,处理完成后清除DMA 接收中断标志位和DMA 总中断标志位。

ethernet_get_eth_rx_size 函数用来获取当前接收到的以太网帧长度,该函数代码如下:

/**
 * @breif 获取接收到的帧长度
 * @param dma_rx_desc : 接收DMA描述符
 * @retval frameLength : 接收到的帧长度
 */
uint32_t ethernet_get_eth_rx_size(ETH_DMADescTypeDef *dma_rx_desc)
{
    uint32_t frameLength = 0;
    if (((dma_rx_desc->Status & ETH_DMARXDESC_OWN) == (uint32_t)RESET) &&
        ((dma_rx_desc->Status & ETH_DMARXDESC_ES) == (uint32_t)RESET) &&
        ((dma_rx_desc->Status & ETH_DMARXDESC_LS) != (uint32_t)RESET))
    {
        frameLength = ((dma_rx_desc->Status & ETH_DMARXDESC_FL) >>
                       ETH_DMARXDESC_FRAME_LENGTHSHIFT);
    }
    return frameLength;
}

ethernet_mem_malloc 函数就是为我们前面提到的那四个指针内存分配,函数代码如下:

/**
 * @breif 为ETH底层驱动申请内存
 * @param 无
 * @retval 0,正常
 * 1,失败
 */
uint8_t ethernet_mem_malloc(void)
{
    g_eth_dma_rx_dscr_tab = mymalloc(SRAMIN, ETH_RXBUFNB *
                                                 sizeof(ETH_DMADescTypeDef)); /* 申请内存*/
    g_eth_dma_tx_dscr_tab = mymalloc(SRAMIN, ETH_TXBUFNB *
                                                 sizeof(ETH_DMADescTypeDef)); /* 申请内存*/
    g_eth_rx_buf = mymalloc(SRAMIN, ETH_RX_BUF_SIZE * ETH_RXBUFNB);           /* 申请内存*/
    g_eth_tx_buf = mymalloc(SRAMIN, ETH_TX_BUF_SIZE * ETH_TXBUFNB);           /* 申请内存*/
    if (!(uint32_t)&g_eth_dma_rx_dscr_tab || !(uint32_t)&g_eth_dma_tx_dscr_tab || !(uint32_t)&g_eth_rx_buf || !(uint32_t)&g_eth_tx_buf)
    {
        ethernet_mem_free();
        return 1; /* 申请失败*/
    }
    return 0; /* 申请成功*/
}

ethernet_mem_free 函数为释放内存函数,功能是将g_eth_rx_buf、g_eth_tx_buf、g_eth_dma_rx_dscr_tab 和g_eth_dma_tx_dscr_tab 这四个指针的内存释放掉,这里就不贴出具体代码了。

至此网卡驱动添加完成,我们编译工程会出现很多错误,如下图所示:

在这里插入图片描述
图2.3.2.8 缺少以太网hal 文件

上述的错误就是工程缺少了以太网驱动文件,我们在工程的Drivers/STM32F4xx_HAL_Driver 分组下添加stm32f4xx_hal_eth.c 以太网驱动文件。添加驱动文件之后我们打开stm3 f4xx _hal_conf.h 文件,并在该文件下使能HAL_ETH_MODULE_ENABLED 以太网模块,这个配置项是在该文件的最顶端定义的,最后我们在该文件下添加以下源码:

/* MAC ADDRESS: MAC_ADDR0:MAC_ADDR1:MAC_ADDR2:MAC_ADDR3:MAC_ADDR4:MAC_ADDR5 */
#define MAC_ADDR0 2U
#define MAC_ADDR1 0U
#define MAC_ADDR2 0U
#define MAC_ADDR3 0U
#define MAC_ADDR4 0U
#define MAC_ADDR5 0U
/* Definition of the Ethernet driver buffers size and count */
#define ETH_RX_BUF_SIZE ETH_MAX_PACKET_SIZE
#define ETH_TX_BUF_SIZE ETH_MAX_PACKET_SIZE
#define ETH_RXBUFNB ((uint32_t)5)
#define ETH_TXBUFNB ((uint32_t)5)
/* Section 2: PHY configuration section */
/* ETHERNET PHY Address*/
#define ETHERNET_PHY_ADDRESS 0x00
/* PHY Reset delay these values are based on a 1 ms Systick interrupt*/
#define PHY_RESET_DELAY ((uint32_t)0x00000FFF)
#define PHY_CONFIG_DELAY ((uint32_t)0x00000FFF)
#define PHY_READ_TO ((uint32_t)0x0000FFFF)
#define PHY_WRITE_TO ((uint32_t)0x0000FFFF)
/* Section 3: Common PHY Registers */
#define PHY_BCR ((uint16_t)0x00)
#define PHY_BSR ((uint16_t)0x01)
#define PHY_RESET ((uint16_t)0x8000)
#define PHY_LOOPBACK ((uint16_t)0x4000)
#define PHY_FULLDUPLEX_100M ((uint16_t)0x2100)
#define PHY_HALFDUPLEX_100M ((uint16_t)0x2000)
#define PHY_FULLDUPLEX_10M ((uint16_t)0x0100)
#define PHY_HALFDUPLEX_10M ((uint16_t)0x0000)
#define PHY_AUTONEGOTIATION ((uint16_t)0x1000)
#define PHY_RESTART_AUTONEGOTIATION ((uint16_t)0x0200)
#define PHY_POWERDOWN ((uint16_t)0x0800)
#define PHY_ISOLATE ((uint16_t)0x0400)
#define PHY_AUTONEGO_COMPLETE ((uint16_t)0x0020)
#define PHY_LINKED_STATUS ((uint16_t)0x0004)
#define PHY_JABBER_DETECTION ((uint16_t)0x0002)
/* Section 4: Extended PHY Registers */
#define LAN8720 0
#define SR8201F 1
#define YT8512C 2
#define RTL8201 3
#define PHY_TYPE YT8512C
#if (PHY_TYPE == LAN8720)
#define PHY_SR ((uint16_t)0x1F)
#define PHY_SPEED_STATUS ((uint16_t)0x0004)
#define PHY_DUPLEX_STATUS ((uint16_t)0x0010)
#elif (PHY_TYPE == SR8201F)
#define PHY_SR ((uint16_t)0x00)
#define PHY_SPEED_STATUS ((uint16_t)0x2020)
#define PHY_DUPLEX_STATUS ((uint16_t)0x0100)
#elif (PHY_TYPE == YT8512C)
#define PHY_SR ((uint16_t)0x11)
#define PHY_SPEED_STATUS ((uint16_t)0x4010)
#define PHY_DUPLEX_STATUS ((uint16_t)0x2000
#elif (PHY_TYPE == RTL8201)
#define PHY_SR ((uint16_t)0x10)
#define PHY_SPEED_STATUS ((uint16_t)0x0022)
#define PHY_DUPLEX_STATUS ((uint16_t)0x0004)
#endif /* PHY_TYPE */

注意:“Section 4”下的代码是笔者对多款的PHY 芯片做了兼容处理,大家请根据PHY_TYPE 宏定义选择。

重新编译一次,工程还是出现一个错误,这个错误是因为在以太网DMA 接收中断服务函数ETH_IRQHandler 中调用了lwip_pkt_handle 函数,而这个函数没有在ethernet.c 文件中定义,它是在lwip_comm.c 文件中定义的,这个错误提示不用管,lwip_pkt_handle 函数后续会讲解。

注意:DMF407 开发板板载的PHY 芯片为LAN8720A(可能以后换成YT8512C),而最新推出的探索者开发板板载的PHY芯片为YT8512C,所以读者必须在PHY_TYPE 配置项写入相应的参数。这里是探索者与DMF407 移植的第二处区别。

添加 lwIP 源文件(src、arch、lwip_app目录)

把lwip-2.1.3 文件夹下的src 文件夹复制到“lwIP 例程1 lwIP 裸机移植”工程Middlewares\lwip 路径下,如下图所示:

在这里插入图片描述


src目录

打开“lwIP 例程1 lwIP 裸机移植”工程添加Middlewares/lwip/src/api、Middlewares/lwip/src/core、Middlewares/lwip/src/netif 和Middlewares/lwip/arch 分组,下面我们分别地讲解这些分组需要添加哪些文件,如下所示:

(1) Middlewares/lwip/src/api 分组添加上图中的src/api 路径下的全部.c 文件,如下图所示:

在这里插入图片描述

图2.3.2.2 Middlewares/lwip/src/api 分组添加的文件

(2) Middlewares/lwip/src/core 分组添加图2.3.2.1 中的src/core 路径下除ipv6 文件夹的全部.c 文件,如下图所示:

在这里插入图片描述

图2.3.2.3 Middlewares/lwip/src/core 分组添加的文件

(3) Middlewares/lwip/src/netif 分组添加图2.3.2.1 中的src/netif 路径下的ethernet.c 文件,如下图所示:

在这里插入图片描述
图2.3.2.4 Middlewares/lwip/src/netif 分组添加的文件


arch目录

(4) Middlewares/lwip/arch 添加图2.3.2.1 中的arch 文件夹下的全部.c 文件,前面笔者没有在该文件夹下放置任何的文件,接下来笔者来讲解这个文件夹到底存放哪些文件。
arch 文件夹主要存放lwipopts.h、cc.h、ethernetif.c/h 和lwip_comm.c/h 六个文件,这些文件的作用如下表所示:

在这里插入图片描述

上表中的文件除了lwip_comm.c/h 以及ethernetif.h 文件之外都可以在“contrib-2.1.0”包下获取,这里笔者建议lwipopts.h 和cc.h 在ST 官方提供的lwIP 工程上获取,因为它们已经在ST 芯片上实现了lwIP 移植,所以我们可以把这两个文件拷贝到工程当中。ST 官方lwIP 对应MCU 的工程可在这个网址下载,该网址为:https://www.st.com/zh/embedded-software/stm32cubef4.html,在此网址上我们下载了HAL 版本的F4 工程包,下载完成之后就得到STM32Cube_FW_F4_V1.26.0 压缩包,这个压缩包已经包含了我们所需要的文件,例如在STM32Cube_FW_F4_V1.26.0\Middlewares\Third_Party\LwIP\system\arch\路径下获取cc.h 文件,在STM32Cube_FW_F4_V1.26.0\Projects\STM32469I_EVAL\Applications\LwIP\LwIP_TCP_Echo_Client\Inc 路径下获取lwipopts.h 文件,这些文件都是针对ST 芯片编写得来的。下面我们根据上述的地址把cc.h 和lwipopts.h 复制到本实验的arch 文件夹下,如下图所示:

在这里插入图片描述

ethernetif.c 网卡文件主要定义了底层驱动函数,这些函数在网卡中至关重要的,ethernetif.c 在contrib 包的contrib-2.1.0\examples\ethernetif 路径下获取,我们可以直接拿来使用,注意:这个网卡文件是一个通用的模板,该模板需要用户自己修改的,大家也可以参考STM32Cube_FW_F4_V1.26.0 压缩包下的ethernetif.c(裸机工程)文件,这里笔者使用正点原子已经编写好的ethernetif.c 网卡文件,最后在工程创建ethernetif.h 文件来声明ethernetif.c 中的函数并保存在arch 文件夹下,如下图所示:

在这里插入图片描述

上图中的ethernetif.h 文件源码如下所示:

#ifndef __ETHERNETIF_H__
#define __ETHERNETIF_H__
#include "lwip/err.h"
#include "lwip/netif.h"
err_t ethernetif_init(struct netif *netif); /* 网卡初始化函数*/
void ethernetif_input(struct netif *netif); /* 数据包输入函数*/
u32_t sys_now(void);
#endif

上述源码可知,ethernetif.h 文件只是声明了三个函数,这三个函数会在其他文件下被引用。

lwip_comm.c 和lwip_comm.h 是将lwIP 源码和前面的以太网驱动库结合起来的桥梁!这两个文件非常重要,它们是由正点原子提供,大家可复制正点原子已经写好例程的lwip_comm.c/.h 文件,复制到arch 文件夹当中,如下图所示:

在这里插入图片描述

图2.3.2.7 arch 文件夹添加lwip_comm.c/h 文件

至此,我们在工程Middlewares/lwip/arch 分组添加arch 文件夹下的全部.c 文件如下图所示:

在这里插入图片描述

图2.3.2.8 Middlewares/lwip/arch 分组添加的文件

(5) 添加lwIP 头文件路径,如下图所示:

在这里插入图片描述

图2.3.2.9 添加文件路径

注意:如果用户使用lwip/src/apps 路径下的文件,则必须添加…\Middlewares\lwip\src\include\lwip\apps 头文件路径。

(6) 修改工程目标名称
本教程是以标准例程-HAL 库版本的内存管理实验工程为基础工程,内存管理实验工程的工程名为“MALLOC”,为了规范工程,笔者建议将工程的目标名修改为“lwIP”或根据笔者的实际场景进行修改,修改如下图所示:

在这里插入图片描述

图2.3.2.10 修改工程目标名称

修改 arch 文件夹下的文件(网卡数据处理ethernetif.c、桥梁文件lwip_comm.c)

前面笔者已经在工程中添加多个分组来描述lwIP 源码并且设置lwIP 源码的文件路径,此时我们已经移植完成了,但是编译工程时会出现很多的错误,这些错误都是指向arch 文件夹下的文件,下面笔者分别地讲解这些文件到底如何修改。

(1) 修改lwipopts.h 文件
该文件是lwIP 的配置文件,如果用户需要启用DHCP,则在该文件把DHCP 宏定义设置为1,其他的宏定义也是一样的操作,lwipopts.h 文件源码如下:

#ifndef __LWIPOPTS_H__
#define __LWIPOPTS_H__
/**
SYS_LIGHTWEIGHT_PROT==1:如果您确实需要任务间保护
*/
#define SYS_LIGHTWEIGHT_PROT 0
/* NO_SYS 表示无操作系统模拟层,无操作系统为1,有操作系统设置为0
注意这个参数设置会编译不同*/
#define NO_SYS 1
/**
 * NO_SYS_NO_TIMERS==1: Drop support for sys_timeout when NO_SYS==1
 * Mainly for compatibility to old versions.
 */
#define NO_SYS_NO_TIMERS 0
/* ---------- 内存选项---------- */
/* 内存对齐,按照4 字节对齐*/
#define MEM_ALIGNMENT 4
/* 堆内存的大小,如果需要更大的堆内存,那么设置高一点*/
#define MEM_SIZE (30 * 1024)
/* MEMP_NUM_PBUF: 设置内存池的数量*/
#define MEMP_NUM_PBUF 25
/* MEMP_NUM_UDP_PCB: UDP协议控制块的数量. */
#define MEMP_NUM_UDP_PCB 4
/* MEMP_NUM_TCP_PCB: TCP的数量. */
#define MEMP_NUM_TCP_PCB 4
/* MEMP_NUM_TCP_PCB_LISTEN: 监听TCP的数量. */
#define MEMP_NUM_TCP_PCB_LISTEN 2
/* MEMP_NUM_TCP_SEG: 同时排队的TCP的数量段. */
#define MEMP_NUM_TCP_SEG 150
/* MEMP_NUM_SYS_TIMEOUT: 超时模拟活动的数量. */
#define MEMP_NUM_SYS_TIMEOUT 6
/* ---------- Pbuf选项---------- */
/* PBUF_POOL 内存池中每个内存块大小*/
#define PBUF_POOL_SIZE 20
/* PBUF_POOL_BUFSIZE: pbuf池中每个pbuf的大小. */
#define PBUF_POOL_BUFSIZE LWIP_MEM_ALIGN_SIZE(TCP_MSS + 40 + PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN)
/* ---------- TCP选项---------- */
#define LWIP_TCP 1
#define TCP_TTL 255
/* 控制TCP是否应该对到达的段进行排队
秩序。如果你的设备内存不足,定义为0. */
#define TCP_QUEUE_OOSEQ 0
/* TCP最大段大小*/
#define TCP_MSS (1500 - 40) /* TCP_MSS = (Ethernet MTU - IP header size - TCP header size) */
/* TCP发送者缓冲区空间(字节). */
#define TCP_SND_BUF (11 * TCP_MSS)
/* TCP_SND_QUEUELEN: TCP发送缓冲区空间。这必须是至少
需要(2 * TCP_SND_BUF/TCP_MSS)才能正常工作*/
#define TCP_SND_QUEUELEN (8 * TCP_SND_BUF / TCP_MSS)
/* TCP接收窗口*/
#define TCP_WND (20 * TCP_MSS)
/* ---------- ICMP 选项---------- */
#define LWIP_ICMP 1
/* ---------- DHCP 选项---------- */
/* 如果您希望DHCP配置为,请将LWIP_DHCP定义为1 */
#define LWIP_DHCP 1
/* ---------- UDP 选项---------- */
#define LWIP_UDP 1
#define UDP_TTL 255
/* ---------- Statistics 选项---------- */
#define LWIP_STATS 0
#define LWIP_PROVIDE_ERRNO 1
/* ---------- 链接回调选项---------- */
/* WIP_NETIF_LINK_CALLBACK==1:支持来自接口的回调函数
每当链接改变(例如,向下链接)
*/
#define LWIP_NETIF_LINK_CALLBACK 1
/*
--------------------------------------
---------- Checksum options ----------
--------------------------------------
*/
#define CHECKSUM_BY_HARDWARE
#ifdef CHECKSUM_BY_HARDWARE
#define CHECKSUM_GEN_IP 0
#define CHECKSUM_GEN_UDP 0
#define CHECKSUM_GEN_TCP 0
#define CHECKSUM_CHECK_IP 0
#define CHECKSUM_CHECK_UDP 0
#define CHECKSUM_CHECK_TCP 0
#define CHECKSUM_GEN_ICMP 0
#else
#define CHECKSUM_GEN_IP 1
#define CHECKSUM_GEN_UDP 1
#define CHECKSUM_GEN_TCP 1
#define CHECKSUM_CHECK_IP 1
#define CHECKSUM_CHECK_UDP 1
#define CHECKSUM_CHECK_TCP 1
#define CHECKSUM_GEN_ICMP 1
#endif
/*
----------------------------------------------
---------- Sequential layer options ----------
----------------------------------------------
*/
/**
 * LWIP_NETCONN==1:启用Netconn API(需要使用 api_lib.c )
 */
#define LWIP_NETCONN 0
/*
------------------------------------
---------- Socket options ----------
------------------------------------
*/
/**
 * LWIP_SOCKET==1:启用Socket API(要求使用Socket .c)
 */
#define LWIP_SOCKET 0
#endif /* __LWIPOPTS_H__ */

该文件非常重要,它是lwIP 内核裁剪和配置文件。

(2) 修改cc.h 文件
该文件主要配置lwIP 数据类型,cc.h 文件源码如下:

#ifndef __CC_H__
#define __CC_H__
#include <stdlib.h>
#include <stdio.h>
typedef int sys_prot_t;
#if defined(__GNUC__) & !defined(__CC_ARM)
#define LWIP_TIMEVAL_PRIVATE 0
#include <sys/time.h>
#endif
/* define compiler specific symbols */
#if defined(__ICCARM__)
#define PACK_STRUCT_BEGIN
#define PACK_STRUCT_STRUCT
#define PACK_STRUCT_END
#define PACK_STRUCT_FIELD(x) x
#define PACK_STRUCT_USE_INCLUDES
#elif defined(__GNUC__)
#define PACK_STRUCT_BEGIN
#define PACK_STRUCT_STRUCT __attribute__((__packed__))
#define PACK_STRUCT_END
#define PACK_STRUCT_FIELD(x) x
#elif defined(__CC_ARM)
#define PACK_STRUCT_BEGIN __packed
#define PACK_STRUCT_STRUCT
#define PACK_STRUCT_END
#define PACK_STRUCT_FIELD(x) x
#elif defined(__TASKING__)
#define PACK_STRUCT_BEGIN
#define PACK_STRUCT_STRUCT
#define PACK_STRUCT_END
#define PACK_STRUCT_FIELD(x) x
#endif
#define LWIP_PLATFORM_ASSERT(x) do
{
    printf("Assertion \"%s\" failed at line %d in %s\n",
           x, __LINE__, __FILE__);
}
while (0)
/* Define random number generator function */
#define LWIP_RAND() ((u32_t)rand())
#endif /* __CC_H__ */

此文件没什么好讲解的,笔者在原文件删除了两处地方,这两处地方分别为“#include “cpu.h””和“#define LWIP_PROVIDE_ERRNO”,因为F4 没有cpu.h 文件以及LWIP_PROVIDE_ERRNO 宏定义已经在lwipopts.h 文件中定义了,这里我们无需重复定义。

(3) ethernetif.c 文件
网卡数据发送接收处理文件,用来描述硬件网卡的收发函数及相关信息。该文件定义的函数如下表所示:

在这里插入图片描述

从上表可知,ethernetif.c 一共定义了六个函数,下面笔者分别地讲解这些函数的作用。

ethernetif_init 函数
该函数主要对网卡描述符初始化,该函数如下所示:

err_t ethernetif_init(struct netif *netif)
{
    struct ethernetif *ethernetif;
    LWIP_ASSERT("netif != NULL", (netif != NULL));
    ethernetif = mem_malloc(sizeof(struct ethernetif));
    if (ethernetif == NULL)
    {
        LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_init: out of memory\n"));
        return ERR_MEM;
    }
#if LWIP_NETIF_HOSTNAME
    /* Initialize interface hostname */
    netif->hostname = "lwip";
#endif /* LWIP_NETIF_HOSTNAME */
    /*
     * Initialize the snmp variables and counters inside the struct netif.
     * The last argument should be replaced with your link speed, in units
     * of bits per second.
  	 初始化结构netif中的snmp变量和计数器。最后一个参数应该替换为您的链接速度,单位为每秒比特数。
     */
    MIB2_INIT_NETIF(netif, snmp_ifType_ethernet_csmacd,
                    LINK_SPEED_OF_YOUR_NETIF_IN_BPS);
    netif->state = ethernetif;
    netif->name[0] = IFNAME0;
    netif->name[1] = IFNAME1;
/* We directly use etharp_output() here to save a function call.
 * You can instead declare your own function an call etharp_output()
 * from it if you have to do some checks before sending (e.g. if link
 * is available...) 
我们在这里直接使用 etharp_output()来保存一个函数调用。
如果在发送之前必须进行一些检查(例如,如果链接可用…),则可以从中声明自己的函数为调用etharp_output()
*/
#if LWIP_IPV4
    netif->output = etharp_output;
#endif /* LWIP_IPV4 */
#if LWIP_IPV6
    netif->output_ip6 = ethip6_output;
#endif /* LWIP_IPV6 */
    netif->linkoutput = low_level_output;
    ethernetif->ethaddr = (struct eth_addr *)&(netif->hwaddr[0]);
    /* initialize the hardware 
    初始化硬件*/
    low_level_init(netif);
    return ERR_OK;
}

netif 结构体,它是对网卡进行抽象,例如描述网卡的状态,收发函数和网络参数等。

low_level_init 函数
该函数用来初始化MAC 地址和初始化ETH 的DMA 描述符,该函数如下所示:

static void
low_level_init(struct netif *netif)
{
    netif->hwaddr_len = ETHARP_HWADDR_LEN; /*设置MAC地址长度,为6个字节*/
    /*初始化MAC地址,设置什么地址由用户自己设置,但是不能与网络中其他设备MAC地址重复*/
    netif->hwaddr[0] = g_lwipdev.mac[0];
    netif->hwaddr[1] = g_lwipdev.mac[1];
    netif->hwaddr[2] = g_lwipdev.mac[2];
    netif->hwaddr[3] = g_lwipdev.mac[3];
    netif->hwaddr[4] = g_lwipdev.mac[4];
    netif->hwaddr[5] = g_lwipdev.mac[5];
    netif->mtu = 1500; /* 最大允许传输单元,允许该网卡广播和ARP功能*/
    /* 网卡状态信息标志位,是很重要的控制字段,它包括网卡功能使能、广播*/
    /* 使能、ARP 使能等等重要控制位*/ /*广播ARP协议链接检测*/
    netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP;
    /*初始化发送描述符*/
    HAL_ETH_DMATxDescListInit(&g_eth_handler, g_eth_dma_tx_dscr_tab,
                              g_eth_tx_buf, ETH_TXBUFNB);
    /*初始化接收描述符*/
    HAL_ETH_DMARxDescListInit(&g_eth_handler, g_eth_dma_rx_dscr_tab,
                              g_eth_rx_buf, ETH_RXBUFNB);
    HAL_ETH_Start(&g_eth_handler); /*开启MAC和DMA*/
}

此函数根据g_lwipdev.mac 数组来设置MAC 地址,这个数组笔者会在lwip_comm.c 下讲解,接着设置lwIP 最大允许传输单元为1500 字节,最后调用函数HAL_ETH_DMATxDescListInit 和HAL_ETH_DMARxDescListInit 初始化以太网DMA 描述符。

low_level_output 函数

static err_t
low_level_output(struct netif *netif, struct pbuf *p)
{
    err_t errval;
    struct pbuf *q;
    uint8_t *buffer = (uint8_t *)(g_eth_handler.TxDesc->Buffer1Addr);
    __IO ETH_DMADescTypeDef *DmaTxDesc;
    uint32_t framelength = 0;
    uint32_t bufferoffset = 0;
    uint32_t byteslefttocopy = 0;
    uint32_t payloadoffset = 0;
    DmaTxDesc = g_eth_handler.TxDesc;
    bufferoffset = 0;
#if ETH_PAD_SIZE
    pbuf_remove_header(p, ETH_PAD_SIZE); /* drop the padding word */
#endif
    /* 从pbuf中拷贝要发送的数据*/
    for (q = p; q != NULL; q = q->next)
    {
        /* 判断此发送描述符是否有效,即判断此发送描述符是否归以太网DMA所有*/
        if ((DmaTxDesc->Status & ETH_DMATXDESC_OWN) != (uint32_t)RESET)
        {
            errval = ERR_USE;
            goto error; /* 发送描述符无效,不可用*/
        }
        byteslefttocopy = q->len; /* 要发送的数据长度*/
        payloadoffset = 0;
        /* 将pbuf中要发送的数据写入到以太网发送描述符中,
        有时候我们要发送的数据可能大于一个以太网
        描述符的Tx Buffer,因此我们需要分多次将数据拷贝到多个发送描述符中*/
        while ((byteslefttocopy + bufferoffset) > ETH_TX_BUF_SIZE)
        {
            /* 将数据拷贝到以太网发送描述符的Tx Buffer中*/
            memcpy((uint8_t *)((uint8_t *)buffer + bufferoffset),
                   (uint8_t *)((uint8_t *)q->payload + payloadoffset),
                   (ETH_TX_BUF_SIZE - bufferoffset));
            /* DmaTxDsc指向下一个发送描述符*/
            DmaTxDesc = (ETH_DMADescTypeDef *)(DmaTxDesc->Buffer2NextDescAddr);
            /* 检查新的发送描述符是否有效*/
            if ((DmaTxDesc->Status & ETH_DMATXDESC_OWN) != (uint32_t)RESET)
            {
                errval = ERR_USE;
                goto error; /* 发送描述符无效,不可用*/
            }
            /* 更新buffer地址,指向新的发送描述符的Tx Buffer */
            buffer = (uint8_t *)(DmaTxDesc->Buffer1Addr);
            byteslefttocopy = byteslefttocopy - (ETH_TX_BUF_SIZE - bufferoffset);
            payloadoffset = payloadoffset + (ETH_TX_BUF_SIZE - bufferoffset);
            framelength = framelength + (ETH_TX_BUF_SIZE - bufferoffset);
            bufferoffset = 0;
        }
        /* 拷贝剩余的数据*/
        memcpy((uint8_t *)((uint8_t *)buffer + bufferoffset),
               (uint8_t *)((uint8_t *)q->payload + payloadoffset),
               byteslefttocopy);
        bufferoffset = bufferoffset + byteslefttocopy;
        framelength = framelength + byteslefttocopy;
    }
    /* 当所有要发送的数据都放进发送描述符的 Tx Buffer 以后就可发送此帧了*/
    HAL_ETH_TransmitFrame(&g_eth_handler, framelength);
    errval = ERR_OK;
error:
    /* 发送缓冲区发生下溢,一旦发送缓冲区发生下溢TxDMA会进入挂起状态*/
    if ((g_eth_handler.Instance->DMASR & ETH_DMASR_TUS) != (uint32_t)RESET)
    {
        /* 清除下溢标志*/
        g_eth_handler.Instance->DMASR = ETH_DMASR_TUS;
        /* 当发送帧中出现下溢错误的时候TxDMA会挂起,这时候需要向DMATPDR寄存器*/
        /* 随便写入一个值来将其唤醒,此处我们写0 */
        g_eth_handler.Instance->DMATPDR = 0;
    }
#if ETH_PAD_SIZE
    pbuf_add_header(p, ETH_PAD_SIZE); /* reclaim the padding word */
#endif
    return errval;
}

此函数非常简单,该函数是把数据包拷贝到以太网发送描述符管理的 Buffer 当中,当所有要发送的数据都放进发送描述符管理的Buffer 以后就可调用函数 HAL_ETH_TransmitFrame 发送此帧了。

ethernetif_input 函数
该函数用来获取以太网数据包,本章的例程是把该函数放在ETH 中断当中,以后操作系统移植lwIP 时会把该函数当成一个任务函数来处理,该函数如下所示:

void ethernetif_input(struct netif *netif)
{
    struct pbuf *p;
    /* move received packet into a new pbuf */
    p = low_level_input(netif);
    /* if no packet could be read, silently ignore this */
    if (p != NULL)
    {
        if (netif->input(p, netif) != ERR_OK)
        {
            LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_input: IP input error\n"));
            pbuf_free(p);
            p = NULL;
        }
    }
}

此函数非常简单,调用函数low_level_input 获取数据包并调用netif->input 函数往上传输。

low_level_input 函数
该函数用来获取以太网帧的数据并保存在pbuf 当中,pbuf 的知识点笔者在第七章中讲解,该函数如下所示:

static struct pbuf *
low_level_input(struct netif *netif)
{
    struct pbuf *p, *q;
    u16_t len;
    uint8_t *buffer;
    __IO ETH_DMADescTypeDef *dmarxdesc;
    uint32_t bufferoffset = 0;
    uint32_t payloadoffset = 0;
    uint32_t byteslefttocopy = 0;
    uint32_t i = 0;
    if (HAL_ETH_GetReceivedFrame(&g_eth_handler) != HAL_OK) /*判断是否接收到数据*/
        return NULL;
    len = g_eth_handler.RxFrameInfos.length; /*获取接收到的以太网帧长度*/
#if ETH_PAD_SIZE
    len += ETH_PAD_SIZE; /* allow room for Ethernet padding */
#endif
    /*获取接收到的以太网帧的数据buffer*/
    buffer = (uint8_t *)g_eth_handler.RxFrameInfos.buffer;
    p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL); /* 申请pbuf */
    if (p != NULL)                            /* pbuf申请成功*/
    {
        /* 获取接收描述符链表中的第一个描述符*/
        dmarxdesc = g_eth_handler.RxFrameInfos.FSRxDesc;
        bufferoffset = 0;
        for (q = p; q != NULL; q = q->next)
        {
            byteslefttocopy = q->len;
            payloadoffset = 0;
            /* 将接收描述符中Rx Buffer的数据拷贝到pbuf中*/
            while ((byteslefttocopy + bufferoffset) > ETH_RX_BUF_SIZE)
            {
                /* 将数据拷贝到pbuf中*/
                memcpy((uint8_t *)((uint8_t *)q->payload + payloadoffset),
                       (uint8_t *)((uint8_t *)buffer + bufferoffset),
                       (ETH_RX_BUF_SIZE - bufferoffset));
                /* dmarxdesc向下一个接收描述符*/
                dmarxdesc = (ETH_DMADescTypeDef *)(dmarxdesc->Buffer2NextDescAddr);
                /* 更新buffer地址,指向新的接收描述符的Rx Buffer */
                buffer = (uint8_t *)(dmarxdesc->Buffer1Addr);
                byteslefttocopy = byteslefttocopy – (ETH_RX_BUF_SIZE - bufferoffset);
                payloadoffset = payloadoffset +
                                (ETH_RX_BUF_SIZE - bufferoffset);
                bufferoffset = 0;
            }
            /* 拷贝剩余的数据*/
            memcpy((uint8_t *)((uint8_t *)q->payload + payloadoffset),
                   (uint8_t *)((uint8_t *)buffer + bufferoffset),
                   byteslefttocopy);
            bufferoffset = bufferoffset + byteslefttocopy;
        }
    }
    else
    {
        /* drop packet(); 丢包函数自行编写*/
        LINK_STATS_INC(link.memerr);
        LINK_STATS_INC(link.drop);
        MIB2_STATS_NETIF_INC(netif, ifindiscards);
    }
    /* 释放DMA描述符*/
    dmarxdesc = g_eth_handler.RxFrameInfos.FSRxDesc;
    for (i = 0; i < g_eth_handler.RxFrameInfos.SegCount; i++)
    {
        dmarxdesc->Status |= ETH_DMARXDESC_OWN; /* 标记描述符归DMA所有*/
        dmarxdesc = (ETH_DMADescTypeDef *)(dmarxdesc->Buffer2NextDescAddr);
    }
    g_eth_handler.RxFrameInfos.SegCount = 0; /* 清除段计数器*/
    /* 接收缓冲区不可用*/
    if ((g_eth_handler.Instance->DMASR & ETH_DMASR_RBUS) != (uint32_t)RESET)
    {
        /* 清除接收缓冲区不可用标志*/
        g_eth_handler.Instance->DMASR = ETH_DMASR_RBUS;
        /* 当接收缓冲区不可用的时候RxDMA会进去挂起状态,
        通过向DMARPDR写入任意一个值来唤醒 Rx DMA */
        g_eth_handler.Instance->DMARPDR = 0;
    }
    return p;
}

此函数非常简单,用来获取以太网发送描述符管理的Buffer 中的数据,接着将数据拷贝到pbuf 当中,最后返回pbuf 数据包。

sys_now 函数
该函数主要提供lwIP 时基,该函数如下所示:

u32_t sys_now(void)
{
	return HAL_GetTick();
}

此函数调用HAL_GetTick 获取HAL 的节拍来提供lwIP 时基。

(4) ethernetif.h 文件
该文件非常简单,主要声明ethernetif_init、ethernetif_input 和sys_now 函数给外部文件使用,该文件源码如下所示:

#ifndef __ETHERNETIF_H__
#define __ETHERNETIF_H__
#include "lwip/err.h"
#include "lwip/netif.h"
err_t ethernetif_init(struct netif *netif); /* 网卡初始化函数*/
void ethernetif_input(struct netif *netif); /* 数据包输入函数*/
u32_t sys_now(void);
#endif

(5) lwip_comm.h 文件
该文件主要声明一个结构体__lwip_dev,该结构体主要保存网络相关的信息,如MAC 地址、本地IP、远程IP 等信息,该文件源码如下所示:

#ifndef _LWIP_COMM_H
#define _LWIP_COMM_H
#include "./BSP/ETHERNET/ethernet.h"
#define LWIP_MAX_DHCP_TRIES 4 /* DHCP服务器最大重试次数*/
/*lwip控制结构体*/
typedef struct
{
    uint8_t mac[6];      /* MAC地址*/
    uint8_t remoteip[4]; /* 远端主机IP地址*/
    uint8_t ip[4];       /* 本机IP地址*/
    uint8_t netmask[4];  /* 子网掩码*/
    uint8_t gateway[4];  /* 默认网关的IP地址*/
    uint8_t dhcpstatus;  /* dhcp状态*/
    /* 0, 未获取DHCP地址;*/
    /* 1, 进入DHCP获取状态*/
    /* 2, 成功获取DHCP地址*/
    /* 0XFF,获取失败*/
} __lwip_dev;
extern __lwip_dev lwipdev;                        /* lwip控制结构体*/
void lwip_packet_handler(void);                   /* 当接收到数据后调用*/
void lwip_periodic_handle(void);                  /* lwip_periodic_handle */
void lwip_comm_default_ip_set(__lwip_dev *lwipx); /* lwip 默认IP设置*/
/* 内存堆ram_heap 和内存池memp_memory 的内存分配*/
uint8_t lwip_comm_mem_malloc(void);
void lwip_comm_mem_free(void); /* lwip中mem和memp内存释放*/
/* lwIP初始化(lwIP启动的时候使用) */
uint8_t lwip_comm_init(void);
void lwip_dhcp_process_handle(void); /* DHCP处理任务*/
#endif

该文件没什么好讲解的,主要声明一些函数。

(6) lwip_comm.c 文件
该文件是将lwIP 源码和前面的以太网驱动库结合起来的桥梁!该文件主要编写了五个函数,下面我们分别来讲解这五个函数的作用,如下所示:

lwip_comm_init 函数
该函数主要对以太网的IO 初始化以及添加在网卡列表中添加一个网口,该函数如下所示:

uint8_t lwip_comm_init(void)
{
    uint8_t retry = 0;
    /* 调用netif_add()函数时的返回值,用于判断网络初始化是否成功*/
    struct netif *netif_init_flag;
    ip_addr_t ipaddr;  /* ip地址*/
    ip_addr_t netmask; /* 子网掩码*/
    ip_addr_t gw;      /* 默认网关*/
    if (ethernet_mem_malloc())
        return 1;                       /* 内存申请失败*/
    lwip_comm_default_ip_set(&lwipdev); /* 设置默认IP等信息*/
    while (ethernet_init())             /* 初始化以太网芯片,如果失败的话就重试5次*/
    {
        retry++;
        if (retry > 5)
        {
            retry = 0; /* 以太网芯片初始化失败*/
            return 3;
        }
    }
    lwip_init(); /* 初始化LWIP内核*/
#if LWIP_DHCP    /* 使用动态IP */
    ipaddr.addr = 0;
    netmask.addr = 0;
    gw.addr = 0;
#else  /* 使用静态IP */
    IP4_ADDR(&ipaddr, lwipdev.ip[0], lwipdev.ip[1],
             lwipdev.ip[2], lwipdev.ip[3]);
    IP4_ADDR(&netmask, lwipdev.netmask[0], lwipdev.netmask[1],
             lwipdev.netmask[2], lwipdev.netmask[3]);
    IP4_ADDR(&gw, lwipdev.gateway[0], lwipdev.gateway[1],
             lwipdev.gateway[2], lwipdev.gateway[3]);
/* 省略代码………………………… */
#endif /* 向网卡列表中添加一个网口*/
    netif_init_flag = netif_add(&lwip_netif, (const ip_addr_t *)&ipaddr,
                                (const ip_addr_t *)&netmask,
                                (const ip_addr_t *)&gw, NULL,
                                &ethernetif_init, &ethernet_input);
    if (netif_init_flag == NULL)
    {
        return 4; /* 网卡添加失败*/
    }
    else /* 网口添加成功后,设置netif为默认值,并且打开netif网口*/
    {
        netif_set_default(&lwip_netif); /* 设置netif为默认网口*/
        if (netif_is_link_up(&lwip_netif))
        {
            netif_set_up(&lwip_netif); /* 打开netif网口*/
        }
        else
        {
            netif_set_down(&lwip_netif);
        }
    }
#if LWIP_DHCP                /* 如果使用DHCP的话*/
    lwipdev.dhcpstatus = 0;  /* DHCP标记为0 */
    dhcp_start(&lwip_netif); /* 开启DHCP服务*/
#endif
    return 0; /* 操作OK. */
}

此函数调用ethernet_mem_malloc 申请DMA 描述符的内存,这个函数前面讲解过,这里我们无需重复讲解,其次我们调用lwip_comm_default_ip_set 函数设置网络参数,然后调用函数ethernet_init 初始化以太网IO 并且初始化lwIP 内核,最后我们调用函数netif_add 向网卡列表添加一个网口,该函数比较特殊,它的第二到第四个形参传入本地IP 地址、子网掩码以及网关,而第五和第六形参需要用户传入ethernetif_init 以及ethernet_input 函数地址。

lwip_comm_default_ip_set 函数
此函数非常简单,主要设置网络的信息,该函数如下所示:

void lwip_comm_default_ip_set(__lwip_dev *lwipx)
{
    /* 默认远端IP为:192.168.1.134 */
    lwipx->remoteip[0] = 192;
    lwipx->remoteip[1] = 168;
    lwipx->remoteip[2] = 1;
    lwipx->remoteip[3] = 27;
    /* MAC地址设置(高三字节固定为:2.0.0,低三字节用STM32唯一ID) */
    lwipx->mac[0] = 0xB8;
    lwipx->mac[1] = 0xAE;
    lwipx->mac[2] = 0x1D;
    lwipx->mac[3] = 0x00; /* 低三字节用STM32的唯一ID 28.0.36 */
    lwipx->mac[4] = 0x07;
    lwipx->mac[5] = 0x00;
    /* 默认本地IP为:192.168.1.30 */
    lwipx->ip[0] = 192;
    lwipx->ip[1] = 168;
    lwipx->ip[2] = 1;
    lwipx->ip[3] = 30;
    /* 默认子网掩码:255.255.255.0 */
    lwipx->netmask[0] = 255;
    lwipx->netmask[1] = 255;
    lwipx->netmask[2] = 255;
    lwipx->netmask[3] = 0;
    /* 默认网关:192.168.1.1 */
    lwipx->gateway[0] = 192;
    lwipx->gateway[1] = 168;
    lwipx->gateway[2] = 1;
    lwipx->gateway[3] = 1;
    lwipx->dhcpstatus = 0; /* 没有DHCP */
}

注意:这里是探索者与DMF407 开发板第三处区别即MAC 地址的数值。

lwip_pkt_handle 函数
这个函数间接调用ethernetif_input 函数,该函数在裸机移植时,一般放在ETH 中断当中。

lwip_periodic_handle 函数
该函数与DHCP 相关,如果工程不启用DHCP,则系统只调用sys_check_timeouts 检测超时;如果工程启用DHCP,则系统会根据DHCP 响应时间内来获取动态IP 地址等网络数据。
该函数如下所示:

void lwip_periodic_handle()
{
    sys_check_timeouts();
#if LWIP_DHCP /* 如果使用DHCP */
    /* 每500ms调用一次dhcp_fine_tmr() */
    /* DHCP_FINE_TIMER_MSECS(500)单位*/
    if (sys_now() - DHCPfineTimer >= DHCP_FINE_TIMER_MSECS)
    {
        DHCPfineTimer = sys_now();
        dhcp_fine_tmr();
        if ((lwipdev.dhcpstatus != 2) && (lwipdev.dhcpstatus != 0XFF))
        {
            lwip_dhcp_process_handle(); /* DHCP处理*/
        }
    }
    /* 每60s执行一次DHCP粗糙处理*/
    if (sys_now() - DHCPcoarseTimer >= DHCP_COARSE_TIMER_MSECS)
    {
        DHCPcoarseTimer = sys_now();
        dhcp_coarse_tmr();
    }
#endif
}

lwip_dhcp_process_handle 函数
该函数与DHCP 相关,如果工程不启用DHCP,则默认使用lwip_comm_default_ip_set 函数的网络信息;如果工程启用DHCP,则获取DHCP 服务器相应的网络信息,该函数如下所示:

/* 如果使能DHCP */
#if LWIP_DHCP
struct dhcp gdhcp;
/**
 * @breif DHCP处理任务
 * @param 无
 * @retval 无
 */
void lwip_dhcp_process_handle(void)
{
    uint32_t ip = 0, netmask = 0, gw = 0;
    switch (lwipdev.dhcpstatus)
    {
    case 0: /* 开启DHCP */
        dhcp_start(&lwip_netif);
        lwipdev.dhcpstatus = 1; /* 等待通过DHCP获取到的地址*/
        printf("正在查找DHCP服务器,请稍等...........\r\n");
        break;
    case 1: /* 等待获取到IP地址*/
    {
        ip = lwip_netif.ip_addr.addr;      /* 读取新IP地址*/
        netmask = lwip_netif.netmask.addr; /* 读取子网掩码*/
        gw = lwip_netif.gw.addr;           /* 读取默认网关*/
        if (ip != 0)                       /*正确获取到IP地址的时候*/
        {
            lwipdev.dhcpstatus = 2; /* DHCP成功*/
            /* 解析出通过DHCP获取到的IP地址*/
            lwipdev.ip[3] = (uint8_t)(ip >> 24);
            lwipdev.ip[2] = (uint8_t)(ip >> 16);
            lwipdev.ip[1] = (uint8_t)(ip >> 8);
            lwipdev.ip[0] = (uint8_t)(ip);
            /* 解析通过DHCP获取到的子网掩码地址*/
            lwipdev.netmask[3] = (uint8_t)(netmask >> 24);
            lwipdev.netmask[2] = (uint8_t)(netmask >> 16);
            lwipdev.netmask[1] = (uint8_t)(netmask >> 8);
            lwipdev.netmask[0] = (uint8_t)(netmask);
        } /* 通过DHCP服务获取IP地址失败,且超过最大尝试次数*/
        else if (gdhcp.tries > LWIP_MAX_DHCP_TRIES)
        {
            lwipdev.dhcpstatus = 0XFF; /* DHCP超时失败. */
            /* 使用静态IP地址*/
            IP4_ADDR(&(lwip_netif.ip_addr), lwipdev.ip[0],
                     lwipdev.ip[1],
                     lwipdev.ip[2],
                     lwipdev.ip[3]);
            IP4_ADDR(&(lwip_netif.netmask), lwipdev.netmask[0],
                     lwipdev.netmask[1],
                     lwipdev.netmask[2],
                     lwipdev.netmask[3]);
            IP4_ADDR(&(lwip_netif.gw), lwipdev.gateway[0],
                     lwipdev.gateway[1],
                     lwipdev.gateway[2],
                     lwipdev.gateway[3]);
        }
    }
    break;
    default:
        break;
    }
}
#endif

至此,我们已经讲解完了arch 文件夹下的文件内容了,如有不解,请大家观看正点原子lwIP 视频。

测试程序ping通ip

到了这里我们编译工程应该不会出现错误了(如果程序出现空间不足,则把malloc.h 文件中的MEM1_MAX_SIZE 修改小一点),下面笔者在main 函数编写测试代码,然后在PC 机上进入cmd 命令行,在此页面上ping 一下开发板的 IP 地址,如果系统能 ping 通,则证明lwIP移植成功。

(7) main 函数

int main(void)
{
    uint8_t t = 0;
    
    /* 一系列初始化*/
    while (lwip_comm_init() != 0)
    {
        lcd_show_string(6, 110, 200, 16, 16, "lwIP Init failed!!", BLUE);
        delay_ms(500);
        lcd_fill(6, 50, 200 + 30, 50 + 16, WHITE);
        lcd_show_string(6, 110, 200, 16, 16, "Retrying... ", BLUE);
        delay_ms(500);
        LED1_TOGGLE();
    }
    while (!ethernet_read_phy(PHY_SR)) /* 检查MCU与PHY芯片是否通信成功*/
    {
        printf("MCU与PHY芯片通信失败,请检查电路或者源码!!!!\r\n");
    }
#if LWIP_DHCP
    /* 等待DHCP获取成功/超时溢出*/
    while ((lwipdev.dhcpstatus != 2) && (lwipdev.dhcpstatus != 0XFF))
    {
        lwip_periodic_handle();
    }
#endif
    while (1)
    {
        lwip_periodic_handle(); /* LWIP轮询任务*/
        delay_ms(2);
        t++;
        if (t >= 200)
        {
            t = 0;
            LED0_TOGGLE();
        }
    }
}

此函数调用lwip_comm_init 函数初始化以太网IO 以及网卡驱动,然后我们调用函数lwip_periodic_handle 获取DHCP 分配的动态IP 地址,最后在while()不断的遍历lwip_periodic_hand
le 函数。

编译工程并下载到开发板,打开串口调式助手查看动态IP 地址,如下图所示:

在这里插入图片描述

在PC 机上按下“win+r”快捷键并输入cmd 进入命令行,在该命令行上输入“ping 192.168.1.39”命令,如果命令行弹出“字节=32 时间<1ms TTL=255”字符串,则系统能与开发板通讯,证明该工程移植成功,下面我们使用命令来ping 一下图2.2.3.1 的IP地址,如下图所示:

在这里插入图片描述

图2.3.3.2 ping IP 地址成功
该实验的实验工程,请参考《lwIP 例程1 lwIP 裸机移植》。

lwIP 不带操作系统启动流程图总结

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

lwIP 带操作系统移植

本章为大家讲解带操作系统的lwIP 移植,操作系统使用的是FreeRTOS,以后的例程中都是携带操作系统的lwIP 例程,至于FreeRTOS 的移植请参考正点原子《FreeRTOS 开发指南》教程,本章的移植例程是在上一章的移植例程上进行。在操作系统的支持下我们可以使用lwIP 提供的另外两种API 编程。没有操作的时候我们只能使用RAW 编程,相较于其他两种API,RAW 编程难度很大,需要用户对lwIP 协议栈有一定的了解。使用操作系统以后我们可以多任务运行,将lwIP 作为任务来运行。

lwIP 前期准备

本章我们使用第二章的例程来作为本章的移植工程并把它命名为《lwIP 例程6lwIP_FreeRTOS 移植》,关于FreeRTOS 操作系统的移植,请大家参考正点原子的《FreeRTOS开发指南》,这里笔者不会讲解FreeRTOS 移植过程。

lwIP 移植流程(探索者、DMF407)

修改lwipopts.h

这个文件是lwIP 的配置文件,在第二章例程的lwipopts.h 文件是针对lwIP 无操作系统下的配置文件,这里我们在STM32Cube_FW_F4_V1.26.0\Projects\STM32469I_EVAL\Applications\LwIP\LwIP_HTTP_Server_Netconn_RTOS\Inc\路径下的lwipopts.h 替换本实验的lwipopts.h 文件,新的lwipopts.h 文件源码如下所示:

#ifndef __LWIPOPTS_H__
#define __LWIPOPTS_H__
/* NO_SYS 表示无操作系统模拟层,无操作系统为1,有操作系统设置为0
注意这个参数设置会编译不同*/
#define NO_SYS 0
/* ---------- 内存选项---------- */
/* 内存对齐,按照4 字节对齐*/
#define MEM_ALIGNMENT 4
/* 堆内存的大小,如果需要更大的堆内存,那么设置高一点*/
#define MEM_SIZE (10 * 1024)
/* MEMP_NUM_PBUF: 设置内存池的数量*/
#define MEMP_NUM_PBUF 10
/* MEMP_NUM_UDP_PCB: UDP协议控制块的数量. */
#define MEMP_NUM_UDP_PCB 6
/* MEMP_NUM_TCP_PCB: TCP的数量. */
#define MEMP_NUM_TCP_PCB 10
/* MEMP_NUM_TCP_PCB_LISTEN: 监听TCP的数量. */
#define MEMP_NUM_TCP_PCB_LISTEN 5
/* MEMP_NUM_TCP_SEG: 同时排队的TCP的数量段. */
#define MEMP_NUM_TCP_SEG 8
/* MEMP_NUM_SYS_TIMEOUT: 超时模拟活动的数量. */
#define MEMP_NUM_SYS_TIMEOUT 10
/* ---------- Pbuf选项---------- */
/* PBUF_POOL 内存池中每个内存块大小*/
#define PBUF_POOL_SIZE 8
/* PBUF_POOL_BUFSIZE: pbuf池中每个pbuf的大小. */
#define PBUF_POOL_BUFSIZE 1524
/* ---------- TCP选项---------- */
#define LWIP_TCP 1
#define TCP_TTL 255
/* 控制TCP是否应该对到达的段进行排队
秩序。如果你的设备内存不足,定义为0. */
#define TCP_QUEUE_OOSEQ 0
/* TCP 最大段大小*/ /* TCP_MSS = (Ethernet MTU - IP header size - TCP header size) */
#define TCP_MSS (1500 - 40)
/* TCP发送者缓冲区空间(字节). */
#define TCP_SND_BUF (4 * TCP_MSS)
/* TCP_SND_QUEUELEN: TCP发送缓冲区空间。这必须是至少
需要(2 * TCP_SND_BUF/TCP_MSS)才能正常工作*/
#define TCP_SND_QUEUELEN (2 * TCP_SND_BUF / TCP_MSS)
/* TCP接收窗口*/
#define TCP_WND (2 * TCP_MSS)
/* ---------- ICMP 选项---------- */
#define LWIP_ICMP 1
/* ---------- DHCP 选项---------- */
/* 如果您希望DHCP配置为,请将LWIP_DHCP定义为1 */
#define LWIP_DHCP 1
/* ---------- UDP 选项---------- */
#define LWIP_UDP 1
#define UDP_TTL 255
/* ---------- Statistics 选项---------- */
#define LWIP_STATS 0
/* ---------- 链接回调选项---------- */
/* WIP_NETIF_LINK_CALLBACK==1:支持来自接口的回调函数
每当链接改变(例如,向下链接)
*/
#define LWIP_NETIF_LINK_CALLBACK 1
/*
--------------------------------------
---------- 帧校验和选项----------
--------------------------------------
*/
/*
The STM32F4x7 allows computing and verifying the IP, UDP, TCP and ICMP checksums by hardware:
- To use this feature let the following define uncommented.
- To disable it and process by CPU comment the the checksum.
*/
#define CHECKSUM_BY_HARDWARE
#ifdef CHECKSUM_BY_HARDWARE
#define CHECKSUM_GEN_IP 0
#define CHECKSUM_GEN_UDP 0
#define CHECKSUM_GEN_TCP 0
#define CHECKSUM_CHECK_IP 0
#define CHECKSUM_CHECK_UDP 0
#define CHECKSUM_CHECK_TCP 0
#define CHECKSUM_GEN_ICMP 0
#else
#define CHECKSUM_GEN_IP 1
#define CHECKSUM_GEN_UDP 1
#define CHECKSUM_GEN_TCP 1
#define CHECKSUM_CHECK_IP 1
#define CHECKSUM_CHECK_UDP 1
#define CHECKSUM_CHECK_TCP 1
#define CHECKSUM_GEN_ICMP 1
#endif
/*
----------------------------------------------
---------- 连续层的选择----------
----------------------------------------------
*/
/**
 * LWIP_NETCONN==1:启用Netconn API(需要使用api_lib.c)
 */
#define LWIP_NETCONN 1
/*
------------------------------------
---------- Socket选项----------
------------------------------------
*/
/**
 * LWIP_SOCKET==1:启用Socket API(要求使用Socket .c)
 */
#define LWIP_SOCKET 0
/*
------------------------------------
---------- httpd options ----------
------------------------------------
*/
/** Set this to 1 to include "fsdata_custom.c" instead of "fsdata.c" for the
 * file system (to prevent changing the file included in CVS) */
#define HTTPD_USE_CUSTOM_FSDATA 1
/*
---------------------------------
---------- 操作系统选项----------
---------------------------------
*/
#define TCPIP_THREAD_NAME " TCP/IP"
#define TCPIP_THREAD_STACKSIZE 1000
#define TCPIP_MBOX_SIZE 6
#define DEFAULT_UDP_RECVMBOX_SIZE 6
#define DEFAULT_TCP_RECVMBOX_SIZE 6
#define DEFAULT_ACCEPTMBOX_SIZE 6
#define DEFAULT_THREAD_STACKSIZE 500
#define TCPIP_THREAD_PRIO 5
#endif /* __LWIPOPTS_H__ */

笔者对新的lwipopts.h 文件做了中文注释以及配置项的数值修改,例如修改了TCPIP_THREAD_PRIO 配置项为5,这个配置项是设置协议栈任务的优先级,当然大家也可以使用正点原子《lwIP 例程5 lwIP_FreeRTOS 移植》例程中的lwipopts.h 文件,该文件的内容是参考上述的源码改编得来的。

添加sys_arch.c/h 文件

如果lwIP 使用操作系统的话,sys_arch.c/h 这两个文件是有必要了解的,该些文件是lwIP内核与操作系统交互的接口文件,也可以说,它们是lwIP 与操作系统的交接文件,本章的实验是使用FreeRTOS 操作系统,显然sys_arch.c 文件包含的邮箱、信号量以及互斥锁等IPC 策略都是由FreeRTOS 操作系统提供的,注意:FreeRTOS 操作系统没有邮箱的概念,可使用消息队列替代邮箱机制。

打开“contrib-2.1.0\ports\freertos\路径下的文件夹,该文件夹包含了与FreeRTOS 相关的sys_arch.c/.h 文件,笔者把这两个文件复制到本章实验的Middlewares\lwip\arch 路径下,该文件夹结构如下图所示:

在这里插入图片描述

图3.2.2.1 arch 文件夹的文件结构
打开工程并在Middlewares/lwip/arch 分组下添加sys_arch.c 文件,如下图所示:

在这里插入图片描述

图3.2.2.2 添加sys_arch.c 文件
至此,我们编译工程会发现有很多的错误,这些错误都是指向err.c 文件中的err_to_errno_table 数组如下图所示:

在这里插入图片描述

图3.2.2.3 err.c 文件错误
这些错误的解决方法是在lwipopts.h 文件添加LWIP_PROVIDE_ERRNO 配置项并且把它设置为1。这里我们再一次编译工程,该工程会出现一个错误,它指向sys_now 函数,该错误如下图所示:

在这里插入图片描述

图3.2.2.4 重复定义sys_now 函数
因为这个sys_now 函数原本定义在ethernetif.c 文件中(请看上一章节的内容),而我们添加的sys_arch.c 文件已经帮用户定义了sys_now 函数,所以我们把ethernetif.c 文件中的sys_now函数去除即可。注意:请把cc.h 文件中“typedef int sys_prot_t;”代码注释掉,因为这个sys_prot_t 变量已经在sys_arch.c 文件中定义了,这里无需重复定义。再一次编译就不会报错了。

修改lwip_comm.c

该文件主要修改网络配置的信息,比如默认IP 的配置、lwIP 的初始化以及DHCP 等处理,下面我们对lwip_comm.c 文件简单修改,如下所示:
(1) 删除lwIP 轮询函数lwip_periodic_handle。
(2) 修改lwip_comm_init 函数,该函数如下所示:

/**
 * @breif LWIP初始化(LWIP启动的时候使用)
 * @param 无
 * @retval 0,成功
 * 1,内存错误
 * 2,以太网芯片初始化失败
 * 3,网卡添加失败.
 */
uint8_t lwip_comm_init(void)
{
    /* 此处省略代码..................... */
    tcpip_init(NULL, NULL);
/* 此处省略代码..................... */
// lwip_init();
#if LWIP_DHCP /* 使用动态IP */
/* 此处省略代码..................... */
#else  /* 使用静态IP */
/* 此处省略代码..................... */
#endif /* 向网卡列表中添加一个网口*/
    // netif_init_flag = netif_add(&lwip_netif, (const ip_addr_t *)&ipaddr,
(const ip_addr_t *)&netmask,
(const ip_addr_t *)&gw, NULL,
&ethernetif_init, &ethernet_input);
netif_init_flag = netif_add(&lwip_netif, (const ip_addr_t *)&ipaddr,
                            (const ip_addr_t *)&netmask,
                            (const ip_addr_t *)&gw, NULL,
                            &ethernetif_init, &tcpip_input);
/* 此处省略代码..................... */
#if LWIP_DHCP /* 如果使用DHCP的话*/
/* 此处省略代码..................... */
#endif
return 0; /* 操作OK. */
}

此函数主要修改三处地方,首先我们把lwip_init 函数注释掉并添加tcpip_init 函数,该函数主要创建邮箱以及tcpip 线程,该线程专门处理数据消息并把它往上层递交,然后我们修改netif_add 函数的第七个形参,该形参不再指向ethernet_input 函数而是指向tcpip_input 函数,最后修改lwip_pkt_handle 函数,该函数如下所示:

#include "FreeRTOS.h"
#include "semphr.h"
#include "task.h"
extern xSemaphoreHandle s_xSemaphore; /* 定义一个信号量*/
/**
 * @breif 当接收到数据后调用
 * @param 无
 * @retval 无
 */
void lwip_pkt_handle(void)
{
    BaseType_t xHigherPriorityTaskWoken;
    /* 释放二值信号量*/
    xSemaphoreGiveFromISR(s_xSemaphore, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken); /* 进行一次任务切换*/
}

当接收到数据时,该函数会释放一个信号量,该信号量会在ethernetif.c 文件中创建,它是实现同步操作的(当有数据发来时,ETH 中断会释放一个信号量,接收线程会获取这个信号量并把RX 的buffer 数据复制到pbuf 当中,然后程序把pbuf 数据以邮箱的形式递交给tcpip 线程处理)。

修改ethernetif.c/h 文件

该些文件是网卡底层驱动文件,它要负责接收数据包和处理数据包,下面我们在ethernetif.c 文件下修改两个函数即可,如下所示:
(1) 修改low_level_init 函数
该函数主要初始化底层相关的信息,比如设置MAC 地址以及初始化ETH 的DMA 描述符等操作,前面我们已经了解了lwIP 操作系统的方式是把接收以及协议处理分为两个线程或者任务,接收线程可在这个函数中创建,该函数如下所示:

static void
low_level_init(struct netif *netif)
{
    netif->hwaddr_len = ETHARP_HWADDR_LEN; /*设置MAC地址长度,为6个字节*/
    /*初始化MAC地址,设置什么地址由用户自己设置,但是不能与网络中其他设备MAC地址重复*/
    netif->hwaddr[0] = lwipdev.mac[0];
    netif->hwaddr[1] = lwipdev.mac[1];
    netif->hwaddr[2] = lwipdev.mac[2];
    netif->hwaddr[3] = lwipdev.mac[3];
    netif->hwaddr[4] = lwipdev.mac[4];
    netif->hwaddr[5] = lwipdev.mac[5];
    netif->mtu = 1500; /*最大允许传输单元,允许该网卡广播和ARP功能*/
    /* 网卡状态信息标志位,是很重要的控制字段,它包括网卡功能使能、广播*/
    /* 使能、ARP 使能等等重要控制位*/ /*广播ARP协议链接检测*/
    netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP;
    /* 创建一个信号量*/
    s_xSemaphore = xSemaphoreCreateBinary();
    /* 创建处理ETH_MAC的任务*/
    sys_thread_new("eth_thread",
                   ethernetif_input,         /* 任务入口函数*/
                   netif,                    /* 任务入口函数参数*/
                   NETIF_IN_TASK_STACK_SIZE, /* 任务栈大小*/
                   NETIF_IN_TASK_PRIORITY);  /* 任务的优先级*/
#endif                                       /* LWIP_ARP || LWIP_ETHERNET */
    HAL_ETH_DMATxDescListInit(&g_eth_handler, g_eth_dma_tx_dscr_tab,
                              g_eth_tx_buf, ETH_TXBUFNB); /* 初始化发送描述符*/
    HAL_ETH_DMARxDescListInit(&g_eth_handler, g_eth_dma_rx_dscr_tab,
                              g_eth_rx_buf, ETH_RXBUFNB); /* 初始化接收描述符*/
    HAL_ETH_Start(&g_eth_handler);                        /* 开启ETH */
}

此函数比第二章实验的low_level_init 函数添加了两处地方,第一处是调用函数xSemaphoreCreateBinary 创建信号量,起到同步的作用,第二处是调用函数sys_thread_new 创
建接收任务,该任务函数为ethernetif_input 函数以及任务函数的形参指向netif。
(2) 修改ethernetif_input 函数
该函数主要获取底层的数据包并递交给tcpip 线程处理,该函数如下所示:

void ethernetif_input(void *pParams)
{
    struct netif *netif;
    struct pbuf *p = NULL;
    netif = (struct netif *)pParams;
    LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_input: IP input error\n"));
    while (1)
    {
        if (xSemaphoreTake(s_xSemaphore, portMAX_DELAY) == pdTRUE)
        {
            /* 将接收到的包移动到新的pbuf中*/
            taskENTER_CRITICAL();
        REGAIN_PBUF:
            /* 调用low_level_input函数接收数据*/
            p = low_level_input(netif);
            taskEXIT_CRITICAL();
            /* 指向包有效负载,它从一个以太网报头开始*/
            if (p != NULL)
            {
                taskENTER_CRITICAL();
                /* 调用netif结构体中的input字段(一个函数)来处理数据包*/
                if (netif->input(p, netif) != ERR_OK)
                {
                    pbuf_free(p);
                    p = NULL;
                }
                else
                {
                    xSemaphoreTake(s_xSemaphore, 0);
                    goto REGAIN_PBUF;
                }
                taskEXIT_CRITICAL();
            }
        }
    }
}

此函数不再是ethernetif_input(struct netif *netif)函数结构了,我们已经把该函数以任务的形式展现出来,该任务函数首先获取信号量,该信号量是由当ETH 中断释放的,如果有数据进来,则ETH 中断会释放一个信号量,此时这个函数获取信号量成功并在low_level_input 函数中获取数据包,最后该数据包在netif->input 指针指向的函数以邮箱形式递交给tcpip 线程处理。
打开ethernetif.h 文件,我们修改为以下源码:

#ifndef __ETHERNETIF_H__
#define __ETHERNETIF_H__
#include "lwip/err.h"
#include "lwip/netif.h"
err_t ethernetif_init(struct netif *netif); /* 网卡初始化函数*/
#endif

这里没什么好讲解的,我们就是把void ethernetif_input(struct netif *netif)和sys_now 函数声明去除了而已。

修改ethernet.c 文件

这个文件我们只修改ETH_IRQHandler 中断服务函数,请把该函数内的while()语句修改为if 语句判断,该函数源码如下所示:

void ETH_IRQHandler(void)
{
    if (ethernet_get_eth_rx_size(g_eth_handler.RxDesc))
    {
        lwip_pkt_handle(); /* 处理以太网数据,即将数据提交给LWIP */
    }
    /* 清除DMA中断标志位*/
    __HAL_ETH_DMA_CLEAR_IT(&g_eth_handler, ETH_DMA_IT_NIS);
    /* 清除DMA接收中断标志位*/
    __HAL_ETH_DMA_CLEAR_IT(&g_eth_handler, ETH_DMA_IT_R);
}

注意:由于ETH 中断服务函数会调用FreeRTOS 操作系统信号量相关的函数,所以ETH中断必须归FreeRTOS 操作系统管理,这里笔者设置该中断优先级为6,如下源码所示:

HAL_NVIC_SetPriority(ETH_IRQn, 6, 0); /* 网络中断优先级应该高一点*/

该函数在ethernet.c 文件下的HAL_ETH_MspInit 函数内调用的。至此我们已经修改完成,编译工程应该不会报错了,下面我们来讲解一下带操作系统例程的工程结构,以后带操作系统的例程都是以这样的结构编写代码的。

添加应用程序

移植好lwIP 之后,当然要测试一下移植是否成功。在本步骤中,一共需要修改1 个文件并添加4 个文件,修改的1 个文件为main.c ,添加的4 个文件为freertos_demo.c、freertos_demo.h、lwip_demo.c 和lwip_demo.h。对于main.c 主要是在main 函数中完成一些硬件的初始化,最后调用freertos_demo.c 文件中的freertos_demo 函数。freertos_demo.c 则是用于编写FreeRTOS 的相关应用程序代码。而lwip_demo.c 则是用于编写lwIP 的相关应用程序代码。
注意:freertos_demo.c、freertos_demo.h 文件保存在User 文件夹下,而lwip_demo.c、lwip_demo.h 文件保存在Middlewares\lwip\lwip_app 文件夹下。

  1. main.c 文件
    由于正点原子的STM32 系列开发板众多,这里以正点原子探索者开发板为例为读者进行演示,读者可以根据自己所移植的目标开发板在本教程的配套例程源码的《lwIP 例程6lwIP_FreeRTOS 移植》中查看对应的main.c 文件。正点原子探索者开发板《lwIP 例程6lwIP_FreeRTOS 移植》中的main.c 文件如下所示:
/**
******************************************************************************
* @file main.c
* @author 正点原子团队(ALIENTEK)
* @version V1.0
* @date 2022-6-18
* @brief lwIP+FreeRTOS操作系统移植实验
* @license Copyright (c) 2020-2032, 广州市星翼电子科技有限公司
*******************************************************************************
* @attention
*
* 实验平台:正点原子探索者开发板
* 在线视频:www.yuanzige.com
* 技术论坛:http://www.openedv.com/forum.php
* 公司网址:www.alientek.com
* 购买地址:zhengdianyuanzi.tmall.com
*
*****************************************************************************
*/
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./USMART/usmart.h"
#include "./BSP/KEY/key.h"
#include "./MALLOC/malloc.h"
#include "freertos_demo.h"
int main(void)
{
    HAL_Init();                         /* 初始化HAL库*/
    sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
    delay_init(168);                    /* 延时初始化*/
    usart_init(115200);                 /* 串口初始化为115200 */
    usmart_dev.init(84);                /* 初始化USMART */
    led_init();                         /* 初始化LED */
    lcd_init();                         /* 初始化LCD */
    key_init();                         /* 初始化按键*/
    my_mem_init(SRAMIN);                /* 初始化内部SRAM内存池*/
    my_mem_init(SRAMEX);                /* 初始化外部SRAM内存池*/
    my_mem_init(SRAMCCM);               /* 初始化内部SRAMCCM内存池*/
    freertos_demo();                    /* 创建lwIP的任务函数*/
}

可以看到,在main.c 文件中只包含了一个main 函数,main 函数主要就是完成了一些外设的初始化,如串口、LED、LCD、按键等,并在最后调用了函数freertos_demo。
2. freertos_demo.c 文件

/**
******************************************************************************
* @file freertos_demo.c
* @author 正点原子团队(ALIENTEK)
* @version V1.0
* @date 2022-01-11
* @brief lwIP+FreeRTOS操作系统移植实验
* @license Copyright (c) 2020-2032, 广州市星翼电子科技有限公司
*******************************************************************************
* @attention
*
* 实验平台:正点原子探索者开发板
* 在线视频:www.yuanzige.com
* 技术论坛:www.openedv.com
* 公司网址:www.alientek.com
* 购买地址:openedv.taobao.com
*
*******************************************************************************
*/
#include "freertos_demo.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "lwip_comm.h"
#include "lwipopts.h"
#include "FreeRTOS.h"
#include "task.h"
/*****************************************************************************/
/*FreeRTOS配置*/
/* START_TASK 任务配置
 * 包括: 任务句柄任务优先级堆栈大小创建任务
 */
#define START_TASK_PRIO 5            /* 任务优先级*/
#define START_STK_SIZE 128           /* 任务堆栈大小*/
TaskHandle_t StartTask_Handler;      /* 任务句柄*/
void start_task(void *pvParameters); /* 任务函数*/
/* LWIP_DEMO 任务配置
 * 包括: 任务句柄任务优先级堆栈大小创建任务
 */
#define LWIP_DMEO_TASK_PRIO 11           /* 任务优先级*/
#define LWIP_DMEO_STK_SIZE 1024          /* 任务堆栈大小*/
TaskHandle_t LWIP_Task_Handler;          /* 任务句柄*/
void lwip_demo_task(void *pvParameters); /* 任务函数*/
/* LED_TASK 任务配置
 * 包括: 任务句柄任务优先级堆栈大小创建任务
 */
#define LED_TASK_PRIO 10           /* 任务优先级*/
#define LED_STK_SIZE 128           /* 任务堆栈大小*/
TaskHandle_t LEDTask_Handler;      /* 任务句柄*/
void led_task(void *pvParameters); /* 任务函数*/
/*****************************************************************************/
/**
 * @breif freertos_demo
 * @param 无
 * @retval 无
 */
void freertos_demo(void)
{
    /* lwip_task任务*/
    xTaskCreate((TaskFunction_t)start_task,
                (const char *)"start_task",
                (uint16_t)START_STK_SIZE,
                (void *)NULL,
                (UBaseType_t)START_TASK_PRIO,
                (TaskHandle_t *)&StartTask_Handler);
    vTaskStartScheduler(); /* 开启任务调度*/
}
/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void *pvParameters)
{
    pvParameters = pvParameters;
    while (lwip_comm_init() != 0)
    {
        lcd_show_string(30, 110, 200, 16, 16, "lwIP Init failed!!", RED);
        delay_ms(500);
        lcd_fill(30, 50, 200 + 30, 50 + 16, WHITE);
        lcd_show_string(30, 110, 200, 16, 16, "Retrying... ", RED);
        delay_ms(500);
        LED1_TOGGLE();
    }
    while (!ethernet_read_phy(PHY_SR)) /* 检查MCU与PHY芯片是否通信成功*/
    {
        printf("MCU与PHY芯片通信失败,请检查电路或者源码!!!!\r\n");
    }
    /* 等待DHCP获取成功/超时溢出*/
    while ((lwipdev.dhcpstatus != 2) && (lwipdev.dhcpstatus != 0XFF))
    {
        lwip_dhcp_process_handle();
        vTaskDelay(5);
    }
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 创建lwIP任务*/
    xTaskCreate((TaskFunction_t)lwip_demo_task,
                (const char *)"lwip_demo_task",
                (uint16_t)LWIP_DMEO_STK_SIZE,
                (void *)NULL,
                (UBaseType_t)LWIP_DMEO_TASK_PRIO,
                (TaskHandle_t *)&LWIP_Task_Handler);
    /* LED测试任务*/
    xTaskCreate((TaskFunction_t)led_task,
                (const char *)"led_task",
                (uint16_t)LED_STK_SIZE,
                (void *)NULL,
                (UBaseType_t)LED_TASK_PRIO,
                (TaskHandle_t *)&LEDTask_Handler);
    vTaskDelete(StartTask_Handler); /* 删除开始任务*/
    taskEXIT_CRITICAL();            /* 退出临界区*/
}
/**
 * @brief lwIP运行例程
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void lwip_demo_task(void *pvParameters)
{
    pvParameters = pvParameters;
    uint8_t t = 0;
    while (1)
    {
        t++;
        if ((t % 40) == 0)
        {
            LED0_TOGGLE(); /* 翻转一次LED0 */
        }
        vTaskDelay(5);
    }
}
/**
 * @brief 系统再运行
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void led_task(void *pvParameters)
{
    pvParameters = pvParameters;
    while (1)
    {
        LED1_TOGGLE();
        vTaskDelay(1000);
    }
}

对于freertos_demo.c 文件,这里先简单的介绍一下这个文件的代码结构,这个文件的代码结构可分为6 个部分,分别是包含头文件、FreeRTOS 相关配置、全局变量及自定义函数、应用程序入口函数、开始任务入口函数、其他任务入口函数,接下来分别地介绍以上这几个部分的代码。
(1) 包含头文件
包含的头文件分成两个部分,分别为FreeRTOS 头文件、lwIP 头文件和其他头文件,如下所示:

#include "freertos_demo.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "lwip_comm.h"
#include "lwipopts.h"
#include "FreeRTOS.h"
#include "task.h"

(2) FreeRTOS 相关配置
FreeRTOS 的配置主要包括所创建FreeRTOS 任务的相关定义(任务优先级、任务堆栈大小、任务句柄、任务函数)以及FreeRTOS 相关变量(信号量、事件、列表、软件定时器等)的定义,如下所示:

/*****************************************************************************/
/*FreeRTOS配置*/
/* START_TASK 任务配置
 * 包括: 任务句柄任务优先级堆栈大小创建任务
 */
#define START_TASK_PRIO 5            /* 任务优先级*/
#define START_STK_SIZE 128           /* 任务堆栈大小*/
TaskHandle_t StartTask_Handler;      /* 任务句柄*/
void start_task(void *pvParameters); /* 任务函数*/
/* LWIP_DEMO 任务配置
 * 包括: 任务句柄任务优先级堆栈大小创建任务
 */
#define LWIP_DMEO_TASK_PRIO 11           /* 任务优先级*/
#define LWIP_DMEO_STK_SIZE 1024          /* 任务堆栈大小*/
TaskHandle_t LWIP_Task_Handler;          /* 任务句柄*/
void lwip_demo_task(void *pvParameters); /* 任务函数*/
/* LED_TASK 任务配置
 * 包括: 任务句柄任务优先级堆栈大小创建任务
 */
#define LED_TASK_PRIO 10           /* 任务优先级*/
#define LED_STK_SIZE 128           /* 任务堆栈大小*/
TaskHandle_t LEDTask_Handler;      /* 任务句柄*/
void led_task(void *pvParameters); /* 任务函数*/
/*****************************************************************************/

(3) 应用程序入口函数
这部分就是函数freertos_demo,函数freertos_demo 一开始就是创建开始任务,最后开启FreeRTOS 系统任务调度,如下所示:

/**
 * @breif freertos_demo
 * @param 无
 * @retval 无
 */
void freertos_demo(void)
{
    /* lwip_task任务*/
    xTaskCreate((TaskFunction_t)start_task,
                (const char *)"start_task",
                (uint16_t)START_STK_SIZE,
                (void *)NULL,
                (UBaseType_t)START_TASK_PRIO,
                (TaskHandle_t *)&StartTask_Handler);
    vTaskStartScheduler(); /* 开启任务调度*/
}

(4) 开始任务入口函数
这部分就是开始任务的入口函数,开始任务主要用于创建或初始化特定实验中使用到的一些硬件外设、lwIP 初始化和FreeRTOS 相关的软件(信号量、事件、列表、软件定时器等)以及创建其他用于实验演示的任务,如下所示:

/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void *pvParameters)
{
    pvParameters = pvParameters;
    while (lwip_comm_init() != 0)
    {
        lcd_show_string(30, 110, 200, 16, 16, "lwIP Init failed!!", RED);
        delay_ms(500);
        lcd_fill(30, 50, 200 + 30, 50 + 16, WHITE);
        lcd_show_string(30, 110, 200, 16, 16, "Retrying... ", RED);
        delay_ms(500);
        LED1_TOGGLE();
    }
    while (!ethernet_read_phy(PHY_SR)) /* 检查MCU与PHY芯片是否通信成功*/
    {
        printf("MCU与PHY芯片通信失败,请检查电路或者源码!!!!\r\n");
    }
    /* 等待DHCP获取成功/超时溢出*/
    while ((lwipdev.dhcpstatus != 2) && (lwipdev.dhcpstatus != 0XFF))
    {
        lwip_dhcp_process_handle();
        vTaskDelay(5);
    }
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 创建lwIP任务*/
    xTaskCreate((TaskFunction_t)lwip_demo_task,
                (const char *)"lwip_demo_task",
                (uint16_t)LWIP_DMEO_STK_SIZE,
                (void *)NULL,
                (UBaseType_t)LWIP_DMEO_TASK_PRIO,
                (TaskHandle_t *)&LWIP_Task_Handler);
    /* LED测试任务*/
    xTaskCreate((TaskFunction_t)led_task,
                (const char *)"led_task",
                (uint16_t)LED_STK_SIZE,
                (void *)NULL,
                (UBaseType_t)LED_TASK_PRIO,
                (TaskHandle_t *)&LEDTask_Handler);
    vTaskDelete(StartTask_Handler); /* 删除开始任务*/
    taskEXIT_CRITICAL();            /* 退出临界区*/
}

(6) 其他任务入口函数

/**
 * @brief lwIP运行例程
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void lwip_demo_task(void *pvParameters)
{
    pvParameters = pvParameters;
    uint8_t t = 0;
    while (1)
    {
        t++;
        if ((t % 40) == 0)
        {
            LED0_TOGGLE(); /* 翻转一次LED0 */
        }
        vTaskDelay(5);
    }
}
/**
 * @brief 系统再运行
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void led_task(void *pvParameters)
{
    pvParameters = pvParameters;
    while (1)
    {
        LED1_TOGGLE();
        vTaskDelay(1000);
    }
}

我们一般在lwip_demo_task 任务函数调用用户编写的函数执行lwIP 例程,该函数是在lwip_demo.c 文件定义。而led_task 任务函数主要表示系统是否再运行。本教程配套的实验例程6 以下的例程都会按着这个代码结构来进行实验代码的编写,建议读者先熟悉这个代码结构。
3. freertos_demo.h

#ifndef __FREERTOS_DEMO_H
#define __FREERTOS_DEMO_H
void freertos_demo(void);
#endif

freertos_demo.h 这个文件很简单,就是将函数freertos_demo 导出给其他C 源文件调用。
4. lwip_demo.c
该文件主要由用户编写的lwIP 程序,如下所示:

/**
* @brief lwIP(lwIP程序入口)
* @param 无
* @retval 无
*/
void lwip_demo(void)
{
	/* 编写lwIP代码*/
}
  1. lwip_demo.h
#ifndef _LWIP_DEMO_H
#define _LWIP_DEMO_H
#include "./SYSTEM/sys/sys.h"
void lwip_demo(void); /* lwIP例程入口*/
#endif

lwip_demo.h 这个文件很简单,就是将函数lwip_demo 导出给其他C 源文件调用。
至此,我们已经移植完成了,把代码下载到开发板中来验证移植释放成功,打开串口调试助手获取网络信息如下图所示:

在这里插入图片描述

图3.2.6.1 串口调试助手接收到IP 信息
在PC 机上按下win+r 快捷键打开命令行,并在命令行输入“ping192.168.1.37”命令,如下图所示:

在这里插入图片描述

图3.2.6.2 ping IP 地址成功
该实验的实验工程,请参考《lwIP 例程6 lwIP_FreeRTOS 移植》。

sys_arch 文件解析

下面我们分别地讲解sys_arch.c 和sys_arch.h 实现源码,如下所示:

  1. sys_arch.c 文件
    该文件是lwIP 内核与操作系统交互的接口文件,该文件定义了非常多的函数,这些函数如下表所示:

在这里插入图片描述
在这里插入图片描述

上述的函数很多,但是我们仔细发现,无非就是三个IPC 策略和几个系统配置函数,下面笔者一一讲解它们的作用。
(1) 邮箱机制
邮箱本质上就是一个指向数据的指针,邮箱在操作系统里是作为数据交互的IPC,这样我们已经明了了,lwIP 与操作系统的数据交互和API 消息是使用邮箱来完成的,例如用户将数据传递给内核也是通过邮箱将一个指针进行传递,如下源码所示:

/**
 * @brief 创建一个邮箱
 * @param mbox :邮箱的句柄
 * @param size :邮箱的大小
 * @retval ERR_OK或者ERR_MEM
 */
err_t sys_mbox_new(sys_mbox_t *mbox, int size)
{
    LWIP_ASSERT("mbox != NULL", mbox != NULL);
    LWIP_ASSERT("size > 0", size > 0);
    mbox->mbx = xQueueCreate((UBaseType_t)size, sizeof(void *));
    if (mbox->mbx == NULL)
    {
        SYS_STATS_INC(mbox.err);
        return ERR_MEM;
    }
    SYS_STATS_INC_USED(mbox);
    return ERR_OK;
}
/**
 * @brief 向邮箱发送消息,一直阻塞
 * @param mbox :邮箱的句柄
 * @param msg :邮箱的参数
 * @retval 无
 */
void sys_mbox_post(sys_mbox_t *mbox, void *msg)
{
    BaseType_t ret;
    LWIP_ASSERT("mbox != NULL", mbox != NULL);
    LWIP_ASSERT("mbox->mbx != NULL", mbox->mbx != NULL);
    ret = xQueueSendToBack(mbox->mbx, &msg, portMAX_DELAY);
    LWIP_ASSERT("mbox post failed", ret == pdTRUE);
}
/**
 * @brief 向邮箱发送消息,非阻塞
 * @param mbox :邮箱的句柄
 * @param msg :邮箱的参数
 * @retval ERR_OK或者ERR_MEM
 */
err_t sys_mbox_trypost(sys_mbox_t *mbox, void *msg)
{
    BaseType_t ret;
    LWIP_ASSERT("mbox != NULL", mbox != NULL);
    LWIP_ASSERT("mbox->mbx != NULL", mbox->mbx != NULL);
    ret = xQueueSendToBack(mbox->mbx, &msg, 0);
    if (ret == pdTRUE)
    {
        return ERR_OK;
    }
    else
    {
        LWIP_ASSERT("mbox trypost failed", ret == errQUEUE_FULL);
        SYS_STATS_INC(mbox.err);
        return ERR_MEM;
    }
}
/**
 * @brief 在中断中向邮箱发送消息
 * @param mbox :邮箱的句柄
 * @param msg :邮箱的参数
 * @retval ERR_OK或者ERR_MEM
 */
err_t sys_mbox_trypost_fromisr(sys_mbox_t *mbox, void *msg)
{
    BaseType_t ret;
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    LWIP_ASSERT("mbox != NULL", mbox != NULL);
    LWIP_ASSERT("mbox->mbx != NULL", mbox->mbx != NULL);
    ret = xQueueSendToBackFromISR(mbox->mbx, &msg, &xHigherPriorityTaskWoken);
    if (ret == pdTRUE)
    {
        if (xHigherPriorityTaskWoken == pdTRUE)
        {
            return ERR_NEED_SCHED;
        }
        return ERR_OK;
    }
    else
    {
        LWIP_ASSERT("mbox trypost failed", ret == errQUEUE_FULL);
        SYS_STATS_INC(mbox.err);
        return ERR_MEM;
    }
}
/**
 * @brief 从邮箱中获取消息,阻塞
 * @param mbox :邮箱的句柄
 * @param msg :邮箱的参数
 * @param timeout_ms 超时时间
 * @retval SYS_ARCH_TIMEOUT
 */
u32_t sys_arch_mbox_fetch(sys_mbox_t *mbox, void **msg, u32_t timeout_ms)
{
    BaseType_t ret;
    void *msg_dummy;
    LWIP_ASSERT("mbox != NULL", mbox != NULL);
    LWIP_ASSERT("mbox->mbx != NULL", mbox->mbx != NULL);
    if (!msg)
    {
        msg = &msg_dummy;
    }
    if (!timeout_ms)
    {
        /* wait infinite */
        ret = xQueueReceive(mbox->mbx, &(*msg), portMAX_DELAY);
        LWIP_ASSERT("mbox fetch failed", ret == pdTRUE);
    }
    else
    {
        TickType_t timeout_ticks = timeout_ms / portTICK_RATE_MS;
        ret = xQueueReceive(mbox->mbx, &(*msg), timeout_ticks);
        if (ret == errQUEUE_EMPTY)
        {
            /* timed out */
            *msg = NULL;
            return SYS_ARCH_TIMEOUT;
        }
        LWIP_ASSERT("mbox fetch failed", ret == pdTRUE);
    }
    return 1;
}
/**
 * @brief 从邮箱中获取消息,非阻塞
 * @param mbox :邮箱的句柄
 * @param msg :邮箱的参数
 * @retval SYS_MBOX_EMPTY
 */
u32_t sys_arch_mbox_tryfetch(sys_mbox_t *mbox, void **msg)
{
    BaseType_t ret;
    void *msg_dummy;
    LWIP_ASSERT("mbox != NULL", mbox != NULL);
    LWIP_ASSERT("mbox->mbx != NULL", mbox->mbx != NULL);
    if (!msg)
    {
        msg = &msg_dummy;
    }
    ret = xQueueReceive(mbox->mbx, &(*msg), 0);
    if (ret == errQUEUE_EMPTY)
    {
        *msg = NULL;
        return SYS_MBOX_EMPTY;
    }
    LWIP_ASSERT("mbox fetch failed", ret == pdTRUE);
    return 1;
}
/**
 * @brief 删除一个邮箱
 * @param mbox :邮箱的句柄
 * @retval 无
 */
void sys_mbox_free(sys_mbox_t *mbox)
{
    LWIP_ASSERT("mbox != NULL", mbox != NULL);
    LWIP_ASSERT("mbox->mbx != NULL", mbox->mbx != NULL);
#if LWIP_FREERTOS_CHECK_QUEUE_EMPTY_ON_FREE
    {
        UBaseType_t msgs_waiting = uxQueueMessagesWaiting(mbox->mbx);
        LWIP_ASSERT("mbox quence not empty", msgs_waiting == 0);
        if (msgs_waiting != 0)
        {
            SYS_STATS_INC(mbox.err);
        }
    }
#endif
    vQueueDelete(mbox->mbx);
    SYS_STATS_DEC(mbox.used);
}

上述源码很简单,由于FreeRTOS 操作系统没有邮箱的IPC ,所以我们只能使用FreeRTOS 的消息队列模拟邮箱,关于FreeRTOS 操作系统的消息队列知识,请大家参考正点原子《FreeRTOS 开发指南》第十三章的内容。
(2) 信号量机制
信号量在操作系统中起到同步的作用,所以lwIP 与操作系统的信号量是为内核提供同步机制。例如用户发送数据到内核时需要调用上层API 接口,在这个过程中用户需要去获取一个信号量,此时系统没有信号量释放,所以用户线程只能在堵塞状态,当lwIP 内核获取到用户的数据并调用网卡去发送数据,直到数据发送完毕之后就会释放一个信号量来告知用户线程发送数据完成,最后用户线程从堵塞状态转成运行态。如下源码所示:

/**
 * @brief 创建一个信号量
 * @param sem :信号量的句柄
 * @param initial_count 初始值
 * @retval ERR_OK或者ERR_MEM
 */
err_t sys_sem_new(sys_sem_t *sem, u8_t initial_count)
{
    LWIP_ASSERT("sem != NULL", sem != NULL);
    LWIP_ASSERT("initial_count invalid (not 0 or 1)",
                (initial_count == 0) || (initial_count == 1));
    sem->sem = xSemaphoreCreateBinary();
    if (sem->sem == NULL)
    {
        SYS_STATS_INC(sem.err);
        return ERR_MEM;
    }
    SYS_STATS_INC_USED(sem);
    if (initial_count == 1)
    {
        BaseType_t ret = xSemaphoreGive(sem->sem);
        LWIP_ASSERT("sys_sem_new: initial give failed", ret == pdTRUE);
    }
    return ERR_OK;
}
/**
 * @brief 释放一个信号量
 * @param sem :信号量的句柄
 * @retval 无
 */
void sys_sem_signal(sys_sem_t *sem)
{
    BaseType_t ret;
    LWIP_ASSERT("sem != NULL", sem != NULL);
    LWIP_ASSERT("sem->sem != NULL", sem->sem != NULL);
    ret = xSemaphoreGive(sem->sem);
    /* queue full is OK, this is a signal only... */
    LWIP_ASSERT("sys_sem_signal: sane return value",
                (ret == pdTRUE) || (ret == errQUEUE_FULL));
}
/**
 * @brief 等待一个信号量
 * @param sem :信号量的句柄
 * @param timeout_ms :无限的等待
 * @retval SYS_ARCH_TIMEOUT或者1
 */
u32_t sys_arch_sem_wait(sys_sem_t *sem, u32_t timeout_ms)
{
    BaseType_t ret;
    LWIP_ASSERT("sem != NULL", sem != NULL);
    LWIP_ASSERT("sem->sem != NULL", sem->sem != NULL);
    if (!timeout_ms)
    {
        /* wait infinite */
        ret = xSemaphoreTake(sem->sem, portMAX_DELAY);
        LWIP_ASSERT("taking semaphore failed", ret == pdTRUE);
    }
    else
    {
        TickType_t timeout_ticks = timeout_ms / portTICK_RATE_MS;
        ret = xSemaphoreTake(sem->sem, timeout_ticks);
        if (ret == errQUEUE_EMPTY)
        {
            /* 请求超时*/
            return SYS_ARCH_TIMEOUT;
        }
        LWIP_ASSERT("taking semaphore failed", ret == pdTRUE);
    }
    /* 旧版本的lwIP要求我们返回等待的时间。
    现在情况已经不一样了。刚刚返回!= SYS_ARCH_TIMEOUT
    这是足够的*/
    return 1;
}
/**
 * @brief 删除一个信号量
 * @param sem :信号量的句柄
 * @retval 无
 */
void sys_sem_free(sys_sem_t *sem)
{
    LWIP_ASSERT("sem != NULL", sem != NULL);
    LWIP_ASSERT("sem->sem != NULL", sem->sem != NULL);
    SYS_STATS_DEC(sem.used);
    vSemaphoreDelete(sem->sem);
    sem->sem = NULL;
}

上述源码是关于FreeRTOS 操作系统的信号量知识,请大家参考正点原子《FreeRTOS 开发指南》第十四章的内容。
(3) 互斥信号量机制
互斥信号量主要作用是防止优先级翻转,当然互斥信号量是在信号量的基础上修改得来的,所以也有同步功能的作用,操作系统的互斥信号量为lwIP 内核提供互斥机制,如下源码所示:

/**
 * @brief 创建一个互斥信号量
 * @param mutex :互斥信号量的句柄
 * @retval ERR_OK或者ERR_MEM
 */
err_t sys_mutex_new(sys_mutex_t *mutex)
{
    LWIP_ASSERT("mutex != NULL", mutex != NULL);
    mutex->mut = xSemaphoreCreateRecursiveMutex();
    if (mutex->mut == NULL)
    {
        SYS_STATS_INC(mutex.err);
        return ERR_MEM;
    }
    SYS_STATS_INC_USED(mutex);
    return ERR_OK;
}
/**
 * @brief 等待一个互斥信号量(阻塞)
 * @param mutex :互斥信号量的句柄
 * @retval 无
 */
void sys_mutex_lock(sys_mutex_t *mutex)
{
    BaseType_t ret;
    LWIP_ASSERT("mutex != NULL", mutex != NULL);
    LWIP_ASSERT("mutex->mut != NULL", mutex->mut != NULL);
    ret = xSemaphoreTakeRecursive(mutex->mut, portMAX_DELAY);
    LWIP_ASSERT("failed to take the mutex", ret == pdTRUE);
}
/**
 * @brief 释放一个互斥信号量
 * @param mutex :互斥信号量的句柄
 * @retval 无
 */
void sys_mutex_unlock(sys_mutex_t *mutex)
{
    BaseType_t ret;
    LWIP_ASSERT("mutex != NULL", mutex != NULL);
    LWIP_ASSERT("mutex->mut != NULL", mutex->mut != NULL);
    ret = xSemaphoreGiveRecursive(mutex->mut);
    LWIP_ASSERT("failed to give the mutex", ret == pdTRUE);
}
/**
 * @brief 删除一个互斥信号量
 * @param mutex :互斥信号量的句柄
 * @retval 无
 */
void sys_mutex_free(sys_mutex_t *mutex)
{
    LWIP_ASSERT("mutex != NULL", mutex != NULL);
    LWIP_ASSERT("mutex->mut != NULL", mutex->mut != NULL);
    SYS_STATS_DEC(mutex.used);
    vSemaphoreDelete(mutex->mut);
    mutex->mut = NULL;
}

关于互斥信号量的相关知识请大家参考正点原子《FreeRTOS 开发指南》第十四章的14.8小节内容。
(4) 其他系统配置
sys_arch.c 文件里有几个配置信息函数,如下源码所示:

/**
 * @brief 系统初始化
 * @param 无
 * @retval 无
 */
void sys_init(void)
{
}
#if LWIP_FREERTOS_SYS_NOW_FROM_FREERTOS
/**
 * @brief 提供lwIP内核时钟
 * @param 无
 * @retval 无
 */
u32_t sys_now(void)
{
    return xTaskGetTickCount() * portTICK_PERIOD_MS;
}
#endif
/**
 * @brief 获取时钟节拍
 * @param 无
 * @retval 无
 */
u32_t sys_jiffies(void)
{
    return xTaskGetTickCount();
}
#if SYS_LIGHTWEIGHT_PROT
/**
 * @brief 进入临界区
 * @param 无
 * @retval 无
 */
sys_prot_t
sys_arch_protect(void)
{
    taskENTER_CRITICAL();
}
/**
 * @brief 退出临界区
 * @param 无
 * @retval 无
 */
void sys_arch_unprotect(sys_prot_t pval)
{
    taskEXIT_CRITICAL();
}
#endif /* SYS_LIGHTWEIGHT_PROT */
/**
 * @brief lwip延时函数
 * @param delay_ms :延时时间
 * @retval 无
 */
void sys_arch_msleep(u32_t delay_ms)
{
    TickType_t delay_ticks = delay_ms / portTICK_RATE_MS;
    vTaskDelay(delay_ticks);
}
/**
 * @brief 创建线程
 * @param name :线程名称
 * @param thread :线程函数
 * @param arg :传入的参数
 * @param stacksize :堆栈的大小
 * @param prio :线程优先级
 * @retval 任务控制块
 */
sys_thread_t
sys_thread_new(const char *name, lwip_thread_fn thread, void *arg,
               int stacksize, int prio)
{
    TaskHandle_t rtos_task;
    BaseType_t ret;
    sys_thread_t lwip_thread;
    size_t rtos_stacksize;
    LWIP_ASSERT("invalid stacksize", stacksize > 0);
#if LWIP_FREERTOS_THREAD_STACKSIZE_IS_STACKWORDS
    rtos_stacksize = (size_t)stacksize;
#else
    rtos_stacksize = (size_t)stacksize / sizeof(StackType_t);
#endif
    /* lwIP's lwip_thread_fn matches FreeRTOS' TaskFunction_t, so we can pass the
    thread function without adaption here. */
    ret = xTaskCreate(thread, name, (configSTACK_DEPTH_TYPE)rtos_stacksize,
                      arg, prio, &rtos_task);
    LWIP_ASSERT("task creation failed", ret == pdTRUE);
    lwip_thread.thread_handle = rtos_task;
    return lwip_thread;
}

上述函数非常简单,这里笔者不做讲解。
2. sys_arch.h 文件
该文件主要声明sys_arch.c 文件的信息的,如下源码所示:

#ifndef LWIP_ARCH_SYS_ARCH_H
#define LWIP_ARCH_SYS_ARCH_H
#include "lwip/opt.h"
#include "lwip/arch.h"
/** This is returned by _fromisr() sys functions to tell the outermost function
 * that a higher priority task was woken and the scheduler needs to be invoked.
 */
#define ERR_NEED_SCHED 123
/* This port includes FreeRTOS headers in sys_arch.c only.
 * FreeRTOS uses pointers as object types. We use wrapper structs instead of
 * void pointers directly to get a tiny bit of type safety.
 */
void sys_arch_msleep(u32_t delay_ms);
#define sys_msleep(ms) sys_arch_msleep(ms)
#if SYS_LIGHTWEIGHT_PROT
typedef u32_t sys_prot_t;
#endif /* SYS_LIGHTWEIGHT_PROT */
#if !LWIP_COMPAT_MUTEX
struct _sys_mut
{
    void *mut;
};
typedef struct _sys_mut sys_mutex_t;
#define sys_mutex_valid_val(mutex) ((mutex).mut != NULL)
#define sys_mutex_valid(mutex) (((mutex) != NULL) && sys_mutex_valid_val(*(mutex)))
#define sys_mutex_set_invalid(mutex) ((mutex)->mut = NULL)
#endif /* !LWIP_COMPAT_MUTEX */
struct _sys_sem
{
    void *sem;
};
typedef struct _sys_sem sys_sem_t;
#define sys_sem_valid_val(sema) ((sema).sem != NULL)
#define sys_sem_valid(sema) (((sema) != NULL) && sys_sem_valid_val(*(sema)))
#define sys_sem_set_invalid(sema) ((sema)->sem = NULL)
struct _sys_mbox
{
    void *mbx;
};
typedef struct _sys_mbox sys_mbox_t;
#define sys_mbox_valid_val(mbox) ((mbox).mbx != NULL)
#define sys_mbox_valid(mbox) (((mbox) != NULL) && sys_mbox_valid_val(*(mbox)))
#define sys_mbox_set_invalid(mbox) ((mbox)->mbx = NULL)
struct _sys_thread
{
    void *thread_handle;
};
typedef struct _sys_thread sys_thread_t;
#if LWIP_NETCONN_SEM_PER_THREAD
sys_sem_t *sys_arch_netconn_sem_get(void);
void sys_arch_netconn_sem_alloc(void);
void sys_arch_netconn_sem_free(void);
#define LWIP_NETCONN_THREAD_SEM_GET() sys_arch_netconn_sem_get()
#define LWIP_NETCONN_THREAD_SEM_ALLOC() sys_arch_netconn_sem_alloc()
#define LWIP_NETCONN_THREAD_SEM_FREE() sys_arch_netconn_sem_free()
#endif /* LWIP_NETCONN_SEM_PER_THREAD */
#endif /* LWIP_ARCH_SYS_ARCH_H */

上述源码没什么好讲解的,主要封装了几个宏定义来判断邮箱,信号量以及互斥信号量是否有效。至此,sys_arch.c/h 文件已经讲解完了。

lwIP 带操作系统启动流程图总结

在这里插入图片描述

lwIP 内存管理

对于嵌入式系统而言,内存管理始终是最重要的一环,内存管理的选择将从根本上决定内存分配和回收效率,最终决定系统的性能。lwIP 为使用者提供了两种简单却又高效的内存管理机制,它们分别为动态内存池管理和动态内存堆管理。

内存的简介

在lwIP 中内存分配策略有两种,一种是:动态内存池管理策略,另一种是:动态内存堆管理策略,它们在lwIP 中起到以长补短的作用,lwIP 内核根据不同的场景而选择不同的分配方式使系统的内存开销和分配效率大大的提高。说到内存分配,我们不得不想起C 语言也是有提供内存分配,它是使用库中的malloc 和free 进行内存分配,当然lwIP 也是支持这种分配方式的,但是lwIP 不建议使用C 标准库内存分配策略,主要原因笔者留到本章的4.5 小节来讲解。

lwIP 的宏配置及内存管理
在lwIP 中内存的选择需要以下几个宏定义的值来决定,用户可以根据宏值来判断lwIP 使用那种内存管理策略,如下表所示:

在这里插入图片描述

注:lwIP 内存堆管理策略和C 标准库管理策略只能选其一,若MEM_LIBC_MALLOC 为0,则lwIP 内核选择内存堆管理策略。

动态内存堆管理策略

动态内存堆也叫可变长分配方式,这种可变长的内存块分配在很多系统中被用到,系统本身就是一个很大的内存堆,随着系统的运行,不断的申请和释放内存造成了系统的内存块的大小和数量随之改变,严重一点可能造成内存碎片。lwIP 动态内存堆策略采用First Fit(首次拟合)内存管理算法。该算法倾向于优先利用内存中低址部分的空闲分区,从而保留了高址部分的大空闲区,这为以后到达的大作业分配大的内存空间创造了条件,但是缺点也是明显的,因为首次拟合(First Fit)算法是从低地址不断被划分的,所以系统会留下许多难以利用的且很小的空闲分区,我们称为内存碎片。每次申请内存时系统每次查找都是从低地址部分开始的,这无疑又会增加查找可用空闲分区时的时间。
下面笔者分几个部分解析lwIP 内存堆算法的实现代码,该算法由mem.c 和mem.h 文件组成,其中mem.c 尤为重要,它实现了lwIP 内存堆的分配和释放原理。
(1) 内存堆的结构体
管理内存块的结构体,如下源码所示:

struct mem {
	mem_size_t next; /* 保存下一个内存块的索引*/
	mem_size_t prev; /* 保存前一个内存块的索引*/
	u8_t used; /* 此内存快是否被用。1使用、0 未使用*/
};

可以看出,这个结构体只定义了三个成员变量,其中next、prev 变量用来保存下一个和前一个内存块的索引,而used 变量用来声明被管理的内存块是否可用。
(2) 内存堆的对齐及最小配置值

#ifndef MIN_SIZE
#define MIN_SIZE 12
#endif /* MIN_SIZE */
/* 最小大小做对齐处理,后面均用对齐后的该宏值*/
#define MIN_SIZE_ALIGNED LWIP_MEM_ALIGN_SIZE(MIN_SIZE)
/* 内存块头大小做对齐处理,后面均用对齐后的该宏值*/
#define SIZEOF_STRUCT_MEM LWIP_MEM_ALIGN_SIZE(sizeof(struct mem))
/* 用户定义的堆大小做对齐处理,后面均用对齐后的该宏值*/
#define MEM_SIZE_ALIGNED LWIP_MEM_ALIGN_SIZE(MEM_SIZE)

lwIP 内核为了有效防止内存碎片,它定义了最小分配大小MIN_SIZE,若用户申请的内存小于最小分配内存,则系统分配MIN_SIZE 大小的内存资源。往下的宏定义是对内存大小进行4 字节对齐。注:内存对齐的作用:1,平台原因:不是全部的硬件平台都能访问随意地址上的随意类型数据的;某些硬件平台仅仅能在某些地址处取某些特定类型的数据,否则抛出硬件异常。2,性能原因:经过内存对齐后,CPU 的内存访问速度大大提升。
(3) 定义内存堆的空间

#ifndef LWIP_RAM_HEAP_POINTER
/*定义堆内存空间*/
LWIP_DECLARE_MEMORY_ALIGNED(ram_heap, MEM_SIZE_ALIGNED + (2U * SIZEOF_STRUCT_MEM));
#define LWIP_RAM_HEAP_POINTER ram_heap
#endif

无论是内存堆还是内存池,它们都是对一个大数组进行操作,上述的宏定义就是指向一个名为ram_heap 数组,该数组的大小为MEM_SIZE_ALIGNED + (2U*SIZEOF_STRUCT_MEM),lwIP 内存堆申请的内存就是从这个数组分配得来的。
(4) 操作内存堆的变量

/* 指向对齐后的内存堆的地址*/
static u8_t *ram;
/* 指向对齐后的内存堆的最后一个内存块*/
static struct mem *ram_end;
/* 指向已被释放的索引号最小的内存块(内存堆最前面的已被释放的)*/
static struct mem *LWIP_MEM_LFREE_VOLATILE lfree;

ram_heap 数组就是lwIP 定义的内存堆总空间,如何从这个总空间申请合适大小的内存,就是利用上述源码的三个指针,ram 指针指向对齐后的内存堆总空间首地址,ram_end 指针指向内存堆总空间尾地址(接近总空间的尾地址),而lfree 指针指向最低内存地址的空闲内存块。
注:lwIP 内核就是根据lfree 指针指向空闲内存块来分配内存,而ram_end 指针用来检测该总内存堆空间是否有空闲的内存。
(5) 内存堆的初始化
结合以上的(1)~(4)的内容,我们来看一下lwIP 动态内存堆是如何实现的,如下源码所示:

  1. mem_init 函数
void mem_init(void)
{
    struct mem *mem;
    /* 对内存堆的地址(全局变量的名)进行对齐指向ram_heap。*/
    ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);
    /* 建立第一个内存块,内存块由内存块头+空间组成。*/
    mem = (struct mem *)(void *)ram;
    /* 下一个内存块不存在,因此指向内存堆的结束*/
    mem->next = MEM_SIZE_ALIGNED;
    /* 前一个内存块就是它自己,因为这是第一个内存块*/
    mem->prev = 0;
    /* 第一个内存块没有被使用*/
    mem->used = 0;
    /* 初始化堆的末端(指向MEM_SIZE_ALIGNED底部位置)*/
    ram_end = ptr_to_mem(MEM_SIZE_ALIGNED);
    /* 最后一个内存块被使用。因为其后面没有可用空间,必须标记为已被使用*/
    ram_end->used = 1;
    /* 下一个不存在,因此指向内存堆的结束*/
    ram_end->next = MEM_SIZE_ALIGNED;
    /* 前一个不存在,因此指向内存堆的结束*/
    ram_end->prev = MEM_SIZE_ALIGNED;
    /* 已释放的索引最小的内存块就是上面建立的第一个内存块。*/
    lfree = (struct mem *)(void *)ram;
    /* 这里建立一个互斥信号量,主要是用来进行内存的申请、释放的保护*/
    if (sys_mutex_new(&mem_mutex) != ERR_OK)
    {
    }
}

上述源码就是对堆空间初始化,一开始lfree 指针指向第一个内存块,该内存块有两个部分组成,一个是控制块(struct mem 大小,标志管理的内存是否可用),另一个是可用内存。
ram_end 指针指向尾内存块,它用来标志这个堆空间是否有可用内存,若lfree 指针指向ram_end 指针,则该堆空间没有可用内存分配,由此可以看出,lfree 指针从堆空间低地址不断查找和划分内存,最终在ram_end 指针指向的地址结束分配。内存堆初始化示意图如下所示:

在这里插入图片描述

注:struct mem 结构体的next 和prev 变量并不是指针类型,它们保存的是内存块的索引,例如定义一个a[10]数组,next 和prev 保存的是0~ 9 的索引号,lwIP 内核根据索引号获取a 数组的索引地址(&a[0~9])。

  1. mem_malloc 函数
void *
mem_malloc(mem_size_t size_in)
{
    mem_size_t ptr, ptr2, size;
    struct mem *mem, *mem2;
    /*******第一:检测用户申请的内存块释放满足LWIP的规则*******/
    /*******第二:从内存堆中划分用户的内存块******/
    /* 寻找足够大的空闲块,从最低的空闲块开始.*/
    for (ptr = mem_to_ptr(lfree); ptr < MEM_SIZE_ALIGNED - size;
         ptr = ((struct mem *)(void *)&ram[ptr])->next)
    {
        mem = ptr_to_mem(ptr); /* 取它的地址*/
        /* 空间大小必须排除内存块头大小*/
        if ((!mem->used) &&
            (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size)
        {
            /* 这个地方需要判断剩余的内存块是否可以申请size内存块*/
            if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >= (size + SIZEOF_STRUCT_MEM +
                                                          MIN_SIZE_ALIGNED))
            {
                /* 上面注释一大堆,主要就是说,
                剩余内存可能连一个内存块的头都放不下了,
                这个时候就没法新建空内存块。其索引也就不能移动*/
                /* 指向申请后的位置,即:
                建立下一个未使用的内存块的头部。
                即:插入一个新空内存块*/
                ptr2 = (mem_size_t)(ptr + SIZEOF_STRUCT_MEM + size);
                /*从Ptr2地址开始创建mem2的结构体*/
                mem2 = ptr_to_mem(ptr2); /* 调用(struct mem *)(void *)&ram[ptr]; */
                mem2->used = 0;
                /* 这个根据下面的if(mem2->next != MEM_SIZE_ALIGNED)判定*/
                mem2->next = mem->next;
                mem2->prev = ptr; /* 空闲内存块的前一个指向上面分配的内存块*/
                /* 前一个内存块指向上面建立的空闲内存块*/
                mem->next = ptr2;
                mem->used = 1; /* 将当前分配的内存块标记为已使用*/
                /* 如果mem2内存块的下一个内存块不是链表中最后一个内存块(结束地址),
                那就将它下一个的内存块的prve指向mem2 */
                if (mem2->next != MEM_SIZE_ALIGNED)
                {
                    ((struct mem *)(void *)&ram[mem2->next])->prev = ptr2;
                }
            }
            else
            { /* 内存块太小了会产生的碎片*/
                mem->used = 1;
            }
            /* 这里处理:当分配出去的内存正好是lfree时,
            因为该内存块已经被分配出去了,
            必须修改lfree的指向下一个最其前面的已释放的内存块*/
            if (mem == lfree)
            {
                struct mem *cur = lfree;
                /* 只要内存块已使用且没到结尾,则继续往后找*/
                while (cur->used && cur != ram_end)
                {
                    cur = ptr_to_mem(cur->next); /* 下一个内存块*/
                }
                /* 指向找到的第一个已释放的内存块。如果上面没有找到,则lfree = lfree不变*/
                lfree = cur;
            }
            /* 这里返回内存块的空间的地址,排除内存块的头*/
            return (u8_t *)mem + SIZEOF_STRUCT_MEM + MEM_SANITY_OFFSET;
        }
    }
    return NULL;
}
}

从上述源码可以看出,lwIP 内存堆申请的内存是从低地址往高地址方向查找合适的内存块,每一个内存块由两个部分组成,一个是(struct mem)大小的内存块,它用来描述和管理可用的内存块,另一个是可用内存块,用户可直接操作它。根据上图4.2.1 图解可以看出,lfree 指针指向的是未被使用的控制块,若用户申请size 大小的内存,则lwIP 内核会把lfree 指针指向的控制块标志为已用内存,并且往高地址偏移(struct mem)结构体+对齐后的size 大小,偏移完成之后lfree 指针指向的地址附加一个struct mem 结构体(下一个控制块)。注:下一个控制块被标志为未使用即used=0,至此我们可以得到以下示意图。

在这里插入图片描述

  1. mem_free 函数
void mem_free(void *rmem)
{
    struct mem *mem;
    /* 第一步:检查内存块的参数*/
    /* 判断释放的内存块释放为空*/
    if (rmem == NULL)
    {
        return; /* 为空则返回*/
    }
    /* 除去指针就剩下内存块了,通过mem_malloc的到的地址是不含struct mem 的*/ * /
        mem = (struct mem *)(void *)((u8_t *)rmem - (SIZEOF_STRUCT_MEM +
                                                     MEM_SANITY_OFFSET));
    /* 第二步:查找指定的内存块,标记为未使用*/
    mem->used = 0;
    /* 第三步:需要移动全局的释放指针,因为lfree始终指向内存堆中最小索引的
    那个已经释放的内存块*/
    if (mem < lfree)
    {
        /* 新释放的结构现在是最低的*/
        lfree = mem;
    }
}

lwIP 内存堆释放内存是非常简单的,它一共分为三个步骤,第一、检测传入的地址是否正确,第二、对这个地址进行偏移,偏移大小为struct mem,这样可以得到释放内存的控制块首地址,并且设置该控制块为未使用标志,第三、判断该控制块的地址是否小于lfree 指针指向的地址,若小于,则证明mem 的内存块在lfree 指向的内存块之前即更接近堆空间首地址,系统会把lfree 指针指向这个释放的内存块(控制块+ 可用内存),以后申请内存时会在lfree指针的内存块开始查找合适的内存。注:申请内存时lwIP 内核会从lfree 指针指向的内存块开始查找,若该内存块不满足申请要求,则lwIP 内核根据这个内存块的next 变量保存的数值作为下一跳查询的地址。

在这里插入图片描述

若申请内存时lfree 指针指向的内存块不满足申请需求,则该内存块的next 数值作为下一跳查询的索引。注:lfree 指针永远指向最低地址的内存空间。

动态内存池管理策略

在内存池初始化时候,系统会将可用的内存块划分为N 个固定大小的内存,这些内存块通过单链表的方式连接起来,在用户申请内存块时,直接从单链表的头部取出一个内存块进行分配,释放内存块时也是挺简单的,只要将内存块释放到链表的头部即可,虽然这样的分配很高效,但是有很明显的缺点,如浪费资源等。
lwIP 内存池的实现是受制于两个宏值MEMP_MEM_MALLOC 和MEM_USE_POOLS 的限制,在该动态内存池的源码文件中,仍然到处可见这两个宏值。
1,IP 内存池的应用场景
lwIP 存在很多固定的数据结构,这些结构的特点就是在使用之前就已经知道了数据结构的大小,而且这些数据结构在使用的过程中不会发生大小改变的。比如在建立一个TCP 连接的时候,lwIP 需要使用一种叫做TCP 控制块的数据结构,这种数据结构大小是固定的,所以为了满足这些数据类型分配的需要,在内存初始化的时候就建立了一定数量的动态内存池POOL。
2,IP 内存池实现的文件
对于内存堆来说,动态内存池分配还是挺麻烦的,主要就是对于宏的巧妙运用,现在笔者就以文件的形式讲解动态内存池分配的原理。动态内存池分配在这四个文件memp.c、memp.h、memp_std.h 和memp_prive.h 有所介绍,下面笔者分别地讲解这四个文件的作用。
(1) memp_std.h 文件
该文件定义了lwIP 内核所需的内存池,由于lwIP 内核的固定数据结构多种多样,所以它们使用宏定义声明是否使用该类型的内存池,如TCP、UDP、DHCP、ICMP 等协议。这些宏定义一般在lwippools.h 文件中声明启用。该文件的源码如下所示:

#if LWIP_RAW
LWIP_MEMPOOL(RAW_PCB, MEMP_NUM_RAW_PCB, sizeof(struct raw_pcb), "RAW_PCB")
#endif /* LWIP_RAW */
#if LWIP_UDP
LWIP_MEMPOOL(UDP_PCB, MEMP_NUM_UDP_PCB, sizeof(struct udp_pcb), "UDP_PCB")
#endif /* LWIP_UDP */
#if LWIP_TCP
LWIP_MEMPOOL(TCP_PCB, MEMP_NUM_TCP_PCB, sizeof(struct tcp_pcb), "TCP_PCB")
LWIP_MEMPOOL(TCP_PCB_LISTEN, MEMP_NUM_TCP_PCB_LISTEN,
             sizeof(struct tcp_pcb_listen), "TCP_PCB_LISTEN")
LWIP_MEMPOOL(TCP_SEG, MEMP_NUM_TCP_SEG, sizeof(struct tcp_seg), "TCP_SEG")
#endif /* LWIP_TCP */
/* …………………………………………………………………………………忽略以下源码……………………………………………………………………………………… */

从上述源码可以看出两个重点内容,第一点:不同类型的内存池是由相应的宏定义声明启用,第二点:LWIP_MEMPOOL 宏定义用来初始化各类型的内存池。
(2) memp_priv.h 文件

/* 管理内存块*/
struct memp
{
    struct memp *next;
};
/* 管理和描述各类型的内存池*/
struct memp_desc
{
    /** 每个内存块的大小*/
    u16_t size;
    /** 内存块的数量*/
    u16_t num;
    /** 指向内存的基地址*/
    u8_t *base;
    /** 每个池的第一个空闲元素。元素形成一个链表*/
    struct memp **tab;
};

这个文件主要定义了两个结构体,它们分别为memp 和memp_desc 结构体,其中memp结构体是把同一类型的内存池以链表的形式链接起来,而memp_desc 结构体是用来管理和描述各类型的内存池,如数量、大小、内存池的起始地址和指向空闲内存池的指针。memp 和memp_desc 结构体的关系如下图所示:

在这里插入图片描述

从上图可以看出,每一个描述符都是用来管理同一类型的内存池,而这些内存池即内存块是以链表的形式链接起来。
(3) memp.h 文件
在memp.h 文件中,笔者重点讲解memp_t 枚举类型以及LWIP_MEMPOOL_DECLARE 宏定义,它们的作用如下所示:

typedef enum
{
/* ##为C语言的连接符,例如MEMP_##A,A = NAME ,所以等于MEMP_NAME */
#define LWIP_MEMPOOL(name, num, size, desc) MEMP_##name,
#include "lwip/priv/memp_std.h"
    MEMP_MAX
} memp_t;
#include "lwip/priv/memp_priv.h" /* 该文件需要使用上面的枚举*/
#include "lwip/stats.h"

该文件最主要的是memp_t 枚举类型,它主要获取各类内存池的数量,这里用到宏的巧妙运用,根据memp_std.h 文件启用的内存池来计算各类内存池的数量MEMP_MAX。如何计算?
请看下面内容:
1,LWIP_MEMPOOL 宏定义指向MEMP_##name(##是C 语言的连接符)
2,根据#include "lwip/priv/memp_std.h 文件启用了哪些类型内存池。
如果memp_std.h 文件只启用了LWIP_RAW 和LWIP_UDP 类型的内存池,那么MEMP_MAX 变量就等于2。这个枚举类型展开之后如下源码所示:

typedef enum {
	MEMP_RAW_PCB,
	MEMP_UDP_PCB,
	MEMP_MAX
} memp_t;

根据枚举类型的特性,MEMP_RAW_PCB 为0,MEMP_UDP_PCB 为1,由此类推。
注:memp.h 文件最主要的任务是计算各类的内存池,最后得出MEMP_MAX 数值。

#define LWIP_MEMPOOL_DECLARE(name, num, size, desc) \
LWIP_DECLARE_MEMORY_ALIGNED(memp_memory_ ## name ## _base,
((num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size))));

LWIP_MEMPOOL_DECLARE_STATS_INSTANCE(memp_stats_##name)

static struct memp *memp_tab_##name;

const struct memp_desc memp_##name = {
    DECLARE_LWIP_MEMPOOL_DESC(desc)
        LWIP_MEMPOOL_DECLARE_STATS_REFERENCE(memp_stats_##name)
            LWIP_MEM_ALIGN_SIZE(size),
    (num),
    memp_memory_##name##_base,
    &memp_tab_##name}; \
};

此宏定义非常重要,各类型的内存池都使用这个宏定义声明,例如内存池的内存由来,各类型内存池的数量、大小、内存由来的地址以及指向空闲的指针。这个宏定义展开后如下源码所示:

#define LWIP_MEMPOOL_DECLARE(name,num,size,desc) \
u8_t memp_memory_ ## name ## _base[((((((num) * (MEMP_SIZE + (((size) +
MEM_ALIGNMENT - 1U) & ~(MEM_ALIGNMENT-1U))))) + MEM_ALIGNMENT - 1U)))];\
static struct memp *memp_tab_ ## name;\
const struct memp_desc memp_ ## name = { \
LWIP_MEM_ALIGN_SIZE(size), \
(num), \
memp_memory_ ## name ## _base, \
&memp_tab_ ## name \
};

展开之后可以看出,各类型的内存池的内存由来和lwIP 内存堆一样,都是由数组分配的。这个宏定义的使用笔者会在memp.c 文件中讲解。

(4) memp.c 文件
在讲解函数之前,我们必须知道LWIP_MEMPOOL 和const memp_pools[MEMP_MAX]这两部分的内容,其中LWIP_MEMPOOL 指向LWIP_MEMPOOL_DECLARE 宏定义,该宏定义笔者已经在memp.h 文件展开过,稍后重点讲解,而const memp_pools[MEMP_MAX]数组是用来管理各类型的内存池描述符。下面笔者分别地讲解这两部分的内容,如下所示:

#define LWIP_MEMPOOL(name,num,size,desc)

LWIP_MEMPOOL_DECLARE(name,num,size,desc)
#include "lwip/priv/memp_std.h"

这里也是一样,对宏的巧妙运用,例如memp_std.h 只启用LWIP_RAW 和LWIP_UDP 类型的内存池,展开之后如下所示:

u8_t memp_memory_RAW_PCB_base[((((((num) * (MEMP_SIZE +
                                            (((size) + MEM_ALIGNMENT - 1U) & ~(MEM_ALIGNMENT - 1U))))) +
                                 MEM_ALIGNMENT - 1U)))];
static struct memp *memp_tab_RAW_PCB;
const struct memp_desc memp_RAW_PCB = {
    LWIP_MEM_ALIGN_SIZE(size),
    (num),
    memp_memory_TCPIP_MSG_API_base,
    &memp_tab_TCPIP_MSG_API};
u8_t memp_memory_UDP_PCB_base[((((((num) * (MEMP_SIZE +
                                            (((size) + MEM_ALIGNMENT - 1U) & ~(MEM_ALIGNMENT - 1U))))) +
                                 MEM_ALIGNMENT - 1U)))];
static struct memp *memp_tab_UDP_PCB;
const struct memp_desc memp_UDP_PCB = {
    LWIP_MEM_ALIGN_SIZE(size),
    (num),
    memp_memory_UDP_PCB_base,
    &memp_tab_UDP_PCB};\
};

LWIP_MEMPOOL_DECLARE 宏定义展开流程笔者已经上面讲解过,这里无需重复讲解。总的来说,这两段代码声明了各类内存池描述和管理信息,例如memp_desc memp_RAW_PCB 结构体,它描述了该类型的内存池的数量、大小、分配内存地址以及指向空闲内存池的指针。

const struct memp_desc *const memp_pools[MEMP_MAX] = {
#define LWIP_MEMPOOL(name, num, size, desc) &memp_##name,
#include "lwip/priv/memp_std.h"
};

这一个数组的大小就是由MEMP_MAX 变量声明,这个变量无需讲解,请看上面的内容。
若memp_std.h 只启用LWIP_RAW 和LWIP_UDP 类型的内存池,则这个数组展开之后如下所示:

const struct memp_desc* const memp_pools[MEMP_MAX] = {
	&memp_memp_RAW_PCB,
	&memp_memp_UDP_PCB,
};

数组的第一个元素取memp_memp_RAW_PCB 地址,它就是我们前面展开之后的memp_RAW_PCB 变量。
memp_init 函数和memp_init_pool 函数
该函数是内存池的初始化,该函数如下所示:

void memp_init(void)
{
    u16_t i;
    /* 遍历,需要多少个内存池*/
    for (i = 0; i < LWIP_ARRAYSIZE(memp_pools); i++)
    {
        memp_init_pool(memp_pools[i]);
    }
}
void memp_init_pool(const struct memp_desc *desc)
{
    int i;
    struct memp *memp;
    *desc->tab = NULL;
    /* 内存对齐*/
    memp = (struct memp *)LWIP_MEM_ALIGN(desc->base);
    /* 将内存块链接成链表形式*/
    for (i = 0; i < desc->num; ++i)
    {
        memp->next = *desc->tab;
        *desc->tab = memp;
        /* 地址偏移*/
        memp = (struct memp *)(void *)((u8_t *)memp +
                                       MEMP_SIZE + desc->size);
    }
}

从上述源码可以看出,每一个类型的描述符都是用来管理和描述该类型的内存池,这些同一类型的内存池里面包含了指向下一个节点的指针,根据第二个for 循环语句让这些同一类型的内存池以链表的形式链接起来,最后不断的循环,我们可以得到以下示意图:

在这里插入图片描述

从上图可知,memp_pool 数组包含了多个类型的内存池描述符,这些描述符管理同一类型的内存池,这些内存池以链表的形式链接起来,最后形成一个单向链表。注:同一类型的内存池都是在同一个数组分配得来,而base 指针指向该数组的首地址,tab 指针指向第一个空闲的内存池,若用户向申请一个内存池,则从tab 指针指向的内存池分配,分配完成之后tab 指针偏移至下一个空闲内存池的地址。

memp_malloc 函数和memp_malloc_pool 函数
前面讲解到,内存池具有多种类型的,所以用户申请内存池时,必须知道申请内存池的类型是哪个?lwIP 内存池申请函数为memp_malloc,该函数如下所示:

void *
memp_malloc(memp_t type)
{
    void *memp;
    memp = do_memp_malloc_pool(memp_pools[type]);
    return memp;
}
static void *
do_memp_malloc_pool(const struct memp_desc *desc)
{
    struct memp *memp;
    memp = *desc->tab;
    if (memp != NULL)
    {
        *desc->tab = memp->next;
        return ((u8_t *)memp + MEMP_SIZE);
    }
    else
    {
    }
    return NULL;
}

memp_malloc 函数需要传入申请内存池的类型,如UDP_PCB…,接着根据传入的类型来查找对应的内存池描述符,查找完成之后根据该内存池描述符的tab 指针指向内存池分配给用户,并且把tab 指针偏移至下一个空闲内存池。分配流程如下图所示:

在这里插入图片描述

memp_free 函数与memp_free_pool 函数
内存池释放函数非常简单,它需要传入两个形参,第一个是释放内存池的类型,第二个是释放内存池的地址。lwIP 内核根据这两个形参就可以知道该类型的内存池描述符位置和该类型内存池描述符的哪个内存池需要释放。内存池释放函数如下所示:

void memp_free(memp_t type, void *mem)
{
    if (mem == NULL) /* 判断内存块的起始地址释放为空*/
    {
        return;
    }
    do_memp_free_pool(memp_pools[type], mem);
}
static void do_memp_free_pool(const struct memp_desc *desc, void *mem)
{
    struct memp *memp;
    /* 据内存块的地址偏移得到内存块的起始地址*/
    memp = (struct memp *)(void *)((u8_t *)mem - MEMP_SIZE);
    /* 内存块的下一个就是链表中的第一个空闲内存块*/
    memp->next = *desc->tab;
    /* *desc->tab指向memp内存块中*/
    *desc->tab = memp;
}

释放函数非常简单,只需对内存池描述符的tab 指针偏移至释放的内存池。释放流程如下图所示:

在这里插入图片描述

使用C 库管理内存策略

lwIP 内核是可以支持C 标准库管理策略,它与lwIP 内存堆管理策略二者只能选其一。打开mem.c 文件找到MEM_LIBC_MALLOC 配置项如下源码所示:

/* in case C library malloc() needs extra protection,
 * allow these defines to be overridden.
 */
#ifndef mem_clib_free
#define mem_clib_free free
#endif
#ifndef mem_clib_malloc
#define mem_clib_malloc malloc
#endif
#ifndef mem_clib_calloc
#define mem_clib_calloc calloc
#endif
#if LWIP_STATS && MEM_STATS
#define MEM_LIBC_STATSHELPER_SIZE LWIP_MEM_ALIGN_SIZE(sizeof(mem_size_t))
#else
#define MEM_LIBC_STATSHELPER_SIZE 0
#endif

上述的free、malloc 以及calloc 就是C 库中的内存管理函数。注:C 标准库内存管理不能与相邻的空闲内存块合并,且容易造成内存碎片。

lwIP 网络接口管理(管理不同网卡)

lwIP 支持多网口设计,它是使用netif 来描述每种网络接口的特性:如IP 地址、接口状态等。为了实现对所有网络接口的有效管理,协议栈内部使用了一个名为netif 的网络接口结构来描述各种网络设备,如果项目中使用多个网卡,那么lwIP 是如何管理这些网卡的呢?链表netif_list就是管理多个netif 网络接口的,当上层应用有数据要发送的时候,lwIP 会从netif_list 链表中选择一个合适的网卡来将数据发送出去。

网络接口结构netif

总所周知,网卡的种类多种多样的,对于lwIP 来说,它是怎么样兼容众多网卡的呢?

lwIP 有一个数据结构—netif 来描述一个网卡,因为网卡是与硬件相关的,不同的硬件处理的方式也是不同的,所以lwIP 提供了统一接口函数来管理这些网卡。由于网卡的种类繁多,所以各个网卡的底层函数需要用户来完成,例如网卡的初始化、网卡的接收、发数据等函数,同样lwIP 底层得到网络数据时,需要层层递交才会传入内核处理,相反lwIP 发送数据也是调用网卡发送函数。对于没有接触lwIP 的学员来说,我们该怎么样写底层驱动呢?lwIP 还是做的挺好的,它已经提供了一个ethernetif.c 文件,该文件是底层接口的驱动模版,用户根据自己的网络设备参照修改即可。

下面笔者来讲解netif 的数据结构,该数据结构是在netif.h 文件中定义的,该结构如下所示:

struct netif
{
    /* 指向下一个netif结构的指针*/
    struct netif *next;
    /* IP地址相关配置*/
    ip_addr_t ip_addr; /* 网络接口的IP 地址*/
    ip_addr_t netmask; /* 子网掩码*/
    ip_addr_t gw;      /* 网关地址*/
    /* 该函数向IP层输入数据包*/
    netif_input_fn input;
    /* 该函数发送IP包--检测目标IP地址的MAC地址等操作*/
    netif_output_fn output;
    /* 该函数实现底层数据包发送*/
    netif_linkoutput_fn linkoutput;
    /* 该字段用户可以自由设置,例如用于指向一些底层设备相关的信息*/
    void *state;
    void *client_data[LWIP_NETIF_CLIENT_DATA_INDEX_MAX + LWIP_NUM_NETIF_CLIENT_DATA];
    /* 该接口允许的最大数据包长度,大于1500则分片处理*/
    u16_t mtu;
    /* 该接口物理地址长度*/
    u8_t hwaddr_len;
    /* 该接口的物理地址*/
    u8_t hwaddr[NETIF_MAX_HWADDR_LEN];
    /* 该接口的状态、属性字段*/
    u8_t flags;
    /* 该接口的名字*/
    char name[2];
    /* 接口的编号*/
    u8_t num;
    /* 需要发送的路由器请求消息的数量*/
    u8_t rs_count;
};

该结构体包含了多个字段,这些字段的作用如下:
(1) next: 该字段指向下一个neitif 类型的结构体,因为lwIP 可以支持多个网络接口,当设备有多个网络接口的话lwIP 就会把所有的netif 结构体组成链表来管理这些网络接口。
(2) ipaddr,netmask 和gw:分别为网络接口的IP 地址、子网掩码和默认网关。
(3) input:此字段为一个函数,这个函数将网卡接收到的数据交给IP 层。
(4) output:此字段为一个函数,当IP 层向接口发送一个数据包时调用此函数。这个函数通常首先解析硬件地址,然后发送数据包。此字段我们一般使用etharp.c 中的etharp_output()函数。
(5) linkoutput:此字段为一个函数,该函数被ARP 模块调用,完成网络数据的发送。上面说的etharp_output 函数将IP 数据包封装成以太网数据帧以后就会调用linkoutput 函数将数据发送出去。
(6) state:用来定义一些关于接口的信息,用户可以自行设置。
(7) mtu:网络接口所能传输的最大数据长度,一般设置为1500。
(8) hwaddr_len:网卡MAC 地址长度,6 个字节。
(9) hwaddr:MAC 地址。
(10) flags:网络的接口状态,属性信息字段。
(11) name:网卡的名字。
(12) num:编号从0 开始,此字段为协议栈为每个网络接口设置的一个编号。
(13) rs_count:发送的路由器请求消息的数量。

这些字段就是用来描述各个网卡的差异,每一个网卡都使用一个netif 结构体来抽象,多个网卡就有多个netif,这些netif 以链表的形式链接起来,形参一个单向的链表。
这些netif 链表的首个节点由netif_list 指针指向,lwIP 内核就是使用netif_list 指针对netif链表进行遍历查询。管理和描述netif 链表由三个全局变量,这些变量如下所示:

struct netif *netif_list;    /* 网络接口链表指针*/
struct netif *netif_default; /* 哪个网络接口(多网口时候) */
static u8_t netif_num;       /* 为网口分配唯一标识*/

netif_default 指针指向netif 链表的默认网卡,如网络层下发一个数据包时,系统优先选择netif_default 指针指向的网卡发送数据,如该网卡没有响应,则选择其他的网卡发送。
netif_num 描述网卡的数量。

下面笔者重点讲解netif.c 重要的几个函数,这些函数如下所示:
(1) netif_add 函数
该函数是把新创建的netif 插入到netiflist 队列当中,以表示添加一个网络接口,该函数如下所示:

struct netif *
netif_add(struct netif *netif,
          const ip4_addr_t *ipaddr, const ip4_addr_t *netmask,
          const ip4_addr_t *gw,
          void *state, netif_init_fn init, netif_input_fn input)
{
    /* 清空主机IP 地址、子网掩码、网关等信息。*/
    ip_addr_set_zero_ip4(&netif->ip_addr);
    ip_addr_set_zero_ip4(&netif->netmask);
    ip_addr_set_zero_ip4(&netif->gw);
    netif->output = netif_null_output_ip4;		// 调用实际的发送函数 netif_null_output_ip4
    /* 传输的最大数据长度*/
    netif->mtu = 1500;
    /* 网络的接口状态*/
    netif->flags = 0;
    memset(netif->client_data, 0, sizeof(netif->client_data));
    /* 传递进来的参数填写网卡state、input等字段的相关信息*/
    netif->state = state;
    /* 并为当前网卡分配唯一标识num */
    netif->num = netif_num;
    /* 网卡输入*/
    netif->input = input;					//调用实际的接收函数 input
    /* 调用网卡设置函数netif_set_addr()设置网卡IP 地址、子网掩码、网关*/
    netif_set_addr(netif, ipaddr, netmask, gw);
    /* 为netif调用用户指定的初始化函数*/
    if (init(netif) != ERR_OK)
    {
        return NULL;
    }
    /* 将这个netif添加到列表中*/
    netif->next = netif_list;
    netif_list = netif;
    mib2_netif_added(netif);
    netif_invoke_ext_callback(netif, LWIP_NSC_NETIF_ADDED, NULL);
    return netif;
}

从上述源码可以看出,每一个netif 结构体就是对一个网卡进行抽象,例如该网卡的收发函数、状态等信息。根据上述函数的运行流程,可得到以下示意图:
①只有一个网络接口

在这里插入图片描述

②两个网络接口

在这里插入图片描述
注:新插入的netif 结构体是在netiflist 队列的首部插入。
(2) netif_set_default 函数
该函数就是设置某一个netif 结构体为默认的网卡,lwIP 内核优先对这个网卡操作,该函数如下所示:

void netif_set_default(struct netif *netif)
{
    if (netif == NULL)
    {
        /* 删除默认路由*/
        mib2_remove_route_ip4(1, netif);
    }
    else
    {
        /* 添加默认路由*/
        mib2_add_route_ip4(1, netif);
    }
    netif_default = netif; /* 选择那个网络接口*/
}
/*********************怎么使用函数netif_set_default()*********************/
/* 通过该函数,将网络接口添加到链表中*/
netif_add(&xnetif, &ipaddr, &netmask, &gw, NULL, &ethernetif_init, &tcpip_input);
/* 注册默认的网络接口*/
netif_set_default(&xnetif);

方法很简单,就是让netif_default 指针指向默认的网卡。

lwIP 网络数据包解析

TCP/IP 协议本质上就是对数据包的处理过程,lwIP 作者为了提高对数据包的处理工作效率,它提供一种高效的数据包管理机制,使得各层之间对数据包灵活操作,同时避免在各层之间的复制数据的巨大开销和减少各层间的传递时间。在 linux 的 BSD 协议中,它描述数据包的结构体叫做 mbuf,而lwIP 与它类似的结构叫做 pbuf,pbuf 数据包的种类和大小也可以说是多种多样的,从网卡读取出来的数据包可以是一千个字节也可以是几个字节的 IP 数据报,这些数据包可能存在于 RAM 和 ROM 中,这个根据用户来决定的,所以 lwIP 为了处理的数据高效,它需要把这些数据进行统一的管理。

在这里插入图片描述
在这里插入图片描述

TCP/IP 协议各层间的操作

我们知道传统的TCP/IP 协议各层之间是独立存在的,每一层只处理该层的数据,它们绝对不允许越界读写数据,如果lwIP 按照这种严格的分层模式来实现TCP/IP 协议,会使数据包在各层间的递交变得非常慢,它涉及到一系列的内存拷贝的问题,所以系统总体性能也会受到影响。因此,lwIP 内部并没有采用完整的分层结构,它会假设各层间的部分数据结构和实现原理在其他层是可见的,这样在数据包递交过程中,各层协议可以直接对数据包中属于其他层次协议的字段进行操作。

从上述可以看出,lwIP 的优点有以下几个:

  • ①不需要数据层层拷贝。
  • ②用户程序可以直接访问内部各层数据包。
  • ③各个层次之间存在交叉存取数据的现象,既节省系统的空间也节省处理的时间,而且更加灵活。
  • ④lwIP 的内存共享机制,使得应用程序能直接对协议栈内核的内存区域直接操作,减少时间和空间的损耗。

lwIP 的线程简介

在操作系统中,任务的创建与任务管理是常见的东西,如果把协议栈的各层变成独立的任务或者线程,那么会导致各层之间是严格分层的,在这种模式下,能够使编程简便、代码组织灵活,但是缺点也是很明显的,例如数据递交时需要进行拷贝和切换任务,任务或者线程频繁切换可能对用户程序不能够准时的处理,一个数据包在各个层次间的递交至少需要进行3 次切换任务,如应用层发送数据时,需要切换到传输层任务处理,当传输层处理完毕之后会把数据报递交给网络层,由此类推,导致任务频繁切换使得协议栈的效率低下。

还有一种方法就是协议栈与操作系统结合,相当于把协议栈成为操作系统的一部分,这样用户任务与协议栈之间通过操作系统的API 函数实现,虽然提高了效率,各层也可以交叉存取,但是协议栈与操作系统融合会导致很严重的后果,总所周知,操作系统最大的优势是实时性高,能准确的运行相关的线程,如果协议栈成为了操作系统的一部分,那么协议栈处理的数据包过慢的话,会导致操作系统的实时性变低。

lwIP 采用了另一种方式,让协议栈与操作系统相互隔离,这样不会影响操作系统的实时性,协议栈只作为操作系统的一个独立的任务,这样我们可以得出两个方法,第一种方法就是让用户程序驻留在协议栈任务里,协议栈通过回调函数实验用户与协议栈之间的数据交互,这个也是 lwIP 所说的RAW API 编程。第二种方法就是用户程序可以作为操作系统的独立任务,用户任务与协议栈任务之间的通信通多 IPC 通信机制交互,这种在lwIP 叫做NETCONN API和Socket API 编程。

网络数据包 pbuf 结构

lwIP 使用 pbuf 对数据进行发送与接收,灵活的pbuf 结构体使得数据在不同层之间传输时可以减少内存的开销以及减少内存复制所占用的时间,一切都是为了节约内存,提高数据在不同层之间传递的速度。lwIP 源码中的pbuf.c 和pbuf.h 这两个文件就是关于pbuf 的,pbuf 结构如下源码所示:

struct pbuf
{
    struct pbuf *next;	/* pbuf链表中指向下一个pbuf结构*/
    void *payload;		/* 数据指针,指向该pbuf所记录的数据区域,通过指针偏移*/
    u16_t tot_len;		/* 当前pbuf及后续所有pbuf中所包含的数据总长度*/
    u16_t len;			/* 当前pbuf中数据的长度*/
    u8_t type;			/* 当前pbuf的类型*/
    u8_t flags;			/* 状态位未用到*/
    LWIP_PBUF_REF_T ref;/* 指向该pbuf的指针数,即该pbuf被引用的次数*/
    u8_t if_idx;		/* 对于传入的数据包,它包含输入netif的索引*/
};

pbuf 结构体具有多个字段,这些字段的作用如下所示:

在这里插入图片描述

从表可以看出,pbuf 具有四个类型,它们的数据存储在不同的区域,下面笔者重点讲解着四个类型的pbuf。

在这里插入图片描述

(1) PBUF_RAM 类型
PBUF_RAM 是lwIP 用的最多的一种类型,pbuf 空间大小是通过内存堆来分配的,一般协议栈中要发送的数据都是采用这种形式,这个类型也是常用的类型之一,申请PBUF_RAM 类型的pbuf 时协议栈会在内存堆中分配相应空间,这里的大小包括如前面所述的pbuf 结构和相应数据缓冲区的大小,并且它们是在一片连续的存储空间。分配完成后的结构如下图所示:

在这里插入图片描述

注:payload 指向并不一定是数据区域的首地址,可以设定一定的offset 偏移,这个offset偏移量常用来存储TCP 报文首部、IP 首部等。当然layer 的大小也可以是0,具体是多少就与数据包的申请方式有关。

(2) PBUF_POOL 类型
PBUF_POOL 类型和PBUF_RAM 类型的pbuf 有很大的相似之处,不同之处时它的空间通过内存池分配得到的,这种类型的pbuf 可以在极短的时间内得到分配。
在网卡接收数据包的时候,我们就使用这种方式包装数据或者存储接收到的数据。其中在系统初始化内存池的时候,还会初始化两类与数据报pbuf 密切相关的POOL,如下源码所示:

LWIP_PBUF_MEMPOOL(PBUF, MEMP_NUM_PBUF, 0, "PBUF_REF/ROM")
LWIP_PBUF_MEMPOOL(PBUF_POOL,PBUF_POOL_SIZE, PBUF_POOL_BUFSIZE, "PBUF_POOL")

内存池是一个固定大小的内存块,若用户数据大于固定大小的内存池,则lwIP 内核会以多个固定大小的内存池来存储这些数据,存储完成之后系统把多个pbuf 以链表的形式链接起来,构建了一个单向链表,如下图所示:

在这里插入图片描述

(3) PBUF_ROM&&和PBUF_REF 类型
剩余的两个PBUF_ROM 和PBUF_REF 比较类似,它们都是在内存池中分配一个相应的pbuf 结构,但不申请数据区的空间,它们两者的区别在于PBUF_ROM 指向ROM 空间内的数据,后者指向RAM 空间内的某段数据。在发送某些静态数据时,可以采用这两种类型的pbuf,这可以大大节省协议栈的内存空间,结构如下图所示:

在这里插入图片描述

另外,对于一个数据包来讲,它可能使用上述任意的pbuf 类型来描述,还可以一大串不同类型的pbuf 连在一起,共同保存一个数据包的数据,如下图所示:

在这里插入图片描述

lwIP 网络数据包pbuf 提供了5 个函数,这些函数如下所示:

在这里插入图片描述

  1. pbuf_alloc 函数
    该函数根据类型、大小和偏移来申请pbuf 空间,若该函数返回NULL,则申请失败。
static void
pbuf_init_alloced_pbuf(struct pbuf *p, void *payload, u16_t tot_len, u16_t len, pbuf_type type, u8_t flags)
{
    p->next = NULL;                /* 指向NULL */
    p->payload = payload;          /* 指向数据区域*/
    p->tot_len = tot_len;          /* 总长度*/
    p->len = len;                  /* 该pbuf长度*/
    p->type_internal = (u8_t)type; /* 申请的pbuf类型*/
    p->flags = flags;              /* 状态位*/
    p->ref = 1;                    /* 指向该pbuf的指针数,即该pbuf被引用的次数*/
    p->if_idx = NETIF_NO_INDEX;    /* 对于传入的数据包,它包含输入netif的索引*/
}
struct pbuf *
pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type)
{
    struct pbuf *p;
    u16_t offset = (u16_t)layer; /* 申请那个层的首部*/
    /* 判断以太网首部*/
    switch (type)
    {
    case PBUF_REF: /* 失败*/
    case PBUF_ROM:
        p = pbuf_alloc_reference(NULL, length, type);
        break;
    case PBUF_POOL:
    {
        struct pbuf *q, *last;
        u16_t rem_len; /* 总大小*/
        p = NULL;
        last = NULL;
        rem_len = length; /* rem_len赋值为总长度*/
        do
        {
            u16_t qlen; /* 减去首部的长度*/
            /* 申请内存池*/
            q = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL);
            if (q == NULL) /* 申请内存池失败*/
            {
                PBUF_POOL_IS_EMPTY();
                if (p)
                {
                    pbuf_free(p);
                }
                return NULL;
            }
            /* 总长度减去offset(首部大小)并赋值给qlen(去除首部的长度)
            LWIP_MIN(x , y) (((x) < (y)) ? (x) : (y)) */
            qlen = LWIP_MIN(rem_len, (u16_t)(PBUF_POOL_BUFSIZE_ALIGNED – LWIP_MEM_ALIGN_SIZE(offset)));
            /* 分配后初始化struct pbuf成员*/
            pbuf_init_alloced_pbuf(q, LWIP_MEM_ALIGN((void *)((u8_t *)q + SIZEOF_STRUCT_PBUF + offset)),
                                   rem_len, qlen, type, 0);
            if (p == NULL) /* 第一次分配p必定指向NULL */
            {
                /* pbuf链分配头*/
                p = q;
            }
            else
                /* 让前面的pbuf指向这个pbuf */
                last->next = q;
        }
        last = q;
        /* 判断是否还有剩余长度*/
        rem_len = (u16_t)(rem_len - qlen);
        offset = 0;
    }
        while (rem_len > 0)
            ; /* 如果有剩余,还需要执行一次do语句*/
        break;
    }
case PBUF_RAM:
{
    u16_t payload_len = (u16_t)(LWIP_MEM_ALIGN_SIZE(offset) +
                                LWIP_MEM_ALIGN_SIZE(length));
    mem_size_t alloc_len =
        (mem_size_t)(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF) + payload_len);
    if ((payload_len < LWIP_MEM_ALIGN_SIZE(length)) ||
        (alloc_len < LWIP_MEM_ALIGN_SIZE(length)))
    {
        return NULL;
    }
    /* 如果要在RAM中分配pbuf,请为它分配内存。*/
    p = (struct pbuf *)mem_malloc(alloc_len);
    if (p == NULL)
    {
        return NULL;
    }
    pbuf_init_alloced_pbuf(p, LWIP_MEM_ALIGN((void *)((u8_t *)p + SIZEOF_STRUCT_PBUF + offset)),
                           length, length, type, 0);
    break;
}
default:
    return NULL;
}
return p;
}

此函数首先判断申请pbuf 的类型,根据type 的值来运行相应的代码段,layer 变量是为了让pbuf 中的payload 指针偏移,lwIP 网络数据包pbuf 就是根据这个指针偏移来添加各层的首部。

  1. pbuf_free 函数
    此函数是对各类型的数据包pbuf 进行释放,该函数实现原理如下所示:
u8_t pbuf_free(struct pbuf *p)
{
    u8_t alloc_src;
    struct pbuf *q;
    u8_t count;
    /* 如果数据包为空则返回0 */
    if (p == NULL)
    {
        return 0;
    }
    PERF_START;
    count = 0;
    /* 判断数据包不为空*/
    while (p != NULL)
    {
        LWIP_PBUF_REF_T ref;
        SYS_ARCH_DECL_PROTECT(old_level);
        SYS_ARCH_PROTECT(old_level);
        /* 减少引用计数(指向pbuf的指针数) */
        ref = --(p->ref);
        SYS_ARCH_UNPROTECT(old_level);
        if (ref == 0)
        {
            /* 为了下一次迭代,请记住链中的下一个pbuf */
            q = p->next;
            alloc_src = pbuf_get_allocsrc(p);
#if LWIP_SUPPORT_CUSTOM_PBUF
            /* is this a custom pbuf? */
            if ((p->flags & PBUF_FLAG_IS_CUSTOM) != 0)
            {
                struct pbuf_custom *pc = (struct pbuf_custom *)p;
                pc->custom_free_function(p);
            }
            else
#endif /* LWIP_SUPPORT_CUSTOM_PBUF */
            {
                /* 判断释放的内存池的类型*/
                if (alloc_src == PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF_POOL)
                {
                    memp_free(MEMP_PBUF_POOL, p);
                    /* is this a ROM or RAM referencing pbuf? */
                }
                else if (alloc_src == PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF)
                {
                    memp_free(MEMP_PBUF, p);
                    /* type == PBUF_RAM */
                }
                else if (alloc_src == PBUF_TYPE_ALLOC_SRC_MASK_STD_HEAP)
                {
                    mem_free(p);
                }
                else
                {
                }
            }
            count++;
            /* 继续到下一个pbuf */
            p = q;
        }
        else
        {
            p = NULL;
        }
    }
    return count;
}

此函数可以分为两个部分讲解,第一、对pbuf 的ref 参数减1 操作,并调用memp_free/mem_free 释放内存池或内存堆,第二、如果数据包具有两个或者两个以上的(pbuf 链表),也
是和第一点一样的操作。

  1. pbuf_realloc 函数
    把相应的pbuf 链表尾部释放一定的空间,并将在数据包pbuf 的数据长度减少到某个长度值,注意:该函数只是修改pbuf 中的长度字段值,并不释放对应的内存池空间。

  2. pbuf_header 函数
    用于调整pbuf 的payload 指针(向前或向后移动一定字节数),可以调用pbuf_header 函数使payload 指针指向数据区前的首部字段,这就为各层对数据包首部的操作提供了方便。当然,进行这个操作的时候,len 和tot_len 字段值也会随之改动。

  3. pbuf_take 函数
    用于向 pbuf 的数据区域拷贝数据。pbuf_copy 函数用于将一个任何类型的pbuf 中的数据拷贝到一个PBUF_RAM 类型的pbuf 中。pbuf_chain 函数用于连接两个pbuf(链表)为一个pbuf 链表。pbuf_ref 函数用于将pbuf 中的值加1。

网络接口接收数据

STM32 基本上使用 ETH 接口来接收数据后产生一个 ETH 中断,在中断中释放一个信号量(s_xSemaphore)通知网络接口任务(ethernetif_input)处理接收的数据,这个任务对数据封装成消息并传递给tcpip_mbox 邮箱,以邮箱发送消息。lwIP 内核有一个协议栈线程,它的作用就是接收tcpip_mbox 邮箱的消息,并且对接收的消息进行解析处理,在处理之前先判断消息的类型,lwIP 内核根据消息的类型处理不同的代码段,如下图所示:

在这里插入图片描述

从上图可以看出,ethernetif_input 是一个接收线程的任务函数,它用来获取ETH 中断释放的信号量,若接收到信号量,则调用low_level_input 函数获取描述符管理缓冲区的数据,并且把这些数据调用tcp_input 函数构建消息,以tcpip_mbox 邮箱的方式发送消息。lwIP 内核在初始化时,创建了TCP/IP 线程,它的作用是接收tcpip_mbox 邮箱的消息,并且对接收的消息进行解析处理,在处理之前先判断消息的类型,lwIP 内核根据消息的类型处理不同的代码段。

lwIP 超时处理

在lwIP 中很多时候会使用到超时处理,超时处理的实现是TCP/IP 协议栈中一个重要部分。
它为每个与外界网络连接的任务都设定了timeout 属性,即等待超时时间。

lwIP 中为什么需要做超时处理呢?这可从其实现的TCP/IP 协议栈功能可以知道,TCP 的建立连接超时、重传超时机制,IP 分片数据报的重装等待超时,ARP 缓存表项的时间管理、ping 接收数据包超时处理等等,都需要使用超时操作来处理。超时处理的相关代码在timeouts.c/h 中实现,下面笔者分别地讲解这两个文件的内容。

(1) timeouts.h 文件
该文件主要定义了两个结构体,它们分别为lwip_cyclic_timer 和sys_timeo,第一个结构体定义了超时等待时间和超时处理函数,另外一个是管理这些超时的定时器,着两个结构体的原型如下所示:

  1. lwip_cyclic_timer 结构体:
struct lwip_cyclic_timer
{
    u32_t interval_ms;                 /* 超时间隔*/
    lwip_cyclic_timer_handler handler; /* 超时处理函数*/
};

const struct lwip_cyclic_timer lwip_cyclic_timers[] = {
    {TCP_TMR_INTERVAL, HANDLER(tcp_tmr)},
    {IP_TMR_INTERVAL, HANDLER(ip_reass_tmr)},
    {ARP_TMR_INTERVAL, HANDLER(etharp_tmr)},
    {DHCP_COARSE_TIMER_MSECS, HANDLER(dhcp_coarse_tmr)},
    {DHCP_FINE_TIMER_MSECS, HANDLER(dhcp_fine_tmr)},
    {AUTOIP_TMR_INTERVAL, HANDLER(autoip_tmr)},
    {IGMP_TMR_INTERVAL, HANDLER(igmp_tmr)},
    {DNS_TMR_INTERVAL, HANDLER(dns_tmr)},
};

可以看到,interval_ms 变量就是超时等待时间,而handler 就是超时处理函数,即超时事件,若超时了,则触发一个超时事件。lwip_cyclic_timers 数组就是定义了lwIP 内核所需的超时定时器,即超时事件。

  1. sys_timeo 结构体:
typedef void (*sys_timeout_handler)(void *arg);
struct sys_timeo
{
    struct sys_timeo *next; /* 下一个超时事件的指针*/
    u32_t time;             /* 当前超时事件的等待时间*/
    sys_timeout_handler h;  /* 指向超时的回调函数*/
    void *arg;              /* 超时的回调函数参数*/
};

这个结构体是用来管理这些超时事件,它的next 指针指向下一个超时事件,最后这些超时事件形成了单向链表。这些超时事件都调用同一的超时回调函数,这个函数由h 函数指针指向,最后根据arg 回调函数形参来调用哪个超时事件处理。

注:time 变量等于系统节拍加上超时等待时间,例如系统当前节拍是1s,超时定时器的等待时间为5s,所以系统在节拍等于6s时才执行超时事件。

(2) timeouts.c 文件

  1. 注册超时事件:
    lwip_cyclic_timers 保存了lwIP 所需的超时事件,这些超时事件由sys_timeouts_init 函数插入到超时链表当中,该函数如下所示:
void sys_timeouts_init(void)
{
    size_t i;
    for (i = (LWIP_TCP ? 1 : 0); i < LWIP_ARRAYSIZE(lwip_cyclic_timers); i++)
    {
        sys_timeout(lwip_cyclic_timers[i].interval_ms, cyclic_timer,
                    LWIP_CONST_CAST(void *, &lwip_cyclic_timers[i]));
    }
}

此函数很简单,获取lwip_cyclic_timers 元素地址和等待超时时间之后调用sys_timeout 函数把超时事件插入到超时链表当中。sys_timeout 函数如下所示:

#define LWIP_MAX_TIMEOUT 0x7fffffff
/* 当前插入超时事件时间与next_timeout指向超时事件时间对比是否大于0x7fffffff
如果t – compare_to为负值的话,由于类型为u32_t所以导致该值比0x7fffffff 大,
如果比LWIP_MAX_TIMEOUT 大则为1,否则为0*/
#define TIME_LESS_THAN(t, compare_to) ((((u32_t)((t) - (compare_to))) > \
                                        LWIP_MAX_TIMEOUT)               \
                                           ? 1                          \
                                           : 0)
void sys_timeout(u32_t msecs, sys_timeout_handler handler, void *arg)
{
    u32_t next_timeout_time;
    /* 由TIME_LESS_THAN宏处理的溢出*/
    next_timeout_time = (u32_t)(sys_now() + msecs);
    sys_timeout_abs(next_timeout_time, handler, arg);
}

从这里可以看出,next_timeout_time 变量等于系统当前节拍加上某个超时事件的等待时间,其实next_timeout_time 变量最终赋给sys_timeo 结构体下的time 成员变量。这个超时事件由这个sys_timeout_abs 函数插入到超时链表当中,该函数如下所示:

static void
sys_timeout_abs(u32_t abs_time, sys_timeout_handler handler, void *arg)
{
    struct sys_timeo *timeout, *t;
    /* 申请节点内存*/
    timeout = (struct sys_timeo *)memp_malloc(MEMP_SYS_TIMEOUT);
    if (timeout == NULL)
    { /* 申请内存失败直接返回*/
        return;
    }
    /* 节点各变量赋值*/
    timeout->next = NULL;
    timeout->h = handler;
    timeout->arg = arg;
    /* abs_time = (u32_t)(sys_now() + msecs) */
    timeout->time = abs_time;
    /* 如果创建的是第一个定时器,则不用特殊处理,
    next_timeout是一个全局指针,指向定时器链表中第一个定时器*/
    if (next_timeout == NULL)
    {
        next_timeout = timeout;
        return;
    }
    /* 如果新添加的定时器小于当前链首定时器的时长,则进入该代码段*/
    if (TIME_LESS_THAN(timeout->time, next_timeout->time))
    {
        timeout->next = next_timeout;
        next_timeout = timeout;
    }
    else
    {
        for (t = next_timeout; t != NULL; t = t->next)
        {
            if ((t->next == NULL) || TIME_LESS_THAN(timeout->time, t->next->time))
            {
                timeout->next = t->next;
                t->next = timeout;
                break;
            }
        }
    }
}

首先此函数为超时事件申请内存,以内存池的方式申请,接着对超时事件各个成员变量赋值,可以看到h 函数指针指向超时回调函数,arg 指针指向lwip_cyclic_timers 数组的某个元素地址,超时回调函数就是根据arg 形参来运行某个超时事件,time 变量等于了next_timeout_time 变量即当前系统节拍加上超时等待时间,最后插入到超时链表当中。下面笔者使用几个示意图来讲解这个函数,如下所示:

在这里插入图片描述

从上图可以知道,该超时事件的time 等于21 即当前系统节拍加上超时事件等待函数,它的next 指针指向为NULL,因为一开始这个超时链表没有超时事件,所以next_timeout 指向新插入的超时事件。
当我们插入第二个超时事件时,系统需要逐一判断这个超时事件的time 是否大于超时链表挂载的超时事件time,逐一对比之后发送插入的超时事件time 比超时链表挂载的超时事件time 要大,则系统把这个超时事件插入这张链表的尾部。如下图所示:

在这里插入图片描述
如果插入的超时事件time 与挂载超时链表的超时事件time 对比之后,发现插入的超时事件time 在两个挂载的超时事件time 之间即a<time<b,那么要插入的超时事件挂载至这两个挂载的超时事件中间,如下图所示:

在这里插入图片描述

  1. 删除超时事件:
    从超时事件链表中删除一个超时事件可调用sys_untimeout 函数删除,如下源码所示:
void sys_untimeout(sys_timeout_handler handler, void *arg)
{
    struct sys_timeo *prev_t, *t;
    /* 从链表头开始遍历这个链表*/
    for (t = next_timeout, prev_t = NULL; t != NULL; prev_t = t, t = t->next)
    {
        /* 查找删除的超时事件,判断超时事件的回调函数与函数参数是否一致*/
        if ((t->h == handler) && (t->arg == arg))
        {
            if (prev_t == NULL)
            {
                next_timeout = t->next;
            }
            else
            {
                prev_t->next = t->next;
            }
            memp_free(MEMP_SYS_TIMEOUT, t);
            return;
        }
    }
    return;
}

此函数非常简单,只需遍历这个超时链表,在遍历过程中判断超时事件的回调函数与函数参数是否一致,若一致,则对超时链表的超时事件排序,排序完成之后调用memp_free 删除这个超时事件。

在这里插入图片描述

超时定时器检查:
不管是OS 的还是裸机的都可以对其进行超时检查和处理,lwIP 使用两个函数来实现超时检查处理。

  1. void sys_check_timeouts(void)函数
    这个函数是用于裸机部分的,用户可以在裸机的应用中周期性调用该函数,每次进来检查定时器链表上定时最短的定时器是否到期,如果没有到期,直接退出该函数,否则,执行该定时器回调函数,并从链表上删除该定时器,然后继续检查下一个定时器,直到没有一个定时器到期退出。
  2. tcpip_timeouts_mbox_fetch(sys_mbox_t *mbox, void **msg)函数
    这个函数在OS 线程中循环执行的,主要等待mbox 消息并可阻塞,如果等待mbox 时超时,则会同时执行超时事件处理,即调用超时回调函数,否则一直没有收到mbox 消息就会一直等待直到下一个超时时间并循环将所有超时定时器检查一遍( 内部调用了void sys_check_timeouts(void)),lwIP 中tcpip 线程就是靠这种方法,即处理了上层及底层的mbox消息,同时处理了所有需要定时处理的事件。

tcpip_thread(协议栈)线程

这个线程由tcpip_init 函数创建,该函数如下所示:

void tcpip_init(tcpip_init_done_fn initfunc, void *arg)
{
    lwip_init();
    tcpip_init_done = initfunc;
    tcpip_init_done_arg = arg;
    if (sys_mbox_new(&tcpip_mbox, TCPIP_MBOX_SIZE) != ERR_OK)
    {
        LWIP_ASSERT("failed to create tcpip_thread mbox", 0);
    }
#if LWIP_TCPIP_CORE_LOCKING
    if (sys_mutex_new(&lock_tcpip_core) != ERR_OK)
    {
        LWIP_ASSERT("failed to create lock_tcpip_core", 0);
    }
#endif /* LWIP_TCPIP_CORE_LOCKING */
    sys_thread_new(TCPIP_THREAD_NAME, tcpip_thread, NULL,
                   TCPIP_THREAD_STACKSIZE, TCPIP_THREAD_PRIO);
}

这个函数在lwip_init 函数调用,它负责几个任务,第一、创建邮箱为数据传输准备,第二、创建互斥锁为防止优先级翻转问题,第三、创建TCP/IP 线程。下面笔者重点讲解tcpip_thread 任务函数的实现源码,如下所示:

static void
tcpip_thread(void *arg)
{
    struct tcpip_msg *msg;
    LWIP_UNUSED_ARG(arg);
    LWIP_MARK_TCPIP_THREAD();
    LOCK_TCPIP_CORE();
    if (tcpip_init_done != NULL)
    {
        tcpip_init_done(tcpip_init_done_arg);
    }
    while (1)
    {
        LWIP_TCPIP_THREAD_ALIVE();
        /* 第一步:等待消息时,将在等待时处理超时*/
        /* TCPIP_MBOX_FETCH的宏定义为sys_timeouts_mbox_fetch
        等待消息并且处理超时事件*/
        TCPIP_MBOX_FETCH(&tcpip_mbox, (void **)&msg);
        if (msg == NULL) /* 如果没有等到消息就继续等待*/
        {
            continue;
        }
        tcpip_thread_handle_msg(msg);
    }
}
static void
tcpip_thread_handle_msg(struct tcpip_msg *msg)
{
    /* 第二步:等待到消息就对消息进行处理*/
    /* 不同类型进行不同的处理*/
    switch (msg->type)
    {
#if !LWIP_TCPIP_CORE_LOCKING
    /* 执行对应的API 函数*/
    case TCPIP_MSG_API:
        msg->msg.api_msg.function(msg->msg.api_msg.msg);
        break;
    case TCPIP_MSG_API_CALL:
        msg->msg.api_call.arg->err =
            msg->msg.api_call.function(msg->msg.api_call.arg);
        sys_sem_signal(msg->msg.api_call.sem);
        break;
#endif /* !LWIP_TCPIP_CORE_LOCKING */
#if !LWIP_TCPIP_CORE_LOCKING_INPUT
    /* 直接交给ARP 层处理*/
    case TCPIP_MSG_INPKT:
        if (msg->msg.inp.input_fn(msg->msg.inp.p,
                                  msg->msg.inp.netif) != ERR_OK)
        {
            pbuf_free(msg->msg.inp.p);
        }
        memp_free(MEMP_TCPIP_MSG_INPKT, msg);
        break;
#endif /* !LWIP_TCPIP_CORE_LOCKING_INPUT */
#if LWIP_TCPIP_TIMEOUT && LWIP_TIMERS
    /* 注册一个超时事件*/
    case TCPIP_MSG_TIMEOUT:
        sys_timeout(msg->msg.tmo.msecs, msg->msg.tmo.h, msg->msg.tmo.arg);
        memp_free(MEMP_TCPIP_MSG_API, msg);
        break;
    /* 删除一个超时事件*/
    case TCPIP_MSG_UNTIMEOUT:
        sys_untimeout(msg->msg.tmo.h, msg->msg.tmo.arg);
        memp_free(MEMP_TCPIP_MSG_API, msg);
        break;
#endif /* LWIP_TCPIP_TIMEOUT && LWIP_TIMERS */
    /* 通过回调方式执行一个回调函数
    他们的回调函数相同*/
    case TCPIP_MSG_CALLBACK:
        msg->msg.cb.function(msg->msg.cb.ctx);
        memp_free(MEMP_TCPIP_MSG_API, msg);
        break;
    case TCPIP_MSG_CALLBACK_STATIC:
        msg->msg.cb.function(msg->msg.cb.ctx);
        break;
    default:
        break;
    }
}

协议栈线程主要负责接收邮箱的消息、递交数据至网络层、遍历超时链表等任务。

lwIP 中的消息

在上一个小节笔者讲解了tcpip_thread 线程的作用,其中接收邮箱的消息到底如何构建,这里涉及到lwIP 数据包消息机制,它专门把ETH 中断接收的数据封装成消息,以邮箱的方式发送至tcpip_thread 线程处理,注:这里以带操作系统为例。

数据包消息(tcpip_msg)

/* 7种tcpip_msg消息类型*/
enum tcpip_msg_type
{
    TCPIP_MSG_API,            /* 用户调用应用层的接口时,就属于API消息类型*/
    TCPIP_MSG_API_CALL,       /* API 函数调用*/
    TCPIP_MSG_INPKT,          /* 底层数据包输入*/
    TCPIP_MSG_TIMEOUT,        /* 注册超时事件*/
    TCPIP_MSG_UNTIMEOUT,      /* 删除超时事件*/
    TCPIP_MSG_CALLBACK,       /* 执行回调函数*/
    TCPIP_MSG_CALLBACK_STATIC /* 执行静态回调函数*/
};
/* tcpip_msg结构体*/
struct tcpip_msg
{
    /* tcpip_msg消息的类型*/
    enum tcpip_msg_type type;
    /* 消息内容,共用体,不同消息类型使用不同的结构*/
    union
    {
        struct
        {
            /* 内核执行函数*/
            tcpip_callback_fn function;
            /* 执行函数的参数*/
            void *msg;
        } api_msg;
        struct
        {
            /* 回调函数*/
            tcpip_api_call_fn function;
            /* 回调函数的参数*/
            struct tcpip_api_call_data *arg;
            /* 用户同步的信号量*/
            sys_sem_t *sem;
        } api_call;
        struct
        {
            /* 接收的数据包*/
            struct pbuf *p;
            /* 接收的数据包的网络接口*/
            struct netif *netif;
            /* 输入的函数接口*/
            netif_input_fn input_fn;
        } inp;
        struct
        {
            /* tcpip回调函数*/
            tcpip_callback_fn function;
            /* 回调函数参数*/
            void *ctx;
        } cb;
        struct
        {
            /* 超时时间*/
            u32_t msecs;
            /* 超时执行的回调函数*/
            sys_timeout_handler h;
            /* 传入超时回调函数的形参*/
            void *arg;
        } tmo;
#endif /* LWIP_TCPIP_TIMEOUT && LWIP_TIMERS */
    } msg;
};

上述的源码中,我们可以看到消息结构的msg 字段是一个共用体union,共用体中定义了各类型消息的具体内容,每种类型的消息对应了共用体中的一个字段,其中注册超时事件和删除超时事件消息共用一个tmo 结构体;回调事件与静态回调事件消息也共用一个cb 结构体;
API 调用与NETIF 的API 调用相关的消息具体内容比较多,不宜直接放在tcpip_msg 中,系统用了专门的结构体api_msg 来描述对应消息的具体内容。注:tcpip_msg 中只保存了一个指向api_msg 指针。

tcpip_thread 线程处理每种类型的消息时,lwIP 内核就会产生与之对应的消息函数,首先产生的消息传递到系统邮箱(tcpip_mbox),tcpip_thread 线程需要判断该消息的类型,从而做出相应的处理,在图7.1.1 中,笔者大概描述了lwIP 接收数据的流程图,直观上它是通过函数tcpip_input 对消息进行构造和投递,当然该函数真正执行的是函数tcpip_inpkt,如下源码所示:

err_t tcpip_input(struct pbuf *p, struct netif *inp)
{
#if LWIP_ETHERNET
    if (inp->flags & (NETIF_FLAG_ETHARP | NETIF_FLAG_ETHERNET))
    {
        /* 把ethernet_input()作用该函数的一部分
        内核接收到这个数据包就调用该函数*/
        return tcpip_inpkt(p, inp, ethernet_input);
    }
    else
#endif /* LWIP_ETHERNET */
        return tcpip_inpkt(p, inp, ip_input);
}
err_t tcpip_inpkt(struct pbuf *p, struct netif *inp, netif_input_fn input_fn)
{
#if LWIP_TCPIP_CORE_LOCKING_INPUT
    err_t ret;
    ret = input_fn(p, inp);
    return ret;
#else  /* LWIP_TCPIP_CORE_LOCKING_INPUT */
    struct tcpip_msg *msg;
    msg = (struct tcpip_msg *)memp_malloc(MEMP_TCPIP_MSG_INPKT);
    if (msg == NULL)
    {
        return ERR_MEM;
    }
    msg->type = TCPIP_MSG_INPKT;
    msg->msg.inp.p = p;       /* 指向pbuf数据包*/
    msg->msg.inp.netif = inp; /* 网络接口*/
    /* 构造消息,消息的类型是数据包消息,处理函数是ethernet_input() */
    msg->msg.inp.input_fn = input_fn;
    if (sys_mbox_trypost(&mbox, msg) != ERR_OK) /* 构造消息完成,发送邮箱*/
    {
        memp_free(MEMP_TCPIP_MSG_INPKT, msg); /* 释放内存池*/
        return ERR_MEM;
    }
    return ERR_OK;
#endif /* LWIP_TCPIP_CORE_LOCKING_INPUT */
}

总的来说,接收数据都是通过函数ethernet_input,当然无操作系统也是如此,只不过就是传递消息的方式不同,无操作系统一般使用回调函数传递消息,而操作系统一般使用IPC通信机制,例如邮箱,信号量等通信机制,lwIP 的IPC 通讯示意图如下图所示:

在这里插入图片描述

API 消息

所谓API 消息,其实就是两个API 部分的交互的消息,它是由用户调用API 函数为起点,使用IPC 通信机制告诉内核需要执行那个部分的API 函数,内核的具体消息内容都可以直接包含内核消息结构tcpip_msg,但是API 消息除外,由于它的消息内容实在庞大,所以协议栈专门用结构体api_msg 来描述API 消息内容,而在tcpip_msg 结构体中只维护该类型的指针,前面笔者也讲解到tcpip_msg 时候,它里面包含了一个api_msg 指针,这个指针只是指向api_msg 结构体,现在的api_msg 结构体在api_msg.h 文件定义的,该结构体如下所示:

struct api_msg
{
    struct netconn *conn; /* 当前连接*/
    err_t err;            /* 返回结果*/
    union
    {
        /* 用于函数lwip_netconn_do_send()参数*/
        struct netbuf *b;
        /* 用于函数lwip_netconn_do_newconn()参数*/
        struct
        {
            u8_t proto;
        } n;
        /* 用于函数lwip_netconn_do_bind()和函数lwip_netconn_do_connect()参数*/
        struct
        {
            API_MSG_M_DEF_C(ip_addr_t, ipaddr); /* ip 地址*/
            u16_t port;                         /* 端口号*/
            u8_t if_idx;
        } bc;
        /* 用于函数lwip_netconn_do_getaddr()参数*/
        struct
        {
            ip_addr_t API_MSG_M_DEF(ipaddr); /* ip 地址*/
            u16_t API_MSG_M_DEF(port);       /* 端口号*/
            u8_t local;
        } ad;
        /* 用于函数lwip_netconn_do_write()参数*/
        struct
        {
            /** 当前要写的向量e */
            const struct netvector *vector;
            /** 未写向量的个数*/
            u16_t vector_cnt;
            /** 偏移成矢量*/
            size_t vector_off;
            /** 向量的总长度*/
            size_t len;
            /** 当err == ERR_OK时写入的字节的总长度/输出的偏移量*/
            size_t offset;
            u8_t apiflags;
#if LWIP_SO_SNDTIMEO
            u32_t time_started;
#endif /* LWIP_SO_SNDTIMEO */
        } w;
        /** 用于函数lwip_netconn_do_recv()参数*/
        struct
        {
            u32_t len;
        } r;
#if LWIP_TCP
        /* 用于函数wip_netconn_do_close (/shutdown)参数*/
        struct
        {
            u8_t shut;
#if LWIP_SO_SNDTIMEO || LWIP_SO_LINGER
            u32_t time_started;
#else  /* LWIP_SO_SNDTIMEO || LWIP_SO_LINGER */
            u8_t polls_left;
#endif /* LWIP_SO_SNDTIMEO || LWIP_SO_LINGER */
        } sd;
#endif /* LWIP_TCP */
#if LWIP_IGMP || (LWIP_IPV6 && LWIP_IPV6_MLD)
        /* 用于函数lwip_netconn_do_join_leave_group()参数*/
        struct
        {
            API_MSG_M_DEF_C(ip_addr_t, netif_addr);
            u8_t if_idx;
            enum netconn_igmp join_or_leave;
        } jl;
#endif /* LWIP_IGMP || (LWIP_IPV6 && LWIP_IPV6_MLD) */
#if TCP_LISTEN_BACKLOG
        struct
        {
            u8_t backlog;
        } lb;
#endif /* TCP_LISTEN_BACKLOG */
    } msg;
#if LWIP_NETCONN_SEM_PER_THREAD
    sys_sem_t *op_completed_sem;
#endif /* LWIP_NETCONN_SEM_PER_THREAD */
};

这个结构体只包含了三个字段,分别为描述连接信息的conn、内核返回的执行结果err、以及msg。在api_msg 结构体中保存conn 字段是必须的,因为conn 结构中包含了与该连接相关的邮箱和信号量等信息,协议栈进程要用这些信息来完成与应用进程间的同步与通信;共用体类型msg 的各个成员与调用它的函数密切相关,如lwip_netconn_do_xxx(xxx 表示不一样的NETCONN 的API 接口)类型的函数执行需要用这些信息来完成与应用线程的通信与同步;
内核执行lwip_netconn_do_xxx 类型的函数返回结果会被记录在err 中;msg 的各个参数记录各个函数执行时需要的详细参数。

到了这里,我们已经理解了底层数据包消息,同理API 函数的调用也是如此,如果用户要与内核进行数据传递,也是需要lwIP 的消息机制,毕竟用户和内核都是独立的线程或者任务,例如我们使用SOCKET 的API 接口和NETCONN 的API 接口时候,lwIP 会把用户调用的函数与参数做成消息传递给tcpip_thraed 线程,这个消息就是lwIP 中的API 消息,lwIP 为什么会使用这些方式呢?首先对于用户来说,不需要很深入的理解lwIP 内核,只需要调用API 函数接口就可以完成实验,例如在NETCONN 的API 中构造数据包时,就会调用netconn_apimsg 函数进行投递消息。

其实lwIP 的协议栈API 实现有两个部分组成,一部分为用户编程接口函数提供给用户,这些函数在用户进程执行,另一部分驻留在内核进程,这两个部分的通信方法是使用IPC 通信机制,被用到的进程通信机制有以下四种:

  1. 邮箱:用于数据交互。
  2. 信号量:用于用户和系统同步。
  3. 互斥信号量:用于优先级翻转的问题。
  4. 共享内存:内核消息结构tcpip_msg 和API 消息内容api_msg。
    根据上述的通信机制,我们可以得到用户调用函数与内核进程的关系图,如下图所示:

在这里插入图片描述

图7.4.2.1 用户API 与内核进程的关系图
接下来笔者就以NETCONN 的API 为例,来讲解lwIP 的API 消息使用,如下源码所示:

err_t netconn_bind(struct netconn *conn, const ip_addr_t *addr, u16_t port)
{
    /*声明api_msg消息结构体*/
    API_MSG_VAR_DECLARE(msg);
    err_t err;
#if LWIP_IPV4
    if (addr == NULL)
    {
        addr = IP4_ADDR_ANY;
    }
#endif /* LWIP_IPV4 */
    /* 第一步:构建api_msg结构体*/
    API_MSG_VAR_ALLOC(msg); /* 申请内存*/
    /* 连接的信息*/
    API_MSG_VAR_REF(msg).conn = conn;
    /* IP地址*/
    API_MSG_VAR_REF(msg).msg.bc.ipaddr = API_MSG_VAR_REF(addr);
    /* 端口号*/
    API_MSG_VAR_REF(msg).msg.bc.port = port;
    /* 发送API消息并等待信号量*/
    err = netconn_apimsg(lwip_netconn_do_bind, &API_MSG_VAR_REF(msg));
    API_MSG_VAR_FREE(msg);
    return err;
}
static err_t
netconn_apimsg(tcpip_callback_fn fn, struct api_msg *apimsg)
{
    err_t err;
    /* 发送API消息并等待信号量*/
    err = tcpip_send_msg_wait_sem(fn, apimsg, LWIP_API_MSG_SEM(apimsg));
    if (err == ERR_OK)
    {
        return apimsg->err; /* 返回API的错误码*/
    }
    return err;
}
err_t tcpip_send_msg_wait_sem(tcpip_callback_fn fn, void *apimsg, sys_sem_t *sem)
{
#if LWIP_TCPIP_CORE_LOCKING
    LWIP_UNUSED_ARG(sem);
    LOCK_TCPIP_CORE();
    fn(apimsg);
    UNLOCK_TCPIP_CORE();
    return ERR_OK;
#else  /* LWIP_TCPIP_CORE_LOCKING */
    /*声明tcpip_msg消息结构体*/
    TCPIP_MSG_VAR_DECLARE(msg);
    /* 第二步:构造tcpip_msg消息*/
    TCPIP_MSG_VAR_ALLOC(msg);                         /* 申请内存*/
    TCPIP_MSG_VAR_REF(msg).type = TCPIP_MSG_API;      /* 消息类型*/
    TCPIP_MSG_VAR_REF(msg).msg.api_msg.function = fn; /* 设置回调函数*/
    TCPIP_MSG_VAR_REF(msg).msg.api_msg.msg = apimsg;  /* 指向api_msg消息*/
    /* 第三步:释放邮箱*/
    sys_mbox_post(&mbox, &TCPIP_MSG_VAR_REF(msg));
    /* 第四步:等待信号量*/
    sys_arch_sem_wait(sem, 0);
    TCPIP_MSG_VAR_FREE(msg);
    return ERR_OK;
#endif /* LWIP_TCPIP_CORE_LOCKING */
}

上述的源码,我们可分为四步讲解,它们分别构造tcpip_msg、api_msg、发送邮箱消息和等待信号量,如下图所示:

在这里插入图片描述

其实上图并不是lwIP 的最优先的流程图,因为在函数tcpip_send_msg_wait_sem 中宏定义
LWIP_TCPIP_CORE_LOCKING 是为1 的,表示无需操作系统的邮箱与信号量参与,在该函数
只执行以下源码:

err_t tcpip_send_msg_wait_sem(tcpip_callback_fn fn, void *apimsg, sys_sem_t *sem)
{
#if LWIP_TCPIP_CORE_LOCKING
    LWIP_UNUSED_ARG(sem);
    LOCK_TCPIP_CORE();
    fn(apimsg);
    UNLOCK_TCPIP_CORE();
    return ERR_OK;
#else  /* LWIP_TCPIP_CORE_LOCKING */
/* 代码省略*/
#endif /* LWIP_TCPIP_CORE_LOCKING */
}

上述函数也非常简单理解,首先此函数调用LOCK_TCPIP_CORE 函数上锁,记者系统直接调用函数lwip_netconn_do_bind 对api_msg 消息做处理,这样的方法省去了tcpip_mag 消息构建、邮箱以及信号量等操作。如下图所示:

在这里插入图片描述

  • 7
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
### 回答1: 《lwip应用开发实战指南》是一本关于lwip协议栈应用开发指南,以PDF形式提供。lwip是一个轻量级的开源TCP/IP协议栈,可以用于嵌入式系统和物联网设备的网络通信。 这本指南主要包含了lwip协议栈的原理介绍、应用示例和实战开发经验等内容。首先,指南会逐步介绍lwip协议栈的工作原理,包括数据包的封装和解封装、路由选择、连接管理等。读者可以通过这些基础知识了解lwip的工作流程和协议实现。 其次,指南还提供了一些lwip应用示例,例如建立基于lwip的服务器和客户端应用、使用lwip进行网络调试等。这些示例可以帮助读者更好地理解lwip的使用方法和技巧,并且提供了实际应用场景中的解决方案。 最后,指南还分享了一些实战开发经验和注意事项,帮助读者在开发lwip应用时避免常见的错误和问题。这些经验包括性能优化、内存管理、多线程处理等方面的技巧,可以帮助读者在实际开发中更好地利用lwip协议栈。 总体来说,这本《lwip应用开发实战指南》是一本对于想要学习和应用lwip协议栈的开发人员非常有用的资料。通过阅读这本指南,读者可以系统性地学习lwip的原理和使用方法,并且可以通过实际示例和经验分享来提升自己的开发水平。 ### 回答2: "lwip应用开发实战指南"是一本介绍lwIP(轻量级IP协议栈)应用开发实战指南。该书针对lwIP提供了详细的开发指导和实例,有助于读者深入了解lwIP的原理和应用开发。 该指南首先介绍了lwIP的概念、特点和基本架构。随后,通过实际的案例演示,讲解了如何在不同的应用场景中使用lwIP进行网络通信。其中包括TCP/IP通信、UDP通信、网络调试等常见应用。 指南还详细介绍了lwIP网络接口、协议栈和内存管理等关键要点。读者可以通过学习这些内容,深入掌握lwIP的核心技术,提升应用的性能和可靠性。 此外,该书还提供了一些实用的开发技巧和调试方法,帮助读者解决lwIP应用开发中常见的问题。通过这些实例和技巧,读者可以更好地理解lwIP的工作原理,掌握lwIP开发中的关键技术。 总之,《lwip应用开发实战指南》是一本对于lwIP应用开发者来说非常有价值的参考书。其以简明扼要的方式介绍了lwIP的原理和应用开发技巧,为读者提供了一个实际操作中的指导。无论是初学者还是有一定经验的开发者,都可以从中获得一些宝贵的经验和启示。希望读者能够通过本书的学习,掌握lwIP开发技术,提高自己在网络应用开发中的水平。 ### 回答3: "LwIP应用开发实战指南"是一本针对LwIP(Lightweight IP网络协议栈的应用开发实践指南的PDF电子书。LwIP是一种独立、可嵌入的开源网络协议栈,被广泛应用于嵌入式系统和物联网设备中。 这本指南通过实际案例和项目演示,详细介绍了如何使用LwIP协议栈进行应用开发。首先,它介绍了LwIP协议栈的基本概念和特性,包括TCP/IP协议、IP地址分配、套接字编程等内容。然后,它详细讲解了如何使用LwIP协议栈进行网络连接的建立和管理,包括网络接口的配置、DNS解析、TCP和UDP连接的建立等。同时,它还介绍了如何实现网络服务,例如HTTP服务器、FTP服务器等。 这本指南的特点之一是提供了大量的实例代码和可供实验的项目案例。读者可以通过按照书中的指导进行实验,逐步学习和掌握LwIP应用开发技巧。同时,这本指南还强调了实践和调试的重要性,通过解决实际问题的方式,帮助读者更好地理解和应用LwIP协议栈。 总的来说,“LwIP应用开发实战指南”是一本面向嵌入式系统和物联网设备开发人员的指导书,它通过实例和项目案例,帮助读者深入了解和应用LwIP协议栈进行网络应用开发。无论是初学者还是有一定经验的开发人员,都能从中获益,并提升他们的应用开发能力。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

行稳方能走远

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值