1.ESP32中断

  • 中断的类型

  1. 硬件中断:比如按下按键时的GPIO中断,或者触摸中断(检测到触摸时)
  2. 软件中断:定时器中断或者看门狗中断(当定时器超时时)
  • ESP32中断相关函数

  1. attachInterrupt:在Arduino中,可以使用下面这个函数添加一个中断处理逻辑,这个函数有三个参数void attachInterrupt(uint8_t pin, std::function<void(void)> intRoutine, int mode)
  • pin:中断触发的中断号。虽然对于esp32,这个参数可以直接写GPIO引脚编号,但是建议尽量调用digitalPinTolnterrupt函数,将原始GPIO引脚转换成中断系统中的对应编号使用,比如digitalPinToInterrupt(23),ESP32能直接使用GPIO编号,是因为对于ESP32,digitalPinToInterrupt函数的返回值就是原样返回,但对于其他类型的单片机,逻辑就不一样了
  • intRoutine:中断处理函数,就是当触发中断时,需要执行的操作注意这个操作应该尽可能轻量,不能将繁重的工作写在中断处理函数中,这样会极大降低单片机响应,最糟糕时可能导致单片机重启(看门狗超时),自定义一个函数,一般是void型
  • mode:中断触发的模式,一共有以下几种模式可选:
  1. detachInterrupt:该函数用于解除某个中断号的中断逻辑,函数具体如下:                      void detachInterrupt(uint8_t pin),函数只有一个参数:

  • pin:要取消中断程序的中断号,同样地,建议使用digitalPinToInterrupt函数来将GPIO编号转换成中断号

中断实战:

  • 外接一个按键来控制LED灯的亮灭,pin选择23引脚,中断触发模式选择下降沿,因为按键默认高电平(上拉),我们按下按键就触发中断。代码实现:

2.FreeRTOS多任务初体验

  • ESP32的Arduino框架里面,已经内置了FreeRTOS框架,并且对ESP32的双核进行了完美的适配,所以我们在使用时,无需引入第三方库就可以直接用了。使用stm32cubemx来运用FreeRTOS我后续会写出来。

  1. 创建多任务:xTaskCreate函数,一共有6个参数

  • pvTaskCode:创建的任务需要执行的逻辑,是一个自定义函数,该函数返回值必须为void,只有一个参数,参数类型必须为void*

  • pcName:任务的名称,一个字符串,自己定义,为了自己更加理解创建了一个怎么样的任务

  • usStackDepth:给任务分配的最大堆栈大小,单位是字节
  • pvParameters:传递给任务的参数,类型是void*,一般为NULL
  • uxPriority:任务优先级,一个数字,取值范围为[0,24],数字越大,优先级越高
  • pxCreatedTask:用于接收该任务的句柄,后续对该任务的操作,需要基于该句柄完成,类型为TaskHandle_t
  • 代码实战:
  1. FreeRTOS延时函数:vTaskDelay,该函数的作用是延迟一段时间,在这段时间内,调用该函数的任务让出CPU资源,其他任务可以借机获取CPU资源
  • 该函数就只有一个参数:xTicksToDelay,需要任务延迟的tick数,对于ESP32,一个tick等于1ms,所以要延迟3秒,就应该调用vTaskDelay(3000),但是对于其他芯片,一个tick就不一定等于1ms。
  • 我们平常使用的Delay函数,其内部也是调用vTaskDelay函数实现的
  1. 任务删除函数:vTaskDelete
  • 该函数就一个参数,xTaskToDelete表示要删除任务的句柄,这个句柄就是xTaskCreate函数中传入的最后一个参数,如果参数设置为NULL,表示删除当前任务,一般都是填NULL,自己删除自己,不建议跨任务删除,会出现一些不可预知的问题
  1. 任务暂停函数:vTaskSuspend
  • 这个函数就一个参数,填写NULL表示暂停当前任务,函数也可传其他任务句柄
  1. 任务继续函数:vTaskResume
  • 这个函数就一个参数,参数为NULL表示恢复当前任务,函数也可传其他任务句柄

小提醒:大家可能会发现FreeRTOS库中的函数名,有些是以x开头的,有些是以v开头的,其实这与函数的返回值有关,如果函数有返回值,一般以x开头,如果函数没有返回值,一般以v(void)开头

3.FreeRTOS任务绑定

  • esp32有2个CPU,我们可以将创建的任务分配到指定的CPU上运行
  1. 创建任务并绑定CPU函数:xTaskCreatePinnedToCore,该函数和xTaskCreate前6个参数一致,最后一个参数:
  • xCoreID:需要将任务分配到的CPU编号,esp32可选编号为0和1,也可以指定常量
  • 需要注意的是:esp32中,与无线通信相关的,比如WiFI或者蓝牙协议栈相关的逻辑都在CPU0上处理,所以我们在使用esp32时,如果有用到esp32的蓝牙或者WiFi功能,则不建议将我们自己的比较复杂的任务分配到CPU0上,因为这样有可能会干扰WiFI和蓝牙的正常运行,影响通信质量。而对于简单任务的分配,也建议不指定CPU,让esp32自行调度,除非有特殊需求

4.看门狗

  1. 什么是看门狗?看门狗就是一个定时器,能够让微控制器在程序发生意外的时候,自动重启单片机,这些意外有什么?
  2. 看门狗分类:中断看门狗和任务看门狗
  • 中断看门狗(IWDT):interrupt watch dof timer,它是用于监控有没有哪一个中断处理程序执行时间过长(默认时间800ms),如果时间过长,直接重启esp32
  • 任务看门狗(TWDT):task watch dog timer,它用来看管FreeRTOS的任务,任务如果长时间(5s)得不到CPU资源,就重启esp32
  1. 看门狗机制:中断开门狗默认开启,esp32有2个CPU,在每一个CPU上,都有一个预先创建的任务,这个任务优先级非常低(0),它有三个作用:
  • 当esp32处于空闲状态时,自动降低功耗,增加续航时间

  • 当自己CPU下有任务被删除时,主动清理相关内存资源

  • 重置看门狗定时器

  1. 喂狗定义:看门狗本质是定时器,只有定时器超时才触发重启逻辑,我们只需要在超时时间之内去重置定时器,就避免被重启,这个就叫喂狗。

5.如何避免日常错误,导致看门狗重启

  • 中断处理函数简单一点,不要在中断处理函数里面进行打印操作
  • 主动让出CPU,使用vTaskDelay函数,主动休眠一段时间

6.FreeRTOS信号量的使用

  • 类型:二值信号量和计数信号量
  • 使用相当多的是二值信号量
  • 二值信号量有三种流程:创建,释放,等待
  • 创建:semaphore = xSemaphoreCreateBinary()
  • 释放:xSemaphoreGive(semaphore)
  • 等待:xSemaphoreTake(semaphore, portMAX_DELAY)

7.FreeRTOS互斥信号量使用

  • 2个线程并行执行的东西强制转换成串行的,避免出现线程不安全的现象,相当于一把锁,谁拿到钥匙谁就干活,怎么开锁?xSemaphoreTakeRecursive(mutex, portMAX_DELAY) == pdTRUE用if语句来判断是否拿到锁,谁拿到就谁执行
  • 互斥信号量,用三个流程:
  1. 定义自变量SemaphoreHandle_t mutex;
  2. 然后set up函数里面用定义的变量创建的函数去获取mutex = xSemaphoreCreateMutex();
  3. 最后一步在自定义函数里面使用使用if语句去判断,然后执行逻辑

举例代码:

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "usart.h"
#include <stdio.h>

/* ============ 1. 定义互斥信号量句柄 ============ */
SemaphoreHandle_t xMutex;

/* ============ 2. 共享资源:UART 打印函数 ============ */
/**
 * @brief 线程安全的打印函数(关键!每次打印前都要加锁)
 */
void Safe_Printf(const char *fmt, ...)
{
    /* 拿钥匙(最多等待 1000ms,防止永久死锁) */
    if (xSemaphoreTakeRecursive(xMutex, pdMS_TO_TICKS(1000)) == pdTRUE)
    {
        va_list args;
        va_start(args, fmt);
        vprintf(fmt, args);   /* 整段打印完成前,不会被打断 */
        va_end(args);

        /* 还钥匙 */
        xSemaphoreGiveRecursive(xMutex);
    }
    else
    {
        /* 等待超时,可能发生死锁,可以在这里报警 */
    }
}

