GD32 实现网口版本下载功能

 

在GD32上实现网口直接下载版本文件到Flash中

在GD32上实现网口直接下载版本文件到Flash中

目标

通过网口把版本文件app.bin下载到Flash指定的地址处,这里涉及到:

  • 网口初始化
  • LwIP代码移植
  • Flash初始化
  • Flash数据的复制

网口初始化

硬件

net

用到的GPIO端口:

GPIO组 管脚 网口名称 网口功能
PA PA1 RMII_REF_CLK 发送数据使用的时钟信号
  PA2 RMII_MDIO 用于与PHY之间的数据传输
PB PB11 RMII_TX_EN 发送使能信号
  PB12 RMII_TXD0 发送数据线0
  PB13 RMII_TXD1 发送数据线1
PC PC1 RMII_MDC 最高频率为2.5MHz的时钟信号
  PC4 RMII_RXD0 接收数据线0
  PC5 RMII_RXD1 接收数据线1

SMI:站点管理接口 SMI用于访问和设置PHY的配置,站点管理接口(SMI)通过MDC时钟线与MDIO数据线与外部PHY通讯,可以通过其访问任意PHY的任意寄存器

软件

配置GPIO、时钟、MAC、DMA

enet_system_setup()

LwIP 移植

LWIP 的移植由两部分组成,分别为 LWIP 协议栈和 contrib 应用实例 。其中,contrib 中是一些和平台移植相关的代码以及一些使用 LWIP 实现的应用,在移植时非常有用;LWIP 则是 TCP/IP 协议栈的核心源码!综上所述,LwIP 的移植主要分为以下这些部分:

  1. 运行环境的搭建
  2. 源代码文件的整理
  3. LwIP 的基本配置: 根据自己的需要来配置功能
  4. 与 CPU 或编译器相关源文件: LWIP 需要用户的文件之一
  5. 与操作系统的接口源文件(仅在系统模式下使用): LWIP 需要用户的文件之一
  6. 与底层网卡驱动的接口源文件: LWIP 需要用户的文件之一
  7. 初始化 LwIP,然后正常使用各功能即可

源码文件整理

源代码文件的整理需要注意目录结构,因为 LWIP 源码的头文件的包含路径是包含路径的:

LWIP
│  lwipopts.h       /* 这四个文件属于用户文件,每个用户可能是不同的,通常放在用户自己的目录*/
│  perf.c
│  sys_arch.c
|  ethernetif.c
├─arch          /* 这个需要我们自己建立目录,存放以下头文件!注意,根据平台,以下文件并不是全部都需要 */
│      bpstruct.h
│      cc.h
│      cpu.h
│      epstruct.h
│      perf.h
│      sys_arch.h
└─lwip          /* 这个目录下就是 LWIP 源码目录下的 src 目录中的所有文件!根据功能需要,里面的部分文件其实是可以不需要的 */
	├─api
	├─apps
	├─core
	├─include
	└─netif

lwIP 可以用于两种基本模式:

  • 主循环模式(通过配置 NO_SYS == 1,在目标系统上运行没有 OS/RTOS)
    • Mainloop Mode 即裸机下的死循环模式。
    • 在主循环模式下,只能使用回调式 API(Raw API)
    • 需要确保确保在 lwIP 中一次只有一个执行环境。
  • OS 模式(TCPIP 线程,目标系统上有一个操作系统)
    • OS Mode
    • 使用者需要提供一个名为sys_arch.c/h 的文件
    • 系统模式下,LwIP 需要系统的信号量、邮箱队列等

第一步:LwIP的基本配置

LwIP的配置涉及到lwIP配置文件opt.h和lwipopts.h的分析

首先了解LwIP对内在的使用。lwip可以使用两种形式的内存,一种是heap,一种是pool。

  • heap就像是一整块蛋糕,我们需要多少就切多少,但会产生内存碎片,导致最后都是切得很细的蛋糕块,再想要大块的内存找不到就会导致内存申请失败。
    • 作为嵌入式系统,我们不能使用PC机所使用的蛋糕刀具来切蛋糕,必须使用更小型的道具,占用CODE更小,这一点Adam 等相对于标准C而实现了几个小的内存分配、重分配和释放函数,它们都以mem_为前缀,已和原来的标准库函数相区别。当然如果你偏要使用大的刀,只需要:
      #define MEM_LIBC_MALLOC 1 
      

      这几个工具不管你是heap型模式切蛋糕还是pool模式切蛋糕,都可以用它们来完成

  • pool型切蛋糕的方法:
    • 找来一块蛋糕,把它切成n等分,每一份都是相同的固定大小,一份不够的可以拿两份

配置heap大小:

#define MEM_SIZE                        (12 * 1024)

配置pool的大小:

#define PBUF_POOL_SIZE                  16
#define PBUF_POOL_BUFSIZE               256

第一个是缓冲池的个数,第二个是每一份的大小

如果要使发送的应用程序也 采用pool的方式而不是heap,则要:

#define MEM_USE_POOLS 1 
#define MEM_USE_CUSTOM_POOLS 1 

并且还要在工程所在目录下创建文件 lwippools.h,里包括:

LWIP_MALLOC_MEMPOOL_START 
LWIP_MALLOC_MEMPOOL(20, 256) 
LWIP_MALLOC_MEMPOOL(10, 512) 
LWIP_MALLOC_MEMPOOL(5, 1512) 
LWIP_MALLOC_MEMPOOL_END

这样协议栈所涉及到的内存都用POOL方式来管理了,这种方法在StellarisWare例程中一般没有采用 如果使用POOL,以下关于内存的两个函数是不会被调用的:mem_init();这个函数主要是对堆内存的初始化,并返回指针。 mem_realloc(mem, size);这个函数对已分配的对内存块进行收缩

// lwipopts.h - Configuration file for lwIP
/*************************************************************************

 NOTE:  This file has been derived from the lwIP/src/include/lwip/opt.h
 header file.
 注:此文件起源于opt.h
 For additional details, refer to the original "opt.h" file, and lwIP
 documentation.
 详情参考opt.h。
**************************************************************************/

#ifndef __LWIPOPTS_H__
#define __LWIPOPTS_H__

//*****************************************************************************
//
// ---------- Platform specific locking ----------
//
//*****************************************************************************

// default is 0      针对Stellaris必须1,主要是因为在分配内存的时候,要确保总中断关闭。防止内存分配失败。
#define SYS_LIGHTWEIGHT_PROT            1    // 平台锁,保护关键区域内缓存的分配与释放
// default is 0   如果为使用RTOS,就置1.
#define NO_SYS                          1           
/* 该宏用来定义我们是否需要C语言标准库函数memcpy(),如果有更有效的函数,该宏可以忽略.不适用C标准库
//#define MEMCPY(dst,src,len)             memcpy(dst,src,len)   
//#define SMEMCPY(dst,src,len)            memcpy(dst,src,len)   //同上


//*****************************************************************************
//
// ---------- Memory options ----------
//
//*****************************************************************************

//如果为1,就表示我们使用c库的 malloc/free/realloc,否则使用lwip自带的函数,注意加了前缀的。
//#define MEM_LIBC_MALLOC                 0          
//Stellaris该值必须为4,设置CPU的对齐方式
#define MEM_ALIGNMENT                    4           
// default is 1600     该值在ZI中占了很大的份额。
// 这就是堆内存的大小,如果应用程序有大量数据在发送是要被复制,
// 那么该值就应该尽量大一点。由此可见,发送缓冲区从这里边分配。
#define MEM_SIZE                        (12 * 1024)  
																									
//是否开启内存POOL溢出检查,即是否使能堆内存溢出检查.
//#define MEMP_OVERFLOW_CHECK             0         
//设置为1表示在每次调用函数memp_free()后,进行一次正常的检查,以确保链表队列没有循环
//#define MEMP_SANITY_CHECK               0         
//是否使用POOL型内存来作为发送缓冲,而不是heap型,如果开启的话,可能还要创建头文件lwippool.h
//#define MEM_USE_POOLS                   0         
//内存Pool是否使用固定大小的POOL,开启这个前提是要开启上面的。
//#define MEMP_USE_CUSTOM_POOLS           0         


//*****************************************************************************
//
// ---------- Internal Memory Pool Sizes ----------
//
//*****************************************************************************
// 来自memp的PBUF_ROM和PBUF_REF类型的数目,
// 如果应用程有大量的数据来自ROM或者静态mem的数据要发送,此值要设大一些。
#define MEMP_NUM_PBUF                     20  // 缓冲池的个数
//原始连接(就是应用程不经过传输层直接到IP层获取数据)PCB的数目,该项依赖lwip_raw项的开启。
//#define MEMP_NUM_RAW_PCB                4   

//UDP的PCB数目,每一活动的UDP “连接” 需要一个PCB。
//#define MEMP_NUM_UDP_PCB                4   
// 同时建立激活的TCP连接的数目(要求参数LWIP_TCP使能).
// 默认为5 我改成1之后和原来的8没有什么区别。但是这里建立tcp连接发送数据之后就立刻关闭了tcp连接.
#define MEMP_NUM_TCP_PCB                  3   
//如果这里设置为1,就要注意了,我们在点亮led的时候实际上是几乎同时发送了两个GET请求,要求建立两个激活的tcp连接,如果设置为1,就会等到一个tcp激活的tcp连接关闭之后
//再建立一个新的tcp连接,所以才会出现延迟返回状态的现象。经过我的实验,发现等于3的时候比较特殊,会使code少4个字节。
//而且ZI的大小也只比2的时候多160,(奇数多160,偶数时多168)。

/* 能够监听的TCP连接数目(要求参数LWIP_TCP使能).
默认为8我改成了1之后对本例程也是无影响的。这个非常规律,多一个ZI就多32个字节。*/
#define MEMP_NUM_TCP_PCB_LISTEN           1     
//最多同时在队列的TCP_SEG的数目.这个数为20,MEMP_NUM_TCP_PCB为3时,code会比其它情况要小4个字节,其它所有值时code大小不变,当从偶数增加到奇数时,ZI增加24,从奇数增加到偶数时ZI增加16。
#define MEMP_NUM_TCP_SEG                  20  
//#define MEMP_NUM_REASSDATA              5  // 最多同时在队列等待重装的IP包数目,是整个IP包,不是IP分片。
//#define MEMP_NUM_ARP_QUEUE              30 //
//#define MEMP_NUM_IGMP_GROUP             8  //
//#define MEMP_NUM_SYS_TIMEOUT            3  // 能够同时激活的timeout的个数(要求NO_SYS==0)。默认为3
//#define MEMP_NUM_NETBUF                 2  // netbufs结构的数目,仅当使用sequential API的时候需要。默认为2
//#define MEMP_NUM_NETCONN                4  // netconns结构的数目,仅当使用sequential API的时候需要。默认为4
//#define MEMP_NUM_TCPIP_MSG_API          8  // tcpip_msg结构的数目,它用于callback/timeout API的通信(仅当使用tcpip.c的时候需要 )。默认为8
//#define MEMP_NUM_TCPIP_MSG_INPKT        8  // 接收包时tcpip_msg结构体的数目

//*****************************************************************************
//
// ---------- ARP options ----------
//
//*****************************************************************************
//#define LWIP_ARP                        1   //开启ARP
//#define ARP_TABLE_SIZE                  10  //ARP表项的大小。激活的MAC-IP地址对存储区的数目
//#define ARP_QUEUEING                    1   //设置为1表示在硬件地址解析期间,将发送数据包放入到队列中
//#define ETHARP_TRUST_IP_MAC             1   

//*****************************************************************************
//
// ---------- IP options ----------
//
//*****************************************************************************
//#define IP_FORWARD                      0
//#define IP_OPTIONS_ALLOWED              1
// default is 1 注意进来的IP分段包就不会被重装,所以大于1500的IP包可能会有些意想不到的问题
#define IP_REASSEMBLY                   0    
// default is 1    这样从这里发送出去的包不会被分片。这个不会出现问题,因为我们的TCP_MSS才512
#define IP_FRAG                         0    
//#define IP_REASS_MAXAGE                 3
//#define IP_REASS_MAX_PBUFS              10
//#define IP_FRAG_USES_STATIC_BUF         1
//#define IP_FRAG_MAX_MTU                 1500
//#define IP_DEFAULT_TTL                  255

//*****************************************************************************
//
// ---------- TCP options ----------
//
//*****************************************************************************
//#define LWIP_TCP                        1
//#define TCP_TTL                         (IP_DEFAULT_TTL)
#define TCP_WND                         2048    // default is 2048, was 4096  改变该值并不影响code和ZI的大小。
//#define TCP_MAXRTX                      12
//#define TCP_SYNMAXRTX                   6
//#define TCP_QUEUE_OOSEQ                 1
// default is 128, was 1500       改变该值并不影响code和ZI的大小
// 该值规定了TCP数据包数据部分的最大值,不包括tcp首部
#define TCP_MSS                         512     
//#define TCP_CALCULATE_EFF_SEND_MSS      1
//改变该值并不影响ZI的大小,但稍稍影响code大小,几个字节。
#define TCP_SND_BUF                     (6 * TCP_MSS)    
//#define TCP_SND_QUEUELEN                (4 * (TCP_SND_BUF/TCP_MSS))
//#define TCP_SNDLOWAT                    (TCP_SND_BUF/2)
//#define TCP_LISTEN_BACKLOG              0
//#define TCP_DEFAULT_LISTEN_BACKLOG      0xff

//*****************************************************************************
//
// ---------- Pbuf options ----------
//
//*****************************************************************************
// default is 14  改成16是因为在Stellaris系列中,FIFO中的帧是开始有两个字节的帧长度,针对Stellaris必须16
#define PBUF_LINK_HLEN                  16          
// 奇数时code比偶数时多4个字节,每+1,RAM多消耗272个字节。这也就是说每个pbuf需要272个字节,而每一个pbuf
//由两部分组成,一部分是缓冲区256个字节,一部分是pbuf首部(16个字节。)
//(不是以太网链路层的帧首部,尽管它从FIFO中是16个字节)。这个pbuf就是直接装入从RX FIFO中传
//过来的数据。每一个pbuf可以存一个帧,可以存256个字节的一个帧。这部分内存主要用来接收的。
#define PBUF_POOL_SIZE                  16            
//这个pbuf包括前边的16个字节的pbuf头,叫首部有点不合适,这个pbuf头里保存这个pbuf的所有信息。
// default is LWIP_MEM_ALIGN_SIZE(TCP_MSS+40+PBUF_LINK_HLEN)
#define PBUF_POOL_BUFSIZE               256            
#define ETH_PAD_SIZE                    2           // default is 0      针对Stellaris必须为2

“回调”的概念。你新建了一个TCP或者UDP的连接,你想等它接收到数据以后去处理它们,这时你需要把处理该数据的操作封装成一个函数,然后将这个函数的指针注册到LwIP内核中。LwIP内核会在需要的时候去检测该连接是否收到数据,如果收到了数据,内核会在第一时间调用注册的函数,这个过程被称为“回调”,这个注册函数被称为“回调函数”。

TCP/IP简介

LwIP是指Light Weight (轻型)IP协议,是瑞典计算机科学院(SICS)的Adam开发的小型TCP/IP协议,为的是适合在嵌入式系统上运行协议栈。所谓协议就是一组多方遵守的规则,遵守了这套特定的规则就可以让数据通过网络传递到目的地。这套规则规定了通信设备如何发送和接收电信号、如何封装和解析数据等。 TCP/IP是一组协议,它包括了从电信号一直到文本的各种规则。而且各规则之间有依赖关系,我们根据依赖关系把TCP/IP协议组分成5层。上层依赖于下层,下层为上层提供服务。简要的说,从低到高它们是:

  1. PHY物理层,是电信号电平的规则
  2. MAC链路层,是网卡要不要接收数据的规则。发送方填好目的地的MAC地址
  3. ARP网络层,在网络中的设备都要有自己的网络地址,而网络层就是规定网络地址该如何写,以及网络地址与MAC地址如何对应,这样每台连网的设备都有自己的网络地址
  4. TCP传输层,一台计算机上可以跑多个网络程序,传输层负责把数据包送给指定的网络程序;另外,数据包丢包、发错的时候,也是由传输层的TCP负责
  5. FTP应用层,应用层就是开发者自己使用这些数据了 数据的整个使用过程是,网卡发现网线上特定规律的高低电平,由MAC链路层来读取它的MAC地址,如果是当前网卡的MAC地址,就把数据去壳上交给网络层;网络层从数据包中查看它的IP网络地址,如果是本机的就去壳上交给传输层;传输层根据端口把数据发给对应的网络程序;至此网络程序就可以拿到它需要的数据了。

以太网简介

Ethernet以太网其实是遵守IEEE 802.3标准的局域网,它主要是规定PHY和MAC两层规则,以太网的标志是水晶头网线;其他局域网还包括了WIFI、蓝牙等等

物理层

  • 现实中水晶头网线、水晶头插座和PHY芯片共同构成了物理层
  • 物理层定义了以太网的传输介质、传输速度、数据编码和冲突检测机制
    • IEEE 802.3规定了物理层用的传输线、传输速度、数据编码
    • 物理层的功能一般通过PHY芯片来实现,GD32F450z开发板的PHY芯片是DP83848CVV。
    • 物理层的传输线一般是光纤、水晶头网线、电缆等等,传输速度一般是100Mbps/10Mbps,对于100Mbps(100BASE-T)使用5B/4B编码;10Mbps(10BASE-T)使用曼彻斯特码

MAC层

  • IEEE规定了以太网传输的数据包格式:MAC数据包
    • 前导字段
    • 帧起始界定字符
    • 等等
  • GD32控制器有一个MAC外设
    • 功能就是收发MAC数据包
    • 本质就是用DMA控制MAC数据包的收发

MAC层和PHY层打交道,它从PHY层取出数据(MAC数据包),去掉MAC壳再传给上层;或者从上层取到数据,加上一些控制数据后发给PHY层。 MAC数据包要让对方知道什么时候是包中数据的节拍的开始,什么时候是包的开始,然后还要告诉对方的MAC层这个MAC包的dst和src,然后收方的MAC层才好确定是不是处理。要处理时,还要知道这个MAC包是什么类型的包,比如是IP包、ARP(地址解析协议)包还是SNMP(指简单网络管理协议),这部分由MAC包的数据类型段负责;数据类型之后就是应用数据和CRC校验段

ARP(Address Resolution Protocol),是根据IP地址获取物理地址的一个TCP/IP协议,主机发送信息时将包含目标IP地址的ARP请求广播到局域网络上的所有主机,并接收返回消息,以此确定目标的物理地址;收到返回消息后将该IP地址和物理地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询ARP缓存以节约资源 简单网络管理协议(SNMP) 是专门设计用于在 IP 网络管理网络节点(服务器、工作站、路由器、交换机及HUBS等)的一种标准协议

GD32F450z的ETH外设模块

GD32F4xx控制器有一个以太网外设模块。该以太网模块包含10/100Mbps以太网MAC(媒体访问控制器) ,采用DMA优化数据帧的发送与接收性能,支持MII(媒体独立接口)与RMII(简化的媒体独立接口)两种与物理层(PHY)通讯的标准接口,实现以太网数据帧的发送与接收

以太网模块由MAC(介质访问控制器)模块、MII/RMII模块和一个以描述符形式控制的DMA模块组成

eth

MAC模块和PHY芯片(物理层收发器)之间通过MII(介质独立接口)连接,RMII 接口是 MII 接口的简化版本, MII 需要 16 根通信线, RMII 只需 7 根通信,在功能上是相同的。

从原理图上看到,GD32F450z的ETH外设模块通过RMII与PHY芯片DP83848CVV连接,PHY芯片连接RJ45水晶头之前还需要一个变压器,一般就使用带有电压转换功能和LED灯的HR911105A插座

硬件设计

eth1

PHY芯片的时钟来源于PA1引脚

软件设计

  1. 在裸机状态下启用PHY芯片和网络外设
  2. 初始化LwIP协议栈
  3. 收发数据

启用PHY芯片和网络外设

enet_system_setup();
void enet_system_setup(void)
{
    uint32_t ahb_frequency = 0;

#ifdef USE_ENET_INTERRUPT
    nvic_configuration();
#endif /* USE_ENET_INTERRUPT */
  
    /* 配置GPIO和以太网管脚 */
    enet_gpio_config();
    
    /* 配置以太网的MAC/DMA */
    enet_mac_dma_config();

    if (0 == enet_init_status){
      while(1){
      }
    }

    enet_interrupt_enable(ENET_DMA_INT_NIE);
    enet_interrupt_enable(ENET_DMA_INT_RIE);
}

nvic_configuration

static void nvic_configuration(void)
{
    nvic_vector_table_set(NVIC_VECTTAB_FLASH, 0x0);
    nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2);
    nvic_irq_enable(ENET_IRQn, 0, 0);
}

enet_mac_dma_config

static void enet_mac_dma_config(void)
{
    ErrStatus reval_state = ERROR;
    
    /* enable ethernet clock  */
    rcu_periph_clock_enable(RCU_ENET);
    rcu_periph_clock_enable(RCU_ENETTX);
    rcu_periph_clock_enable(RCU_ENETRX);
    
    /* reset ethernet on AHB bus */
    enet_deinit();

    enet_software_reset();

    enet_init_status = enet_init(ENET_AUTO_NEGOTIATION, ENET_AUTOCHECKSUM_DROP_FAILFRAMES, ENET_BROADCAST_FRAMES_PASS);
  
}

