c语言timercounter

99ANYc3cd6
预计阅读时长 30 分钟
位置: 首页 C语言 正文

这是一个在嵌入式系统、操作系统和底层编程中非常重要的概念,我会从基本概念讲起,然后深入到不同平台的具体实现,包括 Linux 内核、嵌入式微控制器(如 STM32)以及使用 clock() 函数的用户态编程。

c语言timercounter
(图片来源网络,侵删)

核心概念:定时器 vs. 计数器

要理解定时器和计数器本质上是同一个硬件模块,只是我们看待和使用它的角度不同。

计数器

计数器是一个非常基础的硬件模块,它的核心功能就是对事件进行计数

  • 工作原理:它内部有一个寄存器(通常称为“计数寄存器”或“计数器”),每当一个特定的“时钟事件”发生时,这个寄存器的值就会加 1(或减 1,取决于配置)。
  • 计数的事件
    • 内部时钟:使用系统提供的一个稳定的、高频的时钟信号(8MHz、16MHz、168MHz)作为计数事件,这是最常见用法。
    • 外部信号:对芯片外部的某个引脚上的电平变化(如上升沿、下降沿)进行计数,这在测量输入脉冲频率或数量时非常有用。
  • 特点:纯粹、客观地记录事件发生的次数。

定时器

定时器是基于计数器构建的,它利用计数器的计数值来实现定时功能

  • 工作原理

    c语言timercounter
    (图片来源网络,侵删)
    1. 我们设定一个“预装载值” (Auto-Reload Value)
    2. 计数器从某个值(通常是 0)开始,以固定的频率递增。
    3. 当计数器的值达到我们设定的预装载值时,就会发生一个“事件”(称为“溢出”或“匹配”)。
    4. 发生事件后,计数器可以自动清零并重新开始计数,或者我们手动将其清零。
  • 如何计算时间定时时间 = (预装载值 + 1) * 时钟周期 定时时间 = (预装载值 + 1) / 时钟频率

    举例:假设一个定时器的时钟频率是 1MHz(即每秒 100 万次计数),我们想定时 1 毫秒(0.001秒)。

    • 预装载值 = (定时时间 * 时钟频率) - 1 = (0.001 * 1000000) - 1 = 999
    • 我们将预装载值设置为 999,计数器从 0 开始数到 999,正好是 1000 个计数,耗时 1 毫秒。
  • 特点:主动地、周期性地产生一个时间基准或触发一个动作。

计数器是“因”,定时器是“果”。 我们通过配置计数器来让它为我们完成定时任务,在硬件手册和寄存器描述中,这个模块通常被称为“Timer”或“TIM”,但其核心是一个计数器。


在不同平台下的 C 语言实现

下面我们来看 C 语言如何在不同场景下操作定时器/计数器。

A. Linux 内核环境

在 Linux 内核中,操作硬件定时器通常通过内核提供的 API 来完成,而不是直接读写寄存器,这保证了代码的可移植性和稳定性。

jiffies

jiffies 是 Linux 内核中最基础、最重要的全局计时变量。

  • 是什么:一个在内核启动时被初始化的、无符号长整型变量。
  • 如何工作:内核启动时,会找到一个硬件定时器(通常是 PIT - Programmable Interval Timer,或 APIC Timer)并配置它以一个固定的频率(称为 HZ,100Hz, 250Hz, 1000Hz)产生中断。
  • 每次中断发生时jiffies 的值就会加 1。
  • 用途
    • 测量时间:计算两个 jiffies 值的差,可以得到经过的“滴答”数。jiffies_to_msecs() 等辅助函数可以将其转换为毫秒。
    • 定时器实现:内核中的很多定时器(如 timer_list)都是基于 jiffies 来实现的。
    • 超时判断:在驱动代码中,经常使用 time_after(a, b) 这样的宏来判断时间是否已超时。

示例代码(内核模块中测量时间):

#include <linux/module.h>
#include <linux/jiffies.h>
#include <linux/delay.h>
static int __init my_timer_init(void) {
    unsigned long start_jiffies, end_jiffies;
    unsigned long elapsed_ms;
    printk(KERN_INFO "Timer/Counter example: starting jiffies measurement.\n");
    start_jiffies = jiffies; // 获取当前 jiffies 值
    // 让系统休眠 500 毫秒
    msleep(500);
    end_jiffies = jiffies; // 再次获取 jiffies 值
    // 计算经过的时间(毫秒)
    elapsed_ms = jiffies_to_msecs(end_jiffies - start_jiffies);
    printk(KERN_INFO "Expected sleep: 500 ms, Actual elapsed: %lu ms\n", elapsed_ms);
    return 0;
}
static void __exit my_timer_exit(void) {
    printk(KERN_INFO "Timer/Counter example: module unloaded.\n");
}
module_init(my_timer_init);
module_exit(my_timer_exit);
MODULE_LICENSE("GPL");

timer_list

timer_list 是 Linux 内核中用于实现延迟执行或周期性任务的标准接口,它基于 jiffies 工作。

工作流程

  1. 定义并初始化一个 timer_list 结构体。
  2. 设置定时器的到期时间(expires = jiffies + delay_in_jiffies)。
  3. 指定定时器到期后要执行的回调函数。
  4. 使用 add_timer() 将定时器添加到内核定时器链表中。
  5. 在回调函数中执行任务,如果需要周期性执行,可以在回调函数中再次调用 mod_timer() 来更新到期时间。

示例代码(内核模块中使用 timer_list):

#include <linux/timer.h>
#include <linux/jiffies.h>
#include <linux/module.h>
static struct timer_list my_timer;
// 定时器到期时调用的函数
static void my_timer_callback(struct timer_list *t) {
    printk(KERN_INFO "Timer callback function executed!\n");
    // 可以在这里添加需要周期性执行的代码
    // 如果需要周期性执行,再次启动定时器
    // mod_timer(&my_timer, jiffies + msecs_to_jiffies(1000));
}
static int __init my_timer_list_init(void) {
    // 初始化 timer_list
    timer_setup(&my_timer, my_timer_callback, 0);
    // 设置定时器 1 秒后到期
    my_timer.expires = jiffies + msecs_to_jiffies(1000);
    // 添加并启动定时器
    add_timer(&my_timer);
    printk(KERN_INFO "timer_list module loaded, timer will fire in 1 second.\n");
    return 0;
}
static void __exit my_timer_list_exit(void) {
    // 在模块卸载前,必须删除定时器,防止系统崩溃
    del_timer_sync(&my_timer);
    printk(KERN_INFO "timer_list module unloaded.\n");
}
module_init(my_timer_list_init);
module_exit(my_timer_list_exit);
MODULE_LICENSE("GPL");

B. 嵌入式微控制器环境 (以 STM32 为例)

在裸机编程或 RTOS 环境下,我们直接操作硬件寄存器来配置定时器,这是最底层、最直接的控制方式。

寄存器级操作

以 STM32 的通用定时器为例,我们需要配置以下几个关键寄存器:

  • CR1 (Control Register 1):使能/禁用定时器,设置计数方向等。
  • PSC (Prescaler):预分频器,用于降低定时器时钟频率,得到一个较慢的计数频率,以便实现更长的定时时间。 定时器时钟频率 = APB1/APB2 总线时钟 / (PSC + 1)
  • ARR (Auto-Reload Register):自动重装载值(即前面提到的“预装载值”),决定了定时周期。 定时周期 = (ARR + 1) / 定时器时钟频率
  • SR (Status Register):状态寄存器,当计数器溢出时,对应的更新中断标志位会被置 1。
  • DIER (DMA/Interrupt Enable Register):中断使能寄存器,用于开启更新中断。

配置步骤

  1. 使能定时器所在的总线时钟(RCC_APB1PeriphClockCmd)。
  2. 设置预分频器 PSC
  3. 设置自动重装载值 ARR
  4. 使能更新中断(TIM_IT_UpdateRequest)。
  5. 配置 NVIC(嵌套向量中断控制器)以响应定时器中断。
  6. 使能定时器(TIM_Cmd)。
  7. 编写中断服务函数,在函数中清除中断标志位,并执行定时任务。

使用 HAL/LL 库

为了简化寄存器操作,芯片厂商(如 ST)提供了硬件抽象层库,使用 C 调用库函数,代码会更简洁、可读性更高。

示例代码 (STM32CubeIDE + HAL 库):

