嵌入式設備系統日誌記錄方法

在嵌入式設備應用場景中,系統日誌時常可以監控設備軟件的運行狀態,及時記錄問題點以及關鍵信息,方便開發人員後期定位以及解決問題。本文將講述一種簡易的系統日誌記錄方法,用於保存設備的系統日誌,視具體嵌入式設備情況而定,可存儲在 MCU 內部 Flash、外部 Flash、EEPROM 等,本文采用外部 Flash 作爲示例展開介紹。

思路分析

對於系統日誌可以當成文件系統,可以劃分爲三個重要部分:目錄區、參數區、日誌區。目錄區:根據日期進行歸類,記錄當天的日誌的存儲地址、日誌索引、日誌大小,通過目錄可以獲取整個日誌文件的概況;參數區:存儲記錄日誌寫位置、目錄項個數、寫狀態等參數;日誌區:這是我們主要的存儲區,記錄系統的日誌,支持環寫。這三個區域都需要佔用部分內存,可以自行分配大小。

實現的效果如下圖所示,設置通過指令可查詢到整個日誌目錄區的概況。

查詢系統日誌目錄:AT+CATALOG?

LOG_ID: 存儲日誌按日期分類,該 ID 用於查詢對應日期日誌, 從 1 開始計數;

LOG_DATE: 系統日誌存儲日期;

LOG_ADDR: 系統日誌存儲外部 FLASH 地址;

LOG_OFFSET: 系統日誌存儲偏移量(各日期日誌大小,單位:字節)。

查詢指定日期系統日誌:AT+CATALOG=<LOG_ID>

LOG_ID: 在查詢系統日誌目錄時獲取,當 LOG_ID 爲 0 時,爲查詢整個系統日誌。

另外提供移除系統日誌(清除日誌目錄)指令:AT+RMLOG,後面將講述具體實現。

FLASH 內存劃分

FLASH 內存需要看具體設備進行合理劃分,目錄區、參數區與日誌區實現環形存儲,延長擦寫壽命。

#define FLASH_SECTOR_SIZE   ((uint32_t)0x001000)
#define FLASH_BLOCK_32K_SIZE  ((uint32_t)0x008000)
#define FLASH_BLOCK_64K_SIZE  ((uint32_t)0x010000)
#define SECTOR_MASK             (FLASH_SECTOR_SIZE - 1)         /*扇區掩碼 ------*/
#define SECTOR_BASE(addr)       (addr & (~SECTOR_MASK))        /*扇區的基地址 --*/
#define SECTOR_OFFSET(addr)     (addr & SECTOR_MASK)           /*扇區內的偏移 --*/

#define BLOCK_32K_BASE(addr)  (addr & (~(FLASH_BLOCK_32K_SIZE)))
#define BLOCK_64K_BASE(addr)  (addr & (~(FLASH_BLOCK_64K_SIZE)))

typedef enum {
    FLASH_BLOCK_4K  = 0,          /**< flash erase block size 4k */
    FLASH_BLOCK_32K = 1,          /**< flash erase block size 32k */
    FLASH_BLOCK_64K = 2           /**< flash erase block size 64k */
}flash_block_t;

/* flash 空間索引 */
typedef enum{
    FLASH_CATALOG_ZONE = 0,
    FLASH_SYSLOG_PARA_ZONE,
    FLASH_SYSLOG_ZONE,
    FLASH_ZONEX,
}flash_zone_e;

typedef struct{
    flash_zone_e zone;
    uint32_t start_address;
    uint32_t end_address;
}flash_table_t;

/* 地址劃分 */
static const flash_table_t flash_table[] = {
  { .zone = FLASH_CATALOG_ZONE,       .start_address = 0x03200000, .end_address = 0x032FFFFF},  
  { .zone = FLASH_SYSLOG_PARA_ZONE,   .start_address = 0x03300000, .end_address = 0x033FFFFF},  
  { .zone = FLASH_SYSLOG_ZONE,        .start_address = 0x03400000, .end_address = 0x03FFFFFF},  
};

Flash 底層實現擦除、讀寫操作接口,由讀者自行實現。

flash_table_t *get_flash_table(flash_zone_e zone)
{
  int i = 0;
  for (i = 0; i < flash_zone_count; i++) {
    if (zone == flash_table[i].zone) 
      return (flash_table_t *)&flash_table[i];
  }
  
  return NULL;  
}

int flash_erase(flash_zone_e zone, uint32_t address, flash_block_t block_type)
{
  flash_table_t *flash_table_tmp = get_flash_table(zone);
  
  if (flash_table_tmp == NULL)
    return -1;
    
  if (address < flash_table_tmp->start_address ||address > flash_table_tmp->end_address) 
    return -1;

  return bsp_spi_flash_erase(address, block_type);
}

int flash_write(flash_zone_e zone, uint32_t address, const uint8_t*data, uint32_t length)
{
  flash_table_t *flash_table_tmp = get_flash_table(zone);
     
  if (flash_table_tmp == NULL)
     return -1;

  if ((address < flash_table_tmp->start_address) ||((address + length) > flash_table_tmp->end_address))
     return -1;

  return bsp_spi_flash_buffer_write(address, (uint8_t *)data, length);
}

int flash_read(flash_zone_e zone, uint32_t address, uint8_t*buffer, uint32_t length)
{
  flash_table_t *flash_table_tmp = get_flash_table(zone);
  
  if (flash_table_tmp == NULL)
    return -1;

  if ((address < flash_table_tmp->start_address) ||((address + length) > flash_table_tmp->end_address))
    return -1;
    
  bsp_spi_flash_buffer_read(buffer, address, length);
  return 0;
}

參數與結構體定義

日誌數據存儲時間戳,便於問題定位,需要實現 RTC 接口調用。

typedef struct {
    uint16_t  Year;  /* 年份:YYYY */
    uint8_t  Month;  /* 月份:MM */
    uint8_t  Day;  /* 日:DD */
    uint8_t     Hour;  /* 小時:HH */
    uint8_t     Minute;  /* 分鐘:MM */
    uint8_t  Second;  /* 秒:SS */
}time_t;   

int bsp_rtc_get_time(time_t *date);

參數區應當保證數據的正確性,應加入參數校驗存儲,定義校驗結構體。

#define SYSTEM_LOG_MAGIC_PARAM  0x87654321 /* 日誌參數標識符 */
typedef struct {
    uint32_t magic;  /* 參數標識符 */
    uint16_t crc;  /* 校驗值 */
    uint16_t len;  /* 參數長度 */
} single_sav_t;

參數區需記錄當前日誌記錄的寫位置,以及目錄項個數,還有日誌區和目錄區環寫狀態,並且存儲最新時間等等。

/* 日誌區參數 */
typedef struct {
    uint32_t   write_pos;            /* 寫位置 */
    uint32_t   catalog_num;           /* 目錄項個數 */
    uint8_t    log_cyclic_status;   /* 系統日誌環形寫狀態 */   
    uint8_t    catalog_cyclic_status; /* 日誌目錄環形寫狀態 */
    time_t     log_latest_time;    /* 存儲最新時間 */
}system_log_t;

/* 目錄區參數 */
typedef struct {
    uint32_t log_id;    /* 日誌索引 */ 
    uint32_t log_addr;    /* 日誌地址 */
    uint32_t log_offset;  /* 日誌偏移大小,單位:字節 */
    time_t   log_time;    /* 日誌存儲時間 */
}system_catalog_t;

/* 系統日誌參數 */
typedef struct {
    single_sav_t crc_val;
    system_log_t system_log;
    system_catalog_t system_catalog;
}sys_log_param_t;

typedef struct {
    uint8_t system_log_print_enable; /* 系統日誌打印使能 */
    uint16_t system_log_print_id;    /* 打印指定id系統日誌 */
    uint32_t system_log_param_addr;  /* 當前日誌寫地址 */
} sys_ram_t;

sys_ram_t SysRam;
sys_log_param_t SysLogParam;

sys_ram_t *gp_sys_ram = &SysRam;
sys_log_param_t *gp_sys_log = &SysLogParam;

實現接口說明

CRC 校驗接口,可以自定義實現。

/* 16位CRC校驗高位表 */
static const uint8_t auchCRCHi[]={
0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40,
0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40,0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,
0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40,0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,
0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40,
0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40,0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,
0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40,
0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40,
0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40,0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,

0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40,0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,
0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40,
0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40,
0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40,0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,
0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40,
0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40,0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,
0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40,0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,
0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40
};

/* 16位CRC校驗低位表 */
static const uint8_t auchCRCLo[]={
0x00,0xc0,0xc1,0x01,0xc3,0x03,0x02,0xc2,0xc6,0x06,0x07,0xc7,0x05,0xc5,0xc4,0x04,
0xcc,0x0c,0x0d,0xcd,0x0f,0xcf,0xce,0x0e,0x0a,0xca,0xcb,0x0b,0xc9,0x09,0x08,0xc8,
0xd8,0x18,0x19,0xd9,0x1b,0xdb,0xda,0x1a,0x1e,0xde,0xdf,0x1f,0xdd,0x1d,0x1c,0xdc,
0x14,0xd4,0xd5,0x15,0xd7,0x17,0x16,0xd6,0xd2,0x12,0x13,0xd3,0x11,0xd1,0xd0,0x10,
0xf0,0x30,0x31,0xf1,0x33,0xf3,0xf2,0x32,0x36,0xf6,0xf7,0x37,0xf5,0x35,0x34,0xf4,
0x3c,0xfc,0xfd,0x3d,0xff,0x3f,0x3e,0xfe,0xfa,0x3a,0x3b,0xfb,0x39,0xf9,0xf8,0x38,
0x28,0xe8,0xe9,0x29,0xeb,0x2b,0x2a,0xea,0xee,0x2e,0x2f,0xef,0x2d,0xed,0xec,0x2c,
0xe4,0x24,0x25,0xe5,0x27,0xe7,0xe6,0x26,0x22,0xe2,0xe3,0x23,0xe1,0x21,0x20,0xe0,

0xa0,0x60,0x61,0xa1,0x63,0xa3,0xa2,0x62,0x66,0xa6,0xa7,0x67,0xa5,0x65,0x64,0xa4,
0x6c,0xac,0xad,0x6d,0xaf,0x6f,0x6e,0xae,0xaa,0x6a,0x6b,0xab,0x69,0xa9,0xa8,0x68,
0x78,0xb8,0xb9,0x79,0xbb,0x7b,0x7a,0xba,0xbe,0x7e,0x7f,0xbf,0x7d,0xbd,0xbc,0x7c,
0xb4,0x74,0x75,0xb5,0x77,0xb7,0xb6,0x76,0x72,0xb2,0xb3,0x73,0xb1,0x71,0x70,0xb0,
0x50,0x90,0x91,0x51,0x93,0x53,0x52,0x92,0x96,0x56,0x57,0x97,0x55,0x95,0x94,0x54,
0x9c,0x5c,0x5d,0x9d,0x5f,0x9f,0x9e,0x5e,0x5a,0x9a,0x9b,0x5b,0x99,0x59,0x58,0x98,
0x88,0x48,0x49,0x89,0x4b,0x8b,0x8a,0x4a,0x4e,0x8e,0x8f,0x4f,0x8d,0x4d,0x4c,0x8c,
0x44,0x84,0x85,0x45,0x87,0x47,0x46,0x86,0x82,0x42,0x43,0x83,0x41,0x81,0x80,0x40
};

/* 實現crc功能函數 */
static uint16_t CRC16(uint8_t* puchMsg, uint16_t usDataLen)
{
    uint8_t uchCRCHi=0xff;
    uint8_t uchCRCLo=0xff;
    uint16_t uIndex;
    
    while(usDataLen--) {
        uIndex=uchCRCHi^*(puchMsg++);
        uchCRCHi=uchCRCLo^auchCRCHi[uIndex];
        uchCRCLo=auchCRCLo[uIndex];
    }
    
    return uchCRCHi<<8|uchCRCLo;
}

保存系統日誌參數,每實現寫日誌操作後都需要保存當前的參數值,防止意外丟失。

void save_system_log_param(void)
{
    uint32_t i = 0;
    uint32_t addr = 0;
    uint32_t remainbyte = 0;
    uint32_t start_addr;
    int len = sizeof(sys_log_param_t);
    uint8_t *pdata = (uint8_t *)&SysLogParam;
    flash_table_t *flash_tmp = get_flash_table(FLASH_SYSLOG_PARA_ZONE);
    
    /* 校驗參數 */
    gp_sys_log->crc_val.magic = SYSTEM_LOG_MAGIC_PARAM;
    gp_sys_log->crc_val.len = sizeof(sys_log_param_t) - sizeof(single_sav_t);
    gp_sys_log->crc_val.crc = CRC16(&pdata[sizeof(single_sav_t)], gp_sys_log->crc_val.len);

    start_addr = gp_sys_ram->system_log_param_addr;
    /* 剩餘內存不夠寫,則重新從起始地址開始寫,實現環形存儲功能  */
    if ((start_addr + len) > flash_tmp->end_address) { 
        start_addr = flash_tmp->start_address;
    }
    gp_sys_ram->system_log_param_addr = start_addr + len;
    /* 首地址存儲,擦除整個系統日誌參數存儲區,如果劃分的內存較大,可能出現第一次擦寫等待時間較長,
       但實際應用嵌入式設備應該不會佔用太多的內存存儲系統日誌,只當爲輔助使用,有額外應用可自行實現 */
    if (flash_tmp->start_address == start_addr) {
        /*for (i = flash_tmp->start_address; i < flash_tmp->end_address; i+= FLASH_SECTOR_SIZE) 
            flash_erase(FLASH_SYSLOG_PARA_ZONE, SECTOR_BASE(i), FLASH_BLOCK_4K);
        */
        addr = flash_tmp->start_address;
        do {
            if ((addr + FLASH_BLOCK_64K_SIZE) <= flash_tmp->end_address) {
                flash_erase(FLASH_SYSLOG_PARA_ZONE, BLOCK_64K_BASE(i), FLASH_BLOCK_64K);
                addr += FLASH_BLOCK_64K_SIZE;
            } else if ((addr + FLASH_BLOCK_32K_SIZE) <= flash_tmp->end_address) {
                flash_erase(FLASH_SYSLOG_PARA_ZONE, BLOCK_32K_BASE(i), FLASH_BLOCK_32K);
                addr += FLASH_BLOCK_32K_SIZE;
            } else if ((addr + FLASH_SECTOR_SIZE) <= flash_tmp->end_address) {
                flash_erase(FLASH_SYSLOG_PARA_ZONE, SECTOR_BASE(i), FLASH_BLOCK_4K);
                addr += FLASH_SECTOR_SIZE;
            } else {
                break;
            }
        } while (addr < flash_tmp->end_address); 
    }

    remainbyte = FLASH_SECTOR_SIZE - (start_addr % FLASH_SECTOR_SIZE);
    if (remainbyte > len) {
        remainbyte = len;
    }
    while (1) {
        flash_write(FLASH_SYSLOG_PARA_ZONE, start_addr, pdata, remainbyte);
        if (remainbyte == len) {
            break;
        } else {
            pdata += remainbyte;
            start_addr += remainbyte;
            len -= remainbyte;
            remainbyte = (len > FLASH_SECTOR_SIZE) ? FLASH_SECTOR_SIZE : len;
        }
    }
}

導入系統日誌默認參數接口,初始化默認參數或者移除日誌。

void load_system_log_default_param(void)
{
    /* 系統日誌默認參數 */
    /* 目錄環寫狀態標誌 */
    gp_sys_log->system_log.catalog_cyclic_status = 0x00;
    /* 目錄項個數 */
    gp_sys_log->system_log.catalog_num = 0;
    /* 日誌環寫標誌 , 1:環寫狀態 */
    gp_sys_log->system_log.log_cyclic_status = 0;
    /* 設置默認值,實際會重新從RTC獲取最新時間 */
    gp_sys_log->system_log.log_latest_time.Year = 2019;
    gp_sys_log->system_log.log_latest_time.Month = 5;
    gp_sys_log->system_log.log_latest_time.Day = 8;
    gp_sys_log->system_log.log_latest_time.Hour = 13;
    gp_sys_log->system_log.log_latest_time.Minute = 14;
    gp_sys_log->system_log.log_latest_time.Second = 10;
    /* 日誌寫位置從0開始 */
    gp_sys_log->system_log.write_pos = 0;
    
    gp_sys_log->system_catalog.log_addr = 0;
    gp_sys_log->system_catalog.log_id = 0;
    gp_sys_log->system_catalog.log_offset = 0;
    gp_sys_log->system_catalog.log_time.Year = 2019;
    gp_sys_log->system_catalog.log_time.Month = 5;
    gp_sys_log->system_catalog.log_time.Day = 8;
    gp_sys_log->system_catalog.log_time.Hour = 12;
    gp_sys_log->system_catalog.log_time.Minute = 12;
    gp_sys_log->system_catalog.log_time.Second = 14;
    
    gp_sys_log->crc_val.magic = SYSTEM_LOG_MAGIC_PARAM;

    /* 導入默認參數後進行保存 */
    save_system_log_param();
}

設備開機或者復位都會進行導入系統日誌參數操作,恢復日誌讀寫參數,參數區爲頻繁讀寫操作區域,每一次寫操作都會進行一次偏移,有效的導入參數方法是從參數區結束地址到起始地址進行掃描,掃描不到合法的參數則會導入默認日誌參數。

