Embedded Rust BSPs with uFerris & Xiao: RTC Support with I2C
This is the eighth 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 I2C bus. This post adds the real-time clock, the first device on that bus, and with it the problem of sharing the bus.
Series Past Posts
Introduction
In the last post, we added the I2C bus and scanned it. The scan found two devices that live on the µFerris itself. This post adds the first of them, the real-time clock, a DS1338 at address 0x68 with its own crystal and a coin cell to keep it running when the board is off.
The clock is a component like the LDR, so by the rule we followed in the ADC post, it gets its own struct that holds everything it needs. What it needs is the bus. And the bus already has an owner: the board struct holds the driver, so the Qwiic functions from the last post can use it. Rust will not let two things own one driver, and it will not let the board struct hold a driver and a reference into itself either.
This is the problem the previous post set aside: the bus is one object, and more than one part of the BSP needs it. This post solves it with a small amount of machinery from the embedded-hal-bus crate, and the solution carries over unchanged to every I2C device that comes after.
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 I2C register access, as covered in the previous post.
Familiarity with Rust's ownership rules and interior mutability, mainly
RefCell.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 08-rtc-i2c/:
git clone https://github.com/theembeddedrustacean/learn-bsp-rs
cd learn-bsp-rs/08-rtc-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 on the µFerris as follows:
| Device | XIAO Pin | Notes |
|---|---|---|
| DS1338 RTC | GPIO6 (SDA), GPIO7 (SCL) | I2C address 0x68 |
The clock shares the bus with the Qwiic connector, so there are no new pins. Fit a CR2032 in the holder if you want the time to survive a power disconnect. Without it, the clock starts from scratch each time the board is powered.
👨🎨 Software Design
This post builds on the BSP from the previous post, where we added the I2C bus. 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.
Before updating the BSP, let's look at what it takes to operate the RTC. The µFerris carries a DS1338, which keeps the same time and date register layout as the DS1307. The DS1338 datasheet provides the following table:
| Address | Bit 7 | Bits 6 to 4 | Bits 3 to 0 | Function | Range |
|---|---|---|---|---|---|
| 0x00 | CH | 10 Seconds | Seconds | Seconds | 00 to 59 |
| 0x01 | 0 | 10 Minutes | Minutes | Minutes | 00 to 59 |
| 0x02 | 0 | 12/24 (bit 6), 10 Hour (bits 5 and 4) | Hour | Hours | 00 to 23 |
| 0x03 | 0 | 0 | Day | Day of week | 01 to 07 |
| 0x04 | 0 | 10 Date (bits 5 and 4) | Date | Date | 01 to 31 |
| 0x05 | 0 | 10 Month (bit 4) | Month | Month | 01 to 12 |
| 0x06 | 10 Year | 10 Year | Year | Year | 00 to 99 |
The table above provides an address mapping of the internal memory of the DS1338 that contains the time/date data. Addresses 0x00 to 0x06 are all we need. The table also specifies the valid range of values for the data in each address. All values are BCD (Binary Coded Decimal) encoded. This means each byte holds two decimal digits, the tens in the upper four bits and the units in the lower four. For example, 45 seconds is stored as 0x45 and not 0x2D.
The DS1338 communicates over I2C where we will be reading from it and writing to it. In order to achieve what we want here are the main points we need to know:
The address of the device is 0x68.
All date/time values need to be converted to BCD before writing and back from BCD after reading.
The CH bit in address 0x00 needs to be set to 0 to kick off the internal oscillator. Writing a seconds value clears it.
Bit 6 in address 0x02 selects 12 or 24-hour mode. We keep it at 0 for 24-hour mode.
The year is stored as two digits only. The device does not keep the century.
The date/time of the device can be configured by writing to the different addresses, and retrieved by reading from them.
To communicate with the DS1338 a starting address needs to be provided followed by the data read/written from/to the device. The device will keep on writing/reading to/from consecutive addresses until the master (ESP32) decides to halt.
As such, setting the time is a single write of the starting address 0x00 followed by the seven values. Reading the time is a write of the starting address 0x00 followed by a read of seven bytes.
The Board Struct
The clock needs the bus, and so does the Qwiic connector. The obvious way out is to not give the clock a struct at all, and make rtc_read_time a board method that uses the board's own i2c member directly. That compiles, and for one device it is fine. It stops being fine at the second device, and it breaks as soon as we want to use a driver crate, since a driver expects to be handed something that implements embedded_hal::i2c::I2c and to keep it.
What we want instead is one driver and several handles onto it, where each handle implements the I2c trait and can be owned by whichever component needs it. That is exactly what RefCellDevice from the embedded-hal-bus crate provides. The driver goes into a RefCell, and each RefCellDevice holds a shared reference to that cell. A transaction borrows the driver for its duration and releases it after. Because a RefCell checks borrows at runtime, two handles cannot be mid-transaction at once, and because everything here runs on one thread, they never try to be.
The references need a lifetime, and the only one that lets the board struct own the handles is 'static. So the RefCell goes into a StaticCell, as the LEDC timer did in post 05:
// 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>>;
The clock then gets a component struct holding one handle, along with a type for the time it keeps:
// 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,
}
The board struct gains the clock as a member, and its existing i2c member changes type from the driver to a handle. This is the form of the updated struct:
pub struct UFerris {
led1: Output<'static>,
sw_btn5: Input<'static>,
buzzer: Channel<'static, LowSpeed>,
ldr_driver: LdrAdc,
rtc: Rtc,
i2c: I2cDevice,
}
Notice how the board struct still holds one member per component. The Qwiic functions from the previous post keep their signatures; they now go through a handle instead of the driver, and nothing in application code changes.
The Board Initialization Function
We will update uferris_init to share the bus and build the clock. The steps are as follows:
Build the I2C driver.
Move it into a
RefCellinside theStaticCell, which hands back a'staticreference.Create a
RefCellDevicefor the clock and for the Qwiic connector.Build the
Rtccomponent.
The Board Control Functions
We'll introduce two functions that the BSP will support:
| Function | Behaviour |
|---|---|
rtc_set_time(time) |
Writes a DateTime to the clock. |
rtc_read_time() |
Reads the current DateTime from the clock. |
Note that both return a Result, as the I2C functions do, because the transaction underneath can fail. Note also that DateTime is a plain struct with public fields and no validation. Passing a month of 13 will be written to the chip as is. Optionally, you can add a constructor that checks each field, or add the day of the week, which the chip keeps in register 3 and we ignore. That is left for the reader as an exercise.
Test Application Design
The test application steps are as follows:
Read the clock. If the read fails or the year is earlier than the build-time start date, the clock has never been set, so set it.
Read the clock.
Print the date and time over the serial port.
Wait 1 second (paces the printing).
Go back to step 2.
The start date is a constant in the application. Edit it to the current time before flashing. With a coin cell fitted, the clock keeps running after the first flash and step 1 does nothing on later boots.
👨💻 Code Implementation
1️⃣ Bring in the sharing types. Everything from the previous post stays. We add RefCell, the RefCellDevice handle, and the embedded-hal I2C trait:
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;
RefCellDevice comes from the embedded-hal-bus crate, which this post adds to Cargo.toml. The trait is imported as _ because we only need its methods in scope, and its name would collide with the esp-hal driver type. A RefCellDevice has no write or read of its own; those come from the trait, so without this import the calls in the next steps would not resolve.
2️⃣ Define the shared bus. Below is the code:
// 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>>;
The StaticCell holds a RefCell around the driver. init moves the driver in at runtime and returns a &'static reference to the cell, and it panics if called twice, which is the right behavior for something meant to be built once. I2cDevice is just a name for the handle type, so that the two places it appears do not have to spell out the full generic.
3️⃣ Define the clock component. The address, the time type, and the struct that holds the handle. Below is the code:
// 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,
}
4️⃣ Implement the clock. set_time writes the starting address 0x00 followed by the seven BCD values. read_time uses write_read to send the starting address and read the seven values back. Below is the code:
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])),
})
}
}
Note that the byte written to register 3 is the day of the week, which we don't track, so we set it to 1. The masks on the read clear the CH and 12/24 bits before conversion. Also, since the year is two digits, the 2000 is added on read and removed on write.
The two BCD helpers are plain functions at the bottom of the file:
// 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)
}
5️⃣ Populate the board struct. Add the clock as a member, and change the i2c member to a handle.
pub struct UFerris {
led1: Output<'static>,
sw_btn5: Input<'static>,
buzzer: Channel<'static, LowSpeed>,
ldr_driver: LdrAdc,
rtc: Rtc,
i2c: I2cDevice,
}
6️⃣ Populate the initialization function. In the init function, the driver is built exactly as before. What changes is what happens to it afterward. Below is the code:
// 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 i2c = RefCellDevice::new(i2c_bus);
UFerris {
led1,
sw_btn5,
buzzer,
ldr_driver,
rtc,
i2c,
}
Each RefCellDevice::new takes the same &'static RefCell and hands back an independent handle. The clock takes one and the board keeps the other for the Qwiic functions. Adding a third device later means one more line here.
7️⃣ Add the control functions. In the impl block on UFerris, we add two board methods that delegate to Rtc:
// 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()
}
The three i2c_* functions from the previous post are unchanged. Their bodies call write, read, and write_read on self.i2c, and those names now resolve to the trait methods on the handle instead of the inherent methods on the driver. The signatures are the same, so the code compiles as it was.
🏃♂️ Test Code & Run
#![no_std]
#![no_main]
use esp_backtrace as _;
use esp_hal::{clock::CpuClock, main};
use esp_println::println;
use uferris::{DateTime, UFerris, uferris_init};
esp_bootloader_esp_idf::esp_app_desc!();
// Time to set an unset clock to. Edit before flashing.
const START_TIME: DateTime = DateTime {
year: 2026,
month: 9,
day: 28,
hour: 12,
minute: 0,
second: 0,
};
#[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();
// An unset clock reads a year before ours
let needs_setting = match uferris.rtc_read_time() {
Ok(time) => time.year < START_TIME.year,
Err(_) => true,
};
if needs_setting {
println!("Setting the clock");
uferris.rtc_set_time(START_TIME).expect("RTC not responding");
}
loop {
match uferris.rtc_read_time() {
Ok(t) => println!(
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
t.year, t.month, t.day, t.hour, t.minute, t.second
),
Err(e) => println!("RTC read failed: {:?}", e),
}
// Print once a second
delay.delay_millis(1000);
}
}
Edit START_TIME to the current time, then flash it and open the monitor:
cargo run --release
On the first boot the clock has not been set, so the application sets it and then prints the time once a second:
Setting the clock
2026-09-28 12:00:00
2026-09-28 12:00:01
2026-09-28 12:00:02
Press reset. With a coin cell fitted, the "Setting the clock" line does not appear and the time carries on from where it was. Without one, the clock has lost power along with the board and is set again from START_TIME.
If every read fails, the clock is not answering on the bus. Run the scan from the previous post and check that 0x68 still shows up.
✅ Conclusion
We added the real-time clock, and with it, the BSP has its first I2C device. Getting there took a component struct like the LDR's and a way to let that component and the Qwiic connector share one bus, which RefCellDevice handles for us.
The sharing is the part that carries forward. The next post adds the I/O expander, the second device on the bus, and it needs nothing more than another handle.
📱 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)
}
// 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,
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 i2c = RefCellDevice::new(i2c_bus);
UFerris {
led1,
sw_btn5,
buzzer,
ldr_driver,
rtc,
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();
}
// 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};
use esp_println::println;
use uferris::{DateTime, UFerris, uferris_init};
esp_bootloader_esp_idf::esp_app_desc!();
// Time to set an unset clock to. Edit before flashing.
const START_TIME: DateTime = DateTime {
year: 2026,
month: 9,
day: 28,
hour: 12,
minute: 0,
second: 0,
};
#[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();
// An unset clock reads a year before ours
let needs_setting = match uferris.rtc_read_time() {
Ok(time) => time.year < START_TIME.year,
Err(_) => true,
};
if needs_setting {
println!("Setting the clock");
uferris.rtc_set_time(START_TIME).expect("RTC not responding");
}
loop {
match uferris.rtc_read_time() {
Ok(t) => println!(
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
t.year, t.month, t.day, t.hour, t.minute, t.second
),
Err(e) => println!("RTC read failed: {:?}", e),
}
// Print once a second
delay.delay_millis(1000);
}
}