其中

void enet_deinit(void)
{
    rcu_periph_reset_enable(RCU_ENETRST);
    rcu_periph_reset_disable(RCU_ENETRST);
    enet_initpara_reset();
}

enet_software_reset实际是:

ENET_DMA_BCTL |= ENET_DMA_BCTL_SWR;
  • ENET_DMA_BCTL[0]=1:复位 MAC 所有内核寄存器

下面分析最重要的网络外设初始化enet_init()

ErrStatus enet_init(enet_mediamode_enum mediamode, enet_chksumconf_enum checksum, enet_frmrecept_enum recept)
{
	/* PHY接口的配置,配置SMI时钟,复位PHY芯片 */
 	enet_phy_config();
	/* 初始化ENET外设:
		1. ENET_MAC_CFG、ENET_MAC_FRMF、ENET_MAC_HLH、ENET_MAC_HLL
		2. ENET_MAC_FCTL、ENET_MAC_FCTH
		3. ENET_MAC_VLT、ENET_DMA_CTL、ENET_DMA_BCTL
		把这些寄存器设置成默认值 */
    enet_default_init();

	/***************************************************
	第一步:配置媒介media模式:PHY模式和MAC环回配置,包括:
      \arg        ENET_AUTO_NEGOTIATION: PHY auto negotiation
      \arg        ENET_100M_FULLDUPLEX: 100Mbit/s, full-duplex
      \arg        ENET_100M_HALFDUPLEX: 100Mbit/s, half-duplex
      \arg        ENET_10M_FULLDUPLEX: 10Mbit/s, full-duplex
      \arg        ENET_10M_HALFDUPLEX: 10Mbit/s, half-duplex
      \arg        ENET_LOOPBACKMODE: MAC in loopback mode at the MII	
	 *****************************************************/
	// 如果是自协商模式,把PHY协商好的配置保存到media_temp中,用来设置MAC
	if ((uint32_t)ENET_AUTO_NEGOTIATION == media_temp) {
		// 等待PHY_LINKED_STATUS = 1
		do {
            enet_phy_write_read(ENET_PHY_READ, PHY_ADDRESS, PHY_REG_BSR, &phy_value);
            phy_value &= PHY_LINKED_STATUS;
            timeout++;
        } while ((RESET == phy_value) && (timeout < PHY_READ_TO));

		// 通过RMII,设置PHY启用自协商, PHY[BCR] = PHY_AUTONEGOTIATION
		phy_value = PHY_AUTONEGOTIATION;
        phy_state = enet_phy_write_read(ENET_PHY_WRITE, PHY_ADDRESS, PHY_REG_BCR, &phy_value);

		//等待PHY自协商完成
		do {
            enet_phy_write_read(ENET_PHY_READ, PHY_ADDRESS, PHY_REG_BSR, &phy_value);
            phy_value &= PHY_AUTONEGO_COMPLETE;
            timeout++;
        } while ((RESET == phy_value) && (timeout < (uint32_t)PHY_READ_TO));

		// 根据PHY反馈的自协商结果,得到PHY的配置(单双工模式和通讯速度)
        enet_phy_write_read(ENET_PHY_READ, PHY_ADDRESS, PHY_SR, &phy_value);
		// 设置单双工
        if ((uint16_t)RESET != (phy_value & PHY_DUPLEX_STATUS)) {
            media_temp = ENET_MODE_FULLDUPLEX;
        } else {
            media_temp = ENET_MODE_HALFDUPLEX;
        }
		// 设置通讯速度
        if ((uint16_t)RESET != (phy_value & PHY_SPEED_STATUS)) {
            media_temp |= ENET_SPEEDMODE_100M;
        } else {
            media_temp |= ENET_SPEEDMODE_10M;
        }
	} else { // 不是自协商,根据传入的media_temp先设置PHY的模式和速度
        phy_value = (uint16_t)((media_temp & ENET_MAC_CFG_DPM) >> 3);
        phy_value |= (uint16_t)((media_temp & ENET_MAC_CFG_SPD) >> 1);
        phy_state = enet_phy_write_read(ENET_PHY_WRITE, PHY_ADDRESS, PHY_REG_BCR, &phy_value);
	}
	/* PHY设置好之后,再设置MAC */
    reg_value = ENET_MAC_CFG;
	/* 速度spd、双工duplexmode、环回loopback mode */
    reg_value &= (~(ENET_MAC_CFG_SPD | ENET_MAC_CFG_DPM | ENET_MAC_CFG_LBM));
    reg_value |= media_temp;
    ENET_MAC_CFG = reg_value;

	// 2. 配置checksum
	...
	
	// 3. 配置帧过滤功能
	/************************************************
      \arg        ENET_PROMISCUOUS_MODE: 启用杂散模式
      \arg        ENET_RECEIVEALL: 所有收到的帧都被转发到应用程序
      \arg        ENET_BROADCAST_FRAMES_PASS: 地址过滤器通过所有收到的广播帧
      \arg        ENET_BROADCAST_FRAMES_DROP: 地址过滤器过滤所有传入的广播帧
	***************************************************/
	ENET_MAC_FRMF |= (uint32_t)recept;

	// 4. 配置不同的DMA功能选项, dma的功能由enet_initpara_config函数设定
	// 4.1 配置forward_frame(帧转发)相关的寄存器
	if (RESET != (enet_initpara.option_enable & (uint32_t)FORWARD_OPTION)) {
		/********************************************
		  根据forward_frame转发帧参数配置: MAC的tfcd, apcd
			1. type frame CRC dropping: 丢弃类型帧的CRC
			2. automatic pad/CRC drop: 自动填充/CRC丢弃
		/********************************************
		reg_temp = enet_initpara.forward_frame;
        reg_value = ENET_MAC_CFG;
        reg_value &= (~(ENET_MAC_CFG_TFCD | ENET_MAC_CFG_APCD));
        temp = reg_temp;
        /* configure ENET_MAC_CFG register */
        temp &= (ENET_MAC_CFG_TFCD | ENET_MAC_CFG_APCD);
        reg_value |= temp;
        ENET_MAC_CFG = reg_value;

		/********************************************
		  根据forward_frame转发帧参数配置: DMA参数
			1. forward error frames: 转发错误帧
			2. forward undersized good frames 转发好帧
		/********************************************
        reg_value = ENET_DMA_CTL;
        temp = reg_temp;
        reg_value &= (~(ENET_DMA_CTL_FERF | ENET_DMA_CTL_FUF));
        temp &= ((ENET_DMA_CTL_FERF | ENET_DMA_CTL_FUF) << 2);
        reg_value |= (temp >> 2);
        ENET_DMA_CTL = reg_value;
	}

	// 4.2 配置DMA总线
	// 4.3 配置DMA最大突发
	// 4.4 配置DMA仲裁
	// 4.5 配置DMA转储模式
	// 4.5 配置DMA功能
	// 5. 配置VALN
	// 6. 配置流控
	// 7. 配置Hash表高、低位寄存器
	// 8. 配置帧过滤器
	// 9. 配置半双工
	// 10. 配置定时器(net看门狗)
	// 11. 配置帧间隔


}

这样enet网络外设就初始化完成了

DMA功能的设定由下面的函数决定:

首先看看option的枚举:

typedef enum {
    FORWARD_OPTION = BIT(0),          /*!< 配置帧转发相关参数 */
    DMABUS_OPTION = BIT(1),           /*!< 配置DMA总线模式相关参数 */
    DMA_MAXBURST_OPTION = BIT(2),     /*!< 配置DMA最大突发相关参数 */
    DMA_ARBITRATION_OPTION = BIT(3),  /*!< 配置DMA仲裁相关参数 */
    STORE_OPTION = BIT(4),            /*!< 配置存储转发模式的相关参数 */
    DMA_OPTION = BIT(5),              /*!< 配置DMA控制的相关参数 */
    VLAN_OPTION = BIT(6),             /*!< 配置VLAN标签相关参数 */
    FLOWCTL_OPTION = BIT(7),          /*!< 配置流量控制相关参数 */
    HASHH_OPTION = BIT(8),            /*!< 配置哈希列表高32位相关参数 */
    HASHL_OPTION = BIT(9),            /*!< 配置哈希列表低32位相关参数 */
    FILTER_OPTION = BIT(10),          /*!< 配置frame过滤器控制的相关参数 */
    HALFDUPLEX_OPTION = BIT(11),      /*!< 配置半双工相关参数 */
    TIMER_OPTION = BIT(12),           /*!< 配置帧定时器相关参数 */
    INTERFRAMEGAP_OPTION = BIT(13),   /*!< 配置帧间间隙相关参数 */
}enet_option_enum;

然后是函数本体,就是根据要设置的选项,把para值保存在对应的变量中

void enet_initpara_config(enet_option_enum option, uint32_t para) {
	switch (option) {
    case FORWARD_OPTION:
		enet_initpara.option_enable |= (uint32_t)FORWARD_OPTION;
		enet_initpara.forward_frame = para;
		break;
    case DMABUS_OPTION:
		/* choose to configure dmabus_mode, and save the configuration parameters */
		enet_initpara.option_enable |= (uint32_t)DMABUS_OPTION;
		enet_initpara.dmabus_mode = para;
		break;
	...
 }

使用方法:

enet_initpara_config(HALFDUPLEX_OPTION, 
		ENET_CARRIERSENSE_ENABLE|
		ENET_RECEIVEOWN_ENABLE|
		ENET_RETRYTRANSMISSION_DISABLE|
		ENET_BACKOFFLIMIT_10|
		ENET_DEFERRALCHECK_DISABLE);
enet_initpara_config(DMA_OPTION, 
		ENET_FLUSH_RXFRAME_ENABLE|
		ENET_SECONDFRAME_OPT_ENABLE|
		ENET_NORMAL_DESCRIPTOR); 

那么,因为GD32的驱动中没有显示的配置DMA的功能,那么就要去数据手册中参看他的默认值了

MAC 的发送流程

  • 所有的发送均由以太网模块中专用DMA控制器和MAC控制
  • 收到应用程序发送指令
    • DMA将发送帧从系统存储区读出并存入深度为2K的TxFIFO中
    • 根据选择的模式DMA将数据取出到MAC控制器
      • 直通模式Cut-Through
        • 当FIFO中的数据等于或超过了所设置的阈值ENET_DMA_CTL[TTHC]
        • 数据从FIFO中取出并送入到MAC控制器中
      • 存储转发模式Store-and-Forward
        • 只有当一个完整的帧写入FIFO之后
        • FIFO中的数据才会被送入MAC控制器
          • TxFIFO的大小 < 要发送的以太网帧长度
          • 那么在TxFIFO即将全满时
          • 数据会被送入到MAC控制器
    • MAC控制器通过MII/RMII接口发送到以太网PHY
      • 可以配置MAC控制器自动将硬件计算的CRC值添加到数据帧的帧校验序列中
    • 当MAC控制器收到来自TxFIFO的帧结束信号后
      • 完成整个传输过程
      • 传输状态信息将会由MAC控制器生成并写回到DMA控制器中
      • 应用程序可以通过DMA当前发送描述符查询发送状态

MAC 的接收流程

  • MAC接收到的帧都会被送入RxFIFO中
  • MAC接收到帧后
    • 会离其前导码和帧首界定码
    • 并从帧首界定码后的第一个字节(目标地址)开始向FIFO发送帧数据a
    • 如果使能了IEEE 1588时间戳
      • MAC会在检测到帧的帧首界定码的时候记录下系统的当前时间
      • MAC会把这个时间戳通过接收描述符一并发给应用程序
    • 当 ENET_MAC_CFG[APCD] = 1
      • 接收到的帧长度/类型域的值小于0x600时
      • MAC将自动剥离填充域和帧校验序列
      • APCD: automatic pad/CRC drop
    • 当 ENET_MAC_CFG[WDD] = 1
      • MAC的看门狗使能
      • 帧长度超过2048字节时将被切断
        • 目标地址 + 源地址 + 长度/类型 + 数据 + 帧校验序列
  • RxFIFO
    • 直通模式
      • RxFIFO中的数据量大于门限值
      • MAC的RxMTL就通知RxDMA从RxFIFO中取出数据
      • 当RxFIFO取出整个帧后
        • MAC控制器将接收状态信息字发送给RxDMA控制器
        • RxDMA将状态字写到接收描述符中
      • 问题:
        • 如果是坏帧,RxDMA也会把所有数据拷贝到缓冲区
    • 存储转发模式 ENET_DMA_CTL[RSFD] = 1
      • RxDMA只在RxFIFO完整地收到一帧后,才将其读出
      • 如果MAC设置成将所有错误帧丢弃
        • RxDMA只会读出合法的帧,并转发给应用程序

MAC 回环模式

  • ENET_MAC_CFG[LBM] = 1
  • MAC发射端把帧发送到自身的接收端上

MAC 统计计数器:MSC

  • 了解发送和接收帧的统计情况
  • 发送帧是“好帧”时,MSC发送计数器会自动更新
  • 接收帧是“好帧”时,MSC接收计数器会自动更新

精确时间协议:PTP

型中断事件:正常类和异常类。 无论什么类型的中断事件,都具有相应的中断使能位(屏蔽位)来控制是否产生中断。当所有 中断事件都被清除,或中断使能位被清除,则相应的中断汇总位也被清除。如果正常类和异常 类中断都被清除,则DMA中断将被清除。

  • MAC的PTP模块主要是支持记录PTP包从以太网端口发出和收到的准确时间
  • 并将其返回给应用程序

DMA 控制器描述

  • 以太网专用DMA控制器
    • 减少CPU的干预
    • 实现FIFO和系统存储之间的帧数据传输
  • DMA和CPU之间的通讯通过2种数据结构实现
    • 描述符列表和数据缓存
      • 应用程序需要开辟物理内存
        • 存储描述符列表
          • 描述符是指向缓存的指针
        • 有2个描述符队列
          • 功能
            • 一个用作发送
            • 一个用作接收
          • 结构
            • 链结构
              • TDES0
                • TDES0[FSG] = 1: DMA明确了当前缓存存储的是一个新的帧
                • TDES0[LSG] = 1: 当前缓存存储的是当前帧的最后一部分数据
              • TDES1/RDES1
                • 标明缓存有效长度
              • RDES2、TDES2存放缓存地址
              • RDES3、TDES3存放下一个描述符的地址
            • 环结构
              • RDES2及TDES2,RDES3及TDES3中都存放缓存地址 trdesc
        • 两个队列的基地址
          • ENET_DMA_TDTADDR
          • ENET_DMA_RDTADDR
        • 当DFM=0
          • 发送描述符 = TDES0~TDES3 4个描述符字
          • 接收描述符 = RDES0 ~ RDES3
        • DFM = 1
          • 发送描述符 = TDES0~TDES7 8个描述符字
          • 接收描述符 = RDES0 ~ RDES7
        • 数据缓存
          • 存放在MCU的物理内存里
          • 存放一个帧的全部或者部分
    • 控制寄存器和状态寄存器
  • TxDMA 和 RxDMA 的仲裁器
    • ENET_DMA_BCTL[DAB] = 0: 轮询优先级
      • 在TxDMA和RxDMA同时要求访问数据总线的时候
      • 按照ENET_DMA_BCTL[RTPR]位设定的比例对其访问进行分配
    • ENET_DMA_BCTL[DAB] = 1: 固定优先级
      • RxDMA总是对总线拥有更高的访问优先级
  • TxDMA 与 RxDMA 控制器的初始化
    • 设置ENET_DMA_BCTL总线访问参数
    • 设置ENET_DMA_INTEN,中断
    • 描述符列表基地址写入ENET_DMA_TDTADDR、ENET_DMA_RDTADDR
    • 过滤器配置
    • 从PHY读出的自协商的结果,设置SPD位和DPM位的值
      • 通讯模式(半/全双工)
      • 通讯速度(10Mbit/s或100Mbit/s)
    • ENET_MAC_CFG[TEN] = 1, ENET_MAC_CFG[REN] = 1, 使能MAC的发送和接收操作
    • ENET_DMA_CTL[STE] = 1, ENET_DMA_CTL[SRE] = 1, 使能DMA发送和接收器
  • TxDMA 第二帧的发送设置
    • ENET_DMA_CTL[OSF] = 0,
      • 读取发送描述符
      • 从系统存储读取数据写到 -> FIFO
      • 帧数据通过MAC放到接口上
      • 等待数据发送完毕后
        • 将发送状态写回描述符
    • ENET_DMA_CTL[OSF] = 1
      • 当HCLK远远大于TX_CLK时(系统的频率远大于发送频率)
      • 第二帧的数据可以不等待第一帧的描述符状态信息被写回,
        • 就先读取内存里的第二帧数据
        • 并把它们送进FIFO
  • TxDMA的发送流程
    • 初始化帧数据到发送缓存
      • 设置发送描述符(TDES0-TDES3)
      • 设置TDES0[DAV] = 1
    • 使能TxDMA控制器
      • 设置ENET_DMA_CTL[STE] = 1
    • TxDMA控制器轮询描述符列表来获取待发送的帧
      • 当 TxDMA 发现 TDES0[DAV] = 0,
        • 此时CPU占有描述符
        • TxDMA 终止传输进入挂起状态
        • 并暂停查询发送描述符
      • 设置ENET_DMA_STAT[的发送缓存不可用] = 1, ENET_DMA_STAT[正常中断汇总]=1
    • TxDMA取出描述符
      • 得出TDES0[DAV] = 1(该描述符由DMA占有)
        • 说明
          • DMA会在将帧完整传输 / 描述符指向的缓存里的数据全部被读出以后把该位清’0’
          • 当一个帧位于多个缓存中时,第一个缓存描述符的DAV位,必须在后面缓存描述符的DAV位全部置’1’以后,才能置’1’
        • DMA从描述符中解析出所配置的发送帧
        • 以及发送数据缓存的地址
    • DMA从内存中取出数据并将数据存入TxFIFO
      • 实际上真正的数据发送是由TxDMA模式决定的:
        • 直通模式在FIFO中的字节数大于所配置的阈值时,数据将取出到MAC发送
        • 存储转发模式在整个帧数据都传入FIFO后或FIFO快要填满时再取出数据给MAC进行发送
    • TxDMA控制器一直轮询描述符列表
      • 直到发现TDES0[LSG] = 1(帧结尾被传送出去)
      • 如果TDES0[LSG] = 0
        • 在这个描述符指向的所有缓存数据送入TxFIFO之后
        • 将DAV位清零以关闭这个描述符(这个描述符不DMA占有)
        • TxDMA控制器等待写回描述符状态
          • 在最后一个缓存的数据发送完毕以后
          • DMA会将整个帧的发送状态信息
          • 写入最后一个的发送描述符TDES0并返回
    • 在整个帧发送完成以后
      • 仅当TDES0[INTC] = 1 => ENET_DMA_STAT[TS] = 1(发送状态位置位)
        • INTC: 完成时中断位
        • 当帧发送完成后,把ENET_DMA_STAT[TS] = 1
      • 此时若使能了DMA中断,将进入相应中断
      • 然后DMA控制器返回轮询描述符列表,继续处理下一帧
    • 在挂起状态下
      • 向ENET_DMA_TPEN(发送查询使能寄存器)写入任意值
      • 清除发送溢出标志位
      • TxDMA将重新回到运行状态
      • DMA控制器返回轮询描述符列表,继续处理下一帧
  • 发送帧的内容
    • 前导码,帧首界定码SFD
      • MAC自动生成
    • 目标地址DA,源地址SA
    • QTAG前缀(可选)
    • 长度/类型域LT
    • 数据,PAD填充域
    • 帧校验序列FCS
  • 常规 TxDMA 描述符包含4个32位字
    • TDES0~TDES3 tdes0
    • TDES0
      • TDES0[DAV]
        • 0:表示CPU占有描述符
        • 1:表示DMA占有描述符
      • TDES0[INTC]: 完成时中断位
        • 0:帧发送完成时,ENET_DMA_STAT寄存器的TS位不被置位
        • 1:帧发送完成时,ENET_DMA_STAT寄存器的TS位被置位
      • TDES0[LSG]: 最后分块位
        • 1:该描述符缓存中存放有帧的最后一个分块
      • TDES0[FSG]: 第一分块位
        • 1:该描述符缓存中存放有帧的第一个分块
    • TDES1:发送描述符字1
      • TDES1[TB2S]: 发送缓存 2 大小
        • 第二个数据缓存的大小(以字节记)
      • TDES1[TB1S]: 发送缓存 1 大小
        • 第一个数据缓存的大小(以字节记)
    • TDES2: 发送缓存1地址指针(TB1AP)
    • TDES3: 发送缓存2地址指针 / (下个描述符地址)
  • RxDMA 工作流程
    • DMA接收描述符初始化
      • RDES0[DAV] = 1
    • 使能RxDMA控制器
      • ENET_DMA_CTL[SRE] = 1
      • DMA进入运行状态
        • 从ENET_DMA_RDTADDR指向的列表找到接收描述符
        • 若RDES0[DAV] = 1, 则当前描述符开始接收帧
        • 若RDES0[DAV] = 0(CPU正占有描述符), DMA进入挂起状态
    • DMA记录该描述符的控制位和缓存地址
    • DMA从RxFIFO将数据传输到接收缓存
    • 如果缓存被填满或者帧传输结束
      • RxDMA从描述符队列中获取下一个接收描述符
      • 若下一个描述符的RDES0[DAV]=1
        • RxDMA将当前的RDES0[DAV]=0, 关闭当前描述符
        • 处理接收到的帧,并从RxFIFO将数据传输到接收缓存
  • 接收帧处理
    • 当接口上出现一个帧的时候,MAC开始接收帧
    • 地址过滤模块开始工作
    • RxDMA控制器将数据从RxFIFO中传输到接收缓存中
      • 当前缓存中包含了帧起始
        • RxDMA写帧接收状态时, 将RDES0[FDES] = 1, 表明这个描述符中存储的是帧的第一部分
      • 当前缓存中包含了帧结尾
        • RxDMA => RDES0[LDES]=1, 表明这个描述符中存储的是帧的最后一部分
        • 当RES0[LEDS]=1时(最后一帧)
          • 描述符其他状态也会更新
          • ENET_DMA_STAT[RS] = 1
      • RxDMA获取下一个接收描述符
        • 并将上一个描述符的RDES0[DAV] = 0, 以关闭上个描述符

以太网配置流程

  • 使能以太网时钟
  • 配置通讯接口
    • 选择接口模式(MII或RMII)
    • 配置GPIO模块,将管脚映射到复用功能11(AF11)
  • 等待DMA软复位完成
    • ENET_DMA_BCTL[SWR] = 1
  • 配置DMA寄存器参数
    • 配置 SMI 时钟频率
    • 访问 PHY 寄存器获取 PHY 的信息
      • 是否支持半/全双工
      • 是否支持 10M/100Mbit
    • 配置MAC的ENET_MAC_CFG,和PHY保持一致
  • 初始化以太网DMA模块用于数据传输
    • ENET_DMA_BCTL(DMA 总线控制寄存器)
      • 软件复位
      • DMA 仲裁位
      • 描述符模式
        • 0:常规描述符模式
        • 1:增强描述符模式
      • 地址对齐
    • 描述符列表基地址
      • ENET_DMA_RDTADDR/ENET_DMA_TDTADDR
    • ENET_DMA_CTL
  • 初始化用于存放描述符列表以及数据缓存的物理内存空间
    • 初始化发送和接收描述符(DAV=1)
    • 初始化数据缓存
  • 使能MAC和DMA模块,开始发送和接收
    • 开启 MAC 发送器和接收器
      • ENET_MAC_CFG[TEN] = 1
      • ENET_MAC_CFG[REN] = 1
    • 开启 DMA 的发送和接收
      • ENET_DMA_CTL[STE] = 1
      • ENET_DMA_CTL[SRE] = 1
  • Tx发送数据帧
    • 选择一个或多个描述符发送描述符,将发送帧数据写到 TDES 中指定的缓存地址中
    • TDES0[DAV] = 1
    • 写入任意值到 ENET_DMA_TPEN
      • 使 TxDMA 退出挂起模式,开始发送数据
    • 确定当前帧是否发送完毕
      • 轮询当前描述符的 DAV 位直到其复位
      • 或当TDES0[INTC]=1时
        • 应用程序可以轮询 ENET_DMA_STAT[TS] 直到其置位
  • Rx接收帧
    • 查看描述符列表中的第一个接收描述符(ENET_DMA_RDTADDR)
    • 若RDES0[DAV] = 0
      • 则说明描述符已被DMA使用过
      • DMA已经把RxFIFO中的数据拷贝到了缓存中
    • 处理接收帧数据
    • 置位当前描述符的 DAV 位,以复用当前描述符接收新的帧
    • 查看列表中的下一个描述符的RDES0[DAV]

以太网终端

  • 太网部分一共有2个中断向量
    • 一个用于以太网正常操作
      • 由MAC和DMA产生的中断
    • 一个映射到EXTI线19的以太网唤醒事件

初始化LwIP协议栈

void lwip_stack_init(void) {
    ip_addr_t ipaddr;
    ip_addr_t netmask;
    ip_addr_t gw;

    tcpip_init(NULL, NULL);

    IP4_ADDR(&ipaddr, IP_ADDR0, IP_ADDR1, IP_ADDR2, IP_ADDR3);
    IP4_ADDR(&netmask, NETMASK_ADDR0, NETMASK_ADDR1, NETMASK_ADDR2, NETMASK_ADDR3);
    IP4_ADDR(&gw, GW_ADDR0, GW_ADDR1, GW_ADDR2, GW_ADDR3);

    netif_add(&netif, &ipaddr, &netmask, &gw, NULL, &ethernetif_init, &tcpip_input);

    netif_set_default(&netif);
    if (netif_is_link_up(&netif)) {
        netif_set_up(&netif);
    } else {
        netif_set_down(&netif);
    }
}
tcpip_init
  • 初始化所有子模块
  • 启动tcpip_thread线程

  • tcpip_init
    • lwip_init()
      • mem_init();
        • memp_init();
        • pbuf_init();
        • netif_init();
        • ip_init()
          • ip4_route
          • 为一个给定的IP地址寻找合适的网络接口
        • etharp_init
        • udp_init
        • tcp_init
    • sys_thread_new(tcpip_thread)
      • while(1)
        • TCPIP_MBOX_FETCH(&mbox, (void **)&msg)
        • 等待信息,在等待的过程中会处理超时问题
        • switch (msg->type)
          • case TCPIP_MSG_INPKT:
            • msg->msg.inp.input_fn(msg->msg.inp.p, msg->msg.inp.netif)
          • case TCPIP_MSG_API:
            • msg->msg.api_msg.function(msg->msg.api_msg.msg)
          • case TCPIP_MSG_API_CALL
            • msg->msg.api_call.arg->err = msg->msg.api_call.function(msg->msg.api_call.arg);
          • case TCPIP_MSG_CALLBACK:
            • msg->msg.cb.function(msg->msg.cb.ctx);
netif_add
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)
{
  netif->state = state;
  netif->num = netif_num++;
  netif->input = input; // 	tcpip_input
  netif_set_addr(netif, ipaddr, netmask, gw);

  /* 执行网络接口的硬件初始化 init = ethernetif_init */
  if (init(netif) != ERR_OK) {
    return NULL;
  }

  /* add this netif to the list */
  netif->next = netif_list;
  netif_list = netif;
  mib2_netif_added(netif);
}

这里的ethernetif_init如下:

err_t ethernetif_init(struct netif* netif) {

    netif->hostname = "lwip";
    netif->name[0] = IFNAME0;
    netif->name[1] = IFNAME1;

	/***********************************************
	etharp_output:
	  解决并填入外发IP数据包的以太网地址头
	************************************************/
    netif->output = etharp_output;

	/***********************************************
	这个函数应该完成数据包的实际传输。
	数据包被包含在传递给该函数的pbuf中。
	这个pbuf可能是链式的。
	if (xSemaphoreTake(gtx_semaphore, LOWLEVEL_OUTPUT_WAITING_TIME)) {
		while ((uint32_t)RESET != (dma_current_txdesc->status & ENET_TDES0_DAV)) {
		}
		buffer = (uint8_t*)(enet_desc_information_get(dma_current_txdesc, TXDESC_BUFFER_1_ADDR));

		for (q = p; q != NULL; q = q->next) {
			memcpy((uint8_t*)&buffer[framelength], q->payload, q->len);
			framelength = framelength + q->len;
		}
	************************************************/
    netif->linkoutput = low_level_output;

    /* initialize the hardware */
    low_level_init(netif);

    return ERR_OK;
}

low_level_init的内容如下:

static void low_level_init(struct netif* netif) {
    /* 设置网络接口netif的MAC地址 */
	netif->hwaddr_len = ETHARP_HWADDR_LEN;
    netif->hwaddr[0] = MAC_ADDR0;
    netif->hwaddr[1] = MAC_ADDR1;
    netif->hwaddr[2] = MAC_ADDR2;
    netif->hwaddr[3] = MAC_ADDR3;
    netif->hwaddr[4] = MAC_ADDR4;
    netif->hwaddr[5] = MAC_ADDR5;
	/* 创建信号量 */
	if (grx_semaphore == NULL) {
        grx_semaphore = xSemaphoreCreateCounting(20, 0);
    }
    if (gtx_semaphore == NULL) {
        vSemaphoreCreateBinary(gtx_semaphore);
    }
	/* 初始化MAC控制器的MAC地址 */
	enet_mac_address_set(ENET_MAC_ADDRESS0, netif->hwaddr);

	/*******************************************************
	初始化Tx/Rx描述符列表:
        desc_tab = txdesc_tab; // 全局变量:Tx描述符表
        buf = &tx_buff[0][0];  // 缓冲区
		desc_status = ENET_TDES0_TCHM; // 链式结构

		ENET_DMA_TDTADDR = (uint32_t)desc_tab;
		dma_current_txdesc = desc_tab;
	后面按照链式描述符表进行配置
	*******************************************************/
    enet_descriptors_chain_init(ENET_DMA_TX);
    enet_descriptors_chain_init(ENET_DMA_RX);

	/* 启用中断 */
	for (i = 0; i < ENET_RXBUF_NUM; i++) {
        enet_rx_desc_immediate_receive_complete_interrupt(&rxdesc_tab[i]);
    }

	/*******************************************************
	创建网络接口netif接收任务:
	while(1) {
		TRY_GET_NEXT_FRAME:
			xSemaphoreTake(grx_semaphore);

			// 分配一个pbuf,并将传入的数据包的字节从接口转入pbuf
			// 本质,分配空pbuf,把DMA接收到缓冲区的数据拷贝到pbuf中:
			// 1. p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL)
			// 2. memcpy(p->payload, &buffer, q->len);
			p = low_level_input(low_netif);

			// input = tcpip_input
			low_netif->input(p, low_netif);
			goto TRY_GET_NEXT_FRAME;
	}
	*******************************************************/
	xTaskCreate(ethernetif_input, "ETHERNETIF_INPUT", ETHERNETIF_INPUT_TASK_STACK_SIZE, NULL,
        ETHERNETIF_INPUT_TASK_PRIO, NULL);

	/* 启用MAC和DMA的收发功能 */
    enet_enable();
}

这里tcpip_input:

将收到的数据包传递给tcpip_thread,用ethernet_input或ip_input进行输入处理。

err_t tcpip_input(struct pbuf *p, struct netif *inp)
{
  if (inp->flags & (NETIF_FLAG_ETHARP | NETIF_FLAG_ETHERNET)) {
	  // 将收到的数据包传递给tcpip_thread进行输入处理
    return tcpip_inpkt(p, inp, ethernet_input);
  } else
  return tcpip_inpkt(p, inp, ip_input);
}

/*********************************** 
   p: 接收到的包
   inp: 接收数据包的网络接口
   input_fn: 调用的输入函数
*********************************/
err_t tcpip_inpkt(struct pbuf *p, struct netif *inp, netif_input_fn input_fn)
{
  struct tcpip_msg *msg;

  msg = (struct tcpip_msg *)memp_malloc(MEMP_TCPIP_MSG_INPKT);

  msg->type = TCPIP_MSG_INPKT;
  msg->msg.inp.p = p;
  msg->msg.inp.netif = inp;
  msg->msg.inp.input_fn = input_fn;
  // 把接收到的包投递出去
  sys_mbox_trypost(&mbox, msg);
}

LwIP 应用开发

4. LwIP的网络接口管理

LwIP是软件,而数据收发器PHY芯片是硬件,那么LwIP是怎么把软件和硬件连接在一起的呢?而且PHY芯片多种多样,各有不同,LwIP是如何做到适配不同的硬件接口的呢?

原来LwIP通过netif网卡对象来和不同的硬件交互的,这个网卡对象被LwIP放在了中间层ethernetif.c文件中,用户根据自己的硬件在这个文件中写下自己的硬件相关代码,比如硬件初始化、数据收发函数等等;当LwIP这个TCP/IP处理内核需要数据时,就可以通过ethernetif.c中统一定义的接口读取/发送数据。对于多个网卡,LwIP采用单向链表的方式把它们管理起来。下面就是网卡对象的数据结构:

struct netif {
#if !LWIP_SINGLE_NETIF
  /* 指向netif链表中的下一个 */
  struct netif *next;
#endif

#if LWIP_IPV4
  /* 网络字节中的IP地址、子网掩码、默认网关配置 */
  ip_addr_t ip_addr;
  ip_addr_t netmask;
  ip_addr_t gw;
#endif /* LWIP_IPV4 */

  /** 此函数由网络设备驱动程序调用,将数据包传递到TCP/IP协议栈
   *  对于以太网物理层,这通常是ethernet_input()*/
  netif_input_fn input;

#if LWIP_IPV4
  /* 此函数由IP层调用,在接口上发送数据包。通常这个功能,
  * 首先解析硬件地址,然后发送数据包。
  * 对于以太网物理层,这通常是etharp_output() */
  netif_output_fn output;
#endif /* LWIP_IPV4 */

  /* 此函数由ethernet_output()调用,当需要在网卡上发送一个数据包时。
  * 底层硬件输出数据函数,一般是调用自定义函数low_level_output*/
  netif_linkoutput_fn linkoutput;

#if LWIP_NETIF_STATUS_CALLBACK
	/* 当netif状态设置为up或down时调用此函数 */
	netif_status_callback_fn status_callback;		
#endif /* LWIP_NETIF_STATUS_CALLBACK */

#if LWIP_NETIF_LINK_CALLBACK
	/* 当netif链接设置为up或down时,将调用此函数 */
	netif_status_callback_fn link_callback;		
#endif /* LWIP_NETIF_LINK_CALLBACK */

#if LWIP_NETIF_REMOVE_CALLBACK
	/* 当netif被删除时调用此函数 */
	netif_status_callback_fn remove_callback;
#endif /* LWIP_NETIF_REMOVE_CALLBACK */

	/* 此字段可由设备驱动程序设置并指向设备的状态信息。
	 * 主要是将网卡的某些私有数据传递给上层,用户可以自由发挥,也可以不用。*/
	void *state;

#ifdef netif_get_client_data
   void* client_data[LWIP_NETIF_CLIENT_DATA_INDEX_MAX + LWIP_NUM_NETIF_CLIENT_DATA];
#endif
#if LWIP_NETIF_HOSTNAME
	/* 这个netif的主机名,NULL也是一个有效值 */
	const char*  hostname;
#endif /* LWIP_NETIF_HOSTNAME */

#if LWIP_CHECKSUM_CTRL_PER_NETIF
	u16_t chksum_flags;
#endif /* LWIP_CHECKSUM_CTRL_PER_NETIF*/

	/** 最大传输单位(以字节为单位),对于以太网一般设为 1500 */
	u16_t mtu;		

	/** 此网卡的链路层硬件地址 */
	u8_t hwaddr[NETIF_MAX_HWADDR_LEN];

	/** 硬件地址长度,对于以太网就是 MAC 地址长度,为6字节 */
	u8_t hwaddr_len;		

	/* 网卡状态信息标志位,是很重要的控制字段,
	 * 它包括网卡功能使能、广播使能、 ARP 使能等等重要控制位。 */
	u8_t flags;	

	/* 字段用于保存每一个网卡的名字。用两个字符的名字来标识网络接
	 * 口使用的设备驱动的种类,名字由设备驱动来设置并且应该反映通过网卡
	 * 表示的硬件的种类。比如蓝牙设备( bluetooth)的网卡名字可以是 bt,
	 * 而 IEEE 802.11b WLAN 设备的名字就可以是wl,当然设置什么名字用户是可
	 * 以自由发挥的,这并不影响用户对网卡的使用。当然,如果两个网卡
	 * 具有相同的网络名字,我们就用 num 字段来区分相同类别的不同网卡*/
	char name[2];		

	/* 用来标示使用同种驱动类型的不同网卡 */
	u8_t num;			

#if MIB2_STATS
	/* 连接类型 */
	u8_t link_type;
	/* 连接速度 */
	u32_t link_speed;
	/* 最后一次更改的时间戳 */
	u32_t ts;
	/** counters */
	struct stats_mib2_netif_ctrs mib2_counters;
#endif /* MIB2_STATS */

#if LWIP_IPV4 && LWIP_IGMP
	/** 可以调用此函数来添加或删除多播中的条目
		  以太网MAC的过滤表。*/
	netif_igmp_mac_filter_fn igmp_mac_filter;
#endif /* LWIP_IPV4 && LWIP_IGMP */

#if LWIP_NETIF_USE_HINTS
	struct netif_hint *hints;
#endif /* LWIP_NETIF_USE_HINTS */

#if ENABLE_LOOPBACK
	/* List of packets to be queued for ourselves. */
	struct pbuf *loop_first;
	struct pbuf *loop_last;

#if LWIP_LOOPBACK_MAX_PBUFS
	u16_t loop_cnt_current;
#endif /* LWIP_LOOPBACK_MAX_PBUFS */

#endif /* ENABLE_LOOPBACK */
};

ip_addr, netmask, gw分别是IP地址,子网掩码和网关 IP地址和网卡必须一一对应,有多少网卡就应该有多少IP地址 子网掩码用来从IP地址中提取子网和主机编号,IP发送数据包时会选择发送给与目的IP在同一网络中的网卡 当LwIP要把数据发送给其他网段时,就会把数据包发送给网关,让网关自己进行转发 IP地址相当于住址,子网相当于同城,网关相当于快递

netif的使用

  • netif_add的内容
    • 清空主机IP地址、子网掩码、网关等字段信息。
    • 根据传递进来的参数填写网卡state、input等字段的相关信息。
    • 调用网卡设置函数netif_set_addr()设置网卡IP地址、子网掩码、网关等信息。
    • 通过传递进来的回调函数init()进行网卡真正的初始化操作
      • 该函数是由用户实现的
      • 对于不同网卡就使用不一样的初始化
      • 而此处是以太网,则该回调函数一般为ethernetif_init()
    • 初始化网卡成功,则遍历当前设备拥有多少个网卡,并为当前网卡分配唯一标识num
    • 将当前网卡插入netif_list链表中
  • 代码
    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)
    {
    
      LWIP_ASSERT_CORE_LOCKED();
    
      if (ipaddr == NULL) {
        ipaddr = ip_2_ip4(IP4_ADDR_ANY);
      }
      if (netmask == NULL) {
        netmask = ip_2_ip4(IP4_ADDR_ANY);
      }
      if (gw == NULL) {
        gw = ip_2_ip4(IP4_ADDR_ANY);
      }
    
      /* 清空主机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_SET_CHECKSUM_CTRL(netif, NETIF_CHECKSUM_ENABLE_ALL);
      netif->mtu = 0;
      netif->flags = 0;
    #ifdef netif_get_client_data
      memset(netif->client_data, 0, sizeof(netif->client_data));
    #endif /* LWIP_NUM_NETIF_CLIENT_DATA */
    
    #if LWIP_NETIF_STATUS_CALLBACK
      netif->status_callback = NULL;
    #endif /* LWIP_NETIF_STATUS_CALLBACK */
    #if LWIP_NETIF_LINK_CALLBACK
      netif->link_callback = NULL;
    #endif /* LWIP_NETIF_LINK_CALLBACK */
    #if LWIP_IGMP
      netif->igmp_mac_filter = NULL;
    #endif /* LWIP_IGMP */
    
    
      /* 根据传参填写网卡对象的特定状态信息 */
      netif->state = state;
      netif->num = netif_num;
      netif->input = input;
    
      NETIF_RESET_HINTS(netif);
    #if ENABLE_LOOPBACK && LWIP_LOOPBACK_MAX_PBUFS
      netif->loop_cnt_current = 0;
    #endif /* ENABLE_LOOPBACK && LWIP_LOOPBACK_MAX_PBUFS */
    
    
      /* 设置网卡 */
      netif_set_addr(netif, ipaddr, netmask, gw);
    
      /* 调用用户网卡真正的初始化函数 */
      if (init(netif) != ERR_OK) {
        return NULL;
      }
    
    #if !LWIP_SINGLE_NETIF  
      /* Assign a unique netif number in the range [0..254], so that (num+1) can
        serve as an interface index that fits in a u8_t.
        We assume that the new netif has not yet been added to the list here.
        This algorithm is O(n^2), but that should be OK for lwIP.
        */
      {
        struct netif *netif2;
        int num_netifs;
        do {
          if (netif->num == 255) {
            netif->num = 0;
          }
          num_netifs = 0;
          for (netif2 = netif_list; netif2 != NULL; netif2 = netif2->next) {
            LWIP_ASSERT("netif already added", netif2 != netif);
            num_netifs++;
            LWIP_ASSERT("too many netifs, max. supported number is 255", num_netifs <= 255);
            if (netif2->num == netif->num) {
              netif->num++;
              break;
            }
          }
        } while (netif2 != NULL);
      }
      if (netif->num == 254) {
        netif_num = 0;
      } else {
        netif_num = (u8_t)(netif->num + 1);
      }
    
      /* 将当前网卡插入链表 */
      netif->next = netif_list;
      netif_list = netif;
    #endif /* "LWIP_SINGLE_NETIF */
      mib2_netif_added(netif);
    
    #if LWIP_IGMP
      /* start IGMP processing */
      if (netif->flags & NETIF_FLAG_IGMP) {
        igmp_start(netif);
      }
    #endif /* LWIP_IGMP */
    
      LWIP_DEBUGF(NETIF_DEBUG, ("netif: added interface %c%c IP",
                                netif->name[0], netif->name[1]));
      LWIP_DEBUGF(NETIF_DEBUG, (" addr "));
      ip4_addr_debug_print(NETIF_DEBUG, ipaddr);
      LWIP_DEBUGF(NETIF_DEBUG, (" netmask "));
      ip4_addr_debug_print(NETIF_DEBUG, netmask);
      LWIP_DEBUGF(NETIF_DEBUG, (" gw "));
      ip4_addr_debug_print(NETIF_DEBUG, gw);
      LWIP_DEBUGF(NETIF_DEBUG, ("\n"));
    
      netif_invoke_ext_callback(netif, LWIP_NSC_NETIF_ADDED, NULL);
    
      return netif;
    }
    

