ESP32-S3 ESP Timer 项目笔记
ESP32-S3 ESP Timer 项目笔记
本章重要 API 函数
| API | 功能 | 参数说明 | 头文件 |
|---|---|---|---|
esp_timer_create() | 创建 ESP 高精度定时器 | const esp_timer_create_args_t *create_args:定时器配置结构体指针esp_timer_handle_t *out_handle:返回定时器句柄 | "esp_timer.h" |
esp_timer_start_periodic() | 启动周期性定时器 | esp_timer_handle_t timer:定时器句柄uint64_t period_us:周期时间(微秒) | "esp_timer.h" |
esp_timer_start_once() | 启动一次性定时器 | esp_timer_handle_t timer:定时器句柄uint64_t timeout_us:超时时间(微秒) | "esp_timer.h" |
esp_timer_stop() | 停止定时器 | esp_timer_handle_t timer:定时器句柄 | "esp_timer.h" |
esp_timer_delete() | 删除定时器释放资源 | esp_timer_handle_t timer:定时器句柄 | "esp_timer.h" |
esp_timer_get_time() | 获取当前时间(微秒) | void:无参数,返回 int64_t 微秒值 | "esp_timer.h" |
esp_timer_dump() | 打印所有定时器状态 | FILE *stream:输出流(调试用) | "esp_timer.h" |
esptim_init() | 初始化定时器(自定义封装) | void:无参数 | "esptim.h" |
定时器配置结构体 esp_timer_create_args_t:
| 字段 | 说明 |
|---|---|
.callback | 回调函数指针,定时超时后执行 |
.arg | 传递给回调函数的用户参数 |
.dispatch_method | ESP_TIMER_TASK(任务上下文)或 ESP_TIMER_ISR(中断上下文) |
.name | 定时器名称,用于调试 |
.skip_unhandled_events | true 跳过积压事件,避免回调堆积 |
本章目录
点击条目可跳转到对应小节。
章节导航
- 本章重要 API 函数
- 学习前后依赖
- 一、项目概述
- 二、硬件连接
- 三、项目结构
- 四、ESP Timer 原理
- 五、关键代码解析
- 六、ESP Timer API 详细说明
- 七、定时器调度方式对比
- 八、项目运行效果
- 九、调试与日志
- 十、常见问题与解决
- 十一、扩展应用
- 十二、参考资料
- 容易踩坑
- 十三、总结
- Tips
学习前后依赖
| 方向 | 内容 |
|---|---|
| 前置基础 | GPTimer 的硬件定时概念、微秒分辨率、回调与任务协作边界。 |
| 本章核心 | 掌握 ESP Timer 软件高精度定时器、周期/一次性启动和任务上下文回调。 |
| 后续承接 | 承接 10_PWM 的周期控制、定时刷新和后续传感器周期采样场景。 |
一、项目概述
本项目演示了 ESP32-S3 如何使用 ESP-IDF 的高精度定时器(ESP Timer)实现周期性任务调度。主要功能包括:
- 定时器周期性触发回调函数(500ms 间隔)
- LCD 显示计数器值
- LED 状态翻转(GPIO 38)
ESP Timer 的定位:ESP Timer 是建立在硬件定时器之上的软件抽象层。它内部使用一个硬件定时器作为时基,然后维护一个软件事件队列来管理多个虚拟定时器。这样既保留了微秒级精度,又突破了硬件定时器数量限制。
二、硬件连接
| 引脚 | 功能 | 说明 |
|---|---|---|
| GPIO 38 | LED | 定时器控制闪烁 |
| GPIO 21 | LCD_RST | LCD 复位信号 |
| GPIO 47 | LCD_DC | LCD 数据/命令选择 |
| GPIO 48 | LCD_CS | LCD 片选信号 |
| GPIO 40 | LCD_BLK | LCD 背光控制 |
三、项目结构
09_ESPTimer/├── main/│ └── main.c # 主程序入口├── components/│ ├── ESPTIM/│ │ ├── esptim.c # 定时器驱动实现│ │ └── esptim.h # 定时器驱动头文件│ ├── LED/│ │ ├── LED.c # LED 驱动│ │ └── LED.h # LED 驱动头文件│ ├── LCD/│ │ ├── lcd.c # LCD 驱动│ │ ├── lcd.h # LCD 驱动头文件│ │ └ lcdfont.h # LCD 字库│ └── SPI/│ ├── spi.c # SPI 驱动│ └ spi.h # SPI 驱动头文件└── notes.md # 项目笔记四、ESP Timer 原理
4.1 ESP Timer 特点
ESP Timer 是 ESP-IDF 提供的高精度软件定时器,具有以下特点:
- 高精度:微秒级分辨率(μs)
- 52 位范围:支持长时间定时
- 两种调度方式:
ESP_TIMER_TASK:在定时器任务上下文执行回调(推荐)ESP_TIMER_ISR:在中断上下文执行回调(高实时性)
- 两种模式:
- 一次性定时器(
esp_timer_start_once) - 周期性定时器(
esp_timer_start_periodic)
- 一次性定时器(
4.2 ESP Timer vs FreeRTOS Timer
| 特性 | ESP Timer | FreeRTOS Timer |
|---|---|---|
| 分辨率 | 微秒级(μs) | RTOS Tick 级(通常 1ms) |
| 回调优先级 | 高优先级任务 | 低优先级守护任务 |
| 实时性 | 高 | 较低 |
| 适用场景 | 高精度定时 | 一般定时任务 |
4.3 定时器工作流程
┌─────────────────────────────────────────────────────────────────┐│ ESP Timer 工作流程 │└─────────────────────────────────────────────────────────────────┘
1. 创建定时器 ┌─────────────────┐ │ esp_timer_create│ │ - 配置参数 │ │ - 回调函数 │ │ - 调度方式 │ └─────────────────┘ │ ▼2. 启动定时器 ┌──────────────────────┐ │esp_timer_start_periodic│ │ - 设置周期(500ms) │ └──────────────────────┘ │ ▼3. 定时器运行 ┌────────────────────┐ │ 硬件定时器计数 │ │ - 52位计数器 │ │ - 微秒级精度 │ └────────────────────┘ │ │ 每 500ms ▼4. 触发回调 ┌────────────────────┐ │ esptimer_callback │ │ - 计数器递增 │ │ - LCD 显示更新 │ │ - LED 状态翻转 │ └────────────────────┘ │ ▼5. 循环执行 ┌────────────────────┐ │ 定时器持续运行 │ │ - 周期性触发 │ │ - 直到手动停止 │ └────────────────────┘五、关键代码解析
5.1 定时器初始化流程
void esptim_init(void){ /* 步骤1:定义定时器句柄 */ esp_timer_handle_t esptim;
/* 步骤2:配置定时器参数 */ esp_timer_create_args_t esptim_cfg = { .arg = NULL, // 回调参数 .callback = &esptimer_callback, // 回调函数 .dispatch_method = ESP_TIMER_TASK, // 任务调度方式 .name = "mytim", // 定时器名称 .skip_unhandled_events = true, // 跳过未处理事件 };
/* 步骤3:创建定时器 */ esp_timer_create(&esptim_cfg, &esptim);
/* 步骤4:启动周期性定时器 */ esp_timer_start_periodic(esptim, 500000); // 500ms = 500000μs}配置参数说明:
| 参数 | 说明 |
|---|---|
arg | 回调函数参数,可传递自定义数据 |
callback | 回调函数指针,定时器触发时执行 |
dispatch_method | 调度方式:ESP_TIMER_TASK 或 ESP_TIMER_ISR |
name | 定时器名称,用于调试和日志 |
skip_unhandled_events | 跳过堆积的事件,避免回调过载 |
5.2 定时器回调函数
void esptimer_callback(void *arg){ lcd_cnt++; // 计数器递增 lcd_show_num(1, 5, lcd_cnt, 4, YELLOW, BLACK); // LCD 显示 gpio_toggle(GPIO_NUM_38); // LED 状态翻转}回调函数注意事项:
⚠️ 重要警告:
- 回调函数在定时器任务上下文执行,应避免耗时操作
- 不应执行阻塞操作(如
vTaskDelay、互斥锁等待) - 不应调用可能阻塞的函数(如大量日志输出)
- 建议只做最小必要工作,复杂任务通过队列传递给其他任务
5.3 主程序流程
void app_main(void){ /* 步骤1:初始化外设 */ lcd_init(); // LCD 初始化 LED_init(); // LED 初始化 esptim_init(); // 定时器初始化
/* 步骤2:LCD 显示标签 */ lcd_show_string(1, 1, "CNT:", RED, BLACK);
/* 步骤3:主循环保持运行 */ while(1) { vTaskDelay(pdMS_TO_TICKS(500)); }}六、ESP Timer API 详细说明
6.1 核心 API 函数
| 函数 | 功能 | 参数 | 返回值 |
|---|---|---|---|
esp_timer_create() | 创建定时器 | 配置参数、句柄指针 | esp_err_t |
esp_timer_start_periodic() | 启动周期性定时器 | 定时器句柄、周期(μs) | esp_err_t |
esp_timer_start_once() | 启动一次性定时器 | 定时器句柄、超时(μs) | esp_err_t |
esp_timer_stop() | 停止定时器 | 定时器句柄 | esp_err_t |
esp_timer_delete() | 删除定时器 | 定时器句柄 | esp_err_t |
6.2 时间转换
微秒转换公式:
1 秒 = 1,000,000 微秒(μs)1 毫秒 = 1,000 微秒(μs)常用定时周期:
| 定时周期 | 微秒值 | 应用场景 |
|---|---|---|
| 1ms | 1000 | 快速采样 |
| 10ms | 10000 | 状态检测 |
| 100ms | 100000 | 中速任务 |
| 500ms | 500000 | 本项目 |
| 1s | 1000000 | 慢速任务 |
6.3 最小周期限制
⚠️ ESP Timer 限制:
- 一次性定时器:最小超时 20μs
- 周期性定时器:最小周期 50μs
- 过小的周期会消耗大量 CPU 时间
七、定时器调度方式对比
7.1 ESP_TIMER_TASK 方式(本项目使用)
┌─────────────────────────────────────────┐│ ESP_TIMER_TASK 调度方式 │└─────────────────────────────────────────┘
硬件定时器中断 │ ▼定时器任务(高优先级) │ ▼回调函数执行 │ ▼返回定时器任务
优点:- 不占用中断时间- 可执行较复杂操作- 不影响其他中断响应
缺点:- 实时性略低- 可能被更高优先级任务抢占7.2 ESP_TIMER_ISR 方式
┌─────────────────────────────────────────┐│ ESP_TIMER_ISR 调度方式 │└─────────────────────────────────────────┘
硬件定时器中断 │ ▼中断上下文直接执行回调 │ ▼中断返回
优点:- 实时性最高- 延迟最小
缺点:- 占用中断时间- 只能执行简单操作- 不能调用阻塞函数八、项目运行效果
8.1 程序执行时序
时间轴:0ms ──► 500ms ──► 1000ms ──► 1500ms ──► 2000ms ...
LCD 显示:┌────────────┐│ CNT: 0000 │ 0ms│ CNT: 0001 │ 500ms│ CNT: 0002 │ 1000ms│ CNT: 0003 │ 1500ms│ CNT: 0004 │ 2000ms└────────────┘
LED 状态:GPIO 38: ○ ──► ● ──► ○ ──► ● ──► ○ ... 0ms 500ms 1000ms 1500ms 2000ms8.2 系统资源占用
- 定时器任务:占用一个高优先级 FreeRTOS 任务
- 内存占用:约几十字节(定时器结构体)
- CPU 占用:每 500ms 执行一次回调,占用极低
九、调试与日志
9.1 查看定时器状态
ESP-IDF 提供定时器调试命令:
# 查看所有定时器esp_timer_dump()
# 获取当前时间esp_timer_get_time()9.2 日志输出示例
I (247) cpu_start: Starting app cpu, entry point is 0x40375488I (300) esptim: Timer 'mytim' createdI (350) esptim: Timer started with period 500000us十、常见问题与解决
10.1 编译错误:函数未定义
错误信息:
error: implicit declaration of function 'esp_timer_start_periodic'原因: 未包含头文件
解决:
#include "esp_timer.h"10.2 回调函数执行时间过长
现象: 定时器触发不稳定,其他任务响应延迟
解决:
- 简化回调函数,只做必要操作
- 使用队列将复杂任务传递给其他任务
- 考虑使用
ESP_TIMER_ISR方式
10.3 定时器周期设置错误
错误:
esp_timer_start_periodic(esptim, 500); // 错误:500μs = 0.5ms正确:
esp_timer_start_periodic(esptim, 500000); // 正确:500000μs = 500ms十一、扩展应用
11.1 多定时器管理
/* 定义多个定时器 */esp_timer_handle_t tim1, tim2, tim3;
/* 创建并启动多个定时器 */esp_timer_create(&cfg1, &tim1);esp_timer_create(&cfg2, &tim2);esp_timer_create(&cfg3, &tim3);
esp_timer_start_periodic(tim1, 100000); // 100msesp_timer_start_periodic(tim2, 500000); // 500msesp_timer_start_once(tim3, 2000000); // 2s 一次性11.2 动态修改定时器周期
/* 停止定时器 */esp_timer_stop(esptim);
/* 重新启动,修改周期 */esp_timer_start_periodic(esptim, 1000000); // 改为 1s11.3 传递回调参数
/* 定义参数结构体 */typedef struct { int id; char *name;} timer_arg_t;
timer_arg_t my_arg = {1, "timer1"};
/* 配置定时器 */esp_timer_create_args_t cfg = { .arg = &my_arg, // 传递参数 .callback = &my_callback, ...};
/* 回调函数中使用参数 */void my_callback(void *arg){ timer_arg_t *p = (timer_arg_t *)arg; printf("Timer %d: %s\n", p->id, p->name);}十二、参考资料
12.1 官方文档
12.2 相关示例
- ESP-IDF 示例:
examples/system/esp_timer - FreeRTOS 定时器对比:
examples/freertos/timers
12.3 API 头文件
#include "esp_timer.h" // ESP Timer API#include "esp_err.h" // 错误码定义容易踩坑
| 现象 | 常见原因 | 排查方向 |
|---|---|---|
| 周期比预期快 1000 倍 | 把毫秒当微秒传给 esp_timer_start_periodic() | 500ms 应写 500000,统一标注单位 |
| 回调堆积导致卡顿 | 回调耗时超过定时周期 | 启用 skip_unhandled_events 并把重任务移出回调 |
| 在回调中阻塞异常 | ESP Timer 回调上下文不适合等待锁/延时 | 回调只发队列/置标志,任务中处理 |
| 停止/删除失败 | 句柄作用域丢失或重复操作同一定时器 | 保存全局/静态句柄,按 stop → delete 顺序释放 |
十三、总结
本项目成功实现了基于 ESP Timer 的周期性任务调度,具有以下特点:
✅ 优点:
- 高精度定时(微秒级)
- 简单易用的 API
- 低 CPU 占用
- 稳定可靠的定时
⚠️ 注意事项:
- 回调函数应简洁高效
- 周期设置要合理(最小 50μs)
- 正确使用周期性/一次性定时器
🎯 应用场景:
- 传感器周期采样
- 状态指示灯控制
- 定时数据上报
- 周期性任务调度
Tips
1. 本章最易忽视的细节
- 微秒单位陷阱:ESP Timer 所有时间参数单位都是微秒,不是毫秒。这是初学者最容易犯的错——写 500 以为是 500ms,实际是 0.5ms。建议在代码中添加注释明确单位。
skip_unhandled_events的作用:如果回调执行时间超过定时周期(例如周期 100ms 但回调执行了 200ms),没有skip_unhandled_events时,积压的定时事件会逐个执行,形成「回调雪崩」。开启此选项后,积压事件会被自动丢弃,只保留最新的一次。- 句柄生命周期:
esp_timer_create返回的句柄需要保持在有效作用域内(全局或静态变量)。如果句柄是局部变量,函数返回后句柄失效,后续的stop/delete会导致未定义行为。
2. 踩坑记录
| 场景 | 现象 | 原因 | 教训 |
|---|---|---|---|
ESP_TIMER_TASK 回调中调 vTaskDelay | 不会有编译警告,但逻辑错误 | vTaskDelay 阻塞的是定时器任务本身,导致后续延时的定时器也被延迟 | 区分「当前代码在任务上下文」和「当前任务是否可以被阻塞」 |
| 回调中访问被 FreeRTOS 保护的数据 | 偶发数据损坏 | 虽然 ESP_TIMER_TASK 是任务上下文,但它是独立高优先级任务,与其他任务的共享数据仍需互斥保护 | 定时器回调是一个独立任务,与其他任务的并发问题依然存在 |
| 忘记 delete 直接 stop | 内存泄漏 | esp_timer_stop 只停止计数,资源未释放 | 不再使用的定时器必须 delete |
3. GP 定时器 vs ESP 定时器 横向对比
| 特性 | GPTimer(硬件) | ESP Timer(软件) |
|---|---|---|
| 实现方式 | 硬件计数器,APB 时钟驱动 | 基于硬件定时器 + 软件事件队列 |
| 回调上下文 | ISR 上下文(中断中执行) | 任务上下文 (ESP_TIMER_TASK) 或 ISR 上下文 (ESP_TIMER_ISR) |
| 精度 | 最高,微秒级硬件精度 | 微秒级,但有任务调度延迟(几微秒~几十微秒) |
| 回调限制 | 不能 printf、不能阻塞、需要 IRAM_ATTR | ESP_TIMER_TASK 模式下可以用 printf、SPI 通信等 |
| 资源数量 | 有限(2组 x 2通道 = 4个) | 不受限(可创建大量定时器,内存允许) |
| API 复杂度 | 较复杂(需 IRAM_ATTR、标志位协作) | 较简单(回调就是普通函数) |
| CPU 占用 | 极低(硬件自动计数) | 低(软件维护事件队列,微秒级开销) |
| 时间单位 | Hz(分辨率)+ 计数值,需换算 | 直接以微秒为单位,直观 |
| 典型应用 | 精确波形控制、电机步进脉冲 | 周期数据采集、UI 刷新、状态灯、大多数定时需求 |
选择建议:
- 默认首选 ESP Timer +
ESP_TIMER_TASK——开发简单、够用、安全 - 只有需要 ISR 级别实时性(如与外部硬件信号严格同步)时才用 GPTimer
- 极端实时场景(如高速 ADC 触发)使用 ESP Timer +
ESP_TIMER_ISR
4. 与后续章节的知识衔接
- PWM 信号生成(10_PWM):ESP Timer 常用于动态调节 PWM 参数(如呼吸灯效果的时间基准),也可以在定时器回调中调用
ledc_set_duty实现平滑渐变 - ADC 周期采样(12_ADCContinuous):ADC 连续采样模式需要定时触发,ESP Timer 是其中最简洁的定时源——在回调中读取 ADC 值并更新 LCD
- FreeRTOS 任务通知:本章的
main函数主循环只是vTaskDelay(500),所有工作都在定时器回调中完成。在更复杂的场景中,可以在回调中通过xTaskNotifyGive通知主任务处理
5. 设计决策思考
为什么不直接用 FreeRTOS 的软件定时器?
FreeRTOS 的软件定时器分辨率受限于 configTICK_RATE_HZ(通常 100Hz = 10ms)。如果系统 Tick 是 10ms,任何定时精度都不能高于 10ms。而 ESP Timer 基于硬件定时器(微秒级),不受此限制。
此外,FreeRTOS 的软件定时器回调运行在「定时器守护任务」中,优先级通常很低,不能保证及时响应。ESP Timer 的回调运行在专门的高优先级任务中,延迟更可控。
为什么大多数场景推荐 ESP_TIMER_TASK 而非 ESP_TIMER_ISR?
在 FreeRTOS 环境中,优先级反转是个潜在问题。如果 ESP_TIMER_ISR 回调中使用了某个被低优先级任务持有的锁,就可能导致死锁——ISR 不能被抢占,低优任务无法运行释放锁。
ESP_TIMER_TASK 方式下,回调是普通任务上下文,FreeRTOS 的优先级继承机制可以正常工作。只有当你需要确保定时精度不因任务调度而偏移时(如精确到单个微秒的信号生成),才应使用 ESP_TIMER_ISR。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!