在手表类应用中,有一种需求是关机后再开机,手表显示的时间要与实际时间一致(至少误差不能大)。而实现这类功能的电路,叫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);虽说有这么个框架,实际上不少厂家实现时,会直接将此框架中对接驱动的部分挪至驱动层直接实现,原因暂不清楚。而虽然移到驱动层直接实现,但是对接上层的接口还是一样的没变,因此上层还是能通过那几个对接上层的接口实现具体功能。
我要赚赏金
