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

共3条 1/1 1 跳转至

rtthread RTC框架分析

工程师
2025-01-15 23:44:20     打赏

       在手表类应用中,有一种需求是关机后再开机,手表显示的时间要与实际时间一致(至少误差不能大)。而实现这类功能的电路,叫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);

       虽说有这么个框架,实际上不少厂家实现时,会直接将此框架中对接驱动的部分挪至驱动层直接实现,原因暂不清楚。而虽然移到驱动层直接实现,但是对接上层的接口还是一样的没变,因此上层还是能通过那几个对接上层的接口实现具体功能。




关键词: rtthread     框架     RTC    

院士
2025-01-16 00:03:57     打赏
2楼

这小函数指针

调试起来够刺激的了啊


助工
2025-01-16 10:29:25     打赏
3楼

感谢分享


共3条 1/1 1 跳转至

回复

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