Cheap NeoPixels at the Dollar Store

Exhibit A:

box of "Monster BASICS" Sound reactive RGB+IC Color Flow LED strip

also known as “Monster BASICS Sound reactive RGB+IC Color Flow LED strip”. It’s $5 or so at Dollarama, and includes a USB cable for power and a remote control. It’s two metres long and includes 60 RGB LEDs. Are these really super-cheap NeoPixel clones?

I’m going to keep the USB power so I can power it from a power bank, but otherwise convert it to a string of smart LEDs. We lose the remote control capability.

Pull back the heatshrink at the USB end:

led strip with shrink tubing pulled back to show the +5 V, Din and GND solder tterminals

… and there are our connectors. We want to disconnect the blue Din (Data In) line from the built in controller, and solder new wires to Din and GND to run from a microcontroller board.

led strip with additional wires soldered to Din and GND contacts

Maybe not the best solder job, but there are new wires feeding through the heatshrink and soldered onto the strip.

led strip with two additional wires soldered in and heatshrink pushed back, all held in place by a cable tie

Here’s the heatshrink pushed back, and everything secured with a cable tie.

Now to feed it from standard MicroPython NeoPixel code, suitably jazzed up for 60 pixels.

a glowing multicolour reel of of LEDs

A pretty decent result for $5!

Crickets in February

It’s mid-February in Toronto: -10 °C and snowy. The memory of chirping summer fields is dim. But in my heart there is always a cricket-loud meadow.

Short of moving somewhere warmer, I’m going to have to make my own midwinter crickets. I have micro-controllers and tiny speakers: how hard can this be?

more fun than a bucket of simulated crickets
(video description: a plastic box containing three USB power banks, each with USB cable leading to a Raspberry Pi Pico board. Each board has a small electromagnetic speaker attached between ground and a data pin)

I could have merely made these beep away at a fixed rate, but I know that real crickets tend to chirp faster as the day grows warmer. This relationship is frequently referred to as Dolbear’s law. The American inventor Amos Dolbear published his observation (without data or species identification) in The American Naturalist in 1897: The Cricket as a Thermometer

journal text:

The rate of chirp seems to be entirely determined by the temperature and this to such a degree that one may easily compute the temperature when the number of chirps per minute is known.

Thus at 60° F. the rate is 80 per minute.

At 70° F. the rate is 120 a minute, a change of four chirps a minute for each change of one degree. Below a temperature
of 50° the cricket has no energy to waste in music and there would be but 40 chirps per minute.
One may express this relation between temperature and chirp rate thus.
Let T. stand for temperature and N,  the rate per minute.

(typeset equation)
T. = 50 + (N - 40) / 4
pretty bold assertions there without data eh, Amos old son …?

When emulating crickets I’m less interested in the rate of chirps per minute, but rather in the period between chirps. I could also care entirely less about barbarian units, so I reformulated it in °C (t) and milliseconds (p):

t = ⅑ × (40 + 75000 ÷ p)

Since I know that the micro-controller has an internal temperature sensor, I’m particularly interested in the inverse relationship:

p = 15000 ÷ (9 * t ÷ 5 – 8)

I can check this against one of Dolbear’s observations for 70°F (= 21⅑ °C, or 190/9) and 120 chirps / minute (= 2 Hz, or a period of 500 ms):

p = 15000 ÷ (9 * t ÷ 5 – 8)
   = 15000 ÷ (9 * (190 ÷ 9) ÷ 5 – 8)
   = 15000 ÷ (190 ÷ 5 – 8)
   = 15000 ÷ 30
   = 500

Now I’ve got the timing worked out, how about the chirp sound. From a couple of recordings of cricket meadows I’ve made over the years, I observed:

  1. The total duration of a chirp is about ⅛ s
  2. A chirp is made up of four distinct events:
    • a quieter short tone;
    • a longer louder tone of a fractionally higher pitch;
    • the same longer louder tone repeated;
    • the first short tone repeated
  3. There is a very short silence between each tone
  4. Each cricket appears to chirp at roughly the same pitch: some slightly lower, some slightly higher
  5. The pitch of the tones is in the range 4500–5000 Hz: around D8 on the music scale

I didn’t attempt to model the actual stridulating mechanism of a particular species of cricket. I made what sounded sort of right to me. Hey, if Amos Dolbear could make stuff up and get it accepted as a “law”, I can at least get away with pulse width modulation and tiny tinny speakers …

This is the profile I came up with:

  • 21 ms of 4568 Hz at 25% duty cycle
  • 7 ms of silence
  • 28 ms of 4824 Hz at 50% duty cycle
  • 7 ms of silence
  • 28 ms of 4824 Hz at 50% duty cycle
  • 7 ms of silence
  • 21 ms of 4568 Hz at 25% duty cycle
  • 7 ms of silence

That’s a total of 126 ms, or ⅛ish seconds. In the code I made each instance play at a randomly-selected relative pitch of ±200 Hz on the above numbers.

For the speaker, I have a bunch of cheap PC motherboard beepers. They have a Dupont header that spans four pins on a Raspberry Pi Pico header, so if you put one on the ground pin at pin 23, the output will be connected to pin 26, aka GPIO 20:

Raspberry Pi Pico with small piezo speaker connected to pins 23 (ground) and 26 (GPIO 20)
from a post where I did a very, very bad thing: Nyan Cat, except it gets faster — RTTTL on the Raspberry Pi Pico

So — finally — here’s the MicroPython code:

# cricket thermometer simulator - scruss, 2024-02
# uses a buzzer on GPIO 20 to make cricket(ish) noises
# MicroPython - for Raspberry Pi Pico
# -*- coding: utf-8 -*-

from machine import Pin, PWM, ADC, freq
from time import sleep_ms, ticks_ms, ticks_diff
from random import seed, randrange

freq(125000000)  # use default CPU freq
seed()  # start with a truly random seed
pwm_out = PWM(Pin(20), freq=10, duty_u16=0)  # can't do freq=0
led = Pin("LED", Pin.OUT)
sensor_temp = machine.ADC(4)  # adc channel for internal temperature
TOO_COLD = 10.0  # crickets don't chirp below 10 °C (allegedly)
temps = []  # for smoothing out temperature sensor noise
personal_freq_delta = randrange(400) - 199  # different pitch every time
chirp_data = [
    # cadence, duty_u16, freq
    # there is a cadence=1 silence after each of these
    [3, 16384, 4568 + personal_freq_delta],
    [4, 32768, 4824 + personal_freq_delta],
    [4, 32768, 4824 + personal_freq_delta],
    [3, 16384, 4568 + personal_freq_delta],
]
cadence_ms = 7  # length multiplier for playback


def chirp_period_ms(t_c):
    # for a given temperature t_c (in °C), returns the
    # estimated cricket chirp period in milliseconds.
    #
    # Based on
    # Dolbear, Amos (1897). "The cricket as a thermometer".
    #   The American Naturalist. 31 (371): 970–971. doi:10.1086/276739
    #
    # The inverse function is:
    #     t_c = (75000 / chirp_period_ms + 40) / 9
    return int(15000 / (9 * t_c / 5 - 8))


def internal_temperature(temp_adc):
    # see pico-micropython-examples / adc / temperature.py
    return (
        27
        - ((temp_adc.read_u16() * (3.3 / (65535))) - 0.706) / 0.001721
    )


def chirp(pwm_channel):
    for peep in chirp_data:
        pwm_channel.freq(peep[2])
        pwm_channel.duty_u16(peep[1])
        sleep_ms(cadence_ms * peep[0])
        # short silence
        pwm_channel.duty_u16(0)
        pwm_channel.freq(10)
        sleep_ms(cadence_ms)


led.value(0)  # led off at start; blinks if chirping
### Start: pause a random amount (less than 2 s) before starting
sleep_ms(randrange(2000))

while True:
    loop_start_ms = ticks_ms()
    sleep_ms(5)  # tiny delay to stop the main loop from thrashing
    temps.append(internal_temperature(sensor_temp))
    if len(temps) > 5:
        temps = temps[1:]
    avg_temp = sum(temps) / len(temps)
    if avg_temp >= TOO_COLD:
        led.value(1)
        loop_period_ms = chirp_period_ms(avg_temp)
        chirp(pwm_out)
        led.value(0)
        loop_elapsed_ms = ticks_diff(ticks_ms(), loop_start_ms)
        sleep_ms(loop_period_ms - loop_elapsed_ms)

There are a few more details in the code that I haven’t covered here:

  1. The program pauses for a short random time on starting. This is to ensure that if you power up a bunch of these at the same time, they don’t start exactly synchronized
  2. The Raspberry Pi Pico’s temperature sensor can be slightly noisy, so the chirping frequency is based on the average of (up to) the last five readings
  3. There’s no chirping below 10 °C, because Amos Dolbear said so
  4. The built-in LED also flashes if the board is chirping. It doesn’t mimic the speaker’s PWM cadence, though.

Before I show you the next video, I need to say: no real crickets were harmed in the making of this post. I took the bucket outside (roughly -5 °C) and the “crickets” stopped chirping as they cooled down. Don’t worry, they started back up chirping again when I took them inside.

“If You’re Cold They’re Cold, Bring Them Inside”
(video description: a plastic box containing three USB power banks, each with USB cable leading to a Raspberry Pi Pico board. Each board has a small electromagnetic speaker attached between ground and a data pin)

SYN6288 TTS board from AliExpress

After remarkable success with the SYN-6988 TTS module, then somewhat less success with the SYN-6658 and other modules, I didn’t hold out much hope for the YuTone SYN-6288, which – while boasting a load of background tunes that could play over speech – can only convert Chinese text to speech

small blue circuit board with 6 MHz crystal oscillator, main chip, input headers at bottom and headphone jack/speaker output at top
as bought from quason official store: SYN6288 speech synthesis module

The wiring is similar to the SYN-6988: a serial UART connection at 9600 baud, plus a Busy (BY) line to signal when the chip is busy. The serial protocol is slightly more complicated, as the SYN-6288 requires a checksum byte at the end.

As I’m not interested in the text-to-speech output itself, here’s a MicroPython script to play all of the sounds:

# very crude MicroPython demo of SYN6288 TTS chip
# scruss, 2023-07
import machine
import time

### setup device
ser = machine.UART(
    0, baudrate=9600, bits=8, parity=None, stop=1
)  # tx=Pin(0), rx=Pin(1)

busyPin = machine.Pin(2, machine.Pin.IN, machine.Pin.PULL_UP)


def sendspeak(u2, data, busy):
    # modified from https://github.com/TPYBoard/TPYBoard_lib/
    # u2 = UART(uart, baud)
    eec = 0
    buf = [0xFD, 0x00, 0, 0x01, 0x01]
    # buf = [0xFD, 0x00, 0, 0x01, 0x79]  # plays with bg music 15
    buf[2] = len(data) + 3
    buf += list(bytearray(data, "utf-8"))
    for i in range(len(buf)):
        eec ^= int(buf[i])
    buf.append(eec)
    u2.write(bytearray(buf))
    while busy.value() != True:
        # wait for busy line to go high
        time.sleep_ms(5)
    while busy.value() == True:
        # wait for it to finish
        time.sleep_ms(5)


for s in "abcdefghijklmnopqrstuvwxy":
    playstr = "[v10][x1]sound" + s
    print(playstr)
    sendspeak(ser, playstr, busyPin)
    time.sleep(2)

for s in "abcdefgh":
    playstr = "[v10][x1]msg" + s
    print(playstr)
    sendspeak(ser, playstr, busyPin)
    time.sleep(2)

for s in "abcdefghijklmno":
    playstr = "[v10][x1]ring" + s
    print(playstr)
    sendspeak(ser, playstr, busyPin)
    time.sleep(2)

Each sound starts and stops with a very loud click, and the sound quality is not great. I couldn’t get a good recording of the sounds (some of which of which are over a minute long) as the only way I could get reliable audio output was through tiny headphones. Any recording came out hopelessly distorted:

I’m not too disappointed that this didn’t work well. I now know that the SYN-6988 is the good one to get. It also looks like I may never get to try the XFS5152CE speech synthesizer board: AliExpress has cancelled my shipment for no reason. It’s supposed to have some English TTS function, even if quite limited.

Here’s the auto-translated SYN-6288 manual, if you do end up finding a use for the thing

SYN-6988 Speech with MicroPython

Full repo, with module and instructions, here: scruss/micropython-SYN6988: MicroPython library for the VoiceTX SYN6988 text to speech module

(and for those that CircuitPython is the sort of thing they like, there’s this: scruss/circuitpython-SYN6988: CircuitPython library for the YuTone VoiceTX SYN6988 text to speech module.)

I have a bunch of other boards on order to see if the other chips (SYN6288, SYN6658, XF5152) work in the same way. I really wonder which I’ll end up receiving!

Update (2023-07-09): Got the SYN6658. It does not support English TTS and thus is not recommended. It does have some cool sounds, though.

Embedded Text Command Sound Table

The github repo references Embedded text commands, but all of the sound references were too difficult to paste into a table there. So here are all of the ones that the SYN-6988 knows about:

  • Name is the string you use to play the sound, eg: [x1]sound101
  • Alias is an alternative name by which you can call some of the sounds. This is for better compatibility with the SYN6288 apparently. So [x1]sound101 is exactly the same as specifying [x1]sounda
  • Type is the sound description from the manual. Many of these are blank
  • Link is a playable link for a recording of the sound.
NameAliasTypeLink
sound101sounda
sound102soundb
sound103soundc
sound104soundd
sound105sounde
sound106soundf
sound107soundg
sound108soundh
sound109soundi
sound110soundj
sound111soundk
sound112soundl
sound113soundm
sound114soundn
sound115soundo
sound116soundp
sound117soundq
sound118soundr
sound119soundt
sound120soundu
sound121soundv
sound122soundw
sound123soundx
sound124soundy
sound201phone ringtone
sound202phone ringtone
sound203phone ringtone
sound204phone rings
sound205phone ringtone
sound206doorbell
sound207doorbell
sound208doorbell
sound209doorbell
sound301alarm
sound302alarm
sound303alarm
sound304alarm
sound305alarm
sound306alarm
sound307alarm
sound308alarm
sound309alarm
sound310alarm
sound311alarm
sound312alarm
sound313alarm
sound314alarm
sound315alert/emergency
sound316alert/emergency
sound317alert/emergency
sound318alert/emergency
sound401credit card successful
sound402credit card successful
sound403credit card successful
sound404credit card successful
sound405credit card successful
sound406credit card successful
sound407credit card successful
sound408successfully swiped the card
SYN-6988 Sound Reference

MicroPython on the Seeed Studio Wio Terminal: it works!

A while back, Seeed Studio sent me one of their Wio Terminal devices to review. It was pretty neat, but being limited to using Arduino to access all of it features was a little limiting. I still liked it, though, and wrote about it here: SeeedStudio Wio Terminal

Small screen device showing geometric pattern
Wio Terminal, doing a thing

There wasn’t any proper MicroPython support for the device as it used a MicroChip/Atmel SAMD51 ARM® Cortex®-M4 micro-controller. But since I wrote the review, one developer (robert-hh) has worked almost entirely solo to make SAMD51 and SAMD21 support useful in mainline MicroPython.

Hey! Development is still somewhere between “not quite ready for prime time” and “beware of the leopard”. MicroPython on the SAMD51 works remarkably well for supported boards, but don’t expect this to be beginner-friendly yet.

I thought I’d revisit the Wio Terminal and see what I could do using a nightly build (downloaded from Downloads – Wio Terminal D51R – MicroPython). Turns out, most of the board works really well!

What doesn’t work yet

  • Networking/Bluetooth – this is never going to be easy, especially with Seeed Studio using a separate RTL8720 SoC. It may not be entirely impossible, as previously thought, but so far, wifi support seems quite far away
  • QSPI flash for program storagethis is not impossible, just not implemented yet this works now too, but it’s quite slow since it relies on a software SPI driver. More details: samd51: MicroPython on the Seeed Wio Terminal · Discussion #9838 · micropython
  • RTCthis is a compile-time option, but isn’t available on the stock images. Not all SAMD51 boards have a separate RTC oscillator, and deriving the RTC from the system oscillator would be quite wobbly. RTC works now! It may even be possible to provide backup battery power and have it keep time when powered off. VBAT / PB03 / SPI_SCK is broken out to the 40-pin connector.

