Freertos

Arm架构

        stm32的cpu负责计算和执行指令,RAM负责数据的存储,flash负责代码的存储,cpu中有R0-R15寄存器,其中R13为栈SP,R14为返回地址LR即下一条指令的位置,R15为当前指令地址PC。当发生中断的时候,硬件会自动保存xPSR, R15, R14,R12,R3-R1,R0寄存器,R4-R11需要手动保存。

什么是FreeRtos

        Freertos是基于单片机的一种多线程多任务管理的实时操作系统,相对于单纯的裸机系统,RTOS能够解决更复杂的问题,实现多任务管理是其最大的优势。

裸机开发和rtos开发的区别?

        裸机开发平台直接对硬件进行处理,没有任务调度机制,需要手动管理内存,没有同步机制,rtos不依赖于硬件平台,可移植性高,有任务调度机制,内存管理机制和同步机制。

1. Freertos移植和裁剪

移植代码

创建一个freertos文件夹,包含inc,port,src文件夹。

1. 首先移植freeRTOS/Source中的include文件到inc中

2. 然后找到freertos Demo中找到对应芯片的FreeRtosConfig.h移到inc中

3. 将freeRTOS/Source/里的.c文件移动到src中,包括task.c, queue.c,list.c,  timer.c event_group.c, croutine.c ,前面三个必需,后面三个可选

4. 将freeRTOS/Source/RVDS/ 中根据M3或M4选择port.c 和 portmacro.h移动到port中

5. 将freeRTOS/Source/MenMang/ 中选择其中一个作为内存管理的文件,一般选择heap4.c移动到src中。

移植完以后在FreeRTOSConfig.h中

//我们在FreeRTOSConfig.h中添加如下宏定义
#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler

//这三个函数主要负责的是重新定向这三种中断,让FreeRTOS接手系统
  1. vPortSVCHandler - SVC(Supervisor Call)中断处理函数。在ARM架构中,SVC是软件中断,通常用于操作系统的上下文切换。在FreeRTOS中,这个中断用于处理任务切换。

  2. xPortPendSVHandler - PendSV(Pending Supervisor Call)中断处理函数。PendSV中断通常用于处理软件触发的上下文切换,例如,当任务调用taskYIELD()函数时,就会触发PendSV中断。

  3. xPortSysTickHandler - SysTick中断处理函数。SysTick是ARM架构中的一个定时器,通常用于提供周期性的中断,FreeRTOS使用SysTick来实现其时间管理功能,例如任务的超时、定时器等。

如何实现freertos移植:

        将Freertos里的Source/include移动到include中,然后在demo中找到对应芯片的FreeRTOSConfig.h移动到里面,在Source里面的.c文件包括task.c, list.c,queuq.c,timer.c等等移动到src里,然后将MenMang里的heap.c文件和RVDS对应芯片的port.c文件移动到port中。裁剪可以从任务是否需要某些功能入手,首先可以去src中保留task.c, list.c, queue.c,等然后根据需要选择timer.c, event_group.c等,然后再去内存管理里面保留heap4.c文件。

2. Freertos启动流程

        freertos启动流程是怎么样的?

        单片机启动流程(设置堆栈),硬件和设备初始化,创建任务(xTaskCreate),启动任务调度器(vTaskStartSchedule), 任务调度。

        启动任务调度器过程做的事情: 创建空闲任务,创建定时器服务函数,关闭中断,将xSchedulerRunning变量设置为pdTRUE,表示调度器已经开始运行,初始化PendSV中断,初始化滴答定时器,使能中断,启动第一个任务,初始化第一个任务的堆栈指针,并利用SVC异常启动任务。

        空闲任务(使用vApplicationIdleHook):

  •                 空闲任务是最低优先级的任务,确保调度器至少有一个任务运行。
  •                 可以设置让单片机处于低功耗模式
  •                 用于清除自杀任务的堆栈和TCB结构体, 可能会做一些系统资源管理
  •                 可以执行一些周期性检测任务。    

3. FreeRtos 内存管理

Freertos提供了五种动态内存的管理算法:

文件优点缺点
heap_1.c分配简单,时间确定只分配不回收
heap_2.c允许申请和释放内存,最佳匹配算法分配不合并空闲分区,产生碎片,时间不确定
heap_3.c调用标准库函数,在c库基础上增加了线程安全速度慢,时间不定
heap_4.c允许申请和释放内存,首次适应算法分配,相邻空闲内存可合并可解决碎片问题,时间不定
heap_5.c在heap_4 基础上支持分隔的内存块,能够管理多个非连续内存区域的heap4可解决碎片问题,时间不确定

4. 任务创建

   任务创建分为两种创建方式:

动态创建
xTaskCreate(taskFunction, "taskname", stack_size, 
param, priority, taskHandler);
静态创建
静态创建需要事先定义栈和TCB结构体
StacjType_t xTaskStack[100];
StaticTak_t xTaskTCB;
xTaskCreateStatic(taskFunction, "taskname", stack_size, 
param, priority, xTaskStack, &xTaskTCB);

任务删除:

vTaskDelete(xHandlerTask1); //在任务中杀死其他任务
vTaskDelete(NULL); //在任务中自杀

 vTaskDelete不会释放TCB和栈,而是在空闲任务中进行清理。不断创建和删除会导致内存耗光。

创建任务后会有一个TCB结构体

pxTopOfStack  保存的是栈顶指针
pxStack  保存的是栈的起始地址

伪造现场:

        创建任务最关键的一步是伪造现场,伪造现场本质就是将xPSR,PC(r15),LR(R14),R12,R3,R2,R1,R0,R4-R11依次压栈,并把栈顶指针传到任务结构体的pxTopOfStack中,原因就是为了统一使用中断函数来启动或切换任务,全权交给系统去做,而不是说由我们用户去启动一个任务,所以我们需要去伪造一个现场(由我们自己来保存全部的寄存器因为没有硬件来帮我们保存)   

  步骤:

        1. 伪造xPSR寄存器,设置bit24为1,因为M3只支持Thumb指令。

        2.伪造PC寄存器,存入任务函数地址

        3,伪造LR返回地址,因为任务函数是一个无限循环函数,一般不会返回,所以这里返回的是一个错误信号。

        4. 伪造R12,R3,R2,R1默认初始化为0

        5. 伪造R0,这是用于保存函数传参的

        6. 伪造最后的R4-R11初始化为0

        7. 返回栈顶指针,存入任务结构体中的pxTopOfStack成员变量中,下次就可以从该栈顶位置恢复伪造的现场,启动第一个任务。
后面启动调度函数中会调用SVC系统调用挂起pendsv中断来启动第一个任务。

在创建动态任务的时候:

        由于freertos一开始分配一大块连续的heap来存储动态任务的TCB和任务堆栈,那么这些任务是怎么存储的呢,一般是从低地址到高地址找到第一个合适的空闲块就分配。

         但是任务在使用任务栈空间的时候是从高地址到低地址的,当任务使用堆栈超过分配会造出栈溢出,导致覆盖TCB或其他内存区域的数据,系统可能会出现崩溃或不可预测的问题。

        如何判断堆栈是否溢出?

检测方法一:使用configCHECK_FOR_STACK_OVERFLOW 为1

        堆栈可能会达到其最大(最深)值,因为此时堆栈将包含任务上下文。此时,RTOS 内核可以检查处理器堆栈指针是否保持在有效堆栈空间内。如果堆栈指针包含超出有效堆栈范围的值,则调用堆栈溢出挂钩函数。

检测方法二:初始化堆栈内容,然后最后判读堆栈最后16个字节是否被改变,将 configCHECK_FOR_STACK_OVERFLOW 设置为 2使用该方法.该方法效率低。

检测方法三:使用uxTaskGetStackHighWaterMark 返回剩余堆栈,如果返回接近0则任务可能已经溢出。

          当freertos任务突然陷入死循环,无法执行任务,可以考虑一下堆栈溢出问题,如果任务创建的时候没有创建成功,其他任务任然正常运行,可以考虑任务的总堆栈大小设置得过小导致任务无法创建成功。

5. 任务调度

        freertos中任务是有运行态,就绪态,阻塞态(等待某件事情),暂停态(suspend),四种状态。

        freertos任务调度主要有三种原则,抢占式调度,时间片轮转调度,合作式调度。

        抢占式调度就是高优先级的先运行,可以抢占低优先级任务,时间片轮转就是同等优先级按一定时间间隔获得cpu权运行。

        空闲任务礼让,如果有同是优先级0的其他就绪任务,空闲任务会主动放弃一次运行机会。

如何调度:

        freertos维护三个链表,如何调度是用这三个链表实现的。

        就绪链表是一个数组,该数组的大小与优先级等大,然后数组每个元素维护一个优先级链表,存储相同优先级的任务。当操作系统任务调度的时候,从最高优先级的链表查询就绪的任务,如过不存在跳到小一级的优先级任务链表,如果存在则直接取出链表的第一个任务运行,并将其插入到链表尾部。  注意新创建的同等优先级的当前任务排在前面,是因为newpriority>= oldpriority的时候会被插入到头部。

        延时链表(阻塞态的存储与这):当任务运行vTaskDelay或者读写阻塞的时候就会被放到这个链表里面来,延时时间到或者阻塞条件被破除后就会被重新移动到就绪链表当中,然后调度。

        暂停链表:这里主要是存储使用suspend暂停的任务。

那么谁进行调度?

        Tick中断:每隔固定TickCount时间产生的中断叫Tick中断,由系统滴答定时器产生的,间隔可以配置,一般来说是1ms。

        pensv中断:是一种CPU系统级别的异常,专门用于实现操作系统中的上下文切换,他有缓期执行的特点,所以能够在任务切换的前能够及时响应外部中断。

        SVC异常:SVC异常是主动调动pensv中断请求上下文切换,由任务执行的SVC指令触发。

        Tick中断函数做的事情:

                处理延时链表,查看是否有任务延时到了

                从就绪链表取出下一个要调度的任务,如果有则挂起pendsv中断

        为什么freertos需要将任务切换分为下一个执行任务识别和任务切换两个部分?

          这是因为需要保持外部中断实时性,如果Tick中断把下一个任务识别和任务切换都做了,因为任务切换需要关中断,切切换比较频繁,如果在执行过程外部中断到来可能不会立即达到响应,影响外部中断的处理速度,所以,Tick中断中主要实现的是识别下一个要执行的任务,且挂起pendsv中断,如果在pendsv中断的时候有外部中断产生就可以优先去执行外部中断。

        Tick中断和pensvc中断的优先级都是为0,这是为了能够实时响应外部中断。

        pendsv做的事情:

                保存当前任务的现场:依次把xPSR,PC(R15),LR(R14),R12以及R3-R0由硬件自动压入适当的堆栈中。

                找到就绪任务中优先级最高的任务

                恢复现场:从下一个任务中加载堆栈到寄存器中。

        任务调度的情况:

                任务主动执行xTaskyeild,使用SVC异常调用pendsv中断

                titck中断挂起pendsv中断       

6. 通信机制

        通常都是用队列实现的,freertos基于队列实现了任务与任务之间的互斥,同步, 消息队列,数据传输等通信机制,在里面它会自动关中断,处理数据,开中断,因此能够保证数据的准确性。

任务通知:

        任务通知是在TCB结构体里面实现的,任务通知核心就是一个32位的无符号整数和一个8位的通知状态,而这两玩意就在任务控制块中,则所谓通知任务就是一个任务或者中断改写另外一个任务中的32位的无符号整数,只不过改写这个整数的方式可以有所不同(1.可以让这个整数加1: 模拟信号量 2. 设置该整数的指定的某些位:模拟事件组 3.直接选择覆盖或者不覆盖写入: 模拟消息队列)。

        任务的三种状态

        

1.任务的通知状态:任务通知有三种状态
     
   未等待通知状态:就是任务的初始状态

        等待通知状态:当任务在没有通知的时候接收通知时(也就是任务没有接收到通知的时候调用了接收通知的函数,则此时必定接收不到通知,把该任务标记为等待通知状态(去等别的任务发给我通知),任务进入阻塞态),这样做的用处是什么呢? 答:当另外一个任务发通知给该任务时,此时发现任务处于等待通知的状态,然后就可以即可把该任务唤醒。

        等待接收通知状态:当有其他任务向任务发送通知,但任务还未接收这一通知的这段期间内(当其他任务给该任务发了通知,但是该任务还没有接收,则将该任务标记为等待接收通知状态),这样做的用处就是当该任务调用了接收通知的函数,发现自身的状态为等待接收通知状态,则不用进入阻塞,直接接收通知值。

        任务通知的优缺点:

优点:

        1.使用任务通知比队列,事件标志组或信号量通信方式接触阻塞要块,且更省RAM内存空间。

        2使用任务通知不需要创建,因为当创建任务的时候就已经默认创建了这两个变量,任务控制块中的两个变量如下图所示,当然这里是一个数组(为了方便以后扩展),但是数组的元素个数默认为

缺点:

        1.不能发送通知到中断

        2.不能发送通知给多个任务

        3.发送通知的任务不能进入阻塞:只有等待通知的任务可以被阻塞,发送通知的任务,在任何情况下都不会因为发送失败而进入阻塞态,像队列:写队列当队列满的时候,可以进入阻塞态

        4.通知值只有一个32位的无符号的整形

        

        

消息队列

消息队列中存在的东西:

        存在一个链表保存读队列阻塞的任务,当队列中无数据读取的时候,如果任务愿意等待有数据才读,任务会被阻塞,一方面会放到队列的读阻塞链表中,同时还会从就绪链表移动到延时链表。

        存在一个链表保存写队列阻塞的任务,当队列中数据满了的时候并且该任务愿意等待队列有空间才写,这时候队列会将任务记录在等待写队列当中,同时还会将其从就绪链表移动到延时链表。

        消息队列存储数据的核心是环形缓冲区。

        队列的好处:

        1.可以自动帮我们开关中断来保存数据

        2.读队列无数据的时候可以休眠任务,把任务从就绪链表放到延时链表同时还要在队列里面的链表记录阻塞的任务,写队列进去可以从延时链表中唤醒任务。

信号量与互斥量:

    信号量分为二值信号量和计数信号量,他们是基于队列实现的

与队列相比:

        他不能传输数据,但同时他不需要传输数据,占用的空间也更小,运行更快。

        队列头的结构体中的队列长度被复用为计数值,itemsize被设置为0。

        操作Take和Give的时候跟队列一样要关中断,然后处理计数值,唤醒等操作,再开中断;

互斥量:
        互斥量与二值信号量类似,是特殊的信号量,本质是想谁上锁,谁解决这个问题,他其实也是复用了队列的代码。

与二进制信号量的区别:

        二级制信号量初始值是0,创建后需要Give一次;互斥量初始值是1,创建后不需要Give一次。

        可以实现优先级继承,高优先级任务获取互斥量失败之后,将优先级继承给获取该信号量的低优先级任务。

  

        注意:同一个任务两次调用互斥信号量会被阻塞,这时候又因为需要依赖自己释放,所以会陷入死锁。

递归锁:
它虽然也是一个互斥量,但是和互斥量不同的是,它还有一个专门用来记录任务TCB节点的成员,以及一个记录申请锁次数的计数值。    

  • 任务A获得递归锁M后,还可以多次去获得这个锁。
  • Take了N次,要GiveN次,这锁才会释放。
  • 任务A获得递归锁期间其他任务不能获得。

                

// 创建二值信号量
SemaphoreHandle_t xBinarySemaphore = xSemaphoreCreateBinary();

// 创建计数信号量,最大计数为10,初始计数为3
SemaphoreHandle_t xCountingSemaphore = xSemaphoreCreateCounting(10, 3);

// 创建互斥信号量
xSemaphoreCreateMutex()

// 创建递归锁
xSemaphoreCreateRecursiveMutex()

事件组 

        事件组是一个用于管理和同步任务的机制,允许任务之间通过事件标志来通信和同步
与队列/信号量的区别:

        1.信号量/队列当事件发生时只会去唤醒一个任务,事件组可以唤醒多个任务起到一个广播作用。

        2.信号量/队列是一个消耗性资源,数据读走了就减少,而事件组可以选择清除事件也可以选择保留事件,

        3.事件组起到一个同步作用,不能传递数据。

        4. 事件组可以实现多个任务之间的同步而队列/信号量只能是两个任务之间的同步。

// 创建事件组
EventGroupHandle_t xEventGroupCreate( void );

// 设置事件组的一个或多个事件标志位
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet );

// 清楚事件组标志位
EventBits_t xEventGroupClearBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToClear );


链接:

FreeRTOS学习(8)-pendsv和systick优先级分析以及任务切换场景_freertos systick-CSDN博客

【FreeRTOS】3. PendSV异常_freertos pendsv中断-CSDN博客

  • 13
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值