STM32F407VET6基于FreeRTOS实时操作系统和LAN8720⽹卡移
植LwIP协议栈
本次实验是在STM32F407VET6单⽚机上实现FreeRTOS实时操作系统加LwIP协议栈驱动LAN8720⽹卡,板⼦是购买的最⼩系统开发板,⽹卡是购买的LAN8720模块。使⽤的LwIP内核版本为 lwip-1.4.1,FreeRTOS内核版本为 FreeRTOSv10.2.1。使⽤上⼀篇博客中移植好的FreeRTOS⼯程。
1、STM32F407VET6单⽚机引脚与LAN8720⽹卡的物理连接如下:
  a、ETH_RMII_REF_CLK-------> PA1--------->nINT/RETCLK
  b、ETH_MDIO --------------------> PA2--------->MDIO
  c、ETH_RMII_CRS_DV --------> PA7--------->CRS
  d、ETH_RMII_TX_EN ----------> PB11------->TX_EN
  e、ETH_RMII_TXD0 ------------> PB12------->TX0
  f、ETH_RMII_TXD1 -------------> PB13------->TX1
  g、ETH_RESET-------------------> PC0-------->NC
  h、ETH_MDC ---------------------> PC1-------->MDC
  i、ETH_RMII_RXD0 -------------> PC4-------->RX0
  j、ETH_RMII_RXD1 -------------> PC5-------->RX1
  注:在⽹卡模块上RESET引脚为NC,未连接,但在实际项⽬中把RESET引脚连接起来使⽤效果更好,LAN8720⽹卡的电源建议加⼀个电源控制,如:三极管或MOS管,在复位前先断⼀下电源,再复位,因为实际中我遇到过软复位不成功的情况。
  OK到这⾥,硬件操作完成,接下来就是软件的事情了。
2、LwIP驱动框架
  在项⽬中LwIP与FreeRTOS和LAN8720以太⽹之间的⽂件关系如下:
  a、FreeRTOS与LwIP协议栈之间主要通过sys_arch.c和sys_arch.h⽂件连接起来的,这两个⽂件中主要实现了对FreeRTOS的API封装。其中sys_arch.c中实现的函数在sys.h头⽂件中全部给声明好了,我们只需实现出来即可。
  b、LAN8720以太⽹卡和LwIP协议栈之间主要通过sys_eth.c和sys_eth.h⽂件连接起来的,这两个⽂件主要实现了LwIP协议栈对以太⽹⼝操作的API函数,⽐如,以太⽹的底层收发函数、以太⽹中断等等。
  移植LwIP总共需要修改或新建⼋个⽂件,其中上⾯的四个⽂件为主要的接⼝⽂件,其余四个⽂件为辅助⽂件,分别为:cc.h、cpu.h、perf.h、lwipopts.h。
  c、cc.h主要完成了LwIP协议栈内部使⽤的数据类型的定义,如果使⽤了操作系统的话,就还包含了代码临界保护的API等等。
  d、cpu.h是和CPU相关的⼀个头⽂件,内部主要是定义了CPU⼤⼩端模式。
  e、perf.h是和系统测量、统计相关的⽂件,我们不使⽤任何的测量和统计,因此⽂件内部⽆需做什么操作。
  f、lwipopts.h是⽤来裁剪和配置LwIP的⽂件,如果我们想要使⽤LwIP 中的什么功能,就只要在这个⽂件中配置就⾏了。
  注:上⾯的⼋个⽂件全部保存在LwIP\arch⽬录下,如果⼿上没有这些⽂件时,可以新建⽂件,然后将下⾯的代码拷贝到⽂件中直接使⽤就好了,⽆需做其它修改。
3、LwIP内核源码⽂件拷贝
  在⼯程⽬录下新建⼀个LwIP⽂件夹,在LwIP⽂件夹中再新建app、arch两个⽂件夹。然后将下载的LwIP内核源码也直接解压到LwIP⽬录下,如下图所⽰:
  (注:arch⽂件夹的名字必须是这个,不能使⽤其它名字,因为在LwIP内核源码中使⽤了该⽬录下的CPU接⼝⽂件,arch⽬录下存放的都是接⼝⽂件,包含和CPU的相关的接⼝⽂件)
  然后在arch⽂件中新建如下⼋个⽂件:
