DDR inline ECC 落地与错误上报实战:那 2 台”查无病因”的机器,逼我把内存纠错从”知道”做到”用上”

2026年9月28日 0 By admin

上一期写训练余量的时候埋了个尾巴:那 7 台现场返修的机器,5 台定位到了训练余量不足,剩下 2 台查了一圈始终没找到病因——余量健康、颗粒批次正常、温压扫描干净,但客户就是反馈”隔几周死一次机”。

后来在老化房里泡了一周,抓到一次现场复现:死机前几分钟,DDR 上出现了一次单 bit 读错误,被我们的后台巡检脚本偶然记了下来。也就是说,这不是设计缺陷,是运行期随机位翻转——软错误(Soft Error),来源可能是宇宙射线、颗粒老化、电源噪声的瞬时耦合,防不胜防。

信号设计解决不了的问题,就得换武器:ECC。这一期就把 inline ECC 从选型、落地到错误上报的完整过程捋一遍,坑点密集,建议收藏。

先补课:ECC 到底在纠什么

先说清楚概念,避免混淆三种”校验”:

手段 能力 成本 典型位置
奇偶校验 只能检出奇数个 bit 错,不能定位不能纠 1 bit 总线、接口
CRC 检错能力强,但不能纠错 按多项式长度 通信、存储块校验
ECC(Hamming 系) 纠正 1 bit 错(SBE),检出 2 bit 错(DBE) 每 64 bit 数据约 8 bit 校验位 DDR 控制器、ECC 内存条

我们用的是最典型的 SECDED(Single Error Correction, Double Error Detection)Hamming 码:64 bit 数据配 8 bit 校验位,即俗称的 72 bit 布局。这个比例不是拍脑袋——校验位再少纠不了单 bit,再多成本不划算;云服务器上跑了几十年的标准配置,直接抄作业就行。

关键认知转变:ECC 不是把内存变得”不出错”,而是把”偶发出错”从致命故障降级成一条可记录的事件。SBE 被硬件当场纠正,程序无感;DBE 才是需要处理的真故障。这个降级能力,就是单机设备可靠性和服务器可用性之间的那道墙。

inline ECC 的三种形态,选错形态白忙活

“给 DDR 加 ECC”在嵌入式里有三条路,成本和能力差别巨大:

  1. 控制器 inline ECC(本期主角):DDR 控制器在写数据时自动算校验位,存到 DRAM 的保留区域;读的时候自动校验、纠正。CPU 完全无感,软件只需要处理错误中断和上报。用普通 x8/x16 颗粒,拿一部分容量当校验空间。
  2. ECC 专用颗粒:直接采购带 9 bit 数据位宽的 DDR 颗粒(x72/x36 模组或专用料)。能力最强,但料贵、货期长,很多项目根本拿不到。
  3. 软件 ECC:固件自己做校验块管理。典型如 NAND Flash 的 ECC 搬到 DDR 上,每次读写都要跑计算,性能损失一个数量级,只适合小容量、低频访问的场景。

选型判断就一句话:控制器支持 inline ECC,就用 inline;不支持,评估专用颗粒;都不行,重新评估这个产品的可靠性需求是不是被高估了。

要注意老平台经常没有这条路。我们一块老的 i.MX6 板子,MMDC 对 ECC 的支持有严格的颗粒位宽和容量限制,评估完发现根本落不了地;而 i.MX8M 系列、TI AM64x 这类新平台的控制器都原生支持 inline ECC,u-boot 阶段配置一下就能开。换平台的时候把”是否支持 inline ECC”写进选型表,这个习惯值一次返修的钱。

落地第一步:划 ECC 区域,想清楚保护谁

inline ECC 不是全有或全无,大多数控制器允许按地址区域(region)使能。这一步要做的决策是:哪些内存值得用容量换可靠性。

