Raspberry Pi Pico (RP2040) I2C Example with MicroPython and C/C++
2021-07-19 | By ShawnHymel
License: Attribution
Inter-Integrated Circuit (I2C) is a simple communication protocol that is commonly used to talk to various sensors from microcontrollers. Like SPI, it is a synchronous protocol, as it has a separate clock line to tell the receiver when to sample data. Here is an article that offers a great explanation of how I2C works.
Note that I2C relies on an open drain configuration for both clock and data lines. This requires pull-up resistors on both lines. However, many breakout and prototyping boards (such as the one we will use for this demo) already have the pull-up resistors included. As a result, you should not need to add your own resistors.
This tutorial walks you through the process of connecting an accelerometer to the Raspberry Pi Pico using I2C 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.
You can run the following code to scan the I2C bus for attached devices. It should print out the address of the ADXL343, which is 0x53.
import machine
# Create I2C object
i2c = machine.I2C(0, scl=machine.Pin(17), sda=machine.Pin(16))
# Print out any addresses found
devices = i2c.scan()
if devices:
for d in devices:
print(hex(d))
In a new new document, enter the following code:
import machine
import utime
import ustruct
import sys
###############################################################################
# Constants
# I2C address
ADXL343_ADDR = 0x53
# 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
# Initialize I2C with pins
i2c = machine.I2C(0,
scl=machine.Pin(17),
sda=machine.Pin(16),
freq=400000)
###############################################################################
# Functions
def reg_write(i2c, addr, reg, data):
"""
Write bytes to the specified register.
"""
# Construct message
msg = bytearray()
msg.append(data)
# Write out message to register
i2c.writeto_mem(addr, reg, msg)
def reg_read(i2c, addr, reg, nbytes=1):
"""
Read byte(s) from specified register. If nbytes > 1, read from consecutive
registers.
"""
# Check to make sure caller is asking for 1 or more bytes
if nbytes < 1:
return bytearray()
# Request data from specified register(s) over I2C
data = i2c.readfrom_mem(addr, reg, nbytes)
return data
###############################################################################
# Main
# Read device ID to make sure that we can communicate with the ADXL343
data = reg_read(i2c, ADXL343_ADDR, REG_DEVID)
if (data != bytearray((DEVID,))):
print("ERROR: Could not communicate with ADXL343")
sys.exit()
# Read Power Control register
data = reg_read(i2c, ADXL343_ADDR, 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(i2c, ADXL343_ADDR, REG_POWER_CTL, data)
# Test: read Power Control register back to make sure Measure bit was set
data = reg_read(i2c, ADXL343_ADDR, 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(i2c, ADXL343_ADDR, 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_i2c.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_i2c). 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_i2c) and add the hardware_i2c 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_i2c 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_i2c
)
# 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/i2c.h"
// I2C address
static const uint8_t ADXL343_ADDR = 0x53;
// 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
*/
int reg_write(i2c_inst_t *i2c,
const uint addr,
const uint8_t reg,
uint8_t *buf,
const uint8_t nbytes);
int reg_read( i2c_inst_t *i2c,
const uint addr,
const uint8_t reg,
uint8_t *buf,
const uint8_t nbytes);
/*******************************************************************************
* Function Definitions
*/
// Write 1 byte to the specified register
int reg_write( i2c_inst_t *i2c,
const uint addr,
const uint8_t reg,
uint8_t *buf,
const uint8_t nbytes) {
int num_bytes_read = 0;
uint8_t msg[nbytes + 1];
// Check to make sure caller is sending 1 or more bytes
if (nbytes < 1) {
return 0;
}
// Append register address to front of data packet
msg[0] = reg;
for (int i = 0; i < nbytes; i++) {
msg[i + 1] = buf[i];
}
// Write data to register(s) over I2C
i2c_write_blocking(i2c, addr, msg, (nbytes + 1), false);
return num_bytes_read;
}
// Read byte(s) from specified register. If nbytes > 1, read from consecutive
// registers.
int reg_read( i2c_inst_t *i2c,
const uint addr,
const uint8_t reg,
uint8_t *buf,
const uint8_t nbytes) {
int num_bytes_read = 0;
// Check to make sure caller is asking for 1 or more bytes
if (nbytes < 1) {
return 0;
}
// Read data from register(s) over I2C
i2c_write_blocking(i2c, addr, ®, 1, true);
num_bytes_read = i2c_read_blocking(i2c, addr, buf, nbytes, false);
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 sda_pin = 16;
const uint scl_pin = 17;
// Ports
i2c_inst_t *i2c = i2c0;
// Buffer to store raw reads
uint8_t data[6];
// Initialize chosen serial port
stdio_init_all();
//Initialize I2C port at 400 kHz
i2c_init(i2c, 400 * 1000);
// Initialize I2C pins
gpio_set_function(sda_pin, GPIO_FUNC_I2C);
gpio_set_function(scl_pin, GPIO_FUNC_I2C);
// Read device ID to make sure that we can communicate with the ADXL343
reg_read(i2c, ADXL343_ADDR, 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(i2c, ADXL343_ADDR, 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(i2c, ADXL343_ADDR, REG_POWER_CTL, &data[0], 1);
// Test: read Power Control register back to make sure Measure bit was set
reg_read(i2c, ADXL343_ADDR, 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(i2c, ADXL343_ADDR, 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_i2c.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 I2C with the Raspberry Pi Pico and RP2040:
- Analog Devices - What is I2C?
- 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