4、LwIP移植相关代码的实现如下:
a、cc.h⽂件
#ifndef __CC_H
#define __CC_H
#include "cpu.h"
#include <stdio.h>
#include "FreeRTOS.h"
#include "task.h"
//定义与平台⽆关的数据类型
typedef unsigned  char    u8_t;  //⽆符号8位整数
typedef signed    char    s8_t;    //有符号8位整数
typedef unsigned  short  u16_t;  //⽆符号16位整数
typedef signed    short  s16_t;  //有符号16位整数
typedef unsigned  long    u32_t;  //⽆符号32位整数
typedef signed    long    s32_t;  //有符号32位整数
typedef u32_t mem_ptr_t;            //内存地址型数据
typedef int sys_prot_t;    //临界保护型数据
//使⽤操作系统时的临界区保护
/
/ SCB_ICSR_REG 定义的寄存器是⽤于区分当前是任务级还是中断级的,主要在Enter_Critical()与Exit_Critical()中使⽤#define SCB_ICSR_REG  (*((volatile u32_t *)0xE000ED04))
// Enter_Critical()与Exit_Critical()这两个函数是在sys_arch.c中实现的,所以此处加extern声明为外部函数
extern u32_t Enter_Critical(void); // ⽤于声明进⼊保护临界区
extern void Exit_Critical(u32_t lev); // 声明退出保护临界区
#define SYS_ARCH_DECL_PROTECT(lev) u32_t lev
#define SYS_ARCH_PROTECT(lev)  lev=Enter_Critical()
#define SYS_ARCH_UNPROTECT(lev)  Exit_Critical(lev)
//根据不同的编译器定义⼀些符号
#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 (__CC_ARM)
#define PACK_STRUCT_BEGIN __packed
#define PACK_STRUCT_STRUCT
#define PACK_STRUCT_END
#define PACK_STRUCT_FIELD(x) x
#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 (__TASKING__)
#define PACK_STRUCT_BEGIN
#define PACK_STRUCT_STRUCT
#define PACK_STRUCT_END
#define PACK_STRUCT_FIELD(x) x
#endif
//LWIP⽤printf调试时使⽤到的⼀些类型
#define U16_F "4d"
#define S16_F "4d"
#define X16_F "4x"
#define U32_F "8ld"
#define S32_F "8ld"
#define X32_F "8lx"
//宏定义
#ifndef LWIP_PLATFORM_ASSERT
#define LWIP_PLATFORM_ASSERT(x) do{printf("Assertion \"%s\" failed at line %d in %s\r\n", x, __LINE__, __FILE__);} while(0)
#endif
#ifndef LWIP_PLATFORM_DIAG
#define LWIP_PLATFORM_DIAG(x) do {printf x;} while(0)
#endif
#endif
b、cpu.h⽂件
#ifndef __CPU_H
#define __CPU_H
#define BYTE_ORDER LITTLE_ENDIAN // STM32单⽚机是⼩端模式
#endif
c、perf.h⽂件
#ifndef __PERF_H
#define __PERF_H
#define PERF_START    /* null definition */
#define PERF_STOP(x)  /* null definition */
#endif
d、lwipopts.h⽂件
#ifndef __LWIPOPTS_H
#define __LWIPOPTS_H
//线程优先级
#ifndef TCPIP_THREAD_PRIO
#define TCPIP_THREAD_PRIO  (configMAX_PRIORITIES - 1) // 定义内核任务的优先级为最⾼优先级
#endif
#undef  DEFAULT_THREAD_PRIO
#define DEFAULT_THREAD_PRIO  2
#define SYS_LIGHTWEIGHT_PROT    1  //为1时使⽤实时操作系统的轻量级保护,保护关键代码不被中断打断
#define NO_SYS                  0    //使⽤操作系统
#define MEM_ALIGNMENT          4    //使⽤4字节对齐模式
#define MEM_SIZE                16000  //内存堆heap⼤⼩
#define MEMP_NUM_PBUF          20  //MEMP_NUM_PBUF:memp结构的pbuf数量,如果应⽤从ROM或者静态存储区发送⼤量数据时,这个值应该设置⼤⼀点
#define MEMP_NUM_UDP_PCB        6  //MEMP_NUM_UDP_PCB:UDP协议控制块(PCB)数量.每个活动的UDP"连接"需要⼀个PCB.
#define MEMP_NUM_TCP_PCB        10  //MEMP_NUM_TCP_PCB:同时建⽴激活的TCP数量
#define MEMP_NUM_TCP_PCB_LISTEN 6  //MEMP_NUM_TCP_PCB_LISTEN:能够监听的TCP连接数量
#define MEMP_NUM_TCP_SEG        15  //MEMP_NUM_TCP_SEG:最多同时在队列中的TCP段数量
#define MEMP_NUM_SYS_TIMEOUT    8  //MEMP_NUM_SYS_TIMEOUT:能够同时激活的timeout个数
//pbuf选项
#define PBUF_POOL_SIZE          20  //PBUF_POOL_SIZE:pbuf内存池个数
#define PBUF_POOL_BUFSIZE      512  //PBUF_POOL_BUFSIZE:每个pbuf内存池⼤⼩
#define LWIP_TCP                1    //使⽤TCP
#define TCP_TTL                255  //⽣存时间
#undef TCP_QUEUE_OOSEQ
#define TCP_QUEUE_OOSEQ        0  //当TCP的数据段超出队列时的控制位,当设备的内存过⼩的时候此项应为0
#undef TCPIP_MBOX_SIZE
#define TCPIP_MBOX_SIZE        MAX_QUEUE_ENTRIES  //tcpip创建主线程时的消息邮箱⼤⼩
#undef DEFAULT_TCP_RECVMBOX_SIZE
#define DEFAULT_TCP_RECVMBOX_SIZE      MAX_QUEUE_ENTRIES
#undef DEFAULT_ACCEPTMBOX_SIZE
#define DEFAULT_ACCEPTMBOX_SIZE        MAX_QUEUE_ENTRIES
#define TCP_MSS                (1500 - 40)    //最⼤TCP分段,TCP_MSS = (MTU - IP报头⼤⼩ - TCP报头⼤⼩
#define TCP_SND_BUF            (4*TCP_MSS)  //TCP发送缓冲区⼤⼩(bytes).
#define TCP_SND_QUEUELEN        (2* TCP_SND_BUF/TCP_MSS) //TCP_SND_QUEUELEN: TCP发送缓冲区⼤⼩(pbuf).这个值最⼩为(2 * TCP_SND_BUF/TCP_MSS)
#define TCP_WND                (2*TCP_MSS)  //TCP发送窗⼝
#define LWIP_ICMP              1  //使⽤ICMP协议
#define LWIP_DHCP              0 //禁⽤DHCP
#define LWIP_UDP                1  //使⽤UDP服务
#define UDP_TTL                255 //UDP数据包⽣存时间
#define LWIP_STATS 0
#define LWIP_PROVIDE_ERRNO 1
//帧校验和选项,STM32F4x7允许通过硬件识别和计算IP,UDP和ICMP的帧校验和
#define CHECKSUM_BY_HARDWARE //定义CHECKSUM_BY_HARDWARE,使⽤硬件帧校验
#ifdef CHECKSUM_BY_HARDWARE
//CHECKSUM_GEN_IP==0: 硬件⽣成IP数据包的帧校验和
#define CHECKSUM_GEN_IP                0
//CHECKSUM_GEN_UDP==0: 硬件⽣成UDP数据包的帧校验和
#define CHECKSUM_GEN_UDP                0
/
/CHECKSUM_GEN_TCP==0: 硬件⽣成TCP数据包的帧校验和
#define CHECKSUM_GEN_TCP                0
//CHECKSUM_CHECK_IP==0: 硬件检查输⼊的IP数据包帧校验和
#define CHECKSUM_CHECK_IP              0
//CHECKSUM_CHECK_UDP==0: 硬件检查输⼊的UDP数据包帧校验和
#define CHECKSUM_CHECK_UDP              0
//CHECKSUM_CHECK_TCP==0: 硬件检查输⼊的TCP数据包帧校验和
#define CHECKSUM_CHECK_TCP              0
#else
//CHECKSUM_GEN_IP==1: 软件⽣成IP数据包帧校验和
#define CHECKSUM_GEN_IP                1
/
/ CHECKSUM_GEN_UDP==1: 软件⽣成UDOP数据包帧校验和
#define CHECKSUM_GEN_UDP                1
//CHECKSUM_GEN_TCP==1: 软件⽣成TCP数据包帧校验和
#define CHECKSUM_GEN_TCP                1
// CHECKSUM_CHECK_IP==1: 软件检查输⼊的IP数据包帧校验和
#define CHECKSUM_CHECK_IP              1
// CHECKSUM_CHECK_UDP==1: 软件检查输⼊的UDP数据包帧校验和
#define CHECKSUM_CHECK_UDP              1
//CHECKSUM_CHECK_TCP==1: 软件检查输⼊的TCP数据包帧校验和
#define CHECKSUM_CHECK_TCP              1
#endif
#define LWIP_NETCONN                    1  //LWIP_NETCONN==1:使能NETCON函数(要求使⽤api_lib.c)
#define LWIP_SOCKET                    1 //LWIP_SOCKET==1:使能Sicket API(要求使⽤sockets.c)
#define LWIP_COMPAT_MUTEX              1
#define LWIP_SO_RCVTIMEO                1  //通过定义LWIP_SO_RCVTIMEO使能netconn结构体中recv_timeout,使⽤recv_timeout可以避免阻塞线程
//有关系统的选项
#define TCPIP_THREAD_STACKSIZE          1000 //内核任务堆栈⼤⼩
#define DEFAULT_UDP_RECVMBOX_SIZE      2000
#define DEFAULT_THREAD_STACKSIZE        512
//LWIP调试选项
#define LWIP_DEBUG                      0  //关闭DEBUG选项
#define ICMP_DEBUG                      LWIP_DBG_OFF //开启/关闭ICMPdebug
#endif
e、sys_arch.h⽂件
#ifndef __SYS_ARCH_H
#define __SYS_ARCH_H
#include "FreeRTOS.h"
#include "semphr.h"
#include "queue.h"
#define MAX_QUEUES        10 // 消息邮箱的数量
#define MAX_QUEUE_ENTRIES  20 // 每个消息邮箱的⼤⼩
// LwIP消息邮箱结构体
typedef struct
{
  QueueHandle_t xQueue; // 消息列队
}lwip_mbox;
typedef lwip_mbox sys_mbox_t; // LwIP使⽤的消息邮箱类型
typedef QueueHandle_t sys_mutex_t; // LwIP使⽤的互斥信号量类型
typedef SemaphoreHandle_t sys_sem_t; // LwIP使⽤的信号量类型
typedef unsigned char sys_thread_t; // LwIP线程错误类型
#endif
f、sys_arch.c⽂件
#include "lwip/def.h"
#include "lwip/sys.h"
#include "lwip/mem.h"
#include "sys_arch.h"
const u32_t NullValue; // 定义⼀个常量值,⽤于空指针
/********************************************************
* 函数功能:创建⼀个消息邮箱
* 形参:mbox:消息邮箱
size:邮箱⼤⼩
* 返回值:ERR_OK=创建成功,其他=创建失败
********************************************************/
err_t sys_mbox_new( sys_mbox_t *mbox, int size)
{
  if(size > MAX_QUEUE_ENTRIES)
  {
    size = MAX_QUEUE_ENTRIES; // 消息列队中的消息数⽬检查
  }
  // 创建消息列队,该消息列队存放指针(4字节)
  mbox->xQueue = xQueueCreate(size, sizeof(void *));
  if(mbox->xQueue != NULL)单片机printf函数
  {
    return ERR_OK; // 消息列队创建成功
  }
  return ERR_MEM; // 消息列队创建失败
}
/********************************************************
* 函数功能:释放并删除⼀个消息邮箱
* 形参:mbox:消息邮箱
* 返回值:⽆
********************************************************/
void sys_mbox_free(sys_mbox_t *mbox)
{
  vQueueDelete(mbox->xQueue);
  mbox->xQueue = NULL;
}
/********************************************************
* 函数功能:向消息邮箱中发送⼀条消息(等待发送成功才会返回)
* 形参:mbox:消息邮箱
msg:要发送的消息
* 返回值:⽆
********************************************************/
void sys_mbox_post(sys_mbox_t *mbox, void *msg)
{
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;
  if(msg == NULL)
  {
    msg = (void *)&NullValue; // 空指针的处理⽅式(⽤常量的地址替换)
  }
  if((SCB_ICSR_REG & 0xFF) == 0) // 线程执⾏
  {
    while(xQueueSendToBack(mbox->xQueue, &msg, portMAX_DELAY) != pdPASS); // 等待发送成功
  }
  else // 中断执⾏
  {
    while(xQueueSendToBackFromISR(mbox->xQueue, &msg, &xHigherPriorityTaskWoken) != pdPASS);    portYIELD_FROM_ISR(xHigherPriorityTaskWoken); // 判断是否要进⾏任务切换
  }