3. 系统延时
延时功能在计算机编程中十分常见,它可以暂停程序的执行一段时间,以实现各种应用需求。
3.1 延时的作用
在ESP32S3或其他微控制器的编程中,延时被广泛使用,主要有以下一些原因:
- 处理硬件:许多硬件都需要一些时间来响应某个命令。例如,如果在一个程序中你启动一个电动机然后立即检查其状态,你可能会得到一个错误的读数,因为电动机可能还没有足够的时间开始旋转。此时你需要使用 delay() 函数让系统等待一段时间,使得电动机有足够的时间响应。
- 用户交互:我们常常需要在用户交互中实现延迟效果。例如,在蜂鸣器播放音乐时,音符之间需要一段沉默的时间。或者,在闪烁LED灯的情况下,"开"和"关"状态之间需要延时以控制闪烁的速度。
- 节省能源:在一些应用中,比如电池供电的系统,如果不在需要的时候长期保持系统的高速运转,那么电池的寿命会大大缩短。在此情况下,我们可以让系统在一段时间后进入待机或低功耗模式,直到下一个处理周期到来。
- 定时操作:在许多项目中,我们常常需要实现一些特定时间点的操作。例如,在自动灌溉系统中,我们可能需要在每天的特定时间点进行灌溉。在间隔测量中,我们可能每隔一段时间采集一次数据。
尽管延时函数在很多情况下非常有用,但也需要注意其阻塞性质。过度依赖阻塞延时可能会导致程序对其他事件的响应不及时。为了更好的在ESP-IDF上进行多任务编程,我们还可以学习一些非阻塞延时的编程技术。
什么是阻塞延时?
📌 阻塞延时是在程序执行过程中,当某个操作或函数需要一定时间才能完成时,程序会暂停执行直至该操作完成,这段时间程序被阻塞了。阻塞延时可能会导致程序运行速度变慢或出现假死现象。
举个例子,假设你想要煮开水来泡茶。通常情况下,你会将水壶放在炉灶上加热,等待水烧开后才能使用。在这个过程中,存在阻塞延时。
当你将水壶放在火上时,程序可以看作是“等待”水烧开的操作。在这个等待过程中,你不能立即得到热水来泡茶,需要耐心等待水煮沸。期间,你可能无法做其他与烧水无关的事情,因为你需要留意水壶,并等待时机。即便家里着火了,你也还是在等待烧水。
3.2 延时的实现
在ESP-IDF 中,可以使用以下选项来实现延时:
- vTaskDelay() 函数
这个函数会让当前的任务挂起指定的时间。例如,vTaskDelay(1000 / portTICK_PERIOD_MS) 会让当前任务挂起 1 秒钟,等待一个系统时钟周期。 - usleep() 函数 这个函数可以以微秒为单位精准延时。例如, usleep(1000000) 会让任务休眠 1 秒钟。
- esp_rom_delay_us()函数
可以进行微秒级延时。
下面是使用 vTaskDelay() 函数实现延时的示例代码: 请注意,vTaskDelay() 函数需要引入 freertos/task.h
头文件和 freertos/FreeRTOS.h
头文件。
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
void delay_ms(int ms)
{
vTaskDelay(ms / portTICK_PERIOD_MS);
}
void app_main(void)
{
// 一秒钟的延时
delay_ms(1000);
}
2
3
4
5
6
7
8
9
10
11
这里用到一个FreeRTOS的知识点,void vTaskDelay( const TickType_t xTicksToDelay )
函数,将一个任务延迟给定的xTicksToDelay
数。任务被阻止的实际时间取决于tick rate。常数portTICK_PERIOD_MS
可以用来从tick rate计算实时时间,分辨率为一个tick 周期。至于一个时钟节拍数是1ms,2ms,还是10ms,取决于configTICK_RATE_Hz
,即 cONFI6_FREERTOS_HZ
。CONFIG_FREERTOS_HZ在sdkconfig中定义,默认是100Hz。则一个时钟节拍数是10ms。可以将其修改为1000Hz,则一个时钟节拍数是1ms,计时更加精确。不过这样也会增加系统的开销,造成不必要的浪费。
如果需要更高精度的延时,可以使用 usleep()
函数,下面是使用 usleep()
函数实现延时的示例代码:
#include "unistd.h"
void delay_us(int us)
{
usleep(us);
}
void app_main(void)
{
// 一秒钟的延时
delay_us(1000000);
}
2
3
4
5
6
7
8
9
10
请注意,usleep()
函数需要引入 unistd.h 头文件。
使用esp_rom_delay_us()函数进行微秒级延时:
- 首先,包含头文件
esp_rom_sys.h
:
#include "esp_rom_sys.h"
- 调用ets_delay_us()函数来进行微秒级延时。该函数接受一个以微秒为单位的参数。
esp_rom_delay_us(delay_us);
以上是在ESP-IDF环境下使用延时函数的基本步骤。需要注意的是, ESP-IDF是一个多任务的实时操作系统(RTOS) ,因此使用vTaskDelay()
函数进行延时是推荐的方法,可以避免阻塞其他任务的执行。而usleep()
等函数则是用于需要较精确的微秒级延时的场景。
3.3 闪烁灯验证
将开发板连接LED的GPIO48引脚设置为输出模式,通过延时函数vTaskDelay(),控制LED亮一会灭一会,达到闪烁的效果。(关于LED的代码请参考LED章节。)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#include "bsp_led.h"
void app_main(void)
{
//LED初始化
LedGpioConfig();
while(1)
{
//点亮LED
LedOn();
//延时100ms,即亮100ms
vTaskDelay(100 / portTICK_PERIOD_MS);
//关闭LED
LedOff();
//延时100ms,即灭100ms
vTaskDelay(100 / portTICK_PERIOD_MS);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
3.4 闪烁灯效果
开发板上标记着G48的LED灯,下载代码后将一会亮一会灭,持续循环。