What does work

  • Display – ILI9341 320×240 px, RGB565 via SPI
  • Accelerometer – LIS3DHTR via I²C
  • Microphone – analogue
  • Speaker – more like a buzzer, but this little PWM speaker element does allow you to play sounds
  • Light Sensor – via analogue photo diode
  • IR emitter – PWM, not tied to any hardware protocol
  • Internal LED – a rather faint blue thing, but useful for low-key signalling
  • Micro SD Card – vi SPI. Works well with MicroPython’s built-in virtual file systems
  • Switches and buttons – three buttons on the top, and a five-way mini-joystick
  • I²C via Grove Connector – a second, separate I²C channel.

I’ll go through each of these here, complete with a small working example.

Wio Terminal main board
Inside the remarkably hard-to-open Wio Terminal

LED

Let’s start with the simplest feature: the tiny blue LED hidden inside the case. You can barely see this, but it glows out around the USB C connector on the bottom of the case.

  • MicroPython interfaces: machine.Pin, machine.PWM
  • Control pin: Pin(“LED_BLUE”) or Pin(15), or Pin(“PA15”): any one of these would work.

Example: Wio-Terminal-LED.py

# MicroPython / Seeed Wio Terminal / SAMD51
# Wio-Terminal-LED.py - blink the internal blue LED
# scruss, 2022-10
# -*- coding: utf-8 -*-

from machine import Pin
from time import sleep_ms

led = Pin("LED_BLUE", Pin.OUT)  # or Pin(15) or Pin("PA15")

try:
    while True:
        led.value(not led.value())
        sleep_ms(1200)
except:
    led.value(0)  # turn it off if user quits
    exit()

IR LED

I don’t have any useful applications of the IR LED for device control, so check out Awesome MicroPython’s IR section for a library that would work for you.

  • MicroPython interfaces: machine.PWM
  • Control pin: Pin(“PB31”)

Example: Wio-Terminal-IR_LED.py

# MicroPython / Seeed Wio Terminal / SAMD51
# Wio-Terminal-IR_LED.py - blink the internal IR LED
# scruss, 2022-10
# -*- coding: utf-8 -*-

# Hey! This is a completely futile exercise, unless you're able
# to see into the IR spectrum. But we're here to show you the pin
# specification to use. For actual useful libraries to do stuff with
# IR, take a look on https://awesome-micropython.com/#ir

# So this is a boring blink, 'cos we're keeping it short here.
# You might be able to see the LED (faintly) with your phone camera

from machine import Pin, PWM
from time import sleep_ms

ir = PWM(Pin("PB31"))  # "IR_CTL" not currently defined

try:
    while True:
        ir.duty_u16(32767)  # 50% duty
        ir.freq(38000)  # fast flicker
        sleep_ms(1200)
        ir.duty_u16(0)  # off
        sleep_ms(1200)
except:
    ir.duty_u16(0)  # turn it off if user quits
    exit()

Buttons

There are three buttons on top, plus a 5-way joystick on the front. Their logic is inverted, so they read 0 when pressed, 1 when not. It’s probably best to use machine.Signal with these to make operation more, well, logical.

  • MicroPython interface: machine.Signal (or machine.Pin)
  • Control pins: Pin(“BUTTON_3”) or Pin(92) or Pin(PC28) – top left; Pin(“BUTTON_2”) or Pin(91) or Pin(PC27) – top middle; Pin(“BUTTON_1”) or Pin(90) or Pin(PC26) – top right; Pin(“SWITCH_B”) or Pin(108) or Pin(PD12) – joystick left; Pin(“SWITCH_Y”) or Pin(105) or Pin(PD09) – joystick right; Pin(“SWITCH_U”) or Pin(116) or Pin(PD20) – joystick up; Pin(“SWITCH_X”) or Pin(104) or Pin(PD08) – joystick down; Pin(“SWITCH_Z”) or Pin(106) or Pin(PD10) – joystick button

Example: Wio-Terminal-Buttons.py

# MicroPython / Seeed Wio Terminal / SAMD51
# Wio-Terminal-Buttons.py - test the buttons
# scruss, 2022-10
# -*- coding: utf-8 -*-

# using Signal because button sense is inverted: 1 = off, 0 = on
from machine import Pin, Signal
from time import sleep_ms

pin_names = [
    "BUTTON_3",  # Pin(92)  or Pin(PC28) - top left
    "BUTTON_2",  # Pin(91)  or Pin(PC27) - top middle
    "BUTTON_1",  # Pin(90)  or Pin(PC26) - top right
    "SWITCH_B",  # Pin(108) or Pin(PD12) - joystick left
    "SWITCH_Y",  # Pin(105) or Pin(PD09) - joystick right
    "SWITCH_U",  # Pin(116) or Pin(PD20) - joystick up
    "SWITCH_X",  # Pin(104) or Pin(PD08) - joystick down
    "SWITCH_Z",  # Pin(106) or Pin(PD10) - joystick button
]

pins = [None] * len(pin_names)
for i, name in enumerate(pin_names):
    pins[i] = Signal(Pin(name, Pin.IN), invert=True)

while True:
    for i in range(len(pin_names)):
        print(pins[i].value(), end="")
    print()
    sleep_ms(100)

Buzzer

A very quiet little PWM speaker.

  • MicroPython interfaces: machine.PWM
  • Control pin: Pin(“BUZZER”) or Pin(107) or Pin(“PD11”)

Example: Wio-Terminal-Buzzer.py

# MicroPython / Seeed Wio Terminal / SAMD51
# Wio-Terminal-Buzzer.py - play a scale on the buzzer with PWM
# scruss, 2022-10
# -*- coding: utf-8 -*-

from time import sleep_ms
from machine import Pin, PWM

pwm = PWM(Pin("BUZZER", Pin.OUT))  # or Pin(107) or Pin("PD11")
cmaj = [262, 294, 330, 349, 392, 440, 494, 523]  # C Major Scale frequencies

for note in cmaj:
    print(note, "Hz")
    pwm.duty_u16(32767)  # 50% duty
    pwm.freq(note)
    sleep_ms(225)
    pwm.duty_u16(0)  # 0% duty - silent
    sleep_ms(25)

Light Sensor

This is a simple photo diode. It doesn’t seem to return any kind of calibrated value. Reads through the back of the case.

  • MicroPython interfaces: machine.ADC
  • Control pin: machine.ADC(“PD01”)

Example code: Wio-Terminal-LightSensor.py

# MicroPython / Seeed Wio Terminal / SAMD51
# Wio-Terminal-LightSensor.py - print values from the light sensor
# scruss, 2022-10
# -*- coding: utf-8 -*-

from time import sleep_ms
from machine import ADC

# PD15-22C/TR8 photodiode
light_sensor = ADC("PD01")

while True:
    print([light_sensor.read_u16()])
    sleep_ms(50)

Microphone

Again, a simple analogue sensor:

  • MicroPython interfaces: machine.ADC
  • Control pin: machine.ADC(“MIC”)

Example: Wio-Terminal-Microphone.py

# MicroPython / Seeed Wio Terminal / SAMD51
# Wio-Terminal-Microphone.py - print values from the microphone
# scruss, 2022-10
# -*- coding: utf-8 -*-

from time import sleep_ms
from machine import ADC

mic = ADC("MIC")

while True:
    print([mic.read_u16()])
    sleep_ms(5)

Grove I²C Port

The Wio Terminal has two Grove ports: the one on the left (under the speaker port) is an I²C port. As I don’t know what you’ll be plugging in there, this example does a simple bus scan. You could make a, appalling typewriter if you really wanted.

  • MicroPython interfaces: machine.I2C (channel 3), machine. Pin
  • Control pins: scl=Pin(“SCL1”), sda=Pin(“SDA1”)

Example: Wio-Terminal-Grove-I2C.py

# MicroPython / Seeed Wio Terminal / SAMD51
# Wio-Terminal-Grove-I2C.py - show how to connect on Grove I2C
# scruss, 2022-10
# -*- coding: utf-8 -*-

from machine import Pin, I2C

# NB: This doesn't do much of anything except list what's
# connected to the left (I²C) Grove connector on the Wio Terminal

i2c = I2C(3, scl=Pin("SCL1"), sda=Pin("SDA1"))
devices = i2c.scan()

if len(devices) == 0:
    print("No I²C devices connected to Grove port.")