在开始使用LwIP协议栈的时候,我们就需要将网卡底层移植完成,才能开始使用,而移植的第一步,就是将网络进行初始化,并且设置该网卡为默认网卡,让LwIP能通过网卡进行收发数据,这部分由lwip_stack_init()完成

lwip_stack_init()中,需要添加一个网卡设备

netif_add(&g_mynetif, &ipaddr, &netmask, &gw, NULL, &ethernetif_init, &ethernet_input);

这里就涉及到与netif网卡相关的底层函数ethernetif_initethernetif_input

4.3 与netif相关的底层函数

netif是LwIP设计的网卡对象,但是我们还需要给它提供针对具体硬件的访问函数。这些函数放在ethernetif.c文件中。与具体网卡硬件相关的函数主要是下面三个:

static void low_level_init(struct netif *netif);
static err_t low_level_output(struct netif *netif, struct pbuf *p);
static struct pbuf *low_level_input(struct netif *netif);
  • low_level_init
    • 对网卡的复位
    • 对网卡参数的初始化
      • 根据netif的内容
      • 设置网卡的MAC、MTU
  • low_level_output
    • 将LwIP内核的数据包发送出去
    • 数据包以struct pbuf的形式封装
  • low_level_input
    • 网卡的数据包接收函数
    • 会把网络数据包解析并封装成struct pbuf形式

以上是底层与具体硬件有关的函数,在上层LwIP还有两个管理网卡的函数:

err_t ethernetif_init(struct netif *netif);
void ethernetif_input(void *pParams);
  • netif_add()会调用ethernetif_init()初始化网卡
    • ethernetif_init -> low_level_init()
  • ethernetif_input调用low_level_input接收数据包
    • 根据数据包的类型(ARP数据包/IP数据包)交给上层

4.4 ethernetif.c文件的分析

4.4.1 ethernetif结构体

在ethernetif.c文件中定义了一个结构体:

struct ethernetif
{
	struct eth_addr *ethaddr;
	/* Add whatever per-interface state that is needed here. */
};

这个ethernetif用来记录网卡硬件底层的一些信息,我们可以自己添加一些私有数据,这些信息可以通过netif的stat传递给上层。

4.4.2 ethernetif_init 初始化函数

err_t ethernetif_init(struct netif *netif)
  • 实际上就是填充netif结构体,把信息传递给上层
  • 设置了网卡的hostname和name
      netif->hostname = "lwip"
      netif->name[0] = IFNAME0; // 'G'
      netif->name[1] = IFNAME1; // 'D'
    
  • 把底层信息传递给上层
      netif->state = ethernetif
    
  • 设置了收发函数
      netif->output = etharp_output;
      netif->linkoutput = low_level_output;
    
  • 初始化网卡硬件
      low_level_init(netif);
      ethernetif->ethaddr = (struct eth_addr *) &(netif->hwaddr[0]);
    

4.4.3 low_level_init 硬件初始化

  • low_level_init()函数
    • 可以调用enet_system_setup()来初始化硬件
    • 设置netif的一些参数:mtu、hwaddr、mac地址
    • 初始化Tx/Rx的描述符模式:链表
    • 启用接收中断
    • 启用硬件校验
    • 启用tx/rx功能

网卡对象的初始化

我们通过netif_add()函数把网卡挂载到链表时,就会用到ethernetif_init函数,我们来分析一下

err_t ethernetif_init(struct netif *netif)
{
	LWIP_ASSERT("netif != NULL", (netif != NULL));
  
#if LWIP_NETIF_HOSTNAME
	/* Initialize interface hostname */
	netif->hostname = "Gigadevice.COM_lwip";
#endif /* LWIP_NETIF_HOSTNAME */

	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...) */
	netif->output = etharp_output;
	netif->linkoutput = low_level_output;

	/* initialize the hardware */
	low_level_init(netif);

	return ERR_OK;
}

关键是调用low_level_init初始化函数

static void low_level_init(struct netif *netif)
{
#ifdef CHECKSUM_BY_HARDWARE
	int i; 
#endif /* CHECKSUM_BY_HARDWARE */
	/* set MAC hardware address length */
	netif->hwaddr_len = ETHARP_HWADDR_LEN;

	/* set MAC hardware address */
	netif->hwaddr[0] =  MAC_ADDR0;
	netif->hwaddr[1] =  MAC_ADDR1;
	netif->hwaddr[2] =  MAC_ADDR2;
	netif->hwaddr[3] =  MAC_ADDR3;
	netif->hwaddr[4] =  MAC_ADDR4;
	netif->hwaddr[5] =  MAC_ADDR5;
	
	/* initialize MAC address in ethernet MAC */ 
	enet_mac_address_set(ENET_MAC_ADDRESS0, netif->hwaddr);

	/* maximum transfer unit */
	netif->mtu = 1500;

	/* device capabilities */
	/* don't set NETIF_FLAG_ETHARP if this device is not an ethernet one */
	netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP;

	/* initialize descriptors list: chain/ring mode */
#ifdef SELECT_DESCRIPTORS_ENHANCED_MODE
	enet_ptp_enhanced_descriptors_chain_init(ENET_DMA_TX);
	enet_ptp_enhanced_descriptors_chain_init(ENET_DMA_RX);
#else

	enet_descriptors_chain_init(ENET_DMA_TX);
	enet_descriptors_chain_init(ENET_DMA_RX);
	
#endif /* SELECT_DESCRIPTORS_ENHANCED_MODE */

	/* enable ethernet Rx interrrupt */
	{   int i;
		for(i=0; i<ENET_RXBUF_NUM; i++){ 
		   enet_rx_desc_immediate_receive_complete_interrupt(&rxdesc_tab[i]);
		}
	}

#ifdef CHECKSUM_BY_HARDWARE
	/* enable the TCP, UDP and ICMP checksum insertion for the Tx frames */
	for(i=0; i < ENET_TXBUF_NUM; i++){
		enet_transmit_checksum_config(&txdesc_tab[i], ENET_CHECKSUM_TCPUDPICMP_FULL);
	}
#endif /* CHECKSUM_BY_HARDWARE */

	/* note: TCP, UDP, ICMP checksum checking for received frame are enabled in DMA config */

	/* enable MAC and DMA transmission and reception */
	enet_enable();
}

