起因

盘能读写、U 盘能交换之后,还缺一条跟外界说话的路:程序下发、参数上传、车间 MES 对接、NTP 校时,后面合规篇的 4G 远程管控,载体全是网。要自研的这套东西年纪都不小:以太网是 Metcalfe 1973 年在 Xerox PARC 写进备忘录的实验,1980 年 DIX 三家(DEC/Intel/Xerox)把它定成标准;TCP/IP 是 Cerf 和 Kahn 1974 年的论文,1983 年 1 月 ARPANET 整网切换换来一战成名。半个世纪过去骨架没换,换的只是速度——自研协议栈要实现的正是这套稳定了几十年的骨架。这篇分三块:有线网卡驱动(PCIe 以太网控制器,寄存器级)、无线的现实路径(USB Wi-Fi 的坑与替代方案)、以及之上的自研 TCP/IP 协议栈——延续零依赖原则,一个字节的外部代码都不引。

一、有线网卡:以 Intel e1000 为参考实现

选型理由:e1000(8254x 系列)寄存器文档公开、行为经典(描述符环模型是几乎所有网卡的原型)、QEMU 原生模拟(-device e1000,netdev=n0 -netdev user,id=n0)——真机工业板上最常见的 Realtek r8169 也是同款描述符环思路,换个寄存器表而已。

PCIe 枚举认亲:Vendor 0x8086、Device 0x100E(QEMU 的 82540EM),BAR0 是寄存器窗口。关键寄存器(BAR0 偏移):

偏移 名称 用途
0x0000 CTRL 全局控制(复位、链路使能)
0x0008 STATUS 链路状态/速率
0x0014 EERD EEPROM 读(MAC 地址的原始出处)
0x00D0 / 0x00D8 IMS / IMC 中断使能/屏蔽
0x2800–0x2818 RDBAL/RDBAH/RDLEN/RDH/RDT 接收描述符环:基址/长度/头/尾
0x3800–0x3818 TDBAL/TDBAH/TDLEN/TDH/TDT 发送描述符环
0x5400–0x5404 RAL / RAH MAC 地址寄存器(读它也能拿本机 MAC)

描述符环是网卡驱动的核心抽象:一块 DMA 内存排成环,每项 16 字节,指向一个包缓冲区。初始化按这个顺序来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 0. IMC = 0xFFFFFFFF:先屏蔽全部中断再动寄存器;顺手读一遍 ICR 清掉悬挂事件
// 1. CTRL |= 1<<6(SLU,Set Link Up)拉起链路,STATUS 读回协商速率(1000M/100M)
// 2. MAC 地址:RAL/RAH(0x5400/0x5404)读出即是,QEMU 上电就写好了
//    走 EEPROM 的真机用 EERD:写 (字地址<<8)|1 启动读,轮询 bit4 DONE,数据在 bits16-31,读 3 个字拼 6 字节
// 3. 分配 RX/TX 环(各 64 项×16B,DMA 池 16B 对齐),每项配一个 2048 字节包缓冲(RCTL.BSIZE=00 即 2048)
//    RDBAL/RDBAH/RDLEN 写环基址和长度,RX 每项填缓冲地址后 RDT=63——64 个缓冲一次全交出去
//    TDBAL/TDBAH/TDLEN 同理,TDH=TDT=0
// 4. MTA(0x5200 起的组播过滤表)整片清零——组播一个不收
// 5. RCTL(0x0100)= EN(1<<1) | BAM(1<<15):收包使能 + 广播放行(DHCP 全靠广播,这位不开 DHCP 必死)
//    TCTL(0x0400)= EN(1<<1) | PSP(1<<3) | CT=0x10<<4 | COLD=0x40<<12(全双工取 0x40)
// 6. IMS = 1<<7(RXT0 收包中断)——只开这一个;发送完成不必中断,发完轮询 TDT 追上 TDH 即可

描述符本体(legacy 格式):RX 项 = 缓冲地址 8B + 长度 2B + 校验 2B + 状态 1B(bit0 DD、bit1 EOP)+ 错误 1B + 特殊 2B;TX 项 = 缓冲地址 8B + 长度 2B + CSO/CSS 各 1B + CMD 1B(bit0 EOP、bit1 IFCS、bit3 RS) + 状态 1B + 特殊 2B。DD(Descriptor Done)是唯一可靠的动作信号,收发都看它。

收包:网卡 DMA 写完一个包,把对应 RX 描述符的 DD 位置 1 并触发中断;ISR(设备向量区)只做"把描述符索引进环形队列",协议逻辑一概不碰,协作层的网络任务慢慢消费:读包缓冲 → 处理 → 描述符回填新缓冲地址(状态清零)→ 推进 RDT,网卡才认这个槽还回来了。发包:填 TX 描述符(缓冲地址、长度、CMD 置 EOP|IFCS|RS)→ 推进 TDT → 网卡搬完置 DD,同一槽位下一帧要等 DD。中断上半部轻、协议处理下半部重,跟 NVMe 的完成队列一个套路。

二、无线网卡(USB):为什么不直连

USB Wi-Fi 是裸机驱动里公认的天花板之一,难点不在 USB 传输(EHCI 篇已解决)而在设备本身:

  • 固件加载:多数 USB Wi-Fi 芯片(Realtek RTL8188/8821 系列最常见)上电时 RAM 是空的,主机要通过控制/批量传输把几百 KB 固件灌进去——固件二进制从厂商驱动里提取,许可和来源都是灰色地带
  • 802.11 管理层(MLME):扫描、关联、四次握手、密钥派生,一套完整的状态机,工作量比有线网卡大一个量级

工程上给两条路,推荐第一条:

  • 无线网桥方案(推荐):机箱里加一个 USB 供电的 Wi-Fi 桥/随身路由,它对外是 Wi-Fi、对内是以太网口——我们的系统只看见一块普通 e1000/r8169 有线网卡,无线复杂性整体外包给成品硬件。工业现场这本来就是主流做法(4G/5G 路由器同理)。USB 4G 模块直插还有条中间路:RNDIS/CDC-NCM 本质是"USB 上的以太网",控制传输建好通道后批量端点跑的就是以太网帧,当第二块网卡挂进同一个协议栈(合规篇的 4G 远程管控走这条)——比 USB Wi-Fi 少了固件灌载和 MLME 两座山,量级接近有线网卡
  • 直连 USB Wi-Fi(按需立项):如果必须直连(成本/结构限制),锁定一款芯片(如 RTL8188CUS 级别)单点突破:vendor 特定命令走控制传输、固件灌载、然后是 802.11 数据帧收发 + MLME。当成一个独立项目排期,不塞进基础篇

三、TCP/IP 协议栈:自己写一个最小的

延续零外部依赖原则(smoltcp 这类现成栈按许可证纪律 vendor 也可以,但自写一个工业最小栈比想象中可行,且完全可控)。分层与各层的"最小可用集":

以太网层:帧头 14 字节(目的 MAC/源 MAC/类型),类型表就三个——0x0800 IPv4、0x0806 ARP、(0x86DD IPv6 留位不做)。收发直接对接 e1000 的包缓冲。

ARP:维护一张 16 项的 IP→MAC 缓存;收到请求若目标是自己 IP 就应答;发包前查表,没有就广播请求并暂存待发包。表项带老化时间(TSC 时间戳——APIC 篇的钟再立一功)。

IPv4:头校验和(16 位反码和)必验必算,一个函数两头用:

1
2
3
4
5
6
fn checksum(words: &[u16]) -> u16 {             // 头按 u16 数组看,长度保证偶数
    let mut sum: u32 = 0;
    for &w in words { sum += w as u32; }        // 先当普通加法
    while sum >> 16 != 0 { sum = (sum & 0xFFFF) + (sum >> 16); }  // 进位回卷
    !(sum as u16)
}

发包时校验和字段先清零、算完填入;收包验算把整头(含校验和)喂进去,结果非 0 直接丢。分片直接不支持(以太网内 MTU 1500,工控网可控,遇到分片丢弃并计数);协议号只认 ICMP(1)/UDP(17)/TCP(6)。

ICMP:回显请求→应答,ping 通了链路就活了——调网第一工具,几十行代码。

UDP + DHCP:UDP 头 8 字节,校验和在 IPv4 里允许填 0(表示不算)——最小栈先填 0 省事,链路层有以太网 FCS 兜底;要做端到端校验再按"伪头+数据"补反码和。第一个应用就是 DHCP 客户端:广播 DISCOVER(UDP 67/68)→ 收 OFFER → REQUEST → ACK,从选项段(魔数 0x63825363 后)解析出本机 IP/掩码/网关/DNS;每个包带随机 4 字节 xid,回包先核对 xid 再收——不核对,网络上别的 DHCP 应答会混进你的配置。500ms 无应答重发,三连失败在界面上报"网络故障"而不是干等。没有 DHCP 的车间就静态配置——两条路都留。

TCP:难点全在这,但工业场景能砍掉大半通用复杂度。状态机收敛为:CLOSED → LISTEN → SYN_RCVD → ESTABLISHED → FIN_WAIT/CLOSE;连接表固定 8 槽(现场对接 MES/上位机用不了几个连接);滑动窗口做简化版(固定窗口,不做动态拥塞控制——车间内网不是互联网,丢包率是另一个量级,这个减法说得过去,注释里写明前提);重传定时器用 APIC 定时器驱动,200ms 起步指数退避,3 次封顶报断线。三次握手、序号确认、FIN 挥手——骨架省不得,两个细节也别漏:SYN 里把 MSS 选项写成 1460,不发这个选项对端按默认 536 切包,吞吐直接废掉;收到不属于任何连接的四元组段回 RST 而不是静默丢弃,丢弃会让对端一直重传干等到超时。

应用层先立三个:NTP 客户端(UDP,校时回写 RTC——接上系统时间篇)、TFTP(UDP,最简单的文件传输,配合 exFAT 做程序下发)、Modbus/TCP(本站有 Modbus 基础,工业现场对接现成协议)。

四、网络任务在两级模型里的位置

整个协议栈跑在协作层:一个 net_step 函数,每轮消费 RX 队列里的包描述符 → 逐层解封装 → 分发给 ARP/DHCP/TCP 状态机 → 汇集待发包走 TX 环。网卡中断只进 ISR 搬索引,不碰任何协议逻辑。多核篇把双核分工定下来之后,net_step 归 Core1 的服务循环;网络的事毫秒级抖动无所谓,Core0 上插补和放电的微秒级节拍根本看不见它。RX 队列深度要按"最坏卡顿 × 线速"算:协作层要是被日志落盘卡住几百毫秒,64 项的环形队列会绕圈覆盖——覆盖发生时记一条错误日志,别静默吞包。

总结

实战清单:e1000 认亲(0x8086:0x100E)、RX/TX 描述符环五步初始化、DD 位+RDT/TDT 的收发配合、中断上半部只搬索引;无线走网桥方案(直连 USB Wi-Fi 是独立量级的项目,固件灌载+MLME 是两座山);协议栈最小集=以太网+ARP+IPv4+ICMP+UDP(DHCP)+简化 TCP,反码和校验、8 槽连接表、固定窗口,应用层 NTP/TFTP/Modbus 起步。跑通标志:QEMU -netdev user 下 DHCP 拿到 10.0.2.15,宿主机 ping 通,TFTP 传来一个文件写进 exFAT 盘。物理层到应用层,网这条路也全是自己的代码。下一篇收拾平台:电源控制、风扇调速、内存信息获取。

参考链接