ESP32以及ESP32C3的定时器学习

本文介绍了如何使用ESP32的定时器功能在ArduinoIDE中实现周期性中断控制LED亮灭,包括设置定时器、预分频、警报事件和自定义中断处理。教程还展示了如何调整定时器频率以实现不同周期的LED控制效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

参考教程:ESP32 Timers & Timer Interrupt Tutorial (Arduino IDE) – DeepBlue

这个教程中有两个实验

1.通过定时器产生一个周期性的中断,来实现一个翻转信号控制一个LED的亮灭

2.通过定时器测量两个外部事件之间的时间。

ESP32 定时器

ESP32系列有4个硬件定时器,每个定时器是一个64位,向上/向下的计数器,并带有一个16位的分频器。ESP32C3只有2个定时器,每个定时器是54位。(对比STM32的定时器是16位的)同时ESP32的定时器在最后一个技术周期可以配置自动装载。

ESP32定时器功能描述

每个ESP32定时器都是使用APB时钟(一般是80MHz)作为基础时钟。这时钟可以被一个16位的分频器进行分频后作为 基本的tick 时间。这样我们就可以通过控制分频系数来实现不同的tick time。

16位的预分频器可以把APB_CLK除以2到65536的系数。如果设置为1或者2的话,系数位2.如果设置0的话,分频系数位65536;

ESP32 定时器告警生成

ESP32定时器可以触发警报事件,根据你的配置,决定定时器进行重载或者进行中断。当你保存在警报寄存器与当前寄存器的值相同的时候,就会触发警报。这个在接下来的项目中执行周期性的逻辑非常有用。

ESP32 定时器公式

我们将使用告警事件生成以及定时器的预分频器来实现所需的中断周期性。下面的定时器方程有 3 种特殊情况,分别是预分频器值 = 0、1 和 2 时。当 Prescaler=1or2 时,它将如下所示 [T外= 计时器滴答 x (2/APB_CLK)].当 Prescaler=0 时,它将如下所示 [T外= 计时器滴答 x (65536/APB_CLK)].否则,我们通常可以使用下面的等式。

ESP32 定时器示例 (Arduino)

假设我们希望不使用delay函数来阻塞CPU进而影响系统的时序性能的前提下,每1ms切换一次LED。根据上面的公式。APB默认为80MHz。所需的周期为1ms,因此我们可以设Prescaler=80;

根据这个公式,容易算出TimerTicks为1000;

下面是实现的代码:(乐鑫已经把这个代码的一些函数已经修改了)

#define LED 21
 
hw_timer_t *Timer0_Cfg = NULL;
 
void IRAM_ATTR Timer0_ISR()
{
    digitalWrite(LED, !digitalRead(LED));
}
void setup()
{
    pinMode(LED, OUTPUT);
    Timer0_Cfg = timerBegin(0, 80, true);
    timerAttachInterrupt(Timer0_Cfg, &Timer0_ISR, true);
    timerAlarmWrite(Timer0_Cfg, 1000, true);
    timerAlarmEnable(Timer0_Cfg);
}
void loop()
{
    // Do Nothing!
}

这个是调整过的程序

#define LED 4
 
hw_timer_t *Timer0_Cfg = NULL;
 
void IRAM_ATTR Timer0_ISR()
{
    digitalWrite(LED, !digitalRead(LED));
}
void setup()
{
    pinMode(LED, OUTPUT);
    Timer0_Cfg = timerBegin(1000000);
    timerAttachInterrupt(Timer0_Cfg, &Timer0_ISR);
    timerAlarm(Timer0_Cfg, 1000, true,0);
    timerStart(Timer0_Cfg);
}
void loop()
{
    // Do Nothing!
}

timerBegin()可以直接设置频率,这样就不需要自己去算分频系数了,这里写的1MHz。

timerAttachInterrupt()也只需要两个参数就可以了。

timerAlarmWrite()改为timerAlarm()前面三个参数好理解,第四个参数是重载的次数,比如写10,那么只进行10次重载,然后就结束了。

void timerAlarm(hw_timer_t * timer, uint64_t alarm_value, bool autoreload, uint64_t reload_count);
  • timer timer struct.

  • alarm_value alarm value to generate event.

  • autoreload enabled/disabled autorealod.

  • reload_count number of autoreloads (0 = unlimited). Has no effect if autorealod is disabled.

timerAlarmEnable()函数被改成了timerStart()

实际测试timerAlarm()第四个参数,修改成其他值使用没有影响还是能正常一直输出方波;

实际屏蔽测试ttimerStart()函数,也还是能输出方波。

实测输出方波频率为499.99Hz,还是非常准的。

可以在回调函数中把频率进行修改,这样就实现了扫频的功能,这里把计数修改成1000到10000;

1000对应的是1ms,对应周期为2ms,频率为500Hz;那么10000对应的是10ms,对应周期为20ms,对应频率为50Hz。测试可以实现效果

#define LED 4
 
hw_timer_t *Timer0_Cfg = NULL;
int fre=1000;//计数
void IRAM_ATTR Timer0_ISR()
{
    digitalWrite(LED, !digitalRead(LED));
    fre++;
    if(fre == 10000){
      fre=1000;
    }
    timerAlarm(Timer0_Cfg, fre, true,0);
}
void setup()
{
    pinMode(LED, OUTPUT);
    Timer0_Cfg = timerBegin(1000000);
    timerAttachInterrupt(Timer0_Cfg, &Timer0_ISR);
    timerAlarm(Timer0_Cfg, fre, true,0);
    // timerStart(Timer0_Cfg);
}
void loop()
{
    // Do Nothing!
}

为了效果更加明显,把频率提高,把1000缩短到100;频率从500Hz提高到5000Hz。

由于变化太快,所以这里修改每个频率进行10次的输出。

这是修改后的程序,非常舒服了。

#define LED 4

hw_timer_t *Timer0_Cfg = NULL;
int fre = 100;  //计数
int cnt = 0;
void IRAM_ATTR Timer0_ISR() {
  digitalWrite(LED, !digitalRead(LED));
  cnt++;
  if (cnt == 10) {  //每个频率输出10次
    cnt = 0;
    fre++;
    if (fre == 1000) {
      fre = 1000;
    }
  }


  timerAlarm(Timer0_Cfg, fre, true, 0);
}
void setup() {
  pinMode(LED, OUTPUT);
  Timer0_Cfg = timerBegin(1000000);
  timerAttachInterrupt(Timer0_Cfg, &Timer0_ISR);
  timerAlarm(Timer0_Cfg, fre, true, 0);
  // timerStart(Timer0_Cfg);
}
void loop() {
  // Do Nothing!
}

### ESP32-C3 定时器中断使用教程 #### 初始化定时器 为了初始化ESP32-C3上的硬件定时器,需先创建一个定时器实例并配置其参数。这可以通过调用`timer_init()`函数完成,在此过程中指定定时器编号、模式(一次性或周期性)、自动重载标志以及其他属性。 ```c #include "driver/timer.h" // 创建定时器句柄 TimerHandle_t timer; void setup_timer() { const timer_config_t config = { .divider = 80, // 设置分频因子 .counter_dir = TIMER_COUNT_UP, .counter_en = TIMER_PAUSE, .alarm_en = TIMER_ALARM_EN, .auto_reload = true // 自动重装载使能 }; timer_init(TIMER_GROUP_0, TIMER_0, &config); // 初始化定时器[^3] } ``` #### 注册中断服务程序 一旦定时器被成功初始化,则需要注册相应的ISR(Interrupt Service Routine),以便当发生特定事件如溢出或比较匹配时执行某些动作。这里采用的是安装一个专用的中断处理器,并传递给它必要的上下文信息。 ```c static void IRAM_ATTR on_timer_alarm(void* arg) { uint64_t counter_value; timer_get_counter_value(TIMER_GROUP_0, TIMER_0, &counter_value); printf("Alarm triggered at %llu\n", (unsigned long long)counter_value); // 清除报警标志位 timer_clear_intr_status_in_isr(TIMER_GROUP_0, TIMER_0); } void attach_interrupt_service_routine() { timer_set_alarm_value(TIMER_GROUP_0, TIMER_0, 10 * 1000 * 1000ULL); // 设定警报时间戳 timer_enable_intr(TIMER_GROUP_0, TIMER_0); // 启用中断 timer_register_irq_handler(on_timer_alarm, NULL); // 注册中断处理函数[^5] } ``` #### 开启与停止定时器 启动和暂停定时器的操作分别由`timer_start()`和`timer_pause()`这两个API提供支持。前者会激活计数过程;后者则会使当前运行中的计数暂时停滞下来而不改变内部状态。 ```c void start_stop_timer(bool shouldStart) { if(shouldStart){ timer_start(TIMER_GROUP_0, TIMER_0); // 启动定时器 }else{ timer_pause(TIMER_GROUP_0, TIMER_0); // 暂停定时器 } } ``` #### 解决常见问题 - **精度不足**: 如果发现实际间隔时间和预期不符,可能是因为选择了不合适的分频系数或是存在其他干扰源影响到了系统的稳定性。尝试调整分频比例或将关键代码段标记为原子操作以减少误差。 - **资源冲突**: 当多个外设共享同一个定时器单元时可能会引发竞争条件。确保每次只允许单一设备独占访问该资源,并且在多任务环境中妥善安排优先级顺序。 - **性能瓶颈**: 对于高频率的任务调度场景来说,频繁触发中断可能导致CPU负载过高从而降低整体效率。优化策略包括但不限于增加缓冲区大小、合并相邻的小型事务成批处理等方式减轻负担。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值