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的数据类型指定时间。

  • 命名规则

  • 变量名

    前缀类型
    前缀cchar
    前缀sint16_t
    前缀p指针变量
    大写lint32_t
    小写x表示类型为BaseType_t:其他非标准类型(结构、任务句柄、队列句柄等等)
    前缀u变量是无符号
    如果一个变量是一个指针它也是带前缀的,例如,类型为uint8_t的变量将前缀为uc
  • 函数名

    前缀返回的类型/其中定义的文件
    vTaskPrioritySet()前缀v表示返回类型为viodTask表示在tasks.c中定义
    xQueueReceive()前缀x表示返回BaseType_t类型数据,Queue表示该函数在queue.c中定义;
    pvTimerGetTimerID()前缀pv表示返回void类型指针Timer表示该函数在timer.c.中定义;
    prv前缀prv表示作用范围当前的文件
  • 宏定义

    FreeRTOS的大多数宏都是用大写字母写的,并以小写字母作为前缀来表示

    前缀定义所属文件
    task taskENTER_CRITICAL()task.h
    pdpdTRUEprojdefs.h
    configconfigUSE_PREEMPTIONFreeRTOSConfig.h
    errerrQUEUE_FULLprojdefs.h
    portportMAX_DELAYportable.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(定时) and synchronisation 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 with add-on components. from

  • 什么是线程安全?

并发编程(Concurrency)时,如果多个线程访问同一资源,我们需要保证访问的时候不会产生冲突,数据修改不会发生错误,这就是我们常说的 线程安全

  • 什么是原子操作?

原子操作(atomic operation),指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会切换到其他线程。

  • 如何实现人工原子操作?

多线程下,我们并不能保证我们的代码都具有原子性,因此如何让我们的代码变得具有原子性,就是一件很重要的事。
方法也很简单,就是当你在访问一个多线程间共享的资源时,加锁可以实现类似原子操作的效果,一个代码要嘛不执行,执行了的话就要执行完毕,才能接受线程的调度。
因此,我们使用加锁的方法,使其具备原子性

  • Context SwitchingCortex-M3

Context Switching:上下文切换
Cortex-M:一种ARM处理器架构(Processor Architecture),处理器架构还有Cortex-A , MSP430 , RISC-VXtensa

  • 什么是上下文切换

在这里插入图片描述

上图显示了SP指针和PC指针、寄存器R1R2R3、代码段.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 a sequential piece of code - it does not know when it is going to get suspended (swapped out or switched out) or resumed (swapped in or switched in) by the kernel and does not even know when 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 suspended other tasks will execute 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 for ensuring this is the case - and does so by saving 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 of saving the context of a task being suspended and restoring the context of a task being resumed is called context switching上下文切换).
from

  • GCC 的Signal 和Naked __attribute__

  • __attribute__的记录

attribute 机制使用
attribute 机制使用

  • Signal __attribute__

  • Naked __attribute__

  • 任务间通信为什么不使用全局变量?

  1. 无论是消息队列还是邮箱队列,都是利用了全局变量可以被随意访问的特性,所以使用时都会被定义为全局变量。
  2. 普通全局变量可用于一些简单的任务间通信场合。ps:volatile关键字比较重要。
  3. 相较于普通全局变量,加入队列机制可以存储多个消息,加入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); // 访问结构体数据并打印
        }
    }
}

这两种传递方法的语法非常不同。在第一种方法中,我们需要定义指向结构体的指针,并将其发送到队列中。在接收任务中,我们需要将指向结构体的指针接收到另一个指向结构体的指针中,并访问结构体数据。这需要额外的内存分配和释放,并可能会影响性能。
在第二种传递方法中,我们直接将整个结构体发送到队列中,无需进行任何指针操作。在接收任务中,我们可以直接接收整个结构体。这样做可能更加简单和有效,但在传递大型结构体时可能效率会比较低。因此,需要根据具体情况选择合适的传递方法。

可以看出两者的创建和接收不一样,发送是一样的
在这里插入图片描述

信号量实现责任链

简单版

  1. 定义责任链中的处理对象,每一个对象需要有其独特的任务函数,该任务函数需要根据需要发送或接收信号量。例如:
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);
    }
}
  1. 创建信号量
xSemaphoreHandle xSemaphore1 = xSemaphoreCreateBinary();
xSemaphoreHandle xSemaphore2 = xSemaphoreCreateBinary();
xSemaphoreHandle xSemaphore3 = xSemaphoreCreateBinary();
  1. 将责任链中的任务函数作为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);
  1. 根据你想要的责任链的起始点发送一个信号量。例如:
xSemaphoreGive(xSemaphore1);
  1. 当信号量从一个任务发送到下一个任务时,中间任务的处理函数被激活,这里可以实现任何适当的操作。

当信号量从最后一个任务传递回第一个任务时,责任链就会形成一个循环。通过调节每个任务发送和接收信号量的时间,可以调整责任链何时执行以及何时中断。

复杂版

在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接收任务和遥控接收任务之间的数据传输。具体步骤如下:

  1. 定义一个全局的队列变量,用于在任务之间传输GPS数据。例如:

    QueueHandle_t gpsQueue;
    
  2. 在GPS接收任务中,实时接收GPS数据,并将最新的数据发送到队列中。例如:

    void gpsTask(void* parameters) {
      while (1) {
        float gpsData = receiveGpsData();
        xQueueOverwrite(gpsQueue, &gpsData);
      }
    }
    
  3. 某个任务记录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)表示每个元素的大小。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值