相关参考

内核时间

在玩嵌入式得时候,我们都知道,外接晶振,内接CPU的时钟树,产生了各种时钟:AXI时钟,APB时钟,CPU时钟。
image.png
那么内核时间是怎么获取?

内核时钟框架

Linux Time 中详细的描述了Linux内核时钟框架
image.png
可以看到:linux上 各种时钟架构和服务是基于硬件提供的两种timer而构建的。

  • 基于硬件定时器 timer
  • 基于时钟源

1、定时器Timer

  • 这类 timer 每个cpu都有一个独立的,称为local timer。这类timer的中断一般都是PPI(Private Peripheral Interrupt)类型,即每个cpu都有独立一份中断。

    1. PPI对应的是SPIShared Peripheral Interrupt,即多个cpu共享同一个中断。
  • 这类timer一般是32bit宽度count,最重要的它会频繁的溢出并产生timer到期中断。

  • 这类timer服务于tick timer(低精度)或者hrtimer(高精度)
  • 低精度模式,local timer工作在PERIODIC模式。即timer以tick时间(1/HZ)周期性的产生中断。在tick timer中处理任务调度tick、低精度timer、其他时间更新和统计profile。在这种模式下,所有利用时间的进行的运算,精度都是以tick(1/HZ)为单位的,精度较低。比如HZ=1000,那么tick=1ms。
  • 高精度模式,local timer工作在ONESHOT模式。即系统可以支持hrtimer(high resolution)高精度timer,精度为local timer的计数clk达到ns级别。这种情况下把tick timer也转换成一种hrtimer

2、时间戳Timer

  • 这类timer一个系统多个cpu共享一个,称为global timer
  • 这类timer一般是32bit/64bit宽度count,一般不会溢出产生中断,系统实时的去读取count的值来计算当前的时间戳。
  • 这类timer服务于clocksource/timekeeper。

Linux Time 中 列举了 Exynos4412的时钟框架
image.png
可以看到, 基于PLL出来的TCLK,出来一个64位的时间戳timer;
基于PLL出来的TCLK,引出了8个32位的定时器timer

LDD中关于时间的接口

HZ和tick

定时器中断由系统定时硬件以规律地间隔产生。每一次中断称为一个tick嘀嗒值,HZ代表频率,即1s产生多少次tick值
这两个值依赖具体硬件,不应该直接使用。

jiffies 计数器

jiffies计数器代表自系统启动后,到现在产生了多少tick值(中断计数)。

  1. <linux/jiffies.h>
  2. // 获取jiffies
  3. u64 a = get_jiffies_64()
  4. u64 b = get_jiffies_64()
  5. // 毕竟时间先后
  6. time_after(a,b); // returns true if the time a is after time b
  7. time_before(a,b); // returns true if the time a is before time a
  8. // jiffiesmsec转换
  9. jiffies_to_msecs();
  10. jiffies_to_usecs();
  11. jiffies_to_nsecs();
  12. // jiffies和时间转换
  13. struct timespec64 {
  14. time64_t tv_sec; /* seconds */
  15. long tv_nsec; /* nanoseconds */
  16. };
  17. unsigned long timespec64_to_jiffies(const struct timespec64 *value);
  18. void jiffies_to_timespec64(const unsigned long jiffies,struct timespec64 *value);

timers-howto.rst 中描述了 udelay,mdelay,ndelay; 以及 msleep等

Linux下表示时间的接口

基于硬件定时器的时间操作接口

在内核中常见定时器操作,比如 定时任务,meleep,schedule_timeout等都使用timer进行定时。

timer

低分辨率定时器的原理和实现
利用定时器,我们可以设定在未来的某一时刻,触发一个特定的事件。所谓低分辨率定时器,是指这种定时器的计时单位基于jiffies值的计数,也就是说,它的精度只有1/HZ,假如你的内核配置的HZ是1000,那意味着系统中的低分辨率定时器的精度就是1ms。