这样 LwIP 的网卡对象就初始化完成了

5. LwIP的内存管理

5.1 内存分配策略

LwIP的内存分配策略主要有:

  • 固定大小的内存块
  • 动态内存堆
  • 标准C库的malloc和free(不推荐) 标准c的内存分配时间不固定

内存分配的本质:

  • 准备一大块内存数组,把分配的空间首地址返回给调用者
  • 需要记录每块内存是否分配

5.1.1 固定大小的内存块分配策略

  • 分 LwIP把内存按照固定大小分成块,在用单链表串起来
      • 申请时从链表中取出第一个
      • 把归还的内存插入第一个
  • 优缺点
    • 优点
      • 分配时间固定
    • 缺点
      • 浪费
  • LwIP
    • 固定大小内存块策略:动态内存池
    • 原因
      • TCP、IP、UDP和Ether头部是固定的结构体
      • 不会产生碎片

5.1.2 可变长度分配

  • 策略
    • 初始时是一整块内存
    • 随着用户的申请和释放,内存的大小和数量都不断变化
  • LwIP
    • Frist Fit:首次拟合分配
      • 找到第一块比所需内存大的空闲内存块,切分出所需部分
      • 必须申请MIN_SIZE(12 Bytes)大小
      • 前几个字节是管理器私有数据
      • 管理器查看节点前后,有空闲就合并成一大块
  • 优缺点
    • 优点
      • 浪费小
    • 缺点
      • 内存碎片
      • 再申请不能

5.2 动态内存池

5.2.1 内存池的预处理

  • LwIP的memp.c就是动态内存池策略的实现
  • LwIP内核在初始化时会把内存用链表串起来
  • LwIP会根据用户的宏定义来构建对应协议的内存池
    • 定义了LWIP_UDP宏
    • LwIP会编译UDP协议的数据结构(控制块PCB)
    • LwIP就会初始化UDP协议的内存池
    • 内存池中块的个数由MEMP_NUM_UDP_PCB来定义
  • 高级玩家的memp_std.h
    • memp_std.h里面全是MEMP_POOL(name, num, size, desc)宏
      #if LWIP_RAW
        LWIP_MEMP(RAW_PCB, MEMP_NUM_RAW_PCB, 
                  sizeof(struct raw_pcb), "RAW_PCB")
      #endif /* LWIP_RAW */
      
    • 使用方式 定义LWIP_MEMPOOL之后,再包含lwip/memp_std.h
      typedef enum
      {
      #define LWIP_MEMPOOL(name,num,size,desc)  MEMP_##name,
      #include "lwip/priv/memp_std.h"
          MEMP_MAX
      } memp_t;
      

      最后得到:

      typedef enum
      {
          MEMP_RAW_PCB,
          MEMP_UDP_PCB,
          MEMP_TCP_PCB,
          MEMP_TCP_PCB_LISTEN,
          MEMP_TCP_SEG,
          MEMP_ALTCP_PCB,
          MEMP_REASSDATA,
          MEMP_NETBUF,
          MEMP_NETCONN,
          MEMP_MAX
      } memp_t;   
      

      在memp_std.h文件的最后需要对LWIP_MEMPOOL宏定义进行撤销,因为该文件很会被多个地方调用,在每个调用的地方会重新定义这个宏定义的功能,所以在文件的末尾添加这句#undef LWIP_MEMPOOL代码是非常有必要的最后要对

  • 内存池预处理的简要过程
    • 通过开启宏开关,让编译器把相应内存池的数据结构编译进去 比如:#define LWIP_DUP,就把LWIP_MEMPOOL(UDP_PCB, MEMP_NUM_UDP_PCB, sizeof(struct udp_pcb),”UDP_PCB”)包含进去
    • 在初始化的时候,初始化UDP协议控制块需要的POOL资源,其数量由MEMP_NUM_UDP_PCB宏定义决定

