ESP32-S3 ESP Timer 项目笔记

4094 字
20 分钟
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_methodESP_TIMER_TASK(任务上下文)或 ESP_TIMER_ISR(中断上下文)
.name定时器名称,用于调试
.skip_unhandled_eventstrue 跳过积压事件,避免回调堆积

本章目录#

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

章节导航#


学习前后依赖#

方向内容
前置基础GPTimer 的硬件定时概念、微秒分辨率、回调与任务协作边界。
本章核心掌握 ESP Timer 软件高精度定时器、周期/一次性启动和任务上下文回调。
后续承接承接 10_PWM 的周期控制、定时刷新和后续传感器周期采样场景。

一、项目概述#

本项目演示了 ESP32-S3 如何使用 ESP-IDF 的高精度定时器(ESP Timer)实现周期性任务调度。主要功能包括:

  • 定时器周期性触发回调函数(500ms 间隔)
  • LCD 显示计数器值
  • LED 状态翻转(GPIO 38)

ESP Timer 的定位:ESP Timer 是建立在硬件定时器之上的软件抽象层。它内部使用一个硬件定时器作为时基,然后维护一个软件事件队列来管理多个虚拟定时器。这样既保留了微秒级精度,又突破了硬件定时器数量限制。

二、硬件连接#

引脚功能说明
GPIO 38LED定时器控制闪烁
GPIO 21LCD_RSTLCD 复位信号
GPIO 47LCD_DCLCD 数据/命令选择
GPIO 48LCD_CSLCD 片选信号
GPIO 40LCD_BLKLCD 背光控制

三、项目结构#

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 TimerFreeRTOS 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_TASKESP_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)

常用定时周期:

定时周期微秒值应用场景
1ms1000快速采样
10ms10000状态检测
100ms100000中速任务
500ms500000本项目
1s1000000慢速任务

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 2000ms

8.2 系统资源占用#

  • 定时器任务:占用一个高优先级 FreeRTOS 任务
  • 内存占用:约几十字节(定时器结构体)
  • CPU 占用:每 500ms 执行一次回调,占用极低

九、调试与日志#

9.1 查看定时器状态#

ESP-IDF 提供定时器调试命令:

Terminal window
# 查看所有定时器
esp_timer_dump()
# 获取当前时间
esp_timer_get_time()

9.2 日志输出示例#

I (247) cpu_start: Starting app cpu, entry point is 0x40375488
I (300) esptim: Timer 'mytim' created
I (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); // 100ms
esp_timer_start_periodic(tim2, 500000); // 500ms
esp_timer_start_once(tim3, 2000000); // 2s 一次性

11.2 动态修改定时器周期#

/* 停止定时器 */
esp_timer_stop(esptim);
/* 重新启动,修改周期 */
esp_timer_start_periodic(esptim, 1000000); // 改为 1s

11.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_ATTRESP_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


文章分享

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

ESP32-S3 ESP Timer 项目笔记
https://mjzy.tech/posts/embedded/esp32-s3/04-esp-timer/
作者
ENKIDU
发布于
2026-08-16
许可协议
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

文章目录