RPI Pico with the TM1637 display

In this chapter, we'll connect a TM1637 display to the Raspberry Pi Pico board. The TM1637 is a 4-digit 7-segment display with a built-in driver. It's a very popular display for small projects, and it's easy to use.

We'll add a new module to the project to handle the display, and we'll update the main program to read the value of the potentiometer and display it on the TM1637 display.

This makes it (hopefully) easy to reuse the display in other projects.

Getting prepared

In order to build this project, you need the following:

Wiring

Connect the TM1637 display to the Pico board like this:

TM1637 schematic


Update main.rs to look like this:

#![no_std]
#![no_main]

use defmt::*;
use embassy_executor::Spawner;
use embassy_rp::{
    adc::{self, Adc, Async, Config, InterruptHandler},
    bind_interrupts,
    gpio::{Level, OutputOpenDrain, Pull},
};
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
use embassy_sync::channel::{Channel, Receiver, Sender};
use embassy_time::Timer;
use {defmt_rtt as _, panic_probe as _};

use crate::tm1637::{DIGITS, TM1637};

mod tm1637;

bind_interrupts!(struct Irqs {
    ADC_IRQ_FIFO => InterruptHandler;
});

static CHANNEL: Channel<ThreadModeRawMutex, u16, 64> = Channel::new();

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let p = embassy_rp::init(Default::default());

    info!("Setting up TM1637");
    let clock_pin = OutputOpenDrain::new(p.PIN_14, Level::Low);
    let dio_pin = OutputOpenDrain::new(p.PIN_15, Level::Low);
    spawner
        .spawn(display_numbers(
            TM1637::new(clock_pin, dio_pin),
            CHANNEL.receiver(),
        ))
        .unwrap();

    info!("Setting up ADC");
    let adc = Adc::new(p.ADC, Irqs, Config::default());
    let p26 = adc::Channel::new_pin(p.PIN_26, Pull::None);

    // spawn the task that reads the ADC value
    spawner
        .spawn(read_adc_value(adc, p26, CHANNEL.sender()))
        .unwrap();

    loop {
        Timer::after_millis(500).await;
    }
}

#[embassy_executor::task(pool_size = 2)]
async fn read_adc_value(
    mut adc: Adc<'static, Async>,
    mut p26: adc::Channel<'static>,
    tx_value: Sender<'static, ThreadModeRawMutex, u16, 64>,
) {
    let mut measurements = [0u16; 10];
    let mut pos = 0;

    loop {
        measurements[pos] = adc.read(&mut p26).await.unwrap();
        pos = (pos + 1) % 10;

        if pos == 0 {
            // compute average of measurements
            let average = measurements.iter().sum::<u16>() / 10;

            // send average to main thread
            tx_value.send(average).await;
        }

        Timer::after_millis(100).await;
    }
}

#[embassy_executor::task(pool_size = 2)]
async fn display_numbers(
    mut tm: TM1637<'static, 'static>,
    rx_measurement: Receiver<'static, ThreadModeRawMutex, u16, 64>,
) {
    let mut on = true;

    loop {
        let mut measurement = rx_measurement.receive().await;

        // truncate the temperature to 4 digits
        measurement = measurement.min(9999);

        // decide the brightness level based on the measurement
        // and whether the display should be on or off; for the maximum values we'll blink the display
        // by toggling the on/off state
        let brightness;
        (brightness, on) = match measurement {
            0..=2000 => (0, true),
            2001..=4000 => (2, true),
            _ => (5, !on),
        };

        info!(
            "measured: {} and brightness: {} {}",
            measurement, brightness, on
        );

        // split the last 4 digits of the temperature into individual digits
        let thousands = measurement / 1000;
        measurement -= thousands * 1000;
        let hundreds = measurement / 100;
        measurement -= hundreds * 100;
        let tens = measurement / 10;
        measurement -= tens * 10;
        let ones = measurement;

        let digits: [u8; 4] = [
            DIGITS[thousands as usize],
            DIGITS[hundreds as usize],
            DIGITS[tens as usize],
            DIGITS[ones as usize],
        ];

        tm.display(digits, brightness, on).await;
        Timer::after_millis(500).await;
    }
}

And add the tm1637.rs file:

