基础
- 异步使用变量(外设操作变量/多线程中操作变量/中断中操作变量)
- 一定要加volatile
- 为什么有些函数不能在ISR中执行,必须执行对应的ISR安全版本?
- 因为ISR不能进入阻塞态/挂起态、长时间运行,和IDLE线程类似
- 因此ISR中一旦需要阻塞/挂起,就会通过线程通信发送一个信号,让其他线程来执行阻塞/挂起的后续操作,然后结束中断,从而避免ISR中阻塞等
-
注意:ISR遇到阻塞/挂起/长时间运行,只会发出信号,然后需要写一个辅助线程来接收信号并处理
-
实现同步互斥的方式
- 软件互斥:LockOne、LockTwo、Peterson
- 临界区保护:中断关闭-开启、调度器挂起-恢复
- 硬件互斥锁/软件互斥量
- 实现线程间通信
- 全局变量:需要手动实现互斥==一读一写==
- 单buffer/双buffer/环形buffer:需要手动实现互斥==一读一写==
- 队列/队列集:自带互斥的环形buffer==多读多写==
- 事件标志组==多读多写==
- 任务通知==多读多写==
软件互斥
- 略
临界区保护
- 中断开启-关闭==全局保护==
- 原理:见中断管理
- 注意:只在rtos可管理的中断优先级范围内有效
- 注意:如果在关中断期间使用延时函数会出问题
- vTaskDelay()内部会开关中断,会导致进入两次中断,最终在第二次进中断时卡住
- mdelay()需要调高时钟源的中断优先级,防止关中断时无法延时,而由于systick默认优先级最低,所以最好自定义一个TIM
- 调度器挂起-恢复==全局保护==
- 原理:见任务调度
- 注意:关调度器只是关闭任务切换,但是不会关闭中断
- 软件互斥量/硬件互斥锁==局部保护==
- 注意:只会保护临界区内的资源不被访问,仍然存在任务切换
队列Queue
-
传送数据的方式
-
拷贝(传值):传输小数据
- 引用(传地址):传输大数据
-
相关函数
-
创建:xQueueCreate()/xQueueCreateStatic()
- 删除:vQueueDelete()
- 复位:xQueueReset()
- 读队列:读、窥探
- 写队列:写后、写前、覆盖
- 查询:uxQueueMessagesWaiting()/uxQueueSpacesAvailable()
- 队列的实现(生产者消费者)
- 有一个环形buffer
- 对生产者有一个等待链表(SenderList),对消费者有一个等待链表(RecvList)
- 由于等待的线程会被vTaskDelay,所以同时也会加入线程调度的阻塞链表(BlockList)
- 队列的逻辑(生产者消费者)
- 消费者读队列,队列空时阻塞(T=timeout)
- 阻塞中有生产者写入,由生产者唤醒消费者(在RecvList中找一个线程,然后将它移出等待链表、阻塞链表,加入就绪链表)
- 阻塞中无生产者写入,由时钟中断唤醒消费者(同上)
- 生产者:同理
信号量Semaphore
- 相关函数
- 创建:xSemaphoreCreateBinary()/vSemaphoreCreateBinary()/xSemaphoreCreateMutex()等
- v函数会在创建时先释放(初始sema=初值),而x函数创建时不释放(初始sema=0)
- v函数已经被弃用,现在最好用x函数
- 获取、释放
- 查询计数变量值、查询当前任务是否拥有信号量
- 分类:二值信号量Binary(同步量)、二值信号量Mutex(软件互斥量)、二值信号量RecursiveMutex(软件递归互斥量)、整型信号量Counting(同步量)
- Binary和Mutex的区别:Mutex有优先级继承机制。防止优先级反转问题
- Mutex和RecursiveMutex的区别:递归函数可能会多次获取同一个mutex而导致错误,此时应该更换为RecursiveMutex,使得同一个任务可以多次获取相同的mutex。防止递归函数的自我阻塞
- 在中断中使用信号量
- 注意:ISR只能释放信号量,不能获取,因为ISR不能被阻塞
-
所以ISR中无法实现Mutex的优先级继承机制
-
信号量的实现
- 由队列实现,信号量=int型计数变量+等待队列
- 信号量的等待队列!=队列中的等待链表
- 等待队列:队列元素数=计数变量值,每个元素大小=0B
- 信号量的逻辑
- 被信号量阻塞:调度器会先将任务加入等待队列,然后将任务加入阻塞链表
- 被信号量唤醒:调度器会先将任务移出等待队列,然后将任务移出阻塞链表,加入就绪链表
队列集QueueSet
- 用一套API管理多个队列、信号量
- 队列集中的资源不分先后:防止因为前面队列/信号量阻塞了任务,而导致任务无法操作后续的队列/信号量。
- 相关函数
- 创建:队列集大小=所有队列和信号量的大小之和
- 添加到集合:vSemaphoreCreateBinary()创建的信号量不能被添加到队列集
- 移除出集合
- 查询有效队列
事件标志组EventGroup
- 一个Group有多个标志(1个标志=1bit)
- 事件标志组的逻辑
- 接收者阻塞等待1个或多个标志位
- 发送者对标志位置位,唤醒接收者
任务通知TaskNotify
- 任务通知的实现
- 一个TCB有一个通知数组,用于接收其他任务发来的通知(老版本中不是数组,只是一个元素)
- 数组中的每一个元素都是uint32_t,表示对应通知的计数值
- 任务通知的逻辑
- 接收者阻塞等待通知
-
发送者向接收者发送通知,唤醒接收者
-
中断中的任务通知
- 注意:ISR中只能发送通知,不能接收通知,原因同理
- 使用任务通知的场景
- 可以用于代替缓冲区以外的通信方式,且一般速度更快
缓冲区
- 略
线程通信方式对比
- ISR通信
- 队列:可发可收
- 信号量:同步量可发可收,互斥量可发不可收
- 标志事件组:可发可收
- 任务通知:可发不可收
RTOS软件定时器
- 使用systick作为软件定时器,其精度会受到任务调度的影响
- 调度器启动时会创建一个时钟任务,然后在任务中调用这个软件定时器
- 所以调度器启动之前需要:创建软件定时器,在FreeRTOSConfig.h配置时钟任务的优先级、栈大小
RTOS低功耗模式Tickless
- 一般在空闲Hook中进入Tickless模式,因为此时CPU空闲
- Tickless = STM32低功耗模式的睡眠模式+RTOS扩展功能
- RTOS拓展功能:将systick的中断周期修改为低功耗运行时间,退出低功耗后,systick节拍数=低功耗之前的节拍数+低功耗运行时间
- RTOS将调用portSUPPRESS_TICKS_AND_SLEEP(),然后最终调用__wfi()来进入低功耗
- 开启低功耗:configUSE_TICKLESS_IDLE
- 配置configEXPECTED_IDLE_TIME_BEFORE_SLEEP
- 设置进入低功耗模式的默认时长
- 配置configPRE_SLEEP_PROCESSING
- 实现进入低功耗时,同时将外设修改为低功耗模式(如关闭外设时钟)
- 配置configPOST_SLEEP_PROCESSING
- 实现退出低功耗时,将外设修改为正常模式
RTOS内存管理
- 相关函数:
- 申请释放:pvPortMalloc()、pvPortCalloc()、pvPortRealloc()、pvPortFree()
- 查询:xPortGetFreeHeapSize()/xPortGetMinimumEverFreeHeapSize()
- 在heap4/5申请内存时,会产生额外申请大小,它由两部分组成:
- 使用该内存块需要创建对应结构体,其大小为xHeapStructSize
- 内存的分配必须按字节对齐,若申请一块不对齐区域,会产生对齐偏移