• 【esp32】xQueueReceive 函数调试踩坑记录


    项目过程描述

    在项目过程中基于ESP32开发用到了 FreeRTOS中的任务相关的管理,其中一个典型的例子如下,记录下来作为启发。
    项目中配置ESP32的某个GPIO引脚为上升沿和下降沿都触发中断,并注册中断服务函数,再创建一个 task 任务执行中断需要处理的相关功能。但是发现, 触发一次中断只执行一次任务 但是中断处理函数中是循环的
    简单的说,就是在中断处理中,想完成以下事情,触发一次中断 cnt 开始累加,并不断打印出结果,但是实际上,触发一次中断,只打印一个结果。

    while(1){
    	if(触发中断)
    	{  处理 }
    	   cnt++;
    	   printf("cnt = %d",cnt);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    程序代码分析

    主函数、中断服务函数:

    xQueueHandle gpio_evt_queue = NULL; /*创建一个队列*/
    
    /*中断服务函数*/
    void IRAM_ATTR gpio_isr_handler(void* arg){
        uint32_t gpio_num = (uint32_t) arg;
        xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);  /*若某个gpio口发生中断,便将中断发送到队列中*/
    }
    
    void main(){
    	
        gpio_init();  // GPIO 初始化配置,此处略,配置某个GPIO口为中断触发状态
        xTaskCreate(gpio_task_example, "gpio_task_example", 2048, NULL, 10, NULL);   /*创建一个任务*/
        gpio_install_isr_service(0);   /*在MCU的 core0 安装中断服务*/
        gpio_isr_handler_add(GPIO_INPUT_IO_0, gpio_isr_handler, (void*) GPIO_INPUT_IO_0);   // 添加 gpio0 的中断服务函数
        char cnt = 0;
        while(1) {
            printf("cnt: %d\n", cnt++); /*循环打印 cnt 的值*/
            vTaskDelay(1000 / portTICK_RATE_MS);  /*任务延时1s*/
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    中断服务函数与任务:

    /*任务实例*/
    void gpio_task_example(void* arg){
       uint32_t io_num;
       char test_cnt = 0;
       while(1){        
            if(xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {   /*注意这个地方*/
                printf("GPIO[%d] intr, val: %d\n", io_num, gpio_get_level(io_num));
            }
           test_cnt++;
           printf("test_cnt = %d\n",test_cnt);      
       }    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    分析:原本我以为的过程是,主函数中有一个while(1)循环,开启的任务中也有一个while(1)循环,在任务中的死循环中,如果没有收到任何GPIO的中断,if条件不满足,会打印 test_cnt 变量的值,所以我认为应该输出的结果是以下:
    cnt = 1
    cnt = 2
    cnt = 3
    test_cnt = 1
    test_cnt = 2
    cnt = 4
    cnt = 5
    test_cnt = 3
    ………………………………

    两个 while 循环交替执行,因为freertos采用的是任务时间片的方法,结果……

    调试的时候去惊呆了,代码根本不是和我想的一样执行,打印出的结果只有cnt = 1
    cnt = 2
    cnt = 3
    cnt = 4
    cnt = 5
    cnt = 6 ,
    当我给一个中断的时候,才会打印出一个 test_cnt = 1;于是我把 main 函数中的 while(1)去掉,正常应该打印 test_cnt = 1 test_cnt = 2 test_cnt = 3 test_cnt = 4 ,但是结果确实只有给中断的时候才打印结果而且, test_cnt 的结果和给中断的次数是一样的,也就是只有 xQueueReceive返回true之后,才会执行此函数后边的内容,于是各种不解,开始了打破砂锅问到底的过程。

    xQueueReceive 函数分析

    【xQueueReceive】这个函数到底有什么作用:xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)
    函数作用:从队列中 接收消息并删除消息
    参数1:消息队列句柄
    参数2:从消息队列中复制出数据后,所存储的缓冲地址,缓冲区空间要大于等于消息队列创建函数 xQueueCreate 所指定的单个消息大小,否则取出的数据无法全部存储到缓冲区,从而造成内存溢出。
    参数3:超时等待时间。指定函数(该任务)的阻塞时间。消息队列为空时,等待消息队列有数据的最大等待时间,单位系统时钟节拍。比如可以设置为
    const TickType_t xMaxBlockTime = pdMS_TO_TICKS(300); /* 设置最大等待时间为300ms */

    注意: 不可以在中断中调用该函数, 如果消息队列为空且第三个参数为0,则函数会立即返回(执行完毕,不会阻塞),若果参数3设置为 portMAX_DELAY,则此函数会永久等待直到消息队列有数据

    函数的解释为,从 gpio_evt_queue 这个队列中接受消息,并将接收到的中断 GPIO 端口号赋值给 io_num 这个变量中,portMAX_DELAY 为最大等待超时时间。当有消息产生并且被 xQueueReceive接受(消费后),队列中的消息会被自动删除,如果不想删除就用 xQueuePeek 函数。

    注意:在 0 - portMAX_DELAY 这段时间内,当 gpio_evt_queue 队列中没有中断消息产生时,是 卡在这里死等(无限期等待),因此只有给一个GPIO口中断时,其下边的代码才会执行。

    总结:在任务的while(1)循环中,当参数3设置为0时,当队列中无中断产生,则函数立即返回,那么会卡在while(1)死循环中无法break,超过任务看门狗的喂狗时间会造成 task watchdog reset。但是当参数3设置为最大超时等待时间时,在未接收到中断时,中断消息队列为空,task处于阻塞状态,虽然卡在 while(1) 循环中,但是不会造成任务看门狗复位。

    代码调试过程

    1. 测试任务的“死等”,无限期等待

    测试:将任务实例改为以下:

    /*任务实例*/
    void gpio_task_example(void* arg){
       uint32_t io_num;
       char test_cnt = 0;
       while(1){ 
           test_cnt++;
           printf("test_cnt = %d\n",test_cnt);   
            if(xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {   /*注意这个地方*/
                printf("GPIO[%d] intr, val: %d\n", io_num, gpio_get_level(io_num));
            }   
       }    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    结果:
    test num = 1
    cnt: 0
    cnt: 1
    cnt: 2
    cnt: 3
    cnt: 4
    cnt: 5
    cnt: 6

    ………………当给中断后………………

    GPIO[4] intr, val: 0 test num = 2 cnt: 108
    GPIO[4] intr, val: 1 test num = 3
    cnt: 109

    在这里插入图片描述

    2. 测试 xQueueReceive 函数 的超时等待时间参数

    gpio_task_example函数内容同上,将上边的实例,portMAX_DELAY 参数改为 0,不无限期等待,队列中没有消息就直接跳过。

    这样等同于以下第三种方式,输出的结果和3一样

    3. 测试无“死等”,但是task中存在耗时较长的循环

    如果把 gpio_task_example内容改为:

    void gpio_task_example(void* arg){
       char test_cnt = 0;
       while(1){ 
           test_cnt++;
           printf("test_cnt = %d\n",test_cnt);  }   
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    输出结果:直接卡在task的死循环中,都不执行main函数的while循环了,执行一段时间会提示 任务看门狗复位了,因为卡在task中太长时间,导致没有喂狗,造成复位。关于任务看门狗超时复位时间,参考博客:【ESP32学习笔记(40)——Watchdog看门狗使用】
    在这里插入图片描述

  • 相关阅读:
    【附源码】计算机毕业设计JAVA中小学教务管理平台
    元宇宙将会越来越多地与行业、场景联系在一起
    Vue.js安装步骤和注意事项
    解决Vue多次传递重复参数会报错
    记录下电脑windows安装Tina的过程
    干货,某大厂小姐姐深夜让我说出了秘密-springboot发邮件 原创
    TCP 连接管理机制(二)——TCP四次挥手的TIME_WAIT、CLOSE_WAIT状态
    纯干货解答 | ERP是什么?有什么作用呢?
    java项目实现发送邮箱激活用户功能
    神器 SpringDoc 横空出世!最适合 SpringBoot 的API文档工具来了
  • 原文地址:https://blog.csdn.net/qq_39217004/article/details/127749374