Skip to content

Repository files navigation

mdrivlib

C++20 Embedded driver library

This is a work in progress, the API is in flux. Contributions, feedback, and issues are welcome.

Devices supported:

Cortex M0

  • STM32F030: partial support

Cortex M7

  • STM32H745/55/47/57 (dual-core M7/M4): full support, except built-in ADC
  • STM32F7xx (especially STM32F746, F76x): partial support

Cortex A7

  • STM32MP157 (dual-core A7 + M4): partial support, but adding features daily.

Goals:

  • Easy to use, difficult to misuse.
  • Provide modern C++20 classes for setting up and accessing internal peripherals and common external chips.
  • Leverage constexpr configurations for efficiency: suited for hard real-time systems.
  • Provide high-level functionality for common use cases in embedded projects (debouncing switches, wear-leveling, RGB color blending, filtering ADC readings).
  • Allow use of lambdas as callbacks (e.g. interrupt request handlers) with minimal overhead
  • Does not depend on dynamic memory (but provides some structures if you use it)
  • Bare-metal: does not depend on a RTOS
  • Priortize peripheral access efficiency over one-time cost of peripheral setup.
  • Unit tests (using doctest)
  • Example code (TODO: needs work!)
  • Emphasis on higher-end microcontrollers and microprocessors

Status of Drivers:

STM32 Internal Peripheral Drivers:

PeripheralHALConfig*Notes
ADCSTM32-LLtemplate paramsCircular DMA mode. F7/H7 only. Todo: split off target-specific code. Needs modernizing
BDMACMSIStemplate structMost configurations supported (H7 only, not present on F7 or MP1)
DMA2DCMSIS-Supports FillRect with Rgb565 on H7, no other modes yet. (Not present on F7 or MP1)
I2CSTM32-HALconfig structBlocking, IT, DMA modes
InterruptCortex-Assign a lambda or any callable as any IRQ Handler at run-time. Supports NVIC (Cortex-M) and GIC (Cortex-A)
MDMACMSIStemplate structSupports mem-to-mem only, with byte/word-swapping.
MPUCMSIS-Only supports disabling Data Cache on a given memory region. MPU is only present on Cortex-M7
GPIO (Pin)STM32-LLinit/ctor paramsSupports all GPIO functions (except Locking?). Run-time values.
GPIO (FPin)CMSIStemplate params"Fast Pin". Constexpr-configured, maximum efficiency. Supports high(), low(), read().
EXTI (Pin change interrupt)CMSIStemplate paramsAssign ISR callback for rising and/or falling edge for any pin. Todo: Finish migration from config struct to template params
HSEM (Semaphores)CMSISSafer wrappers over raw register access for HSEM (hardware semaphore). Supports managing multiple channel interrupt callbacks. Only for dual-core devices (H7 and MP1)
QSPISTM32-HALconfig structSupports blocking and IT modes. Some chip-specific setup may be required in qspi_flash_registers.h file. Todo: use mdrivlib::Interrupt class instead of HAL Callbacks
RCCCMSIS-Safer wrapper over raw register access to enable/disable/reset peripherals
SAISTM32-HALconfig structFor streaming audio. DMA mode only. Supports RX- or TX-master. Caller provides tx/rx buffers and callbacks (where audio processing is done)
SDRAMSTM32-HALconfig structSetup/init FMC for SDRAM. Single bank supported only. Includes self-test. H7 only (MP1 SDRAM is init by U-boot)
SPI (direct peripheral)CMSIStemplate structSpiPeriph: efficient wrapper over SPI registers (Currently onle for ST's SPI v2.x.x, found in H7 and MP1). Supports multi-chip buses (CS0 - CS3)
SPI (transfer)CMSIStemplate structSpiTransfer: wraps SpiPeriph. Supports blocking, polling, FIFO, fixed-size transmissions in IT mode, multi-chip buses.
TIMSTM32-LLinit/ctor paramsSimple class to initialize a TIM peripheral. Can skip initialization if periph has already been init.
UARTSTM32-HAL-WIP

External Chip Drivers:

PeripheralConfig*Notes
ADC, I2C (MAX11645)config structBlocking and IT mode
ADC, SPI (MAX11666)template structHeavily optimized. Takes FilterType template param
Codec, I2C/I2S (WM8731)config structVirtual class, ties I2C init with SAI+DMA init. Todo: make template class
DAC, SPI (MCP48FVBxx)template structSupports streaming and blocking modes
GPIO Expander, I2C (TCA9535)config structSupports inputs only (so far), I2C in IT mode
LED Driver, I2C (PCA9685)ctor/init paramsUses STM32-HAL, heavily (HAL callbacks). Supports DMA, IT, and blocking modes. Supports mono and RGB LEDs. Works well with Frame Buffer LED class. Needs modernizing
Screen, SPI/DMA (ST77XX)template structUse DMA to dump a framebuffer to SPI. Wraps SpiPeriph and a DMA driver (which is passed in as part of the config), and calls a callback when done.

High-level helpers (target-agnostic):

PeripheralConfig*Notes
AnalogIntemplate paramsGeneric class to combine multi-channel ADC sources with post-filtering. WIP
Clocks-enable/disable/reset peripheral clocks
CycleCounter-Measures cycles spent between a start and stop point
DAC Streamtemplate structAllows circular buffer to be filled in bursts, and output to DAC a steady rate
GPIO Streamtemplate structSame as DAC Stream, but for a GPIO Pin output
Frame Buffer LED-Allows random access to a contigious frame buffer, used with LED DMA or IT drivers
Peripheral Utils-Utilities for peripherals, such as finding the maximum frequency of a timer. WIP: many more helper functions to be added
Pin debouncingtemplate paramsBasic debouncing of physical pins (for buttons and other inputs). Requires util lib
PWM analog outputctor paramsSets up, starts, adjusts, and stops PWM output on pins with TIM OC functionality. (F7 only, H7 and MP1 may work but not tested)
QSPI Flash Cell Blocktemplate paramsStore and recall any data struct in a block or sector or FLASH memory,
RGB LED-Supports color blending 24-bit color, floats, RGB565 mode, flashing and fading animations. Supports different LED element types (e.g. Red and Blue are PWM-controlled, but Green is GPIO-controlled)
Rotary Encoderctor paramsSupports half-step and full-step rotaries. Accumulates net position until read.
Timekeeperconfig structSets up a timer to call a given callback at regular intervals (lambda or any callable object).

*See 'Configuring a Driver' section for description of Config types.

Boot/Startup/Low Level:

FileNotes
startup_*.sAssembly for Cortex startup (vector table, Reset handler, libc/cpp and static object initialization, calls SystemInit and main
*.ldLinker script for memory layout. You may want to customize this for advanced applications (allocate more or less memory for DMA buffers, for example).
cache.hhWrappers for target-specific cache maintanance
system_startup.cc/hhC++ startup code for F7 and H7 (M4 and M7 cores): managing multiple cores at startup, loading RCC clock configuration

Usage

Add the following directories as INCLUDE directories for your build system: (e.g. add -Ipath to your compile commands for each of the following paths):

  • mdrivlib
  • mdrivlib/target/stm32XXxx, where XX is the target architecture, e.g. stm32f7xx (look in the target dir to see what the options are)
  • mdrivlib/target/stm32XXyy_zz, (if present): chip or core-specific architecture. e.g. stm32f723 or stm32mp1_ca7 (look in the target dir to see what the options are)
  • CMSIS device header: if you've downloaded STM32Cube, then include the path something like this (for H7xx): STM32Cube_FW_H7_V1.8.0/Drivers/CMSIS/Device/ST/STM32H7xx/Include/
  • If you're using any drivers that require STM32-HAL or STM32-LL: STM32 HAL Include directory, e.g. STM32Cube_FW_H7_V1/Drivers/STM32H7xx_HAL_Driver/Inc

Compile and link any mdrivlib/drivers/*.cc and mdrivlib/target/XXX/drivers/*.cc that you wish to use. Also compile and link any STM32-HAL or STM32-LL drivers used.

Finally, add https://github.com/4ms/cpputil to your project. You only need to include the cpputil/ directory (using -Ipath/to/cpputil/cpputil).

Build with the proper symbols defined for your target: (TODO: specify these clearly in one place) example: -DSTM32H7x5 -DCORE_CM7

Use it:

#include "pin.hh"
#include "timekeeper.hh"
//Values for an STM32H745 Discovery board. Adjust these values to suit your chip/board:
Pin userLED {GPIO::J, 2, PinMode::Output};
Pin pwmPin {GPIO::A, 6, PinMode::Alt, PinAF::AltFunc2};
TimPwmChannel myAnalogOut{{.TIMx = TIM3, .channel = 1, .period = 10000}};
userLED.high();
myAnalogOut.start_output();
uint8_t ramp_up = 0;
Timekeeper lightFlasher({.TIMx=TIM2, .period=100000000/*ns*/}, [&ramp_up](){
myRedLED.toggle(); myAnalogOut.set(ramp_up);
});
lightFlasher.start();
while (...) {
if (some_user_input())
ramp_up++;
}