5.2.2 内存池的初始化

初始化

  1. 根据每种内存池的描述memp_desc来初始化内存池
  2. memp_desc.num个空闲内存穿进链表中
  3. 把内存清空

5.2.3 内存分配

  1. 从对应类型的内存池中取出*desc->tab
  2. desc->tab + MEMP_SIZE的地址就是申请的空闲内存
  3. *desc->tab向前移动一个

5.2.4 内存释放

  1. 通过要释放的内存的地址,减去对应内存类型的固定偏移,得到要释放内存的实际首地址
  2. 通过内存类型,找到内存池的第一个空闲内存块的首地址
  3. 要释放的内存块的next指向内存池的第一个空闲内存块
  4. 把释放的内存块插入内存池*memp_desc->tab

5.3 动态内存堆

5.3.1 简介

  • 嵌入式系统的内存管理策略,决定着内存的分配和回收的效率,从而影响了系统同的性能
  • LwIP内核为使用者提供了自己实现的pool动态内存池和head动态内存堆
  • 动态内存堆又可以分成
    • 标准C库提供的内存管理
    • 自己实现的管理
    • 通过 MEM_LIBC_MALLOC 选择
  • LwIP的pool可以由head实现,反之亦然
    • 通过 MEM_USE_POOL 和 MEMP_MEM_MALLOC 选择

5.3.2 内存堆的数据结构

struct mem {
  /** index (-> ram[next]) of the next struct */
  mem_size_t next;
  /** index (-> ram[prev]) of the previous struct */
  mem_size_t prev;
  /** 1: 内存块已经使用; 0: 未使用 */
  u8_t used;
};

/* 定义最小的内存块,它至少要能保存管理数据 */
#ifndef MIN_SIZE
#define MIN_SIZE             12
#endif /* MIN_SIZE */

/* 定义堆内存的地址 */
#ifndef LWIP_RAM_HEAP_POINTER
/* 1. MEM_SIZE_ALIGNED: MEM_SIZE对齐后的堆内存大小 
   2. MEM_ALIGNEMENT: CPU n字节对齐 
   3. ARM_HEAP_POINTER:堆内存地址 */
u8_t ram_heap[MEM_SIZE_ALIGNED + (2*SIZEOF_STRUCT_MEM) + MEM_ALIGNMENT];
#define LWIP_RAM_HEAP_POINTER ram_heap
#endif /* LWIP_RAM_HEAP_POINTER */

/* 指向堆内存对齐后的起始地址 */
static u8_t *ram;

/* 堆内存的最后一块内存,总是未使用的 */
static struct mem *ram_end;

/* 指向最低地址的空闲块,用于快速查找 */
static struct mem *lfree;

5.3.3 堆内存的初始化

堆内存的初始化使用mem_init函数

void mem_init(void)
{
  struct mem *mem;

  LWIP_ASSERT("Sanity check alignment",
	(SIZEOF_STRUCT_MEM & (MEM_ALIGNMENT-1)) == 0);

  /* 1. 得到对齐后的堆内存首地址 */
  ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);
  /* 2. 在堆内存的首地址处放置一个mem管理块 */
  mem = (struct mem *)(void *)ram;
  /* 2.1 mem管理后面整个对齐了的堆内存 */
  mem->next = MEM_SIZE_ALIGNED;
  mem->prev = 0; // 前面没有内存
  mem->used = 0;
  /* 3. 在堆内存最后放置一个mem内存管理快 */
  ram_end = (struct mem *)(void *)&ram[MEM_SIZE_ALIGNED];
  /* 已使用,因为结束的地方是没有内存可分配的 */
  ram_end->used = 1;
  /* 指向自身,表示已经到了堆内存的结束处 */
  ram_end->next = MEM_SIZE_ALIGNED;
  ram_end->prev = MEM_SIZE_ALIGNED;

  /* 初始化最低地址指针指向 ram 堆内存的起始地址,因为当前只有一块整的内存 */
  lfree = (struct mem *)(void *)ram;

  MEM_STATS_AVAIL(avail, MEM_SIZE_ALIGNED);

  /* 初始化一个互斥锁 */
  if(sys_mutex_new(&mem_mutex) != ERR_OK) {
	LWIP_ASSERT("failed to create mem_mutex", 0);
  }
}
  • 初始化之后,内存被分配成两块
    • 整个内存块,大小为整个堆内存,可分配
    • 结束内存块,大小为0,不可分配
  • 系统运行中
    • lfree会不断改变,都指向最低空闲内存地址
    • ram_end不会改变

mem_init

5.3.4 内存分配

  • LwIP提供的内存分配策略
    • C标准库
    • pools 内存池
    • heap 内存堆
MEMP_MEM_MALLOC MEM_USE_POOLS 内存分配策略
0 0 LwIP中默认的宏定义,内存池与内存堆独立实现,互不相干。
0 1 内存堆的实现由内存池实现。
1 0 内存池的实现由内存堆实现。
1 1 不允许的方式

用内存池方式视线内存堆还需要创建lwippools.h文件,增加内存池相关代码

LWIP_MALLOC_MEMPOOL_START
LWIP_MALLOC_MEMPOOL(20, 256)
LWIP_MALLOC_MEMPOOL(10, 512)
LWIP_MALLOC_MEMPOOL(5, 1512)
LWIP_MALLOC_MEMPOOL_END

设置好内存池的数量和容量,并且容量由小到大 系统将这些内存块放入内存池,在申请时根据最合适的大小选择内存块

GPIO的8中输入输出工作模式

GPIO输入输出各种模式(推挽、开漏、准双向端口)详解

输入模式

  • 输入浮空(float)
    • 逻辑器件与引脚即不接高电平,也不接低电平
    • 浮空最大的特点就是电压的不确定性,它可能是0V,页可能是VCC,还可能是介于两者之间的某个值
    • I2C或者UART
  • 输入上拉模式
    • 将不确定的信号通过一个电阻嵌位在高电平
  • 输入下拉
    • 就是把电压拉低,拉到GND

输出模式

  • push-pull
    • 推挽输出的最大特点是可以真正能真正的输出高电平和低电平,在两种电平下都具有驱动能力
    • 推挽输出不能实现” 线与”的原因
    • 推拉式输出级即提高电路的负载能力,又提高开关速度
  • Open Drain Output
    • 开漏输出无法真正输出高电平,即高电平时没有驱动能力,需要借助外部上拉电阻完成对外驱动
    • 开漏输出的这一特性一个明显的优势就是可以很方便的调节输出的电平,因为输出电平完全由上拉电阻连接的电源电平决定。所以在需要进行电平转换的地方,非常适合使用开漏输出
    • 开漏输出的这一特性另一个好处在于可以实现”线与”功能,所谓的”线与”指的是多个信号线直接连接在一起,只有当所有信号全部为高电平时,合在一起的总线为高电平;只要有任意一个或者多个信号为低电平,则总线为低电平。而推挽输出就不行,如果高电平和低电平连在一起,会出现电流倒灌,损坏器件。

      关于驱动能力

  • 驱动能力就是输出电流的能力

    所谓的驱动能力,就是指输出电流的能力。对于驱动大负载(即负载内阻越小,负载越大)时,例如IO输出为5V,驱动的负载内阻为10ohm,于是根据欧姆定律可以正常情况下负载上的电流为0.5A(推算出功率为2.5W)。显然一般的IO不可能有这么大的驱动能力,也就是没有办法输出这么大的电流。于是造成的结果就是输出电压会被拉下来,达不到标称的5V。

在开发板上移植lwip裸机程序

  1. 初始化LED灯
LED GPIO
LED_CHN0 G
  1. 初始化串口0
端口 GPIO
CPU_DBG_TX PA9
CPU_DBG_RX PA10
  • 初始化GPIO管脚
    • 使能GPIO[P]的时钟
    • 使能PD[5/6]管脚的时钟
    • 把PD[5/6]管脚设置成UART_Tx/Rx的GPIO备用功能
    • 配置PD[5/6]管脚为推挽输出模式
  • 初始化UART控制器
    • 波特率:115200
    • 开启接收和发送使能
    • 开启UART控制器
  1. 初始化PHY和eth控制器
端口 GPIO In/Out
RMII_REF_CLK PA1 Out
RMII_MDIO PA2 I&O
RMII_CRS_DV PA7 In
RMII_MDC PC1 Out
RMII_RXD0 PC4 In
RMII_RXD1 PC5 In
PHY_INTB PE3 In
PHY_RSTN PE4 Out
RMII_TX_EN PG11 Out
RMII_TXD0 PG13 Out
RMII_TXD1 PG14 Out
  1. 初始化LwIP
  2. 初始化Tcp服务端

GD32F450z网络控制器

TFTP服务器的移植

目标:在GD32板上通过LwIP实现TFTP服务器

TFTP协议

TFTP是一种简单的文件传输协议。是在UDP之上建立一个类似于FTP的但仅支持文件上传和下载功能的传输协议,所以它不包含FTP协议中的目录操作和用户权限等内容。它只能从文件服务器上获得或写入文件,不能列出目录,它传输8位数据。传输中有三种模式:netascii,这是8位的ASCII码形式,另一种是octet,这是8位源数据类型;最后一种mail已经不再支持,它将返回的数据直接返回给用户而不是保存为文件。

代码分析

初始化tftpd服务端

tftp_init(&tftpContext);

const struct tftp_context tftpContext = {
	OpenFile,
	Close_File,
	Read_File,
	Write_File,
};

tftp_init实现的功能:

  • 创建一个IP4的UDP控制块PCB
  • 绑定端口
  • 设置接收回调函数
    pcb->recv = recv;
    pcb->recv_arg = recv_arg;
    

打开文件open

  1. 如果是写文件
    1. 解锁Flash
    2. 记录要写入的flash地址
    3. 擦除写入地址所在的扇区
  2. 如果是:读取文件
    1. 根据文件名,记录要读取的flash地址
  3. 返回文件句柄

关闭文件close

  • flash上锁
  • 如果之前是写文件
    • 检查是否是合法的跳转地址
    • 设置跳转地址
      • JumpAddress = *(__IO uint32_t *)(USER_FLASH_FIRST_PAGE_ADDRESS + 4);
    • 设置用户的应用程序的栈指针
      • __set_MSP(*(__IO uint32_t *)USER_FLASH_FIRST_PAGE_ADDRESS);
    • 跳转到用户的应用程序
      • Jump_To_Application();

读取文件数据read

本质就是把Flash内容赋值给RAM

((uint8_t *)buf)[Count] = *((__IO uint8_t *)Filehandle->Flash_Addr);

写文件write

FLASH_If_Write((__IO uint32_t *)&Filehandle->Flash_Addr, (uint32_t *)p->payload, Count);

p->payload中的内容,以字的方式写到Flash中