// 在 main.c 中
// 在 main 函数中初始化定时器
void MX_TIM2_Init(void)
{
  TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};
  htim2.Instance = TIM2;
  htim2.Init.Prescaler = 8399; // 假设 APB1 时钟为 84MHz
  htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim2.Init.Period = 9999;     // 自动重装载值
  htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim2) != HAL_OK)
  {
    Error_Handler();
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
}
// 启动定时器
HAL_TIM_Base_Start_IT(&htim2);
// 在 stm32f4xx_it.c 中找到 TIM2 的中断服务函数
void TIM2_IRQHandler(void)
{
  /* USER CODE BEGIN TIM2_IRQn 0 */
  /* USER CODE END TIM2_IRQn 0 */
  HAL_TIM_IRQHandler(&htim2);
  /* USER CODE BEGIN TIM2_IRQn 1 */
  /* USER CODE END TIM2_IRQn 1 */
}
// 在 main.c 中实现 HAL 库的回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM2) // 确认是 TIM2 的中断
  {
    /* USER CODE BEGIN Callback 0 */
    // 在这里执行你的定时任务,例如翻转 LED
    // HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
    /* USER CODE END Callback 0 */
  }
}

计算

  • TIM2 时钟频率 = 84MHz / (8399 + 1) = 10kHz
  • 定时周期 = (9999 + 1) / 10kHz = 10000 / 10000 = 1秒

C. 用户态环境 (标准 C)

在标准的 C 语言应用程序中,我们没有硬件定时器,但可以使用操作系统提供的时间函数来实现软件定时。

clock() 函数

clock() 函数在 <time.h> 中定义,它返回从程序开始运行起处理器所消耗的时钟节拍数。

  • 头文件<time.h>
  • 函数原型clock_t clock(void);
  • 返回值clock_t 类型的值,表示处理器时钟节拍数。
  • CLOCKS_PER_SEC:表示每秒有多少个时钟节拍,它的值是实现相关的,但通常是 1000000。

示例代码(计算程序运行时间):

#include <stdio.h>
#include <time.h>
int main() {
    clock_t start, end;
    double cpu_time_used;
    start = clock(); // 获取开始时间
    // 模拟一段耗时操作
    for (int i = 0; i < 100000000; i++) {
        // do nothing
    }
    end = clock(); // 获取结束时间
    cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
    printf("程序执行耗时: %f 秒\n", cpu_time_used);
    return 0;
}

注意clock() 测量的是 CPU 的实际执行时间,而不是 wall-clock time(挂钟时间),如果程序在等待 I/O 或被其他进程阻塞,clock() 的值不会增加。

nanosleep()sleep()

这两个函数用于让当前进程休眠指定的时间,是实现软件延迟的常用方法。

  • sleep():精度较低,以秒为单位。
  • nanosleep():精度较高,可以精确到纳秒,但实际精度取决于操作系统和硬件,它比 sleep() 更现代,也更灵活。

示例代码(使用 nanosleep):

#include <stdio.h>
#include <time.h>
#include <unistd.h> // for sleep()
int main() {
    printf("开始...\n");
    fflush(stdout); // 确保立即输出
    // 休眠 2 秒 500 毫秒
    struct timespec ts;
    ts.tv_sec = 2;      // 秒
    ts.tv_nsec = 500000000; // 纳秒 (0.5秒)
    nanosleep(&ts, NULL);
    printf("休眠结束,\n");
    // 使用 sleep() 的例子
    printf("再休眠 1 秒...\n");
    sleep(1);
    printf("再次结束,\n");
    return 0;
}

总结与对比

特性 Linux 内核 (jiffies, timer_list) 嵌入式微控制器 (寄存器/HAL库) 标准 C (clock(), nanosleep())
运行环境 Linux 操作系统内核 裸机或 RTOS 环境 用户态应用程序
底层原理 封装硬件定时器中断,基于 jiffies 全局变量 直接操作或通过库操作硬件定时器寄存器 调用操作系统提供的系统调用或库函数
主要用途 内核延迟、驱动超时、内核定时任务 精确定时、PWM 输出、输入捕获、系统时钟节拍 程序性能分析、软件延迟、非精确定时
精度 取决于 HZ 配置 (通常为 ms 级) 非常高 (取决于时钟源,可达 ns 级) 较低 (受操作系统调度影响,通常为 ms 级)
编程模型 事件驱动 (中断) 硬件中断或轮询 轮询或阻塞式系统调用
C 语言体现 jiffies, timer_list 结构体,add_timer() 寄存器地址操作,或 HAL_TIM_Base_Init() clock(), nanosleep(), sleep()

希望这个详细的解释能帮助你全面理解 C 语言中定时器和计数器的概念与应用!

-- 展开阅读全文 --
头像
dede 友情链接 内页 首页
« 上一篇 01-04
dede alt=图片显示区
下一篇 » 01-04

相关文章

取消
微信二维码
支付宝二维码

目录[+]