In our early-access pilot, teams got the most consistent value from Embedd when CMake handled driver downloads directly. Teams that downloaded files by hand and committed them to the repository saw the same pattern: within a few weeks, drivers became stale, someone edited them locally, and their link to the source datasheet disappeared. This post shows how to avoid that.
Embedd Driver Output
For each chip, Embedd produces a directory with a consistent layout. An STM32L4 USART output looks roughly like this:
embedd_output/
stm32l4_usart/
include/
embx_stm32l4_usart.h # Register map + HAL API declarations
embx_stm32l4_usart_regs.h # Raw register definitions (struct + macros)
src/
embx_stm32l4_usart.c # HAL implementation
test/
embx_stm32l4_usart_test.c # Register-level unit tests (Unity)
CMakeLists.txt # Adds the driver as a CMake INTERFACE library
embedd_manifest.json # Chip ID, generation timestamp, datasheet hash
The embedd_manifest.json file drives the automation. It stores the chip identifier and the SHA-256 of the source datasheet PDF, allowing CI to spot a driver that no longer matches a newer datasheet.
CMake Setup
The generated CMakeLists.txt presents the driver as an interface library, making project integration simple:
# In your project CMakeLists.txt
include(FetchContent)
FetchContent_Declare(
embedd_stm32l4_usart
URL https://api.embeddx.org/drivers/download/stm32l4-usart-v1.4.2.tar.gz
URL_HASH SHA256=a3f7...
)
FetchContent_MakeAvailable(embedd_stm32l4_usart)
target_link_libraries(your_firmware PRIVATE embedd_stm32l4_usart)
Your Embedd project's driver registry supplies the URL and hash. The CLI exposes them through embedd driver list --chip stm32l4-usart --format cmake, which outputs a paste-ready FetchContent_Declare block containing the locked current version hash.
To vendor the source instead of downloading it during configuration, use add_subdirectory with the downloaded output directory. Either method works. FetchContent suits CI because CMake caches the download between runs, while hash verification makes a corrupted or replaced cached file fail the build clearly.
GitHub Actions
Here is a minimal workflow for building firmware and running driver unit tests:
name: firmware-build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Install ARM toolchain
run: |
sudo apt-get install -y gcc-arm-none-eabi cmake ninja-build
- name: Install Embedd CLI
run: |
curl -fsSL https://embeddx.org/install.sh | sh
echo "$HOME/.embedd/bin" >> $GITHUB_PATH
- name: Authenticate
run: embedd auth --api-key ${{ secrets.EMBEDD_API_KEY }}
- name: Check driver freshness
run: embedd driver check --manifest embedd_output/*/embedd_manifest.json
- name: Configure
run: cmake -B build -G Ninja -DCMAKE_TOOLCHAIN_FILE=arm-none-eabi.cmake
- name: Build
run: cmake --build build
- name: Run unit tests (QEMU)
run: |
cmake --build build --target run_tests
Including embedd driver check is optional, but useful. It asks the Embedd API whether the manifest's datasheet hash has a newer indexed version. Nothing is updated automatically, so choosing a new driver version remains your decision. That is deliberate: in a safety-adjacent system, an automated driver update should receive the same review as any other code change.
CI Test Runs
Embedd creates Unity-based unit tests with the driver source. They test register setup sequences by writing to a mock peripheral model, then reading the register state to check bit patterns. Hardware is not required, but you need QEMU for Cortex-M targets or a native x86 test build.
For CI, the native build is quicker and tests the same logic. Add a second CMake target that omits the ARM toolchain and links to a POSIX mock for the peripheral address space:
# CMakeLists.txt, test-only target
if(BUILD_TESTS)
add_executable(driver_tests
embedd_output/stm32l4_usart/test/embx_stm32l4_usart_test.c
test/mock_peripheral.c
)
target_include_directories(driver_tests PRIVATE
embedd_output/stm32l4_usart/include
test/
vendor/unity/src
)
target_link_libraries(driver_tests PRIVATE unity)
add_test(NAME DriverTests COMMAND driver_tests)
endif()
The mock_peripheral.c stub allocates a byte array at the driver's expected base address and returns it. Driver writes go into that array, and the test reads them back. This exposes bitfield encoding errors, access-order bugs, and initialization problems before a logic analyser is connected.
Updating the Chip Library
When Embedd adds a peripheral or fixes a bitfield definition, the chip library receives a new driver version. Updates should be intentional rather than automatic. Pin versions in FetchContent_Declare with explicit hashes, then change them in a dedicated PR containing the regenerated driver diff and a review of the underlying datasheet extraction.
The manifest's datasheet hash distinguishes the source of a version change: a changed hash indicates a datasheet correction, while an unchanged hash points to a generator improvement. Generator improvements carry less risk. A datasheet correction means the earlier driver encoded at least one register incorrectly, so review which peripheral configuration code relied on it.
The GitLab CI version follows the same pattern, using .gitlab-ci.yml in place of the GitHub Actions workflow file. The Embedd CLI works across CI platforms; only secret injection syntax changes.
Limits of This Approach
This walkthrough covers driver integration hygiene, not driver test coverage beyond register-level unit tests. Complete peripheral validation still needs hardware in the loop: QEMU cannot reproduce the actual timing of an SPI peripheral. Generated stubs confirm that configuration writes the expected values to the expected registers. They cannot show that a real SPI transaction succeeds at 10 MHz with the target slave device. Those checks belong in a separate hardware-in-the-loop stage using physical boards.
This also does not replace the datasheet. Embedd generates the register map and HAL, but not the application logic that chooses between UART RS-485 mode and full-duplex operation. The driver translates the silicon documentation precisely. Decisions built on top of it remain yours.
A Pipeline in Practice
One pilot hardware team was bringing up a board with an NXP i.MX RT1064, an STM32L476 managing power, and a Nordic nRF52832 handling BLE. That meant three MCUs, three datasheets, and three driver sets. Before Embedd entered their CI, updates happened ad hoc, with regeneration only after a bug appeared. After they added three FetchContent declarations and the manifest freshness check, CI detected a datasheet correction in the nRF52832 SAADC peripheral register map that might otherwise have surfaced on hardware. The correction moved a two-bit field offset by one position, which would have caused consistently incorrect ADC readings.
CI found it during a routine dependency-update run, not a search for that issue. That is the value of automation: the checks run even when nobody remembers to start them.