/* * Copyright (c) 2022 Bjarki Arge Andreasen * * SPDX-License-Identifier: Apache-2.0 */ #define DT_DRV_COMPAT zephyr_rtc_emul #include #include #include 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; } #ifdef CONFIG_RTC_ALARM static bool rtc_emul_validate_alarm_time(const struct rtc_time *timeptr, uint32_t mask) { if ((mask & RTC_ALARM_TIME_MASK_SECOND) && (timeptr->tm_sec < 0 || timeptr->tm_sec > 59)) { return false; } if ((mask & RTC_ALARM_TIME_MASK_MINUTE) && (timeptr->tm_min < 0 || timeptr->tm_min > 59)) { return false; } if ((mask & RTC_ALARM_TIME_MASK_HOUR) && (timeptr->tm_hour < 0 || timeptr->tm_hour > 23)) { return false; } if ((mask & RTC_ALARM_TIME_MASK_MONTH) && (timeptr->tm_mon < 0 || timeptr->tm_mon > 11)) { return false; } if ((mask & RTC_ALARM_TIME_MASK_MONTHDAY) && (timeptr->tm_mday < 1 || timeptr->tm_mday > 31)) { return false; } if ((mask & RTC_ALARM_TIME_MASK_YEAR) && (timeptr->tm_year < 0 || timeptr->tm_year > 199)) { return false; } return true; } #endif /* CONFIG_RTC_ALARM */ 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_emul_validate_alarm_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 */ 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);