【嵌入式项目-2】mini-RTOS
作者简介和专栏内容见专栏介绍:https://www.nowcoder.com/creation/manager/columnDetail/0eL5bM
麻烦看到贴子的伙伴点点赞大家点赞订阅支持下,提前祝各位offer多多,有问题评论区见~~
在讲的时候要和Linux穿起来,把操作系统八股融进来
项目描述
实现了基于 FreeRTOS 的实时操作系统内核,可在嵌入式系统中实现高效的内存管理、任务调度及任务间通信。具体内容如下:
- 模块化编程: 使用模块化编程思想降低各个功能耦合度,实现的主要功能模块包括任务创建、任务调度、阻塞延时、多优先级和时间片轮转;
- 任务间通信: 实现消息队列模块保证任务间的同步和互斥,支持任务间不定长消息的传递和读写消息队列任务的阻塞等待和及时唤醒,以满足不同任务之间的实时性要求;
- 内存管理: 实现并改进了 FreeRTOS 内存管理函数,用双向链表结构管理内存块并支持内存块合并功能,提高了内存分配效率和利用率;
- 扩展性:项目提供详细的实现笔记和完整的代码逻辑图,方便快速理解 miniRTOS 内核函数调用情况并根据不同需求添加或修改功能模块。
背景:个人很感兴趣系统内部的运行,之前学过操作系统的课程,但理解不够深入,所以刚好有时间就动手实现了一个RTOS内核,同时在实现过程中通过和Linux做比较,加深自己对Linux的理解,对我来说收获还是很大的。整个实现大概是2个多月。
思路:整个实现主要分为四个模块吧,一个是内存管理模块,即heap.c,里边通过链表实现了对堆区内存的分配与回收;一个是任务模块,这里主要涉及任务的创建,主要是TCB和任务栈初始化这两部分内容;难度最高的是任务调度模块,这里涉及多个中断服务函数,空闲任务的创建,时间片的轮转,任务的阻塞延迟等操作;最后一块是消息队列,实现任务之间的通信,即同步与互斥。除此之外的通信方式还有信号量,互斥量,通知,事件组等,其中信号量和互斥量的底层实现和消息队列基本相同。任务通知是在TCB中加入了一个状态,从而实现通知指定的任务(这里可看下边的对比)
难点:其实各个部分都有些难度,但是通过看源码搞清楚内核的逻辑后实现起来就简单了。我这边是通过画逻辑图,把每个函数及函数功能步骤,以及他们之间的调用关系都画在一张图上,这个对我帮助很大,做完之后整个逻辑就一下子清晰了。
内存管理:
- 控制块两个元素解释;
- 首先链表地址按由低到高排序方便实现内存的回收;
- 头尾节点分别指向数组头尾防止越界和方便回收判断;
- 分配和回收时维护记录剩余大小的全局变量,对分配出去的内存块的blocksize最高位置1标记。
链表:
- 三个结构体内部的元素和作用
- 插入的两种操作
任务模块:
- 内存控制块的元素功能,尤其栈顶指针,控制块的元素赋值初始化;stateitem的pvowner
- 任务栈的初始化,外部参数如入口地址放pc,参数放r0,返回移动后的栈顶指针,即为tcb地址;
- 两个初始化完成后,初始化就绪列表,之后将任务添加至对应优先级的就绪链表(将位图对应位置1,insertEnd插入尾部并设置pvcontainer标识链表)
任务通过stateItem的container找到对应链表,链表通过pvowner找到对应任务
任务调度:
- 初始化空闲任务,配置两个中断优先级,初始化时钟,开启第一个任务svc
- systick中断服务根据更新石基单元返回值判断是否需要切换任务
- 更新tickcount,根据nextunblocktime唤醒所有到期任务,最后看当前任务所在链表大小是否大于1(时间片)
阻塞延时:
- 就一个从就绪列表删除,若删除后链表无元素则更新位图,nextunblocktime,插入延时链表的操作,延时链表按阻塞时间排序,方便唤醒。
任务切换:
- pendsv悬起位置1,没其他中断时响应并调用中断服务函数
- 上文保存+switch+下文恢复
- switch里就是找到最高优先级的下一个任务将currentTCB更新,具体先看位图最高位,再找对应优先级pxindex指向的下一个任务的pvowner
消息队列:
- 创建:内存分配,控制块中头尾读写指针,两个链表初始化
- 发送:有空闲直接拷贝并更新指针和messagewaiting,之后唤醒等待任务(事件链表和延时链表删除,加入就绪链表),不等待的直接返回,有阻塞同时在两个链表记录
逻辑图
RTT|FreeRTOS|Linux对比
- Linux内核实现系统管理四大块:系统调度、内存管理、文件系统、进程(线程)管理, RTOS没有文件系统。
- 在内存管理上也简单很多,RTOS里就是对一个数组使用链表进行管理;而Linux中使用虚拟内存(虚拟内存相关八股),还划分了内核态和用户态保证安全和稳定(为什么稳定?)。
- RTOS类似Linux线程多协程模型,因为协程是一种用户态的轻量级线程,不依赖于操作系统的调度,而是由程序控制;协程可以共用栈空间,从而节省内存;而线程是一种内核态的执行单元,由操作系统调度;线程需要独立的栈空间,占用更多的内存。相比与RTOS中的任务,进程线程机制更为复杂。
- 进程间通信:这里实现了线程间通信(消息队列),LInux对应的就是进程间通信,大部分原理都差不多,LInux中更丰富一些,有管道,消息队列....(进程间通信的八股)
- 调度策略里其实是最大的区别,也是RTOS实时性的体现(为什么Linux不是)。RTOS是抢占式的的调度策略,而linux有多种调度策略如先进先出,短作业优先,优先级队列等(介绍Linux的调度策略)
Linux |
FreeRTOS |
|
内存分配方式 |
动态分配,运行时进行 |
静态分配,编译器完成 |
内存分配方法 |
C的malloc和free |
链表管理大数组ucheap |
内存管理粒度 |
按页来分配 |
内存按块分配,未考虑碎片 |
消息队列结构 |
链表结构,支持动态大小的队列 |
底层是固定大小的环形缓冲区 |
任务切换 |
进程的上下文切换,内核调度器触发 |
协程的上下文切换,任务主动发起 |
上下文保存 |
寄存器状态、页表、文件描述符等更多 |
任务的寄存器状态和堆栈指针 |
调度策略 |
FIFO,时间片轮转,短作业优先等 |
抢占式 |
uC/OS和RT-Thread |
FreeRTOS |
|
优先级 |
数字越大优先级越小 |
数字越小优先级越小 |
解锁延时链表 |
扫描定时器链表,时间不确定 |
全局变量xNextTaskUnblockTime 来确定有无需要就绪的任务 |
时间片 |
sysTick中断周期可指定多个tick |
sysTick中断周期固定1个tick |
关中断 |
使用basepri寄存器预留一部分中断,粒度更细 |
操作PRIMASK关闭所有中断 |
软件定时器 |
直接在硬件中断中处理 |
单独一个任务来读取队列处理定时器 |
FreeRTOS开发
目录结构
数据类型
空闲任务及其钩子函数
为什么需要
一个良好的程序,它的任务都是事件驱动的:平时大部分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调度器必须能找到一个可以运行的任务:所以,我们要提供空闲任务。
钩子函数:空闲任务的循环 每执行一次,就会调用一次
- 执行一些低优先级的、后台的、需要连续执行的函数
- 测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。
- 让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式了。
- 空闲任务的钩子函数的限制: 不能导致空闲任务进入阻塞状态、暂停状态
- 如果你会使用 vTaskDelete()来删除任务,那么钩子函数要非常高效地执行。
调度算法
配置
- configUSE_PREEMPTION(可否抢占)
- configUSE_TIME_SLICING(可抢占前提下同优先级任务是否轮流执行)
同步和互斥
消息队列 |
信号量 |
互斥量/锁 |
事件组 |
|
特点 |
任务、中断间传递信息。可传递结构体获取消息源,传递地址的方式传大数据。发送和接收都可以阻塞 |
特殊的队列,队列里使用环形缓冲区存放数据,信号量里只记录计数值。只传递状态。 |
特殊的二值信号量,多了优先级继承解决反转,用来保护资源互锁,同步一般用二值信号量。 |
传递事件,发送者、接受者无限制,可以唤醒多个接收者:像广播,队列和信号量一次只唤醒一个 |
实现 |
使用pcReadFrom指向出队的位置 uxMessagesWaiting:记录消息个数 uxLengh:队列长度 |
uxMessagesWaiting: 有效信号量个数 uxLengh:最大信号量可用个数 |
使用uxRecursiveCallCount来记录递归互斥量被调用次数。 uxMessagesWaiting: 0互斥量无效 uxLengh:最大信号量可用个数=1 |
|
函数 |
xQueueSend/toBack/toFront xQueueReceive/Peek/Overwrite xQueueCreate /Reset/Delete |
xSemaphoreGive/Take/Delete xSemaphoreCreateBinary xSemaphoreCreateCounting |
xSemaphoreGive/Take/Delete xSemaphoreCreateMutex 和信号量一样发送不阻塞 |
|
特色 |
邮箱:覆盖+peek |
递归锁+优先级继承 |
队列
函数
- 创建:QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize )
- 复位:BaseType_t xQueueReset( QueueHandle_t pxQueue)
- 删除:void vQueueDelete( QueueHandle_t xQueue )
用法
- 常规:主函数创建队列,任务1发数据,任务2接收并打印
- 分辨数据源:使用结构体,传递结构体地址
- 传输大块数据:传递指针即可
- 邮箱:overwirte写,peek读
信号量
函数
- 创建二进制信号量:SemaphoreHandle_t xSemaphoreCreateBinary( void )
- 创建计数信号量:SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount)
- 删除:void vSemaphoreDelete( SemaphoreHandle_t xSemaphore )
用法
- 常规:主函数创建信号量,任务1give,任务2take
- 防止数据丢失:上一任务中发送任务发送多次只能接收到1次,故give时在串口中断中将数据放入缓冲区,take时一次将任务全读走
互斥量(互斥锁)
多用于互斥,同步用二进制信号量,不能在ISR中使用
函数
- 创建:SemaphoreHandle_t xSemaphoreCreateMutex( void )
- 删除:同信号量
用法
- 常规:主函数创建信号量,访问共享资源时take和give,其他任务等待
- 互斥锁未实现谁上锁谁解锁
- 优先级反转与继承:见下边
优先级反转是什么,如何解决(优先级继承)?
是什么
假设任务 A、 B 都想使用串口, A 优先级比较低:
- 任务 A 获得了串口的互斥量
- 任务 B 也想使用串口,它将会阻塞、等待 A 释放互斥量
- 高优先级的任务,被低优先级的任务延迟,这被称为"优先级反转"(priorityinversion)
如果涉及 3 个任务,可以让"优先级反转"的后果更加恶劣。 例子:假设有优先级为123,的三个任务LP, MP,HP
- HP:获得锁前开启延时,让MP先运行,延时结束后在获得锁的地方阻塞
- MP:获得锁前开启延时,让LP先运行,一段时间后抢占LP进入无限循环
- LP:无延时,获得任务锁,运行一段较长的代码,期间会被MP抢占导致无法释放锁。
总结:LP先持有信号量,但是 MPTask 抢占 LPTask,使得 LPTask 一直无法运行也就无法释放信号量, HP也无法运行。
怎么解决
HP获取锁时将持有锁的低优先级的任务优先级提升,待其释放锁后恢复原来优先级。
递归锁
为了解决自我死锁的问题,允许任务对已经持有的锁多次获取,释放和获取需成对出现
递归锁的原理是,在每次锁定或解锁操作时,都要检查当前执行的进程或线程是否是该锁的拥有者,并更新该锁的状态和计数器。如果当前进程或线程是第一次获取该锁,那么它将成为该锁的拥有者,并将计数器设置为1。如果当前进程或线程已经是该锁的拥有者,并再次获取该锁,那么它将增加计数器的值。如果当前进程或线程是该锁的拥有者,并释放该锁,那么它将减少计数器的值。如果计数器变为0,那么该进程或线程将放弃该锁的拥有权,并允许其他等待的进程或线程获取该锁。
事件组
事件发生时,会唤醒所有符号条件的任务,简单地说它有"广播"的作用
被唤醒的任务有两个选择,可以让事件保留不动,也可以清除事件
函数
- 创建:EventGroupHandle_t xEventGroupCreate( void );
- 删除:void vEventGroupDelete( EventGroupHandle_t xEventGroup )
- 等待事件:EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToWaitFor, const BaseType_t xClearOnExit, const BaseType_t xWaitForAllBits, TickType_t xTicksToWait );
- 同步点:EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet, const EventBits_t uxBitsToWaitFor, TickType_t xTicksToWait );
A、B、C 做好自己的事后,还要等别人做完;大家一起做完,才可开饭
用法
- 等待多个事件:大厨要等手下做完这些事才可以炒菜:洗菜、生火
- 任务同步:假设 ABC 三人要吃饭,各司其职: A:炒菜 B:买酒
C:摆台 三人都做完后,才可以开饭。三个代码类似
任务通知
在任务结构体 TCB 中包含了内部对象,可以直接接收别人发过来的 "通知"
数据独享,无法缓冲,无法广播,发送方阻塞
- 一个是 uint8_t 类型,用来表示通知状态
- 一个是 uint32_t 类型,用来表示通知值
typedef struct tskTaskControlBlock { ...... /* configTASK_NOTIFICATION_ARRAY_ENTRIES = 1 */ volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ]; volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ]; ...... } tskTCB;
- 可实现邮箱、信号量、事件组
函数
- 创建:SemaphoreHandle_t xSemaphoreCreateMutex( void )
- 删除:同信号量
- 发送:BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );
- 全能型发送:BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction )
xTaskNotify 函数功能更强大,可以使用不同参数实现各类功能,比如:
- 让接收任务的通知值加一:这时 xTaskNotify()等同于 xTaskNotifyGive()
- 设置接收任务的通知值的某一位、某些位,这就是一个轻量级的、更高效的事件组
- 把一个新值写入接收任务的通知值:上一次的通知值被读走后,写入才成功。这就是轻量级的、长度为 1 的队列
- 用一个新值覆盖接收任务的通知值:无论上一次的通知值是否被读走,覆盖都成功。类似 xQueueOverwrite()函数,这就是轻量级的邮箱。
- 接收:uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
- 全能接收:BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, uint32_t ulBitsToClearOnExit, uint32_t *pulNotificationValue, TickType_t xTicksToWait );
用法
- 传输计数值:发送任务将数据写入环形缓冲区,使用give让通知值+1;接受任务使用take取出通知值(数据个数),打印缓冲区数值
- 传输任意值:直接给对应TCB写数据,接收方wait到了读出参数即可
软件定时器
分一次性和自动加载,通过回调函数完成定时后的功能,不在Tick中断里执行定时器函数,因为时间不确定,所以用守护任务来执行
函数
- 创建
/* 使用动态分配内存的方法创建定时器 * pcTimerName:定时器名字, 用处不大, 尽在调试时用到 * xTimerPeriodInTicks: 周期, 以 Tick 为单位 * uxAutoReload: 类型, pdTRUE 表示自动加载, pdFALSE 表示一次性 * pvTimerID: 回调函数可以使用此参数, 比如分辨是哪个定时器 * pxCallbackFunction: 回调函数 * 返回值: 成功则返回 TimerHandle_t, 否则返回 NULL */ TimerHandle_t xTimerCreate( const char * const pcTimerName, const TickType_t xTimerPeriodInTicks,const UBaseType_t uxAutoReload, void * const pvTimerID,TimerCallbackFunction_t pxCallbackFunction );
- 删除:BaseType_t xTimerDelete( TimerHandle_t xTimer, TickType_t xTicksToWait );
用法
- 常规:和创建任务一样在主函数创建定时器,并启动定时器,定义好回调函数。需在配置文件中设置相关选项
- 消除抖动:在任务中多次reset定时器
中断管理
假设当前系统正在运行 Task1 时,用户按下了按键,触发了按键中断。这个中断的处
理流程如下:
- CPU 跳到固定地址去执行代码,这个固定地址通常被称为中断向量,这个跳转时硬件实现的
- 执行代码做什么?
ISR在内核中调用,执行过程中用户任务无法执行,所以需要快速处理。若时间较长就把复杂的事情放到任务中去
使用原则
- 任务与硬件无关,优先级由程序员确定,何时运行由调度器指定
- ISR和硬件密切相关,执行和调度均由硬件决定
- ISR优先级高于任务
两类API
原因
- 很多API导致任务进入阻塞(消息队列),ISR调用API函数时不能阻塞
好处
- 省去额外的条件判断,方便调试
- 参数列表不同
缺点及解决
ISR有时会使用第三方库,库函数会调用API,解决如下:
- 中断的处理推迟到任务中进行,在任务中调用库函数
- 在库函数中使用FromISR函数
两类中断
更高优先级的任务不依赖于RTOS,可做一些更可靠时间可控的事情,所以分两类。A类不能使用RTOS API,通过configassert函数判断优先级保证不能调用,断言不成立会死循环
调试与优化
调试
- 打印
- 断言configASSERT():内容必须为真,否则停止程序
- Trace:很多宏放在关键位置,不影响代码,当需要调试某些功能时可以修改宏
- Malloc Hook函数:检测内存越界、栈溢出
优化
- 查看栈使用情况:UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask ); 从栈底到栈顶逐字节判断,一直是0xa5就表示是空闲的
- 运行时间统计:在 vTaskSwitchContext 函数中使用比TIck中断更快的定时器来统计才精确
模拟常见问题
实时性体现在哪里?
1.抢占式调度,在任务切换时(时钟中断,任务delay)都会在PendSV 中断服务函数中切换任务时选择最高优先级的任务
会经常检查是否需要切换函数,比如在消息队列中,当成功发送数据到队列后,需要唤醒事件列表中的任务,会比较当前任务的优先级,执行任务切换
为什么需要操作系统
核心是中断,有了中断才有了进程,有了状态和切换,调度等一系列事情。所有的目的都是为了让CPU效率更高
函数重入
"可重入的函数"是指:多个任务同时调用它、任务和中断同时调用它,函数的运行也是安全的。可重入的函数也被称为"线程安全"(thread safe)。
每个任务都维持自己的栈、自己的 CPU 寄存器,如果一个函数只使用局部变量,那么它就是线程安全的。
函数中一旦使用了全局变量、静态变量、其他外设,它就不是"可重入的",如果该函数正在被调用,就必须阻止其他任务、中断再次调用它。
1为什么需要自己实现内存分配
- 不适合用在资源紧缺的嵌入式系统中
- 这些函数的实现过于复杂、占据的代码空间太大
- 并非线程安全的(thread-safe)
- 运行有不确定性:每次调用这些函数时花费的时间可能都不相同
- 内存碎片化
- 使用不同的编译器时,需要进行复杂的配置
- 有时候难以调试
临界资源的访问
不想使用队列等方式,可以自己保护临界资源
- taskENTER_CRITICAL:屏蔽中断
- vTaskSuspendAll:如果有别的任务来跟你竞争临界资源,你可以把中断关掉:这当然可以禁止别的任务运行,但是这代价太大了。它会影响到中断的处理。如果只是禁止别的任务来跟你竞争,不需要关中断,暂停调度器就可以了:在这期间,中断还是可以发生、处理。
taskENTER_CRITICAL(); /* 可以避免其他任务、ISR来破坏 */ taskEXIT_CRITICAL(); ----------------------------------------- vTaskSuspendAll(); /* 可以避免其他任务来破坏 */ ( void ) xTaskResumeAll();
taskEXIT_CRITICAL和vTaskSuspendAll有什么区别
`taskENTER_CRITICAL`和`vTaskSuspendAll`都是FreeRTOS中用于临界区保护的API函数,但它们的作用有所不同。
`taskENTER_CRITICAL`函数用于进入临界区,并禁止更高优先级的任务抢占当前任务。在临界区内,任务可以安全地访问共享资源,而不必担心其他任务的干扰。当任务退出临界区时,FreeRTOS会根据当前的中断嵌套层数来决定是否恢复中断。如果中断嵌套层数为0,则会启用中断,允许更高优先级的任务抢占当前任务。
`vTaskSuspendAll`函数用于暂停调度器,并禁用所有可抢占的任务,包括当前任务和更高优先级的任务。这可以用于实现临界区保护,以确保在临界区内不会有任务抢占当前任务。当调用`vTaskResumeAll`函数时,调度器会重新启动,并且所有被暂停的任务会重新启用。需要注意的是,调用`vTaskSuspendAll`函数后,任务的时间戳(timestamp)和延迟计时器(delayed timer)也会被暂停,因此如果在临界区内等待时间,需要使用`ulTaskNotifyTake()`函数等待事件通知。
综上所述,`taskENTER_CRITICAL`和`vTaskSuspendAll`都是用于临界区保护的函数,但它们的作用有所不同。`taskENTER_CRITICAL`用于进入临界区,并禁止更高优先级的任务抢占当前任务,而`vTaskSuspendAll`用于暂停所有可抢占的任务,并在调用`vTaskResumeAll`函数时重新启用它们。
内存管理中改进了什么?
测试了
有哪些关键变量,作用是什么?
- 全局TCB指针,pxCurrentTCB:指向当前或将要运行的任务控制块(栈顶指针)
- xBlockAllocatedBit:改变blocksize最高位,指示内存块状态
- xNextTaskUnblockTime:记录最快的延时任务到期时间,就不用扫描了,不像RTT
- uxMessageWaiting:记录当前消息队列的消息个数,在发送和接收时靠他判断
- xTickCount:系统时钟计数器,用于实现延迟函数和时间片轮转算法等
- uxTopReadyPriority:记录最高优先级的就绪任务的优先级,位图,在任务状态改变时需要修改,任务切换时要拿他判断
控制块结构体汇总
- 任务控制块(TCB):存储任务的状态、优先级、堆栈指针等信息。
- 队列控制块(Queue Control Block,QCB):存储队列的状态、队列长度、队列元素大小等信息。
- 链表结点结构体:排序值,前后指针,属于的TCB, 所在的链表,任务控制块中包含它,以此来加入对应链表
- 链表结构体:节点数,当前再用的节点指针,尾结点结构体
- 内存控制块:下一个空闲块,块大小
内存分配中要注意什么?
- 加上0x0007,再把超过的清零,这里用到
- 用0x0008 - 原始地址低三位,得到具体差的值,加到原始地址上去
portBYTE_ALIGNMENT:选择对齐的位数,这里是8位对齐
portBYTE_ALIGNMENT_MASK:0x0007若低三位不为0,则清零,完成对齐操作
RTOS启动流程
两种方法
- 在 main 函数中将硬件初始化, RTOS 系统初始化,所有任务的创建这些都弄好 ,最后启动调度器
- 在 main 函数中将硬件和 RTOS 系统先初始化好,然后创建一个启动任务后就启动调度器,然后在启动任务里面创建各种应用任务,当所有任务都创建成功后,启动任务把自己删除
默认第二种
启动流程
- 创建任务 xTaskCreate()函数 在 main()函数中直接初始化我们的板级外设——BSP_Init(),然后进行任务的创建即可xTaskCreate(),在任务创建中, FreeRTOS 会帮我们进行一系列的系统初始化,例如初始化堆内存调用prvHeapInit
- vTaskStartScheduler() 在创建完任务的时候,我们需要开启调度器,因为创建仅仅是把任务添加到系统中,还没真正调度,并且空闲任务也没实现,定时器任务也没实现,这些都是在开启调度函数vTaskStartScheduler()中实现的。
- main 函数 创建并启动一些任务和硬件初始化
注意:在启动任务调度器的时候,假如启动成功的话,任务就不会有返回了,假如启动没成功,则通过 LR 寄存器指定的地址退出,在创建 AppTaskCreate 任务的时候,任务栈对应 LR 寄存器指向是任务退出函数prvTaskExitError(),该函数里面是一个死循环, 这代表着假如创建任务没成功的话,就会进入死循环,该任务也不会运行。
保存/恢复现场的应用场景
PendSV 中断服务函数是真正实现任务切换的地方,负责保存/恢复现场。
- 任务切换:需保存所有寄存器的值。(在时钟中断中的切换等),portYIELD 的实现很简单,实际就是将 PendSV 的悬起位置 1,当没有其它中断运行的时候响应 PendSV 中断,去执行我们写好的 PendSV中断服务函数,在里面实现任务切换。
- 函数中调用另一函数可不保存传入的形参对应的寄存器 r0 r1 r2,
- 硬件中断:硬件和软件都保存寄存器,对应自动加载和手动加载部分,防止在中断中调用函数破坏寄存器现场
?:各种中断,时钟产生中断后是做了什么?
哪些函数执行时需要开关中断?
- xPortPendSVHandler()中,调用void vTaskSwitchContext( void )期间
- xPortSysTickHandler(),更新时基和切换任务时
关键的汇编
ldr |
从存储器加载字到一个寄存器 |
str |
把一个寄存器按字存储到存储器 |
ldmia |
将多个字从存储器加载到 CPU寄存器, 先操作,指针在递增 |
stmdb |
将多个字从 CPU寄存器存储到存储器, 指针先递减,再操作 |
mrs |
加载特殊功能寄存器的值到通用寄存器 |
msr |
存储通用寄存器的值到特殊功能寄存器 |
orr |
按位与 |
bx |
直接跳转到由寄存器给定的地址 |
bl |
跳转到 标号对应的地址,并且把跳转前的下条指令地址保存到 LR |
任务栈相关问题
- FreeRTOS任务形参只能有一个吗?
使得,它是一个void *类型的指针,用于传递给任务函数的参数。如果需要传递多个参数,可以使用结构体或者数组来封装参数,并把它们的地址作为任务形参。
- 寄存器r4-r11作用
是ARM处理器的通用寄存器,它们可以用于存储函数的局部变量或者中间结果。如果一个函数调用了另一个函数,它需要在调用前把r4-r11的值保存在栈上,以免被覆盖
- 为什么r0上边的寄存器需要自动加载?
根据代码,在保存上文和恢复时,需要用到那几个寄存器,为了防止被覆盖,所以自动加载
- 空闲栈是干什么的?
- 初始化函数时在LR寄存器放的无限循环函数有什么作用?
它的作用是防止任务函数退出或返回到它的调用者,因为这样做是不合法的。如果一个任务想要退出,它应该调用vTaskDelete(NULL)来删除自己
什么情况下,任务在tick中途放弃运行?
- 主动放弃:调用vTaskDelay或读队列阻塞
- 被动放弃:硬件中断写入了队列唤醒了另一任务
时间片在程序中如何实现?
SysTick 中断服务函数在调度器中启动,会开启时钟,从而激活中断服务函数xPortSysTickHandler,每一个tick调用一次。函数中需要依次进行系统时基的更新和根据条件的任务切换。在xTaskIncrementTick()更新时基的函数最后会判断当前任务链表的大小是否大于1,大于1会置xSwitchRequired = pdTRUE,从而执行任务切换。
任务切换开启pendSV中断,保存好上文后调用vTaskSwitchContext切换pxCurrentTCB的指向。具体是调用taskSELECT_HIGHEST_PRIORITY_TASK,里边会调用portGET_HIGHEST_PRIORITY找到最高优先级,接着调用listGET_OWNER_OF_NEXT_ENTRY获取当前优先级就绪链表下pxIndex指向的下一个任务的pvOwner,然后更新到pxCurrentTCB。至此完成时间片的轮转,可见逻辑图,从调度器开始看
时间片轮转过程中任务休眠吗?
在时间片轮转过程中,FreeRTOS不会将任务直接放入阻塞链表。阻塞链表是由那些由于某些原因(如等待事件、等待资源等)而进入阻塞状态的任务组成的链表。时间片轮转是一种非抢占式的调度算法,任务在执行完时间片后会主动让出CPU,等待下次调度。
FreeRTOS中的任务调度器使用了一个就绪队列来管理所有就绪(可执行)状态的任务,这些任务都属于同一个优先级。当一个任务的时间片用完后,它会被放回到就绪队列中,等待下一次调度。任务调度器会选择就绪队列中的下一个任务来执行。
只有当任务处于阻塞状态时,它才会被放入阻塞链表中。时间片轮转是针对就绪状态的任务进行调度,并不涉及阻塞链表。阻塞链表中的任务会在满足特定条件时,如等待队列中的事件或资源就绪,才会被调度器从阻塞链表中移出并放入就绪队列中,等待下一次调度。
系统怎么处理中断的?过程是什么?
cpu怎么接收中断的?通过什么装置?
改动|思考|注意(可以问问题)
- 改:heap.c中,分配内存时直接将第二块接前一块后边,而不是删除后重新插入
- 思考:xFreeBytesRemaining变量用来判断是否可分配,考虑记录最大的块更好,但会有以下问题
- 思考:pxNewTCB是如何变化实现任务的切换的:void vTaskSwitchContext( void )
- 注意:只有就绪列表是数组形式,配套有对应的初始化函数,其他的列表都是单个链表(两个事件链表,一个延时链表)
- 注意:prvAddNewTaskToReadyList函数中 <= 的逻辑决定了相同优先级任务插入时,最后插入的先执行
- 注意:各个函数的返回值
存在的问题
- PSP与MSP的区别,在任务切换和运行中是怎么移动的?
- PSP堆栈是进程栈指针(Process Stack Pointer)的缩写,它是任务自己的栈,用于指向当前任务的栈顶1。PSP堆栈在线程模式下使用,当控制寄存器(CONTROL)的第1位(SPSEL)为1时,PSP堆栈就是活动的栈指针1。PSP堆栈通常用于多任务操作系统中,每个任务都有自己的栈空间2。当任务切换时,PSP堆栈会保存和恢复任务的上下文2。
- MSP堆栈是主栈指针(Main Stack Pointer)的缩写,它是main函数的栈指针,也给中断函数使用。
- 任务切换模块后:需要把当前任务控制块指针 pxCurrentTCB、就绪列表 pxReadyTaskLists、每个任务的控制块和任务的栈这些变量统统添加到观察窗口,然后单步执行程序,看看这些变量是怎么变化的。特别是任务切换时,CPU 寄存器、任务栈和 PSP 这些是怎么变化的,让机器执行代码的过程在自己的脑子里面过一遍。
- R14和R15寄存器
- R14是链接寄存器(Link Register)的缩写,它用于存储返回地址,比如在进行函数调用时¹。当执行分支并链接(BL或BLX)指令时,返回地址会被存储在R14寄存器中²。当函数或子程序执行完毕时,返回地址会从R14寄存器中加载到程序计数器(PC)中,从而恢复调用者的执行²。您可以用R14或LR来表示这个寄存器¹
- R15是程序计数器(Program Counter)的缩写,它用于指向当前执行的指令的地址¹。当CPU执行一条指令时,R15寄存器会自动增加,以便指向下一条指令的地址¹。当发生分支、跳转或异常时,R15寄存器会被修改为目标地址,以便改变程序的执行流程¹。您可以用R15或PC来表示这个寄存器¹。
- pendSV和SVC
pendSV和SVC是两种针对软件和操作系统的异常。它们都可以通过软件指令来触发,用于执行系统功能调用。
- SVC是监督调用(Supervisor Call)的缩写,它用于生成系统函数调用¹。例如,操作系统可以通过SVC来提供对硬件的访问,而不是让用户程序直接访问硬件¹。SVC不能被挂起(pend),一个调用SVC的应用程序会期望所需的任务立即完成¹。
- pendSV是可挂起服务调用(Pendable Service Call)的缩写,它与SVC配合在操作系统中工作¹。pendSV可以被挂起,它对于操作系统来说很有用,可以挂起一个异常,以便在完成其他重要任务后执行一个动作¹。例如,pendSV可以用于实现上下文切换²,当操作系统需要切换到另一个任务时,它可以设置PENDSVSET位来触发pendSV异常²。pendSV异常会在其他更高优先级的中断返回后执行,并执行上下文切换的代码²。
移植到STM32
前置知识见下边的创建工程
提取最简代码
src:存放原来在Source下的源码(list.c,task.c....)
port: 原MemMang中内存管理代码+处理器架构代码(RVDS/ARM_CM3)
include:原include文件夹
拷贝 FreeRTOS 到裸机工程根目录 ![](https://uploadfiles.nowcoder.com/files/20240403/845797552_1712103096865/1686708774870-2249a269-253e-4fd2-bd71-9b50120533f3.png)
- 拷贝 FreeRTOSConfig.h (配置文件,在原Demo中)文件到 user 文件夹
- Keil内操作添加组和文件
- 指定 FreeRTOS 头文件的路径 (FreeRTOS 的源码里面只有 FreeRTOS\include 和FreeRTOS\port\RVDS\ARM_CM?这两个文件夹下面有头文件+ FreeRTOSConfig.h路径)
- 修改 FreeRTOSConfig.h
- 修改 stm32f10x_it.c :SysTick 中断服务函数是一个非常重要的函数, FreeRTOS 所有跟时间相关的事情都在里面处理
- 下载验证
创建工程
创建文件夹保存对应文件
- Deme 例程,我们可以直接打开里面的工程文件, 各种开发平台的完整 Demo,开发者可以方便的以此搭建出自己的项目,甚至直接使用。
Keil项目创建
- 选择处理器(Device) ARMCM3
- 环境配置
CMSIS中选择CORE,Device中选择StartUp
这两个文件刚开始都是存放在 KEIL 的安装目录下,软件就会把选中好的文件从 KEIL 的安装目录拷贝到我们的工 程 目 录 : Project\RTE\Device\ARMCM3 下 面 。 其 中startup_ARMCM3.s 是汇编编写的启动文件,system_ARMCM3.c 是 C 语言编写的跟时钟相关的文件。
- 添加文件组和对应文件
- 设置软件仿真
- 修改时钟大小
- 添加头文件路径
裸机系统和多任务系统介绍
裸机系统分为轮询系统和前后台系统。轮询系统就是没有中断的主函数循环,前后台就是加入了中断的主循环,多任务就是加入操作系统对任务划分优先级进行调度。
内存分配heap.c
实现heap4.c End参考heap2, 不需要搞成指针
文件 |
优点 |
缺点 |
heap_1.c |
分配简单,时间确定 |
只分配、不回收 |
heap_2.c |
动态分配、最佳匹配 |
碎片、时间不定 |
heap_3.c |
调用标准库函数 |
速度慢、时间不定 |
heap_4.c |
相邻空闲内存可合并 |
可解决碎片问题、时间不定 |
heap_5.c |
在heap_4基础上支持分隔的内存块 |
可解决碎片问题、时间不定 |
跟heap_2的对比
- 相同点:都是使用BlockLink_t结构体管理堆,都有一个空闲Block链表
- 差别:
堆的内存来源
一开始,"堆"是一些空闲内存,怎么得到这些空闲内存?
- 在汇编代码里指定一个AREA:在汇编代码里,使用SPACE命令可以分配一段空间
- 在C代码里,定义一个全局数组:
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) )
链表结构体 A_BLOCK_LINK
heap.c里,使用链表来管理内存。链表结构体为:
这个结构体用来表示空闲块:
- pxNextFreeBlock:指向下一个空闲块
- xBlockSize:当前空闲块的内存大小
这个结构体后面,紧跟着空闲内存,如下图所示:
堆初始化prvHeapInit
示意图
关键步骤
- 让pucAlignedHeap指向对齐后的地址
- 初始化xStart和pxEnd的结构体
- 初始化第一个空闲块
- 其他函数使用的变量进行初始化
两种对齐方法:
- 加上0x0007,再把超过的清零,这里用到
- 用0x0008 - 原始地址低三位,得到具体差的值,加到原始地址上去
static void prvHeapInit( void ) /* PRIVILEGED_FUNCTION */ { BlockLink_t * pxFirstFreeBlock; uint8_t * pucAlignedHeap; size_t uxAddress; size_t xTotalHeapSize = configTOTAL_HEAP_SIZE; /* 1.地址对齐操作 让pucAlignedHeap指向ucHeap中对齐的首地址 portBYTE_ALIGNMENT:选择对齐的位数,这里是8位对齐 portBYTE_ALIGNMENT_MASK:若低三位不为0,则清零,完成对齐操作 更新堆的总大小 */ uxAddress = ( size_t ) ucHeap; if( ( uxAddress & portBYTE_ALIGNMENT_MASK ) != 0 ) //8位对齐,MASK=0x0007 { // 8 - 1 ,因为向后对齐,所以以0001为极端情况,给他+7,可以保证进位,找到最近的对齐位 uxAddress += ( portBYTE_ALIGNMENT - 1 ); uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK ); xTotalHeapSize -= uxAddress - ( size_t ) ucHeap; } pucAlignedHeap = ( uint8_t * ) uxAddress; /* 2.头结点结构体xStart 的初始化 让next指针指向 pucAlignedHeap (强转是防止编译器报错) 大小为固定为0 */ xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap; xStart.xBlockSize = ( size_t ) 0; /* 3.尾指针pxEnd初始化 先找到堆的尾部,然后减去一个block大小(注意对齐操作) pxEnd指向NULL,大小为0,同start */ uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize; uxAddress -= xHeapStructSize;//对block结构体对齐后返回的大小 uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK ); pxEnd = ( void * ) uxAddress; pxEnd->xBlockSize = 0; pxEnd->pxNextFreeBlock = NULL; /* 4.第一个空闲块的创建 pxFirstFreeBlock指向pucAlignedHeap,即为第一个空闲块地址 */ pxFirstFreeBlock = ( void * ) pucAlignedHeap; //uxAddress之前是指向pxEnd的地址 pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock; pxFirstFreeBlock->pxNextFreeBlock = pxEnd; /* 为其他函数准备的最小块大小和剩余块大小 剩余块大小:为了判断是否可分配,每分配出一块内存,就减一下 最小块大小:不知道什么作用,分配成功后和剩余块大小取最小 */ xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize; xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize; /* 后续使用最高位是1还是0判断当前块是否被使用, 被使用时与该变量进行&操作 heapBITS_PER_BYTE = 8 sizeof( size_t ) : 32位系统size_t是4字节 1 << 31 */ xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 ); }
uxAddress复用,完成pucAlignedHeap,xStart,pxEnd,xBlockSize的初始化
内存分配:pvPortMalloc
示意图
关键步骤
- 给需要的内存加Block头
- 对新需要的内存进行向上对齐
- 从空闲链表头开始找第一个满足大小的块
- 找到后,返回指针指向pxBlock再过一个Block大小
- 让pxPreviousBlock指向下一块->空闲链表中删除了当前块
- 对这块内存进行切割, 剩余的部分需要大于对齐后block结构体的两倍,然后赋给新块
- 更新两个块的大小并将没用到的块插回去prvInsertBlockIntoFreeList
- 更新两个记录空闲块大小的变量,并更改分配出去的块的xBlockSize最高位为1
注意事项
操作期间要关闭任务调度器
中间时刻对地址越界进行检查,越界会导致+一个数变负数
static const size_t xHeapStructSize = ( sizeof( BlockLink_t ) + ( ( size_t ) ( portBYTE_ALIGNMENT - 1 ) ) ) & ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
可优化方向
- 分配内存时采取的策略是将整块剔除原链表,然后把没用到的插回去。考虑地址是连续的,这样的操作有些浪费。可不可以改为,切出用到的,没用的加个block头,首地址让pxPreviousBlock的next指针指向即可
void * pvPortMalloc( size_t xWantedSize ) { BlockLink_t * pxBlock, * pxPreviousBlock, * pxNewBlockLink; void * pvReturn = NULL; vTaskSuspendAll(); { /* 若是第一次分配,需要先初始化 */ if( pxEnd == NULL ) { prvHeapInit(); } /* 通过判断xWantedSize最高位是否为1,检查需要的大小不能太大 */ if( ( xWantedSize & xBlockAllocatedBit ) == 0 ) { /* 给需要的内存加block头 */ if( ( xWantedSize > 0 ) && ( ( xWantedSize + xHeapStructSize ) > xWantedSize ) ) /* Overflow check */ { xWantedSize += xHeapStructSize;//对齐后的block大小 /* 需求内存内进行对齐 eg:100->104 */ if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 ) { /* Byte alignment required. Check for overflow. */ if( ( xWantedSize + ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) ) ) > xWantedSize ) { xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) ); configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 ); } else { xWantedSize = 0; } } } else { xWantedSize = 0; } if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) ) { /* 从空闲链表头开始找第一个满足大小的块 初始化后xStart是 pucAlignedHeap */ pxPreviousBlock = &xStart; pxBlock = xStart.pxNextFreeBlock; while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) ) { pxPreviousBlock = pxBlock; pxBlock = pxBlock->pxNextFreeBlock; } /* pxBlock != pxEnd 说明找到了*/ if( pxBlock != pxEnd ) { /* 返回指针指向pxBlock再过一个Block大小 */ pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize ); /* 删除这块内存 */ pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock; /* 对这块内存进行切割,没用到的再插入回去 剩余的部分需要大于对齐后block结构体的两倍 */ if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE ) { /* 让pxNewBlockLink指向第二块block */ pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize ); configASSERT( ( ( ( size_t ) pxNewBlockLink ) & portBYTE_ALIGNMENT_MASK ) == 0 ); /* 设置两块内存的xBlockSize */ pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize; pxBlock->xBlockSize = xWantedSize; /* 把剩下的那块插回去 */ prvInsertBlockIntoFreeList( pxNewBlockLink ); } //更新两个变量 xFreeBytesRemaining -= pxBlock->xBlockSize; if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining ) { xMinimumEverFreeBytesRemaining = xFreeBytesRemaining; } /* 分配标志位置1,*/ pxBlock->xBlockSize |= xBlockAllocatedBit; pxBlock->pxNextFreeBlock = NULL; xNumberOfSuccessfulAllocations++; } /* 用于在FreeRTOS中跟踪内存分配的情况。 它可以记录每次调用pvPortMalloc()或vPortFree()时的堆大小、堆使用量和堆剩余量1。 它还可以记录每次分配或释放内存时的函数名和行号1。这些信息可以帮助调试内存泄漏或内存不足的问题1。 要使用traceMALLOC,需要在FreeRTOSConfig.h中定义configUSE_TRACE_FACILITY为1, 并且定义configUSE_MALLOC_FAILED_HOOK为11。 然后,要实现一个钩子函数vApplicationMallocFailedHook(),在这个函数中调用traceMALLOC()宏1。 */ traceMALLOC( pvReturn, xWantedSize ); } ( void ) xTaskResumeAll(); /*用于设置是否使用内存分配失败钩子函数 如果这个宏的值为1,那么当动态内存分配函数pvPortMalloc()返回NULL时, 就会调用应用程序定义的钩子函数vApplicationMallocFailedHook()。 这个钩子函数可以帮助识别由于堆内存不足而导致的问题。 如果这个宏的值为0,那么就不会使用这个钩子函数。 */ #if ( configUSE_MALLOC_FAILED_HOOK == 1 ) { if( pvReturn == NULL ) { extern void vApplicationMallocFailedHook( void ); vApplicationMallocFailedHook(); } } #endif /* if ( configUSE_MALLOC_FAILED_HOOK == 1 ) */ //检查返回的地址是否对齐 configASSERT( ( ( ( size_t ) pvReturn ) & ( size_t ) portBYTE_ALIGNMENT_MASK ) == 0 ); return pvReturn; }
mtCOVERAGE_TEST_MARKER()是一个宏,用于在FreeRTOS源码中标记一些决策或条件点。它的目的是为了进行代码覆盖率分析,检查这些点是否在测试中被执行过,以及执行的结果是真还是假。默认情况下,这个宏什么也不做,所以不会产生任何代码。当进行测试时,这个宏可以被定义为一个空操作,这样测试工具就可以告诉我们这一行是否被执行过。这里就删除了
内存释放:void vPortFree
示意图
关键步骤
- 给返回内存块腾出block的位置(分配出去时没有block)
- 复位xBlockSize分配标志位
- 更新xFreeBytesRemaining
- 插入返回的内存块到空闲链表
注意事项
void vPortFree( void * pv ) { uint8_t * puc = ( uint8_t * ) pv; BlockLink_t * pxLink; if( pv != NULL ) { /* 给返回的内存加个头的位置 */ puc -= xHeapStructSize; /* This casting is to keep the compiler from issuing warnings. */ pxLink = ( void * ) puc; /* Check the block is actually allocated. */ configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 ); configASSERT( pxLink->pxNextFreeBlock == NULL ); if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 ) { if( pxLink->pxNextFreeBlock == NULL ) { /* 复位分配标志位 */ pxLink->xBlockSize &= ~xBlockAllocatedBit; vTaskSuspendAll(); { /* 插入 */ xFreeBytesRemaining += pxLink->xBlockSize; traceFREE( pv, pxLink->xBlockSize ); prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) ); xNumberOfSuccessfulFrees++; } ( void ) xTaskResumeAll(); } } } }
插入空闲链表:prvInsertBlockIntoFreeList
关键步骤
- 从xStart开始找第一个地址大于当前地址的块,停止时指向前一个位置
- 算出当前内存的结尾地址和带插入的地址是否相同,判断和前边是否可以合并
- 算出带插入内存结尾地址和下一块地址是否相同 ,判断和后边是否可以合并(不能和尾块合并)
- 判断一下是否已经在空闲链表中,然后再把前一个指针指向它
注意事项
- 在和前一个合并失败后,需要判断一下是否已经在空闲链表中再把前一个指针指向它,故没有在合并的else里处理,而是在最后处理。
static void prvInsertBlockIntoFreeList( BlockLink_t * pxBlockToInsert ) /* PRIVILEGED_FUNCTION */ { BlockLink_t * pxIterator; uint8_t * puc; /* 从xStart开始找第一个地址大于当前地址的块,停止时指向前一个位置. */ for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock ) { /* Nothing to do here, just iterate to the right position. */ } /* Do the block being inserted, and the block it is being inserted after * make a contiguous block of memory? */ puc = ( uint8_t * ) pxIterator; /* 判断和前边是否可以合并 算出结尾地址和带插入的地址是否相同 */ if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert ) { pxIterator->xBlockSize += pxBlockToInsert->xBlockSize; pxBlockToInsert = pxIterator; } /* 判断和后边是否可以合并 算出带插入内存结尾地址和下一块地址是否相同 不能和尾块合并 */ puc = ( uint8_t * ) pxBlockToInsert; if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock ) { if( pxIterator->pxNextFreeBlock != pxEnd ) { /* Form one big block from the two blocks. */ pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize; pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock; } else { pxBlockToInsert->pxNextFreeBlock = pxEnd; } } else { pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock; } /* 判断一下是否已经在空闲链表中再把前一个指针指向它,故没有在合并的else里处理 */ if( pxIterator != pxBlockToInsert ) { pxIterator->pxNextFreeBlock = pxBlockToInsert; } }
链表实现:list.c
在 FreeRTOS 中,凡是涉及到数据类型的地方,FreeRTOS 都会将标准的 C 数据类型用 typedef 重新取一个类型名。这些经过重定义的数据类型放在 portmacro.h (portmacro.h 第一次使用需要在 include 文件夹下面新建然后添加到工程 freertos/source 这个组文件)这个头文件,具体见代码清单 6-4。代码清单 6-4 中除了 TickType_t 外,其它数据类型重定义是本章后面内容需要使用到,这里统一贴出来,后面将不再赘述。
示意图
在上图中,链表中元素是顺序是:item1、item2、item3、xListEnd。
list中有一个pxIndex,指向当前真在使用的item。链表的遍历过程如下:
- pxIndex初始时指向xListEnd
- 要取出第一个元素时,pxIndex就会指向item1
- 再取出下一个元素时,pxIndex就会指向item2
- 再取出下一个元素时,pxIndex就会指向item3
- 再取出下一个元素时,pxIndex就会指向xListEnd
- 发现它是xListEnd时,继续去下一个元素,pxIndex就会指向item1
list.h主要部分
xLIST只记录链表的关键值:数量和当前遍历到的节点;具体插入删除托管给xListEnd。xListEnd既做头又做尾
/* 节点结构体定义 */ struct xLIST_ITEM { TickType_t xItemValue; /* 辅助值,用于帮助节点做顺序排列 */ struct xLIST_ITEM * pxNext; /* 指向链表下一个节点 */ struct xLIST_ITEM * pxPrevious; /* 指向链表前一个节点 */ void * pvOwner; /* 指向拥有该节点的内核对象,通常是TCB */ void * pvContainer; /* 指向该节点所在的链表 */ }; typedef struct xLIST_ITEM ListItem_t; /* 节点数据类型重定义 */ /* mini节点结构体定义,作为双向链表的结尾 因为双向链表是首尾相连的,头即是尾,尾即是头 */ struct xMINI_LIST_ITEM { TickType_t xItemValue; /* 辅助值,用于帮助节点做升序排列 */ struct xLIST_ITEM * pxNext; /* 指向链表下一个节点 */ struct xLIST_ITEM * pxPrevious; /* 指向链表前一个节点 */ }; typedef struct xMINI_LIST_ITEM MiniListItem_t; /* 最小节点数据类型重定义 */ /* 链表结构体定义 */ typedef struct xLIST { UBaseType_t uxNumberOfItems; /* 链表节点计数器 */ ListItem_t * pxIndex; /* 链表节点索引指针 ,指向当前在用的Item*/ MiniListItem_t xListEnd; /* 链表最后一个节点 */ } List_t; /* ************************************************************************ * 宏定义 ************************************************************************ */ /* 初始化节点的拥有者 */ #define listSET_LIST_ITEM_OWNER( pxListItem, pxOwner ) ( ( pxListItem )->pvOwner = ( void * ) ( pxOwner ) ) /* 获取节点拥有者 */ #define listGET_LIST_ITEM_OWNER( pxListItem ) ( ( pxListItem )->pvOwner ) /* 初始化节点排序辅助值 */ #define listSET_LIST_ITEM_VALUE( pxListItem, xValue ) ( ( pxListItem )->xItemValue = ( xValue ) ) /* 获取节点排序辅助值 */ #define listGET_LIST_ITEM_VALUE( pxListItem ) ( ( pxListItem )->xItemValue ) /* 获取链表根节点的节点计数器的值 */ #define listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxList ) ( ( ( pxList )->xListEnd ).pxNext->xItemValue ) /* 获取链表的入口节点 */ #define listGET_HEAD_ENTRY( pxList ) ( ( ( pxList )->xListEnd ).pxNext ) /* 获取链表的第一个节点 */ #define listGET_NEXT( pxListItem ) ( ( pxListItem )->pxNext ) /* 获取链表的最后一个节点 */ #define listGET_END_MARKER( pxList ) ( ( ListItem_t const * ) ( &( ( pxList )->xListEnd ) ) ) /* 判断链表是否为空 */ #define listLIST_IS_EMPTY( pxList ) ( ( BaseType_t ) ( ( pxList )->uxNumberOfItems == ( UBaseType_t ) 0 ) ) /* 获取链表的节点数 */ #define listCURRENT_LIST_LENGTH( pxList ) ( ( pxList )->uxNumberOfItems ) /* 获取链表节点的OWNER,即TCB */ #define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) \ { \ List_t * const pxConstList = ( pxList ); \ /* 节点索引指向链表第一个节点调整节点索引指针,指向下一个节点, 如果当前链表有N个节点,当第N次调用该函数时,pxInedex则指向第N个节点 */\ ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \ /* 当前链表为空 */ \ if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \ { \ ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \ } \ /* 获取节点的OWNER,即TCB */ \ ( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; \ } #define listGET_OWNER_OF_HEAD_ENTRY( pxList ) ( (&( ( pxList )->xListEnd ))->pxNext->pvOwner ) #define listREMOVE_ITEM( pxItemToRemove ) \ { \ /* The list item knows which list it is in. Obtain the list from the list \ * item. */ \ List_t * const pxList = ( pxItemToRemove )->pxContainer; \ \ ( pxItemToRemove )->pxNext->pxPrevious = ( pxItemToRemove )->pxPrevious; \ ( pxItemToRemove )->pxPrevious->pxNext = ( pxItemToRemove )->pxNext; \ /* Make sure the index is left pointing to a valid item. */ \ if( pxList->pxIndex == ( pxItemToRemove ) ) \ { \ pxList->pxIndex = ( pxItemToRemove )->pxPrevious; \ } \ \ ( pxItemToRemove )->pxContainer = NULL; \ ( pxList->uxNumberOfItems )--; \ }
链表初始化:vListInitialise
List_t中有一个Item: xListEnd,初始化链表后,结果如下:
/* 链表根节点初始化 */ void vListInitialise( List_t * const pxList ) { /* 将链表索引指针指向最后一个节点 */ pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd ); /* 将链表最后一个节点的辅助排序的值设置为最大,确保该节点就是链表的最后节点 */ pxList->xListEnd.xItemValue = portMAX_DELAY; /* 将最后一个节点的pxNext和pxPrevious指针均指向节点自身,表示链表为空 */ pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd ); pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd ); /* 初始化链表节点计数器的值为0,表示链表为空 */ pxList->uxNumberOfItems = ( UBaseType_t ) 0U; }
节点初始化:vListInitialiseItem
/* 节点初始化 */ void vListInitialiseItem( ListItem_t * const pxItem ) { /* 初始化该节点所在的链表为空,表示节点还没有插入任何链表 */ pxItem->pvContainer = NULL; }
插入链表尾部:vListInsertEnd
- 在当前遍历位置的前一个位置插入保证公平,不然1先排队却让4先执行,造成饥饿
- 记录所在链表,更新链表计数器
/* 将节点插入到链表的尾部 */ void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem ) { ListItem_t * const pxIndex = pxList->pxIndex; pxNewListItem->pxNext = pxIndex; pxNewListItem->pxPrevious = pxIndex->pxPrevious; pxIndex->pxPrevious->pxNext = pxNewListItem; pxIndex->pxPrevious = pxNewListItem; /* 记住该节点所在的链表 */ pxNewListItem->pvContainer = ( void * ) pxList; /* 链表节点计数器++ */ ( pxList->uxNumberOfItems )++; }
按升序排列插入链表:vListInsert
- 获取排序值
- 找到插入位置
- 插入操作
- 记录所在链表,更新链表计数器
/* 将节点按照升序排列插入到链表 */ void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem ) { ListItem_t *pxIterator; /* 获取节点的排序辅助值 */ const TickType_t xValueOfInsertion = pxNewListItem->xItemValue; /* 寻找节点要插入的位置 ,指针停在插入位置的前一节点 */ if( xValueOfInsertion == portMAX_DELAY ) { pxIterator = pxList->xListEnd.pxPrevious; } else { for( pxIterator = ( ListItem_t * ) &( pxList->xListEnd ); pxIterator->pxNext->xItemValue <= xValueOfInsertion; pxIterator = pxIterator->pxNext ) { /* 没有事情可做,不断迭代只为了找到节点要插入的位置 */ } } pxNewListItem->pxNext = pxIterator->pxNext; pxNewListItem->pxNext->pxPrevious = pxNewListItem; pxNewListItem->pxPrevious = pxIterator; pxIterator->pxNext = pxNewListItem; /* 记住该节点所在的链表 */ pxNewListItem->pvContainer = ( void * ) pxList; /* 链表节点计数器++ */ ( pxList->uxNumberOfItems )++; }
删除节点:uxListRemove
- 由ListItem_t找到对应的链表(为了防止删除的是当前链表正在访问的结点)
- 完成删除操作
- 对特殊情况进行处理,将pxIndex前移一位 ?
- 复位节点所属链表,更新计数器,返回剩余个数
/* 将节点从链表中删除 */ UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove ) { /* 获取节点所在的链表 */ List_t * const pxList = ( List_t * ) pxItemToRemove->pvContainer; pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious; pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext; /* Make sure the index is left pointing to a valid item. */ if( pxList->pxIndex == pxItemToRemove ) { pxList->pxIndex = pxItemToRemove->pxPrevious; } /* 初始化该节点所在的链表为空,表示节点还没有插入任何链表 */ pxItemToRemove->pvContainer = NULL; /* 链表节点计数器-- */ ( pxList->uxNumberOfItems )--; /* 返回链表中剩余节点的个数 */ return pxList->uxNumberOfItems; }
任务定义: task.c
任务=函数+栈 == 运行中的函数
定义任务栈
- 在多任务系统中,每个任务都是独立的,互不干扰的,所以要为每个任务都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一段内存空间,都存在于 RAM 中。
- 任务栈其实就是一个预先定义好的全局数据,数据类型为 StackType_t,大小由 TASK1_STACK_SIZE 这个宏来定义,默认为 128,单位为字,即 512 字节,这也是 FreeRTOS 推荐的最小的任务栈。
#define TASK1_STACK_SIZE 128 StackType_t Task1Stack[TASK1_STACK_SIZE]; #define TASK2_STACK_SIZE 128 StackType_t Task2Stack[TASK2_STACK_SIZE];
定义任务函数
任务是一个独立的函数,函数主体无限循环且不能返回。
定义任务控制块: tskTaskControlBlock-1
在裸机系统中,程序的主体是 CPU 按照顺序执行的。而在多任务系统中,任务的执行是由系统调度的。系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块,这个任务控制块就相当于任务的身份证,里面存有任务的所有信息,比如任务的栈指针,任务名称,任务的形参等。有了这个任务控制块之后,以后系统对任务的全部操作都可以通过这个任务控制块来实现。
typedef struct tskTaskControlBlock { volatile StackType_t *pxTopOfStack; /* 栈顶 */ ListItem_t xStateListItem; /* 任务节点 */ StackType_t *pxStack; /* 任务栈起始地址 */ /* 任务名称,字符串形式 */ char pcTaskName[ configMAX_TASK_NAME_LEN ]; TickType_t xTicksToDelay; /* 用于延时 */ } tskTCB; typedef tskTCB TCB_t;
任务创建函数
可参考快速入门中的使用部分了解各个参数作用
任务的栈,任务的函数实体,任务的控制块最终需要联系起来才能由系统进行统一调度。
xTaskCreateStatic()函数-1
用指定的地址和参数,创建并初始化TCB,返回操作句柄
关键步骤
- 创建TCB,指定TCB地址,初始化任务栈起始地址pxStack
- 其余初始化交给下一个函数
- 返回贯穿函数的句柄
注意事项
#if( configSUPPORT_STATIC_ALLOCATION == 1 ) //FreeRTOSConfig.h 中定义,支持静态分配配置为1 /* 任务入口,即任务的函数名称。 TaskFunction_t 是在 projdefs.h 中重定义的一个数据类型,实际就是空指针。*/ TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode, const char * const pcName, /* 任务名称,字符串形式 */ const uint32_t ulStackDepth, /* 任务栈大小,单位为字 */ void * const pvParameters, /* 任务形参 */ StackType_t * const puxStackBuffer, /* 任务栈起始地址,外部创建的栈 */ TCB_t * const pxTaskBuffer ) /* 任务控制块指针 */ { TCB_t *pxNewTCB; //任务控制块 //任务句柄,指向任务的TCB,方便外部通过句柄操作任务。 //TaskHandle_t在 task.h 中定义,实际上就是一个空指针 TaskHandle_t xReturn; //用参数初始化TCB的pxStack,剩下的名字和任务结点在后边函数中赋值 if( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) ) { //pxTaskBuffer传进来的,现在目的是在内部在指定地址给他放好TCB,然后返回句柄 pxNewTCB = ( TCB_t * ) pxTaskBuffer; pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer; /* 创建新的任务 */ prvInitialiseNewTask( pxTaskCode, /* 任务入口 */ pcName, /* 任务名称,字符串形式 */ ulStackDepth, /* 任务栈大小,单位为字 */ pvParameters, /* 任务形参 */ &xReturn, /* 任务句柄 */ pxNewTCB); /* 任务栈起始地址 */ } else { xReturn = NULL; } /* 返回任务句柄,如果任务创建成功,此时xReturn应该指向任务控制块 */ return xReturn; } #endif /* configSUPPORT_STATIC_ALLOCATION */
prvInitialiseNewTask()函数-1
创建新任务:完成TCB中任务名称pcTaskName,和任务节点xStateListItem的初始化
关键步骤
- 栈顶指针处理:初始化+对齐
- TCB中存储任务名
- TCB中任务节点的初始化和pvOwner的指向,调用链表相关的函数和宏
- TCB中栈顶pxTopOfStack的初始化,调用函数初始化任务栈
- 句柄指向PCB并返回给上层函数
注意事项
- 向下做8字节对齐:总线宽度是 32 位的,通常只要栈保持 4 字节对齐就行,可这样为啥要 8 字节?难道有哪些操作是 64 位的?确实有,那就是浮点运算,所以要 8 字节对齐(但是目前我们都还没有涉及到浮点运算,只是为了后续兼容浮点运行的考虑),向下和之前的向上不同。
static void prvInitialiseNewTask( TaskFunction_t pxTaskCode, /* 任务入口 */ const char * const pcName, /* 任务名称,字符串形式 */ const uint32_t ulStackDepth, /* 任务栈大小,单位为字 */ void * const pvParameters, /* 任务形参 */ TaskHandle_t * const pxCreatedTask, /* 任务句柄 */ TCB_t *pxNewTCB ) /* 任务控制块指针 */ { StackType_t *pxTopOfStack; UBaseType_t x; //unsign long 4字节 /* 获取栈顶地址 栈顶地址=栈底地址+元素总数-1 */ pxTopOfStack = pxNewTCB-> + ( ulStackDepth - ( uint32_t ) 1 ); //pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) ); /* 向下做8字节对齐 */ pxTopOfStack = ( StackType_t * ) ( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) ); /* 将任务的名字存储在TCB中,最长16个UBase */ for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ ) { pxNewTCB->pcTaskName[ x ] = pcName[ x ]; if( pcName[ x ] == 0x00 )// \0停止 { break; } } /* 任务名字的长度不能超过configMAX_TASK_NAME_LEN */ pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0'; /* 初始化TCB中的xStateListItem节点,该节点所属链表置空,表示还未插入任何链表 */ vListInitialiseItem( &( pxNewTCB->xStateListItem ) ); /* 设置xStateListItem节点的拥有者,即本身的TCB ,链表节点的pvOwner设置为pxNewTCB*/ listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB ); /* 初始化任务栈 */ pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters ); /* 让任务句柄指向任务控制块 */ if( ( void * ) pxCreatedTask != NULL ) { *pxCreatedTask = ( TaskHandle_t ) pxNewTCB; } }
pxPortInitialiseStack()函数
TCB中栈顶pxTopOfStack的初始化,更新栈顶指针,任务第一次运行的环境参数就存在任务栈中。
关键步骤
- 异常发生时,需要自动加载到CPU寄存器的内容放入栈
- 异常发生时,需要手动加载到CPU寄存器的内容放入栈
- 返回栈顶指针,此时pxTopOfStack指向空闲栈,上层函数会赋给TCB
注意事项
- 通常任务不会返回,所以返回的话就跳转无限循环函数
- 将函数入口地址放入PC计数器,这样就能调用了,形参也是在这里放进去的,所以默认在R0
#define portINITIAL_XPSR ( 0x01000000 ) #define portSTART_ADDRESS_MASK ( ( StackType_t ) 0xfffffffeUL ) static void prvTaskExitError( void ) { /* 函数停止在这里 */ for (;;); } StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters ) { /* 异常发生时,自动加载到CPU寄存器的内容 */ pxTopOfStack--; *pxTopOfStack = portINITIAL_XPSR; /* xPSR的bit24必须置1,表示Thumb状态 */ pxTopOfStack--; *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK;/* PC,即任务入口函数 */ pxTopOfStack--; *pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR,函数返回地址 */ pxTopOfStack -= 5; /* R12, R3, R2 and R1 默认初始化为0 */ *pxTopOfStack = ( StackType_t ) pvParameters; /* R0,任务形参 */ /* 异常发生时,手动加载到CPU寄存器的内容 */ pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4默认初始化为0 */ /* 返回栈顶指针,此时pxTopOfStack指向空闲栈 */ return pxTopOfStack; }
就序列表
整体上是调用链表的相关函数对元素进行初始化和插入删除的操作
就绪列表定义
- 任务创建好之后,我们需要把任务添加到就绪列表里面,表示任务已经就绪,系统随时可以调度。
- 就绪列表实际上就是一个 List_t 类型的数组,即每个元素就是个链表,下标对应了任务的优先级。
- 数组的大小由决定最大任务优先级的宏configMAX_PRIORITIES 决 定 , configMAX_PRIORITIES 在 FreeRTOSConfig.h 中默认定义为 5,最大支持 256 个优先级。
List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
列表初始化:prvInitialiseTaskLists()-1
对列表中每个链表调用链表初始化函数vListInitialise即可(对list和listEnd结构体元素初始化)
void prvInitialiseTaskLists( void ) { UBaseType_t uxPriority; for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ ) { vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) ); } }
任务插入到就绪列表
调用之前链表的函数vListInsertEnd,对TCB中初始化后的任务节点xStateListItem的节点结构体成员进行配置,之前初始化为空(pvOwner在任务创建时已经指定,ItemValue一直未指定,没有选择按顺序插),后边引入专门的函数处理,并把相关操作放到任务创建里,不需要外部调用
/* 初始化与任务相关的列表,如就绪列表 */ prvInitialiseTaskLists(); /* 创建任务 */ Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任务入口 */ (char *)"Task1", /* 任务名称,字符串形式 */ (uint32_t)TASK1_STACK_SIZE , /* 任务栈大小,单位为字 */ (void *) NULL, /* 任务形参 */ (StackType_t *)Task1Stack, /* 任务栈起始地址 */ (TCB_t *)&Task1TCB ); /* 任务控制块 */ /* 将任务添加到就绪列表 */ vListInsertEnd( &( pxReadyTasksLists[1] ), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) ); Task2_Handle = xTaskCreateStatic( (TaskFunction_t)Task2_Entry, /* 任务入口 */ (char *)"Task2", /* 任务名称,字符串形式 */ (uint32_t)TASK2_STACK_SIZE , /* 任务栈大小,单位为字 */ (void *) NULL, /* 任务形参 */ (StackType_t *)Task2Stack, /* 任务栈起始地址 */ (TCB_t *)&Task2TCB ); /* 任务控制块 */ /* 将任务添加到就绪列表 */ vListInsertEnd( &( pxReadyTasksLists[2] ), &( ((TCB_t *)(&Task2TCB))->xStateListItem ) ); /* 启动调度器,开始多任务调度,启动成功则不返回 */ vTaskStartScheduler();
调度器
汇编
启动调度器
vTaskStartScheduler()函数-1
关键步骤
- 指针指向第一个任务的TCB
- 启动调度器
注意事项
- pxCurrentTCB 是一个在 task.c 定义的全局指针,用于指向当前正在运行或者即将要运行的任务的任务控制块。目前我们还不支持优先级,则手动指定第一 个要运行的任务。
- 调用函数 xPortStartScheduler()启动调度器,调度器启动成功,则不会返回。该函数在 port.c 中实现
void vTaskStartScheduler( void ) { /* 手动指定第一个运行的任务 */ pxCurrentTCB = &Task1TCB; /* 启动调度器 */ if( xPortStartScheduler() != pdFALSE ) { /* 调度器启动成功,则不会返回,即不会来到这里 */ } }
xPortStartScheduler()函数-1
关键步骤
- 配置PendSV 和 SysTick中断优先级
- 启动第一个任务(调用函数)
注意事项
- SysTick 和 PendSV 都会涉及到系统调度,系统调度的优先级要低于系统的其它硬件中断优先级,即优先响应系统中的外部硬件中断,所以 SysTick 和 PendSV 的中断优先级配置为最低。
/* * 在Cortex-M中,内核外设SCB中SHPR3寄存器用于设置SysTick和PendSV的异常优先级 * System handler priority register 3 (SCB_SHPR3) SCB_SHPR3:0xE000 ED20 * Bits 31:24 PRI_15[7:0]: Priority of system handler 15, SysTick exception * Bits 23:16 PRI_14[7:0]: Priority of system handler 14, PendSV */ #define portNVIC_SYSPRI2_REG ( * ( ( volatile uint32_t * ) 0xe000ed20 ) ) #define portNVIC_PENDSV_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 16UL ) #define portNVIC_SYSTICK_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 24UL ) BaseType_t xPortStartScheduler( void ) { /* 配置PendSV 和 SysTick 的中断优先级为最低 */ portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; /* 启动第一个任务,不再返回 */ prvStartFirstTask(); /* 不应该运行到这里 */ return 0; }
prvStartFirstTask()函数
CPU可以使用MSP来管理栈的操作,所以调用任务前需要设置MSP,
关键步骤
- 更新 MSP 的值:SCB_VTOR寄存器中存的,寄存器地址为0xE000ED008
- 产生 SVC 系统调用,然后去到 SVC 的中断服务函数里面真正切换到第一个任务。
注意事项
- 向量表:是一个存储中断服务例程(ISR)地址的表,它位于程序存储器的末尾。当发生中断时,CPU会根据中断类型从向量表中读取相应的ISR地址,并跳转到该地址执行ISR。
- MSP:是主栈指针(Main Stack Pointer)的缩写,它是一个寄存器,用于指向当前任务的栈顶。当CPU启动或复位时,它会从向量表的第一个位置读取MSP的初始值,并将其加载到MSP寄存器中。这样,CPU就可以使用MSP来管理栈的操作,比如压入和弹出寄存器或局部变量等。
- SCB_VTOR寄存器:是系统控制块(System Control Block)的一个寄存器,用于指定向量表的基地址。SCB_VTOR寄存器的作用是可以重新映射向量表,以便CPU能够从不同的地址开始查找ISR地址。这在使用引导程序(bootloader)来更新主应用程序时很有用,因为引导程序和主应用程序可能有不同的向量表。通过修改SCB_VTOR寄存器的值,可以让CPU从主应用程序的向量表中读取ISR地址,而不是从0x00000000开始。
/* * 在Cortex-M中,内核外设SCB的地址范围为:0xE000ED00-0xE000ED3F * 0xE000ED008为SCB外设中SCB_VTOR这个寄存器的地址,里面存放的是向量表的起始地址,即MSP的地址 */ __asm void prvStartFirstTask( void ) { //当前栈需按照 8 字节对齐,如果都是 32 位的操作则 4 个字节对齐即可。 //在 Cortex-M 中浮点运算是 8 字节的。 PRESERVE8 /* 在Cortex-M中,0xE000ED08是SCB_VTOR这个寄存器的地址, 里面存放的是向量表的起始地址,即MSP的地址 */ ldr r0, =0xE000ED08 //将 0xE000ED08 这个立即数加载到寄存器 R0 ldr r0, [r0] //将 0xE000ED08 这个地址指向的内容加载到寄存器 R0,此时 R0 ldr r0, [r0] //将 0x00000000 这个地址指向的内容加载到 R0,此时 R0 等于0x200008DB /* 设置主堆栈指针msp的值 */ msr msp, r0 //将 R0 的值存储到 MSP,此时 MSP 等于 0x200008DB,这是主堆栈的栈顶指针 /* 使能全局中断:使用 CPS 指令把全局中断打开。 为了快速地开关中断,Cortex-M 内核 专门设置了一条 CPS 指令,有 4 种用法 */ cpsie i cpsie f dsb isb /* 调用SVC去启动第一个任务: 产生系统调用,服务号 0表示 SVC 中断,接下来将会执行 SVC 中 断服务函数*/ svc 0 nop nop }
vPortSVCHandler()函数
SVC 中断要想被成功响应,其函数名必须与向量表注册的名称一致,在启动文件的向量表中,SVC 的中断服务函数注册的名称是 SVC_Handler,所以 SVC 中断服务函数的名称我们应该写成 SVC_Handler,但是在 FreeRTOS 中,官方版本写的是 vPortSVCHandler(),为
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
作者简介:2个月时间逆袭嵌入式开发,拿下理想汽车-ssp、小米汽车-sp、oppo-sp、迈瑞医疗、三星电子等八家制造业大厂offer~ 专栏内容:涵盖算法、八股、项目、简历等前期准备的详细笔记和模板、面试前中后的各种注意事项以及后期谈薪、选offer等技巧。保姆级全阶段教程帮你获得信息差,早日收到理想offer~