ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32F407通过FSMC驱动W5100S硬件TCP/IP以太网方案

STM32F407通过FSMC驱动W5100S硬件TCP/IP以太网方案

简介:围绕STM32F407VET6单片机与W5100S以太网模块展开的网络通信例程包,重点演示通过FSMC并行总线连接HS-NM5100SA模块的软硬件实现,适合具备单片机基础、正在做嵌入式以太网接入与TCP通信调试的开发者参考,也可用于课程设计、毕业设计和工程样机验证。压缩包共143个文件,约5.12MB,C源文件与头文件各56个,构成外设驱动、芯片底层访问及主程序主体,另含MDK5.25工程、HEX烧录文件、调试配置和多种TCP调试助手工具,便于编译验证与联调。例程覆盖服务器模式和客户端模式,可帮助读者理解总线时序配置、寄存器访问、连接建立与数据收发流程,并借助附带工具排查通信异常。目前已有1147人学习下载,适合作为网络通信入门、实验教学、课程实践与项目原型搭建的完整参考。

1. 从一块 F407 板子接入以太网说起:STM32F407 + W5100S 这条路子

手上有块 STM32F407 的板子,项目要求加以太网。很多人会先填一个 LwIP,结果 RAM 被啃掉一大半,中断稍微密一点就开始丢包,抓包看现象又找不到头绪。另一种更省心的做法是:STM32F407 通过 FSMC 连接 W5100S 做网络通信,让 TCP/IP 协议栈在芯片内部运行,MCU 只按地址读写寄存器,把数据写入 W5100S 内部的发送缓冲区。

这份工程包正是按这个思路搭建的:FSMC 总线配置、W5100S 寄存器驱动、Socket 收发、一个可直接运行的示例,从硬件接线到 ping 通再到 TCP 传数据都在里面。适合两类人:第一类是刚接触以太网外设,想先让板子“通”起来,再回头补细节;第二类是已经被软件协议栈的内存占用和稳定性折腾过,想换一套确定性的方案。需要的基础不多,GPIO 复用和 FSMC 有个概念就能跟上。

2. 硬件与总线设计:从 FSMC Bank1 到 W5100S 的引脚映射

W5100S 对外有两种总线访问方式:直接总线和间接总线。直接总线是把芯片内部的 32KB 地址空间原样暴露到引脚上,A0~A14 加 8 位数据线,MCU 一组地址就能选中某个寄存器或缓冲区字节。间接总线则只暴露三个寄存器(偏移地址寄存器、地址寄存器、数据寄存器),每次访问都要写三次总线。这两条路的选择基本决定了后面所有代码的形态。

这个工程走的是直接总线。理由很直接:FSMC 天生就是干这个的,把 W5100S 当成一片 8 位异步 SRAM 挂在 Bank1 上,读写一个字节就是一条STRB/LDRB指令,不需要额外的时序翻转。代价是占用引脚多,A0~A14 加上 D0~D7、NE1、NOE、NWE,接近 26 根线。如果板子引脚紧张,间接总线是退路,但吞吐会明显下降,因为每读一个字节都要多两次写操作。

2.1 引脚映射:NE1、地址线与 8 位数据总线怎么接

STM32F407 的 FSMC 一共四组片选,每组对应一段固定基址。工程里用的是 NE1,也就是 Bank1 的 NOR/SRAM 子区 1,基址为0x60000000。W5100S 的片选 CS 接 PD7(FSMC_NE1),读使能 RD 接 PD4(FSMC_NOE),写使能 WR 接 PD5(FSMC_NWE)。这三根控制线决定了一次访问是读还是写、以及选中哪颗芯片。

数据线用 8 位宽度,接 FSMC 的 D0~D7:PD14、PD15、PD0、PD1、PE7~PE10。地址线按 W5100S 的 A0~A14 对应接 FSMC 的 A0~A14,在 F407 上主要落在 PF0~PF5、PF12~PF15 和 PG0~PG4。具体引脚号跟封装有关,用 CubeMX 生成一次比自己数手册快得多,但生成之后一定要回头核对这三根控制线有没有被别的外设抢走。

下面这张表是我接线时习惯先填一遍的对照表,接完对着量一遍通断,比事后调试省事:

W5100S 信号STM32F407 侧FSMC 功能说明
CSPD7FSMC_NE1片选,低有效,对应 0x60000000
RDPD4FSMC_NOE读使能,低有效
WRPD5FSMC_NWE写使能,低有效
D0~D7PD14/PD15/PD0/PD1/PE7~PE10FSMC_D0~D78 位双向数据
A0~A14PF/PG 分散FSMC_A0~A14直接映射内部 32KB 空间
INT任意 EXTI 脚—可选,事件中断用
RST任意 GPIO—硬复位,建议留出

有一处容易接错:W5100S 的 RST 引脚不能悬空。我见过有人只靠软件写 MR 的 RST 位复位,结果上电瞬间芯片状态不确定,寄存器读出来一片乱码,排查了很久。硬复位脚拉一个 10k 上拉、再引到 MCU 一个普通 GPIO,上电时主动拉低 2ms,这习惯值得养成。

2.2 地址映射与时序:把 W5100S 当成一片 SRAM

地址映射是这套方案里最省心的部分。因为走直接总线,W5100S 内部偏移0x0000就是基址0x60000000,偏移0x0029就是0x60000029,依此类推,中间不需要任何换算。W5100S 内部空间大致分三段:0x0000~0x0029是通用寄存器(网关、子网掩码、本机 IP、重传参数等),0x0400起是 4 个 Socket 各自的寄存器块,每块0x100字节,0x4000起是发送缓冲区,0x6000起是接收缓冲区,每个 Socket 默认各 2KB。

需要单独说明一点:8 位总线模式下,FSMC 的 A0 对应 CPU 地址的 bit0,所以基址加偏移就是芯片偏移,不必像 16 位模式那样做地址移位。这个是很多人第一次用 FSMC 会犯的错误:照抄 16 位 SRAM 的代码,把偏移左移一位,结果所有寄存器都读错位。

时序参数是第二处关键。F407 的 FSMC 时钟来自 HCLK,168MHz 下单个周期约 5.95ns。W5100S 的并行总线不算快,读周期在几十纳秒量级,所以时序可以给得相对宽松。工程里用的是异步模式 A(SRAM 的常规模式),地址建立时间 3 个 HCLK,数据保持时间 9 个 HCLK,一个读周期大约 70ns 出头。这个值不算最优,但足够稳。

调试期的经验是:先给宽,再收窄。第一次点通的时候把 DataSetupTime 拉到 15,确认寄存器读写全部正常,再逐级往下降到 9、7、5,每次下降跑一遍连续 1000 次读写比对。我在一块板子上见过降到 4 就偶发读错的情况,示波器上看是 WR 的下降沿和数据的保持时间贴太近。这种偶发错误最耗时间,因为十次里可能只错一次。

3. FSMC 驱动落地:初始化代码与寄存器访问封装

从这一章开始进入代码。整个驱动分三层:最底下是 FSMC 的硬件初始化,中间是寄存器读写封装,上面是 Socket API。分层的好处是换芯片型号时只需要改最上面一层,换 MCU 时只需要改最下面一层。工程里的组织方式也是这样,w5100s_port.c只负责总线和延时,w5100s_reg.c只负责读写,业务逻辑碰不到0x60000000这个地址。

3.1 时钟与 GPIO 复用配置

FSMC 挂在 AHB3 上,使能时钟的同时要把它用到的那几组 GPIO 全部打开。这里有个小细节:F407 上 FSMC 的时钟使能宏是__HAL_RCC_FSMC_CLK_ENABLE(),不是FMC,两个名字在 HAL 里是分开的,写错了编译能过但外设不工作。GPIO 全部配成复用推挽、AF12、最高速度,因为并行总线的边沿比较陡,速度等级低了波形会变圆。

/* w5100s_port.c —— FSMC 底层硬件初始化 */ #include "stm32f4xx_hal.h" SRAM_HandleTypeDef hsram1; void HAL_SRAM_MspInit(SRAM_HandleTypeDef *hsram) { GPIO_InitTypeDef gpio = {0}; __HAL_RCC_FSMC_CLK_ENABLE(); /* 注意是 FSMC 不是 FMC */ __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; /* 复用推挽 */ gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH; gpio.Alternate = GPIO_AF12_FSMC; /* PD: D0/D1(D14/D15)、D2/D3(D0/D1)、NOE(D4)、NWE(D5)、NE1(D7) */ gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_7 | GPIO_PIN_14 | GPIO_PIN_15; HAL_GPIO_Init(GPIOD, &gpio); /* PE: D4~D7 对应 PE7~PE10 */ gpio.Pin = GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10; HAL_GPIO_Init(GPIOE, &gpio); /* PF/PG: A0~A14,按实际接线补齐 */ 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); }

这段是 CubeMX 生成的框架,但有两个地方值得自己改:一是把GPIO_NOPULL保持住,别图省事改成上拉,并行总线加上拉会让上升沿变缓;二是地址线里没用到的引脚别顺手配上,FSMC 一旦使能某组 GPIO 的复用,那几个脚就不能再当普通 IO 用了,我见过有人把 PG2 配成 FSMC_A12 之后又拿去点 LED,结果灯永远不亮。

3.2 FSMC 时序参数怎么定

初始化的主体是填FSMC_NORSRAM_InitTypeDef和时序结构体。类型要选 SRAM,不是 PSRAM 也不是 NOR Flash,因为 W5100S 的读写行为和静态 RAM 一致,没有命令序列。总线宽度选 8 位,扩展模式关闭(读写共用一套时序,省事),突发模式关闭(并行外设不支持)。

void W5100S_Bus_Init(void) { FSMC_NORSRAM_TimingTypeDef Timing = {0}; hsram1.Instance = FSMC_NORSRAM_DEVICE; hsram1.Extended = FSMC_NORSRAM_EXTENDED_DEVICE; hsram1.Init.NSBank = FSMC_NORSRAM_BANK1; /* NE1 -> 0x60000000 */ hsram1.Init.DataAddressMux = FSMC_DATA_ADDRESS_MUX_DISABLE; hsram1.Init.MemoryType = FSMC_MEMORY_TYPE_SRAM; hsram1.Init.MemoryDataWidth = FSMC_NORSRAM_MEM_BUS_WIDTH_8; /* 关键:8 位 */ hsram1.Init.BurstAccessMode = FSMC_BURST_ACCESS_MODE_DISABLE; hsram1.Init.WaitSignalPolarity = FSMC_WAIT_SIGNAL_POLARITY_LOW; hsram1.Init.WrapMode = FSMC_WRAP_MODE_DISABLE; hsram1.Init.WaitSignalActive = FSMC_WAIT_TIMING_BEFORE_WS; hsram1.Init.WriteOperation = FSMC_WRITE_OPERATION_ENABLE; hsram1.Init.WaitSignal = FSMC_WAIT_SIGNAL_DISABLE; hsram1.Init.ExtendedMode = FSMC_EXTENDED_MODE_DISABLE; hsram1.Init.AsynchronousWait = FSMC_ASYNCHRONOUS_WAIT_DISABLE; hsram1.Init.WriteBurst = FSMC_WRITE_BURST_DISABLE; /* HCLK = 168MHz,1 个 HCLK ≈ 5.95ns */ Timing.AddressSetupTime = 3; /* ≈18ns 地址建立 */ Timing.AddressHoldTime = 1; Timing.DataSetupTime = 9; /* ≈54ns 数据保持 */ Timing.BusTurnAroundDuration = 0; Timing.CLKDivision = 0; Timing.DataLatency = 0; Timing.AccessMode = FSMC_ACCESS_MODE_A; /* SRAM 用模式 A */ HAL_SRAM_Init(&hsram1, &Timing, &Timing); }

参数怎么理解:AddressSetupTime是 NE 拉低之后到 NOE/NWE 有效之间的等待,太短会导致片选还没稳定就开始读写;DataSetupTime是读写信号有效后数据线上的保持时间,这个值直接决定 W5100S 能不能采到数据,是四个参数里最敏感的。判据不用背手册,用一个笨办法:写一段循环,往0x60000000依次写 0x00~0xFF 再读回来比对,跑一万次不出错就算过。

提示:改完时序一定要重新编译并完整复位。FSMC 的配置寄存器写入后不会自动生效,有些调试器下载程序后只做软复位,FSMC 还是上一次的配置,很容易误判成“改了没用”。

3.3 寄存器读写封装与软复位流程

总线上所有访问最后都收拢到四个函数:读 8 位、写 8 位、读 16 位、写 16 位。前两个是直接指针解引用,后两个要特别处理字节序。W5100S 的 16 位寄存器是高字节存在低地址(大端),而 Cortex-M4 是小端,如果直接*(volatile uint16_t*)读,高低字节是反的。这个坑很典型,症状是 IP 地址配成192.168.1.100,读回来变成168.192.100.1。

/* w5100s_reg.h —— 寄存器地址与读写封装 */ #define W5100S_BASE 0x60000000UL #define W5100S_REG(a) (*(volatile uint8_t *)(W5100S_BASE + (a))) /* 通用寄存器 */ #define W5100S_MR 0x0000 #define W5100S_GAR 0x0001 /* 网关,4 字节 */ #define W5100S_SUBR 0x0005 /* 子网掩码,4 字节 */ #define W5100S_SHAR 0x0009 /* MAC,6 字节 */ #define W5100S_SIPR 0x000D /* 本机 IP,4 字节 */ #define W5100S_IR 0x0015 #define W5100S_IMR 0x0016 #define W5100S_RTR 0x0017 /* 重传超时,单位 100us */ #define W5100S_RCR 0x0019 /* 重传次数 */ /* Socket 寄存器:第 n 个 socket 的基址 */ #define SOCK_BASE(n) (0x0400 + (n) * 0x100) #define Sn_MR(n) (SOCK_BASE(n) + 0x0000) #define Sn_CR(n) (SOCK_BASE(n) + 0x0001) #define Sn_IR(n) (SOCK_BASE(n) + 0x0002) #define Sn_SR(n) (SOCK_BASE(n) + 0x0003) #define Sn_PORT(n) (SOCK_BASE(n) + 0x0004) #define Sn_DIPR(n) (SOCK_BASE(n) + 0x000C) #define Sn_DPORT(n) (SOCK_BASE(n) + 0x0010) #define Sn_TX_FSR(n) (SOCK_BASE(n) + 0x0020) #define Sn_TX_RD(n) (SOCK_BASE(n) + 0x0022) #define Sn_TX_WR(n) (SOCK_BASE(n) + 0x0024) #define Sn_RX_RSR(n) (SOCK_BASE(n) + 0x0026) #define Sn_RX_RD(n) (SOCK_BASE(n) + 0x0028) /* 缓冲区:默认每 socket 2KB */ #define TX_BASE(n) (0x4000 + (n) * 0x0800) #define RX_BASE(n) (0x6000 + (n) * 0x0800) static inline void w5100s_w8(uint16_t a, uint8_t v) { W5100S_REG(a) = v; } static inline uint8_t w5100s_r8(uint16_t a) { return W5100S_REG(a); } /* 16 位寄存器:高字节在低地址,必须手工拼 */ static inline void w5100s_w16(uint16_t a, uint16_t v) { W5100S_REG(a) = (uint8_t)(v >> 8); W5100S_REG(a + 1) = (uint8_t)(v & 0xFF); } static inline uint16_t w5100s_r16(uint16_t a) { uint16_t hi = W5100S_REG(a); uint16_t lo = W5100S_REG(a + 1); return (uint16_t)((hi << 8) | lo); }

这几个函数是内联的,展开后就是一条LDRB/STRB,没有函数调用开销。之所以不用 16 位指针直读,除了字节序,还有一个原因是 FSMC 在 8 位总线模式下处理LDRH时行为跟编译器优化等级有关,某些优化下会合并成两次访问,反而更慢且不好调试。手工拼两次 8 位读,代价可控,行为可预期。

复位流程简单但顺序不能错:先写MR的 bit7 置 1 触发软复位,等一段时间,然后轮询MR直到 bit7 自己清零。有些资料写的是固定延时 100ms,我在几块板子上测下来,复位实际在几毫秒内就完成了,轮询比死等快,也不会因为芯片批次差异而失败。

uint8_t w5100s_reset(void) { uint32_t t0; w5100s_w8(W5100S_MR, 0x80); /* RST = 1 */ HAL_Delay(2); t0 = HAL_GetTick(); while (w5100s_r8(W5100S_MR) & 0x80) { if (HAL_GetTick() - t0 > 200) { /* 200ms 超时,说明总线根本没通 */ return 1; } } return 0; }

这个返回值很有用。如果复位超时,说明问题出在总线层,跟协议栈无关,可以直接去查片选和时序;如果复位很快通过但后面 ping 不通,问题才在网络配置层。一个函数把故障范围劈成两半,排查时能省掉很多猜测。

4. 硬件协议栈的 Socket 编程:从初始化到 TCP 收发

总线通了之后,工作方式就变成“往寄存器里写,从缓冲区里搬”。W5100S 把 TCP/IP 的状态机做在硬件里,MCU 需要关心的只有五件事:写网络参数、开 Socket、握手、搬数据、关 Socket。命令通过Sn_CR下发,状态从Sn_SR读回,中间的 SYN/ACK、重传、超时全由芯片处理。

4.1 网络参数配置与 Socket 内存分配

上电第一步是把 MAC、IP、网关、子网掩码写进去。MAC 不能全 0,也不能和网络里已有设备冲突,我一般用芯片出厂唯一 ID 拼一个本地管理地址,避免批量生产时撞车。重传参数RTR和RCR也在这里设,RTR单位是 100 微秒,写 0x07D0 就是 200ms,RCR写 8 表示重传 8 次,两者相乘大约是 TCP 断开检测的时间量级。

void w5100s_set_netinfo(const uint8_t mac[6], const uint8_t ip[4], const uint8_t gw[4], const uint8_t mask[4]) { for (int i = 0; i < 6; i++) w5100s_w8(W5100S_SHAR + i, mac[i]); for (int i = 0; i < 4; i++) w5100s_w8(W5100S_GAR + i, gw[i]); for (int i = 0; i < 4; i++) w5100s_w8(W5100S_SUBR + i, mask[i]); for (int i = 0; i < 4; i++) w5100s_w8(W5100S_SIPR + i, ip[i]); w5100s_w16(W5100S_RTR, 2000); /* 2000 × 100us = 200ms */ w5100s_w8(W5100S_RCR, 8); /* 重传 8 次 */ }

缓冲区分配容易被忽略。W5100S 内部发送、接收各 8KB,默认 4 个 Socket 平分,每个 2KB。如果只跑一个 Socket,可以把 8KB 全部分给它,Sn_TXBUF_SIZE和Sn_RXBUF_SIZE都写 8,随之而来的变化是缓冲区环回掩码要从0x07FF改成0x1FFF。这两处必须同步改,只改容量不改掩码,数据会在写到一半的时候回绕到错误位置,症状是大于 2KB 的报文后半段内容错乱。

分配大小掩码适用场景
2KB(默认)0x07FF4 个 socket 同时用,短报文
4KB0x0FFF2 个 socket,中等吞吐
8KB0x1FFF单 socket 大包传输

4.2 TCP 连接的状态机与数据搬运

以 TCP 客户端为例,流程是:Sn_MR写 0x01 声明 TCP 模式,Sn_PORT写本地端口,Sn_CR写 OPEN,等Sn_SR变成 0x13(INIT)。然后填对端 IP 和端口,Sn_CR写 CONNECT,等状态变成 0x17(ESTABLISHED)。所有命令都是“写进去,芯片执行完后自动清零”,所以后面跟一句while (w5100s_r8(Sn_CR(n)));就是在等它执行完。

#define SOCK_OPEN 0x01 #define SOCK_LISTEN 0x02 #define SOCK_CONNECT 0x04 #define SOCK_DISCON 0x08 #define SOCK_CLOSE 0x10 #define SOCK_SEND 0x20 #define SOCK_RECV 0x40 #define SOCK_CLOSED 0x00 #define SOCK_INIT 0x13 #define SOCK_ESTAB 0x17 #define SOCK_CLOSE_WAIT 0x1C uint8_t tcp_client_open(uint8_t n, uint16_t lport, const uint8_t rip[4], uint16_t rport) { w5100s_w8(Sn_MR(n), 0x01); /* TCP */ w5100s_w16(Sn_PORT(n), lport); w5100s_w8(Sn_CR(n), SOCK_OPEN); while (w5100s_r8(Sn_CR(n))); if (w5100s_r8(Sn_SR(n)) != SOCK_INIT) return 1; for (int i = 0; i < 4; i++) w5100s_w8(Sn_DIPR(n) + i, rip[i]); w5100s_w16(Sn_DPORT(n), rport); w5100s_w8(Sn_CR(n), SOCK_CONNECT); while (w5100s_r8(Sn_CR(n))); return 0; }

CONNECT是异步的,命令发出去就返回,握手在芯片内部进行。所以发完不能立刻发数据,要在一个循环里等Sn_SR变成 ESTABLISHED。等多久合适?我一般给 3 秒,超过就当作对端没开或者网络不通,直接 CLOSE 重来。这个超时不要设太短,因为 SYN 本身就有重传,设 500ms 会在网络稍慢的时候误判。

发送数据分三步:查发送空闲大小、写数据进缓冲区、更新写指针并发 SEND 命令。空闲大小从Sn_TX_FSR读,写指针从Sn_TX_WR读,写入位置是写指针 & 掩码。因为缓冲区是环形的,一次写入可能跨过末尾回到开头,这时候要拆成两段拷贝,不能一次memcpy到底。

uint16_t tcp_send(uint8_t n, const uint8_t *buf, uint16_t len) { uint16_t freesize, ptr, off; uint32_t t0 = HAL_GetTick(); /* 1. 等发送缓冲腾出空间 */ while ((freesize = w5100s_r16(Sn_TX_FSR(n))) < len) { <p> <a href="https://download.csdn.net/download/sgjiajia/13122385" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>
返回列表