K3 GMAC 网卡驱动调试教训
0. 背景
K3 COM260 开发板上,我需要让 StarryOS(基于 ArceOS 的通用内核)的以太网口可用, 以支持 DHCP、网络文件系统等服务。硬件为进迭时空 K3 SoC,搭载 DWMAC 5.10a 以太网 MAC IP + RTL8211F PHY,通过 RGMII 接口连接。
U-Boot 阶段 TFTP 工作正常,证明硬件没问题。但自己写的 GMAC 驱动在 StarryOS 上长期 无法工作:PHY 自协商完成、链路 UP,但 DHCP 始终超时。这个驱动我反复调试了跨多个 session、数十次真板测试,最终定位到三个独立的寄存器定义/地址错误,逐一修复后 驱动终于可用——DHCP 成功、ping 成功。
本文是我对这段调试经历的复盘:走过的弯路、最终找到的三个 bug、以及由此沉淀的教训。
1. 症状
驱动初始化日志显示一切"正常":
k3-gmac: PHY0 link state: up=true speed=1000Mbps duplex=full
k3-gmac: after start_dma: TX_ST=true chan_stat=0x84
k3-gmac: post-init: GMAC_CONFIG=0x00072203 (RE=true TE=true DM=true ...)
TX_CTRL=0x00080011 (ST=true OSP=true) RX_CTRL=0x00081001 (SR=true RBSZ=2048)
k3-gmac: TX#0 desc[0] des3=0xb0000131(OWN=true) | tail wrote=0xfcf64040 readback=0xfcf64040
cur_tx=0xfcf64000 chan_stat=0x84(TBU=true FBE=false) dbg0=0x1300我在驱动里加了诊断日志,关键信息是:
- TX 描述符 OWN=1(CPU 已交由 DMA),但 DMA 从不清除 OWN
cur_tx(DMA 当前读取位置)始终停在 ring base,从不前进dbg0=0x1300:TX DMA 引擎状态机处于 Idle(状态 0),从未启动- RX 路径在特定配置下能收到包(说明 DMA AXI 总线、EAME、地址解码都正常)
核心矛盾:TX_CTRL 的 ST=1(启动位已设),tail pointer 已正确写入,描述符 OWN=1, 但 TX DMA 引擎就是不读描述符。
2. 调试历程与发现的三个 Bug
2.1 探索阶段:大量试错排除假根因
早期我走了很多弯路,靠"推测哪个寄存器可能有问题 → 改 → 上板测"的方式排查, 测了大量假设:
| 假设 | 验证方式 | 结论 |
|---|---|---|
| DMA 软复位缺失导致引擎锁死 | 对照 U-Boot eqos_start,恢复 reset_dma | 软复位成功,但 TX 仍不工作(非根因,但保留) |
| DMA 软复位导致引擎锁死 | 移除软复位 | 无变化(非根因) |
update()(RMW)保留了 U-Boot 残留位 | 改为绝对写(write) | 让 RX 工作了(消除了残留),但 TX 仍不工作 |
| EAME 未启用(>4GB DMA 寻址) | 检查 DMA_SYS_BUS_MODE bit11 | 已启用,非根因 |
DMA_MASK=u32::MAX(32 位) | K3 无 <4GB DRAM,改回 u64::MAX | 修复了 NoMemory,非 TX 根因 |
DMA_AXI_BUS_MODE 错误偏移 | 合并到 DMA_SYS_BUS_MODE | 修正了寄存器布局,非 TX 根因 |
| PBL=0 无突发 | 加 PBL=8 | 正确修复,非 TX 根因 |
DMA_CHAN_CONTROL(PBLX8+DSL)未写 | 加绝对写 | 正确修复,非 TX 根因 |
| TX buffer cache 未 flush | 检查 prepare_for_device | 已 flush,非根因 |
| Zicbom CMO 未真正执行 | 追踪 feature 链 | 已启用,非根因 |
| 描述符格式错误 | 对照 dwmac4_descs.h 逐位检查 | 格式正确,非根因 |
| RISC-V IOMMU 阻塞 | 检查 DTS iommus 属性 | GMAC 无 IOMMU 绑定,pass-through 模式,非根因 |
十几个假设全部排除了。这个阶段我反复上板测试,效率很低,而且每次测试结果都是 "TX 还是不工作",无法区分到底是哪个改动起了作用。
转折点:我决定不再猜测,而是让 AI 助手做一次 U-Boot eqos_start + Linux stmmac_hw_setup vs 我的 init_hardware 的逐行寄存器写入对照,系统性地列出 每一个差异。正是这次穷尽的对照找到了最终的 bug。
2.2 Bug #1:MTL_OP_MODE_TSF 位定义错误
// 我的错误定义:
pub const MTL_OP_MODE_TSF: u32 = 1 << 0; // BIT0 是 FTQ(Flush TX Queue)!
// 正确(对照 U-Boot BIT(1) + Linux BIT(1)):
pub const MTL_OP_MODE_TSF: u32 = 1 << 1; // BIT1 才是 TSF(Store-and-Forward)BIT0 在 MTL TX 操作模式寄存器中是 FTQ(Flush TX Queue)——我写 1 进去等于持续 flush TX 队列,DMA 写入 TX FIFO 的数据被立即丢弃。
这个 bug 是逐行对照 U-Boot dwc_eth_qos.h:114-115 时发现的:
#define EQOS_MTL_TXQ0_OPERATION_MODE_TSF BIT(1)
#define EQOS_MTL_TXQ0_OPERATION_MODE_FTQ BIT(0)但修了它 TX 还是不工作。它是加重因素,不是终极根因。
2.3 Bug #2:DMA 软复位缺失(次要修复)
DMA 软复位(DMA_BUS_MODE bit0,写 1 后硬件自清)能重启 DMA 内部状态机。我之前 错误地移除了它——当时误以为"U-Boot 不做软复位",实际上 U-Boot 在 eqos_start 最开头就做(line 767-773),Linux 的 stmmac_hw_setup 也是。
加回软复位后,日志确认 DMA soft reset completed after 257773 iterations(~258ms), 但 TX 仍然不工作。这个修复对照参考实现是正确的,但不是 TX 不工作的根因。
2.4 Bug #3:MTL_CHAN_BASE_ADDR 错误(终极根因)⭐
// 我的错误定义:
pub const MTL_CHAN_BASE_ADDR: u32 = 0xc00;
// 正确(对照 U-Boot EQOS_MTL_REGS_BASE + Linux MTL_CHAN_BASE_ADDR):
pub const MTL_CHAN_BASE_ADDR: u32 = 0xd00;这是终极根因。我所有的 MTL 寄存器写入(TX 队列使能 TSF|TXQEN|TQS、RX 队列 RSF|RQS 等)都写到了错误地址——0xc00 而非 0xd00。MTL TX 队列从未被使能, TXQEN 位写到了不存在的地址,硬件 MTL TX 队列保持默认的禁用状态。
怎么发现的:修了 TSF 位定义后 TX 还是不工作,逐行对照报告确认"寄存器初始化 序列在所有控制 TX DMA fetch 的位上都是正确的",迫使我转向 MTL 层。最终在检查 MTL TX debug 寄存器偏移时,我注意到 U-Boot 定义 EQOS_MTL_REGS_BASE = 0xd00 (dwc_eth_qos.h:96),而我的代码用 0xc00——差了整整 0x100。
为什么 TX 不工作而 RX 工作:DWMAC4 硬件中,RX 队列默认使能(复位后 RXQ0EN 默认值非零),TX 队列默认禁用(复位后 TXQEN=0)。RX 不依赖我写错的 MTL 配置 (默认就开了),TX 必须显式使能但我写错了地址。这完美解释了一直困扰我的 "RX 工作、TX 不工作"的不对称现象。
DMA 引擎为何 Idle:DMA 引擎 fetch 描述符后,需要把数据写入 MTL TX FIFO。但 MTL TX 队列未被使能,MTL 拒绝接收 DMA 数据。DMA 的写入路径被阻塞,fetch engine 因此停滞在 Idle 状态——cur_tx 从不前进,dbg0 TX state=0。
2.5 修复确认
三个 Bug 修复后,真板测试结果:
k3-gmac: TX#0 desc[0] ... cur_tx=0xfcf64040 ← cur_tx 前进了!
eth0: DHCP bound: 192.168.1.100/24 ← DHCP 成功!
ping 192.168.1.1: 64 bytes, time=0.5ms ← ping 成功!驱动终于工作了。
3. 为什么调试这么久?——教训总结
整个调试过程跨越了多个 session、数十次真板测试。反思为何耗时如此之长:
教训 1:不要在没有参考源码的情况下猜测硬件行为
早期我大量依赖"推测"——猜哪个寄存器可能有问题,改了就上板测。这导致了大量的 试错循环(见 §2.1 的表格,列出了十几个被排除的假根因),每次真板测试 5-10 分钟, 效率极低。
正确做法:第一时间找到参考实现(U-Boot / Linux 源码),逐行对照。本次调试的 转折点正是让 AI 助手做 U-Boot eqos_start vs 我的 init_hardware 的逐行寄存器 写入对照。这种系统性的 diff 比任何猜测都有效。
对于 SoC 外设驱动移植,U-Boot 驱动(尤其是同一厂商的 SDK)是最可靠的参考,因为 U-Boot 初始化序列通常最精简、最直接,没有 Linux 的抽象层(stmmac_ops 等)。 先对照 U-Boot,再对照 Linux。
教训 2:寄存器地址和位定义必须逐个对照源码,不能凭记忆/猜测
三个 Bug 都是常量定义错误(MTL_CHAN_BASE_ADDR 差 0x100、MTL_OP_MODE_TSF 差 1 位)。这类错误编译器无法发现,运行时的症状又很模糊("DMA 不工作"),极难定位。
正确做法:新建驱动时,每一个寄存器偏移量和位定义都应该从参考头文件直接对照 (dwmac4.h、dwmac4_dma.h、dwc_eth_qos.h),而不是凭记忆或"差不多"写。建一个 对照表:
| 我的常量 | 值 | Linux/U-Boot 对应 | 参考值 | 一致? |
|-----------|-----|-------------------|--------|--------|
| MTL_CHAN_BASE_ADDR | 0xc00 | EQOS_MTL_REGS_BASE | 0xd00 | ❌ |
| MTL_OP_MODE_TSF | BIT(0) | EQOS_..._TSF | BIT(1) | ❌ |在驱动开发的寄存器定义阶段就做这种对照,能避免后期数天的调试。
教训 3:不对称现象(部分工作)是重要线索,要深入解释
"RX 工作但 TX 不工作"这个现象在调试早期就出现了,但我没有深入追问"为什么"。 如果早一点追问"什么硬件机制能让 RX 默认工作但 TX 不工作",可能更快定位到 MTL 队列 使能的问题。
正确做法:当观察到不对称现象(一个方向工作、另一个不工作)时,列出所有 "RX 和 TX 的不同点",包括:
- 硬件默认状态(RX 队列默认开 vs TX 队列默认关)
- 配置依赖(RX 需要什么、TX 需要什么)
- 代码路径差异(
submit_rx有 SR doorbell,submit_tx没有)
不对称现象通常指向配置差异或默认值差异,值得专门深入。
教训 4:诊断日志要包含"引擎状态"而不只是"配置状态"
早期诊断日志主要打印配置寄存器的值(TX_CTRL、CHAN_CTRL 等),这些值看起来都正确, 但无法反映 DMA 引擎的实际运行状态。
正确做法:加入 DMA 引擎状态寄存器的诊断:
DMA_DEBUG_STATUS_0(0x100c):TX/RX DMA 引擎状态机状态DMA_CHAN_CUR_TX_DESC(0x1144):DMA 当前正在处理的描述符地址MTL_CHAN_TX_DEBUG:MTL TX 队列状态
这些寄存器直接反映引擎"在做什么"而非"配了什么"。dbg0=0x1300(TX state=Idle)和 cur_tx=base(从不前进)是定位问题的关键信号。
诊断日志应该回答"引擎为什么不工作",而不只是"配置是否正确"。
教训 5:逐行对照时要警惕确认偏差
人眼对照源码容易"看到自己期望看到的"——确认偏差。我多次"确认"过寄存器定义"正确", 但实际上 MTL base 地址和 TSF 位都是错的。
正确做法:让 AI 助手做自动化的逐行对照,输出结构化的 diff 表格。机器不受确认 偏差影响,会忠实地报告每一个差异。对于复杂的源码对照任务,穷尽的 diff 比人工 对照更可靠。
教训 6:真板测试成本高,改代码前要做足推演
每次真板测试需要刷写固件、重启开发板、观察串口日志,耗时 5-10 分钟。大量"试错式" 修改导致测试效率极低——这也是我在调试中途明确要求"测试之前严格进行代码推演, 保证测试有必要的进展"的原因。
正确做法:每次提交测试前,应该明确:
- 当前修改的假设是什么
- 这个假设有什么源码证据
- 如果假设错误,诊断日志能否区分
尊重真板测试的成本,把调试的"不确定性"消化在代码推演阶段,而不是测试阶段。
4. 调试方法论沉淀
基于本次经验,我总结出 SoC 外设驱动调试的标准流程:
第一阶段:参照源码(动手前)
- 找到参考实现:U-Boot SDK 驱动 > Linux mainline 驱动 > 数据手册
- 逐行对照初始化序列:列出每一个寄存器写入的对照表
- 特别关注常量定义:寄存器偏移量、位位置、位掩码——这些是最容易出错且最难发现的地方
第二阶段:诊断设计(第一次测试前)
- 打印引擎状态:不只是配置寄存器,还要有状态机状态、当前指针位置
- 打印不对称信息:如果 TX 和 RX 路径类似,对比两者的诊断输出
- 设计可区分的诊断:每次测试应该能排除至少一个假设
第三阶段:迭代调试
- 一次只改一个变量:不要同时改多个地方
- 解释所有现象:特别是"部分工作"的不对称现象
- 用穷尽对照代替猜测:当人眼对照无法定位时,做自动化逐行 diff
分支完整代码分析和工作内容总结见周报 《第三十三周:K3 GMAC 网卡驱动调试成功 + AMP IPC 链路修复》。
5. 附录:关键寄存器速查
DMA 块(基址偏移 0x1000)
| 寄存器 | 偏移 | 说明 |
|---|---|---|
| DMA_BUS_MODE | 0x1000 | bit0=SWR(软复位) |
| DMA_SYS_BUS_MODE | 0x1004 | EAME(bit11) + BLEN + OSR |
| DMA_DEBUG_STATUS_0 | 0x100c | bits[3:0]=TX state, [19:16]=RX state |
DMA Channel 0(基址 0x1100,stride 0x80)
| 寄存器 | 偏移 | 说明 |
|---|---|---|
| DMA_CHAN_CONTROL | 0x1100 | PBLX8(bit16) + DSL(bits[20:18]) |
| DMA_CHAN_TX_CONTROL | 0x1104 | ST(bit0) + OSP(bit4) + PBL(bits[21:16]) |
| DMA_CHAN_RX_CONTROL | 0x1108 | SR(bit0) + RBSZ(bits[14:1]) + RXPBL(bits[21:16]) |
| DMA_CHAN_TX_BASE_HI | 0x1110 | 描述符环基址高 32 位 |
| DMA_CHAN_TX_BASE | 0x1114 | 描述符环基址低 32 位 |
| DMA_CHAN_RX_BASE_HI | 0x1118 | |
| DMA_CHAN_RX_BASE | 0x111c | |
| DMA_CHAN_TX_END | 0x1120 | TX 尾指针(doorbell) |
| DMA_CHAN_RX_END | 0x1128 | RX 尾指针 |
| DMA_CHAN_TX_RING_LEN | 0x112c | TX 环长度(N-1) |
| DMA_CHAN_RX_RING_LEN | 0x1130 | RX 环长度(N-1) |
| DMA_CHAN_INTR_ENA | 0x1134 | 中断使能 |
| DMA_CHAN_CUR_TX_DESC | 0x1144 | DMA 当前 TX 描述符(只读) |
| DMA_CHAN_STATUS | 0x1160 | TI(bit0) TBU(bit2) RI(bit6) RBU(bit7) |
MTL Channel 0(基址 0xd00,stride 0x40)
| 寄存器 | 偏移 | 说明 |
|---|---|---|
| MTL_CHAN_TX_OP_MODE | 0xd00 | TSF(bit1) FTQ(bit0!) TXQEN(bits[3:2]) TQS(bits[24:16]) |
| MTL_CHAN_TX_DEBUG | 0xd08 | TX 队列状态 |
| MTL_CHAN_RX_OP_MODE | 0xd30 | RSF(bit5) RQS(bits[29:20]) |
MAC(基址 0x0000)
| 寄存器 | 偏移 | 说明 |
|---|---|---|
| GMAC_CONFIG | 0x0000 | RE(bit0) TE(bit1) DM(bit13) FES(bit14) PS(bit15) |
| GMAC_PACKET_FILTER | 0x0008 | PR(bit0) PM(bit1) |
| GMAC_RXQ_CTRL0 | 0x00a0 | RXQ0EN(bits[1:0]),DCB=2 |
| GMAC_ADDR_HIGH0 | 0x0300 | MAC 地址高 16 位 + AE(bit31) |
| GMAC_ADDR_LOW0 | 0x0304 | MAC 地址低 32 位 |
| GMAC_VERSION | 0x0310 | synopsys_id(0x54 = DWMAC 5.10a) |
| GMAC_HW_FEATURE0-3 | 0x0358+ | 硬件特性(FIFO 大小、地址宽度等) |
6. 参考源码索引
| 参考源 | 路径 | 关键函数 |
|---|---|---|
| U-Boot | drivers/net/dwc_eth_qos.c | eqos_start()(初始化)、eqos_send()(TX)、eqos_recv()(RX) |
| U-Boot | drivers/net/dwc_eth_qos.h | 寄存器定义 + 位定义 |
| Linux | drivers/net/ethernet/stmicro/stmmac/stmmac_main.c | stmmac_hw_setup()、stmmac_xmit() |
| Linux | drivers/net/ethernet/stmicro/stmmac/dwmac4_dma.c | dwmac4_dma_init()、dwmac4_dma_reset() |
| Linux | drivers/net/ethernet/stmicro/stmmac/dwmac4_lib.c | dwmac4_dma_start_tx()、dwmac4_set_tx_tail_ptr() |
| Linux | drivers/net/ethernet/stmicro/stmmac/dwmac4_descs.c | dwmac4_rd_prepare_tx_desc()、dwmac4_set_tx_owner() |
| Linux | drivers/net/ethernet/stmicro/stmmac/dwmac4.h | MAC/MTL 寄存器定义 |
| Linux | drivers/net/ethernet/stmicro/stmmac/dwmac4_dma.h | DMA 寄存器 + 位定义 |
文档完成于 2026-08-14。基于 K3 COM260 开发板真板调试。