else:
    print("Found these I²C devices on the Grove port:")
    for n, id in enumerate(devices):
        print(" device", n, ": ID", id, "(hex:", hex(id) + ")")

LIS3DH Accelerometer

This is also an I²C device, but connected to a different port (both logically and physically) than the Grove one.

  • MicroPython interfaces: machine.I2C (channel 4), machine. Pin
  • Control pins: scl=Pin(“SCL0”), sda=Pin(“SDA0”)
  • Library: from MicroPython-LIS3DH, copy lis3dh.py to the Wio Terminal’s small file system. Better yet, compile it to mpy using mpy-cross to save even more space before you copy it across

Example: Wio-Terminal-Accelerometer.py (based on tinypico-micropython/lis3dh library/example.py)

# MicroPython / Seeed Wio Terminal / SAMD51
# Wio-Terminal-Accelerometer.py - test out accelerometer
# scruss, 2022-10
# -*- coding: utf-8 -*-
# based on
#  https://github.com/tinypico/tinypico-micropython/tree/master/lis3dh%20library

import lis3dh, time, math
from machine import Pin, I2C

i2c = I2C(4, scl=Pin("SCL0"), sda=Pin("SDA0"))
imu = lis3dh.LIS3DH_I2C(i2c)

last_convert_time = 0
convert_interval = 100  # ms
pitch = 0
roll = 0

# Convert acceleration to Pitch and Roll
def convert_accell_rotation(vec):
    x_Buff = vec[0]  # x
    y_Buff = vec[1]  # y
    z_Buff = vec[2]  # z

    global last_convert_time, convert_interval, roll, pitch

    # We only want to re-process the values every 100 ms
    if last_convert_time < time.ticks_ms():
        last_convert_time = time.ticks_ms() + convert_interval

        roll = math.atan2(y_Buff, z_Buff) * 57.3
        pitch = (
            math.atan2((-x_Buff), math.sqrt(y_Buff * y_Buff + z_Buff * z_Buff)) * 57.3
        )

    # Return the current values in roll and pitch
    return (roll, pitch)


# If we have found the LIS3DH
if imu.device_check():
    # Set range of accelerometer (can be RANGE_2_G, RANGE_4_G, RANGE_8_G or RANGE_16_G).
    imu.range = lis3dh.RANGE_2_G

    # Loop forever printing values
    while True:
        # Read accelerometer values (in m / s ^ 2).  Returns a 3-tuple of x, y,
        # z axis values.  Divide them by 9.806 to convert to Gs.
        x, y, z = [value / lis3dh.STANDARD_GRAVITY for value in imu.acceleration]
        print("x = %0.3f G, y = %0.3f G, z = %0.3f G" % (x, y, z))

        # Convert acceleration to Pitch and Roll and print values
        p, r = convert_accell_rotation(imu.acceleration)
        print("pitch = %0.2f, roll = %0.2f" % (p, r))

        # Small delay to keep things responsive but give time for interrupt processing.
        time.sleep(0.1)

SD Card

  • MicroPython interfaces: machine.SPI (channel 6), machine.Pin, machine.Signal
  • Control Pins: Pin(“SD_SCK”), Pin(“SD_MOSI”), Pin(“SD_MISO”) for SD access. Pin(“SD_DET”) is low if an SD card is inserted, otherwise high
  • Library: copy sdcard.py from micropython-lib to the Wio Terminal’s file system.

Rather than provide a small canned example (there’s one here, if you must: Wio-Terminal-SDCard.py) here’s my boot.py startup file, showing how I safely mount an SD card if there’s one inserted, but keep booting even if it’s missing:

# boot.py - MicroPython / Seeed Wio Terminal / SAMD51

import sys

sys.path.append("/lib")

import machine
import gc
import os
import sdcard

machine.freq(160000000)  # fast but slightly jittery clock
gc.enable()

# mount SD card if there's one inserted
try:
    sd_detected = machine.Signal(
        machine.Pin("SD_DET", machine.Pin.IN),
        invert=True,
    )
    sd_spi = machine.SPI(
        6,
        sck=machine.Pin("SD_SCK"),
        mosi=machine.Pin("SD_MOSI"),
        miso=machine.Pin("SD_MISO"),
        baudrate=40000000,
    )
    sd = sdcard.SDCard(sd_spi, machine.Pin("SD_CS"))
    if sd_detected.value() == True:
        os.mount(sd, "/SD")
        print("SD card mounted on /SD")
    else:
        raise Exception("SD card not inserted, can't mount /SD")
except:
    print("SD card not found")


ILI9341 Display

I’m going to use the library rdagger/micropython-ili9341: MicroPython ILI9341Display & XPT2046 Touch Screen Driver because it’s reliable, and since it’s written entirely in MicroPython, it’s easy to install. It’s not particularly fast, though.

The Wio Terminal may have an XPT2046 resistive touch controller installed, but I haven’t been able to test it. There are LCD_XL, LCD_YU, LCD_XR and LCD_YD lines on the schematic that might indicate it’s there, though.

  • MicroPython interfaces: machine.SPI (channel 7), machine.Pin.
  • Control Pins: Pin(“LCD_SCK”), Pin(“LCD_MOSI”), Pin(“LCD_MISO”). Pin(“LED_LCD”) is the backlight control
  • Library: copy ili9341.py from rdagger /micropython-ili9341 to the Wio Terminal’s file system.

This demo draws rainbow-coloured diamond shapes that change continuously.

Example: Wio-Terminal-Screen.py

# MicroPython / Seeed Wio Terminal / SAMD51
# Wio-Terminal-Screen.py - output something on the ILI9341 screen
# scruss, 2022-10
# -*- coding: utf-8 -*-


from time import sleep
from ili9341 import Display, color565
from machine import Pin, SPI


def wheel565(pos):
    # Input a value 0 to 255 to get a colour value.
    # The colours are a transition r - g - b - back to r.
    # modified to return RGB565 value for ili9341 - scruss
    (r, g, b) = (0, 0, 0)
    if (pos < 0) or (pos > 255):
        (r, g, b) = (0, 0, 0)
    if pos < 85:
        (r, g, b) = (int(pos * 3), int(255 - (pos * 3)), 0)
    elif pos < 170:
        pos -= 85
        (r, g, b) = (int(255 - pos * 3), 0, int(pos * 3))
    else:
        pos -= 170
        (r, g, b) = (0, int(pos * 3), int(255 - pos * 3))
    return (r & 0xF8) << 8 | (g & 0xFC) << 3 | b >> 3


# screen can be a little slow to turn on, so use built-in
# LED to signal all is well
led = Pin("LED_BLUE", Pin.OUT)

backlight = Pin("LED_LCD", Pin.OUT)  # backlight is not a PWM pin
spi = SPI(
    7, sck=Pin("LCD_SCK"), mosi=Pin("LCD_MOSI"), miso=Pin("LCD_MISO"), baudrate=4000000
)
display = Display(spi, dc=Pin("LCD_D/C"), cs=Pin("LCD_CS"), rst=Pin("LCD_RESET"))
display.display_on()
display.clear()
led.on()  # shotgun debugging, embedded style
backlight.on()

# use default portrait settings: x = 0..239, y = 0..319
dx = 3
dy = 4
x = 3
y = 4
i = 0

try:
    while True:
        # display.draw_pixel(x, y, wheel565(i))
        display.fill_hrect(x, y, 3, 4, wheel565(i))
        i = (i + 1) % 256
        x = x + dx
        y = y + dy
        if x <= 4:
            dx = -dx
        if x >= 234:
            dx = -dx
        if y <= 5:
            dy = -dy
        if y >= 313:
            dy = -dy
except:
    backlight.off()
    led.off()
    display.display_off()

MicroPython MIDI mayhem (kinda)

It pleased me to learn about umidiparser – MIDI file parser for Micropython. Could I use my previous adventures in beepy nonsense to turn a simple MIDI file into a terrible squeaky rendition of same? You betcha!

MIDI seems to be absurdly complex. In all the files I looked at, there didn’t seem to be much of a standard in encoding whether the note duration was in the NOTE_ON event or the NOTE_OFF event. Eventually, I managed to fudge a tiny single channel file that had acceptable note durations in the NOTE_OFF events. Here is the file:

I used the same setup as before:

Raspberry Pi Pico with small piezo speaker connected to pins 23 and 26
piezo between pins 26 and 23

With this code:

# extremely crude MicroPython MIDI demo
# MicroPython / Raspberry Pi Pico - scruss, 2022-08
# see https://github.com/bixb922/umidiparser

import umidiparser
from time import sleep_us
from machine import Pin, PWM

# pin 26 - GP20; just the right distance from GND at pin 23
#  to use one of those PC beepers with the 4-pin headers
pwm = PWM(Pin(20))
led = Pin('LED', Pin.OUT)


def play_tone(freq, usec):
    # play RTTL/midi notes, also flash onboard LED
    # original idea thanks to
    #   https://github.com/dhylands/upy-rtttl
    print('freq = {:6.1f} usec = {:6.1f}'.format(freq, usec))
    if freq > 0:
        pwm.freq(int(freq))       # Set frequency
        pwm.duty_u16(32767)       # 50% duty cycle
    led.on()
    sleep_us(int(0.9 * usec))     # Play for a number of usec
    pwm.duty_u16(0)               # Stop playing for gap between notes
    led.off()
    sleep_us(int(0.1 * usec))     # Pause for a number of usec


# map MIDI notes (0-127) to frequencies. Note 69 is 440 Hz ('A4')
freqs = [440 * 2**((float(x) - 69) / 12) for x in range(128)]

for event in umidiparser.MidiFile("lg2.mid", reuse_event_object=True):
    if event.status == umidiparser.NOTE_OFF and event.channel == 0:
        play_tone(freqs[event.note], event.delta_us)

This isn’t be any means a general MIDI parser, but is rather specialized to play monophonic tunes on channel 0. The result is gloriously awful:

apologies to LG

INA219 Current Sensor and MicroPython

More Micropython programmers — and especially beginners — should know about Awesome MicroPython. It’s a community-curated list of remarkably decent MicroPython libraries, frameworks, software and resources. If you need to interface to a sensor, look there first.

For example, take the INA219 High Side DC Current Sensor. It’s an I²C sensor able to measure up to 26 V, ±3.2 A. It does this by measuring the voltage across a 0.1 ohm precision shunt resistor with its built-in 12-bit ADC. I got a customer return from the store that was cosmetically damaged but still usable, so I thought I’d try it with the simplest module I could find in Awesome MicroPython and see how well it worked.

I guess I needed a test circuit too. Using all of what was immediately handy — a resistor I found on the bench and measured at 150.2 ohm — I came up with this barely useful circuit:

simple circle with 3.3 V DC supply ad two resistors of 150.2 ohms and 0.1 ohms in series
Should indicate a current of 3.3 / (150.2 + 0.1) = 21.96 mA

The INA219 would be happier with a much higher current to measure, but I didn’t have anything handy that could do that.

Looking in Awesome MicroPython’s Current section, I found robert-hh/INA219: INA219 Micropython driver. It doesn’t have much (okay, any) documentation, but it’s a very small module and the code is easy enough to follow. I put the ina219.py module file into the /lib folder of a WeAct Studio RP2040 board, and wrote the following code:

# INA219 demo - uses https://github.com/robert-hh/INA219

from machine import Pin, I2C
import ina219

i = I2C(0, scl=Pin(5), sda=Pin(4))
print("I2C Bus Scan: ", i.scan(), "\n")

sensor = ina219.INA219(i)
sensor.set_calibration_16V_400mA()

# my test circuit is 3V3 supply through 150.2 ohm resistor
r_1 = 150.2
r_s = 0.1  # shunt resistor on INA219 board

# current is returned in milliamps
print("Current       / mA: %8.3f" % (sensor.current))
# shunt_voltage is returned in volts
print("Shunt voltage / mV: %8.3f" % (sensor.shunt_voltage * 1000))
# estimate supply voltage from known resistance * sensed current
print("3V3 (sensed)  / mV: %8.3f" % ((r_1 + r_s) * sensor.current))

with everything wired up like this (Blue = SDA, Yellow = SCL):

breadboard with RP2040 pico board and INA219 sensor board benath it, and the 150 ohm wired as a circuit on the side
all of the wires

Running it produced this:

I2C Bus Scan:  [64] 

Current       / mA:   22.100
Shunt voltage / mV:    2.210
3V3 (sensed)  / mV: 3321.630

So it’s showing just over 22 mA: pretty close to what I calculated!

The Quite Rubbish Clock, mk.2

this is bad and I should feel bad

In early 2013, I must’ve been left unsupervised for too long since I made The Quite Rubbish Clock:

It still isn’t human readable …

Written in (Owen Wilson voice) kind of an obsolete vernacular and running on hardware that’s now best described as “quaint”, it was still absurdly popular at the time. Raspberry Pis were still pretty new, and people were looking for different things to do with them.

I happened across the JASchilz/uQR: QR Code Generator for MicroPython the other day, and remembered I had some tiny OLED screens that were about the same resolution as the old Nokia I’d used in 2013. I wondered: could I …?

small microcontroller board with USB C cable attached and an OLED screen on top. The OLED is displaying a QR code which reads '172731'
OLED Shield on a LOLIN S2 Mini: very smol indeed

The board is a LOLIN S2 Mini with a OLED 0.66 Inch Shield on top, all running MicroPython. One limitation I found in the MicroPython QR library was that it was very picky about input formats, so it only displays the time as HHMMSS with no separators.

Source, of course:

# -*- coding: utf-8 -*-
# yes, the Quite Rubbish Clock rides again ...
# scruss, 2022-06-30
# MicroPython on Lolin S2 Mini with 64 x 48 OLED display
# uses uQR from https://github.com/JASchilz/uQR
# - which has problems detecting times with colons

from machine import Pin, I2C, RTC
import s2mini  # on Lolin ESP32-S2 Mini
import ssd1306
from uQR import QRCode

WIDTH = 64  # screen size
HEIGHT = 48
SIZE = 8  # text size
r = RTC()

# set up and clear screen
i2c = I2C(0, scl=Pin(s2mini.I2C_SCL), sda=Pin(s2mini.I2C_SDA))
oled = ssd1306.SSD1306_I2C(WIDTH, HEIGHT, i2c)
oled.fill(0)


def snazz():
    marquee = [
        "   **",
        "   **",
        "   **",
        "   **",
        "   **",
        "********",
        " ******",
        "  ****",
        "   **",
        " quite",
        "rubbish",
        " clock",
        "  mk.2",
        "<scruss>",
        " >2022<"
    ]
    for s in marquee:
        oled.scroll(0, -SIZE)  # scroll up one text line
        oled.fill_rect(0, HEIGHT-SIZE, WIDTH,
                       SIZE, 0)  # blank last line
        oled.text("%-8s" % s, 0, HEIGHT-SIZE)  # write text
        oled.show()
        time.sleep(0.25)
    time.sleep(5)
    oled.fill(1)
    oled.show()


snazz()  # tedious crowd-pleasing intro

qr = QRCode()
while True:
    qr.add_data("%02d%02d%02d" % r.datetime()[4:7])
    qr.border = 1  # default border too big to fit small screen
    m = qr.get_matrix()
    oled.fill(1)
    for y in range(len(m)):
        for x in range(len(m[0])):
            # plot a double-sized QR code, centred, inverted
            oled.fill_rect(9 + 2*x, 1 + 2*y, 2, 2, not m[y][x])
    oled.show()
    time.sleep(0.05)
    qr.clear()

If your output is glitchy, you might need to put the following in boot.py:

import machine
machine.freq(240000000)

This increases the ESP32-S2’s frequency from 160 to 240 MHz.

Update: there’s a fork of uQR that provides better character support, particularly those required for sending Wi-Fi Network config.

Modding an Adafruit PIR for 3.3 volts

green circuit board covered in surface mount components. A grey wire has been soldered to the output pin of the SOT-89 package 7133-1 voltage regulator
slightly dodgy soldering of a grey jumper wire to the Vout pin of the PIR’s voltage regulator

Consider the Adafruit PIR (motion) sensor (aka PIR Motion Sensor, if you’re in Canada). Simple, reliable device, but only runs from a 5 V supply. Yes, there are smaller PIRs that run off 3.3 V, but if this is what you have, you need to do some soldering. Annoyingly, the sensor on the board is a 3.3 V part, but the carrier was designed in Olden Tymes when King 5 V ruled.

You can try powering it from 3.3 V, but it’ll go all off on its own randomly as its own power supply won’t be supplying enough voltage. There are a couple of sites on how to modify these PIRs that either describe old kit you can’t get any more, or do it completely wrongly. Just one post on the Adafruit support forum gets it right.

One way of doing this is to provide 3.3 V directly to the output pin of the voltage regulator, and ignore the 5 V power line entirely. The regulator’s a SOT89-3 part that looks a bit like this:

71xx-1 SOT-89 package outline, with three pins at the bottom and one large ground tab (connected to centre pin, but not visible) at the top
wee leggy thing

In the photo above, it’s flipped over. Whichever way it’s oriented, we want to put power directly into the Vout pin. There may be easier points to solder this to than a tiny surface mount pin (almost definitely one of the capacitors) but this has held for me.

How to use it in MicroPython? Like the TTP223 capacitive touch sensors I looked at before, a PIR gives a simple off/on output, so you can use something like this:

from machine import Pin
from time import sleep_ms

pir = Pin(21, Pin.IN)

while True:
    print("[", pir.value(), "]")
    sleep_ms(1000)

value() will return 1 if there’s movement, 0 if not. There are trigger time and sensitivity potentiometers to fiddle with on the board if you need to tweak the output.

line graph showing output signal going from 0 to 1, back down to 0 and ending at one over a period of about 20 seconds
Thonny plotter output showing a couple of movement detections. High output (on my device) stays up for about 4 seconds, so you can be pretty leisurely about polling PIRs

Just remember: don’t connect the 5 V power line if you make this mod. I’m not responsible for any smoke emitted if you do — but I can always sell you a replacement …

Raspberry Pi Pico with TTP223 Touch Sensor

This is almost too trivial to write up, as the TTP223 does exactly what you’d expect it to do with no other components.

breadboard with Raspberry Pi Pico and small blue capacitive touch sensor
TTP223 sensor board connected to GP22 / physical pin 29

Breakout boards for the TTP223 capacitive touch sensor come in a whole variety of sizes. The ones I got from Simcoe DIY are much smaller, have a different connection order, and don’t have an indicator LED. What they all give you, though, is a single touch/proximity switch for about $1.50

Trivial code to light the Raspberry Pi Pico’s LED when a touch event is detected looks like this:

import machine
touch = machine.Pin(22, machine.Pin.IN)
led = machine.Pin(25, machine.Pin.OUT)

while True:
    led.value(touch.value())

For the default configuration, the sensor’s output goes high while a touch is detected, then goes low. This might not be the ideal configuration for you, so these sensor boards have a couple of solder links you can modify:

  1. Active Low — sometimes you want a switch to indicate a touch with a low / 0 V signal. On the boards I have, the A link controls that: put a blob of solder across it to reverse the switch’s sense.
  2. Toggle — if you want the output to stay latched at one level until you touch it again, a blob of solder across the T link will do that. Unlike a mechanical switch, this won’t stay latched after a power cycle, though.

And that’s all it does. Sometimes it’s nice to have a sensor that does exactly one thing perfectly well.

CardKB mini keyboard with MicroPython

small computer screen with text
*** APPALLING TYPEWRITER ***
** Type stuff, Esc to end **

then further down: "hello I am smol keeb"
it really is the size of a credit card
(running with a SeeedStudio Wio Terminal)

I got one of these CardKB Mini Keyboards to see if I could use it for small interactives with MicroPython devices. It’s very small, and objectively not great as a mass data entry tool. “Better than a Pocket C.H.I.P. keyboard” is how I’d describe the feel. It’s also pretty reliable.

It’s got an I²C Grove connector, and its brains are an ATMega chip just like an Arduino. It’s strictly an ASCII keyboard: that is, it sends the 8-bit ASCII code of the key combination you pressed. It doesn’t send scan codes like a PC keyboard. The driver source is in the CardKB m5-docs, so if you really felt ambitious you could write a scan code-like firmware for yourself.

The device appears at I²C peripheral address 95, and returns a single byte when polled. That byte’s either 0 if no key was pressed, or the character code of what was pressed. The Esc key returns chr(27), and Enter returns CR. If you poll the keyboard too fast it seems to lose the plot a little, so a tiny delay seems to help

Here’s a small demo for MicroPython that acts as the world’s worst typewriter:

# M5Stack CardKB tiny keyboard - scruss, 2021-06
# MicroPython - Raspberry Pi Pico

from machine import Pin, I2C
from time import sleep_ms

i2c = I2C(1, scl=Pin(7), sda=Pin(6))
cardkb = i2c.scan()[0]  # should return 95
if cardkb != 95:
    print("!!! Check I2C config: " + str(i2c))
    print("!!! CardKB not found. I2C device", cardkb,
          "found instead.")
    exit(1)

ESC = chr(27)
NUL = '\x00'
CR = "\r"
LF = "\n"
c = ''

print("*** APPALLING TYPEWRITER ***")
print("** Type stuff, Esc to end **")

while (c != ESC):
    # returns NUL char if no character read
    c = i2c.readfrom(cardkb, 1).decode()
    if c == CR:
        # convert CR return key to LF
        c = LF
    if c != NUL or c != ESC:
        print(c, end='')
    sleep_ms(5)

And here’s the CircuitPython version. It has annoying tiny differences. It won’t let me use the I²C Grove connector on the Wio Terminal for some reason, but it does work much the same:

# M5Stack CardKB tiny keyboard - scruss, 2021-06
# CircuitPython - SeeedStudio Wio Terminal
# NB: can't use Grove connector under CPY because CPY

import time
import board
import busio

i2c = busio.I2C(board.SCL, board.SDA)

while not i2c.try_lock():
    pass

cardkb = i2c.scan()[0]  # should return 95
if cardkb != 95:
    print("!!! Check I2C config: " + str(i2c))
    print("!!! CardKB not found. I2C device", cardkb,
          "found instead.")
    exit(1)

ESC = chr(27)
NUL = '\x00'
CR = "\r"
LF = "\n"
c = ''
b = bytearray(1)

# can't really clear screen, so this will do
for i in range(12):
    print()
print("*** APPALLING TYPEWRITER ***")
print("** Type stuff, Esc to end **")
for i in range(8):
    print()

while (c != ESC):
    # returns NUL char if no character read
    i2c.readfrom_into(cardkb, b)
    c = b.decode()
    if c == CR:
        # convert CR return key to LF
        c = LF
    if c != NUL or c != ESC:
        print(c, end='')
    time.sleep(0.005)

# be nice, clean up
i2c.unlock()

The cheapest Micro SD card interface in the world

a micro-sd adapter with 7 0.1"-pitch header pins  soldered onto its contacts
micro-SD adapter + pins + solder = working SD interface

It’s only a serial SPI interface, but you can’t beat the price. It should only be used with 3.3 V micro-controllers like the Raspberry Pi Pico, since micro-SD cards don’t like 5 V directly at all.

You might want to pre-tin the pins and apply some extra flux on both surfaces, because these pads are thin and you don’t want to melt them. I used my standard SnAgCu lead-free solder without trouble, though.

label sticker image for 7 pins, from left to right DO, GND, CLK, 3V3, GND, DI, CS
got a label maker? This label’s the same length as an SD card is wide, as shown above.
Made entirely with netpbm

You only need to use one of the Ground connections for the card to work.

Visualizing PWM

my desk is not usually this tidy

I built a DS0150 — successfully, on my second try — and wanted to measure something. My demo MicroPython program from MicroPython on the terrible old ESP8266-12 Development Board has been running since May 2020, and the RGB LED’s red channel is conveniently broken out to a header. Since the DSO150 has precisely one channel, it let me see the how the PWM duty cycle affects the voltage (and brightness) of the LED.

I can’t think of oscilloscopes without being reminded of a scene from one of my favourite sci-fi books, The Reproductive System by John Sladek. Cal, the hapless new hire in the Wompler toy factory-turned-military research lab, is showing the boss some equipment while making up more and more extravagant names for them for the clueless owner:

At each exhibit, Grandison [Wompler] would pause while Cal named the piece of equipment. Then he would repeat the name softly, with a kind of wonder, nod sagely, and move on. Cal was strongly reminded of the way some people look at modern art exhibitions, where the labels become more important to them than the objects. He found himself making up elaborate names.
“And this, you’ll note, is the Mondriaan Modular Mnemonicon.”
“—onicon, yes.”
“And the Empyrean diffractosphere.”
“—sphere. Mn. I see.”
Nothing surprised Grandison, for he was looking at nothing. Cal became wilder. Pointing to Hita’s desk, he said, “The chiarascuro thermocouple.”
“Couple? Looks like only one, to me. Interesting, though.”
A briar pipe became a “zygotic pipette,” the glass ashtray a “Piltdown retort,” and the lamp a “phase-conditioned Aeolian.” Paperclips became “nuances.”
“Nuances, I see. Very fine. What’s that thing, now?”
He pointed to an oscilloscope. Cal took a deep breath.
“Its full name,” he said, “is the Praetorian eschatalogical morphomorphic tangram, Endymion-type, but we usually just call it a ramification.”
The old man fixed him with a stern black eye. “Are you trying to be funny or something? I mean, I may not be a smart-aleck scientist, but I sure as hell know a television when I see one.”
Cal assured him it was not a television, and proved it by switching it on. “See,” he said, pointing to a pattern of square waves, “there are the little anapests.”

— The Reproductive system, by John Sladek (text copypasta from Grey Goo in the 1960s)

So we were displaying roughly 500 anapests/s there. Not bad, not bad at all …

Raspberry Pi Pico: DS18x20 in MicroPython

Updated: Thanks to Ben, who noticed the Fritzing diagrams had the sensors the wrong way round. Fixed now…

Hidden away in the Pico MicroPython guide is a hint that there may be more modules installed in the system than they let on. In the section about picotool, the guide has a seemingly innocuous couple of lines:

frozen modules: _boot, rp2, ds18x20, onewire, uasyncio, uasyncio/core, uasyncio/event, uasyncio/funcs, uasyncio/lock, uasyncio/stream

The third and fourth ‘frozen modules’ are a giveaway: it shows that support for the popular Dallas/Maxim DS18x20 1-Wire temperature sensors is built in. Nowhere else in the guide are they mentioned. I guess someone needs to write them up.

DS18x20 digital temperature sensors – usually sold as DS18B20 by Maxim and the many clone/knock-off suppliers – are handy. They can report temperatures from -55 to 125 °C, although not every sensor will withstand that range. They come in a variety of packages, including immersible sealed units. They give a reliable result, free from ADC noise. They’re fairly cheap, the wiring’s absurdly simple, and you can chain long strings of them together from the same input pin and they’ll all work. What they aren’t, though, is fast: 1-Wire is a slow serial protocol that takes a while to query all of its attached devices and ferry the results back to the controller. But when we’re talking about environmental temperature, querying more often than a few times a minute is unnecessary.

So this is the most complex way you can wire up a DS18x20 sensor:

breadboard with raspberry Pi Pico, DS18x20 sensor with 47 k? pull-up resistor between 3V3 power and sensor data line
Raspberry Pi Pico connected to a single DS18x20 sensor

and this is how it’s wired:

   DS18X20    Pico
   =========  =========
   VDD      ? 3V3
              /
     --47 k?--
    /
   DQ       ? GP22
   GND      ? GND

 (47 k? resistor between DQ and 3V3 as pull-up)

Adding another sensor is no more complicated: connect it exactly as the first, chaining the sensors together –

breadboard with raspberry Pi Pico, two DS18x20 sensors with 47 k? pull-up resistor between 3V3 power and sensor data line
Two DS18x20 sensors, though quite why you’d want two temperature sensors less than 8 mm apart, I’ll never know. Imagine one is a fancy immersible one on a long cable

The code is not complex, either:

# Raspberry Pi Pico - MicroPython DS18X20 Sensor demo
# scruss - 2021-02
# -*- coding: utf-8 -*-

from machine import Pin
from onewire import OneWire
from ds18x20 import DS18X20
from time import sleep_ms
from ubinascii import hexlify    # for sensor ID nice display

ds = DS18X20(OneWire(Pin(22)))
sensors = ds.scan()

while True:
    ds.convert_temp()
    sleep_ms(750)     # mandatory pause to collect results
    for s in sensors:
        print(hexlify(s).decode(), ":", "%6.1f" % (ds.read_temp(s)))
        print()
    sleep_ms(2000)

This generic code will read any number of attached sensors and return their readings along with the sensor ID. The sensor ID is a big ugly hex string (the one I’m using right now has an ID of 284c907997070344, but its friends call it ThreeFourFour) that’s unique across all of the sensors that are out there.

If you’re reading a single sensor, the code can be much simpler:

# Raspberry Pi Pico - MicroPython 1x DS18X20 Sensor demo
# scruss - 2021-02
# -*- coding: utf-8 -*-

from machine import Pin
from onewire import OneWire
from ds18x20 import DS18X20
from time import sleep_ms

ds = DS18X20(OneWire(Pin(22)))
sensor_id = ds.scan()[0]  # the one and only sensor

while True:
    ds.convert_temp()
    sleep_ms(750)         # wait for results
    print(ds.read_temp(sensor_id), " °C")
    sleep_ms(2000)

The important bits of the program:

  1. Tell your Pico you have a DS18x20 on pin GP22:
    ds = DS18X20(OneWire(Pin(22)))
  2. Get the first (and only) sensor ID:
    sensor_id = ds.scan()[0]
  3. Every time you need a reading:
    1. Request a temperature reading:
      ds.convert_temp()
    2. Wait for results to come back:
      sleep_ms(750)
    3. Get the reading back as a floating-point value in °C:
      ds.read_temp(sensor_id)

That’s it. No faffing about with analogue conversion factors and mystery multipliers. No “will it feel like returning a result this time?” like the DHT sensors. While the 1-Wire protocol is immensely complicated (Trevor Woerner has a really clear summary: Device Enumeration on a 1-Wire Bus) it’s not something you need to understand to make them work.

Presentation: Getting Started with MicroPython on the Raspberry Pi Pico

I just gave this talk to the Toronto Raspberry Pi Meetup group: Getting Started with MicroPython on the Raspberry Pi Pico. Slides are here:

or, if you must, pptx:

PDF, for the impatient:

If I were to do this again, I’d drop the messy thermistor code as an example and use a DS18x20, like here: Raspberry Pi Pico: DS18x20 in MicroPython

also, simple potentiometer demo I wrote during the talk: potentiometer.py

WeAct F411 + MicroPython + NeoPixels

Further to the Canaduino STM32 boards with MicroPython writeup, I thought I’d start showing how you’d interface common electronics to the WeAct F411 boards. First off, NeoPixels!

Rather than use the Adafruit trade name, these are more properly called WS2812 LEDs. Each one contains a tiny microcontroller and it only takes three connections to drive a long chain of addressable colour LEDs. The downside is that the protocol to drive these is a bit of a bear, and really needs an accurate, fast clock signal to be reliable.

The STM32F411 chip does have just such a clock, and the generic micropython-ws2812 library slightly misuses the SPI bus to handle the signalling. The wiring’s simple:

  • F411 GND to WS2812 GND;
  • F411 3V3 to WS2812 5V;
  • F411 PA7 (SPI1_MOSI) PB15 (SPI2_MOSI) to WS2812 DIn

Next, copy ws2812.py into the WeAct F411’s flash. Now create a script to drive the LEDs. Here’s one to drive 8 LEDs, modified from the library’s advanced example:

# -*- coding: utf-8 -*-

import time
import math

from ws2812 import WS2812

ring = WS2812(spi_bus=2, led_count=8, intensity=0.1)

def data_generator(led_count):
    data = [(0, 0, 0) for i in range(led_count)]
    step = 0
    while True:
        red = int((1 + math.sin(step * 0.1324)) * 127)
        green = int((1 + math.sin(step * 0.1654)) * 127)
        blue = int((1 + math.sin(step * 0.1)) * 127)
        data[step % led_count] = (red, green, blue)
        yield data
        step += 1

for data in data_generator(ring.led_count):
    ring.show(data)
    time.sleep_ms(100)

Previously I said you’d see your WS2812s flicker and shimmer from the SPI bus noise. I thought it was cool, but I suspect it was also why the external flash on my F411 board just died. By pumping data into PA7, I was also hammering the flash chip’s DI line …

Canaduino STM32 boards with MicroPython

Volker Forster at Universal Solder was kind enough to send me a couple of these boards for free when I asked about availability. By way of thanks, I’m writing this article about what’s neat about these micro-controller boards.

always neat packaging from Universal Solder

Can I just say how nicely packaged Universal Solder’s own or customized products are? They want it to get to you, and they want it to work.

I’d previously played around with Blue Pill and Black Pill boards with limited success. Yes, they’re cheap and powerful, but getting the toolchain to work reliably was so much work. So when I read about the WeAct STM32F411CEU6 board on the MicroPython forum, I knew they’d be a much better bet.

Canaduino Black Pill Carrier Board with STM32F411 (and battery) installed

Volker sent me two different things:

Let’s start with the STM32 Screw Terminal Adapter:

Canaduino Black Pill Carrier Board (front)

It’s a neat, solid board built on a black 1.6 mm thick PCB. Apart from the obvious screw terminals — essential for long-term industrial installations — it adds three handy features:

  • a real-time clock battery. If you’re using a micro-controller for data logging, an RTC battery helps you keep timestamps accurate even if the device loses power.
  • mounting holes! This may seem a small thing, but if you can mount your micro-controller solidly, your project will look much more professional and last longer too.
  • A 6–30 V DC regulator. Connect this voltage between Vin and GND and the regulator will keep the board happy. From the helpful graph on the back of the board, it doesn’t look as if things start getting efficient until around 12 V, but it’s really nice to have a choice.
Canaduino Black Pill Carrier Board (back)

I made a little slip-case for this board so it wouldn’t short out on the workbench. The project is here: Canaduino STM32 Screw Terminal board tray and you can download a snapshot here:

The boards themselves are pretty neat:

two STM32F411 Black Pill boards from Canaduino

Gone are the lumpy pin headers of the earlier Blue and Black Pill boards, replaced by tactile switches. The iffy micro USB connectors are replaced by much more solid USB C connectors. According to STM32-base, the STM32F411 has:

  • 100 MHz ARM Cortex-M4 core. This brings fast (single-precision) floating point so you don’t have to fret over integer maths
  • 512 K Flash, 128 K RAM. MicroPython runs in this, but more flash is always helpful
  • Lots of digital and analogue I/O, including a 12-bit ADC
  • A user LED and user input switch.

About the only advanced features it’s missing are a true RNG, a DAC for analogue outputs, and WiFi. But on top of all this, Volker added:

the all-important 128 Mbit flash chip (and capacitor) fitted by Universal Solder

128 Mbit of Flash! This gives the board roughly 16 MB of storage that, when used with MicroPython, appears as a small USB drive for your programs and data. I found I was able to read the ADC more than 22,000 times/second under MicroPython, so who needs slow-to-deploy compiled code?

STM32F411 board pinout
board pinout from STM32F4x1 MiniF4 / WeAct Studio 微行工作室 出品.
Avoid A4-A7 if you’re using a flash chip.

Building and Installing MicroPython

This is surprisingly easy. You’ll need to install the gcc-arm-none-eabi compiler set before you start, but following the instructions at mcauser/WEACT_F411CEU6: MicroPython board definition for the WeAct STM32F411CEU6 board will get you there.

I had to run make a couple of times before it would build, but it built and installed quickly. This board doesn’t take UF2 image files that other boards use, so the installation is a little more complicated than other. But it works!

Once flashed, you should have a USB device with two important MicroPython files on it: boot.py and main.py. boot.py is best left alone, but main.py can be used for your program. I’m going into more details in a later article, but how about replacing the main.py program with the fanciest version if Blink you ever saw:

# main.py -- fancy Blink (scruss, 2020-05)

from pyb import LED
from machine import Timer
tim = Timer(-1)
tim.init(period=1000, mode=Timer.PERIODIC,
         callback=lambda t: LED(1).toggle())

None of that blocking delay() nonsense: we’re using a periodic timer to toggle the user LED every second!

debugging the mystery huge potentiometer using two ADC channels

I’m really impressed with the Universal Solder-modified board as an experimentation/discovery platform. MicroPython makes development and testing really quick and easy.

[and about the mystery huge potentiometer: it’s a Computer Instruments Corporation Model 206-IG multi-turn, multi-track potentiometer I picked up from the free table at a nerd event. I think it’s a 1950s (so Servo-control/Cybernetics age) analogue equivalent of a shaft encoder, looking at the patent. Best I can tell is that each pot (there are two, stacked, with precision bearings) appears to have two 120° 10k ohm sweep tracks offset 90° to one another. The four wipers are labelled -COS, -SIN, +COS and +SIN. If anyone knows more about the thing, let me know!]

Circuit Playground Express Chord Guitar

Hey! This doesn’t work any more, as CircuitPython changed and I haven’t found a way to update it with the new interpreter.

Since there are seven touch pads on a Circuit Playground Express, that’s enough for traditional 3-chord (â… , â…£, â…¤) songs in the keys of C, D and G. That leaves one pad extra for a â…¥min chord for so you can play Neutral Milk Hotel songs in G, of course.

CircuitPython source and samples: cpx-chord_guitar.zip. Alternatively, on github: v1.0 from scruss/cpx_chord_guitar

The code is really simple: poll the seven touch pads on the CPX, and if one of them is touched, play a sample and pause for a short time:

# Circuit Playground Express Chord Guitar
# scruss - 2017-12

# these libraries should be installed by default in CircuitPython
import touchio
import board
import time
import neopixel
import digitalio
import audioio

# touch pins, anticlockwise from battery connector
touch_pins= [
    touchio.TouchIn(board.A1),
    touchio.TouchIn(board.A2),
    touchio.TouchIn(board.A3),
    touchio.TouchIn(board.A4),
    touchio.TouchIn(board.A5),
    touchio.TouchIn(board.A6),
    touchio.TouchIn(board.A7)
]

# 16 kHz 16-bit mono audio files, in same order as pins
chord_files = [
    "chord-C.wav",
    "chord-D.wav",
    "chord-E.wav",
    "chord-Em.wav",
    "chord-F.wav",
    "chord-G.wav",
    "chord-A.wav"
]

# nearest pixels to touch pads
chord_pixels = [ 6, 8, 9, 0, 1, 3, 4 ]

# set up neopixel access
pixels = neopixel.NeoPixel(board.NEOPIXEL, 10, brightness=.2)
pixels.fill((0, 0, 0))
pixels.show()

# set up speaker output
speaker_enable = digitalio.DigitalInOut(board.SPEAKER_ENABLE)
speaker_enable.switch_to_output(value=True)

# poll touch pins
while True:
    for i in range(len(touch_pins)):
        # if a pin is touched
        if touch_pins[i].value:
            # set nearest pixel
            pixels[chord_pixels[i]] = (0, 0x10, 0)
            pixels.show()
            # open and play corresponding file
            f=open(chord_files[i], "rb")
            a = audioio.AudioOut(board.A0, f)
            a.play()
            # blank nearest pixel
            pixels[chord_pixels[i]] = (0, 0, 0)
            pixels.show()
            # short delay to let chord sound
            # might want to try this a little shorter for faster play
            time.sleep(0.2)

This is roughly how I synthesized the samples, but I made them quieter (the MEMS speaker on the CPX went all buzzy at full volume, and not in a good way) and added a bit of reverb. Here’s the sox command from the modified script:

sox -n -r 16000 -b 16 "chord-${chord}.wav" synth 1 pl "$first" pl "$third" pl "$fifth" delay 0 .05 .1 remix - fade p 0 1 0.5 norm -5 reverb

Really, you do want to take a look at shortening the delay between the samples: you want it long enough for all of the notes of the chord to sound, but short enough that you can play faster songs. I came up with something that worked for me, kinda, and quickly; it’s worth fixing if you have the time.

Circuit Playground Express Remote-Controlled Fart Machine

I’m not proud of this, but I made it so you won’t have to:

Craig at Elmwood Electronics very kindly gave me an ADABOX 006. It’s based around Adafruit’s Circuit Playground Express which just happens to feature a small built-in speaker, IR remote control and the ability to play back audio samples. You see where this is going, don’t you?

If you must make this, the code and samples are here: circuit_playground_express-ir_remote_fartbox_unfortunately.zip. You’ll also need to install the Adafruit CircuitPython IRRemote package into the lib/ folder of your Circuit Playground Express. Point the remote at the board, and it’s left arrow to fart, right arrow to chuckle.

The package includes CC0-licensed samples downloaded from Freesound.