/* ============ 3. 任务 A:模拟温度采集 ============ */
void TaskA_Temperature(void *pvParameters)
{
    while (1)
    {
        /* 模拟读温度传感器(耗时操作) */
        float temp = 26.5f;

        /* 打印前加锁,确保完整输出 */
        Safe_Printf("[TaskA] Temperature: %.1f C\r\n", temp);

        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

/* ============ 4. 任务 B:模拟湿度采集 ============ */
void TaskB_Humidity(void *pvParameters)
{
    while (1)
    {
        /* 模拟读湿度传感器 */
        int humidity = 60;

        Safe_Printf("[TaskB] Humidity: %d%%\r\n", humidity);

        vTaskDelay(pdMS_TO_TICKS(700));
    }
}

/* ============ 5. setup / main 函数 ============ */
int main(void)
{
    /* 硬件初始化 */
    USART_Init();

    /* 创建互斥信号量(相当于创建一把锁) */
    xMutex = xSemaphoreCreateMutex();
    if (xMutex == NULL)
    {
        printf("Mutex create failed!\r\n");
        while (1);  /* 创建失败,停下来检查 */
    }

    /* 创建两个任务 */
    xTaskCreate(TaskA_Temperature, "TaskA", 256, NULL, 1, NULL);
    xTaskCreate(TaskB_Humidity,   "TaskB", 256, NULL, 1, NULL);

    /* 启动调度器 */
    vTaskStartScheduler();

    while (1) { /* 不会到达这里 */ }
}

8.FreeRTOS事件组的使用

  • 概念:允许任务等待多个事件的组合每个事件对应事件组中的一个位(bit),任务可以设置、清除或等待这些位的特定组合。
  • 怎么运用?首先还是创建出事件组句柄EventGroupHandle_t xEventGroup,然后在setup中创建出事件组xEventGroup=xEventGroupCreate();在任务中使用xEventGroupWaitBits函数

举例代码:

#include "FreeRTOS.h"
#include "task.h"
#include "event_groups.h"
#include "usart.h"
#include <stdio.h>

/* ============ 1. 定义事件位(用 bit 的位置表示) ============ */
#define BIT_CARD_OK   (1 << 0)   // bit0:刷卡成功
#define BIT_PWD_OK    (1 << 1)   // bit1:密码正确
#define BIT_ALL_OK    (BIT_CARD_OK | BIT_PWD_OK)  // 两个都满足

/* ============ 2. 创建事件组句柄 ============ */
EventGroupHandle_t xEventGroup;

/* ============ 3. 任务 A:模拟刷卡(200ms 后刷卡成功) ============ */
void Task_SwipeCard(void *pvParameters)
{
    while (1)
    {
        vTaskDelay(pdMS_TO_TICKS(200));
        printf("[刷卡中...] 请刷卡\r\n");
        vTaskDelay(pdMS_TO_TICKS(1000));

        printf("[刷卡] 刷卡成功!\r\n");
        /* 设置 bit0:刷卡成功事件发生 */
        xEventGroupSetBits(xEventGroup, BIT_CARD_OK);
    }
}

/* ============ 4. 任务 B:模拟输密码(500ms 后密码正确) ============ */
void Task_InputPwd(void *pvParameters)
{
    while (1)
    {
        vTaskDelay(pdMS_TO_TICKS(500));
        printf("[密码] 请输入密码\r\n");
        vTaskDelay(pdMS_TO_TICKS(2000));

        printf("[密码] 密码正确!\r\n");
        /* 设置 bit1:密码正确事件发生 */
        xEventGroupSetBits(xEventGroup, BIT_PWD_OK);
    }
}

/* ============ 5. 门控任务:等两个条件都满足才开门 ============ */
void Task_DoorControl(void *pvParameters)
{
    EventBits_t uxBits;

    while (1)
    {
        printf("[门控] 等待:刷卡 + 密码\r\n");

        /* 等待 bit0 和 bit1 都置 1(pdTRUE 表示 AND 逻辑) */
        uxBits = xEventGroupWaitBits(
                    xEventGroup,
                    BIT_ALL_OK,        // 等待这些位
                    pdTRUE,            // 满足后自动清除
                    pdTRUE,            // 全部满足才返回(AND)
                    portMAX_DELAY);    // 永久等待

        printf("[门控] ✅ 所有条件满足,开门!\r\n\n");

        /* 模拟开门动作 2 秒 */
        vTaskDelay(pdMS_TO_TICKS(2000));
        printf("[门控] 关门,等待下一次\r\n\n");
    }
}

/* ============ 6. 主函数 ============ */
int main(void)
{
    USART_Init();

    /* ① 创建事件组 */
    xEventGroup = xEventGroupCreate();
    if (xEventGroup == NULL)
    {
        printf("Event group create failed!\r\n");
        while (1);
    }

    /* ② 创建任务 */
    xTaskCreate(Task_SwipeCard,  "Card",  256, NULL, 1, NULL);
    xTaskCreate(Task_InputPwd,   "Pwd",   256, NULL, 1, NULL);
    xTaskCreate(Task_DoorControl,"Door",  256, NULL, 1, NULL);

    /* ③ 启动调度器 */
    vTaskStartScheduler();

    while (1);
}

结果:

时间线
  0ms   [门控] 等待:刷卡 + 密码
 200ms  [刷卡中...] 请刷卡
 500ms  [密码] 请输入密码
1200ms  [刷卡] 刷卡成功!      ← bit0 置 1,但密码还没好
2500ms  [密码] 密码正确!      ← bit1 置 1,两个都满足!
2500ms  [门控] ✅ 所有条件满足,开门!
4500ms  [门控] 关门,等待下一次

9.FreeRTOS队列的使用

  • 之前学习的信号量和事件组只是任务和任务之间传递信号,相互告诉某件事情发生了没有
  • 而队列则是任务和任务之间传递真实的数据
  • 创建队列:先创建句柄,然后再在setup函数里面句柄函数的形式来书写:句柄=xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
  • 发送数据:
  1. BaseType_t xQueueSend(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait);正常发送
  2. BaseType_t xQueueSendToBack(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait);往后排
  3. BaseType_t xQueueSendToFront(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait);往前插队
  • 接受数据:BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);
  • 检查队列中是否有数据:UBaseType_t uxQueueMessagesWaiting(const QueueHandle_t xQueue);

举例代码:

#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "usart.h"
#include <stdio.h>

/* ============ 1. 定义"订单"结构体 ============ */
typedef struct {
    uint8_t  table_id;    // 几号桌
    char     dish[20];    // 什么菜
} Order_t;

/* ============ 2. 创建队列句柄 ============ */
/* 队列容量 5 条,每条是 Order_t 大小 */
QueueHandle_t xOrderQueue;

/* ============ 3. 生产者任务:模拟"点餐"(每 1 秒下一个单) ============ */
void Task_Waiter(void *pvParameters)
{
    Order_t order;
    int cnt = 0;

    while (1)
    {
        cnt++;
        order.table_id = (cnt % 5) + 1;  // 轮流给 1~5 号桌
        sprintf(order.dish, "红烧肉 #%d", cnt);

        printf("[服务员] %d号桌点了 %s,扔进队列\r\n",
               order.table_id, order.dish);

        /* ✅ 发送数据到队尾 */
        xQueueSend(xOrderQueue, &order, portMAX_DELAY);

        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

/* ============ 4. 消费者任务:模拟"做菜"(每 2 秒做一个) ============ */
void Task_Chef(void *pvParameters)
{
    Order_t order;

    while (1)
    {
        /* ✅ 从队列里接收一条数据 */
        if (xQueueReceive(xOrderQueue, &order, portMAX_DELAY) == pdTRUE)
        {
            printf("[厨师]  正在做 %s ...\r\n", order.dish);
            vTaskDelay(pdMS_TO_TICKS(2000));   // 模拟做菜 2 秒
            printf("[厨师]  ✅ %s 上桌!\r\n\n", order.dish);
        }
    }
}

/* ============ 5. 监控任务:每秒看队列里堆了几个单 ============ */
void Task_Monitor(void *pvParameters)
{
    UBaseType_t cnt;

    while (1)
    {
        cnt = uxQueueMessagesWaiting(xOrderQueue);
        printf("[监控]  当前队列里有 %lu 个订单等待\r\n\n", cnt);
        vTaskDelay(pdMS_TO_TICKS(3000));
    }
}

/* ============ 6. 主函数 ============ */
int main(void)
{
    USART_Init();

    /* ① 创建队列:容量 5 条,每条 sizeof(Order_t) 字节 */
    xOrderQueue = xQueueCreate(5, sizeof(Order_t));
    if (xOrderQueue == NULL)
    {
        printf("Queue create failed!\r\n");
        while (1);
    }

    /* ② 创建任务 */
    xTaskCreate(Task_Waiter,  "Waiter", 256, NULL, 1, NULL);
    xTaskCreate(Task_Chef,    "Chef",   256, NULL, 1, NULL);
    xTaskCreate(Task_Monitor, "Monitor",256, NULL, 1, NULL);

    /* ③ 启动调度器 */
    vTaskStartScheduler();

    while (1);
}

结果:

[服务员] 1号桌点了 红烧肉 #1,扔进队列
[监控]  当前队列里有 1 个订单等待

[服务员] 2号桌点了 红烧肉 #2,扔进队列
[厨师]  正在做 红烧肉 #1 ...
[监控]  当前队列里有 2 个订单等待

[服务员] 3号桌点了 红烧肉 #3,扔进队列
[监控]  当前队列里有 3 个订单等待

[厨师]  ✅ 红烧肉 #1 上桌!
[厨师]  正在做 红烧肉 #2 ...
...

10.FreeRTOS环形缓冲区使用

  • 暂存数据达到削峰的效果,比如我们开展下一次对话,我们把上一次对话就暂存在循环缓冲区,由于不需要重新分配内存,所以比队列效率高
  • 创建缓冲区:xRingbufferCreate(size_t xBufferSize, RingbufferType_t xBufferType);
  • 往缓冲区写入数据:BaseType_t xRingbufferSend(RingbufHandle_t xRingbuffer, const void *pvItem, size_t xItemSize, TickType_t xTicksToWait);
  • 从缓冲区接受数据:void *xRingbufferReceive(RingbufHandle_t xRingbuffer, size_t *pxItemSize, TickType_t xTicksToWait);
  • 归还内存:void vRingbufferReturnItem(RingbufHandle_t xRingbuffer, void *pvItem);

举例代码:

#include "FreeRTOS.h"
#include "task.h"
#include "ringbuffer.h"
#include "usart.h"
#include <stdio.h>
#include <string.h>

/* ============ 1. 定义句柄 ============ */
RingbufHandle_t xRingBuf;

/* ============ 2. 串口接收任务:模拟"瞬间来一大段数据" ============ */
void Task_UartRx(void *pvParameters)
{
    const char *fake_data[] = {
        "Hello World!\r\n",
        "STM32 is great!\r\n",
        "FreeRTOS Ringbuffer demo\r\n"
    };
    int i = 0;

    while (1)
    {
        const char *p = fake_data[i % 3];
        size_t len = strlen(p) + 1;   // 含 \0

        printf("[串口] 收到 %d 字节: %s", (int)len, p);

        /* ✅ 写入环形缓冲区(队尾) */
        xRingbufferSend(xRingBuf, p, len, pdMS_TO_TICKS(100));

        i++;
        vTaskDelay(pdMS_TO_TICKS(800));
    }
}

/* ============ 3. 数据处理任务:慢慢取出来解析 ============ */
void Task_Process(void *pvParameters)
{
    while (1)
    {
        size_t recv_size;
        char *pItem;

        /* ✅ 从缓冲区取出一段数据(自动复制出来) */
        pItem = (char *)xRingbufferReceive(xRingBuf, &recv_size, portMAX_DELAY);

        if (pItem != NULL)
        {
            printf("[处理] 取出 %d 字节: %s", (int)recv_size, pItem);

            /* ⚠️ 关键:处理完必须归还内存!否则缓冲区会耗尽 */
            vRingbufferReturnItem(xRingBuf, (void *)pItem);

            vTaskDelay(pdMS_TO_TICKS(1500));   // 模拟处理耗时
        }
    }
}

/* ============ 4. 监控任务:每秒看缓冲区状态 ============ */
void Task_Monitor(void *pvParameters)
{
    while (1)
    {
        /* getFreeSize() / getCurSize() 查看使用情况(如果支持) */
        printf("[监控]  缓冲区状态:...\r\n\n");
        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

/* ============ 5. 主函数 ============ */
int main(void)
{
    USART_Init();

    /* ① 创建环形缓冲区:1024 字节,字节流模式(NO_SPLIT 性能高) */
    xRingBuf = xRingbufferCreate(
                    1024,                          // 容量
                    RINGBUF_TYPE_BYTEBUF);         // 按字节流方式
    if (xRingBuf == NULL)
    {
        printf("Ringbuffer create failed!\r\n");
        while (1);
    }

    /* ② 创建任务 */
    xTaskCreate(Task_UartRx,   "UartRx",   256, NULL, 2, NULL);
    xTaskCreate(Task_Process,  "Process",  256, NULL, 1, NULL);
    xTaskCreate(Task_Monitor,  "Monitor",  256, NULL, 1, NULL);

    /* ③ 启动调度器 */
    vTaskStartScheduler();

    while (1);
}

结果:

[串口] 收到 14 字节: Hello World!

[串口] 收到 18 字节: STM32 is great!
[处理] 取出 14 字节: Hello World!

[串口] 收到 26 字节: FreeRTOS Ringbuffer demo
[处理] 取出 18 字节: STM32 is great!
...

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