简介:STM32_W5300_16Bit标准例程面向嵌入式网络开发初学者与进阶工程师,聚焦STM32通过FSMC总线与W5300以太网控制器进行16位并行通信的完整实现,帮助解决网络接入、高速数据传输与异常断网恢复等实际问题。压缩包共999个文件,约15.56MB,以321个h头文件、288个c源文件为核心,辅以uvproj、uvopt工程配置、sct分散加载、hex与bin烧录文件、bat脚本及map、lst等编译中间文件,覆盖从源码到可执行镜像的完整工程链路。例程内容涉及FSMC初始化、W5300寄存器配置、TCP/UDP收发、IP数据包封装解封装、断网重连机制以及STM32与W5300的接线说明,便于读者理解总线时序与硬件资源分配。目前已有771人学习下载,适合希望将网络功能快速集成到STM32项目、并掌握FSMC外设扩展方法的开发者参考。
1. STM32 挂 W5300 走 16 位并口:为什么这个组合至今还有人用
手里有一块带 W5300 的板子,MCU 是 STM32,硬件工程师把数据总线接成了 16 位并口模式,然后丢给你一个压缩包,名字大概就叫「STM32_W5300_16Bit 标准例程」。这个场景在工业网关、串口服务器、老设备联网改造里非常常见。W5300 是一颗硬件 TCP/IP 协议栈芯片,把 TCP、UDP、IP、ARP、ICMP 这些协议全部塞进芯片内部,MCU 只需要通过总线读写寄存器就能收发网络数据,不需要自己跑协议栈,也不需要大内存。16 位并口模式是它吞吐最高的一种主机接口方式,比间接总线模式快得多,适合需要跑满 100Mbps 或者多 socket 并发的场合。
这篇文章面向的是拿到类似例程但不知道怎么吃透的人:想知道 16 位并口怎么接线、FSMC 怎么配、寄存器怎么读写、socket 怎么初始化、数据怎么收发、跑不通的时候先查哪里。我会按「先讲清楚这个组合的定位和选型理由,再落到 FSMC 配置和寄存器操作,然后是 socket 编程和收发流程,接着是避坑排查,最后给一个进阶的吞吐优化技巧」这条线走完。新手能照着步骤把最小系统跑起来,熟手能对照参数和边界条件检查自己的实现。
2. 16 位并口模式到底怎么选、怎么接、怎么配 FSMC
2.1 W5300 三种主机接口的取舍
W5300 提供三种主机接口模式:直接总线模式(Direct Bus)、间接总线模式(Indirect Bus)、以及 SPI 模式。直接总线模式又分 8 位和 16 位两种数据宽度。选哪种,取决于你的 MCU 引脚资源和吞吐需求。
间接总线模式只用几个引脚,MCU 通过一个地址端口和一个数据端口分时复用去访问 W5300 内部寄存器,省引脚但速度慢,每次读写要两个总线周期,适合引脚紧张、吞吐要求不高的场合。SPI 模式更省引脚,但速率受 SPI 时钟限制,跑 TCP 大流量时容易成为瓶颈。16 位直接总线模式把 W5300 的寄存器空间直接映射到 MCU 的外部存储控制器上,读写一个寄存器就是一个总线周期,吞吐最高,代价是占用 16 根数据线加若干地址线和控制线。
我一般会这样判断:如果目标只是偶尔发几个字节的状态上报,间接总线或 SPI 就够了;如果要跑 Modbus TCP 网关、多路 socket 并发、或者需要接近线速的吞吐,直接上 16 位并口。标题里既然写了 16Bit,说明硬件已经按这个模式设计,软件侧的重点就是把 FSMC 配好。
2.2 硬件连接与地址映射关系
W5300 在直接总线模式下,内部寄存器按地址偏移访问。关键信号包括:16 位数据总线 D0-D15、地址线 A0-A9(具体位数看模式)、片选 CS、读使能 RD、写使能 WR、以及中断 INT。地址线决定了你能访问的寄存器范围,W5300 的寄存器空间分几个区:模式寄存器区、socket 寄存器区、TX/RX 缓冲区。
在 16 位模式下,W5300 的地址线 A0 通常不接,因为数据宽度是 16 位,地址按半字对齐。也就是说,MCU 侧的一个地址对应 W5300 的一个 16 位寄存器。这一点很容易搞错,如果 A0 接错,会出现读写寄存器值错位、高低字节颠倒的现象。
常见接法是:W5300 的 CS 接到 STM32 FSMC 的某个片选(比如 NE1),RD 接 FSMC_NOE,WR 接 FSMC_NWE,数据线接 FSMC 的 D0-D15,地址线接 FSMC 的 A1-A10 左右(具体看你要访问的地址范围)。中断线接一个外部中断引脚,用于接收数据到达通知。
提示:接线确认之前不要急着写代码。拿万用表把 CS、RD、WR、数据线、地址线逐根对一遍,比调试半天软件最后发现是硬件接错要省时间。
2.3 用 FSMC 配置 16 位并口的最小代码
STM32 的 FSMC(Flexible Static Memory Controller)可以把外部 SRAM/NOR 类型的设备映射到固定地址空间。W5300 的直接总线接口时序上接近 SRAM,所以用 FSMC 的 SRAM 模式来驱动是常见做法。下面是一段基于 HAL 库风格的 FSMC 初始化代码,重点是时序参数和 16 位数据宽度。
#include "stm32f1xx_hal.h" FSMC_NORSRAM_TimingTypeDef Timing; FSMC_NORSRAM_InitTypeDef Init; void W5300_FSMC_Init(void) { /* 使能 FSMC 时钟和 GPIO 时钟 */ __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 配置:数据线 D0-D15、地址线、控制线,略去逐脚配置 */ /* 时序配置:地址建立、地址保持、数据建立时间 */ Timing.AddressSetupTime = 3; /* 地址建立时间,单位 HCLK 周期 */ Timing.AddressHoldTime = 1; /* 地址保持时间 */ Timing.DataSetupTime = 6; /* 数据建立时间,W5300 较慢,给足 */ Timing.BusTurnAroundDuration = 1; Timing.CLKDivision = 2; Timing.DataLatency = 2; Timing.AccessMode = FSMC_ACCESS_MODE_A; Init.NSBank = FSMC_NORSRAM_BANK1; Init.DataAddressMux = FSMC_DATA_ADDRESS_MUX_DISABLE; Init.MemoryType = FSMC_MEMORY_TYPE_SRAM; Init.MemoryDataWidth = FSMC_NORSRAM_MEM_BUS_WIDTH_16; /* 关键:16 位 */ Init.BurstAccessMode = FSMC_BURST_ACCESS_MODE_DISABLE; Init.WaitSignalPolarity = FSMC_WAIT_SIGNAL_POLARITY_LOW; Init.WrapMode = FSMC_WRAP_MODE_DISABLE; Init.WaitSignalActive = FSMC_WAIT_TIMING_BEFORE_WS; Init.WriteOperation = FSMC_WRITE_OPERATION_ENABLE; Init.WaitSignal = FSMC_WAIT_SIGNAL_DISABLE; Init.ExtendedMode = FSMC_EXTENDED_MODE_DISABLE; Init.AsynchronousWait = FSMC_ASYNCHRONOUS_WAIT_DISABLE; Init.WriteBurst = FSMC_WRITE_BURST_DISABLE; HAL_FSMC_NORSRAM_Init(FSMC_NORSRAM_DEVICE, &Init); HAL_FSMC_NORSRAM_Timing_Init(FSMC_NORSRAM_DEVICE, &Timing, FSMC_NORSRAM_BANK1); HAL_FSMC_NORSRAM_Extended_Timing_Init(FSMC_NORSRAM_EXTENDED_DEVICE, &Timing, FSMC_NORSRAM_BANK1, FSMC_EXTENDED_MODE_DISABLE); __HAL_FSMC_NORSRAM_ENABLE(FSMC_NORSRAM_DEVICE, FSMC_NORSRAM_BANK1); }这段代码里最需要盯的是三个参数:MemoryDataWidth必须是 16 位,否则读写会错位;DataSetupTime要给够,W5300 的寄存器访问不是零等待,给太小会出现读回值不稳定;AddressSetupTime和AddressHoldTime影响地址锁存的可靠性。具体给多少,取决于你的 HCLK 频率和 W5300 的时序手册要求,我一般先用保守值跑通,再逐步压缩。
2.4 寄存器读写封装与基地址定义
FSMC 把 W5300 映射到某个基地址后,读写寄存器就是对这个地址空间的指针操作。下面是一个简单的封装。
/* FSMC BANK1 的基地址,具体值看芯片参考手册 */ #define W5300_BASE_ADDR ((uint32_t)0x60000000) /* 按 16 位访问,地址偏移以半字为单位 */ #define W5300_REG(offset) (*(volatile uint16_t *)(W5300_BASE_ADDR + ((offset) << 1))) void W5300_WriteReg(uint16_t addr, uint16_t data) { W5300_REG(addr) = data; } uint16_t W5300_ReadReg(uint16_t addr) { return W5300_REG(addr); }这里offset << 1是因为 16 位数据宽度下,地址按字节编址但每次访问两个字节,所以寄存器偏移要乘以 2。如果你的硬件把 A0 接到了 FSMC 的 A1 上,这个移位就是对的;如果接法不同,需要相应调整。写完封装后,第一件事是读 W5300 的版本寄存器或者模式寄存器,看能不能读回合理值,这是验证总线通不通的最快手段。
3. socket 初始化与数据收发的完整流程
3.1 W5300 内部寄存器分区与 socket 编号
W5300 内部有 8 个独立 socket,每个 socket 有自己的寄存器组和收发缓冲区。寄存器大致分几类:通用寄存器(模式、中断、源硬件地址等)、socket 寄存器(socket 模式、命令、状态、端口号、目标 IP 等)、以及收发缓冲区。每个 socket 的寄存器在地址空间里有一个基偏移,socket n 的寄存器地址 = socket 0 基地址 + n × 偏移量。
初始化流程一般是:先软复位,配置主机接口模式(16 位直接总线),设置本机 MAC 和 IP,然后逐个初始化需要用的 socket,设置协议类型(TCP/UDP)、端口号、以及收发缓冲区大小。W5300 的 128KB 内部存储可以分配给 8 个 socket 做收发缓冲,分配比例通过寄存器设置。
3.2 最小初始化代码:从复位到 socket 打开
下面是一段 TCP 服务端 socket 初始化的核心代码,省略了部分重复的寄存器定义。
/* 通用寄存器偏移,具体值参考 W5300 数据手册 */ #define MR 0x00 /* 模式寄存器 */ #define SHAR 0x08 /* 源硬件地址,6 字节 */ #define SIPR 0x0F /* 源 IP 地址,4 字节 */ #define IR 0x02 /* 中断寄存器 */ #define IMR 0x03 /* 中断屏蔽寄存器 */ /* socket 寄存器基偏移,socket n 的偏移 = Sn_BASE + n * 0x40 */ #define Sn_MR 0x00 #define Sn_CR 0x01 #define Sn_SR 0x02 #define Sn_PORT 0x03 #define Sn_DIPR 0x0C #define Sn_DPORT 0x0D #define Sn_TX_FSR 0x10 #define Sn_TX_FIFOR 0x12 #define Sn_RX_RSR 0x16 #define Sn_RX_FIFOR 0x18 void W5300_Init(void) { /* 软复位 */ W5300_WriteReg(MR, 0x8000); HAL_Delay(10); while (W5300_ReadReg(MR) & 0x8000); /* 设置 16 位直接总线模式 */ W5300_WriteReg(MR, 0x3800); /* 设置 MAC 地址 */ W5300_WriteReg(SHAR, 0x0011); W5300_WriteReg(SHAR+1, 0x2233); W5300_WriteReg(SHAR+2, 0x4455); /* 设置 IP 地址,例如 192.168.1.100 */ W5300_WriteReg(SIPR, 0xC0A8); W5300_WriteReg(SIPR+1, 0x0164); /* 分配收发缓冲区,每个 socket 8KB */ W5300_WriteReg(TMSR, 0x0000); /* 全部给 TX,具体看需求 */ W5300_WriteReg(RMSR, 0x0000); } void W5300_Socket_Init_TCP_Server(uint8_t sn, uint16_t port) { uint16_t base = Sn_BASE + sn * 0x40; /* 设置 socket 为 TCP 模式 */ W5300_WriteReg(base + Sn_MR, 0x0001); /* 设置本地端口 */ W5300_WriteReg(base + Sn_PORT, port); /* 打开 socket */ W5300_WriteReg(base + Sn_CR, 0x0001); while (W5300_ReadReg(base + Sn_CR)); /* 等待 socket 进入 LISTEN 状态 */ while (W5300_ReadReg(base + Sn_SR) != 0x0014); }这段代码的关键点:软复位后必须等复位位自动清零才能继续;模式寄存器要设成 16 位直接总线;socket 命令寄存器写入后要等它自己清零,表示命令执行完毕;最后轮询状态寄存器直到进入 LISTEN。如果卡在最后一步,说明前面的配置有问题,或者网络物理层没通。
3.3 数据接收与发送的缓冲区操作
W5300 的收发走 FIFO 缓冲区。接收时,先读 RX 接收数据大小寄存器,确认有数据,然后从 RX FIFO 寄存器连续读出。发送时,先读 TX 空闲大小寄存器,确认空间够,然后往 TX FIFO 寄存器连续写入,最后发 SEND 命令。
uint16_t W5300_Recv(uint8_t sn, uint8_t *buf, uint16_t len) { uint16_t base = Sn_BASE + sn * 0x40; uint16_t rx_size = W5300_ReadReg(base + Sn_RX_RSR); if (rx_size == 0) return 0; if (rx_size > len) rx_size = len; for (uint16_t i = 0; i < rx_size; i++) { buf[i] = (uint8_t)W5300_ReadReg(base + Sn_RX_FIFOR); } /* 发 RECV 命令,告知芯片已读取的字节数 */ W5300_WriteReg(base + Sn_CR, 0x0040); while (W5300_ReadReg(base + Sn_CR)); return rx_size; } uint16_t W5300_Send(uint8_t sn, uint8_t *buf, uint16_t len) { uint16_t base = Sn_BASE + sn * 0x40; uint16_t free_size = W5300_ReadReg(base + Sn_TX_FSR); if (free_size < len) return 0; for (uint16_t i = 0; i < len; i++) { W5300_WriteReg(base + Sn_TX_FIFOR, buf[i]); } /* 发 SEND 命令 */ W5300_WriteReg(base + Sn_CR, 0x0020); while (W5300_ReadReg(base + Sn_CR)); return len; }接收侧要注意:读完数据后必须发 RECV 命令,否则芯片认为缓冲区没被消费,后续数据会堵住。发送侧要注意:写入 FIFO 之前必须确认空闲空间够,否则会覆盖未发送的数据。这两个命令的等待循环不能省,省了会出现偶发的数据错乱,而且很难查。
4. 跑不通的时候先查这五个地方
4.1 读寄存器全为 0 或全为 0xFFFF
现象:初始化后读任何寄存器,返回值不是 0 就是 0xFFFF,没有合理值。
原因:总线根本没通。可能是 FSMC 片选没使能、时序配置太激进导致采样错误、或者硬件上 CS/RD/WR 接错。
解决:先把 FSMC 时序参数放大到很保守的值(比如 AddressSetupTime 和 DataSetupTime 都给 15),确认能读到合理值后再逐步压缩。同时用示波器看 CS 和 RD 有没有正常翻转。如果硬件上 A0 接法和你代码里的移位假设不一致,也会出现读回值整体偏移。
4.2 socket 状态一直停在 CLOSED 或卡在命令等待
现象:发 OPEN 命令后,轮询命令寄存器一直不清零,或者状态寄存器不进入 LISTEN。
原因:常见的是 socket 模式寄存器没配对,或者端口号设置有问题,或者芯片根本没完成初始化。
解决:先确认通用初始化(MAC、IP、模式寄存器)已经正确写入并能读回。然后单独检查这个 socket 的模式寄存器值。如果命令寄存器一直不清零,说明芯片内部状态机没动,通常是前面的配置没生效。可以尝试先关闭 socket 再重新打开。
4.3 能建立连接但收不到数据
现象:TCP 连接能建立,状态进入 ESTABLISHED,但接收数据大小寄存器一直是 0。
原因:可能是对端没发数据,也可能是接收缓冲区分配为 0,或者中断没处理导致芯片内部逻辑卡住。
解决:先确认对端确实发了数据。然后检查收发缓冲区分配寄存器,确认这个 socket 分到了空间。如果用了中断方式,检查中断寄存器有没有未处理的中断标志,未清除的中断会阻塞后续事件。
4.4 发送大数据时丢包或卡死
现象:小数据能发,一发几百字节以上就丢或者卡住。
原因:TX 空闲空间判断有问题,或者写入 FIFO 的速度超过了芯片消费速度,或者 FSMC 时序在连续访问时出现建立/保持时间不足。
解决:发送前严格检查 TX 空闲大小,分片发送,每片不要超过空闲空间。连续写 FIFO 时,如果 FSMC 时序偏紧,可以在写操作之间插入少量延时,或者放宽 DataSetupTime。另外确认发送命令发出后等待命令清零再发下一次。
4.5 网络能通但吞吐远低于预期
现象:功能正常,但吞吐只有几 Mbps,远达不到 100Mbps。
原因:FSMC 时序太保守、数据搬运没有用批量读、或者 MCU 主频太低导致总线成为瓶颈。
解决:在保证稳定的前提下压缩 FSMC 时序参数;接收时尽量一次读走整个缓冲区的数据,减少命令交互次数;如果 MCU 支持 DMA 配合 FSMC,可以考虑用 DMA 搬运数据,把 CPU 解放出来。但 W5300 的 FIFO 访问是寄存器式的,DMA 不一定直接适用,需要看具体型号的总线行为。
5. 把吞吐再往上推一档:批量搬运与双缓冲思路
前面把功能跑通了,但如果你的场景是持续大流量,比如视频流转发或者高速数据采集上传,光跑通还不够,得把吞吐往上推。这里讲一个我实际用过的思路:减少命令交互次数,配合双缓冲。
W5300 的接收流程里,每次读数据前要读一次 RX 大小寄存器,读完要发一次 RECV 命令。如果每收到几十字节就交互一次,命令开销占比很高。我的做法是:轮询到 RX 大小超过一个阈值(比如 1KB)再一次性读走,然后发一次 RECV。这样命令交互次数大幅下降,总线利用率明显提升。
发送侧同理,攒够一定数据再发 SEND,而不是来一个字节发一次。但要注意 TX 空闲空间的限制,不能攒过头。
下面是一个带阈值的接收循环示例。
#define RECV_THRESHOLD 1024 #define RECV_BUF_SIZE 2048 uint8_t recv_buf[RECV_BUF_SIZE]; void W5300_Recv_Loop(uint8_t sn) { uint16_t base = Sn_BASE + sn * 0x40; uint16_t rx_size = W5300_ReadReg(base + Sn_RX_RSR); if (rx_size >= RECV_THRESHOLD) { if (rx_size > RECV_BUF_SIZE) rx_size = RECV_BUF_SIZE; /* 批量读出,减少循环内其他操作 */ for (uint16_t i = 0; i < rx_size; i++) { recv_buf[i] = (uint8_t)W5300_ReadReg(base + Sn_RX_FIFOR); } /* 一次性发 RECV 命令 */ W5300_WriteReg(base + Sn_CR, 0x0040); while (W5300_ReadReg(base + Sn_CR)); /* 这里把 recv_buf 交给上层处理,处理逻辑和接收解耦 */ Process_Data(recv_buf, rx_size); } }这个循环里,RECV_THRESHOLD是关键参数。设太小,命令开销大;设太大,延迟高,而且缓冲区可能不够。我一般从 512 或 1024 开始试,根据实际流量和延迟要求调整。RECV_BUF_SIZE要大于阈值,留出余量。
双缓冲的思路是:准备两块缓冲区,一块在接收时,另一块交给上层处理,处理完再交换。这样接收和处理可以重叠,不会因为处理耗时导致接收停顿。实现上可以用一个状态标志或者简单的指针交换。
还有一个容易忽略的点:W5300 的 TX 和 RX 缓冲区是共享内部 128KB 存储的,分配比例要按实际流量调。如果发送多,就多分 TX;接收多,就多分 RX。分配寄存器写错会导致某个方向空间不足,表现为发送卡住或者接收丢数据。
最后说一个验证吞吐的方法:用一个固定的数据包,比如 1400 字节,连续发送几千包,统计总时间和丢包数。对比不同 FSMC 时序和阈值设置下的结果,找到稳定前提下的最优组合。我自己的习惯是,每次改时序参数都跑一遍这个测试,确认没有因为压缩时序导致偶发错误。这个习惯帮我省了很多后悔药,因为时序问题往往在实验室短时间跑不出问题,一到现场长时间运行就翻车。
希望帮到你。
本文还有配套的精品资源,点击获取