回顾下Linux内核定时器

  1. #include <linux/timer.h>
  2. init_timer(struct timer_list *timer);
  3. struct timer_list {
  4. unsigned long expires;
  5. void (*function)(unsigned long);
  6. unsigned long data;
  7. };
  8. add_timer(struct timer_list * timer);
  9. del_timer(struct timer_list * timer);
  10. mod_timer(struct timer_list *timer, unsigned long expires);

很简单的一个借口,可惜Linux内核不用了,这里上新版本使用方法

  1. #include <linux/timer.h>
  2. struct timer_list {
  3. unsigned long expires;
  4. void (*function)(struct timer_list *);
  5. u32 flags
  6. };
  7. struct TEST_INFO{
  8. .....
  9. struct timer_list timer;
  10. };
  11. timer_setup(struct timer_list * timer, callback, flags); // 初始化timer,
  12. // 回调函数接口
  13. static void ad7879_timer(struct timer_list *t)
  14. {
  15. struct TEST_INFO *ts = from_timer(ts, t, timer);
  16. ad7879_ts_event_release(ts);
  17. }

htimer

基于时钟源的时间操作接口

clocksource 时钟源

参考: Linux时间子系统之一:clock source(时钟源)

clock source 为linux内核提供一个时间基线(也是个计数器),如果你用linux的date命令获取当前时间,内核会读取当前的clock source,转换并返回合适的时间单位给用户空间。

在硬件层,它通常实现为一个由固定时钟频率驱动的计数器,计数器只能单调地增加,直到溢出为止。
时钟源是内核计时的基础,系统启动时,内核通过硬件RTC获得当前时间,在这以后,在大多数情况下,内核通过选定的时钟源更新实时时间信息(墙上时间),而不再读取RTC的时间

内核注册时钟源
  1. #include <linux/clocksource.h>
  2. int clocksource_register_hz(struct clocksource *cs, u32 hz); // API
  3. 参考:drivers/clocksource/ingenic-timer.c ingenic_tcu_clocksource_init 接口
  4. struct clk *cs_clk; // 从设备树获取时钟源
  5. clk_prepare_enable(tcu->cs_clk);
  6. unsigned long rate = clk_get_rate(tcu->cs_clk); // 这就就是时钟频率
  7. struct clocksource * cs
  8. cs->name = "ingenic-timer";
  9. cs->rating = 200;
  10. cs->flags = CLOCK_SOURCE_IS_CONTINUOUS;
  11. cs->mask = CLOCKSOURCE_MASK(16);
  12. cs->read = ingenic_tcu_timer_cs_read;
  13. clocksource_register_hz(cs, rate);

image.png
这里有几个参数比较有意思:

  • 利用mult和shift推算频率
  • 每次clocksource注册,都会触发 clocksource_select 来使用rating 最好的值 作为系统时钟。并通知timekeeping。
  • 全局变量 clocksource_list 为系统所有的clocksource,curr_clocksource 为__clocksource_select 后选择当前时钟 作为系统时钟使用。


  1. # linux-git/kernel/time/jiffies.c 注册了 精度只有1/HZ秒,rating值为1的时钟源
  2. static struct clocksource clocksource_jiffies = {
  3. .name = "jiffies",
  4. .rating = 1, /* lowest valid rating*/
  5. .read = jiffies_read,
  6. .mask = CLOCKSOURCE_MASK(32),
  7. .mult = TICK_NSEC << JIFFIES_SHIFT, /* details above */
  8. .shift = JIFFIES_SHIFT,
  9. .max_cycles = 10,
  10. };
  11. static int __init init_jiffies_clocksource(void)
  12. {
  13. return __clocksource_register(&clocksource_jiffies);
  14. }

timekeeper 墙上时间(重点)

clocksource 给我们提供的其实就是个计数器。
但我们真是使用时间基本上都基于 墙上时间(真实时间) 来计算,那么clocksource怎么转换为墙上时间?
Linux时间子系统之三:时间的维护者:timekeeper
Linux内核文档-ktime accessorstimekeeping.rst

