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.
- Ensure you've read the Zephyr Getting Started guide and can successfully build and flash the Blinky project.
- Create an empty directory for this project.
- 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 themain.cpp
file we'll be creating in the next step. - Define the target (
app
) which CMake will instructmake
to build.
- Create a
main.cpp
file alongsideCMakeLists.txt
with an emptymain
function:
void main() { }
- Run
west build
. You shouldn't get any errors.
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.
- 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 thespi1
peripheral we saw in the.dtsi
file earlier. It's defined globally here, but you can just as easily call the macro withinmain
. - We get an instance of the
device
struct forspi1
. - We declare a
spi_config
struct and configurespi1
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!
- 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.