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 -*-
from ws2812 import WS2812
ring = WS2812(spi_bus=2, led_count=8, intensity=0.1)
data = [(0, 0, 0) for i in range(led_count)]
step = 0
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)
step += 1
for data in data_generator(ring.led_count):
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 …
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.
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.
Let’s start with the STM32 Screw Terminal Adapter:
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.
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:
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?
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)
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!
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!]
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.
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
# touch pins, anticlockwise from battery connector
# 16 kHz 16-bit mono audio files, in same order as pins
chord_files = [
# 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))
# set up speaker output
speaker_enable = digitalio.DigitalInOut(board.SPEAKER_ENABLE)
# poll touch pins
for i in range(len(touch_pins)):
# if a pin is touched
# set nearest pixel
pixels[chord_pixels[i]] = (0, 0x10, 0)
# open and play corresponding file
a = audioio.AudioOut(board.A0, f)
# blank nearest pixel
pixels[chord_pixels[i]] = (0, 0, 0)
# short delay to let chord sound
# might want to try this a little shorter for faster play
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:
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.
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?