ESP32开发相关链接

开发ESP32可以采用IDF和Arduino IDE进行开发。为了进一步学习LVGL图形库和FreeRTOS,我选择使用乐鑫IDF进行开发。IDT开发官方文档可见快速入门

一些idf常用命令

idf工具主要调用idf.py这个python脚本,通过传递不同的参数实现不同功能,如交叉编译等。为了终端可以调用该指令需要将其添加路径到系统环境变量中,Linux系统下在目录下运行shell脚本$ ./source export.sh即可自动添加系统环境变量。

创建一个空工程

其中my_project为自定义工程名,该命令会创建一个空工程,并生成工程文件。

1
$ idf.py create-project my_project

设置芯片类型

  • ESP32系列模组:idf.py set-target esp32
  • ESP32-S2系列模组:idf.py set-target esp32s2
  • ESP32-C3系列模组:idf.py set-target esp32c3
  • ESP32-S3系列模组:idf.py set-target esp32s3

这里我们以ESP32-S3系列模组为例,在终端内运行命令行

1
$ idf.py set-target ESP32s3

idf自动切换为S3型号。

构建&清除工程

构建指令如下:

1
$ idf.py build

由于芯片具有一定复杂度,编译中产生大量中间文件,编译后工程内存占用高达百兆不利于传递存储。清除编译中间文件指令如下:

1
$ idf.py clean

下载&调试工程

下载指令如下:

1
$ idf.py flash

可以使用下面指令监视串口输出,PORT为串口设备,如/dev/ttyUSB0。

1
$ idf.py -p PORT monitor

也可以直接调试工程,使用下面指令:

1
$ idf.py monitor

