BPI-PicoW-S3 PWM dimming, breathing light [CircuitPython]


BPI-Pico-S3 has the same dimension as the Raspberry Pi Pico board, equipped with an ESP32S3 chip, an 8MB flash, 4-layer PCB, electroplated half-hole process, ceramic antenna, supports 2.4 GHz Wi-Fi and Bluetooth® LE dual-mode wireless communication, it’s a development board perfect for IoT development and Maker DIY Projects.

TinyUF2 + CircuitPython is built-in at the factory.

It is recommended to use Mu Editor to get started with CircuitPython development.


Hardware interface


PWM output, control LED brightness

  1. The brightness of the LED light can be controlled by controlling the PWM duty cycle. The control duty cycle is from 0% to 100%, using 16-bit precision, 0 to 65535 in decimal and 0 to FFFF in hexadecimal. Enter the following code in the REPL:
import board
import pwmio
ledpin = pwmio.PWMOut(board.LED, frequency=25000, duty_cycle=0)
ledpin.duty_cycle = 32768  # mid-point 0-65535 = 50 % duty-cycle
  1. Just enter the last line of code again in the REPL to change the PWM duty cycle to bring the LED to maximum brightness:
ledpin.duty_cycle = 65535
  1. Use while and for loops to make breathing lights:
import board
import pwmio
import time

ledpin = pwmio.PWMOut(board.LED, frequency=25000, duty_cycle=0)

while True:
    for i in range(0, 65535, 1):
        ledpin.duty_cycle = i
    for i in range(65535, 0, -1):
        ledpin.duty_cycle = i

BPI-PicoW-S3 + CircuitPython Tutorial Aggregation Link


However, the led brightness perceived is not linear to the duty cycle. To make this work even more smoothly, use a table (or formula) to follow a curve. It is defined in CIE 1931.

You can create a static table in C at compile time by the following code. I used it on an attiny88. It is a byte array table, but it can easily be adopted to an int array table. (Or you could even use the same formula to calculate real time on the Leaf/PicoW.)

// CIE 1931 definitions, for linear brightness correction
#define CIE_ARRAY_SIZE 101
#define CIE_RANGE 255
#define CIE(X) ( ( X<(8.0*(CIE_ARRAY_SIZE-1)/100) )? \
   0.5+  X*100.0*CIE_RANGE/((CIE_ARRAY_SIZE-1)*902.3) : \
   0.5+ ((X*100.0/(CIE_ARRAY_SIZE-1)+16.0)/116.0) * \
        ((X*100.0/(CIE_ARRAY_SIZE-1)+16.0)/116.0) * \
        ((X*100.0/(CIE_ARRAY_SIZE-1)+16.0)/116.0) * CIE_RANGE \
static const byte cie_table[CIE_ARRAY_SIZE] = { 0,
  CIE( 1),CIE( 2),CIE( 3),CIE( 4),CIE( 5),CIE( 6),CIE( 7),CIE( 8),CIE( 9),CIE(10),

Oh what a surprise! Your knowledge of optics amazes me.

By the way, the small case I gave above just shows how to control the MCU output PWM in circuitpython with the simplest code.

The application of CIE 1931 you mentioned, can you show it with python code?

I have the same visual experience, the led brightness perceived is not linear to the duty cycle.

Looking forward to your show.


For python I found it here, it is the same formula.

gpshead/pwm_lightness (github.com)

After application, the change of brightness can be felt very smoothly from the subjective vision.


# Copyright 2020 Gregory P. Smith
# https://github.com/gpshead/pwm_lightness
# Licensed under the Apache License, Version 2.0
    from typing import Sequence
except ImportError:

_pwm_tables = {}  # Our cache.

def get_pwm_table(max_output: int,
                  max_input: int = 255) -> 'Sequence[int]':
    """Returns a table mapping 0..max_input to int PWM values.
    Computed upon the first call with given value, cached thereafter.
    assert max_output > 0
    assert max_input > 0
    table = _pwm_tables.get((max_output, max_input))
    if table:
        return table
    value_gen = (round(_cie1931(l_star/max_input) * max_output)
                 for l_star in range(max_input+1))
    table = bytes(value_gen) if max_output <= 255 else tuple(value_gen)
    _pwm_tables[(max_output, max_input)] = table
    return table

def clear_table_cache():
    """Empties the cache of get_pwm_tables() return values."""

# CIE 1931 Lightness curve calculation.
# derived from https://jared.geek.nz/2013/feb/linear-led-pwm @ 2020-06
# License: MIT
# additional reference
# https://www.photonstophotos.net/GeneralTopics/Exposure/Psychometric_Lightness_and_Gamma.htm
def _cie1931(l_star: float) -> float:
    l_star *= 100
    if l_star <= 8:
        return l_star/903.3  # Anything suggesting 902.3 has a typo.
    return ((l_star+16)/116)**3


import time, board, pwmio, pwm_lightness
PWM = pwm_lightness.get_pwm_table(0xffff, max_input=255)
output_pin = pwmio.PWMOut(board.LED)
while True:
    for v in range(255, -1, -1):
        output_pin.duty_cycle = PWM[v]
    for v in range(1, 255):
        output_pin.duty_cycle = PWM[v]

Thank you for sharing :wink: