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.txtfile 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
BOARDvariable 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_MAINvariable to themain.cppfile we'll be creating in the next step. - Define the target (
app) which CMake will instructmaketo build.
- Create a
main.cppfile alongsideCMakeLists.txtwith an emptymainfunction:
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.cppand 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_NODELABELmacro to get the identifier of thespi1peripheral we saw in the.dtsifile earlier. It's defined globally here, but you can just as easily call the macro withinmain. - We get an instance of the
devicestruct forspi1. - We declare a
spi_configstruct and configurespi1to transcieve at 1.625Mhz, most significant bit first, using 8-bit words, and in master mode. - We instantiate an array of
spi_bufobjects 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_setstruct. - 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
devicebinding 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.