STM32+W5100S以太网例程详解:从SPI移植到TCP通信实战
简介面向STM32嵌入式开发者的W5100S网络接口参考例程包聚焦MCU通过SPI总线驱动W5100S、并借助其硬件TCP/IP协议栈实现网络通信的常见开发需求。压缩包共1892个文件约7.74MB核心源码包括786个H头文件与745个C源文件用于驱动移植、协议处理和业务示例同时提供汇编文件、INI配置、HEX/BIN固件、BAT脚本和Keil工程文件便于直接导入Keil uVision或IAR环境编译烧录。例程覆盖DHCP、ARP、UDP、TCP Client、HTTP Server、PING、NTP、WebSocket等典型网络应用场景细化到SPI时钟与引脚复用、MAC地址和IP/子网掩码/网关配置、Socket套接字建立与数据收发、超时与错误状态寄存器读取、固件升级流程等关键环节可帮助开发者减少重复造轮子快速完成网络功能移植和二次开发。已有785人学习浏览适合正在做嵌入式联网产品、需要把W5100S接入STM32项目的软硬件工程师参考。1. 为什么STM32W5100S的参考例程值得重新翻一遍给STM32加以太网很多工程师第一反应是“挂一颗PHY然后移植lwip”。如果你的芯片本身带MAC这条路没问题但当你手上只有一颗没有以太网MAC的普通MCU又要在一个周五前把板子上网时W5100S这种内置全硬件TCP/IP协议栈的芯片就是更快的选择。W5100S把ARP、IP、ICMP、TCP、UDP的状态机全部放到芯片内部STM32只负责通过SPI读写数据包应用代码不必关心重传、分片和定时器。官方配套的W5100S参考例程恰好把这条链路上的所有底层细节整理成了一组可调用的socket接口。参考例程不是万能模板但它把寄存器操作、SPI回调、socket收发流程串了起来适合那些想跳过lwip移植、直接进入业务协议的场景。这篇文章我按自己在实际项目里的做法把W5100S参考例程从硬件接口、最小初始化、TCP收发一直到排错和生产化改造拆开讲尽量做到照着做就能ping通。2. W5100S与STM32的接口先立住硬件设计再谈例程W5100S同时支持直接总线接口和SPI接口参考例程默认走的是SPI。STM32这边只需要6根线就能完成物理连接SCLK、MOSI、MISO、SCS、RSTn、INTn。但接线简单不代表可以直接抄W5100S的IO是3.3V电平虽然大部分STM32也是3.3V可如果你用的是带5V容忍引脚的型号或者板上存在5V供电的外设就必须确认电平转换是否到位。W5100S手册写得很清楚它的输入引脚不按5V容忍设计直接接5V TTL电平会缩短寿命甚至烧引脚。参考例程里通常默认使用“SPI模式0”即CPOL0、CPHA0。这个模式在很多STM32 SPI外设上是比较容易配置的但纯软件模拟SPI也可以跑只是速度会掉到1Mbps以下。实际项目里我用过硬件SPI和GPIO模拟两种方式硬件的吞吐量明显更高而且能配合DMA减小CPU占用。W5100S支持的最高SPI时钟取决于走线和PCB质量保守起见我会先在CubeMX里把分频系数调大使时钟落在10MHz左右等通信稳定后再逐步提频。2.1 SPI接线与电压匹配W5100S不支持5V容忍典型连接方式可以参考下表假设STM32使用SPI1片选和复位用普通GPIO控制W5100S引脚STM32引脚方向STM32引脚示例说明SCLK输出PA5 / SPI1_SCKSPI时钟模式0下空闲为低MOSI输出PA7 / SPI1_MOSISTM32发给W5100SMISO输入PA6 / SPI1_MISOW5100S返回数据SCS输出PB0片选低电平有效软件控制RSTn输出PB1复位低电平有效复位后拉高INTn输入PB10中断输出低电平有效可接EXTI参考例程里SCS和RSTn基本都是GPIO口这样做有个好处SPI外设可以全程使用软件NSS避免硬件自动片选造成时序上的不确定性。RSTn上电后至少要拉低500ns再释放W5100S内部复位完成后才能接受SPI操作INTn如果不使用可以悬空或者上拉到3.3V但建议还是接一个STM32中断引脚方便后面做socket事件触发。2.2 用STM32CubeMX把SPI引脚和复位脚配好在STM32CubeMX中配置SPI1时我一般会把参数设为hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_8; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB;这段代码对应SPI模式0即时钟空闲为低、数据在第一个边沿采样和W5100S的默认要求一致。使用软件NSS意味着SPI外设不会自动拉低NSS引脚片选由GPIO控制这样在连续读写W5100S寄存器时可以保持片选持续拉低避免中间被硬件中断操作打断。BaudRatePrescaler的值需要根据APB频率折算假如APB1或APB2是80MHz8分频就是10MHz这个起点是安全的。复位引脚和中断引脚不需要特殊复用功能直接在CubeMX里配置为GPIO输出和输入就好。初始化顺序上先复位W5100S再配置SPI模式最后才执行例程里的wizchip_init。如果你先初始化SPI再去拉复位引脚W5100S内部逻辑刚复位完SPI配置早已就绪通信会更稳定。2.3 参考例程对硬件配置的默认假设参考例程里的wizchip_spi_readbyte和wizchip_spi_writebyte是桥接代码它默认你会提供两个函数指针一个读8位一个写8位。因此SPI底层用HAL库还是标准库参考例程不关心。它只关心你这两个函数会不会在片选拉低的情况下正确完成读写。这里有一个常见误会W5100S的SPI读操作不是先写后读而是通过一个周期性的发送指令来完成。参考例程中的wizchip_spi_readbyte实际会先调用WIZCHIP_READ由芯片内部根据当前寄存器地址自动返回数据。这意味着应用层不能随意在读写之间拉高片选否则地址指针会丢失。参考例程对片选的有效时间有明确约定移植时务必把SCS当作SPI事务的一部分来处理。3. 跑通W5100S例程的最小代码骨架从SPI读写到ping通拿到参考例程后最容易直接照搬的是库文件w5100s.c和socket.c但真正需要你动手写的是底层IO注册和初始化顺序。W5100S的库通过函数指针把SPI操作与芯片驱动解耦注册顺序必须严格先注册片选、复位、临界区保护函数再注册SPI读写函数最后初始化芯片。很多例程跑不起来不是库有问题而是注册顺序反了导致芯片在初始化时调用了空的回调。我习惯把这段代码放在一个独立的w5100s_board.c里方便不同STM32板卡共用逻辑。注册代码看起来像这样void w5100s_board_init(void) { // 先注册临界区影响SPI读写是否会被中断打扰 reg_wizchip_cris_cbfunc(w5100s_cris_enter, w5100s_cris_exit); // 再注册片选控制 reg_wizchip_cs_cbfunc(w5100s_select, w5100s_deselect); // 最后注册SPI字节读写 reg_wizchip_spi_cbfunc(w5100s_spi_readbyte, w5100s_spi_writebyte); }每个注册函数都有明确作用。临界区函数用来在SPI读写时关闭可能抢占的中断如果你没有Freertos或者中断里不碰SPI可以用空函数代替片选函数控制SCS引脚电平要保证高电平期间W5100S不响应SPI读写函数直接调用HAL库发送和接收。3.1 W5100S初始化缓冲区分发与网络信息写入wizchip_init是参考例程里最关键的初始化入口。这个函数不仅要配置芯片工作模式还要告诉芯片每个socket各分配多少收发包缓冲。以下代码来自我自己的板级适配uint8_t tx_buf[2][2048]; // 给0号和1号socket的发送缓冲 uint8_t rx_buf[2][2048]; // 给0号和1号socket的接收缓冲 wizchip_init(tx_buf, rx_buf); wiz_NetInfo netinfo { .mac {0x00, 0x08, 0xdc, 0x00, 0x01, 0x23}, .ip {192, 168, 1, 100}, .sn {255, 255, 255, 0}, .gw {192, 168, 1, 1}, .dns {8, 8, 8, 8} }; wizchip_setnetinfo(netinfo);缓冲区数组二维的含义是每个socket各占一块连续RAM。W5100S内部总共有32KB收发缓冲你在数组里的分配不一定要和芯片内部完全对应wizchip_init会按照数组大小自动配置每个socket的TX_RX寄存器。注意这里每个socket都有自己的收发缓冲如果只配一个sock那另外7个socket无法使用。示例中只分配了2个socket也就是2KB发送加2KB接收数量不大适合先把例程跑起来。设置网络信息后最好把IP、网关、掩码打印出来确认。wizchip_setnetinfo会在库内部把参数写入对应寄存器这一步如果漏掉ping不通时第一反应会是SPI问题实际上已经是网络信息配置问题了。3.2 用ping验证读版本寄存器和socket状态ping通之前先确认W5100S已经响应SPI。读版本寄存器是最快的验证方式。W5100S的版本寄存器地址在头文件里有定义可以通过库函数ctlreg_read读取uint8_t ver; if (ctlreg_read(VER, ver) OK) { // W5100S的版本寄存器一般不为0读到0xFF说明MISO这根线有问题 }如果读到0xFF通常就是MISO没接好或SPI模式不匹配。能读到正常版本号后再配好IP然后从PC发起ping。注意ping使用的是ICMP协议芯片内部会自动回复不需要STM32写回调所以ping通只代表初始化正确不代表应用层socket代码正常工作。下面这张表是初始化后需要检查的节点检查项方法异常表现SPI通信是否正常读版本寄存器读到0xFF或随机值网络信息是否写入读取IP寄存器与配置不一致网线是否连接读取PHY链路状态LINK位为0socket是否可分配读取Sn_SR寄存器状态不为SOCK_CLOSED多数例程把上述检查写成断言放到初始化末尾如果环境不满足就进入错误循环。建议保留这个行为避免后续在上层协议里排查半天才发现底层没通。3.3 从参考例程里拿走的超时策略参考例程中的wizchip_init和wizchip_setnetinfo默认不阻塞但你调用socket、listen、connect时内核会等待一段时间返回。W5100S的socket超时由寄存器控制参考例程中设置的是定时器超时值单位是100us。没做过网络编程的STMM32开发者容易忽略connect返回不是成功就是失败不会永远卡在那里但如果把time-out寄存器配成0某些固件版本可能产生长时间等待。我一般会显式调用setSn_RTR和setSn_RCR设置重传时间和重试次数避免应用层卡死。4. 参考例程里的TCP Server与TCP Client怎么改成自己的协议参考例程真正的价值在socket层。它把W5100S的寄存器操作封装成接近BSD socket的API比如socket、listen、accept、recv、send。这些函数在socket.c里实现底层调用wizchip_write把数据写入芯片内部缓冲。你不需要关心TCP状态机但要搞清楚每个返回值代表什么。在把例程改成自己的协议前先看一张socket状态与API调用的对照表函数调用触发状态转换说明socket()SOCK_CLOSED - SOCK_INIT指定协议类型和端口listen()SOCK_INIT - SOCK_LISTEN服务器等待连接connect()SOCK_INIT - SOCK_SYNSENT客户端发起连接通信中SOCK_ESTABLISHED连接建立可读写close()SOCK_ESTABLISHED - SOCK_CLOSED关闭连接这个表虽然简单但排错时非常实用。如果你调用recv时socket状态还停在SOCK_LISTEN那就说明连接根本没有建立成功此时接收长度寄存器自然为0。4.1 TCP Server例程监听一个端口并回显参考例程提供了一个标准TCP Server模型初始化sn后进入循环不断检查socket状态是否变成SOCK_ESTABLISHED是则读取接收数据处理完再发送。我摘取核心循环uint8_t sn 0; uint8_t buf[1460]; socket(sn, Sn_MR_TCP, 5000, 0); listen(sn); while (1) { if (getSn_SR(sn) SOCK_ESTABLISHED) { int len getSn_RX_RSR(sn); if (len 0) { len recv(sn, buf, len); // 把收到的数据回显给客户端 send(sn, buf, len); } } }socket()的第三个参数是本地端口第四个参数是socket标志这里0表示使用默认安全策略。getSn_RX_RSR返回接收缓冲中可读字节数这个值不是接收一次就清零而是读取后由recv从芯片缓存取走。这里要注意recv会开启自动回绕模式一次最多读取缓冲长度如果你需要读取完整大数据包确认循环里不会因为单次长度限制丢数据。这个循环是参考例程中最基础的server写法但它在持续空闲时会消耗较多CPU因为它每次都去查寄存器。更合理的做法是把INTn引脚接到STM32外部中断当W5100S收到数据时拉低INTn由中断进入socket状态检查。参考例程里没有直接给出基于中断的完整例程但底层的中断标志寄存器已经有对应接口。4.2 TCP Client例程连接、发送与断线重连TCP Client在W5100S上比Server多一点麻烦connect是异步过程不能假设调用后立即进入SOCK_ESTABLISHED。参考例程中的client接入服务器后如果服务器中途断电或者网络断开socket状态可能变成SOCK_FIN_WAIT或SOCK_CLOSE_WAIT。如果应用层还在不停send数据会一直堆积在芯片buffer里。我一般在client循环里这样处理uint8_t sn 0; uint8_t remote_ip[4] {192, 168, 1, 50}; uint16_t remote_port 8080; socket(sn, Sn_MR_TCP, 0, 0); connect(sn, remote_ip, remote_port); while (1) { uint8_t status getSn_SR(sn); if (status SOCK_ESTABLISHED) { send(sn, (uint8_t *)hello, 5); } else if (status SOCK_CLOSED) { // 连接已断开重新初始化socket再connect close(sn); socket(sn, Sn_MR_TCP, 0, 0); connect(sn, remote_ip, remote_port); } HAL_Delay(1000); }这里用getSn_SR轮询状态比每次盲目调用send更安全。connect返回后socket如果没有立即变成SOCK_ESTABLISHED可能还在SOCK_SYNSENT此时重发SYN由芯片重传定时器控制不需要应用层干预。判定连接失败要看一段超时时间后是否回到SOCK_CLOSED参考例程的重传计数寄存器会在这个过程结束后自动释放socket。如果你的应用要求客户端主动发起连接但服务器可能不在线那么connect调用前要先设置重传时间和重试次数。重传时间Sn_RTR的单位是100us重试次数Sn_RCR为0表示无限重试这会阻塞后续socket操作生产代码建议设成3次左右。4.3 多socket并发你的例程会做多少事情参考例程最常见的问题是只用一个socket做server时没有问题但一旦设备既要上传传感器数据又要接收远程配置最好使用两个socket。W5100S支持最多8个socket每个socket独立分配端口和协议类型。使用多个socket时要注意wizchip_init里分配的缓冲数组必须和socket数量对应。如果你在例程里看到tx_buf[8][2048]那就意味着8个socket都能同时工作。多socket编码模型可以这样理解每个socket都有自己的状态机W5100S内部为它们调度。你可以在一个循环里依次查询每个socket的状态也可以给每个socket分配不同端口。参考例程的interrupt里有一个Sn_IMR寄存器它控制哪些socket事件会触发INTn引脚这比轮询8个状态位省电得多。5. 排W5100S例程的错SPI乱码、socket卡死和网线状态检测W5100S例程移植过程中问题通常集中在三个层面SPI不通信、网络连不上、连接不稳定。这几个问题看似独立实际经常互相纠缠。比如SPI时序边缘不对时寄存器和socket状态看起来“好像”都正常但ping会隔三岔五丢包。下面按排查顺序展开。5.1 SPI读写时序的三个检查点先检查SCLK空闲电平和采样沿。参考例程默认模式0也就是SCLK空闲为低、上升沿采样。如果STM32的SPI配置成了模式1几乎所有寄存器写入都会错位。用示波器抓SCLK和MOSI确认片选拉低后SCLK从低电平开始跳变。第二个检查点是片选引脚下拉和SCLK启动之间的间隔。W5100S要求SCS拉低后至少等待一小段时间再发起SCLK这个时间参数在数据手册的SPI时序图中。用GPIO控制片选时如果软件在函数里先拉低SCS立刻调用HAL_SPI_TransmitReceive中间插入的软件开销一般足够但如果你开了一个很大的编译优化有可能被优化掉稳妥做法是在片选和SPI传输之间加一个空操作。第三个检查点是SPI收发同时进行。W5100S的读操作需要STM32同时发送一个字节来提供时钟。用HAL库的HAL_SPI_Receive时它其实也会自动发送0xFF这个没问题但如果你自己用中断方式分两次执行发送和接收第二次接收已经丢失了前导字节读到的数据就会整体偏移一位。参考例程里的函数最终都落到“同时读写一个字节”这是最重要的。5.2 socket停在SYN/CLOSE_WAIT时读哪些寄存器socket状态卡死是最难排查的。一个常见场景是TCP客户端拔掉网线过很久才插回来socket状态已经变成SOCK_FIN_WAIT但上层没有感知到。此时单靠getSn_SR检查状态不一定够因为TCP的断开过程由芯片处理你看到的可能是SOCK_CLOSE_WAIT或者SOCK_TIME_WAIT。对付这种情况我通常会组合使用两个寄存器Sn_SR判断当前状态Sn_CR触发关闭命令。参考例程里close(sn)会强制关闭socket即使连接还在TIME_WAIT。但这时W5100S会进入一段等待时间如果你立刻再次socket可能得到一个SOCKERR_SOCKERR。正确的等待方式是在循环里读取getSn_SR(sn)直到它变成SOCK_CLOSED再重新建立socket。如果长时间停在SOCK_TIME_WAIT就要检查Sn_LINGER寄存器的设置它控制关闭后保持TIME_WAIT的时间。5.3 用PHY链路状态判断网线是否插好参考例程初始之后没有强制要求检查链路状态但实际项目中网线没插好时芯片依然能完成SPI初始化甚至能收到来自PC的ARP请求但那只是物理层噪声。W5100S提供PHY状态寄存器可以直接读取链路是否正常。我一般在主循环里定时检查uint8_t link 0; if (ctlreg_read(PHYSR, link) OK) { if (link LINK_ON_MASK) { // 网线已连接可以继续socket操作 } else { // 网线断开主动关闭现有socket close(sn); } }LINK_ON_MASK的值根据W5100S设备头文件定义官方例程中会写清楚。这个寄存器不需要频繁读1秒一次足够。检测到网线断开后应该主动关闭所有处于SOCK_ESTABLISHED状态的socket避免重新插线后状态错乱。参考例程没有强制做这一步但这是把它从demo变产品的重要分水岭。6. 把官方W5100S例程改成生产级代码的收尾技巧例程能ping通、能收发TCP数据只是第一步。要放到产品里长期运行还需要处理三件事SPI传输占用CPU太高、网络参数如何保存、以及异常后如何自恢复。6.1 SPI发送改成DMA并保留片选时序参考例程最慢的部分是每一个字节都通过HAL_SPI_TransmitReceive阻塞发送一次1460字节的收包会占死CPU。改成DMA其实不难但有一个关键点DMA传输结束前不能拉高片选。我用HAL库时会在HAL_SPI_TxRxCpltCallback里拉高SCS并且把片选操作放到回调中这样不会切断最后一个字节的传输。如果使用裸机DMA还要考虑SPI总线速度是否超过DMA能维持的连续请求速率否则会在数据中间插入间隙。6.2 多socket的buffer分配与内存管理很多例程为了省事把8个socket全部配上2KB发送、2KB接收结果单socket的最大吞吐被限制在2KB。W5100S内部总缓冲32KB分配策略应该是“常用socket多给休眠socket少给”。比如0号socket做TCP下发给8KB发送、4KB接收1号socket做UDP状态上报给1KB发送、1KB接收。分配时注意wizchip_init的tx_buf和rx_buf数组维度必须和socket数量一致否则后续socket访问会越界。网络参数不能写死在代码里产品生产时每台设备MAC和IP都要独立。我在参考例程基础上加了一个wizchip_config_load函数从EEPROM读出wiz_NetInfo结构体如果EEPROM中标志错误才使用默认配置。这样调试时改一个IP不用重新烧固件。6.3 最后的自恢复技巧给W5100S加一个“无通信超时看门狗”如果上层应用超过N秒没有收到来自任意socket的数据就主动执行wizchip_reset重新初始化芯片并重新注册socket。这个操作参考例程里没有但W5100S和STM32之间是纯SPI接口不会出现像USB枚举那样的副作用所以软复位代价非常低。实际实现时把复位函数放进定时器回调记录当前时间戳即可。如果你的项目遇到丢包率过高或者吞吐量上不去不要先怀疑W5100S性能先确认片选低电平期间是否被中断插入导致SPI时钟停顿。保持SPI整个事务不被打断通常比提高SPI时钟更能稳定吞吐。把临界区函数真正关掉中断而不是只关掉一个低优先级中断这几个字节的时间开销远比一次重传小。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →