S1-02 FreeRTOS线程控制

2024-01-11 16:20
文章标签 线程 02 控制 freertos s1

本文主要是介绍S1-02 FreeRTOS线程控制,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

回顾

上节课讲到Android的安装,以及在Android上运行普通的Super Loop程序,也初步一睹FreeRTOS的风采。
通过多线程构建了一个让LED灯闪烁的三个线程的程序。
也通过程序初步了解了串口打印。

本节课的学习任务

  1. 熟悉FreeRTOS中的多线程相关操作
  2. 熟悉各种参数传入的方式
  3. 了解两个创建、一个延迟以及一个删除函数。

将点灯程序改为单参数传入的

共享代码位置:https://wokwi.com/projects/362410134939151361

byte LED1_PIN = 4;
byte LED2_PIN = 5;
byte LED3_PIN = 6;
// 第一个任务,控制红灯,每1秒亮灭一次
void task1(void *param_t){// 从参数中取出掺入的Pin端口指针byte *pin_p = (uint8_t *)param_t;// 从指针中提取出内容byte pin = *pin_p;pinMode(pin, OUTPUT);while(1){// 先读取引脚的高低电平,然后翻转,最后重新设置给这个引脚digitalWrite(pin, !digitalRead(pin));// 等待1秒钟vTaskDelay(1000/portTICK_PERIOD_MS);}
}
// 第二个任务,控制绿灯,每2秒亮灭一次
void task2(void *param_t){// 直接提取内容byte pin = *(uint8_t *)param_t;pinMode(pin, OUTPUT);while(1){// 先读取引脚的高低电平,然后翻转,最后重新设置给这个引脚digitalWrite(pin, !digitalRead(pin));vTaskDelay(2000/portTICK_PERIOD_MS);}
}
// 第三个任务,控制蓝灯,每3秒亮灭一次
void task3(void *param_t){byte pin = *(uint8_t *)param_t;pinMode(pin, OUTPUT);while(1){// 先读取引脚的高低电平,然后翻转,最后重新设置给这个引脚digitalWrite(pin, !digitalRead(pin));vTaskDelay(3000/portTICK_PERIOD_MS);}
}
void setup() {Serial.begin(115200);xTaskCreate(task1, "Blink Red",1024,&LED1_PIN,1,NULL);Serial.println("第一个任务被创建,将控制红灯每秒亮灭一次");xTaskCreate(task2, "Blink Green",1024,&LED2_PIN,1,NULL);Serial.println("第二个任务被创建,将控制红灯每2秒亮灭一次");xTaskCreate(task3, "Blink Blue",1024,&LED3_PIN,1,NULL);Serial.println("第三个任务被创建,将控制红灯每3秒亮灭一次");
}
void loop() {}

这段代码中我们首先定义了三个byte类型的变量,用于保存引脚编号。
然后依然延续我们上一节中的点灯任务,一共三个任务,但不同之处在于,我们通过参数的方式将引脚传入。
xTaskCreate(task1, “Blink Red”,1024,&LED1_PIN,1,NULL);
传入的时候一定要通过取址符(&)以指针方式传入。
在任务入口函数中,通过

byte *pin_p = (uint8_t *)param_t;

将void* 类型指针转换为byte * 类型,然后在通过

byte pin = *pin_p;

方式将pin_p指针中的内容取出,操作比较复杂,大家一定要领会 & 和 *的不同之处
这行可以缩写成

byte pin = *(uint8_t *)param_t;

回顾指针和引用

指针运算符 &
引用运算符 *
代码共享位置:https://wokwi.com/projects/362434637117039617

void setup() {// put your setup code here, to run once:Serial.begin(115200);Serial.println("Hello, ESP32-S3!");uint16_t val_a = 10;    // 定义一个变量uint16_t val_b = 20;    // 定义第二个变量uint16_t *val_p = &val_a;   // 声明一个指针,这里可以不赋值,而&val_a 表示将变量 val_a 的地址付给这个指针val_p = &val_b;             // 指针类型的是可以二次赋值的uint16_t &val_c = val_b;    // 引用类型的变量相当于给变量起了个别名,他们的地址是相同的printf(" val_a= %d\n", val_a);printf(" val_b= %d\n", val_b);printf(" val_p= 0x%X\n", val_p);    // val_p 的值是val_b 的地址printf("&val_p= 0x%X\n", &val_p);   // 但val_p的地址和val_b是不同的printf("&val_b= 0x%X\n", &val_b);   // val_b 的地址和 val_c的地址是相同的printf("&val_c= 0x%X\n", &val_c);printf(" val_b= %d\n", val_b);printf(" val_c= %d\n", val_c);      // val_b的值和val_c的值也是相同的
}
void loop() {// put your main code here, to run repeatedly:delay(10); // this speeds up the simulation
}
void printf(String format, ...) {char buffer[128]; // 创建缓冲区va_list args; // 定义可变参数列表va_start(args, format); // 初始化可变参数列表vsnprintf(buffer, sizeof(buffer), format.c_str(), args); // 格式化输出到缓冲区va_end(args); // 结束可变参数列表Serial.print(buffer); // 输出到串口
}

以上例子说明了指针和引用的区别,
指针变量有自己独立的地址,他所指向地址中的内容和变量的内容是相同的,但指针的地址和变量的地址不同
引用就相当于给变量起了个别名,引用的地址和原始变量的地址是相同的

下面的例子中,演示了参数传递的三种方式,我们在项目开发中,需要根据实际情况选择传值方式。
代码共享位置:https://wokwi.com/projects/362436365397929985

void setup() {// put your setup code here, to run once:Serial.begin(115200);Serial.println("Hello, ESP32-S3!");uint16_t a = 10;    // 定义一个变量uint16_t b = 20;    // 定义第二个变量printf("-> 交换前A和B的地址分别是:%X , %X\n",&a, &b);printf("-> 交换前A和B的值分别是:%d , %d\n", a, b);swap_1(a, b);// swap_2(&a, &b);// swap_3(a, b);printf("-> 交换后A和B的地址分别是:%X , %X\n",&a, &b);printf("-> 交换后A和B的值分别是:%d , %d\n", a, b);
}
void loop() {// put your main code here, to run repeatedly:delay(10); // this speeds up the simulation
}
// 第一个交换函数,按值传递
void swap_1(uint16_t a, uint16_t b){printf("交换前A和B的地址分别是:%X , %X\n",&a, &b);printf("交换前A和B的值分别是:%d , %d\n", a, b);uint16_t t=a;a=b;b=t;printf("交换后A和B的地址分别是:%X , %X\n",&a, &b);printf("交换后A和B的值分别是:%d , %d\n", a, b);
}
// 第二个交换函数,按地址传递
void swap_2(uint16_t *a, uint16_t *b){printf("交换前A和B的地址分别是:%X , %X\n",a, b);printf("交换前A和B的值分别是:%d , %d\n", *a, *b);uint16_t t=*a;*a=*b;*b=t;printf("交换后A和B的地址分别是:%X , %X\n",a, b);printf("交换后A和B的值分别是:%d , %d\n", *a, *b);
}
// 第三个交换函数,按引用传递
void swap_3(uint16_t &a, uint16_t &b){printf("交换前A和B的地址分别是:%X , %X\n",&a, &b);printf("交换前A和B的值分别是:%d , %d\n", a, b);uint16_t t=a;a=b;b=t;printf("交换后A和B的地址分别是:%X , %X\n",&a, &b);printf("交换后A和B的值分别是:%d , %d\n", a, b);
}
void printf(String format, ...) {char buffer[128]; // 创建缓冲区va_list args; // 定义可变参数列表va_start(args, format); // 初始化可变参数列表vsnprintf(buffer, sizeof(buffer), format.c_str(), args); // 格式化输出到缓冲区va_end(args); // 结束可变参数列表Serial.print(buffer); // 输出到串口
}

代码中swap_1是按值传递的函数,传入之前变量的地址和参数中ab的地址是不同的,说明他们是两个不同的变量,在函数中我们对两个变量进行了交换,可以看到其实在函数中变量已经被交换过来了,但出了函数之后外面的变量并没有交换。
代码中swap_2是按指针传递的,所以在调用函数的时候我们需要用取址符(&)取得两个变量的地址,在传入到函数中a和b的地址与外面的地址是相同的,所以我们对a和b进行操作的时候外面的值也会有相应的变化。
代码中swap_3的函数体基本和swap_1是一样的,知识在形参上有些改变,给变量加了一个&表示引用,而我们调用函数的方法和swap_1也是相同的,但传入到函数体中后,我们可以看到,函数内参数的地址和外面a b的地址是一样的,也就意味着,我们在函数体中操作a b的时候,其实操作的就是外面的a b。

多参数传入

在FreeRTOS编程中,任务的参数是通过指针传入的,而在C语言中,一切皆可指针。
在上一个例程中,任务入口函数的代码大部分都相同,只有LED引脚和延迟时间两个参数有变化,而在上一个例程中我们只传入了引脚这一个参数,导致程序还是过于冗长,如果把引脚编号和时间都作为参数传入,我们就可以把三个任务入口函数合并成一个了,所以这个例程中,我们将传入两个参数完成这一优化。
多参数传入的方式有很多种,这个例程中我们将采用数组方式传入。
众所周知,数组本身就是一个指针,例如 int vals[3] ,当我们使用的时候,只需要将 vals 传入即可,而无需再对 vals 进行取址。

代码分享位置:https://wokwi.com/projects/362410683605524481

// 统一的任务,传入两个参数,第一个是pin,第二个是延迟时间
void led_task(void *param_t){// 从传入的参数中提取数组uint16_t *arr = (uint16_t *)param_t;// 数组第一个值为pin编号byte pin = (uint8_t)arr[0];// 数字第二个值为延迟时间uint16_t delayTime = arr[1];pinMode(pin, OUTPUT);while(1){// 先读取引脚的高低电平,然后翻转,最后重新设置给这个引脚digitalWrite(pin, !digitalRead(pin));vTaskDelay(delayTime/portTICK_PERIOD_MS);}
}
uint16_t LED1[] = {4,1000};
uint16_t LED2[] = {5,2000};
uint16_t LED3[] = {6,3000};
void setup() {Serial.begin(115200);xTaskCreate(led_task, "Blink Red",1024,LED1,1,NULL);Serial.println("第一个任务被创建,将控制红灯每秒亮灭一次");xTaskCreate(led_task, "Blink Green",1024,LED2,1,NULL);Serial.println("第二个任务被创建,将控制红灯每2秒亮灭一次");xTaskCreate(led_task, "Blink Blue",1024,LED3,1,NULL);Serial.println("第三个任务被创建,将控制红灯每3秒亮灭一次");
}
void loop() {}

C语言规定了,数组中元素的类型必须都是一样的,而在之前的操作中我们得知,pin的类型是byte类型的,只有一个字节,最大只能表示255,这对于我们动辄1000ms的延迟是有点不够用的,所以我们将类型统一为uint16,这样最大可以表示65535。
例程中,首先声明三个uint16_t的一维数组,长度为2,分别存放引脚编号和延迟时间,这个数组的总大小是4个字节。
这里需要注意,我们将这三个数组的作用域定义为全局;如果把三个数组定义到setup函数中,当三个任务创建完毕后,会退出setup函数,这时有可能任务还没有执行,这就会导致在setup函数中声明的数组空间被释放,而我们的参数是按照指针传递进去的,在任务中取值的时候不会报错,但却有可能出现数据混乱的现象,这点尤为注意。
这个例程中,创建任务函数的第一个入参都是相同的 led_task 函数,这大大降低了代码量。
的那这个例程中仍然存在一些不足,就是我们浪费了1个字节的空间,要知道,整个ESP32-S3只有512K的SRAM,这不比我们PC编程,动辄就 byte[1024],在嵌入式编程中,我们一定要把一个字节掰成八瓣用,本着能省则省的原则,我们必须要把多余的空间省出来。

我们可以选择两个方式:

  1. 声明一个长度为3的byte,其中后两个字节分配给delayTime使用
  2. 使用结构体

第一种操作方式过于复杂,还需要有位移运算,会牺牲CPU的速度(CPU也是很紧缺的资源),所以下面的代码优化中,我们采用结构体的方式。

使用结构体传入参数

代码共享位置:https://wokwi.com/projects/362413677559674881

// 定义LED灯的结构体
typedef struct{byte pin;             // 操控引脚uint16_t delayTime;   // 延迟时间
}LED;
LED led1,led2,led3;
// 统一的任务,传入两个参数,第一个是pin,第二个是延迟时间
void led_task(void *param_t){// 从传入参数中提取结构体LED led = *(LED *)param_t;byte pin = led.pin;uint16_t delayTime =  led.delayTime;pinMode(pin, OUTPUT);while(1){// 先读取引脚的高低电平,然后翻转,最后重新设置给这个引脚digitalWrite(pin, !digitalRead(pin));vTaskDelay(delayTime/portTICK_PERIOD_MS);}
}
void setup() {Serial.begin(115200);// 设置参数led1.pin=4;   led1.delayTime=1000;led2.pin=5;   led2.delayTime=2000;led3.pin=6;   led3.delayTime=3000;xTaskCreate(led_task, "Blink Red",1024,&led1,1,NULL);Serial.println("第一个任务被创建,将控制红灯每秒亮灭一次");xTaskCreate(led_task, "Blink Green",1024,&led2,1,NULL);Serial.println("第二个任务被创建,将控制红灯每2秒亮灭一次");xTaskCreate(led_task, "Blink Blue",1024,&led3,1,NULL);Serial.println("第三个任务被创建,将控制红灯每3秒亮灭一次");
}
void loop() {}

代码中首先定义了一个结构体,分别有一个byte和一个unit16的成员,总共占用3个字节大小,led_task从原来数组取值编程了从结构体取值,其他基本不变。

ESP32-S3的CPU

之前我们说过,ESP32-S3是双核的MCU。
其中Core-0是系统CPU,又叫SYS-Core,一般运行的是WIFI和蓝牙的协议栈及相关服务程序;
Core-1是应用CPU,又叫APP-Core,我们自己的程序一般会运行在这里。
虽然两个CPU都可以指定程序运行,但强烈建议,如果资源允许的情况下,尽量不要打CPU-0的主意,在物联网开发中,让协议栈安全运行比什么都重要!
那如何控制线程在哪个核心运行呢?
想知道如何运行在指定核心,我们先需要知道我们创建的任务运行在哪个核心上。

首先,在Android的设置中可以进行选择,打开工具 -> Android Run On 这个菜单,可以选择在Core-0还是Core-1运行,在Android中默认是在核心1运行。
但这里仅仅说的是setup和loop函数在哪个核心运行,通过xTaskCreate创建的任务则会随机选择CPU运行。

在代码中,可以通过 xPortGetCoreID 函数可以获得当前任务在哪个CPU运行,这个函数只有在任务中运行的时候才能得到正确结果。

代码共享地址:https://wokwi.com/projects/362415309699772417

void task1(void* patam_t){vTaskDelay(300/portTICK_PERIOD_MS);Serial.print("任务1运行在 CPU - ");Serial.println(xPortGetCoreID());vTaskDelete(NULL);
}
void task2(void* patam_t){vTaskDelay(600/portTICK_PERIOD_MS);Serial.print("任务2运行在 CPU - ");Serial.println(xPortGetCoreID());vTaskDelete(NULL);
}
void task3(void* patam_t){vTaskDelay(900/portTICK_PERIOD_MS);Serial.print("任务3运行在 CPU - ");Serial.println(xPortGetCoreID());vTaskDelete(NULL);
}
void setup() {Serial.begin(115200);Serial.print("当前CPU:");Serial.println(xPortGetCoreID());xTaskCreate(task1,"TASK1",1024,NULL,1,NULL);xTaskCreatePinnedToCore(task2,"TASK2",1024,NULL,1,NULL,0);xTaskCreatePinnedToCore(task3,"TASK3",1024,NULL,1,NULL,1);
}
void loop() {}

之前我们都是通过 xTaskCreate 创建任务,而FreeRTOS提供了另外一个强劲的函数 xTaskCreatePinnedToCore ,这个函数的参数和 xTaskCreate 基本相同,只是在最后加入了一个指定CPU的参数。

在这段程序中我们注意到,每个任务的最后都有一条

vTaskDelete(NULL);

这个函数用于删除当前任务,如果缺失了这行代码,程序会报错,因为任务一旦出了入口函数,调度器将不知道向哪运行,FreeRTOS任务,只有运行中、就绪、挂起、阻塞、等待删除五种状态,我们之前的任务都是放在一个while大循环中运行,永远不会退出,但本次的例程中少了while循环,也就意味着任务会退出,当任务退出后,就不属于这五种状态的任何一种,CPU直接懵圈,索性就挂了……
所以,我们在显性结束任务的时候,必须手动调用 vTaskDelete 函数将任务删除。
vTaskDelete 传入一个参数,就是任务的句柄,当传入为NULL的时候,表示删除当前任务。(NULL这个规则适用于之后的其他函数,如果是针对任务的句柄,传入NULL则表示当前任务)
调用 vTaskDelete 之后任务并不会马上删除,会进入待删除列表,但此时等待任务的之后被删除,是不可恢复的,此时的任务不参与调度,也无法通过 vTaskResume 恢复。 任务删除时,一并释放的还有创建任务时分配的栈空间(如果是静态创建,则栈空间不被回收)。

任务的执行状态

在这里插入图片描述

就绪态 :指任务目前处于等待运行状态,如被创建后、delay之后、结束挂起、被调度等都会到达这个状态
运行态 :指任务处于运行中,每个CPU同时只会有一个任务处于运行中的状态,可通过delay函数进入阻塞,或通过暂停等进入挂起
挂起态 :任务暂时不执行,也不参与调度,只有通过vTaskResume 函数才可解除任务的挂起态
阻塞态 :当调用delay函数,或者等待某个信号时处于阻塞态,此时任务不参与调度,等待时间将状态改变为就绪
待删除 :当任务执行 vTaskDelete 函数之后,任务并没有马上删除,但此时任务已经不参与任何调度,也不会改变为其他状态,唯一的结局就是等待删除。

静态任务

通过静态方法创建任务和动态方法创建任务形同,使用的是:xTaskCreateStaticxTaskCreateStaticPinnedToCore 两个函数。

TaskHandle_t xTaskCreateStatic(TaskFunction_t pvTaskCode, const char *const pcName, const uint32_t ulStackDepth, void *const pvParameters, UBaseType_t uxPriority, StackType_t *const puxStackBuffer, StaticTask_t *const pxTaskBuffer)

该函用于静态方法创建任务,参数分别表示如下:
pvTaskCode 任务入口函数
pcName 任务名称
ulStackDepth 任务栈大小
pvParameters 任务参数
uxPriority 任务优先级
pxTaskBuffer 栈缓冲区首地址
pxTaskBuffer 任务控制块指针
同时,这个函数将返回任务指针
与动态方法创建不同点在于,在创建任务之前首先要定义栈缓冲区和任务控制块,这两个部分在创建时不会动态分配。

指定CPU运行的静态任务创建函数如下:

TaskHandle_t xTaskCreateStaticPinnedToCore(TaskFunction_t pvTaskCode, const char *const pcName, const uint32_t ulStackDepth, void *const pvParameters, UBaseType_t uxPriority, StackType_t *const pxStackBuffer, StaticTask_t *const pxTaskBuffer, const BaseType_t xCoreID)

这两个函数前期阶段我们用到的比较少,待后期有用例的时候我们再详细讲解。

这篇关于S1-02 FreeRTOS线程控制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/594958

相关文章

Java多线程父线程向子线程传值问题及解决

《Java多线程父线程向子线程传值问题及解决》文章总结了5种解决父子之间数据传递困扰的解决方案,包括ThreadLocal+TaskDecorator、UserUtils、CustomTaskDeco... 目录1 背景2 ThreadLocal+TaskDecorator3 RequestContextH

浅析如何使用Swagger生成带权限控制的API文档

《浅析如何使用Swagger生成带权限控制的API文档》当涉及到权限控制时,如何生成既安全又详细的API文档就成了一个关键问题,所以这篇文章小编就来和大家好好聊聊如何用Swagger来生成带有... 目录准备工作配置 Swagger权限控制给 API 加上权限注解查看文档注意事项在咱们的开发工作里,API

java父子线程之间实现共享传递数据

《java父子线程之间实现共享传递数据》本文介绍了Java中父子线程间共享传递数据的几种方法,包括ThreadLocal变量、并发集合和内存队列或消息队列,并提醒注意并发安全问题... 目录通过 ThreadLocal 变量共享数据通过并发集合共享数据通过内存队列或消息队列共享数据注意并发安全问题总结在 J

异步线程traceId如何实现传递

《异步线程traceId如何实现传递》文章介绍了如何在异步请求中传递traceId,通过重写ThreadPoolTaskExecutor的方法和实现TaskDecorator接口来增强线程池,确保异步... 目录前言重写ThreadPoolTaskExecutor中方法线程池增强总结前言在日常问题排查中,

Spring IOC控制反转的实现解析

《SpringIOC控制反转的实现解析》:本文主要介绍SpringIOC控制反转的实现,IOC是Spring的核心思想之一,它通过将对象的创建、依赖注入和生命周期管理交给容器来实现解耦,使开发者... 目录1. IOC的基本概念1.1 什么是IOC1.2 IOC与DI的关系2. IOC的设计目标3. IOC

Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单

《Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单》:本文主要介绍Springboot的ThreadPoolTaskScheduler线... 目录ThreadPoolTaskScheduler线程池实现15分钟不操作自动取消订单概要1,创建订单后

C语言线程池的常见实现方式详解

《C语言线程池的常见实现方式详解》本文介绍了如何使用C语言实现一个基本的线程池,线程池的实现包括工作线程、任务队列、任务调度、线程池的初始化、任务添加、销毁等步骤,感兴趣的朋友跟随小编一起看看吧... 目录1. 线程池的基本结构2. 线程池的实现步骤3. 线程池的核心数据结构4. 线程池的详细实现4.1 初

Java子线程无法获取Attributes的解决方法(最新推荐)

《Java子线程无法获取Attributes的解决方法(最新推荐)》在Java多线程编程中,子线程无法直接获取主线程设置的Attributes是一个常见问题,本文探讨了这一问题的原因,并提供了两种解决... 目录一、问题原因二、解决方案1. 直接传递数据2. 使用ThreadLocal(适用于线程独立数据)

Python实现局域网远程控制电脑

《Python实现局域网远程控制电脑》这篇文章主要为大家详细介绍了如何利用Python编写一个工具,可以实现远程控制局域网电脑关机,重启,注销等功能,感兴趣的小伙伴可以参考一下... 目录1.简介2. 运行效果3. 1.0版本相关源码服务端server.py客户端client.py4. 2.0版本相关源码1

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去