我们的策略:

  • 内核/应用关键段、长期驻留的数据缓冲区:划进 ECC region。这些区域一翻转就是死机或数据损坏,值得保护;
  • 图形帧缓冲、大块临时 scratch buffer:留在非 ECC 区。一帧画面错几个像素没人看得出来,拿 12.5% 容量去保护它纯属浪费;
  • ECC 校验位本身:控制器从 region 对应的 DRAM 末尾或者独立地址段扣除,具体规则看 TRM,这部分容量对应用不可见,uboot 的内存布局表里会体现。

算一笔账:64 bit 数据 8 bit 校验,ECC region 的有效容量是配置容量的 8/9 ≈ 89%。所以 ECC region 不是越大越好,划得越精细,浪费越少——但也别走极端,region 划太多块,管理复杂度和踩坑面都在涨。我们的经验是两到三块 region 封顶:一块保护关键数据,一块保护常驻缓冲,剩下的放弃保护。

落地第二步:初始化,不擦除就是”错误风暴”

这是整个 inline ECC 落地里最大的一个坑,我们在这上面烧了两天。

逻辑是这样的:ECC 校验位存在 DRAM 里,而 DRAM 上电后的内容是随机的。如果 ECC 使能之后,你去读一个”从未写过”的地址,控制器会拿随机数据算出的校验位去比对随机内容——大概率校验失败,直接给你报一个 SBE。

更糟的是如果系统把 DDR 当作”上电即随机、按需写入”的裸内存用(很多 boot 流程确实如此),使能 ECC 的瞬间开始,所有未初始化区域的读访问都会持续报错,中断把 CPU 打满,看起来就像”一开 ECC 就死机”。

正确姿势是先擦除(scrub),再使能:在 ECC 使能前或使能后立即(取决于控制器要求,看 TRM),把整个 ECC region 用 DMA 或 memset 写一遍零,让校验位和数据同步归零。示意流程:

/* 示意:ECC region 使能前的全量初始化(scrub) */
void ddr_ecc_scrub_region(uint32_t start, uint32_t size)
{
    uint32_t *p = (uint32_t *)start;
    uint32_t *end = (uint32_t *)(start + size);

    /* 用 64 bit 或 cacheline 粒度的写,比逐字快得多;
       DMA 搬运零块更快,但注意别把 DMA 也绕过 cache 搞出一致性问题 */
    while (p < end) {
        *p++ = 0;
        if (((uint32_t)p & 0xFFF) == 0) {
            feed_watchdog();    /* 2GB region 刷一遍不短,喂狗 */
        }
    }
    /* 关键:scrub 完刷 cache + 内存屏障,确保真写到 DDR */
    __asm volatile("dsb sy");
    __asm volatile("isb");
}

两个细节:

  • scrub 期间必须喂狗,否则大 region 初始化本身就是一次”看门狗复位”;
  • scrub 完成的时机要在内存被任何代码读之前。如果 uboot 已经在 DDR 里放了 DTB 再进内核,内核侧再开 ECC 就已经晚了——ECC 的使能点必须在 boot 早期确定,这也是为什么这个配置通常死死焊在 bootloader 阶段。

落地第三步:SBE 之后要回写,不然单错熬成双错

SECDED 能纠一个 bit,但有个隐藏问题:纠错只发生在读路径上。某个 bit 坏了,你不读那个地址,错误就一直躺在那;等到第二个 bit 也坏了,SBE 变 DBE,直接从”可纠正”跳到”致命”。

所以成熟的控制器会提供 scrub 回写机制:检测到 SBE 并纠正后,把纠正后的数据写回 DRAM,把坏 bit “洗掉”。大多数平台这个功能是个配置位,但默认关着。

我们的配置清单(通用性描述,具体寄存器名查各平台 TRM):

  • ☑ SBE 纠正后自动回写(scrub on corrected error):开;
  • ☑ SBE 触发中断:开(用于计数和上报);
  • ☑ DBE 触发中断或直接 fault:开(不能静默);
  • ☑ 周期性后台 scrub(部分平台支持硬件自动定时扫全 region):开,把”没人读的区域”里的潜伏错误也洗掉。

