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.

  • sudo apt update
  • sudo bash kitware-archive.sh
  • sudo apt install –no-install-recommends git cmake ninja-build gperf ccache dfu-util device-tree-compiler wget python3-dev python3-pip python3-setuptools python3-tk python3-wheel xz-utils file make gcc gcc-multilib g++-multilib libsdl2-dev
  • pip3 install –user -U west
  • echo 'export PATH=~/.local/bin:“$PATH”' » ~/.bashrc
  • source ~/.bashrc

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.

  • export ZEPHYR_BASE=/home/“user”/zephyrproject/zephyr
  • export ZEPHYR_GCC_VARIANT=zephyr
  • export ZEPHYR_SDK_INSTALL_DIR=/home/“user”/zephyr-sdk-0.13.1
  • export ZEPHYR_TOOLCHAIN_VARIANT=“zephyr”
  • export CMAKE_PREFIX_PATH=/home/“user”/zephyrproject/zephyr/share/zephyr-package/cmake
  • export IDF_PATH=”/home/“user”/zephyrproject/modules/hal/raspberrypi”

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

  • cd ~/zephyrproject/zephyr
  • git remote add “remote_name” https://github.com/andrei-edward-popa/zephyr
  • git remote update
  • git pull
  • git checkout rpi_pico_working_uart OR rpi_pico_working_i2c OR rpi_pico_working_i2c_samples

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:

  • It is based on RP2040 microcontroller with 2MB of Flash Memory.
  • It contains a Micro-USB B port for power the board at 5V and for reprogramming the Flash Memory using a file in UF2 format.
  • It has 40 pins and exposes 26 multi-function 3.3V GPIOs, 23 being digital-only and 3 of them can be used as an ADC.
  • 3 dedicated pins for debug purposes using Serial Wire Debug (SWD) debug protocol. You can check Open On Chip Debugger (openocd) for the best experience on debugging applications and drivers on embedded systems.
  • Multiple modes of powering the board like micro USB, external supplies or batteries.

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

  • Dual Core Cortex M0+, that can be up to 133MHz (PLL allows variable core frequency).
  • 264kB of SRAM.
  • External Quad-SPI Flash.
  • 30 multi-function GPIOs at 1.8V/3.3V (4 of those can act as an 12-bit 500ksps ADC).
  • Digital peripherals like 2 UART, 2 I2C, 16 PWM channels, 2 SPI, 1 Timer, 1 RTC.
  • 2 Programmable IO (PIO) blocks, that can be used to emulate the functionality of display interfaces.

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:

  • poll_in: Read a character from the input in polled mode.
  • poll_out: Write a character to the output in polled mode.
  • err_check: Check whether an error was detected.
  • configure: Set UART configuration.
  • get_config: Get UART configuration.
  • fifo_fill: Fill FIFO with data.
  • fifo_read: Read data from FIFO.
  • irq_tx_enable: Enable TX interrupt.
  • irq_tx_disable: Disable TX interrupt.
  • irq_rx_enable: Enable RX interrupt.
  • irq_rx_disable: Disable RX interrupt.
  • irq_tx_ready: Check if UART TX buffer can accept a new character.
  • irq_rx_ready: Check if UART RX buffer has a received character.
  • irq_err_enable: Enable error interrupt.
  • irq_err_disable: Disable error interrupt.
  • irq_is_pending: Check if any IRQs is pending.
  • irq_update: Start processing interrupts in ISR.
  • irq_callback_set: Set the IRQ callback function pointer.

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:

  • uart_rpi_err_check: The data register (DR) contains some bits that are automatically set when errors like overun, frame, parity or break error occurs. I read those bits and I checked what bits are set and return those error bits.
  • uart_rpi_fifo_fill: In order to fill the TX FIFO we need to check if it is full. We can to that by reading the TX FIFO full bit in Flag Register (FR). If the FIFO is not full, we can put the character in data register (DR) and from there it is pushed in TX FIFO.
  • uart_rpi_fifo_read: In order to read from RX FIFO we need to check if it is empty. We can to that by reading the RX FIFO empty bit in Flag Register (FR). If the FIFO is not empty, we can get the character from the FIFO which is aumatically pulled into the data register (DR), so we need to read the data register.
  • uart_rpi_irq_tx_enable: We can enable the TX interrupts by setting the Transmit Interrput Mask bit from the Interrupt Mask Set/Clear Register (IMSC). Additional work was done here by setting the Transmit interrupt FIFO level select bit from Interrupt FIFO Level Select Register (IFLS) for setting the threshold of the FIFO as low as possible.
  • uart_rpi_irq_tx_disable: We can disable the TX interrupts by clearing the Transmit Interrput Mask bit from the Interrupt Mask Set/Clear Register (IMSC).
  • uart_rpi_irq_rx_enable: We can enable the RX interrupts by setting the Receive Interrput Mask bit from the Interrupt Mask Set/Clear Register (IMSC). Additional work was done here by setting the Receive interrupt FIFO level select bit from Interrupt FIFO Level Select Register (IFLS) for setting the threshold of the FIFO as low as possible.
  • uart_rpi_irq_rx_disable: We can disable the RX interrupts by clearing the Receive Interrput Mask bit from the Interrupt Mask Set/Clear Register (IMSC).
  • uart_rpi_irq_tx_ready: We can check if we can accept a new character in TX buffer by checking if the Transmit masked interrupt status bit from Masked Interrupt Status Register (MIS) is set.
  • uart_rpi_irq_rx_ready: We can check if we have a new character in RX buffer by checking if the Receive masked interrupt status bit from Masked Interrupt Status Register (MIS) is set.
  • uart_rpi_irq_err_enable: We can enable those errors by masking the appropriate errors bits in Interrupt Mask Set/Clear Register (IMSC).
  • uart_rpi_irq_err_disable: We can enable those errors by unmasking the appropriate errors bits in Interrupt Mask Set/Clear Register (IMSC).
  • uart_rpi_irq_is_pending: The IRQ is pending if TX is ready or RX is ready, so we can call the implemented functions for this.
  • uart_rpi_irq_update: We don't want to update the IRQ, so no implementation is required.
  • uart_rpi_irq_callback_set: The idea of the callback is that we let the user to set a custom function that ISR calls when an interrupt occurs. So here we only need to store the callback function provided by the user.

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:

  • configure: Configure operation of a host controller.
  • transfer: Perform data transfer to another I2C device in master mode.
  • slave_register: Registers the provided config as slave device of a controller.
  • slave_unregister: Unregisters the provided config as slave device.
  • recover_bus: Recover the I2C bus.

I will describe the implementation for RP2040 API functions below:

  • i2c_rpi_configure: The configuration function needs to register the I2C device in master mode. The first step is to configure the pins used for I2C communication using the pin controller. Before that, we need to configure the baudrate and initialize the device writing some registers. When the slave API is active, we need to configure and enable the interrupts because slave can only works with an interrupt driven implementation. In other case, the slave needs to poll the bus and check if his address is on the bus and this need to be performed periodically and slows down the processor. In master mode, we need to unmask all the interrupts by clearing the Interrupt Mask Reset bit in Interrupt Mask register. After we set the RX and TX FIFOs depth, our configuration is done. This function is called also in the init function of the driver.
  • i2c_rpi_transfer: The transfer API function takes an i2c_msgs structure as parameter. Object of this type need to be passed when we want to do a transfer. The structure contains a pointer to where to read or write data and the length of the buffer in bytes. The transfer function is generic for read and write, so we need to specify also some flags into the i2c_msgs objects to know when to do a read or a write (I2C_MSG_READ and I2C_MSG_WRITE). We need to get all the messages and if the flag on the current message is set as I2C_MSG_WRITE we need to write to a slave and if the flag is I2C_MSG_READ we need to read from a slave device. The read and write functions for this transfer are implemented separately, but they are quite long to explain, so I'll let you to get to my repository and figured out what's going on there.
  • i2c_rpi_slave_register: For register the device as a slave, we first need to disable the master mode by clearing the Master Mode bit in Control Register. After that, we need to configure the slave address in Slave Address Register. After we enable all the interrupts from Interrupt Mask Register, the function is done.
  • i2c_rpi_slave_unregister: The unregister slave function needs to configure the I2C back into master mode. We need to do the reverse operations from slave register and the important part is to disable all the interrupts because master is not implemented with interrupt driven.
  • i2c_rpi_recover_bus: For recovering the bus, we need to reset and unreset the device by using the Reset Controller from the pico sdk and call the configure function to reconfigure the device in master mode.

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:

  • When a master device sends data to a slave device, the data is stored into the RX FIFO of the slave. We want to trigger an interrupt if at least one character was written. For that, in slave mode, we need to set the depth of the FIFOs to 1. Now, when the master device sends one character, the RX FIFO of the slave is full and we can check the RX_FULL bit from Interrupt Status Register. If we are in that case, we need to read that data with the non-blocking read function and call a callback function provided by the user (we will discuss about that) of the driver to handle that data that just arrives.
  • When a master device wants to read from a slave device, we need to check if the RD_REQ bit (Read Requested) from the Interrupt Status Register is set. If that's the case, we need to clear that interrupt, call a callback function provided by the user to get the value that master needs and write it to the master with the non-blocking write function.
  • If the slave detects a stop bit from master, we can call a callback function provided by the user to stop some configuration for read/write from master.

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:

  • write_requested: Function called when a write to the device is initiated.
  • read_requested: Function called when a read from the device is initiated.
  • write_received: Function called when a write to the device is continued.
  • read_processed: Function called when a read from the device is continued.
  • stop: Function called when a stop condition is observed after a start condition addressed to a particular device.

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:

  • west build -b raspberrypi_pico samples/subsys/shell/shell_module
  • cp build/zephyr/zephyr.uf2 /path/to/pico/mount/point
  • minicom -b 115200 /dev/ttyUSB0

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:

  • west build -b raspberrypi_pico samples/drivers/i2c_lcd_2004
  • cp build/zephyr/zephyr.uf2 /path/to/pico/mount/point

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:

  • west build -b raspberrypi_pico samples/drivers/rpi_pico_slave
  • cp build/zephyr/zephyr.uf2 /path/to/pico/mount/point
  • minicom -b 115200 /dev/ttyUSB0

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:

  • Device Driver Model of the Zephyr Project.
  • UART and I2C protocols in depth.
  • Create custom applications and use the drivers APIs for creating the applications.

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

  • SPI driver
  • Watchdog Timer driver
  • Real Time Counter driver
  • ADC driver
iothings/proiecte/2021/zephyr.txt · Last modified: 2022/01/28 19:20 by andrei_edward.popa
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0