415 lines
11 KiB
C
415 lines
11 KiB
C
// MIT License
|
|
// Copyright (c) 2025 UniTheCat
|
|
// This library supports two WCH hardware implementations for an RTC
|
|
// and distinguishes between them using compile-time flags.
|
|
#include <stdio.h>
|
|
#if defined(CH32V10x)
|
|
#include "ch32v103hw.h"
|
|
#elif defined (CH32L103)
|
|
#include "ch32l103hw.h"
|
|
#elif defined (CH32V20x)
|
|
#include "ch32v20xhw.h"
|
|
#elif defined (CH32V30x)
|
|
#include "ch32v30xhw.h"
|
|
#elif defined (CH5xx)
|
|
#include "ch5xxhw.h"
|
|
#else
|
|
#error "Currently unsupported RTC"
|
|
#endif
|
|
|
|
#if !defined(RTC_CLOCK_SOURCE_LSI) && !defined(RTC_CLOCK_SOURCE_LSE)
|
|
#define RTC_CLOCK_SOURCE_LSI
|
|
#endif
|
|
|
|
#define RTC_CLOCK_TICKS_PER_SECOND 32768
|
|
|
|
#ifndef CH5xx // No custom RTC divider support on CH5xx
|
|
#ifndef RTC_CLOCK_TICKS_PER_INCREMENT
|
|
#define RTC_CLOCK_TICKS_PER_INCREMENT RTC_CLOCK_TICKS_PER_SECOND
|
|
#endif
|
|
#define RTC_INCREMENTS_PER_SECOND (RTC_CLOCK_TICKS_PER_SECOND / RTC_CLOCK_TICKS_PER_INCREMENT)
|
|
#endif // !CH5xx
|
|
|
|
#define SECONDS_PER_MINUTE 60
|
|
#define SECONDS_PER_HOUR 3600
|
|
#define SECONDS_PER_DAY 86400
|
|
|
|
#define IS_LEAP_YEAR(year) ((((year) % 4 == 0) && ((year) % 100 != 0)) || ((year) % 400 == 0))
|
|
|
|
// Array of days in each month (non-leap year)
|
|
const u8 DAYS_IN_MONTH[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
|
|
|
|
|
|
typedef struct {
|
|
u16 year;
|
|
u8 month;
|
|
u8 day;
|
|
} rtc_date_t;
|
|
|
|
typedef struct {
|
|
u8 hr;
|
|
u8 min;
|
|
u8 sec;
|
|
u16 ms;
|
|
} rtc_time_t;
|
|
|
|
typedef struct {
|
|
rtc_date_t date;
|
|
rtc_time_t time;
|
|
} rtc_datetime_t;
|
|
|
|
const rtc_date_t date_reference = {2020, 1, 1};
|
|
|
|
// calculate days of the current year. eg. 2020-02-05 is 36 day
|
|
u32 RTC_days_of_year(u16 year, u8 month, u8 day) {
|
|
u32 day_of_year = 0;
|
|
|
|
// Add days from January to month-1
|
|
for (u8 m = 0; m < month - 1; m++) {
|
|
day_of_year += DAYS_IN_MONTH[m];
|
|
}
|
|
|
|
// Add extra day for February (full month) if it's a leap year
|
|
if (month > 2 && IS_LEAP_YEAR(year)) {
|
|
day_of_year += 1;
|
|
}
|
|
|
|
// Add days in the current month
|
|
day_of_year += day;
|
|
|
|
return day_of_year;
|
|
}
|
|
|
|
u32 RTC_get_seconds(u16 year, u8 month, u8 day, u8 hr, u8 min, u8 sec) {
|
|
//# Validate input
|
|
if (month < 1 || month > 12 || day < 1 || day > 31 ||
|
|
hr > 23 || min > 59 || sec > 59 || year < 1970) { return 0; }
|
|
|
|
// calculate days of the current year, -1 for 0-based days
|
|
u32 days = RTC_days_of_year(year, month, day) - 1;
|
|
|
|
// add the days count excluding the current year
|
|
// (start from epoch time 1970-01-01 00:00:00)
|
|
for (int y=1970; y < year; y++) {
|
|
days += IS_LEAP_YEAR(y) ? 366 : 365;
|
|
}
|
|
|
|
// calculate total seconds
|
|
return days * SECONDS_PER_DAY +
|
|
hr * SECONDS_PER_HOUR +
|
|
min * SECONDS_PER_MINUTE + sec;
|
|
}
|
|
|
|
rtc_time_t RTC_get_time(u32 total_seconds, u16 ms) {
|
|
u32 minutes = total_seconds / 60;
|
|
|
|
return (rtc_time_t) {
|
|
.sec = total_seconds % 60,
|
|
.min = minutes % 60,
|
|
.hr = (minutes / 60) % 24,
|
|
.ms = ms
|
|
};
|
|
}
|
|
|
|
rtc_date_t RTC_get_date(u32 total_seconds) {
|
|
rtc_date_t output = {
|
|
.year = date_reference.year,
|
|
.month = date_reference.month,
|
|
.day = date_reference.day
|
|
};
|
|
|
|
// Days since epoch
|
|
u32 days_remaining = total_seconds / SECONDS_PER_DAY;
|
|
|
|
// Find the year
|
|
while(1) {
|
|
u16 days_in_year = IS_LEAP_YEAR(output.year) ? 366 : 365;
|
|
if (days_remaining < days_in_year) break;
|
|
days_remaining -= days_in_year;
|
|
output.year++;
|
|
}
|
|
|
|
// find the month
|
|
for (u8 m = 0; m < 12; m++) {
|
|
u8 days_in_month = DAYS_IN_MONTH[m];
|
|
if (m == 1 && IS_LEAP_YEAR(output.year)) days_in_month = 29;
|
|
if (days_remaining < days_in_month) break;
|
|
|
|
days_remaining -= days_in_month;
|
|
output.month++;
|
|
}
|
|
|
|
// add 1 because days_remaining is 0-based
|
|
output.day = days_remaining + 1;
|
|
|
|
return output;
|
|
}
|
|
|
|
void RTC_print_date(rtc_date_t date, char *delimiter) {
|
|
printf("%04d", date.year);
|
|
printf("%s%02d", delimiter, date.month);
|
|
printf("%s%02d", delimiter, date.day);
|
|
}
|
|
|
|
void RTC_print_time(rtc_time_t time) {
|
|
printf("%02d:%02d:%02d.%03d",
|
|
time.hr, time.min, time.sec, time.ms);
|
|
}
|
|
|
|
|
|
//! ####################################
|
|
//! CORE FUNCTION
|
|
//! ####################################
|
|
|
|
// There are potential speed improvements to this function, such as not resetting the
|
|
// backup domain or the LSI/LSE clock if they are already enabled. However, after
|
|
// testing with standby resets, it either didn't work at all or stopped working
|
|
// after a few minutes. Tread carefully!
|
|
void RTC_init() {
|
|
#ifdef CH5xx
|
|
SYS_SAFE_ACCESS(
|
|
R32_RTC_TRIG = 0;
|
|
R32_RTC_CTRL |= RB_RTC_LOAD_HI;
|
|
R32_RTC_CTRL |= RB_RTC_LOAD_LO;
|
|
R8_RTC_MODE_CTRL |= RB_RTC_TRIG_EN; //enable RTC trigger
|
|
);
|
|
#else
|
|
RCC->APB1PCENR |= RCC_APB1Periph_PWR | RCC_APB1Periph_BKP;
|
|
|
|
// backup domain enable
|
|
PWR->CTLR |= PWR_CTLR_DBP;
|
|
|
|
// prepare to configure clock source
|
|
RCC->BDCTLR |= RCC_BDRST;
|
|
RCC->BDCTLR &=~ RCC_BDRST;
|
|
|
|
#ifdef RTC_CLOCK_SOURCE_LSI
|
|
// Takes 300us, even if already enabled
|
|
RCC->RSTSCKR |= RCC_LSION;
|
|
while ( !(RCC->RSTSCKR & RCC_LSIRDY) );
|
|
|
|
// Set clock source for RTC
|
|
RCC->BDCTLR &=~ RCC_RTCSEL;
|
|
RCC->BDCTLR |= RCC_RTCSEL_LSI;
|
|
#elif defined(RTC_CLOCK_SOURCE_LSE)
|
|
// Enable LSE
|
|
RCC->BDCTLR |= RCC_LSEON;
|
|
uint16_t timeout = 0;
|
|
while ( !(RCC->BDCTLR & RCC_LSERDY) ) {
|
|
Delay_Ms(1);
|
|
timeout++;
|
|
if (timeout > 1000) {
|
|
printf("Could not start LSE.\r\n");
|
|
break;
|
|
}
|
|
}
|
|
// Set clock source for RTC
|
|
RCC->BDCTLR &=~ RCC_RTCSEL;
|
|
RCC->BDCTLR |= RCC_RTCSEL_LSE;
|
|
#else
|
|
#error "No RTC clock source defined"
|
|
#endif
|
|
|
|
RCC->BDCTLR |= RCC_RTCEN;
|
|
|
|
// Wait until we can safely communicate with RTC registers
|
|
RTC->CTLRL &= ~RTC_FLAG_RSF;
|
|
while (!(RTC->CTLRL & RTC_FLAG_RSF));
|
|
|
|
// Reset alarm, second, and overflow flags
|
|
RTC->CTLRL &= ~(RTC_CTLRL_ALRF | RTC_CTLRL_SECF | RTC_CTLRL_OWF);
|
|
|
|
RTC_CONFIG_CHANGE(
|
|
// Set prescaler value
|
|
RTC->PSCRH = ((RTC_CLOCK_TICKS_PER_INCREMENT-1) & PRLH_MSB_MASK) >> 16;
|
|
RTC->PSCRL = (RTC_CLOCK_TICKS_PER_INCREMENT-1) & RTC_LSB_MASK;
|
|
);
|
|
#endif
|
|
}
|
|
|
|
#ifdef CH5xx
|
|
void RTC_getCounter(u16 *days, u16 *seconds_2s, u16 *ticks_32k) {
|
|
u16 days_1, days_2, seconds_2s_1, seconds_2s_2, ticks;
|
|
|
|
// properly deal with asynchronous read of multiple registers in
|
|
// rollover case
|
|
days_1 = R32_RTC_CNT_DAY;
|
|
seconds_2s_1 = R16_RTC_CNT_2S;
|
|
ticks = R16_RTC_CNT_32K;
|
|
days_2 = R32_RTC_CNT_DAY;
|
|
seconds_2s_2 = R16_RTC_CNT_2S;
|
|
|
|
if (days_1 != days_2 || seconds_2s_1 != seconds_2s_2) {
|
|
// Rollover occurred, re-read all values
|
|
*days = R32_RTC_CNT_DAY;
|
|
*seconds_2s = R16_RTC_CNT_2S;
|
|
*ticks_32k = R16_RTC_CNT_32K;
|
|
} else {
|
|
// No rollover, use the captured values
|
|
*days = days_1;
|
|
*seconds_2s = seconds_2s_1;
|
|
*ticks_32k = ticks;
|
|
}
|
|
}
|
|
|
|
#else
|
|
uint32_t RTC_getCounter(void) {
|
|
uint16_t high1, high2, low;
|
|
uint32_t s;
|
|
|
|
// Wait for registers to be synchronized
|
|
RTC->CTLRL &= ~RTC_FLAG_RSF;
|
|
while (!(RTC->CTLRL & RTC_FLAG_RSF));
|
|
|
|
high1 = RTC->CNTH;
|
|
low = RTC->CNTL;
|
|
high2 = RTC->CNTH;
|
|
|
|
// properly deal with asynchronous read of multiple registers in
|
|
// rollover case
|
|
if (high1 != high2) {
|
|
s = (high2 << 16) | RTC->CNTL;
|
|
} else {
|
|
s = (high1 << 16) | low;
|
|
}
|
|
|
|
return s;
|
|
}
|
|
#endif
|
|
|
|
rtc_datetime_t RTC_getDateTime() {
|
|
#ifdef CH5xx
|
|
u16 days, seconds_2s, ticks_32k;
|
|
RTC_getCounter(&days, &seconds_2s, &ticks_32k);
|
|
u32 total_seconds = (days * SECONDS_PER_DAY) + (seconds_2s * 2) + (ticks_32k / RTC_CLOCK_TICKS_PER_SECOND);
|
|
u16 ms = ((ticks_32k % RTC_CLOCK_TICKS_PER_SECOND) * 1000UL) / RTC_CLOCK_TICKS_PER_SECOND;
|
|
#else
|
|
u32 count = RTC_getCounter();
|
|
u32 total_seconds = count / RTC_INCREMENTS_PER_SECOND;
|
|
u16 ms = ((count % RTC_INCREMENTS_PER_SECOND) * 1000UL) / RTC_INCREMENTS_PER_SECOND;
|
|
#endif
|
|
|
|
rtc_date_t date = RTC_get_date(total_seconds);
|
|
rtc_time_t time = RTC_get_time(total_seconds, ms);
|
|
return (rtc_datetime_t) {date, time};
|
|
}
|
|
|
|
#ifdef CH5xx
|
|
void RTC_setDayCounter(u32 day_counter) {
|
|
SYS_SAFE_ACCESS(
|
|
R32_RTC_TRIG = day_counter - 1;
|
|
R8_RTC_MODE_CTRL |= RB_RTC_LOAD_HI;
|
|
);
|
|
}
|
|
|
|
void RTC_setTimeCounter(u32 time_counter) {
|
|
SYS_SAFE_ACCESS(
|
|
R32_RTC_TRIG = time_counter;
|
|
R8_RTC_MODE_CTRL |= RB_RTC_LOAD_LO;
|
|
);
|
|
}
|
|
#endif // CH5xx
|
|
|
|
void RTC_setDateTime(rtc_datetime_t *datetime) {
|
|
// Clip year
|
|
u16 year_min = date_reference.year;
|
|
#ifdef CH5xx
|
|
u16 year_max = date_reference.year + 0x3FFF / 365; // 14 bits = ~44 years
|
|
#else
|
|
u16 year_max = date_reference.year + 0xFFFFFFFF / 60 / 60 / 24 / 365 / RTC_INCREMENTS_PER_SECOND;
|
|
#endif
|
|
|
|
if (datetime->date.year < year_min) datetime->date.year = year_min;
|
|
if (datetime->date.year > year_max) datetime->date.year = year_max;
|
|
|
|
#ifdef CH5xx
|
|
u32 days_counter = 0;
|
|
// Add days for complete years from year_min to year-1
|
|
for (u16 y = year_min; y < datetime->date.year; y++) {
|
|
days_counter += IS_LEAP_YEAR(y) ? 366 : 365;
|
|
}
|
|
|
|
// Add days in the current year
|
|
days_counter += RTC_days_of_year(datetime->date.year, datetime->date.month, datetime->date.day);
|
|
RTC_setDayCounter(days_counter);
|
|
|
|
u16 seconds = datetime->time.hr * 3600 + datetime->time.min * 60 + datetime->time.sec;
|
|
u16 time_2s = seconds / 2;
|
|
u32 time_32k = (seconds % 2) * RTC_CLOCK_TICKS_PER_SECOND + datetime->time.ms * RTC_CLOCK_TICKS_PER_SECOND / 1000;
|
|
u32 time_counter = (time_2s << 16) | (time_32k & 0xFFFF);
|
|
RTC_setTimeCounter(time_counter);
|
|
|
|
#else
|
|
u32 total_seconds = RTC_get_seconds(datetime->date.year, datetime->date.month, datetime->date.day,
|
|
datetime->time.hr, datetime->time.min, datetime->time.sec);
|
|
u32 increments = total_seconds * RTC_INCREMENTS_PER_SECOND +
|
|
(datetime->time.ms * RTC_INCREMENTS_PER_SECOND) / 1000;
|
|
RTC_CONFIG_CHANGE(
|
|
RTC->CNTH = increments >> 16;
|
|
RTC->CNTL = increments & RTC_LSB_MASK;
|
|
);
|
|
#endif
|
|
}
|
|
|
|
#ifdef CH5xx
|
|
// Set RTC to generate an interrupt after cyc ticks.
|
|
// Keep RTCTrigger function name for legacy reasons
|
|
void RTCTrigger(uint32_t cyc)
|
|
{
|
|
//get the rtc current time
|
|
uint32_t alarm = (uint32_t) R16_RTC_CNT_LSI | ( (uint32_t) R16_RTC_CNT_DIV1 << 16 );
|
|
alarm += cyc;
|
|
|
|
if( alarm > RTC_MAX_COUNT ) {
|
|
alarm -= RTC_MAX_COUNT;
|
|
}
|
|
|
|
SYS_SAFE_ACCESS(
|
|
R32_RTC_TRIG = alarm;
|
|
);
|
|
}
|
|
void RTC_setAlarm(u32 increments) { RTCTrigger(increments); }
|
|
#else
|
|
void RTC_setAlarm(u32 increments) {
|
|
// Make sure AFIO power is on for EXTI interrupts
|
|
RCC->APB2PCENR |= RCC_AFIOEN;
|
|
|
|
// Clear any pending alarm interrupt before reconfiguring
|
|
NVIC_ClearPendingIRQ(RTCAlarm_IRQn);
|
|
EXTI->INTFR = EXTI_Line17;
|
|
RTC->CTLRL &= ~RTC_CTLRL_ALRF;
|
|
|
|
RTC_CONFIG_CHANGE(
|
|
RTC->ALRMH = increments >> 16;
|
|
RTC->ALRML = increments & RTC_LSB_MASK;
|
|
);
|
|
|
|
// A few RTC ticks are necessary before enabling EXTI, as they are on two
|
|
// different clock domains and <my current working theory> EXTI fires prematurely
|
|
// on stale alarm register values?
|
|
// What is weird is that waiting for the register synchronization flag (RSF) to be
|
|
// set is not sufficient and doesn't appear to make any difference for the timing!
|
|
Delay_Us(100);
|
|
|
|
EXTI->INTENR |= EXTI_Line17;
|
|
EXTI->RTENR |= EXTI_Line17; // RTC alarm is rising-edge
|
|
NVIC_EnableIRQ(RTCAlarm_IRQn);
|
|
RTC->CTLRH |= RTC_CTLRH_ALRIE;
|
|
}
|
|
|
|
void RTC_setAlarmAbsolute(rtc_datetime_t *datetime) {
|
|
u32 total_seconds = RTC_get_seconds(datetime->date.year, datetime->date.month, datetime->date.day,
|
|
datetime->time.hr, datetime->time.min, datetime->time.sec);
|
|
u32 increments = total_seconds * RTC_INCREMENTS_PER_SECOND +
|
|
(datetime->time.ms * RTC_INCREMENTS_PER_SECOND) / 1000;
|
|
RTC_setAlarm(increments);
|
|
}
|
|
|
|
void RTC_setAlarmRelative(u16 seconds_from_now, u16 milliseconds_from_now) {
|
|
u32 current = RTC_getCounter();
|
|
u32 alarm_value = current + (seconds_from_now * RTC_INCREMENTS_PER_SECOND) +
|
|
(milliseconds_from_now * RTC_INCREMENTS_PER_SECOND) / 1000;
|
|
RTC_setAlarm(alarm_value);
|
|
}
|
|
|
|
#endif // !CH5xx
|