这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 嵌入式开发 » 软件与操作系统 » rtthread软件RTC代码分析

共2条 1/1 1 跳转至

rtthread软件RTC代码分析

工程师
2025-01-19 13:51:13     打赏

      软件RTC,总感觉有些奇怪,RTC给我的印象是为了保证断电后还能计时,系统起来后,其实使用任何时钟信号都可以更新时间。那缺少了硬件RTC模块,那RTC的功能也就仅剩供电状态下的计时功能以及基于此功能附加的应用功能了。

源码解读

源码路径

components\drivers\rtc\soft_rtc.c

软件RTC注册

/* 2018-01-30 14:44:50 = RTC_TIME_INIT(2018, 1, 30, 14, 44, 50)  */
#define RTC_TIME_INIT(year, month, day, hour, minute, second)        \
    {.tm_year = year - 1900, .tm_mon = month - 1, .tm_mday = day, .tm_hour = hour, .tm_min = minute, .tm_sec = second}

#ifndef SOFT_RTC_TIME_DEFAULT
#define SOFT_RTC_TIME_DEFAULT                    RTC_TIME_INIT(2018, 1, 1, 0, 0 ,0)
#endif

#ifdef RT_USING_ALARM
static void alarm_timeout(void *param)
{
    // rt_alarm_update在alarm模块中被注释成未使用,实际上软件rtc是通过这个接口与alarm模块交互的
    rt_alarm_update(param, 1);
}
#endif

static int rt_soft_rtc_init(void)
{
    static rt_bool_t init_ok = RT_FALSE;
    struct tm time_new = SOFT_RTC_TIME_DEFAULT;

    if (init_ok)
    {
        return 0;
    }
    // 系统只需要一个rtc设备,因此启用软件RTC时,需要检查是否有已有的RTC设备
    RT_ASSERT(!rt_device_find("rtc"));

    // 如果启用了闹钟功能,则使用软件定时器的方式通知闹钟模块时间到
#ifdef RT_USING_ALARM
    rt_timer_init(&alarm_time,
                  "alarm",
                  alarm_timeout,
                  &soft_rtc_dev,
                  0,
                  RT_TIMER_FLAG_SOFT_TIMER|RT_TIMER_FLAG_ONE_SHOT);
#endif

    init_tick = rt_tick_get();     // 获取初始计时
    init_time = timegm(&time_new); // 设置初始计时

    soft_rtc_dev.type    = RT_Device_Class_RTC;

    // 注册设备
#ifdef RT_USING_DEVICE_OPS
    soft_rtc_dev.ops     = &soft_rtc_ops;
#else
    soft_rtc_dev.init    = RT_NULL;
    soft_rtc_dev.open    = RT_NULL;
    soft_rtc_dev.close   = RT_NULL;
    soft_rtc_dev.read    = RT_NULL;
    soft_rtc_dev.write   = RT_NULL;
    soft_rtc_dev.control = soft_rtc_control;
#endif

    // 声明没有内部维护的变量
    // 但个人理解,其实init_tick,init_timeinit_time之类的参数都可以放在里面维护
    soft_rtc_dev.user_data = RT_NULL;

    rt_device_register(&soft_rtc_dev, "rtc", RT_DEVICE_FLAG_RDWR);

    init_ok = RT_TRUE; // 标记为初始化完成

    return 0;
}
INIT_DEVICE_EXPORT(rt_soft_rtc_init);

       与硬件RTC模块有区别的是,软件RTC只有control入口,另外,为了实现闹钟功能,软件RTC使用软件定时器的方式向闹钟模块发送闹钟状态更新消息。

软件RTC控制入口

#ifdef RT_USING_ALARM
// 更新alarm
static void soft_rtc_alarm_update(struct rt_rtc_wkalarm *palarm)
{
    rt_tick_t next_tick;

    // 如果闹钟启动的,则设置闹钟1s后启动,若是关闭的,则再一次关闭闹钟所使用的定时器
    if (palarm->enable)
    {
        next_tick = RT_TICK_PER_SECOND;
        rt_timer_control(&alarm_time, RT_TIMER_CTRL_SET_TIME, &next_tick);
        rt_timer_start(&alarm_time);
    }
    else
    {
        rt_timer_stop(&alarm_time);
    }
}
#endif

