Part III — Hardware·Chapter 8

Building the
TM1637 Driver

The TM1637 is your first real hardware driver: a chip with its own custom serial protocol that you implement from scratch using bit-banging. This chapter reads the datasheet, derives the segment encoding from the schematic, and builds a production-quality async driver complete with ACK checking and error handling.
§ 8.1
Reading a Datasheet — Turning Specification into Protocol

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.2
The Wire Protocol — Every Bit, Every Edge

Your 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.
Figure 8.1 — TM1637 wire protocol timing. Every edge, every setup/hold requirement.
§ 8.3
7-Segment Encoding — Deriving the Lookup Table

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.

segments.rs — building the digit lookup table from segment definitions
// 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
§ 8.4
The Complete Driver
tm1637.rs — full production driver
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;
    }
}
§ 8.5
Exercises
Exercise 8.1 — Elapsed Time Display

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.

Exercise 8.2 — Clock Mode with Colon

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.

Exercise 8.3 — NAK Error Handling

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.