特别注意Linux终端退出需要按Ctrl+[, 其他什么都不好使。

FreeRTOS基础

由于ESP32的芯片具有多核特性(如ESP32s3为基于Tensilica Xtensa LX6架构双核芯片),主核和协处理器分别采用240MHz和160MHz的频率。为了更好的分配任务,所以IDF使用FreeRTOS操作系统进行多线程编程。
一个任务可以为以下几种状态:

  • 运行(Running) :如果处理器为单核,同一时刻只有一个任务被执行。
  • 准备状态 :任务处于能够执行但是没有执行的状态,原因为操作系统在执行一些高优先级的任务。
  • 阻塞(Blocked) :任务正在等待时间或者外部事件,如调用vTaskDelay()延时。任务也可以通过阻塞来等待队列、信号量、事件组、通知或信号量事件。处于阻塞状态的任务通常有一个”超时”期, 超时后任务将被超时,并被解除阻塞。
  • 挂起(Suspended) :与“阻塞”状态下的任务一样, “挂起”状态下的任务不能 被选择进入运行状态,但处于挂起状态的任务没有超时。相反,任务只有在分别通过 vTaskSuspend()xTaskResume()API 调用明确命令时 才会进入或退出挂起状态。

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

FreeRTOS创建任务

如下是创建一个乐鑫官方移植的任务创建函数xTaskCreatePinnedToCor(推荐使用)它有7个参数,参数为任务函数指针、任务名称、堆栈大小、任务参数、优先级、任务句柄、内核号。

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

如果任务创建成功则返回pdPASS,否则返回pdFAIL。
创建函数还可以是xTaskCreate(内部还是使用了xTaskCreatePinnedToCore但是会自动分配内核)函数及参数如下:

1
2
3
4
5
6
7
8
BaseType_t xTaskCreate(
TaskFunction_t pxTaskCode,//指向任务函数的指针。
const char * const pcName,//任务的名称(用于调试)。
configSTACK_DEPTH_TYPE usStackDepth,//任务堆栈的深度(以堆栈项为单位)。
void * pvParameters,//传递给任务函数的参数。
UBaseType_t uxPriority,//任务的优先级
TaskHandle_t * pxCreatedTask//用于存储创建的任务句柄的指针(可以为 NULL)。
);

直接创建任务和静态任务的区别

在FreeRTOS中,直接创建任务(通常指的是使用xTaskCreate函数动态创建任务)和创建静态任务(使用xTaskCreateStatic函数)之间存在几个关键区别。以下是对这两者的详细比较:
1. 内存分配方式

  • 直接创建任务(动态创建):内存是在运行时通过动态内存分配函数(如pvPortMalloc)来分配的。创建任务时,不需要预先定义和初始化额外的变量来存储任务信息。任务的相关信息直接存储在动态分配的内存中。动态创建任务的内存空间可以在任务完成后释放,因此可以在运行时动态地创建和删除任务。
  • 创建静态任务:内存是在编译时分配的,需要定义并初始化一个StaticTask_t类型的变量来存储任务的相关信息。静态创建任务的内存空间在任务整个运行期间都被任务所占用,直到任务被删除。静态创建任务通常在应用程序的启动阶段进行,任务的数量是固定的,无法在运行时动态调整。
    2. 内存管理
  • 直接创建任务:由于内存是动态分配的,因此需要在任务完成后手动释放内存(尽管FreeRTOS本身可能并不直接提供释放任务内存的API,但可以通过删除任务来间接释放内存)。动态内存分配可能增加内存碎片化的风险。
  • 创建静态任务:内存是静态分配的,因此不需要担心内存碎片化和手动释放内存的问题。但由于内存是在编译时分配的,因此需要预先知道任务所需的内存大小。
    3. 灵活性
  • 直接创建任务:提供了更高的灵活性,因为可以在运行时根据需要动态地创建和删除任务。适用于任务数量可能会根据系统状态或用户交互而动态变化的应用场景。
  • 创建静态任务:灵活性较低,因为任务的数量和内存大小在编译时就已确定。但由于内存是静态分配的,因此可以提供更稳定的内存使用模式,并减少运行时内存分配的开销。
    1. 使用场景
  • 直接创建任务: 适用于需要动态调整任务数量和类型的系统,如事件驱动系统、多协议通信系统或并行处理系统。
  • 创建静态任务:适用于任务数量和类型在编译时就已确定的系统,或对内存使用有严格要求的应用场景。

创建静态任务的函数

乐鑫官方移植的FreeRTOS提供了创建静态任务的函数xTaskCreateStaticPinnedToCore,它需要用户提前分配好任务栈和任务控制块的内存,然后调用该函数创建静态任务。

1
2
3
4
5
6
7
8
9
10
BaseType_t xTaskCreateStaticPinnedToCore(
TaskFunction_t pxTaskCode, // 指向任务函数的指针,任务函数是任务执行的具体逻辑
const char * const pcName, // 任务的名称,用于调试和识别任务
const uint32_t ulStackDepth, // 任务堆栈的大小,以字为单位(通常是4字节)
void * pvParameters, // 传递给任务函数的参数
UBaseType_t uxPriority, // 任务的优先级,优先级较高的任务会优先执行
StackType_t * pxStackBuffer, // 指向任务堆栈缓冲区的指针,缓冲区必须由调用者分配
StaticTask_t * pxStaticTaskBuffer, // 指向静态任务控制块(TCB)的指针,结构体用于存储任务的相关信息,必须由调用者分配
BaseType_t xCoreID // 指定任务运行的 CPU 核心 ID,0 或 1
);

FreeRTOS的创建函数:

1
2
3
4
5
6
7
8
9
BaseType_t xTaskCreateStatic(
TaskFunction_t pxTaskCode, // 指向任务函数的指针,任务函数是任务执行的具体逻辑
const char * const pcName, // 任务的名称,用于调试和识别任务
const uint32_t ulStackDepth, // 任务堆栈的大小,以字为单位(通常是4字节)
void * pvParameters, // 传递给任务函数的参数
UBaseType_t uxPriority, // 任务的优先级,优先级较高的任务会优先执行
StackType_t * pxStackBuffer, // 指向任务堆栈缓冲区的指针,缓冲区必须由调用者分配
StaticTask_t * pxStaticTaskBuffer // 指向静态任务控制块(TCB)的指针,结构体用于存储任务的相关信息,必须由调用者分配
);

FreeRTOS任务创建案例

如下创建两个任务并打印相关的数据:

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
#include <stdio.h>
#include "freertos/FreeRTOS.h"//只要使用FreeRTOS就需要包含该文件,
#include "freertos/task.h"//创建任务有关的函数
#include "esp_log.h"//用于调试的功能,通过串口打印数据

/*任务A函数*/
void TaskA(void* param) //void*是一种特殊的指针类型,被称为“无类型指针”或“通用指针”。
{
uint16_t i = 0;
while(1)
{
if(i<100) i++;
else i = 0;
ESP_LOGI("TaskA","Timer_%d",i);
vTaskDelay(pdMS_TO_TICKS(500));//pdMS_TO_TICKS(500)将系统节拍转换成具体时间
}

}
/*任务B函数*/
void TaskB(void* param) //void*是一种特殊的指针类型,被称为“无类型指针”或“通用指针”。
{
uint16_t i = 0;
while(1)
{
if(i<100) i++;
else i = 0;
ESP_LOGI("TaskB","Timer_%d",i);
vTaskDelay(pdMS_TO_TICKS(2000));//pdMS_TO_TICKS(500)将系统节拍转换成具体时间
}

}

void app_main(void)
{
xTaskCreatePinnedToCore(TaskA, "TaskA", 2048, NULL, 1, NULL, 1);//栈空间最小2048
xTaskCreatePinnedToCore(TaskB, "TaskB", 2048, NULL, 2, NULL, 1);//栈空间最小2048
}

FreeRTOS任务阻塞延时

FreeRTOS提供了两种阻塞延时函数:

1
2
//延时 xTicksToDelay 个周期
void vTaskDelay( const TickType_t xTicksToDelay );
1
void vTaskDelayUntil( const TickType_t *pxPreviousWakeTime, const TickType_t xTimeIncrement );

参数说明const TickType_t *pxPreviousWakeTime: 指向上次任务唤醒时间的指针。这个变量应该在第一次调用 vTaskDelayUntil 之前初始化为当前时间。const TickType_t xTimeIncrement: 任务每次执行之间的时间间隔,以滴答数(ticks)为单位。
使用步骤:初始化 pxPreviousWakeTime: 在任务第一次执行时,需要初始化 pxPreviousWakeTime 为当前时间。调用 vTaskDelayUntil: 在每次任务执行结束时,调用vTaskDelayUntil函数,传入pxPreviousWakeTimexTimeIncrement
示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定义任务函数
void vPeriodicTask(void *pvParameters) {
TickType_t xLastWakeTime;
const TickType_t xFrequency = 1000 / portTICK_PERIOD_MS; // 1000 ms (1 second)

// 初始化 xLastWakeTime 为当前时间
xLastWakeTime = xTaskGetTickCount();

while (1) {
// 任务执行的逻辑
// 例如:打印一条消息
printf("Task is running\n");

// 延迟直到下次唤醒时间
vTaskDelayUntil(&xLastWakeTime, xFrequency);
}
}

FreeRTOS任务同步

RTOS中的同步 :是指是不同任务之间或者任务与外部事件之间的协同工作方式,确保多个并发执行的任务按照预期的顺序或时机执行。它涉及到线程或任务间的通信和协调机制,目的是为了避免数据竞争、解决竞态条件,并确保系统的正确行为。
互斥 :某一资源同时只允许一个线程或任务访问,其他线程或任务必须等待。具有唯一性和排他性

队列

队列是任务间通信的主要形式。 它们可以用于在任务之间以及中断和任务之间发送消息。 在大多数情况下,它们作为线程安全的 FIFO(先进先出)缓冲区使用,新数据被发送到队列的后面, 尽管数据也可以发送到前面。尾部进入,头部读出
QueueHandle_t为队列的句柄类型,本质上是一个指针。常用API函数如下:

1
2
3
4
QueueHandle_t xQueueCreate( //创建一个队列,成功返回队列句柄
UBaseType_t uxQueueLength, //队列容量
UBaseType_t uxItemSize //每个队列项所占内存的大小(单位是字节)
);
1
2
3
4
BaseType_t xQueueSend( //向队列头部发送一个消息
QueueHandle_t xQueue, // 队列句柄
const void * pvItemToQueue, //要发送的消息指针
TickType_t xTicksToWait ); //等待时间
1
2
3
4
BaseType_t xQueueSendToBack( //向队列尾部发送一个消息
QueueHandle_t xQueue, // 队列句柄
const void * pvItemToQueue, //要发送的消息指针
TickType_t xTicksToWait ); //等待时间
1
2
3
4
BaseType_t xQueueReceive( //从队列接收一条消息
QueueHandle_t xQueue, //队列句柄
void * pvBuffer, //指向接收消息缓冲区的指针。
TickType_t xTicksToWait ); //等待时间
1
2
3
4
BaseType_t xQueueSendFromISR( //xQueueSend 的中断版本
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
42
43
44
45
46
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_log.h"
#include "string.h"

QueueHandle_t my_queue = NULL;//创建队列指针

typedef struct{ //定义一个数据类型
int vslue;
int Num;
}QueueData;

void TaskA(void* param)//创建任务A,向队列中发送数据
{
QueueData Temp;
memset(&Temp, 0, sizeof(QueueData));//初始化结构体

while(1)
{
Temp.Num++;
Temp.vslue+=2;
xQueueSend(my_queue, &Temp, 100);//向队列中发送数据
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void TaskB(void* param)//创建任务B,从队列中接收数据
{
QueueData Temp;

while(1)
{
if(pdTRUE == xQueueReceive(my_queue, &Temp, 100))//从队列中接收数据
{
ESP_LOGI("TaskB_Receive", "Value = %d, Num = %d。",Temp.vslue, Temp.Num);
}
}
}

void app_main(void)
{
my_queue = xQueueCreate(10, sizeof(QueueData));//初始化队列
xTaskCreatePinnedToCore(TaskA, "TaskA", 2048, NULL, 1, NULL, 1);//创建任务
xTaskCreatePinnedToCore(TaskB, "TaskB", 2048, NULL, 3, NULL, 1);
}

信号量

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

  • 二进制信号量很简单,就是信号量总数只有 1,也就是这个图中总雨伞数量只有 1。
  • 计数信号量则可以自定义总共的信号量

信号量的句柄SemaphoreHandle_t,常用API函数如下:

1
2
//创建二值信号量,成功则返回信号量句柄(二值信号量最大只有 1 个)
SemaphoreHandle_t xSemaphoreCreateBinary( void );
1
2
3
4
//创建计数信号量,成功则返回信号量句柄
SemaphoreHandle_t xSemaphoreCreateCounting(
UBaseType_t uxMaxCount, //最大信号量数
UBaseType_t uxInitialCount); //初始信号量数
1
2
3
4
//获取一个信号量,如果获得信号量,则返回 pdTRUE
xSemaphoreTake(
SemaphoreHandle_t xSemaphore,//信号量句柄
TickType_t xTicksToWait ); //等待时间
1
2
//释放一个信号量,及放回信号量,成功返回 pdTRUE
xSemaphoreGive( SemaphoreHandle_t xSemaphore ); //信号量句柄
1
2
//删除信号量
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
30
31
32
33
34
35
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"//信号量有关函数
#include "freertos/task.h"
#include "esp_log.h"

SemaphoreHandle_t My_sem;//定义信号量的句柄

void TaskA(void* param)//任务A负责发送信号量
{
while (1)
{
vTaskDelay(pdMS_TO_TICKS(1000));
xSemaphoreGive(My_sem); //信号量句柄
}

}
void TaskB(void* param)//任务B接受到信号量之后打印标记
{
while (1)
{
if(pdTRUE == xSemaphoreTake(My_sem, 100))
{
ESP_LOGI("TaskB", "sem is OK");
}
}

}

void app_main(void)
{
My_sem = xSemaphoreCreateBinary();//初始化句柄
xTaskCreatePinnedToCore(TaskA, "TaskA", 2048, NULL, 1, NULL, 1);
xTaskCreatePinnedToCore(TaskB, "TaskB", 2048, NULL, 2, NULL, 1);
}

互斥锁

互斥锁:与二进制信号量类似,但会发生优先级翻转。保证优先级一致性。
互斥锁的句柄SemaphoreHandle_t和信号量一样,常用API函数如下:

1
2
//创建一个互斥锁
SemaphoreHandle_t xSemaphoreCreateMutex( void );
1
2
3
4
//互斥量获取函数,成功则返回 pdTRUE
xSemaphoreTake(
SemaphoreHandle_t xSemaphore,//互斥锁句柄
TickType_t xTicksToWait ); //等待时间

获取函数的TickType_t xTicksToWait最好填写portMAX_DELAY无限延时,这样当任务A获取互斥锁后,任务B就无法获取锁了。

1
2
//互斥量释放函数 
xSemaphoreGive( SemaphoreHandle_t xSemaphore ); //互斥锁句柄
1
2
//删除互斥锁
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
30
31
32
33
34
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"//互斥锁有关函数
#include "freertos/task.h"
#include "esp_log.h"

SemaphoreHandle_t My_Mutex;//定义互斥锁的句柄

void TaskA(void* param)//任务A负责发送信号量
{
while (1)
{
xSemaphoreTake(My_Mutex, portMAX_DELAY);//获取互斥锁 //
ESP_LOGI("TaskA", "Mutex is OK"); //互斥锁保护区域
xSemaphoreGive(My_Mutex); //释放互斥锁 //
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
void TaskB(void* param)//任务B接受到信号量之后打印标记
{
while (1)
{
xSemaphoreTake(My_Mutex, portMAX_DELAY);//获取互斥锁 //
ESP_LOGI("TaskB", "Mutex is OK"); //互斥锁保护区域
xSemaphoreGive(My_Mutex); //释放互斥锁 //
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main(void)
{
My_Mutex = xSemaphoreCreateMutex();//初始化互斥锁句柄
xTaskCreatePinnedToCore(TaskA, "TaskA", 2048, NULL, 1, NULL, 1);
xTaskCreatePinnedToCore(TaskB, "TaskB", 2048, NULL, 2, NULL, 1);
}

FreeRTOS事件组

事件位:用于指示事件是否发生,事件位通常称为事件标志。
事件组:就是一组事件位,事件组中的事件位通过位编号来引用。
下图表示一个 24 位事件组, 使用 3 个位来保存前面描述的 3 个示例事件
EventGroup
接收事件时,可以根据感兴趣的参事件类型接收事件的单个或者多个事件类型。事件接收成功后,必须使用xClearOnExit选项来清除已接收到的事件类型,否则不会清除已接收 到的 事件 ,这样就需要用户显式清除事位。
函数句柄EventGroupHandle_t,相关API函数如下:

1
2
//创建一个事件组,返回事件组句柄,失败返回 NULL
EventGroupHandle_t xEventGroupCreate( void );
1
2
3
4
5
6
7
//等待事件组中某个标志位,用返回值以确定哪些位已完成设置
EventBits_t xEventGroupWaitBits(
const EventGroupHandle_t xEventGroup, //事件组句柄
const EventBits_t uxBitsToWaitFor, //哪些位需要等待
const BaseType_t xClearOnExit, //是否读取完成后自动清除标志位
const BaseType_t xWaitForAllBits, //是否等待的标志位都成功了才返回
TickType_t xTicksToWait ); //最大阻塞时间
1
2
3
4
//设置标志位
EventBits_t xEventGroupSetBits(
EventGroupHandle_t xEventGroup,//事件组句柄
const EventBits_t uxBitsToSet ); //设置哪个位
1
2
3
4
//清除标志位
EventBits_t xEventGroupClearBits(
EventGroupHandle_t xEventGroup, //事件组句柄
const EventBits_t uxBitsToClear ); //清除的标志位
1
2
3

// xEventGroup: 事件组句柄,你要删除哪个事件组
void vEventGroupDelete( EventGroupHandle_t xEventGroup )

示例代码如下

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
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_log.h"

static EventGroupHandle_t s_testEvent;//事件组句柄

void event_taskA(void* param)//事件任务A,用于定时标记事件
{
while(1)
{
xEventGroupSetBits(s_testEvent,BIT0);
vTaskDelay(pdMS_TO_TICKS(1000));
xEventGroupSetBits(s_testEvent,BIT1);
vTaskDelay(pdMS_TO_TICKS(1500));
}
}
void event_taskB(void* param)//事件任务B,等待事件组中BIT0和BIT1位
{
EventBits_t ev;
while(1)
{
ev = xEventGroupWaitBits(s_testEvent,BIT0|BIT1,pdTRUE,pdFALSE,portMAX_DELAY);
if(ev & BIT0)
{
ESP_LOGI(TAG,"Event BIT0 set");
}
if(ev& BIT1)
{
ESP_LOGI(TAG,"Event BIT1 set");
}
}
}
void rtos_event_sample(void)//事件例程初始化
{
s_testEvent = xEventGroupCreate();
xTaskCreatePinnedToCore(event_taskA,"event_taskA",2048,NULL,3,NULL,1);
xTaskCreatePinnedToCore(event_taskB,"event_taskB",2048,NULL,3,NULL,1);
}

直达任务通知

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

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

需要注意的是参数 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_log.h"

//要使用任务通知,需要记录任务句柄
static TaskHandle_t s_notifyTaskAHandle;
static TaskHandle_t s_notifyTaskBHandle;

/** 任务通知A,用于定时向任务通知B直接传输数据
* @param 无
* @return 无
*/
void notify_taskA(void* param)
{
uint32_t rec_val = 0;
while(1)
{
if (xTaskNotifyWait(0x00, ULONG_MAX, &rec_val, pdMS_TO_TICKS(1000)) == pdTRUE)
{
ESP_LOGI(TAG,"receive notify value:%lu",rec_val);
}
}
}

/** 任务通知B,实时接收任务通知A的数据
* @param 无
* @return 无
*/
void notify_taskB(void* param)
{
int notify_val = 0;
while(1)
{
xTaskNotify(s_notifyTaskAHandle, notify_val, eSetValueWithOverwrite);
notify_val++;
vTaskDelay(pdMS_TO_TICKS(1000));
}
}

/** 任务通知例程初始化
* @param 无
* @return 无
*/
void rtos_notify_sample(void)
{
xTaskCreatePinnedToCore(notify_taskA,"notify_taskA",2048,NULL,3,&s_notifyTaskAHandle,1);
xTaskCreatePinnedToCore(notify_taskB,"notify_taskB",2048,NULL,3,&s_notifyTaskBHandle,1);
}

LVGL移植

拉取相关源码

在github网站上拉取相关的源码,在终端下执行命令:

1
$git clone --recursive https://github.com/lvgl/lvgl.git

其中--recursive这是一个可选参数,它告诉Git在克隆主仓库的同时,也递归地克隆所有子模块。如果你的项目使用了Git子模块来引用其他项目,那么你需要使用这个参数来确保所有子模块也都被正确地克隆下来。
由于LVGL包含很多的版本,使用指令查看包含的版本:

1
$git tag

q退出查看,使用指令切换需要的版本v8.3.10:

1
$git checkout v8.3.10

添加屏幕驱动

将驱动文件st7789_driver.ccst816t_driver.c添加到components文件夹下,并添加CMakeLists.txt。内容如下:

1
2
3
4
5
idf_component_register(
SRCS "st7789_driver.c" "cst816t_driver.c" //添加相关文件
INCLUDE_DIRS "." //添加相关路径为当前路径
REQUIRES esp_lcd //导入依赖
)

链接lvgl接口

链接lvgl接口步骤如下:

  1. 初始化和注册LVGL显示驱动
  2. 初始化和注册LVGL触摸驱动
  3. 初始化ST7789硬件接口
  4. 初始化CST816T硬件接口
  5. 提供一个定时器给LVGL使用,作为心跳

在main目录下添加文件lv_port.clv_port.h用于链接lvgl。内容如下:
lv_port.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
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "lvgl.h"
#include "esp_log.h"
#include "st7789_driver.h"
#include "cst816t_driver.h"
#include "driver/gpio.h"
#include "esp_timer.h"

/*
1.初始化和注册LVGL显示驱动
2.初始化和注册LVGL触摸驱动
3.初始化ST7789硬件接口
4.初始化CST816T硬件接口
5.提供一个定时器给LVGL使用
*/

#define TAG "lv_port"

#define LCD_WIDTH 240
#define LCD_HEIGHT 280

static lv_disp_drv_t disp_drv;//显示驱动变量

void lv_flush_done_cb(void* param)
{
lv_disp_flush_ready(&disp_drv);//写入完成通知
}

void disp_flush(struct _lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
st7789_flush(area->x1,area->x2 + 1,area->y1+20,area->y2+20 + 1,color_p);
}

void lv_disp_init(void)
{
static lv_disp_draw_buf_t disp_buf;
const size_t disp_buf_size = LCD_WIDTH*(LCD_HEIGHT/7);

//不能使用C语言的malloc函数
lv_color_t *disp1 = heap_caps_malloc(disp_buf_size*sizeof(lv_color_t),MALLOC_CAP_INTERNAL|MALLOC_CAP_DMA);//MALLOC_CAP_INTERNAL表示从内部ram中申请 MALLOC_CAP_DMA表示需要使用DMA传输
lv_color_t *disp2 = heap_caps_malloc(disp_buf_size*sizeof(lv_color_t),MALLOC_CAP_INTERNAL|MALLOC_CAP_DMA);
if(!disp1 || !disp2)
{
ESP_LOGE(TAG,"disp buff malloc fail!");
return;
}
lv_disp_draw_buf_init(&disp_buf,disp1,disp2,disp_buf_size);//双缓存

lv_disp_drv_init(&disp_drv);//初始化显示成员

disp_drv.hor_res = LCD_WIDTH;
disp_drv.ver_res = LCD_HEIGHT;
disp_drv.draw_buf = &disp_buf;
disp_drv.flush_cb = disp_flush;//显示函数指针
lv_disp_drv_register(&disp_drv);//将显示驱动注册
}

void IRAM_ATTR indev_read(struct _lv_indev_drv_t * indev_drv, lv_indev_data_t * data)//触摸接口函数
{
int16_t x,y;
int state;
cst816t_read(&x,&y,&state);
data->point.x = x;
data->point.y = y;
data->state = state;
}

void lv_indev_init(void)//注册输入驱动
{
static lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;//输入方式--触摸
indev_drv.read_cb = indev_read;//触摸函数指针
lv_indev_drv_register(&indev_drv);
}

void st7789_hw_init(void)//初始化LCD硬件接口
{
st7789_cfg_t st7789_config = {
.cs = GPIO_NUM_39,
.bl = GPIO_NUM_45,
.dc = GPIO_NUM_38,
.clk = GPIO_NUM_18,
.mosi = GPIO_NUM_19,
.rst = GPIO_NUM_47,
.spi_fre = 40*1000*1000,
.height = LCD_HEIGHT,
.width = LCD_WIDTH,
.spin = 0,
.done_cb = lv_flush_done_cb,
.cb_param = &disp_drv,
};

st7789_driver_hw_init(&st7789_config);
}

void cst816t_hw_init(void)//初始化触摸硬件接口
{
cst816t_cfg_t cst816t_config =
{
.scl = GPIO_NUM_10,
.sda = GPIO_NUM_11,
.fre = 300*1000,
.x_limit = LCD_WIDTH,
.y_limit = LCD_HEIGHT,
};
cst816t_init(&cst816t_config);
}

void lv_timer_cb(void* arg)
{
uint32_t tick_interval = *((uint32_t*)arg);
lv_tick_inc(tick_interval);
}

void lv_tick_init(void)//初始化定时器
{
static uint32_t tick_interval = 5;
const esp_timer_create_args_t arg =
{
.arg = &tick_interval,
.callback = lv_timer_cb,//回调函数
.name = "",
.dispatch_method = ESP_TIMER_TASK,
.skip_unhandled_events = true,
};

esp_timer_handle_t timer_handle;
esp_timer_create(&arg,&timer_handle);
esp_timer_start_periodic(timer_handle,tick_interval*1000);
}

void lv_port_init(void)
{
lv_init();
st7789_hw_init();
cst816t_hw_init();
lv_disp_init();
lv_indev_init();
lv_tick_init();
}

lv_port.h

1
2
3
4
5
6
7
#ifndef _LV_PORT_H_
#define _LV_PORT_H_

void lv_port_init(void);


#endif

修改ESP32配置文件

在终端输入:

1
$ idf.py menuconfig
  1. 配置flash
    Serial flasher configuration —> Flash size —> 8MB (32Mb)

  2. 打开psram
    Component config —> ESP PSRAM

  3. 修改CUP频率
    Component config —> ESP32 System Settings —> CPU frequency —> 240MHz

  4. 配置LVGL
    Component config —> LVGL configuration —> Color setings —> Swap the 2 bytes of RGB565 color.
    Component config —> LVGL configuration —> Memory settings —> If ture use custom malloc/free.//将空间放置于psram中

ESP32基础外设

GPIO

ESP32开发小坑

自带rtos延时函数不准问题

vTaskDelay();函数使用时,要想做到延时确定的时间,需要注意使用portTICK_PERIOD_MS宏进行tick时间转换

1
vTaskDelay(1000/portTICK_PERIOD_MS);
1
vTaskDelay(pdMS_TO_TICKS(500));