4c880f4b0a
RTC drivers should validate the `struct rtc_time`'s contents against the provided `mask`. Promote this common code to a new rtc_utils file and modify existing drivers to use this functionality. Extend the test coverage to include verifying this behaviour. This is groundwork ahead of adding support for the RP2040's (as used in the Raspberry Pi Pico) RTC and alarm. Signed-off-by: Andrew Featherstone <andrew.featherstone@gmail.com>
480 lines
10 KiB
C
480 lines
10 KiB
C
/*
|
|
* Copyright (c) 2022 Bjarki Arge Andreasen
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#define DT_DRV_COMPAT zephyr_rtc_emul
|
|
|
|
#include <zephyr/kernel.h>
|
|
#include <zephyr/device.h>
|
|
#include <zephyr/drivers/rtc.h>
|
|
|
|
#include "rtc_utils.h"
|
|
|
|
struct rtc_emul_data;
|
|
|
|
struct rtc_emul_work_delayable {
|
|
struct k_work_delayable dwork;
|
|
const struct device *dev;
|
|
};
|
|
|
|
struct rtc_emul_alarm {
|
|
struct rtc_time datetime;
|
|
rtc_alarm_callback callback;
|
|
void *user_data;
|
|
uint16_t mask;
|
|
bool pending;
|
|
};
|
|
|
|
struct rtc_emul_data {
|
|
bool datetime_set;
|
|
|
|
struct rtc_time datetime;
|
|
|
|
struct k_spinlock lock;
|
|
|
|
struct rtc_emul_work_delayable dwork;
|
|
|
|
#ifdef CONFIG_RTC_ALARM
|
|
struct rtc_emul_alarm *alarms;
|
|
uint16_t alarms_count;
|
|
#endif /* CONFIG_RTC_ALARM */
|
|
|
|
#ifdef CONFIG_RTC_UPDATE
|
|
rtc_update_callback update_callback;
|
|
void *update_callback_user_data;
|
|
#endif /* CONFIG_RTC_UPDATE */
|
|
|
|
#ifdef CONFIG_RTC_CALIBRATION
|
|
int32_t calibration;
|
|
#endif /* CONFIG_RTC_CALIBRATION */
|
|
};
|
|
|
|
static const uint8_t rtc_emul_days_in_month[12] = {
|
|
31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
|
|
};
|
|
|
|
static const uint8_t rtc_emul_days_in_month_with_leap[12] = {
|
|
31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
|
|
};
|
|
|
|
static bool rtc_emul_is_leap_year(struct rtc_time *datetime)
|
|
{
|
|
if ((datetime->tm_year % 400 == 0) ||
|
|
(((datetime->tm_year % 100) > 0) && ((datetime->tm_year % 4) == 0))) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static int rtc_emul_get_days_in_month(struct rtc_time *datetime)
|
|
{
|
|
const uint8_t *dim = (rtc_emul_is_leap_year(datetime) == true) ?
|
|
(rtc_emul_days_in_month_with_leap) :
|
|
(rtc_emul_days_in_month);
|
|
|
|
return dim[datetime->tm_mon];
|
|
}
|
|
|
|
static void rtc_emul_increment_tm(struct rtc_time *datetime)
|
|
{
|
|
/* Increment second */
|
|
datetime->tm_sec++;
|
|
|
|
/* Validate second limit */
|
|
if (datetime->tm_sec < 60) {
|
|
return;
|
|
}
|
|
|
|
datetime->tm_sec = 0;
|
|
|
|
/* Increment minute */
|
|
datetime->tm_min++;
|
|
|
|
/* Validate minute limit */
|
|
if (datetime->tm_min < 60) {
|
|
return;
|
|
}
|
|
|
|
datetime->tm_min = 0;
|
|
|
|
/* Increment hour */
|
|
datetime->tm_hour++;
|
|
|
|
/* Validate hour limit */
|
|
if (datetime->tm_hour < 24) {
|
|
return;
|
|
}
|
|
|
|
datetime->tm_hour = 0;
|
|
|
|
/* Increment day */
|
|
datetime->tm_wday++;
|
|
datetime->tm_mday++;
|
|
datetime->tm_yday++;
|
|
|
|
/* Limit week day */
|
|
if (datetime->tm_wday > 6) {
|
|
datetime->tm_wday = 0;
|
|
}
|
|
|
|
/* Validate month limit */
|
|
if (datetime->tm_mday <= rtc_emul_get_days_in_month(datetime)) {
|
|
return;
|
|
}
|
|
|
|
datetime->tm_mday = 1;
|
|
|
|
/* Increment month */
|
|
datetime->tm_mon++;
|
|
|
|
/* Validate month limit */
|
|
if (datetime->tm_mon < 12) {
|
|
return;
|
|
}
|
|
|
|
/* Increment year */
|
|
datetime->tm_mon = 0;
|
|
datetime->tm_yday = 0;
|
|
datetime->tm_year++;
|
|
}
|
|
|
|
#ifdef CONFIG_RTC_ALARM
|
|
static void rtc_emul_test_alarms(const struct device *dev)
|
|
{
|
|
struct rtc_emul_data *data = (struct rtc_emul_data *)dev->data;
|
|
struct rtc_emul_alarm *alarm;
|
|
|
|
for (uint16_t i = 0; i < data->alarms_count; i++) {
|
|
alarm = &data->alarms[i];
|
|
|
|
if (alarm->mask == 0) {
|
|
continue;
|
|
}
|
|
|
|
if ((alarm->mask & RTC_ALARM_TIME_MASK_SECOND) &&
|
|
(alarm->datetime.tm_sec != data->datetime.tm_sec)) {
|
|
continue;
|
|
}
|
|
|
|
if ((alarm->mask & RTC_ALARM_TIME_MASK_MINUTE) &&
|
|
(alarm->datetime.tm_min != data->datetime.tm_min)) {
|
|
continue;
|
|
}
|
|
|
|
if ((alarm->mask & RTC_ALARM_TIME_MASK_HOUR) &&
|
|
(alarm->datetime.tm_hour != data->datetime.tm_hour)) {
|
|
continue;
|
|
}
|
|
|
|
if ((alarm->mask & RTC_ALARM_TIME_MASK_MONTHDAY) &&
|
|
(alarm->datetime.tm_mday != data->datetime.tm_mday)) {
|
|
continue;
|
|
}
|
|
|
|
if ((alarm->mask & RTC_ALARM_TIME_MASK_MONTH) &&
|
|
(alarm->datetime.tm_mon != data->datetime.tm_mon)) {
|
|
continue;
|
|
}
|
|
|
|
if ((alarm->mask & RTC_ALARM_TIME_MASK_WEEKDAY) &&
|
|
(alarm->datetime.tm_wday != data->datetime.tm_wday)) {
|
|
continue;
|
|
}
|
|
|
|
if (alarm->callback == NULL) {
|
|
alarm->pending = true;
|
|
|
|
continue;
|
|
}
|
|
|
|
alarm->callback(dev, i, alarm->user_data);
|
|
|
|
alarm->pending = false;
|
|
}
|
|
}
|
|
#endif /* CONFIG_RTC_ALARM */
|
|
|
|
#ifdef CONFIG_RTC_UPDATE
|
|
static void rtc_emul_invoke_update_callback(const struct device *dev)
|
|
{
|
|
struct rtc_emul_data *data = (struct rtc_emul_data *)dev->data;
|
|
|
|
if (data->update_callback == NULL) {
|
|
return;
|
|
}
|
|
|
|
data->update_callback(dev, data->update_callback_user_data);
|
|
}
|
|
#endif /* CONFIG_RTC_UPDATE */
|
|
|
|
static void rtc_emul_update(struct k_work *work)
|
|
{
|
|
struct rtc_emul_work_delayable *work_delayable = (struct rtc_emul_work_delayable *)work;
|
|
const struct device *dev = work_delayable->dev;
|
|
struct rtc_emul_data *data = (struct rtc_emul_data *)dev->data;
|
|
|
|
k_work_schedule(&work_delayable->dwork, K_MSEC(1000));
|
|
|
|
K_SPINLOCK(&data->lock) {
|
|
rtc_emul_increment_tm(&data->datetime);
|
|
|
|
#ifdef CONFIG_RTC_ALARM
|
|
rtc_emul_test_alarms(dev);
|
|
#endif /* CONFIG_RTC_ALARM */
|
|
|
|
#ifdef CONFIG_RTC_UPDATE
|
|
rtc_emul_invoke_update_callback(dev);
|
|
#endif /* CONFIG_RTC_UPDATE */
|
|
}
|
|
}
|
|
|
|
static int rtc_emul_set_time(const struct device *dev, const struct rtc_time *timeptr)
|
|
{
|
|
struct rtc_emul_data *data = (struct rtc_emul_data *)dev->data;
|
|
|
|
/* Validate arguments */
|
|
if (timeptr == NULL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
K_SPINLOCK(&data->lock)
|
|
{
|
|
data->datetime = *timeptr;
|
|
data->datetime.tm_isdst = -1;
|
|
data->datetime.tm_nsec = 0;
|
|
|
|
data->datetime_set = true;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int rtc_emul_get_time(const struct device *dev, struct rtc_time *timeptr)
|
|
{
|
|
struct rtc_emul_data *data = (struct rtc_emul_data *)dev->data;
|
|
int ret = 0;
|
|
|
|
/* Validate arguments */
|
|
if (timeptr == NULL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
K_SPINLOCK(&data->lock)
|
|
{
|
|
/* Validate RTC time is set */
|
|
if (data->datetime_set == false) {
|
|
ret = -ENODATA;
|
|
|
|
K_SPINLOCK_BREAK;
|
|
}
|
|
|
|
*timeptr = data->datetime;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
#ifdef CONFIG_RTC_ALARM
|
|
static int rtc_emul_alarm_get_supported_fields(const struct device *dev, uint16_t id,
|
|
uint16_t *mask)
|
|
{
|
|
struct rtc_emul_data *data = (struct rtc_emul_data *)dev->data;
|
|
|
|
if (data->alarms_count <= id) {
|
|
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_WEEKDAY);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int rtc_emul_alarm_set_time(const struct device *dev, uint16_t id, uint16_t mask,
|
|
const struct rtc_time *timeptr)
|
|
{
|
|
struct rtc_emul_data *data = (struct rtc_emul_data *)dev->data;
|
|
|
|
if (data->alarms_count <= id) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
if ((mask > 0) && (timeptr == NULL)) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (mask > 0) {
|
|
if (rtc_utils_validate_rtc_time(timeptr, mask) == false) {
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
K_SPINLOCK(&data->lock)
|
|
{
|
|
data->alarms[id].mask = mask;
|
|
|
|
if (timeptr != NULL) {
|
|
data->alarms[id].datetime = *timeptr;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int rtc_emul_alarm_get_time(const struct device *dev, uint16_t id, uint16_t *mask,
|
|
struct rtc_time *timeptr)
|
|
{
|
|
struct rtc_emul_data *data = (struct rtc_emul_data *)dev->data;
|
|
|
|
if (data->alarms_count <= id) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
K_SPINLOCK(&data->lock)
|
|
{
|
|
*timeptr = data->alarms[id].datetime;
|
|
*mask = data->alarms[id].mask;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int rtc_emul_alarm_is_pending(const struct device *dev, uint16_t id)
|
|
{
|
|
struct rtc_emul_data *data = (struct rtc_emul_data *)dev->data;
|
|
int ret = 0;
|
|
|
|
if (data->alarms_count <= id) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
K_SPINLOCK(&data->lock)
|
|
{
|
|
ret = (data->alarms[id].pending == true) ? 1 : 0;
|
|
|
|
data->alarms[id].pending = false;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int rtc_emul_alarm_set_callback(const struct device *dev, uint16_t id,
|
|
rtc_alarm_callback callback, void *user_data)
|
|
{
|
|
struct rtc_emul_data *data = (struct rtc_emul_data *)dev->data;
|
|
|
|
if (data->alarms_count <= id) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
K_SPINLOCK(&data->lock)
|
|
{
|
|
data->alarms[id].callback = callback;
|
|
data->alarms[id].user_data = user_data;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
#endif /* CONFIG_RTC_ALARM */
|
|
|
|
#ifdef CONFIG_RTC_UPDATE
|
|
static int rtc_emul_update_set_callback(const struct device *dev,
|
|
rtc_update_callback callback, void *user_data)
|
|
{
|
|
struct rtc_emul_data *data = (struct rtc_emul_data *)dev->data;
|
|
|
|
K_SPINLOCK(&data->lock)
|
|
{
|
|
data->update_callback = callback;
|
|
data->update_callback_user_data = user_data;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
#endif /* CONFIG_RTC_UPDATE */
|
|
|
|
#ifdef CONFIG_RTC_CALIBRATION
|
|
static int rtc_emul_set_calibration(const struct device *dev, int32_t calibration)
|
|
{
|
|
struct rtc_emul_data *data = (struct rtc_emul_data *)dev->data;
|
|
|
|
K_SPINLOCK(&data->lock)
|
|
{
|
|
data->calibration = calibration;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int rtc_emul_get_calibration(const struct device *dev, int32_t *calibration)
|
|
{
|
|
struct rtc_emul_data *data = (struct rtc_emul_data *)dev->data;
|
|
|
|
K_SPINLOCK(&data->lock)
|
|
{
|
|
*calibration = data->calibration;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
#endif /* CONFIG_RTC_CALIBRATION */
|
|
|
|
static const struct rtc_driver_api rtc_emul_driver_api = {
|
|
.set_time = rtc_emul_set_time,
|
|
.get_time = rtc_emul_get_time,
|
|
#ifdef CONFIG_RTC_ALARM
|
|
.alarm_get_supported_fields = rtc_emul_alarm_get_supported_fields,
|
|
.alarm_set_time = rtc_emul_alarm_set_time,
|
|
.alarm_get_time = rtc_emul_alarm_get_time,
|
|
.alarm_is_pending = rtc_emul_alarm_is_pending,
|
|
.alarm_set_callback = rtc_emul_alarm_set_callback,
|
|
#endif /* CONFIG_RTC_ALARM */
|
|
#ifdef CONFIG_RTC_UPDATE
|
|
.update_set_callback = rtc_emul_update_set_callback,
|
|
#endif /* CONFIG_RTC_UPDATE */
|
|
#ifdef CONFIG_RTC_CALIBRATION
|
|
.set_calibration = rtc_emul_set_calibration,
|
|
.get_calibration = rtc_emul_get_calibration,
|
|
#endif /* CONFIG_RTC_CALIBRATION */
|
|
};
|
|
|
|
int rtc_emul_init(const struct device *dev)
|
|
{
|
|
struct rtc_emul_data *data = (struct rtc_emul_data *)dev->data;
|
|
|
|
data->dwork.dev = dev;
|
|
k_work_init_delayable(&data->dwork.dwork, rtc_emul_update);
|
|
|
|
k_work_schedule(&data->dwork.dwork, K_MSEC(1000));
|
|
|
|
return 0;
|
|
}
|
|
|
|
#ifdef CONFIG_RTC_ALARM
|
|
#define RTC_EMUL_DEVICE_DATA(id) \
|
|
static struct rtc_emul_alarm rtc_emul_alarms_##id[DT_INST_PROP(id, alarms_count)]; \
|
|
\
|
|
struct rtc_emul_data rtc_emul_data_##id = { \
|
|
.alarms = rtc_emul_alarms_##id, \
|
|
.alarms_count = ARRAY_SIZE(rtc_emul_alarms_##id), \
|
|
};
|
|
#else
|
|
#define RTC_EMUL_DEVICE_DATA(id) \
|
|
struct rtc_emul_data rtc_emul_data_##id;
|
|
#endif /* CONFIG_RTC_ALARM */
|
|
|
|
#define RTC_EMUL_DEVICE(id) \
|
|
RTC_EMUL_DEVICE_DATA(id) \
|
|
\
|
|
DEVICE_DT_INST_DEFINE(id, rtc_emul_init, NULL, &rtc_emul_data_##id, NULL, POST_KERNEL, \
|
|
CONFIG_RTC_INIT_PRIORITY, &rtc_emul_driver_api);
|
|
|
|
DT_INST_FOREACH_STATUS_OKAY(RTC_EMUL_DEVICE);
|