The nRF Connect SDK contains the Zephyr SPI API to interface with the SPI peripheral, and we will use it in the exercises in this lesson.
The choice of the API to use with external peripherals depends on the needs and requirements of the application and the capabilities of the peripheral device. Similarly, it is recommended to use a sensor API to communicate with the sensor and some graphics libraries (like LVGL and others) to communicate with a TFT screen. Nonetheless, the emphasis of this lesson is on learning and utilizing raw SPI transactions, which those APIs ultimately connect to for the SPI communication with the peripheral. Therefore, for our exercises, we will rely on the Zephyr SPI API, which will provide a solid understanding of using SPI in Zephyr on Nordic chips.
Enable the SPI driver by adding the following Kconfig to the prj.conf
file:
CONFIG_SPI=y
KconfigIn the source code, we include the header file of the SPI API.
#include <zephyr/drivers/spi.h>
CFirst, we must add the SPI slave device on the SPI node in the devicetree using an overlay file. Overlay files define which SPI controller we are using, the device bindings, status, and the configurations to be used for MOSI, MISO and SCLK pins. As per bindings, we can specify other properties for the slave, like max-clock speed. We also specify which driver to use for this device in the compatible property. A basic overlay is shown below that uses the spi1
controller, nordic-spi
bindings, is active (status okay), defines a CS pin on gpio0
with the active-low flag and uses the default pin configuration of spi1
for MISO, MOSI and SCKL. The pin configuration for the active and sleep mode (spi1_default
and spi1_sleep
respectively) can be configured using pinctrl
and is not shown here for conciseness. You will find a complete overlay with pinctrl defined in the hands-on exercises.
A general SPI device, gendev
, is added as a subnode of the spi1
controller in the code snippet below.
&spi1 {
compatible = "nordic,nrf-spi";
status = "okay";
cs-gpios = <&gpio0 18 GPIO_ACTIVE_LOW>;
pinctrl-0 = <&spi1_default>;
pinctrl-1 = <&spi1_sleep>;
pinctrl-names = "default", "sleep";
gendev: gendev@0 {
compatible = "vnd,spi-device";
reg = <0>;
spi-max-frequency = <1600000>;
label = "GenDev";
};
};
DevicetreeThe SPI API has an API-specific struct spi_dt_spec
, with the following signature
This structure contains the device pointer for the SPI device, const struct device *bus
, and the slave specific configuration spi_config config
.
struct spi_config
has the following signature
frequency:
The clock-frequency for SPI communication.operation:
Operation flags, refer to the API documentation for different flags defined and their bit positions.slave:
The number of the slave device on the bus.cs:
The GPIO chip-select line.To retrieve this structure, we will use the API-specific function SPI_DT_SPEC_GET()
, which has the following signature
In the following code snippet, we retrieve the device structure for the gendev
SPI slave that we added in the overlay file, with the SPI operation SPI_WORD_SET(8)
and SPI_TRANSFER_MSB
, which here is to say that the spi-word-size
is 8-bit
and the MSB
should be transferred first (that is how the data over SPI lines should be interpreted).
#define SPIOP SPI_WORD_SET(8) | SPI_TRANSFER_MSB
struct spi_dt_spec spispec = SPI_DT_SPEC_GET(DT_NODELABEL(gendev), SPIOP, 0);
CLastly, we will check if the SPI device is ready using the API-specific function spi_is_ready_dt()
, which has the following signature
err = spi_is_ready_dt(&spispec);
if (!err) {
LOG_ERR("Error: SPI device is not ready, err: %d", err);
return 0;
}
CTo read and write data to and from an SPI bus, we have the functions spi_read_dt()
, spi_write_dt()
, and spi_transceive_dt()
. They are very similar, except that spi_read_dt
only performs the read operation, spi_write_dt
only performs the write operation, and spi_transceive_dt
performs both read and write operations.
These functions have the following signatures
Notice that all functions take in the SPI-specific device structure spi_dt_spec
, and one or two buffer pointers, for the transmission and the reception.
Below is an example of using the spi_transceive_dt
function
uint8_t tx_buffer = 0x88;
struct spi_buf tx_spi_buf = {.buf = (void *)&tx_buffer, .len = 1};
struct spi_buf_set tx_spi_buf_set = {.buffers = &tx_spi_buf, .count = 1};
struct spi_buf rx_spi_bufs = {.buf = data, .len = size};
struct spi_buf_set rx_spi_buf_set = {.buffers = &rx_spi_bufs, .count = 1};
err = spi_transceive_dt(&spispec, &tx_spi_buf_set, &rx_spi_buf_set);
if (err < 0) {
LOG_ERR("spi_transceive_dt() failed, err: %d", err);
return err;
}
CThe same procedure can be followed if only reading or writing is required using spi_read_dt
or spi_write_dt
.
The nRF Connect SDK contains the Zephyr SPI API to interface with the SPI peripheral, and we will use it in the exercises in this lesson.
The choice of the API to use with external peripherals depends on the needs and requirements of the application and the capabilities of the peripheral device. Similarly, it is recommended to use a sensor API to communicate with the sensor and some graphics libraries (like LVGL and others) to communicate with a TFT screen. Nonetheless, the emphasis of this lesson is on learning and utilizing raw SPI transactions, which those APIs ultimately connect to for the SPI communication with the peripheral. Therefore, for our exercises, we will rely on the Zephyr SPI API, which will provide a solid understanding of using SPI in Zephyr on Nordic chips.
Enable the SPI driver by adding the following Kconfig to the prj.conf
file:
CONFIG_SPI=y
KconfigIn the source code, we include the header file of the SPI API.
#include <zephyr/drivers/spi.h>
CFirst, we must add the SPI slave device on the SPI node in the devicetree using an overlay file. Overlay files define which SPI controller we are using, the device bindings, status, and the configurations to be used for MOSI, MISO and SCLK pins. As per bindings, we can specify other properties for the slave, like max-clock speed. We also specify which driver to use for this device in the compatible property. A basic overlay is shown below that uses the spi1
controller, nordic-spi
bindings, is active (status okay), defines a CS pin on gpio0
with the active-low flag and uses the default pin configuration of spi1
for MISO, MOSI and SCKL. The pin configuration for the active and sleep mode (spi1_default
and spi1_sleep
respectively) can be configured using pinctrl
and is not shown here for conciseness. You will find a complete overlay with pinctrl defined in the hands-on exercises.
A general SPI device, gendev
, is added as a subnode of the spi1
controller in the code snippet below.
&spi1 {
compatible = "nordic,nrf-spi";
status = "okay";
cs-gpios = <&gpio0 18 GPIO_ACTIVE_LOW>;
pinctrl-0 = <&spi1_default>;
pinctrl-1 = <&spi1_sleep>;
pinctrl-names = "default", "sleep";
gendev: gendev@0 {
compatible = "vnd,spi-device";
reg = <0>;
spi-max-frequency = <1600000>;
label = "GenDev";
};
};
DevicetreeThe SPI API has an API-specific struct spi_dt_spec
, with the following signature
This structure contains the device pointer for the SPI device, const struct device *bus
, and the slave specific configuration spi_config config
.
struct spi_config
has the following signature
frequency:
The clock-frequency for SPI communication.operation:
Operation flags, refer to the API documentation for different flags defined and their bit positions.slave:
The number of the slave device on the bus.cs:
The GPIO chip-select line.To retrieve this structure, we will use the API-specific function SPI_DT_SPEC_GET()
, which has the following signature
In the following code snippet, we retrieve the device structure for the gendev
SPI slave that we added in the overlay file, with the SPI operation SPI_WORD_SET(8)
and SPI_TRANSFER_MSB
, which here is to say that the spi-word-size
is 8-bit
and the MSB
should be transferred first (that is how the data over SPI lines should be interpreted).
#define SPIOP SPI_WORD_SET(8) | SPI_TRANSFER_MSB
struct spi_dt_spec spispec = SPI_DT_SPEC_GET(DT_NODELABEL(gendev), SPIOP, 0);
CLastly, we will check if the SPI device is ready using the API-specific function spi_is_ready_dt()
, which has the following signature
err = spi_is_ready_dt(&spispec);
if (!err) {
LOG_ERR("Error: SPI device is not ready, err: %d", err);
return 0;
}
CTo read and write data to and from an SPI bus, we have the functions spi_read_dt()
, spi_write_dt()
, and spi_transceive_dt()
. They are very similar, except that spi_read_dt
only performs the read operation, spi_write_dt
only performs the write operation, and spi_transceive_dt
performs both read and write operations.
These functions have the following signatures
Notice that all functions take in the SPI-specific device structure spi_dt_spec
, and one or two buffer pointers, for the transmission and the reception.
Below is an example of using the spi_transceive_dt
function
uint8_t tx_buffer = 0x88;
struct spi_buf tx_spi_buf = {.buf = (void *)&tx_buffer, .len = 1};
struct spi_buf_set tx_spi_buf_set = {.buffers = &tx_spi_buf, .count = 1};
struct spi_buf rx_spi_bufs = {.buf = data, .len = size};
struct spi_buf_set rx_spi_buf_set = {.buffers = &rx_spi_bufs, .count = 1};
err = spi_transceive_dt(&spispec, &tx_spi_buf_set, &rx_spi_buf_set);
if (err < 0) {
LOG_ERR("spi_transceive_dt() failed, err: %d", err);
return err;
}
CThe same procedure can be followed if only reading or writing is required using spi_read_dt
or spi_write_dt
.