Embedded Rust BSPs with uFerris & Xiao: Seven-Segment Display Support with I2C

This is the tenth post in the µFerris & Xiao BSP series, where we build a Board Support Package from scratch, one peripheral at a time. In the previous post, we added the I/O expander. This post adds the four-digit seven-segment display, which hangs off that same expander.
Series Past Posts
Introduction
The last post wired up the I/O expander and left twelve of its pins alone: the four digit selects on port 1 and the eight segment lines on port 2. Those drive the seven-segment display, and this post adds it.
The display has four digits, but only one set of segment lines. Every digit's segment A is tied to the same expander pin, and so on for the other seven. What tells the digits apart is the select line, one per digit, which is the digit's common pin. Put a pattern on the segment lines and only the digits whose select is active show it. This is multiplexing, and it means the display can show only one pattern at a time. To show four different numbers, the application lights each digit in turn, quickly enough that the eye sees all four at once.
That refresh loop is the application's job, not the BSP's. The BSP has no timer of its own and no way to run code between the application's calls, so what it offers is a function that shows one value on one digit. The output copy from the last post makes that a single write. Let's get started.
📚 Knowledge Prerequisites
To understand the content of this post, you need the following:
Basic knowledge of coding in Rust.
Basic understanding of embedded systems development concepts.
Familiarity with
no_stddevelopment in Rust, preferably usingesp-hal.Familiarity with the I/O expander code from the previous post.
Familiarity with bit masks.
Familiarity with the µFerris platform and flashing a XIAO module.
💾 Software Setup
All the code presented in this post is available in the µFerris & XIAO series repo. Every post in the series has its own self-contained Cargo project in a numbered folder. The code for this post lives in 10-seven-segment-i2c/:
git clone https://github.com/theembeddedrustacean/learn-bsp-rs
cd learn-bsp-rs/10-seven-segment-i2c
cargo run --release
The examples revolve around the ESP32-C3. If you want to follow along with a different device, please refer to the xiao-generate post for more detail.
🛠 Hardware Setup
The required hardware includes:
- µFerris Megalops Baseboard — the board this BSP is written for. Available from The Embedded Rustacean Store.
- Seeed Studio XIAO ESP32-C3 — the controller used throughout this phase of the series. Available from the SeeedStudio Store.
- USB-C cable for power, flashing, and serial output.
🔌 Connections
No connections are required. Every component the BSP will eventually drive is prewired; you only need to assemble the board. If you haven't assembled the board yet, this post walks through it.
The component added in this post is connected to the I/O expander as follows:
| Expander Pin | Display Pin | Notes |
|---|---|---|
| P10 to P13 | Digit 1 to 4 common | High selects the digit. Digit 1 is on the left. |
| P20 to P25 | Segments A to F | High lights the segment. |
| P26 | Decimal point | Carries the colon LEDs on digits 2 and 4. |
| P27 | Segment G | High lights the segment. |
Nothing changes on the XIAO side. The display is reached through the expander at 0x22 on the bus from post 7.
👨🎨 Software Design
This post builds on the BSP from the previous post, where we added the I/O expander. This post assumes you understand that code. Recall the BSP code is composed of three parts; The Board struct, The Board Initialization Function, and The Board Control Functions. Each post adds to all three, except this one.
Before updating the BSP, let's look at what it takes to operate the seven-segment display. I recommend you read this post to understand how seven-segment devices operate.
The µFerris display has four digits. Each digit is made of seven segments labeled A to G, plus a decimal point (DP). Rather than giving every segment of every digit its own pin, the four digits share the same eight segment lines, and each digit has its own select line. The display lines connect to the I/O expander as follows:
| Port | Bits | Connected to |
|---|---|---|
| 1 | 0 to 3 | Digit 1 to Digit 4 select |
| 2 | 0 to 5 | Segments A to F |
| 2 | 6 | DP (the colon on digits 2 and 4) |
| 2 | 7 | Segment G |
In order to operate the display, here are the main points we need to know:
A segment lights when its segment bit is 1 and the select bit of its digit is 1.
Since the segment lines are shared, every selected digit shows the same pattern. To show a different value on each digit, we select one digit at a time and cycle through all four fast enough that they all appear lit. This is called multiplexing.
Each numeral is a fixed segment pattern. For example, 1 lights segments B and C, which is
0b0000_0110on port 2.The colon is wired to the DP line of digits 2 and 4. Selecting both digits with only the DP bit set lights the colon.
The select bits and the segment bits need to change together. Writing command 0x84 followed by the three output bytes updates both ports in one transaction.
The Board Struct
The display has no pins of its own on the XIAO and no driver of its own. It is twelve pins on a component we already hold, so the board struct does not change. What the display needs is a way to set the four select bits and the segment port together, and that goes on IoExpander, where the output copy lives.
We need a type for the digit, and the segment pattern for each numeral:
// One of the four display digits, left to right.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SevenSegDigit {
Digit1 = 0b0001,
Digit2 = 0b0010,
Digit3 = 0b0100,
Digit4 = 0b1000,
}
Each variant's value is its select bit in port 1, so the enum casts straight to the mask.
The Board Initialization Function
uferris_init does not change either. The expander already configures ports 1 and 2 as outputs and drives them low, which blanks the display.
The Board Control Functions
We'll introduce two functions that the BSP will support:
| Function | Behaviour |
|---|---|
write_seven_segment_digit(digit, value) |
Shows value on digit, or blanks the digit if value is None. |
seven_segment_colon(on) |
Lights or blanks the colon. |
Note that each call selects one digit and deselects the rest, so the display shows one thing at a time. The application loops through four values. Note also that the segments and the selects go out in one write, but the chip updates the two ports a byte apart, so a newly selected digit briefly shows the previous pattern. It is too faint to see at the refresh rate used here. Optionally, you can blank the segments before switching digits, or add a function that takes all four values and runs the refresh loop for a given time. That is left for the reader as an exercise.
Test Application Design
The test application shows the time since boot as minutes and seconds. The steps are as follows:
Read the elapsed time and split it into four decimal digits.
Show each digit in turn, holding it for 2 ms.
Light the colon on even seconds and blank it on odd ones, for 2 ms.
Go back to step 1.
One pass of the loop is five writes and about 10 ms, so the display refreshes at roughly 100 Hz and each digit is lit a fifth of the time.
👨💻 Code Implementation
1️⃣ Define the display pins. The select nibble on port 1, the colon on port 2, and the segment pattern for each numeral. Below is the code:
// Port 1 pins. The low nibble selects the display digits.
const DIGIT_MASK: u8 = 0x0F;
const LED2: u8 = 1 << 4;
const LED3: u8 = 1 << 5;
const SW6_POS2: u8 = 1 << 6;
const SW6_POS1: u8 = 1 << 7;
// Port 2 pin for the colon (decimal point on digits 2 and 4).
const COLON: u8 = 1 << 6;
const COLON_DIGITS: u8 = 0b0010 | 0b1000;
// Segment patterns for 0 to 9, bit order G DP F E D C B A.
const DIGIT_SEGMENTS: [u8; 10] = [
0b00111111, // 0
0b00000110, // 1
0b10011011, // 2
0b10001111, // 3
0b10100110, // 4
0b10101101, // 5
0b10111101, // 6
0b00000111, // 7
0b10111111, // 8
0b10101111, // 9
];
2️⃣ Define the digit type. Below is the code:
// One of the four display digits, left to right.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SevenSegDigit {
Digit1 = 0b0001,
Digit2 = 0b0010,
Digit3 = 0b0100,
Digit4 = 0b1000,
}
3️⃣ Add the display write to the expander. In the impl block on IoExpander, we add one private function that replaces the select nibble and the segment port in the output copy and pushes it. The LED bits in port 1 are left alone. Below is the code:
// Set the digit selects and segments together.
fn write_display(&mut self, digits: u8, segments: u8) -> Result<(), I2cError> {
self.outputs[1] = (self.outputs[1] & !DIGIT_MASK) | (digits & DIGIT_MASK);
self.outputs[2] = segments;
self.write_outputs()
}
4️⃣ Add the control functions. In the impl block on UFerris, we add the two board methods. Below is the code:
// Show a value on one digit, or blank it with `None`.
pub fn write_seven_segment_digit(
&mut self,
digit: SevenSegDigit,
value: Option<u8>,
) -> Result<(), I2cError> {
let segments = match value {
Some(v) => DIGIT_SEGMENTS[usize::from(v.min(9))],
None => 0,
};
self.expander.write_display(digit as u8, segments)
}
// Light or blank the colon.
pub fn seven_segment_colon(&mut self, on: bool) -> Result<(), I2cError> {
let segments = if on { COLON } else { 0 };
self.expander.write_display(COLON_DIGITS, segments)
}
Note that a value above 9 is clamped rather than rejected. The function cannot fail on its input, so its Result only ever carries a bus error.
🏃♂️ Test Code & Run
#![no_std]
#![no_main]
use esp_backtrace as _;
use esp_hal::{clock::CpuClock, main, time::Instant};
use uferris::{SevenSegDigit, UFerris, uferris_init};
esp_bootloader_esp_idf::esp_app_desc!();
// Digits left to right
const DIGITS: [SevenSegDigit; 4] = [
SevenSegDigit::Digit1,
SevenSegDigit::Digit2,
SevenSegDigit::Digit3,
SevenSegDigit::Digit4,
];
#[main]
fn main() -> ! {
// Configure the device and hand the peripherals to the BSP
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
let mut uferris: UFerris = uferris_init(peripherals);
// Instantiate a delay provider
let delay = esp_hal::delay::Delay::new();
// The display shows time since boot as minutes and seconds
let start = Instant::now();
loop {
// Split the elapsed time into the four digits
let seconds = start.elapsed().as_secs() as u32 % 6000;
let minutes = seconds / 60;
let secs = seconds % 60;
let values = [
(minutes / 10) as u8,
(minutes % 10) as u8,
(secs / 10) as u8,
(secs % 10) as u8,
];
// Light each digit in turn, briefly, then the colon
for (digit, value) in DIGITS.iter().zip(values) {
uferris.write_seven_segment_digit(*digit, Some(value)).ok();
delay.delay_millis(2);
}
uferris.seven_segment_colon(secs.is_multiple_of(2)).ok();
delay.delay_millis(2);
}
}
Flash it and open the monitor:
cargo run --release
The display counts up from 00:00, with the colon blinking once a second. Reset the board and it starts over.
If the display stays dark, run the scan from post 7 and check that 0x22 still shows up. If all four digits show the same value, the select bits are not being written; check that port 1 is configured as an output in the previous post's init.
✅ Conclusion
We added the seven-segment display, and with it every component on the I/O expander is reachable from the BSP. The display took no new members and no initialization changes, only a write function that sets the select and segment ports together, plus two board methods on top of it.
That closes the onboard I2C devices, and with it the BSP drives every component on the µFerris from one controller. The next post starts generalizing the BSP so it can support other XIAO controllers, beginning with feature flags.
📱 Full Code
src/lib.rs
#![no_std]
use core::cell::RefCell;
use embedded_hal::i2c::I2c as _;
use embedded_hal::pwm::SetDutyCycle;
use embedded_hal_bus::i2c::RefCellDevice;
use esp_hal::{
Blocking,
analog::adc::{Adc, AdcConfig, AdcPin, Attenuation},
gpio::{DriveMode, Input, InputConfig, Level, Output, OutputConfig},
i2c::master::{Config as I2cConfig, Error as I2cError, I2c},
ledc::{
LSGlobalClkSource, Ledc, LowSpeed,
channel::{self, Channel, ChannelIFace},
timer::{self, Timer, TimerIFace},
},
peripherals::{ADC1, GPIO2, Peripherals},
time::Rate,
};
use static_cell::StaticCell;
// The LEDC timer the buzzer channel runs on.
static BUZZER_TIMER: StaticCell<Timer<'static, LowSpeed>> = StaticCell::new();
// Resonant frequency of the µFerris buzzer, in hertz.
const BUZZER_FREQ_HZ: u32 = 2700;
// The shared I2C driver.
static I2C_BUS: StaticCell<RefCell<I2c<'static, Blocking>>> = StaticCell::new();
// One handle onto the shared I2C bus.
type I2cDevice = RefCellDevice<'static, I2c<'static, Blocking>>;
// I2C bus clock in kHz (standard mode).
const I2C_FREQ_KHZ: u32 = 100;
// I2C address of the real-time clock.
const RTC_ADDR: u8 = 0x68;
// Date and time, 24-hour clock.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DateTime {
pub year: u16,
pub month: u8,
pub day: u8,
pub hour: u8,
pub minute: u8,
pub second: u8,
}
// The real-time clock.
pub struct Rtc {
i2c: I2cDevice,
}
impl Rtc {
// Write the seven time registers, starting at register 0.
pub fn set_time(&mut self, time: DateTime) -> Result<(), I2cError> {
let bytes = [
0x00,
to_bcd(time.second),
to_bcd(time.minute),
to_bcd(time.hour),
1,
to_bcd(time.day),
to_bcd(time.month),
to_bcd((time.year % 100) as u8),
];
self.i2c.write(RTC_ADDR, &bytes)
}
// Read the seven time registers, starting at register 0.
pub fn read_time(&mut self) -> Result<DateTime, I2cError> {
let mut buf = [0u8; 7];
self.i2c.write_read(RTC_ADDR, &[0x00], &mut buf)?;
Ok(DateTime {
second: from_bcd(buf[0] & 0x7F),
minute: from_bcd(buf[1] & 0x7F),
hour: from_bcd(buf[2] & 0x3F),
day: from_bcd(buf[4] & 0x3F),
month: from_bcd(buf[5] & 0x1F),
year: 2000 + u16::from(from_bcd(buf[6])),
})
}
}
// Pack a two-digit value into binary-coded decimal.
fn to_bcd(value: u8) -> u8 {
((value / 10) << 4) | (value % 10)
}
// Unpack a binary-coded decimal byte into a two-digit value.
fn from_bcd(value: u8) -> u8 {
10 * (value >> 4) + (value & 0x0F)
}
// I2C address of the I/O expander.
const EXPANDER_ADDR: u8 = 0x22;
// TCA6424 command bytes, auto-increment bit set.
const IN_PORT0: u8 = 0x80;
const OUT_PORT0: u8 = 0x84;
const CONFIG_PORT0: u8 = 0x8C;
// Port directions, 1 = input, 0 = output.
const PORT0_DIR: u8 = 0xFF; // SW1 to SW4 and the two SW7 contacts
const PORT1_DIR: u8 = 0xC0; // The two SW6 contacts; the rest drive LEDs
const PORT2_DIR: u8 = 0x00; // All outputs
// Port 0 inputs, all active low.
const SW7_POS1: u8 = 1 << 0;
const SW7_POS2: u8 = 1 << 1;
const SW4: u8 = 1 << 4;
const SW3: u8 = 1 << 5;
const SW2: u8 = 1 << 6;
const SW1: u8 = 1 << 7;
// Port 1 pins. The low nibble selects the display digits.
const DIGIT_MASK: u8 = 0x0F;
const LED2: u8 = 1 << 4;
const LED3: u8 = 1 << 5;
const SW6_POS2: u8 = 1 << 6;
const SW6_POS1: u8 = 1 << 7;
// Port 2 pin for the colon (decimal point on digits 2 and 4).
const COLON: u8 = 1 << 6;
const COLON_DIGITS: u8 = 0b0010 | 0b1000;
// Segment patterns for 0 to 9, bit order G DP F E D C B A.
const DIGIT_SEGMENTS: [u8; 10] = [
0b00111111, // 0
0b00000110, // 1
0b10011011, // 2
0b10001111, // 3
0b10100110, // 4
0b10101101, // 5
0b10111101, // 6
0b00000111, // 7
0b10111111, // 8
0b10101111, // 9
];
// One of the four display digits, left to right.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SevenSegDigit {
Digit1 = 0b0001,
Digit2 = 0b0010,
Digit3 = 0b0100,
Digit4 = 0b1000,
}
// Position of a slide switch.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SwPos {
Up,
Down,
// Neither contact closed
Undefined,
}
// The I/O expander and a copy of its output registers.
pub struct IoExpander {
i2c: I2cDevice,
outputs: [u8; 3],
}
impl IoExpander {
// Set pin directions and drive all outputs low.
fn init(&mut self) -> Result<(), I2cError> {
self.i2c.write(
EXPANDER_ADDR,
&[CONFIG_PORT0, PORT0_DIR, PORT1_DIR, PORT2_DIR],
)?;
self.write_outputs()
}
// Write the output copy to all three ports.
fn write_outputs(&mut self) -> Result<(), I2cError> {
let [port0, port1, port2] = self.outputs;
self.i2c
.write(EXPANDER_ADDR, &[OUT_PORT0, port0, port1, port2])
}
// Set the digit selects and segments together.
fn write_display(&mut self, digits: u8, segments: u8) -> Result<(), I2cError> {
self.outputs[1] = (self.outputs[1] & !DIGIT_MASK) | (digits & DIGIT_MASK);
self.outputs[2] = segments;
self.write_outputs()
}
// Set or clear output pins on one port.
fn set_output(&mut self, port: usize, mask: u8, high: bool) -> Result<(), I2cError> {
if high {
self.outputs[port] |= mask;
} else {
self.outputs[port] &= !mask;
}
self.write_outputs()
}
// Read one input port.
fn read_input(&mut self, port: u8) -> Result<u8, I2cError> {
let mut buf = [0u8];
self.i2c
.write_read(EXPANDER_ADDR, &[IN_PORT0 + port], &mut buf)?;
Ok(buf[0])
}
// Check if an active-low port 0 input is low.
fn port0_low(&mut self, mask: u8) -> Result<bool, I2cError> {
Ok(self.read_input(0)? & mask == 0)
}
// Decode a slide switch from its two contacts.
fn slide_position(reading: u8, pos1: u8, pos2: u8) -> SwPos {
if reading & pos1 == 0 {
SwPos::Down
} else if reading & pos2 == 0 {
SwPos::Up
} else {
SwPos::Undefined
}
}
}
// The LDR ADC driver and pin.
pub struct LdrAdc {
adc: Adc<'static, ADC1<'static>, Blocking>,
pin: AdcPin<GPIO2<'static>, ADC1<'static>>,
}
impl LdrAdc {
// Run a conversion and return the raw count.
pub fn read_raw(&mut self) -> u16 {
nb::block!(self.adc.read_oneshot(&mut self.pin)).unwrap_or(0)
}
}
// The µFerris board.
pub struct UFerris {
led1: Output<'static>,
sw_btn5: Input<'static>,
buzzer: Channel<'static, LowSpeed>,
ldr_driver: LdrAdc,
rtc: Rtc,
expander: IoExpander,
i2c: I2cDevice,
}
// Initialize the board.
pub fn uferris_init(peripherals: Peripherals) -> UFerris {
// Instantiate LED 1
let led1 = Output::new(peripherals.GPIO3, Level::Low, OutputConfig::default());
// Instantiate SW5
let sw_btn5 = Input::new(peripherals.GPIO5, InputConfig::default());
// Instantiate the LEDC peripheral
let mut ledc = Ledc::new(peripherals.LEDC);
ledc.set_global_slow_clock(LSGlobalClkSource::APBClk);
// Configure the timer attached to the LEDC
let mut buzzer_timer = ledc.timer::<LowSpeed>(timer::Number::Timer0);
buzzer_timer
.configure(timer::config::Config {
duty: timer::config::Duty::Duty14Bit,
clock_source: timer::LSClockSource::APBClk,
frequency: Rate::from_hz(BUZZER_FREQ_HZ),
})
.expect("buzzer timer configuration failed");
// Promote the timer to 'static
let buzzer_timer = BUZZER_TIMER.init(buzzer_timer);
// Configure the LEDC Channel and attach pin.
let mut buzzer = ledc.channel(channel::Number::Channel0, peripherals.GPIO4);
buzzer
.configure(channel::config::Config {
timer: buzzer_timer,
duty_pct: 0,
drive_mode: DriveMode::PushPull,
})
.expect("buzzer channel configuration failed");
// Enable the LDR pin, then build the ADC
let mut adc_config = AdcConfig::new();
let ldr_pin = adc_config.enable_pin(peripherals.GPIO2, Attenuation::_11dB);
let adc1 = Adc::new(peripherals.ADC1, adc_config);
let ldr_driver = LdrAdc {
adc: adc1,
pin: ldr_pin,
};
// Instantiate the I2C peripheral and attach its pins
let i2c = I2c::new(
peripherals.I2C0,
I2cConfig::default().with_frequency(Rate::from_khz(I2C_FREQ_KHZ)),
)
.expect("I2C configuration failed")
.with_sda(peripherals.GPIO6)
.with_scl(peripherals.GPIO7);
// Promote the bus to 'static so it can be shared
let i2c_bus = I2C_BUS.init(RefCell::new(i2c));
// One handle per device on the bus
let rtc = Rtc {
i2c: RefCellDevice::new(i2c_bus),
};
let mut expander = IoExpander {
i2c: RefCellDevice::new(i2c_bus),
outputs: [0; 3],
};
let i2c = RefCellDevice::new(i2c_bus);
// Configure the expander pins
expander.init().expect("I/O expander not responding");
UFerris {
led1,
sw_btn5,
buzzer,
ldr_driver,
rtc,
expander,
i2c,
}
}
impl UFerris {
// Turn LED 1 on.
pub fn led1_on(&mut self) {
self.led1.set_high();
}
// Turn LED 1 off.
pub fn led1_off(&mut self) {
self.led1.set_low();
}
// Read switch 5.
pub fn sw5_pressed(&mut self) -> bool {
self.sw_btn5.is_low()
}
// Sound the buzzer at a duty from 0 to 100 percent.
pub fn buzz_on(&mut self, duty_percent: u8) {
let _ = self.buzzer.set_duty_cycle_percent(duty_percent);
}
// Silence the buzzer.
pub fn buzz_off(&mut self) {
let _ = self.buzzer.set_duty_cycle_fully_off();
}
// Turn LED 2 on.
pub fn led2_on(&mut self) -> Result<(), I2cError> {
self.expander.set_output(1, LED2, true)
}
// Turn LED 2 off.
pub fn led2_off(&mut self) -> Result<(), I2cError> {
self.expander.set_output(1, LED2, false)
}
// Turn LED 3 on.
pub fn led3_on(&mut self) -> Result<(), I2cError> {
self.expander.set_output(1, LED3, true)
}
// Turn LED 3 off.
pub fn led3_off(&mut self) -> Result<(), I2cError> {
self.expander.set_output(1, LED3, false)
}
// Read switch 1.
pub fn sw1_pressed(&mut self) -> Result<bool, I2cError> {
self.expander.port0_low(SW1)
}
// Read switch 2.
pub fn sw2_pressed(&mut self) -> Result<bool, I2cError> {
self.expander.port0_low(SW2)
}
// Read switch 3.
pub fn sw3_pressed(&mut self) -> Result<bool, I2cError> {
self.expander.port0_low(SW3)
}
// Read switch 4.
pub fn sw4_pressed(&mut self) -> Result<bool, I2cError> {
self.expander.port0_low(SW4)
}
// Read the position of slide switch 6.
pub fn sw6_position(&mut self) -> Result<SwPos, I2cError> {
let port1 = self.expander.read_input(1)?;
Ok(IoExpander::slide_position(port1, SW6_POS1, SW6_POS2))
}
// Read the position of slide switch 7.
pub fn sw7_position(&mut self) -> Result<SwPos, I2cError> {
let port0 = self.expander.read_input(0)?;
Ok(IoExpander::slide_position(port0, SW7_POS1, SW7_POS2))
}
// Show a value on one digit, or blank it with `None`.
pub fn write_seven_segment_digit(
&mut self,
digit: SevenSegDigit,
value: Option<u8>,
) -> Result<(), I2cError> {
let segments = match value {
Some(v) => DIGIT_SEGMENTS[usize::from(v.min(9))],
None => 0,
};
self.expander.write_display(digit as u8, segments)
}
// Light or blank the colon.
pub fn seven_segment_colon(&mut self, on: bool) -> Result<(), I2cError> {
let segments = if on { COLON } else { 0 };
self.expander.write_display(COLON_DIGITS, segments)
}
// Set the clock.
pub fn rtc_set_time(&mut self, time: DateTime) -> Result<(), I2cError> {
self.rtc.set_time(time)
}
// Read the clock.
pub fn rtc_read_time(&mut self) -> Result<DateTime, I2cError> {
self.rtc.read_time()
}
// Read the LDR as a raw 12-bit ADC count.
pub fn read_ldr(&mut self) -> u16 {
self.ldr_driver.read_raw()
}
// Write bytes to a device. An empty write probes the address.
pub fn i2c_write(&mut self, address: u8, bytes: &[u8]) -> Result<(), I2cError> {
self.i2c.write(address, bytes)
}
// Read from a device until the buffer is full.
pub fn i2c_read(&mut self, address: u8, buffer: &mut [u8]) -> Result<(), I2cError> {
self.i2c.read(address, buffer)
}
// Write then read in one transaction.
pub fn i2c_write_read(
&mut self,
address: u8,
bytes: &[u8],
buffer: &mut [u8],
) -> Result<(), I2cError> {
self.i2c.write_read(address, bytes, buffer)
}
}
src/bin/main.rs
#![no_std]
#![no_main]
use esp_backtrace as _;
use esp_hal::{clock::CpuClock, main, time::Instant};
use uferris::{SevenSegDigit, UFerris, uferris_init};
esp_bootloader_esp_idf::esp_app_desc!();
// Digits left to right
const DIGITS: [SevenSegDigit; 4] = [
SevenSegDigit::Digit1,
SevenSegDigit::Digit2,
SevenSegDigit::Digit3,
SevenSegDigit::Digit4,
];
#[main]
fn main() -> ! {
// Configure the device and hand the peripherals to the BSP
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
let mut uferris: UFerris = uferris_init(peripherals);
// Instantiate a delay provider
let delay = esp_hal::delay::Delay::new();
// The display shows time since boot as minutes and seconds
let start = Instant::now();
loop {
// Split the elapsed time into the four digits
let seconds = start.elapsed().as_secs() as u32 % 6000;
let minutes = seconds / 60;
let secs = seconds % 60;
let values = [
(minutes / 10) as u8,
(minutes % 10) as u8,
(secs / 10) as u8,
(secs % 10) as u8,
];
// Light each digit in turn, briefly, then the colon
for (digit, value) in DIGITS.iter().zip(values) {
uferris.write_seven_segment_digit(*digit, Some(value)).ok();
delay.delay_millis(2);
}
uferris.seven_segment_colon(secs.is_multiple_of(2)).ok();
delay.delay_millis(2);
}
}





