Bare-metal logging for ARM Cortex-M.
These notes describe what BareLog is, which targets it supports, how to build and integrate it, and what to watch for when something looks wrong. Read the flushing section before shipping firmware that depends on logs.
BareLog is a small C11 logging library. It does not use the heap, does not link against stdio, and formats messages with its own printf subset.
Log lines are written into an IRQ-safe ring buffer. A separate flush path
moves bytes to a backend (UART DMA, SWO/ITM, SEGGER RTT, or on-chip flash).
ISR-safe entry points (LOG_*_ISR) exist alongside the normal macros.
Optional FreeRTOS / ThreadX locking covers the flush path. Compile-time and
runtime level filters, module whitelist/blacklist, hex dumps, optional DWT
timestamps, and optional ANSI colours are available.
Version macros live in include/blog_config.h (BLOG_VERSION_*). See
CHANGELOG.md for release history. The current tree is 0.9.1.
License: BSD-3-Clause (LICENSE).
Originally exercised on STM32F1; STM32F4 is also supported. CMake selects linker script, startup, device header, and clocks from the chip name.
| Family | Coverage (high level) | Flash | RAM | Max HCLK |
|---|---|---|---|---|
| STM32F1 | F100 / F101 / F102 / F103 / F105 / F107 | 32–1024K | 6–96K | 72 MHz |
| STM32F4 | F401…F479 lineup used by the port (40+ parts) | 64–2048K | 32–384K | 84–180 MHz |
Other Cortex-M parts are not first-class ports yet. SWO and DWT paths assume ARMv7-M / ARMv7E-M (M3/M4-class).
Host tests (Unity, no ARM toolchain required):
just test
Cross builds need arm-none-eabi-gcc on PATH (or ARM_GCC_PATH):
just build-arm STM32F103xB # default F1 chip if omitted
just build-arm-f4 STM32F407VG
just build-all # all four backends (F1 demo)
just size # arm-none-eabi-size per backend
just flash-jlink # or: just flash-stlink
CMake examples:
cmake -B build --toolchain toolchain-arm.cmake \
-DSTM32F1_CHIP=STM32F103xB -DBLOG_BACKEND=BLOG_BACKEND_UART_DMA .
cmake -B build --toolchain toolchain-arm.cmake \
-DBLOG_TARGET=stm32f4 -DSTM32F4_CHIP=STM32F429VI \
-DBLOG_BACKEND=BLOG_BACKEND_SWO .
When this repository is the CMake top level, BLOG_BUILD_DEMO defaults to
ON and produces BareLog.elf / .hex / .bin. When BareLog is pulled in
with add_subdirectory(), the demo defaults to OFF and only the blog
static library is built.
cmake ... -DBLOG_BUILD_DEMO=OFF # library only
cmake ... -DBLOG_BUILD_DEMO=ON # force demo even as a subdirectory
set(BLOG_BACKEND BLOG_BACKEND_UART_DMA CACHE STRING "")
set(BLOG_TARGET stm32f1 CACHE STRING "")
set(STM32F1_CHIP STM32F103xB CACHE STRING "")
add_subdirectory(path/to/BareLog)
target_link_libraries(your_app PRIVATE blog)
Port paths resolve via BLOG_ROOT inside port/*/target.cmake, so the
tree need not be the CMake source root.
Common compile definitions (defaults in include/blog_config.h):
| Define | Default | Meaning |
|---|---|---|
BLOG_BACKEND |
UART DMA | 1 UART DMA, 2 SWO, 3 RTT, 4 Flash |
BLOG_LEVEL |
DEBUG | Compile-time minimum level |
BLOG_RINGBUF_SIZE |
256 | Power of two |
BLOG_UART_BAUDRATE |
115200 | UART backend |
BLOG_UART_CLOCK_HZ |
72e6 / port | UART clock used for BRR |
BLOG_ENABLE_TIMESTAMP |
0 | Prefix with DWT CYCCNT |
BLOG_ENABLE_COLOR |
0 | ANSI colour in level tags |
BLOG_MODULE |
0 | Per-translation-unit module id |
BLOG_RTOS |
0 | 0 none, 1 FreeRTOS, 2 ThreadX |
BLOG_BUILD_DEMO |
top-level | Build demo firmware image |
Printf subset: %d %i %u %x %X %p %c %s %%, length modifiers l / ll
(including 64-bit for ll), optional width and 0 pad. No floating point.
#include "blog.h"
int main(void)
{
blog_init();
LOG_INFO("System started, clock=%u", SystemCoreClock);
LOG_DEBUG("value=0x%08x", reg);
LOG_WARN("Low memory: %u bytes free", free);
LOG_ERROR("Sensor %d failed", id);
uint8_t buf[64];
LOG_HEXDUMP(BLOG_LEVEL_INFO, "RX", buf, sizeof(buf));
blog_flush_blocking(BLOG_FLUSH_MAX_ITERATIONS);
blog_deinit();
}
From interrupt context use the ISR variants; still flush from thread or main context:
void DMA1_Channel4_IRQHandler(void)
{
LOG_DEBUG_ISR("ISR flag=0x%x", flags);
}
Module filtering (optional):
#define BLOG_MODULE 1u /* in the producing .c file */
blog_set_module_whitelist(0x03u);
blog_set_module_blacklist(0x04u);
RTOS: define BLOG_RTOS=1 (FreeRTOS) or 2 (ThreadX) on the blog
target, and call blog_init() after the scheduler is up so the recursive
mutex can be created.
LOG_* only formats into the ring buffer. Nothing is emitted on the wire
(or into flash) until blog_flush() or blog_flush_blocking() runs.
Call blog_flush() from the main loop, an idle hook, or a periodic timer.
Call blog_flush_blocking(...) before sleep, reboot, or blog_deinit().
If you never flush, the buffer fills and overflow is counted in dropped.
| Backend | Typical use | Notes |
|---|---|---|
| UART DMA | General / production | Needs UART pins; baud via config |
| SWO/ITM | Debug without UART | Probe + SWO pin; ARMv7-M/E-M |
| SEGGER RTT | Fast interactive debug | J-Link; needs submodule ext/segger-rtt |
| Flash | Post-mortem / crash log | blog_init() resumes; wipe with blog_flash_reset() |
Minimal samples live under examples/ (uart_dma, swo, rtt, flash).
Figures below are for the demo BareLog.elf on STM32F103xB with -Os
(library + startup + small main), not a stripped consumer link. Remeasure
with just build-all && just size.
| Backend | text (ROM) | data+bss (RAM) |
|---|---|---|
| UART DMA | 4412 B | 920 B |
| SWO | 4236 B | 920 B |
| SEGGER RTT | 5036 B | 1264 B |
| Flash | 4520 B | 928 B |
include/ Public headers
src/ Core and default backend implementations
port/stm32f1/ F1 port (startup, linker, device, UART/flash hooks)
port/stm32f4/ F4 port
cmsis/ CMSIS core headers shipped for the ports
ext/ SEGGER RTT (git submodule)
tests/ Host Unity tests
tools/ Flash decode, clang_check.py
examples/ Per-backend sketches
- No output: confirm a flush path is running; check backend and probe wiring.
- Partial output / gaps: ring buffer overflow (
dropped); flush more often or enlargeBLOG_RINGBUF_SIZE. - Flash log empty after reset:
blog_init()does not erase; useblog_flash_reset()when you intentionally wipe. Decode dumps withjust flash-decode <file>. - IDE noise about unused includes in
blog.h: that header is an umbrella API; see.clangd(UnusedIncludes: None). - Host
just testhangs on older trees: current concurrent tests need the host IRQ shim (src/blog_irq_host.c).
clang syntax pass over compile databases:
python tools/clang_check.py