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

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

4072 字
20 分钟
ESP32-S3 通用定时器(GPTimer)学习笔记
StarryLin/Geek · 摘要

本文介绍ESP32-S3通用定时器API与初始化流程,说明定时周期计算、IRAM回调及标志位轮询用法,并与ESP Timer比较适用场景。

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)。当计数值达到预设的报警阈值时,触发报警事件,硬件自动执行以下动作之一:

  • 产生中断信号,调用用户注册的回调函数
  • 自动重载计数值(周期模式)或停止(单次模式)
flowchart TD A["计数器从 0 开始计数<br/>每个时钟周期增加一个计数值"] A --> B["计数值: 0→1→2→3→...→alarm_count"] B --> C["触发报警(Alarm)<br/>执行回调函数"] C --> D{"auto_reload?"} D -->|"是"| E["计数器重置为 reload_count<br/>继续下一轮计数"] E --> A

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

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

2.2 ESP32-S3 定时器资源#

flowchart TD A["ESP32-S3 定时器类型"] A --> B["GPTimer(通用)<br/>2 组(TIMER0, TIMER1)<br/>每组有 2 个通道<br/>64-bit 计数器<br/>支持报警和自动重载"] A --> C["FreeRTOS Tick<br/>系统定时器<br/>默认 100Hz(10ms周期)"] A --> D["ESP Timer<br/>高精度微秒级定时<br/>esp_timer API"]

定时器类型选择决策:

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

3. 软件架构传递树#

3.1 定时器模块结构#

flowchart TD A["app_main()<br/>(main/main.c)"] A --> B["lcd_init()<br/>(components/LCD/lcd.c)"] A --> C["gptimer_init()<br/>(components/GPTimer/gptim.c)"] B --> D["SPI 初始化"] C --> E["gptimer_new_timer()<br/>gptimer_set_alarm_action()<br/>gptimer_register_event_callbacks()<br/>gptimer_enable()<br/>gptimer_start()"] E --> F["TimerCallback<br/>(ISR)<br/>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,这段内存始终可访问。

flowchart TD A["IRAM_ATTR 属性<br/>将函数放置在内部 RAM(IRAM)中"] A --> B["优点<br/>- 执行速度更快(比 Flash 快)<br/>- 即使 Flash 被禁用也能执行"] A --> C["适用场景<br/>- 中断服务程序(ISR)<br/>- 高频执行的代码"] A --> D["注意<br/>- IRAM 空间有限<br/>- 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 只做标志设置,任务层轮询检查标志并执行实际业务逻辑。

flowchart TD A["ISR(TimerCallback)<br/>flag_timer = 1<br/>✓ 极短执行时间<br/>✓ 无阻塞调用<br/>✓ 仅设置标志"] A -->|"异步通知"| B["主任务循环<br/>if(flag_timer == 1) {<br/> timer_cnt++;<br/> lcd_show_num(...);<br/> gpio_toggle(...);<br/> flag_timer = 0;<br/>}<br/>vTaskDelay(10);"]

优点:

  • 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_hz 与 alarm_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

常用配置:

flowchart TD subgraph T["常用配置(resolution=1MHz)"] C1["1ms → alarm_count=1,000"] C2["10ms → alarm_count=10,000"] C3["100ms → alarm_count=100,000"] C4["500ms → alarm_count=500,000"] C5["1s → alarm_count=1,000,000"] end

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/
作者
JIA
发布于
2026-08-15
许可协议
CC BY-NC-SA 4.0

评论区

正在加载 GitHub 评论…

Tips · 随手记
仅自己可见 · 每篇文章单独保存

文章目录