static void set_rtc_time(time_t t)
{ // 更新软件RTC时钟,更新的同时,alarm也需要相应的更新
    init_time = t - (rt_tick_get() - init_tick) / RT_TICK_PER_SECOND;
#ifdef RT_USING_ALARM
    soft_rtc_alarm_update(&wkalarm);
#endif
}

// 向实际的软件RTC模块设置参数的执行入口
static void _source_device_control(int cmd, void *args)
{
    if (source_device == RT_NULL)
        return;

    if (rt_device_open(source_device, 0) == RT_EOK)
    {
        rt_device_control(source_device, cmd, args);
        rt_device_close(source_device);
    }
}


static rt_err_t soft_rtc_control(rt_device_t dev, int cmd, void *args)
{
    time_t *t;
    struct tm time_temp;

    RT_ASSERT(dev != RT_NULL);
    rt_memset(&time_temp, 0, sizeof(struct tm));

    switch (cmd)
    {
    case RT_DEVICE_CTRL_RTC_GET_TIME:
    { // 获取当前计时,逻辑很简单,init_time是预置的时间基准,后面所加的时间可以看作软件RTC启动到当前所消耗的秒数
        t = (time_t *) args;
        *t = init_time + (rt_tick_get() - init_tick) / RT_TICK_PER_SECOND;
        break;
    }
    case RT_DEVICE_CTRL_RTC_SET_TIME:
    {  // 将time_t格式的时间同步至软件RTC并向实际的软件RTC模块设置
        t = (time_t *) args;
        set_rtc_time(*t);
        _source_device_control(RT_DEVICE_CTRL_RTC_SET_TIME, t);
        break;
    }
#ifdef RT_USING_ALARM
    case RT_DEVICE_CTRL_RTC_GET_ALARM:
        //如果有闹钟,则返回当前闹钟指针
        *((struct rt_rtc_wkalarm *)args) = wkalarm;
        break;
    case RT_DEVICE_CTRL_RTC_SET_ALARM:
        // 如果启用闹钟,则保存闹钟并启用闹钟
        wkalarm = *((struct rt_rtc_wkalarm *)args);
        soft_rtc_alarm_update(&wkalarm);
        break;
#endif
#ifdef RT_USING_KTIME // 如果启用了内核时钟,则运行该部分
    case RT_DEVICE_CTRL_RTC_GET_TIMEVAL:
    {//获取天级的时间戳
        struct timeval _tv;
        struct timeval *tv = (struct timeval *)args;
        rt_ktime_boottime_get_us(&_tv);
        tv->tv_sec  = init_time + _tv.tv_sec;
        tv->tv_usec = init_tv.tv_usec + _tv.tv_usec;
        break;
    }
    case RT_DEVICE_CTRL_RTC_SET_TIMEVAL:
    {//设置天级的时间戳
        struct timeval _tv;
        struct timeval *tv = (struct timeval *)args;
        rt_ktime_boottime_get_us(&_tv);
        set_rtc_time(tv->tv_sec);
        init_tv.tv_usec = tv->tv_usec - _tv.tv_usec;
        _source_device_control(RT_DEVICE_CTRL_RTC_SET_TIME, &(tv->tv_sec));
        break;
    }
    case RT_DEVICE_CTRL_RTC_GET_TIMESPEC:
    {    // 获取精度小于1s的时间戳
        struct timespec _ts;
        struct timespec *ts = (struct timespec *)args;
        rt_ktime_boottime_get_ns(&_ts);
        ts->tv_sec  = init_time + _ts.tv_sec;
        ts->tv_nsec = init_ts.tv_nsec + _ts.tv_nsec;
        break;
    }
    case RT_DEVICE_CTRL_RTC_SET_TIMESPEC:
    {    // 设置精度小于1s的时间戳
        struct timespec _ts;
        struct timespec *ts = (struct timespec *)args;
        rt_ktime_boottime_get_ns(&_ts);
        set_rtc_time(ts->tv_sec);
        init_ts.tv_nsec = ts->tv_nsec - _ts.tv_nsec;
        _source_device_control(RT_DEVICE_CTRL_RTC_SET_TIME, &(ts->tv_sec));
        break;
    }
    case RT_DEVICE_CTRL_RTC_GET_TIMERES:
    {    // 返回时钟分辨率
        struct timespec *ts = (struct timespec *)args;
        ts->tv_sec  = 0;
        ts->tv_nsec = (rt_ktime_cputimer_getres() / RT_KTIME_RESMUL);
        break;
    }
#else
    case RT_DEVICE_CTRL_RTC_GET_TIMEVAL:
    {  //获取天级的时间戳
        struct timeval *tv = (struct timeval *)args;
        rt_tick_t tick = rt_tick_get() - init_tick;
        tv->tv_sec = init_time + tick / RT_TICK_PER_SECOND;
        tv->tv_usec = init_tv.tv_usec + ((tick % RT_TICK_PER_SECOND) * (1000000 / RT_TICK_PER_SECOND));
        break;
    }
    case RT_DEVICE_CTRL_RTC_SET_TIMEVAL: 
    {  // 设置天级的时间戳
        struct timeval *tv = (struct timeval *)args;
        rt_tick_t tick = rt_tick_get() - init_tick;
        set_rtc_time(tv->tv_sec);
        init_tv.tv_usec = tv->tv_usec - ((tick % RT_TICK_PER_SECOND) * (1000000 / RT_TICK_PER_SECOND));
        _source_device_control(RT_DEVICE_CTRL_RTC_SET_TIME, &(tv->tv_sec));
        break;
    }
    case RT_DEVICE_CTRL_RTC_GET_TIMERES:
    {    // 返回时钟分辨率
        struct timespec *ts = (struct timespec *)args;
        ts->tv_sec  = 0;
        ts->tv_nsec = (1000UL * 1000 * 1000) / RT_TICK_PER_SECOND;
        break;
    }
#endif /* RT_USING_KTIME */
    default:
        return -RT_EINVAL;
    }

    return RT_EOK;
}

     这里面闹钟引入了内核时钟的模块,这个模块是23年才被加入rtt源码中的,具体功能,就是通过内核中的计数模块获取计数值,并换算成不同单位的时钟值。

