Scan to FollowLearn Embedded Together, learn and grow together

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:
- Multi-level Logging: Support for different levels of log importance (DEBUG, INFO, WARN, ERROR, etc.)
- Low Resource Usage: Minimal memory usage and low CPU overhead
- Real-time Performance: Does not affect the real-time performance of the main program
- Multiple Output Methods: Supports various output methods such as serial port, file system, network, etc.
- Timestamp: Provides accurate time information
- Thread/Task Safety: Safe to use in a multi-tasking environment
- 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.

Follow 【Learn Embedded Together】 to become better together。
If you find this article useful, click “Share”, “Like”, “Recommend”!