How to Design a Logging System for Embedded Software

Scan to FollowLearn Embedded Together, learn and grow together

How to Design a Logging System for Embedded Software

In embedded system development, a logging system is a crucial debugging and diagnostic tool.

A well-designed logging system can help developers quickly locate issues, analyze system behavior, and monitor operational status.

This article will detail how to develop a fully functional embedded software logging system.

Requirements Analysis

Before starting the design, it is essential to clarify the core requirements of the logging system:

  1. Multi-level Logging: Support for different levels of log importance (DEBUG, INFO, WARN, ERROR, etc.)
  2. Low Resource Usage: Minimal memory usage and low CPU overhead
  3. Real-time Performance: Does not affect the real-time performance of the main program
  4. Multiple Output Methods: Supports various output methods such as serial port, file system, network, etc.
  5. Timestamp: Provides accurate time information
  6. Thread/Task Safety: Safe to use in a multi-tasking environment
  7. Low Power Mode: Can operate normally in energy-saving mode

System Design

Architecture Design

Adopt a layered architecture design:

  • Interface Layer: Provides logging API
  • Processing Layer: Formats, filters, and processes logs
  • Output Layer: Controls the destination of log output

Log Level Definition

typedef enum 
{
    LOG_LEVEL_DEBUG = 0,
    LOG_LEVEL_INFO,
    LOG_LEVEL_WARNING,
    LOG_LEVEL_ERROR,
    LOG_LEVEL_CRITICAL,
    LOG_LEVEL_NONE // Disable all logs
} log_level_t;

Data Structure Design

typedef struct 
{
    uint32_t timestamp;
    log_level_t level;
    uint16_t line_number;
    const char *filename;
    const char *function;
    char message[LOG_MAX_MESSAGE_LENGTH];
} log_entry_t;

typedef struct 
{
    void (*output_func)(const log_entry_t*);
    log_level_t current_level;
    bool enabled;
    uint32_t dropped_count; // Count of dropped logs
} logger_t;

Implementation

Core API Implementation

// Initialize logging system
void log_init(log_level_t default_level, 
              void (*output_func)(const log_entry_t*));

// Set log level
void log_set_level(log_level_t level);

// Core logging function
void log_write(log_level_t level, 
               const char* filename, 
               uint16_t line,
               const char* function,
               const char* format, ...);

Macro Definitions to Simplify Calls

