FreeRTOS问答
-
怎么看FreeRTOS代码?
-
源码结构
-
数据类型
FreeRTOS的数据类型基本上都定义在
portmacro.h
文件中,比如在FreeRTOS\Source\portable\RVDS\ARM_CM3
下就可以找到这个文件;除此之外,FreeRTOS
中还有两个特定的数据类型;
BaseType_t
:这个类型被定义为架构中最有效的数据类型;比如32位体系结构使用32 bit数据类型/int32_t,16位架构使用16 bit数据类型/int16_t,8位架构上使用8 bit数据类型/int8_t。BaseType_t
适用于数据范围比它小的类型的值,所以也适用于pdTRUE/pdFALSE
类型的布尔值;
TickType_t
:FreeRTOS配置一个称为tick interrupt
的定时中断;两次滴答中断之间的时间称为滴答中断周期;所以tick number
是指定滴答中断周期的倍数,TickType_t
是用来保存滴答计数值和to的数据类型指定时间。 -
命名规则
-
变量名
前缀 类型 前缀 c
char
前缀 s
int16_t
前缀 p
指针变量
大写 l
int32_t
小写 x
表示类型为 BaseType_t
:其他非标准类型(结构、任务句柄、队列句柄等等)
前缀 u
变量是 无符号
的如果一个变量是一个 指针
它也是带前缀的,例如,类型为 uint8_t
的变量将前缀为uc
-
函数名
前缀
返回的类型
/其中定义的文件
vTaskPrioritySet()
前缀 v
表示返回类型为viod
,Task
表示在tasks.c
中定义xQueueReceive()
前缀 x
表示返回BaseType_t
类型数据,Queue
表示该函数在queue.c
中定义;pvTimerGetTimerID()
前缀 pv
表示返回void类型指针
,Timer
表示该函数在timer.c
.中定义;prv
前缀 prv
表示作用范围
为当前的文件
; -
宏定义
FreeRTOS的大多数宏都是用
大写字母
写的,并以小写字母作为前缀
来表示前缀 定义 所属文件 task
taskENTER_CRITICAL()
task.h
pd
pdTRUE
projdefs.h
config
configUSE_PREEMPTION
FreeRTOSConfig.h
err
errQUEUE_FULL
projdefs.h
port
portMAX_DELAY
portable.h or portmacro.h
-
FreeRTOS核心内容有哪些?
Microcontrollers are used in deeply embedded applications (those applications where you never actually see the processors themselves, or the software they are running) that normally have a very specific and dedicated job to do. The size constraints, and dedicated end application nature, rarely warrant the use of a full RTOS implementation - or indeed make the use of a full RTOS implementation possible. FreeRTOS therefore provides
the core real time scheduling functionality
(实时调度功能),inter-task communication
(任务间通信),timing
(定时) andsynchronisation primitives
(同步原语、原子操作) only. This means it is more accurately described as a real time kernel, or real time executive. Additional functionality, such as a command console interface, or networking stacks, can then be included withadd-on components
. from
在
并发编程
(Concurrency)时,如果多个线程
访问同一资源
,我们需要保证访问的时候不会产生冲突
,数据修改不会发生错误,这就是我们常说的线程安全
。
原子操作(atomic operation),指不会被
线程调度机制
打断的操作,这种操作一旦开始,就一直
运行到结束,中间不会切换到其他线程。
在
多线程
下,我们并不能保证我们的代码都具有原子性
,因此如何让我们的代码变得具有原子性
,就是一件很重要的事。
方法也很简单,就是当你在访问一个多线程间共享的资源
时,加锁
可以实现类似原子操作的效果,一个代码要嘛不执行,执行了的话就要执行完毕,才能接受线程的调度。
因此,我们使用加锁
的方法,使其具备原子性
。
Context Switching
:上下文切换
Cortex-M
:一种ARM处理器架构(Processor Architecture),处理器架构还有Cortex-A , MSP430 , RISC-V,Xtensa …
上图显示了
SP
指针和PC
指针、寄存器R1
、R2
、R3
、代码段.code
(Program Memory)、数据段.data
(Data Memory).的情况。这些资源被称为上下文
As a task executes it utilizes the processor / microcontroller registers and accesses RAM and ROM just as any other program. These
resources
together (the processor registers, stack, etc.
) comprise the task execution context (上下文
)
A task is asequential piece of code
- it does not know when it is going to getsuspended
(swapped out or switched out) orresumed
(swapped in or switched in) by the kernel and does not even knowwhen this has happened
. Consider the example of a task being suspended immediately before executing an instruction that sums the values contained within two processor registers. While the task is suspendedother tasks
willexecute and may modify
the processor register values. Upon resumption the task will not know that the processor registers have been altered - if it used the modified values the summation would result in an incorrect value.
To prevent this type of error it is essential that upon resumption a task has a context identical to that immediately prior to its suspension. The operating system kernel is responsible forensuring this is the case
- and does so bysaving the context of a task as it is suspended
. When the task is resumed its saved context is restored by the operating system kernel prior to its execution. The process ofsaving
the context of a task being suspended andrestoring
the context of a task being resumed is called context switching(上下文切换
).
from
- 无论是消息队列还是邮箱队列,都是利用了
全局变量
可以被随意访问的特性,所以使用时都会被定义为全局变量。- 普通全局变量可用于一些简单的任务间通信场合。ps:
volatile
关键字比较重要。- 相较于普通全局变量,加入
队列机制
可以存储多个消息,加入pend-post机制
可以拥有任务等待和唤醒的机制,用于解决队列已满或队列为空的问题。
多任务访问全局变量会带来
共享资源管理
问题。
消息队列最终是用的全局变量!但是消息队列对这个全局变量做了保护
,重点就是资源管理的保护!
假如你直接使用全局变量,那么在代码中任何任务都可以随时随地的访问、修改这个全局变量!
如果在mov指令
之后发生一次任务切换,在其它任务中global的值被修改,则这段代码中使用的仍然是global的旧值
,执行结果将不符合预期。其他arch也有类似的问题。
如果使用消息队列的话,A任务要使用队列S,先申请
,申请成功以后才可以使用。B任务也要使用S的时候也要先申请,当时发现S已经被A任务使用了,所以B任务就没法使用(假设当前的队列长度为1),直到A任务使用完S并且释放掉B任务才申请使用!
(from:Mculover666)
队列传递结构体指针和用队列传递结构体(esp-idf)
传递结构体指针:
typedef struct {
uint8_t id;
uint16_t value;
} my_struct_t;
// 创建队列
QueueHandle_t queue = xQueueCreate(10, sizeof(my_struct_t*));
// 创建任务
void task_send(void *pvParameters) {
my_struct_t my_struct = {
.id = 1,
.value = 42
};
my_struct_t *my_struct_ptr = &my_struct; // 获取结构体指针
xQueueSend(queue, &my_struct_ptr, portMAX_DELAY); // 将结构体指针发送到队列中
}
void task_receive(void *pvParameters) {
my_struct_t *my_struct_ptr;
while (1) {
if (xQueueReceive(queue, &my_struct_ptr, portMAX_DELAY) == pdTRUE) { // 从队列中接收结构体指针
printf("Received struct with id %d and value %d\n", my_struct_ptr->id, my_struct_ptr->value); // 访问结构体指针并打印
}
}
}
传递结构体:
typedef struct {
uint8_t id;
uint16_t value;
} my_struct_t;
// 创建队列
QueueHandle_t queue = xQueueCreate(10, sizeof(my_struct_t));
// 创建任务
void task_send(void *pvParameters) {
my_struct_t my_struct = {
.id = 1,
.value = 42
};
xQueueSend(queue, &my_struct, portMAX_DELAY); // 将结构体发送到队列中
}
void task_receive(void *pvParameters) {
my_struct_t my_struct;
while (1) {
if (xQueueReceive(queue, &my_struct, portMAX_DELAY) == pdTRUE) { // 从队列中接收结构体
printf("Received struct with id %d and value %d\n", my_struct.id, my_struct.value); // 访问结构体数据并打印
}
}
}
这两种传递方法的语法非常不同。在第一种方法中,我们需要定义指向结构体的指针,并将其发送到队列中。在接收任务中,我们需要将指向结构体的指针接收到另一个指向结构体的指针中,并访问结构体数据。这需要额外的内存分配和释放,并可能会影响性能。
在第二种传递方法中,我们直接将整个结构体发送到队列中,无需进行任何指针操作。在接收任务中,我们可以直接接收整个结构体。这样做可能更加简单和有效,但在传递大型结构体时可能效率会比较低。因此,需要根据具体情况选择合适的传递方法。
可以看出两者的创建和接收不一样,发送是一样的
信号量实现责任链
简单版
- 定义责任链中的处理对象,每一个对象需要有其独特的任务函数,该任务函数需要根据需要发送或接收信号量。例如:
void task1(void* pvParameters)
{
while (1) {
xSemaphoreTake(xSemaphore1, portMAX_DELAY);
// 执行任务1的处理逻辑
xSemaphoreGive(xSemaphore2);
}
}
void task2(void* pvParameters)
{
while (1) {
xSemaphoreTake(xSemaphore2, portMAX_DELAY);
// 执行任务2的处理逻辑
xSemaphoreGive(xSemaphore3);
}
}
void task3(void* pvParameters)
{
while (1) {
xSemaphoreTake(xSemaphore3, portMAX_DELAY);
// 执行任务3的处理逻辑
xSemaphoreGive(xSemaphore1);
}
}
- 创建信号量
xSemaphoreHandle xSemaphore1 = xSemaphoreCreateBinary();
xSemaphoreHandle xSemaphore2 = xSemaphoreCreateBinary();
xSemaphoreHandle xSemaphore3 = xSemaphoreCreateBinary();
- 将责任链中的任务函数作为FreeRTOS任务函数创建任务
xTaskCreate(task1, "Task 1", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY, NULL);
xTaskCreate(task2, "Task 2", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY, NULL);
xTaskCreate(task3, "Task 3", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY, NULL);
- 根据你想要的责任链的起始点发送一个信号量。例如:
xSemaphoreGive(xSemaphore1);
- 当信号量从一个任务发送到下一个任务时,中间任务的处理函数被激活,这里可以实现任何适当的操作。
当信号量从最后一个任务传递回第一个任务时,责任链就会形成一个循环。通过调节每个任务发送和接收信号量的时间,可以调整责任链何时执行以及何时中断。
复杂版
在FreeRTOS中,使用信号量可以很方便地实现责任链模式。责任链模式是一种行为型设计模式,它让多个对象都有机会处理请求,从而避免了请求的发送者和接收者之间的耦合关系。
实现步骤:
1
. 首先定义一个信号量,用于表示是否有任务在处理请求。这个信号量的初值为1,表示当前没有任务在处理请求。
2
. 定义一个结构体,用于表示处理请求的任务。这个结构体至少包含两个字段:一个指向下一个任务的指针和一个函数指针,表示任务处理请求的方法。
3
. 定义多个处理请求的任务,并将它们按照任务执行顺序组成一个链表。
4
. 在任务中获取信号量,如果信号量的值为1,则表示当前没有任务在处理请求,当前任务可以处理请求,并将信号量的值置为0;如果信号量的值为0,则表示当前有任务在处理请求,当前任务不能处理请求,需要将请求转发给下一个任务。
5
. 如果当前任务不能处理请求,则将请求转发给下一个任务。下一个任务的处理方法是通过函数指针调用的,如果下一个任务的指针不为NULL,则调用下一个任务的处理方法;否则,表示没有任务可以处理请求,将信号量的值置为1。
6
. 重复上述步骤,直到所有的任务都处理完请求。
示例代码:
#define MAX_TASKS 5
SemaphoreHandle_t xSemaphore;
typedef struct Task
{
struct Task* nextTask;
void (*handleRequest)(void*);
} Task;
Task* headTask = NULL;
void addTask(Task* task)
{
task->nextTask = headTask;
headTask = task;
}
void taskFunction(void* arg)
{
Task* currentTask = (Task*) arg;
if (xSemaphoreTake(xSemaphore, 0) == pdTRUE) {
currentTask->handleRequest(NULL);
xSemaphoreGive(xSemaphore);
} else if (currentTask->nextTask != NULL) {
currentTask->nextTask->handleRequest(NULL);
} else {
xSemaphoreGive(xSemaphore);
}
}
void createTasks()
{
for (int i = 0; i < MAX_TASKS; i++) {
Task* task = (Task*) malloc(sizeof(Task));
task->nextTask = NULL;
task->handleRequest = NULL;
addTask(task);
}
headTask->handleRequest = task1Function;
headTask->nextTask = headTask + 1;
for (int i = 1; i < MAX_TASKS - 1; i++) {
headTask[i].handleRequest = task2Function;
headTask[i].nextTask = headTask + i + 1;
}
headTask[MAX_TASKS - 1].handleRequest = task3Function;
}
int main(void)
{
xSemaphore = xSemaphoreCreateBinary();
xSemaphoreGive(xSemaphore);
createTasks();
for (int i = 0; i < MAX_TASKS; i++) {
xTaskCreate(taskFunction, "Task", 128, headTask + i, tskIDLE_PRIORITY + i, NULL);
}
vTaskStartScheduler();
return 0;
}
在该示例代码中,
首先
定义了一个信号量xSemaphore,表示是否有任务在处理请求。在任务中获取信号量,如果信号量的值为1,则表示当前没有任务在处理请求,当前任务可以处理请求,并将信号量的值置为0;如果信号量的值为0,则表示当前有任务在处理请求,当前任务不能处理请求,需要将请求转发给下一个任务。
接下来
定义Task结构体用于表示处理请求的任务,并按照任务执行顺序将它们组成一个链表,链表的头节点是headTask。在createTasks函数中,根据MAX_TASKS的值创建多个Task对象,并将它们按照任务执行顺序组成一个链表。
最后
,在main函数中创建多个任务,并将每个任务指向不同的taskFunction函数。每个taskFunction函数中,首先获取信号量,如果当前没有任务在处理请求则当前任务可以处理请求,并将信号量的值置为0;否则,当前任务不能处理请求,需要将请求转发给下一个任务。转发请求的操作是通过调用下一个任务的handleRequest函数指针实现的。
广播:去给不同任务传输传感器的值
例如GPS数据,有的任务是记录某时刻的数据,有的任务是要实时获取到GPS数据。
那么要采用xQueueOverwrite(gpsQueue, &gpsData)
(覆写数据)和 xQueuePeek(gpsQueue, &gpsData, portMAX_DELAY)
(只读不删)来配合实现。
要实现这个功能,可以使用队列来实现GPS接收任务和遥控接收任务之间的数据传输。具体步骤如下:
-
定义一个全局的队列变量,用于在任务之间传输GPS数据。例如:
QueueHandle_t gpsQueue;
-
在GPS接收任务中,实时接收GPS数据,并将最新的数据发送到队列中。例如:
void gpsTask(void* parameters) { while (1) { float gpsData = receiveGpsData(); xQueueOverwrite(gpsQueue, &gpsData); } }
-
某个任务记录GPS信息。例如:
void remoteControlTask(void* parameters) { while (1) { int command = receiveCommand(); if (command == 8) { float gpsData; xQueuePeek(gpsQueue, &gpsData, portMAX_DELAY); recordData(gpsData); } } }
在上面的代码中,gpsTask
任务负责实时接收GPS数据,通过调用receiveGpsData()
函数获取最新的GPS数据,并使用xQueueOverwrite()
函数将数据发送到gpsQueue
队列中。remoteControlTask
任务负责接收遥控指令,当收到指令为8时,通过调用xQueuePeek()
函数从队列中获取最新的GPS数据,并调用recordData()
函数进行记录。
需要注意的是,使用xQueueOverwrite()
函数可以确保队列中只保存最新的GPS数据,避免冗余数据的堆积。同时,使用xQueuePeek()
函数可以查看队列中的数据,而不会将其从队列中删除。
在任务创建之前,记得使用xQueueCreate()
函数创建队列,例如:
gpsQueue = xQueueCreate(1, sizeof(float));
这里的参数1表示队列中只存放一个元素,sizeof(float)
表示每个元素的大小。