27、可靠性设计:看门狗定时器、三模冗余(TMR)、CRC校验、EDAC纠错码

做FPGA开发,尤其是工业级或航天级项目,最怕什么?

怕死机,怕数据出错,怕单粒子翻转。

我早年做一款电力保护装置,设备在强电磁干扰环境下跑着跑着就“死”了。复位按钮按下去,又活了。但现场工人不可能天天盯着它按复位。从那以后,我对可靠性设计就特别上心。

今天聊的这四个技术——看门狗、三模冗余、CRC校验、EDAC纠错码,说白了就是给FPGA上“保险”。

27.1 看门狗定时器(Watchdog Timer)

看门狗是啥?就是一个“监工”。

你的主程序每隔一段时间得跟它打个招呼:“我还活着”。如果监工发现你超过时间没打招呼,它就认为你死机了,直接给你来一个硬复位。

27.1.1 工作原理

看门狗本质上是一个递减计数器。主程序在正常工作循环中,定期“喂狗”——把计数器重新置为最大值。如果计数器减到0,说明没人喂它,就触发复位信号。

关键点:喂狗的位置要选对。不能放在中断里,否则主程序卡死了,中断还能喂狗,看门狗就形同虚设了。

27.1.2 紫光同创FPGA实现

紫光的PDS软件里,我们可以用软核实现看门狗,也可以用逻辑自己搭一个。我个人习惯用纯逻辑实现,不依赖软核,这样更可靠。

// 简单的看门狗模块
module watchdog (
    input  wire        clk,
    input  wire        rst_n,
    input  wire        feed_dog,   // 喂狗信号
    output reg         wdt_rst_n   // 看门狗复位输出
);

parameter TIMEOUT_CNT = 32'd100_000_000; // 假设1秒超时(100MHz时钟)

reg [31:0] cnt;
reg        timeout_flag;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        cnt <= 32'd0;
        timeout_flag <= 1'b0;
    end else if (feed_dog) begin
        cnt <= 32'd0;               // 喂狗,计数器清零
        timeout_flag <= 1'b0;
    end else if (cnt >= TIMEOUT_CNT) begin
        timeout_flag <= 1'b1;       // 超时
    end else begin
        cnt <= cnt + 1'b1;
    end
end

// 产生复位脉冲
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        wdt_rst_n <= 1'b0;
    else if (timeout_flag)
        wdt_rst_n <= 1'b0;          // 超时后拉低复位
    else
        wdt_rst_n <= 1'b1;
end

endmodule

我的经验:喂狗周期建议设为超时时间的1/3到1/2。比如超时1秒,每300ms喂一次。这样即使程序偶尔抖动一下,也不会误触发复位。

27.2 三模冗余(TMR)

三模冗余,英文叫Triple Modular Redundancy。名字听着高大上,原理其实很简单:

把同一个逻辑复制三份,三个模块同时跑。输出时用“三取二”表决器,取两个相同的结果作为最终输出。

27.2.1 为什么需要TMR?

在太空或高辐射环境里,高能粒子可能击中FPGA的某个寄存器,导致它的值翻转——这就是单粒子翻转(SEU)。

你想想看,一个关键的状态机,如果某个bit被翻转了,可能直接从“空闲”状态跳到“故障”状态。后果不堪设想。

TMR就是对付这个的。三个模块同时被击中的概率极低,所以只要两个模块结果一致,我们就相信它是正确的。

27.2.2 表决器实现

// 三取二表决器
module voter (
    input  wire a, b, c,
    output wire y
);

assign y = (a & b) | (b & c) | (a & c);

endmodule

这个电路很简单,但很实用。三个输入里,只要任意两个是1,输出就是1。

注意:TMR不是万能的。它只能对付单点故障。如果三个模块的时钟树或电源网络是共享的,那一个电源故障就能干掉所有三份逻辑。所以,真正的TMR设计,时钟、电源、复位都要做冗余。

27.2.3 什么时候用TMR?

  • 状态机:关键状态机建议做TMR,比如通信协议的状态机
  • 配置寄存器:控制关键参数的寄存器
  • 计数器:长时间运行的计数器,比如时间戳

我做过一个卫星通信项目,里面的所有关键状态机都做了TMR。说实话,资源消耗翻了三倍不止,但为了可靠性,值。

27.3 CRC校验

CRC(Cyclic Redundancy Check)是数据通信里最常用的校验方式。它不像奇偶校验那么简单,也不像MD5那么复杂,属于“刚刚好”的那种。

27.3.1 CRC原理简述

发送端把数据看作一个多项式,除以一个约定的生成多项式,余数就是CRC校验码。接收端用同样的方式计算,如果余数为0,说明数据没出错。

说白了,就是做多项式除法。只不过在硬件里,我们用移位寄存器和异或门来实现。

27.3.2 紫光FPGA上的CRC实现

紫光的PDS里提供了CRC硬核,可以直接调用。但如果你需要自定义多项式,也可以用逻辑实现。

// CRC-16-CCITT 实现 (多项式 x^16 + x^12 + x^5 + 1)
module crc16_ccitt (
    input  wire        clk,
    input  wire        rst_n,
    input  wire        data_in,
    input  wire        data_valid,
    output reg [15:0]  crc_out
);

reg [15:0] crc_reg;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        crc_reg <= 16'hFFFF;  // 初始值
    else if (data_valid) begin
        crc_reg[0]  <= crc_reg[15] ^ data_in;
        crc_reg[1]  <= crc_reg[0];
        crc_reg[2]  <= crc_reg[1];
        crc_reg[3]  <= crc_reg[2];
        crc_reg[4]  <= crc_reg[3];
        crc_reg[5]  <= crc_reg[4] ^ crc_reg[15] ^ data_in;
        crc_reg[6]  <= crc_reg[5];
        crc_reg[7]  <= crc_reg[6];
        crc_reg[8]  <= crc_reg[7];
        crc_reg[9]  <= crc_reg[8];
        crc_reg[10] <= crc_reg[9];
        crc_reg[11] <= crc_reg[10];
        crc_reg[12] <= crc_reg[11] ^ crc_reg[15] ^ data_in;
        crc_reg[13] <= crc_reg[12];
        crc_reg[14] <= crc_reg[13];
        crc_reg[15] <= crc_reg[14];
    end
end

assign crc_out = crc_reg;

endmodule

避坑指南:我曾经在一个项目里,CRC计算一直不对。查了半天,发现是字节序搞反了。发送端是MSB first,接收端用了LSB first。记住,通信双方的多项式、初始值、输出异或值、字节序,这四个参数必须完全一致。

27.4 EDAC纠错码

EDAC(Error Detection And Correction)比CRC更进一步。CRC只能发现错误,EDAC还能纠正错误。

最常用的是汉明码(Hamming Code)。它能纠正1位错误,检测2位错误。

27.4.1 汉明码原理

汉明码在数据位之间插入校验位。校验位的位置是2的幂次方(第1、2、4、8...位)。每个校验位负责校验一组数据位。

当数据出错时,通过校验位的组合,可以定位到具体是哪一位错了,然后取反纠正。

数据位宽 校验位数量 总位宽 纠错能力
4 3 7 纠正1位
8 4 12 纠正1位
16 5 21 纠正1位
32 6 38 纠正1位

27.4.2 紫光FPGA中的EDAC应用

紫光的某些系列FPGA,内部BRAM自带EDAC功能。你只需要在PDS里勾选一个选项,硬件自动帮你做纠错。

但如果你用的是不带EDAC的BRAM,或者你想在自定义逻辑里用,那就得自己实现了。

// 简化的汉明码编码(8位数据 -> 12位码字)
// 校验位位置: p1(bit1), p2(bit2), p4(bit4), p8(bit8)
module hamming_encoder (
    input  wire [7:0] data_in,
    output wire [11:0] code_out
);

wire p1, p2, p4, p8;

// 计算校验位
assign p1 = data_in[0] ^ data_in[1] ^ data_in[3] ^ data_in[4] ^ data_in[6];
assign p2 = data_in[0] ^ data_in[2] ^ data_in[3] ^ data_in[5] ^ data_in[6];
assign p4 = data_in[1] ^ data_in[2] ^ data_in[3] ^ data_in[7];
assign p8 = data_in[4] ^ data_in[5] ^ data_in[6] ^ data_in[7];

// 组合码字
assign code_out = {data_in[7:4], p8, data_in[3:1], p4, data_in[0], p2, p1};

endmodule

重要提醒:EDAC不是免费的午餐。它需要额外的存储空间和延迟。每32位数据要额外6位校验位,存储效率约84%。而且编码和解码都会引入几个时钟周期的延迟。

27.5 如何选择?

这四个技术不是互斥的,实际项目中往往是组合使用:

  • 看门狗:防止程序跑飞,所有系统必备
  • TMR:对付单粒子翻转,航天、核工业必备
  • CRC:通信链路校验,几乎所有接口都要用
  • EDAC:保护存储数据,BRAM或DDR内存必备

我做过一个综合项目:主控状态机用TMR,内部BRAM开EDAC,通信链路加CRC,最外层再套一个看门狗。四层防护,设备在高温高湿的工业现场跑了三年没出过一次故障。

嗯,可靠性设计就是这样——平时看着多余,出问题的时候才知道它的价值。