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

Seeeduino XIAO simple USB volume control with CircuitPython

round computer device with USB cable exiting at left. Small microcontroller at centreshowing wiring to LED ring and rotary encoder
Slightly blurry image of the underside of the device, showing the Seeeduino XIAO and the glow from the NeoPixel ring. And yes, the XIAO is really that small

Tod Kurt’s QTPy-knob: Simple USB knob w/ CircuitPython is a fairly simple USB input project that relies on the pin spacing of an Adafruit QT Py development board being the same as that on a Bourns Rotary Encoder. If you want to get fancy (and who wouldn’t?) you can add a NeoPixel Ring to get an RGB glow.

The QT Py is based on the Seeeduino XIAO, which is a slightly simpler device than the Adafruit derivative. It still runs CircuitPython, though, and is about the least expensive way of doing so. The XIAO is drop-in replacement for the Qt Py in this project, and it works really well! Everything you need for the project is described here: todbot/qtpy-knob: QT Py Media Knob using rotary encoder & neopixel ring

I found a couple of tiny glitches in the 3d printed parts, though:

  1. The diffuser ring for the LED ring is too thick for the encoder lock nut to fasten. It’s 2 mm thick, and there’s exactly 2 mm of thread left on the encoder.
  2. The D-shaft cutout in the top is too deep to allow the encoder shaft switch to trigger.

I bodged these by putting an indent in the middle of the diffuser, and filling the top D-shaft cutout with just enough Blu Tack.

Tod’s got a bunch of other projects for the Qt Py that I’m sure would work well with the XIAO: QT Py Tricks. And yes, there’s an “Output Farty Noises to DAC” one that, regrettably, does just that.

Maybe I’ll add some mass to the dial to make it scroll more smoothly like those buttery shuttle dials from old video editing consoles. The base could use a bit more weight to stop it skiting about the desk, so maybe I’ll use Vik’s trick of embedding BB gun shot into hot glue. For now, I’ve put some rubber feet on it, and it mostly stays put.


Hey! Unlike my last Seeed Studio device post, I paid for all the bits mentioned here.

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.