This is an old revision of the document!


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: https://github.com/andrei-edward-popa/zephyr/

Branches: rpi_pico_working_uart, rpi_pico_working_i2c

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.

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 powered by Zephyr, so I implemented the interrupt driver 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

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.

5.Issues and Solutions

6.Conclusions

iothings/proiecte/2021/zephyr.1643369487.txt.gz · Last modified: 2022/01/28 13:31 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