Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

impl Rtc

Let's implement the Rtc trait from rtc-hal for the DS3231 driver. We need to implement get_datetime and set_datetime methods.

Get DateTime

Similar to the DS1307, we'll read data from the Seconds register through the Year register in a single burst read operation. However, the DS3231 has some key differences.

  • Clock Halt Bit Difference: We don't need to mask out bit 7 of the seconds byte. The DS1307 required bit masking to remove the clock halt bit, but in the DS3231, bit 7 is always 0

  • Century Bit Handling: We extract the century bit from the month register. If the century bit is set, we add +1 to our base century. For example, if our base century is 20 and the century bit is set, then year 25 becomes 2125 instead of 2025

#![allow(unused)]
fn main() {
fn get_datetime(&mut self) -> Result<rtc_hal::datetime::DateTime, Self::Error> {
    // Since DS3231 allows Subsequent registers can be accessed sequentially until a STOP condition is executed
    // Read all 7 registers in one burst operation
    let mut data = [0; 7];
    self.read_register_bytes(Register::Seconds, &mut data)?;

    // Convert from BCD format and extract fields
    let second = bcd::to_decimal(data[0]);
    let minute = bcd::to_decimal(data[1]);

    // Handle both 12-hour and 24-hour modes for hours
    let raw_hour = data[2];
    let hour = if (raw_hour & 0b0100_0000) != 0 {
        // 12-hour mode
        // Extract the Hour part (4-0 bits)
        let hr = bcd::to_decimal(raw_hour & 0b0001_1111);
        // Extract the AM/PM (5th bit). if it is set, then it is PM
        let pm = (raw_hour & 0b0010_0000) != 0;

        // Convert it to 24 hour format:
        match (hr, pm) {
            (12, false) => 0,    // 12 AM = 00:xx
            (12, true) => 12,    // 12 PM = 12:xx
            (h, false) => h,     // 1-11 AM
            (h, true) => h + 12, // 1-11 PM
        }
    } else {
        // 24-hour mode
        // Extrac the hour value from 5-0 bits
        bcd::to_decimal(raw_hour & 0b0011_1111)
    };

    // let weekday = Weekday::from_number(bcd::to_decimal(data[3]))
    //     .map_err(crate::error::Error::DateTime)?;

    let day_of_month = bcd::to_decimal(data[4]);
    // Extract century bit
    // If it is set, then it is next century
    // Let's say base century is 20, then next century will be 21
    let is_century_bit_set = (data[5] & 0b1000_0000) != 0;
    let mut century = self.base_century;
    if is_century_bit_set {
        century += 1;
    }

    let month = bcd::to_decimal(data[5] & 0b0111_1111);

    let year = (century as u16 * 100) + bcd::to_decimal(data[6]) as u16;

    rtc_hal::datetime::DateTime::new(year, month, day_of_month, hour, minute, second)
        .map_err(crate::error::Error::DateTime)
}
}

Set DateTime

Let's implement the set_datetime method. The logic is slightly different than DS1307 since we have the century bit handling.

We calculate the full century base (i.e., 20 × 100 = 2000) and validate that the user-provided year falls within the DS3231's representable range of 200 consecutive years.

Next, we check whether the given year belongs to the base century (2000-2099) or the next century (2100-2199). Based on this determination, we set the century bit (bit 7 of the month register): clear (0) for the base century, or set (1) for the next century.

#![allow(unused)]
fn main() {
fn set_datetime(&mut self, datetime: &rtc_hal::datetime::DateTime) -> Result<(), Self::Error> {
    let century_base = self.base_century as u16 * 100;

    // Validate year is within the current or next century
    if datetime.year() < century_base || datetime.year() > (century_base + 199) {
        return Err(crate::error::Error::DateTime(DateTimeError::InvalidYear));
    }

    let is_next_century = datetime.year() >= (century_base + 100);
    let year_2digit = if is_next_century {
        (datetime.year() - century_base - 100) as u8
    } else {
        (datetime.year() - century_base) as u8
    };

    // Prepare data array for burst write (7 registers)
    let mut data = [0u8; 8];
    data[0] = Register::Seconds.addr();

    // Seconds register (0x00)
    data[1] = bcd::from_decimal(datetime.second());

    // Minutes register (0x01)
    data[2] = bcd::from_decimal(datetime.minute());

    // Hours register (0x02) - set to 24-hour mode
    // Clear bit 6 (12/24 hour mode bit) to enable 24-hour mode
    data[3] = bcd::from_decimal(datetime.hour()) & 0b0011_1111;

    let weekday = datetime
        .calculate_weekday()
        .map_err(crate::error::Error::DateTime)?;

    // Day of week register (0x03) - 1=Sunday, 7=Saturday
    data[4] = bcd::from_decimal(weekday.to_number());

    // Day of month register (0x04)
    data[5] = bcd::from_decimal(datetime.day_of_month());

    // Month register(0x05) with century bit
    let mut month_reg = bcd::from_decimal(datetime.month());
    if is_next_century {
        month_reg |= 0b1000_0000; // Set century bit
    }
    data[6] = month_reg;

    data[7] = bcd::from_decimal(year_2digit);

    // Write all 7 registers in one burst operation
    self.write_raw_bytes(&data)?;

    Ok(())
}
}

The full code for the datetime module (datetime.rs)

#![allow(unused)]
fn main() {
//! # DateTime Module
//!
//! This module provides an implementation of the [`Rtc`] trait for the
//! DS3231 real-time clock (RTC).

use rtc_hal::{bcd, datetime::DateTimeError, rtc::Rtc};

use crate::{Ds3231, registers::Register};

impl<I2C> Rtc for Ds3231<I2C>
where
    I2C: embedded_hal::i2c::I2c,
{
    /// Read the current date and time from the DS3231.
    fn get_datetime(&mut self) -> Result<rtc_hal::datetime::DateTime, Self::Error> {
        // Since DS3231 allows Subsequent registers can be accessed sequentially until a STOP condition is executed
        // Read all 7 registers in one burst operation
        let mut data = [0; 7];
        self.read_register_bytes(Register::Seconds, &mut data)?;

        // Convert from BCD format and extract fields
        let second = bcd::to_decimal(data[0]);
        let minute = bcd::to_decimal(data[1]);

        // Handle both 12-hour and 24-hour modes for hours
        let raw_hour = data[2];
        let hour = if (raw_hour & 0b0100_0000) != 0 {
            // 12-hour mode
            // Extract the Hour part (4-0 bits)
            let hr = bcd::to_decimal(raw_hour & 0b0001_1111);
            // Extract the AM/PM (5th bit). if it is set, then it is PM
            let pm = (raw_hour & 0b0010_0000) != 0;

            // Convert it to 24 hour format:
            match (hr, pm) {
                (12, false) => 0,    // 12 AM = 00:xx
                (12, true) => 12,    // 12 PM = 12:xx
                (h, false) => h,     // 1-11 AM
                (h, true) => h + 12, // 1-11 PM
            }
        } else {
            // 24-hour mode
            // Extrac the hour value from 5-0 bits
            bcd::to_decimal(raw_hour & 0b0011_1111)
        };

        // let weekday = Weekday::from_number(bcd::to_decimal(data[3]))
        //     .map_err(crate::error::Error::DateTime)?;

        let day_of_month = bcd::to_decimal(data[4]);
        // Extract century bit
        // If it is set, then it is next century
        // Let's say base century is 20, then next century will be 21
        let is_century_bit_set = (data[5] & 0b1000_0000) != 0;
        let mut century = self.base_century;
        if is_century_bit_set {
            century += 1;
        }

        let month = bcd::to_decimal(data[5] & 0b0111_1111);

        let year = (century as u16 * 100) + bcd::to_decimal(data[6]) as u16;

        rtc_hal::datetime::DateTime::new(year, month, day_of_month, hour, minute, second)
            .map_err(crate::error::Error::DateTime)
    }

    /// Set the current date and time in the DS3231.
    ///
    /// The DS3231 stores years as 2-digit values (00-99). This method interprets
    /// the provided year based on the configured base century and its successor.
    ///
    /// # Year Range
    ///
    /// The year must be within one of these ranges:
    /// - **Base century**: `base_century * 100` to `(base_century * 100) + 99`
    /// - **Next century**: `(base_century + 1) * 100` to `(base_century + 1) * 100 + 99`
    ///
    /// For example, with `base_century = 20`:
    /// - Allowed years: 2000-2099 (stored as 00-99, century bit = 0)
    /// - Allowed years: 2100-2199 (stored as 00-99, century bit = 1)
    /// - Rejected years: 1900-1999, 2200+
    ///
    /// # Century Bit Handling
    ///
    /// The method automatically sets the DS3231's century bit based on which
    /// century range the year falls into, avoiding the ambiguity issues with
    /// this hardware feature.
    ///
    /// # Time Format
    ///
    /// The DS3231 is configured to use 24-hour time format. The weekday is
    /// calculated from the date and stored in the day register (1=Sunday, 7=Saturday).
    ///
    /// # Arguments
    ///
    /// * `datetime` - The date and time to set
    ///
    /// # Returns
    ///
    /// Returns `Err(Error::DateTime(DateTimeError::InvalidYear))` if the year
    /// is outside the supported range.
    ///
    /// # Examples
    ///
    /// ```
    /// // With base_century = 20, you can set dates from 2000-2199
    /// let datetime = DateTime::new(2023, 12, 25, 15, 30, 0)?;
    /// rtc.set_datetime(&datetime)?;
    ///
    /// // To set dates in a different century, update base_century first
    /// let rtc = rtc.with_base_century(21)?; // Now supports 2100-2299
    /// let datetime = DateTime::new(2150, 1, 1, 0, 0, 0)?;
    /// rtc.set_datetime(&datetime)?;
    /// ```
    fn set_datetime(&mut self, datetime: &rtc_hal::datetime::DateTime) -> Result<(), Self::Error> {
        let century_base = self.base_century as u16 * 100;

        // Validate year is within the current or next century
        if datetime.year() < century_base || datetime.year() > (century_base + 199) {
            return Err(crate::error::Error::DateTime(DateTimeError::InvalidYear));
        }

        let is_next_century = datetime.year() >= (century_base + 100);
        let year_2digit = if is_next_century {
            (datetime.year() - century_base - 100) as u8
        } else {
            (datetime.year() - century_base) as u8
        };

        // Prepare data array for burst write (7 registers)
        let mut data = [0u8; 8];
        data[0] = Register::Seconds.addr();

        // Seconds register (0x00)
        data[1] = bcd::from_decimal(datetime.second());

        // Minutes register (0x01)
        data[2] = bcd::from_decimal(datetime.minute());

        // Hours register (0x02) - set to 24-hour mode
        // Clear bit 6 (12/24 hour mode bit) to enable 24-hour mode
        data[3] = bcd::from_decimal(datetime.hour()) & 0b0011_1111;

        let weekday = datetime
            .calculate_weekday()
            .map_err(crate::error::Error::DateTime)?;

        // Day of week register (0x03) - 1=Sunday, 7=Saturday
        data[4] = bcd::from_decimal(weekday.to_number());

        // Day of month register (0x04)
        data[5] = bcd::from_decimal(datetime.day_of_month());

        // Month register(0x05) with century bit
        let mut month_reg = bcd::from_decimal(datetime.month());
        if is_next_century {
            month_reg |= 0b1000_0000; // Set century bit
        }
        data[6] = month_reg;

        data[7] = bcd::from_decimal(year_2digit);

        // Write all 7 registers in one burst operation
        self.write_raw_bytes(&data)?;

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as I2cTrans};
    use rtc_hal::datetime::DateTime;

    fn new_ds3231(i2c: I2cMock) -> Ds3231<I2cMock> {
        Ds3231::new(i2c)
    }

    #[test]
    fn test_get_datetime_24h_mode() {
        // Simulate reading: sec=0x25(25), min=0x59(59), hour=0x23(23h 24h mode),
        // day_of_week=0x04, day_of_month=0x15(15), month=0x08(August), year=0x23(2023)
        let data = [0x25, 0x59, 0x23, 0x04, 0x15, 0x08, 0x23];
        let expectations = [I2cTrans::write_read(
            0x68,
            vec![Register::Seconds.addr()],
            data.to_vec(),
        )];
        let mut ds3231 = new_ds3231(I2cMock::new(&expectations));

        let dt = ds3231.get_datetime().unwrap();
        assert_eq!(dt.second(), 25);
        assert_eq!(dt.minute(), 59);
        assert_eq!(dt.hour(), 23);

        assert_eq!(dt.day_of_month(), 15);
        assert_eq!(dt.month(), 8);

        assert_eq!(dt.year(), 2023);

        ds3231.release_i2c().done();
    }

    #[test]
    fn test_set_datetime_within_base_century() {
        let datetime = DateTime::new(2025, 8, 27, 15, 30, 45).unwrap();
        // base_century = 20, so 2000-2199 valid. 2023 fits.
        let expectations = [I2cTrans::write(
            0x68,
            vec![
                Register::Seconds.addr(),
                0x45, // sec
                0x30, // min
                0x15, // hour (24h)
                0x04, // weekday (2025-08-27 is Wednesday)
                0x27, // day
                0x8,  // month
                0x25, // year (25)
            ],
        )];

        let mut ds3231 = new_ds3231(I2cMock::new(&expectations));

        ds3231.set_datetime(&datetime).unwrap();

        ds3231.release_i2c().done();
    }

    #[test]
    fn test_set_datetime_next_century() {
        let datetime = DateTime::new(2150, 1, 1, 0, 0, 0).unwrap();
        // Expect century bit set in month register
        let expectations = [I2cTrans::write(
            0x68,
            vec![
                Register::Seconds.addr(),
                0x00, // sec
                0x00, // min
                0x00, // hour
                0x05, // weekday (Thursday 2150-01-01)
                0x01, // day
                0x81, // month with century bit
                0x50, // year (50)
            ],
        )];

        let mut ds3231 = new_ds3231(I2cMock::new(&expectations));

        ds3231.set_datetime(&datetime).unwrap();

        ds3231.release_i2c().done();
    }

    #[test]
    fn test_set_datetime_invalid_year() {
        let datetime = DateTime::new(1980, 1, 1, 0, 0, 0).unwrap();
        let mut ds3231 = new_ds3231(I2cMock::new(&[]));

        let result = ds3231.set_datetime(&datetime);
        assert!(matches!(
            result,
            Err(crate::error::Error::DateTime(DateTimeError::InvalidYear))
        ));

        ds3231.release_i2c().done();
    }

    #[test]
    fn test_get_datetime_12h_mode_am() {
        // 01:15:30 AM, January 1, 2023 (Sunday)
        let data = [
            0x30,        // seconds = 30
            0x15,        // minutes = 15
            0b0100_0001, // hour register: 12h mode, hr=1, AM
            0x01,        // weekday = Sunday
            0x01,        // day of month
            0x01,        // month = January, century=0
            0x23,        // year = 23
        ];
        let expectations = [I2cTrans::write_read(
            0x68,
            vec![Register::Seconds.addr()],
            data.to_vec(),
        )];
        let mut ds3231 = new_ds3231(I2cMock::new(&expectations));

        let dt = ds3231.get_datetime().unwrap();
        assert_eq!((dt.hour(), dt.minute(), dt.second()), (1, 15, 30));

        ds3231.release_i2c().done();
    }

    #[test]
    fn test_get_datetime_12h_mode_pm() {
        // 11:45:50 PM, December 31, 2023 (Sunday)
        let data = [
            0x50,        // seconds = 50
            0x45,        // minutes = 45
            0b0110_1011, // hour register: 12h mode, hr=11, PM
            0x01,        // weekday = Sunday
            0x31,        // day of month
            0x12,        // month = December
            0x23,        // year = 23
        ];
        let expectations = [I2cTrans::write_read(
            0x68,
            vec![Register::Seconds.addr()],
            data.to_vec(),
        )];
        let mut ds3231 = new_ds3231(I2cMock::new(&expectations));

        let dt = ds3231.get_datetime().unwrap();
        assert_eq!(dt.hour(), 23); // 11 PM -> 23h
        assert_eq!(dt.month(), 12);
        assert_eq!(dt.day_of_month(), 31);

        ds3231.release_i2c().done();
    }

    #[test]
    fn test_get_datetime_12h_mode_12am() {
        // 12:10:00 AM, Feb 1, 2023 (Wednesday)
        let data = [
            0x00,        // seconds = 0
            0x10,        // minutes = 10
            0b0101_0010, // 12h mode (bit 6=1), hr=12 (0x12), AM (bit5=0)
            0x03,        // weekday = Tuesday
            0x01,        // day of month
            0x02,        // month = Feb
            0x23,        // year = 23
        ];
        let expectations = [I2cTrans::write_read(
            0x68,
            vec![Register::Seconds.addr()],
            data.to_vec(),
        )];
        let mut ds3231 = new_ds3231(I2cMock::new(&expectations));

        let dt = ds3231.get_datetime().unwrap();
        assert_eq!(dt.hour(), 0); // 12 AM should be 0h
        assert_eq!(dt.minute(), 10);

        ds3231.release_i2c().done();
    }

    #[test]
    fn test_get_datetime_12h_mode_12pm() {
        // 12:45:00 PM, Mar 1, 2023 (Wednesday)
        let data = [
            0x00,        // seconds = 0
            0x45,        // minutes = 45
            0b0111_0010, // 12h mode, hr=12, PM bit set (bit5=1)
            0x04,        // weekday = Wednesday
            0x01,        // day of month
            0x03,        // month = Mar
            0x23,        // year = 23
        ];
        let expectations = [I2cTrans::write_read(
            0x68,
            vec![Register::Seconds.addr()],
            data.to_vec(),
        )];
        let mut ds3231 = new_ds3231(I2cMock::new(&expectations));

        let dt = ds3231.get_datetime().unwrap();
        assert_eq!(dt.hour(), 12); // 12 PM should stay 12h
        assert_eq!(dt.minute(), 45);

        ds3231.release_i2c().done();
    }
}
}