You dont have javascript enabled! Please enable it!

Practical case: UART Echo on Pico-ICE FPGA & Raspberry Pi

Practical case: UART Echo on Pico-ICE FPGA & Raspberry Pi — hero

Objective and use case

What you’ll build: This project demonstrates how to send low-power UART messages from a Pico-ICE FPGA (Lattice iCE40UP5K) to a Raspberry Pi using Python and C firmware.

Why it matters / Use cases

  • Enable low-power communication between FPGA and Raspberry Pi for IoT applications, reducing energy consumption.
  • Facilitate rapid prototyping of embedded systems by utilizing UART for simple data exchange.
  • Provide a foundation for more complex FPGA and microcontroller co-designs in future projects.
  • Demonstrate effective use of the Pico SDK for developing firmware on the RP2040 microcontroller.

Expected outcome

  • Successful transmission of UART messages at a baud rate of 9600, verified by Python script output.
  • Reduction in power consumption by running the RP2040 at 12 MHz instead of 125 MHz.
  • Validation of UART communication with minimal latency, ensuring timely message delivery.
  • Establishment of a reliable 3-wire UART link between the Pico-ICE and Raspberry Pi.

Audience: Embedded systems developers; Level: Basic

Architecture/flow: UART communication from Pico-ICE to Raspberry Pi, utilizing Python for message validation.

Practical Case: Raspberry Pi — Pico-ICE (Lattice iCE40UP5K) — “uart-hello-world-eco”

This hands-on, basic‑level project shows how to send a minimal, low‑power UART message from a Pico‑ICE (Lattice iCE40UP5K) to a Raspberry Pi running Raspberry Pi OS Bookworm 64‑bit, and validate the output using Python 3.11. You will build tiny C firmware for the RP2040 on the Pico‑ICE using the Pico SDK, configure the Raspberry Pi’s serial interface, wire a simple 3‑wire UART link, and verify the “uart-hello-world-eco” message with a short Python script in a virtual environment.

We intentionally keep the microcontroller in a low‑power configuration (“eco”) by:
– Running the RP2040 at a reduced system clock (12 MHz instead of 125 MHz).
– Using UART at a modest baud rate (9600).
– Disabling default stdio backends to avoid unnecessary USB/printf overhead.

The end result is a deterministic, resource‑efficient UART “Hello World” that is easy to reproduce and extends well to more advanced FPGA+MCU co‑designs later.


Prerequisites

  • A Raspberry Pi 4, 400, or 5 running Raspberry Pi OS Bookworm 64‑bit.
  • Internet connectivity for package installation.
  • Basic familiarity with the Linux command line (shell), Git, and CMake.
  • You have admin rights (sudo) on the Raspberry Pi.
  • A clean microSD card with Raspberry Pi OS Bookworm (64‑bit) already flashed.

Verify OS and Python:

lsb_release -a
uname -m
python3 --version

You should see aarch64 (64‑bit) and Python 3.11.x on Bookworm.


Materials (with exact model)

  • 1x Raspberry Pi 4/400/5 (40‑pin header; running Raspberry Pi OS Bookworm 64‑bit).
  • 1x Pico-ICE (Lattice iCE40UP5K) board (RP2040 + iCE40UP5K FPGA).
  • 1x USB‑C cable for the Pico‑ICE (data‑capable, not charge‑only).
  • 3x Female‑female jumper wires (Dupont) for UART (TX, RX, GND).
  • Optional: ESD mat and wrist strap, USB isolator (recommended in lab settings).

Setup/Connection

1) Update system and install base tools

Run these commands on the Raspberry Pi to update packages and install build tools needed for the RP2040 firmware:

sudo apt update
sudo apt full-upgrade -y
sudo apt install -y git cmake build-essential \
  gcc-arm-none-eabi libnewlib-arm-none-eabi \
  pkg-config python3-venv python3-pip \
  minicom screen
  • gcc-arm-none-eabi and libnewlib-arm-none-eabi: cross toolchain for RP2040.
  • minicom and screen: useful for quick serial tests.

2) Enable UART on the Raspberry Pi

We will route the Pi’s primary UART (/dev/serial0) to the 40‑pin header (GPIO14/15). Disable login shell over serial and enable the UART hardware.

Option A — raspi-config:

sudo raspi-config
  • Interface Options → Serial Port
  • “Login shell accessible over serial?” → No
  • “Enable serial port hardware?” → Yes
  • Interface Options → I2C → Enable (optional; not used here)
  • Interface Options → SPI → Enable (optional; not used here)
  • Finish and reboot.

Option B — manual edit:
– Ensure the serial console is not in cmdline:
sudo sed -i 's/console=serial0,[0-9]* //g' /boot/firmware/cmdline.txt
– Ensure UART is enabled via config.txt:
echo 'enable_uart=1' | sudo tee -a /boot/firmware/config.txt
– Reboot:
sudo reboot

After reboot, confirm:

ls -l /dev/serial0

It should exist and link to ttyAMA0 or ttyS0 depending on model.

3) Create a Python 3.11 virtual environment for validation

We will use this venv for the host‑side UART validation script.

python3 -m venv ~/venv-uart-eco
source ~/venv-uart-eco/bin/activate
pip install --upgrade pip
pip install pyserial

Per the Raspberry Pi family defaults, also install these (even if not required for UART):
– System packages with apt:
sudo apt install -y python3-gpiozero python3-smbus python3-spidev
– Or inside the venv (optional via pip; not necessary if apt is used):
pip install gpiozero smbus2 spidev

We will only use pyserial for the validation program, but the others are common Raspberry Pi interfaces useful in later projects.

4) Wiring the UART (3 wires, 3.3 V logic)

Both the Raspberry Pi and Pico‑ICE use 3.3 V logic. Do not connect 5 V to any data pin. We’ll use RP2040 UART0 pins on the Pico‑ICE: GPIO0 (TX) and GPIO1 (RX). These are the standard default UART pins on RP2040 and are presented on the Pico‑style edge castellations of the Pico‑ICE.

Make the following connections:

Pico-ICE (RP2040) Function Raspberry Pi 40‑pin header
GP0 (pin 1) UART0 TX (output) GPIO15 RXD (physical pin 10)
GP1 (pin 2) UART0 RX (input) GPIO14 TXD (physical pin 8)
GND (any GND) Ground reference GND (physical pin 6)

Notes:
– TX from Pico‑ICE must go to RX on the Raspberry Pi (crossed), and RX to TX.
– Connect the ground line; without a common ground, UART will be unreliable or fail.
– Leave 3V3 and 5V power lines disconnected for UART; we will power the Pico‑ICE via its USB‑C port.

5) Power the Pico‑ICE

  • Connect Pico‑ICE to the Raspberry Pi’s USB‑A port using a USB‑C cable.
  • The board should power up; a storage device may appear if it is in BOOTSEL mode.

Full Code

We provide two pieces of code:

1) RP2040 firmware (C with Pico SDK) to send the eco message over UART0 at 9600 baud, while running the system clock at a reduced 12 MHz.
2) A Raspberry Pi Python validation script using pyserial to read and assert the exact message.

1) RP2040 “uart-hello-world-eco” firmware (C)

Create a project directory on the Pi (host build machine):

mkdir -p ~/pico-ice-uart-eco/firmware
cd ~/pico-ice-uart-eco

Fetch the Pico SDK (release tracking via Git); we’ll place it under ~/pico-sdk and set PICO_SDK_PATH accordingly.

cd ~
git clone --depth 1 https://github.com/raspberrypi/pico-sdk.git
cd pico-sdk
git submodule update --init

Now in your firmware directory:

cd ~/pico-ice-uart-eco/firmware

Create these files:

  • pico_sdk_import.cmake (copy from the SDK’s external template):
cp ~/pico-sdk/external/pico_sdk_import.cmake .
  • CMakeLists.txt:
cmake_minimum_required(VERSION 3.13)

include(pico_sdk_import.cmake)

project(uart_eco C CXX ASM)

pico_sdk_init()

add_executable(uart_eco
    main.c
)

target_link_libraries(uart_eco
    pico_stdlib
    hardware_uart
    hardware_clocks
)

pico_enable_stdio_usb(uart_eco 0)
pico_enable_stdio_uart(uart_eco 0)

# Create UF2, bin, etc.
pico_add_extra_outputs(uart_eco)
  • main.c:
#include "pico/stdlib.h"
#include "hardware/uart.h"
#include "hardware/clocks.h"

// Eco UART "Hello World" on RP2040 (Pico-ICE board)
// UART0 on pins GP0 (TX) and GP1 (RX), at 9600 baud
// System clock reduced to 12 MHz to save power.

#define UART_ID     uart0
#define BAUD_RATE   9600
#define UART_TX_PIN 0
#define UART_RX_PIN 1

static void eco_init(void) {
    // Lower the system clock; 12 MHz is plenty for 9600 baud UART
    // Returns true if set successfully.
    (void)set_sys_clock_khz(12000, true);

    // Initialize chosen UART
    uart_init(UART_ID, BAUD_RATE);

    // Set the GPIO function for the UART pins
    gpio_set_function(UART_TX_PIN, GPIO_FUNC_UART);
    gpio_set_function(UART_RX_PIN, GPIO_FUNC_UART);

    // Optionally, disable unnecessary pulls on RX to reduce microamps
    gpio_disable_pulls(UART_TX_PIN);
    gpio_disable_pulls(UART_RX_PIN);

    // Initialize stdlib timing (sleep_ms)
    stdio_init_all();
}

int main() {
    eco_init();

    // Minimal loop: send message at a gentle duty cycle
    const char *msg = "uart-hello-world-eco\r\n";

    while (true) {
        uart_puts(UART_ID, msg);
        // Keep the device mostly idle; sleep for 2 seconds
        sleep_ms(2000);
    }
}

This firmware:
– Sets the RP2040 system clock to 12 MHz to reduce current consumption.
– Uses UART0 at 9600 baud on GP0/GP1 (default mapping).
– Avoids setting up stdio over USB/UART (no printf), keeping the output strictly on UART0 via uart_puts.

2) Raspberry Pi validation script (Python 3.11, pyserial)

In your venv:

source ~/venv-uart-eco/bin/activate
mkdir -p ~/pico-ice-uart-eco/host
cd ~/pico-ice-uart-eco/host

Create read_uart_validate.py:

#!/usr/bin/env python3
import time
import sys
import serial

PORT = "/dev/serial0"
BAUD = 9600
EXPECTED = "uart-hello-world-eco"

def main():
    try:
        with serial.Serial(PORT, BAUD, timeout=2) as ser:
            print(f"Opened {PORT} at {BAUD} baud")
            # Flush any stale input
            ser.reset_input_buffer()
            t0 = time.time()
            seen = False

            while time.time() - t0 < 10:
                line = ser.readline().decode("utf-8", errors="ignore").strip()
                if line:
                    print(f"RX: {line}")
                    if line == EXPECTED:
                        seen = True
                        break

            if not seen:
                print("Did not see the expected message within 10 seconds.", file=sys.stderr)
                sys.exit(2)

            print("Validation OK: received exact 'uart-hello-world-eco'")
            sys.exit(0)
    except serial.SerialException as e:
        print(f"Serial error: {e}", file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    main()

Make it executable:

chmod +x ~/pico-ice-uart-eco/host/read_uart_validate.py

Build/Flash/Run commands

All commands are to be run on the Raspberry Pi.

1) Build the RP2040 firmware

export PICO_SDK_PATH=~/pico-sdk
cd ~/pico-ice-uart-eco/firmware
mkdir -p build
cd build
cmake -DPICO_SDK_PATH=$PICO_SDK_PATH -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc)

On success, you will have uart_eco.uf2 under ~/pico-ice-uart-eco/firmware/build/.

2) Put Pico-ICE into BOOTSEL and flash UF2

  • Unplug the Pico‑ICE USB‑C if connected.
  • Press and hold the BOOT/BOOTSEL button on the Pico‑ICE.
  • Plug in the USB‑C cable to the Raspberry Pi while holding the button.
  • Release the button after the board enumerates as a USB mass storage device (often named RPI-RP2).

Copy the UF2:

UF2=~/pico-ice-uart-eco/firmware/build/uart_eco.uf2

# Identify the mount path (commonly /media/pi/RPI-RP2)
lsblk -o NAME,MOUNTPOINT | grep RP2 || true

# If auto-mounted at /media/pi/RPI-RP2:
cp "$UF2" /media/$USER/RPI-RP2/
sync

The device will automatically reboot after the UF2 is copied.

If your desktop environment doesn’t auto‑mount, you can mount manually:

# Find the device (e.g., /dev/sda1)
lsblk -p -o NAME,LABEL | grep RPI-RP2
# Suppose it is /dev/sda1
sudo mkdir -p /mnt/rp2
sudo mount /dev/sda1 /mnt/rp2
sudo cp "$UF2" /mnt/rp2/
sync
sudo umount /mnt/rp2

3) Run and read on the Raspberry Pi

Ensure wiring is as per the table (TX↔RX, GND↔GND). Then run the validation script:

source ~/venv-uart-eco/bin/activate
python ~/pico-ice-uart-eco/host/read_uart_validate.py

Expected terminal output:
– It opens /dev/serial0 at 9600.
– It prints lines received.
– It confirms: “Validation OK: received exact ‘uart-hello-world-eco’”.

If you prefer a quick manual check:

minicom -b 9600 -o -D /dev/serial0
# Press Ctrl-A then Q to quit (without reset) when done.

Step‑by‑step Validation

1) Confirm UART is free (login shell disabled):
sudo systemctl status serial-getty@ttyAMA0.service 2>/dev/null || true
sudo systemctl status serial-getty@ttyS0.service 2>/dev/null || true

The getty service should be inactive/disabled on whichever UART /dev/serial0 targets.

2) Confirm /dev/serial0 exists:
ls -l /dev/serial0

3) Check wiring:
– Pico‑ICE GP0 → Raspberry Pi GPIO15 (pin 10).
– Pico‑ICE GP1 → Raspberry Pi GPIO14 (pin 8).
– GND → GND (pin 6).
– USB‑C provides power to Pico‑ICE.

4) Confirm Pico‑ICE firmware is running:
– After UF2 copy, the board reboots automatically.
– The RP2040 is running at 12 MHz and broadcasting “uart-hello-world-eco” every ~2 seconds.

5) Validate with Python:
source ~/venv-uart-eco/bin/activate
python ~/pico-ice-uart-eco/host/read_uart_validate.py

– You should see a line like: “RX: uart-hello-world-eco”.
– The script exits with code 0 on success. To verify programmatically:
echo $?
Expect 0.

6) Optional sanity check with stty:
stty -F /dev/serial0 9600
cat /dev/serial0

Press Ctrl‑C to exit. You should see periodic lines.

7) Optional: Confirm low‑baud and smooth timing:
– Observed interval between lines should be ~2 seconds.
– Lower baud reduces switching activity and IO power.

8) Optional: Alternative test with screen:
screen /dev/serial0 9600
Exit with Ctrl‑A then K.

If any of these steps fails, see Troubleshooting below.


Troubleshooting

  • No output on /dev/serial0:
  • Re‑check that the serial login shell was disabled in raspi-config.
  • Confirm enable_uart=1 is present in /boot/firmware/config.txt.
  • Ensure you rebooted after configuration changes.

  • Wrong pins:

  • TX and RX must be crossed (Pico‑ICE TX → Pi RX, Pico‑ICE RX → Pi TX).
  • Confirm you used GPIO14 (TXD) and GPIO15 (RXD) on the Pi’s 40‑pin header.
  • Verify ground is connected.

  • Baud mismatch:

  • Firmware sets BAUD_RATE to 9600; open the host port at 9600.
  • If you modified the code, keep host and device in sync.

  • UF2 not copying:

  • Ensure the board is in BOOTSEL (hold BOOT while plugging in).
  • Try a different USB‑C cable (must support data).
  • Make sure the RPI-RP2 drive is mounted before copying.

  • Build errors:

  • PICO_SDK_PATH must point to the pico-sdk:
    export PICO_SDK_PATH=~/pico-sdk
  • Ensure submodules are initialized:
    cd ~/pico-sdk
    git submodule update --init
  • If CMake can’t find the SDK, verify pico_sdk_import.cmake is present in your firmware directory.

  • Conflicting serial usage:

  • Some HATs or services may grab the serial port. Stop them or remove overlays that reassign UART pins.

  • USB power issues:

  • If the board resets or disappears, try a different USB port or use a powered USB hub.
  • Avoid also powering the Pico‑ICE from another source simultaneously.

  • Python environment:

  • If import serial fails, ensure you activated the venv and installed pyserial:
    source ~/venv-uart-eco/bin/activate
    pip install pyserial

Improvements

  • Dynamic eco modes:
  • Add a “command listener” on UART RX to switch between fast (e.g., 48–125 MHz) and eco (12 MHz) modes at runtime.
  • Gate the UART transmitter if no message needs to be sent.

  • Deeper low‑power tricks:

  • Use the RP2040 dormant or sleep states between transmissions, waking via a timer alarm.
  • Reduce peripheral clocks to the minimum necessary.
  • Disable unused GPIO pulls systematically.

  • Higher‑integrity UART:

  • Add CRLF normalization and a simple checksum (e.g., CRC8) to the message for robust host validation.
  • Enable hardware flow control (CTS/RTS) for higher baud rates (requires extra pins).

  • FPGA offload (leveraging the iCE40UP5K):

  • Implement a UART TX core in the FPGA to offload the RP2040.
  • RP2040 provides the message via SPI to the FPGA, which transmits autonomously at very low duty cycle.
  • Toolchain: yosys/nextpnr-ice40/icestorm (can be installed on Raspberry Pi), then integrate with the Pico‑ICE bitstream loader used by the board vendor.

  • Advanced host logging:

  • Extend the Python script to timestamp lines, write to CSV/JSON, and auto‑retry if the serial port is busy.
  • Integrate with gpiozero to signal activity on a Pi GPIO LED only when new lines are received.

  • Device tree overlays:

  • Pin the UART to core clocks most suitable for stability on Pi models with a secondary mini‑UART when needed.

Final Checklist

  • Raspberry Pi OS Bookworm 64‑bit installed and updated.
  • Python 3.11 venv created; pyserial installed:
  • venv path: ~/venv-uart-eco
  • pyserial installed and working.
  • Interfaces:
  • Serial login disabled.
  • UART hardware enabled (enable_uart=1).
  • Pico‑ICE (Lattice iCE40UP5K) wired to Raspberry Pi:
  • GP0 (TX) → GPIO15 (RX, pin 10).
  • GP1 (RX) → GPIO14 (TX, pin 8).
  • GND → GND (pin 6).
  • Firmware built using Pico SDK:
  • PICO_SDK_PATH exported to ~/pico-sdk.
  • Firmware UF2: ~/pico-ice-uart-eco/firmware/build/uart_eco.uf2.
  • UF2 flashed via BOOTSEL mass storage:
  • RPI-RP2 mounted.
  • UF2 copied.
  • Validation:
  • Python script reads /dev/serial0 at 9600 baud.
  • Output shows exact “uart-hello-world-eco”.
  • Eco characteristics applied:
  • RP2040 clock set to 12 MHz (set_sys_clock_khz(12000, true)).
  • Low baud (9600) used to minimize switching and IO power.
  • Next steps considered:
  • Dormant sleeps, FPGA offload, or dynamic eco mode switching.

This completes the basic “uart-hello-world-eco” project on Raspberry Pi with the Pico-ICE (Lattice iCE40UP5K). You now have a reproducible, power‑conscious UART pipeline from the RP2040 to the Raspberry Pi suitable for lab demos, automated tests, and as a foundation for deeper MCU+FPGA co‑design work.

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the main purpose of the 'uart-hello-world-eco' project?




Question 2: Which microcontroller is used in the Pico-ICE project?




Question 3: What is the reduced system clock speed used in the project?




Question 4: What baud rate is used for UART in this project?




Question 5: What operating system must the Raspberry Pi run for this project?




Question 6: Which programming language is used to validate the output?




Question 7: How many jumper wires are required for the project?




Question 8: What type of cable is needed for the Pico-ICE?




Question 9: What is necessary to check before starting the project?




Question 10: What type of environment is suggested for running the Python script?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:
Scroll to Top