#define LOG_DEBUG(format, ...) \
    log_write(LOG_LEVEL_DEBUG, __FILE__, __LINE__, __func__, format, ##__VA_ARGS__)

#define LOG_INFO(format, ...) \
    log_write(LOG_LEVEL_INFO, __FILE__, __LINE__, __func__, format, ##__VA_ARGS__)

#define LOG_WARN(format, ...) \
    log_write(LOG_LEVEL_WARNING, __FILE__, __LINE__, __func__, format, ##__VA_ARGS__)

#define LOG_ERROR(format, ...) \
    log_write(LOG_LEVEL_ERROR, __FILE__, __LINE__, __func__, format, ##__VA_ARGS__)

Circular Buffer Implementation

To avoid blocking the main program during log output, implement a circular buffer:

#define LOG_BUFFER_SIZE 1024

typedef struct 
{
    log_entry_t entries[LOG_BUFFER_SIZE];
    uint32_t head;
    uint32_t tail;
    bool full;
} log_buffer_t;

// Initialize buffer
void log_buffer_init(log_buffer_t* buffer);

// Write log to buffer
bool log_buffer_put(log_buffer_t* buffer, const log_entry_t* entry);

// Read log from buffer
bool log_buffer_get(log_buffer_t* buffer, log_entry_t* entry);

Output Processing Thread

Create a dedicated thread to handle log output:

void log_output_task(void* argument) {
    log_entry_t entry;
    
    while(1) {
        if(log_buffer_get(&log_buffer, &entry)) {
            // Call output function
            if(logger.output_func != NULL) {
                logger.output_func(&entry);
            }
        } else {
            // Buffer empty, sleep and wait
            osDelay(10);
        }
    }
}

Formatting Output Function

void log_format_default(const log_entry_t* entry, char* buffer, size_t size) {
    const char* level_str;
    
    switch(entry->level) {
        case LOG_LEVEL_DEBUG: level_str = "DEBUG"; break;
        case LOG_LEVEL_INFO: level_str = "INFO"; break;
        case LOG_LEVEL_WARNING: level_str = "WARN"; break;
        case LOG_LEVEL_ERROR: level_str = "ERROR"; break;
        case LOG_LEVEL_CRITICAL: level_str = "CRITICAL"; break;
        default: level_str = "UNKNOWN"; break;
    }
    
    snprintf(buffer, size, "[%08lu][%s] %s:%d (%s) - %s",
             entry->timestamp,
             level_str,
             entry->filename,
             entry->line_number,
             entry->function,
             entry->message);
}

Advanced Features

Log Filtering

Implement filtering based on module, level, and keywords:

typedef struct {
    const char* module_name;
    log_level_t min_level;
} log_filter_t;

void log_add_filter(const char* module, log_level_t min_level);
bool log_check_filter(const char* filename, log_level_t level);

Asynchronous Log Processing

Use DMA or dedicated hardware to implement asynchronous serial output, reducing CPU usage.

Log Compression

Perform simple compression before storing to the file system:

void log_compress_entry(const log_entry_t* entry, uint8_t* output, size_t* output_size);

System Status Logging

Automatically log critical system status:

void log_system_status(void) {
    LOG_INFO("System status - Heap free: %lu, CPU usage: %d%%", 
             get_free_heap(), get_cpu_usage());
}

Optimization

Memory Optimization

  • Use static memory allocation to avoid fragmentation
  • Optimize string storage to avoid duplication
  • Implement log message pool to reuse memory

Performance Optimization

  • Reduce string operations, use memcpy instead of strcpy
  • Avoid calling formatting functions in critical paths
  • Use bitwise operations instead of division and multiplication

Power Consumption Optimization

  • Implement log batching to reduce device wake-up frequency
  • Disable non-critical logs in low power mode
  • Dynamically adjust log level based on power status

Testing

Unit Testing

Write test cases to validate core functionality:

void test_log_level_filtering(void) {
    log_set_level(LOG_LEVEL_INFO);
    
    LOG_DEBUG("This should not appear"); // Should be filtered
    LOG_INFO("This should appear"); // Should be displayed
    
    // Validate actual output
}

Performance Testing

Measure execution time and memory usage in worst-case scenarios:

void test_log_performance(void) {
    uint32_t start_time = get_current_time();
    
    for(int i = 0; i < 1000; i++) {
        LOG_INFO("Performance test message %d", i);
    }
    
    uint32_t duration = get_current_time() - start_time;
    printf("1000 logs took %lu ms\n", duration);
}

Stress Testing

Test behavior when the buffer is full:

void test_log_stress(void) {
    log_set_level(LOG_LEVEL_DEBUG);
    
    // Rapidly generate a large number of logs to fill the buffer
    for(int i = 0; i < LOG_BUFFER_SIZE * 2; i++) {
        LOG_DEBUG("Stress test message %d", i);
    }
    
    // Verify the system did not crash and recorded the count of dropped logs
}

Integration

Integration with RTOS

Provide RTOS-specific adaptation layer:

#ifdef USE_FREERTOS
#include "FreeRTOS.h"
#define LOG_MUTEX_TYPE SemaphoreHandle_t
#define LOG_MUTEX_CREATE() xSemaphoreCreateMutex()
#define LOG_MUTEX_LOCK(m) xSemaphoreTake(m, portMAX_DELAY)
#define LOG_MUTEX_UNLOCK(m) xSemaphoreGive(m)
#endif

Configuration System

Provide compile-time and runtime configuration options:

typedef struct {
    log_level_t default_level;
    size_t buffer_size;
    bool enable_timestamp;
    bool enable_filename;
    output_destination_t destination;
} log_config_t;

void log_configure(const log_config_t* config);

Production Environment Considerations

  • Provide mechanisms to dynamically adjust log levels without restarting the system
  • Implement remote log transmission and centralized management
  • Add log rotation and automatic cleanup features

Conclusion

The design presented in this article provides a solid foundation that can be expanded and optimized according to specific project requirements.

A good logging system not only accelerates the debugging process but also provides valuable operational insights after product deployment, making it an indispensable part of embedded systems.

By following layered design principles, implementing appropriate abstractions, and providing flexible configuration options, a powerful and efficient logging system can be created to meet the full lifecycle needs from development debugging to production monitoring.

How to Design a Logging System for Embedded Software

Follow 【Learn Embedded Together】 to become better together

If you find this article useful, click “Share”, “Like”, “Recommend”!

Leave a Comment