ESP32学习记录01

核心知识

Posted by mkk on May 27, 2025

freeRTOS

esp-idf是基于freeRTOS的框架,所以是首先要掌握的内容

freeRTOS任务概述

在低端设备中,程序基本分为裸机和RTOS,针对简单的程序,我们用裸机程序完全可以满足,一旦功能复杂,程序模块众多,裸机程序往往很难满足我们的需求。因此我们就要用到RTOS系统。

使用 FreeRTOS 的实时应用程序可以被构建为一组独立的任务。每个任务在自己的上下文中执行,不依赖于系统内的其他任务或 RTOS 调度器本身。

任务分为四个状态:运行、准备就绪、阻塞、挂起

运行

当任务实际执行时,它被称为处于运行状态。 任务当前正在使用处理器。 如果运行RTOS 的处理器只有一个内核, 那么在任何给定时间内都只能有一个任务处于运行状态。

准备就绪

准备就绪任务指那些能够执行(它们不处于阻塞或挂起状态),但目前没有执行的任务,因为同等或更高优先级的不同任务已经处于运行状态。

阻塞

如果任务当前正在等待时间或外部事件,则该任务被认为处于阻塞状态。例如,如果一个任务调用vTaskDelay(),它将被阻塞(被置于阻塞状态),直到延迟结束-一个时间事件。任务也可以通过阻塞来等待队列、信号量、事件组、通知或信号量事件。处于阻塞状态任 务通常有一个”超时”期, 超时后任务将被超时,并被解除阻塞,即使该任务所等待的事件没有发生。“阻塞”状态下的任务不使用任何处理时间,不能被选择进入运行状态。

挂起

与“阻塞”状态下的任务一样,“挂起”状态下的任务不能 被选择进入运行状态,但处于挂起状态的任务没有超时。相反,任务只有在分别通过 vTaskSuspend() 和 xTaskResume()API 调用明确命令时 才会进入或退出挂起状态。

优先级

每个任务均被分配了从 0 到 (configMAX_PRIORITIES-1) 的优先级,其中的configMAX_PRIORITIES 在 FreeRTOSConfig.h 中定义,低优先级数字表示低优先级任务。空闲任务的优先级为零。

任务创建

1
2
3
4
5
6
7
8
9
BaseType_t xTaskCreatePinnedToCore(
 TaskFunction_t pvTaskCode, //任务函数指针,原型是 voidfun(void *param)
 const char *constpcName, //任务的名称,打印调试可能会有用
const uint32_t usStackDepth,//指定的任务堆栈空间大小(字节)
void *constpvParameters, //任务参数
UBaseType_t uxPriority,
 (configMAX_PRIORITIES- 1)// 优 先 级,数字越大,优先级越大,0 到
TaskHandle_t *constpvCreatedTask,//传回来的任务句柄
const BaseType_t xCoreID) //分配在哪个内核上运行

延迟函数

1
2
3
4
// 阻塞指定的时间,单位为系统时钟节拍数
void vTaskDelay( const TickType_t xTicksToDelay )
//用于在指定的时间点之前阻塞任务,直到时间到达。
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement)

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"

void TASK1(void *pvParameters){
    while(1){
        printf("TASK 1\n");
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}
void app_main(void)
{
    xTaskCreatePinnedToCore(TASK1, "TASK1", 2048, NULL, 1, NULL, 0);
    
}

队列

队列是任务间通信的主要形式。它们可以用于在任务之间以及中断和任务之间发送消息。在大多数情况下,它们作为线程安全的 FIFO(先进先出)缓冲区使用,新数据被发送到队列的后面, 尽管数据也可以发送到前面。
常用如下API:

1
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
29
//创建一个队列,成功返回队列句柄
QueueHandle_t xQueueCreate( 
 UBaseType_t uxQueueLength,//队列容量
 UBaseType_t uxItemSize//每个队列项所占内存的大小(单位是字节)
 );

//向队列头部发送一个消息
BaseType_t xQueueSend(
 QueueHandle_t xQueue,// 队列句柄
 const void * pvItemToQueue, //要发送的消息指针
 TickType_t xTicksToWait  //等待时间);

 //向队列尾部发送一个消息
BaseType_t xQueueSendToBack(
 QueueHandle_t xQueue,// 队列句柄
 const void * pvItemToQueue, //要发送的消息指针
 TickType_t xTicksToWait  //等待时间);

//从队列接收一条消息
BaseType_t xQueueReceive(
 QueueHandle_t xQueue,//队列句柄
 void * pvBuffer,//指向接收消息缓冲区的指针。
 TickType_t xTicksToWait //等待时间);
 
//xQueueSend 的中断版本
BaseType_t xQueueSendFromISR(
 QueueHandle_t xQueue,// 队列句柄
 const void * pvItemToQueue, //要发送的消息指针
 BaseType_t *pxHigherPriorityTaskWoken );////指出是否有高优先级的任务被唤醒

示例

1
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
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "freertos/queue.h"

QueueHandle_t xQueue = NULL;
typedef struct{
    int x;
    int y;
}data;
void TASK1(void *pvParameters){//从队列中读取数据
    while(1){
        data data1;
        if (xQueueReceive(xQueue, &data1, portMAX_DELAY))
        {
            ESP_LOGI("TASK1", "Received data: %d %d", data1.x, data1.y);
        }
        
    }
}

void TASK2(void *pvParameters){//向队列中写入数据
    while(1){
        data data1;
        data1.x = 1;
        data1.y = 2;
        if (xQueueSend(xQueue, &data1, portMAX_DELAY))
        {
            ESP_LOGI("TASK2", "Sent data: %d %d", data1.x, data1.y);
            vTaskDelay(1000 / portTICK_PERIOD_MS);
        }
    }
}
void app_main(void)
{
    xQueue = xQueueCreate(10, sizeof(data));
    xTaskCreatePinnedToCore(TASK1, "TASK1", 2048, NULL, 1, NULL, 0);
    xTaskCreatePinnedToCore(TASK2, "TASK2", 2048, NULL, 1, NULL, 0);

}

信号量

信号量是用来保护共享资源不会被多个任务并发使用,信号量使用起来比较简单。因为在freeRTOS中它本质上就是队列,只不过信号量只关心队列中的数量而不关心队列中的消息内容,在freeRTOS中有两种常用的信号量,一是计数信号量,而是二进制信号量。

二进制信号量很简单,就是信号量总数只有1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//创建二值信号量,成功则返回信号量句柄(二值信号量最大只有1个)
SemaphoreHandle_t xSemaphoreCreateBinary( void );

//创建计数信号量,成功则返回信号量句柄
SemaphoreHandle_t xSemaphoreCreateCounting(
UBaseType_t uxMaxCount,//最大信号量数
UBaseType_t uxInitialCount);//初始信号量数

//获取一个信号量,如果获得信号量,则返回 pdTRUE
 xSemaphoreTake( SemaphoreHandle_t xSemaphore,//信号量句柄
 TickType_t xTicksToWait );//等待时间

 //释放一个信号量
xSemaphoreGive( SemaphoreHandle_t xSemaphore ); //信号量句柄

//删除信号量
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );

示例

1
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
29
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
SemaphoreHandle_t xSemaphore;

void TASK1(void *pvParameters){//释放信号量
    while(1){
        xSemaphoreGive(xSemaphore);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void TASK2(void *pvParameters){//获取信号量
    while(1){
        if(xSemaphoreTake(xSemaphore,portMAX_DELAY)==pdTRUE){
            ESP_LOGI("TASK2","TASK2 is running");
        }
    }
}
void app_main(void)
{
    xSemaphore = xSemaphoreCreateBinary();
    xTaskCreatePinnedToCore(TASK1, "TASK1", 2048, NULL, 1, NULL, 0);
    xTaskCreatePinnedToCore(TASK2, "TASK2", 2048, NULL, 1, NULL, 0);

}

互斥锁

互斥锁是一种特殊的信号量,它只有两种状态,即锁定和解锁。当一个任务获得互斥锁时,其他任务无法获得该互斥锁,直到该任务释放该互斥锁。核心就是优先级发生反转

1
2
//创建互斥锁
SemaphoreHandle_t xSemaphoreCreateMutex( void );

事件组

事件位:用于指示事件是否发生,事件位通常称为事件标志
事件组:就是一组事件位。 事件组中的事件位通过位编号来引用
以下是常用的API函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//创建一个事件组,返回事件组句柄,失败返回NULL
 EventGroupHandle_t xEventGroupCreate( void );

//等待事件组中某个标志位,用返回值以确定哪些位已完成设置
EventBits_t xEventGroupWaitBits(
    const EventGroupHandle_t xEventGroup, //事件组句柄
    const EventBits_t uxBitsToWaitFor, //哪些位需要等待
    const BaseType_t xClearOnExit, //退出时是否清除标志位
    const BaseType_t xWaitForAllBits, //是否等待所有位
    TickType_t xTicksToWait ); //等待时间

//设置事件组中某个标志位
EventBits_t xEventGroupSetBits(
    const EventGroupHandle_t xEventGroup, //事件组句柄
    const EventBits_t uxBitsToSet ); //哪些位需要设置

//清除事件组中某个标志位
EventBits_t xEventGroupClearBits(
    const EventGroupHandle_t xEventGroup, //事件组句柄
    const EventBits_t uxBitsToClear ); //哪些位需要清除

示例

1
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
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "freertos/event_groups.h"

#define NUM_BIT_1 BIT0
#define NUM_BIT_2 BIT1

static EventGroupHandle_t event_group;


void TASK1(void *pvParameters){//定时设置不同标志位
    while(1){
        xEventGroupSetBits(event_group,NUM_BIT_1);
        vTaskDelay(1000/portTICK_PERIOD_MS);
        xEventGroupSetBits(event_group,NUM_BIT_2);
        vTaskDelay(1000/portTICK_PERIOD_MS);

    }
}

void TASK2(void *pvParameters){//获取不同标志位
    while(1){
        EventBits_t bits = xEventGroupWaitBits(event_group,NUM_BIT_1|NUM_BIT_2,pdTRUE,pdFALSE,portMAX_DELAY);
        if(bits&NUM_BIT_1){
            ESP_LOGI("TASK2","NUM_BIT_1 is set");
        }
        if(bits&NUM_BIT_2){
            ESP_LOGI("TASK2","NUM_BIT_2 is set");
        }
    }
}
void app_main(void)
{
    event_group = xEventGroupCreate();
    xTaskCreatePinnedToCore(TASK1, "TASK1", 2048, NULL, 1, NULL, 0);
    xTaskCreatePinnedToCore(TASK2, "TASK2", 2048, NULL, 1, NULL, 0);

}

直达任务通知

定义:每个RTOS任务都有一个任务通知数组。 每条任务通知 都有“挂起”或“非挂起”的通知状态, 以及一个32位通知值。直达任务通知是直接发送至任务的事件,而不是通过中间对象(如队列、事件组或信号量)间接发送至任务的事件。向任务发送“直达任务通知” 会将目标任务通知设为“挂起”状态(此挂起不是挂起任务)。

1
2
3
4
5
 //用于将事件直接发送到 RTOS 任务并可能取消该任务的阻塞状态
 BaseType_t xTaskNotify(
    TaskHandle_t xTaskToNotify, //要通知的任务句柄
    uint32_t ulValue,//携带的通知值
    eNotifyAction eAction ); //执行的操作

需要注意的是参数eAction如下表所述

  • eNoAction 目标任务接收事件,但其通知值未更新。在这种情况下,不使用ulValue。
  • eSetBits 目标任务的通知值使用ulValue按位或运算
  • eIncrement 目标任务的通知值自增1(类似信号量的give操作)
  • eSetValueWithOverwrite 目标任务的通知值无条件设置为ulValue。
  • eSetValueWithoutOrwrite 如果目标任务没有挂起的通知,则其通知值将设置为ulValue。如果目标任务已经有挂起的通知,则不会更新其通知值。
1
2
3
4
5
6
 //等待接收任务通知
BaseType_t xTaskNotifyWait(
    uint32_t ulBitsToClearOnEntry,//进入函数清除的通知值位
    uint32_t ulBitsToClearOnExit,//退出函数清除的通知值位
    uint32_t *pulNotificationValue,//通知值
    TickType_t xTicksToWait );//等待时长

示例

1
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
29
30
31
32
33
34
35
36
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"

//任务句柄
TaskHandle_t xHandle1;
TaskHandle_t xHandle2;


void TASK1(void *pvParameters){
    uint32_t value = 0;
    while(1){//发送任务通知
        vTaskDelay(1000 / portTICK_PERIOD_MS);
        xTaskNotify(xHandle2, value, eSetValueWithOverwrite);
        value++;
    }
}

void TASK2(void *pvParameters){
    while(1){//接收任务通知
        uint32_t value = 0;
        xTaskNotifyWait(0, ULONG_MAX,&value, portMAX_DELAY);
        ESP_LOGI("Task 2", "Value: %lu", value);
    }
}
void app_main(void)
{
    /*
    任务1:发送任务通知
    任务2:接收任务通知
    */
    xTaskCreatePinnedToCore(TASK1, "TASK1", 2048, NULL, 1, &xHandle1, 0);
    xTaskCreatePinnedToCore(TASK2, "TASK2", 2048, NULL, 1, &xHandle2, 0);

}

GPIO

涉及到GPIO,那就不得不点灯了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/定义LED的GPIO口
#define LED_GPIO GPIO_NUM_27
 //LED 闪烁初始化
void led_flash_init(void){
    gpio_config_t led_gpio_cfg = {
        .pin_bit_mask = (1<<LED_GPIO),//指定GPIO
        .mode = GPIO_MODE_OUTPUT,//设置为输出模式
        .pull_up_en = GPIO_PULLUP_DISABLE,//禁止上拉
        .pull_down_en = GPIO_PULLDOWN_DISABLE, //禁止下拉
        .intr_type = GPIO_INTR_DISABLE,//禁止中断
        };
    gpio_config(&led_gpio_cfg);
    xTaskCreatePinnedToCore(led_run_task,"led",2048,NULL,3,NULL,1);
 }

这个函数中,会定义一个关于gpio的配置结构体,然后通过gpio_config函数将配置设置到底层,通过这步,们就完成了gpio的初始化,这里我们将gpio设置成输出,这里注意,一般来说,GPIO有四种较为常见的工作模式:

  • 输出:可以设置GPIO的高低电平
  • 输入:可以获取外部输入的高低电平信息,一般要设置加上拉电阻或下拉电阻
  • 浮空输出:可以设置GPIO的高低电平,但要在电路外部中增加上拉电阻
  • 开漏输入:可以获取外部输入的高低电平信息,但要在电路外部中增加上拉电阻
1
2
3
4
5
6
7
8
9
10
 void led_run_task(void* param)
 {
    int gpio_level = 0;
    while(1)
    {
        vTaskDelay(pdMS_TO_TICKS(500));
        gpio_set_level(LED_GPIO,gpio_level);
        gpio_level = gpio_level?0:1;
    }
 }

每隔500ms切换一次电平输出,就可以达到LED灯闪烁的效果了

进阶——呼吸灯

当我们在GPIO引脚上增加一段高低电平的脉冲时,我们会看到灯一闪一闪,高低电平脉冲切换速度达到一定程度时(大约是25Hz),我们人眼是看不出来一闪一闪的效果,只会看到LED较暗,那到底暗多少,这就需要PWM脉宽调制的占空比来决定,所谓占空比,简单来说就是高电平时间占PWM周期的百分比时间。如果我们动态的改变占空比,那么就可以看到LED从暗到亮,从亮到暗的变化,这就是呼吸灯的效果

下面来看一下呼吸灯的演示程序

首先进行宏定义,我用到了48引脚的GPIO,以及定义了事件标志组和他的两个位

1
2
3
4
5
6
7
8
//定义LED灯的引脚
#define LED_PIN GPIO_NUM_48
//定义事件组的位
#define NUM_BITS0 BIT0//占空比满了
#define NUM_BITS1 BIT1//占空比为0

//定义事件组
static EventGroupHandle_t event_group;

然后是主程序,初始化了GPIO口,以及初始化了两个内容,分别是ledc_timer_config和ledc_channel_config,ledc_timer_config用于初始化用到的定时器,ledc_channel_config用于初始化ledc输出通道以及将timer关联起来
ledc_set_fade_with_time设置一个PWM占空比目标值和渐变周期,这里代码示例是,需要在2000ms,将目前的占空比渐变至LEDC_DUTY(满占空比)ledc_fade_start函数启动渐变,通过LEDC_FADE_NO_WAIT参数设置为立刻返回,那我们怎么知道渐变完成呢?可以通过ledc_cb_register函数,注册一个回调函数,当渐变完成的时候会调用我们的回调函数

1
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
void app_main(void)
{
    //创建事件组
    event_group = xEventGroupCreate();
    //配置GPIO
    gpio_config_t io_conf;
    io_conf.intr_type = GPIO_INTR_DISABLE;//禁止中断
    io_conf.mode = GPIO_MODE_OUTPUT;//设置为输出模式
    io_conf.pin_bit_mask = 1ULL<<LED_PIN;//设置引脚
    io_conf.pull_down_en = 0;//禁止下拉
    io_conf.pull_up_en = 0;//禁止上拉
    gpio_config(&io_conf);//配置GPIO

    //初始化定时器
    ledc_timer_config_t ledc_timer = {
        .speed_mode = LEDC_LOW_SPEED_MODE,//低功耗模式
        .duty_resolution = LEDC_TIMER_13_BIT,//分辨率
        .timer_num = LEDC_TIMER_0,//定时器编号
        .freq_hz = 5000,//频率
        .clk_cfg = LEDC_AUTO_CLK//自动时钟
    };
    ledc_timer_config(&ledc_timer);//配置定时器
    //初始化通道
    ledc_channel_config_t ledc_channel = {
        .gpio_num = LED_PIN,//GPIO引脚
        .speed_mode = LEDC_LOW_SPEED_MODE,//低功耗模式
        .channel = LEDC_CHANNEL_0,//通道编号
        .intr_type = LEDC_INTR_DISABLE,//禁止中断
        .timer_sel = LEDC_TIMER_0,//定时器编号
        .duty = 0,//占空比
    };
    ledc_channel_config(&ledc_channel);//配置通道
    ledc_fade_func_install(0);//安装fade函数
    ledc_set_fade_with_time(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 8191, 2000);//设置占空比和时间
    ledc_fade_start(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, LEDC_FADE_NO_WAIT);//启动fade函数

    //注册fade回调函数
    ledc_cbs_t cbs = {
        .fade_cb = lcd_finish_cb //fade回调函数
    };
    ledc_cb_register(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, &cbs, NULL);

  
    //创建任务
    xTaskCreatePinnedToCore(LED_run_task, "LED_run_task", 2048, NULL, 1, NULL, 0);
}

下面便是渐变完成后所执行的回调函数,用于通知下一轮的渐变

1
2
3
4
5
6
7
8
9
10
11
12
13
bool IRAM_ATTR lcd_finish_cb(const ledc_cb_param_t *param, void *user_arg){
    BaseType_t xHigherPriorityTaskWoken;
    if (param->duty)
    {
        //占空比满了
        xEventGroupSetBitsFromISR(event_group, NUM_BITS0,&xHigherPriorityTaskWoken);
    }else
    {   
        //占空比为0
        xEventGroupSetBitsFromISR(event_group, NUM_BITS1,&xHigherPriorityTaskWoken);
    }
    return xHigherPriorityTaskWoken;
}

LED任务用于重新初始化下一轮的渐变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void LED_run_task(void *pvParameters){
    EventBits_t uxBits;
    while(1){
        //等待事件组的位
        uxBits = xEventGroupWaitBits(event_group, NUM_BITS0 | NUM_BITS1, pdTRUE, pdFALSE, portMAX_DELAY);

        //根据事件组的位执行相应的操作
        //重新开启fade函数
        if (uxBits & NUM_BITS0){
            ledc_set_fade_with_time(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 0, 2000);
            ledc_fade_start(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, LEDC_FADE_NO_WAIT);//启动fade函数
        }
        if (uxBits & NUM_BITS1){
            ledc_set_fade_with_time(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 8191, 2000);
            ledc_fade_start(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, LEDC_FADE_NO_WAIT);//启动fade函数
        }

        ledc_cbs_t cbs = {
            .fade_cb = lcd_finish_cb //fade回调函数
        };
        ledc_cb_register(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, &cbs, NULL);

    }
}

再进阶——ws2812全彩灯

WS2812 是集成控制单元以及RGB灯珠的器件,集成度高,功能强大,并且可以串接。
WS2812 将控制电路和 RGB 灯珠集成在一个 5050 封装的元器件中,每个灯珠即为一个独立的像素点,能够实现红(R)、绿(G)、蓝(B)三基色 256 级灰度显示,可组合出 1677 万种颜色,能实现丰富多样的灯光效果。
WS2812 通过单总线接收控制信号,数据传输采用特定的编码格式。每个灯珠接收到数据后,会提取出属于自己的 24 位数据(分别为 8 位绿色、8 位红色、8 位蓝色),然后将剩余的数据转发给下一个灯珠。控制信号的高低电平持续时间决定了传输的数据是 0 还是 1。

  • 逻辑 0:高电平持续时间约 0.35μs,低电平持续时间约 0.8μs。
  • 逻辑 1:高电平持续时间约 0.7μs,低电平持续时间约 0.6μs。

该内容为进阶选学内容,下面我将给出丰富注释的示例程序,以供参考。

主函数内容,主要为创建任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "ws2812.h"
#include "driver/rmt_tx.h"

//彩灯任务
void WS2812_run_task(void *pvParameters){
    while (1){
        ws2812_coloful(50);
    }
}

void app_main(void)
{
    //初始化ws2812
    ws2812_init();
    //创建任务
    xTaskCreatePinnedToCore(WS2812_run_task, "WS2812_run_task", 2048, NULL, 1, NULL, 0);
}

.h文件,主要为函数声明和宏定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifndef __WS2812_H__
#define __WS2812_H__
#include "driver/rmt_tx.h"
#include <stdint.h>
#include "esp_check.h"


#define READ 0xFF0000//红色
#define GREEN 0x00FF00//绿色
#define BLUE 0x0000FF//蓝色
#define YELLOW 0xFFFF00//黄色
#define CYAN 0x00FFFF//紫色


typedef struct {
    uint32_t resolution; //设置编码器分辨率
} led_strip_encoder_config_t;

void ws2812_init(void);
void ws2812_one_colour(uint32_t colour);
void ws2812_coloful(uint8_t light);
esp_err_t rmt_new_led_strip_encoder(const led_strip_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder);

.c文件的变量定义

1
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
29
#include "ws2812.h"
#include "esp_check.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <string.h>

#define WS2812_PIN GPIO_NUM_8// 控制WS2812 灯珠的 GPIO 引脚
#define WS2812_NUM 1        // 灯珠的数量
#define WS2812_SPEED 20 // 灯珠颜色变换速度

// 定义 LED 灯带编码器的标签,用于日志记录
static const char *TAG = "led_encoder";
// 定义 LED 灯带的像素数据数组,每个像素由 3 个字节表示 RGB 颜色
static uint8_t led_strip_pixels[WS2812_NUM * 3];
static rmt_channel_handle_t led_chan = NULL;// 定义 RMT 通道句柄,用于控制 RMT 模块
static rmt_encoder_handle_t led_encoder = NULL;// 定义RMT编码器句柄
static rmt_transmit_config_t tx_config = {};// 定义RMT传输参数


// 定义 LED 灯带编码器的配置结构体
typedef struct {
    rmt_encoder_t base;  // 基础编码器结构体,作为当前编码器的基类,包含通用的编码器操作函数指针等
    rmt_encoder_t *bytes_encoder;  // 指向字节编码器的指针,用于将字节数据编码为 RMT 符号
    rmt_encoder_t *copy_encoder;  // 指向复制编码器的指针,用于复制特定的 RMT 符号
    int state;  // 编码器的状态变量,用于记录当前编码过程所处的阶段
    rmt_symbol_word_t reset_code;  // 重置信号的 RMT 符号,用于在发送数据后发送重置信号
} rmt_led_strip_encoder_t;
// 函数声明
static void led_strip_hsv2rgb(uint32_t h, uint32_t s, uint32_t v, uint32_t *r, uint32_t *g, uint32_t *b);

.c文件的函数

1
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
/// @brief ws2812初始化
/// @param  
void ws2812_init(void)
{   
    // 配置RMT TX通道参数
    rmt_tx_channel_config_t tx_chan_config = {
        .clk_src = RMT_CLK_SRC_DEFAULT, // 选择默认的源时钟
        .gpio_num = WS2812_PIN, // 设置GPIO引脚号
        .mem_block_symbols = 64, // 增加内存块大小可减少LED闪烁
        .resolution_hz = 10000000, // 设置分辨率
        .trans_queue_depth = 4, // 设置后台可挂起的事务数量
    };
    // 创建RMT TX通道
    ESP_ERROR_CHECK(rmt_new_tx_channel(&tx_chan_config, &led_chan));   

    // 配置LED灯带编码器参数
    led_strip_encoder_config_t encoder_config = {
        .resolution = 10000000, // 设置编码器分辨率
    }; 
    // 创建LED灯带编码器
    ESP_ERROR_CHECK(rmt_new_led_strip_encoder(&encoder_config, &led_encoder));
    // 启用RMT TX通道
    ESP_ERROR_CHECK(rmt_enable(led_chan)); 
    // 配置RMT传输参数
    tx_config.loop_count = 0; // 不进行传输循环
}

/// @brief ws2812显示单一颜色
/// @param colour 颜色
void ws2812_one_colour(uint32_t colour){
    //制作灯带的像素数据
    for (uint8_t i = 0; i < WS2812_NUM; i++)
    {
        led_strip_pixels[i * 3 + 0] = (colour >> 16) & 0xFF; // 提取绿色分量
        led_strip_pixels[i * 3 + 1] = (colour >> 8) & 0xFF; // 提取红色分量
        led_strip_pixels[i * 3 + 2] = colour & 0xFF; // 提取蓝色分量
    }
    // 将RGB值发送到LED灯带
    ESP_ERROR_CHECK(rmt_transmit(led_chan, led_encoder, led_strip_pixels, sizeof(led_strip_pixels), &tx_config));
    // 等待所有传输完成,并检查是否完成
    ESP_ERROR_CHECK(rmt_tx_wait_all_done(led_chan, portMAX_DELAY));
}

/**
 * @brief 彩虹灯
 * @param light 亮度
 */
void ws2812_coloful(uint8_t light){
    static uint32_t red = 0;
    static uint32_t green = 0;
    static uint32_t blue = 0;
    static uint16_t hue = 0;
    static uint16_t start_rgb = 0;
    for (int i = 0; i < 3; i++) {
        for (int j = i; j < WS2812_NUM; j += 3) {
            hue = j * 360 / WS2812_NUM + start_rgb;
            led_strip_hsv2rgb(hue, 100, light, &red, &green, &blue);
            led_strip_pixels[j * 3 + 0] = green;
            led_strip_pixels[j * 3 + 1] = blue;
            led_strip_pixels[j * 3 + 2] = red;
        }
        ESP_ERROR_CHECK(rmt_transmit(led_chan, led_encoder, led_strip_pixels, sizeof(led_strip_pixels), &tx_config));
        ESP_ERROR_CHECK(rmt_tx_wait_all_done(led_chan, portMAX_DELAY));
        vTaskDelay(pdMS_TO_TICKS(WS2812_SPEED));
        }
    start_rgb += 5;
}


/**
 * @brief 对 WS2812 LED 灯带数据进行编码
 * 
 * 该函数负责将输入的 RGB 数据编码为适合 RMT 通道发送的信号,并在数据发送完成后发送重置信号。
 * 
 * @param encoder 编码器句柄,指向 rmt_encoder_t 结构体
 * @param channel RMT 通道句柄,用于发送编码后的数据
 * @param primary_data 指向待编码的 RGB 数据的指针
 * @param data_size 待编码的 RGB 数据的大小(字节)
 * @param ret_state 指向编码状态变量的指针,用于返回编码的最终状态
 * @return size_t 编码生成的 RMT 符号数量
 */
static size_t rmt_encode_led_strip(rmt_encoder_t *encoder, rmt_channel_handle_t channel, const void *primary_data, size_t data_size, rmt_encode_state_t *ret_state)
{
    // 通过基类指针获取 rmt_led_strip_encoder_t 结构体实例
    rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
    // 获取字节编码器句柄    
    rmt_encoder_handle_t bytes_encoder = led_encoder->bytes_encoder;
    // 获取复制编码器句柄
    rmt_encoder_handle_t copy_encoder = led_encoder->copy_encoder;
    // 初始化会话编码状态为重置状态
    rmt_encode_state_t session_state = RMT_ENCODING_RESET;
    // 初始化最终编码状态为重置状态    
    rmt_encode_state_t state = RMT_ENCODING_RESET;
    // 初始化编码生成的 RMT 符号数量为 0
    size_t encoded_symbols = 0;
    // 根据编码器的当前状态进行不同的编码操作    
    switch (led_encoder->state) {
    case 0: //// 发送 RGB 数据
        // 调用字节编码器对 RGB 数据进行编码,并累加生成的 RMT 符号数量
        encoded_symbols += bytes_encoder->encode(bytes_encoder, channel, primary_data, data_size, &session_state);
        // 检查当前编码会话是否完成
        if (session_state & RMT_ENCODING_COMPLETE) {
            // 若完成,切换到下一个状态(发送重置信号)
            led_encoder->state = 1;
        }
        // 检查 RMT 内存是否已满
        if (session_state & RMT_ENCODING_MEM_FULL) {
            // 若已满,标记最终状态为内存已满
            state |= RMT_ENCODING_MEM_FULL;
            // 跳转到退出标签,结束本次编码操作            
            goto out; 
        }
        
    case 1: // 发送重置信号
        // 调用复制编码器发送重置信号,并累加生成的 RMT 符号数量
        encoded_symbols += copy_encoder->encode(copy_encoder, channel, &led_encoder->reset_code,
                                                sizeof(led_encoder->reset_code), &session_state);
        // 检查重置信号是否发送完成                                                
        if (session_state & RMT_ENCODING_COMPLETE) {
            // 标记最终状态为编码完成            
            led_encoder->state = RMT_ENCODING_RESET; 
            // 标记最终状态为编码完成
            state |= RMT_ENCODING_COMPLETE;
        }
        // 检查 RMT 内存是否已满
        if (session_state & RMT_ENCODING_MEM_FULL) {
            // 若已满,标记最终状态为内存已满
            state |= RMT_ENCODING_MEM_FULL;
            // 跳转到退出标签,结束本次编码操作
            goto out; 
        }
    }
out:
    // 将最终编码状态返回给调用者
    *ret_state = state;
    // 返回编码生成的 RMT 符号数量    
    return encoded_symbols;
}


/**
 * @brief 删除 WS2812 LED 灯带编码器并释放相关资源
 * 
 * 该函数用于删除指定的 WS2812 LED 灯带编码器,会依次删除其内部的字节编码器和复制编码器,
 * 最后释放 LED 灯带编码器本身占用的内存。
 * 
 * @param encoder 指向基础编码器结构体的指针,用于获取实际的 LED 灯带编码器实例
 * @return esp_err_t 操作结果,成功时返回 ESP_OK
 */
static esp_err_t rmt_del_led_strip_encoder(rmt_encoder_t *encoder)
{
    // 通过基类指针获取 rmt_led_strip_encoder_t 结构体实例    
    rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
    rmt_del_encoder(led_encoder->bytes_encoder);// 删除字节编码器
    rmt_del_encoder(led_encoder->copy_encoder);// 删除复制编码器
    free(led_encoder);// 释放 LED 灯带编码器本身占用的内存
    return ESP_OK;
}


/**
 * @brief 重置 WS2812 LED 灯带编码器的状态
 * 
 * 该函数用于将指定的 WS2812 LED 灯带编码器重置到初始状态,
 * 会依次重置内部的字节编码器和复制编码器,并将编码器的状态变量置为初始值。
 * 
 * @param encoder 指向基础编码器结构体的指针,用于获取实际的 LED 灯带编码器实例
 * @return esp_err_t 操作结果,成功时返回 ESP_OK
 */
static esp_err_t rmt_led_strip_encoder_reset(rmt_encoder_t *encoder)
{
    // 通过基类指针获取 rmt_led_strip_encoder_t 结构体实例    
    rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
    // 重置字节编码器,将其状态恢复到初始状态    
    rmt_encoder_reset(led_encoder->bytes_encoder);
    // 重置复制编码器,将其状态恢复到初始状态
    rmt_encoder_reset(led_encoder->copy_encoder);
    // 将 LED 灯带编码器的状态变量设置为初始状态    
    led_encoder->state = RMT_ENCODING_RESET;
    return ESP_OK;
}


/**
 * @brief 创建一个新的 WS2812 LED 灯带编码器。
 *
 * 此函数用于分配并初始化一个新的 LED 灯带编码器,该编码器可用于将 RGB 数据编码为适合 WS2812 LED 灯带的信号。
 *
 * @param config 指向 LED 灯带编码器配置结构体的指针,包含编码器的配置参数。
 * @param ret_encoder 指向编码器句柄指针的指针,用于返回新创建的编码器句柄。
 * @return esp_err_t 操作结果,ESP_OK 表示成功,其他错误码表示失败。
 */
esp_err_t rmt_new_led_strip_encoder(const led_strip_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder)
{
    // 初始化返回值为成功状态
    esp_err_t ret = ESP_OK;
    // 定义一个指向 LED 灯带编码器结构体的指针
    rmt_led_strip_encoder_t *led_encoder = NULL;
    // 检查输入参数是否有效,如果 config 或 ret_encoder 为 NULL,则跳转到错误处理标签 err
    ESP_GOTO_ON_FALSE(config && ret_encoder, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
    // 为 LED 灯带编码器分配内存
    led_encoder = rmt_alloc_encoder_mem(sizeof(rmt_led_strip_encoder_t));
    // 检查是否分配成功,如果失败,则跳转到错误处理标签 err
    ESP_GOTO_ON_FALSE(led_encoder, ESP_ERR_NO_MEM, err, TAG, "no mem for led strip encoder");
    // 初始化 LED 灯带编码器结构体的成员变量
    // 设置编码器的编码函数
    led_encoder->base.encode = rmt_encode_led_strip;
    // 设置编码器的删除函数
    led_encoder->base.del = rmt_del_led_strip_encoder;
    // 设置编码器的重置函数
    led_encoder->base.reset = rmt_led_strip_encoder_reset;
    // 不同的 LED 灯带可能有不同的时序要求,以下参数是针对 WS2812 的
    rmt_bytes_encoder_config_t bytes_encoder_config = {
        .bit0 = {//编码0的时序
            .level0 = 1,
            .duration0 = 0.3 * config->resolution / 1000000, // T0H=0.3us
            .level1 = 0,
            .duration1 = 0.9 * config->resolution / 1000000, // T0L=0.9us
        },
        .bit1 = {//编码1的时序
            .level0 = 1,
            .duration0 = 0.9 * config->resolution / 1000000, // T1H=0.9us
            .level1 = 0,
            .duration1 = 0.3 * config->resolution / 1000000, // T1L=0.3us
        },
        .flags.msb_first = 1 // WS2812 传输位顺序: G7...G0R7...R0B7...B0
    };
    // 创建字节编码器,如果失败则跳转到错误处理标签 err
    ESP_GOTO_ON_ERROR(rmt_new_bytes_encoder(&bytes_encoder_config, &led_encoder->bytes_encoder), err, TAG, "create bytes encoder failed");
    // 初始化复制编码器配置
    rmt_copy_encoder_config_t copy_encoder_config = {};
    // 创建复制编码器,如果失败则跳转到错误处理标签 err
    ESP_GOTO_ON_ERROR(rmt_new_copy_encoder(&copy_encoder_config, &led_encoder->copy_encoder), err, TAG, "create copy encoder failed");
    // 计算重置信号的时钟周期数,默认重置信号持续时间为 50us
    uint32_t reset_ticks = config->resolution / 1000000 * 50 / 2; 
    // 设置重置信号的符号字
    led_encoder->reset_code = (rmt_symbol_word_t) {
        .level0 = 0,
        .duration0 = reset_ticks,
        .level1 = 0,
        .duration1 = reset_ticks,
    };
    // 将新创建的编码器句柄返回给调用者    
    *ret_encoder = &led_encoder->base;
    return ESP_OK;
    // 错误处理标签
err:
    if (led_encoder) {
        // 如果字节编码器存在,则删除字节编码器
        if (led_encoder->bytes_encoder) {
            rmt_del_encoder(led_encoder->bytes_encoder);
        }
        // 如果复制编码器存在,则删除复制编码器        
        if (led_encoder->copy_encoder) {
            rmt_del_encoder(led_encoder->copy_encoder);
        }
        free(led_encoder);
    }
    return ret;
}

/**
 * @brief 将 HSV(色相、饱和度、明度)颜色空间转换为 RGB(红、绿、蓝)颜色空间。
 * 
 * 该函数接收 HSV 颜色模型的三个参数,通过一系列计算将其转换为 RGB 颜色模型的三个参数。
 * HSV 颜色模型更符合人类对颜色的感知,而 RGB 颜色模型常用于数字显示设备。
 * 
 * @param h 色相,取值范围为 0 到 360,表示颜色的种类,例如 0 代表红色,120 代表绿色,240 代表蓝色。
 * @param s 饱和度,取值范围为 0 到 100,表示颜色的纯度,值越大颜色越鲜艳。
 * @param v 明度,取值范围为 0 到 100,表示颜色的明亮程度,值越大颜色越亮。
 * @param r 指向存储转换后红色分量的指针,取值范围为 0 到 255。
 * @param g 指向存储转换后绿色分量的指针,取值范围为 0 到 255。
 * @param b 指向存储转换后蓝色分量的指针,取值范围为 0 到 255。
 */
static void led_strip_hsv2rgb(uint32_t h, uint32_t s, uint32_t v, uint32_t *r, uint32_t *g, uint32_t *b)
{
    h %= 360; // 将色相值限制在 0 到 360 的范围内
    // 计算 RGB 颜色的最大值,将明度值从 0-100 转换为 0-255 范围
    uint32_t rgb_max = v * 2.55f;
    // 计算 RGB 颜色的最小值,根据饱和度调整颜色纯度
    uint32_t rgb_min = rgb_max * (100 - s) / 100.0f;

    // 计算色相所在的 6 个区域中的哪一个,每个区域 60 度
    uint32_t i = h / 60;
    // 计算当前色相在所在区域内的偏移量
    uint32_t diff = h % 60;

    // 根据色相偏移量计算 RGB 颜色的调整值
    uint32_t rgb_adj = (rgb_max - rgb_min) * diff / 60;

    // 根据色相所在的区域,计算对应的 RGB 颜色分量
    switch (i) {
    case 0: // 色相在 0 到 60 度之间,红色为主
        *r = rgb_max;
        *g = rgb_min + rgb_adj;
        *b = rgb_min;
        break;
    case 1: // 色相在 60 到 120 度之间,绿色和红色混合
        *r = rgb_max - rgb_adj;
        *g = rgb_max;
        *b = rgb_min;
        break;
    case 2: // 色相在 120 到 180 度之间,绿色为主
        *r = rgb_min;
        *g = rgb_max;
        *b = rgb_min + rgb_adj;
        break;
    case 3: // 色相在 180 到 240 度之间,蓝色和绿色混合
        *r = rgb_min;
        *g = rgb_max - rgb_adj;
        *b = rgb_max;
        break;
    case 4: // 色相在 240 到 300 度之间,蓝色为主
        *r = rgb_min + rgb_adj;
        *g = rgb_min;
        *b = rgb_max;
        break;
    default: // 色相在 300 到 360 度之间,红色和蓝色混合
        *r = rgb_max;
        *g = rgb_min;
        *b = rgb_max - rgb_adj;
        break;
    }
}