Cortex-M0 through M7 spans about 15 years of silicon and a 10x capability range, yet peripheral register access is nearly the same. The exceptions are where bring-up bugs tend to gather. This post maps those differences and shows how Embedd's generated drivers address them.
The core sets the memory model and interrupt behavior, while the silicon vendor defines the peripheral register layout. Embedd reads that layout from the chip's reference manual, not the ARM Architecture Reference Manual. The resulting code still has to suit the physical core, so each driver template accounts for its applicable limitations.
What M0 through M7 share
Peripheral MMIO works the same way throughout: a 32-bit address space, registers at fixed offsets from a base address, and access through volatile pointer dereferences. Each generated driver follows the same form for a register write:
*(volatile uint32_t *)(base + UART_CR1_OFFSET) = value;
The NVIC structure is also shared. Every core uses NVIC register addresses from 0xE000E100 through 0xE000EF00, along with the same enable, disable, pending, and IPRI layouts. Vendors vary the interrupt-line count and priority-bit count, but the operating mechanism remains unchanged.
Bitfield changes use the same read-modify-write pattern:
uint32_t reg = *(volatile uint32_t *)(base + OFFSET);
reg &= ~(MASK << SHIFT);
reg |= (value & MASK) << SHIFT;
*(volatile uint32_t *)(base + OFFSET) = reg;
This three-part RMW sequence underpins every Embedd-generated configuration function. It is simple rather than fast, but it works across the cores, can be reviewed, and corresponds directly to the datasheet's field descriptions.
How M0 and M0+ differ from M3+
Cortex-M0 and M0+ implement only part of Thumb and do not allow unaligned memory access. On an M0, a 32-bit read from a non-word-aligned address faults, even though M3 and M4 permit it. Peripheral tables rarely create this issue because their registers are word-aligned. Data buffers can: if a receive buffer begins at an odd address, writing 32 bits at a time can trigger an unaligned-access fault.
For M0 targets, Embedd templates assemble bytes individually when the data register is under 32 bits wide or the API permits unaligned buffers. M3 and M4 templates use 32-bit writes, relying on those cores' unaligned-access support.
M0 also provides a simpler NVIC, with at most 32 external interrupts and 2 priority bits, giving four priority levels. Generated M0 NVIC code limits the priority value to 0x00 through 0xC0 before writing NVIC_IPR, stopping a higher value from being silently reduced to the 2-bit field.
M4 and M7: FPU and cache details
M7 adds separate instruction and data caches. For peripheral drivers, that matters to DMA coherency: when DMA writes cached SRAM, the CPU can see older cache data instead of the updated SRAM. M7 DMA-mode drivers therefore call SCB_CleanDCache_by_Addr before transmit DMA and SCB_InvalidateDCache_by_Addr after receive DMA completion.
These calls matter only when the DMA buffer is in cacheable memory. If the linker script places buffers in non-cacheable SRAM, the generated cache operations become no-ops under a compile-time define. They are included by default, and you can opt out when the memory map makes them unnecessary.
The M4 FPU does not change peripheral driver patterns, though lazy stacking affects ISR entry overhead. In a tight interrupt handler, its first FPU instruction triggers a lazy stack-frame push. This behavior belongs to ARM, not Embedd, but it matters when ISR timing is profiled.
UART patterns across Cortex-M cores
STM32 UARTs retain a broadly similar logical layout from F1 through H7, but register names and some field positions changed between the v1 peripheral, USART_SR/USART_DR in F1/F4, and v2, USART_ISR/USART_RDR/USART_TDR in F7/H7. The parser detects the version from the register-table headers and chooses the matching accessors.
USART_BRR uses different baud formulas for OVER8 and OVER16 oversampling. The generated init function selects the formula for the configured mode and calculates mantissa and fraction fields at compile time from the APB clock and target baud rate. Runtime floating point is not needed.
SPI on M-series cores
Across STM32 generations, SPI layouts change less than UART layouts. SPI_CR1, SPI_CR2, SPI_SR, and SPI_DR remain present with small additions from F1 through H7. The main generation issue is the NSS pulse behavior added with the STM32H7 SPI v2 peripheral, including the NSSP bit in SPI_CFG2. Where the bit exists, generated init sets it from your selection. Where it does not, the configuration struct omits the field, avoiding reserved-bit writes.
I2C: version 1 errata and version 2 differences
The STM32F1 and F4 I2C peripheral, usually called I2C v1, has a documented BUSY-flag erratum: it can lock if SCL or SDA is low during initialization. Recovery bit-bangs SCL nine times to free the bus, then initializes the peripheral again. Generated F1 and F4 init code includes this by default, using function pointers you supply for GPIO toggling. F7, H7, and L4 use I2C v2, also called I2C3 in some documentation, so their generated code omits the workaround.
I2C v2 timing takes more calculation than v1. I2CTIMINGR needs PRESC, SCLDEL, SDADEL, SCLH, and SCLL values based on the input clock and target SCL frequency. The generated init function supplies computed constants for the specified pair, following the formulas used by the STM32 I2C timing configuration tool.
ADC from M0 through M7
ADC variation is greatest among the four peripheral types here. STM32 ADC v1, used on F1/F4, sequences continuous conversions with a length in SQR1 and channel selections across SQR1/SQR2/SQR3. STM32 ADC v2, on F7/H7, substantially changes that sequencer and adds calibration that v1 lacks. NXP's LPC5500 ADC12B uses a separate trigger-and-FIFO model.
ADC templates are the most chip-specific because the feature differences are too wide for a useful uniform abstraction. An uploaded chip's documentation determines the generated driver's features. A 16-bit-capable chip gets a resolution field in its config struct; one without that capability does not. The code therefore stays close to the silicon instead of presenting a fictional shared API.