如何在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

相关文章

基于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

MiniGPT-3D, 首个高效的3D点云大语言模型,仅需一张RTX3090显卡,训练一天时间,已开源

项目主页:https://tangyuan96.github.io/minigpt_3d_project_page/ 代码:https://github.com/TangYuan96/MiniGPT-3D 论文:https://arxiv.org/pdf/2405.01413 MiniGPT-3D在多个任务上取得了SoTA,被ACM MM2024接收,只拥有47.8M的可训练参数,在一张RTX

批处理以当前时间为文件名创建文件

批处理以当前时间为文件名创建文件 批处理创建空文件 有时候,需要创建以当前时间命名的文件,手动输入当然可以,但是有更省心的方法吗? 假设我是 windows 操作系统,打开命令行。 输入以下命令试试: echo %date:~0,4%_%date:~5,2%_%date:~8,2%_%time:~0,2%_%time:~3,2%_%time:~6,2% 输出类似: 2019_06

Android Environment 获取的路径问题

1. 以获取 /System 路径为例 /*** Return root of the "system" partition holding the core Android OS.* Always present and mounted read-only.*/public static @NonNull File getRootDirectory() {return DIR_ANDR

【MRI基础】TR 和 TE 时间概念

重复时间 (TR) 磁共振成像 (MRI) 中的 TR(重复时间,repetition time)是施加于同一切片的连续脉冲序列之间的时间间隔。具体而言,TR 是施加一个 RF(射频)脉冲与施加下一个 RF 脉冲之间的持续时间。TR 以毫秒 (ms) 为单位,主要控制后续脉冲之前的纵向弛豫程度(T1 弛豫),使其成为显著影响 MRI 中的图像对比度和信号特性的重要参数。 回声时间 (TE)

LeetCode:64. 最大正方形 动态规划 时间复杂度O(nm)

64. 最大正方形 题目链接 题目描述 给定一个由 0 和 1 组成的二维矩阵,找出只包含 1 的最大正方形,并返回其面积。 示例1: 输入: 1 0 1 0 01 0 1 1 11 1 1 1 11 0 0 1 0输出: 4 示例2: 输入: 0 1 1 0 01 1 1 1 11 1 1 1 11 1 1 1 1输出: 9 解题思路 这道题的思路是使用动态规划

JS和jQuery获取节点的兄弟,父级,子级元素

原文转自http://blog.csdn.net/duanshuyong/article/details/7562423 先说一下JS的获取方法,其要比JQUERY的方法麻烦很多,后面以JQUERY的方法作对比。 JS的方法会比JQUERY麻烦很多,主要则是因为FF浏览器,FF浏览器会把你的换行也当最DOM元素。 <div id="test"><div></div><div></div

O(n)时间内对[0..n^-1]之间的n个数排序

题目 如何在O(n)时间内,对0到n^2-1之间的n个整数进行排序 思路 把整数转换为n进制再排序,每个数有两位,每位的取值范围是[0..n-1],再进行基数排序 代码 #include <iostream>#include <cmath>using namespace std;int n, radix, length_A, digit = 2;void Print(int *A,

vcpkg子包路径批量获取

获取vcpkg 子包的路径,并拼接为set(CMAKE_PREFIX_PATH “拼接路径” ) import osdef find_directories_with_subdirs(root_dir):# 构建根目录下的 "packages" 文件夹路径root_packages_dir = os.path.join(root_dir, "packages")# 如果 "packages"