简介:面向STM32以太网通信入门与进阶开发者的W5100S实战例程包,主控选用STM32F407VET6,通过FSMC并行总线连接成都浩然电子的HS-NM5100SA模块,并在MDK5.25环境下提供可直接编译的TCP服务器与TCP客户端两套完整工程。压缩包共143个文件、约5.12MB,以56个C源文件和56个头文件构成底层驱动与应用主体,同时包含Keil工程配置、调试配置、HEX烧录文件以及5个可执行网络调试助手,便于搭建联调环境。已有1147人学习下载,适合希望打通硬件接口与Socket编程的中高级学习者。读者可从中掌握FSMC地址映射与时序配置、W5100S寄存器读写、Socket端口与连接管理,并借助服务器与客户端双模式对照理解连接建立、数据收发和异常处理;配套调试工具可用于端口监听、数据回环与报文分析,驱动层亦可迁移到同类项目,减少底层调试与移植时间。
1. 需求与选型:为什么给 STM32F407 挂一颗硬件协议栈的以太网芯片,并且绕开 SPI
某实验室的数据采集网关要求 STM32F407 同时跑四路串口、一片 SD 卡和一路以太网回传。最初版本用 SPI 接 W5100S,实测下来吞吐卡在 1.8 Mbps 上下,而且 SPI 总线一忙,SD 卡的写入就被打断,采样数据开始出现缺口。后来改成 FSMC 并行总线,同一颗 W5100S 的寄存器访问退化成一次存储指令,CPU 开销和总线争用同时降下来,瓶颈重新回到串口采样率本身。
这套 STM32F407 通过 FSMC 连接 W5100S 进行网络通信的方案,适合三类人:正在做相关课程设计或毕设、需要一套能跑通又能讲清原理的完整实现;手上有并行接口网口模块、但对 15 根地址线和 FSMC 时序不知道从哪下手的工程师;以及已经用 SPI 跑通、想搞清楚吞吐为什么上不去的人。下面按硬件连接、时序配置、驱动落地、故障排查、验收答辩的顺序讲完整条链路。
2. 硬件连接与地址映射:15 根地址线怎么和 FSMC Bank1 对齐
W5100S 的并行总线叫间接总线模式,本质是把它内部 32KB 的地址空间原样暴露给外部主机。主机给出地址、数据、片选,芯片内部自己完成寄存器访问或缓冲区搬运。FSMC 的 NOR/SRAM 控制器恰好能生成这套时序,两者对接的关键只有两件事:地址线一根不多一根不少地接上,片选和控制信号别接反。
2.1 引脚分配与片选、读写控制信号
STM32F407 的 FSMC Bank1 分四个子区,NE1 对应基址 0x60000000。这里选 Bank1 是因为它和 Bank2(0x64000000)在时序寄存器上完全独立,后面如果还要挂别的外设,不会互相牵制。数据宽度选 8 位,因为 W5100S 的数据总线就是 8 根。
| W5100S 信号 | STM32F407 引脚 | FSMC 复用功能 | 说明 |
|---|---|---|---|
| DATA0 ~ DATA3 | PD14、PD15、PD0、PD1 | FSMC_D0 ~ D3 | 低四位数据 |
| DATA4 ~ DATA7 | PE7 ~ PE10 | FSMC_D4 ~ D7 | 高四位数据 |
| ADDR0 ~ ADDR5 | PF0 ~ PF5 | FSMC_A0 ~ A5 | 低位地址 |
| ADDR6 ~ ADDR9 | PF12 ~ PF15 | FSMC_A6 ~ A9 | 中位地址 |
| ADDR10 ~ ADDR14 | PG0 ~ PG4 | FSMC_A10 ~ A14 | 高位地址 |
| /CS | PD7 | FSMC_NE1 | 片选,低有效 |
| /WR | PD5 | FSMC_NWE | 写使能 |
| /RD | PD4 | FSMC_NOE | 读使能 |
| /INT | 任选 EXTI 引脚 | GPIO_EXTI | 中断输出,低有效 |
| /RST | 任选 GPIO | GPIO_OUT | 复位,低有效至少 2ms |
提示:地址线接满 15 根是最省事的做法。如果为了省引脚只接低 8 根,那么访问 0x4000 以上的缓冲区就要靠软件分段换算,调驱动时会多出一层容易出错的偏移计算,不划算。
引脚初始化必须走复用推挽,速度和上下拉按下面这段配置:
void HAL_SRAM_MspInit(SRAM_HandleTypeDef *hsram) { GPIO_InitTypeDef gpio = {0}; __HAL_RCC_FSMC_CLK_ENABLE(); __HAL_RCC_GPIOD_CLK_ENABLE(); __HAL_RCC_GPIOE_CLK_ENABLE(); __HAL_RCC_GPIOF_CLK_ENABLE(); __HAL_RCC_GPIOG_CLK_ENABLE(); gpio.Mode = GPIO_MODE_AF_PP; /* 复用推挽,输出由 FSMC 接管 */ gpio.Pull = GPIO_NOPULL; /* 内部上下拉会拖慢边沿,关掉 */ gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH; gpio.Alternate = GPIO_AF12_FSMC; /* 数据线:D0-D3 在 PD,D4-D7 在 PE */ gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_14 | GPIO_PIN_15; HAL_GPIO_Init(GPIOD, &gpio); gpio.Pin = GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10; HAL_GPIO_Init(GPIOE, &gpio); /* 控制线:NE1(PD7) / NWE(PD5) / NOE(PD4) */ gpio.Pin = GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_7; HAL_GPIO_Init(GPIOD, &gpio); /* 地址线 A0-A9 在 PF,A10-A14 在 PG */ gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15; HAL_GPIO_Init(GPIOF, &gpio); gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4; HAL_GPIO_Init(GPIOG, &gpio); }这里有个容易漏的细节:GPIO_Pull必须是GPIO_NOPULL。曾经有一版工程在地址线上开了内部上拉,结果地址建立时间被拉长,读出来的寄存器值在高位地址区间全是乱的,排查了大半天才定位到这一行。另外速度档位选VERY_HIGH不是玄学,8 位总线一次访问只有几十纳秒,边沿不够陡会直接被 W5100S 判成无效电平。
2.2 W5100S 内部地址空间与基址偏移换算
W5100S 内部 32KB 地址空间分区很规整,理解这张表,后面写驱动基本就是照着偏移量填空:
| 地址区间 | 大小 | 用途 |
|---|---|---|
| 0x0000 ~ 0x002F | 48 字节 | 公共寄存器(MR、GAR、SUBR、SHAR、SIPR、IR、IMR、RTR、RCR、RMSR、TMSR 等) |
| 0x0400 ~ 0x07FF | 1KB | 8 个 Socket 的寄存器,每个占 0x100 |
| 0x4000 ~ 0x5FFF | 8KB | 发送缓冲区,默认每个 Socket 2KB |
| 0x6000 ~ 0x7FFF | 8KB | 接收缓冲区,默认每个 Socket 2KB |
因为 15 根地址线全部直连,芯片内部的偏移量就等于 FSMC 基址上的偏移量,不需要任何移位或翻转。于是寄存器读写可以简化成两个函数:
#define W5100S_BASE 0x60000000UL #define W5100S_Sn_BASE(n) (0x0400UL + ((uint32_t)(n) * 0x100UL)) #define W5100S_TX_BASE(n) (0x4000UL + ((uint32_t)(n) * 2048UL)) #define W5100S_RX_BASE(n) (0x6000UL + ((uint32_t)(n) * 2048UL)) static inline void w5100s_write8(uint32_t off, uint8_t val) { *(volatile uint8_t *)(W5100S_BASE + off) = val; } static inline uint8_t w5100s_read8(uint32_t off) { return *(volatile uint8_t *)(W5100S_BASE + off); }volatile这个关键字不能省。编译器看到连续两次写同一地址,会认为后一次覆盖前一次而把前一次优化掉,但这两次写在硬件上是两个独立的命令周期,比如先写 Sn_CR 再读 Sn_CR 判断命令是否执行完,去掉 volatile 之后整个状态机直接失效。
基址 0x60000000 不是随便挑的,它由 NE1 片选决定。如果改用 NE2,基址变成 0x64000000,W5100S_BASE必须跟着改,否则所有访问都会落到一片无人响应的地址上,读回来是 0xFF 或者直接进 HardFault。
2.3 复位、中断与电源去耦
复位脚的处理经常被简化成「上电自动复位就行」,实际项目里这是长期稳定性的隐患。W5100S 的 /RST 低电平需要保持至少 2ms,而上电时 3.3V 电源的上升沿在有些板子上要几十毫秒才稳定,光靠 RC 电路不够可靠。稳妥的做法是用一个 GPIO 控制复位,初始化时主动拉低 5ms 再拉高,然后延时 10ms 等内部锁相环稳定,再去读寄存器。
中断脚接 EXTI 有实际收益。轮询 Socket 状态虽然也能跑,但每个循环都要访问一次 Sn_SR,在 8 位并行总线上就是一次完整的读写周期,跑高频轮询会白白吃掉 CPU。把 /INT 接到 EXTI 下降沿,收到中断后再进芯片读 IR 寄存器判断是哪个 Socket 的什么事件,CPU 占用能降一个量级。
电源部分,W5100S 的 3.3V 和 1.8V(内部稳压输出)都要按手册配去耦电容,尤其是 1.8V 那一脚,它是芯片内部核心电压的输出,必须有 1uF 以上的电容。这块省了,现象是芯片能识别、能配置,但连续收发几十秒后突然掉线,重新上电又正常,非常像软件 bug,实际上是电源纹波把内部状态机打乱了。
3. FSMC 时序配置:把 168MHz 的 HCLK 压到 W5100S 跟得上的节奏
硬件连对只是第一步,真正让这套方案从「能读到 ID」变成「能稳定跑 TCP」的是时序。FSMC 跑在 168MHz 的 HCLK 上,一个时钟周期不到 6ns,而 W5100S 的并行总线访问周期是几十纳秒量级,如果直接用默认最短时序去捅,读回来的数据大概率是错的,而且错得很随机。
3.1 CubeMX 里那几个参数到底在控制什么
FSMC 的 NOR/SRAM 模式配成异步访问,关键参数集中在FSMC_NORSRAM_TimingTypeDef里。逐项说明:
static void MX_FSMC_Init(void) { FSMC_NORSRAM_TimingTypeDef timing = {0}; hsram.Instance = FSMC_NORSRAM_DEVICE; hsram.Extended = FSMC_NORSRAM_EXTENDED_DEVICE; hsram.Init.NSBank = FSMC_NORSRAM_BANK1; /* 对应 0x60000000 */ hsram.Init.DataAddressMux = FSMC_DATA_ADDRESS_MUX_DISABLE; /* 地址数据线分开,不复用 */ hsram.Init.MemoryType = FSMC_MEMORY_TYPE_SRAM; /* 当成异步 SRAM 访问 */ hsram.Init.MemoryDataWidth = FSMC_NORSRAM_MEM_BUS_WIDTH_8; /* 8 位数据总线 */ hsram.Init.BurstAccessMode = FSMC_BURST_ACCESS_MODE_DISABLE; /* 不支持突发,必须关 */ hsram.Init.WaitSignalPolarity = FSMC_WAIT_SIGNAL_POLARITY_LOW; hsram.Init.WrapMode = FSMC_WRAP_MODE_DISABLE; hsram.Init.WriteOperation = FSMC_WRITE_OPERATION_ENABLE; /* 允许写 */ hsram.Init.ExtendedMode = FSMC_EXTENDED_MODE_DISABLE; /* 读写共用一套时序 */ hsram.Init.AsynchronousWait = FSMC_ASYNCHRONOUS_WAIT_DISABLE; /* 不用 WAIT 引脚 */ hsram.Init.WriteBurst = FSMC_WRITE_BURST_DISABLE; timing.AddressSetupTime = 5; /* 地址建立:5 个 HCLK ≈ 30ns */ timing.AddressHoldTime = 1; /* 地址保持:Mode A 下不生效 */ timing.DataSetupTime = 9; /* 数据建立:9 个 HCLK ≈ 54ns */ timing.BusTurnAroundDuration = 0; /* 异步模式用不到总线周转 */ timing.CLKDivision = 0; timing.DataLatency = 0; timing.AccessMode = FSMC_ACCESS_MODE_A; /* 读写共用同一套时序 */ HAL_SRAM_Init(&hsram, &timing, &timing); }ExtendedMode关掉意味着读和写用同一组时序参数。W5100S 的读和写周期其实略有差异,但在这个速度量级下用一套保守参数就够,开两套反而增加调试复杂度。AccessMode选 A 而不是 B,是因为 Mode B 会在读操作时额外插入一个地址保持周期,只是为了兼容某些需要更长地址保持的器件,W5100S 不需要。
参数单位都是 HCLK 周期数,168MHz 下 1 个周期约 5.95ns。按上面这组配置,一次完整的 8 位写访问大约是 (5 + 9) × 5.95 ≈ 83ns,换算成理论带宽 12MB/s。这个数字远高于以太网的实际需求(100Mbps 全双工满打满算也就 12.5MB/s,而且不是所有数据都要经过 MCU),所以时序上是安全的。
3.2 用读公共寄存器做第一轮时序验证
代码烧进去之后,别急着配 IP 开 Socket,先做一次最小验证:读公共寄存器里的固定内容。W5100S 的 RTR(重试时间)和 RCR(重试次数)复位默认值是 0x07D0 和 0x03,这两个值是芯片出厂就写死在硅片里的,读出来不对,说明总线本身有问题,跟网络协议无关。
void w5100s_bus_selftest(void) { uint16_t rtr; uint8_t rcr; /* 先软复位,确保芯片处于已知状态 */ w5100s_write8(W5100S_MR, 0x80); HAL_Delay(10); rtr = ((uint16_t)w5100s_read8(W5100S_RTR) << 8) | w5100s_read8(W5100S_RTR + 1); rcr = w5100s_read8(W5100S_RCR); printf("RTR=0x%04X RCR=0x%02X\r\n", rtr, rcr); /* 期望输出 RTR=0x07D0 RCR=0x03,不符就要回头查硬件 */ if (rtr != 0x07D0 || rcr != 0x03) { printf("FSMC bus fault, check address line / timing\r\n"); } }这个自检的价值在于它把问题范围一分为二。RTR 和 RCR 读对了,说明地址线、数据线、片选、读写控制全部正常,剩下的问题只可能出在协议层;读不对,就专心查 FSMC,不用去怀疑 Socket 寄存器配置。经验上,读出来全 0x00 通常是没有片选到(NE 接错或者NSBank配错),读出来全 0xFF 通常是数据线虚焊或者总线没人响应,读出来是规律性的错位(比如 A0 和 A1 颠倒)则是对应那根地址线接反了。
3.3 从保守值往回收:时序压缩的实测方法
先用明显偏大的时序跑通,再逐步压缩到出错前的 1.5 倍,这是我一直用的做法。上面的 5/9 已经是比较保守的取值,如果想进一步提高吞吐,可以按下面的步骤做:
第一步,把DataSetupTime固定,只动AddressSetupTime,从 5 往下降到 3、2、1,每次下降后跑一轮 1000 次寄存器读写比对,只要有一次不一致就退回上一档。第二步,把AddressSetupTime固定在上一轮的最小稳定值,再动DataSetupTime,同样从 9 往下降到 7、6、5。第三步,把最终得到的两个值各自加 1 作为裕量,固化到工程里。
这套流程听起来笨,但它能把「时序不够」和「时序够了但别的有问题」这两种情况彻底分开。如果跳过这步直接上最终参数,后面出现丢包时,就分不清是时序临界还是缓冲指针算错了。
注意:每次改完时序参数都要重新生成代码并全片重烧,不要只靠调试器热改寄存器。FSMC 的时序寄存器有写保护,部分型号在运行时改写需要先解锁,热改失败会让人误以为参数无效。
4. 驱动层落地:寄存器封装、环形缓冲与 Socket 状态机
硬件和时序都稳了之后,剩下的工作就是把「怎么发一个字节」变成「怎么发一包数据」。这一层如果写成每次业务代码都直接对着地址赋值,工程会很快失控。合理的做法是分三层:寄存器访问层只做地址到变量的映射,缓冲管理层处理环形指针,Socket 层跑状态机。
4.1 Socket 寄存器偏移与命令握手
W5100S 的每个 Socket 有自己独立的 0x100 字节寄存器区,偏移是固定的,用宏定义包一层,可读性比裸地址好很多:
#define Sn_MR(n) (W5100S_Sn_BASE(n) + 0x00) #define Sn_CR(n) (W5100S_Sn_BASE(n) + 0x01) #define Sn_IR(n) (W5100S_Sn_BASE(n) + 0x02) #define Sn_SR(n) (W5100S_Sn_BASE(n) + 0x03) #define Sn_PORT(n) (W5100S_Sn_BASE(n) + 0x04) #define Sn_DIPR(n) (W5100S_Sn_BASE(n) + 0x0C) #define Sn_DPORT(n) (W5100S_Sn_BASE(n) + 0x10) #define Sn_TX_FSR(n) (W5100S_Sn_BASE(n) + 0x20) #define Sn_TX_WR(n) (W5100S_Sn_BASE(n) + 0x24) #define Sn_RX_RSR(n) (W5100S_Sn_BASE(n) + 0x26) #define Sn_RX_RD(n) (W5100S_Sn_BASE(n) + 0x28) /* 命令码 */ #define Sn_CR_OPEN 0x01 #define Sn_CR_LISTEN 0x02 #define Sn_CR_CONNECT 0x04 #define Sn_CR_DISCON 0x08 #define Sn_CR_CLOSE 0x10 #define Sn_CR_SEND 0x20 #define Sn_CR_RECV 0x40 /* Sn_SR 状态 */ #define SOCK_CLOSED 0x00 #define SOCK_INIT 0x13 #define SOCK_LISTEN 0x14 #define SOCK_ESTABLISHED 0x17 #define SOCK_CLOSE_WAIT 0x1C写 Sn_CR 之后必须等它自动清零,这一步叫命令握手,是 W5100S 的硬性要求:
static void w5100s_exec_cmd(uint8_t sn, uint8_t cmd) { w5100s_write8(Sn_CR(sn), cmd); /* 芯片执行完命令会把 Sn_CR 清 0,超时则说明芯片卡住了 */ uint32_t timeout = 100000; while (w5100s_read8(Sn_CR(sn)) != 0x00) { if (--timeout == 0) { break; } } }超时判断不能省。芯片没响应时如果不加超时,程序会死在这个 while 里,看门狗一复位,现象就成了「设备每隔几秒自动重启」,这个现象和电源问题几乎一模一样,排查方向会被带偏。
4.2 环形缓冲区的分段搬运
W5100S 的每个 Socket 默认分配 2KB 发送缓冲和 2KB 接收缓冲,Sn_TX_WR和Sn_RX_RD这些指针都是相对各自缓冲区起始的偏移,取值范围 0~2047,超过 2047 会自动回绕。这意味着往缓冲区写数据时必须处理跨边界的情况:
static void w5100s_write_buf(uint32_t base, uint16_t offset, const uint8_t *src, uint16_t len) { const uint16_t size = 2048; /* 每个 Socket 默认 2KB 缓冲 */ uint16_t first = size - offset; /* 从 offset 到缓冲区末尾的字节数 */ if (len <= first) { /* 不跨边界,连续地址直接整块拷贝,8 位 FSMC 会自动产生时序 */ memcpy((void *)(W5100S_BASE + base + offset), src, len); } else { /* 跨边界,分两段 */ memcpy((void *)(W5100S_BASE + base + offset), src, first); memcpy((void *)(W5100S_BASE + base), src + first, len - first); } }这里直接用memcpy往映射地址上拷,比逐字节 for 循环快一个数量级。原因是 FSMC 的地址是连续的,编译器会生成成串的存储指令,总线利用率接近理论带宽;而逐字节调用读写函数会引入函数调用开销和额外的指针运算,实测下来差了五六倍。
发送一包数据的完整流程是:先读Sn_TX_FSR确认剩余空间够不够,再读Sn_TX_WR拿到当前写指针,把数据搬进去,更新Sn_TX_WR,最后发Sn_CR_SEND命令。发送完成后芯片会把Sn_IR的 SEND_OK 位置起,业务层清掉这个标志才能发下一包。少了这一步,第二包数据会直接覆盖第一包还没发出去的内容。
4.3 Socket 状态机与 TCP Echo 主循环
TCP 服务端的状态机如果写成一个大 switch,逻辑会清楚很多。下面这段是 Socket 0 作为 TCP 服务端的骨架:
void socket0_poll(void) { switch (w5100s_read8(Sn_SR(0))) { case SOCK_CLOSED: w5100s_write8(Sn_MR(0), 0x01); /* TCP 模式 */ w5100s_write8(Sn_PORT(0), 0x13); w5100s_write8(Sn_PORT(0) + 1, 0x88); /* 端口 5000 */ w5100s_exec_cmd(0, Sn_CR_OPEN); if (w5100s_read8(Sn_SR(0)) == SOCK_INIT) { w5100s_exec_cmd(0, Sn_CR_LISTEN); /* 进入监听 */ } break; case SOCK_ESTABLISHED: /* 收到数据就原样回发,用于链路验证 */ if (w5100s_read8(Sn_IR(0)) & 0x04) { uint16_t len = socket0_recv_echo(); if (len > 0) { socket0_send(len); } w5100s_write8(Sn_IR(0), 0x04); /* 写 1 清标志 */ } break; case SOCK_CLOSE_WAIT: w5100s_exec_cmd(0, Sn_CR_DISCON); /* 对端先关,本端也要关 */ break; default: break; } }状态机里最容易漏的是SOCK_CLOSE_WAIT这一支。对端主动断开时,本端会停在这个状态,如果没人处理,这个 Socket 就一直占着,新连接进不来。表现是「第一次连接正常,断开之后再也连不上」,只有整机重启才恢复。加上这一支之后,连接断开后芯片会自动回到SOCK_CLOSED,重新走 OPEN 流程。
5. 避坑与排查:FSMC 挂 W5100S 最常见的五类翻车
这套方案跑通不难,难的是跑稳。下面五条是实际项目里反复出现的,每一条都按现象、原因、解决的顺序写清楚,遇到问题时可以直接对照。
5.1 读出来全是 0xFF,但芯片是好的
现象:自检代码打印 RTR=0xFFFF,RCR=0xFF,换一块确认完好的模块也一样。
原因:FSMC 根本没有产生有效的读周期。最常见的是NSBank配成了 BANK2 但硬件接的是 NE1,或者MemoryDataWidth配成了 16 位而实际数据线只有 8 根。16 位配置下 FSMC 会把地址左移一位,所有偏移量都错位,读到的地址实际上是无人响应的区域,返回上拉电平 0xFF。
解决:先确认工程里NSBank和硬件 NE 引脚一一对应,再确认数据宽度是 8 位。这两项对不上时,用示波器看 NOE 引脚是能看到波形的,容易误判成芯片没响应,实际是地址跑偏了。
5.2 ping 得通,但 TCP 连接一直建不起来
现象:配置好 IP 之后能 ping 通,说明物理链路和 IP 层都正常,但客户端发起 TCP 连接时一直超时,Sn_SR 停在 SOCK_INIT 不动。
原因:Sn_PORT的字节序写反了。W5100S 的端口寄存器是高字节在前,直接w5100s_write16(Sn_PORT, 5000)在小端架构的 STM32 上会写成 0x1388 存成 88 13,实际监听端口变成了 34824。ping 用的是 ICMP,不查端口,所以完全看不出异常。
解决:端口、IP、MAC 全部按字节逐个写,或者写一个显式的字节序转换函数。IP 地址同理,192.168.1.100要按 192、168、1、100 的顺序分别写进SIPR到SIPR+3,用 32 位整数整体赋值一定会错。
5.3 发了几包之后程序卡死
现象:echo 测试发到第三第四包就没响应了,调试器暂停进去发现停在w5100s_exec_cmd的 while 循环里。
原因:上一包的 SEND 命令还没完成就发了下一个 SEND。W5100S 的发送缓冲是有指针的,上一包的数据还在缓冲里没搬完,此时更新Sn_TX_WR并再次发 SEND,芯片内部的指针计算越界,状态机进入异常状态,Sn_CR 不再被清零。
解决:发完 SEND 之后必须等Sn_IR的 SEND_OK 位(0x10)置起再写下一包。同时Sn_TX_FSR要重新读一次,别用缓存值。如果业务上确实需要连续发送,就在应用层加一个环形队列,把数据攒起来,驱动层仍然保持一包一确认的节奏。
5.4 连续跑几个小时之后开始丢包
现象:刚上电时一切正常,跑四五个小时之后丢包率缓慢上升,重启后又恢复。
原因:Sn_RX_RD忘了更新。接收流程是先读Sn_RX_RSR看有多少数据,从Sn_RX_BASE + Sn_RX_RD把数据拷出来,然后把Sn_RX_RD加上读走的长度,最后发 RECV 命令。少了更新指针这一步,芯片会认为缓冲区里一直有旧数据,新的数据包进来没有空间可放,直接丢掉。
解决:把「读数据、更新指针、发 RECV」封装成单个函数,任何接收路径都只调这个函数,不允许业务代码直接操作指针。另外Sn_RX_RD更新后要发一次Sn_CR_RECV,这一步也不能漏,否则指针更新不生效。
5.5 换一块板子就不好使
现象:同一份固件在 A 板上跑得好好的,换到 B 板就读不到芯片,两块板子的原理图看起来一样。
原因:B 板的 FSMC 走线上多了两个串联电阻或者走线长了十几厘米,边沿变缓,原本 5/9 的时序在 A 板上够用,在 B 板上就临界了。还有一种可能是 B 板的 W5100S 复位电路用的是 RC 自动复位,上电时芯片还没准备好,MCU 已经开始配置寄存器了。
解决:固件里把时序参数放到一个单独的头文件里,出厂的板子如果硬件批次不同,允许通过宏切换。更根本的办法是把时序留够裕量,按 3.3 节的方法测出临界值后加 1 个周期,而不是取刚好能跑的最小值。复位则改成 GPIO 主动控制,上电后固定等 10ms 再访问。
6. 验收与答辩:从 ping 到 24 小时连续压测的完整证据链
答辩时最尴尬的场面是演示到一半网络断了,台下问「这个能跑多久」,答不上来。验收这套方案,我一般准备四层证据,从弱到强,每一层都能独立说明一件事。
第一层是总线自检。前面那段读 RTR 和 RCR 的代码不要删,做成上电日志打印出来。这一层证明 FSMC 硬件通路没问题,是所有后续功能的基础,评委看到 0x07D0 和 0x03 这两个固定值,就知道你不是靠运气跑通的。
第二层是链层连通性。用 ping 测,记录平均延迟和丢包率,连续 ping 1000 个包。正常情况下局域网内延迟应该在 1ms 以内,丢包 0%。如果延迟波动很大,说明有别的流量在抢总线或者中断处理太慢。
第三层是应用层吞吐。TCP echo 模式下跑一次定量的数据回环,比如连续发 10MB 数据,统计耗时。按 100Mbps 全双工的理论值算,经过 MCU 中转的实际吞吐大概在 6~9MB/s 之间,低于这个区间就要回头看是不是每次收发都在等命令握手、缓冲区是不是设得太小。
第四层是长时间稳定性。这一层最容易被跳过,但恰恰是答辩时最有说服力的。让设备连续跑 24 小时,每 10 分钟发一包数据并记录时间戳和 CRC 校验结果,最后统计总包数、错误包数和最大间隔。这份日志打印出来一页纸,比任何口头描述都管用。
| 验收项 | 方法 | 通过标准 | 说明的问题 |
|---|---|---|---|
| 总线自检 | 读 RTR、RCR 默认值 | 0x07D0 / 0x03 | FSMC 地址线与时序正确 |
| 链路连通 | ping 1000 包 | 丢包 0%,延迟 < 1ms | PHY 与 IP 层正常 |
| 应用吞吐 | TCP echo 10MB | 6~9MB/s | 驱动层无多余等待 |
| 长期稳定 | 24 小时定时收发 | 错误包 0,无重启 | 缓冲指针与内存管理无泄漏 |
还有一个答辩时经常被问到的点:为什么不用 SPI。回答时不要只说「并行更快」,要给出对比数据。SPI 在 20MHz 时钟下,每传一个字节要 8 个时钟周期外加命令开销,实际有效吞吐被压到 2Mbps 以下;而 FSMC 一次存储指令就是一个字节,168MHz 主频下理论带宽 12MB/s,并且不占用 CPU 的移位和等待时间。把这个数字摆出来,选型理由就站得住。
最后说一个我自己的习惯。这套方案我第一次做的时候,时序参数是照着一份网上的例程抄的,跑通了就再没管过。结果换了个批次的板子,连着三次演示都在第十几分钟掉线,最后查出来是DataSetupTime刚好卡在临界值上,温度一上来就不够了。从那以后我每次配 FSMC,都强制走一遍第 3.3 节的压缩流程,测出临界值再加一个周期做裕量,把这个数写进注释里,注明测试日期和板子批次。硬件这个东西不讲情面,省下来的那点裕量,迟早要在别的地方还回去。
希望这套流程能帮到你,少走几段弯路。
本文还有配套的精品资源,点击获取