Table of Contents

Support for Raspberry Pi Pico in Zephyr RTOS

All drivers were implemented by Andrei-Edward Popa from ACES Master Program and those are licensed under Apache 2.0 Open Source License.

Project repository: Andrei-Edward Popa Github Repository

Branches: rpi_pico_working_uart, rpi_pico_working_i2c

Demo: UART and I2C Driver Usage in Zephyr Project for Raspberry Pi Pico Board

1.Project Objective and Description

The purpose of this project is to introduce support for Raspberry Pi Pico board in one of the most popular Real Time Operating Systems (RTOS) for embedded systems, known as Zephyr Project.

In a nutshell, the support of Raspberry Pi Pico board in Zephyr was started on 7th May 2021 and the main contributor is Yonatan Schachter. He managed to introduce some functionality of the board in collaboration with Pete Johanson and the main achievements are: usage of one ARM Cortex-M0+ core of the RP2040 microcontroller, UF2 support for generating the binary file in a format that the second stage bootloader can understand and load into memory, GPIO driver, pin controller and UART driver without using interrupts (polling).

For me, this was a good start to learning how Zephyr is structured, understand the drivers API and how protocols like UART and I2C work and what registers we can use to make the driver functional. So, my project is about drivers for Raspberry Pi Pico board and how can we implement drivers like UART and I2C in this RTOS called Zephyr.

I will provide all steps for Zephyr installation and add the support for Raspberry Pi Pico below. All the steps are done in Ubuntu 20.04 LTS Linux Distribution.

First we need to install west and all its dependencies.

The next step is to initialize the Zephyr Project, update all hal modules and install the Zephyr SDK.

Open ~/.bashrc file and add the following environment variables and then execute bash command.

Finally, add my personal remote to your git local repository.

All the steps are tested on a fresh machine and it works without problems. You need to change the “user” with your machine username.

2.Hardware Description

The Raspberry Pi Pico board is a microcontroller board based on the RP2040 microcontroller chip designed by Raspberry Pi Foundation. This has been designed to be a low cost flexible development platform for RP2040, with the following key features:

However, the most important component of this board is the RP2040 microcontroller, which has the following key features:

In the photo below you can see the Raspberry Pi Pico board with the RP2040 microcontroller in the middle.

The BOOTSEL button is used for reflashing the board. If you pull down the button and power up the board via micro USB, a partition appears and we can copy the UF2 file into that partition to reflash the board. Below that button is the small Flash Memory chip. We can see all the 26 GPIOs with all functions which they may have, some ground pins, a dedicated pin for ADC voltage reference and some power pins. Also, you can see the SWD debug pins composed from an I/O, a clock and a ground pin.

For this project I also used an USB to TTL module to be able to connect to the serial interface of the Raspberry Pi Pico board and a Liquid Crystal Display (LCD) 2004 with an I2C bus for testing the I2C driver.

3.Drivers Description

In order to understand how I implemented those drivers, you need to understand the method that Zephyr let us to define new instances of drivers and what APIs we need to implement for them. In every driver description I will provide additional information about the Zephyr structure.

For a new instance of a driver, we need to provide some functions that initialize that driver and API functions that are specific to the driver and a user can use in his application. In order to define a new driver we need to provide a initialization function, a structure that contains data which can be used anywhere in the driver API, a structure that contains configuration parameters such as baudrates and pin numbers that cannot be modified, a stage of loading the driver (PRE_KERNEL1, PRE_KERNEL2 or POST_KERNEL) that is used to know what drivers to load in first, second or third stage of boot and an API structure that contains pointers to the API functions. All of those information is finally stored in a generic device driver structure that contains a name, a pointer to the configuration, a pointer to the data and a pointer to the API.

In order to create interrupt driven drivers we need to create an interrupt request configuration function that enables a particular interrupt based on the IRQ number and w need to provide an Interrupt Service Routine (ISR) for handling the interrupt request.

I will describe below the APIs and the implementation of UART and I2C drivers.

3.1 UART Driver

The API that Zephyr makes available to us consists of the following functions:

Those are not all API functions for UART driver, but I did not use the async API for this driver. The initialization part and poll_in and poll_out API functions were implemented by Yonathan. I made only a modification in the init function that disables the FIFOs and that means that the data are not stored in FIFOs anymore, but in a single 8 bit register (like we have a FIFO with one element). The polling method is not enough for using the well known shell module powered by Zephyr, so I implemented the interrupt driven part of the driver. I will describe the functions implemented below:

The Zephyr's shell application provides such a callback that treats UART interrupts. The interrupt controller for ARM Cortex M0+ is called Nested Vector Interrupt Controller (NVIC), which is implemented in Zephyr enables the interrupt IRQ number stored in the Device Tree (DTS) of the microcontroller. The configurations and information about the device driver are stored in nodes in DTS and for UART driver can be the base address of the registers, IRQ number, clocks used, baudrate etc. The information about the pin numbers is stored in a default configuration of the pin controller device tree. You can check the file stored in drivers/serial/uart_rpi_pico.c for more details about the implementation.

3.2 I2C Driver

The API that Zephyr makes available to us consists of the following functions:

I will describe the implementation for RP2040 API functions below:

After the API is done, the master mode works. But now, what about the slave mode? As I say previously on this driver, the slave mode cannot work without interrupts because we don't need to poll the device every time to see if his address is on the bus. So, when we configured the device in slave mode, we need to enable all the interrupts and write some Interrupt Service Routine function to handle that interrupt.

Because we want that ISR to be executed faster, I create some non-blocking read and write function that read data from RX FIFO if it is not empty and write data into the TX FIFO if it is not full. We need to read the interrupt status register to see that interrupt occurs and how to treat it. Now let's discuss what happens when a master device what to read or write to a slave:

The slave callbacks are function provided by the users to instruct the slave device that data to write to a master or what to do with the data that comes from the master device. The API of the slave callbacks is the following:

Anytime when we want to use the Raspberry Pi Pico board as an I2C slave, we need to provide those callback functions (maybe not all) for dealing this data read and write from or to a slave.

4.Drivers Usage Examples

For testing the UART driver, the only thing I need to do was to load the Zephyr shell module into the Raspberry Pi Pico board and see if it works. For I2C driver, I have created 2 applications. One of that can use the I2C module in a master configuration for writing some commands to the LCD 2004 with I2C bus and see if the characters are written on the LCD. The other one uses both I2C modules, one configured in master mode and the other one configured in slave mode. The only thing that this application do is to write some data from master to slave, save this data into a memory buffer at some addresses and then read the same information from slave.

4.1 UART Driver Example

In order to use this application, you need to clone my repository from Github and move to rpi_pico_working_uart branch. After that, you need to do the next commands in terminal:

After you build the shell module sample, you need to copy the UF2 file to the partition that in mounted when you press the BOOTSEL button and power the board. After that, you need to open a serial monitor to see the shell module. You can see the setup in photos below and some outputs of the pico. You can see that the USB to TTL TX and RX pins are connected to GP0 and GP1 pins of the Pico. Also, the ground pin is connected to pin number 3 of the board.

4.2 I2C Master Mode Driver Example

In order to use this application, you need to clone my repository from Github and move to rpi_pico_working_i2c branch. After that, you need to do the next commands in terminal:

The i2c_lcd_2004 application was made by me, it is not a default sample in zephyr. For that, you need to checkout to rpi_pico_working_i2c_samples branch and cherry-pick the last commit into the rpi_pico_working_i2c branch. This application initialize the LCD by writing some registers directly from I2C and write some text on the LCD display. The LCD was powered by the USB to TTL module and uses the Pico ground pin and the clock pin (SCK) and data pin (SDA) are connected on GP4 and GP5 pins on the Raspberry Pi Pico.

4.3 I2C Slave Mode Driver Example

In order to use this application, you need to clone my repository from Github and move to rpi_pico_working_i2c branch. After that, you need to do the next commands in terminal:

The rpi_pico_slave application was made by me, it is not a default sample in zephyr. For that, you need to checkout to rpi_pico_working_i2c_samples branch and cherry-pick the last commit into the rpi_pico_working_i2c branch. This application configures the I2C0 as a master and the I2C1 as a slave. The master write an addrees and some text to the slave and slave save the text at the specified address into a 256B memory buffer. Then, master reads data from the slave and slave gives the data stored in the last address written by the slave. GP4 and GP14 are the data pins (SDAs) of the I2C IPs and are connected together and GP5 and GP15 are the clock pins SCK pins (SCKs).

5.Issues and Solutions

During the implementation of the UART Driver I had one major problem which takes me around 4 days to solve. The problem was that the UART IP that is integrated into the RP2040 microcontroller was kept into the ISR forever because some interrupt pins were masked when the initialization of the UART Driver was done. Because the initialization part wasn't done by me, the bug was hard to find. For that, I needed to clear all the interrupts before I configure the IRQ number of UART and enable the interrupt.

During the implementation of the I2C Driver I had the some problem as the UART Driver, some interrupts needed to be unmasked before the I2C interrupts to be enabled. Also, the configuration of the FIFOs was buggy because, for some reason, the depth of the FIFOs cannot be larger that 16. In the documentation, it says that the FIFOs can have a depth of 256 entries, but it seems like it doesn't.

6.Conclusions

The purpose of this project was to adding more support to the Raspberry Pi Pico board in Zephyr Project. The important drivers that I made are UART Interrupt Driven Driver and I2C Driver, both master and slave modes. In addition, I have a working version of the PWM driver, but I didn't have time to discuss about it, but you can check the rpi_pico_working_pwm branch on my repository.

The lessons learned during the development of this project are not few and here are just some of them:

For future work, I want to implement the next list of drivers: