Added RtcClock DateTime and alarms (#213)

* Added RealTimeClock, DateTime and RTC alarms

* Improved documentation on weird behaviors in the RealTimeClock

* Fixed incorrect leap_year_check in RealTimeClock

* Fixed rtc-datetime PR feedback
This commit is contained in:
Trangar 2021-11-29 11:15:20 +01:00 committed by GitHub
parent de53600199
commit f68f148d12
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 533 additions and 4 deletions

View file

@ -22,3 +22,7 @@ jobs:
with: with:
command: test command: test
args: --doc --target x86_64-unknown-linux-gnu args: --doc --target x86_64-unknown-linux-gnu
- uses: actions-rs/cargo@v1
with:
command: test
args: --doc --target x86_64-unknown-linux-gnu --features chrono

View file

@ -25,6 +25,7 @@ void = { version = "1.0.2", default-features = false }
rand_core = "0.6.3" rand_core = "0.6.3"
futures = { version = "0.3", default-features = false, optional = true } futures = { version = "0.3", default-features = false, optional = true }
chrono = { version = "0.4", default-features = false, optional = true }
# namespaced features will let use use "dep:embassy-traits" in the features rather than using this # namespaced features will let use use "dep:embassy-traits" in the features rather than using this
# trick of renaming the crate. # trick of renaming the crate.

View file

@ -1,3 +0,0 @@
//! Real Time Clock (RTC)
// See [Chapter 4 Section 8](https://datasheets.raspberrypi.org/rp2040/rp2040_datasheet.pdf) for more details
// TODO

View file

@ -0,0 +1,67 @@
use chrono::{Datelike, Timelike};
use rp2040_pac::rtc::{rtc_0, rtc_1, setup_0, setup_1};
/// Alias for [`chrono::NaiveDateTime`]
pub type DateTime = chrono::NaiveDateTime;
/// Alias for [`chrono::Weekday`]
pub type DayOfWeek = chrono::Weekday;
/// Errors regarding the [`DateTime`] and [`DateTimeFilter`] structs.
///
/// [`DateTimeFilter`]: struct.DateTimeFilter.html
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Error {
/// The [DateTime] has an invalid year. The year must be between 0 and 4095.
InvalidYear,
/// The [DateTime] contains an invalid date.
InvalidDate,
/// The [DateTime] contains an invalid time.
InvalidTime,
}
pub(super) fn day_of_week_to_u8(dotw: DayOfWeek) -> u8 {
dotw.num_days_from_sunday() as u8
}
pub(crate) fn validate_datetime(dt: &DateTime) -> Result<(), Error> {
if dt.year() < 0 || dt.year() > 4095 {
// rp2040 can't hold these years
Err(Error::InvalidYear)
} else {
// The rest of the chrono date is assumed to be valid
Ok(())
}
}
pub(super) fn write_setup_0(dt: &DateTime, w: &mut setup_0::W) {
// Safety: the `.bits()` fields are marked `unsafe` but all bit values are valid
unsafe {
w.year().bits(dt.year() as u16);
w.month().bits(dt.month() as u8);
w.day().bits(dt.day() as u8);
}
}
pub(super) fn write_setup_1(dt: &DateTime, w: &mut setup_1::W) {
// Safety: the `.bits()` fields are marked `unsafe` but all bit values are valid
unsafe {
w.dotw().bits(dt.weekday().num_days_from_sunday() as u8);
w.hour().bits(dt.hour() as u8);
w.min().bits(dt.minute() as u8);
w.sec().bits(dt.second() as u8);
}
}
pub(super) fn datetime_from_registers(rtc_0: rtc_0::R, rtc_1: rtc_1::R) -> Result<DateTime, Error> {
let year = rtc_1.year().bits() as i32;
let month = rtc_1.month().bits() as u32;
let day = rtc_1.day().bits() as u32;
let hour = rtc_0.hour().bits() as u32;
let minute = rtc_0.min().bits() as u32;
let second = rtc_0.sec().bits() as u32;
let date = chrono::NaiveDate::from_ymd_opt(year, month, day).ok_or(Error::InvalidDate)?;
let time = chrono::NaiveTime::from_hms_opt(hour, minute, second).ok_or(Error::InvalidTime)?;
Ok(DateTime::new(date, time))
}

View file

@ -0,0 +1,133 @@
use rp2040_pac::rtc::{rtc_0, rtc_1, setup_0, setup_1};
/// Errors regarding the [`DateTime`] and [`DateTimeFilter`] structs.
///
/// [`DateTimeFilter`]: struct.DateTimeFilter.html
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Error {
/// The [DateTime] contains an invalid year value. Must be between `0..=4095`.
InvalidYear,
/// The [DateTime] contains an invalid month value. Must be between `1..=12`.
InvalidMonth,
/// The [DateTime] contains an invalid day value. Must be between `1..=31`.
InvalidDay,
/// The [DateTime] contains an invalid day of week. Must be between `0..=6` where 0 is Sunday.
InvalidDayOfWeek(
/// The value of the DayOfWeek that was given.
u8,
),
/// The [DateTime] contains an invalid hour value. Must be between `0..=23`.
InvalidHour,
/// The [DateTime] contains an invalid minute value. Must be between `0..=59`.
InvalidMinute,
/// The [DateTime] contains an invalid second value. Must be between `0..=59`.
InvalidSecond,
}
/// Structure containing date and time information
pub struct DateTime {
/// 0..4095
pub year: u16,
/// 1..12, 1 is January
pub month: u8,
/// 1..28,29,30,31 depending on month
pub day: u8,
///
pub day_of_week: DayOfWeek,
/// 0..23
pub hour: u8,
/// 0..59
pub minute: u8,
/// 0..59
pub second: u8,
}
/// A day of the week
#[repr(u8)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)]
#[allow(missing_docs)]
pub enum DayOfWeek {
Sunday = 0,
Monday = 1,
Tuesday = 2,
Wednesday = 3,
Thursday = 4,
Friday = 5,
Saturday = 6,
}
fn day_of_week_from_u8(v: u8) -> Result<DayOfWeek, Error> {
Ok(match v {
0 => DayOfWeek::Sunday,
1 => DayOfWeek::Monday,
2 => DayOfWeek::Tuesday,
3 => DayOfWeek::Wednesday,
4 => DayOfWeek::Thursday,
5 => DayOfWeek::Friday,
6 => DayOfWeek::Saturday,
x => return Err(Error::InvalidDayOfWeek(x)),
})
}
pub(super) fn day_of_week_to_u8(dotw: DayOfWeek) -> u8 {
dotw as u8
}
pub(super) fn validate_datetime(dt: &DateTime) -> Result<(), Error> {
if dt.year > 4095 {
Err(Error::InvalidYear)
} else if dt.month < 1 || dt.month > 12 {
Err(Error::InvalidMonth)
} else if dt.day < 1 || dt.day > 31 {
Err(Error::InvalidDay)
} else if dt.hour > 23 {
Err(Error::InvalidHour)
} else if dt.minute > 59 {
Err(Error::InvalidMinute)
} else if dt.second > 59 {
Err(Error::InvalidSecond)
} else {
Ok(())
}
}
pub(super) fn write_setup_0(dt: &DateTime, w: &mut setup_0::W) {
// Safety: the `.bits()` fields are marked `unsafe` but all bit values are valid
unsafe {
w.year().bits(dt.year);
w.month().bits(dt.month);
w.day().bits(dt.day);
}
}
pub(super) fn write_setup_1(dt: &DateTime, w: &mut setup_1::W) {
// Safety: the `.bits()` fields are marked `unsafe` but all bit values are valid
unsafe {
w.dotw().bits(dt.day_of_week as u8);
w.hour().bits(dt.hour);
w.min().bits(dt.minute);
w.sec().bits(dt.second);
}
}
pub(super) fn datetime_from_registers(rtc_0: rtc_0::R, rtc_1: rtc_1::R) -> Result<DateTime, Error> {
let year = rtc_1.year().bits();
let month = rtc_1.month().bits();
let day = rtc_1.day().bits();
let day_of_week = rtc_0.dotw().bits();
let hour = rtc_0.hour().bits();
let minute = rtc_0.min().bits();
let second = rtc_0.sec().bits();
let day_of_week = day_of_week_from_u8(day_of_week)?;
Ok(DateTime {
year,
month,
day,
day_of_week,
hour,
minute,
second,
})
}

View file

@ -0,0 +1,120 @@
use super::DayOfWeek;
use rp2040_pac::rtc::{irq_setup_0, irq_setup_1};
/// A filter used for [`RealTimeClock::schedule_alarm`].
///
/// [`RealTimeClock::schedule_alarm`]: struct.RealTimeClock.html#method.schedule_alarm
#[derive(Default)]
pub struct DateTimeFilter {
/// The year that this alarm should trigger on, `None` if the RTC alarm should not trigger on a year value.
pub year: Option<u16>,
/// The month that this alarm should trigger on, `None` if the RTC alarm should not trigger on a month value.
pub month: Option<u8>,
/// The day that this alarm should trigger on, `None` if the RTC alarm should not trigger on a day value.
pub day: Option<u8>,
/// The day of week that this alarm should trigger on, `None` if the RTC alarm should not trigger on a day of week value.
pub day_of_week: Option<DayOfWeek>,
/// The hour that this alarm should trigger on, `None` if the RTC alarm should not trigger on a hour value.
pub hour: Option<u8>,
/// The minute that this alarm should trigger on, `None` if the RTC alarm should not trigger on a minute value.
pub minute: Option<u8>,
/// The second that this alarm should trigger on, `None` if the RTC alarm should not trigger on a second value.
pub second: Option<u8>,
}
impl DateTimeFilter {
/// Set a filter on the given year
pub fn year(mut self, year: u16) -> Self {
self.year = Some(year);
self
}
/// Set a filter on the given month
pub fn month(mut self, month: u8) -> Self {
self.month = Some(month);
self
}
/// Set a filter on the given day
pub fn day(mut self, day: u8) -> Self {
self.day = Some(day);
self
}
/// Set a filter on the given day of the week
pub fn day_of_week(mut self, day_of_week: DayOfWeek) -> Self {
self.day_of_week = Some(day_of_week);
self
}
/// Set a filter on the given hour
pub fn hour(mut self, hour: u8) -> Self {
self.hour = Some(hour);
self
}
/// Set a filter on the given minute
pub fn minute(mut self, minute: u8) -> Self {
self.minute = Some(minute);
self
}
/// Set a filter on the given second
pub fn second(mut self, second: u8) -> Self {
self.second = Some(second);
self
}
}
// register helper functions
impl DateTimeFilter {
pub(super) fn write_setup_0(&self, w: &mut irq_setup_0::W) {
// Safety: setting .bits() is considered unsafe because
// svd2rust doesn't know what the valid values are.
// But all values in these bitmasks are safe
if let Some(year) = self.year {
w.year_ena().set_bit();
unsafe {
w.year().bits(year);
}
}
if let Some(month) = self.month {
w.month_ena().set_bit();
unsafe {
w.month().bits(month);
}
}
if let Some(day) = self.day {
w.day_ena().set_bit();
unsafe {
w.day().bits(day);
}
}
}
pub(super) fn write_setup_1(&self, w: &mut irq_setup_1::W) {
// Safety: setting .bits() is considered unsafe because
// svd2rust doesn't know what the valid values are.
// But all values in these bitmasks are safe
if let Some(day_of_week) = self.day_of_week {
w.dotw_ena().set_bit();
let bits = super::datetime::day_of_week_to_u8(day_of_week);
unsafe {
w.dotw().bits(bits);
}
}
if let Some(hour) = self.hour {
w.hour_ena().set_bit();
unsafe {
w.hour().bits(hour);
}
}
if let Some(minute) = self.minute {
w.min_ena().set_bit();
unsafe {
w.min().bits(minute);
}
}
if let Some(second) = self.second {
w.sec_ena().set_bit();
unsafe {
w.sec().bits(second);
}
}
}
}

207
rp2040-hal/src/rtc/mod.rs Normal file
View file

@ -0,0 +1,207 @@
//! Real time clock functionality
//!
//! A [`RealTimeClock`] can be configured with an initial [`DateTime`]. Afterwards the clock will track time automatically. The current `DateTime` can be retrieved by [`RealTimeClock::now()`].
//!
//! With the **chrono** feature enabled, the following types will be alias for chrono types:
//! - `DateTime`: `chrono::NaiveDateTime`
//! - `DayOfWeek`: `chrono::Weekday`
//!
//! # Notes
//!
//! There are some things to take into account. As per the datasheet:
//!
//! - **Day of week**: The RTC will not compute the correct day of the week; it will only increment the existing value.
//! - With the `chrono` feature, the day of week is calculated by chrono and should be correct. The value from the rp2040 itself is not used.
//! - **Leap year**: If the current year is evenly divisible by 4, a leap year is detected, then Feb 28th is followed by Feb 29th instead of March 1st.
//! - There are cases where this is incorrect, e.g. century years have no leap day, but the chip will still add a Feb 29th.
//! - To disable leap year checking and never have a Feb 29th, call `RealTimeClock::set_leap_year_check(false)`.
//!
//! Other limitations:
//!
//! - **Leap seconds**: The rp2040 will not take leap seconds into account
//! - With the `chrono` feature, leap seconds will be silently handled by `chrono`. This means there might be a slight difference between the value of [`RealTimeClock::now()`] and adding 2 times together in code.
use crate::clocks::Clock;
use crate::clocks::RtcClock;
use embedded_time::fixed_point::FixedPoint;
use rp2040_pac::{RESETS, RTC};
mod filter;
pub use self::filter::DateTimeFilter;
#[cfg_attr(feature = "chrono", path = "datetime_chrono.rs")]
#[cfg_attr(not(feature = "chrono"), path = "datetime_no_deps.rs")]
mod datetime;
pub use self::datetime::{DateTime, DayOfWeek, Error as DateTimeError};
/// A reference to the real time clock of the system
pub struct RealTimeClock {
rtc: RTC,
}
impl RealTimeClock {
/// Create a new instance of the real time clock, with the given date as an initial value.
///
/// Note that the [`ClocksManager`] should be enabled first. See the [`clocks`] module for more information.
///
/// # Errors
///
/// Will return `RtcError::InvalidDateTime` if the datetime is not a valid range.
///
/// [`ClocksManager`]: ../clocks/struct.ClocksManager.html
/// [`clocks`]: ../clocks/index.html
pub fn new(
rtc: RTC,
clock: RtcClock,
resets: &mut RESETS,
initial_date: DateTime,
) -> Result<Self, RtcError> {
// Toggle the RTC reset
resets.reset.modify(|_, w| w.rtc().set_bit());
resets.reset.modify(|_, w| w.rtc().clear_bit());
while resets.reset_done.read().rtc().bit_is_clear() {
core::hint::spin_loop();
}
// Set the RTC divider
let freq = clock.freq().integer() - 1;
rtc.clkdiv_m1.write(|w| unsafe { w.bits(freq) });
let mut result = Self { rtc };
result.set_leap_year_check(true); // should be on by default, make sure this is the case.
result.set_datetime(initial_date)?;
Ok(result)
}
/// Enable or disable the leap year check. The rp2040 chip will always add a Feb 29th on every year that is divisable by 4, but this may be incorrect (e.g. on century years). This function allows you to disable this check.
///
/// Leap year checking is enabled by default.
pub fn set_leap_year_check(&mut self, leap_year_check_enabled: bool) {
self.rtc
.ctrl
.modify(|_, w| w.force_notleapyear().bit(!leap_year_check_enabled));
}
/// Checks to see if this RealTimeClock is running
pub fn is_running(&self) -> bool {
self.rtc.ctrl.read().rtc_active().bit_is_set()
}
/// Set the datetime to a new value.
///
/// # Errors
///
/// Will return `RtcError::InvalidDateTime` if the datetime is not a valid range.
pub fn set_datetime(&mut self, t: DateTime) -> Result<(), RtcError> {
self::datetime::validate_datetime(&t).map_err(RtcError::InvalidDateTime)?;
// disable RTC while we configure it
self.rtc.ctrl.modify(|_, w| w.rtc_enable().clear_bit());
while self.rtc.ctrl.read().rtc_active().bit_is_set() {
core::hint::spin_loop();
}
self.rtc.setup_0.write(|w| {
self::datetime::write_setup_0(&t, w);
w
});
self.rtc.setup_1.write(|w| {
self::datetime::write_setup_1(&t, w);
w
});
// Load the new datetime and re-enable RTC
self.rtc.ctrl.write(|w| w.load().set_bit());
self.rtc.ctrl.write(|w| w.rtc_enable().set_bit());
while self.rtc.ctrl.read().rtc_active().bit_is_clear() {
core::hint::spin_loop();
}
Ok(())
}
/// Return the current datetime.
///
/// # Errors
///
/// Will return an `RtcError::InvalidDateTime` if the stored value in the system is not a valid [`DayOfWeek`].
pub fn now(&self) -> Result<DateTime, RtcError> {
if !self.is_running() {
return Err(RtcError::NotRunning);
}
let rtc_0 = self.rtc.rtc_0.read();
let rtc_1 = self.rtc.rtc_1.read();
self::datetime::datetime_from_registers(rtc_0, rtc_1).map_err(RtcError::InvalidDateTime)
}
/// Disable the alarm that was scheduled with [`schedule_alarm`].
///
/// [`schedule_alarm`]: #method.schedule_alarm
pub fn disable_alarm(&mut self) {
self.rtc
.irq_setup_0
.modify(|_, s| s.match_ena().clear_bit());
while self.rtc.irq_setup_0.read().match_active().bit() {
core::hint::spin_loop();
}
}
/// Schedule an alarm. The `filter` determines at which point in time this alarm is set.
///
/// Keep in mind that the filter only triggers on the specified time. If you want to schedule this alarm every minute, you have to call:
/// ```no_run
/// # #[cfg(feature = "chrono")]
/// # fn main() { }
/// # #[cfg(not(feature = "chrono"))]
/// # fn main() {
/// # use rp2040_hal::rtc::{RealTimeClock, DateTimeFilter};
/// # let mut real_time_clock: RealTimeClock = unsafe { core::mem::zeroed() };
/// let now = real_time_clock.now().unwrap();
/// real_time_clock.schedule_alarm(
/// DateTimeFilter::default()
/// .minute(if now.minute == 59 { 0 } else { now.minute + 1 })
/// );
/// # }
/// ```
pub fn schedule_alarm(&mut self, filter: DateTimeFilter) {
self.disable_alarm();
self.rtc.irq_setup_0.write(|w| {
filter.write_setup_0(w);
w
});
self.rtc.irq_setup_1.write(|w| {
filter.write_setup_1(w);
w
});
// Set the enable bit and check if it is set
self.rtc.irq_setup_0.modify(|_, w| w.match_ena().set_bit());
while self.rtc.irq_setup_0.read().match_active().bit_is_clear() {
core::hint::spin_loop();
}
}
/// Clear the interrupt. This should be called every time the `RTC_IRQ` interrupt is triggered,
/// or the next [`schedule_alarm`] will never fire.
///
/// [`schedule_alarm`]: #method.schedule_alarm
pub fn clear_interrupt(&mut self) {
self.disable_alarm();
}
}
/// Errors that can occur on methods on [RtcClock]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RtcError {
/// An invalid DateTime was given or stored on the hardware.
InvalidDateTime(DateTimeError),
/// The RTC clock is not running
NotRunning,
}