基础

  • 异步使用变量(外设操作变量/多线程中操作变量/中断中操作变量)
  • 一定要加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
  • 内存的分配必须按字节对齐,若申请一块不对齐区域,会产生对齐偏移