前言

  这篇主要是介绍在项目里可以用到的调试程序的组件,包括多级日志打印,日志文件系统和shell、重写hardfault异常等。其中也会穿插一些常问的问题。

基于串口的Shell组件

  • 串口驱动
      串口驱动的实现其实是面试的时候非常喜欢问的一个问题。可以看看你对数据流的理解,比如问你如何实现驱动向上层数据的提交,这里有两种常见的做法。

    • 串口接收中断(RXNE) + 环形缓冲区
      当接受中断触发的时候,把接收到的字符放入缓冲区当中并唤醒接收线程;接收线程如果解析到了结束信号(比如’\n’),就把本次记录的所有数据从缓冲区中取出,向上提交(信号量唤醒之类的方式),这里可以通过有限状态机记录当前的接收状态(缓冲区溢出\正在接收等等)
    • 串口空闲中断 + DMA
      通过DMA接收缓冲区接收数据,当串口空闲中断触发后,可以将数据从DMA缓冲区拷贝到上层数据区(DMA会记录接收了多少数据)。
      这个方案是比较推荐的,因为CPU占用率会不高,当然你可能担心,那假如我的数据发送方把包分成了两份发送,那一次空闲中断触发可不能认为包接收完全了。我个人的做法是再封装一个FIFO缓冲区 + 解析线程 + 计数信号量。串口空闲中断触发后只负责把数据放入FIFO缓冲区并通过计数信号量通知解析线程,然后解析线程去扫描FIFO解析帧

      因为shell命令不会太长嘛,也不会那么麻烦,所以这里直接串口空闲中断+DMA就行,触发了空闲中断就直接认为收到了一条shell指令,通知shell线程去解析即可。

  • 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;
      }
      
  • 底层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状态标志位切换
    while(HAL_SD_GetCardState(&g_sdcard_handler) == HAL_SD_CARD_PROGRAMMING);
    
      这行代码非常的关键,因为在HAL库的设计中,DMA传输完成的中断触发是它和SD卡的控制器交互,把数据传给SD卡控制器就行。但是,从SD卡控制器到真正写入SD卡存储介质是需要时间的!! 一旦在还没有真正写到存储介质就直接开始下次传输,直接就是触发ERR_callback,打印对应标志位发现SD_BUSY。
      因为实际写入耗费时间不多,所以你会发现,哎呀怎么我在这里加个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);
    }
    
  • 上层日志系统具体实现
      异步日志系统,那肯定就是消息队列就完事了。
    当然了为了性能,在这里不能有一条日志就写入,所以日志系统还提供了以下实现
    1. 延迟刷新:所以就再来个缓存(实际上little fs自身就有缓存,它也不是每次你说写就写入SD卡的),然后假设2s都没新日志,就把现有缓存的日志强行刷新到SD卡
    2. 循环写入:一个超大的txt文件还是不方便,所以这里规定每个日志文件最大大小是2MB,超过2MB就生成新的log_x.txt。
    3. 多条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, &current_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, &current_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, &current_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

更多推荐