Give Embedd a real peripheral datasheet and an SPI init function can appear almost immediately. This guide follows that generation step, shows the resulting files, and explains how to add them to a CMake project.
The examples use an STM32F4 for SPI master operation and an NXP LPC55S6x for I2C target operation. Concrete register names make the shared principles easier to track across supported chips.
From uploaded PDF to register model
Upload the chip reference manual or peripheral guide as a PDF. For SPI, that PDF contains the peripheral chapter, often 30 to 80 pages of register tables, timing diagrams, mode details, and errata. The STM32F4 example uses the RM0090 manual, whose SPI/I2S chapter begins near page 850 in its most recent revision.
Embedd parses the PDF tables, finds the peripheral block through headings and register prefixes (SPI_CR1, SPI_CR2, SPI_SR, SPI_DR, SPI_CRCPR, SPI_RXCRCR, SPI_TXCRCR, SPI_I2SCFGR, SPI_I2SPR for STM32F4), and creates an internal register model. The model records the base address, register offsets, field names, bit positions, widths, access modes (RW/RO/WO/W1C), and reset values.
For the LPC55S6x I2C target, parsing covers the I2C chapter of the UM11126 user manual. Its peripheral comes from NXP FlexComm, so its register names differ from STM32. The extraction process remains the same: read the tables, capture fields, and construct the model.
SPI master output layout
By default, the STM32F4 SPI master generator produces four files:
stm32f4_spi.h: register offsets, bit masks, and access declarationsstm32f4_spi.c: init, TX, RX, transfer functionsstm32f4_spi_types.h: config struct definitionstm32f4_spi_test.c: optional register read-back test stubs
Each header register uses an offset macro instead of an absolute address. That lets one init function serve SPI1 through SPI6 on the STM32F4 when given the relevant base address. A typical call looks like this:
embx_spi_config_t cfg = {
.base_addr = SPI1_BASE,
.cpol = EMBX_SPI_CPOL_LOW,
.cpha = EMBX_SPI_CPHA_1EDGE,
.baud_div = EMBX_SPI_BAUD_DIV_8,
.data_size = EMBX_SPI_DS_8BIT,
.nss_mode = EMBX_SPI_NSS_SW,
.direction = EMBX_SPI_DIR_FULL_DUPLEX,
};
embx_spi_init(&cfg);
The baud divisor enum corresponds to the BR[2:0] field in SPI_CR1. With APB2 at 84 MHz on an STM32F4, EMBX_SPI_BAUD_DIV_8 produces 10.5 MHz, a sensible initial value before checking your device's maximum clock.
By default, the transfer routine supports full duplex: write SPI_DR, wait for TXE, wait for RXNE, then read SPI_DR. After the final byte, it also checks BSY as recommended by the datasheet before chip select is released. DMA is selected before download, and the DMA version connects the documented request lines from the reference manual's DMA channel assignment table.
I2C target output layout
The LPC55S6x I2C target, or secondary/slave, driver has a similar structure but different settings. Target mode adds address matching, clock-stretching behavior, and ACK/NACK control, none of which applies to SPI master mode.
The generated lpc55_i2c_target.h provides a 7-bit or 10-bit address field in its config struct. The LPC55 I2C peripheral has four match registers, I2C_SLVADR0 through I2C_SLVADR3. Init code configures the registers you select and disables the others, avoiding a manual read of the SADISABLE and SLVENSSEL interaction.
The data handler uses the pattern described in NXP application notes: one callback for receive events, another for transmit requests, and the pending byte count as an argument. You provide the callbacks; Embedd creates the ISR stub and routes events according to the SLVSTATE field in I2C_STAT.
Generated output enables clock stretching by default. On the LPC55, releasing the clock after address or data events requires clearing SLVCONTINUE explicitly. If you turn stretching off during generation, the code still comments the relevant register field and explains the result of missing the release window.
Generation-time configuration choices
Before download, both generators offer a configuration panel. SPI master settings include transfer mode (polling, interrupt, or DMA), chip select handling (software NSS or hardware NSS), and CRC generation. I2C target settings include 7-bit or 10-bit addressing, the number of address-match slots, clock stretching, and RTOS-aware locking, which adds mutex acquire/release calls around the state machine for FreeRTOS or Zephyr integration.
The RTOS option has a limited role. It places a FreeRTOS mutex acquire before the peripheral state machine and releases it on exit. It does not make a polling transfer loop an RTOS-aware blocking call, which requires interrupt or DMA mode. Interrupt mode with RTOS locking lets the transfer yield while it waits. Polling with the same locking only stops two tasks entering simultaneously, without removing the busy-wait.
CMake setup
The archive contains a CMakeLists.txt defining a static library target. Include it in your project with three lines:
add_subdirectory(drivers/stm32f4_spi)
target_link_libraries(my_firmware PRIVATE stm32f4_spi)
target_include_directories(my_firmware PRIVATE
drivers/stm32f4_spi/include
)
The generated CMakeLists.txt relies on neither a specific HAL nor a CMSIS version. Source files access peripheral registers through the header's offset macros and do not include stm32f4xx.h or vendor HAL headers. Your project supplies the base address constant, such as SPI1_BASE in the example above.
That independence is deliberate. A driver tied to vendor HAL headers also takes on that HAL's initialization order, clock-enable conventions, and interrupt names. Without that dependency, the generated code can be placed in a bare-metal, HAL-based, or RTOS project without changes.
Reviewing output before use
Before application code calls the generated driver, inspect its init function and confirm where clock enabling occurs. Embedd writes the peripheral registers, but it does not enable the peripheral clock. For STM32F4 SPI1/4/5/6, that means RCC_APB2ENR; for LPC55 FlexComm instances, it means SYSCON_AHBCLKCTRL. Clock setup belongs in system initialization because the required call depends on your RCC configuration and board-specific clock tree, not only the reference manual. The generated init marks where to place that enable before calling it.
The stm32f4_spi_test.c stub checks register read-back and confirms that CR1 matches the init writes. Running these checks on a QEMU model before hardware bring-up can expose offset and mask errors early. The checks are plain C functions that return pass/fail status, so any unit test runner can call them.
For most SPI and I2C peripheral combinations, generated code compiles and passes register checks on the first try. A frequent exception is a mismatch between the uploaded datasheet revision and the target chip. The STM32F4 manual has gone through several revisions, and revision 19 altered the description of BSY behavior in SPI full-duplex mode. Using the matching revision resolves that difference.