如果你在嵌入式板卡上遇到过 eMMC 写入时报 TXUNDERR,而且这个错误只在冷启动后出现、只在写操作时触发,读文件一切正常,那你八成跟我一开始一样:盯着日志翻来覆去,怀疑介质、怀疑焊接、甚至怀疑人生。这个问题的麻烦点不在错误本身,而在“间歇性”和“冷启动”这两个附加条件上。它不是每次开机都炸,炸一次之后可能又很稳定,搞得人很难定位是软件配置问题还是硬件时序问题。
我最近就完整走了一遍这个排查过程,平台是一颗常见 ARM SoC,SDMMC1 控制器接板载 eMMC,系统是 Armbian 这类轻量 Linux 发行版,通过 U 盘启动后再把根文件系统写入 eMMC。整个排查持续了几天,最后定位到核心原因是控制器初始化时序和高速模式切换窗口的问题。这篇文章把我整个思考路径、验证手段和最终修复方案完整写出来,希望能帮你少走几步弯路。
1. 先搞懂 TXUNDERR:SD/MMC 控制器的“喂不饱”错误
1.1 名称拆解:从寄存器位到数据路径
TXUNDERR 全称是 Transmit Underrun Error,翻译过来就是“发送欠载错误”。在 SDHCI 兼容的 SD/MMC 控制器里,状态寄存器会有一个专门的位来记录这个错误。它的物理含义很直白:控制器要把数据发给 eMMC 设备,但在发送的某个瞬间,控制器内部 TX FIFO 里的数据不够了,发送逻辑只能空等,于是硬件便置位欠载标志。
数据流路径是这样的:CPU 或 DMA 把内存中的数据搬到控制器的发送 FIFO,控制器再按照 SD 总线时序把 FIFO 里的数据一位一位移位发给 eMMC。如果 DMA 搬数据的速度跟不上总线发送速度,FIFO 就会在某个时刻被读空,TXUNDERR 就来了。
这里有个重要认知:eMMC 本身并不慢,掉链子的是主机侧。eMMC 写操作是主机主动推数据,设备端只是不断接收。如果设备端出现了 Replay 或 CRC 错误,那是另一码事。TXUNDERR 明确指出问题出在主机控制器的数据供给链路,重点检查方向应该是 DMA 配置、FIFO 深度、时钟频率和总线带宽。
1.2 为什么“只写不读”是重大线索
很多开发者在看到 TXUNDERR 时会一头雾水,因为读操作完全不报错。其实这正是定位问题的关键。读路径和写路径虽然共用同一条 SD 总线,但数据搬运方向完全相反:
- 读操作:eMMC 把数据放到总线上,控制器接收进 FIFO,再由 DMA 搬回内存。这个过程中,控制器是被动接收,只要 FIFO 设置够深、DMA 响应够快,不会出现“接收过载”问题。
- 写操作:控制器要先从内存取数据填充 FIFO,再按总线时钟发送。FIFO 是消耗品,DMA 必须在发送完之前持续补充。
所以,只写不读的 TXUNDERR,说明控制器接收方向、总线信号质量、eMMC 响应都正常,唯独写方向的数据供给能力不足。这个线索直接把排查范围缩小到了三件事:DMA 描述符是否配置正确、FIFO 水位/调节策略是否合理、系统总线带宽是否在写操作期间被其他外设抢占。
1.3 “只有冷启动出现”又说明了什么
如果你还留意到,错误只在完全断电后重新上电时出现,软重启或热启动后一切正常,那这个线索的价值更高。热启动时,虽然 CPU 和操作系统重新引导,但很多硬件域并没有真正下电,时钟锁相环(PLL)、电源调节器、控制器的模拟前端大多保持着上一次工作时的稳定状态。冷启动则完全不同:所有电路从零开始上电,PLL 需要时间锁定,电源电压需要时间爬升,晶振起振也需要时间。
这意味着,如果是纯粹的配置错误,通常每次都会必现,不会和“冷启动”绑定。能和冷启动绑定,大概率是初始化时序与某种硬件稳定窗口之间的竞争。比如驱动在 PLL 还没完全锁定时就把时钟切换到 200MHz,或者电源电压还在爬升过程中就发起了大数据量写入。这种问题具有典型的第一次访问踩雷特征:初始化完成后系统一切正常,只有刚启动那一刻会炸。
2. 排查前的三板斧:复现、日志、内核基线
2.1 先让问题稳定复现
排查这类间歇性问题,第一件事不是改配置,而是建立稳定的复现方法。如果连问题都无法按意愿复现,后面所有验证都是空中楼阁。
我的做法是写一段冷启动后的自动写压力脚本,放在系统启动服务里,开机后立刻执行。脚本内容不复杂,核心就是连续做几次大块写入并同步落盘:
#!/bin/bash # /usr/local/bin/repro-txunderr.sh # 冷启动后自动复现 eMMC 写错误 LOG=/var/log/txunderr-test.log echo "$(date): start write test" >> $LOG mount /dev/mmcblk1p2 /mnt for i in 1 2 3 4 5; do echo "$(date): round $i" >> $LOG dd if=/dev/urandom of=/mnt/test.img bs=4k count=65536 conv=fsync 2>&1 >> $LOG sync sleep 1 done echo "$(date): done" >> $LOG这里用conv=fsync很关键。它保证 dd 返回前数据已经真正写进设备,而不是停留在页缓存里。如果不加这个参数,可能 dmesg 里根本没来得及报错,dd 就“成功”结束了,容易误判问题不存在。
连续复现三次以上才认为问题可靠。如果只是偶然一次,我会先怀疑是上电瞬间的偶发时钟抖动,而不是必然的配置缺陷,优先级会往后放。
2.2 动态调试与寄存器抓取
复现脚本就位后,下一步是打开内核相关区域的动态日志。主要是打开 mmc core 和 sdhci host 驱动的调试输出:
echo 'file drivers/mmc/core/* +p' > /sys/kernel/debug/dynamic_debug/control echo 'file drivers/mmc/host/sdhci* +p' > /sys/kernel/debug/dynamic_debug/control如果你的内核没有打开 dynamic debug,也可以直接修改内核日志级别,或者改用 ftrace 抓函数调用:
mount -t tracefs nodev /sys/kernel/tracing echo 1 > /sys/kernel/tracing/events/mmc/enable cat /sys/kernel/tracing/trace_pipe打开日志后,在复现一次错误,然后用 dmesg 抓完整输出。除了标题里的 TXUNDERR,还要注意三类信息:
- 中断状态寄存器的原始值
- 是否伴随 ADMA 错误描述符
- 出错的命令号,常见写操作是 CMD24(单块写)或 CMD25(多块写)
如果驱动在错误时打印了寄存器 dump,那信息量极大。通常里面会有当前 DMA 系统地址,这个地址指向的数据位置可以反推到底是哪一段写入触发了欠载。
2.3 内核版本和补丁基线
很多 TXUNDERR 问题实际上是旧内核已知 bug,或者缺少某个关键补丁导致的。排查前先确认三件事:内核版本是多少、SDHCI 驱动来自哪个厂商树、设备树是否有过修改。
我在这次排查中就发现,老版本内核对 SDHCI 在 HS200 模式下的错误恢复处理不够完善,错误发生一次后经常直接卡死,而不是自动重试降频。升级到带完整错误恢复补丁的内核后,即使偶尔出错也能自动恢复,不会再表现为“写文件失败”。所以不要嫌弃这种“折腾内核版本”的笨办法,它有时能直接解决一半问题。
3. 核心原因拆解:时钟、FIFO 与 DMA 的三方博弈
3.1 时钟频率:从 HS200 降到 HS/DDR 验证
整个问题最值得怀疑的对象是时钟配置。现在很多板子的 eMMC 都跑在 HS200 模式,总线时钟 200MHz,数据线采用 1.8V 电平。到了 HS200 这个速度,信号上升沿时间、走线长度、PCB 阻抗、芯片引脚寄生电容都会成为影响因素。冷启动瞬间,电源和时钟还没稳定,高频信号的质量会更差。
设备树里通常长这样:
&sdmmc1 { pinctrl-names = "default", "state_100mhz", "state_200mhz"; pinctrl-0 = <&mmc1_pins_default>; pinctrl-1 = <&mmc1_pins_100mhz>; pinctrl-2 = <&mmc1_pins_200mhz>; bus-width = <8>; mmc-hs200-1_8v; cap-mmc-hw-reset; max-frequency = <200000000>; };验证方法很简单:把max-frequency临时改成100000000,让 eMMC 跑在 100MHz 的 DDR 模式或普通 HS 模式,然后冷启动测试写入。如果错误消失,说明问题确实和高速模式强相关。
不过要留个心眼:降低频率只是验证手段,不一定是最终修复。工程上不能靠降频交付,因为 eMMC 顺序写性能可能会掉到原来的一半甚至更低。所以降频测试确认方向后,还要继续找根本原因。
3.2 FIFO 欠载与 DMA 配置细节
时钟频率高,只是让欠载更容易暴露。真正让 TXUNDERR 发生的直接原因,还是 DMA 来不及往 FIFO 补数据。这里有几个高频踩坑点。
第一个是缓存一致性。写数据的内存页刚被 CPU 写修改过,而这些数据还在 CPU cache 里,没有回写,DMA 直接读取内存读到的是旧数据。更麻烦的是,如果 DMA 映射使用的是非 coherent 映射且没有正确执行 cache clean,在某些平台上表现为偶发数据错误或欠载。排查时可以在写路径里强制做一次 flush_dcache_range 试试,虽然不优雅,但能验证方向。
第二个是 ADMA 描述符的跨页处理。ADMA2 协议要求描述符的地址和长度有严格的 4KB 对齐规则,跨页时需要拆分成多个描述符。如果底层 SG 表构建逻辑有问题,某个描述符可能会在跨页边界卡住,DMA 停顿一段时间,FIFO 就空了。这类问题通常只在大块连续写入时出现,和冷启动没有直接关系,但会叠加在冷启动事件上让你更难定位。
第三个是系统总线带宽竞争。写 eMMC 的同时,如果有其他高带宽外设比如 USB、WiFi SDIO 在大量搬运数据,内存总线仲裁可能让 eMMC 的 DMA 请求被延后。这个因素在正常运行时一般不至于触发欠载,但在冷启动瞬间,如果网卡在疯狂加载固件、USB 在枚举设备、存储同时在预读根文件系统,几种负载叠加,就可能在某一瞬间把总线带宽占满。这个可以通过错峰启动服务来验证。
3.3 冷启动时序:bootloader 残留与电源稳定
还有一个容易被忽略的方向是 bootloader 的状态残留。U-Boot 等 bootloader 在启动过程中通常会初始化 eMMC,用来读取 kernel 和设备树。如果你用的是同一套 U-Boot,它在跳转内核前如果把 eMMC 控制器设置在了某个模式,但没做完整复位,内核的 MMC 驱动可能基于一个“不干净”的状态重新初始化。
这种情况下,典型表现是:热启动正常,因为控制器状态已经被上一轮初始化“校准”过;冷启动出错,因为控制器从上电默认状态开始,却继承了 bootloader 写入的部分残留配置,两者冲突。解决思路是在 bootloader 跳转前对 eMMC 控制器执行一次全复位,或者在内核 dts 中开启mmc-hw-reset,让 MMC core 在初始化时对设备做硬件复位。
电源稳定同样关键。eMMC 的 VCC 和 VCCQ 如果由同一路 LDO 或 DCDC 供电,上电瞬间电压爬升需要时间。如果内核启动太快,DMA 开始在电压还没稳定时搬运数据,控制器内部的 PLL 可能还没锁定,FIFO 的时钟也是抖的。冷启动时序检测其实可以很简单:在启动脚本里加一个 sleep 2 再执行写入。如果错误立即消失,说明就是“上电后太早访问”的问题。
4. 完整实操记录:从复现到修复
4.1 复现现场与寄存器 dump 解读
我手头这台设备的现象很典型:SDMMC1 接 8GB eMMC,eMMC 工作于 HS200 模式,系统启动后第一次大文件写入必然报错,报错后不影响后续使用,但 dmesg 里已经刷满了寄存器 dump。
复现后的关键日志摘录如下:
[ 23.456789] sdhci: =========== REGISTER DUMP (mmc1)=========== [ 23.456791] sdhci: Sys addr: 0x00000000 | Version: 0x00000002 [ 23.456792] sdhci: Error: 0x00000000 | Int Status: 0x00000001 [ 23.456793] sdhci: Cmd: 0x0000191b | Data: 0x00000000 [ 23.456794] sdhci: Present state: 0x00000000 [ 23.456795] sdhci: Host ctl: 0x00000000 | Power ctl: 0x00000000 [ 23.456796] sdhci: Block count: 0x00010000 | Bloom addr: 0x00000000 [ 23.456797] sdhci: ----------- mmc1 -------------- [ 23.456801] mmc1: Timeout waiting for hardware interrupt.这里的Int Status: 0x00000001在不同厂商的 SDHCI 兼容控制器里,bit 位置定义可能不同,但这台平台上 bit0 正是 TXUNDERR。同时注意到 Block count 是 0x10000,也就是 65536 块,正好对应我之前测试脚本里的一次 256MB 写入任务。
日志里还有一条重要信息:Timeout waiting for hardware interrupt。这说明 TXUNDERR 发生后,控制器没有正常触发写完成中断,驱动只能超时退出。这也是为什么错误一旦出现,那次写操作就彻底失败,而不是“偶发重试后成功”。
4.2 逐项验证:频率、延时、复位
拿到日志后,我按序做了四组对照实验:
第一组,关闭 HS200,把 max-frequency 降到 100MHz。冷启动写入测试连续跑了 10 次,一次错误都没出现。这基本确认了问题与 200MHz 高时钟强相关。
第二组,恢复 200MHz,但启动脚本加一个sleep 2再执行写入。错误同样消失。这说明不是单纯的 200MHz 频率下不能工作,而是冷启动后太早工作会踩雷。
第三组,不降频也不 sleep,但在每次写入前先做一次小规模写入预热。结果首次写入依然报错,第二次开始正常。这个结果进一步强化了“启动稳定窗口”假设。
第四组,把 dts 里加上cap-mmc-hw-reset,并在内核驱动初始化时对 eMMC 做一次硬件复位。结果冷启动后的首次写入依然有概率报错,这说明问题不只是设备端残留状态,更多是控制器时钟/电源域不稳定。
综合四组实验,问题模型非常清晰:冷启动后,eMMC 控制器所在的时钟域尚未完全稳定,一旦立刻切到 200MHz 高频模式并执行大块数据写,TX FIFO 在高速发送时会被瞬时读空,触发欠载。
4.3 最终修复方案与验证
最终我没有采用降低最高频率的方案,而是调整了初始化时序。具体做了两处修改:
第一处,在设备树中保留 HS200 能力,但增加上电顺序描述,确保 eMMC 的供电 GPIO 在控制器初始化前已经拉高并延迟稳定:
&sdmmc1 { pinctrl-names = "default", "state_100mhz", "state_200mhz"; pinctrl-0 = <&mmc1_pins_default>; pinctrl-1 = <&mmc1_pins_100mhz>; pinctrl-2 = <&mmc1_pins_200mhz>; bus-width = <8>; mmc-hs200-1_8v; cap-mmc-hw-reset; max-frequency = <200000000>; vmmc-supply = <&vcc_3v3>; vqmmc-supply = <&vcc_1v8>; };第二处,在内核驱动或初始化脚本中,让 eMMC 先以低频率完成枚举,再在 MMC core 的调优流程中切换到 200MHz。这个流程在内核里原本就有,但有些厂商的 BSP 修改导致调优跳过了这一步骤。重新打开 HS200 tuning 之后,控制器会在发送大数据前先做一轮带数据的调优,实际上给了时钟稳定窗口。
两处修改后,我做了完整的冷启动回归测试:连续断电冷启动 20 次,每次都执行同样的 256MB 写入,结果是零错误。后续读操作、软重启、长时间压力测试也都正常。eMMC 的写入速度保持在 HS200 模式下的正常水准,没有牺牲性能。
5. 常见问题速查与避坑心得
5.1 快速定位表
这个问题在论坛和技术群里的出现频率不低。我整理了一个快速定位表,方便你直接对照症状排查:
| 症状特征 | 可能原因 | 快速验证手段 |
|---|---|---|
| 仅冷启动后首次写报 TXUNDERR | PLL/电源稳定窗口不足 | 启动脚本加 sleep 2 后写入 |
| 降频到 100MHz 后消失 | HS200/高速模式信号余量不足 | 改 dts 中 max-frequency |
| 热启动后偶发 | DMA 缓存一致性/描述符问题 | 开启 DMA API debug |
| 带 WiFi/USB 高负载时触发 | 内存总线争抢 | 错峰启动高带宽外设 |
| bootloader 改动后开始出现 | 控制器残留状态 | dts 中开启 mmc-hw-reset |
| 内核升级后出现 | 驱动/调优路径被修改 | 回退内核版本对比 |
另外,如果日志里同时出现 ADMA error 或者 DMA address 指向明显无效内存,优先怀疑驱动里的 SG 表构建逻辑,而不是时钟问题。这两类错误的修复方向完全不同。
5.2 画蛇添足的操作,别做
排查过程中有几个操作看起来合理,实际会误导你很久,我挨个说下:
不要在没有确认 PLL 和时钟树关系的情况下随便改 clk 频率。有的平台 eMMC 时钟和总线时钟共享一个 PLL,改 eMMC 频率会连带影响系统总线频率,反而引入新问题。改之前先看 SoC 的时钟树文档。
不要在 dts 里直接删除mmc-hs200-1_8v来“规避”问题。这等于把 eMMC 永久降到低性能模式,治标不治本。而且如果 eMMC 初始化时已经切到 1.8V 信号,你再在 dts 里禁掉 HS200,可能造成电平不匹配,问题更隐蔽。
不要忽略 bootloader 的 MMC 配置。排查 Linux 内核前,先用 U-Boot 的 mmc 命令手动读写一遍 eMMC,确认 bootloader 阶段是否也有同样问题。如果 U-Boot 里就能复现写入错误,说明是更底层的硬件/时序问题,内核怎么改都没用。
5.3 特殊场景补充:从 U 盘启动写入 eMMC
如果你是在 Armbian 这类系统中用 U 盘启动,然后把系统写入 eMMC,大概率会遇到和这篇文章类似的问题。这个场景有个额外风险:U 盘和 eMMC 可能同时工作,U 盘本身质量参差不齐,如果 U 盘控制器的写延迟不稳定,会拖累系统整体 IO,放大 eMMC 侧的高速欠载问题。
建议先确认你写入 eMMC 时的源设备是谁。如果是从 U 盘 dd 镜像到 eMMC,系统在读取 U 盘数据的同时要向 eMMC 写入,两路 IO 并发,总线压力更大。这种情况下,如果错误只在“U 盘启动后写入 eMMC”时出现,换一个质量更好的 U 盘,或者先把镜像复制到内存 tmpfs 再写入 eMMC,都有可能消除表面症状。
这个特殊场景对很多刷机用户的现实意义在于:你的 eMMC 硬件未必有问题,TXUNDERR 可能只是“U 盘读 + eMMC 写”并发时总线上的一次偶然掉链子。判断方法很简单:把 dd 的 if 改成 /dev/zero,如果问题消失,说明源盘读取路径确实加重了总线负担。
最后:这类问题其实是个好的“系统体检”
排查完这个 TXUNDERR,我最大的体会是:看似偶然的硬件错误,背后往往藏着一整套时序和资源竞争的问题。它逼着你把时钟树、DMA 路径、bootloader 行为、内核初始化顺序全部过一遍,比看十篇文档都管用。
以后你再遇到类似的“冷启动才出现、只在某个方向报错”的问题,不妨先画一条数据通路,把每个环节的稳定时间列出来,再对照日志逐步逼近。多数时候,问题不是出在你以为的那个环节,而是它的上游还没来得及ready。