ESP32-S3 通用定时器(GPTimer)学习笔记

4339 字
22 分钟
ESP32-S3 通用定时器(GPTimer)学习笔记

ESP32-S3 通用定时器(GPTimer)学习笔记#

本章重要 API 函数#

API功能参数说明头文件
gptimer_new_timer()创建通用定时器const gptimer_config_t *config:配置结构体
gptimer_handle_t *ret_timer:返回定时器句柄
"driver/gptimer.h"
gptimer_set_alarm_action()设置报警值和自动重载gptimer_handle_t timer:定时器句柄
const gptimer_alarm_config_t *alarm_config:报警配置
"driver/gptimer.h"
gptimer_register_event_callbacks()注册报警回调函数gptimer_handle_t timer:定时器句柄
const gptimer_event_callbacks_t *cbs:回调结构体
void *user_data:用户数据
"driver/gptimer.h"
gptimer_enable()使能定时器模块gptimer_handle_t timer:定时器句柄"driver/gptimer.h"
gptimer_start()启动定时器计数gptimer_handle_t timer:定时器句柄"driver/gptimer.h"
gptimer_stop()停止定时器计数gptimer_handle_t timer:定时器句柄"driver/gptimer.h"
gptimer_disable()禁用定时器gptimer_handle_t timer:定时器句柄"driver/gptimer.h"
gptimer_del_timer()删除定时器释放资源gptimer_handle_t timer:定时器句柄"driver/gptimer.h"
gptimer_init()初始化定时器(自定义)void:无参数"gptim.h"
IRAM_ATTR将函数放入 IRAM(属性宏)放在函数声明前:bool IRAM_ATTR TimerCallback(...)"esp_attr.h"

定时器初始化流程:

gptimer_new_timer(&config, &handle); // 1. 创建定时器
gptimer_set_alarm_action(handle, &alarm); // 2. 设置报警
gptimer_register_event_callbacks(handle, &cbs, NULL); // 3. 注册回调
gptimer_enable(handle); // 4. 使能
gptimer_start(handle); // 5. 启动

定时时间计算公式:

定时周期 = alarm_count / resolution_hz
// 例: alarm_count=500000, resolution_hz=1000000 → 定时周期=500ms

本章目录#

点击条目可跳转到对应小节。

章节导航#


学习前后依赖#

方向内容
前置基础FreeRTOS 任务协作、GPIO/LCD 输出反馈和中断回调基本规范。
本章核心掌握 GPTimer 创建、分辨率、报警自动重载、IRAM 回调和标志位通知任务。
后续承接承接 09_ESPTimer 的软件高精度定时,并为 10_PWM 的周期/频率理解打基础。

1. 概述#

本章节学习 ESP32-S3 的通用定时器(General Purpose Timer),实现周期性定时任务。GPTimer 是 ESP-IDF 的新版定时器驱动,相比旧版 timer 驱动具有更好的 API 设计和性能。

为什么需要硬件定时器? FreeRTOS 自带软件定时器和 vTaskDelay 可以满足部分定时需求,但精度受限于系统 Tick(默认 10ms)。需要微秒级精确定时(如波形生成、精确采样间隔)时,就必须使用硬件定时器直接驱动计数器。


2. 定时器原理#

2.1 定时器基本概念#

硬件定时器的核心是一个由时钟源驱动的计数器。每个时钟脉冲使计数器加 1(或减 1)。当计数值达到预设的报警阈值时,触发报警事件,硬件自动执行以下动作之一:

  • 产生中断信号,调用用户注册的回调函数
  • 自动重载计数值(周期模式)或停止(单次模式)
┌─────────────────────────────────────────────┐
│ 定时器工作原理 │
├─────────────────────────────────────────────┤
│ │
│ 计数器从 0 开始计数 │
│ 每个时钟周期增加一个计数值 │
│ │
│ 计数值 ──────────────────────────────► │
│ 0 1 2 3 ... alarm_count │
│ │ │ │
│ │←── 计数增长 ──────→│ │
│ │ │
│ ▼ │
│ 触发报警(Alarm) │
│ 执行回调函数 │
│ │
│ if (auto_reload): │
│ 计数器重置为 reload_count │
│ 继续下一轮计数 │
│ │
└─────────────────────────────────────────────┘

定时器精度由两个因素决定:

  1. 时钟源精度:APB 时钟通常为 80MHz(晶体振荡器倍频而来),精度取决于晶振(一般 ±20ppm,即每百万个周期偏差 ±20 个)
  2. 分辨率(resolution_hz):决定每个 tick 的时间粒度。分辨率越高(即每个 tick 时间越短),定时精度越高,但计数范围越小

2.2 ESP32-S3 定时器资源#

┌─────────────────────────────────────────────┐
│ ESP32-S3 定时器类型 │
├─────────────────┬───────────────────────────┤
│ GPTimer (通用) │ 2 组(TIMER0, TIMER1) │
│ │ 每组有 2 个通道 │
│ │ 64-bit 计数器 │
│ │ 支持报警和自动重载 │
├─────────────────┼───────────────────────────┤
│ FreeRTOS Tick │ 系统定时器 │
│ │ 默认 100Hz (10ms周期) │
├─────────────────┼───────────────────────────┤
│ ESP Timer │ 高精度微秒级定时 │
│ │ esp_timer API │
└─────────────────┴───────────────────────────┘

定时器类型选择决策:

需求推荐方案原因
简单延时(> 10ms)vTaskDelay最简单,无需额外资源
周期任务(> 10ms)FreeRTOS Timer 或 ESP Timer软件定时器足够
微秒级精确定时GPTimer硬件计数器,精度最高
多个定时器同时运行ESP Timer不受硬件定时器数量限制
需要 ISR 级别实时性GPTimer回调直接在中断上下文执行

3. 软件架构传递树#

3.1 定时器模块结构#

┌─────────────────────────────────────────────┐
│ app_main() │
│ (main/main.c) │
└───────────────────┬─────────────────────────┘
┌───────────┼───────────┐
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ lcd_init() │ │ gptimer_init()│
│ (components/ │ │ (components/ │
│ LCD/lcd.c) │ │ GPTimer/ │
│ │ │ gptim.c) │
└───────┬───────┘ └───────┬───────┘
│ │
▼ ▼
┌───────────────┐ ┌───────────────────────┐
│ SPI 初始化 │ │ gptimer_new_timer() │
│ │ │ gptimer_set_alarm_ │
│ │ │ action() │
│ │ │ gptimer_register_ │
│ │ │ event_callbacks() │
│ │ │ gptimer_enable() │
│ │ │ gptimer_start() │
└───────────────┘ └───────────────────────┘
┌───────────────┐
│TimerCallback │
│ (ISR) │
│ flag_timer=1 │
└───────────────┘

3.2 定时器触发流程#

硬件定时器计数过程:
0 ──► 1 ──► 2 ──► ... ──► 500000 ──► Alarm!
│ │
│←───── 500ms 计数时间 ──────────────→│
┌─────────────────┐
│ TimerCallback │
│ flag_timer = 1 │
└─────────────────┘
┌─────────────────┐
│ 自动重载 │
│ count = 0 │
└─────────────────┘
0 ──► 1 ──► 2 ──► ... ──► 500000 ──► Alarm!
│←───── 新一轮 500ms ────────────────────→│

4. 代码详细解析#

4.1 定时器初始化 - gptimer_init()#

源代码(gptim.c):

