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

核心概念:定时器 vs. 计数器
要理解定时器和计数器本质上是同一个硬件模块,只是我们看待和使用它的角度不同。
计数器
计数器是一个非常基础的硬件模块,它的核心功能就是对事件进行计数。
- 工作原理:它内部有一个寄存器(通常称为“计数寄存器”或“计数器”),每当一个特定的“时钟事件”发生时,这个寄存器的值就会加 1(或减 1,取决于配置)。
- 计数的事件:
- 内部时钟:使用系统提供的一个稳定的、高频的时钟信号(8MHz、16MHz、168MHz)作为计数事件,这是最常见用法。
- 外部信号:对芯片外部的某个引脚上的电平变化(如上升沿、下降沿)进行计数,这在测量输入脉冲频率或数量时非常有用。
- 特点:纯粹、客观地记录事件发生的次数。
定时器
定时器是基于计数器构建的,它利用计数器的计数值来实现定时功能。
-
工作原理:
(图片来源网络,侵删)- 我们设定一个“预装载值” (Auto-Reload Value)。
- 计数器从某个值(通常是 0)开始,以固定的频率递增。
- 当计数器的值达到我们设定的预装载值时,就会发生一个“事件”(称为“溢出”或“匹配”)。
- 发生事件后,计数器可以自动清零并重新开始计数,或者我们手动将其清零。
-
如何计算时间:
定时时间 = (预装载值 + 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 工作。
工作流程:
- 定义并初始化一个
timer_list结构体。 - 设置定时器的到期时间(
expires = jiffies + delay_in_jiffies)。 - 指定定时器到期后要执行的回调函数。
- 使用
add_timer()将定时器添加到内核定时器链表中。 - 在回调函数中执行任务,如果需要周期性执行,可以在回调函数中再次调用
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):中断使能寄存器,用于开启更新中断。
配置步骤:
- 使能定时器所在的总线时钟(
RCC_APB1PeriphClockCmd)。 - 设置预分频器
PSC。 - 设置自动重装载值
ARR。 - 使能更新中断(
TIM_IT_UpdateRequest)。 - 配置 NVIC(嵌套向量中断控制器)以响应定时器中断。
- 使能定时器(
TIM_Cmd)。 - 编写中断服务函数,在函数中清除中断标志位,并执行定时任务。
使用 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 语言中定时器和计数器的概念与应用!