注:
gettimeofday和do_gettimeofday 使用墙上时间,可内核在5.0不支持do_gettimeofday了, 所以这里主要考虑ktime
以及 current_kernel_time(void); 等 都不支持了,所以不再研究。

推荐使用:
参考:timekeeping.rst

  1. #include <linux/time.h>
  2. #include <linux/timekeeping.h>
  3. void ktime_get_real_ts64(struct timespec64 *ts);
  4. ktime_t ktime_get_real( void )

注:coarse 相关API是精度不准确的时间,在精度要求不高的情况可以使用

时间分类

内核管理着多种时间,它们分别是:

  • RTC时间
  • wall time:墙上时间 xtime(关键点)
  • monotonic time
  • raw monotonic time
  • boot time:总启动时间

RTC时间 在PC中,RTC时间又叫CMOS时间,它通常由一个专门的计时硬件来实现,软件可以读取该硬件来获得年月日、时分秒等时间信息,而在嵌入式系统中,有使用专门的RTC芯片,也有直接把RTC集成到Soc芯片中,读取Soc中的某个寄存器即可获取当前时间信息。一般来说,RTC是一种可持续计时的,也就是说,不管系统是否上电,RTC中的时间信息都不会丢失,计时会一直持续进行,硬件上通常使用一个后备电池对RTC硬件进行单独的供电。因为RTC硬件的多样性,开发者需要为每种RTC时钟硬件提供相应的驱动程序,内核和用户空间通过驱动程序访问RTC硬件来获取或设置时间信息。
xtime xtime和RTC时间一样,都是人们日常所使用的墙上时间,只是RTC时间的精度通常比较低,大多数情况下只能达到毫秒级别的精度,如果是使用外部的RTC芯片,访问速度也比较慢,为此,内核维护了另外一个wall time时间:xtime,取决于用于对xtime计时的clocksource,它的精度甚至可以达到纳秒级别,因为xtime实际上是一个内存中的变量,它的访问速度非常快,内核大部分时间都是使用xtime来获得当前时间信息。xtime记录的是自1970年1月1日24时到当前时刻所经历的纳秒数。
monotonic time 该时间自系统开机后就一直单调地增加,它不像xtime可以因用户的调整时间而产生跳变,不过该时间不计算系统休眠的时间,也就是说,系统休眠时,monotoic时间不会递增。
raw monotonic time 该时间与monotonic时间类似,也是单调递增的时间,唯一的不同是:raw monotonic time“更纯净”,他不会受到NTP时间调整的影响,它代表着系统独立时钟硬件对时间的统计。
boot time 与monotonic时间相同,不过会累加上系统休眠的时间,它代表着系统上电后的总时间。
image.png
我们这里只关心xtime

timekeeper的初始化
  1. start_kernel
  2. ->timekeeping_init();
  3. ->获取RTC时间-参考http://kernel.meizu.com/linux-time.html 中描述了,在RTC初始化后会更新timekeeping
  4. ->clock = clocksource_default_clock(); // 获取默认时钟源
  5. ->tk_set_xtime(tk, &wall_time); // 设置默认xtime
  6. # 时间的更新
  7. /boot/config-5.4.0-58-generic
  8. CONFIG_NO_HZ=y # 可能会在好几个tick后,do_timer才会被调用一次
  9. do_timer 函数在几个tick后,调用clocksource来获取计数器值,来更新墙上时间

常用API接口

  1. ## 内核态墙上时间获取
  2. #include <linux/timekeeping.h>
  3. void getboottime64(struct timespec64 *ts) 获取系统启动时刻的实时时间
  4. void get_monotonic_boottime(struct timespec *ts); 获取系统启动以来所经过的时间,包含休眠时间
  5. ktime_t ktime_get_boottime(void); 获取系统启动以来所经过的c时间,包含休眠时间,返回ktime类型
  6. ktime_t ktime_get(void); 获取系统启动以来所经过的c时间,不包含休眠时间,返回ktime类型
  7. void ktime_get_ts(struct timespec *ts) ; 获取系统启动以来所经过的c时间,不包含休眠时间,返回timespec结构
  8. unsigned long get_seconds(void); 返回xtime中的秒计数值
  9. struct timespec current_kernel_time(void); 返回内核最后一次更新的xtime时间,不累计最后一次更新至今clocksource的计数值
  10. // void getnstimeofday(struct timespec *ts); 获取当前时间,返回timespec结构 已删除
  11. // void do_gettimeofday(struct timeval *tv); 获取当前时间,返回timeval结构 已删除
  12. ## 用户态墙上时间获取
  13. #include <time.h>
  14. clock_gettime(clockid_t clk_id, struct timespec *tp);
  15. 对应ID配置
  16. xtime CLOCK_REALTIME
  17. monotonic-time CLOCK_MONOTONIC
  18. raw_time CLOCK_MONOTONIC_RAW

注:在Linux内核删除 do_gettimeofday的今天,我们使用什么来计算时间?(在: 33e26418193f58d1895f2f968e1953b1caf8deb7 版本删除,参考 timekeeping.rst)
Linux内核中的时间函数 描述了部分替代接口

  1. struct timespec64 {
  2. time64_t tv_sec; /* seconds */
  3. long tv_nsec; /* nanoseconds */
  4. };

其中,Ktime和其他结构体间转换可参考:Linux时间子系统之二:表示时间的单位和结构

  1. # ktime和timespec64的转换
  2. #define ktime_to_timespec64(kt) ns_to_timespec64((kt))
  3. ktime_t timespec64_to_ktime(struct timespec64 ts);
  4. ktime_t ktime_set(const s64 secs, const unsigned long nsecs); 可以将时间转为ktime_t的数值
  5. s64 ktime_to_ns(const ktime_t kt);
  6. int ktime_equal(const ktime_t cmp1, const ktime_t cmp2);
  7. s64 ktime_to_us(const ktime_t kt);
  8. s64 ktime_to_ms(const ktime_t kt);
  9. ktime_t ns_to_ktime(u64 ns)

struct ktime得使用

相关API
  1. #include <linux/ktime.h>
  2. typedef signed long long s64;
  3. typedef s64 ktime_t;
  4. ktime_t ktime_set(const long secs, const unsigned long nsecs);
  5. ktime_t ktime_sub(const ktime_t lhs, const ktime_t rhs);
  6. ktime_t ktime_add(const ktime_t add1, const ktime_t add2);
  7. ktime_t ktime_add_ns(const ktime_t kt, u64 nsec);
  8. ktime_t ktime_sub_ns(const ktime_t kt, u64 nsec);
  9. ktime_t timespec_to_ktime(const struct timespec ts);
  10. ktime_t timeval_to_ktime(const struct timeval tv);
  11. struct timespec ktime_to_timespec(const ktime_t kt);
  12. struct timeval ktime_to_timeval(const ktime_t kt);
  13. s64 ktime_to_ns(const ktime_t kt);
  14. int ktime_equal(const ktime_t cmp1, const ktime_t cmp2);
  15. s64 ktime_to_us(const ktime_t kt);
  16. s64 ktime_to_ms(const ktime_t kt);

demo
  1. # 内核态时间
  2. // dmatest.c 中dmatest_func
  3. #include <linux/ktime.h>
  4. ktime_t ktime, start, diff;
  5. ktime_t filltime = 0;
  6. ktime_t comparetime = 0;
  7. s64 runtime = 0;
  8. ktime = ktime_get(); // 获取当前时间
  9. start = ktime_get(); // 获取每次任务开始时间
  10. ..... // 任务处理
  11. diff = ktime_sub(ktime_get(), start); // 获取任务处理所用得时间
  12. filltime = ktime_add(filltime, diff); // 总时间
  13. runtime = ktime_to_us(ktime); // 时间转为us数

Linux时钟驱动

Linux下时钟框架实践—-一款芯片的时钟树配置