为了学习lwIP,网购了一块正点原子的Mini STM32开发板和一个ENC28J60以太网模块,发现正点原子所给的示例代码是基于lwIP1.4.1的,有点偏老,最新版本的lwIP是2.1.2,使用的开发平台是Keil uVison5,而自己习惯了在STM32CubeIDE进行编程。STM32CubeIDE是免费的,其图形界面配置生成代码的方式非常便捷,而且其代码编辑能力丝毫不比Keil差,这是我选择STM32CubeIDE进行开发的原因。于是就产生了在STM32CubeIDE上移植lwIP的想法,经过几天的摸索,踩了不少坑,终于移植成功了。现在把移植步骤和要点作简要记录。
一开始采用从http://download.savannah.nongnu.org/releases/lwip/上下载lwip-2.1.2.zip和contrib-2.1.0.zip,参照网上移植步骤一步一步来,发现比较麻烦,且容易出错。后来发现STM32CubeIDE(1.5.0版本)自己带有lwIP库,是最新的2.1.2版本,而且可以通过图形界面配置lwIP的参数,真是非常的方便。只是因为STM32F103芯片不带以太网控制器,所以无法在IDE的图形配置中调出lwIP的配置选项。
要想在STM32F103中使用STM32CubeIDE自带的lwIP包,首先必须选择一款带有以太网控制器的芯片(本例中选择STM32F407VE), 来建立一个临时工程,在临时工程中配置好lwIP,然后把配置好的lwIP包拷贝到F103的项目工程中,再修改lwIP的底层接口函数和ENC28J60的驱动函数。具体步骤实现如下:
首先,使用STM32F407VE建立临时工程,在Connectivity中选择以太网控制器“ETH”,在Mode中选“RMII”(选择其他也可,只要Mode不为“Disable”就行,主要目的是使能“ETH”后,才能配置lwIP),如下图所示:

在配置“ETH”后,可使能lwIP,并进行相应的配置了:



下面接着对Key Options中内存堆的大小进行调整,原来大小为1600字节,调整为2048字节(ENC28J60驱动在接收数据包时申请的内存大约1600字节),其他的保持默认配置不用改动。本例为无操作系统移植,所以没有使能Middleware中的FREERTOS,如果想要RTOS的支持,可采用类似的方式进行配置。因为我们只要临时项目生成配置好的lwIP包,其他的项目配置内容,如系统时钟、中断、GPIO口等,都不用去管。至此可以点击“Generate Code”生成代码了,在生成代码的过程中,如果有警告信息,请忽视,这是警告项目中还有很多配置没设置好的原因。代码生成完后,项目根目录下多出两个文件夹“LWIP”和“Middllewares”:

下一步是生成STM32F103的项目(MiniSTM32使用的是STM32F103RCT6),并把F407临时项目生成的“LWIP”和“Middllewares”文件夹拷贝到F103项目的根目录下,并按照下图在项目的“Properties”->“C/C++ General”->"Paths and Symbols"中添加头文件、代码的包含路径。

Include包含的头文件比较多,如果一个一个添加嫌麻烦的话,可用下面的“Export Settings…”在F407临时项目中导出xml配置文件,直接导入移植目标项目中,然后删除包含“STM32F4xx”的条目即可。至此,lwIP包的核心部分移植基本完成,下面进行lwIP底层和ENC28J60驱动接口的更改、SPI相关GPIO口的设置。
因为本移植是在正点原子的STM32 Mini开发板上进行的,所以ENC28J60的驱动也是在正点原子提供的驱动基础上进行修改的,具体下载地址为http://www.openedv.com/docs/book-videos/zdyzshipin/4free/Lwip.html,包含两个文件:enc28j60.c和enc28j60.h,把这两个文件拷贝到移植项目的“Core/Src”文件下,注意,这两个文件的编码方式为GB2312,需要使用notepad++或sublime等软件转换为UTF-8编码,不然在STM32CubeIDE中打开时,中文注释会出现乱码。
正点原子ENC28J60模块有8个引脚,分别是: GND、 RST、MISO、SCK、MOSI、INT、CS 和 V3.3。其中GND和 V3.3用于给模块供电,MISO/MOSI/SCK用于SPI通信,CS是片选信号,INT为中断输出引脚,RST为模块复位信号。网上有很多ENC28J60模块是10引脚的,不能和Mini开发板直接连接,这点请注意。除了电源和地,其他6个引脚和STM32F103的GPIO的对应关系如下:

在本项目中,ENC28J60模块是和STM32F103的SPI1连接通信的,所以接下来是对SPI1端口以及相关的片选、中断、复位的GPIO端口设置(STM32F103的RCC基本时钟、JTAG调试的配置请参阅相关资料,这里就不赘述了)。首先设置SPI1模块,Mode设置为“Full-Duplex Master”,波特率的分频系数设为8,其他的参数保持默认即可:

其他GPIO端口设置:PA1(INT)设置为中断模式,其“GPIO mode”配置为下降沿触发方式(External Interrupt Mode with Falling edge trigger detection),“GPIO Pull-up/Pull-down”设置为上拉模式(Pull-up),PA4(RST)和PC4(CS)都设置成:GPIO mode为“Output Push Pull”;GPIO output level为“High”,具体见图9。

接下来在NVIC中配置ENC28J60模块的中断引脚,中断连接STM32F103的PA1口,使能EXTI line1 interrupt,并设置中断优先级为2(具体优先级的设置可由自己决定,但需掌握STM32的中断机制):

至此,图形界面的软件、硬件配置基本完成,进入软件修改部分,主要修改的内容分三块:ENC28J60驱动、lwIP底层接口和main函数。下面依次来说明软件的修改过程。
在修改ENC28J60驱动前,需要在“Core/Src”目录下新建一个头文件“”enc28j60_sys.h",该文件定义了一些驱动需要的数据类型、GPIO端口位操作宏等(参考正点原子驱动中的sys.h),其内容如下:
#ifndef SRC_ENC28J60_SYS_H_
#define SRC_ENC28J60_SYS_H_#include "stm32f1xx_hal.h"
#include "stm32f103xe.h"
#include "ethernetif.h"typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;
typedef __IO uint32_t vu32;#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
#define GPIOA_ODR_Addr (GPIOA_BASE+12) //0x4001080C
#define GPIOC_ODR_Addr (GPIOC_BASE+12) //0x4001100C#define GPIOA_IDR_Addr (GPIOA_BASE+8) //0x40010808
#define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //输出
#define PCout(n) BIT_ADDR(GPIOC_ODR_Addr,n) //输出
#define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //输入#define delay_ms(x) HAL_Delay(x) //用HAL库的延时函数替代 delay_ms()
#define INTX_DISABLE() __disable_irq() //关中断
#define INTX_ENABLE() __enable_irq() //开中断
#define printf(x,...) //因为没有使能串口,未重定义printf()函数,用宏定义去掉原来驱动的打印函数
#endif /* SRC_ENC28J60_SYS_H_ */
在前面的步骤中,已经把ENC28J60的驱动拷贝到“Core/Src”目录下,对enc28j60.h的修改比较简单,只需要把原来的#include "sys.h"替换为#include "enc28j60_sys.h"即可。
enc28j60.c的修改相对来说麻烦点,需要声明两个外部变量:“hspi1”为IDE自动生成的SPI1句柄,在main.c中定义,netif结构体gnetif,在lwip.c中定义,增加三个函数,其中两个SPI1_ReadWriteByte()、SPI1_SetSpeed()为SPI1的读写和速度调整函数,另一个为ENC28J60接收到数据而触发的中断回调函数HAL_GPIO_EXTI_Callback(),原来的中断处理函数“void EXTI1_IRQHandler(void)”和“void ENC28J60_ISRHandler(void)”都注释掉,enc28j60.c中变动部分的代码如下:
extern SPI_HandleTypeDef hspi1; //IDE自动生成,在main.c文件中定义
extern struct netif gnetif; //lwIP netif结构体,在lwip.c文件中定义void SPI1_SetSpeed(u8 SPI_BaudRatePrescaler)
{assert_param(IS_SPI_BAUDRATE_PRESCALER(SPI_BaudRatePrescaler));//判断有效性__HAL_SPI_DISABLE(&hspi1); //关闭SPIhspi1.Instance->CR1&=0XFFC7; //位3-5清零,用来设置波特率hspi1.Instance->CR1|=SPI_BaudRatePrescaler;//设置SPI速度__HAL_SPI_ENABLE(&hspi1); //使能SPI}u8 SPI1_ReadWriteByte(u8 TxData)
{u8 Rxdata;HAL_SPI_TransmitReceive(&hspi1,&TxData,&Rxdata,1, 1000);return Rxdata; //返回收到的数据
}//初始化ENC28J60
//macaddr:MAC地址
//返回值:0,初始化成功;
// 1,初始化失败;
u8 ENC28J60_Init(void)
{u8 version;u16 retry=0;u32 temp;__HAL_SPI_ENABLE(&hspi1); //使能SPI外设//初始化MAC地址temp=*(vu32*)(0x1FFFF7E8); //获取STM32的唯一ID的前24位作为MAC地址后三字节enc28j60_dev.macaddr[0]=2;enc28j60_dev.macaddr[1]=0;enc28j60_dev.macaddr[2]=0;enc28j60_dev.macaddr[3]=(temp>>16)&0XFF; //低三字节用STM32的唯一IDenc28j60_dev.macaddr[4]=(temp>>8)&0XFFF;enc28j60_dev.macaddr[5]=temp&0XFF;ENC28J60_RST=0; //复位ENC28J60delay_ms(10); ENC28J60_RST=1; //复位结束 delay_ms(10); ENC28J60_Write_Op(ENC28J60_SOFT_RESET,0,ENC28J60_SOFT_RESET); //软件复位while(!(ENC28J60_Read(ESTAT)&ESTAT_CLKRDY)&&retry<250) //等待时钟稳定{retry++;delay_ms(1);} if(retry>=250)return 1; //ENC28J60初始化失败version=ENC28J60_Get_EREVID(); //获取ENC28J60的版本号printf("ENC28J60 Version:%d\r\n",version); enc28j60_dev.NextPacketPtr=RXSTART_INIT;ENC28J60_Write(ERXSTL,RXSTART_INIT&0XFF); //设置接收缓冲区起始地址低8位ENC28J60_Write(ERXSTH,RXSTART_INIT>>8); //设置接收缓冲区起始地址高8位//设置接收接收字节ENC28J60_Write(ERXNDL,RXSTOP_INIT&0XFF); ENC28J60_Write(ERXNDH,RXSTOP_INIT>>8);//设置发送起始字节ENC28J60_Write(ETXSTL,TXSTART_INIT&0XFF);ENC28J60_Write(ETXSTH,TXSTART_INIT>>8);//设置发送结束字节ENC28J60_Write(ETXNDL,TXSTOP_INIT&0XFF);ENC28J60_Write(ETXNDH,TXSTOP_INIT>>8);//ERXWRPTH:ERXWRPTL 寄存器定义硬件向FIFO 中//的哪个位置写入其接收到的字节。 指针是只读的,在成//功接收到一个数据包后,硬件会自动更新指针。 指针可//用于判断FIFO 内剩余空间的大小 8K-1500。 //设置接收读指针字节ENC28J60_Write(ERXRDPTL,RXSTART_INIT&0XFF);ENC28J60_Write(ERXRDPTH,RXSTART_INIT>>8);//接收过滤器ENC28J60_Write(ERXFCON,0);ENC28J60_Write(EPMM0,0X3F);ENC28J60_Write(EPMM1,0X30);ENC28J60_Write(EPMCSL,0Xf9);ENC28J60_Write(EPMCSH,0Xf7);ENC28J60_Write(MACON1,MACON1_MARXEN|MACON1_TXPAUS|MACON1_RXPAUS);//将MACON2 中的MARST 位清零,使MAC 退出复位状态。ENC28J60_Write(MACON2,0x00);ENC28J60_Write(MACON3,MACON3_PADCFG0|MACON3_TXCRCEN|MACON3_FRMLNEN|MACON3_FULDPX);// 最大帧长度 1518ENC28J60_Write(MAMXFLL,MAX_FRAMELEN&0XFF);ENC28J60_Write(MAMXFLH,MAX_FRAMELEN>>8);ENC28J60_Write(MABBIPG,0x15);ENC28J60_Write(MAIPGL,0x12);ENC28J60_Write(MAIPGH,0x0C);//设置MAC地址ENC28J60_Write(MAADR5,enc28j60_dev.macaddr[0]);ENC28J60_Write(MAADR4,enc28j60_dev.macaddr[1]);ENC28J60_Write(MAADR3,enc28j60_dev.macaddr[2]);ENC28J60_Write(MAADR2,enc28j60_dev.macaddr[3]);ENC28J60_Write(MAADR1,enc28j60_dev.macaddr[4]);ENC28J60_Write(MAADR0,enc28j60_dev.macaddr[5]);//配置PHY为全双工 LEDB为拉电流ENC28J60_PHY_Write(PHCON1,PHCON1_PDPXMD); ENC28J60_PHY_Write(PHCON2,PHCON2_HDLDIS);ENC28J60_Set_Bank(ECON1);ENC28J60_Write_Op(ENC28J60_BIT_FIELD_SET,EIE,EIE_INTIE|EIE_PKTIE|EIE_TXIE|EIE_TXERIE|EIE_RXERIE);ENC28J60_Write_Op(ENC28J60_BIT_FIELD_SET,ECON1,ECON1_RXEN);printf("ENC28J60 Duplex:%s\r\n",ENC28J60_Get_Duplex()?"Full Duplex":"Half Duplex"); //获取双工方式return 0;
}void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{while(ENC28J60_INT == 0){u8 status;u8 packetnum;u16 temp;ENC28J60_Write_Op(ENC28J60_BIT_FIELD_CLR,EIE,EIE_INTIE); //关闭ENC28J60的全局中断status=ENC28J60_Read(EIR); //读取以太网中断标志寄存器if(status&EIR_PKTIF) //接收到数据,处理数据{ENC28J60_Write_Op(ENC28J60_BIT_FIELD_CLR,EIR,EIR_PKTIF); //清除ENC28J60的接收中断标志位ethernetif_input(&gnetif);}if(status&EIR_TXIF) //以太网发送中断{ENC28J60_Write_Op(ENC28J60_BIT_FIELD_CLR,EIR,EIR_TXIF); //清除ENC28J60的接收中断标志位}if(status&EIR_RXERIF) //接收错误中断标志位{ENC28J60_Write_Op(ENC28J60_BIT_FIELD_CLR,EIR,EIR_RXERIF);packetnum=ENC28J60_Read(EPKTCNT);temp=ENC28J60_Read(ERXRDPTH)<<8; //读取高字节temp|=ENC28J60_Read(ERXRDPTL); //读取低字节temp++;ENC28J60_Write(ERXRDPTL,temp&0XFF); //先写入低字节ENC28J60_Write(ERXRDPTH,temp>>8); //先写入低字节ENC28J60_Write_Op(ENC28J60_BIT_FIELD_SET,ECON2,ECON2_PKTDEC);printf("接收错误!接收到数据包个数:%d\r\n",packetnum);}if(status&EIR_TXERIF) //发送错误中断标志位{ENC28J60_Write_Op(ENC28J60_BIT_FIELD_CLR,EIR,EIR_TXERIF);ENC28J60_Write_Op(ENC28J60_BIT_FIELD_CLR,ESTAT,ESTAT_LATECOL|ESTAT_TXABRT);printf("发送错误!\r\n");}ENC28J60_Write_Op(ENC28J60_BIT_FIELD_SET,EIE,EIE_INTIE); //打开ENC28J60的全局中断}
}
lwIP底层接口代码的修改。在文件中增加extern dev_strucrt enc28j60_dev变量的声明,主要修改的函数是low_level_init()、low_level_output()、low_level_input()、ethernetif_input()和ethernetif_init()五个函数,保留原来的sys_jiffies()和sys_now()函数,其余的函数都删除掉,修改后的ethernet.c驱动文件如下:
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "lwip/opt.h"
#include "lwip/mem.h"
#include "lwip/memp.h"
#include "lwip/timeouts.h"
#include "netif/ethernet.h"
#include "netif/etharp.h"
#include "lwip/ethip6.h"
#include "ethernetif.h"
#include <string.h>
#include "../../Core/Src/enc28j60.h"
/* Network interface name */
#define IFNAME0 's'
#define IFNAME1 't'
extern dev_strucrt enc28j60_dev;static void low_level_init(struct netif *netif)
{netif->hwaddr_len = ETHARP_HWADDR_LEN; //设置MAC地址长度,为6个字节//初始化MAC地址,和ENC28J60驱动中的MAC地址一致netif->hwaddr[0] = enc28j60_dev.macaddr[0];netif->hwaddr[1] = enc28j60_dev.macaddr[1];netif->hwaddr[2] = enc28j60_dev.macaddr[2];netif->hwaddr[3] = enc28j60_dev.macaddr[3];netif->hwaddr[4] = enc28j60_dev.macaddr[4];netif->hwaddr[5] = enc28j60_dev.macaddr[5];netif->mtu=1500; //最大允许传输单元,允许该网卡广播和ARP功能netif->flags = NETIF_FLAG_BROADCAST|NETIF_FLAG_ETHARP|NETIF_FLAG_LINK_UP;}static err_t low_level_output(struct netif *netif, struct pbuf *p)
{struct pbuf *q;int l=0;u8 *buffer;buffer=mem_malloc(p->tot_len); //申请内存if(buffer==NULL)printf("发送数据缓冲区内存申请失败\r\n");for(q=p;q!=NULL;q=q->next){memcpy((u8_t*)&buffer[l], q->payload, q->len);l=l+q->len;}ENC28J60_Packet_Send(p->tot_len,buffer);mem_free(buffer); //释放内存return ERR_OK;
}static struct pbuf * low_level_input(struct netif *netif)
{struct pbuf *p,*q;u32 len;u8 *buffer;int l=0;p=NULL;buffer=mem_malloc(1600); //申请内存if(buffer!=NULL)len=ENC28J60_Packet_Receive(MAX_FRAMELEN,buffer); //接收数据else{printf("接收数据缓冲区内存申请失败\r\n");return p;}p=pbuf_alloc(PBUF_RAW,len,PBUF_POOL); //pbufs内存池分配pbufif(p!=NULL){for(q=p;q!=NULL;q=q->next){memcpy((u8_t*)q->payload,(u8_t*)&buffer[l], q->len);l=l+q->len;}}mem_free(buffer); //释放内存return p;
}void ethernetif_input(struct netif *netif)
{err_t err;struct pbuf *p;p=low_level_input(netif); //调用low_level_input函数接收数据if(p==NULL) return;err=netif->input(p, netif); //调用netif结构体中的input字段(一个函数)来处理数据包if(err!=ERR_OK){LWIP_DEBUGF(NETIF_DEBUG,("ethernetif_input: IP input error\n"));pbuf_free(p);p = NULL;}return;
}err_t ethernetif_init(struct netif *netif)
{LWIP_ASSERT("netif!=NULL",(netif!=NULL));
#if LWIP_NETIF_HOSTNAME //LWIP_NETIF_HOSTNAMEnetif->hostname="lwip"; //初始化名称
#endifnetif->name[0]=IFNAME0; //初始化变量netif的name字段netif->name[1]=IFNAME1; //在文件外定义这里不用关心具体值netif->output=etharp_output;//IP层发送数据包函数netif->linkoutput=low_level_output;//ARP模块发送数据包函数low_level_init(netif); //底层硬件初始化函数return ERR_OK;
}
void ethernetif_update_config(struct netif *netif)
{//定义的空函数,lwip.c中有对该函数的调用,避免编译报错
}
u32_t sys_jiffies(void)
{return HAL_GetTick();
}u32_t sys_now(void)
{return HAL_GetTick();
}
最后是对main.c文件以及一些相关变量的调整,当ENC28J60模块接收到报文时,可采用两种方式读取数据:轮询方式和中断方式,在本移植中采用中断方式来接收报文。首先在main.c中声明四个外部函数:
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
extern void MX_LWIP_Init(void);
extern void MX_LWIP_Process(void);
extern unsigned char ENC28J60_Init(void);
extern void sys_check_timeouts(void);
/* USER CODE END 0 */
再在main()函数while循环的前面添加ENC28J60_Init()和MX_LWIP_Init()两个初始化函数,在while循环内添加sys_check_timeouts()函数:
/* USER CODE BEGIN WHILE */ENC28J60_Init();MX_LWIP_Init();while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */sys_check_timeouts();}/* USER CODE END 3 */
另外需要在lwip.h头文件注释掉“extern ETH_HandleTypeDef heth;”,已防编译报错,这个变量是在临时项目中IDE自动生成的以太网控制器的类型声明,在本项目中不需要。
至此,全部的代码修改已完成,IP地址的定义在lwip.c文件中的MX_LWIP_Init()函数内,如需修改IP地址,请在此处进行。编译下载后就可进行验证测试了,用网线连接开发板和PC机网口,注意首先要关闭windows的防火墙,不然会出现ping不通的现象,配置PC上网卡的IP地址为:192.168.1.20,掩码:255.255.255.0,网关:192.168.1.1,完成后从电脑上ping开发板地址192.168.1.100,结果如下:

能ping通,则说明lwIP内部的协议栈运行正常。本次lwIP移植为无操作系统实验性质的移植,如需要RTOS的支持,可采用类似的方式进行,在有以太网控制器的芯片上建立临时项目,配置好lwIP和RTOS选项后,拷贝相关的内容至STM32F103项目下进行开发。

