#![allow(unused)]
fn main() {
use embassy_rp::gpio::OutputOpenDrain;
use embassy_time::Timer;

const MAX_FREQ_KHZ: u64 = 500;
const USECS_IN_MSEC: u64 = 1_000;
const DELAY_USECS: u64 = USECS_IN_MSEC / MAX_FREQ_KHZ;

const ADDRESS_AUTO_INCREMENT_1_MODE: u8 = 0x40;
const ADDRESS_COMMAND_BITS: u8 = 0xc0;
const ADDRESS_COMM_3: u8 = 0x80;

const DISPLAY_CONTROL_BRIGHTNESS_MASK: u8 = 0x07;

// SEG_A 0b00000001
// SEG_B 0b00000010
// SEG_C 0b00000100
// SEG_D 0b00001000
// SEG_E 0b00010000
// SEG_F 0b00100000
// SEG_G 0b01000000
// SEG_DP 0b10000000

//
//      A
//     ---
//  F |   | B
//     -G-
//  E |   | C
//     ---
//      D
pub(crate) const DIGITS: [u8; 16] = [
// XGFEDCBA
    0b00111111, // 0
    0b00000110, // 1
    0b01011011, // 2
    0b01001111, // 3
    0b01100110, // 4
    0b01101101, // 5
    0b01111101, // 6
    0b00000111, // 7
    0b01111111, // 8
    0b01101111, // 9
    0b01110111, // A
    0b01111100, // b
    0b00111001, // C
    0b01011110, // d
    0b01111001, // E
    0b01110001, // F
];

pub(crate) struct TM1637<'clk, 'dio> {
    clk: OutputOpenDrain<'clk>,
    dio: OutputOpenDrain<'dio>,
}

impl<'clk, 'dio> TM1637<'clk, 'dio> {
    pub fn new(clk: OutputOpenDrain<'clk>, dio: OutputOpenDrain<'dio>) -> Self {
        Self { clk, dio }
    }

    async fn delay(&mut self) {
        Timer::after_micros(DELAY_USECS).await;
    }

    fn brightness(&self, level: u8, on: bool) -> u8 {
        (level & DISPLAY_CONTROL_BRIGHTNESS_MASK) | (if on { 0x08 } else { 0x00 })
    }

    pub async fn set_brightness(&mut self, level: u8, on: bool) {
        self.start().await;
        let brightness = self.brightness(level, on);
        self.write_cmd(ADDRESS_COMM_3 + (brightness & 0x0f)).await;
        self.stop().await;
    }

    async fn send_bit_and_delay(&mut self, bit: bool) {
        self.clk.set_low();
        self.delay().await;
        if bit {
            self.dio.set_high();
        } else {
            self.dio.set_low();
        }
        self.delay().await;
        self.clk.set_high();
        self.delay().await;
    }

    pub async fn write_byte(&mut self, data: u8) {
        for i in 0..8 {
            self.send_bit_and_delay((data >> i) & 0x01 != 0).await;
        }
        self.clk.set_low();
        self.delay().await;
        self.dio.set_high();
        self.delay().await;
        self.clk.set_high();
        self.delay().await;
        self.dio.wait_for_low().await;
    }

    pub async fn start(&mut self) {
        self.clk.set_high();
        self.dio.set_high();
        self.delay().await;
        self.dio.set_low();
        self.delay().await;
        self.clk.set_low();
        self.delay().await;
    }

    pub async fn stop(&mut self) {
        self.clk.set_low();
        self.delay().await;
        self.dio.set_low();
        self.delay().await;
        self.clk.set_high();
        self.delay().await;
        self.dio.set_high();
        self.delay().await;
    }

    pub async fn write_cmd(&mut self, cmd: u8) {
        self.start().await;
        self.write_byte(cmd).await;
        self.stop().await;
    }

    pub async fn write_data(&mut self, addr: u8, data: u8) {
        self.start().await;
        self.write_byte(addr).await;
        self.write_byte(data).await;
        self.stop().await;
    }

    pub async fn display(&mut self, data: [u8; 4], brightness: u8, on: bool) {
        self.write_cmd(ADDRESS_AUTO_INCREMENT_1_MODE).await;
        self.start().await;
        let mut address = ADDRESS_COMMAND_BITS;
        for data_item in data.into_iter() {
            self.write_data(address, data_item).await;
            address += 1;
        }
        self.set_brightness(brightness, on).await;
    }
}

}

In this example, we are using the TM1637 display to show the measurement of the potentiometer. The display will show the last 4 digits of the measurement, and the brightness of the display will be adjusted based on the measurement. At the maximum value, the display will blink.

We'll launch two parallel tasks: one to read the ADC value and another to display the value on the TM1637 display. The two processes communicate through a channel.