Raspberry Pi Pico (RP2040) SPI Example with MicroPython and C/C++
2021-07-12 | By ShawnHymel
License: Attribution
Serial Peripheral Interface (SPI) is a simple communication protocol used to talk to various sensors, driver boards, microcontrollers, etc. It is a synchronous protocol, as it uses a separate clock line to tell the receiver when to sample data. Here is an article that offers a great explanation of SPI.
This tutorial walks you through the process of connecting an accelerometer to the Raspberry Pi Pico using SPI to reading data from it with MicroPython as well as C.
You can also view this tutorial in video form:
Required Hardware
You will need the following hardware:
Hardware Hookup
Connect the sensor to the Pico as follows:
Here is how I connected the sensor to the Pico:
Bootloader Mode
Whenever this guide tells you to put your Pico into “bootloader mode,” you will need to unplug the USB cable. Press and hold the BOOTSEL button, and plug the USB cable back in. This will force the Pico to enumerate as a mass storage device on your computer, and you should see a drive appear on your computer with the name “RPI-RP2.”
MicroPython Example
Open Thonny. If you do not already have the MicroPython firmware running on the Pico, click on the bottom-right button and select the Raspberry Pi Pico as your board. Click again and select Configure Interpreter. In the pop-up window, select Install or update firmware.
Click Install to install the latest MicroPython firmware. Close the pop-up windows when installation is done.
In a new new document, enter the following code:
import machine
import utime
import ustruct
import sys
###############################################################################
# Constants
# Registers
REG_DEVID = 0x00
REG_POWER_CTL = 0x2D
REG_DATAX0 = 0x32
# Other constants
DEVID = 0xE5
SENSITIVITY_2G = 1.0 / 256 # (g/LSB)
EARTH_GRAVITY = 9.80665 # Earth's gravity in [m/s^2]
###############################################################################
# Settings
# Assign chip select (CS) pin (and start it high)
cs = machine.Pin(17, machine.Pin.OUT)
# Initialize SPI
spi = machine.SPI(0,
baudrate=1000000,
polarity=1,
phase=1,
bits=8,
firstbit=machine.SPI.MSB,
sck=machine.Pin(18),
mosi=machine.Pin(19),
miso=machine.Pin(16))
###############################################################################
# Functions
def reg_write(spi, cs, reg, data):
"""
Write 1 byte to the specified register.
"""
# Construct message (set ~W bit low, MB bit low)
msg = bytearray()
msg.append(0x00 | reg)
msg.append(data)
# Send out SPI message
cs.value(0)
spi.write(msg)
cs.value(1)
def reg_read(spi, cs, reg, nbytes=1):
"""
Read byte(s) from specified register. If nbytes > 1, read from consecutive
registers.
"""
# Determine if multiple byte (MB) bit should be set
if nbytes < 1:
return bytearray()
elif nbytes == 1:
mb = 0
else:
mb = 1
# Construct message (set ~W bit high)
msg = bytearray()
msg.append(0x80 | (mb << 6) | reg)
# Send out SPI message and read
cs.value(0)
spi.write(msg)
data = spi.read(nbytes)
cs.value(1)
return data
###############################################################################
# Main
# Start CS pin high
cs.value(1)
# Workaround: perform throw-away read to make SCK idle high
reg_read(spi, cs, REG_DEVID)
# Read device ID to make sure that we can communicate with the ADXL343
data = reg_read(spi, cs, REG_DEVID)
if (data != bytearray((DEVID,))):
print("ERROR: Could not communicate with ADXL343")
sys.exit()
# Read Power Control register
data = reg_read(spi, cs, REG_POWER_CTL)
print(data)
# Tell ADXL343 to start taking measurements by setting Measure bit to high
data = int.from_bytes(data, "big") | (1 << 3)
reg_write(spi, cs, REG_POWER_CTL, data)
# Test: read Power Control register back to make sure Measure bit was set
data = reg_read(spi, cs, REG_POWER_CTL)
print(data)
# Wait before taking measurements
utime.sleep(2.0)
# Run forever
while True:
# Read X, Y, and Z values from registers (16 bits each)
data = reg_read(spi, cs, REG_DATAX0, 6)
# Convert 2 bytes (little-endian) into 16-bit integer (signed)
acc_x = ustruct.unpack_from("<h", data, 0)[0]
acc_y = ustruct.unpack_from("<h", data, 2)[0]
acc_z = ustruct.unpack_from("<h", data, 4)[0]
# Convert measurements to [m/s^2]
acc_x = acc_x * SENSITIVITY_2G * EARTH_GRAVITY
acc_y = acc_y * SENSITIVITY_2G * EARTH_GRAVITY
acc_z = acc_z * SENSITIVITY_2G * EARTH_GRAVITY
# Print results
print("X:", "{:.2f}".format(acc_x), \
"| Y:", "{:.2f}".format(acc_y), \
"| Z:", "{:.2f}".format(acc_z))
utime.sleep(0.1)
If you wish, save the program as a file on your computer for safekeeping (e.g. adxl343_spi.py).
Click the Run button. You should see the values of the POWER_CTL register printed out. 2 seconds later, the X, Y, and Z acceleration readings should start streaming across the console. Try moving the breadboard/accelerometer around to see how the values are affected.
C/C++ Example
If you have not done so already, follow this guide to set up the C/C++ SDK for Pico on your computer and create a Blink program. We will use that Blink program as a template for this project.
Open a file explorer. Create a copy of the blink directory you created in the C/C++ setup guide. Rename it to match your project (e.g. adxl343_spi). Delete the build directory inside the newly created project folder.
Open VS Code. Click File > Open Folder. Select your newly created project folder. Open CMakeLists.txt. Change the project name (e.g. blink to adxl343_spi) and add the hardware_spi library in the target_link_libraries() function. You may also want to set the USB or UART serial output, depending on if you are using a picoprobe for debugging (e.g. enable UART serial output for picoprobe, otherwise, use USB serial output).
# Set minimum required version of CMake
cmake_minimum_required(VERSION 3.12)
# Include build functions from Pico SDK
include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)
# Set name of project (as PROJECT_NAME) and C/C++ standards
project(adxl343_spi C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
# Creates a pico-sdk subdirectory in our project for the libraries
pico_sdk_init()
# Tell CMake where to find the executable source file
add_executable(${PROJECT_NAME}
main.c
)
# Create map/bin/hex/uf2 files
pico_add_extra_outputs(${PROJECT_NAME})
# Link to pico_stdlib (gpio, time, etc. functions)
target_link_libraries(${PROJECT_NAME}
pico_stdlib
hardware_spi
)
# Enable usb output, disable uart output
pico_enable_stdio_usb(${PROJECT_NAME} 1)
pico_enable_stdio_uart(${PROJECT_NAME} 0)
In main.c replace the code with the following:
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/spi.h"
// Registers
static const uint8_t REG_DEVID = 0x00;
static const uint8_t REG_POWER_CTL = 0x2D;
static const uint8_t REG_DATAX0 = 0x32;
// Other constants
static const uint8_t DEVID = 0xE5;
static const float SENSITIVITY_2G = 1.0 / 256; // (g/LSB)
static const float EARTH_GRAVITY = 9.80665; // Earth's gravity in [m/s^2]
/*******************************************************************************
* Function Declarations
*/
void reg_write( spi_inst_t *spi,
const uint cs,
const uint8_t reg,
const uint8_t data);
int reg_read( spi_inst_t *spi,
const uint cs,
const uint8_t reg,
uint8_t *buf,
uint8_t nbytes);
/*******************************************************************************
* Function Definitions
*/
// Write 1 byte to the specified register
void reg_write( spi_inst_t *spi,
const uint cs,
const uint8_t reg,
const uint8_t data) {
uint8_t msg[2];
// Construct message (set ~W bit low, MB bit low)
msg[0] = 0x00 | reg;
msg[1] = data;
// Write to register
gpio_put(cs, 0);
spi_write_blocking(spi, msg, 2);
gpio_put(cs, 1);
}
// Read byte(s) from specified register. If nbytes > 1, read from consecutive
// registers.
int reg_read( spi_inst_t *spi,
const uint cs,
const uint8_t reg,
uint8_t *buf,
const uint8_t nbytes) {
int num_bytes_read = 0;
uint8_t mb = 0;
// Determine if multiple byte (MB) bit should be set
if (nbytes < 1) {
return -1;
} else if (nbytes == 1) {
mb = 0;
} else {
mb = 1;
}
// Construct message (set ~W bit high)
uint8_t msg = 0x80 | (mb << 6) | reg;
// Read from register
gpio_put(cs, 0);
spi_write_blocking(spi, &msg, 1);
num_bytes_read = spi_read_blocking(spi, 0, buf, nbytes);
gpio_put(cs, 1);
return num_bytes_read;
}
/*******************************************************************************
* Main
*/
int main() {
int16_t acc_x;
int16_t acc_y;
int16_t acc_z;
float acc_x_f;
float acc_y_f;
float acc_z_f;
// Pins
const uint cs_pin = 17;
const uint sck_pin = 18;
const uint mosi_pin = 19;
const uint miso_pin = 16;
// Buffer to store raw reads
uint8_t data[6];
// Ports
spi_inst_t *spi = spi0;
// Initialize chosen serial port
stdio_init_all();
// Initialize CS pin high
gpio_init(cs_pin);
gpio_set_dir(cs_pin, GPIO_OUT);
gpio_put(cs_pin, 1);
// Initialize SPI port at 1 MHz
spi_init(spi, 1000 * 1000);
// Set SPI format
spi_set_format( spi0, // SPI instance
8, // Number of bits per transfer
1, // Polarity (CPOL)
1, // Phase (CPHA)
SPI_MSB_FIRST);
// Initialize SPI pins
gpio_set_function(sck_pin, GPIO_FUNC_SPI);
gpio_set_function(mosi_pin, GPIO_FUNC_SPI);
gpio_set_function(miso_pin, GPIO_FUNC_SPI);
// Workaround: perform throw-away read to make SCK idle high
reg_read(spi, cs_pin, REG_DEVID, data, 1);
// Read device ID to make sure that we can communicate with the ADXL343
reg_read(spi, cs_pin, REG_DEVID, data, 1);
if (data[0] != DEVID) {
printf("ERROR: Could not communicate with ADXL343\r\n");
while (true);
}
// Read Power Control register
reg_read(spi, cs_pin, REG_POWER_CTL, data, 1);
printf("0xX\r\n", data[0]);
// Tell ADXL343 to start taking measurements by setting Measure bit to high
data[0] |= (1 << 3);
reg_write(spi, cs_pin, REG_POWER_CTL, data[0]);
// Test: read Power Control register back to make sure Measure bit was set
reg_read(spi, cs_pin, REG_POWER_CTL, data, 1);
printf("0xX\r\n", data[0]);
// Wait before taking measurements
sleep_ms(2000);
// Loop forever
while (true) {
// Read X, Y, and Z values from registers (16 bits each)
reg_read(spi, cs_pin, REG_DATAX0, data, 6);
// Convert 2 bytes (little-endian) into 16-bit integer (signed)
acc_x = (int16_t)((data[1] << 8) | data[0]);
acc_y = (int16_t)((data[3] << 8) | data[2]);
acc_z = (int16_t)((data[5] << 8) | data[4]);
// Convert measurements to [m/s^2]
acc_x_f = acc_x * SENSITIVITY_2G * EARTH_GRAVITY;
acc_y_f = acc_y * SENSITIVITY_2G * EARTH_GRAVITY;
acc_z_f = acc_z * SENSITIVITY_2G * EARTH_GRAVITY;
// Print results
printf("X: %.2f | Y: %.2f | Z: %.2f\r\n", acc_x_f, acc_y_f, acc_z_f);
sleep_ms(100);
}
}
Run cmake and make (either from the command line or using the CMake extension in VS Code as outlined in this guide).
If you are using the picoprobe debugger, start debugging to upload the program and click the Run button to begin running it.
If you do not have picoprobe set up, put the Pico into bootloader mode and copy adxl343_spi.uf2 from the build directory to the RPI-RP2 drive that should have mounted on your computer.
Open your favorite serial terminal program and connect to the Pico with a baud rate of 115200. You might miss the printing of the POWER_CTL register, but you should see the values of the X, Y, and Z axes flying across the console.
Going Further
I recommend checking out the following documents if you wish to learn more about using SPI with the Raspberry Pi Pico and RP2040:
- Analog Devices - Introduction to SPI
- ADXL343 Datasheet
- Raspberry Pi Pico Datasheet
- Raspberry Pi Pico MicroPython SDK Guide
- Raspberry Pi Pico C/C++ SDK Guide
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.
Visit TechForum