首页 / 资讯中心 / 文章详情

STM32F103移植W5500 ioLibrary:从SPI底层到socket通信实现

STM32F103移植W5500 ioLibrary:从SPI底层到socket通信实现 ★ FEATURED ARTICLE
简介针对MINISTM32平台核心为STM32F103RCT6的W5500 ioLibrary移植工程适合需要为STM32快速加入以太网能力的嵌入式开发者。工程覆盖SPI接口配置、底层驱动移植及socket编程示例借助W5500内置的硬核TCP/IP协议栈可像操作标准网络库一样收发数据省去繁琐的协议处理。压缩包共217个文件大小9.17MB以C源码与头文件为主其余为编译中间文件、Keil工程配置、说明文档及辅助脚本还包含启动文件、map映射与列表文件等既适合直接编译运行也方便研究底层链接细节CHM/HTM文档可离线查阅接口说明BAT脚本便于清理编译产物整体结构清晰可直接导入工程参考。目前已有1005人学习下载属于同类移植方案中关注度较高的资源。通过该工程能够获取GPIO初始化、SPI读写、中断处理等关键实现并结合调试配置与文档快速排查时序失配、片选失效等常见问题从而显著缩短W5500的上手与移植周期。1. 把 W5500 跑在 STM32F103 上ioLibrary 移植包到底省了什么很多工程师第一次接触 W5500 都会有个错觉这芯片不就是个 SPI 接口的以太网收发器吗自己写寄存器不就行了真做起来才发现TCP/IP 协议栈、ARP 应答、重传超时、socket 状态机这些活儿全在硬件里但你要是不用官方 ioLibrary光是把寄存器读写封装对、再把 socket 层的缓冲和状态捋顺就够加两周班。这份 MINISTM32 移植 W5500 ioLiberary 的资源其实就是把 STM32F103 的 SPI 底层、中断处理、socket 接口全部接好的现成工程。它能解决的是“从零到 socket 能建连收发”这段最磨人的移植工作适合手里有 MiniSTM32 开发板、想直接跑 TCP/UDP 通信的中级开发者也适合被 W5500 数据手册绕晕的新手照着抄作业。2. 移植前要铺好的底子硬件连接与 ioLibrary 目录结构2.1 ioLibrary 帮你管了哪几层W5500 的官方驱动库叫 ioLibrary里面不是一堆零散的寄存器操作而是分了两层底层是 WIZCHIP 相关的 SPI 读写、片选、复位、临界区保护上层是 socket()、connect()、send()、recv() 这套和 BSD socket 非常像的接口。移植的本质就是把你 MCU 的 SPI 能力“注册”进底层让上层协议代码不用改。这个分包里最关键的文件是 Ethernet/ 目录下的 wizchip_conf.c/.h 和 socket.c/.h前者负责 WIZCHIP 初始化与回调注册后者提供 socket API。你不需要动 socket.c 里的逻辑只需要把 spi_cbfunc、cs_cbfunc、cris_cbfunc 这三个回调函数指到你自己的实现上。/* 注册底层回调ioLibrary 所有上层调用都会走这三个函数 */ reg_wizchip_cris_cbfunc(W5500_CRIS_Enter, W5500_CRIS_Exit); // 临界区保护 reg_wizchip_cs_cbfunc(W5500_CS_Select, W5500_CS_Deselect); // 片选控制 reg_wizchip_spi_cbfunc(W5500_ReadByte, W5500_WriteByte); // SPI 单字节收发逻辑说明reg_wizchip_cris_cbfunc 是注册临界区进入/退出函数通常用来在片选拉低期间关闭全局中断防止时序被别的任务打断reg_wizchip_cs_cbfunc 注册片选拉低拉高函数reg_wizchip_spi_cbfunc 注册 SPI 字节收发函数。这三个回调一接ioLibrary 内部所有寄存器读写都会经由你的 MCU 底层代码完成。参数说明W5500_CRIS_Enter 和 W5500_CRIS_Exit 分别对应 __disable_irq() 和 __enable_irq() 这类操作片选函数里用 GPIO 拉低/拉高 CS 引脚SPI 收发函数则是和硬件 SPI 外设打交道。这几个函数名可以在你工程里任意改关键是形参类型和返回值要匹配。2.2 硬件连接SPI 引脚、复位、中断一个都不能少MiniSTM32 开发板上 W5500 模块一般接在 SPI1 上我建议你先把引脚关系写死在头文件里。以常见接法为例SCK 接 PA5、MISO 接 PA6、MOSI 接 PA7、CS 接 PA4、RST 接 PA3、INT 接 PA2。SCK/MOSI 是 STM32 输出MISO 是输入CS、RST 是普通推挽输出INT 是 W5500 输出给 MCU 的输入引脚。这里最容易想当然的一点很多人觉得 CS 只是用的时候拉低就行忽略了 INT 引脚和 RST 引脚在初始化顺序里的作用。RST 引脚必须在 SPI 初始化之后拉低再拉高而且高电平要保持足够时间等内部 PHY 起来否则后面读寄存器全是 0xFF。INT 引脚则用于收到数据或连接状态变化时通知 MCU轮询模式下可以不接但接了能大幅降低 CPU 占用。void W5500_GPIO_Config(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); /* SCK - PA5, MOSI - PA7 推挽复用输出 */ gpio.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); /* MISO - PA6 浮空输入 */ gpio.GPIO_Pin GPIO_Pin_6; gpio.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, gpio); /* CS - PA4, RST - PA3, INT - PA2 设置为通用推挽或输入 */ gpio.GPIO_Pin GPIO_Pin_4 | GPIO_Pin_3; gpio.GPIO_Mode GPIO_Mode_Out_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); GPIO_SetBits(GPIOA, GPIO_Pin_4 | GPIO_Pin_3); gpio.GPIO_Pin GPIO_Pin_2; gpio.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, gpio); }逻辑说明这段代码先把 SPI1 的 SCK/MOSI/MISO 引脚配好再单独把 CS、RST 配成推挽输出并默认拉高电平。INT 引脚配成输入但不一定立刻用先留着。CS 默认高电平是为了避免 W5500 在上电瞬间被误选中。参数说明GPIO_Speed_50MHz 对 SPI 时钟是足够的PA2 的输入模式可以后续换成外部中断模式。不同开发板的引脚映射可能不同但原则一致凡是接 W5500 控制脚的输出配推挽输入配浮空或上拉。2.3 目录结构里哪些文件要改、哪些不用动拿到这个移植包第一件事不是打开 Keil 工程而是先看 Ethernet/ 目录。wizchip_conf.c 负责 WIZCHIP 初始化你大概率要动里面的端口号和 SPI 回调注册部分socket.c 全程不需要改它是纯软件逻辑。Internet/ 目录下有 DNS、DHCP 客户端实现这两个文件各自带着针对 W5500 的移植接口如果你只用静态 IP甚至可以不管它们。我习惯把整个包分成三层底层硬件驱动SPI、GPIO、定时器自己写在 application 层中间层是 WIZCHIP 芯片相关文件稍微调整回调注册最上层 socket API 直接 include 头文件调用。这样后面换 MCU 型号时只需要重写底层驱动socket 业务代码一行都不用动。3. 把底层 SPI 注册进 ioLibrary寄存器读写与字节收发3.1 W5500 的 SPI 帧格式W5500 的 SPI 通信和普通 SPI 从机不一样每一帧固定是三个段地址段 16 位、控制段 8 位、数据段 8 位或连续多字节。控制段里决定是读还是写、是寄存器还是 TX/RX 缓冲区。ioLibrary 已经把帧格式封装好了但你得把最底层的单字节收发函数写对。读寄存器的完整过程是CS 拉低先发 16 位地址的高字节、低字节再发控制字节bit2 为 0 表示读随后读回数据字节最后 CS 拉高。写寄存器则是在控制字节里把 bit2 置 1后面跟要写入的数据。整个过程必须在 CS 低电平期间一气呵成中间不能被打断。uint8_t W5500_ReadReg(uint16_t addr) { uint8_t frame[3]; uint8_t val; frame[0] (uint8_t)(addr 8); // 地址高字节 frame[1] (uint8_t)(addr 0xFF); // 地址低字节 frame[2] 0x00; // 读操作控制字节bit20 W5500_CS_Select(); W5500_WriteByte(frame[0]); W5500_WriteByte(frame[1]); W5500_WriteByte(frame[2]); val W5500_ReadByte(); // 读回数据 W5500_CS_Deselect(); return val; }逻辑说明W5500_CS_Select 把 PA4 拉低之后三个 WriteByte 依次发送地址和控制字节再用 ReadByte 取回数据最后拉高片选。这个函数就是 ioLibrary 读寄存器的最终落地实现很多移植问题都出在这个函数上。参数说明addr 是 16 位寄存器地址比如版本寄存器地址是 0x0039。控制字节的 bit2 决定读写方向bit0/bit1 决定访问类型0x00 就是读通用寄存器。如果你要连续读 RX 缓冲区控制字节里还要带上递增模式。3.2 写寄存器与收发缓冲区的底层实现写寄存器的逻辑和读对称差别只在控制字节的 bit2 置 1。但收发缓冲区读写需要注意W5500 的 TX/RX 缓冲区地址是从 0x0000 到 0x7FFF 映射的socket 0 到 socket 7 的缓冲区分段由 TMSR/RMSR 寄存器决定。ioLibrary 在 wizchip_init 的时候会帮你算好偏移底层读写函数只需要按帧格式把地址发出去然后连续读写 N 个字节。void W5500_WriteReg(uint16_t addr, uint8_t data) { frame[0] (uint8_t)(addr 8); frame[1] (uint8_t)(addr 0xFF); frame[2] 0x04; // 写操作控制字节bit21 W5500_CS_Select(); W5500_WriteByte(frame[0]); W5500_WriteByte(frame[1]); W5500_WriteByte(frame[2]); W5500_WriteByte(data); W5500_CS_Deselect(); }逻辑说明控制字节 0x04 的二进制约 0b00000100bit2 为 1 表示写。地址段完成后直接跟数据字节不需要额外的写等待。参数说明addr 同样是 16 位寄存器地址。这里的 data 只是单字节如果要写缓冲区可以在同一个 CS 低电平周期内连续调用多次 W5500_WriteByte这比每次重发地址高效得多。3.3 用版本寄存器验证移植底子底层函数写完第一件事不是急着调 socket而是读版本寄存器 VERSIONR地址 0x0039。W5500 的这个寄存器固定返回 0x04如果你能稳定读到 0x04说明 SPI 时序、片选控制、寄存器地址封装全都对了。这一步过了再往上走 socket 层基本不会出底层问题。void W5500_Check(void) { uint8_t version; version W5500_ReadReg(0x0039); if (version 0x04) { /* 版本校验通过硬件连接没问题 */ } else { /* 版本异常检查 SPI 引脚和时序 */ } }我一般会在初始化后循环读三次版本寄存器三次都是 0x04 才往下走。如果第一次就对但后面两次错基本可以断定是 SPI 时钟太快或者 CS 时序有隐患。4. 让 socket 接口跑起来模式选择、缓冲区划分与超时参数4.1 socket 模式的选型与 Sn_MR 参数ioLibrary 的 socket() 函数签名是 socket(sn, protocol, port, flag)protocol 用的就是 Sn_MR_TCP、Sn_MR_UDP 这些宏。TCP 模式下 W5500 硬件会自己处理三次握手和重传你在 MCU 里只需要发起 connect 或者 listen之后用 send/recv 收发。UDP 模式则更简单sn 分配好后直接 sendto/recvfrom。端口参数是本地端口号flag 在 TCP 客户端模式下一般填 0在 UDP 下可以用 Sn_MR_MULTICAST 开启组播。要注意的是同一时间一个 socket 只能一种模式总共 8 个 socket 通道但通道 0 到 7 不是都默认可用得看缓冲区配置。int32_t sock; /* 创建 TCP socket使用通道 0本地端口 5000 */ sock socket(0, Sn_MR_TCP, 5000, 0); if (sock 0) { /* socket 创建失败多半是缓冲区没配或通道被占用 */ }逻辑说明socket() 返回值小于 0 就是失败最常见原因是该通道的 TX/RX 缓冲区大小配成了 0或者 W5500 还没初始化成功。成功返回非负值后这个值就是后续 connect/send/recv 都要用的 socket 描述符。参数说明第一个参数 0 是 socket 通道号范围 0-7Sn_MR_TCP 是 TCP 模式宏5000 是本地端口最后一个 flag 是选项标志位。4.2 TX/RX 缓冲区划分别把 16KB 全给一个口W5500 内部有 16KB 发送缓冲和 16KB 接收缓冲这 32KB 要在 8 个 socket 之间分配。ioLibrary 里通过 setTMSR/setRMSR 设置每个 socket 最少分配 1KB最多可以独占全部。默认配置下如果你调用 wizchip_init() 时不传缓冲区数组库会平均分配也就是每个 socket 2KB 发送、2KB 接收。这在大多数场景够用但如果你的应用要收发大于 2KB 的包就得手动调。uint8_t txsize[8] {2, 2, 2, 2, 2, 2, 2, 2}; // 每 socket 2KB uint8_t rxsize[8] {2, 2, 2, 2, 2, 2, 2, 2}; if (wizchip_init(txsize, rxsize) 0) { /* 缓冲区配置成功 */ }逻辑说明wizchip_init 的入参是两个数组分别表示每个 socket 的发送/接收缓冲区大小单位是 KB。数组下标对应 socket 通道号。传 NULL 时库使用默认平均分配。参数说明如果你只用一个 socket 且需要大包收发可以把其他通道的缓冲都设为 0把资源集中到 socket 0。比如 txsize[0]16、rxsize[0]16、其余全 0这样单通道能收发接近 16KB 的 IP 包。但注意TCP 分段还是受 MTU 影响不是缓冲区越大单包就能发越大。4.3 超时与重传参数RTR 和 RCR 的合理值W5500 的 TCP 重传由硬件管理两个关键寄存器是 RTR重传超时和 RCR重传次数。RTR 默认值 2000单位是 100us也就是 200msRCR 默认 8 次。这两个值偏保守适合公网环境但在局域网里会导致断线检测非常慢。setRTR(250); // 重传超时 25ms setRCR(5); // 重传 5 次后放弃逻辑说明setRTR/setRCR 是 ioLibrary 提供的直接寄存器配置函数。RTR 设成 250 表示 25msRCR 设成 5 表示重传 5 次。这样一旦对端不可达大约 100ms 多就能判定连接失败而不是默认的 1 秒以上。参数说明这两个值不是越小越好。在 Wi-Fi 或有线丢包率较高的环境重传太激进反而容易误判。一般局域网用 RTR250、RCR5 起步公网建议保留默认或只把 RCR 降到 6。5. 避坑移植 W5500 ioLibrary 最容易翻车的五个问题5.1 复位后读版本寄存器全是 0xFF现象初始化顺序完全按例程写但 W5500_Check() 里读 VERSIONR 死活读不到 0x04读回来全是 0xFF。原因RST 引脚拉低时间不够或者拉高后没有等内部 PHY 稳定就开始 SPI 通信。W5500 上电或复位后需要一段时间准备手册上给的时间比较宽实际工程里至少留 100ms。解决拉低 RST 至少 1ms拉高后再延时 200ms然后再读版本寄存器。我一般还会在复位函数末尾加一个 for 循环连续读到 0x04 才返回否则重试三次。5.2 SPI 时钟太快导致数据偶发错位现象W5500_Check() 第一次读 0x04 正确第二次第三次偶尔变成 0x00或者 socket 收发时好时坏。原因STM32F103 的 SPI1 最高可以到 36MHz但 W5500 在实际走线上往往跑不了这么高尤其是模块和开发板之间用了杜邦线。信号反射导致建立时间不够。解决把 SPI 波特率预分频调低先用 SPI_BaudRatePrescaler_8约 9MHz 72MHz 主频跑通再逐步提速。注意代码里 SPI 初始化后最好加一个 10ms 延时再开始读寄存器。5.3 CS 片选低电平期间被中断打断现象网络功能单独跑没问题一开定时器中断或串口中断收发就出现寄存器错乱、socket 状态异常。原因CS 拉低到拉高之间的整个 SPI 帧过程被中断抢占了中断服务函数里如果也碰了 SPI就会把 W5500 的时序搅乱。即使中断里不碰 SPI过长的中断延迟也会让 W5500 侧超时。解决在 CS 拉低前进入临界区CS 拉高后再退出。ioLibrary 提供 reg_wizchip_cris_cbfunc 就是干这个的Enter 里关中断、Exit 里开中断。注意若中断里也要访问 W5500必须单独处理不要在中断里调用 socket API。5.4 socket() 返回 -1 但硬件初始化是对的现象VERSIONR 能读到 0x04wizchip_init() 也返回 0但 socket(0, Sn_MR_TCP, 5000, 0) 就是返回 -1。原因socket 通道的缓冲区大小配成了 0。如果你自己写了 txsize/rxsize 数组并且把某些通道设为 0而 socket() 调用恰好用了那个通道就会失败。还有一种情况是上一个连接没正常关闭该通道还处于 SOCK_CLOSED 之外的状态。解决调用 socket() 前先确认对应通道的缓冲配置非 0用 getSn_SR(sn) 查状态。如果卡在 SOCK_CLOSE_WAIT 这类状态先 close() 再重新 socket()。5.5 TCP 断线后重连慢甚至迟迟发现不了现象把网线拔了服务器端好几分钟后才感知到 TCP 连接断开重新 connect 也一直失败。原因W5500 的 TCP 重传超时默认太长加上 RCR 次数多硬件层面要等很久才放弃。另外 keep-alive 机制默认关闭探测包也不发。解决调小 RTR/RCR 参数同时定期用 getSn_SR() 轮询 socket 状态发现不是 SOCK_ESTABLISHED 就主动 close 再 reconnect。如果业务允许可以让服务器端先发心跳包MCU 端收不到心跳就关闭重连。6. 上电先跑回环把底层验证写成习惯移植完成后别急着接业务协议先做一次最朴素的 TCP 回环测试。MCU 端开一个 TCP server电脑上用网络调试助手连上来发多少字节回多少字节。int32_t server_sock, client_sock; uint8_t rxbuf[1460]; int32_t rxlen; server_sock socket(0, Sn_MR_TCP, 8080, 0); listen(server_sock, 0); while (1) { if (getSn_SR(server_sock) SOCK_ESTABLISHED) { client_sock server_sock; rxlen recv(client_sock, rxbuf, sizeof(rxbuf)); if (rxlen 0) { send(client_sock, rxbuf, rxlen); } } }逻辑说明socket 创建并 listen 后W5500 硬件会在收到对端连接请求时自动完成三次握手socket 状态变为 SOCK_ESTABLISHED。此时调用 recv 收数据再原样发回去。这个回环逻辑验证了 TCP 建连、接收、发送整个链路。参数说明rxbuf 大小 1460 是常见的 TCP 最大载荷段你可以改成 2048 测试但记得 socket 0 的 RX 缓冲区要大于这个值否则收不完整。回环通了之后我还会做两个额外验证一是连续收发 10000 包不丢统计重传率二是拔网线再插回观察重连耗时。这些测试能暴露缓冲区分配、超时参数设置是否合适。从那以后我每次移植 W5500 都强制先跑一遍回环测试再上业务代码这个习惯帮我省掉了至少三次定位协议的麻烦。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站