How to Implement Lightweight System Logging on Resource‑Constrained MCUs
This article explains a compact method for recording system logs on embedded devices with limited storage, detailing flash memory partitioning, data structures, parameter handling, and a set of AT‑style commands for querying, writing, and managing logs in external flash.
Background
Many embedded applications need to record logs, but microcontrollers often have very limited storage. A lightweight logging solution that stores system logs in external flash (or internal flash/EEPROM) is therefore essential for debugging and post‑mortem analysis.
Log Architecture
The log is treated as a simple file system divided into three zones:
Catalog zone : indexed by date, stores the address, index and size of each day's log.
Parameter zone : keeps write pointers, catalog entry count, and status flags.
Log zone : the main circular buffer where actual log messages are written.
Each zone occupies a configurable portion of flash memory to enable circular storage and extend erase‑write life.
Flash Memory Partition
#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;
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 }
};Data Structures
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;
#define SYSTEM_LOG_MAGIC_PARAM 0x87654321
typedef struct {
uint32_t magic; /* identifier */
uint16_t crc; /* checksum */
uint16_t len; /* length */
} 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;
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;Flash Access Helpers
flash_table_t *get_flash_table(flash_zone_e zone) {
for (int i = 0; i < sizeof(flash_table)/sizeof(flash_table[0]); i++) {
if (zone == flash_table[i].zone) return &flash_table[i];
}
return NULL;
}
int flash_erase(flash_zone_e zone, uint32_t address, flash_block_t block_type) {
flash_table_t *tbl = get_flash_table(zone);
if (!tbl) return -1;
if (address < tbl->start_address || address > tbl->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 *tbl = get_flash_table(zone);
if (!tbl) return -1;
if (address < tbl->start_address || (address + length) > tbl->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 *tbl = get_flash_table(zone);
if (!tbl) return -1;
if (address < tbl->start_address || (address + length) > tbl->end_address) return -1;
bsp_spi_flash_buffer_read(buffer, address, length);
return 0;
}Parameter Management
void save_system_log_param(void) {
uint32_t i = 0, addr = 0, remainbyte = 0, 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) {
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;
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;
gp_sys_log->system_log.log_cyclic_status = 0;
gp_sys_log->system_log.log_latest_time = (time_t){2019,5,8,13,14,10};
gp_sys_log->system_log.write_pos = 0;
gp_sys_log->system_catalog = (system_catalog_t){0,0,0,(time_t){2019,5,8,12,12,14}};
gp_sys_log->crc_val.magic = SYSTEM_LOG_MAGIC_PARAM;
save_system_log_param();
}
int load_system_log_param(void) {
uint32_t i, end_addr, interal = sizeof(sys_log_param_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) {
single_sav_t psav;
flash_read(FLASH_SYSLOG_PARA_ZONE, i, (uint8_t *)&psav, sizeof(single_sav_t));
if (psav.magic == SYSTEM_LOG_MAGIC_PARAM && psav.len == (interal - sizeof(single_sav_t))) {
flash_read(FLASH_SYSLOG_PARA_ZONE, i + sizeof(single_sav_t), &pram[sizeof(single_sav_t)], psav.len);
if (psav.crc != CRC16(&pram[sizeof(single_sav_t)], psav.len)) continue;
gp_sys_ram->system_log_param_addr = i;
log_info("Load System Log Param Addr[0x%08x]!", i);
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]!", flash_tmp->start_address);
return 1;
}Catalog Read/Write
int system_catalog_read(system_catalog_t *catalog, uint32_t id) {
if (id == 0) return -1;
flash_table_t *tbl = get_flash_table(FLASH_CATALOG_ZONE);
uint32_t addr = tbl->start_address + sizeof(system_catalog_t) * (id - 1);
if (addr > tbl->end_address) return -1;
return flash_read(FLASH_CATALOG_ZONE, addr, (uint8_t *)catalog, sizeof(system_catalog_t));
}
int system_catalog_write(system_catalog_t *catalog, uint32_t id) {
if (id == 0) return -1;
flash_table_t *tbl = get_flash_table(FLASH_CATALOG_ZONE);
uint32_t start_addr = tbl->start_address + sizeof(system_catalog_t) * (id - 1);
if ((start_addr + sizeof(system_catalog_t)) > tbl->end_address) start_addr = tbl->start_address;
uint32_t remain = FLASH_SECTOR_SIZE - (start_addr % FLASH_SECTOR_SIZE);
if (remain > sizeof(system_catalog_t)) remain = sizeof(system_catalog_t);
while (1) {
uint32_t base = SECTOR_BASE(start_addr);
uint32_t offset = SECTOR_OFFSET(start_addr);
flash_read(FLASH_CATALOG_ZONE, base, sector_buf, FLASH_SECTOR_SIZE);
flash_erase(FLASH_CATALOG_ZONE, base, FLASH_BLOCK_4K);
memcpy(§or_buf[offset], catalog, remain);
flash_write(FLASH_CATALOG_ZONE, base, sector_buf, FLASH_SECTOR_SIZE);
if (remain == sizeof(system_catalog_t)) break;
catalog = (system_catalog_t *)((uint8_t *)catalog + remain);
start_addr += remain;
remain = (sizeof(system_catalog_t) - remain > FLASH_SECTOR_SIZE) ? FLASH_SECTOR_SIZE : (sizeof(system_catalog_t) - remain);
}
return 0;
}Log Writing and Retrieval
int system_log_write(uint8_t *wbuf, int wlen) {
flash_table_t *tbl = get_flash_table(FLASH_SYSLOG_ZONE);
uint32_t start_addr = tbl->start_address + gp_sys_log->system_log.write_pos;
if ((start_addr + wlen) > tbl->end_address) {
start_addr = tbl->start_address;
gp_sys_log->system_log.write_pos = 0;
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) {
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;
gp_sys_log->system_catalog.log_id = (gp_sys_log->system_catalog.log_id + 1) % ((tbl->end_address - tbl->start_address) / sizeof(system_catalog_t));
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;
}
uint32_t remain = FLASH_SECTOR_SIZE - (start_addr % FLASH_SECTOR_SIZE);
if (remain > wlen) remain = wlen;
while (1) {
flash_write(FLASH_SYSLOG_ZONE, start_addr, wbuf, remain);
if (remain == wlen) break;
wbuf += remain;
start_addr += remain;
wlen -= remain;
remain = (wlen > FLASH_SECTOR_SIZE) ? FLASH_SECTOR_SIZE : wlen;
if ((start_addr % FLASH_SECTOR_SIZE) == 0) flash_erase(FLASH_SYSLOG_ZONE, SECTOR_BASE(start_addr), FLASH_BLOCK_4K);
}
save_system_log_param();
return 0;
}
int system_log_task(int argc) {
if (!gp_sys_ram->system_log_print_enable) return 1;
gp_sys_ram->system_log_print_enable = 0x00;
uint32_t start_addr, offset, end_addr;
flash_table_t *tbl = get_flash_table(FLASH_SYSLOG_ZONE);
if (gp_sys_ram->system_log_print_id == ALL_LOG_PRINT) {
if (gp_sys_log->system_log.log_cyclic_status) {
start_addr = tbl->start_address;
end_addr = tbl->end_address;
offset = end_addr - start_addr;
} else {
start_addr = tbl->start_address;
end_addr = start_addr + gp_sys_log->system_log.write_pos;
offset = gp_sys_log->system_log.write_pos;
}
} else {
system_catalog_t cat;
if (gp_sys_ram->system_log_print_id == gp_sys_log->system_catalog.log_id) {
cat = gp_sys_log->system_catalog;
} else {
system_catalog_read(&cat, gp_sys_ram->system_log_print_id);
}
start_addr = cat.log_addr;
offset = cat.log_offset;
}
if (offset == 0) return 1;
while (offset) {
int 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;
}
return 0;
}Debug Level Interface
#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
#define LOG_RECORD_LEVEL 0x10
#define LOG_PRINT_LEVEL 0xFF
#define LOG_ERROR(fmt, ...) log_format(LOG_ERROR_LEVEL, fmt, ##__VA_ARGS__)
#define LOG_WARN(fmt, ...) log_format(LOG_WARN_LEVEL, fmt, ##__VA_ARGS__)
#define LOG_INFO(fmt, ...) log_format(LOG_INFO_LEVEL, fmt, ##__VA_ARGS__)
#define LOG_DEBUG(fmt, ...) log_format(LOG_DEBUG_LEVEL, fmt, ##__VA_ARGS__)
#define LOG_RECORD(fmt, ...) log_format(LOG_RECORD_LEVEL, fmt, ##__VA_ARGS__)
int log_format(uint8_t level, const char *fmt, ...) {
static QueueHandle_t sem = NULL;
if (!sem) sem = xSemaphoreCreateCounting(1, 1);
xSemaphoreTake(sem, portMAX_DELAY);
// Build prefix based on level
// Append timestamp for ERROR and RECORD levels
// Send to debug UART and, for RECORD, also write to log storage
xSemaphoreGive(sem);
return 0;
}Command Examples
Query the whole catalog: AT+CATALOG? Query a specific day: AT+CATALOG=<LOG_ID> Delete all logs:
AT+RMLOGConclusion
The presented method offers a compact, easily portable system‑log solution for microcontrollers with limited resources. By partitioning flash into catalog, parameter, and log zones and using circular buffers, developers can reliably record, retrieve, and manage logs without excessive code size or flash wear.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Liangxu Linux
Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
