Skip to content

Instantly share code, notes, and snippets.

@disposedtrolley
Last active March 1, 2024 16:34
Show Gist options
  • Save disposedtrolley/f0edbef0e65dbd7b56207e4ffc35c8d1 to your computer and use it in GitHub Desktop.
Save disposedtrolley/f0edbef0e65dbd7b56207e4ffc35c8d1 to your computer and use it in GitHub Desktop.
Basic SPI on STM32 with Zephyr

This is a basic guide on setting up SPI for the STM32F401RE Nucleo board on Zephyr. We'll be bootstrapping a new Zephyr project with CMake, getting a binding to a SPI peripheral on the STM32, and writing a Hello, Zephyr string over the wire.

While I'll be using an STM32F401RE, the process should be similar for most STM32 microcontrollers with a SPI peripheral. Also worth noting that this won't be an exhaustive tutorial, nor does it contain best practice! I struggled with this for a whole afternoon, so I'm posting it just in the hopes of getting people on the right track.

Bootstrapping the project

  1. Ensure you've read the Zephyr Getting Started guide and can successfully build and flash the Blinky project.
  2. Create an empty directory for this project.
  3. Create a CMakeLists.txt file in the root with the following content:
cmake_minimum_required(VERSION 3.13.1)

set(BOARD nucleo_f401re)

find_package(Zephyr)
project(spi_stm32_zephyr C CXX)

set(SRC_MAIN src/main.cpp)

target_sources(app PRIVATE ${SRC_MAIN})

In this file, we:

  • Set the BOARD variable to the identifier used by Zephyr. You can find a full list of supported boards here. The identifier can be found somewhere within the specific page for the board.
  • Instruct CMake to find the Zephyr package. This is all that's needed to link the Zephyr headers.
  • Declare the project name (spi_stm32_zephyr) and the languages in use. Zephyr is mostly in C, but we'll be using C++ for our code.
  • Set the SRC_MAIN variable to the main.cpp file we'll be creating in the next step.
  • Define the target (app) which CMake will instruct make to build.
  1. Create a main.cpp file alongside CMakeLists.txt with an empty main function:
void main() { }
  1. Run west build. You shouldn't get any errors.

Initialising SPI and writing to it

Before we can initialise the SPI peripheral, we need to do a bit of digging in the Zephyr codebase to find its label. Zephyr uses devicetrees to declare hardware features, which are hierarchical data structures that describe all of the peripherals a particular chip exposes. You can find the .dtsi files for the STM32F4 series here.

stm32f4.dtsi forms the base definition for all STM32F4 series devices, including peripherals common to all chips. The stm32f401Xe.dtsi file contains definitions specific to the STM32F401xE series. In this example, memory regions are the only thing specific to this chip series.

Searching for spi in the stm32f4.dtsi file, we can see a definition for the spi1 peripheral. The identifier used before the colon is the value we need to use in our code when referencing it, so spi1 in this instance.

  1. Go back to main.cpp and modify it to include the following content:
#include <device.h>
#include <drivers/spi.h>

#define SPI1_NODE           DT_NODELABEL(spi1)


void main() {
    struct device *spi1_dev = device_get_binding(SPI1_NODE);

    struct spi_config spi_cfg {
        .frequency = 1625000U,
        .operation = SPI_WORD_SET(8) | SPI_TRANSFER_MSB | SPI_OP_MODE_MASTER,
    };

    struct spi_buf bufs[] = {
            {
                    .buf = (uint8_t *)"Hello, Zephyr",
                    .len = 13
            },
    };

    struct spi_buf_set tx = {
            .buffers =  bufs,
            .count = 1
    };

    return spi_write(spi1_dev, spi_cfg, &tx);

    while (true) {}
}

Let's try and unpack what's happening here:

  • We use the DT_NODELABEL macro to get the identifier of the spi1 peripheral we saw in the .dtsi file earlier. It's defined globally here, but you can just as easily call the macro within main.
  • We get an instance of the device struct for spi1.
  • We declare a spi_config struct and configure spi1 to transcieve at 1.625Mhz, most significant bit first, using 8-bit words, and in master mode.
  • We instantiate an array of spi_buf objects with the data we want to send. Here, we're only interesting in sending a single message -- Hello, Zephyr.
  • We wrap the buffer array in a spi_buf_set struct.
  • We send the message over SPI using spi_write.

So there you have it, a probably too bare-bones tutorial on getting SPI to work on Zephyr with an STM32F4!

Notes

  • There's no need to configure alternate modes on the GPIO pins used for the SPI peripheral. If you're sticking with the defaults for whatever micro you're building on, all you need to do is find the label of the peripheral and getting a device binding for it. In fact, attempting to configure GPIO pins used by the SPI peripheral will cause it not to work.
  • You're supposedly allowed to configure a GPIO pin manually to control the CS line independently of the SPI hardware. You can read up on this via the very limited documentation.
  • Have fun! There's nothing quite like a whole afternoon of debugging and being rewarded by seeing bits flow through the wire on your scope.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment