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

2024-01-22 01:48
文章标签 时间 获取 同步 mcu pps tod

本文主要是介绍如何在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 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 获取同步时间(二)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/631495

相关文章

详谈redis跟数据库的数据同步问题

《详谈redis跟数据库的数据同步问题》文章讨论了在Redis和数据库数据一致性问题上的解决方案,主要比较了先更新Redis缓存再更新数据库和先更新数据库再更新Redis缓存两种方案,文章指出,删除R... 目录一、Redis 数据库数据一致性的解决方案1.1、更新Redis缓存、删除Redis缓存的区别二

如何用Java结合经纬度位置计算目标点的日出日落时间详解

《如何用Java结合经纬度位置计算目标点的日出日落时间详解》这篇文章主详细讲解了如何基于目标点的经纬度计算日出日落时间,提供了在线API和Java库两种计算方法,并通过实际案例展示了其应用,需要的朋友... 目录前言一、应用示例1、天安门升旗时间2、湖南省日出日落信息二、Java日出日落计算1、在线API2

python获取当前文件和目录路径的方法详解

《python获取当前文件和目录路径的方法详解》:本文主要介绍Python中获取当前文件路径和目录的方法,包括使用__file__关键字、os.path.abspath、os.path.realp... 目录1、获取当前文件路径2、获取当前文件所在目录3、os.path.abspath和os.path.re

Java子线程无法获取Attributes的解决方法(最新推荐)

《Java子线程无法获取Attributes的解决方法(最新推荐)》在Java多线程编程中,子线程无法直接获取主线程设置的Attributes是一个常见问题,本文探讨了这一问题的原因,并提供了两种解决... 目录一、问题原因二、解决方案1. 直接传递数据2. 使用ThreadLocal(适用于线程独立数据)

如何使用 Bash 脚本中的time命令来统计命令执行时间(中英双语)

《如何使用Bash脚本中的time命令来统计命令执行时间(中英双语)》本文介绍了如何在Bash脚本中使用`time`命令来测量命令执行时间,包括`real`、`user`和`sys`三个时间指标,... 使用 Bash 脚本中的 time 命令来统计命令执行时间在日常的开发和运维过程中,性能监控和优化是不

python中的与时间相关的模块应用场景分析

《python中的与时间相关的模块应用场景分析》本文介绍了Python中与时间相关的几个重要模块:`time`、`datetime`、`calendar`、`timeit`、`pytz`和`dateu... 目录1. time 模块2. datetime 模块3. calendar 模块4. timeit

Java将时间戳转换为Date对象的方法小结

《Java将时间戳转换为Date对象的方法小结》在Java编程中,处理日期和时间是一个常见需求,特别是在处理网络通信或者数据库操作时,本文主要为大家整理了Java中将时间戳转换为Date对象的方法... 目录1. 理解时间戳2. Date 类的构造函数3. 转换示例4. 处理可能的异常5. 考虑时区问题6.

Nacos集群数据同步方式

《Nacos集群数据同步方式》文章主要介绍了Nacos集群中服务注册信息的同步机制,涉及到负责节点和非负责节点之间的数据同步过程,以及DistroProtocol协议在同步中的应用... 目录引言负责节点(发起同步)DistroProtocolDistroSyncChangeTask获取同步数据getDis

基于MySQL Binlog的Elasticsearch数据同步实践

一、为什么要做 随着马蜂窝的逐渐发展,我们的业务数据越来越多,单纯使用 MySQL 已经不能满足我们的数据查询需求,例如对于商品、订单等数据的多维度检索。 使用 Elasticsearch 存储业务数据可以很好的解决我们业务中的搜索需求。而数据进行异构存储后,随之而来的就是数据同步的问题。 二、现有方法及问题 对于数据同步,我们目前的解决方案是建立数据中间表。把需要检索的业务数据,统一放到一张M

服务器集群同步时间手记

1.时间服务器配置(必须root用户) (1)检查ntp是否安装 [root@node1 桌面]# rpm -qa|grep ntpntp-4.2.6p5-10.el6.centos.x86_64fontpackages-filesystem-1.41-1.1.el6.noarchntpdate-4.2.6p5-10.el6.centos.x86_64 (2)修改ntp配置文件 [r