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: