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

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:

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.