注册软件RTC所依赖的时钟源入口

// 可以看到,默认的软件RTC同步时间明显偏大,实际使用时需根据需要自行定义时间
#ifndef RTC_AUTO_SYNC_FIRST_DELAY
#define RTC_AUTO_SYNC_FIRST_DELAY 25
#endif
#ifndef RTC_AUTO_SYNC_PERIOD
#define RTC_AUTO_SYNC_PERIOD 3600
#endif

rt_err_t rt_soft_rtc_sync(void)
{
    time_t time = 0;

    if (source_device == RT_NULL)
    {
        rt_kprintf("error: rtc source not found, please set it!!!\n");
        return RT_ENOSYS;
    }

    // 从时钟源中获取当前时间
    _source_device_control(RT_DEVICE_CTRL_RTC_GET_TIME, &time);
    // 将当前时间同步至软件RTC
    set_rtc_time(time);
    return RT_EOK;
}

static void rtc_sync_work_func(struct rt_work *work, void *work_data)
{
    // 同步rtc时间
    rt_soft_rtc_sync();
    
    // 启用下一次的工作队列
    rt_work_submit(work, rt_tick_from_millisecond(RTC_AUTO_SYNC_PERIOD * 1000));
}

rt_err_t rt_soft_rtc_set_source(const char *name)
{
    RT_ASSERT(name != RT_NULL);
    RT_ASSERT(rt_device_find(name));

    // 指定时钟源
    source_device = rt_device_find(name);

    //创建工作队列并启用第一次tick
    rt_work_init(&rtc_sync_work, rtc_sync_work_func, RT_NULL);
    rt_work_submit(&rtc_sync_work, rt_tick_from_millisecond(RTC_AUTO_SYNC_FIRST_DELAY * 1000));

    return RT_EOK;
}

总结

    从源码上看,软件RTC需要有一个模块模拟RTC模块的计数功能,且此此模块注册时指定的control函数需要实现以下入口:

RT_DEVICE_CTRL_RTC_GET_TIME // 读取秒级时间戳
RT_DEVICE_CTRL_RTC_SET_TIME // 设置秒级时间戳

      但很遗憾的是,通过搜索源码,我并未发现除了硬件RTC框架,还有哪个框架实现了此接口,也就是说,如果我们需要使用软件RTC,那就需要改造现有的能计时设备的设备框架代码,在他的control函数中实现设置和读取秒级时间戳的功能。






关键词: rtthread     框架     软件RTC    

专家
2025-01-19 19:48:18     打赏
2楼

感谢分享


共2条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]