1. 硬件平台搭建与初始化
搞过嵌入式开发的都知道,硬件是软件的基础。我们这次用的STM32F407+LAN8720A组合,在工业控制领域算是黄金搭档了。STM32F407自带MAC控制器,配合LAN8720A这颗性价比超高的PHY芯片,搭建以太网通信简直不要太方便。
先说说硬件连接的关键点。LAN8720A通过RMII接口与STM32F407连接,这里特别注意时钟配置:PHY芯片的50MHz时钟可以由外部晶振提供,也可以通过STM32的MCO引脚输出。我推荐用后者,能省一个晶振的位置。具体硬件连接如下表:
| STM32F407引脚 | LAN8720A引脚 | 功能说明 |
|---|---|---|
| PA1 | REF_CLK | RMII参考时钟 |
| PA2 | MDIO | 管理数据IO |
| PA7 | CRS_DV | 载波侦听 |
| PC1 | MDC | 管理时钟 |
| PC4 | RXD0 | 接收数据0 |
| PC5 | RXD1 | 接收数据1 |
| PG11 | TX_EN | 发送使能 |
| PG13 | TXD0 | 发送数据0 |
| PG14 | TXD1 | 发送数据1 |
硬件搭好后,上电前记得检查LAN8720A的nINT/REFCLKO引脚配置。这个引脚决定了时钟输出模式,我们这里需要配置为50MHz时钟输出模式。
2. LwIP协议栈移植与配置
LwIP作为轻量级TCP/IP协议栈,在资源受限的嵌入式系统中表现优异。但初次移植时,有几个坑我不得不提醒大家。
首先是内存池的配置,在lwipopts.h文件中,这几个参数需要特别注意:
#define MEM_SIZE (12 * 1024) // 内存池大小 #define PBUF_POOL_SIZE 16 // PBUF缓冲池数量 #define PBUF_POOL_BUFSIZE 512 // 每个PBUF的大小 #define TCP_MSS 1460 // TCP最大分段大小 #define TCP_SND_BUF (4 * TCP_MSS) // TCP发送缓冲区内存分配不足会导致各种奇怪的网络问题,比如ping不通、TCP连接不稳定等。我建议先用大一些的值,等系统稳定后再逐步优化。
网络接口的初始化也很关键,在ethernetif.c文件中需要实现low_level_init函数:
static void low_level_init(struct netif *netif) { /* 设置MAC地址 */ netif->hwaddr_len = ETHARP_HWADDR_LEN; netif->hwaddr[0] = 0x00; netif->hwaddr[1] = 0x80; netif->hwaddr[2] = 0xE1; netif->hwaddr[3] = 0x00; netif->hwaddr[4] = 0x00; netif->hwaddr[5] = 0x01; /* 最大传输单元 */ netif->mtu = 1500; /* 设备能力 */ netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP; /* 初始化以太网外设 */ ETH_Init(); }3. FreeModbus TCP从机移植
FreeModbus是一个开源的Modbus协议栈,支持RTU/ASCII/TCP三种模式。我们要用的是TCP模式,需要重点关注以下几个文件的修改:
首先是porttcp.c文件,需要实现TCP相关的接口函数。这里给出关键部分的实现:
BOOL xMBTCPPortInit(USHORT usTCPPort) { struct tcp_pcb *pcb; /* 创建新的TCP控制块 */ pcb = tcp_new(); if(pcb == NULL) return FALSE; /* 绑定到指定端口 */ if(tcp_bind(pcb, IP_ADDR_ANY, usTCPPort) != ERR_OK) { tcp_close(pcb); return FALSE; } /* 开始监听 */ pcb = tcp_listen(pcb); if(pcb == NULL) return FALSE; /* 设置接收回调 */ tcp_accept(pcb, xMBTCPPortAccept); return TRUE; }然后是modbus寄存器回调函数的实现。这是Modbus通信的核心,主机所有的读写操作最终都会调用这些回调函数。以保持寄存器为例:
eMBErrorCode eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode) { eMBErrorCode eStatus = MB_ENOERR; int iRegIndex; /* 检查地址范围 */ if((usAddress + usNRegs) > REG_HOLDING_NREGS) { return MB_ENOREG; } switch(eMode) { case MB_REG_READ: /* 读取保持寄存器值 */ for(iRegIndex = 0; iRegIndex < usNRegs; iRegIndex++) { pucRegBuffer[iRegIndex*2] = (UCHAR)(usRegHoldingBuf[usAddress+iRegIndex] >> 8); pucRegBuffer[iRegIndex*2+1] = (UCHAR)(usRegHoldingBuf[usAddress+iRegIndex] & 0xFF); } break; case MB_REG_WRITE: /* 写入保持寄存器 */ for(iRegIndex = 0; iRegIndex < usNRegs; iRegIndex++) { usRegHoldingBuf[usAddress+iRegIndex] = (pucRegBuffer[iRegIndex*2] << 8) | pucRegBuffer[iRegIndex*2+1]; } break; } return eStatus; }4. 主循环与任务调度
在FreeRTOS环境下,我们需要创建两个主要任务:LwIP的tcpip_thread和Modbus的轮询任务。任务优先级设置很关键,建议给LwIP任务更高的优先级:
void StartDefaultTask(void const *argument) { /* 初始化LwIP */ tcpip_init(NULL, NULL); /* 初始化网络接口 */ netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ðernetif_init, &tcpip_input); netif_set_default(&gnetif); netif_set_up(&gnetif); /* 初始化Modbus TCP */ eMBTCPInit(MB_TCP_PORT); eMBEnable(MB_TCP); /* 主循环 */ for(;;) { /* 处理Modbus请求 */ (void)eMBPoll(); /* 喂狗等其他操作 */ HAL_IWDG_Refresh(&hiwdg); osDelay(50); } }如果没有使用RTOS,主循环可以这样写:
while(1) { /* 处理LwIP协议栈 */ sys_check_timeouts(); /* 处理Modbus请求 */ (void)eMBPoll(); /* 简单的延时 */ HAL_Delay(10); }5. 调试技巧与常见问题
调试网络应用时,我习惯先用简单的ping测试验证底层是否正常。如果ping不通,按这个顺序排查:
- 检查PHY芯片的电源和复位信号
- 用示波器看RMII时钟是否正常
- 确认MAC和PHY的寄存器配置正确
- 检查LwIP的内存配置是否足够
当Modbus TCP通信异常时,Wireshark抓包是必备技能。过滤条件可以设为:
tcp.port == 502常见的问题及解决方法:
问题1:主机连接不上从机
- 检查防火墙设置
- 确认端口号是否正确(默认502)
- 用netstat查看端口监听状态
问题2:通信时断时续
- 检查网络电缆和连接器
- 增大LwIP的内存池大小
- 调整TCP的超时参数
问题3:响应速度慢
- 优化Modbus轮询频率
- 检查是否有其他任务占用太多CPU
- 考虑使用DMA传输
6. 性能优化实战
在工业现场,通信的实时性和稳定性至关重要。经过多次项目实践,我总结了几个优化点:
- 中断优化:将ETH中断优先级设置为最高,避免丢包
HAL_NVIC_SetPriority(ETH_IRQn, 0, 0); HAL_NVIC_EnableIRQ(ETH_IRQn);- 内存优化:使用零拷贝技术减少数据搬运
struct pbuf_custom p; p.custom_free_function = my_free_function; p.payload = my_buffer; pbuf_alloced_custom(PBUF_RAW, len, PBUF_REF, &p);- 协议优化:调整TCP窗口大小提高吞吐量
#define TCP_WND (4 * TCP_MSS) #define TCP_SND_BUF (4 * TCP_MSS)- 任务优化:合理设置任务优先级
- LwIP任务 > Modbus任务 > 应用任务
- 以太网中断 > 系统定时器中断
7. 工业应用实例
去年我做了一个智能电表采集项目,正好用到了这套方案。现场有30多台电表通过交换机连接到我们的STM32F407网关,要求每5分钟采集一次数据。
系统架构如下:
[电表1] ----+ [电表2] ----+ ... ---- [交换机] ---- [STM32F407网关] ---- [云平台] [电表30] ---+关键实现代码:
/* 自定义的Modbus映射表 */ typedef struct { float voltage; // 电压 float current; // 电流 float power; // 功率 uint32_t energy; // 电能 } MeterData; MeterData meterData[MAX_METER_NUM]; /* 保持寄存器回调函数 */ eMBErrorCode eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode) { int meterId = usAddress / METER_REG_SIZE; int regOffset = usAddress % METER_REG_SIZE; if(meterId >= MAX_METER_NUM) { return MB_ENOREG; } /* 根据寄存器地址映射到数据结构 */ switch(regOffset) { case VOLTAGE_REG: // 处理电压寄存器 break; case CURRENT_REG: // 处理电流寄存器 break; // 其他寄存器... } return MB_ENOERR; }这个项目让我深刻体会到,好的架构设计能大大降低后期维护成本。比如将Modbus寄存器地址映射到数据结构,而不是硬编码,后续新增电表类型时只需修改映射关系即可。