在手表类应用中,有一种需求是关机后再开机,手表显示的时间要与实际时间一致(至少误差不能大)。而实现这类功能的电路,叫RTC电路,其基本逻辑是芯片掉电后,有一个极低功耗的外围模块还在工作(用于计时),此时使用的一颗外接纽扣电池供电,用于维持RTC相关的外围模块的运行。因此,带RTC功能的机器,都能看到电路板的某个位置会挂一颗电池,此电池接到带RTC功能芯片的VBAT脚上。
源码分析
源码路径
components\drivers\rtc\rtc.c
对接驱动的入口
设备注册入口
#ifdef RT_USING_DEVICE_OPS const static struct rt_device_ops rtc_core_ops = { rt_rtc_init, rt_rtc_open, rt_rtc_close, RT_NULL, RT_NULL, rt_rtc_control, }; #endif /* RT_USING_DEVICE_OPS */ rt_err_t rt_hw_rtc_register(rt_rtc_dev_t *rtc, const char *name, rt_uint32_t flag, void *data) { struct rt_device *device; RT_ASSERT(rtc != RT_NULL); device = &(rtc->parent); device->type = RT_Device_Class_RTC; device->rx_indicate = RT_NULL; device->tx_complete = RT_NULL; #ifdef RT_USING_DEVICE_OPS device->ops = &rtc_core_ops; #else device->init = rt_rtc_init; device->open = rt_rtc_open; device->close = rt_rtc_close; device->read = RT_NULL; device->write = RT_NULL; device->control = rt_rtc_control; #endif /* RT_USING_DEVICE_OPS */ device->user_data = data; /* register a character device */ return rt_device_register(device, name, flag); }
从注册入口上看,RTC设备还是依照设备框架的思路实现的,但RTC没有实现read write功能,这有些不太懂,按照我的理解,应用至少需要干两件事,一件是设置RTC的基准时间,另一个是获取当前RTC计时,如果没有read write接口,那就只能通过control接口实现这种功能了。另外,open接口实际上是无功能的,因此rtc设备的打开就全部交由init接口实现了。
RTC初始化接口
static rt_err_t rt_rtc_init(struct rt_device *dev) { rt_rtc_dev_t *rtc_core; RT_ASSERT(dev != RT_NULL); rtc_core = (rt_rtc_dev_t *)dev; if (rtc_core->ops->init) { return (rtc_core->ops->init()); } return -RT_ENOSYS; }
RTC关闭接口
static rt_err_t rt_rtc_close(struct rt_device *dev) { /* Add close member function in rt_rtc_ops when need, * then call that function here. * */ return RT_EOK; }
可能开源版本的认为RTC不需要关闭吧,所以在代码上没有实现关闭入口,但留了一个注释,说若有需要,则自行实现关闭相关代码。
RTC控制接口
static rt_err_t rt_rtc_control(struct rt_device *dev, int cmd, void *args) { #define TRY_DO_RTC_FUNC(rt_rtc_dev, func_name, args) \ rt_rtc_dev->ops->func_name ? rt_rtc_dev->ops->func_name(args) : -RT_EINVAL; rt_rtc_dev_t *rtc_device; rt_err_t ret = -RT_EINVAL; RT_ASSERT(dev != RT_NULL); rtc_device = (rt_rtc_dev_t *)dev; switch (cmd) { case RT_DEVICE_CTRL_RTC_GET_TIME: // 获取秒级的时间戳 ret = TRY_DO_RTC_FUNC(rtc_device, get_secs, args); break; case RT_DEVICE_CTRL_RTC_SET_TIME: // 设置秒级的时间戳 ret = TRY_DO_RTC_FUNC(rtc_device, set_secs, args); break; case RT_DEVICE_CTRL_RTC_GET_TIMEVAL: //获取比秒单位更细级别的时间戳 ret = TRY_DO_RTC_FUNC(rtc_device, get_timeval, args); break; case RT_DEVICE_CTRL_RTC_SET_TIMEVAL: // 设置比秒单位更细级别的时间戳 ret = TRY_DO_RTC_FUNC(rtc_device, set_timeval, args); break; case RT_DEVICE_CTRL_RTC_GET_ALARM: // 获取闹钟时间 ret = TRY_DO_RTC_FUNC(rtc_device, get_alarm, args); break; case RT_DEVICE_CTRL_RTC_SET_ALARM: // 设置闹钟时间 ret = TRY_DO_RTC_FUNC(rtc_device, set_alarm, args); break; default: break; } return ret; #undef TRY_DO_RTC_FUNC }
对接上层接口
按日期设置日期
// 查表法的快速计算总天数 static const short __spm[13] = { 0, (31), (31 + 28), (31 + 28 + 31), (31 + 28 + 31 + 30), (31 + 28 + 31 + 30 + 31), (31 + 28 + 31 + 30 + 31 + 30), (31 + 28 + 31 + 30 + 31 + 30 + 31), (31 + 28 + 31 + 30 + 31 + 30 + 31 + 31), (31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30), (31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31), (31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30), (31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31), }; #ifndef __isleap static int __isleap(int year) { // 闰年的计算方法,年能被4整除但不能被100整除,或者年能被400整除 return (!(year % 4) && ((year % 100) || !(year % 400))); } #endif // 实际执行 struct tm 转 time_t的函数 time_t timegm(struct tm * const t) { time_t day; time_t i; time_t years; if(t == RT_NULL) { rt_set_errno(EFAULT); return (time_t)-1; } years = (time_t)t->tm_year - 70; if (t->tm_sec > 60) // 如果秒有超过60的计数,则标准化成60以内的时间,同步将分钟部分更新 { t->tm_min += t->tm_sec / 60; t->tm_sec %= 60; } if (t->tm_min >= 60) // 同样的,如果分钟有超过60的,则标准化成60分以内的时间,时同步更新 { t->tm_hour += t->tm_min / 60; t->tm_min %= 60; } if (t->tm_hour >= 24) // 同样的,如果存在超过24小时的计数,则标准化成24小时以内,天同步更新 { t->tm_mday += t->tm_hour / 24; t->tm_hour %= 24; } if (t->tm_mon >= 12) // 同样的,如果存在超过12个月的计数,则标准化成12个月以内的计数,年同步更新 { t->tm_year += t->tm_mon / 12; t->tm_mon %= 12; } while (t->tm_mday > __spm[1 + t->tm_mon]) // 如果记录的一年的所在的天数超过了记录的下个月的天数,则说明月份状态不对,需要更新月信息 { if (t->tm_mon == 1 && __isleap(t->tm_year + 1900)) { // 如果是闰年,则天数预先减一(其实可以反过来看,天数不会,月数的天数加1,但因为后续计算需要减掉,所以更快的方法就直接减天数了) --t->tm_mday; } t->tm_mday -= __spm[t->tm_mon]; // 剩余天数更新 ++t->tm_mon; // 同样的,月数同步加1 if (t->tm_mon > 11) // 如果月数超过了12,则月数标准化成12个月以内,同步年增加一年 { t->tm_mon = 0; ++t->tm_year; } } if (t->tm_year < 70) // 如果时间早于1970年,则认为传入时间错误 { rt_set_errno(EINVAL); return (time_t) -1; } // 获取从1970年到当前年的天数,但不知道为啥使用的years不是更新后的t->tm_year,反而还是更新前的years。 day = years * 365 + (years + 1) / 4; /* After 2100 we have to substract 3 leap years for every 400 years This is not intuitive. Most mktime implementations do not support dates after 2059, anyway, so we might leave this out for it's bloat. */ if (years >= 131) { years -= 131; years /= 100; day -= (years >> 2) * 3 + 1; if ((years &= 3) == 3) years--; day -= years; } // 将当前年剩余天数也计算上,也就是获得了1970年至今日所有的天数 // 同时更新 t->tm_yday标记,此标记代表当前年已经经过的天数 day += t->tm_yday = __spm[t->tm_mon] + t->tm_mday - 1 + (__isleap(t->tm_year + 1900) & (t->tm_mon > 1)); // 更新从1970至今所经过的月数标记 i = 7; t->tm_wday = (int)((day + 4) % i); /* Sunday=0, Monday=1, ..., Saturday=6 */ // 将天数转换成小时 i = 24; day *= i; // 小时转换成秒 i = 60; return ((day + t->tm_hour) * i + t->tm_min) * i + t->tm_sec; } time_t mktime(struct tm * const t) { time_t timestamp; timestamp = timegm(t); #if defined(RT_LIBC_USING_LIGHT_TZ_DST) timestamp = timestamp - rt_tz_get(); #else timestamp = timestamp - 0U; #endif /* RT_LIBC_USING_LIGHT_TZ_DST */ return timestamp; } // time_t转换成struct tm具体实现 struct tm *gmtime_r(const time_t *timep, struct tm *r) { int i; int work; // 输入参数检验,不符合标准不计算 if(timep == RT_NULL || r == RT_NULL) { rt_set_errno(EFAULT); return RT_NULL; } rt_memset(r, RT_NULL, sizeof(struct tm)); // 获取当天已经经过的时间 work = *timep % (24*60*60); // 获取秒级计数 r->tm_sec = work % 60; // 获取分级和小时级计数 work /= 60; r->tm_min = work % 60; r->tm_hour = work / 60; // 获取当前经过的天数 work = (int)(*timep / (24*60*60)); // 获取从1970至当前天所经过的周数 r->tm_wday = (4 + work) % 7; // 获取今年已走完的天数 for (i = 1970;; ++i) { int k = __isleap(i) ? 366 : 365; if (work >= k) work -= k; else break; } r->tm_year = i - 1900; // 将年转换成1900开始的时间,struct tm标准的年是1900年开始计算的,即0代表1900年 r->tm_yday = work; // 更新今年已走完的天数 r->tm_mday = 1; if (__isleap(i) && (work > 58)) { // 如果是闰年,且超过了2月,则总天数减1,理解通timegm中的闰年处理 if (work == 59) r->tm_mday = 2; /* 29.2. */ work -= 1; } // 获取当前所在月i for (i = 11; i && (__spm[i] > work); --i); r->tm_mon = i; // 更新月计数 r->tm_mday += work - __spm[i]; // 更新当月所处的天 #if defined(RT_LIBC_USING_LIGHT_TZ_DST) r->tm_isdst = rt_tz_is_dst(); #else r->tm_isdst = 0U; #endif /* RT_LIBC_USING_LIGHT_TZ_DST */ return r; } struct tm* localtime_r(const time_t* t, struct tm* r) { time_t local_tz; #if defined(RT_LIBC_USING_LIGHT_TZ_DST) local_tz = *t + rt_tz_get(); #else local_tz = *t + 0U; #endif /* RT_LIBC_USING_LIGHT_TZ_DST */ return gmtime_r(&local_tz, r); } rt_err_t set_date(rt_uint32_t year, rt_uint32_t month, rt_uint32_t day) { time_t now, old_timestamp = 0; struct tm tm_new = {0}; rt_err_t ret = -RT_ERROR; if (_rtc_device == RT_NULL) { _rtc_device = rt_device_find("rtc"); if (_rtc_device == RT_NULL) { return -RT_ERROR; } } // 从驱动中读出当前时间 ret = rt_device_control(_rtc_device, RT_DEVICE_CTRL_RTC_GET_TIME, &old_timestamp); if (ret != RT_EOK) { return ret; } // 将当前时间转换成 struct tm 格式的时间 localtime_r(&old_timestamp, &tm_new); // 更新成新的时间 tm_new.tm_year = year - 1900; tm_new.tm_mon = month - 1; /* tm_mon: 0~11 */ tm_new.tm_mday = day; // 将新的时间转换成驱动能识别的时间 now = mktime(&tm_new); //更新驱动中的时间基准 ret = rt_device_control(_rtc_device, RT_DEVICE_CTRL_RTC_SET_TIME, &now); return ret; }
从代码实现上,我们会发现,整个设置日期的入口,存在一个格式转换的情况(time_t 转换成 struct tm,之后再转换回 time_t),而进一步跟代码,会发现time_ttime_t其实只是一个计数值,而体现最关键功能的点还是struct tm。另外,从时间格式转换上看,个人感觉timegm函数的实现可能存在bug,如果刚好满足可以再次执行while循环,那天和月之间的关系就会出现明显偏差。
按秒设置时间
rt_err_t set_time(rt_uint32_t hour, rt_uint32_t minute, rt_uint32_t second) { time_t now, old_timestamp = 0; struct tm tm_new = {0}; rt_err_t ret = -RT_ERROR; if (_rtc_device == RT_NULL) { _rtc_device = rt_device_find("rtc"); if (_rtc_device == RT_NULL) { return -RT_ERROR; } } /* get current time */ ret = rt_device_control(_rtc_device, RT_DEVICE_CTRL_RTC_GET_TIME, &old_timestamp); if (ret != RT_EOK) { return ret; } /* converts calendar time into local time. */ localtime_r(&old_timestamp, &tm_new); /* update time. */ tm_new.tm_hour = hour; tm_new.tm_min = minute; tm_new.tm_sec = second; /* converts the local time into the calendar time. */ now = mktime(&tm_new); /* update to RTC device. */ ret = rt_device_control(_rtc_device, RT_DEVICE_CTRL_RTC_SET_TIME, &now); return ret; }
分析了set_date函数,这个函数的逻辑就很清晰了,仅仅是换一种方式设置(小时,分,秒)的设置时间。
直接设置时间
同上,只是设置更加直接了,直接按time_t的方式设置时间,也就是1970到现在所经过的秒数
rt_err_t set_timestamp(time_t timestamp) { if (_rtc_device == RT_NULL) { _rtc_device = rt_device_find("rtc"); if (_rtc_device == RT_NULL) { return -RT_ERROR; } } /* update to RTC device. */ return rt_device_control(_rtc_device, RT_DEVICE_CTRL_RTC_SET_TIME, ×tamp); }
获取时间
rt_err_t get_timestamp(time_t *timestamp) { if (_rtc_device == RT_NULL) { _rtc_device = rt_device_find("rtc"); if (_rtc_device == RT_NULL) { return -RT_ERROR; } } /* Get timestamp from RTC device. */ return rt_device_control(_rtc_device, RT_DEVICE_CTRL_RTC_GET_TIME, timestamp); }
由于整个上层的时间判断都是按照time_t算的,因此直接读了返回即可。
总结
至此,RTC框架已经分析完毕,经过此部分的分析,我们会发现RTC框架对驱动的要求其实挺简单的,就那么几个接口,初始化,设置获取秒级时间戳,外加一个设置和获取闹钟参数的接口。因此驱动层的适配接口就变得清晰多了。驱动层的适配框架如下:
#include <sys/time.h> struct rtc_device_object { rt_rtc_dev_t rtc_dev; // other param }; static rt_err_t rtc_init(void) { // TODO: Add rtc init&open function } static rt_err_t rtc_get_secs(time_t *sec) { // TODO: Add get rtc count function } static rt_err_t rtc_set_secs(time_t *sec) { // TODO: Add set rtc count function } static rt_err_t rtc_get_alarm(struct rt_rtc_wkalarm *alarm) { // TODO: Add get alarm function } static rt_err_t rtc_set_alarm(struct rt_rtc_wkalarm *alarm) { // TODO: Add set alarm function } static rt_err_t rtc_get_timeval(struct timeval *tv) { // TODO: Add get time val } static rt_err_t rtc_set_timeval(struct timeval *tv) { // TODO: Add set time val } static const struct rtrtc_ops rtc_ops = { rtc_init, rtc_get_secs, rtc_set_secs, rtc_get_alarm, rtc_set_alarm, rtc_get_timeval, rtc_set_timeval, }; static int rt_hwrtc_init(void) { rt_err_t result; rtc_device.rtc_dev.ops = &rtc_ops; result = rt_hw_rtc_register(&rtc_device.rtc_dev, "rtc", RT_DEVICE_FLAG_RDWR, RT_NULL); if (result != RT_EOK) { LOG_E("rtc register err code: %d", result); return result; } LOG_D("rtc init success"); return RT_EOK; } INIT_BOARD_EXPORT(rt_hwrtc_init);
虽说有这么个框架,实际上不少厂家实现时,会直接将此框架中对接驱动的部分挪至驱动层直接实现,原因暂不清楚。而虽然移到驱动层直接实现,但是对接上层的接口还是一样的没变,因此上层还是能通过那几个对接上层的接口实现具体功能。