In Part 2, we added colors, severity tags, and file/line information to MicrOS logs. Now we’re going to take it further: instead of just printing everything, we’ll give developers fine-grained control over what gets logged.
This is crucial in embedded systems, where every byte of flash and every CPU cycle counts. We want debug detail when we need it, but zero overhead when we don’t.
The Two Layers of Filtering
MicrOS logging supports two filters:
- Global filter
- Controlled by
CONFIG_MICROS_LOG_LEVEL. - Defines the absolute maximum verbosity compiled into the system.
- Example: if you set it to
INFO, then allD()logs vanish at compile time.
- Controlled by
- Per-module filter
- Defined by
MICROS_LOG_REGISTER(module, level). - Lets each source file decide how chatty it should be.
- Example:
bootmight default toINFO, whileschedulerruns atDEBUG.
- Defined by
The effective log level is the intersection of both.
The Global Ceiling
At build time, you choose the global level:
#define CONFIG_MICROS_LOG_LEVEL MICROS_LOG_LEVEL_DEBUG
This controls which macros are compiled in. For example, if the global level is INFO, then the D() macro literally expands to nothing:
#if CONFIG_MICROS_LOG_LEVEL >= MICROS_LOG_LEVEL_DEBUG
#define D(...) _log(MICROS_LOG_LEVEL_DEBUG, _micros_log_module_name, __FILE__, __LINE__, __VA_ARGS__)
#else
#define D(...) do {} while (0)
#endif
So debug logs don’t even exist in the binary.
Registering a Module
Each source file declares itself like this:
MICROS_LOG_REGISTER(main, MICROS_LOG_LEVEL_INFO);
This expands to:
static const char* _micros_log_module_name = "main";
static const micros_log_level_t _micros_log_level = MICROS_LOG_LEVEL_INFO;
So now the macros know both which module is speaking and how chatty it’s allowed to be.
The Logging Macros
Here’s how an info log works:
#if CONFIG_MICROS_LOG_LEVEL >= MICROS_LOG_LEVEL_INFO
#define I(...) \
do { \
if (_micros_log_level >= MICROS_LOG_LEVEL_INFO) { \
_log(MICROS_LOG_LEVEL_INFO, _micros_log_module_name, __FILE__, __LINE__, __VA_ARGS__); \
} \
} while (0)
#else
#define I(...) do {} while (0)
#endif
- First, we check the global level (
CONFIG_MICROS_LOG_LEVEL). - Then, at runtime, we check the module’s level (
_micros_log_level).
This way, you can build with DEBUG globally but keep certain modules quieter by registering them with INFO or WARN.
Real Output: init_functions in QEMU
Here’s what it looks like when running the init_functions sample:
[D] [main] --/home/grzegorz/projects/micros/samples/init_functions/main.c:22-- System clock initialized - simulated
[D] [main] --/home/grzegorz/projects/micros/samples/init_functions/main.c:26-- UART initialized - simulated
[D] [main] --/home/grzegorz/projects/micros/samples/init_functions/main.c:31-- System initialized successfully - simulated
[D] [main] --/home/grzegorz/projects/micros/samples/init_functions/main.c:35-- Very early init function in section .init_array.50 - simulated
[I] [main] MicrOS Initialization Framework Example
[I] [main] System is up and running!
[I] [main] Main function is exiting now.
[I] [boot] Main function returned with code 0
[D] [main] --/home/grzegorz/projects/micros/samples/init_functions/main.c:39-- Very late fini function in section .fini_array - simulated
[I] [boot] System halted after main return
Notice:
mainis set toDEBUG, so we see detailed traces with file/line.bootis set toINFO, so only info-level logs are printed — no debug noise.
This is the power of per-module filtering: you see the details where you need them, but keep the rest of the system quiet.
Efficiency Matters
Why not just check levels at runtime? Because:
- Logs above the global level are compiled out completely → zero code size.
- The runtime check only applies inside enabled macros → minimal cost.
- Disabled logs don’t even reach
_log()→ no wasted formatting.
This makes the system suitable even for tiny Cortex-M0 devices with <32 KB of flash.
Where We Stand
At this point, MicrOS logging provides:
- Severity levels with colors (from Part 2).
- Global filtering to keep the binary lean.
- Per-module filtering for fine-grained verbosity.
- Minimal runtime overhead when logs are disabled.
This is already a professional-grade logging system for an embedded OS.
Coming Up Next
In Part 4, we’ll look at how to extend the framework:
- Changing log levels at runtime (via UART shell or debug CLI).
- Routing logs to different backends (UART, RTT, memory buffer, network).
- Adding timestamps for precise sequencing.
- Even exploring structured logs for automated testing.
That’s where logging goes from debugging tool to system-level instrumentation.
👉 Stay tuned for Part 4: Extending the Framework — where we make MicrOS logs dynamic, flexible, and even more powerful.