当前位置:首页 > 工业控制 > 工业控制
[导读]工业设备的“黑匣子”不是把printf重定向到串口。现场设备死机重启后,串口缓冲没了、RAM清空了,啥也查不到。正经做法是四层:RAM环形缓冲接实时日志 → WARN/ERROR分级落盘 → HardFault抓快照 → 掉电中断写最小必要数据。普通INFO只在内存里滚,关键事件才进非易失区。


工业设备的“黑匣子”不是把printf重定向到串口。现场设备死机重启后,串口缓冲没了、RAM清空了,啥也查不到。正经做法是四层:RAM环形缓冲接实时日志 → WARN/ERROR分级落盘 → HardFault抓快照 → 掉电中断写最小必要数据。普通INFO只在内存里滚,关键事件才进非易失区。

一、存储选型先定边界

配置参数/校准:EEPROM,字节级写、偶尔改

大批量日志文件:SPI Flash,但要扇区擦除+磨损均衡

高频故障记录/状态快照:FRAM或MRAM,免擦写、写入快、掉电窗口极小

工业黑匣子场景写操作频繁且常在掉电瞬间提交,FRAM最省心:没有“先擦后写”,写一条记录就是覆盖写,掉电风险窗口远小于EEPROM/Flash。

二、RAM环形缓冲:日志先进内存

#define LOG_BUF 4096

typedef struct {

   uint32_t ts;

   uint8_t  level;

   uint16_t code;

   char     msg[64];

} log_t;


log_t ring[64];

volatile uint8_t widx, cnt;


void log_push(uint8_t lv, uint16_t code, const char*m){

   ring[widx].ts = sys_ms();

   ring[widx].level = lv;

   ring[widx].code = code;

   strncpy(ring[widx].msg, m, 63);

   widx = (widx+1)%64;

   if(cnt<64) cnt++;

}

应用层只调log_push,几微秒返回,不碰Flash、不阻塞。

分级过滤:

#define L_ERR 0x10

#define L_WARN 0x08

#define L_INFO 0x04

uint8_t g_log_mask = L_ERR|L_WARN;

三、落盘策略:分级+提交字节

FRAM里划环形日志区,每条固定长度:

typedef struct __attribute__((packed)){

   uint32_t magic;     // 0x4C4F4711

   uint32_t seq;

   uint32_t ts;

   uint8_t  level;

   uint16_t code;

   uint16_t crc;

   uint8_t  commit;    // 最后写,0x00=有效

} rec_t;


uint32_t log_head_addr;


void log_commit(rec_t*r){

   r->magic = 0x4C4F4711;

   r->seq = ++g_seq;

   r->commit = 0xFF;

   r->crc = crc16((uint8_t*)r, offsetof(rec_t,crc));

   fram_write(log_head_addr, r, sizeof(rec_t)-1);

   fram_write_byte(log_head_addr+offsetof(rec_t,commit), 0x00);

   log_head_addr = next_slot(log_head_addr);

}

掉电发生在commit字节写之前 → 重启扫描时这条不认,前面记录不受影响。

四、HardFault快照

崩溃现场最有价值的不是“死机”两个字,是寄存器:

void HardFault_Handler(void){

   uint32_t *sp = (uint32_t*)__get_MSP();

   crash_snap.r0 = sp[0]; sp[1]; sp[2]; sp[3];

   crash_snap.r12 = sp[4];

   crash_snap.lr = sp[5];

   crash_snap.pc = sp[6];

   crash_snap.xpsr = sp[7];

   crash_snap.cfsr = SCB->CFSR;

   crash_snap.hfsr = SCB->HFSR;

   log_commit(&crash_rec(L_ERR, ERR_HARDFAULT));

   NVIC_SystemReset();

}

栈高水位、任务名、最后N条RAM日志一起冻进FRAM。

五、掉电保护:只写最小必要数据

PVD或外部比较器触发EXTI中断,VDD跌到阈值立刻进power-fail:

void PVD_IRQHandler(void){

   disable_noncritical_periph();   // 关采样/通信,留能量

   fram_write(ADDR_LAST_STATE, &g_state, sizeof(g_state));

   fram_write(ADDR_LOG_HEAD, &log_head_addr, 4);

   fram_write16(ADDR_MAGIC, 0xFACE);

}

规则:

中断里不格式化、不刷全盘、不写文件

只存运行状态字+日志头指针+未刷环缓

储能电容按“写字节数×时间”留余量,100μF从5V跌到2.1V约几十ms

FRAM写512字节才亚毫秒级,Flash擦扇区几十ms扛不住

六、上电自检

void boot_scan_blackbox(void){

   if(fram_read16(ADDR_MAGIC)!=0xFACE){

       normal_boot(); return;

   }

   reset_reason = RCC->CSR & 0xFFFF;

   scan_log_region();          // 按seq连续+CRC校验

   export_crash_if_exists();

   RCC->CSR |= RCC_CSR_RMVF;

}

不连续seq、CRC错 → 最后一条丢弃;主区参数CRC错 → 回备份槽。

七、落坑清单

每条日志都写Flash → 擦写寿命耗尽

中断里sprintf → 掉电窗口被吃掉

只存文本不存seq → 掉电后顺序乱

无commit字节 → 半截记录被误认有效

HardFault只打印PC → 没CFSR/LR定位不到行

储能电容算小了 → 写完一半复位

SPI Flash不磨损均衡 → 固定扇区先坏

环缓覆盖不冻结 → 故障前现场被冲掉

不记录复位原因 → 看门狗/掉电分不清

参数和日志同区 → 互相踩坏

八、落地顺序

日志分级,INFO只进RAM环缓

ERROR/WARN+崩溃进FRAM环形区

固定长度记录:magic+seq+ts+CRC+commit

HardFault抓寄存器快照提交

PVD中断只写最小状态,关外设保能量

上电读复位标志+扫描日志区

参数区双槽冗余,CRC校验回退

串口/网口导出最近N条做现场复盘

黑匣子日志的本质不是“多记”,是关键时刻留得下、掉电丢不了、重启读得出。RAM环缓保实时性,FRAM保掉电写入,commit字节保半截记录不污染,PVD中断保最后一笔能落下,HardFault快照保死因可溯。SPI Flash不是不能做,但要自己管擦除和磨损;工业故障记录这种“少量、高频、掉电即写”的场景,FRAM方案复杂度低一个量级。真正现场排障时,一条带seq和寄存器的ERROR记录,比串口里刷几千行INFO有用得多。



本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