S1-11 定时器
定时器
定时器是一种计时器件,它可以在一定时间间隔内产生一个或多个事件。在嵌入式系统中,定时器通常用于处理周期性的任务、事件触发和控制系统时间等场景。定时器又分为软件定时器和硬件定时器,硬件定时器在芯片中数量是有限的,在 ESP32-S3 中也仅有两个硬件定时器(其中有一个还被用作了FreeRTOS的Tick精确计时和任务调度),而软件定时器在一个系统中就可以有无数个,软件定时器和硬件定时器都是定时器的实现方式,它们的区别主要体现在实现方式和精度上。
软件定时器是通过 CPU 软件实现的定时器,不需要特殊的硬件支持。在 FreeRTOS 中,开发者可以使用 xTimerCreate 和 xTimerStart 函数创建和启动软件定时器。软件定时器通常采用时钟中断来实现定时功能,其精度受到系统负载和执行时间等因素的影响,一般精度较低,适用于对时间精度要求不高的场景。
为什么不用任务(Task)解决?
定时器是一个不属于核心 FreeRTOS 内核的可选功能, 由 定时器服务(或守护进程)任务提供。
FreeRTOS中的定时器和任务虽然都可以用来完成一些周期性操作,但是它们的设计思路和实现方式有很大的不同。
定时器主要是为了在系统中间隔一段时间后执行某个特定的动作。而通常情况下,定时器并不需要占用CPU资源,比如每秒钟更新一下系统时钟,定时触发一些事件等等。在FreeRTOS中,可以通过软件定时器(xTimer)和硬件定时器来实现定时器功能。
任务则是一种独立运行的程序单元,相当于是操作系统调度的最小单位。它拥有一定的运行优先级、堆栈空间、代码和数据等资源,并且可以与其他任务交互和共享资源。任务通常会被安排在可用的处理器时间片中,以完成某种特定的系统功能,例如计算、通信、数据采集和控制等等。
对于任务和定时器的选择,一般建议按照以下原则进行:
- 如果需要高精度的时间控制,或者需要处理实时事件,那么应该优先选择任务。
- 如果只需要简单的周期性操作,例如延迟、LED闪烁等,可以使用软件定时器。
- 如果需要对外部设备进行周期性的通讯或者控制,可以使用硬件定时器。
在实际应用中,通常需要根据具体的场景和需求来选择任务或定时器。需要注意的是,在使用定时器和任务时,一定要合理利用系统资源并避免占用过多的CPU时间,以保证系统的稳定性和实时响应性。
软件定时器例程
之前我们在任务中实现了LED灯每秒两灭一次,在实际项目开发中 LED 灯这样简单的器件不可能单独为其开放一个任务操作,这样太浪费资源了,一般这种闪烁的 LED 都会被放在定时器中。
本例中需要求如下:
- 1号按键开关控制 LED 闪烁,按下一次后启动闪烁,再按一次关闭闪烁,LED 灯每500ms闪烁一次
- 2号按键开关控制 LED 闪烁,按下一次后启动闪烁,再按一次关闭闪烁,LED 灯每1000ms闪烁一次
代码共享位置:https://wokwi.com/projects/364597601972463617
优化过的代码:https://wokwi.com/projects/364602232901689345
#define KEY_1_PIN 4
#define KEY_2_PIN 5
#define KEY_3_PIN 6
#define LED_PIN 40
#define KEY1_EVENT 1
#define KEY2_EVENT 2
#define KEY3_EVENT 4
EventGroupHandle_t key_event; // 按键事件组
volatile TickType_t keyDeounce = 0; // 按下按钮的时间
TimerHandle_t flash_timer = NULL; // 闪烁用的定时器
// 定时器回调函数
void vTimerCallback( TimerHandle_t xTimer ){
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}
// 按键时间处理函数
void key_event_task_entry(void *params){
printf("安检服务程序启动...\n");
EventBits_t uxBits;
uint16_t period=0; // 闪烁间隔时间
while(1){
uxBits= xEventGroupWaitBits(key_event, // 事件组句柄
KEY1_EVENT | KEY2_EVENT | KEY3_EVENT, // 等待事件
pdTRUE, // 退出后清空所有位
pdFALSE, // 任何一位到达都触发
portMAX_DELAY); // 无限期等待(50天)
if(uxBits>0 && ((xTaskGetTickCount() - keyDeounce) < 200)){
// 如果不是超时
switch(uxBits){
case KEY1_EVENT:
period = 1000;
break;
case KEY2_EVENT:
period = 500;
break;
case KEY3_EVENT:
// 暂停或恢复闪烁
if(flash_timer !=NULL){
if(xTimerIsTimerActive(flash_timer)){
// 启动中,暂停
if(xTimerStop(flash_timer, 0) == pdPASS){
printf("定时器停止成功!\n");
}else{
printf("错误,定时器无法停止!\n");
}
}else{
// 暂停中,启动
if(xTimerReset(flash_timer, 0) == pdPASS){ // Reset后会重启,暂停时间也会归零
printf("定时器重置成功!\n");
}else{
printf("错误,定时器无法重置!\n");
}
}
}else{
printf("还没有创建定时器!\n");
}
goto exit;
break;
}
if(flash_timer == NULL){
// 创建新的定时器
flash_timer = xTimerCreate("Timer_Flash", // 定时器的名字
period, // 调用间隔时间
pdTRUE, // 是否重复运行
NULL, // 定时器标识符,相当于参数
vTimerCallback);
if(xTimerStart(flash_timer,0) == pdPASS){ //立即启动
printf("开启闪烁,间隔 %d ms\n", period);
}else{
printf("错误,启动闪烁失败!");
}
}else{
// 如果定时器存在,则停止并删除定时器
if(xTimerIsTimerActive(flash_timer)){
// 定时器正在运行中,先停止,这一步可以不操作,直接删除效果也是一样的
if(xTimerStop(flash_timer, 0) != pdPASS){
printf("错误,定时器无法停止!\n");
}
}
if(xTimerDelete(flash_timer,0) == pdPASS){ // 立即删除
flash_timer = NULL;
printf("停止闪烁!\n");
digitalWrite(LED_PIN, LOW);
}else{
printf("错误,定时器无法删除!\n");
}
}
exit:
delay(500);
xEventGroupGetBits(key_event); // 去抖动
}
}
}
// 中断按键
void IRAM_ATTR KEY1_ISR(){
keyDeounce = xTaskGetTickCountFromISR(); // 记录下按下的时间,用于放抖动,正式开发中不要这样写,有Bug
xEventGroupSetBitsFromISR(key_event, KEY1_EVENT, NULL);
}
void IRAM_ATTR KEY2_ISR(){
keyDeounce = xTaskGetTickCountFromISR(); // 记录下按下的时间,用于放抖动,正式开发中不要这样写,有Bug
xEventGroupSetBitsFromISR(key_event, KEY2_EVENT, NULL);
}
void IRAM_ATTR KEY3_ISR(){
keyDeounce = xTaskGetTickCountFromISR(); // 记录下按下的时间,用于放抖动,正式开发中不要这样写,有Bug
xEventGroupSetBitsFromISR(key_event, KEY3_EVENT, NULL);
}
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
Serial.println("Hello, ESP32-S3!");
// 初始化LED
pinMode(LED_PIN, OUTPUT);
// 初始化事件组
key_event = xEventGroupCreate();
// 安装中断
pinMode(KEY_1_PIN, INPUT_PULLUP);
pinMode(KEY_2_PIN, INPUT_PULLUP);
pinMode(KEY_3_PIN, INPUT_PULLUP);
attachInterrupt(KEY_1_PIN, KEY1_ISR, FALLING);
attachInterrupt(KEY_2_PIN, KEY2_ISR, FALLING);
attachInterrupt(KEY_3_PIN, KEY3_ISR, FALLING);
// 启动按键服务线程
xTaskCreate(key_event_task_entry, "KEY_SERVICE", 10240, NULL, 1, NULL);
vTaskDelete(NULL);
}
void loop() {
}
定时器代码中综合运用了事件组和中断。
在 setup 函数中,首先初始化了 LED 控制引脚和按键引脚,并为其增加了中断服务函数,为优化的代码中,每个按键一个中断服务函数,优化过的代码中,三个中断使用了一个服务函数,并在其中判断是哪个按键被按下了,结果都是一样的。
本例程的重点在 key_event_task_entry 任务中,白任务中首先使用 xEventGroupWaitBits 等待一个事件的到达,等待的事件是任意三个按键事件中一个。
当事件到达后,首先判断是哪个按键,如果是一号二号按键,则是用于设置 LED 闪烁的,如果闪烁存在,则删除这个闪烁,如果不存在则创建一个定时器执行闪烁。
创建定时器函数运行如下:
TimerHandle_t xTimerCreate
( const char * const pcTimerName,
const TickType_t xTimerPeriod,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction );
参数 | 描述 |
---|---|
pcTimerName | 分配给定时器的可读文本名称。 这样做纯粹是为了协助 调试。 RTOS 内核本身只通过句柄引用定时器, 而从不通过名字引用。 |
xTimerPeriod | 定时器的周期。 以 tick 为单位指定此周期,宏 pdMS_TO_TICKS() 可用于将以毫秒为单位指定的时间转换为以 tick 为单位指定的时间。 例如, 如果定时器必须要在 100 次 tick 后到期,那么只需将 xTimerPeriod 设置为 100。 或者,如果定时器 必须在 500 毫秒后到期,则需要将 xTimerPeriod 设置为 pdMS_TO_TICKS( 500 )。 使用 pdMS_TO_TICKS() 的唯一前提条件是 configTICK_RATE_HZ 小于或等于 1000。 定时器周期必须大于 0。 |
uxAutoReload | 如果 uxAutoReload 设置为 pdTRUE ,则定时器将按 xTimerPeriod 参数设置的频率重复到期。 如果 uxAutoReload 设置为 pdFALSE,则此定时器为一次性定时器, 它会在到期后进入休眠状态。 |
pvTimerID | 分配给正在创建的定时器的标识符。 通常,此标识符用于定时器回调函数: 当同一个回调函数分配给了多个定时器时,此标识符可以识别哪个定时器已到期。 或者此标识符可与 vTimerSetTimerID() 和 pvTimerGetTimerID() API 函数一起使用, 以便保存调用 定时器回调函数之间的值。 |
pxCallbackFunction | 定时器到期时调用的函数。 回调函数必须有 TimerCallbackFunction_t 定义的原型,即:void vCallbackFunction( TimerHandle_t xTimer );。 |
返回值 | 如果定时器创建成功, 则返回新创建的定时器的句柄。 如果由于剩余的 FreeRTOS 堆不足以分配定时器结构体而无法创建定时器, 则返回 NULL。 |
我们利用 xTimerCreate 创建了一个重复执行的函数,并通过 xTimerStart 将定时器启动起来,第二个参数为等待事件,因为定时器在启动、停止、重置、删除等操作的时候仍然有其他动作正在执行中,需要有个时间等待,我们这里不等待,如果有其他动作执行则马上返回,并报告错误。
定时器启动后,会重复调用 vTimerCallback 回掉函数,如果在创建定时器的时候设置过 pvTimerID 参数,则该参数会传递给这个回掉函数,在回调函数中,不需要做大循环,因为定时器会按照设置( uxAutoReload 为 true )自动重复调用。
单击第一个和第二个按钮的时候,如果发现定时器存在,则调用 xTimerDelete 删除定时器,在删除之前,还进行了定时器是否活跃中的判断,如果活跃中,则先停止再删除,这一步对于删除来说是多余的,这里我们仅用于演示。
第三个按钮演示了定时器的启动和停止,值得一提的是,启动定时器必须使用 xTimerStart,如果直接使用 xTimerReset则会报错,两者最本质上的区别就是 xTimerStart 是启动一个未运行的定时器,而 xTimerReset 是重新启动一个已运行或已超时的定时器,xTimerStart 函数会启动一个已经创建但是被停止的定时器,并开始倒计时。如果该定时器已经在运行状态或者尚未被创建,则该函数不会产生任何作用。需要注意的是,如果一个定时器启动后超时,则仅执行一次定时器回调函数,如果需要周期性执行该回调函数,需要在回调函数中再次启动该定时器。xTimerReset 函数会重新启动一个已经创建并正在运行或已经超时的定时器,这意味着重新开始倒计时。如果该定时器尚未被创建,则将不会有任何作用,也不会启动该定时器。与 xTimerStart 不同的是,xTimerReset 可以用于周期性定时器,每次重新启动定时器时都会触发回调函数执行。
单次执行的定时器
上个例程中,在使用 xTimerCreate 创建定时器的时候,uxAutoReload 传入的是 pdTRUE,表示重复执行,如果这个函数传入的是 pdFALSE,则表示不重复执行,定时器时间到后,执行一次回调函数则会退出,退出后通过 xTimerIsTimerActive 可以看到定时器确实已经停止了,这时候可以再次调用xTimerStart 启动定时器。
下面一个例程中,演示了按下开后2秒钟之后再打开和关闭LED的例子,但在本例中有个小小的BUG,printf函数似乎有冲突,所以在演示的时候把输出改成了黄色 LED 表示。
代码共享位置:https://wokwi.com/projects/364603793995498497
#define KEY_PIN 20
#define LED_PIN 14
#define WAR_PIN 39
SemaphoreHandle_t led = NULL; // 二进制信号量
volatile TickType_t keyDeounce = 0; // 按下按钮的时间
TimerHandle_t timer = NULL; // 定时器句柄
// 定时器回调函数
void vTimerCallback( TimerHandle_t xTimer ){
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}
void led_task(void *param_t){
pinMode(LED_PIN, OUTPUT);
pinMode(WAR_PIN, OUTPUT);
while(1){
// 这种去抖方式是很Low的,正确的方式要使用定时器。
if((xSemaphoreTake(led, 1000) == pdTRUE) && ((xTaskGetTickCount() - keyDeounce) < 200)){
// 创建定时器并启动他
if(timer == NULL){
timer = xTimerCreate("Timer_LED", // 定时器的名字
2000, // 调用间隔时间
pdFALSE, // 只运行一次
NULL, // 定时器标识符,相当于参数
vTimerCallback);
}
if(xTimerIsTimerActive(timer) == pdFALSE){
xTimerStart(timer,0);
// printf("按了开关...\n");
}else{
// printf("迷瞪中...\n");
digitalWrite(WAR_PIN, !digitalRead(WAR_PIN));
}
vTaskDelay(500);
}
}
}
// 中断服务函数
void IRAM_ATTR ISR() {
keyDeounce = xTaskGetTickCountFromISR(); // 记录下按下的时间,用于放抖动,正式开发中不要这样写,有Bug
xSemaphoreGiveFromISR(led, NULL);
}
void setup() {
Serial.begin(115200);
led = xSemaphoreCreateBinary(); //创建二进制信号量
xTaskCreate(led_task, "LED-DSP", 1024, NULL, 1, NULL);
// 安装中断
pinMode(KEY_PIN, INPUT_PULLUP);
attachInterrupt(KEY_PIN, ISR, FALLING);
}
void loop() {
delay(10);
}
原文地址:https://blog.csdn.net/suolong123/article/details/135616587
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!