The logging system is the system that records diagnostic messages with a level, source, and timestamp and delivers them to one or more backends.
On a microcontroller, a serial console remains one of the most useful windows for observing what is happening inside the firmware.
Console debugging (the famous “printf debugging”) is still the most widely used technique in the embedded world. But be careful. In a Real-Time Operating System (RTOS) like Zephyr, using printf indiscriminately can be dangerous. It can block interrupts, break a sensor’s timing, or cause Bluetooth communication to fail.
We will compare direct output with printk and the logging subsystem, which allows filtering and deferring messages.
printk vs logging: Comparison
| Feature | printk | Logging (LOG_INF) |
|---|---|---|
| Execution | Immediate or redirected | Immediate or deferred |
| Performance | High impact | Minimal impact |
| Usage in Interrupts | Avoid long messages | Supported, with limited cost and buffer |
| Format | Manual (%d, \n) | Automatic (Timestamp, color, level) |
| Filtering | No (everything is output) | Yes (by module and level) |
The Classic Method: printk
Zephyr provides a function called printk. It is the direct equivalent of the standard C printf or Arduino’s Serial.print.
Its operation is simple: it formats a string and sends it byte-by-byte over the UART configured as the console.
#include <zephyr/sys/printk.h>
int main(void) {
int counter = 10;
printk("Hello from Zephyr! Counter: %d\n", counter);
return 0;
}When to use it?
- During the early boot phase.
- For critical messages that must be output no matter what, even if the system is falling apart.
- In very simple examples (“Hello World”).
The Problem with printk
Without redirection to the logging subsystem, printk delivers output immediately, and the cost depends on the console backend. If CONFIG_LOG_PRINTK is active, its messages are redirected to the logger and, in deferred mode, they can also remain pending.
At 115200 baud, sending a long phrase can take several milliseconds. In CPU time, that is an eternity. If you do this inside an Interrupt Service Routine (ISR) or a high-priority thread, you can kill your system’s performance.
The Professional Method: Logging Subsystem
The logging subsystem separates the generation of the message from its final output when it works in deferred mode.
Instead of spitting data directly to the wire, the Logging system works like this:
- Your code calls
LOG_INF("Message"). - The message is copied very quickly to a buffer in RAM.
- Your code continues executing immediately (barely losing any cycles).
- A processing thread extracts the messages and delivers them to the configured backend.
How to Implement Logging in Your Code
To use this, we need three steps in our .c file:
Register the module: Give our file a name (e.g., “my_motor”).
Include the header: <zephyr/logging/log.h>.
Use the macros: LOG_INF, LOG_ERR, etc.
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
/* 1. Register the module. The name will appear on the console */
LOG_MODULE_REGISTER(my_app, LOG_LEVEL_INF);
int main(void) {
int sensor_val = 25;
/* This is informative */
LOG_INF("System has started correctly");
if (sensor_val > 20) {
/* This is a warning */
LOG_WRN("High temperature: %d", sensor_val);
}
/* This is debug (won't be output if level is INF) */
LOG_DBG("Raw sensor value: 0x1A");
return 0;
}The console output will look like this, automatically formatted with colors and timestamps:
[00:00:00.100,000] <inf> my_app: System has started correctly
[00:00:00.105,000] <wrn> my_app: High temperature: 25Notice that the LOG_DBG message did not appear. This is because we set LOG_LEVEL_INF when registering the module. The system automatically discards any message with a lower level to save time and space.
Configuration in prj.conf
For all this to work, you need to enable the subsystem in your prj.conf:
# Enable console and logging
CONFIG_CONSOLE=y
CONFIG_UART_CONSOLE=y
CONFIG_LOG=y
# Optional: Deferred mode is the default and recommended mode
CONFIG_LOG_MODE_DEFERRED=y
# Optional: Increase the buffer if messages are being lost
CONFIG_LOG_BUFFER_SIZE=2048Module Filtering
In a project with 10 files, the motor driver, the Wi-Fi driver, the sensor driver… can all be outputting logs simultaneously. The console becomes chaos.
With printk you would have to go and comment out lines of code. With Zephyr’s Logging, you can silence modules from the configuration file without touching the C code.
The global level can be adjusted in prj.conf:
# Default global level: errors only
CONFIG_LOG_DEFAULT_LEVEL=1Subsystems that expose their own symbol allow configuring CONFIG_<MODULE>_LOG_LEVEL. For an application module, you can also set the level in LOG_MODULE_REGISTER, use runtime filtering, or the logging shell commands.
Using RTT (Real Time Transfer)
If you are using a professional debugger (like a Segger J-Link), you can use RTT instead of UART.
UART is slow. RTT uses the debug interface to write directly to the microcontroller’s RAM from the PC. It is extremely fast (microseconds).
To switch from UART to RTT, you just change the configuration; the LOG_INF code remains the same!
CONFIG_USE_SEGGER_RTT=y
CONFIG_LOG_BACKEND_RTT=y
CONFIG_LOG_BACKEND_UART=nDeferred mode reduces the wait at the point where the message is generated, but it does not make logging free: it consumes RAM, CPU, and bandwidth, and can lose messages if the buffer fills up.