blob: ec5a5bb9533b6a8a8b56ff32821d8208c97a3804 [file] [log] [blame]
/*
* Copyright 2025 NXP
*
* SPDX-License-Identifier: Apache-2.0
*/
#define DT_DRV_COMPAT zephyr_rtc_counter
#include <zephyr/drivers/rtc.h>
#include <zephyr/drivers/counter.h>
#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <zephyr/types.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/timeutil.h>
#include "rtc_utils.h"
LOG_MODULE_REGISTER(rtc_counter, CONFIG_RTC_LOG_LEVEL);
struct rtc_counter_config {
const struct device *counter_dev;
/* Number of alarm channels */
uint8_t alarms_count;
};
struct rtc_counter_data {
/* Unix seconds offset from raw counter ticks */
int64_t epoch_offset;
/* protects epoch_offset */
struct k_spinlock lock;
#ifdef CONFIG_RTC_ALARM
/* false if counter has no alarm channels */
bool alarm_capable;
/* number of alarm channels exposed (<= underlying counter channels) */
uint8_t num_alarm_chans;
const struct device *rtc_dev;
/* Per-instance arrays provided by init macro */
rtc_alarm_callback *alarm_callback;
void **alarm_user_data;
uint16_t *alarm_mask;
struct rtc_time *alarm_time;
bool *alarm_pending;
#endif /* CONFIG_RTC_ALARM */
};
/*
* Generic RTC time to ticks conversion using time.h and counter API.
* Returns 0 on success, -EINVAL on invalid time or overflow.
*/
static int rtc_counter_time_to_ticks(const struct rtc_time *timeptr, uint32_t *ticks_out)
{
struct tm tm_val;
int64_t seconds64;
if (timeptr == NULL || ticks_out == NULL) {
return -EINVAL;
}
/* Populate broken-down time structure */
memset(&tm_val, 0, sizeof(tm_val));
tm_val.tm_sec = timeptr->tm_sec;
tm_val.tm_min = timeptr->tm_min;
tm_val.tm_hour = timeptr->tm_hour;
tm_val.tm_mday = timeptr->tm_mday;
tm_val.tm_mon = timeptr->tm_mon;
tm_val.tm_year = timeptr->tm_year;
tm_val.tm_isdst = -1;
/* UTC, 64-bit, no DST/timezone ambiguity */
seconds64 = timeutil_timegm64(&tm_val);
/* Reject invalid/negative/out-of-range for 32-bit tick domain */
if (seconds64 < 0 || seconds64 > (int64_t)UINT32_MAX) {
return -EINVAL;
}
*ticks_out = (uint32_t)seconds64;
return 0;
}
/* Generic RTC ticks to time conversion using time.h and counter API */
static void rtc_counter_ticks_to_time(uint32_t ticks, struct rtc_time *timeptr)
{
time_t seconds;
struct tm tm_val;
seconds = (time_t)ticks;
if (gmtime_r(&seconds, &tm_val) == NULL) {
memset(timeptr, 0, sizeof(struct rtc_time));
return;
}
timeptr->tm_sec = tm_val.tm_sec;
timeptr->tm_min = tm_val.tm_min;
timeptr->tm_hour = tm_val.tm_hour;
timeptr->tm_mday = tm_val.tm_mday;
timeptr->tm_mon = tm_val.tm_mon;
timeptr->tm_year = tm_val.tm_year;
timeptr->tm_wday = tm_val.tm_wday;
timeptr->tm_yday = tm_val.tm_yday;
timeptr->tm_isdst = -1;
timeptr->tm_nsec = 0;
}
#if defined(CONFIG_RTC_ALARM)
static void rtc_counter_alarm_callback(const struct device *counter_dev, uint8_t chan_id,
uint32_t ticks, void *user_data)
{
struct rtc_counter_data *data = (struct rtc_counter_data *)user_data;
const struct device *rtc_dev = data->rtc_dev;
if (chan_id < data->num_alarm_chans && data->alarm_callback[chan_id] != NULL) {
data->alarm_callback[chan_id](rtc_dev, chan_id, data->alarm_user_data[chan_id]);
data->alarm_pending[chan_id] = false;
} else if (chan_id < data->num_alarm_chans) {
data->alarm_pending[chan_id] = true;
} else {
LOG_DBG("Spurious alarm callback on channel %u (max %u)", chan_id,
data->num_alarm_chans ? (data->num_alarm_chans - 1U) : 0U);
}
}
static int rtc_counter_alarm_get_supported_fields(const struct device *dev, uint16_t id,
uint16_t *mask)
{
struct rtc_counter_data *data = dev->data;
if (!data->alarm_capable) {
return -ENOTSUP;
}
if (id >= data->num_alarm_chans) {
return -EINVAL;
}
*mask = (RTC_ALARM_TIME_MASK_SECOND | RTC_ALARM_TIME_MASK_MINUTE |
RTC_ALARM_TIME_MASK_HOUR | RTC_ALARM_TIME_MASK_MONTHDAY |
RTC_ALARM_TIME_MASK_MONTH | RTC_ALARM_TIME_MASK_YEAR |
RTC_ALARM_TIME_MASK_WEEKDAY | RTC_ALARM_TIME_MASK_YEARDAY);
return 0;
}
static int rtc_counter_alarm_set_time(const struct device *dev, uint16_t id, uint16_t mask,
const struct rtc_time *timeptr)
{
const struct rtc_counter_config *config = dev->config;
struct rtc_counter_data *data = dev->data;
uint32_t desired_ticks;
int ret;
int64_t epoch;
int64_t raw_alarm_ticks_64;
uint32_t top;
uint32_t alarm_ticks;
uint32_t now_raw;
struct counter_alarm_cfg alarm_cfg;
if (!data->alarm_capable) {
return -ENOTSUP;
}
if (id >= data->num_alarm_chans) {
return -EINVAL;
}
if (mask == 0U) {
/* Disable alarm on selected channel */
ret = counter_cancel_channel_alarm(config->counter_dev, (uint8_t)id);
if (ret == 0) {
K_SPINLOCK(&data->lock) {
data->alarm_mask[id] = 0U;
data->alarm_pending[id] = false;
memset(&data->alarm_time[id], 0, sizeof(struct rtc_time));
}
}
return ret;
}
if (timeptr == NULL) {
return -EINVAL;
}
if (!rtc_utils_validate_rtc_time(timeptr, mask)) {
return -EINVAL;
}
ret = rtc_counter_time_to_ticks(timeptr, &desired_ticks);
/* -EINVAL on overflow/invalid time */
if (ret < 0) {
return ret;
}
/* Convert desired absolute Unix time to a raw tick value for the counter */
K_SPINLOCK(&data->lock) {
epoch = data->epoch_offset;
}
raw_alarm_ticks_64 = (int64_t)desired_ticks - epoch;
/* Reject wraparound: must be within current window and not in the past */
top = counter_get_top_value(config->counter_dev);
/* would require wrap */
if (raw_alarm_ticks_64 < 0 || raw_alarm_ticks_64 > (int64_t)top) {
return -ERANGE;
}
alarm_ticks = (uint32_t)raw_alarm_ticks_64;
ret = counter_get_value(config->counter_dev, &now_raw);
if (ret < 0) {
return ret;
}
/* target already passed in this window or equals 'now' */
if (alarm_ticks <= now_raw) {
/* 1-tick guard, but cannot wrap */
if (now_raw == top) {
return -ERANGE;
}
alarm_ticks = now_raw + 1U;
}
alarm_cfg.callback = rtc_counter_alarm_callback;
alarm_cfg.ticks = alarm_ticks;
alarm_cfg.user_data = data;
alarm_cfg.flags = COUNTER_ALARM_CFG_ABSOLUTE | COUNTER_ALARM_CFG_EXPIRE_WHEN_LATE;
/* Record configured mask and time for get_time; clear pending */
K_SPINLOCK(&data->lock) {
data->alarm_mask[id] = mask;
data->alarm_time[id] = *timeptr;
data->alarm_pending[id] = false;
}
return counter_set_channel_alarm(config->counter_dev, (uint8_t)id, &alarm_cfg);
}
static int rtc_counter_alarm_get_time(const struct device *dev, uint16_t id, uint16_t *mask,
struct rtc_time *timeptr)
{
struct rtc_counter_data *data = dev->data;
int ret = 0;
if (!data->alarm_capable) {
return -ENOTSUP;
}
if (id >= data->num_alarm_chans) {
return -EINVAL;
}
if (mask == NULL || timeptr == NULL) {
return -EINVAL;
}
K_SPINLOCK(&data->lock) {
if (data->alarm_mask[id] == 0U) {
ret = -EINVAL;
} else {
*mask = data->alarm_mask[id];
*timeptr = data->alarm_time[id];
}
}
return ret;
}
static int rtc_counter_alarm_is_pending(const struct device *dev, uint16_t id)
{
int ret = 0;
struct rtc_counter_data *data = dev->data;
if (!data->alarm_capable) {
return -ENOTSUP;
}
if (id >= data->num_alarm_chans) {
return -EINVAL;
}
K_SPINLOCK(&data->lock) {
ret = data->alarm_pending[id] ? 1 : 0;
data->alarm_pending[id] = false;
}
return ret;
}
static int rtc_counter_alarm_set_callback(const struct device *dev, uint16_t id,
rtc_alarm_callback callback, void *user_data)
{
struct rtc_counter_data *data = dev->data;
if (!data->alarm_capable) {
return -ENOTSUP;
}
if (id >= data->num_alarm_chans) {
return -EINVAL;
}
K_SPINLOCK(&data->lock) {
data->alarm_callback[id] = callback;
data->alarm_user_data[id] = user_data;
}
return 0;
}
/* Compute rearm tick within current counter window.
* Returns true and writes the tick to out_ticks; returns false if it should be skipped.
*/
static inline bool rtc_counter_compute_rearm_ticks(int64_t raw_alarm_ticks_64, uint32_t now_raw,
uint32_t top, uint32_t *out_ticks)
{
/* Out of range */
if (raw_alarm_ticks_64 > (int64_t)top) {
return false;
}
/* In the past: arm to next tick if possible */
if (raw_alarm_ticks_64 < 0) {
if (now_raw == top) {
return false;
}
*out_ticks = now_raw + 1U;
return true;
}
/* Within range */
*out_ticks = (uint32_t)raw_alarm_ticks_64;
if (*out_ticks <= now_raw) {
if (now_raw == top) {
return false;
}
*out_ticks = now_raw + 1U;
}
return true;
}
/* Recompute and rearm all active alarms after a time base (epoch) change */
static void rtc_counter_reschedule_alarms(const struct device *dev)
{
const struct rtc_counter_config *config = dev->config;
struct rtc_counter_data *data = dev->data;
uint16_t configured_mask;
struct rtc_time configured_time;
uint32_t alarm_abs_ticks;
int64_t epoch;
int64_t raw_alarm_ticks_64;
uint32_t top;
uint32_t now_raw;
uint32_t alarm_ticks;
struct counter_alarm_cfg alarm_cfg;
/* Time base changed: reschedule any active alarms to the new epoch */
if (!(data->alarm_capable && data->num_alarm_chans > 0U)) {
return;
}
for (uint8_t id = 0U; id < data->num_alarm_chans; id++) {
/* Snapshot configured alarm (if any) and clear pending */
K_SPINLOCK(&data->lock) {
configured_mask = data->alarm_mask[id];
configured_time = data->alarm_time[id];
data->alarm_pending[id] = false;
}
if (configured_mask == 0U) {
continue;
}
/* Cancel any in-flight alarm before reprogramming */
(void)counter_cancel_channel_alarm(config->counter_dev, (uint8_t)id);
if (rtc_counter_time_to_ticks(&configured_time, &alarm_abs_ticks) < 0) {
/* Should not happen: skip this alarm */
continue;
}
K_SPINLOCK(&data->lock) {
epoch = data->epoch_offset;
}
raw_alarm_ticks_64 = (int64_t)alarm_abs_ticks - epoch;
top = counter_get_top_value(config->counter_dev);
if (counter_get_value(config->counter_dev, &now_raw) < 0) {
continue;
}
if (!rtc_counter_compute_rearm_ticks(raw_alarm_ticks_64, now_raw, top,
&alarm_ticks)) {
continue;
}
alarm_cfg.callback = rtc_counter_alarm_callback;
alarm_cfg.ticks = alarm_ticks;
alarm_cfg.user_data = data;
alarm_cfg.flags = COUNTER_ALARM_CFG_ABSOLUTE | COUNTER_ALARM_CFG_EXPIRE_WHEN_LATE;
(void)counter_set_channel_alarm(config->counter_dev, (uint8_t)id, &alarm_cfg);
}
}
#endif /* CONFIG_RTC_ALARM */
static int rtc_counter_set_time(const struct device *dev, const struct rtc_time *timeptr)
{
const struct rtc_counter_config *config = dev->config;
struct rtc_counter_data *data = dev->data;
uint32_t desired_ticks = 0;
uint32_t now_ticks = 0;
int ret;
if (timeptr == NULL) {
return -EINVAL;
}
ret = rtc_counter_time_to_ticks(timeptr, &desired_ticks);
/* -EINVAL on overflow/invalid time */
if (ret < 0) {
return ret;
}
/* Stop counter */
ret = counter_stop(config->counter_dev);
if (ret < 0) {
return ret;
}
ret = counter_get_value(config->counter_dev, &now_ticks);
if (ret < 0) {
return ret;
}
/* Update the software offset: offset = desired_time - now_ticks */
K_SPINLOCK(&data->lock) {
data->epoch_offset = (int64_t)desired_ticks - (int64_t)now_ticks;
}
#ifdef CONFIG_RTC_ALARM
rtc_counter_reschedule_alarms(dev);
#endif /* CONFIG_RTC_ALARM */
/* Restart counter */
ret = counter_start(config->counter_dev);
if (ret < 0) {
return ret;
}
return 0;
}
static int rtc_counter_get_time(const struct device *dev, struct rtc_time *timeptr)
{
const struct rtc_counter_config *config = dev->config;
struct rtc_counter_data *data = dev->data;
uint32_t now_ticks;
int ret;
int64_t epoch;
int64_t current_seconds;
if (timeptr == NULL) {
return -EINVAL;
}
ret = counter_get_value(config->counter_dev, &now_ticks);
if (ret < 0) {
return ret;
}
K_SPINLOCK(&data->lock) {
epoch = data->epoch_offset;
}
current_seconds = (int64_t)now_ticks + epoch;
if (current_seconds < 0 || current_seconds > UINT32_MAX) {
return -ERANGE;
}
memset(timeptr, 0, sizeof(struct rtc_time));
rtc_counter_ticks_to_time((uint32_t)current_seconds, timeptr);
return 0;
}
#if defined(CONFIG_RTC_UPDATE)
static int rtc_counter_update_set_callback(const struct device *dev, rtc_update_callback callback,
void *user_data)
{
ARG_UNUSED(dev);
ARG_UNUSED(callback);
ARG_UNUSED(user_data);
return -ENOTSUP;
}
#endif /* CONFIG_RTC_UPDATE */
#if defined(CONFIG_RTC_CALIBRATION)
static int rtc_counter_set_calibration(const struct device *dev, int32_t calibration)
{
ARG_UNUSED(dev);
ARG_UNUSED(calibration);
return -ENOTSUP;
}
static int rtc_counter_get_calibration(const struct device *dev, int32_t *calibration)
{
ARG_UNUSED(dev);
ARG_UNUSED(calibration);
return -ENOTSUP;
}
#endif /* CONFIG_RTC_CALIBRATION */
static int rtc_counter_init(const struct device *dev)
{
const struct rtc_counter_config *config = dev->config;
struct rtc_counter_data *data = dev->data;
uint32_t freq;
if (!device_is_ready(config->counter_dev)) {
LOG_ERR("Counter device %s not ready", config->counter_dev->name);
return -ENODEV;
}
/* Require a 1 Hz counter frequency */
freq = counter_get_frequency(config->counter_dev);
if (freq != 1U) {
LOG_ERR("Unsupported counter frequency: %u Hz (expected 1 Hz)", freq);
return -ENOTSUP;
}
/* Start with zero offset until rtc_set_time is called */
data->epoch_offset = 0;
#ifdef CONFIG_RTC_ALARM
data->rtc_dev = dev;
if (config->alarms_count == 0U) {
data->alarm_capable = false;
} else {
data->alarm_capable = true;
data->num_alarm_chans = config->alarms_count;
/* Clear per-channel state */
for (uint8_t i = 0; i < data->num_alarm_chans; i++) {
data->alarm_callback[i] = NULL;
data->alarm_user_data[i] = NULL;
data->alarm_mask[i] = 0;
memset(&data->alarm_time[i], 0, sizeof(struct rtc_time));
data->alarm_pending[i] = false;
}
}
#endif /* CONFIG_RTC_ALARM */
return 0;
}
static DEVICE_API(rtc, rtc_counter_driver_api) = {
.set_time = rtc_counter_set_time,
.get_time = rtc_counter_get_time,
#if defined(CONFIG_RTC_ALARM)
.alarm_get_supported_fields = rtc_counter_alarm_get_supported_fields,
.alarm_set_time = rtc_counter_alarm_set_time,
.alarm_get_time = rtc_counter_alarm_get_time,
.alarm_is_pending = rtc_counter_alarm_is_pending,
.alarm_set_callback = rtc_counter_alarm_set_callback,
#endif /* CONFIG_RTC_ALARM */
#if defined(CONFIG_RTC_UPDATE)
.update_set_callback = rtc_counter_update_set_callback,
#endif /* CONFIG_RTC_UPDATE */
#if defined(CONFIG_RTC_CALIBRATION)
.set_calibration = rtc_counter_set_calibration,
.get_calibration = rtc_counter_get_calibration,
#endif /* CONFIG_RTC_CALIBRATION */
};
/* Ensure RTC init priority is bigger than counter */
BUILD_ASSERT(CONFIG_RTC_INIT_PRIORITY > CONFIG_COUNTER_INIT_PRIORITY,
"RTC init priority must be bigger than counter");
#define RTC_COUNTER_ALARMS_COUNT(n) DT_PROP_OR(DT_DRV_INST(n), alarms_count, 0)
#define RTC_COUNTER_ALARMS_SZ(n) MAX(RTC_COUNTER_ALARMS_COUNT(n), 1)
#if defined(CONFIG_RTC_ALARM)
/* Per-instance static storage for alarm context, sized from DT */
#define RTC_COUNTER_DECLARE_ALARM_STORAGE(n) \
static rtc_alarm_callback rtc_counter_alarm_callback_arr_##n[RTC_COUNTER_ALARMS_SZ(n)]; \
static void *rtc_counter_alarm_user_data_arr_##n[RTC_COUNTER_ALARMS_SZ(n)]; \
static uint16_t rtc_counter_alarm_mask_arr_##n[RTC_COUNTER_ALARMS_SZ(n)]; \
static struct rtc_time rtc_counter_alarm_time_arr_##n[RTC_COUNTER_ALARMS_SZ(n)]; \
static bool rtc_counter_alarm_pending_arr_##n[RTC_COUNTER_ALARMS_SZ(n)];
DT_INST_FOREACH_STATUS_OKAY(RTC_COUNTER_DECLARE_ALARM_STORAGE)
#endif /* CONFIG_RTC_ALARM */
#define RTC_COUNTER_DEVICE_INIT(n) \
static const struct rtc_counter_config rtc_counter_config_##n = { \
.counter_dev = DEVICE_DT_GET(DT_INST_PARENT(n)), \
.alarms_count = DT_PROP_OR(DT_DRV_INST(n), alarms_count, 0), \
}; \
static struct rtc_counter_data rtc_counter_data_##n = { \
IF_ENABLED(CONFIG_RTC_ALARM, ( \
.alarm_callback = rtc_counter_alarm_callback_arr_##n, \
.alarm_user_data = rtc_counter_alarm_user_data_arr_##n, \
.alarm_mask = rtc_counter_alarm_mask_arr_##n, \
.alarm_time = rtc_counter_alarm_time_arr_##n, \
.alarm_pending = rtc_counter_alarm_pending_arr_##n, \
)) \
}; \
DEVICE_DT_INST_DEFINE(n, rtc_counter_init, NULL, &rtc_counter_data_##n, \
&rtc_counter_config_##n, POST_KERNEL, CONFIG_RTC_INIT_PRIORITY, \
&rtc_counter_driver_api);
DT_INST_FOREACH_STATUS_OKAY(RTC_COUNTER_DEVICE_INIT)