Practical case: LED dimming with PWM on Pico-ICE iCE40UP5K

Practical case: LED dimming with PWM on Pico-ICE iCE40UP5K — hero

Objective and use case

What you’ll build: Control LED brightness using PWM on the Pico-ICE board with Raspberry Pi, allowing for interactive adjustments from a host device.

Why it matters / Use cases

  • Demonstrate real-time control of LED brightness in IoT applications using Raspberry Pi and MicroPython.
  • Utilize PWM for efficient LED dimming, which is crucial for energy-saving in smart lighting systems.
  • Provide a foundational understanding of GPIO interfacing and PWM signal generation for beginners in embedded systems.
  • Enable interactive projects where users can adjust lighting conditions based on environmental factors.

Expected outcome

  • Achieve LED brightness control from 0% to 100% with a response time of less than 100ms.
  • Validate successful communication between the Raspberry Pi and Pico-ICE via USB-serial interface.
  • Measure power consumption reduction when using PWM compared to constant current methods.
  • Document troubleshooting steps for common issues encountered during setup and operation.

Audience: Beginners in embedded systems; Level: Basic

Architecture/flow: Raspberry Pi (host) communicates with Pico-ICE (RP2040) via USB-serial to control LED brightness using PWM.

Practical Case: PWM LED Brightness Control (“control-brillo-led-pwm”) on Pico-ICE (Lattice iCE40UP5K)

This hands‑on project uses the Raspberry Pi device family with the exact model Pico‑ICE (Lattice iCE40UP5K). The Pico‑ICE board includes an RP2040 microcontroller in a Raspberry Pi Pico–compatible form factor plus an on‑board Lattice iCE40UP5K FPGA. For this Basic‑level exercise, we’ll drive an LED’s brightness using PWM on the RP2040 and control it interactively from a Raspberry Pi host running Raspberry Pi OS Bookworm 64‑bit and Python 3.11.

You will:

  • Flash MicroPython to the Pico‑ICE (RP2040).
  • Wire an external LED with a resistor to a single GPIO pin.
  • Run MicroPython code that exposes a simple USB‑serial interface for brightness commands (0–100%).
  • Validate operation from the Raspberry Pi host using Python and terminal tools.
  • Learn how to troubleshoot common issues and extend the design.

Note: In this Basic project we do not program the Lattice iCE40UP5K FPGA; we focus on RP2040 PWM. The FPGA remains unused here and can be explored in the Improvements section later.


Prerequisites

  • A Raspberry Pi host computer (e.g., Raspberry Pi 4B/400/5) with:
  • Raspberry Pi OS Bookworm 64‑bit installed
  • Python 3.11
  • Internet access
  • A free USB‑A port (or a USB‑C hub on Pi 5)

  • Comfort with the Linux command line on the Pi.

  • Basic breadboarding skills (placing an LED and resistor, making GND and GPIO connections).

  • You must be able to connect to your Raspberry Pi:

  • Locally with keyboard/monitor, or
  • Remotely via SSH.

Verify OS and Python versions

Run these commands on your Raspberry Pi host:

uname -a
cat /etc/os-release
python3 --version

You should see Raspberry Pi OS Bookworm and Python 3.11.x.

Example (versions may differ slightly):
– PRETTY_NAME=»Debian GNU/Linux 12 (bookworm)»
– Python 3.11.2


Materials (Exact Model Specified)

  • 1× Pico‑ICE (Lattice iCE40UP5K) board (RP2040 + iCE40UP5K, USB‑C)
  • 1× USB‑C data cable (to connect Pico‑ICE to Raspberry Pi)
  • 1× 5 mm LED (any color)
  • 1× 330 Ω resistor (¼ W)
  • 1× Breadboard (mini is fine)
  • 2–3× Male‑to‑male jumper wires

Optional for debugging:
– USB power meter or a known good USB‑C data cable
– Multimeter


Setup/Connection

We’ll use a single RP2040 GPIO pin to drive an LED with PWM. The Pico‑ICE follows the Raspberry Pi Pico header labels; we will use the pin labelled GP15 and any GND pin.

Important notes:
– Always place the resistor in series with the LED to limit current.
– LED polarity matters: the longer lead is the anode (+); the flat side/shorter lead is the cathode (−) to GND.

Wiring Plan (text‑only, no drawings)

  • Connect Pico‑ICE GP15 to one leg of the 330 Ω resistor.
  • Connect the other leg of the resistor to the LED anode (+).
  • Connect the LED cathode (−) to a GND pin on the Pico‑ICE.

Connection Table

Function Pico‑ICE (RP2040) label Breadboard/Part Notes
PWM output GP15 Resistor (330 Ω) GP15 → resistor → LED anode
LED anode (+) LED Connect to resistor’s free end
LED cathode (−) GND LED LED cathode → any GND on Pico‑ICE
USB data/power USB‑C USB‑C cable to Raspberry Pi host

If you prefer another GPIO, you can change it later in the code, but keep to a pin that supports PWM (most RP2040 GPIOs do).


Enabling Interfaces on Raspberry Pi OS (Bookworm)

This project primarily uses the USB CDC serial interface that MicroPython exposes; it does not require the Pi’s 40‑pin GPIO. However, we’ll show how to enable useful interfaces and SSH for general development.

Using raspi-config

sudo raspi-config
  • Interface Options:
  • I2C: Enable (optional, not used here)
  • SPI: Enable (optional, not used here)
  • Serial Port: Enable serial interface but disable login shell over serial (this avoids conflicts on UART; USB CDC is separate, but it’s a good practice)
  • SSH: Enable (if you intend to work remotely)

Finish and reboot when prompted.

Alternative: Editing /boot/firmware/config.txt

If you prefer manual configuration, you can add or confirm the following lines (optional features):

sudo nano /boot/firmware/config.txt

Append (only if needed; these are optional and safe):

dtparam=i2c_arm=on
dtparam=spi=on
enable_uart=1

Save and reboot:

sudo reboot

Python Environment and Tools on the Raspberry Pi Host

We will:
– Install system tools with apt (picotool, minicom, gpiozero, spidev).
– Create a Python 3.11 virtual environment.
– Install packages with pip (pyserial, mpremote, smbus2).

Run:

sudo apt update
sudo apt install -y python3-venv python3-pip python3-gpiozero python3-spidev picotool minicom curl wget unzip

Create and activate a venv for project scripts:

python3 -m venv ~/picoice-pwm-venv
source ~/picoice-pwm-venv/bin/activate
pip install --upgrade pip
pip install pyserial mpremote smbus2

Note:
– gpiozero and spidev are installed via apt; we won’t use them directly in this project but they are commonly used in Raspberry Pi tutorials.
– smbus2 is installed via pip because it may not be available as a Debian package under the same name. It’s not required for this project but can be useful later.


Flash MicroPython onto Pico‑ICE (RP2040)

We’ll install the official MicroPython UF2 for the RP2040 (Raspberry Pi Pico). The Pico‑ICE is Pico‑compatible on the microcontroller side, so use the “rp2-pico” build.

1) Download a known stable MicroPython UF2:

mkdir -p ~/picoice-fw && cd ~/picoice-fw
wget https://micropython.org/resources/firmware/rp2-pico-20240224-v1.22.2.uf2 -O micropython-pico-v1.22.2.uf2

2) Put Pico‑ICE into BOOTSEL mode:
– Unplug the USB‑C cable from Pico‑ICE.
– Press and hold the BOOTSEL button on the board.
– While holding BOOTSEL, plug the USB‑C into the Raspberry Pi.
– Release BOOTSEL after the storage device appears on the Pi (mounted as RPI-RP2).

3) Copy the UF2:

Identify the mount path (usually /media/$USER/RPI-RP2):

ls /media/$USER
ls /media/$USER/RPI-RP2

Copy the firmware:

cp ~/picoice-fw/micropython-pico-v1.22.2.uf2 /media/$USER/RPI-RP2/
sync

The board will automatically reboot into MicroPython; the mass‑storage device will disappear and a USB CDC serial device will appear (typically /dev/ttyACM0).

4) Verify with picotool (optional but recommended):

Unplug and replug the Pico‑ICE normally (no BOOTSEL). Then:

picotool info -a

If picotool can’t read while MicroPython is running, re‑enter BOOTSEL mode temporarily and run picotool; otherwise proceed.


Full Code

We will use two small programs:

1) main.py (MicroPython on Pico‑ICE): Sets up PWM on GP15 and listens for brightness commands (0–100) over USB serial. It prints status messages so you can validate from the host.
2) host_send_brightness.py (Python 3 on Raspberry Pi): Sends a sequence of brightness values over /dev/ttyACM0 to demonstrate control.

1) MicroPython firmware (main.py) for Pico‑ICE

Save this as main.py on your Raspberry Pi host (we’ll transfer it to the Pico‑ICE in the next section):

# main.py (MicroPython for RP2040 on Pico-ICE)
# Objective: control-brillo-led-pwm (PWM LED brightness control)
#
# - Uses GP15 as PWM output to drive LED brightness with a 330 ohm resistor to LED anode, LED cathode to GND.
# - Listens for integers 0..100 over USB serial (CDC) and adjusts brightness percentage.
# - If no command is received for a while, it keeps the last brightness.
#
# Tested with MicroPython v1.22.2 (RP2 Pico build).

from machine import Pin, PWM
import sys
import time

PWM_PIN = 15       # GP15
PWM_FREQ = 1000    # 1 kHz is flicker-free for most use cases
MAX_DUTY = 65535

pwm = PWM(Pin(PWM_PIN))
pwm.freq(PWM_FREQ)

def set_brightness(percent: int):
    # clamp to 0..100
    if percent < 0:
        percent = 0
    if percent > 100:
        percent = 100
    duty = int(percent * MAX_DUTY / 100)
    pwm.duty_u16(duty)
    return percent

# Start with low brightness (10%)
current = set_brightness(10)
print("READY: PWM on GP%d at %d Hz. Type 0..100 + Enter to set brightness." % (PWM_PIN, PWM_FREQ))
print("Current brightness: %d%%" % current)
print("Example: 0, 25, 50, 75, 100")

buffer = b""
last_print = time.ticks_ms()

while True:
    # Non-blocking read from stdin; we read one byte at a time
    if sys.stdin in (None,):
        # USB not ready; small delay and continue
        time.sleep(0.05)
        continue

    # Try to read available bytes
    try:
        b = sys.stdin.buffer.read(1)
    except Exception:
        b = None

    if b:
        if b in (b'\r', b'\n'):
            line = buffer.strip().decode('utf-8', errors='ignore')
            buffer = b""
            if line:
                try:
                    val = int(line)
                    current = set_brightness(val)
                    print("OK: brightness set to %d%%" % current)
                except ValueError:
                    print("ERR: invalid input '%s'. Send integer 0..100." % line)
        else:
            buffer += b

    # Periodic heartbeat message every 10 seconds so host can see we are alive
    now = time.ticks_ms()
    if time.ticks_diff(now, last_print) > 10000:
        print("STATUS: brightness=%d%%" % current)
        last_print = now

    time.sleep(0.01)

Key features:
– PWM at 1 kHz on GP15.
– Accepts human‑readable integers (0–100) via USB‑serial.
– Periodic status messages every 10 seconds.

2) Host script (host_send_brightness.py) for Raspberry Pi

This script opens the serial port exposed by MicroPython and sends a deterministic brightness sweep. Save this next to your main.py on the Pi host:

# host_send_brightness.py
# Send brightness percentages to Pico-ICE MicroPython over USB CDC (/dev/ttyACM0)
#
# Requires: pip install pyserial

import sys
import time
import serial

PORT = "/dev/ttyACM0"  # adjust if different
BAUD = 115200          # MicroPython USB CDC ignores baud but pyserial needs a value
TIMEOUT = 1.5

def open_port():
    return serial.Serial(PORT, BAUD, timeout=TIMEOUT)

def read_available(ser):
    # Non-blocking read of any available text
    try:
        text = ser.read(ser.in_waiting or 1).decode(errors='ignore')
        if text:
            sys.stdout.write(text)
            sys.stdout.flush()
    except Exception:
        pass

def send_value(ser, val):
    line = f"{val}\n".encode()
    ser.write(line)
    ser.flush()
    time.sleep(0.2)
    read_available(ser)

def main():
    print(f"Opening serial port {PORT} ...")
    with open_port() as ser:
        # Give device a moment to (re)enumerate
        time.sleep(1.0)
        # Flush any previous prints from the device
        read_available(ser)

        # Sweep: 0, 25, 50, 75, 100, 75, 50, 25, 0
        pattern = [0, 25, 50, 75, 100, 75, 50, 25, 0]
        for val in pattern:
            print(f"\n>>> Sending {val}%")
            send_value(ser, val)
            time.sleep(0.8)

        print("\nDone. Reading a few more messages...")
        for _ in range(10):
            read_available(ser)
            time.sleep(0.25)

if __name__ == "__main__":
    main()

Build/Flash/Run Commands

We will use mpremote to copy main.py to the MicroPython filesystem on the Pico‑ICE, then run the host script.

1) Confirm the Pico‑ICE USB device:

Plug the Pico‑ICE into the Raspberry Pi (normal boot, not BOOTSEL). Then:

dmesg | tail -n 20
ls -l /dev/ttyACM*

You should see something like /dev/ttyACM0.

2) List devices with mpremote:

source ~/picoice-pwm-venv/bin/activate
mpremote connect list

Expect a line such as:
– /dev/ttyACM0

3) Copy main.py to the board and reset:

Assuming your main.py is in ~/picoice-fw or current directory:

cd ~/picoice-fw
mpremote connect /dev/ttyACM0 fs cp main.py :main.py
mpremote connect /dev/ttyACM0 reset

After reset, the MicroPython script will auto‑run if it’s named main.py at the root of the device filesystem. The device should start printing “READY…” and “STATUS…” over USB‑serial.

4) Observe output using minicom (optional):

sudo usermod -aG dialout $USER
newgrp dialout
minicom -b 115200 -o -D /dev/ttyACM0

You should see the “READY” banner. Exit with Ctrl‑A, then X, then Yes.

5) Run the host script to send brightness commands:

cd ~/picoice-fw
python3 host_send_brightness.py

Watch the LED change in perceptible steps as the script prints device responses (OK: brightness set to …). If you’re connected via minicom at the same time, close it first so only one program accesses the serial device.


Step‑by‑Step Validation

Follow this sequence to validate both the electrical and software paths:

1) Physical wiring check:
– Confirm GP15 is wired to the resistor, resistor to LED anode, LED cathode to GND.
– Confirm the resistor is in series, not parallel.
– Confirm LED orientation (long lead/anode toward GPIO via resistor, short lead/cathode to GND).

2) Power and enumeration:
– Connect Pico‑ICE via USB‑C to the Raspberry Pi.
– Run: ls -l /dev/ttyACM* and confirm a port exists (e.g., /dev/ttyACM0).
– If missing, reseat the cable, try a different port, or try a different cable known to be a data cable.

3) Firmware presence:
– If you see repeated resets or no serial, re‑flash MicroPython UF2 (BOOTSEL method) and try again.
– Optionally validate with picotool info -a while in BOOTSEL.

4) MicroPython program deployment:
mpremote connect /dev/ttyACM0 fs cp main.py :main.py
mpremote connect /dev/ttyACM0 reset
– The LED should light dimly at startup (10% default). If it’s dark, try 100% via the host script; if always dark, reverse LED orientation.

5) Serial output:
– Use minicom -b 115200 -o -D /dev/ttyACM0 to confirm the device prints:
– READY: PWM on GP15 at 1000 Hz…
– Current brightness: 10%
– STATUS: brightness=10% every ~10 seconds
– Exit minicom before running the Python host script.

6) Host script test:
python3 host_send_brightness.py
– Observe LED brightness changes in discrete steps: 0%, 25%, 50%, 75%, 100%, then back down.
– The terminal should show “OK: brightness set to …” messages echoed from the microcontroller.

7) Manual control:
– Use mpremote to interactively send values:
mpremote connect /dev/ttyACM0 repl
– Press Ctrl‑C to interrupt the script, then type:
import sys
sys.stdin won’t be used here since you’re in REPL; instead, exit REPL (Ctrl‑X) and use minicom/host script to send plain integers.

Alternatively, echo a value directly from Linux:
printf "75
" | sudo tee /dev/ttyACM0

– LED should jump to ~75% brightness.

8) Edge cases:
– Send invalid input (e.g., “hello”) via minicom:
– Expect ERR: invalid input 'hello'. Send integer 0..100.
– Send out‑of‑range (e.g., 150):
– Expect it to clamp to 100% and print OK: brightness set to 100%.

If all the above work, the “control‑brillo‑led‑pwm” objective is met.


Troubleshooting

  • No /dev/ttyACM0 appears:
  • Try a different USB‑C cable (some are power‑only).
  • Try a different USB port on the Raspberry Pi.
  • Check dmesg | tail -n 50 for USB enumeration errors.
  • Reflash MicroPython via BOOTSEL.
  • Ensure user is in dialout group: sudo usermod -aG dialout $USER then newgrp dialout.

  • minicom cannot open the port:

  • Ensure no other process is using /dev/ttyACM0 (only one program can open it).
  • Close Thonny, mpremote, or any other serial session.

  • LED never lights:

  • Reverse LED orientation; double‑check the resistor is in series.
  • Verify you used the correct GPIO label (GP15) and not a different pad.
  • From the MicroPython REPL (Ctrl‑C), try a quick test:
    from machine import Pin, PWM
    p=PWM(Pin(15)); p.freq(1000); p.duty_u16(65535) # full brightness

    If it lights, your hardware is fine; the issue may be with the program not running or serial not sending values.

  • LED is always on full brightness or flickers:

  • Check for shorts on the breadboard or misplacement of resistor.
  • Lower or raise PWM frequency if you observe perceptible flicker:

    • In main.py, change PWM_FREQ to 500 or 2000 and re‑copy.
  • mpremote copy fails:

  • Confirm the port with mpremote connect list.
  • Unplug/replug the device.
  • Try mpremote connect /dev/ttyACM0 mount . to run the local file temporarily:
    mpremote connect /dev/ttyACM0 mount .
    mpremote connect /dev/ttyACM0 run main.py

  • Serial input not recognized:

  • Ensure you send a newline ‘
    ’ after the integer (minicom adds it when you press Enter).
  • Use the host script which formats the commands correctly.

  • Performance and power:

  • The LED current should be below 10–12 mA. With 330 Ω and a typical red LED at ~2 V drop, current is about (3.3 V − 2.0 V) / 330 ≈ 3.9 mA, safe for the RP2040 pin.

Improvements

  • Smooth “breathing” effect:
  • Modify main.py to implement a non‑blocking fade when no commands are received. For example, increment duty every 10 ms modulo MAX_DUTY and accept interactive overrides.

  • Multiple LEDs on different PWM slices:

  • Drive several GPIO pins (e.g., GP14, GP15, GP16) with independent PWM objects to mix colors from separate LEDs.

  • Read a potentiometer to control brightness:

  • Connect a 10 kΩ potentiometer to an ADC input (e.g., GP26/ADC0) and map the analog value to PWM duty. Use MicroPython’s machine.ADC.

  • Tie‑in the FPGA (iCE40UP5K):

  • Use the RP2040 to stream duty cycle values to the FPGA and implement PWM generation in programmable logic for deterministic timing or multi‑channel control.
  • Program the FPGA via open‑source toolchains (nextpnr‑ice40, yosys) once comfortable. For Basic level, treat this as a future exercise.

  • Host UI:

  • Build a simple Tkinter or web UI (Flask) on the Raspberry Pi to send brightness commands. The host could provide sliders and store presets.

  • Logging and metrics:

  • Parse the “STATUS” lines on the host to record usage over time, or extend the protocol to include frequency and duty queries.

  • Safety and robustness:

  • Add bounds checking, watchdog timers, and a defined reset brightness.
  • Implement a CRC or checksum if you expand to structured multi‑byte commands.

Final Checklist

  • Raspberry Pi host:
  • Raspberry Pi OS Bookworm 64‑bit verified.
  • Python 3.11 available.
  • Interfaces enabled as needed (SSH, Serial disabled login shell).
  • Virtual environment created: ~/picoice-pwm-venv.
  • Packages installed:

    • apt: python3-venv, python3-pip, python3-gpiozero, python3-spidev, picotool, minicom
    • pip (inside venv): pyserial, mpremote, smbus2
  • Pico‑ICE (Lattice iCE40UP5K):

  • MicroPython flashed via UF2 (v1.22.2 used in this guide).
  • Appears as /dev/ttyACM0 under normal boot.
  • main.py deployed to the device via mpremote.

  • Hardware connections:

  • GP15 → 330 Ω → LED anode; LED cathode → GND.
  • USB‑C cable is known good (data‑capable).

  • Validation:

  • READY and STATUS messages visible via minicom or host script.
  • host_send_brightness.py cycles brightness 0–100% correctly.
  • Manual values entered in minicom change brightness immediately.

  • Troubleshooting performed if needed:

  • Checked cable, port, permissions, LED orientation, resistor, and MicroPython state.
  • Verified no serial conflicts.

With these steps, you have a working “control‑brillo‑led‑pwm” implementation on the Raspberry Pi family using the exact model Pico‑ICE (Lattice iCE40UP5K), centered on RP2040 PWM control with clear code, connections, and validation workflow.

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 microcontroller is included in the Pico-ICE board?




Question 2: Which programming language is used in this project?




Question 3: What percentage range can the LED brightness be controlled?




Question 4: What is the primary function of the Lattice iCE40UP5K in this project?




Question 5: Which Raspberry Pi OS version is required for this project?




Question 6: What type of connection is needed to control the Raspberry Pi remotely?




Question 7: What is a prerequisite skill for this project?




Question 8: Which command is used to check the Python version on the Raspberry Pi?




Question 9: What hardware component is used to wire the LED?




Question 10: What is the main focus of this project?




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

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

Follow me:
error: Contenido Protegido / Content is protected !!
Scroll to Top