A terrible guide to singing with DECtalk

It’s now possible to build and run the DECtalk text to speech system on Linux. It even builds under emscripten, enabling DECtalk for Web in your browser. You too can annoy everyone within earshot making it prattle on about John Madden.

But DECTalk can sing! Because it’s been around so long, there are huge archives of songs in DECtalk format out there. The largest archive is at THE FLAME OF HOPE website, under the Dectalk section.

Building DECtalk songs isn’t easy, especially for a musical numpty like me. You need a decent grasp of music notation, phonemic/phonetic markup and patience with DECtalk’s weird and ancient text formats.

DECtalk phonemes

While DECtalk can accept text and turn it into a fair approximation of spoken English, for singing you have to use phonemes. Let’s say we have a solfège-ish major scale:

do re mi fa sol la ti do

If we’re all fancy-like and know our International Phonetic Alphabet (IPA), that would translate to:

/do? ?e? mi? f?? so? l?? ti? do?/

(sorry about this, my website hates Unicode still)

DECtalk uses a variant on the ARPABET convention to represent IPA symbols as ASCII text. The initial consonant sounds remain as you might expect: D, R, M, F, S, L and T. The vowel sounds, however, are much more complex. This will give us a DECtalk-speakable phrase:

[dow rey miy faa sow laa tiy dow].

Note the opening and closing brackets and the full stop at the end. The brackets introduce phonemes, and the full stop tells DECtalk that the text is at an end. Play it in the DECtalk for Web window and be unimpressed: while the pitch changes are non-existent, the sounds are about right.

For more information about DECtalk phonemes, please see Phonemic Symbols Listed By Language and chapter 7 of DECtalk DTC03 Text-to-Speech System Owner’s Manual.

If you want to have a rough idea of what the phonemes in a phrase might be, you can use DECtalk’s :log phonemes option. You might still have to massage the input and output a bit, like using sed to remove language codes:

say -l us -pre '[:log phonemes on]' -post '[:log phonemes off]' -a "doe ray me fah so lah tea doe" | sed 's/us_//g;'
d ' ow  r ' ey  m iy  f ' aa) s ow  ll' aa  t ' iy  d ' ow.

Music notation

To me — a not very musical person — staff notation looks like it was designed by a maniac. A more impractical system to indicate arrangement of notes and their durations I don’t think I could come up with: and yet we’re stuck with it.

DECtalk uses a series of numbered pitches plus durations in milliseconds for its singing mode. The notes (1–37) correspond to C2 to C5. If you’re familiar with MIDI note numbers, DECtalk’s 1–37 correspond to MIDI note numbers 36–72. This is how DECtalk’s pitch numbers would look as major scales on the treble clef:

a treble clef showing quarter notes from C2 to C5 in the scale of C Major
The entire singing range of DECtalk as a C Major scale, from note 1 (C2, 65.4 Hz) to note 37 (C5, 523.4 Hz)

I’m not sure browsers can play MIDI any more, but here you go (doremi-abc.mid):

and since I had to learn abc notation to make these noises, here is the source:

T:Do Re Mi
C,, D,, E,, F,,| G,, A,, B,, C,| D, E, F, G,| A, B, C D| E F G A| B c z2 |]
w:do re mi fa sol la ti do re mi fa sol la ti do re mi fa sol la ti do

Each element of a DECtalk song takes the following form:

phoneme <duration, pitch number>

The older DTC-03 manual hints that it takes around 100 ms for DECtalk to hit pitch, so for each ½ second utterance (or quarter note at 120 bpm, ish), I split it up as:

  • 100 ms of the initial consonant;
  • 337 ms of the vowel sound;
  • 63 ms of pause (which has the phoneme code “_”). Pauses don’t need pitch numbers, unless you want them to preempt DECtalk’s pitch-change algorithm.

So the three lowest notes in the major scale would sing as:


I’ve split them into line for ease of reading, but DECtalk adds extra pauses if you include spaces, so don’t.

The full three octave major scale looks like this:


You can paste that into the DECtalk browser window, or run the following from the command line on Linux:

say -pre '[:PHONE ON]' -a '[d<100,1>ow<337,1>_<63>r<100,3>ey<337,3>_<63>m<100,5>iy<337,5>_<63>f<100,6>aa<337,6>_<63>s<100,8>ow<337,8>_<63>l<100,10>aa<337,10>_<63>t<100,12>iy<337,12>_<63>d<100,13>ow<337,13>_<63>r<100,15>ey<337,15>_<63>m<100,17>iy<337,17>_<63>f<100,18>aa<337,18>_<63>s<100,20>ow<337,20>_<63>l<100,22>aa<337,22>_<63>t<100,24>iy<337,24>_<63>d<100,25>ow<337,25>_<63>r<100,27>ey<337,27>_<63>m<100,29>iy<337,29>_<63>f<100,30>aa<337,30>_<63>s<100,32>ow<337,32>_<63>l<100,34>aa<337,34>_<63>t<100,36>iy<337,36>_<63>d<100,37>ow<337,37>_<63>].'

It sounds like this:

Singing a scale is hardly singing a tune, but hey, you were warned that this was a terrible guide at the outset. I hope I’ve given you a start on which you can build your own songs.

(One detail I haven’t tried yet: the older DTC-03 manual hints that singing notes can take Hz values instead of pitch numbers, and apparently loses the vibrato effect. It’s not that hard to convert from a note/octave to a frequency. Whether this still works, I don’t know.)

Using the IBM Wheelwriter 10 Series II Typewriter as a printer

I can’t believe I’m having to write this article again. Back in 2004, I picked up an identical model of typewriter on Freecycle, also complete with the parallel printer option board. The one I had back then had an incredible selection of printwheels. And I gave it all away! Aaargh! Why?

Last month, I ventured out to a Value Village in more affluent part of town. On the shelf for $21 was a familiar boxy shape, another Wheelwriter 10 Series II Typewriter model 6783. This one also has the printer option board, but it only has one printwheel, Prestige Elite. It powered on enough at the test rack enough for me to see it mostly worked, so I bought it.

Once I got it home, though, I could see it needed some work. The platen was covered in ink and correction fluid splatters. Worse, the carriage would jam in random places. It was full of dust and paperclips. But the printwheel did make crisp marks on paper, so it was worth looking at a repair.

Thanks to Phoenix Typewriter’s repair video “IBM Wheelwriter Typewriter Repair Fix Carriage Carrier Sticks Margins Reset Makes Noise”, I got it going again. I’m not sure how much life I’ve got left in the film ribbon, but for now it’s doing great.

Note that there are lots of electronics projects — such as tofergregg/IBM-Wheelwriter-Hack: Turning an IBM Wheelwriter into a unique printer — that use an Arduino or similar to drive the printer. This is not that (or those). Here I’m using the Printer Option board plus a USB to Parallel cable. There’s almost nothing out there about how these work.

Connecting the printer

You’ll need a USB to Parallel adapter, something like this: StarTech 10 ft USB to Parallel Printer Adapter – M/M. You need the kind with the big Centronics connector, not the 25-pin D-type. My one (old) has a chunky plastic case that won’t fit into the port on the Wheelwriter unless you remove part of the cable housing. On my Linux box, the port device is /dev/usb/lp0. You might want to add yourself to the lp group so you can send data to the printer without using sudo:

sudo adduser user lp

The Wheelwriter needs to be switched into printer mode manually by pressing the Code + Printer Enable keys.

Printer Codes

As far as I can tell, the Wheelwriter understands a subset of IBM ProPrinter codes. Like most simple printers, most control codes start with an Esc character (ASCII 27). Lines need to end with both a Carriage Return (ASCII 13) and newline (ASCII 10). Sending only CRs allows overprinting, while sending only newlines gives stair-step output.

The codes I’ve found to work so far are:

  • Emphasized printingEsc E
  • Cancel emphasized printingEsc F
    (double strike printing [Esc G, Esc H] might also work, but I haven’t tried them)
  • Continuous underscoreEsc – 1
  • Cancel continuous underscoreEsc – 0
    (technically, these are Escn, where n = ASCII 1 or 0, not character “1” or “0”. But the characters seem to work, too)
  • 7/72″ inch line spacingEsc 1
  • Set text line spacing to n / 72″ unitsEsc A n
    (this one really matters: if you send “6” (ASCII 66) instead of 6, you’ll get 66/72 = 11/12″ [= 28.3 mm] line spacing instead of the 1/12″ [= 2.1 mm] you expected)
  • Start text line spacingEsc 2

Text functions such as italics and extended text aren’t possible with a daisywheel printer. You can attempt dot-matrix graphics using full stops and micro spacing, but I don’t want to know you if you’d try.

Sending codes from the command line

echo is about the simplest way of doing it. Some systems provide an echo built-in that doesn’t support the -e (interpret special characters) and -n (don’t send newline) options. You may have to call /usr/bin/echo instead.

To print emphasized:

echo -en 'well \eEhello\eF there!\r\n' > /dev/usb/lp0

which prints

well hello there!

To print underlined:

echo -en 'well \e-1hello\e-0 there!\r\n' > /dev/usb/lp0

which types

well hello there!

To set the line spacing to a (very cramped) 1/12″ [= 2.1 mm] and print a horizontal line of dots and a vertical line of dots, both equally spaced (if you’re using Prestige Elite):

echo -en '\eA\x05\e2\r\n..........\r\n.\r\n.\r\n.\r\n.\r\n.\r\n.\r\n.\r\n.\r\n.\r\n\r\n' > /dev/usb/lp0

Character set issues

IBM daisywheels typically can’t represent the whole ASCII character set. Here’s what an attempt to print codes 33 to 126 in Prestige Elite looks like:

tabulation of printable ASCII characters on orange background. Some characters are clearly missing

The following characters are missing:

< > \ ^ ` { | } ~

So printing your HTML or Python is right out. FORTRAN, as ever, is safe.

Prestige Elite is a 12 character per inch font (“12 pitch”, or even “Elite” in typewriter parlance) that’s mostly been overshadowed by Courier (typically 10 characters per inch) in computer usage. This is a shame, as it’s a much prettier font.

Related, yet misc.

There’s very little out there about printing with IBM daisywheels. This is a dump of the stuff I’ve found that may help other people:

  • Wheelwriter 10 Series II Typewriter 6783 Operator’s Guide (Internet Archive; nothing about the printer option)
  • IBM didn’t make too many daisywheel printers. Two models were the 5216 Wheelprinter and 5223 Wheelprinter E, possibly intended for larger IBM machines. The 5216 Wheelprinter looks like it may use similar character codes. Here’s a (Printer Definition File?? An IBM thing, I think) for that printer that might help the interested: ibm5216_pdf
  • The IBM 6901 “Personal Typing System” included a daisywheel printer (Correcting Wheelwriter Printer 6902) that looks almost identical to a Wheelwriter 10 Series II with the keyboard lopped off. But I can find nothing about it.
  • Word Perfect 5 may have had a driver for this typewriter/printer, but that doesn’t help me with the control codes.

The Joy of BirdNetPi

I don’t think I’ve had as much enjoyment for a piece of software for a very long time as I’ve had with BirdNET-Pi. It’s a realtime acoustic bird classification system for the Raspberry Pi. It listens through a microphone you place somewhere near where you can hear birds, and it’ll go off and guess what it’s hearing, using a cut-down version of the BirdNET Sound ID model. It does this 24/7, and saves the samples it hears. You can then go to a web page (running on the same Raspberry Pi) and look up all the species it has heard.

Our Garden

a somewhat overgrown garden with budding green trees against blue sky

Not very impressive, kind of overgrown, in the wrong part of town. Small, too. But birds love it. At this time of year, it’s alive with birds. You can’t make them out, but there’s a pair of Rose-breasted Grosbeaks happily snacking near the top of the big tree. There are conifers next door too, so we get birds we wouldn’t expect.

We are next to two busy subway/train stations, and in between two schools. There’s a busy intersection nearby, too. Consequently, the background noise is horrendous

What I used

This was literally “stuff I had lying around”:

  • Raspberry Pi 3B+ (with power supply, case, thermostatic fan and SD card)
  • USB extension cable (this, apparently, is quite important to isolate the USB audio device from electrical noise)
  • Horrible cheap USB sound card: I paid about $2 for a “3d sound” thing about a decade ago. It records in mono. It works. My one is wrapped in electrical tape as the case keeps threatening to all off, plus it has a hugely bright flashing LED the is annoying.
  • Desktop mic (circa 2002): before video became a thing, PCs had conferencing microphones. I think I got this one free with a PC over 20 years ago. It’s entirely unremarkable and is not an audiophile device. I stuck it out a back window and used a strip of gaffer tape to stop bugs getting in. It’s not waterproof, but it didn’t rain the whole week it was out the window.
  • Raspberry Pi OS Lite 64-bit. Yes, it has to be 64 bit.
  • BirdNET-Pi installation on top.

I spent very little time optimizing this. I had to fiddle with microphone gain slightly. That’s all.

What I heard

To the best of my knowledge, I have actual observations of 30 species, observed between May 7th and May 16th 2023:

American Goldfinch, American Robin, Baltimore Oriole, Blue Jay, Cedar Waxwing, Chimney Swift, Clay-colored Sparrow, Common Grackle, Common Raven, Gray Catbird, Hermit Thrush, House Finch, House Sparrow, Killdeer, Mourning Dove, Nashville Warbler, Northern Cardinal, Northern Parula, Orchard Oriole, Ovenbird, Red-winged Blackbird, Ring-billed Gull, Rose-breasted Grosbeak, Ruby-crowned Kinglet, Song Sparrow, Veery, Warbling Vireo, White-throated Sparrow, White-winged Crossbill, Wood Thrush

I’ll put the recordings at the end of this post. Note, though, they’re noisy: Cornell Lab quality they ain’t.

What I learned

This is the first time that I’ve let an “AI” classifier model run with no intervention. If it flags some false positives, then it’s pretty low-stakes when it’s wrong. And how wrong did it get some things!

allegedly a Barred Owl, this is clearly a two-stroke leafblower
Black-Billed Cuckoo? How about kids playing in the school yard?
Emergency vehicles are Common Loons now, according to BirdNetPi
Police cars at 2:24 am are Eastern Screech-Owls. I wonder if we could use this classifier to detect over-policed, under-served neighbourhoods?
Great Black-backed Gulls, or kids playing? The latter
Turkey Vulture? How about a very farty two-stroke engine in a bicycle frame driving past?
(This thing stinks out the street, blecch)

There are also false positive for Trumpeter Swans (local dog) and Tundra Swans (kids playing). These samples had recognizable voices, so I didn’t include them here.

The 30 positive species identifications

Many of these have a fairly loud click at the start of the sample, so mind your ears.

American Goldfinch

American Robin

Baltimore Oriole

(I dunno what’s going on here; the next sample’s much more representative)

Blue Jay

Cedar Waxwing

Chimney Swift

Clay-colored Sparrow

Common Grackle

Common Raven

Gray Catbird

Hermit Thrush

House Finch

House Sparrow


Mourning Dove

Nashville Warbler

Northern Cardinal

Hey, we’ve got both of the repetitive songs that these little doozers chirp out all day. Song 1:

and song 2 …

Northern Parula

Orchard Oriole


Red-winged Blackbird

Ring-billed Gull

Rose-breasted Grosbeak

Ruby-crowned Kinglet

Song Sparrow


Warbling Vireo

White-throated Sparrow

White-winged Crossbill

Wood Thrush

Boring technical bit

BirdNetPi doesn’t create combined spectrograms with audio as a single video file. What it does do is create an mp3 plus a PNG of the spectrogram. ffmpeg can make a nice not-too-large webm video for sharing:

ffmpeg -loop 1 -y -i 'birb.mp3.png' -i 'birb.mp3' -ac 1 -crf 48 -vf scale=720:-2 -shortest 'birb.webm'

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


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")

    while True:
        led.value(not led.value())
    led.value(0)  # turn it off if user quits


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

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


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="")


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.duty_u16(0)  # 0% duty - silent

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:


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:

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.")
    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.

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


import machine
import gc
import os
import sdcard

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

# mount SD card if there's one inserted
    sd_detected = machine.Signal(
        machine.Pin("SD_DET", machine.Pin.IN),
    sd_spi = machine.SPI(
    sd = sdcard.SDCard(sd_spi, machine.Pin("SD_CS"))
    if sd_detected.value() == True:
        os.mount(sd, "/SD")
        print("SD card mounted on /SD")
        raise Exception("SD card not inserted, can't mount /SD")
    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))
        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"))
led.on()  # shotgun debugging, embedded style

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

    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

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)

# 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
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)

def snazz():
    marquee = [
        "   **",
        "   **",
        "   **",
        "   **",
        "   **",
        " ******",
        "  ****",
        "   **",
        " quite",
        " clock",
        "  mk.2",
        " >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

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()
    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])

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

import machine

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

A new old calendar

simulated dot-matrix printer on ruled paper layout of an ASCII-art Snoopy in WW1 Air Ace regalia yelling "Curse you Red Baron!" at his imaginary nemesis, and a 2022 calendar laid out on the facing page
not seen the likes of this since ’78, I’ll be bound

It’s unlikely anyone wanted a faux-lineprinter ASCII art calendar for 2022, but you’re getting one anyway. You can print this yourself:

If you want to make your own, here’s a script: snoopycal.sh


  • Original “WW1 Fighter Pilot” Snoopy ASCII art from “SNOOPY.BA” for the DEC PDP-8, written by Mr Kay R. Fisher of DEC some time before July 1973. It’s referred to in the first printing of the “101 Basic Computer Games” book, which was published in 1973.
  • ncal, banner: their respective authors
  • pstext an ascii to PS filter by Dan Judd, usenet comp.lang.postscript, December 1989. I had to really mess around with the output of this program to use a custom font and add the music ruling, but it produces cleaner PostScript than the giant messes that enscript and a2ps have become
  • Font: mnicmp, by me. Based on the DecWriter II font.
  • iffy scripting, scribbly lines: also me.


Something has gone very wrong with the database encoding on this blog after a recent update, so all my lovely UTF-8 characters have gone mojibake.

Trying to find ways to fix it. It may have to be manual. Remember, kids: have backups before letting WordPress upgrade!

Here’s the Python equivalent of what I think the database has done:

bytes("I ???? UTF-8", encoding='utf-8').decode(encoding='cp1252')
'I 💔 UTF-8'

Quite why my hosting thought a character encoding from last century was appropriate, I’ll never know.

Autumn in Canada: PicoMite version

more leaves

So I ported autumn in canada from OpenProcessing to PicoMite BASIC on the Raspberry Pi Pico:

a small black screen images with text in the centre: autumn in canada scruss, 2021-11 just watch ...
no leaves
a small black screen images with text in the centre: autumn in canada scruss, 2021-11 just watch ... with one red and one orange maple leaf sitting on top of it
a couple of leaves
a small black screen images with text in the centre: autumn in canada scruss, 2021-11 just watch ... with four red/yellow/orange maple leaves sitting on top of it
more leaves
a small black screen images with text in the centre: autumn in canada scruss, 2021-11 just watch ... with sixteen simulated fallen maple leaves mostly covering it
plenty of leaves
a small black screen image completely covered with many simulated fallen maple leaves
far too many leaves

The biggest thing that tripped me up was that PicoMite BASIC starts arrays at 0. OPTION BASE 1 fixes that oversight. It would have been nice to have OpenProcessing’s HSV colour space, and an editor that could handle lines longer than 80 characters that didn’t threaten to bomb out if you hit the End key, but it’ll serve.

Source below:

' autumn in canada
' scruss, 2021-11
' a take on my https://openprocessing.org/sketch/995420 for picomite

OPTION base 1
' *** initialize polar coords of leaf polygon and colour array
DIM leaf_rad(24), leaf_ang(24), px%(24), py%(24)
FOR i=1 TO 24
    READ leaf_rad(i)
FOR i=1 TO 24
    READ x

DIM integer c%(8)
FOR i=1 TO 8
    READ r%, g%, b%

' *** set up some limits
min_scale%=INT(MIN(MM.HRES, MM.VRES)/8)
max_scale%=INT(MIN(MM.HRES, MM.VRES)/6)
max_x%=MM.HRES - min_x%
max_y%=MM.VRES - min_y%

TEXT MM.HRES/2, INT(MM.VRES/3), "autumn in canada", "CM"
TEXT MM.HRES/2, INT(MM.VRES/2), "scruss, 2021-11", "CM"
TEXT MM.HRES/2, INT(2*MM.VRES/3), "just watch ...", "CM"

    cx% = min_x% + INT(RND * (max_x% - min_x%))
    cy% = min_y% + INT(RND * (max_y% - min_y%))
    angle = min_angle + RND * (max_angle - min_angle)
    sc% = min_scale% + INT(RND * (max_scale% - min_scale%))
    col% = 1 + INT(RND * 7)
    leaf cx%, cy%, sc%, angle, c%(7), c%(col%)
    kt% = kt% + 1
LOOP UNTIL kt% >= 1024


SUB leaf x%, y%, scale%, angle, outline%, fill%
    FOR i=1 TO 24
        px%(i) = INT(x% + scale% * leaf_rad(i) * COS(RAD(angle)+leaf_ang(i)))
        py%(i) = INT(y% - scale% * leaf_rad(i) * SIN(RAD(angle)+leaf_ang(i)))
    NEXT i
    POLYGON 24, px%(), py%(), outline%, fill%

' radii
DATA 0.536, 0.744, 0.608, 0.850, 0.719
DATA 0.836, 0.565, 0.589, 0.211, 0.660, 0.515
DATA 0.801, 0.515, 0.660, 0.211, 0.589, 0.565
DATA 0.836, 0.719, 0.850, 0.608, 0.744, 0.536, 1.000
' angles
DATA 270.000, 307.249, 312.110, 353.267, 356.540
DATA 16.530, 18.774, 33.215, 3.497, 60.659, 72.514
DATA 90.000, 107.486, 119.341, 176.503, 146.785, 161.226
DATA 163.470, 183.460, 186.733, 227.890, 232.751, 270.000, 270.000
' leaf colours
DATA 255,0,0, 255,36,0, 255,72,0, 255,109,0
DATA 255,145,0, 255,182,0, 255,218,0, 255,255,0

You could probably use AUTOSAVE and paste the text into the PicoMite REPL. I used an ILI9341 SPI TFT LCD Touch Panel with my Raspberry Pi Pico along with some rather messy breadboard wiring.

Fun fact: the maple leaf polygon points are derived from the official definition of the flag of Canada.

Raspberry Pi Zero 2 W: slides and thermals

2 out of 4 cores burning, 32-bit mode: time to overheat = basically never

Slides from last night’s talk:

It’s impossible to have a Raspberry Pi Zero overheat unless you overclock it. That’s why you don’t get any cases for it with fans or heat sinks. The quad-core Raspberry Pi Zero 2 W, though, has the potential to do so. Here are some numbers:

  • Used official case with lid fitted: increases SoC temperature +3 °C over free air
  • Test – CPUBurn: https://github.com/pmylund/cpuburn
  • Tested 4, 3 and 2 cores burning in 32-bit and 64-bit modes: time from idle to throttling (80 °C) measured
  • GPU overheat not tested.
line graph of cpu temperature against time. Temperature rises sharply from about 47 degrees C to 82 degrees C in around four minutes
All 4 cores burning, 64-bit mode: time to overheat = under 3½ minutes
line graph of cpu temperature against time. Temperature rises sharply from about 47 degrees C to 82 degrees C in just over four minutes
All 4 cores burning, 32-bit mode: time to overheat = just over 4 minutes
line graph of cpu temperature against time. Temperature rises moderately from about 47 degrees C to 81 degrees C in around seven minutes
3 out of 4 cores burning, 64-bit mode: time to overheat = just over 7 minutes
line graph of cpu temperature against time. Temperature rises slowly from about 47 degrees C to 81 degrees C in around ten minutes
3 out of 4 cores burning, 32-bit mode: time to overheat = 9½ minutes
line graph of cpu temperature against time. Temperature rises very slowly, reach 70 degrees C in 40 minutes and then only rising very slightly to about 73 degrees C in the entire run time of 3 hours 20 minutes
2 out of 4 cores burning, 32-bit mode: time to overheat = basically never

Unless you’re doing things that might indicate you should be using a bigger computer, a Raspberry Pi Zero 2 W won’t overheat and doesn’t need any form of cooling. If you’re overclocking, well … it’s your choice to have cooling equipment worth more than the computer it’s trying to cool.

Raspberry Pi Zero 2 W: initial performance

Running A Pi Pie Chart turned out some useful performance numbers. It’s almost, but not quite, a Raspberry Pi 3B in a Raspberry Pi Zero form factor.

32-bit mode

Running stock Raspberry Pi OS with desktop, compiled with stock options:

pie chart comparing multi-thread numeric performance of Raspberry Pi Zero 2 W: slightly faster than a Raspberry Pi 2B
multi-thread results
pie chart comparing single-thread numeric performance of Raspberry Pi Zero 2 W: slightly faster than a Raspberry Pi 2B
single-thread results
time ./pichart-openmp -t "Zero 2W, OpenMP"
pichart -- Raspberry Pi Performance OPENMP version 36

Prime Sieve          P=14630843 Workers=4 Sec=2.18676 Mops=427.266
Merge Sort           N=16777216 Workers=8 Sec=1.9341 Mops=208.186
Fourier Transform    N=4194304 Workers=8 Sec=3.10982 Mflops=148.36
Lorenz 96            N=32768 K=16384 Workers=4 Sec=4.56845 Mflops=705.102

The Zero 2W, OpenMP has Raspberry Pi ratio=8.72113
Making pie charts...done.

real	8m20.245s
user	15m27.197s
sys	0m3.752s


time ./pichart-serial -t "Zero 2W, Serial"
pichart -- Raspberry Pi Performance Serial version 36

Prime Sieve          P=14630843 Workers=1 Sec=8.77047 Mops=106.531
Merge Sort           N=16777216 Workers=2 Sec=7.02049 Mops=57.354
Fourier Transform    N=4194304 Workers=2 Sec=8.58785 Mflops=53.724
Lorenz 96            N=32768 K=16384 Workers=1 Sec=17.1408 Mflops=187.927

The Zero 2W, Serial has Raspberry Pi ratio=2.48852
Making pie charts...done.

real	7m50.524s
user	7m48.854s
sys	0m1.370s


Running stock/beta 64-bit Raspberry Pi OS with desktop. Curiously, these ran out of memory (at least, in oom-kill‘s opinion) with the desktop running, so I had to run from console. This also meant it was harder to capture the program run times.

The firmware required to run in this mode should be in the official distribution by now.

pie chart comparing 64 bit multi-thread numeric performance of Raspberry Pi Zero 2 W: slightly faster than a Raspberry Pi 2B
multi-thread, 64 bit: no, I can’t explain why Lorenz is better than a 3B+
pie chart comparing 64 bit single-thread numeric performance of Raspberry Pi Zero 2 W: slightly faster than a Raspberry Pi 2B
single thread, again with the bump in Lorenz performance
pichart -- Raspberry Pi Performance OPENMP version 36

Prime Sieve          P=14630843 Workers=4 Sec=1.78173 Mops=524.395
Merge Sort           N=16777216 Workers=8 Sec=1.83854 Mops=219.007
Fourier Transform    N=4194304 Workers=4 Sec=2.83797 Mflops=162.572
Lorenz 96            N=32768 K=16384 Workers=4 Sec=2.66808 Mflops=1207.32

The Zero2W-64bit has Raspberry Pi ratio=10.8802
Making pie charts...done.


pichart -- Raspberry Pi Performance Serial version 36

Prime Sieve          P=14630843 Workers=1 Sec=7.06226 Mops=132.299
Merge Sort           N=16777216 Workers=2 Sec=6.75762 Mops=59.5851
Fourier Transform    N=4194304 Workers=2 Sec=7.73993 Mflops=59.6095
Lorenz 96            N=32768 K=16384 Workers=1 Sec=9.00538 Mflops=357.7

The Zero2W-64bit has Raspberry Pi ratio=3.19724
Making pie charts...done.

The main reason for the Raspberry Pi Zero 2 W appearing slower than the 3B and 3B+ is likely that it uses LPDDR2 memory instead of LPDDR3. 64-bit mode provides is a useful performance increase, offset by increased memory use. I found desktop apps to be almost unusably swappy in 64-bit mode, but there might be some tweaking I can do to avoid this.

Unlike the single core Raspberry Pi Zero, the Raspberry Pi Zero 2 W can be made to go into thermal throttling if you’re really, really determined. Like “3 or more cores running flat-out“-determined. In my testing, two cores at 100% (as you might get in emulation) won’t put it into thermal throttling, even in the snug official case closed up tight. More on this later.

(And a great big raspberry blown at Make, who leaked the Raspberry Pi Zero 2 W release a couple of days ago. Not classy.)

Another Raspberry Pi Pico language: MMBasic

It’s very much a work in progress, but Geoff Graham and Peter Mather’s MMBasic runs nicely on the Raspberry Pi Pico. Development is mostly coordinated on TheBackShed.com forum.

It supports an impressive range of displays and peripherals. The project gives me a very distinct “This is how we do things” vibe, and it’s quite unlike any other Raspberry Pi Pico project.

To show you what MMBasic code looks like, here’s a little demo that uses one of those “Open Smart” LED traffic lights on physical pins 14-16 which cycles through the phases every few seconds:

' traffic light on gp10-12 (green, yellow, red), pins 14-16

' set up ports for output
FOR i=14 TO 16


  ' green on for 5 seconds
  PAUSE 5000
  ' amber on for 3 seconds
  PAUSE 3000
  ' red on for 5 seconds
  PAUSE 5000

Some okay CMYK results from DrawingBot, finally …

decorative 8-sided symmetrical square tile sketched out in cyan, magenta, yellow and grey felt tip pen on a plotter
drawn using a Roland DXY-1300 plotter on Strathmore Multimedia using DeSerres medium tip pens, ~60 minutes plotting time, 180 × 180 mm

After a tonne of faffing about, I finally got something out of my plotter using Drawing Bot. I’d heard about it during the Bold Machines’ Intro to Pen Plotters course I’m taking, and the results that other people were getting looked encouraging. But for me, they weren’t great.

Maybe I was choosing too large images, but my main problem was ending up with plots with far too many lines: these would take days to plot. The controls on Drawing Bot also seemed limited: density and resolution seemed to be the only controls that do much. Drawing Bot itself wasn’t very reliable: it would sometimes go into “use all the cores!” mode when it was supposed to be idling. It would also sometimes zoom in on part of the image and fail to unzoom without quitting. Is a 32 GB i7 8-core (oldish, but still game) too little for this software? Forget any of the Voronoi plots if you want to see results today.

The source image was a geometric tile that I’d frisketed out years ago, forgotten about, and then found when I unstuck it from under a stack of papers. It’s somewhat artisanally coloured by me in watercolour, and the mistakes and huge water drop are all part of its charm:

geometric tile picked out in brown, red, pink, green  and various shades of faded blue, separated by rough white frisket lines
source image for plotter output

If WordPress will allow an SVG, here’s what Drawing Bot made of it:

scribbly linedrawing of the tile image in CMYK process colours
Drawing Bot SVG output: yes, it’s that faint

I do like the way that Drawing Bot seems to have ignored some colours, like the rose pink around the outside. The green border really is mostly cyan with a touch of black.

I haven’t magically found perfect CMYK pens in HP/Roland pen format. I couldn’t even find the Schwan-Stabilo Point 88 pens that Lauren Gardner at Bold Machines recommends. But the local DeSerres did deliver a selection of their own-brand 1.0mm Mateo Markers that are physically close to the Point 88s in size, but use a wider 1 mm fibre tip. They are also cheap; did I mention that?

The colours I chose were:

  • for cyan: Mint Green; RGB colour: #52C3A5; SKU: DFM-53
  • for magenta: Neon Pink; RGB colour: #FF26AB; SKU: DFM-F23
  • for yellow: Neon Yellow; RGB colour: #F3DE00; SKU: DFM-F01
  • for black: Green Grey 5; RGB colour: #849294; SKU: DFM-80

The RGB colours are from DeSerres’ website, and show that I’m not wildly off. Target process colours are the top row versus nominal pen colours on the bottom:

target vs pen CMYK colours
yes, there are fluo colours in there

I knew to avoid pure black, as it would overpower everything in the plot.

To make the pens work with the DXY-1300, I modified juliendorra/3D-printable-plotter-adapters-for-pens-and-refills: Use your favorite pens with vintage HP plotters: parametric code to create custom adapters to work the the DeSerres pens. Here are my changed files, just in case my PR isn’t accepted:

Overall, it plotted quite well. I plotted directly from Inkscape, one layer/pen at a time, from light (yellow) to dark (grey). Using the pen 1 slot had its disadvantages: the DXY has little pen boots to stop the pens drying, but these unfortunately get filled with old ink. The scribbly dark markings in the NNE and SSW orange kites in the plot are from the yellow pen picking up old black ink from the pen boot. Next time I’ll clean the plotter better.

Tetris for Applesoft BASIC

a tetris game tableau, paused, with a completed line of bricks about to be removed
a very paused game

Paleotronic’s Tetris for Applesoft BASIC is a surprisingly decent version for something that’s written in an interpreted language. It’s not what anyone would call zippy, but it’s not so slow that you want to give up.

Paleotronic want you to type it in, but life’s too short for that. You can play it in your browser on the Internet Archive: Tetris for Applesoft BASIC by Mark Stock

Or download an auto-booting Apple II disk image:


  • , — move left
  • . — move right
  • A — rotate left
  • R — rotate right
  • Z — drop
  • P — pause
  • Q — quit

Life’s also too short for correcting OCR errors in BASIC code. Tesseract is hilariously bad at recognizing source code, so I had to go through this several times. AppleCommander’s BASIC Tools was very handy for catching the last of the errors with its variable dump: caught the cases of the TO keyword converted to the variable T0 … and frankly, I am no fan of SmartQuotes when applied to source code, either.

Here’s the source, straight from the interpreter with all its weird spacing:

10  GOSUB 1000
100 W = W +1: IF W >LV  THEN W = 0: GOSUB 350
110 K =  PEEK(KB): IF K > = H  THEN  POKE KC,H:K = K -H: GOSUB 300
190  GOTO 100
225 PY = PY *A2: HLIN X1,X2 AT PY: HLIN X1,X2 AT PY +A1: RETURN 
300  ON E(K) GOTO 30000,330,340,350,360,30100
310  RETURN 
330 X = X -1: GOTO 400
340 X = X +1: GOTO 400
350 DN = 1:Y = Y +1: GOSUB 400:DN = 0: RETURN 
360 S = S +1: IF S/4 =  INT(S/4)  THEN S = S -4
400  GOSUB 500
410  GOSUB 800: IF F = 0  THEN X = XX:Y = YY:S = SS: GOSUB 420: IF DN  THEN  GOSUB 900
420  COLOR= CF: FOR PP = 1 TO 4:PX = X +X(S,PP):PY = Y +Y(S,PP): GOSUB 200: NEXT PP:XX = X:YY = Y:SS = S:D = 0: RETURN 
510  COLOR= CB: FOR PP = 1 TO 4:PX = XX +X(SS,PP):PY = YY +Y(SS,PP): GOSUB 200: NEXT PP:DD = 0: RETURN 
800 F = 1: FOR PP = 1 TO 4:PY = Y +Y(SS,PP): ON ( FN PC(X +X(S,PP)) >0) GOTO 805: NEXT PP: RETURN 
805 F = 0: RETURN 
850 F = 1: RETURN 
900 P = 10: GOSUB 30300
905 RN = 0:Y = YM
910 X = XL
920 PY = Y: IF  FN PC(X) = CB  THEN 950
930 X = X +1: IF X < = XR  THEN 920
940 R(RN) = Y:RN = RN +1
950 Y = Y -1: IF Y > = 0  THEN 910
960  IF RN  THEN  GOSUB 30400
970 Y = 0
980 X =  INT((XR -XL)/2) +XL
985 S =  INT( RND(1) *NS):CF = C(S):S = S *4
995  GOTO 31000
1000  DIM E(127),X(27,4),Y(27,4),R(40)
1010  TEXT : HOME : GR 
1011  PRINT "WELCOME..."
1014 LM = 10
1015 XM = 10:YM = 15
1016 XL =  INT((40 -XM)/2)
1017 XR = XL +XM -1
1021 A1 = 1
1022 A2 = 2
1030  DEF  FN PC(X) =  SCRN( X,PY *A2)
1040 CB = 0
1050 XX = 20:YY = 0:SS = 0
1100 KB =  -16384
1110 KC =  -16368
1120 H = 128
1130  REM QUIT
1131 E( ASC("Q")) = 1
1132 E( ASC("Q") -64) = 1
1141 E(8) = 2
1142 E( ASC(",")) = 2
1151 E(21) = 3
1152 E( ASC(".")) = 3
1161 E(32) = 4
1162 E( ASC("Z")) = 4
1171 E( ASC("R")) = 5
1172 E(13) = 5
1173 E( ASC("A")) = 5
1180 E( ASC("P")) = 6
1181 E( ASC("P") -64) = 6
1185  GOSUB 2000
1186  GOSUB 1300
1191  PRINT 
1193  GOTO 31020
1300  COLOR= 4: FOR I = 0 TO 19:X1 = 0:X2 = 39:PY = I: GOSUB 225: NEXT 
1320  COLOR= CB: FOR I = 0 TO YM:X1 = XL:X2 = XR:PY = I: GOSUB 225: NEXT 
1350  RETURN 
1400  DATA 1
1401  DATA 0,0,1,0,0,1,1,1
1402  DATA 0,0,1,0,0,1,1,1
1403  DATA 0,0,1,0,0,1,1,1
1404  DATA 0,0,1,0,0,1,1,1
1410  DATA 2
1411  DATA 0,1,1,1,2,1,3,1
1412  DATA 1,0,1,1,1,2,1,3
1413  DATA 0,1,1,1,2,1,3,1
1414  DATA 1,0,1,1,1,2,1,3
1420  DATA 12
1421  DATA 1,1,0,1,1,0,2,1
1422  DATA 1,1,0,1,1,0,1,2
1423  DATA 1,1,0,1,2,1,1,2
1424  DATA 1,1,1,0,2,1,1,2
1430  DATA 13
1431  DATA 1,1,0,1,2,1,0,2
1432  DATA 1,1,1,0,1,2,2,2
1433  DATA 1,1,0,1,2,1,2,0
1434  DATA 1,1,1,0,1,2,0,0
1440  DATA 9
1441  DATA 1,1,0,1,2,1,2,2
1442  DATA 1,1,1,0,1,2,2,0
1443  DATA 1,1,0,1,2,1,0,0
1444  DATA 1,1,1,0,1,2,0,2
1450  DATA 3
1451  DATA 1,1,1,0,0,0,2,1
1452  DATA 1,1,1,0,0,1,0,2
1453  DATA 1,1,1,0,0,0,2,1
1454  DATA 1,1,1,0,0,1,0,2
1460  DATA 6
1461  DATA 1,1,0,1,1,0,2,0
1462  DATA 1,1,0,1,0,0,1,2
1463  DATA 1,1,0,1,1,0,2,0
1464  DATA 1,1,0,1,0,0,1,2
1990  DATA  -1
2000 X = 0:Y = 0
2010 NS = 0
2020  READ C: IF C < > -1  THEN C(NS) = C: FOR J = 0 TO 3: FOR I = 1 TO 4: READ X(NS *4 +J,I): READ Y(NS *4 +J,I): NEXT I: NEXT J:NS = NS +1: GOTO 2020
2030  RETURN 
21210 P = 1: RETURN 
30000  TEXT : HOME : END 
30100  HOME 
30120 P = 1
30130 K =  PEEK(KB): IF K > = H  THEN  POKE KC,H:K = K -H: GOSUB 30200
30140  IF P  THEN 30130
30150  HOME 
30160  PRINT "SCORE ";SC; TAB( 21);"LEVEL ";LM -LV +1
30170  RETURN 
30200  ON E(K) GOTO 30000,30210,30210,30210,30210,30220
30210  RETURN 
30220 P = 0
30230  RETURN 
30300 SC = SC +P
30310  VTAB 21: HTAB 7
30320  PRINT SC;
30330  RETURN 
30400 RN = RN -1
30410  FOR C = 0 TO 32
30415  COLOR= C
30420  FOR I = 0 TO RN:X1 = XL:X2 = XR:PY = R(I): GOSUB 225: NEXT I
30430  FOR I = 0 TO 2: NEXT I
30440  NEXT C
30450  FOR I = 0 TO RN
30460 Y = R(I) +I
30470 YP = Y -1: FOR X = XL TO XR:PY = YP: COLOR=  FN PC(X):PX = X:PY = Y: GOSUB 200: NEXT X:Y = Y -1: IF Y >0  THEN 30470
30480 P = 100: GOSUB 30300
30490  NEXT I
30495  RETURN 
31000  VTAB 22: PRINT 
31010  PRINT "              GAME OVER"
31020 P = 1
31030 K =  PEEK(KB): IF K > = H  THEN  POKE KC,H:K = K -H: GOSUB 31200
31040  IF P  THEN 31030
31050 D = 1
31060 SC = 0:LV = LM
31070  GOSUB 30150
31080  GOSUB 1300
31090  GOTO 905
31200  ON E(K) GOTO 30000
31210 P = 0: RETURN 

If only AppleSoft had a RENUM command, the code might not look so messy