如何在MCU上通过ToD+PPS 获取同步时间(二)

欢迎淘宝搜索飞灵科技,我司相关新产品陆续上线

实现过程

根据上面介绍,我们需要这样一个时钟来记录系统时间,即记录至1970-01-01以来的ns 数:

  1. 精度为ns级别。
  2. 位数足够长,不会发生回绕, 通常为64bits。
  3. 支持计数频率调整。

在硬件上,很难找到一个这样的Timer。 所以首先我们需要用软件虚拟一个Timer。然后周期性的校准这个虚拟Timer。

我们需要分成两个部分来实现这个目标:

  • 构造一个64bits的虚拟时钟。
  • 编写一个周期性的校准程序。

构造一个64bits的虚拟时钟。

为了构造一个64bits的虚拟时钟,我们需要一个处在循环计数模式(准确的讲,应该处在输入捕获模式,因为后面介绍的校准机制需要捕获功能)的硬件时钟。

我们构造以下结构来抽象一个硬件Timers。

struct cyclecounter {
    u64 (*read)(const struct cyclecounter *cc);
    u64 mask;
    u32 mult;
    u32 shift;
};

@read: 读取硬件Timer 的cycle 值的回调函数。
@mask: 硬件Timer的位数。
@mult: 硬件Timer 的cycle 值转化为ns的multiplier。
@shift: 硬件Timer 的cycle 值转化为ns的divisor 。

使用上面的硬件Timer,我们来构造一个虚拟的64Bits时钟。

struct timecounter {
    const struct cyclecounter *cc;
    u64 cycle_last;
    u64 nsec;
    u64 mask;
    u64 frac;
};

@cc: 硬件Timer的抽象实例
@cycle_last: 上一次读取的硬件Timer 的cycle值
@nsec: 至1970-01-01以来的ns 数
@mask: 不满1ns的小数部分的累加值的bit位掩码。
@frac: 不满1ns的小数部分的累加值。

除了上面的数据结构体之外,我们还需要一些辅助的API来运行这个Timer。

void timecounter_init(struct timecounter *tc, const struct cyclecounter *cc, u64 start_tstamp)
{
    tc->cc = cc;
    tc->cycle_last = cc->read(cc);
    tc->nsec = start_tstamp;
    tc->mask = (1ULL << cc->shift) - 1;
    tc->frac = 0;
}

u64 timecounter_read(struct timecounter *tc)
{
    u64 nsec;
    u64 cycle_now, cycle_delta;
    /* read cycle counter: */
    cycle_now = tc->cc->read(tc->cc);
    cycle_delta = (cycle_now - tc->cycle_last) & tc->cc->mask;
    nsec= (cycle_delta * tc->cc->mult) + tc->frac;
    tc->frac = nsec & tc->mask;
    nsec = nsec >> tc->cc->shift;
    tc->cycle_last = cycle_now;
    nsec += tc->nsec;
    tc->nsec = nsec;
    return nsec;
}

u64 timecounter_cyc2time(struct timecounter *tc, u64 cycle_tstamp)
{
    u64 delta = (cycle_tstamp - tc->cycle_last) & tc->cc->mask;
    u64 nsec = tc->nsec, frac = tc->frac;
    u64 ns;
    if (delta > tc->cc->mask / 2) {
        delta = (tc->cycle_last - cycle_tstamp) & tc->cc->mask;
        nsec -= ((delta * tc->cc->mult) - frac) >> tc->cc->shift;
    } else {
        nsec += ((delta * tc->cc->mult) + frac) >> tc->cc->shift;
    }
    return nsec;
}

void timecounter_adjtime(struct timecounter *tc, s64 delta)
{
    tc->nsec += delta;
}

@timecounter_init: 初始化或重置虚拟Timer。
@timecounter_read:读取虚拟Timer的ns值。并且更新虚拟Timer。
@timecounter_cyc2time: 将读到的一个硬件Timer的cycle 值转换为Timer的时间。在校准程序里需要此函数计算ppm。
@timecounter_adjtime: 调整虚拟Timer的ns数。

从timecounter_read 函数可以看出,我们在硬件Timer计数的一个周期内,必须调用一次这个函数,否则造成计数错误。

假如我们硬件Timer 时钟是100MHz的32bits时钟。则一个计数周期为 10ns * pow(2,32) / pow(10,9) = 42.94967296秒, 即在42.9秒内,应用软件必须调用timecounter_read 读取一次时间值。

在STM32上,仅仅有两个32Bits的timer, 在其他MCU上,大多是16bits的Timer。对于16bits的Timer,一个计数周期为 10ns * pow(2,16) / pow(10,9) = 0.00065536秒, 即0.65ms。 这个时间对CPU来说是太频繁了,并且容易丢失周期。

有两种方法解决这个问题,一种是减低Timer的时钟,例如使用1MHz,则计数周期变为65ms。但精度随之变低。

另一种是使用硬件Timer的overflow 中断。计数值回绕时产生中断。在中断函数里,我们可以调用timecounter_read函数来更新Timer。但在主程序在调用此函数中时,如果被overflow 中断,可能会造成更新错误。同理,timecounter_read 函数是非线程安全的。即在多任务环境中,需要加锁保护。 另一种方法是通过在中断函数里递增周期数变量来扩展一个硬件Timer的位数,代码如下:

u32 timer_overflow_num = 0;
void Timer_overflow_irq()
{
    timer_overflow_num++;
}

struct cyclecounter {
    u64 (*read)(const struct cyclecounter *cc);
    u64 (*read_from_timer)(const struct cyclecounter *cc);
    u64 mask;
    u32 last_ overflow_num;
    u32 mult;
    u32 shift;
};

@last_ overflow_num: 记录上一次读时timer_overflow_num 的值。
@read_from_timer: 从硬件Timer读取cycle 的回调函数。

现在我们拦截struct timecounter 从struct cyclecounter中的读取硬件Timer cycle的 read函数,并重实现如下:

u64 timer_read_with_overflow(const struct cyclecounter *cc)
{
    u32 overflow_num = timer_overflow_num;
    u64 cycle = cc->read_from_timer(cc);
    u32 overflow_num_again = timer_overflow_num;

    if (overflow_num != overflow_num_again) {
        cycle = cc->read_from_timer(cc);
    }

    cycle += ((overflow_num_again - cc->last_ overflow_num) & 0xFFFFFFFF - 1) * (cc->mask + 1);
    cc->last_ overflow_num = overflow_num_again;
    return cycle;
}

通过上面扩展,我们的硬件timer位数增加了32bits。

虽然我们扩展了硬件Timer的位数,但并不是说我的一个计数周期变长了,就可以长时间的不去调用timecounter_read 函数了。两个原因:

  • 如果两次读取的间隔很长,间隔值会在浮点转定点过程中,造成溢出从而产生错误。
  • 晶振在随着时间,温度等环境在缓慢变化,长时间不更新的话,造成误差累积。

我们根据具体使用case,实现了虚拟Timer后,再来构造一个统一的API接口给用户程序使用:

int time_adjfine(struct timecounter *p, long scaled_ppm);
int time_adjtime(struct timecounter *p, s64 delta);
int time_gettime(struct timecounter *p, struct timeval*ts);
int time_settime(struct timecounter *p, const struct timeval*ts);

@time_adjfine: 微调Timer的multiplier/shift
@time_adjtime: 增加delta ns 到struct timecounter 中的ns值。
@time_gettime: 获取struct timecounter 中的ns值。
@time_settime: 重置struct timecounter 中的ns值。

int time_gettime(struct timecounter *p, struct timeval*ts)
{
    u64 ns;
    # Lock for Multiple Threads
    ns = timecounter_read(p);
    # Unock for Multiple Threads
    ns_to_timeval(ns,ts);
    return 0;
}

int time_settime(struct timecounter *p, struct timeval*ts)
{
    u64 ns = timeval_to_ns(ts);
    # Lock for Multiple Threads
    timecounter_init(p, &hardware_cc, ns);
    # Unock for Multiple Threads
    return 0;
}

int time_adjfine(struct timecounter *p, long scaled_ppm)
{
    s64 clkrate;
    clkrate = (s64)scaled_ppm * HARDWARE_CC_MULT_NUM;
    clkrate = clkrate / HARDWARE_CC_MULT_DEM;
    # Lock for Multiple Threads
    timecounter_read(p);
    p->mult = HARDWARE_CC_MULT + clkrate;
    # Unock for Multiple Threads
    return 0;
}

int time_adjtime(struct timecounter *p, s64 delta)
{
    # Lock for Multiple Threads
    timecounter_adjtime(p, delta);
    # Unock for Multiple Threads
    return 0;
}

后续。。。

  • 5
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值