These templates provide clean, reusable C++ interfaces for STM32 HAL-based communication peripherals - including UART, I²C, SPI, and CAN.
They serve as a foundation for developing firmware that needs structured communication layers without relying on autogenerated or overly complex code.
Each communication class wraps STM32’s HAL drivers with simple, consistent APIs that handle initialization, blocking or DMA data transfers, and common helper functions (e.g. register access, buffer management).
The goal is to make embedded communication code easy to read, reuse, and extend - while still keeping full control over low-level HAL handles and timing.
| Interface | Description | Key Features |
|---|---|---|
| UART | Universal Asynchronous Receiver/Transmitter | DMA circular RX buffer, blocking TX, string and binary send helpers |
| I²C | Inter-Integrated Circuit | 8/16-bit register access, memory read/write, device scanning |
| SPI | Serial Peripheral Interface | Manual CS control, blocking TX/RX, register read/write helpers |
| CAN | Controller Area Network (optional) | Standard ID transmit/receive, configurable filters |
/Core
/Inc
CommStatus.hpp
UartComm.hpp
I2cComm.hpp
SpiComm.hpp
CanComm.hpp
/Src
UartComm.cpp
I2cComm.cpp
SpiComm.cpp
CanComm.cpp
main.cpp
Each class is self-contained. You only need to include the header for the peripheral you’re using.
#include "main.h"
#include "UartComm.hpp"
#include "I2cComm.hpp"
#include "SpiComm.hpp"
#include "CanComm.hpp"
extern UART_HandleTypeDef huart2;
extern I2C_HandleTypeDef hi2c1;
extern SPI_HandleTypeDef hspi1;
static uint8_t rxBuf[256];
int main() {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_USART2_UART_Init();
MX_I2C1_Init();
MX_SPI1_Init();
UartComm uart(&huart2, rxBuf, sizeof(rxBuf));
I2cComm i2c(&hi2c1);
SpiComm spi(&hspi1, GPIOA, GPIO_PIN_4);
uart.begin();
uart.writeStr("System initialized.\r\n");
// Example: I²C scan
auto addr = i2c.scanFirst();
if (addr != 0xFFFF) {
char msg[32];
snprintf(msg, sizeof(msg), "I2C device found at 0x%02X\r\n", addr);
uart.write(reinterpret_cast<uint8_t*>(msg), strlen(msg));
}
// Example: SPI read register
uint8_t whoami = 0;
if (spi.readReg(0x00, whoami) == CommStatus::OK) {
char msg[32];
snprintf(msg, sizeof(msg), "WHOAMI=0x%02X\r\n", whoami);
uart.write(reinterpret_cast<uint8_t*>(msg), strlen(msg));
}
while (1) {
uint8_t in[32];
auto n = uart.read(in, sizeof(in));
if (n) uart.write(in, n); // echo
}
}Each class expects its HAL handles to be generated by STM32CubeMX (or CubeIDE).
Enable the following:
- Mode: Asynchronous
- DMA: RX enabled (Circular)
- NVIC: Enabled
- Baud rate, parity, word length as needed
- Mode: Standard or Fast
- Internal or external pull-ups on SDA/SCL
- No special interrupts required
- Mode: Full-Duplex Master
- Add manual GPIO for chip select (set high in init)
- Configure bit timing
- Activate filter bank 0 (or custom)
- Set standard ID mask to open filter if needed
- Simplicity first: HAL handles are passed into constructors, not hidden.
- Low overhead: No dynamic memory or STL dependencies.
- Deterministic: No heap, no interrupts unless configured by HAL.
- Extensible: Add DMA TX or IRQ callbacks easily without changing the API.
- Portable: Works across STM32 series with minimal CubeMX reconfiguration.
While STM32 HAL is written in C, C++ enables clearer abstraction layers and safer APIs:
- Constructors ensure peripheral handles are initialized before use.
- Encapsulation prevents global-variable sprawl typical in HAL projects.
- Function overloading and enums reduce parameter errors.
This hybrid approach keeps firmware readable and maintainable while preserving low-level control.
- Non-blocking TX queues with DMA
- Interrupt-driven receive callbacks
- LL (Low-Level) versions using direct register access
- Common base class for all comm interfaces