周期 scrub 的频率是笔交易:扫得勤,错误存活期短,但内存带宽被占用;扫得懒,带宽省了,风险窗口拉长。我们的设备读多写少、带宽余量足,配置成 10 分钟扫一遍 ECC region,占用带宽不到 1%,没感知。

错误上报:把”内存事件”变成”运维数据”

ECC 真正的价值不只是当场纠错,而是错误数据本身——它告诉你这批设备的内存健康度,是预测性维护的原始素材。上报链路我们是这样搭的(老读者会发现这就是之前 MQTT 那套通道的复用):

/* 示意:ECC 错误中断服务(伪代码) */
void ddr_ecc_isr(void)
{
    uint32_t err_status = ddrc_read(ERR_STATUS);

    if (err_status & ST_SBE) {
        uint32_t addr = ddrc_read(ERR_ADDR);
        uint32_t syndrome = ddrc_read(ERR_SYNDROME);

        /* 记录:地址、syndrome(可定位到 bit 位)、时间戳 */
        ecc_log_append(addr, syndrome, get_uptime_ms());
        g_ecc_stats.sbe_count++;

        /* 阈值告警:单机 SBE 短期暴涨 = 颗粒或电源异常 */
        if (g_ecc_stats.sbe_count - g_ecc_stats.sbe_reported
                >= SBE_REPORT_THRESHOLD) {
            health_report_flag = true;   /* 由上报任务走 MQTT 发出 */
            g_ecc_stats.sbe_reported = g_ecc_stats.sbe_count;
        }
    }

    if (err_status & ST_DBE) {
        uint32_t addr = ddrc_read(ERR_ADDR);
        /* DBE 是真故障:记录 + 主动重启,别指望程序自己挺过去 */
        crashlog_write(CRASH_DBE, addr);
        system_emergency_reset("DDR double-bit error");
    }

    ddrc_write(ERR_STATUS, err_status);   /* 写 1 清中断,别忘了 */
}

上报的数据结构保持精简,一段典型的设备健康上报长这样:

{
  "ts": 1727500800,
  "mem": {
    "sbe_total": 3,
    "sbe_last24h": 1,
    "dbe_total": 0,
    "last_event": {
      "type": "sbe",
      "addr": "0x81234000",
      "bit": "d7",
      "uptime_s": 864123
    }
  }
}

运维侧的用法:

  • 单机 SBE 稳定为 0~偶尔 1:正常,宇宙射线级别,不用管;
  • 单机 SBE 周期性或持续增长:颗粒老化或个别单元劣化,列入观察名单,下次到现场换料;
  • 某一批次设备 SBE 集中偏高:批次性问题(颗粒、焊接、电源),触发质量分析;
  • 出现 DBE:按故障处理,重启后拉取 crashlog 复盘。

这套数据跑了半年,最大的收获是:我们第一次有了”内存健康”这个维度,而不是等到死机了才知道内存不行了。

和训练余量的关系:ECC 不是遮羞布

必须泼一盆冷水:ECC 是兜底,不是遮羞布。

如果一块板子训练余量本来就不足(上一期那 5 台),边沿长期踩线,ECC 上去之后 SBE 计数会非常难看——它把原本”看不见的持续出错”变成了”看得见的持续报错”。这不是 ECC 的功劳,是信号问题的体检报告。

正确的分工是:信号完整性问题在设计和训练余量管理阶段解决;ECC 负责解决”设计正确也挡不住的随机错误”。落地 ECC 之后,SBE 统计反而成了信号质量的长期监控手段——两件事形成闭环,而不是用后者掩盖前者。

成本与收益:一笔明账

给新版本板子上 inline ECC,我们付出的和拿回的:

维度 付出/收益 说明
容量 −11%(ECC region 内) 2GB 保护的 region 有效 1.78GB,按需规划
带宽 写路径少量开销 + 周期 scrub <1% 实测无感知
延迟 读路径增加校验/纠正逻辑 纳秒级,远低于访存本身
Boot 复杂度 +scrub 流程 2GB 全量刷一遍约数秒,串在启动里
软件工作量 中断、日志、上报链路 复用既有健康上报通道,增量约两周
收益:SBE 随机单 bit 翻转全部免疫 那 2 台”查无病因”的类别直接消灭
收益:DBE 从”神秘死机”变为”有记录、可复盘” 故障定性时间从周级降到分钟级
收益:运维 内存健康数据纳入预测性维护 批次性问题提前数月暴露

对于跑在现场、几年不开机箱的设备,这笔账非常划算。如果是消费级、生命周期短的产品,那就另说了——可靠性预算要花在刀刃上。

怎么验证 ECC 真的在干活

ECC 最大的麻烦是”平时看不见”,你得主动制造错误来验证链路。三板斧:

  1. 错误注入寄存器:不少平台的调试接口支持错误注入(强制把某个 bit 的校验算错),注入 SBE → 验证中断、日志、计数、上报全链路;注入 DBE → 验证崩溃记录和重启路径。这是首选,安全可控;
  2. 读写后物理翻转:没有注入功能时,在已知地址写数据、通过调试器直接改 DRAM 某个 bit(绕过控制器)、再读回,观察是否被纠正。注意时机和 cache,容易翻车,建议只在 bring-up 阶段用;
  3. 温度/电压应力:把板子推到极限工况跑压测,观察 SBE 计数。健康设计应该几乎为 0;如果应力下 SBE 暴涨,先回头查信号,别急着感谢 ECC”救了场”。

我们的验收标准:注入 SBE,10 秒内云端看到计数 +1 且地址记录正确;注入 DBE,设备重启且 crashlog 含 DBE 字样。两条过了,ECC 链路才算真正”上线”。

踩坑清单:inline ECC 落地 7 坑

  1. ECC 使能前不 scrub:未初始化区域读访问触发 SBE 风暴,看起来像”开了就死机”;
  2. scrub 不喂狗:大 region 初始化跑几分钟,看门狗先把板子复位了;
  3. ECC 使能点放错位置:放在内核里才开,前面 bootloader 写过的内存校验位对不上,启动即崩;
  4. SBE 只纠不写回:坏 bit 长期潜伏,等第二个 bit 一起爆发成 DBE;
  5. 关 DBE 中断图清净:DBE 静默丢弃是数据损坏,比死机更恶劣——错了必须响;
  6. 把 ECC 当信号问题遮羞布:余量不足的板子加 ECC,SBE 天天刷屏,治标不治本;
  7. 容量规划忘了扣校验空间:按配置容量算缓冲区大小,跑着跑着撞上 ECC 保留区,越界写直接踩坏校验数据。

上线自查清单

  • ☑ 已确认控制器支持 inline ECC,且颗粒位宽/容量组合在支持列表内
  • ☑ ECC region 划分完成:关键数据受保护,scratch/帧缓冲按需放弃
  • ☑ ECC 使能点位于 boot 最早阶段(uboot 或更早),早于任何 DDR 读操作
  • ☑ 使能前后完成全 region scrub,scrub 循环内喂狗
  • ☑ SBE 纠正后回写(scrub on SBE)已开启
  • ☑ 周期性后台 scrub 已开启,频率与带宽预算匹配
  • ☑ SBE 中断 + 计数 + 地址/syndrome 日志已落地
  • ☑ SBE 阈值上报已接入健康数据通道(MQTT/后台)
  • ☑ DBE 中断路径验证:记录 + 主动复位,无静默丢弃
  • ☑ 错误注入验证通过:SBE 全链路 10 秒内上报,DBE 重启含 crashlog
  • ☑ 容量规划已扣除 ECC 校验空间,应用层无越界风险
  • ☑ 训练余量管理仍然独立执行,SBE 统计未替代眼图扫描

—

ECC 这套东西,是内存可靠性链条上”信号设计、训练余量”之后的最后一道闸。三件事做成闭环,DDR 才算真正让人睡得着觉。

下期预告:换个口味,回到软件侧——FreeRTOS 低功耗 Tickless 模式:把空闲任务里的空转 Tick 停掉,睡眠电流从 3mA 干到 200μA 的过程中,Tick 补偿和时间基准的那些坑。