void gptimer_init(void)
{
/* ---- 1. 定时器基础配置 ---- */
gptimer_config_t gptimer_structure = {
.clk_src = GPTIMER_CLK_SRC_DEFAULT, // 默认时钟源(APB)
.direction = GPTIMER_COUNT_UP, // 向上计数
.flags.intr_shared = 0, // 不共享中断
.intr_priority = 0, // 默认优先级
.resolution_hz = 1000000, // 1MHz 分辨率
};
gptimer_new_timer(&gptimer_structure, &gptimer); // 创建定时器
/* ---- 2. 报警配置 ---- */
gptimer_alarm_config_t alarm_structure = {
.alarm_count = 500000, // 报警阈值:500000 ticks
.flags.auto_reload_on_alarm = 1, // 自动重载
.reload_count = 0, // 重载值:从0开始
};
gptimer_set_alarm_action(gptimer, &alarm_structure);
/* ---- 3. 注册回调 ---- */
gptimer_event_callbacks_t callbacks_structure = {
.on_alarm = TimerCallback, // 报警回调函数
};
gptimer_register_event_callbacks(gptimer, &callbacks_structure, NULL);
/* ---- 4. 使能并启动 ---- */
gptimer_enable(gptimer); // 使能定时器模块
gptimer_start(gptimer); // 启动计数
}

4.2 配置参数详解#

定时器基础配置(gptimer_config_t):

参数说明
clk_srcGPTIMER_CLK_SRC_DEFAULTAPB 时钟(通常 80MHz)
directionGPTIMER_COUNT_UP从 0 向上计数
resolution_hz10000001MHz,即每 tick = 1us
intr_shared0不与其他设备共享中断

报警配置(gptimer_alarm_config_t):

参数说明
alarm_count500000500000 ticks = 500ms
auto_reload_on_alarm1报警后自动重载
reload_count0重载到 0

4.3 时间计算#

为什么选择 1MHz 分辨率?

  • 1MHz 意味着每 tick = 1μs,这个粒度对大多数定时需求(ms 级)已经足够精确
  • 如果设为 80MHz(不解分频),虽然精度极高,但 64 位计数器在如此高频下也会更快溢出(虽然 64 位远未到实际溢出边界)
  • 较低的分辨率让 alarm_count 值更小、更直观:500000 直接对应 500ms
分辨率计算:
resolution_hz = 1MHz = 1,000,000 Hz
每个 tick 时间 = 1 / 1MHz = 1us = 0.000001s
报警时间计算:
alarm_count = 500,000 ticks
报警周期 = 500,000 × 1us = 500ms = 0.5s
因此:
- 定时器每 500ms 触发一次报警
- 回调函数每 500ms 执行一次

4.4 定时器回调函数 - TimerCallback()#

源代码(gptim.c):

bool IRAM_ATTR TimerCallback(gptimer_handle_t timer,
const gptimer_alarm_event_data_t *edata,
void *user_ctx)
{
flag_timer = 1; // 设置标志位,通知主任务
return false; // 不需要高优先级任务唤醒
}

IRAM_ATTR 属性的技术原因:

ESP32-S3 的 Flash 使用 SPI 接口访问,当 CPU 同时操作 Flash(如写数据或擦除)时,如果 ISR 函数存放在 Flash 中,CPU 将无法读取指令——这会导致 Cache 异常。将 ISR 函数标记为 IRAM_ATTR 后,链接器会把它放入内部 SRAM,这段内存始终可访问。

┌─────────────────────────────────────────────┐
│ IRAM_ATTR 属性 │
├─────────────────────────────────────────────┤
│ │
│ 将函数放置在内部 RAM(IRAM)中 │
│ │
│ 优点: │
│ - 执行速度更快(比 Flash 快) │
│ - 即使 Flash 被禁用也能执行 │
│ │
│ 适用场景: │
│ - 中断服务程序(ISR) │
│ - 高频执行的代码 │
│ │
│ 注意: │
│ - IRAM 空间有限 │
│ - ISR 中不能调用 Flash 中的函数 │
│ │
└─────────────────────────────────────────────┘

返回值 false 的含义:GPTimer 回调可以返回 true 来请求唤醒一个高优先级任务。本项目不需要此功能,返回 false

4.5 主程序逻辑 - app_main()#

源代码(main.c):

