嵌入式秋招必备项目(三):调试组件开发
前言
这篇主要是介绍在项目里可以用到的调试程序的组件,包括多级日志打印,日志文件系统和shell、重写hardfault异常等。其中也会穿插一些常问的问题。
基于串口的Shell组件
-
串口驱动
串口驱动的实现其实是面试的时候非常喜欢问的一个问题。可以看看你对数据流的理解,比如问你如何实现驱动向上层数据的提交,这里有两种常见的做法。- 串口接收中断(RXNE) + 环形缓冲区
当接受中断触发的时候,把接收到的字符放入缓冲区当中并唤醒接收线程;接收线程如果解析到了结束信号(比如’\n’),就把本次记录的所有数据从缓冲区中取出,向上提交(信号量唤醒之类的方式),这里可以通过有限状态机记录当前的接收状态(缓冲区溢出\正在接收等等) - 串口空闲中断 + DMA
通过DMA接收缓冲区接收数据,当串口空闲中断触发后,可以将数据从DMA缓冲区拷贝到上层数据区(DMA会记录接收了多少数据)。
这个方案是比较推荐的,因为CPU占用率会不高,当然你可能担心,那假如我的数据发送方把包分成了两份发送,那一次空闲中断触发可不能认为包接收完全了。我个人的做法是再封装一个FIFO缓冲区 + 解析线程 + 计数信号量。串口空闲中断触发后只负责把数据放入FIFO缓冲区并通过计数信号量通知解析线程,然后解析线程去扫描FIFO解析帧
因为shell命令不会太长嘛,也不会那么麻烦,所以这里直接串口空闲中断+DMA就行,触发了空闲中断就直接认为收到了一条shell指令,通知shell线程去解析即可。
- 串口接收中断(RXNE) + 环形缓冲区
-
shell实现
shell的实现这里完全相信AI就行,就是一个单向链表的增/删/改(不过它能用侵入式链表就更好了,还能吹嘘一下offset_of和container_of宏)。小小单片机可怜的运存和内存,没必要上更复杂的实现。注意互斥就行,还有shell毕竟是人机交互,优先级要给的高一些,平时反正也是阻塞着的void shell_task(void *argument) { uint8_t recv_buffer[UART_DMA_RX_BUF_SIZE]; uint16_t recv_len = 0; shell_mutex = xSemaphoreCreateMutex(); configASSERT(shell_mutex); shell_printf("Shell System Initialized. Type 'help' for commands.\r\n"); shell_register_command(&help_cmd); shell_register_command(&set_debug_cmd); shell_register_command(&get_debug_cmd); shell_register_command(&top_cmd); /*需要一个额外的定时器和设置FreeRTOS对应的宏开启*/ shell_register_command(&set_time_cmd); shell_register_command(&get_time_cmd); for (;;) { // 等待信号量(由串口 IDLE 中断给出) if (xSemaphoreTake(uart_dma_finish, portMAX_DELAY) == pdPASS) { recv_len = uart_copy_form_dma(recv_buffer,UART_DMA_RX_BUF_SIZE); recv_buffer[recv_len] = '\0'; shell_parse_multi_commands((char*)recv_buffer, recv_len); shell_printf("\r\n$ "); // 打印提示符 } } } void shell_parse_and_exec(char *cmd_line) { char *argv[8]; // 最多支持8个参数 int argc = 0; // 移除末尾的 \r\n cmd_line[strcspn(cmd_line, "\r\n")] = 0; // 字符串切分 char *token = strtok(cmd_line, " "); while (token != NULL && argc < 8) { argv[argc++] = token; token = strtok(NULL, " "); } if (argc == 0) return; // 遍历链表查找指令 xSemaphoreTake(shell_mutex, portMAX_DELAY); shell_command_t *curr = shell_commands_list; while (curr) { if (strcmp(curr->name, argv[0]) == 0) { xSemaphoreGive(shell_mutex); curr->func(argc, argv); return; } curr = curr->next; } shell_printf("Unknown command: %s\r\n", argv[0]); xSemaphoreGive(shell_mutex); }
多级日志打印
这个也是一个非常常用的调试手段,在开发初期可以用来打印不同等级日志,项目正式上线直接关了就行
typedef enum
{
LOG_LEVEL_NONE = 0, // 0: 关闭所有日志 (用于发布版/极度省电模式)
LOG_LEVEL_ERROR = 1, // 1: 错误 (系统故障、断言失败、硬件异常) -> 必须看
LOG_LEVEL_WARN = 2, // 2: 警告 (重试、参数边界、非致命异常) -> 需要关注
LOG_LEVEL_INFO = 3, // 3: 信息 (状态切换、连接成功、关键流程节点) -> 正常运行的流水账
LOG_LEVEL_DEBUG = 4, // 4: 调试 (函数入口出口、变量细节、时序分析) -> 开发期专用
} LogLevel;
extern LogLevel g_debug_level; /*可以通过shell设定*/
// 3. 核心宏逻辑
// 如果 (当前日志等级 <= 全局设定等级),则执行 printf,否则什么都不做 ((void)0)
#define LOG_PRINT(level, fmt, ...) \
do { \
if ((level) <= g_debug_level) { \
printf(fmt, ##__VA_ARGS__); \
} \
} while(0)
#define LOG_ERR(fmt, ...) LOG_PRINT(LOG_LEVEL_ERROR, "[ERR] " fmt, ##__VA_ARGS__)
#define LOG_WARN(fmt, ...) LOG_PRINT(LOG_LEVEL_WARN, "[WARN] " fmt, ##__VA_ARGS__)
#define LOG_INFO(fmt, ...) LOG_PRINT(LOG_LEVEL_INFO, "[INFO] " fmt, ##__VA_ARGS__)
#define LOG_DEBUG(fmt, ...) LOG_PRINT(LOG_LEVEL_DEBUG, "[DBG] " fmt, ##__VA_ARGS__)
/*使用的话类似这样*/
// LOG_ERR("fail to write log:%d\r\n", written_len);
// LOG_INFO("try to watch dog\r\n");
重新hardfault异常
这里主要是参考RTT的实现,
HardFault_Handler PROC ; 定义硬故障异常处理程序过程
; 获取当前上下文(判断故障发生在任务还是中断上下文)
TST lr, #0x04 ; 测试LR寄存器的第2位(EXC_RETURN[2])
; EXC_RETURN是异常返回时LR寄存器的值,第2位标识栈类型
ITE EQ ; 如果结果为0(EQ,相等),执行下一条;否则执行再下一条(条件执行)
MRSEQ r0, msp ; 若EXC_RETURN[2]=0(EQ):故障发生在中断上下文(使用主栈MSP)
; 将主栈指针MSP的值存入r0(作为后续函数的参数)
MRSNE r0, psp ; 若EXC_RETURN[2]=1(NE,不相等):故障发生在任务上下文(使用进程栈PSP)
; 将进程栈指针PSP的值存入r0(作为后续函数的参数)
STMFD r0!, {r4 - r11} ; 从r0指向的栈地址开始,递减栈指针并压入r4~r11寄存器
; 硬件自动保存r0~r3、r12、LR、PC、xPSR,此处补充保存r4~r11以完整记录上下文
IF {FPU} != "SoftVFP" ; 条件编译:如果使用硬件FPU(非软件模拟)
STMFD r0!, {lr} ; 压入LR(占位,用于后续记录FPU标志)
ENDIF ; 结束条件编译
STMFD r0!, {lr} ; 压入LR寄存器(实际存储的是EXC_RETURN值)
; 保存异常返回标志,用于后续分析故障发生的上下文环境
; 更新栈指针(因压入了新数据,栈指针位置已变化)
TST lr, #0x04 ; 再次测试EXC_RETURN[2],判断原上下文类型
ITE EQ ; 条件执行分支
MSREQ msp, r0 ; 若原上下文是中断(EQ):更新主栈指针MSP为新的栈地址(r0)
MSRNE psp, r0 ; 若原上下文是任务(NE):更新进程栈指针PSP为新的栈地址(r0)
PUSH {lr} ; 保存当前LR(EXC_RETURN)到栈中,供后续恢复使用
BL rt_hw_hard_fault_exception ; 调用C函数处理硬故障(r0已传递完整上下文的栈指针)
POP {lr} ; 从栈中恢复LR(EXC_RETURN)
ORR lr, lr, #0x04 ; 强制将EXC_RETURN[2]设为1,确保返回时使用主栈MSP
; 避免故障状态下栈配置错误导致二次异常
BX lr ; 跳转到LR指定的地址(异常返回),结束硬故障处理
ENDP ; 结束硬故障处理程序过程
ALIGN 4 ; 按4字节对齐,确保指令地址正确(ARM架构要求)
END ; 汇编程序结束
- rt_hw_hard_fault_exception:
注意此时是通过R0传参的,得到的LR的值可不是EXEC_RETURN而是对于当前函数的返回后的下一条指令地址
因为有的时候单看PC 不好确定,PC的值万一异常了全0怎么办,所以看LR最起码能知道是从哪里进入的,然后慢慢定位到当前函数
SD + LittleFS的掉电安全日志系统
上述的调试方法有时候还是不能让完全满意,这时候就可以通过SD卡加日志系统实现关键日志的全纪录,同时这里使用littleFS是因为它是掉电安全的
LittleFS介绍:https://zhuanlan.zhihu.com/p/652074347
不过LittleFS有一个不好的地方时,没办法向FAT32那样能被windows识别,这时候可以借助这个项目的实现:https://github.com/bluscape/LittleFS-Explorer-for-Windows
(不过这个项目里面关于littlefs的部分参数写死了,所以只能我们和它支持的littlefs的参数一致才可以)
- littleFS的移植
littleFS提供了相对应的接口,只要把这几个接口和SDIO驱动联系上即可- Lfs文件系统对象–lfs_config
这里的block_size是512是要和上面说的项目的lfs的参数对上,否则肯定无法识别当前的文件系统的。 LFS也是支持RTOS的,只要完善lock和unlock参数就行(一把大锁保平安),并且使能对应的宏
static uint8_t lfs_static_read_buf[LITTLEFS_READ_SIZE] __attribute__((aligned(4))); static uint8_t lfs_static_prog_buf[LITTLEFS_PROG_SIZE] __attribute__((aligned(4))); static uint32_t lfs_static_lookahead_buf[LITTLEFS_LOOKAHEAD_SIZE] __attribute__((aligned(4))); const struct lfs_config LittleFS_config = { .context = (void*)0, .read = lfs_sdmmc_read, .prog = lfs_sdmmc_prog, .erase = lfs_sdmmc_erase, .sync = lfs_sdmmc_sync, .lock = lfs_sdmmc_lock, .unlock = lfs_sdmmc_unlock, .read_size = LITTLEFS_READ_SIZE, .prog_size = LITTLEFS_PROG_SIZE, .block_size = 512, /*原本是4096 为了兼容内个LFS FOR WINDOWS*/ .block_count = 25600, /*目前是12.5MB*/ .block_cycles = 500, .cache_size = LITTLEFS_CACHE_SIZE, /*和SD的的物理扇区大小一致 512KB*/ .lookahead_size = LITTLEFS_LOOKAHEAD_SIZE, .read_buffer = lfs_static_read_buf, // 填入地址 .prog_buffer = lfs_static_prog_buf, // 填入地址 (可同上) .lookahead_buffer = lfs_static_lookahead_buf, // 填入地址 };- read/write
LFS设计的初衷是为FLASH设计的,所以它给下层驱动提供的接口都是地址而并非SD卡的扇区,但好在地址和扇区的转换是线性的,做点计算就行int lfs_sdmmc_read(const struct lfs_config *lfsc, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) { //printf("read..\r\n"); configASSERT(lfsc); /*计算flash的起始地址*/ uint64_t flash_addr = block * lfsc->block_size + off; /*把FALSH地址转换为扇区地址之后再操作 并计算扇区数量*/ uint32_t sector_addr = flash_addr / LITTLEFS_SD_SECTOR_SIZE; uint32_t sec_cnt = size / LITTLEFS_SD_SECTOR_SIZE; //LOG_INFO("sector_addr:%d sec_cnt:%d size:%d\r\n", sector_addr, sec_cnt, size); if (sd_read_disk (buffer, sector_addr, sec_cnt ) != 0) return LFS_ERR_IO; return LFS_ERR_OK; } int lfs_sdmmc_prog(const struct lfs_config *lfsc, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { //printf("lfs_sdmmc_prog..\r\n"); uint8_t status = 0; configASSERT(lfsc); uint64_t flash_addr = block * lfsc->block_size + off; for (uint32_t page_ofs = 0; page_ofs < size; page_ofs += LITTLEFS_SD_SECTOR_SIZE) { /*因为每次偏移量就是LITTLEFS_SD_SECTOR_SIZE*/ /*实际上这里封装的不好 感觉直接计算扇区数就行了*/ status = sd_write_disk((void *)((uint8_t*)buffer + page_ofs), (flash_addr + page_ofs) / LITTLEFS_SD_SECTOR_SIZE, 1); if (status != 0) break; } if (status != 0) return LFS_ERR_IO; return LFS_ERR_OK; }
- Lfs文件系统对象–lfs_config
- 底层SDIO驱动实现
底层SDIO仍然是经典的DMA + 二值信号量组合(中断不能用互斥信号量)
底层启动DMA传输–>然后TAKE二值信号量阻塞–>SDIO传输完成,在中断释放信号量–>接着往下运行(同步阻塞方案)uint8_t sd_write_disk(uint8_t *pbuf, uint32_t saddr, uint32_t cnt) { if ((uint32_t)pbuf & 0x03) { LOG_ERR("err write addr\r\n"); } if (saddr + cnt> (0x80000)) { LOG_ERR("err write saddr:%d cnt:%d\r\n",saddr, cnt); return 0xFF; } uint8_t sta = HAL_OK; long long lsector = saddr; xSemaphoreTake(sd_mutex, portMAX_DELAY); sta = HAL_SD_WriteBlocks_DMA(&g_sdcard_handler, (uint8_t *)pbuf, lsector, cnt); /* 多个sector的写操作 */ if(sta != HAL_OK) { LOG_ERR("write status:%d\r\n", sta); HAL_SD_Abort(&g_sdcard_handler); } configASSERT(xSemaphoreTake(sd_tx_rx_sem, portMAX_DELAY) == pdTRUE); while(HAL_SD_GetCardState(&g_sdcard_handler) == HAL_SD_CARD_PROGRAMMING); xSemaphoreGive(sd_mutex); return sta; } /****************************************************/ /* DMA回调 */ /****************************************************/ void HAL_SD_TxCpltCallback(SD_HandleTypeDef *hsd) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; xSemaphoreGiveFromISR(sd_tx_rx_sem, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } void HAL_SD_RxCpltCallback(SD_HandleTypeDef *hsd) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; xSemaphoreGiveFromISR(sd_tx_rx_sem, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }- 踩过的坑:HAL库的SDIO状态标志位切换
这行代码非常的关键,因为在HAL库的设计中,DMA传输完成的中断触发是它和SD卡的控制器交互,把数据传给SD卡控制器就行。但是,从SD卡控制器到真正写入SD卡存储介质是需要时间的!! 一旦在还没有真正写到存储介质就直接开始下次传输,直接就是触发ERR_callback,打印对应标志位发现SD_BUSY。while(HAL_SD_GetCardState(&g_sdcard_handler) == HAL_SD_CARD_PROGRAMMING);
因为实际写入耗费时间不多,所以你会发现,哎呀怎么我在这里加个printf打印这个程序就正常了,去掉printf打印怎么就又触发ERR了,这就是printf调试带来的一些弊端,因为打印相当于给了一定的延时,导致一些错误不能被及时发现。void HAL_SD_ErrorCallback(SD_HandleTypeDef *hsd) { uint32_t static_flags = SDIO->STA; BaseType_t xHigherPriorityTaskWoken = pdFALSE; LOG_ERR("[SD_DRV] err happen!!!!\r\n"); LOG_ERR(" -> HAL State: %ld\r\n", (uint32_t)hsd->State); LOG_ERR(" -> Error Code: 0x%lx\r\n", (uint32_t)hsd->ErrorCode); if (hsd->ErrorCode & HAL_SD_ERROR_DMA) LOG_ERR(" -> Reason: DMA Transfer Error\r\n"); if (hsd->ErrorCode & HAL_SD_ERROR_TIMEOUT) LOG_ERR(" -> Reason: Timeout\r\n"); if (hsd->ErrorCode & HAL_SD_ERROR_ILLEGAL_CMD) LOG_ERR(" -> Reason: Illegal Command\r\n"); if (hsd->ErrorCode & HAL_SD_ERROR_COM_CRC_FAILED) LOG_ERR(" -> Reason: Command CRC Failed\r\n"); if (hsd->ErrorCode & HAL_SD_ERROR_ADDR_OUT_OF_RANGE) LOG_ERR(" -> Reason: Address Out of Range\r\n"); if (hsd->ErrorCode & HAL_SD_ERROR_RX_OVERRUN) LOG_ERR(" -> Reason: RX Overrun\r\n"); if (hsd->ErrorCode & HAL_SD_ERROR_TX_UNDERRUN) LOG_ERR(" -> Reason: TX Underrun\r\n"); LOG_ERR(" -> SDIO STA Register: 0x%x\r\n", static_flags); if (static_flags & SDIO_FLAG_DCRCFAIL) LOG_ERR(" [DCRCFAIL] Data CRC Fail\r\n"); if (static_flags & SDIO_FLAG_DTIMEOUT) LOG_ERR(" [DTIMEOUT] Data Timeout\r\n"); if (static_flags & SDIO_FLAG_RXOVERR) LOG_ERR(" [RXOVERR] RX Overrun\r\n"); if (static_flags & SDIO_FLAG_TXUNDERR) LOG_ERR(" [TXUNDERR] TX Underrun\r\n"); if (static_flags & SDIO_FLAG_STBITERR) LOG_ERR(" [STBITERR] Start Bit Error\r\n"); if (hsd->hdmarx != NULL) { // 检查 DMA 是否有错误标志 // 这里简单判断一下是否处于 BUSY 或 ERROR 状态 if (hsd->hdmarx->State == HAL_DMA_STATE_ERROR) { HAL_DMA_Abort(&hdma_sdio_rx); LOG_ERR("[DMA ERROR] RX Stream reported error!\r\n"); } } if (hsd->hdmatx != NULL) { if (hsd->hdmatx->State == HAL_DMA_STATE_ERROR) { HAL_DMA_Abort(&hdma_sdio_tx); LOG_ERR("[DMA ERROR] TX Stream reported error!\r\n"); } } LOG_ERR("-----------------------------\r\n"); __HAL_SD_CLEAR_FLAG(hsd, SDIO_STATIC_FLAGS); xSemaphoreGiveFromISR(sd_tx_rx_sem, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } - 上层日志系统具体实现
异步日志系统,那肯定就是消息队列就完事了。
当然了为了性能,在这里不能有一条日志就写入,所以日志系统还提供了以下实现- 延迟刷新:所以就再来个缓存(实际上little fs自身就有缓存,它也不是每次你说写就写入SD卡的),然后假设2s都没新日志,就把现有缓存的日志强行刷新到SD卡
- 循环写入:一个超大的txt文件还是不方便,所以这里规定每个日志文件最大大小是2MB,超过2MB就生成新的log_x.txt。
- 多条shell命令: 日志总归是要给人看的,所以编写了shell命令方便日志调试。 这里的话设计就见仁见智了:假设我们的log预留了64byte 但实际上每条才50byte一条就浪费 14byte多可惜,所以我在这里会以50byte就写入50byte(再加上换行符结尾)。不过这样在查询的时候就费劲了,只能遍历日志而且容定位错(万一日志内容里也有换行符呢qaq)。而如果固定每次写入64byte的话,虽然具有一定的浪费,但好处是查询快,通过lseek就能算出文件偏移到哪里开始读。现在AI这么强,想改也很容易啦(我懒得改了)。
void log_task(void *pvParameters) { struct log_msg recv_msg; bool need_sync = false; char filename[16]; int err; uint32_t current_file_size = 0; uint32_t last_sync_file_size = 0; lfs_file_t idx_file; lfs_file_t log_file; show_sdcard_info(); /* 打印SD卡相关信息 */ log_msg_queue = xQueueCreate(LOG_QUEUE_LEN, sizeof(struct log_msg)); configASSERT(log_msg_queue); lfs_system_init(); /* 挂载LFS文件系统 */ #if 1 shell_register_command(&ls_cmd); shell_register_command(&read_cmd); shell_register_command(&tail_cmd); shell_register_command(&rmv_cmd); shell_register_command(&export_cmd); #endif crash_dump_check(); // 1. 读取索引文件 err = lfs_file_open(&lfs, &idx_file, INDEX_FILE_NAME, LFS_O_RDWR); if (err >= 0) { lfs_file_read(&lfs, &idx_file, ¤t_file_idx, sizeof(current_file_idx)); lfs_file_close(&lfs, &idx_file); if (current_file_idx >= MAX_LOG_FILES) current_file_idx = 0; } else { current_file_idx = 0; lfs_file_open(&lfs, &idx_file, INDEX_FILE_NAME, LFS_O_WRONLY | LFS_O_CREAT); lfs_file_write(&lfs, &idx_file, ¤t_file_idx, sizeof(current_file_idx)); lfs_file_close(&lfs, &idx_file); } // 2. 打开初始日志文件 sprintf(filename, "log%d.txt", current_file_idx); err = lfs_file_open(&lfs, &log_file, filename, LFS_O_WRONLY | LFS_O_APPEND | LFS_O_CREAT); if (err < 0) { LOG_ERR("Failed to open initial log file: %d\r\n", err); } current_file_size = lfs_file_size(&lfs, &log_file); last_sync_file_size = current_file_size; LOG_INFO("Log System Init: %s, Size: %d Bytes\r\n", filename, current_file_size); while (1) { if (xQueueReceive(log_msg_queue, &recv_msg, pdMS_TO_TICKS(2000)) == pdPASS) { uint8_t len = recv_msg.log_len; // 检查是否需要切分文件 if (current_file_size + len > MAX_LOG_SIZE) { LOG_DEBUG("File %s full, switching...\r\n", filename); lfs_file_close(&lfs, &log_file); current_file_idx = (current_file_idx + 1) % MAX_LOG_FILES; // 建议用宏 // 更新索引文件 err = lfs_file_open(&lfs, &idx_file, INDEX_FILE_NAME, LFS_O_WRONLY | LFS_O_TRUNC); if (err >= 0) { lfs_file_write(&lfs, &idx_file, ¤t_file_idx, sizeof(current_file_idx)); lfs_file_close(&lfs, &idx_file); } else LOG_ERR("update idx_file failed :%d\r\n",err); sprintf(filename, "log%d.txt", current_file_idx); // 关键点:检查新文件是否打开成功 err = lfs_file_open(&lfs, &log_file, filename, LFS_O_WRONLY | LFS_O_TRUNC | LFS_O_CREAT); if (err < 0) { LOG_ERR("Critical: Failed to open new log file %s (err: %d)\r\n", filename, err); continue; // 打开失败则无法写入,跳过本次循环 } current_file_size = 0; LOG_DEBUG("Now writing to %s\r\n", filename); } // 写入操作 lfs_ssize_t written = lfs_file_write(&lfs, &log_file, recv_msg.log_buf, len); if (written < 0) { LOG_ERR("Write Error: %d\r\n", (int)written); } else { current_file_size += written; need_sync = true; //LOG_DEBUG("Write OK: %d bytes current_file_size:%d\r\n", (int)written,current_file_size); } if (current_file_size - last_sync_file_size >= LITTLEFS_SD_SECTOR_SIZE) { err = lfs_file_sync(&lfs, &log_file); if (err < 0) { LOG_ERR("Sync Error: %d\r\n", err); } else { last_sync_file_size = current_file_size; need_sync = false; //LOG_DEBUG("Disk Sync Success\r\n"); } } } else { if (need_sync) { err = lfs_file_sync(&lfs, &log_file); if (err < 0) { LOG_ERR("Sync Error: %d\r\n", err); } else { LOG_DEBUG("Disk Sync Success\r\n"); } last_sync_file_size = current_file_size; need_sync = false; } } } } - dump文件抓取
之前提到了如果系统发生了异常,那么会进入重新编写的hardfault_exception当中,哎那么能不能把这个保存下来呢?但是直接在hardfault-handler里面操作日志系统风险很大,万一崩溃就是日志系统引起的就完了。所以就借助了前面提到过的,在SRAM里额外留的那块内存,保存一些信息,只要崩溃了不掉电,那还是可以把崩溃现场保存下来的
typedef struct {
uint32_t magic; // 魔数,用于校验是否存在崩溃日志
struct stack_frame context; // 寄存器现场
uint32_t cfsr; // 故障状态寄存器
uint32_t hfsr;
uint32_t mmfar;
uint32_t bfar;
uint8_t fpu_active; // FPU是否激活
} CrashInfo_t;
/* 使用 noinit 属性,防止复位后被 C 库的 startup 代码清零 */
/*这里需要配置一下链接脚本 .sct 给这个变量额外预留一段固定的RAM空间*/
#if defined(__CC_ARM) || defined(__CLANG_ARM) /* Keil MDK */
__attribute__((section(".bss.NoInit"), zero_init)) CrashInfo_t g_crash_info;
#elif defined(__GNUC__) /* GCC */
__attribute__((section(".noinit"))) CrashInfo_t g_crash_info;
#endif
void rt_hw_hard_fault_exception(struct exception_info *exception_info)
{
/*要是FLASH有额外的 还可以保存一下现场*/
struct stack_frame *context = &exception_info->stack_frame;
iwdg_feed();
appErrFlag err_flag = appNoErr;
memcpy(&g_crash_info.context, context, sizeof(struct stack_frame));
g_crash_info.fpu_active = ((exception_info->exc_return & 0x10) == 0) ? 1 : 0;
g_crash_info.cfsr = SCB->CFSR;
g_crash_info.hfsr = SCB->HFSR;
g_crash_info.mmfar = SCB->MMFAR;
g_crash_info.bfar = SCB->BFAR;
g_crash_info.magic = CRASH_MAGIC_WORD;
__DSB(); /*保证操作完成*/
}
/*日志系统启动后先查看是否发生过崩溃 如果发生过就存储崩溃现场*/
uint8_t crash_dump_check(void)
{
lfs_file_t crash_file;
int err;
char report_buffer[512]; // 用于存放格式化后的超大字符串
TimeStruct cur_time;
if (g_crash_info.magic != CRASH_MAGIC_WORD)
{
LOG_INFO("nothing err\r\n");
return 0;
}
LOG_INFO("Detected previous HardFault crash! Formatting report...\r\n");
log_get_time(&cur_time);
// --- 1. 按照人类可读的格式封装字符串 ---
int len = snprintf(report_buffer, sizeof(report_buffer),
"================ Crash Dump Report ================\r\n"
"Time: %02d-%02d %02d:%02d:%02d\r\n" // 可选:也在文件内容里加上时间
"Fault Status Registers:\r\n"
" CFSR: 0x%08X\r\n"
" HFSR: 0x%08X\r\n"
" MMFAR: 0x%08X\r\n"
" BFAR: 0x%08X\r\n"
" FPU: %s\r\n\r\n"
"CPU Registers:\r\n"
" R0: 0x%08X R1: 0x%08X R2: 0x%08X R3: 0x%08X\r\n"
" R4: 0x%08X R5: 0x%08X R6: 0x%08X R7: 0x%08X\r\n"
" R8: 0x%08X R9: 0x%08X R10: 0x%08X R11: 0x%08X\r\n"
" R12: 0x%08X LR: 0x%08X PC: 0x%08X PSR: 0x%08X\r\n"
"===================================================\r\n",
cur_time.month, cur_time.date, cur_time.hour, cur_time.min, cur_time.sec, // 文件内容里的时间
g_crash_info.cfsr, g_crash_info.hfsr, g_crash_info.mmfar, g_crash_info.bfar,
g_crash_info.fpu_active ? "Active" : "Inactive",
g_crash_info.context.exception_stack_frame.r0, g_crash_info.context.exception_stack_frame.r1,
g_crash_info.context.exception_stack_frame.r2, g_crash_info.context.exception_stack_frame.r3,
g_crash_info.context.r4, g_crash_info.context.r5, g_crash_info.context.r6, g_crash_info.context.r7,
g_crash_info.context.r8, g_crash_info.context.r9, g_crash_info.context.r10, g_crash_info.context.r11,
g_crash_info.context.exception_stack_frame.r12, g_crash_info.context.exception_stack_frame.lr,
g_crash_info.context.exception_stack_frame.pc, g_crash_info.context.exception_stack_frame.psr
);
uint8_t cur_sec = at24cxx_read_one_byte(APP_ACTIVE_ADDRESS);
/*是这样 如果APPA运行崩溃了 跳转BOOT->APPB APPB检查对应SRAM后写入APPA的崩溃文件*/
/*不过万一崩溃是日志系统本身引起的嘞 QAQ*/
if (cur_sec == sectorA)
{
err = lfs_file_open(&lfs, &crash_file, CORE_DUMP_FILEB_NAME, LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC);
}
else if(cur_sec == sectorB)
{
err = lfs_file_open(&lfs, &crash_file, CORE_DUMP_FILEA_NAME, LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC);
}
else
{
LOG_ERR("ERR SEC:%d\r\n",cur_sec);
}
if (err < 0)
{
LOG_ERR("open core_dump failed\r\n");
return 1;
}
err = lfs_file_write(&lfs, &crash_file, report_buffer, len);
lfs_file_close(&lfs, &crash_file);
if (err < 0)
{
LOG_ERR("write core_dump failed\r\n");
return 2;
}
//打印一份到控制台,方便立即观察
LOG_INFO("%s", report_buffer);
g_crash_info.magic = 0;
__DSB();
return 0;
}
看门狗
看门狗也是作为调试的一种手段,比如我们初始化一个看门狗线程,定时喂狗的时候同时检查系统的状态,如果状态出现问题就记录日志,然后不喂狗等着复位。
和调试相关的常问的问题
- 调试最重要的数据结构–栈帧
栈帧是一个非常重要的数据结构,也是支持gdb调试的时候回溯的重要支撑
这个在我最早的八股里面有讲过:https://blog.csdn.net/qq_45731845/article/details/146113547?spm=1001.2014.3001.5502 - 有哪些常见的调试手段,遇到问题你是怎么调试的
- gdb相关的调试命令
backtrace
更多推荐

所有评论(0)