/* 參數初始化,在終端啓動時調用 */
int load_system_log_param(void)
{
    uint32_t i = 0;
    single_sav_t psav;
    uint32_t end_addr;
    uint32_t interal = sizeof(sys_log_param_t);
    int data_len = sizeof(sys_log_param_t) - sizeof(single_sav_t);
    uint8_t *pram = (uint8_t *)&SysLogParam;
    flash_table_t *flash_tmp = get_flash_table(FLASH_SYSLOG_PARA_ZONE);
    
    end_addr =flash_tmp->end_address - (flash_tmp->end_address - flash_tmp->start_address) % interal;
    for (i = end_addr - interal; i > flash_tmp->start_address; i -= interal) {
        flash_read(FLASH_SYSLOG_PARA_ZONE, i, (uint8_t *)&psav, sizeof(single_sav_t));
        if ((psav.magic == SYSTEM_LOG_MAGIC_PARAM) && (psav.len ==data_len)) {   
            flash_read(FLASH_SYSLOG_PARA_ZONE, i + sizeof(single_sav_t)&pram[sizeof(single_sav_t)], data_len);
            if (psav.crc != CRC16(&pram[sizeof(single_sav_t)], data_len)) 
                continue;
            gp_sys_ram->system_log_param_addr = i;
            log_info("Load System Log Param Addr[0x%08x]!", gp_sys_ram->system_log_param_addr);
            return 0;
        }
    }
    
    /* 掃描不到合法的參數,導入默認系統日誌參數 */
    load_system_log_default_param();
    /* 獲取日誌寫地址 */
    gp_sys_ram->system_log_param_addr = flash_tmp->start_address;
    log_info("Load System Log Param Addr(Default)[0x%08x]!", gp_sys_ram->system_log_param_addr);
    return 1;
}

讀寫系統日誌目錄接口,讀寫指定日誌索引目錄信息。實際實現會定義最新的目錄信息存儲在日誌參數區,當日期發生改變,則表示當前目錄信息已經完結,將最新的目錄信息錄入日誌目錄區保存,最多每天寫入一次目錄區。

/* 讀取日誌目錄區指定日誌索引目錄信息 */
int system_catalog_read(system_catalog_t *catalog, uint32_t id)
{
    uint32_t addr;
    int rlen = sizeof(system_catalog_t);
    uint8_t *pbuf = (uint8_t *)catalog;
    flash_table_t *flash_tmp = get_flash_table(FLASH_CATALOG_ZONE);

    if (0 == id) 
        return -1;
    addr = flash_tmp->start_address + (rlen * (id - 1));
    if (addr > flash_tmp->end_address) 
        return -1;
        
    return flash_read(FLASH_CATALOG_ZONE, addr, pbuf, rlen);
}

/* 寫日誌目錄區目錄信息 */
int system_catalog_write(system_catalog_t *catalog, uint32_t id)
{
    uint32_t start_offset;
    uint32_t start_addr;
    uint32_t start_base;
    uint32_t remainbyte;
    int wlen = sizeof(system_catalog_t);
    uint8_t *pdata = (uint8_t *)catalog;
    flash_table_t *flash_tmp = get_flash_table(FLASH_CATALOG_ZONE);
    
    if (0 == id) return -1;
    start_addr = flash_tmp->start_address + wlen * (id - 1);
    if ((start_addr + wlen) > flash_tmp->end_address) {
        start_addr = flash_tmp->start_address;
    }
    
    /* 本扇區剩餘空間大小 */
    remainbyte = FLASH_SECTOR_SIZE - (start_addr % FLASH_SECTOR_SIZE);
    /* 寫入數據長度小於本扇區剩餘長度,直接寫入 */
    if (remainbyte > wlen) {
        remainbyte = wlen;
    }
    /* 寫目錄次數不會太頻繁,視具體情況改寫操作實現 */
    while (1) {
        start_base = SECTOR_BASE(start_addr);
        start_offset = SECTOR_OFFSET(start_addr);
        flash_read(FLASH_CATALOG_ZONE, start_base, sector_buf, FLASH_SECTOR_SIZE);
        flash_erase(FLASH_CATALOG_ZONE, start_base, FLASH_BLOCK_4K);
        memcpy((char *)§or_buf[start_offset], pdata, remainbyte);
        flash_write(FLASH_CATALOG_ZONE, start_base, sector_buf, FLASH_SECTOR_SIZE);
        if (remainbyte == wlen) {
            break;
        } else {
            pdata += remainbyte;
            start_addr += remainbyte;
            wlen -= remainbyte;
            remainbyte = (wlen > FLASH_SECTOR_SIZE) ? FLASH_SECTOR_SIZE : wlen;
        }
    }
    
    return 0;
}

打印系統日誌目錄區信息,可實現通過指令查詢到目錄區信息。

int system_catalog_all_print(void)
{
    int i = 0;
    system_catalog_t catalog;

    printf("System Log Command Information:\r\n");
    printf("Query Specifies Log : AT+CATALOG=<LOG_ID><CR><LF>\r\n");
    printf("Query All Log : AT+CATALOG=<0><CR><LF>\r\n\r\n");
    printf("Query All System Catalog:\r\n");
    printf("LOG_ID  LOG_DATE  LOG_ADDR  LOG_OFFSET \r\n");
    for (i = 0; i < gp_sys_log->system_log.catalog_num; i++) {
        /* 當前最新目錄信息 */  
        if (i == (gp_sys_log->system_catalog.log_id - 1)) {
            catalog = gp_sys_log->system_catalog; /* 獲取當前最新目錄信息 */
        } else {
            system_catalog_read(&catalog, i + 1);
        }
        printf("%d  %04d-%02d-%02d  0x%08X  %d \r\n", 
            catalog.log_id, catalog.log_time.Year, catalog.log_time.Month, catalog.log_time.Day, 
            catalog.log_addr, catalog.log_offset);
        memset((char *)&catalog, 0, sizeof(system_catalog_t));
    }
    return 0;
}

讀取指定日誌目錄索引信息接口,可指定日誌索引或者讀取全部日誌數據。

int system_log_task(int argc)
{
    int rlen = 0;
    uint32_t offset, start_addr, end_addr;
    system_catalog_t catalog;
    flash_table_t *flash_tmp =get_flash_table(FLASH_SYSLOG_ZONE);
    
    if (0 == gp_sys_ram->system_log_print_enable) 
        return 1;

    gp_sys_ram->system_log_print_enable = 0x00;
    if (gp_sys_ram->system_log_print_id == ALL_LOG_PRINT) {
        /* log迴環寫標誌,打印整個LOG存儲區 */
        if (0x01 == gp_sys_log->system_log.log_cyclic_status) { 
            start_addr = flash_tmp->start_address;
            end_addr = flash_tmp->end_address;
            offset = end_addr - start_addr;
        } else {
            start_addr = flash_tmp->start_address;
            end_addr = start_addr + gp_sys_log->system_log.write_pos;
            offset = gp_sys_log->system_log.write_pos;
        }
    } else { /* 讀取指定ID日誌 */
        if (gp_sys_ram->system_log_print_id == gp_sys_log->system_catalog.log_id) {
            catalog = gp_sys_log->system_catalog;
        } else {
            system_catalog_read(&catalog, gp_sys_ram->system_log_print_id);
        }
        start_addr = catalog.log_addr;
        offset = catalog.log_offset;
    } 

    if (0 == offset)
        return 1;

    while (1) {
        rlen = (offset > 512) ? 512 : offset;
        system_log_read(sector_buf, start_addr, rlen);
        HAL_Delay(80);
        /* 目錄信息通過調式串口打印 */
        bsp_debug_send(sector_buf, rlen);
        start_addr += rlen;
        offset -= rlen;
        if (0 == offset) 
            break;
    }
    return 0;
}

存儲系統日誌接口,實現更新存儲日期,當寫位置爲扇區地址,則擦除一個扇區作爲存儲日誌,這樣避免每寫一次就擦除一次。

int system_log_write(uint8_t *wbuf, int wlen)
{
    uint32_t start_addr;
    uint8_t *pdata = wbuf;
    uint32_t remainbyte;
    int system_catalog_max_id;
    flash_table_t *flash_tmp =get_flash_table(FLASH_SYSLOG_ZONE);
    
    /* 計算目錄區的最大存儲目錄項個數 */
    system_catalog_max_id = ((flash_tmp->end_address - flash_tmp->start_address) / sizeof(system_catalog_t));
    start_addr = flash_tmp->start_address + gp_sys_log->system_log.write_pos;
    /* 存儲數據地址大於規劃內存地址範圍處理 */
    if ((start_addr + wlen) > flash_tmp->end_address) { 
        start_addr = flash_tmp->start_address;
        /* 寫位置偏移量重置 */
        gp_sys_log->system_log.write_pos = 0;
        /* LOG迴環存儲標誌置位 */
        gp_sys_log->system_log.log_cyclic_status = 0x01; 
    }
    /* 寫位置偏移 */
    gp_sys_log->system_log.write_pos += wlen; 

    if ((gp_sys_log->system_log.log_latest_time.Year != gp_sys_log->system_catalog.log_time.Year) ||
        (gp_sys_log->system_log.log_latest_time.Month != gp_sys_log->system_catalog.log_time.Month) ||
        (gp_sys_log->system_log.log_latest_time.Day != gp_sys_log->system_catalog.log_time.Day)) {

        /* 日期改變,記錄目錄信息,當log_id爲0,則不寫入 */
        system_catalog_write(&gp_sys_log->system_catalog, gp_sys_log->system_catalog.log_id);
        /* 記錄存儲日期 */
        gp_sys_log->system_catalog.log_time = gp_sys_log->system_log.log_latest_time;
        
        if ((gp_sys_log->system_catalog.log_id + 1) >= system_catalog_max_id) {
            gp_sys_log->system_log.catalog_num = system_catalog_max_id; /* 目錄循環寫,目錄數應爲最大 */
            gp_sys_log->system_log.catalog_cyclic_status = 1; /* 目錄迴環寫標誌 */
        } else {
            if (0 == gp_sys_log->system_log.catalog_cyclic_status) {
                /* 獲取目錄數 */
                gp_sys_log->system_log.catalog_num = gp_sys_log->system_catalog.log_id + 1; 
            }
        }
        
        /* 存儲最新目錄項信息 */
        gp_sys_log->system_catalog.log_id = (gp_sys_log->system_catalog.log_id + 1) % system_catalog_max_id;
        gp_sys_log->system_catalog.log_addr = start_addr;
        gp_sys_log->system_catalog.log_offset = wlen; 
    } else {
        gp_sys_log->system_catalog.log_offset += wlen; 
    }
    
    /* 寫位置爲存儲起始地址並且不爲扇區首地址 */
    if ((flash_tmp->start_address == start_addr) && (SECTOR_OFFSET(flash_tmp->start_address))){
        flash_read(FLASH_SYSLOG_ZONE, SECTOR_BASE(start_addr), sector_buf, FLASH_SECTOR_SIZE);
        flash_erase(FLASH_SYSLOG_ZONE, SECTOR_BASE(start_addr), FLASH_BLOCK_4K);
        /* 將扇區頭部至起始地址區間的數據回寫 */
        flash_write(FLASH_SYSLOG_ZONE, SECTOR_BASE(start_addr), §or_buf[0], SECTOR_OFFSET(start_addr)); 
    }
    /* 寫位置爲扇區首地址,則擦除一個扇區的存儲區    */
    if (0 == SECTOR_OFFSET(start_addr)) {
        flash_erase(FLASH_SYSLOG_ZONE, SECTOR_BASE(start_addr), FLASH_BLOCK_4K);
    }

    /* 本扇區剩餘空間大小 */
    remainbyte = FLASH_SECTOR_SIZE - (start_addr % FLASH_SECTOR_SIZE);
    /* 寫入數據長度小於本扇區剩餘長度,直接寫入 */
    if (remainbyte > wlen) {
        remainbyte = wlen;
    }
    while (1) {
        flash_write(FLASH_SYSLOG_ZONE, start_addr, pdata, remainbyte);
        if (remainbyte == wlen) {
            break;
        } else {
            pdata += remainbyte;
            start_addr += remainbyte;
            wlen -= remainbyte;
            remainbyte = (wlen > FLASH_SECTOR_SIZE) ? FLASH_SECTOR_SIZE : wlen;
            /* 扇區首地址則擦除整個扇區,該扇區數據不保存 */
            if (0 == SECTOR_OFFSET(start_addr)) {
                flash_erase(FLASH_SYSLOG_ZONE, SECTOR_BASE(start_addr), FLASH_BLOCK_4K);
            }
        }
    }

    /* 環形存儲參數 */
    save_system_log_param();
    return 0;
}

系統調試對接

爲了更好記錄系統日誌,將應用調試等級結合一塊,實現記錄錯誤調試信息以及需要保存的關鍵信息。定義的調試等級有:關閉調試等級、錯誤調試等級、警告調試等級、關鍵調試等級、debug 調試等級,而 LOG_RECORD_LEVEL 將主動保存日誌並輸出信息,LOG_ERROR_LEVEL 會存儲對應的日誌信息,但需要根據應用調試等級輸出信息。設置與讀取應用調試等級由讀者自行定義。

#define LOG_CLOSE_LEVEL    0x00 /* 關閉調試信息 */
#define LOG_ERROR_LEVEL    0x01 /* 錯誤調試信息 */
#define LOG_WARN_LEVEL    0x02 /* 警告調試信息 */
#define LOG_INFO_LEVEL    0x03 /* 關鍵調試信息 */
#define LOG_DEBUG_LEVEL    0x04 /* debug調試信息 */
#define LOG_RECORD_LEVEL   0x10 /* 保存日誌並輸出信息 */ 
#define LOG_PRINT_LEVEL    0xff

#define SET_LOG_LEVEL(LEVEL)  (gp_sys_param->system_print_level = LEVEL)
#define GET_LOG_LEVEL()    (gp_sys_param->system_print_level)

#define log_debug(fmt, args...)  log_format(LOG_DEBUG_LEVEL, fmt, ##args)
#define log_info(fmt, args...)  log_format(LOG_INFO_LEVEL, fmt, ##args)
#define log_warn(fmt, args...)  log_format(LOG_WARN_LEVEL, fmt, ##args)
#define log_error(fmt, args...)  log_format(LOG_ERROR_LEVEL, fmt, ##args)
#define log_record(fmt, args...) log_format(LOG_RECORD_LEVEL, fmt, ##args)
#define printf(fmt, args...)  log_format(LOG_PRINT_LEVEL, fmt, ##args)

typedef struct {
    int level;
    char *fmt_str;
}system_print_fmt_t;

system_print_fmt_t system_print_fmt_list[] = {
    { .level = LOG_ERROR_LEVEL,   .fmt_str = "<error>:"},
    { .level = LOG_WARN_LEVEL,    .fmt_str = "<warn>:"},
    { .level = LOG_INFO_LEVEL,    .fmt_str = "<info>:"},
    { .level = LOG_DEBUG_LEVEL,   .fmt_str = "<debug>:"},
    { .level = LOG_RECORD_LEVEL,  .fmt_str = "<record>:"},
};

int log_format(uint8_t level, const char *fmt, ...)
{
    #define TIME_PREFIX_SIZE (21)
    #define PRINT_MAX_SIZE  (1024 + TIME_PREFIX_SIZE)
    
    va_list args;
    int num = 0, i = 0, fmt_index = 0;
    int fmt_str_len = 0, ret = -1;
    int file_str_len = 0, line_str_len = 0;
    char line_buf[20] = {0};
    static char buf[PRINT_MAX_SIZE];
    static QueueHandle_t sem = NULL;
    time_t time = {0};
    
    /* 針對os系統 */
    if (NULL == sem) {
          sem = xSemaphoreCreateCounting(1, 1); /* always think of success */
    }
    
    xSemaphoreTake(sem, portMAX_DELAY);

    ret = -1;
    fmt_str_len = 0;
    if (level != LOG_PRINT_LEVEL) {
        if ((GET_LOG_LEVEL() < level) && (level != LOG_RECORD_LEVEL) && (level != LOG_ERROR_LEVEL))
            goto exit_end;

        for (i = 0; i < SYSTEM_PRINT_FMT_LIST_MAX; i++) {
            if (level == system_print_fmt_list[i].level) {
                fmt_index = i;
                break;
            }
        }
        if (i > SYSTEM_PRINT_FMT_LIST_MAX) {
            goto exit_end;
        }

        fmt_str_len = strlen(system_print_fmt_list[fmt_index].fmt_str);
        strncpy((char *)&buf[TIME_PREFIX_SIZE], system_print_fmt_list[fmt_index].fmt_str, fmt_str_len);
    }

    va_start(args, fmt);
    num = vsnprintf((char *)&buf[fmt_str_len + TIME_PREFIX_SIZE], PRINT_MAX_SIZE - fmt_str_len - TIME_PREFIX_SIZE - 2, fmt, args);
    va_end(args);

    if (num <= 0) {
        goto exit_end;
    }

    if (level != LOG_PRINT_LEVEL) {
        num += fmt_str_len;
        buf[num + TIME_PREFIX_SIZE] = '\r';
        buf[num + TIME_PREFIX_SIZE + 1] = '\n';
        num += 2;
    }

    if ((GET_LOG_LEVEL() < level) && (level == LOG_ERROR_LEVEL)) {
        //do nothing
    } else {
        ret = bsp_debug_send((uint8_t*)&buf[TIME_PREFIX_SIZE], num); 
    }

    if ((LOG_ERROR_LEVEL == level) || (LOG_RECORD_LEVEL == level)) {
        bsp_rtc_get_time(&time);
        sprintf(&buf[0]"[%04d-%02d-%02d %02d:%02d:%02d",
            time.Year, time.Month, time.Day,time.Hour, time.Minute, time.Second);
        buf[TIME_PREFIX_SIZE - 1] = ']';
        gp_sys_log->system_log.log_latest_time = time;
        system_log_write((uint8_t *)buf, num + TIME_PREFIX_SIZE);
    } 

exit_end:
    xSemaphoreGive(sem);
    return ret;
}

結語

本文提供的一種簡易嵌入式設備系統日誌記錄方法,代碼量不多,實現簡單,針對不同的設備需要合理規劃內存使用,根據軟件運行狀態,合適加入調試信息並保存對應的日誌信息,方便開發人員瞭解系統或軟件運行狀況,協助開發分析數據資源從而更好完善系統,提高定位以及解決問題的效果。

原文:http://t.csdn.cn/gFDSG

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/YDVPnIW0R5p8VW93DsOxwg