uint8_t timer_cnt = 0 ;
void app_main(void)
{
lcd_init(); // LCD 初始化
LED_init(); // LED 初始化
gptimer_init(); // 定时器初始化
lcd_show_string(1,1,"CNT:",GREEN,BLACK);
while (1)
{
if(flag_timer == 1) // 检查定时器标志
{
timer_cnt++; // 计数增加
lcd_show_num(1,5,timer_cnt,3,YELLOW,BLACK); // LCD 显示
gpio_toggle(GPIO_NUM_38); // LED 状态翻转
flag_timer = 0; // 清除标志
}
vTaskDelay(pdMS_TO_TICKS(10)); // 让出 CPU
}
}

5. 定时器与任务协作模式#

5.1 标志位轮询模式#

这是嵌入式系统中最基础的 ISR-任务协作模式:ISR 只做标志设置,任务层轮询检查标志并执行实际业务逻辑。

定时器 ISR 与主任务的协作:
┌─────────────────────────────────────────────┐
│ ISR (TimerCallback) │
│ │
│ flag_timer = 1 │
│ │
│ ✓ 极短执行时间 │
│ ✓ 无阻塞调用 │
│ ✓ 仅设置标志 │
└───────────────────┬─────────────────────────┘
│ (异步通知)
┌─────────────────────────────────────────────┐
│ 主任务循环 │
│ │
│ if(flag_timer == 1) { │
│ // 处理定时事件 │
│ timer_cnt++; │
│ lcd_show_num(...); │
│ gpio_toggle(...); │
│ flag_timer = 0; // 清除标志 │
│ } │
│ vTaskDelay(10); // 让出 CPU │
│ │
└─────────────────────────────────────────────┘
优点:
- ISR 简短高效
- 复杂处理在任务中完成
- 避免 ISR 中的阻塞操作

标志位轮询 vs 队列/信号量通知:

方式优点缺点适用场景
标志位轮询零开销、无内存分配任务需周期性检查;可能丢失事件(若两次 ISR 间任务未检查)周期性事件、低频率事件
队列通知不会丢失事件、可携带数据需要内存分配、ISR 中需使用 xQueueSendFromISR高频事件、需要数据传递
信号量任务可阻塞等待、响应及时ISR 中需使用 xSemaphoreGiveFromISR需要即时响应的场景

本项目使用标志位轮询,因为定时周期(500ms)远大于任务检查周期(10ms),不会丢失事件。

5.2 定时器 API 调用序列#

定时器生命周期:
创建 ─────────► 配置 ─────────► 注册回调 ─────────► 使能 ─────────► 启动
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
gptimer_ gptimer_set_ gptimer_register_ gptimer_ gptimer_
new_timer() alarm_action() event_callbacks() enable() start()
│ │ │ │ │
│ │ │ │ │
│ │ │ │ ▼
│ │ │ │ 运行中...
│ │ │ │ │
│ │ │ │ ▼
│ │ │ │ Alarm!
│ │ │ │ │
│ │ │ │ ▼
│ │ │ │ TimerCallback
│ │ │ │ │
└───────────────┴───────────────┴─────────────────┴──────────────┘
自动重载
继续运行...
停止 ─────────► 禁用 ─────────► 删除
│ │ │
▼ ▼ ▼
gptimer_stop() gptimer_disable() gptimer_del_timer()

6. 时序分析#

6.1 定时器触发时序#

时间轴 ───────────────────────────────────────────────────►
时间: 0ms 500ms 1000ms 1500ms 2000ms
定时器: ──────────────────────────────────────────────
计数: 0→500000 0→500000 0→500000 0→500000
Alarm: ▲ ▲ ▲ ▲
│ │ │ │
ISR: ┃ ┃ ┃ ┃
flag_timer=1 ┃ ┃ ┃ ┃
主任务: ─────────┃─────────┃─────────┃─────────┃
检测标志: ┃ ┃ ┃ ┃
┌────────┃┐ ┌──────┃┐ ┌──────┃┐ ┌──────┃┐
│处理 │ │ │处理 │ │ │处理 │ │ │处理 │ │
│cnt++ │ │ │cnt++ │ │ │cnt++ │ │ │cnt++ │ │
│LED闪 │ │ │LED闪 │ │ │LED闪 │ │ │LED闪 │ │
│flag=0 │ │ │flag=0│ │ │flag=0│ │ │flag=0│ │
└────────┘ └───────┘ └───────┘ └───────┘
timer_cnt: 0 → 1 → 2 → 3 → 4
LED状态: ○ → ● → ○ → ● → ○

容易踩坑#

现象常见原因排查方向
定时周期不准resolution_hzalarm_count 换算错误alarm_count / resolution_hz 重新计算
回调里操作 LCD 崩溃在 ISR 上下文调用阻塞或非 IRAM 函数回调只置标志,LCD 更新放到任务中
定时器只触发一次未开启 auto_reload_on_alarm检查报警配置和重载值
编译提示回调签名不匹配GPTimer 回调返回值或参数类型写错gptimer_event_callbacks_t.on_alarm 原型修改

7. GPTimer API 总结#

7.1 核心 API 函数#

API功能说明
gptimer_new_timer()创建定时器返回定时器句柄
gptimer_set_alarm_action()设置报警配置报警值和重载
gptimer_register_event_callbacks()注册回调设置报警回调函数
gptimer_enable()使能定时器准备运行
gptimer_start()启动定时器开始计数
gptimer_stop()停止定时器暂停计数
gptimer_disable()禁用定时器释放资源
gptimer_del_timer()删除定时器完全释放

7.2 配置结构体#

gptimer_config_t(定时器配置):

typedef struct {
gptimer_clock_source_t clk_src; // 时钟源
gptimer_count_direction_t direction; // 计数方向
uint32_t resolution_hz; // 分辨率(Hz)
int intr_priority; // 中断优先级
struct {
uint32_t intr_shared; // 共享中断标志
} flags;
} gptimer_config_t;

gptimer_alarm_config_t(报警配置):

typedef struct {
uint64_t alarm_count; // 报警计数值
uint64_t reload_count; // 重载值
struct {
uint32_t auto_reload_on_alarm; // 自动重载标志
} flags;
} gptimer_alarm_config_t;

8. 学习要点总结#

8.1 核心知识点#

知识点说明
定时器分辨率1MHz = 1us/tick
报警机制计数值达到阈值触发回调
自动重载周期性定时实现方式
ISR 简化仅设置标志,处理在任务中完成
IRAM_ATTRISR 函数应放置在 IRAM

8.2 定时时间计算公式#

定时周期 = alarm_count / resolution_hz
示例:
- resolution_hz = 1MHz
- alarm_count = 500,000
- 定时周期 = 500,000 / 1,000,000 = 0.5s = 500ms
常用配置:
┌────────────────┬────────────────┬─────────────┐
│ 定时周期 │ alarm_count │ resolution │
├────────────────┼────────────────┼─────────────┤
│ 1ms │ 1,000 │ 1MHz │
│ 10ms │ 10,000 │ 1MHz │
│ 100ms │ 100,000 │ 1MHz │
│ 500ms │ 500,000 │ 1MHz │
│ 1s │ 1,000,000 │ 1MHz │
└────────────────┴────────────────┴─────────────┘

8.3 进阶方向#

  • 多定时器协同
  • 定时器捕获模式
  • PWM 生成(使用定时器)
  • 高精度计时应用

Tips#

1. 本章最易忽视的细节#

  • IRAM_ATTR 不只是优化,而是硬性要求。没有 IRAM_ATTR 的 ISR 在某些条件下(如 Flash 擦写期间)会导致 Cache 访问异常,表现为偶发性系统崩溃。调试时极难定位。
  • reload_count 不一定为 0。可以设置为非零值来产生不等周期的脉冲序列。例如 reload_count = 250000 配合 alarm_count = 500000,意味着每次报警后计数器从 250000 开始计数,触发间隔变为 (500000-250000)/1MHz = 250ms。
  • GPTimer 资源是有限的。ESP32-S3 只有 2 组 GPTimer,如果多个模块需要独立定时,建议使用 ESP Timer(软件定时器,数量不受硬件限制)。

