Using spdlog in Embedded Systems: Configuration Guide for Microcontrollers
Yes, spdlog can be used in embedded systems by defining compile-time macros to disable exceptions and thread safety, utilizing level filters to strip logging code at compile time, and implementing lightweight custom sinks for hardware-specific output.
The gabime/spdlog library is a header-only C++ logging framework designed with compile-time configuration options that make it adaptable to microcontroller environments. By leveraging preprocessor definitions found in include/spdlog/common.h and the pluggable sink architecture defined in include/spdlog/sinks/base_sink.h, you can deploy a type-safe logging interface on memory-constrained devices without requiring dynamic memory allocation or OS threading primitives.
Essential Compile-Time Macros for Embedded Targets
Disabling heavy runtime features is the first step toward fitting spdlog into a firmware image. The library provides several macros in include/spdlog/common.h that remove dependencies on OS-specific threading and exception handling.
Removing Exception Handling
Embedded toolchains often disable C++ exceptions to save code space. Define SPDLOG_NO_EXCEPTIONS before including spdlog headers to replace spdlog::spdlog_ex throws with error codes or assertions:
#define SPDLOG_NO_EXCEPTIONS
#include <spdlog/spdlog.h>
This macro ensures that error conditions in include/spdlog/common.h use alternative reporting mechanisms compatible with -fno-exceptions compiler flags.
Disabling Thread Safety Overhead
By default, spdlog protects log calls with std::mutex to support multi-threaded applications. For single-threaded firmware or bare-metal contexts, define SPDLOG_DISABLE_THREAD_SAFETY (available since v1.5) to compile out all mutex operations:
#define SPDLOG_DISABLE_THREAD_SAFETY
#include <spdlog/spdlog.h>
Alternatively, use SPDLOG_NO_TLS to disable thread-local storage while retaining basic thread safety if your RTOS does not support TLS.
Compile-Time Log Level Filtering
Control code size by setting the maximum log level at compile time. When you define SPDLOG_LEVEL_INFO or SPDLOG_LEVEL_OFF in include/spdlog/common.h, the preprocessor removes all lower-level logging calls entirely:
#define SPDLOG_LEVEL_INFO // Debug and trace calls are compiled out
#include <spdlog/spdlog.h>
Setting SPDLOG_LEVEL_OFF generates zero logging overhead, effectively removing all SPDLOG_* macros from the binary.
Memory Management Strategies
Embedded systems often avoid dynamic heap allocation. While some standard sinks like basic_file_sink allocate buffers internally, you can configure spdlog to operate with static memory or minimal stack usage.
Using the Null Sink
For size-optimized builds where logging is temporarily disabled or routed elsewhere, include/spdlog/sinks/null_sink.h provides a sink that discards output without heap allocation:
#include <spdlog/sinks/null_sink.h>
auto null_logger = std::make_shared<spdlog::logger>(
"null",
std::make_shared<spdlog::sinks::null_sink_mt>()
);
spdlog::set_default_logger(null_logger);
Stack-Based Formatting
By default, spdlog uses the {fmt} library for formatting. You can reduce heap usage by ensuring formatting occurs on the stack, or by compiling with SPDLOG_USE_STD_FORMAT disabled to keep formatting operations within static or stack memory bounds.
Creating Custom Hardware Sinks
The pluggable sink interface in include/spdlog/sinks/base_sink.h allows you to route logs to hardware peripherals like UART, USB CDC, or CAN bus. Inherit from spdlog::sinks::base_sink<Mutex> and implement the pure virtual methods sink_it_() and flush_().
UART Sink Implementation
Below is a complete example for an ARM Cortex-M MCU using HAL drivers. This implementation disables exceptions and threading, then creates a UART sink that transmits formatted logs via blocking UART calls:
#define SPDLOG_NO_EXCEPTIONS
#define SPDLOG_DISABLE_THREAD_SAFETY
#define SPDLOG_LEVEL_DEBUG
#include <spdlog/spdlog.h>
#include <spdlog/sinks/base_sink.h>
#include "uart_driver.h" // Hardware-specific UART abstraction
template<typename Mutex>
class uart_sink : public spdlog::sinks::base_sink<Mutex>
{
public:
explicit uart_sink(UART_HandleTypeDef* huart) : huart_(huart) {}
protected:
void sink_it_(const spdlog::details::log_msg& msg) override
{
fmt::memory_buffer formatted;
spdlog::sinks::base_sink<Mutex>::formatter_->format(msg, formatted);
HAL_UART_Transmit(
huart_,
reinterpret_cast<const uint8_t*>(formatted.data()),
static_cast<uint16_t>(formatted.size()),
100
);
}
void flush_() override { /* No buffering required for UART */ }
private:
UART_HandleTypeDef* huart_;
};
int main()
{
UART_HandleTypeDef huart1; // Assume initialized by HAL
auto uart = std::make_shared<uart_sink<std::mutex>>(&huart1);
auto logger = std::make_shared<spdlog::logger>("uart", uart);
spdlog::set_default_logger(logger);
SPDLOG_DEBUG("Sensor reading: {:.2f}", 23.456);
}
When threading is disabled, you can substitute std::mutex with spdlog::details::null_mutex to eliminate lock overhead entirely.
Build System Configuration
When integrating spdlog via CMake, set the relevant options in your CMakeLists.txt to ensure the macros propagate throughout your build:
target_compile_definitions(your_firmware PRIVATE
SPDLOG_NO_EXCEPTIONS
SPDLOG_DISABLE_THREAD_SAFETY
SPDLOG_LEVEL_INFO
)
This approach ensures that all translation units respect the embedded constraints without requiring macro definitions in every source file.
Summary
- Define
SPDLOG_NO_EXCEPTIONSininclude/spdlog/common.hto remove C++ exception dependencies when using toolchains with-fno-exceptions. - Use
SPDLOG_DISABLE_THREAD_SAFETYto eliminate mutex overhead in single-threaded or bare-metal firmware. - Set
SPDLOG_LEVEL_*macros to strip logging calls at compile time, withSPDLOG_LEVEL_OFFgenerating zero overhead. - Implement custom sinks inheriting from
include/spdlog/sinks/base_sink.hto redirect output to UART or other hardware peripherals without dynamic allocation. - Leverage header-only design by including specific headers like
include/spdlog/spdlog.handinclude/spdlog/sinks/null_sink.hwithout linking external libraries.
Frequently Asked Questions
Can spdlog run on bare-metal microcontrollers without an RTOS?
Yes, spdlog can run on bare-metal systems by defining SPDLOG_DISABLE_THREAD_SAFETY to remove mutex dependencies and SPDLOG_NO_EXCEPTIONS to handle errors without C++ exceptions. You must provide a custom sink implementation that writes to your specific hardware interface, such as a UART peripheral.
How do I reduce the flash size overhead of spdlog in embedded builds?
Define SPDLOG_LEVEL_OFF or SPDLOG_LEVEL_INFO before including headers to remove debug and trace logging calls at compile time. Additionally, use include/spdlog/sinks/null_sink.h or minimal custom sinks that avoid formatting overhead when logging is disabled.
Does spdlog require dynamic memory allocation on embedded targets?
While some standard sinks allocate heap memory, you can avoid dynamic allocation by implementing custom sinks that use static buffers or write directly to hardware registers. The base_sink interface in include/spdlog/sinks/base_sink.h supports stack-based formatting through fmt::memory_buffer, allowing zero-heap operation.
What C++ standard library components does spdlog require?
spdlog requires basic STL headers including <chrono>, <string>, and <vector>. Most modern embedded toolchains (ARM GCC, Clang for embedded) provide these. If your environment lacks standard library support, you can define SPDLOG_FMT_EXTERNAL to use an external fmt library adapted to your platform, as referenced in include/spdlog/fmt.h.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →