# Wrist Timecode Device — MicroPython for RP2040
# Display: MAX7219 8-digit 7-seg (SPI)
# RTC: DS1307 (I2C) — stands in for DS3231 in Wokwi
# SW: GP15 — TC on/off
# BTN: GP14 — show battery %
# ADC: GP26 — voltage divider for battery
from machine import Pin, SPI, I2C, ADC
import utime
# ─── MAX7219 driver ──────────────────────────────────────────
class MAX7219:
REG_NOOP = 0x0
REG_DIGIT = [0x1,0x2,0x3,0x4,0x5,0x6,0x7,0x8]
REG_DECODE = 0x9
REG_INTENSITY = 0xA
REG_SCANLIMIT = 0xB
REG_SHUTDOWN = 0xC
REG_TEST = 0xF
# 7-seg encoding: 0-9, dash, blank, letters
CHARS = {
'0': 0x7E, '1': 0x30, '2': 0x6D, '3': 0x79,
'4': 0x33, '5': 0x5B, '6': 0x5F, '7': 0x70,
'8': 0x7F, '9': 0x7B, '-': 0x01, ' ': 0x00,
'b': 0x1F, 'A': 0x77, 't': 0x0F, 'E': 0x4F,
'r': 0x05, 'P': 0x67, '%': 0x63
}
def __init__(self, spi, cs, num_digits=8):
self.spi = spi
self.cs = cs
self.n = num_digits
self.cs.value(1)
self._write(self.REG_SHUTDOWN, 0x1) # normal op
self._write(self.REG_TEST, 0x0)
self._write(self.REG_DECODE, 0x0) # raw seg mode
self._write(self.REG_SCANLIMIT, self.n - 1)
self._write(self.REG_INTENSITY, 0x4) # mid brightness
self.clear()
def _write(self, reg, data):
self.cs.value(0)
self.spi.write(bytes([reg, data]))
self.cs.value(1)
def clear(self):
for i in range(8):
self._write(self.REG_DIGIT[i], 0x00)
def show(self, text):
# text is exactly 8 chars, '.' suffix on a char sets decimal point
# e.g. "12.34.56.78" won't fit — we handle dots manually
segs = self._parse(text)
for i, s in enumerate(segs):
self._write(self.REG_DIGIT[i], s)
def _parse(self, text):
# Build list of (char, dot) tuples then pad/trim to 8
result = []
i = 0
while i < len(text):
c = text[i]
dot = False
if i + 1 < len(text) and text[i+1] == '.':
dot = True
i += 1
seg = self.CHARS.get(c, 0x00)
if dot:
seg |= 0x80
result.append(seg)
i += 1
# pad to 8
while len(result) < 8:
result.append(0x00)
return result[:8]
def set_brightness(self, level): # 0-15
self._write(self.REG_INTENSITY, level & 0xF)
# ─── DS1307 RTC driver ───────────────────────────────────────
class DS1307:
ADDR = 0x68
def __init__(self, i2c):
self.i2c = i2c
def _bcd2dec(self, b):
return (b >> 4) * 10 + (b & 0x0F)
def _dec2bcd(self, d):
return ((d // 10) << 4) | (d % 10)
def get_time(self):
data = self.i2c.readfrom_mem(self.ADDR, 0x00, 7)
sec = self._bcd2dec(data[0] & 0x7F)
min_ = self._bcd2dec(data[1])
hour = self._bcd2dec(data[2] & 0x3F)
return hour, min_, sec
def set_time(self, hour, min_, sec):
self.i2c.writeto_mem(self.ADDR, 0x00, bytes([
self._dec2bcd(sec),
self._dec2bcd(min_),
self._dec2bcd(hour),
0x01, 0x01, 0x01, 0x25 # day/date/month/year placeholder
]))
# ─── Hardware init ───────────────────────────────────────────
spi = SPI(0, baudrate=1000000, polarity=0, phase=0,
sck=Pin(18), mosi=Pin(20))
cs = Pin(19, Pin.OUT)
disp = MAX7219(spi, cs)
i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400000)
rtc = DS1307(i2c)
sw_tc = Pin(15, Pin.IN, Pin.PULL_DOWN) # slide switch TC on/off
btn_batt = Pin(14, Pin.IN, Pin.PULL_DOWN) # battery % button
adc = ADC(Pin(26)) # voltage divider
# ─── Set RTC to a known time for testing ────────────────────
rtc.set_time(10, 0, 0) # 10:00:00
# ─── State ───────────────────────────────────────────────────
tc_running = False
show_batt_until = 0 # timestamp to stop showing battery
BATT_SHOW_MS = 3000 # show battery for 3 seconds
prev_sw = 0
prev_btn = 0
# sub-second counter using utime.ticks_ms
last_rtc_read = utime.ticks_ms()
last_sec = -1
subsec_ms = 0
def read_battery_pct():
# Voltage divider: 3V3 → R1(100k) → ADC → R2(100k) → GND
# ADC reads Vbat/2, full scale 3.3V = 65535
# LiPo: 3.0V (0%) to 4.2V (100%)
raw = adc.read_u16()
v_adc = raw / 65535 * 3.3 # voltage at ADC pin
v_batt = v_adc * 2 # actual battery voltage
pct = (v_batt - 3.0) / (4.2 - 3.0) * 100
pct = max(0, min(100, int(pct)))
return pct
def format_timecode(h, m, s, cs):
# HH.MM.SS.cc — dots between each pair
return "{:02d}.{:02d}.{:02d}.{:02d}".format(h, m, s, cs)
def show_battery(pct):
# Shows "bAt 87" or "bAt 100"
s = "bAt {:3d}".format(pct)
disp.show(s)
# ─── Main loop ───────────────────────────────────────────────
while True:
now_ms = utime.ticks_ms()
# ── Slide switch: TC on/off (detect edge) ──
sw = sw_tc.value()
if sw != prev_sw:
tc_running = bool(sw)
if not tc_running:
disp.clear()
prev_sw = sw
# ── Battery button (detect press edge) ──
btn = btn_batt.value()
if btn and not prev_btn:
show_batt_until = utime.ticks_add(now_ms, BATT_SHOW_MS)
prev_btn = btn
# ── Show battery % if active ──
if utime.ticks_diff(show_batt_until, now_ms) > 0:
pct = read_battery_pct()
show_battery(pct)
utime.sleep_ms(100)
continue
# ── TC display ──
if tc_running:
# Read RTC every second, track sub-seconds locally
elapsed = utime.ticks_diff(now_ms, last_rtc_read)
if elapsed >= 1000 or last_sec == -1:
h, m, s = rtc.get_time()
last_sec = s
last_rtc_read = utime.ticks_add(last_rtc_read, 1000) if elapsed < 1500 else now_ms
subsec_ms = elapsed % 1000
# centiseconds from local timer
cs_val = (utime.ticks_diff(now_ms, last_rtc_read) % 1000) // 10
tc = format_timecode(h, m, last_sec, cs_val)
disp.show(tc)
utime.sleep_ms(20) # ~50fps update rate