2. 踩坑记录#

场景现象原因教训
ISR 中调 lcd_show_num系统偶尔卡死LCD 写入底层调用 SPI 驱动,SPI 传输可能阻塞等待 DMAISR 只做标志位操作
flag_timer 没有加 volatile标志位不生效编译器优化将变量缓存到寄存器,ISR 修改后任务看不到跨 ISR/任务共享的变量必须声明为 volatile
多次创建 GPTimer 不释放内存泄漏,定时器资源耗尽每次调用 gptimer_new_timer 都分配新资源创建和删除必须配对

3. GP 定时器 vs ESP 定时器 横向对比#

特性GPTimer(硬件)ESP Timer(软件)
实现方式硬件计数器,APB 时钟驱动基于硬件定时器 + 软件事件队列
回调上下文ISR 上下文(中断中执行)任务上下文 (ESP_TIMER_TASK) 或 ISR 上下文 (ESP_TIMER_ISR)
精度最高,微秒级硬件精度微秒级,但有任务调度延迟(几微秒~几十微秒)
回调限制不能调用阻塞函数、不能 printfESP_TIMER_TASK 模式下可以用 printf、延时等
资源数量有限(2组 x 2通道 = 4个)不受限(可创建大量定时器)
典型应用精确波形控制、电机步进脉冲周期数据采集、UI 刷新、状态灯
编程复杂度较复杂(需 IRAM_ATTR、标志位协作)较简单(回调就是普通函数)
CPU 占用极低(硬件自动计数)低(软件维护事件队列)

选择建议:大多数场景优先选择 ESP Timer (ESP_TIMER_TASK),开发更安全便捷。只有在需要 ISR 级别实时性(如与外部硬件同步的信号生成)时才使用 GPTimer。

4. 与后续章节的知识衔接#

  • ESP 定时器(09_ESPTimer):ESP Timer 的底层实现依赖硬件定时器,本章的 resolution_hz 概念直接对应 ESP Timer 的时间粒度。对比学习能理解「硬件定时器 → 软件定时器抽象」的层次关系。
  • PWM(10_PWM):LEDC PWM 控制器本质上是专用定时器,其工作原理(时钟→分频→计数器→比较匹配)与 GPTimer 完全一致。理解 GPTimer 有助于深入理解 PWM 的频率/分辨率约束。
  • FreeRTOS 任务通知:本章的「标志位轮询」可以升级为 FreeRTOS 的 Task Notification(xTaskNotifyFromISR),在后续中断+任务协作的高级场景中会用到。

5. 设计决策思考#

为什么回调返回 bool 而不是 void

GPTimer 的回调签名设计为返回 bool,这允许 ISR 主动请求唤醒一个高优先级任务。如果回调返回 true,驱动层会在 ISR 退出前触发一次任务调度,确保高优任务能立即响应定时事件。这个设计比传统的「ISR 设标志 + 任务轮询」多了一层调度保证——对于硬实时场景(如电机控制环路),减少调度延迟至关重要。本项目返回 false 是因为 500ms 周期对响应延迟不敏感。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

ESP32-S3 通用定时器(GPTimer)学习笔记
https://mjzy.tech/posts/embedded/esp32-s3/03-gp-timer/
作者
ENKIDU
发布于
2026-08-15
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
ENKIDU
深度学习 & 计算机底层原理 | 用代码理解世界
低语
天之锁永系天与地,而这里系着文字与记忆。欢迎来到乌鲁克的数字荒原。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
49
分类
6
标签
45
总字数
180,771
运行时长
0
最后活动
0 天前
站点信息
构建平台
Vercel
文章许可
CC BY-NC-SA 4.0

文章目录