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

相关文章

Python的Darts库实现时间序列预测

《Python的Darts库实现时间序列预测》Darts一个集统计、机器学习与深度学习模型于一体的Python时间序列预测库,本文主要介绍了Python的Darts库实现时间序列预测,感兴趣的可以了解... 目录目录一、什么是 Darts?二、安装与基本配置安装 Darts导入基础模块三、时间序列数据结构与

MyBatis Plus实现时间字段自动填充的完整方案

《MyBatisPlus实现时间字段自动填充的完整方案》在日常开发中,我们经常需要记录数据的创建时间和更新时间,传统的做法是在每次插入或更新操作时手动设置这些时间字段,这种方式不仅繁琐,还容易遗漏,... 目录前言解决目标技术栈实现步骤1. 实体类注解配置2. 创建元数据处理器3. 服务层代码优化填充机制详

python获取指定名字的程序的文件路径的两种方法

《python获取指定名字的程序的文件路径的两种方法》本文主要介绍了python获取指定名字的程序的文件路径的两种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要... 最近在做项目,需要用到给定一个程序名字就可以自动获取到这个程序在Windows系统下的绝对路径,以下

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

SpringBoot 获取请求参数的常用注解及用法

《SpringBoot获取请求参数的常用注解及用法》SpringBoot通过@RequestParam、@PathVariable等注解支持从HTTP请求中获取参数,涵盖查询、路径、请求体、头、C... 目录SpringBoot 提供了多种注解来方便地从 HTTP 请求中获取参数以下是主要的注解及其用法:1

Python与MySQL实现数据库实时同步的详细步骤

《Python与MySQL实现数据库实时同步的详细步骤》在日常开发中,数据同步是一项常见的需求,本篇文章将使用Python和MySQL来实现数据库实时同步,我们将围绕数据变更捕获、数据处理和数据写入这... 目录前言摘要概述:数据同步方案1. 基本思路2. mysql Binlog 简介实现步骤与代码示例1

C# LiteDB处理时间序列数据的高性能解决方案

《C#LiteDB处理时间序列数据的高性能解决方案》LiteDB作为.NET生态下的轻量级嵌入式NoSQL数据库,一直是时间序列处理的优选方案,本文将为大家大家简单介绍一下LiteDB处理时间序列数... 目录为什么选择LiteDB处理时间序列数据第一章:LiteDB时间序列数据模型设计1.1 核心设计原则

MySQL按时间维度对亿级数据表进行平滑分表

《MySQL按时间维度对亿级数据表进行平滑分表》本文将以一个真实的4亿数据表分表案例为基础,详细介绍如何在不影响线上业务的情况下,完成按时间维度分表的完整过程,感兴趣的小伙伴可以了解一下... 目录引言一、为什么我们需要分表1.1 单表数据量过大的问题1.2 分表方案选型二、分表前的准备工作2.1 数据评估

C#控制台程序同步调用WebApi实现方式

《C#控制台程序同步调用WebApi实现方式》控制台程序作为Job时,需同步调用WebApi以确保获取返回结果后执行后续操作,否则会引发TaskCanceledException异常,同步处理可避免异... 目录同步调用WebApi方法Cls001类里面的写法总结控制台程序一般当作Job使用,有时候需要控制

MySQL中DATE_FORMAT时间函数的使用小结

《MySQL中DATE_FORMAT时间函数的使用小结》本文主要介绍了MySQL中DATE_FORMAT时间函数的使用小结,用于格式化日期/时间字段,可提取年月、统计月份数据、精确到天,对大家的学习或... 目录前言DATE_FORMAT时间函数总结前言mysql可以使用DATE_FORMAT获取日期字段