← ブログ一覧へ
FPGAZynq可靠性看门狗ECCTMR故障恢复工业控制FreeRTOS

Zynq 实战 22|系统可靠性设计:看门狗、ECC 内存保护与故障恢复

この記事は中国語で書かれ、Google 翻訳で自動翻訳されています。
中国語の原文を見る →

Zynq 实战 22|系统可靠性设计:看门狗、ECC 内存保护与故障恢复

这是《Zynq FPGA 嵌入式系统设计实战》系列的第 22 篇。 板子:Pynq-Z2(XC7Z020-1CLG400C)。工具链:Vivado / Vitis / PetaLinux 2023.2。 上一篇:《Zynq 实战 21|Vitis AI 全流程:把训练好的模型跑在 PL 加速器上》


0. 这一篇要解决什么问题

消费电子系统挂了重启就行,但工业控制器和汽车 ECU 不行——一台注塑机的 PLC 崩了可能导致模具损坏,一个 ADAS 子系统的软错误可能造成安全事故。

MTBF(平均无故障时间)目标通常是 10 万小时(约 11.4 年),这意味着系统必须主动防御以下几类故障:

  • 软件死锁 / 任务挂起:看门狗(WDT)
  • 内存位翻转(SEU,Single Event Upset):ECC 纠错
  • 逻辑电路单点故障:TMR 三模冗余
  • 故障后无法恢复:分级重启 + 安全状态机

这一篇在 Pynq-Z2 上逐一实现这四个机制,给出完整的可运行代码。


1. 故障模式全景

Zynq 可靠性防御体系 故障源 软件死锁 / 任务挂起 超时无响应,系统假死 内存位翻转(SEU) 宇宙射线 / 电磁噪声 PL 逻辑单点故障 温度 / 电压漂移 OS 崩溃 / Panic 内核异常,系统不可用 防御机制 多级 WDT(PS SWDT + 自定义) XScuWdt · 任务级 + 系统级 ECC 内存保护 BRAM ECC(Pynq-Z2 DDR 不支持) TMR 三模冗余(PL 逻辑) 3x 面积代价,2/3 多数表决 分级故障恢复 热重启 → 冷重启 → 安全状态 目标指标 覆盖率 > 99% 任务级 <100ms 超时 1-bit 自动纠正 2-bit 检测并告警 单点故障不影响输出 SIL-2 / ASIL-B 级 MTBF > 100,000h 5 秒内恢复服务 本篇实现平台:Pynq-Z2(XC7Z020) DDR:16-bit,不支持 ECC → 改用 BRAM ECC 保护关键数据 | SEU @ 地面:约 10⁻¹⁰ 翻转/bit/天 工具链:Vitis 2023.2(Baremetal / FreeRTOS)| 内核 panic 自动重启:kernel.panic=5
图 1. Zynq 可靠性防御体系——四类故障源与对应机制

2. 看门狗(WDT):多级超时检测

Zynq-7000 的 PS 侧有两个硬件看门狗:

看门狗名称时钟源超时精度触发动作
SWDT(系统 WDT)PS7_WDTAPU_FREQ(CPU 时钟÷N)1ms 级系统复位
SCU WDTSCUWDTCPU_3x2x = 333MHz/2约 3ns 精度私有 CPU 中断

工业场景建议用双级策略

  • 任务级 WDT(SCU WDT):每个关键任务必须在 100ms 内喂狗,否则触发中断,尝试软恢复
  • 系统级 WDT(SWDT):超时 5s 触发硬复位,处理软恢复失败的情形

2.1 SCU WDT 初始化与喂狗

/*
 * wdt_demo.c — Pynq-Z2 多级看门狗演示
 *
 * 构建:在 Vitis 2023.2 中新建 Standalone BSP 项目,
 *       或 FreeRTOS 项目(BSP 配置方式相同)。
 * 依赖:xscuwdt.h(Xilinx SCU WDT 驱动,BSP 自带)
 *
 * 测试:在 JTAG 模式下运行,用串口观察输出;
 *       屏蔽喂狗调用,验证看门狗触发后的中断处理。
 */

#include <stdio.h>
#include "xscuwdt.h"
#include "xscugic.h"
#include "xil_exception.h"
#include "xparameters.h"

/* ── 硬件参数 ── */
#define WDT_DEVICE_ID      XPAR_SCUWDT_0_DEVICE_ID
#define GIC_DEVICE_ID      XPAR_SCUGIC_SINGLE_DEVICE_ID
#define WDT_IRQID          XPAR_SCUWDT_INTR

/*
 * 超时配置:
 * SCU WDT 时钟 = CPU_3x2x = 666MHz / 2 = 333MHz
 * 预分频因子(Prescaler)= 255(最大,降低时钟到 333M/256 ≈ 1.3MHz)
 * Load 值 = 超时时间(秒)× 时钟频率 - 1
 *
 * 任务级超时 100ms:Load = 0.1 × 1.3M ≈ 130,000
 * 系统级超时  5s:使用 PS 系统 WDT(SWDT),配置更长时间
 */
#define WDT_PRESCALER      0xFF      /* 最大预分频,降低到约 1.3MHz */
#define WDT_LOAD_TASK_100MS  130000  /* 100ms 任务级超时 */

static XScuWdt   WdtInstance;
static XScuGic   GicInstance;

/* 系统级故障计数(持久化到不掉电的寄存器,实际产品用 BBRAM 或 Flash) */
static volatile u32 fault_count     = 0;
static volatile u32 watchdog_fired  = 0;

/*
 * 看门狗中断服务程序(ISR)
 * 任务级 WDT 超时时进入这里,尝试软恢复
 */
static void WdtIntrHandler(void *CallBackRef)
{
    XScuWdt *WdtInstPtr = (XScuWdt *)CallBackRef;

    /* 确认是 WDT 中断(非误触发) */
    if (!XScuWdt_IsTimerExpired(WdtInstPtr))
        return;

    watchdog_fired = 1;
    fault_count++;

    xil_printf("\r\n[WDT ISR] 任务级看门狗超时!fault_count=%d\r\n", fault_count);

    if (fault_count <= 3) {
        /* 前 3 次:软恢复——重新初始化关键任务状态,继续喂狗 */
        xil_printf("[WDT ISR] 尝试软恢复(热重启任务)...\r\n");
        /* 实际项目:在这里重置任务状态机、清理互斥锁、重新初始化外设 */

        /* 继续喂狗,给软恢复一次机会 */
        XScuWdt_RestartWdt(WdtInstPtr);
        watchdog_fired = 0;
    } else {
        /* 超过 3 次:升级到系统级重启(触发 SWDT 或 SRST) */
        xil_printf("[WDT ISR] 软恢复失败 %d 次,触发系统复位!\r\n", fault_count);
        /*
         * 触发 Zynq PS 系统复位:写 SLCR REBOOT_STATUS 寄存器
         * 或者直接让 SWDT 超时,不再喂它
         */
        /* 进入无限循环,等待 SWDT 系统级超时触发硬复位 */
        while (1) { /* SWDT 会在 5s 内触发 PS 复位 */ }
    }
}

/*
 * 初始化 SCU WDT 为任务级看门狗(100ms 超时,中断模式)
 * 注意:SCU WDT 有两种模式:
 *   - Timer Mode(定时器):超时仅触发中断,不复位系统
 *   - Watchdog Mode:超时触发 CPU 复位信号
 * 我们用 Timer Mode,中断里自己决定要不要复位。
 */
static int InitWatchdog(void)
{
    XScuWdt_Config *ConfigPtr;
    int Status;

    /* 查找设备配置(从 xparameters.h 生成) */
    ConfigPtr = XScuWdt_LookupConfig(WDT_DEVICE_ID);
    if (!ConfigPtr) {
        xil_printf("错误:找不到 WDT 配置(DEVICE_ID=%d\r\n", WDT_DEVICE_ID);
        return XST_FAILURE;
    }

    /* 初始化驱动 */
    Status = XScuWdt_CfgInitialize(&WdtInstance, ConfigPtr,
                                    ConfigPtr->BaseAddr);
    if (Status != XST_SUCCESS) return Status;

    /* 自检 */
    Status = XScuWdt_SelfTest(&WdtInstance);
    if (Status != XST_SUCCESS) {
        xil_printf("错误:WDT 自检失败\r\n");
        return Status;
    }

    /* 配置为 Timer Mode(非 Watchdog Mode) */
    XScuWdt_SetTimerMode(&WdtInstance);

    /* 设置预分频和载入值 */
    XScuWdt_SetControlRegister(&WdtInstance,
        (WDT_PRESCALER << XSCUWDT_CONTROL_PRESCALER_SHIFT) |
        XSCUWDT_CONTROL_IT_ENABLE_MASK);  /* 使能中断 */

    XScuWdt_LoadWdt(&WdtInstance, WDT_LOAD_TASK_100MS);

    xil_printf("[WDT] 初始化完成,超时 ~100ms\r\n");
    return XST_SUCCESS;
}

/*
 * 初始化 GIC,注册 WDT 中断
 */
static int SetupInterrupt(void)
{
    XScuGic_Config *GicConfig;
    int Status;

    GicConfig = XScuGic_LookupConfig(GIC_DEVICE_ID);
    if (!GicConfig) return XST_FAILURE;

    Status = XScuGic_CfgInitialize(&GicInstance, GicConfig,
                                    GicConfig->CpuBaseAddress);
    if (Status != XST_SUCCESS) return Status;

    /* 注册 WDT 中断处理函数 */
    Status = XScuGic_Connect(&GicInstance, WDT_IRQID,
                              (Xil_ExceptionHandler)WdtIntrHandler,
                              &WdtInstance);
    if (Status != XST_SUCCESS) return Status;

    XScuGic_Enable(&GicInstance, WDT_IRQID);

    /* 使能 ARM 异常处理 */
    Xil_ExceptionInit();
    Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_IRQ_INT,
        (Xil_ExceptionHandler)XScuGic_InterruptHandler, &GicInstance);
    Xil_ExceptionEnable();

    xil_printf("[GIC] WDT 中断注册完成(IRQ_ID=%d\r\n", WDT_IRQID);
    return XST_SUCCESS;
}

/*
 * 主函数:模拟一个需要定期喂狗的控制循环
 */
int main(void)
{
    int Status;
    u32 loop_count = 0;

    xil_printf("\r\n=== Zynq 多级看门狗演示 ===\r\n");

    Status = SetupInterrupt();
    if (Status != XST_SUCCESS) {
        xil_printf("GIC 初始化失败!\r\n");
        return -1;
    }

    Status = InitWatchdog();
    if (Status != XST_SUCCESS) {
        xil_printf("WDT 初始化失败!\r\n");
        return -1;
    }

    /* 启动 WDT 计时 */
    XScuWdt_Start(&WdtInstance);
    xil_printf("[主循环] WDT 已启动,每 50ms 喂一次狗\r\n");

    while (1) {
        loop_count++;

        /* 模拟工作任务(这里只是延时) */
        /* 实际项目:在这里执行传感器读取、控制算法、通信处理等 */
        for (volatile u32 i = 0; i < 500000; i++);  /* ~50ms @ 666MHz */

        /* ── 任务完成,喂狗 ──
         * RestartWdt 把计数器重新载入 WDT_LOAD_TASK_100MS 值,
         * 重置 100ms 超时窗口。
         */
        if (!watchdog_fired) {
            XScuWdt_RestartWdt(&WdtInstance);
        }

        /* 打印心跳(实际产品删掉,减少串口开销) */
        if (loop_count % 20 == 0) {
            xil_printf("[主循环] tick=%d, fault_count=%d\r\n",
                       loop_count, fault_count);
        }

        /* 模拟第 100 次循环时任务死锁(用于测试 WDT) */
        /* 把下面这行注释去掉,观察 WDT 触发 */
        /* if (loop_count == 100) while(1); */
    }

    return 0;  /* 不会到达 */
}

2.2 PS 系统看门狗(SWDT)配置

SWDT(System Watch-Dog Timer,PS7_WDT)在系统级,超时直接触发 PS 复位,不经过中断。在 Vitis BSP 配置里启用:

/*
 * swdt_enable.c — 启用 PS 系统级看门狗(5 秒系统复位)
 * 配合 SCU WDT 任务级看门狗使用
 *
 * 包含头文件:xwdtps.h(PSS WDT 驱动)
 */
#include "xwdtps.h"
#include "xparameters.h"

#define SWDT_DEVICE_ID  XPAR_XWDTPS_0_DEVICE_ID

static XWdtPs SwdtInstance;

int InitSystemWatchdog(void)
{
    XWdtPs_Config *ConfigPtr;
    int Status;

    ConfigPtr = XWdtPs_LookupConfig(SWDT_DEVICE_ID);
    if (!ConfigPtr) return XST_FAILURE;

    Status = XWdtPs_CfgInitialize(&SwdtInstance, ConfigPtr,
                                   ConfigPtr->BaseAddress);
    if (Status != XST_SUCCESS) return Status;

    /*
     * 超时配置:
     * SWDT 时钟源 = CPU_1x(约 111MHz,CPU_6x4x/6)
     * 预分频 = 4096(XWDTPS_CLK_PRESCALE_4096)
     * 计数值 = 0xFFF(最大,约 150 秒)
     * 实际需要的 5 秒:CounterValue = 5 × 111M/4096 ≈ 135500
     * 选最接近的 2 的幂次表示(SWDT 计数器是 12-bit)
     *
     * 简化处理:用 XWDTPS_CLK_PRESCALE_4096 + 最大计数 4095 ≈ 150s
     * 实际产品建议根据 UG585 Table 11-2 精确计算
     */
    XWdtPs_SetControlValues(&SwdtInstance,
        XWDTPS_CLK_PRESCALE_4096,
        0xFFF);  /* 约 150 秒,确保 SCU WDT 总是先触发 */

    /* 复位模式:超时触发 PS 系统复位(不是仅中断) */
    XWdtPs_EnableOutput(&SwdtInstance, XWDTPS_RESET_SIGNAL);

    XWdtPs_Start(&SwdtInstance);

    /* 主循环里定期调用此函数喂狗(比 SCU WDT 频率低) */
    xil_printf("[SWDT] 系统级看门狗已启动,超时约 150s\r\n");
    return XST_SUCCESS;
}

/* 每隔 1 秒调用一次,只要软恢复在进行就继续喂 */
void FeedSystemWatchdog(void)
{
    XWdtPs_RestartWdt(&SwdtInstance);
}

3. ECC 内存保护:Pynq-Z2 的限制与替代方案

3.1 为什么 Pynq-Z2 不支持 DDR ECC

Zynq-7000 DDR 控制器支持 ECC 模式,但 ECC 需要 72-bit 内存总线(64-bit 数据 + 8-bit ECC)。

Pynq-Z2 板子上的 DDR3L 芯片是 16-bit 接口(两片 8-bit 芯片并联),物理上无法提供额外的 8-bit ECC 位。这是 PCB 设计决定的,软件层面无法绕过。

特性Pynq-Z2(7Z020)ZCU102(XCZU9EG)
DDR 数据宽度16-bit64-bit
ECC 支持❌ 不支持✅ 支持(72-bit)
ECC 配置方式UG1085 DDR 控制器配置

🚧 避坑:Pynq-Z2 的 DDR 是 16-bit 宽,不是 64-bit 宽。网上很多文章照搬 ZCU102 的 ECC 配置方法,在 Pynq-Z2 上完全不适用。如果你在 PS7 配置里尝试开启 ECC,PS 会进入死循环(内存读写全部出错),系统无法启动。不要试。

3.2 替代方案:BRAM ECC

Zynq 的 BRAM(Block RAM)支持内置 ECC,可以用于保护关键数据(控制参数、状态表、历史记录)。在 Vivado 的 Block Design 里,用 FIFO GeneratorBlock Memory Generator IP,勾选 ECC 选项:

# Vivado Tcl 脚本:配置带 ECC 的 Block Memory Generator
create_bd_cell -type ip -vlnv xilinx.com:ip:blk_mem_gen:8.4 bram_ecc_0

# 配置参数
set_property -dict [list \
    CONFIG.Memory_Type        {True_Dual_Port_RAM} \
    CONFIG.Use_ECC            {true} \
    CONFIG.ECC_Type           {Hamming_Code_ECC} \
    CONFIG.Enable_32bit_Address {false} \
    CONFIG.Write_Width_A      {32} \
    CONFIG.Read_Width_A       {32} \
    CONFIG.Write_Depth_A      {1024} \
    CONFIG.Register_PortA_Output_of_Memory_Primitives {true} \
] [get_bd_cells bram_ecc_0]

Vivado Block Memory Generator 的 ECC 选项说明:

参数含义
ECC_TypeHamming_Code_ECC汉明码,1-bit 纠正,2-bit 检测
Use_ECCtrue使能 ECC,实际占用额外 8-bit 存储空间(内部透明)
输出端口 dbiterr单比特1 = 检测到不可纠正的 2-bit 错误
输出端口 sbiterr单比特1 = 检测到并已纠正 1-bit 错误

在 AXI 驱动代码里读取 ECC 状态:

/*
 * bram_ecc_check.c — 读取 BRAM ECC 错误状态
 * 假设 BRAM ECC IP 挂在 AXI4 接口,基地址 0x40000000
 */
#include <stdio.h>
#include "xil_io.h"

#define BRAM_BASE       0x40000000UL
/* Block Memory Generator ECC 状态寄存器(UG473 表 3-4) */
#define BRAM_ECC_STATUS 0x0           /* 偏移 0:ECC 状态 */
#define BRAM_SBE_COUNT  0x4           /* 偏移 4:单比特错误计数 */
#define BRAM_DBE_COUNT  0x8           /* 偏移 8:双比特错误计数 */

void CheckBramEcc(void)
{
    u32 status = Xil_In32(BRAM_BASE + BRAM_ECC_STATUS);
    u32 sbe    = Xil_In32(BRAM_BASE + BRAM_SBE_COUNT);
    u32 dbe    = Xil_In32(BRAM_BASE + BRAM_DBE_COUNT);

    if (sbe > 0) {
        /* 单比特错误:BRAM 已自动纠正,记录告警 */
        xil_printf("[ECC] 单比特错误(已纠正)计数: %d\r\n", sbe);
    }
    if (dbe > 0) {
        /* 双比特错误:无法纠正,必须触发系统恢复 */
        xil_printf("[ECC] 严重:双比特错误(不可纠正)计数: %d\r\n", dbe);
        /* 上报错误,触发故障恢复流程 */
    }

    /* 清除错误计数(写 1 清零) */
    if (sbe > 0 || dbe > 0) {
        Xil_Out32(BRAM_BASE + BRAM_ECC_STATUS, 0x3);  /* 清 sbiterr + dbiterr */
    }
}

4. TMR 三模冗余(PL 逻辑)

TMR(Triple Modular Redundancy)是最经典的硬件冗余方案:同一逻辑复制三份,用多数表决器(Majority Voter)决定输出。任意一个模块故障,其他两个模块的正确输出会覆盖它。

代价:面积 × 3,功耗 × 3,时序分析需要三份副本都满足约束。

4.1 Verilog 实现(关键寄存器 TMR)

/*
 * tmr_register.v — 三模冗余寄存器组
 *
 * 用途:保护关键控制寄存器(例如:电机速度设定值、安全停机阈值)
 * 仿真:Vivado Behavioral Simulation,注入故障验证表决器
 *
 * 参数:
 *   DATA_WIDTH:寄存器宽度,默认 32-bit
 */

module tmr_register #(
    parameter DATA_WIDTH = 32
) (
    input  wire                  clk,
    input  wire                  rst_n,    // 同步复位,低有效
    input  wire                  we,       // 写使能
    input  wire [DATA_WIDTH-1:0] din,      // 写入数据
    output wire [DATA_WIDTH-1:0] dout,     // 多数表决后的安全输出
    output wire                  error_flag // 1 = 检测到三模不一致(有单元故障)
);

    // ── 三份独立寄存器 ──
    (* keep = "true" *) reg [DATA_WIDTH-1:0] reg_a;  // 副本 A
    (* keep = "true" *) reg [DATA_WIDTH-1:0] reg_b;  // 副本 B
    (* keep = "true" *) reg [DATA_WIDTH-1:0] reg_c;  // 副本 C
    // (* keep = "true" *) 防止综合器把三份合并为一份

    // ── 同步写入(三份同时写) ──
    always @(posedge clk) begin
        if (!rst_n) begin
            reg_a <= {DATA_WIDTH{1'b0}};
            reg_b <= {DATA_WIDTH{1'b0}};
            reg_c <= {DATA_WIDTH{1'b0}};
        end else if (we) begin
            reg_a <= din;
            reg_b <= din;
            reg_c <= din;
        end
    end

    // ── 多数表决器(逐比特) ──
    // 三个值中,至少两个相同的值为"正确值"
    // 真值表:(A·B) | (B·C) | (A·C)
    genvar i;
    generate
        for (i = 0; i < DATA_WIDTH; i = i + 1) begin : majority_voter
            assign dout[i] = (reg_a[i] & reg_b[i]) |
                             (reg_b[i] & reg_c[i]) |
                             (reg_a[i] & reg_c[i]);
        end
    endgenerate

    // ── 错误检测:三份不全相同时报警 ──
    assign error_flag = (reg_a != reg_b) | (reg_b != reg_c) | (reg_a != reg_c);

endmodule

4.2 Vivado 综合约束

为了确保三个副本被放置到不同的 PL 区域(避免相同的局部故障同时击中三个副本):

# tmr_constraints.xdc — Pynq-Z2 (XC7Z020) TMR 布局约束

# 把三个副本分别约束到 PL 的不同区域(XC7Z020 有 3 行 CLB)
set_property LOC SLICE_X0Y50  [get_cells {tmr_reg_inst/reg_a[*]}]
set_property LOC SLICE_X20Y50 [get_cells {tmr_reg_inst/reg_b[*]}]
set_property LOC SLICE_X40Y50 [get_cells {tmr_reg_inst/reg_c[*]}]

🚧 避坑:TMR 的面积代价是 3 倍,不是简单的 1+ε。在 Pynq-Z2 这种资源有限的板子上(53k LUT),一个 32-bit TMR 寄存器组消耗约 100 个 LUT(表决器逻辑 + 三份寄存器)。如果你要保护 100 个寄存器,TMR 本身就要 10k LUT——占整片 PL 的 19%。需要权衡保护范围,只保护真正关键的寄存器(安全停机阈值、运行模式状态),而不是所有数据路径。


5. 故障恢复:分级重启策略

5.1 Linux 内核 panic 自动重启

在 PetaLinux 工程的 system-user.dtsi 或 U-Boot 启动参数里设置:

# 方法 1:bootargs(最常用)
# 在 PetaLinux project-spec/meta-user/recipes-bsp/u-boot/files/platform-top.h
# 或 system-user.dtsi 里修改 chosen 节点
bootargs = "console=ttyPS0,115200 root=/dev/mmcblk0p2 rw cma=512M panic=5"
#                                                               ^^^^^^^^
# panic=5 表示内核 panic 后等待 5 秒,然后自动硬重启

# 方法 2:运行时修改(立即生效,重启后失效)
echo 5 > /proc/sys/kernel/panic

# 方法 3:写到 /etc/sysctl.conf(持久化)
echo "kernel.panic = 5" >> /etc/sysctl.conf

5.2 分级恢复状态机(C 代码)

/*
 * recovery_fsm.c — 分级故障恢复状态机
 *
 * 恢复级别:
 *   Level 0:正常运行
 *   Level 1:热重启(仅重新初始化软件状态,不重置硬件)
 *   Level 2:冷重启(重置所有外设 + 重新初始化,约 500ms)
 *   Level 3:安全状态(停止所有输出,保持最小安全状态,等待人工干预)
 *
 * 使用场景:工业控制器的故障恢复策略
 */

#include <stdio.h>
#include "xil_printf.h"
#include "sleep.h"  /* usleep / sleep */

typedef enum {
    RECOVERY_NORMAL     = 0,
    RECOVERY_WARM_RESET = 1,
    RECOVERY_COLD_RESET = 2,
    RECOVERY_SAFE_STATE = 3,
} RecoveryLevel_t;

typedef struct {
    RecoveryLevel_t level;
    u32             consecutive_faults;
    u32             total_faults;
    u32             last_fault_timestamp;  /* 简化:用循环计数代替实际时间戳 */
} RecoveryState_t;

static RecoveryState_t recovery_state = {
    .level                = RECOVERY_NORMAL,
    .consecutive_faults   = 0,
    .total_faults         = 0,
    .last_fault_timestamp = 0,
};

/* 模拟"停止所有输出到安全状态"的硬件操作 */
static void EnterSafeState(void)
{
    xil_printf("[安全状态] 停止所有 PWM 输出\r\n");
    xil_printf("[安全状态] 关闭继电器,切断执行器电源\r\n");
    xil_printf("[安全状态] 激活刹车信号\r\n");
    /* 实际项目:写 PL 侧 IO 寄存器,强制所有输出到安全位置 */
}

/* 热重启:仅重置软件状态,外设硬件不重启(< 100ms) */
static void WarmReset(void)
{
    xil_printf("[热重启] 重置任务状态机...\r\n");
    xil_printf("[热重启] 清理互斥锁...\r\n");
    xil_printf("[热重启] 重新订阅传感器数据...\r\n");
    usleep(50000);  /* 50ms 初始化延迟 */
    xil_printf("[热重启] 完成,恢复正常运行\r\n");
}

/* 冷重启:重置所有外设,重新初始化(约 500ms) */
static void ColdReset(void)
{
    xil_printf("[冷重启] 停止所有任务...\r\n");
    EnterSafeState();  /* 先进安全状态,再重启 */
    xil_printf("[冷重启] 复位所有外设驱动...\r\n");
    usleep(200000);  /* 200ms 外设复位延迟 */
    xil_printf("[冷重启] 重新初始化通信接口...\r\n");
    usleep(200000);  /* 200ms 链路重建延迟 */
    recovery_state.consecutive_faults = 0;
    xil_printf("[冷重启] 完成,恢复正常运行\r\n");
}

/*
 * 故障处理入口:根据故障次数决定恢复级别
 * 调用者:WDT ISR、ECC 错误处理、任务监控
 */
void HandleFault(const char *fault_desc)
{
    recovery_state.total_faults++;
    recovery_state.consecutive_faults++;

    xil_printf("\r\n[故障处理] %s (连续=%d, 累计=%d)\r\n",
               fault_desc,
               recovery_state.consecutive_faults,
               recovery_state.total_faults);

    if (recovery_state.consecutive_faults <= 2) {
        /* 1-2 次:热重启 */
        recovery_state.level = RECOVERY_WARM_RESET;
        WarmReset();
    } else if (recovery_state.consecutive_faults <= 5) {
        /* 3-5 次:冷重启 */
        recovery_state.level = RECOVERY_COLD_RESET;
        ColdReset();
    } else {
        /* 6 次以上:进入安全状态,等待人工干预 */
        recovery_state.level = RECOVERY_SAFE_STATE;
        xil_printf("[故障处理] 升级到安全状态,等待人工干预\r\n");
        EnterSafeState();
        /* 在这里发送告警(CAN 总线、以太网、串口) */
        while (1) {
            xil_printf("[安全状态] 系统已停机,等待复位...\r\n");
            sleep(10);
        }
    }
}

/* 任务正常完成时调用,重置连续故障计数 */
void ReportTaskSuccess(void)
{
    if (recovery_state.consecutive_faults > 0) {
        xil_printf("[恢复] 任务连续成功,重置故障计数\r\n");
        recovery_state.consecutive_faults = 0;
        recovery_state.level = RECOVERY_NORMAL;
    }
}

6. SEU 概率与加固策略参考

地面环境(无辐射加固)的 SEU 统计数据:

场景SEU 概率说明
地面低海拔(< 1000m)~10⁻¹⁰ 翻转/bit/天宇宙射线中子通量低
高海拔(> 5000m)~10⁻⁸ 翻转/bit/天中子通量约 100x 增加
航空(10km 巡航高度)~10⁻⁷ 翻转/bit/天民航设备的设计基准
近地轨道太空~10⁻⁵ 翻转/bit/天需要专用抗辐射器件

对于 Pynq-Z2 上 53k LUT(约 200KB 配置 SRAM):

  • 地面低海拔:每天期望约 0.02 次翻转——基本不会发生
  • 高海拔工业场景:每天期望约 2 次翻转——需要 BRAM ECC + TMR

🚧 避坑:PL 逻辑的 **Configuration Memory(CRAM)**本身也可能被 SEU 翻转,导致 FPGA 逻辑异常。Xilinx 提供 SEU Controller IP(免费,随 Vivado 附带)可以定期扫描 CRAM 并修正错误。在高可靠场景,必须在 Block Design 里加入这个 IP,并配置为定期 Scrubbing(建议间隔 1-100ms,根据 SEU 率设定)。


7. 本篇 Checklist

  • SCU WDT 初始化,100ms 任务级超时,中断 ISR 能捕获并记录
  • SWDT 系统级看门狗启动,超时 > SCU WDT,触发 PS 硬复位
  • 理解 Pynq-Z2 DDR 16-bit 不支持 ECC 的原因
  • BRAM ECC 在 Vivado Block Memory Generator 里配置,sbiterr/dbiterr 端口连接到中断或状态寄存器
  • TMR 寄存器模块综合后确认三份副本未被合并(检查 utilization report)
  • 分级恢复状态机验证:连续故障 1-2 次热重启,3-5 次冷重启,6+ 次安全状态
  • Linux 侧 kernel.panic = 5 写入 /etc/sysctl.conf,重启后验证

8. 下一篇预告

下一篇 《Zynq 实战 23|混合关键性系统:TrustZone 隔离 + FreeRTOS + Linux 并存》,我们会:

  • 用 ARM TrustZone 把 CPU0(Linux)和 CPU1(FreeRTOS)隔离到不同安全域
  • 配置 TZPC/TZASC 设定内存访问权限
  • 实现 CPU1 FreeRTOS 任务响应时间 < 10µs,Linux 无法干扰
  • 捋清楚 FSBL 里怎么唤醒 CPU1,以及地址对齐的坑

参考资料

文档号名称用途
UG585Zynq-7000 SoC TRM第 11 章:SWDT;第 13 章:SCU WDT;DDR 控制器 ECC 配置
UG4737 Series FPGAs Memory ResourcesBRAM ECC 功能,Hamming Code 位宽要求
PG058FIFO Generator Product GuideBRAM FIFO 的 ECC 使能选项
XAPP1098SEU Strategies for Zynq-7000 DevicesSEU 概率数据,CRAM Scrubbing 策略
DS190Zynq-7000 SoC Data Sheet7Z020 资源上限,DDR 总线位宽

所有文档均可在 AMD 官方文档页 免费下载。