拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32F103+DM9000+LWIP以太网通信实战:FSMC配置与协议栈移植详解

简介面向嵌入式物联网单片机项目开发者该压缩包提供一份基于STM32F103和DM9000以太网芯片的LWIP协议通信完整例程。代码采用KEIL HAL库编写已在STM32F103上运行验证更换同系列其他型号时仅需调整芯片型号与FLASH容量并注意选择J-Link或ST-Link调试器移植性表现良好可直接嵌入到实际网络产品原型。包内共374个文件以157个C源码和181个头文件为核心覆盖以太网协议栈、底层外设驱动及上层应用逻辑同时附带hex固件、Keil工程、烧录脚本和说明文档整体仅2.71MB便于快速下载与对照学习。目前已有217人学习代码中引脚接线均有清晰定义注释详尽。阅读后可深入理解DM9000驱动方式、LWIP协议栈集成思路及单片机网络应用开发流程尤其适合毕业设计、项目立项或从入门到进阶的开发者作为实用参考资料。1. 这块板子真没必要换MAC芯片DM9000配合HAL库跑通LWIP才是性价比最高的一条路做单片机以太网项目第一步就卡在选型上带MAC的芯片贵且缺货用外部PHY又绕不开MAC层实现。其实STM32F103配上DM9000这颗以太网控制器走FSMC总线挂载、HAL库初始化、LWIP协议栈通信是成本最低、资料最全、踩坑最可控的一条路线。这套例程把FSMC时序配置、DM9000寄存器读写、LWIP底层netif适配都提前拆好了你拿到手改下引脚和芯片型号就能跑。适合正在做嵌入式联网设备、需要快速实现TCP/UDP通信的工程师也适合想把LWIP内部数据流捋清楚的学生。下面我按实际调试顺序把从寄存器到协议栈的每个关键点过一遍。2. FSMC总线初始化与DM9000寄存器读写时序2.1 DM9000挂载在FSMC Bank1上的地址映射关系DM9000的数据线直接接STM32F103的FSMC_D0-D15命令/数据引脚CMD由地址线A1控制片选CS接到FSMC_NE1。这个接法决定了DM9000在内存中被映射到Bank1的起始地址0x60000000CMD位为低时访问的是索引寄存器CMD位为高时访问的是数据寄存器。因为A1接CMD所以命令地址偏移是0x00数据地址偏移是0x02这也是代码里常见0x60000000和0x60000002两个宏的由来。2.1.1 地址错位问题很多人在读写DM9000时发现数据全是0xFF或者写入无效多半是地址偏移算错。A1作为寄存器选择位访问地址需要按字对齐不能简单地以为命令地址是0x60000000、数据地址是0x60000001。FSMC的16位模式下地址线是右移一位对齐的因此数据地址必须是0x60000002。我一般会在初始化后先读芯片ID寄存器读到0x90000A46说明地址映射正确否则先查地址线和FSMC配置。#define DM9000_CMD_ADDR ((uint32_t)0x60000000) #define DM9000_DATA_ADDR ((uint32_t)0x60000002) static uint8_t dm9000_read_reg(uint8_t reg) { *(volatile uint16_t *)DM9000_CMD_ADDR reg; return *(volatile uint8_t *)DM9000_DATA_ADDR; }这段代码是DM9000所有寄存器操作的基础。第一步向命令地址写入寄存器编号第二步从数据地址读取寄存器内容。读写函数本身很短但时序参数没配好会在这里卡很久。2.2 用STM32CubeMX配置FSMC后手工校准时序参数在CubeMX中把FSMC的Bank1 NOR/PSRAM使能、选择16位数据宽度、Memory Type选NOR Flash会自动生成HAL_SRAM_Init相关的初始化代码。生成完后不要直接烧录要用示波器或者逻辑分析仪看NE1、NOE、NWE的时序是否符合DM9000数据手册要求。DM9000对读周期要求tRD最小约80ns对写周期要求tWP最小约80nsFSMC默认参数往往偏快高性能配置下可能跑不稳。static void fsmc_dm9000_init(void) { FSMC_NORSRAM_TimingTypeDef timing {0}; FSMC_NORSRAM_InitTypeDef fsmc {0}; __HAL_RCC_FSMC_CLK_ENABLE(); timing.AddressSetupTime 8; timing.DataSetupTime 10; timing.AccessMode FSMC_ACCESS_MODE_A; fsmc.FSMC_Bank FSMC_BANK1_NORSRAM1; fsmc.FSMC_DataAddressMux FSMC_DATA_ADDRESS_MUX_DISABLE; fsmc.FSMC_MemoryType FSMC_MEMORY_TYPE_SRAM; fsmc.FSMC_MemoryDataWidth FSMC_NORSRAM_MEM_BUS_WIDTH_16; fsmc.FSMC_BurstAccessMode FSMC_BURST_ACCESS_MODE_DISABLE; fsmc.FSMC_WaitSignalPolarity FSMC_WAIT_SIGNAL_POLARITY_LOW; fsmc.FSMC_WrapMode FSMC_WRAP_MODE_DISABLE; fsmc.FSMC_WaitSignalActive FSMC_WAIT_TIMING_BEFORE_NS_ACCESS; fsmc.FSMC_WriteOperation FSMC_WRITE_OPERATION_ENABLE; fsmc.FSMC_WaitSignal FSMC_WAIT_SIGNAL_DISABLE; fsmc.FSMC_ExtendedMode FSMC_EXTENDED_MODE_DISABLE; fsmc.FSMC_WriteBurst FSMC_WRITE_BURST_DISABLE; fsmc.FSMC_ReadWriteTimingStruct timing; fsmc.FSMC_WriteTimingStruct timing; HAL_SRAM_Init(fsmc, timing); }AddressSetupTime设成8、DataSetupTime设成10适用于72MHz主频下FSMC时钟为HCLK的配置换算下来读写周期在110ns左右给DM9000留出了余量。如果主频改了这个数字要重新折算比如跑48MHz时DataSetupTime可以降到6。改完整后先做一次连续读ID的操作大于100次循环都稳定读到同一个值才算时序过关。2.3 DM9000寄存器读写与PHY状态读取验证DM9000上电后要通过软件复位寄存器0x00写0x01复位然后等待内部PHY完成上电自检。PHY是否就绪要看寄存器0x01的bit0这个位为1表示PHY已经准备好。之后读取芯片ID寄存器0x02和0x03正常情况下高字节是0x90、低字节是0xA6。void dm9000_reset(void) { dm9000_write_reg(DM9000_REG_RESET, 0x01); HAL_Delay(20); dm9000_write_reg(DM9000_REG_GPCR, 0x01); dm9000_write_reg(DM9000_REG_GPR, 0x00); uint8_t id_h dm9000_read_reg(DM9000_REG_VID_H); uint8_t id_l dm9000_read_reg(DM9000_REG_VID_L); if (id_h ! 0x90 || id_l ! 0xA6) { dm9000_write_reg(DM9000_REG_RESET, 0x01); HAL_Delay(50); id_h dm9000_read_reg(DM9000_REG_VID_H); id_l dm9000_read_reg(DM9000_REG_VID_L); } uint8_t phy_status dm9000_read_reg(DM9000_REG_PHY_STATUS); if (phy_status 0x04) { // bit2为1表示链路已连接 } }这里的PHY状态寄存器地址是0x1F是DM9000内部的PHY状态汇总bit2对应网线连接状态。调试时如果发现读到的phy_status一直是0先检查网线是否插好再检查DM9000的TX、TX-、RX、RX-有没有接反。DM9000是10/100M自适应但要求网络变压器中心抽头处理正确不然PHY状态也会异常。3. LWIP协议栈移植从netif接口到TCP/IP链路打通3.1 LWIP组件包与HAL库驱动的衔接点文件列表里出现的mib2.c和fsdata.c说明这个例程集成了SNMP和HTTP Server服务这两个组件依赖LWIP的netif和pbuf机制正常工作。LWIP对底层网卡驱动暴露的标准接口是low_level_init、low_level_output和low_level_input三个函数分别负责初始化网卡、发送数据包、接收数据包。HAL库的DM9000驱动只需要做好一件事把DM9000收到的数据帧填充成struct pbuf把struct pbuf里的数据拆成以太网帧发出去。理解这个衔接点是移植的关键。LWIP内部不管底层是DM9000还是ENC28J60它只认netif结构体里注册的这三个函数指针。DM9000的收包方式是中断或者轮询读取FIFOLWIP的收包方式是主动调用low_level_input所以两个模块之间需要一个缓冲区交接。3.2 lwipopts.h内存参数配置STM32F103的RAM资源有限ZET6是64KBRCT6是48KBLWIP的内存配置不能直接照搬网上那些给F407或者F429用的配置。PBUF池和内存堆设置太大编译能过但运行时会频繁申请失败设置太小TCP传输速率上不去。我习惯先把以下参数作为起点跑通PING后再逐步调大。#define MEM_ALIGNMENT 4 #define MEM_SIZE (16 * 1024) #define MEMP_NUM_PBUF 20 #define MEMP_NUM_UDP_PCB 8 #define MEMP_NUM_TCP_PCB 8 #define MEMP_NUM_TCP_SEG 16 #define PBUF_POOL_SIZE 20 #define PBUF_POOL_BUFSIZE 512 #define LWIP_DHCP 1 #define LWIP_DNS 0 #define LWIP_NETCONN 1 #define LWIP_SOCKET 0这里PBUF_POOL_SIZE设为20每个pbuf的缓冲区大小是512字节加起来占用的RAM约10KB再加上MEM_SIZE的16KB堆空间整体内存占用在30KB以内在F103上跑得稳。LWIP_SOCKET关掉是因为BSD Socket层对RAM消耗大用netconn API开发效率更高。如果后续要跑HTTP ServerMEMP_NUM_TCP_SEG建议加到24否则并发连接下发送队列会阻塞。3.3 low_level_init与网卡轮询收发逻辑low_level_init里要设置netif的MAC地址、MTU和硬件地址长度。DM9000的MAC地址存储在芯片内部EEPROM中出厂可能是全0xFF我们代码里直接用一个静态数组定义烧录后每次上电都用同一个MAC。做产品时MAC地址要按批次分配不能所有板子都一样否则局域网内会地址冲突。static err_t low_level_init(struct netif *netif) { static const uint8_t mac[6] {0x00, 0x80, 0xE1, 0x00, 0x01, 0x01}; netif-hwaddr_len 6; MEMCPY(netif-hwaddr, mac, 6); netif-mtu 1500; netif-flags NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP; dm9000_set_mac(mac); dm9000_rx_enable(); return ERR_OK; }接收数据时LWIP在主循环里通过ethernetif_input(netif)轮询网卡DM9000有数据帧到达时RX SRAM里会写入帧头信息我们读取状态字判断帧长度然后分配pbuf把数据搬进去。这里有个性能细节DM9000的数据宽度是16位读取时用uint16_t指针操作比逐字节读取快很多。struct pbuf *low_level_input(struct netif *netif) { uint16_t status dm9000_read_reg(DM9000_REG_RX_STATUS_H); uint16_t len dm9000_read_reg(DM9000_REG_RX_LEN_H); if (status 0x01) { dm9000_rx_skip(); return NULL; } struct pbuf *p pbuf_alloc(PBUF_RAW, len, PBUF_POOL); if (p ! NULL) { uint8_t *buf (uint8_t *)p-payload; dm9000_rx_read(buf, len); dm9000_rx_skip(); } else { dm9000_rx_skip(); } return p; }这段是典型的接收流程。先判断RX状态寄存器的最高位是否为1为1说明当前帧无错误再读帧长度。分配pbuf失败时不能停在这里必须调用rx_skip跳过当前帧否则RX FIFO不释放会一直报溢出错误。DM9000的RX SRAM需要手动写命令字来释放很多人在这一步漏了导致通信到第二次就卡死。3.4 协议栈初始化顺序与主循环集成协议栈初始化顺序错了后面所有网络请求都会异常。先初始化底层硬件再初始化LWIP核心最后注册网卡接口。顺序反了比如先调tcpip_init再调FSMC和DM9000收发回调还没准备好就被触发程序直接跑飞。void ethernet_lwip_init(void) { fsmc_dm9000_init(); dm9000_reset(); lwip_init(); netif_add(g_netif, g_ipaddr, g_netmask, g_gateway, NULL, ethernetif_init, ethernet_input); netif_set_default(g_netif); netif_set_up(g_netif); }主循环里调用ethernetif_check_link检测链路变化并调用ethernetif_input处理收包。LWIP的定时器靠sys_check_timeouts驱动这个函数必须在主循环里高频调用否则ARP老化、TCP重传都不工作。我一般把主循环的周期控制在1ms以内用HAL_GetTick做非阻塞延时确保协议栈的时基拉得均匀。while (1) { ethernetif_check_link(g_netif); ethernetif_input(g_netif); sys_check_timeouts(); user_tcp_poll(); }ethernetif_input和sys_check_timeouts不能互换位置先收包后处理超时保证刚收到的ACK能立刻驱动TCP状态机推进。个人项目里把sys_check_timeouts放前面看似没问题但高负载下会额外引入几十毫秒的确认延迟下载吞吐量会掉一截。4. 抓包验证与排查网络通信故障4.1 用Wireshark确认ARP与ICMP流程板子烧录后先不急着写应用层代码第一步是用网线直连电脑给板子静态IP例如192.168.1.10电脑配置192.168.1.2。然后打开Wireshark监听对应网卡在电脑上ping 192.168.1.10。Wireshark里依次能看到三个关键过程电脑发出ARP广播询问192.168.1.10的MAC地址、板子应答ARP包、电脑发出ICMP echo request、板子回ICMP echo reply。如果只看到ARP request没有应答问题出在以太网驱动层LWIP根本没收到ARP广播优先排查FSMC时序和DM9000接收中断如果ARP有应答但ICMP不回问题出在LWIP的IP层或者内存配置重点看pbuf分配是否失败。ping 192.168.1.10 -t持续ping的同时观察Wireshark如果有规律地出现几个request对应一个reply说明收发路径上的某一段在丢包通常是pbuf池不够或者中断和主循环竞争导致。这时可以把PBUF_POOL_SIZE临时调到40再试如果恢复稳定再反过来分析内存预算。4.2 常见故障定位表故障现象排查方向处理建议读ID全为0xFFFSMC时序过快DM9000未响应拉长DataSetupTime检查NE1片选PHY状态bit2一直是0网线未接好变压器虚焊TX/RX差分对接反换网线、用万用表量变压器来线、核对原理图ARP能通但ICMP不通LWIP内存池耗尽或IP层校验失败检查PBUF_POOL_SIZE打开LWIP_DEBUG查看日志TCP连接建立后马上断开TCP定时器未驱动或ACK超时确认主循环里调用了sys_check_timeouts通信一段时间后死机RX SRAM溢出或指针越界检查rx_skip是否执行校验pbuf分配释放配对4.3 串口日志配合调试网络协议栈的问题靠串口日志辅助定位是最直接的手段。LWIP本身提供了LWIP_DEBUG宏但STM32F103跑全量调试日志太耗资源我一般是把LWIP_DEBUG关闭自己在DM9000驱动层加关键节点打印。#define DBG_PRINT(fmt, ...) \ do { \ char buf[128]; \ snprintf(buf, sizeof(buf), fmt, ##__VA_ARGS__); \ HAL_UART_Transmit(huart1, (uint8_t *)buf, strlen(buf), 100); \ } while (0)用串口1输出日志时注意串口1挂载在APB2总线上时钟频率72MHz波特率设置到115200没问题如果换成串口3或者串口2它们在APB1总线上最高36MHz波特率误差会大一些建议不要超过57600。每次收包和发包都打印会严重影响实时性只打印异常分支就够了比如ID读取失败、pbuf_alloc返回NULL、PHY链路断开。5. 让DM9000跑到更快的几个实用调整5.1 开启DHCP动态获取IP静态IP只适合调试做产品要支持DHCP。LWIP的DHCP客户端是可选组件lwipopts.h里LWIP_DHCP设为1后再在代码里调用dhcp_start(g_netif)协议栈会自动完成发现、请求、确认流程。要注意DHCP在网线插上后需要一段时间协商不能在netif_set_up后立刻去做TCP连接要等dhcp_supplied_address返回非0值。另外DHCP的lease时间到了之后会自动续租续租失败也不应该主动断开TCP连接LWIP内部已经处理了不要自己加业务逻辑干扰它。5.2 调整TCP窗口与PBUF池避免吞吐量瓶颈TCP的吞吐量上限受接收窗口和发送缓冲限制。在RAM允许的前提下把TCP_WND从默认的4KB提升到8KBTCP_SND_BUF从2KB提升到6KB配合前面的PBUF_POOL_SIZE调整下载速率会明显改善。具体做法是在lwipopts.h中覆盖默认值。注意这里不是越大越好F103的64KB RAM要留给系统栈和业务数据调完用FreeRTOS统计或者mallinfo看一下剩余堆空间留出至少10KB余量。STM32F103用DM9000的内核瓶颈主要在FSMC总线拷贝和LWIP内存搬运实际吞吐量做到5-7Mbps已经算正常水平不要拿它和带MAC的F407比。5.3 从轮询切到中断收包降低CPU占用例程默认是主循环轮询CPU利用率在高速传输时接近100%。接手量产项目时建议把DM9000的INT引脚接到STM32的外部中断线上收到帧后置位一个标志位主循环发现标志位再调用收包逻辑。这样CPU空闲时不会反复查询芯片寄存器功耗和发热都会降下来。void EXTI15_10_IRQHandler(void) { if (__HAL_GPIO_EXTI_GET_IT(DM9000_INT_PIN) ! RESET) { g_dm9000_rx_flag 1; __HAL_GPIO_EXTI_CLEAR_IT(DM9000_INT_PIN); } }中断服务函数里不能直接调用pbuf_alloc和low_level_input这些函数带临界区保护放在中断里容易和主循环的LWIP调用产生嵌套。用标志位把实际收包动作延后到主循环执行是习惯做法。配合这个改动ethernetif_input里要加一下判断标志位是0就跳过收包避免对DM9000寄存器的无意义访问。这套改下来以后同样的工程CPU占有率能降30%以上也为以后移植到FreeRTOS环境留好了接口。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门