4、开发环境搭建与工程模板:基于STM32CubeMX生成基础工程,配置ETH外设与DMA,集成LwIP源码包
好,咱们进入第四章。这一章,说白了就是动手干活了。
前面三章我们把网络协议栈的来龙去脉、硬件选型、整体架构都捋了一遍。现在,是时候把袖子撸起来,真正把开发环境搭好,把工程模板建起来。
我个人习惯,做嵌入式项目,第一步永远是先把基础工程跑通。别急着写业务代码,先把灯点亮,把串口打通,把网络调通。地基不牢,后面全是坑。
4.1 开发环境准备:工具链与软件包
先看看我们需要哪些家伙什儿。
| 工具/软件 | 版本建议 | 用途 |
|---|---|---|
| STM32CubeMX | 6.x 及以上 | 图形化配置MCU外设、时钟、引脚 |
| STM32CubeIDE 或 Keil MDK | 最新稳定版 | 代码编辑、编译、调试 |
| LwIP源码包 | 2.1.3 或 2.2.0 | 轻量级TCP/IP协议栈 |
| 以太网PHY驱动 | LAN8720A / DP83848 | 物理层芯片驱动代码 |
嗯,这里要注意。STM32CubeMX版本别太老,6.0以下的版本对ETH外设的DMA支持不够好。我去年帮一个客户排查问题,他用的5.x版本,生成的代码里DMA描述符配置全是手写的,折腾了两天才发现是工具版本的问题。
4.2 使用STM32CubeMX生成基础工程
打开CubeMX,新建一个工程,选择你的芯片型号。咱们以STM32F407VET6为例,这个片子带MAC控制器,性价比很高。
第一步,配置时钟。ETH外设需要两个时钟:一个是AHB总线时钟,另一个是ETH外设自己的时钟(通常来自PLL或者外部晶振)。我个人习惯把主频配到168MHz,ETH时钟用50MHz的外部晶振。
第二步,配置ETH外设。在Pinout & Configuration界面里,找到Connectivity -> ETH。勾选“Activated”,然后选择RMII接口模式。为什么用RMII?因为引脚少啊,只用9个引脚,MII要16个。对于F4这种引脚紧张的芯片,RMII是首选。
- PHY Address: 0(具体看你的PHY芯片,LAN8720A默认是0)
- MAC Address: 随便填一个,比如 02:00:00:00:00:01
- DMA Descriptor Length: 4(这个后面会讲为什么)
- Receive Mode: Polling 或 Interrupt
第三步,配置DMA。ETH外设内部自带DMA控制器,不需要额外配置一个独立的DMA通道。你只需要在ETH配置界面里,把DMA相关的参数设好就行。我记得第一次做这个项目时,以为要单独配一个DMA2的通道,结果折腾了半天发现ETH自己就有DMA引擎。
第四步,配置一个串口,用于打印调试信息。我一般用USART1,波特率115200。这个不是必须的,但强烈建议加上。你想想看,网络协议栈跑起来后,你总得知道它连没连上吧?
配置完成后,点击“Generate Code”。嗯,这里有个坑——生成代码前,记得在Project Manager里把“Generate peripheral initialization as a pair of .c/.h files per peripheral”勾上。这样每个外设单独一个文件,代码结构清晰很多。
4.3 集成LwIP源码包
CubeMX生成的工程里,其实已经带了LwIP的选项。在Middleware里可以直接勾选LwIP。但我个人不建议这么干。
为什么?因为CubeMX集成的LwIP版本往往比较旧,而且它帮你生成的代码封装得太死了。你想改个内存池大小、调个超时参数,都得去翻它自动生成的代码,改起来束手束脚。
我习惯的做法是:手动下载LwIP源码包,自己集成进去。
步骤如下:
- 从LwIP官网或GitHub下载源码包,解压后得到
src文件夹。 - 把
src文件夹复制到你的工程目录下,比如Middlewares/Third_Party/LwIP/。 - 在IDE里添加头文件路径:
src/include、src/include/lwip、src/include/netif。 - 把
src/api、src/core、src/netif下的所有 .c 文件添加到工程里。 - 注意:
src/arch目录下的文件是平台相关的,需要自己实现。别直接加进去,会编译报错。
src/core/ipv4 和 src/core/ipv6 下的文件加全,结果编译报了一堆“undefined reference”。检查了半小时才发现少了 icmp.c。所以,加文件时仔细点,别漏。
4.4 配置ETH与DMA的底层驱动
LwIP集成好后,还需要写一个底层接口文件,把ETH的DMA驱动和LwIP的 netif 层连接起来。这个文件通常叫 stm32_eth.c 或 lan8720.c。
核心要做三件事:
- 初始化ETH外设: 配置MAC地址、速度、双工模式、DMA描述符。
- 实现数据发送函数:
low_level_output(),把LwIP的pbuf数据通过DMA发送出去。 - 实现数据接收函数:
low_level_input(),从DMA接收描述符里把数据取出来,封装成pbuf交给LwIP。
这里贴一段发送函数的伪代码,大家感受一下:
static err_t low_level_output(struct netif *netif, struct pbuf *p)
{
struct pbuf *q;
uint8_t *buffer;
uint32_t len = 0;
// 1. 获取一个空闲的DMA发送描述符
// 2. 遍历pbuf链表,把数据拷贝到DMA缓冲区
for (q = p; q != NULL; q = q->next) {
memcpy(&buffer[len], q->payload, q->len);
len += q->len;
}
// 3. 设置描述符的字节长度
// 4. 触发DMA发送
// 5. 等待发送完成(或使用中断回调)
return ERR_OK;
}
嗯,这里要注意。DMA描述符的数量和长度要匹配好。我一般配4个发送描述符、4个接收描述符,每个描述符指向的缓冲区大小设为1518字节(以太网最大帧长)。
为什么是4个?因为LwIP的 pbuf 池默认也是4个。你想想看,如果描述符太少,数据来了没地方放,就会丢包。如果太多,又浪费内存。4个是个比较平衡的值。
4.5 验证工程模板
所有代码集成好后,写一个简单的测试程序:
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
MX_ETH_Init();
// 初始化LwIP
lwip_init();
// 添加网络接口
netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ðernetif_init, ðernet_input);
// 设置默认接口
netif_set_default(&gnetif);
netif_set_up(&gnetif);
while (1)
{
// 轮询接收数据
ethernetif_input(&gnetif);
// 处理LwIP定时任务
sys_check_timeouts();
}
}
编译下载后,用网线把开发板和电脑连起来。给开发板配一个静态IP,比如 192.168.1.10。然后在电脑上 ping 192.168.1.10。
如果看到 Reply from 192.168.1.10: bytes=32 time<1ms TTL=64,恭喜你,工程模板搭建成功了!
好了,这一章的内容就到这儿。工程模板搭好了,后面咱们就可以在这个基础上,一步步把TCP、UDP、HTTP这些协议栈组件加进去。下一章,咱们聊聊LwIP的内存管理——说白了就是 pbuf 和内存池的那些事儿。