ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32F4通过FSMC驱动W5300:从总线时序到Socket移植实战

STM32F4通过FSMC驱动W5300:从总线时序到Socket移植实战 简介面向STM32F4系列微控制器驱动W5300以太网控制器的工程配置包适合嵌入式开发人员快速移植网络通信功能。压缩包包含53个文件主要以C语言源文件、头文件、Keil MDK工程文件及编译生成文件组成覆盖芯片寄存器定义、底层驱动配置、启动文件与链接脚本等关键模块整体体积仅249KB便于直接集成到现有项目中。已有474人学习使用说明该配置方案具备一定参考价值。通过这套工程读者可掌握W5300的寄存器配置流程、中断与DMA收发机制并基于STM32F4平台完成100M以太网通信的硬件驱动搭建缩短开发调试周期。1. STM32F4 接 W5300先别急着调 socket很多人的第一块网卡芯片是 W5500SPI 接口、三根线、几行代码就能跑 TCP。等拿到一块 W5300 模块按 W5500 的思路初始化结果发现读写寄存器完全不对甚至连引脚都不认识。W5300 是并行总线接口16 位数据、多位地址、读写选通、片选、中断要把它跑起来先得把 FSMC 或 GPIO 模拟总线的时序搞定再谈 socket。网上流传的“野火 W5300”例程多数也是基于 STM32F407 的 FSMC NOR/SRAM 模式写的拿到 F405、F427 甚至 F429 上第一步要改的不是协议而是总线和时钟。这篇文章按驱动交付的顺序来推演总线时序 → 寄存器配置 → 内存分配 → Socket 初始化 → 移植经验 → 验证手段把 W5300 在 STM32F4 上的配置要点一次捋清楚。2. W5300 并行总线与 FSMC 初始化时序参数决定 Fifo 是否丢数据2.1 为什么选并行总线W5300 和 W5500 的驱动差异W5300 内部集成了 128 KB 的 TX/RX 缓冲数据吞吐能力比 W5500 高一个量级这靠 SPI 是喂不饱的所以它使用并行总线接口数据宽度支持 8/16 位地址线直接映射内部寄存器。读一个寄存器和读 SRAM 一样是内存映射访问不是“发命令再读数据”的协议时序。这就决定了两个驱动姿势W5500 要的是 SPI 的 CPOL/CPHAW5300 要的是片选、读、写、地址建立/保持时间的配合。实际使用中如果只用 GPIO 模拟总线最高频通常跑不过 1 MHz 的等效寄存器访问周期遇到大流量回环测试会丢包。走 FSMC 才是正路把 W5300 映射到寻址空间里一个*(volatile uint16_t *)addr就是一次寄存器读写效率和可靠性都上得来。2.2 FSMC 的 Bank 选择与地址映射0x6C00 0000 的由来STM32F4 的 FSMC 把外部存储空间分成 4 个 BankW5300 这类并行外设一般挂在 Bank1 的 NOR/PSRAM 控制器上。Bank1 又分成 NE1 ~ NE4 四个片选区NE1 的基地址是0x6000 0000NE2 是0x6400 0000NE3 是0x6800 0000NE4 是0x6C00 0000。很多 W5300 例程里都会出现0x6C00 0000就是因为模块的片选脚接在 FSMC_NE4 上。硬件连接上W5300 的nSCS对应 FSMC_NE4nWR对应 FSMC_NWEnRD对应 FSMC_NOEA[14:0]对应 FSMC_A[14:0]D[15:0]对应 FSMC_D[15:0]。软件上把 W5300 的一组寄存器当作一个连续地址空间即可。#define W5300_BASE ((uint32_t)0x6C000000) #define W5300_REG(addr) (*(volatile uint16_t *)(W5300_BASE ((addr) 1)))注意这里用((addr) 1)是因为 W5300 内部地址线以 16 位字为单位对齐而 STM32 的 FSMC 地址线是按字节编址的所以寄存器序号要先左移一位。这个细节写错读出来的寄存器值全是乱的排查起来非常隐蔽。2.3 用库函数配置 FSMC 总线时序FSMC 初始化要关心 4 个参数地址建立时间、地址保持时间、数据建立时间、总线周转时间。W5300 的数据手册里读周期典型值在 80 ns 级别STM32F4 在 168 MHz 主频下一个 HCLK 周期约 5.95 ns所以把地址建立设成 6 个 HCLK、数据建立设成 8 个 HCLK基本能覆盖 W5300 的时序要求。写周期同样按这个量级给。保守一点不是坏事毕竟 W5300 不是高速 SRAM总线跑得太快反而容易读到中间态。void W5300_FSMC_Init(void) { GPIO_InitTypeDef gpio; FSMC_NORSRAMInitTypeDef fsmc; FSMC_NORSRAMTimingInitTypeDef timing; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD | RCC_AHB1Periph_GPIOE | RCC_AHB1Periph_GPIOF | RCC_AHB1Periph_GPIOG, ENABLE); RCC_AHB3PeriphClockCmd(RCC_AHB3Periph_FSMC, ENABLE); // 数据线 D[15:0] 在 PD/PE 上地址线 A[14:0] 在 PF/PG 上 // 这里只列关键引脚实际工程按原理图补齐 gpio.GPIO_Mode GPIO_Mode_AF; gpio.GPIO_Speed GPIO_Speed_50MHz; gpio.GPIO_OType GPIO_OType_PP; gpio.GPIO_PuPd GPIO_PuPd_NOPULL; GPIO_PinAFConfig(GPIOD, GPIO_PinSource0, GPIO_AF_FSMC); // ... 每个 FSMC 引脚都要做 PinAFConfig timing.FSMC_AddressSetupTime 6; timing.FSMC_AddressHoldTime 1; timing.FSMC_DataSetupTime 8; timing.FSMC_BusTurnAroundDuration 0; timing.FSMC_CLKDivision 0; timing.FSMC_DataLatency 0; timing.FSMC_AccessMode FSMC_AccessMode_A; fsmc.FSMC_Bank FSMC_Bank1_NORSRAM4; fsmc.FSMC_DataAddressMux FSMC_DataAddressMux_Disable; fsmc.FSMC_MemoryType FSMC_MemoryType_SRAM; fsmc.FSMC_MemoryDataWidth FSMC_MemoryDataWidth_16b; fsmc.FSMC_BurstAccessMode FSMC_BurstAccessMode_Disable; fsmc.FSMC_AsynchronousWait FSMC_AsynchronousWait_Disable; fsmc.FSMC_WaitSignalPolarity FSMC_WaitSignalPolarity_Low; fsmc.FSMC_WrapMode FSMC_WrapMode_Disable; fsmc.FSMC_WaitSignalActive FSMC_WaitSignalActive_BeforeWaitState; fsmc.FSMC_WriteOperation FSMC_WriteOperation_Enable; fsmc.FSMC_WaitSignal FSMC_WaitSignal_Disable; fsmc.FSMC_ExtendedMode FSMC_ExtendedMode_Disable; fsmc.FSMC_WriteBurst FSMC_WriteBurst_Disable; fsmc.FSMC_ReadWriteTimingStruct timing; fsmc.FSMC_WriteTimingStruct timing; FSMC_NORSRAMInit(fsmc); FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM4, ENABLE); }这段代码里FSMC_AddressHoldTime给 1 个 HCLK够用如果布板走线长、寄生电容大可以加到 2。FSMC_AccessMode_A是读和写共用一套时序参数。初始化完成后不需要每次都重新配置 FSMC系统起来配置一次即可。调试时怀疑时序问题就把FSMC_AddressSetupTime和FSMC_DataSetupTime同时往上加优先让寄存器读写稳定再考虑压时序提性能。FSMC 跑通后验证方法很简单往某个可写寄存器写0x1234再读回来。W5300 的版本寄存器 VERSIONR 是只读的读到非 0xFF、非 0x00 的值就说明总线和地址映射基本正常。这一步没通过后面 Socket 配置全都不用开始。3. W5300 寄存器配置模式字、MAC/IP 与 128 KB 缓冲分配3.1 配置第一步模式控制寄存器 MR 与操作模式W5300 的寄存器空间分两块通用寄存器和 Socket 寄存器。通用寄存器在最前面第一个就是模式寄存器 MR。MR 控制芯片的整体行为包括复位、休眠、网络唤醒、数据总线宽度、读写时序模式等。复位后第一件事是往 MR 写0x0100进入复位模式再写0x0000退出复位然后才是正常配置。#define W5300_MR 0x0000 #define W5300_IR 0x0002 #define W5300_IMR 0x0004 #define W5300_SHAR 0x0008 // MAC 地址6 字节 #define W5300_GAR 0x0010 // 网关地址 #define W5300_SUBR 0x0014 // 子网掩码 #define W5300_SIPR 0x0018 // 本机 IP #define W5300_TMSR 0x0028 // TX 内存分配 #define W5300_RMSR 0x002A // RX 内存分配 void W5300_InitRegs(void) { // 复位 W5300 W5300_REG(W5300_MR) 0x0100; delay_ms(10); W5300_REG(W5300_MR) 0x0000; delay_ms(10); // 配置 MAC 地址 uint8_t mac[6] {0x00, 0x08, 0xDC, 0x12, 0x34, 0x56}; for (int i 0; i 6; i) W5300_REG(W5300_SHAR i * 2) mac[i]; // 配置 IP / 网关 / 掩码 uint8_t ip[4] {192, 168, 1, 100}; uint8_t gw[4] {192, 168, 1, 1}; uint8_t mask[4] {255, 255, 255, 0}; for (int i 0; i 4; i) { W5300_REG(W5300_GAR i * 2) gw[i]; W5300_REG(W5300_SUBR i * 2) mask[i]; W5300_REG(W5300_SIPR i * 2) ip[i]; } }MAC 和 IP 寄存器的字节排列是按一个字的高 8 位、低 8 位排的所以循环里用 i * 2来偏移。这里的delay_ms不能用太短的延时W5300 复位需要几个微秒到十几微秒保险起见给 10 ms。如果有硬件复位脚也可以在 FSMC 初始化之前先把硬件复位拉低再拉高双保险。3.2 网络参数寄存器与内存分配寄存器 TMSR/RMSRW5300 的 128 KB 收发缓冲来自外部 SRAM 接口TMSR和RMSR分别控制 8 个 socket 的 TX/RX 缓冲大小。每个寄存器是 16 位每 2 位对应一个 socket4 种取值00表示 0 KB01表示 8 KB10表示 16 KB11表示 32 KB。所有 socket 的缓冲总和不能超过 128 KB。如果只用一个 socket就把它设成 32 KB TX 32 KB RX剩下的空间给其他 socket 留 0。刚开始调试的时候建议每个 socket 都留一点比如 2 KB这样后面要开第二个 socket 做测试时不用回头改配置。寄存器位域Socket 号取值缓冲大小TMSR[15:14]S000 / 01 / 10 / 110 / 8 / 16 / 32 KBTMSR[13:12]S1同上同上RMSR[15:14]S0同上同上RMSR[13:12]S1同上同上void W5300_Buffer_Init(void) { // S0: TX 32KB, RX 32KB, 其余 socket 全 0 W5300_REG(W5300_TMSR) 0x0000; // 先全清 W5300_REG(W5300_RMSR) 0x0000; W5300_REG(W5300_TMSR) 0xC000; // 11 00 00 00 ... - S0 32KB W5300_REG(W5300_RMSR) 0xC000; // S0 32KB }这里有个常见的误解TMSR和RMSR配置完以后socket 的收发缓冲就固定了后续往 Socket TX FIFO 写数据不能超过这个上限。但是 W5300 的发送缓冲写操作并不是像普通 SRAM 那样直接一次写完而是要先写 Socket 的 TX 写指针寄存器再通过 FIFO 窗口写入所以缓冲分配决定的是上限不直接影响单个包的写入过程。3.3 把缓冲空间与 Socket 对应起来W5300 内部为每个 socket 维护独立的寄存器组起始地址从0x0400开始每个 socket 占0x0040字节的空间。Socket 寄存器包括模式Sn_MR、命令Sn_CR、中断Sn_IR、状态Sn_SR、端口Sn_PORTR、收发指针Sn_TX_WR、Sn_RX_RD等。用宏定义来访问比较清晰#define W5300_S0_BASE 0x0400 #define W5300_S1_BASE 0x0440 #define Sn_MR(base) (W5300_REG((base) 0x0000)) #define Sn_CR(base) (W5300_REG((base) 0x0002)) #define Sn_IMR(base) (W5300_REG((base) 0x0004)) #define Sn_IR(base) (W5300_REG((base) 0x0006)) #define Sn_SR(base) (W5300_REG((base) 0x0008)) #define Sn_PORTR(base) (W5300_REG((base) 0x000C)) #define Sn_TX_WR(base) (W5300_REG((base) 0x0024)) #define Sn_RX_RD(base) (W5300_REG((base) 0x0028))这样写出来的代码后续切换 socket 只需要改base参数。实际项目里如果要用两个 socket 分别做 TCP Server 和 UDP 采集务必在初始化时把 TMSR/RMSR 的对应位域分配好否则第二个 socket 打开时硬件没有可用的缓冲空间命令序列会一直卡在SOCK_INIT状态出不来。4. W5300 Socket 初始化与 TCP 通信流程命令序列不能跳步4.1 Socket 命令寄存器 Sn_CR 的五个关键命令W5300 的 socket 状态机靠Sn_CR驱动。命令字是一次性触发写完以后硬件立即开始执行执行完Sn_CR自动清零所以软件在提交下一个命令之前要等Sn_CR归零否则命令会丢失。常用的命令有OPEN0x01、LISTEN0x02、CONNECT0x04、DISCON0x08、CLOSE0x10、SEND0x20、RECV0x40。看起来和 W5500 很相似但寄存器地址和缓冲窗口完全不同不能直接套用 W5500 的宏。4.2 TCP Server 的完整扫描序列跑一个 TCP Server 的过程比 W5500 多两步打开 socket、监听。它的状态流转是SOCK_CLOSED→ 写Sn_MR为 TCP 模式 → 写Sn_PORTR→ 发送OPEN→ 状态变为SOCK_INIT→ 发送LISTEN→ 状态变为SOCK_LISTEN→ 收到客户端连接后状态自动变为SOCK_ESTABLISHED。uint8_t W5300_Socket_Listen(uint8_t sock_base, uint16_t port) { // 先关闭可能存在的残留连接 Sn_CR(sock_base) 0x10; // CLOSE while (Sn_CR(sock_base) ! 0); // 设置为 TCP 服务端模式 Sn_MR(sock_base) 0x01; // TCP Sn_PORTR(sock_base) port; Sn_CR(sock_base) 0x01; // OPEN while (Sn_CR(sock_base) ! 0); while (Sn_SR(sock_base) ! 0x13) ; // SOCK_INIT Sn_CR(sock_base) 0x02; // LISTEN while (Sn_CR(sock_base) ! 0); while (Sn_SR(sock_base) ! 0x14) ; // SOCK_LISTEN return 0; }Sn_SR的状态码是关键0x13是SOCK_INIT0x14是SOCK_LISTEN0x17是SOCK_ESTABLISHED。如果卡在OPEN之后的状态不是0x13多半是Sn_MR的模式字没配对。W5300 的Sn_MR里 TCP 模式的数值是0x01UDP 是0x02和 W5500 的Sn_MR位定义并不完全一致踩过坑的人都知道这个差异有多隐蔽。4.3 数据收发、中断与 Socket 状态建立连接后收发数据不再需要操作命令寄存器。发送是往Sn_TX_WR指向的 TX 缓冲窗口写数据再更新写指针并触发SEND接收是读Sn_RX_RSR拿到接收字节数再从 RX 缓冲窗口读数据最后更新读指针并触发RECV。命令序列错了数据就发不出去。uint16_t W5300_SendData(uint8_t sock_base, uint8_t *data, uint16_t len) { uint16_t free_size; uint16_t tx_wr; free_size W5300_REG(sock_base 0x0020); // Sn_TX_FSR if (free_size len) return 0; tx_wr W5300_REG(sock_base 0x0024); // Sn_TX_WR // 往 TX 缓冲窗口写入数据 for (uint16_t i 0; i len; i) W5300_REG(sock_base 0x0040 (((tx_wr i) 0x7FFF) 1) * 2) data[i]; W5300_REG(sock_base 0x0024) (uint16_t)(tx_wr len); // 更新写指针 Sn_CR(sock_base) 0x20; // SEND while (Sn_CR(sock_base) ! 0); return len; }这段代码里Sn_TX_FSR是发送缓冲剩余字节数0x0040是 TX 缓冲窗口的基地址(tx_wr i) 0x7FFF是 32 KB 缓冲的地址回绕。0x0040这个偏移不同 socket 是一样的因为窗口寄存器是每个 socket 独立的一块地址取决于基地址不是取决于 socket 号。中断方面一般只用 socket 中断Sn_IR的SEND_OK和RECV位全局中断IR里再开 socket 中断使能。中断处理函数里要读Sn_IR按位清标志否则中断会一直被拉起。5. 野火 W5300 例程的移植要点从 F407 换到其他 F4 的变化5.1 野火例程里通常有哪些文件和宏网上流传的野火 W5300 例程目录结构一般是工程核心 外设驱动。W5300 的驱动部分常见的有w5300.c、w5300.h、w5300_conf.h另外会有一个w5300_platform.c放硬件初始化。w5300_conf.h里面通常定义 W5300 基地址、FSMC 参数、socket 数量、缓冲大小这是移植时改动最频繁的文件。搬到一个新板子先看W5300_BASE和FSMC_Bank是否匹配硬件片选脚。很多例程默认 NE4 片选如果你的板子上片选接的是 NE10x6C000000要改成0x60000000这个不对读出来的寄存器全变成0xFFFF或0x0000。5.2 引脚复用、时钟与中断整改野火例程面向 F407 居多换到 F405 基本兼容但换到 F427、F429 这类带 LCD 接口的型号要注意引脚冲突。F429 的 PF0 ~ PF5 往往被 LTDC 或 SDRAM 占用如果你又要接 W5300 又要跑屏幕引脚分配就得重新规划。用 FSMC 引脚时先把GPIO_PinAFConfig全列出来核对一遍FSMC 功能属于GPIO_AF_FSMC在 F4 系列上是固定的不能随便复用。中断引脚一般接到 STM32F4 的一个 EXTI 引脚初始化为下降沿触发。void W5300_Interrupt_Init(void) { GPIO_InitTypeDef gpio; EXTI_InitTypeDef exti; NVIC_InitTypeDef nvic; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE); gpio.GPIO_Mode GPIO_Mode_IN; gpio.GPIO_PuPd GPIO_PuPd_UP; GPIO_Init(GPIOE, gpio); SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource0); exti.EXTI_Line EXTI_Line0; exti.EXTI_Mode EXTI_Mode_Interrupt; exti.EXTI_Trigger EXTI_Trigger_Falling; exti.EXTI_LineCmd ENABLE; EXTI_Init(exti); nvic.NVIC_IRQChannel EXTI0_IRQn; nvic.NVIC_IRQPreemptionPriority 3; nvic.NVIC_IRQSubPriority 0; nvic.NVIC_IRQCmd ENABLE; NVIC_Init(nvic); }中断优先级这里要特别说一句。很多基于 STM32F4 的嵌入式工程里开发者习惯把外设中断优先级都设成一样比如只用定时器输入捕获或者用 DAC 由定时器 6 启动做波形输出时觉得无所谓但 W5300 这种高吞吐外设中断优先级如果比 SysTick 还高主循环里又依赖delay_ms做超时判断就会出现中断把延时函数打穿的情况。建议把 W5300 中断优先级设得比系统滴答低、比普通外设高比如抢占优先级 2 或 3。5.3 RTOS 下用 W5300中断线程与进程模型跑 FreeRTOS 时W5300 驱动不能像裸机那样在主循环里死等状态寄存器。常见做法是中断里只置位一个事件标志组把Sn_IR的读取和缓冲搬运放到一个专用任务里。W5300 的 socket 中断会把对应位置位但清除和数据处理不能全堆在 ISR 里否则高频率的数据接收会拖垮整个系统的实时性。具体分工ISR 里关中断、读Sn_IR、清标志、记录 socket 号然后BaseType_t xHigherPriorityTaskWoken pdFALSE触发任务唤醒TCP 任务里再根据 socket 号去搬数据。W5300 的寄存器读写有几百 ns 的延迟放在任务里执行比放在中断里合理得多。任务模式还影响缓冲分配策略。RTOS 系统里如果 TCP 任务和另一个 UDP 采集任务同时跑TMSR/RMSR里要给两个 socket 都分缓冲否则第二个 socket 的OPEN命令无法完成。分配方案可以写死在w5300_conf.h里但更好的做法是用一个结构体把 socket 配置集中管理typedef struct { uint8_t base; uint8_t mode; uint16_t port; uint16_t tx_size; uint16_t rx_size; } W5300_SocketCfg;这样每个 socket 的缓冲大小和端口一目了然后续加 socket 只需要追加一个结构体元素不用到处改宏。6. 收尾验证逻辑分析仪看写时序、PING 通、echo 压力测试驱动写完先别急着连电脑跑整包项目按三个步骤做验证每一步都只看一件事定位问题最快。第一步用逻辑分析仪抓 FSMC 写时序。把FSMC_NE4、FSMC_NWE、最低位地址线和一根数据线接出来触发条件设为NWE下降沿。初始化完成后手动写一次MR寄存器看NWE低电平宽度。F407 168 MHz 下如果 HCLK 周期是 5.95 nsDataSetupTime8对应的数据建立时间应该在 47 ns 左右拿这个值和逻辑分析仪的量测值对比。偏差太大先调 FSMC 参数不要动协议栈。第二步PING 通测试。TCP Server 初始化好之后把网线插到电脑上Windows 或 Linux 上直接ping 192.168.1.100。这一步验证链路层和 IP 层PING不通时先看 W5300 的IR寄存器有没有LINK位变化再查 MAC 和 IP 寄存器。如果连PING都不通不要怀疑上层协议问题一定在SHAR、SIPR或网络物理层。有的模块上会有一颗网络变压器丝印标着 like sox8e 之类的封装型号注意检查它的中心抽头有没有接隔离电源或正确的偏置电阻焊接问题会导致LINK灯亮但收不到包。第三步echo 压力测试。用 Socket 调试工具连接 W5300 的监听端口开启 TCP Server 模式连续发数据包同时在 W5300 端统计RECV中断次数和收到的字节数。如果出现数据完整性错误比如收到的内容里有固定间隔的乱码优先怀疑Sn_TX_WR更新和缓冲回绕逻辑。回绕判断必须用((tx_wr len) 0x7FFF)这种方式强制限制在缓冲大小内不能直接拿uint16_t的天然溢出当回绕用因为0x8000之后还有Sn_TX_WR高位参与运算。最后记录一个耗时数据写一个小工具在W5300_SendData前后读一次DWT-CYCCNT算出单包发送的 CPU 周期数把 1 秒内能发的最大包数算出来作为驱动上线的性能基线。之后每次改动 FSMC 时序或中断模型都用这个基线对比判断改动是优化还是回退。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进