Dave's RP2040 Multitimer

Created: 2021-11-27 Updated: 2021-11-28

Repo link for the impatient: https://github.com/ratfactor/pico-multitimer

my hardware multi-timer in action with pretty colors

This is my multiple-category project/productivity timer with RGB keys (real keyboard switches and keycaps!) and a colorful RGB backlit LCD.

It’s a device I couldn’t buy for any price.

I’ve tried software timers (and have made several) for desktop and phones. But when tasks start switching quickly (exactly when I want to track them the most), it’s always been too much of a hassle to operate a software timer. I really wanted to be able to hit a single key to switch timer categories.

drawing from my pocket notebook

I made a super short video of it in action:

My device is perhaps extreme overkill, but didn’t really cost all that much to make. It could have been made much cheaper. I happily paid more for components that were intended to make life easier for hobby-level makers such as myself. Almost all hardware communication uses the 2-wire I2C protocol. The keypad and LCD both come with their own microcontrollers to facilitate this, which means this timer actually has at least three computers onboard.

Hardware components

Off-the-shelf hardware:

  • Raspberry Pi Pico microcontroller (RP2040 32-bit dual ARM Cortex-M0+)

  • Adafruit NeoKey 1x4 QT I2C Breakout (Cherry-compatible keypad + NeoPixels)

  • SparkFun SerLCD (RGB 16x2 LCD + AVR microcontroller with I2C, etc.)

  • Qwiic (Sparkfun) aka STEMMA QT (Adafruit) 4 Pin JST SH connector mini-breakout

TODO: link the above

First steps

By the end of August, I learned the basics of the RPi Pico and MicroPython by reading and doing the exercises in the official book, Get Started with MicroPython on Raspberry Pi Pico by Gareth Halfacree and Ben Everard (which I found to be absolutely delightful, by the way. An extremely excellent introduction, guide, and reference for all ages).

breadboarding with the pico and the book

A single weekend was all it took to get comfortable with the device thanks to the book and the friendly nature of the Pico itself.

(Note: I initially tried to get an LCD I already owned working. But I bought the SerLCD after seeing in the Pico book. I highly recommend that path for other beginners.)

Breadboard prototype and programming

I got the basic timing functionality and the SerLCD working first.

The Neokey keypad took longer. Adafruit has a fork of MicroPython called CircuitPython which is used in their examples and libraries.

breadboard prototype works and has lots of pretty colors

I chose not to use any external libraries. But I referenced the Sparkfun and Adafruit libraries for Python/Micropython/Circuitpython.

Check out the reference directory with the specific library source I used to learn how the devices work: https://github.com/ratfactor/pico-multitimer/tree/master/reference

Some things were as simple as blinking the onboard LED. Here’s the pin setup:

pico_led = machine.Pin(25, machine.Pin.OUT)

The timer is interrupt-driven. This was wonderfully simple with the RP2040. Here we can see the onboard LED blinking every second:

# Create an interrupt every second from the RTC (Real Time Clock)
timer = Timer(period=1000, mode=Timer.PERIODIC, callback=per_second)

...

def per_second(arg1):

    # Wink Pico's on-board LED so we can see the ticks
    pico_led.value(not pico_led.value())

    ...

Note that my comment says "Real Time Clock", but it’s actually a "Real Time Counter" because there’s no separate clock with a battery to keep "wall time". The Pico has no idea what the current date or time of day it is.

All communication with the SerLCD display and NeoKey keypad are done on a single I2C bus. That is, the same two pins on the Pico connect to both devices. Each device answers to a particular hard-coded address or ID number. Here’s the I2C setup for pins 0 (data) and 1 (clock):

bus_id = 0   # i2c bus 0 on pi pico
sda = machine.Pin(0)
scl = machine.Pin(1)
i2c = machine.I2C(bus_id, sda=sda, scl=scl, freq=200000)

Here are the default hard-coded addresses of the devices:

SERLCD_ID = 114 # sparkfun SerLCD default
NEOKEY_ID = 48  # adafruit NeoKey 1x4 default

Most MicroPython (and CircuitPython) libraries seem to lean heavily on the Object-Oriented Programming paradigm, which makes perfect sense when there are potentially multiple devices of a given type in the build.

However, since I have one of each, I chose to make extremely lightweight wrapper read/write functions for each. The write functions take a list of data which will be converted into a bytearray (which, vitally, supports the buffer operations needed by the processor for the I2C communication specifically, writeto()).

def write_lcd(byte_list):
    #print("writing",byte_list,"to",SERLCD_ID)
    i2c.writeto(SERLCD_ID, bytearray(byte_list))
    utime.sleep_ms(10)

def write_neokey(byte_list):
    #print("writing",byte_list,"to",NEOKEY_ID)
    i2c.writeto(NEOKEY_ID, bytearray(byte_list))

def read_lcd(amount_bytes):
    return i2c.readfrom(SERLCD_ID, amount_bytes)

def read_neokey(amount_bytes):
    return i2c.readfrom(NEOKEY_ID, amount_bytes)

The SerLCD was the easiest to get going since Get Started with MicroPython on Raspberry Pi Pico had a simple example. There were only a couple tricky bits to look up in the Sparkfun driver library for slightly more advanced usage.

Here’s the entire setup:

# All SerLCD commands start with one of two prefixes. Everything else is
# treated as character data to be written directly to the screen.
LCD_SETCMD     = 0x7C
LCD_CLEAR      = [LCD_SETCMD, 0x2D]
LCD_CONTRAST   = [LCD_SETCMD, 0x18] # followed by 0-255 contrast level
LCD_SETRGB     = [LCD_SETCMD, 0x2B] # followed by 0xFFFFFF style byte list (RGB)
LCD_SPECIALCMD = [0xFE]
LCD_SETDDRAMADDR = 0x80

# Set contrast and clear screen.
# (Contrast was determined by a little looping test program.)
#write_lcd(LCD_CONTRAST + [0xAA])
write_lcd(LCD_CLEAR)
write_lcd(LCD_SETRGB + [0xFF, 0xFF, 0xFF])

write_lcd("Once upon a time...")

Note that the SerLCD stores settings, so it’ll power back up with the same contrast, backlight color and brightness you last set. That’s why the contrast line is commented out above.

The SerLCD "wraps" lines of text, so if you write a long sentence that doesn’t fit on the first line, it automatically wraps the sentence onto the second line.

However, I wanted to put my timer counts on the second line. It turns out that you simply position the "cursor" (just a pointer to memory) at a specific offset to accomplish this. Since the LCD microcontroller supports larger sized LCD displays, the offset is 64 (0x40) characters from the start address, allowing for 64 character-wide lines.

Here’s my simple function to position the cursor at the start of line two:

def write_lcd_line_two():
    # Setting DRAM address is a "special command"
    # Addr 0 is the first character on the first row.
    LCD_SETDDRAMADDR = 0x80
    write_lcd(LCD_SPECIALCMD + [LCD_SETDDRAMADDR | 0x40])

The Adafruit NeoKey is a subtype of Seesaw, which is Adafruit’s microcontroller-based general purpose I/O device with the ability to communicate with I2C and has a driver for "NeoPixel" RGB LEDs.

Since the Seesaw controller has many sub-functions, it has a sort of internal addressing to let you specify which portion of the functionality to access and then the specific action.

I won’t put the whole setup preamble here (see the repo linked above for that) but here’s the first thing I got working - a write and read (the print() output shows up in a console hooked up to the Raspberry Pi Pico’s serial communication (in my case, in the Thonny Python IDE connected via USB)):

# Reset seesaw and get the ID just to prove we can talk to it
write_neokey_status([SS_SWRST, 0xFF]) # reset (write to reset "register")
utime.sleep_ms(500)
write_neokey_status([SS_HW_ID]) # request chip id
utime.sleep_ms(10)
chip_id = read_neokey(1) # read (bytes)
print("seesaw chip id:", chip_id)

The NeoPixel LEDs are, themselves individually addressable and are connected to pin 3 on the NeoKey:

# Set neopixel pin 3, buffer length 12 bytes (3x4 pixels)
write_neopixel([NP_PIN, 3])
write_neopixel([NP_BUF_LENGTH, 0, 12]) # two bytes: 00 12

(So now we’re at least three layers deep in the communication chain. This "simple" timer is like a whole computer network of its own!)

Getting the NeoPixels to light up a specific color means writing a series of bytes to specify RGB values for each LED device:

neopixel_buffer_cmd = [
    NP_BUF,
    0, 0, # start address
    0x11, 0x00, 0x00,
    0x00, 0x33, 0x00,
    0x00, 0x00, 0x33,
    0x00, 0x33, 0x33,
    ]

write_neopixel(neopixel_buffer_cmd)
write_neopixel([NP_SHOW])

Again, this is not a complete listing. And yet the entire multitimer source is still under 300 lines!

By far, the hardest part of the entire timer was figuring out how to set up the keypad switches to be read from the GPIO pins on the Seesaw and enabling the interrupt line (which changes voltage on a wire connected to a GPIO pin on the Pico):

# Set keypad pins to PULLUP and enable interrupts
kp_pins = [0, 0, 0, 0b11110000] # pins 4-7 (out of 64 possible bits)
write_gpio([GP_DIRCLR] + kp_pins)     # input
write_gpio([GP_PULLENABLE] + kp_pins) # enable pullup
write_gpio([GP_SET] + kp_pins)        # enable I/O
utime.sleep_ms(10)
write_gpio([GP_INTENSET] + kp_pins)

The above lines took several weekends to accomplish.

Though a more careful reading of the Adafruit library source would have done the trick, I earned myself some major geek points by decoding the I2C traffic with my oscilloscope. Since I found it very difficult to learn how to do that with my 'scope, I wrote up a whole article about it so others will hopefully have an easier time:

It was incredibly exciting to get the keypad working with the Pico using nothing but my own program!

Finally, the interrupt from the NeoKey Seesaw (when a key is pressed) is easily handled by MicroPython on the Pico:

# Setup IRQ and handler for keypress interrupt from neokey
def on_keypress(pin):
    micropython.schedule(handle_keypress, 0) # second param not used

# Set IRQ for the interrupt coming from the keypad into the pico
p14 = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_UP)
p14.irq(on_keypress, machine.Pin.IRQ_FALLING)

(Careful readers will notice that the real interrupt handler is scheduled for calling so that the interrupt is processed as fast as possible. The MicroPython docs have a good explanation for why this is the way to go.)

The vital part of the handler function reads the key pressed from the NeoKey:

def handle_keypress(throwaway_arg):
    # Get keypress statuses
    write_gpio([GP_INTFLAG])
    utime.sleep_ms(10)
    flags = read_neokey(4) # bytes

    ...timer logic...

Putting it all together with the one second interrupt from the Pico RP2040’s RTC is then just a "simple matter of programming":

  • Interrupt from the NeoKey lets us know key is pressed

  • Current timer "category" set according to NeoKey key pressed

  • LCD backlight is changed to match NeoKey RGB color of the pressed key

  • Interrupt every second from Pico RTC timer increments current category timer

  • Seconds converted to minutes with integer division

  • If the minutes have increased, update the LCD display to show current totals

That’s it!

The enclosure

Once I had the prototype working, I needed to put it in some sort of enclosure. Nothing fancy, I just wanted to be able to punch those keys and use my timer!

The "box" started life as a piece of old nasty pine 2x4 lumber.

a nasty old 2x4 pine board

I knew I wanted a lower section for the keypad and an angled upper section for the LCD display. The Pico "brains" would live in a hollowed-out area underneath.

the enclosure is roughed in

I have very little experience with wood chisels. It was slow, rough work at first. I spaced it out over several weekends and rapidly gained confidence.

the keypad area is hollowed out

I simply glued the keypad and LCD portions together. The biggest challenge in making the enclosure was getting the LCD to sit flush. I just went really slowly, carving out space for the various components on the back of the PCB.

This was all done sitting cross-legged on my back porch. Chip a little out. Test fit. Chip a little more. It was very meditative.

the final enclosure - still super rough

In the end, it’s incredibly rough. But funtional, which is all I ever wanted.

I worked on this really slowly - mostly just here and there on weekends. I also waited a lot as I ordered parts (cables, tiny screws to secure the PCBs), but it still only took two months.

Using it

It works great! I finally made it display seconds rather than minutes last weekend. But it’s been working for a full month. I absolutely love the keypad and overall funtionality.

What I don’t love is the fact that it depends on external USB power and there’s no other on/off switch. So I have to plug/unplug it each day to use it.

I do wish it ran on a battery and were generally more portable.

Time to start on a MultiTimer Version Two?

I guess the next frontier is trying a little OLED screen (and only powering it on when requested) and then powering it from a rechargable batter and, finally, designing a custom PCB, which I understand is becoming quite accessible for hobbyists such as myself.

animation of the timer in action