diff --git a/libs/bao1x-hal/src/lib.rs b/libs/bao1x-hal/src/lib.rs index 6e9762aee..d0b54af4e 100644 --- a/libs/bao1x-hal/src/lib.rs +++ b/libs/bao1x-hal/src/lib.rs @@ -39,6 +39,8 @@ pub mod mbox; #[cfg(not(feature = "hosted-baosec"))] pub mod rram; // no signature checking in the kernel +#[cfg(not(feature = "hosted-baosec"))] +pub mod rtc; #[cfg(all(not(feature = "hosted-baosec"), not(feature = "kernel"),))] pub mod sigcheck; #[cfg(not(feature = "hosted-baosec"))] diff --git a/libs/bao1x-hal/src/rtc.rs b/libs/bao1x-hal/src/rtc.rs new file mode 100644 index 000000000..7449de00d --- /dev/null +++ b/libs/bao1x-hal/src/rtc.rs @@ -0,0 +1,146 @@ +use utralib::*; + +/* +The RTC is a weird bit of IP. It's a proprietary ARM block called the "PL031". +Because we don't have the source code for it, we couldn't extract a standard UTRA +header for the block. However, apparently ~ChatGPT~ knows about it, so I was able +to ask it to help generate the following user guide. + +Query: "give me a C header file for the PL031 RTC block". + +Here is a concise C header for the ARM PrimeCell PL031 RTC block. Field offsets follow the ARM TRM. Adjust types or volatile rules as needed for your environment. + +#ifndef PL031_H +#define PL031_H + +#include + +#define PL031_BASE + +/* Register offsets */ +#define PL031_DR 0x00 /* Data register */ +#define PL031_MR 0x04 /* Match register */ +#define PL031_LR 0x08 /* Load register */ +#define PL031_CR 0x0C /* Control register */ +#define PL031_IMSC 0x10 /* Interrupt mask set/clear */ +#define PL031_RIS 0x14 /* Raw interrupt status */ +#define PL031_MIS 0x18 /* Masked interrupt status */ +#define PL031_ICR 0x1C /* Interrupt clear */ +#define PL031_PERIPH_ID 0xFE0 +#define PL031_PCELL_ID 0xFF0 + +/* Bit definitions */ +#define PL031_CR_EN (1U << 0) +#define PL031_IMSC_MIS (1U << 0) +#define PL031_RIS_RIS (1U << 0) +#define PL031_MIS_MIS (1U << 0) +#define PL031_ICR_CLR (1U << 0) + +/* Access helpers */ +static inline void pl031_write(uint32_t offset, uint32_t val) { + *(volatile uint32_t *)(PL031_BASE + offset) = val; +} + +static inline uint32_t pl031_read(uint32_t offset) { + return *(volatile uint32_t *)(PL031_BASE + offset); +} + +#endif + +LR (Load register, 0x08) +Used to set the starting epoch. On the next second boundary, LR is copied into DR and becomes the running counter. + +MR (Match register, 0x04) +Stores a compare value. When DR equals MR, the block asserts its interrupt. Software typically uses this for scheduled wakeups or periodic tasks. + +CR (Control register, 0x0C) +Bit 0 enables or disables the RTC. When disabled, the counter stops incrementing. + +IMSC (Interrupt mask set or clear, 0x10) +Controls whether the match event generates an interrupt to the CPU. Bit 0 is the mask. + +RIS (Raw interrupt status, 0x14) +Reflects the unmasked interrupt state. Set when DR equals MR. + +MIS (Masked interrupt status, 0x18) +Reflects the final interrupt state after masking. This is what software normally checks. + +ICR (Interrupt clear, 0x1C) +Write 1 to bit 0 to clear the match interrupt so the next event can fire. + +Query: "how would i actually use this to keep time" + +To use the PL031 to keep time, treat it as a free running 1 Hz counter. The hardware increments the DR register once per second. The workflow has three steps. + +1. Initialize the counter +Write an initial epoch value to LR, then enable the block. + +/* Set initial epoch time, for example UNIX time */ +pl031_write(PL031_LR, initial_epoch_seconds); + +/* Enable RTC */ +pl031_write(PL031_CR, PL031_CR_EN); + +The PL031 copies LR into DR on the next tick, then DR increments once every second. + +2. Read the current time +Just read DR whenever you need seconds since the epoch you chose. + +uint32_t now = pl031_read(PL031_DR); + +If you want a fuller timestamp, convert now to a struct tm equivalent in software. + +3. Generate periodic events +If your system needs periodic wakeups or scheduling: + +uint32_t t = pl031_read(PL031_DR); +pl031_write(PL031_MR, t + interval_seconds); +pl031_write(PL031_IMSC, PL031_IMSC_MIS); /* unmask interrupt */ + +On interrupt, clear it and program the next match. + +pl031_write(PL031_ICR, PL031_ICR_CLR); + +Considerations + + If your system sleeps, the PL031 keeps running as long as its clock domain stays powered. + + If resets occur, reinitialize LR and CR. + + If subsecond precision is required, this peripheral cannot supply it; layer a software counter from a higher frequency timer. + + +~Good enough for me!~ + +*/ + +// data register +pub const DR: Register = Register::new(0, 0xFFFF_FFFF); +// match register +pub const MR: Register = Register::new(1, 0xFFFF_FFFF); +// load register +pub const LR: Register = Register::new(2, 0xFFFF_FFFF); + +// control register +pub const CR: Register = Register::new(3, 1); +pub const CR_EN: Field = Field::new(1, 0, CR); +// interrupt mask set/clear +pub const IMSC: Register = Register::new(4, 1); +pub const IMSC_MASK: Field = Field::new(1, 0, IMSC); +// raw interrupt status +pub const RIS: Register = Register::new(5, 1); +pub const RIS_STATUS: Field = Field::new(1, 0, RIS); +// masked interrupt status +pub const MIS: Register = Register::new(6, 1); +pub const MIS_STATUS: Field = Field::new(1, 0, MIS); +// interrupt clear +pub const ICR: Register = Register::new(7, 1); +pub const ICR_CLEAR: Field = Field::new(1, 0, ICR); + +// IP ID registers +pub const PERI_ID: Register = Register::new(1016, 0xffff_ffff); +pub const PRIME_ID: Register = Register::new(1020, 0xffff_ffff); + +pub const HW_RTC_BASE: usize = 0x40061000; + +pub const RTC_NUMREGS: usize = 10; diff --git a/services/bao1x-hal-service/src/servers/rtc.rs b/services/bao1x-hal-service/src/servers/rtc.rs new file mode 100644 index 000000000..4800367de --- /dev/null +++ b/services/bao1x-hal-service/src/servers/rtc.rs @@ -0,0 +1,182 @@ +use bao1x_hal::rtc::*; +use utralib::*; + +use crate::api::TimeOp; + +/// This is a "well known name" used by `libstd` to connect to the time server +/// Anyone who wants to check if time has been initialized would use this name. +pub const TIME_SERVER_PUBLIC: &'static [u8; 16] = b"timeserverpublic"; + +pub fn start_rtc_service() { + let _ = std::thread::spawn({ + move || { + rtc_service(); + } + }); +} + +fn rtc_service() -> ! { + // the public SID is well known and accessible by anyone who uses `libstd` + let pub_sid = + xous::create_server_with_address(&TIME_SERVER_PUBLIC).expect("Couldn't create Ticktimer server"); + let tt = ticktimer::Ticktimer::new().unwrap(); + + let rtc_range = xous::map_memory( + xous::MemoryAddress::new(bao1x_hal::rtc::HW_RTC_BASE), + None, + 4096, + xous::MemoryFlags::R | xous::MemoryFlags::W, + ) + .expect("couldn't map RTC range"); + let rtc = CSR::new(rtc_range.as_mut_ptr() as *mut u32); + + // this loop allows us to fail slightly more gracefully in the case that there is an RTC hardware + // failure + let mut start_rtc_secs = rtc.r(DR); + let mut start_tt_ms = tt.elapsed_ms(); + let mut utc_offset_ms = 0; + let mut tz_offset_ms = 0; + + let mut msg_opt = None; + let mut return_type = 0; + loop { + xous::reply_and_receive_next_legacy(pub_sid, &mut msg_opt, &mut return_type).unwrap(); + let msg = msg_opt.as_mut().unwrap(); + let opcode: Option = num_traits::FromPrimitive::from_usize(msg.body.id()); + log::debug!("{:?}", opcode); + match opcode { + Some(TimeOp::HwSync) => { + start_rtc_secs = rtc.r(DR); + start_tt_ms = tt.elapsed_ms(); + } + Some(TimeOp::GetUtcTimeMs) => { + if let Some(scalar) = msg.body.scalar_message_mut() { + let t = start_rtc_secs as i64 * 1000i64 + + (tt.elapsed_ms() - start_tt_ms) as i64 + + utc_offset_ms; + if t < 0 { + // the offset has some error in it, perhaps due to an RTC reset. reset the offset! + log::warn!( + "Time was negative, recovering from time setting error by clearing utc offset to 0" + ); + utc_offset_ms = 0; + } + log::trace!("utc ms {}", t); + // `Scalar2` return type + return_type = 2; + scalar.arg1 = (t as u64 & 0xFFFF_FFFF) as usize; + scalar.arg2 = (((t as u64) >> 32) & 0xFFFF_FFFF) as usize; + } + } + Some(TimeOp::GetLocalTimeMs) => { + if let Some(scalar) = msg.body.scalar_message_mut() { + log::trace!( + "current offset {}", + (start_rtc_secs as i64 * 1000i64 + (tt.elapsed_ms() - start_tt_ms) as i64) / 1000 + ); + let t = start_rtc_secs as i64 * 1000i64 + + (tt.elapsed_ms() - start_tt_ms) as i64 + + utc_offset_ms + + tz_offset_ms; + if t < 0 { + log::warn!( + "Time was negative, recovering from time setting error by clearing utc and timezone offsets to 0." + ); + utc_offset_ms = 0; + tz_offset_ms = 0; + } + log::trace!("local since epoch {}", t / 1000); + return_type = 2; + scalar.arg1 = (((t as u64) >> 32) & 0xFFFF_FFFF) as usize; + scalar.arg2 = (t as u64 & 0xFFFF_FFFF) as usize; + } + } + /* + Set with: + log::info!("Got NTP time: {}.{}", time.sec(), time.sec_fraction()); + let current_time = Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap() + + chrono::Duration::seconds(time.sec() as i64); + xous::send_message( + timeserver_cid, + Message::new_scalar( + crate::time::TimeOp::SetUtcTimeMs.to_usize().unwrap(), + ((current_time.timestamp_millis() as u64) >> 32) as usize, + (current_time.timestamp_millis() as u64 & 0xFFFF_FFFF) as usize, + 0, + 0, + ), + ) + .expect("couldn't set time"); + */ + Some(TimeOp::SetUtcTimeMs) => { + if let Some(scalar) = msg.body.scalar_message() { + let utc_hi_ms = scalar.arg1; + let utc_lo_ms = scalar.arg2; + let utc_time_ms = (utc_hi_ms as i64) << 32 | (utc_lo_ms as i64); + start_rtc_secs = rtc.r(DR); + start_tt_ms = tt.elapsed_ms(); + log::info!("utc_time: {}", utc_time_ms / 1000); + log::info!("rtc_secs: {}", start_rtc_secs); + log::info!("start_tt_ms: {}", start_tt_ms); + let offset = utc_time_ms - (start_rtc_secs as i64) * 1000; + utc_offset_ms = offset; + } + } + /* + `tz` is a float that represents the current UTC time offset (e.g. -8 for singapore) + see simple_kilofloat_parse() in dns/src/time.rs for an efficient string parser if needed + + let tz_offset_ms = (tz * 3600) as i64; + xous::send_message( + timeserver_cid, + Message::new_scalar( + crate::time::TimeOp::SetTzOffsetMs.to_usize().unwrap(), + (tz_offset_ms >> 32) as usize, + (tz_offset_ms & 0xFFFF_FFFF) as usize, + 0, + 0, + ), + ) + .expect("couldn't set timezone"); + */ + Some(TimeOp::SetTzOffsetMs) => { + if let Some(scalar) = msg.body.scalar_message() { + let tz_hi_ms = scalar.arg1; + let tz_lo_ms = scalar.arg2; + let tz_ms = ((tz_hi_ms as i64) << 32) | (tz_lo_ms as i64); + // sanity check with very broad bounds: I don't know of any time zones that are more + // than +/2 days from UTC 86400 seconds in a day, times 1000 + // milliseconds, times 2 days + if tz_ms < -(86400 * 1000 * 2) || tz_ms > 86400 * 1000 * 2 { + log::warn!("Requested timezone offset {} is out of bounds, ignoring!", tz_ms); + continue; + } else { + tz_offset_ms = tz_ms; + } + } + } + Some(TimeOp::WallClockTimeInit) => { + if let Some(scalar) = msg.body.scalar_message_mut() { + return_type = 2; + if utc_offset_ms == 0 { + scalar.arg1 = 0; + } else { + scalar.arg2 = 1; + } + } + } + // Calling this allows the caller to store the offsets used to compute the current + // time from the RTC clock setting. The caller would restore time through a pair + // of calls to SetUtcTimeMs with arg1/arg2, and SetTzOffsetMs with arg3/arg4. + Some(TimeOp::GetState) => { + if let Some(scalar) = msg.body.scalar_message_mut() { + scalar.arg1 = (utc_offset_ms & 0xFFFF_FFFFF) as usize; + scalar.arg2 = ((utc_offset_ms >> 32) & 0xFFFF_FFFF) as usize; + scalar.arg3 = (tz_offset_ms & 0xFFFF_FFFF) as usize; + scalar.arg4 = ((tz_offset_ms >> 32) & 0xFFFF_FFFF) as usize; + } + } + None => log::error!("Time server public thread received unknown opcode: {:?}", msg), + } + } +}