Zynq 实战 22|系统可靠性设计:看门狗、ECC 内存保护与故障恢复
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. 故障模式全景
2. 看门狗(WDT):多级超时检测
Zynq-7000 的 PS 侧有两个硬件看门狗:
| 看门狗 | 名称 | 时钟源 | 超时精度 | 触发动作 |
|---|---|---|---|---|
| SWDT(系统 WDT) | PS7_WDT | APU_FREQ(CPU 时钟÷N) | 1ms 级 | 系统复位 |
| SCU WDT | SCUWDT | CPU_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-bit | 64-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 Generator 或 Block 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_Type | Hamming_Code_ECC | 汉明码,1-bit 纠正,2-bit 检测 |
Use_ECC | true | 使能 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,以及地址对齐的坑
参考资料
| 文档号 | 名称 | 用途 |
|---|---|---|
| UG585 | Zynq-7000 SoC TRM | 第 11 章:SWDT;第 13 章:SCU WDT;DDR 控制器 ECC 配置 |
| UG473 | 7 Series FPGAs Memory Resources | BRAM ECC 功能,Hamming Code 位宽要求 |
| PG058 | FIFO Generator Product Guide | BRAM FIFO 的 ECC 使能选项 |
| XAPP1098 | SEU Strategies for Zynq-7000 Devices | SEU 概率数据,CRAM Scrubbing 策略 |
| DS190 | Zynq-7000 SoC Data Sheet | 7Z020 资源上限,DDR 总线位宽 |
所有文档均可在 AMD 官方文档页 免费下载。