1. 从一颗芯片说起:为什么W5500在嵌入式Linux里值得折腾
第一次接触W5500的人,多半是被它的"硬核"吸引——这是一颗把TCP/IP协议栈直接塞进硅片的以太网控制器,SPI接口,硬件实现10/100M以太网,支持8个独立Socket同时工作。换句话说,主控MCU或MPU只需要通过SPI读写寄存器,就能收发网络数据,协议栈的握手、重传、分片这些琐事全部由芯片自己扛。
这个特性在MCU裸机场景下早就被玩烂了,官方ioLibrary加上一堆移植教程,半天就能跑通。但一旦把场景换到Linux平台,事情就变得微妙起来。Linux内核自带一套完整的网络协议栈,从socket层到驱动层一应俱全,你完全可以用一颗几块钱的PHY芯片配合MAC控制器搞定以太网。那为什么还要在Linux上挂一颗W5500?
答案藏在具体的工程约束里。我见过几类真实需求:一是主控芯片压根没有原生MAC控制器,比如某些低成本Cortex-A7或RISC-V核心的SoC,想加网口只能走USB网卡或SPI网卡;二是产品需要多路物理隔离的以太网口,用W5500每颗芯片独立走SPI,比堆MAC+PHY方案在布线和隔离上更干净;三是有些场景需要硬件协议栈来卸载CPU负载,主核跑实时任务,网络收发交给W5500自己处理,CPU占用率能压到极低。
所以"Linux平台Wiznet W5500移植"这件事,本质上是把一颗自带协议栈的SPI网卡芯片,接入Linux的网络子系统,让它以标准网络接口(比如eth1)的形式出现在系统里,上层应用用普通的socket编程就能收发数据,完全感知不到底层是SPI在搬数据。
这里有个关键认知必须先建立:W5500在Linux下的移植,不是"把协议栈移植进内核",而是"写一个网络设备驱动,把W5500当成一个以太网MAC+PHY的合体来用"。芯片内部的硬件协议栈负责和外部网络交互,Linux内核这边只需要负责把以太网帧通过SPI送进W5500的TX缓冲区,以及从RX缓冲区把帧读出来交给内核。这个定位搞清楚了,后面所有的代码结构就顺理成章。
适合读这篇内容的人:有Linux驱动基础、懂SPI子系统、想给手头板子加一路SPI以太网的嵌入式工程师;或者正在评估W5500方案、想知道Linux下到底能不能跑、跑起来性能如何的技术选型者。如果你连设备树和platform driver都没写过,建议先把这两块补一补再往下看,不然会有点吃力。
2. 动手之前必须想清楚的几件事:方案选型与内核接口抉择
2.1 为什么走网络设备驱动而不是字符设备或SPI裸驱动
很多人第一反应是:我写个字符设备,应用层直接read/write不就行了?或者干脆在用户态用spidev操作W5500寄存器。这两种做法在简单场景下确实能跑,但代价很大。
走字符设备意味着你要在用户态自己实现一套socket语义,或者让应用直接面对W5500的寄存器操作。任何想用标准网络工具(ping、iperf、tcpdump)或者标准socket API的程序都得改,移植成本极高。而走Linux网络设备驱动,W5500会注册成一个标准的net_device,系统启动后ifconfig能看到它,能配IP,能跑TCP/UDP,应用层零改动。
从内核架构看,网络设备驱动需要实现net_device_ops里的一系列回调:ndo_open、ndo_stop、ndo_start_xmit、ndo_set_mac_address、ndo_tx_timeout等。数据发送路径是内核调用ndo_start_xmit把sk_buff交给你,你负责把帧写进W5500;数据接收路径是你在中断或轮询里发现W5500收到帧,读出后封装成sk_buff,调用netif_rx或napi_gro_receive交给内核。
这个模型是Linux网络驱动的标准范式,W5500的移植工作就是把这套范式套到SPI传输上。
2.2 SPI传输模式的选择:中断、轮询还是NAPI
W5500通过SPI和主控通信,数据收发都靠SPI搬。这里第一个要决策的是:接收路径用什么机制感知新数据?
W5500的INT引脚在收到数据、发送完成、超时等事件时会拉低,可以接到主控的GPIO中断上。用中断触发,然后在中断处理里读W5500的IR寄存器判断事件类型,再决定是否收包。这是最直观的做法,但有个坑:SPI传输本身是同步阻塞的,在中断上下文里做SPI读写会睡死,必须用工作队列或线程化中断把实际的数据搬运放到进程上下文。
另一种做法是纯轮询,在ndo_open里起一个内核线程,周期性读W5500的RX接收状态寄存器,有数据就收。实现简单,但CPU占用和延迟都不理想。
我实际推荐的是NAPI+线程化中断的组合:中断只做最小工作(读IR寄存器、判断事件、napi_schedule),实际收包在NAPI poll函数里批量处理。这样既避免了中断上下文的SPI阻塞问题,又能在高负载时通过轮询降低中断频率。不过NAPI对SPI设备来说有点重,如果数据量不大,用线程化中断+工作队列也完全够用。
2.3 内核版本与SPI子系统的适配差异
W5500驱动在不同内核版本上的写法差异不小。4.x时代SPI驱动还是spi_driver的probe/remove老接口,5.x之后逐步向spi_mem和更规范的设备树绑定靠拢。设备树里W5500节点通常挂在SPI控制器下,需要声明compatible、reg(片选号)、spi-max-frequency、interrupts、interrupt-parent等属性。
spi-max-frequency这个参数要特别注意。W5500手册标称SPI最高80MHz,但实际能跑多快取决于主控SPI控制器、PCB走线、片选信号质量。我见过在20MHz下就出现偶发丢包的板子,降到10MHz立刻稳定。所以设备树里先写保守值(比如10MHz或15MHz),跑通后再逐步往上试,别一上来就拉满。
另外,W5500的SPI模式是Mode 0(CPOL=0, CPHA=0),设备树里spi-cpol和spi-cpha都不写就是Mode 0,这点别搞错,否则读出来的寄存器全是0xFF。
3. 驱动骨架搭建:从设备树到net_device注册的完整链路
3.1 设备树节点的正确写法与常见错误
先看一个能用的设备树节点示例:
&spi1 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&spi1_pins>; w5500: ethernet@0 { compatible = "wiznet,w5500"; reg = <0>; spi-max-frequency = <15000000>; interrupt-parent = <&gpio1>; interrupts = <12 IRQ_TYPE_EDGE_FALLING>; reset-gpios = <&gpio1 13 GPIO_ACTIVE_LOW>; local-mac-address = [00 08 dc 11 22 33]; }; };几个容易踩的点:reg = <0>里的0是片选号,要和硬件上W5500的CS接到哪个SPI片选对应,接错了SPI通信直接不通。interrupts里的触发类型,W5500的INT是低电平有效,但实际用边沿触发还是电平触发要看你的中断处理逻辑——如果用边沿触发,要确保中断处理里把IR寄存器读干净,否则会丢事件;用电平触发则要防止中断风暴。
reset-gpios是可选的,但强烈建议加上。W5500上电后需要一段复位时间,软件主动拉低再拉高复位引脚,能保证芯片进入确定状态。没有复位引脚的板子,只能靠上电复位,偶发初始化失败的概率会高一些。
3.2 probe函数里必须完成的初始化序列
probe函数是整个驱动的入口,顺序不能乱。我把它拆成几个阶段:
第一阶段是资源获取:拿到spi_device、申请GPIO、注册中断、申请复位引脚。这里要注意devm_系列函数的使用,能自动管理资源释放,减少remove里的清理代码。
第二阶段是硬件复位与寄存器自检。拉低复位引脚至少500微秒,拉高后等1毫秒以上,然后读VERSIONR寄存器(地址0x0039),正常应该返回0x04。如果读出来是0x00或0xFF,说明SPI通信有问题,直接返回错误,别往下走。
第三阶段是配置W5500。关键寄存器包括:
| 寄存器 | 地址 | 作用 | 典型值 |
|---|---|---|---|
| MR | 0x0000 | 模式寄存器 | 0x08(软件复位后) |
| RTR | 0x0019 | 重传超时 | 0x07D0(200ms) |
| RCR | 0x001B | 重传次数 | 0x08 |
| RMSR | 0x001A | 接收缓冲大小 | 0x55(2KB每socket) |
| Sn_MR | 0x0000+n*0x100 | Socket模式 | 0x01(TCP)或0x02(UDP) |
第四阶段是注册net_device。用alloc_etherdev分配,设置netdev_ops、ethtool_ops,填MAC地址,然后register_netdev。注册成功后系统里就能看到这个网口了。
3.3 发送路径:ndo_start_xmit里到底做了什么
发送路径相对简单。内核把sk_buff交到ndo_start_xmit,你要做的是:
- 检查W5500对应socket的TX空闲缓冲区大小(读
Sn_TX_FSR寄存器),如果放不下当前帧,返回NETDEV_TX_BUSY让内核重试。 - 通过SPI把帧数据写进W5500的TX缓冲区,写之前先写
Sn_TX_WR寄存器指定写指针。 - 更新
Sn_TX_WR,然后写Sn_CR寄存器的SEND命令位,触发W5500发送。 - 释放
sk_buff,返回NETDEV_TX_OK。
这里有个性能关键点:SPI写数据要用spi_sync还是spi_async?spi_sync是同步阻塞的,在发送路径里用它会阻塞内核网络栈。更好的做法是用spi_async配合完成回调,或者把发送放到工作队列里异步处理。但异步会引入复杂度,如果数据量不大,spi_sync也能接受,只是要注意不要在持有自旋锁的情况下调用。
3.4 接收路径:从INT中断到netif_rx的完整流转
接收路径是W5500驱动里最需要仔细设计的部分。完整链路是这样的:
INT引脚触发中断 → 中断处理函数读IR寄存器 → 判断是RECV事件 → 禁用该socket的RECV中断 →napi_schedule或调度工作队列 → 在poll/工作函数里读Sn_RX_RSR获取收到数据长度 → 从RX缓冲区读出数据 → 封装成sk_buff→netif_rx交给内核 → 重新使能RECV中断。
读数据时要注意W5500的RX缓冲区是环形结构,读指针Sn_RX_RD要正确维护。每次读完一帧后更新Sn_RX_RD并写RECV命令,告诉W5500这块缓冲区可以复用了。如果忘记写RECV命令,缓冲区很快会满,后续数据全部丢弃。
还有一个细节:W5500的每个socket有独立的RX缓冲区,大小由RMSR配置。如果配置成2KB每socket,8个socket就是16KB,这是W5500内部RAM的极限。实际用几个socket就配几个,别浪费。
4. 实测中那些文档不会告诉你的坑
4.1 SPI时钟相位配错导致寄存器读出全0xFF
这是最经典的坑,我至少见过三次。现象是probe阶段读VERSIONR返回0xFF,或者返回0x00,驱动直接初始化失败。原因几乎都是SPI模式不对。W5500要求Mode 0,但有些主控的SPI控制器默认是Mode 3,或者设备树里误写了spi-cpha。
排查方法很简单:用逻辑分析仪抓SPI的CLK和MOSI,看数据在时钟的哪个边沿采样。Mode 0是上升沿采样、下降沿输出。如果抓到的波形对不上,先改设备树,别急着怀疑硬件。
另一个相关问题是spi-max-frequency设太高。我遇到过一块板子在24MHz下读寄存器偶尔返回错误值,降到12MHz就完全正常。SPI时序余量不足时,表现为偶发通信错误,很难查。所以调试阶段先用低频,稳定后再往上加。
4.2 中断丢事件与中断风暴的两难
W5500的INT引脚是多个事件共享的,读IR寄存器才能知道具体是什么事件。如果中断处理里没有把IR读干净,或者读完之后没有清除对应的事件标志,中断会一直触发,形成中断风暴,系统直接卡死。
反过来,如果用边沿触发,而中断处理耗时太长,在两次读IR之间又来了新事件,边沿可能被错过,导致丢事件。我实际的做法是:中断处理里循环读IR直到没有未处理事件,同时用自旋锁保护IR读取,防止并发。如果事件处理逻辑复杂,把实际的数据搬运放到线程化中断里,中断上半部只做IR读取和事件分发。
4.3 发送完成中断与TX缓冲区回收的时序
W5500发送完成后会置位Sn_IR的SEND_OK,很多人在这里直接释放TX缓冲区。但要注意,SEND_OK只表示数据已经发到物理层,不代表对端收到了。如果开了TCP,重传由W5500硬件负责,你不需要管。但如果你在SEND_OK之前就复用了TX缓冲区,可能覆盖还没发出去的数据。
正确的做法是:在ndo_start_xmit里检查TX空闲缓冲区大小,只有空间足够才写入。发送完成后通过SEND_OK中断通知,再更新可用的缓冲区指针。这个逻辑和W5500的TX缓冲区环形管理是配套的,不能想当然。
4.4 多socket并发时的寄存器访问竞争
W5500的SPI接口是单通道的,多个socket共享同一组SPI总线。如果两个socket同时要读写寄存器,必须加锁串行化。我在驱动里用一个全局互斥锁保护所有SPI传输,虽然牺牲了一点并发性能,但保证了正确性。
更细粒度的做法是按socket加锁,但W5500的公共寄存器(比如MR、RTR)是全局的,还是需要一把大锁。实际测试下来,单锁方案在8个socket满负载时CPU占用率会上去,但功能没问题。如果追求极致性能,可以考虑把SPI传输做成队列,但复杂度会大幅上升。
5. 性能调优与稳定性验证:让W5500在Linux下真正能打
5.1 用iperf实测吞吐量并定位瓶颈
驱动跑通只是第一步,能不能达到可用性能是另一回事。我用iperf做过几轮测试,环境是Cortex-A7双核1GHz,SPI时钟15MHz,W5500单socket TCP。
实测结果:TCP发送约8-10Mbps,接收约6-8Mbps。这个数字看起来不高,但瓶颈在SPI。15MHz SPI理论带宽约1.875MB/s,换算成以太网帧(考虑帧头帧尾和SPI命令开销),实际有效吞吐就在10Mbps量级。想提高吞吐,只能拉高SPI时钟或优化SPI传输效率。
优化SPI传输效率有几个方向:一是用DMA做SPI传输,减少CPU搬运开销;二是批量读写,W5500支持连续地址读写,一次SPI传输可以搬多个寄存器或大块数据;三是减少锁竞争,把公共寄存器的访问频率降下来。
5.2 长时间运行的稳定性观察要点
嵌入式网络设备最怕跑几天就挂。W5500驱动要重点观察几个指标:
- RX缓冲区溢出计数:如果
Sn_RX_RSR经常接近缓冲区上限,说明收包速度跟不上,需要优化接收路径或增大缓冲区。 - 重传次数:W5500的
Sn_TX_RD和Sn_TX_WR差值如果持续增大,说明发送受阻,可能是对端问题或SPI传输卡顿。 - 中断计数:如果中断频率异常高,检查是否有未清除的事件标志。
- 内存泄漏:
sk_buff的分配和释放要配对,长时间跑用slabtop看skbuff_head_cache有没有持续增长。
我一般会跑一个72小时的ping+iperf混合测试,同时用脚本每10分钟记录一次上述指标。如果72小时无异常,基本可以认为驱动稳定。
5.3 电源管理与热插拔场景的边界处理
W5500本身功耗不高,但Linux的电源管理框架可能会在系统休眠时关掉SPI控制器,导致W5500失联。如果产品需要休眠唤醒,要在驱动里实现ndo_set_features或ethtool的set_wol回调,在休眠前把W5500配置成低功耗模式,唤醒后重新初始化。
热插拔场景更麻烦。W5500是焊在板子上的,一般不存在热插拔,但如果SPI控制器支持动态开关,或者系统有多个SPI设备共享总线,要确保W5500的remove回调正确释放所有资源,包括注销net_device、释放中断、复位GPIO。漏掉任何一项,下次insmod就会失败。
6. 几个能直接抄的调试技巧和代码片段
6.1 用sysfs暴露W5500内部状态
调试阶段最有用的是能实时看W5500的寄存器状态。我在驱动里加了一个sysfs属性,读的时候返回所有socket的Sn_SR、Sn_RX_RSR、Sn_TX_FSR:
static ssize_t w5500_status_show(struct device *dev, struct device_attribute *attr, char *buf) { struct w5500_priv *priv = dev_get_drvdata(dev); int i, len = 0; for (i = 0; i < W5500_MAX_SOCKET; i++) { u8 sr = w5500_read_reg(priv, W5500_Sn_SR(i)); u16 rx = w5500_read_reg16(priv, W5500_Sn_RX_RSR(i)); u16 tx = w5500_read_reg16(priv, W5500_Sn_TX_FSR(i)); len += scnprintf(buf + len, PAGE_SIZE - len, "sock%d: SR=0x%02x RX_RSR=%u TX_FSR=%u\n", i, sr, rx, tx); } return len; }这样cat /sys/class/net/eth1/device/status就能看到实时状态,比反复读寄存器方便得多。
6.2 用ftrace跟踪SPI传输耗时
如果怀疑SPI传输是瓶颈,可以用ftrace的function_graph跟踪spi_sync的调用耗时:
echo function_graph > /sys/kernel/debug/tracing/current_tracer echo spi_sync > /sys/kernel/debug/tracing/set_ftrace_filter echo 1 > /sys/kernel/debug/tracing/tracing_on # 跑一段网络流量 cat /sys/kernel/debug/tracing/trace看每次spi_sync的耗时分布,如果单次传输超过100微秒,说明SPI时钟太低或传输数据量太大,需要优化。
6.3 一个最小化的发送函数参考
static netdev_tx_t w5500_start_xmit(struct sk_buff *skb, struct net_device *ndev) { struct w5500_priv *priv = netdev_priv(ndev); u16 free_size; u8 *buf; int ret; mutex_lock(&priv->spi_lock); free_size = w5500_read_reg16(priv, W5500_Sn_TX_FSR(priv->sock)); if (free_size < skb->len) { mutex_unlock(&priv->spi_lock); return NETDEV_TX_BUSY; } buf = kmalloc(skb->len, GFP_ATOMIC); if (!buf) { mutex_unlock(&priv->spi_lock); return NETDEV_TX_BUSY; } skb_copy_bits(skb, 0, buf, skb->len); w5500_write_tx_buffer(priv, priv->sock, buf, skb->len); w5500_write_cmd(priv, priv->sock, W5500_CMD_SEND); kfree(buf); mutex_unlock(&priv->spi_lock); dev_kfree_skb(skb); return NETDEV_TX_OK; }这段代码里kmalloc用了GFP_ATOMIC,因为发送路径可能在软中断上下文。如果确定发送路径在进程上下文,可以用GFP_KERNEL。mutex_lock保护SPI访问,防止多socket并发。
7. 写在最后:W5500在Linux下的定位与取舍
折腾完这一圈,我对W5500在Linux平台的价值有了更清晰的认识。它不是用来替代原生MAC+PHY方案的,性能上打不过,成本上也不一定更便宜。它的价值在于特定约束下的灵活性:主控没有MAC、需要多路隔离网口、需要硬件协议栈卸载CPU负载,这些场景下W5500是少数能快速落地的选择。
移植过程中最大的工作量不在驱动本身,而在调试SPI时序、处理中断竞争、验证长时间稳定性。驱动代码量其实不大,核心逻辑几百行就能写完,但要把边界情况都处理干净,需要反复测试和打磨。
如果你正准备做类似的事情,我的建议是:先用逻辑分析仪把SPI波形抓对,再写驱动;先用低频SPI跑通功能,再逐步提频;先单socket验证,再上多socket并发。每一步都稳了再往下走,比一口气写完再调试要快得多。