Fundamentals 24 min read

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.

Liangxu Linux
Liangxu Linux
Liangxu Linux
How to Implement Lightweight System Logging on Resource‑Constrained MCUs

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+RMLOG

Conclusion

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Microcontrollerflash memorysystem logging
Liangxu Linux
Written by

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.)

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.