为了学习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); //关闭SPI
hspi1.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的唯一ID
enc28j60_dev.macaddr[4]=(temp>>8)&0XFFF;
enc28j60_dev.macaddr[5]=temp&0XFF;
ENC28J60_RST=0; //复位ENC28J60
delay_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);
// 最大帧长度 1518
ENC28J60_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内存池分配pbuf
if(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_HOSTNAME
netif->hostname="lwip"; //初始化名称
#endif
netif->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项目下进行开发。