Building the
TM1637 Driver
The TM1637 is a chip made by Titan Micro Electronics specifically for driving 7-segment LED displays. It communicates over a 2-wire serial protocol that resembles I2C but is not I2C — it has a different timing, different addressing, different ACK mechanism, and requires bit-banging rather than using the RP2350's hardware I2C peripheral. The datasheet is the authoritative source. Everything in this chapter comes from it.
The TM1637 datasheet is four pages. Three things to look for in any datasheet: the electrical characteristics (voltage levels, timing constraints, current limits), the register map (what memory addresses control what behaviour), and the timing diagrams (the exact sequence of signals required). The TM1637 has two commands you need: Data Command (0x40 — tells the chip how to write) and Display Control (0x88 | brightness — sets intensity).
§ 8.2Your bench wiring: GPIO2 = CLK (clock), GPIO3 = DIO (data). The TM1637 protocol consists of START, bytes, and STOP sequences, each with specific timing requirements.
TM1637 PROTOCOL TIMING DIAGRAM ───────────────────────────────────────────────────────────── START CONDITION: CLK ‾‾‾‾‾‾‾‾‾\_______ DIO ‾‾‾‾\___________ ↑ DIO falls while CLK is HIGH = START (opposite of STOP) STOP CONDITION: CLK _______/‾‾‾‾‾‾‾‾ DIO _________/‾‾‾‾‾‾ ↑ DIO rises while CLK is HIGH = STOP DATA BIT (LSB first): CLK ___/‾‾‾‾‾\___ DIO ═══════════ ← data valid while CLK is HIGH ↑ set DIO before rising CLK edge ACK BIT (after each byte): CLK ‾‾‾\___/‾‾‾ DIO → release (tristate) → TM1637 pulls LOW = ACK ↑ switch DIO to input for one CLK cycle if TM1637 does not pull low → NAK (device not ready) TIMING CONSTRAINTS (from datasheet): t_su = 1µs minimum setup time (DIO stable before CLK rises) t_hd = 1µs minimum hold time (DIO stable after CLK falls) Clock period ≥ 2µs → max frequency ≈ 500kHz In practice: 1µs delays between every state change is reliable.
A 7-segment display has seven LED segments labeled a through g, plus an optional decimal point. The TM1637 stores one byte per digit, where each bit controls one segment. The bit-to-segment mapping is: bit 0 = segment a (top), bit 1 = segment b (top-right), bit 2 = segment c (bottom-right), bit 3 = segment d (bottom), bit 4 = segment e (bottom-left), bit 5 = segment f (top-left), bit 6 = segment g (middle), bit 7 = decimal point.
// Segment bit positions: // _ // |_| a=bit0 top b=bit1 top-right c=bit2 bottom-right // |_| d=bit3 bot e=bit4 bot-left f=bit5 top-left // g=bit6 middle .=bit7 decimal point // Digit 0: segments a,b,c,d,e,f lit → bits 0,1,2,3,4,5 = 0b0011_1111 = 0x3F // Digit 1: segments b,c lit → bits 1,2 = 0b0000_0110 = 0x06 // Digit 2: segments a,b,d,e,g lit → bits 0,1,3,4,6 = 0b0101_1011 = 0x5B // ... and so on for 3-9 pub const DIGITS: [u8; 10] = [ 0x3F, // 0 0x06, // 1 0x5B, // 2 0x4F, // 3 0x66, // 4 0x6D, // 5 0x7D, // 6 0x07, // 7 0x7F, // 8 0x6F, // 9 ]; pub const DASH: u8 = 0x40; // only segment g (middle bar) pub const COLON: u8 = 0x80; // decimal point bit on digit 1 = colon on TM1637 pub const BLANK: u8 = 0x00; // all segments off
use embassy_rp::gpio::{Flex, Level}; use embassy_time::{Duration, Timer}; const DELAY_US: u64 = 2; // 2µs between state changes — reliable at 250kHz pub struct Tm1637<'d> { clk: Flex<'d>, dio: Flex<'d>, } impl<'d> Tm1637<'d> { pub fn new(mut clk: Flex<'d>, mut dio: Flex<'d>) -> Self { clk.set_as_output(); clk.set_high(); dio.set_as_output(); dio.set_high(); Self { clk, dio } } async fn delay(&self) { Timer::after(Duration::from_micros(DELAY_US)).await; } async fn start(&mut self) { self.dio.set_as_output(); self.dio.set_high(); self.delay().await; self.clk.set_high(); self.delay().await; self.dio.set_low(); self.delay().await; // DIO falls while CLK high = START self.clk.set_low(); self.delay().await; } async fn stop(&mut self) { self.dio.set_as_output(); self.dio.set_low(); self.delay().await; self.clk.set_high(); self.delay().await; self.dio.set_high(); self.delay().await; // DIO rises while CLK high = STOP } async fn write_byte(&mut self, byte: u8) -> bool { for i in 0..8 { self.clk.set_low(); self.dio.set_as_output(); if (byte >> i) & 1 == 1 { self.dio.set_high(); } else { self.dio.set_low(); } self.delay().await; self.clk.set_high(); self.delay().await; } // ACK bit: release DIO, let TM1637 pull low self.clk.set_low(); self.dio.set_as_input(); self.delay().await; self.clk.set_high(); let ack = self.dio.is_low(); // true = ACK, false = NAK self.delay().await; self.clk.set_low(); ack } pub async fn show_number(&mut self, n: u16, brightness: u8) { let segs = [ DIGITS[(n / 1000) as usize], DIGITS[(n / 100 % 10) as usize], DIGITS[(n / 10 % 10) as usize], DIGITS[(n % 10) as usize], ]; // Phase 1: Data command — auto-increment address mode self.start().await; self.write_byte(0x40).await; // data command self.stop().await; // Phase 2: Address + data — write all 4 digits self.start().await; self.write_byte(0xC0).await; // address 0 for s in segs { self.write_byte(s).await; } self.stop().await; // Phase 3: Display on + brightness self.start().await; self.write_byte(0x88 | (brightness.min(7))).await; self.stop().await; } }
Show seconds since boot on the TM1637
Using the driver from §8.4, write a task that displays elapsed seconds since boot on the TM1637. Reset to 0000 after 9999. Brightness level 3. Log the time every 10 seconds to defmt. Verify the display increments once per second and the brightness is comfortable for a desk display.
Show a blinking colon for a clock display
Add a colon blink to the display. The TM1637's colon is controlled by setting bit 7 (0x80) of the second digit. Display HH:MM where HH starts at 00 and MM increments every 60 seconds. Make the colon blink at 1Hz — on for 500ms, off for 500ms. The visual should resemble a digital alarm clock. This exercise requires managing two independent time-based behaviours in a single task.
Handle device not-ready gracefully
Modify write_byte to return Result<(), Tm1637Error> where Tm1637Error::Nak is returned when the ACK bit is not received. Modify show_number to propagate this error. In your task, retry up to 3 times on NAK before logging an error and continuing. Simulate a NAK by briefly disconnecting the DIO wire — the display should recover within 3 frames when reconnected.