In embedded development, a basic logging system can record the program's runtime status, but a powerful logging system can proactively help us discover and diagnose issues. Once the basic functionality is implemented, we can use some advanced techniques to transform the logging system from a passive "recorder" into an active "diagnostic assistant".
Hex Dump — Highly recommended for debugging communication protocols such as UART, I2C, SPI, CAN, or parsing unknown data formats. Plain text logs often fall short; Hex Dump can display each byte of binary data in hexadecimal form, like taking an X-ray of the data stream, allowing us to clearly see the true nature of each data bit.
The standard assert macro typically just terminates the program when triggered, but in embedded systems, it should also capture contextual states: variable values, function call stacks, system status, etc. By integrating assertions with the LOG_FATAL level, we can log critical diagnostic information just before the system "dies".
// Enhanced assertion implementation
#define ENHANCED_ASSERT(condition) \
do { \
if (!(condition)) { \
LOG_FATAL("Assertion Failed: %s", #condition); \
LOG_FATAL("File: %s, Line: %d", __FILE__, __LINE__); \
LOG_FATAL("Free Heap: %lu bytes", xPortGetFreeHeapSize()); \
LOG_FATAL("Task: %s", pcTaskGetName(NULL)); \
/* Log more system status */ \
log_system_status(); \
/* Save critical logs to non-volatile storage */ \
log_backup_to_flash(); \
/* Wait for a while to ensure log output is complete */ \
vTaskDelay(pdMS_TO_TICKS(100)); \
/* Perform system reset */ \
NVIC_SystemReset(); \
} \
} while(0)
// Usage example
void process_sensor_data(int16_t *data) { ENHANCED_ASSERT(data != NULL);
// Boundary check
int32_t average = calculate_average(data, SENSOR_COUNT); ENHANCED_ASSERT(average >= -1000 && average <= 1000);
// Normal processing flow...
}
Different levels of assertions can also be implemented.
// Strict assertions during development
#ifdef DEBUG
#define STRICT_ASSERT(cond) ENHANCED_ASSERT(cond)
#else
#define STRICT_ASSERT(cond) ((void)0) // Remove in release version
#endif
// Critical assertions in production (always enabled)
#define CRITICAL_ASSERT(cond) ENHANCED_ASSERT(cond)
Practical solution: Circular buffer design
// Log black box structure definition
typedef struct { uint32_t magic; // Magic number to indicate data structure validity uint32_t sequence; // Sequence number for sorting uint32_t timestamp; // Timestamp uint8_t level; // Log level char message[64]; // Log content} log_blackbox_entry_t;
// Black box manager
typedef struct { uint32_t start_addr; // Flash start address uint32_t capacity; // Total capacity uint32_t write_ptr; // Write pointer uint8_t initialized; // Initialization flag} log_blackbox_t;
// API implementation
void blackbox_init(void) { // Initialize Flash driver, check existing data, etc.}
void blackbox_save(uint8_t level, const char *msg) { // Only save logs of ERROR level and above if (level < LOG_ERROR) return;
log_blackbox_entry_t entry; entry.magic = 0xDEADBEEF; entry.sequence = get_next_sequence(); entry.timestamp = HAL_GetTick(); entry.level = level; strncpy(entry.message, msg, sizeof(entry.message) - 1);
// Write to Flash flash_write(blackbox.write_ptr, &entry, sizeof(entry));
// Update write pointer to implement circular buffer blackbox.write_ptr += sizeof(entry); if (blackbox.write_ptr >= blackbox.start_addr + blackbox.capacity) { blackbox.write_ptr = blackbox.start_addr; }}
void blackbox_dump_after_reboot(void) { // Called at system startup to read and print black box content log_info("=== System Blackbox Dump ===");
// Read all valid entries from Flash and output for (uint32_t addr = blackbox.start_addr; addr < blackbox.start_addr + blackbox.capacity; addr += sizeof(log_blackbox_entry_t)) {
log_blackbox_entry_t entry; flash_read(addr, &entry, sizeof(entry));
if (entry.magic == 0xDEADBEEF) { const char *level_str[] = {"FATAL", "ERROR", "WARN"}; log_info("[%s] Seq:%lu Time:%lums %s", level_str[entry.level], entry.sequence, entry.timestamp, entry.message); } }}
Integrate into the logging system
// Wrap log function to automatically save critical logs to black box
void log_message(uint8_t level, const char *format, ...) { va_list args; va_start(args, format);
// Normal log output vprintf(format, args);
// If it's a critical error, also save to black box if (level <= LOG_ERROR) { char buf[64]; vsnprintf(buf, sizeof(buf), format, args); blackbox_save(level, buf); }
va_end(args);}
With these techniques, we can build a truly powerful embedded logging diagnostic system. When issues arise, you no longer need to guess blindly; instead, you have a comprehensive diagnostic tool to quickly locate and resolve problems, making problem analysis much simpler.