
In our embedded development, printing logs is the most commonly used debugging method. Properly printing logs can help us quickly analyze problems.
This article summarizes some rules for logging in embedded systems.
1. When to Add Logs?
(1) Error Handling
For unrecoverable serious errors, the log content should be detailed enough to help locate the problem, but should not contain sensitive information. For example, use the Error level when memory allocation fails along with log information.
(2) Key Operations
For critical processes, logs should be printed for both normal and abnormal situations. For instance, there should be corresponding log information when WiFi is turned on.
(3) System Startup and Shutdown
Recording key steps during system startup and shutdown helps analyze whether the system initialization is correct or whether the system shuts down normally.
(4) Performance Monitoring
Logs can record key performance indicators of system operation, such as CPU and memory usage, IO operations, etc., for system performance analysis and optimization.
(5) Key Data
Some key data need to be printed, as many functional issues are directly linked to the data.
(6) Communication Logs
For embedded systems that need to communicate with external devices or networks, recording communication logs can help analyze and debug communication protocols or data exchange issues.
(7) Record User Behavior
When analyzing how users interact with embedded devices, logging user behavior can be very helpful.
(8) if, switch
In branch judgments, each execution branch needs to have corresponding log information added, which can help us accurately know the direction of program execution.
(9) Information During Program Crash
For example, the call stack information when an application process crashes under Linux.
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <execinfo.h>
void func0(void)
{
printf("This is func0\n");
int *p = NULL;
*p = 1234;
}
void func1(void)
{
printf("This is func1\n");
func0();
}
void func2(void)
{
printf("This is func2\n");
func1();
}
void dump(int signo)
{
void *array[100];
size_t size;
char **strings;
size = backtrace(array, 100);
strings = backtrace_symbols(array, size);
printf("Obtained %zd stacks.\n", size);
for(int i = 0; i < size; i++)
{
printf("%s\n", strings[i]);
}
free(strings);
exit(0);
}
int main(int argc, char **argv)
{
printf("==================segmentation fault test5==================\n");
signal(SIGSEGV, &dump);
func2();
return 0;
}

2. Module Tagging
In a project, multiple modules will definitely be divided, and a module tag string can be assigned to each module, included in the log entries. This way, we can filter logs for a specific module in the log file, improving our efficiency in locating problems.
For example:
// app_wifi.c
#define LOG_TAG "[wifi_module]"
#define LOG_D(fmt, arg...) LOG_D_TAG(LOG_TAG, fmt, ##arg)
LOG_D("hello wifi module");
Output:
[wifi_module]hello wifi module
3. Module Log Switch
Setting a module log switch can conveniently narrow down the analysis range when debugging and analyzing problems. When our function design has multiple functional modules, if a problem occurs in a certain module, we only care about that module and can turn off the logging functionality of other modules while keeping the logs of the concerned module on.
For example:
// module1.c
#include "module1.h"
#if MODULE1_LOG_SWITCH
#define LOG_MODULE1(fmt, args...) DBG_PRINTF(fmt, ##args)
#else
#define LOG_MODULE1(fmt, args...)
#endif
// module2.c
#include "module2.h"
#if MODULE2_LOG_SWITCH
#define LOG_MODULE2(fmt, args...) DBG_PRINTF(fmt, ##args)
#else
#define LOG_MODULE2(fmt, args...)
#endif
// config.h
#define MODULE1_LOG_SWITCH 0
#define MODULE2_LOG_SWITCH 1
4. Timestamps
Logs should include timestamps, making it easy to view the execution time of a certain piece of code and determine when problems occur. The timestamp should ideally be accurate to microseconds/milliseconds.
5. Log Levels
Using different log levels can help filter and control the amount of information output.
Common log levels include:
-
Error: The program cannot run or there is a serious issue. -
Warning: Potential issues, the program can continue running. -
Info: Information about the program’s running status. -
Debug: Detailed debugging information, including variable values and program flow. -
Verbose: Very detailed information for in-depth debugging.
6. Unified Format
To make logs easy to read, all logs should maintain a consistent format.
Common fixed information in logs includes:
-
Filename -
Line Number -
Date/Time Timestamp -
Function Name -
Module Name -
Process ID -
Thread ID
These can be combined as needed. For example:
[2024-01-14 11:12:30.666][wifi_module][func:wifi_init]
7. Filtering Control
Using dynamic filtering control for logs can dynamically adjust the output of logs, provided that the logging component used in the project has such capability. For example, EasyLogger (https://github.com/armink/EasyLogger).

8. Release/Debug Switch
Since log printing may consume considerable system resources, attention should be paid to its impact on performance. In the Release version, it may be necessary to reduce log output or remove some unnecessary logs, requiring a switch to toggle.
9. Encapsulating Log Interfaces
In embedded systems, logs are mostly output to serial terminals. However, there are scenarios where logs need to be output to screens, network devices, customized upper computers, etc. Corresponding interfaces should be reserved for flexible switching.
That concludes this sharing session. Feel free to save and forward!
———— END ————

● Column “Embedded Tools”
● Column “Embedded Development”
● Column “Keil Tutorial”
● Selected Tutorials for Embedded Columns
Click “Read Original” for more shares.