本文主要是介绍如何在MCU上通过ToD+PPS 获取同步时间(二),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
欢迎淘宝搜索飞灵科技,我司相关新产品陆续上线
实现过程
根据上面介绍,我们需要这样一个时钟来记录系统时间,即记录至1970-01-01以来的ns 数:
- 精度为ns级别。
- 位数足够长,不会发生回绕, 通常为64bits。
- 支持计数频率调整。
在硬件上,很难找到一个这样的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 Threadsns = timecounter_read(p);# Unock for Multiple Threadsns_to_timeval(ns,ts);return 0;
}int time_settime(struct timecounter *p, struct timeval*ts)
{u64 ns = timeval_to_ns(ts);# Lock for Multiple Threadstimecounter_init(p, &hardware_cc, ns);# Unock for Multiple Threadsreturn 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 Threadstimecounter_read(p);p->mult = HARDWARE_CC_MULT + clkrate;# Unock for Multiple Threadsreturn 0;
}int time_adjtime(struct timecounter *p, s64 delta)
{# Lock for Multiple Threadstimecounter_adjtime(p, delta);# Unock for Multiple Threadsreturn 0;
}
后续。。。
这篇关于如何在MCU上通过ToD+PPS 获取同步时间(二)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!