Common themes throughout drivers:

STM32-HAL vs STM32-LL vs CMSIS

mdrivlib contains a mix of three methods of hardware access:

  • STM32-HAL (stm32xxxxx_XXX_hal.c/.h files): The STM32 HAL library is used only when the peripheral is complex enough that re-writing it would be non-trivial and would offer very little benefit in terms of efficiency, code space, or additional features. Often STM32-HAL is used for initialization only, since this library does not prioritize efficiency in initialization procedures.

  • STM32-LL (stm32xxxxx_XXX_ll.c/.h files): In some drivers, ST's LL drivers are used, which are thin wrappers over direct register control via CMSIS headers. I am mostly migrating the drivers that use LL to use CMSIS since the LL drivers aren't as portable as I originally believed.

  • CMSIS (stm32xxxxx.h files): These are huge (2MB+) header files provided by the vendor (ST) that map all the registers to C-style structs and macros. The benefit of these is that the datasheet (Reference Manual) can be used along-side to setup and debug peripherals. Many drivers in mdrivlib use RegisterBits and RegisterSection which wrap the CMSIS macros. These can be specified as ReadWrite, ReadOnly, or WriteOnly, making this a bit safer to use than raw CMSIS register macros without sacrificing any efficiency. For instance, we can say RCC_Enable::ADC_1::set() instead of RCC->APB2ENR = RCC->APB2ENR | RCC_APB2ENR_ADCEN

Configuring a driver

The tables above mentions different types of configuration methods.

Most drivers are being updated to use the constexpr template struct method, since it offers no drawbacks.

  • constexpr non-type template param: C++20 allows constexpr objects of custom classes for template parameters. e.g:
// mdrivlib configuration struct:
struct AdcConfig {
uint32_t ADCx;
uint32_t BitDepth = 12;
bool oversample = false;
...
};
// mdrivlib peripheral:
template<AdcConfig Conf> AdcPeriph {...};
// your application:
constexpr AdcConfig MyConfig {
.ADCx = 1,
// BitDepth is not initialized here so it will use default value of 12
.oversample = true;
}
AdcPeriph<MyConfig> adc(...);
  • Template params: Each configuration value is passed as a separate parameter. e.g.
AdcChan<AdcPeriphNum::_1, AdcChanNum::_13, uint16_t> myADC;
  • Template struct: wraps multiple params into a struct passed as a typename template argument:
struct MySpiAdcConfig : DefaultSpiConfig { static constexpr uint16_t NumChips = 2;
//...
};
AdcSpi_MAX11666<MySpiAdcConfig> myADC;
  • Config struct: wraps multiple params into a struct passed as a constructor argument:
I2CPeriph MyI2C { .I2Cx = I2C3, .SCL = {GPIO::B, 8, LL_GPIO_AF4},
.SDA = {GPIO::B, 9, LL_GPIO_AF4},
// ...
};
MyI2C.write(deviceAddr, data, dataSize);
  • Ctor/init params: normal function parameters passed to the constructor or an init function. e.g.:
	TIM_TypeDef *const TIMx = TIM2;
TimChannelNum const channelNum = TimChannelNum::2;
uint32_t period = 256;
uint16_t prescaler = 1;
uint32_t clock_division = 1;
TimPwmChannel myAnalogOut{TIMx, channelNum, period, prescaler, clock_division};
myAnalogOut.start_output();
for (int i = 0; i<256; i++) myAnalogOut.set(i);

About

Embedded driver library using modern C++. Currently supports STM32MP1, STM32H7, STM32F7, STM32F4 and STM32F0

Topics

Resources

Stars

29 stars

Watchers

7 watching

Forks

Contributors

Languages