视频名:视频专辑 - FreeRTOS实时操作系统(十年功力,力求全面,通俗易懂)
https://www.bilibili.com/video/BV18R4y147DB/?spm_id_from=333.999.0.0&vd_source=6815e3297898473155e44cdd9339ebe0
非常推荐这套FreeRTOS教程。对新手友好,收获很多。但有部分视频不公开(少数),要收费
这个笔记根据公开的免费视频整理,部分内容还需补充,但不影响知识的获取
视频中未公开章节,章节名后面用***号标注
UP主对源码进行了解析,有助于培养初学者的良好习惯
内含一些小作业
好了,那就开始吧
(¬‿¬)
入门FreeRTOS笔记整理
绪论
第一部分:介绍操作系统、移植操作系统等
第二部分:比较重要,特别是任务管理
第三部分:任务跟任务之间、任务跟中断之间通讯,数据传输问题。操作系统中链表用的非常多,一定要了解。通讯方面的
第四部分:
第五部分:把一些外设结合起来,进行一些系统的应用
RTOS简介
嵌入式操作系统
分时操作系统:按时间片完成,任务之间没有优先级
例如电脑是分时操作系统,FreeRTOS是实时操作系统(弱实时操作系统)
FreeRTOS实时操作系统简介
好处
裸机系统与多任务系统
裸机系统事件在主函数中完成。多任务系统事件在任务中完成,同时,任务有优先级
操作系统编程核心在任务,裸机系统编程核心在主函数
FreeRTOS移植
手动移植FreeRTOS***
STM32cubeMX生成FreeRTOS***
FreeRTOS启动流程
FreeRTOS编码风格
32位和64位设置
FreeRTOS调试方法
理论
查看栈的使用情况,避免栈溢出,栈多了就减一点
运行状态,空闲状态
定时器2,HAL_Delay()时间基准。现在滴答定时器作为系统定时器调用了,所以需要单独的定时器作为延时
定时器3配置为20kHz,作为系统的计数
任务的配置:
划线部分(从上到下):任务名称长度(最大32)
配置操作系统内存(默认3k)这里是10k
启用任务状态的功能宏定义
在回调函数里面定义一个变量,让它加1
第一个是初始化函数,系统启用任务调度时,会调用这个函数,把这个数据清0
第二个函数,每隔一段时间获取这个值,这个值在定时器里面计数的。每个任务单独获取,任务启动获取一次,任务结束获取一次,知道任务占用时间,除以总数得到利用率
创建一个任务,定义一个数组保存信息,调用函数就会把信息放到数组里面,只需要打印就行了
现场编程***
系统配置
说明
FreeRTOS.h:如果没有定义,就定义
在FreeRTOSConfig.h中定义
Config宏-内核配置***
Config宏-其他配置***
INCLUDE宏配置***
通过预编译移除未使用的内核组件
任务管理
任务概念与任务状态
理论上对任务数量没有限制。耦合性小。任何时刻,只有一个任务得到运行,因为单片机是单CPU。栈空间:一段连续的内存。任务不会退出,只不过是由调度器调度而已。释放系统资源(栈空间)
因为任务是由调度器调用的,所以有任务状态。就绪态任务在等待CPU(等待调度),因为FreeRTOS是抢占式的,任务是有优先级的。挂起需要手动挂起、手动恢复,阻塞是等待一些标志位
系统启动与空闲任务
函数没有形参,很容易看。下面是标准的启动函数(task.c里面),上面是HAL库的封装
FreeRTOSConfig.h中定义内存空间
任务创建
静态任务、动态任务创建。栈空间,任务控制块(结构体、任务的参数都在里面)
任务函数(任务体)名和任务名(只是个字符串)可以一样,也可以不一样
栈空间太大就浪费,太小就出错,在32里面是4字节。128就是512字节
0是优先级最小的,一般给空闲任务
可以通过任务句柄,操作任务。非常重要。指针的指针,所以需要进行取址
方便创建不同的任务
详解任务句柄***
指向任务控制块的一个结构体指针,可以操作任务
任务删除、挂起、恢复
可裁剪
动态内存:比如一个任务,任务里面又重新申请了内存 (通过内存申请函数),需要手动删除,任务释放只是释放了任务的栈空间
删除任务前,要判断任务存不存在,不存在就不能删除,否则会出错
删除后,设置任务句柄为空,下一次不会再删除
可以多次调用,任务肯定存在。跟删除不一样(还需要判断任务是否存在)
中断与任务的同步:比如在任务里面挂起,在中断里面恢复。按道理会立马执行这个任务(同步了),但在恢复中,如果别的任务优先级更高,就会去执行别的任务(不会执行这个任务),中断与任务之间就不会同步了
比如:外部中断中使用
任务管理编程***
任务调度
调度器
调度算法应用不需要了解。合作式调度很少使用
调度方式
单任务,不是多任务,不能进行任务的抢占
不需要给每个任务分配单独的栈空间,RAM占用空间小。算法简单,ROM占用空间小
这里的多任务是每次只有一个程序在运行,但在反复切换(速度非常快,感觉在同时进行)
CPU永远都在执行高优先级任务
系统默认开启,不要关闭
同优先级任务:如果没有启用时间片调度,那么所有任务的优先级就不能使用同样的优先级了
任务运行时,按时间片来的
时间片可以在配置文件里设置Tick…()(Tick:1000-1ms,100-10ms,20-50ms),一个任务一个时间片
FreeRTOS只能设置的一个Tick,不能设置例如4个Tic。其他操作系统可能可以
抢占式调度编程测试***
时间片调度编程测试
时间片设为20,50ms,任务是10ms,可以执行5次
写错了。不能阻塞10ms,应该正常延时10ms
可以在配置文件中,用宏定义禁止时间片调度
任务栈大小确认***
任务栈溢出检测
栈溢出检测机制在出产品时,可以关掉
任务栈溢出编程测试***
任务与中断优先级、临界保护
中断优先级
NVIC知识回顾
抢占优先级:
响应优先级:
调度器通过SVC中断启动第一个任务
系统服务中断配置为0,优先级最高
PendSV任务挂起中断、Systick滴答定时器中断优先级最低。不会打断任务中断。PendSV用于做任务切换,Systick中断是一个时基中断。中断优先级太高,影响系统实时性
源码里面有临界段代码(非常重要的代码):并不希望任何中断把它停下来。在执行这段代码前,会关全局中断。结束后,再开启全局中断。这会带来副作用,任务非常重要,任务中断被关闭,系统实时性遭到破坏,比如飞机、有害气体检测
basepri一般配置为5
不受系统临界处理,管理的中断
任务优先级
中断优先级值越小,优先级越高。任务优先级值越小,优先级越低
时间片调度要启用
永远执行优先级最高的任务,因为是抢占系统
必须是阻塞式的原因:如果不阻塞,低优先级得不到执行
IRQ任务,比如:按键中断、串口中断(比如通过MODBUS解析协议)
低优先级的时间片调度任务,这类任务一般设置成一样的(1、 2),让它们时间片运行
开关中断与临界段函数
不允许中断打断的方法:关操作系统可以管理的中断
全局变量被很多任务使用,需要做临界段处理的原因:一个变量一次性只能被一个任务调用,如果被两个任务同时调用就会出问题,这个时候就要保护了
临界区关闭了中断,同时会计数
退出临界区,变量会减1,变量为0,才会开启中断
临界段处理函数必须要成对使用,每进入一次,计数值都会加一。进来几次,退几次
有形参(会保存原来的值,然后恢复原来的值),跟开关中断函数不一样(默认是0)
编程测试临界段处理函数***
调度锁、中断锁、任务锁
调度锁编程测试
不是开挂函数
应用代码:比如对时间要求很高的代码,通过一定的时序采集数据,避免被其他任务打断
使用定时器延时,而不是系统延时
时间管理(系统节拍、延时函数)
延时,比如说,高优先级任务需要延时,延时之后释放CPU
超时,等待一个时间,不能一直等待
任务切换也需要时钟节拍。通常有一定的时间,去检查任务需不需要切换
系统的额外开销:占用CPU的时间
SysTick裸机编程中,当成定时器使用。操作系统中,用作系统节拍
100KHz-10ms,10KHz-100ms,一般是1-100ms之间。如果配置太小,CPU一直在响应中断,开销特别大。配置太大,没办法精确延时。一般用1KHz
释放CPU使用权,也可以采用等待一个事件。时间延迟用的比较多一点
第一个是相对延时,第二个是绝对延时,第三个是获取当前计数值(任务里面),第四个是获取当前计数值(中断里面)
延时多少个Ticks(时钟节拍),相对调用这一刻的延时
第一个时参。采集任务开始运行的时间赋给它
50s后150s调用这个函数,时间精确,能够实现周期性延时
相对延时和绝对延时的区别与编程测试***
链表
操作系统,任务的数据就是非连续的,可以给所有任务新建一个链表,然后进行排序,就可以进行系统调度了。添加任务是添加任务节点,删除任务是删除任务节点
双向链表更加灵活
单向、双向链表
首尾相连,首就是尾,尾就是首。根节点:不存储数据,链表的生产者,比如记录整个节点的数量。通过一个节点,只能找到下一个节点,不能找到上一个节点。所占内存小一点
离散的数据:比如操作系统中任务、软件定时器、消息队列、信号量
FreeRTOS中链表实现***
链表与节点初始化函数
后面两行用于验证
里面的指针,因为没有节点,指向自己
链表操作函数
排列,比如任务优先级查找最高优先级,非常方便
第一个参数是链表,第二个参数是节点。将节点按升序插入链表
只需要一个形参,需要删除的节点。节点里面已经包含链表,所以不需要链表
链表编程仿真与测试***
消息队列
不固定长度的消息:比如说串口接收、spi接口传输
具有超时机制,比如说用FreeRTOS读数据,如果没有数据,会进行等待(把任务挂起来),一旦有数据,任务就立马去获得这个数据。如果超时,就退出来
有效管理任务,比如任务1发数据,如果消息队列满了,发不了。就会进行任务1挂起操作,等空了,再发送数据。任务2读数据,如果消息队列里面没有数据,任务2挂起,一旦有数据,就会立马获得这个数据
防止多任务访问冲突,比如说任务在读队列(锁定队列),其他任务也要读这个队列(这个任务就读不了,只能等待)。如果使用数组,有可能两个任务同时读写数组,有可能出错
FIFO:默认是FIFO,先进先出
LIFO:后进先出
通过标志位可以判断,读到Pass,没有读到Fail
队列控制块:一个结构体,里面有一些变量,比如:队列的长度、个数、队头、队尾
数据是复制过去的,有些操作系统是传地址。所以中断里面一般不建议搬运太长的数据(中断要快进快出),可以通过一个信号量发送一个标志位,让任务去搬运数据
消息队列常用API函数
第一个是消息队列的动态创建,第二个是消息队列的删除,第三、四个队列的发送(任务、中断),第五个是接收数据(任务里面)
传数据一般是从中断里面发送,用任务来接收
消息队列句柄、创建与删除***
任务中消息队列的发送
复制指定固定长度的数据,比如一个变量只占了四个字节(32位),传10个字节,后面六个地址是连续地址(无效数据)。创建队列长度要合适
太长数据可以分段发送
pdPASS改为pdTRUE
中断中消息队列的发送
中断中发送与任务中发送不一样。中断中发送,没有超时机制,不会等待
如果有高优先级任务准备就绪,立马进行一次任务切换,不等系统调度,提高系统实时性
第三个参数不一样
因为是数组,所以没有取址
任务中消息队列的接收
如果空间小了,比如说5个比特的数据,只有4个比特的空间,第5个比特也会写过来(未定义的地址),随便写数据可能会导致系统故障
消息队列应用编程-任务与任务***
消息队列应用编程-中断与任务***
信号量
信号量是消息队列的一种简化应用
资源管理:比如说机房的电脑管理、文件的管理
信号量本身不是数据,本身只是一个机制
技术信号量改为计数信号量
二值信号量的定义与应用
二值信号量是操作系统的一个资源,可以直接用
消息大小为0,说明本身不能存数据
信号量的实现(创建、发送、获取、释放)都是通过队列的函数实现的
采集一次,刷新一次。更好的发挥CPU性能
二值信号量的运作机制
任务1获取信号量,因为信号量没有,所以进入阻塞。需要任务2发送一个同步信号,任务1才能继续执行
任务1获取信号量,因为信号量没有,所以进入阻塞。串口接收中断,接收数据之后,发送一个信号,任务1(处理串口数据)开始执行
二值信号量的常用API函数
释放和获取需要同步操作
第一个是创建二值信号量。第二、三个是释放二值信号量(任务中、中断中)。第四个是获取二值信号量。第五个是删除二值信号量
没有超时机制的原因:0释放变为1,1不需要释放
不支持递归互斥(用不到)
有两个形参,第二个形参为了提高系统实时性(中断中任务不能切换,所以中断完之后立马切换一下)
第二个形参是超时机制。获取时,可能是无效的,也可能是有效的。如果有效,就会立马同步,如果无效就需要等待(等待多少个Ticks)。如果超时了,就会自动退出来
任务中获取
很少在中断里面获取信号
串口接收一般都是永久等待。可用可不用就不需要永久等待
等待原因:串口数据还没过来
这里只是打印串口信息,也可以通过MODBUS协议控制一些外设
二值信号量同步应用编程(任务与任务)***
二值信号量同步应用编程(中断与任务)***
计数信号量的定义与应用
计数信号量,因为可以计数,所以可以用作资源管理
计数信号量为0时,需要等待
计数信号量的运作机制
中断和应用一般都是同步使用,很少会用计数
共享资源:比如停车位、机房电脑、产品外设
任务1获取资源,如果资源变为0,可以等待,如果超时,自动退出来。如果资源被别的任务释放了,就可以获取资源,继续运行,运行之后,释放资源
计数信号量的常用API函数
一般不删除信号量,除非真的不用了
第一个是创建计数信号量,其他的函数和二值信号量一样
如果初始值定义为0,没信号量可用,必须等待释放。如果定义为N,就可以使用计数信号量
heap大小不足,内存不够
configUSE_COUNTING_SEMAPHORES,默认是0(节约内存),必须配置为1。后面的那个是动态分配,基本都是1,因为一些任务,还有其他的一些组件都是动态分配的
形参校验 configASSERT
删除的形参应该是句柄,不是void
释放的形参只有句柄,没有超时(要么成功,要么失败)。比如停车场把车开出去,不需要等待。开进来,要等待
第三个形参设为0,不会等待
计数信号量的应用编程详解***
互斥信号量的定义与应用
二值信号量可以用于同步(类似于裸机编程的Flag),可以但不建议临界资源管理(存在优先级反转问题)。互斥信号量可以用于临界资源管理
默认长度是1,资源可用。二值信号量默认可能是0或1
互斥信号量只能用于任务与任务之间
死锁,比如说只是普通的互斥信号量,任务1获取,互斥信号量无效(1->0),任务1再次获取(用完互斥信号量没有释放),就会等待(任务挂起),信号量发生死锁(信号量本身是无效的,别的任务用不了,任务永久等待),出现问题
获取一次,释放一次
优先级翻转详解
如果使用互斥信号量,优先级继承机制:当任务1等待任务3的时候,会临时把任务3的优先级抬高到任务1的优先级。低优先级任务继承高优先级任务。这样一来,任务3执行的时候,任务2就不能打断任务3了,任务3执行完之后,优先级会恢复。任务1开始执行(任务1只要等待任务3执行完成,不用等待任务2),然后再执行任务2
优先级翻转编程测试***
优先级翻转的串口打印详解***
互斥信号量的运作机制
因为会抬高任务优先级,所以中断中是无效的,只能用于任务中
互斥信号量的常用API函数
释放是不会等待的
获取需要等待,第二个形参为0就是不等待,这里是永远等待
互斥信号量的应用编程***
事件
事件的概念与应用
如果要用到数据传输,可以用队列
信号量只能实现一对一的同步,任务可以一对多(比如裸机编程中的if语句)
比如按键标志、串口接收都可以看作事件
事件的运作机制
事件的常用API函数
删除事件组一般很少用,除非不用了
第一个是动态创建事件组,一般很少静态。第三个等待事件组一般在任务中等待,很少在中断中等待
在中断里面调用了这个函数,但不会在中断里面置位这个事件标志组,中断把这个任务交给定时器,在定时器里面进行置位
优先级设置高,退出中断后,立马执行这个任务(置位)。优先级太低,可能被别的任务抢占,影响同步功能
事件的编程应用-任务与任务***
事件的编程应用-中断与任务***
软件定时器
软件定时器的概念
软件定时器在任务中执行回调函数,上下文是任务。硬件定时器通过中断实现
很重要,所有软件定时器都是通过定时器守护任务管理
软件定时器的应用
对时间精度要求不高的任务:比如说间隔1s或者一定时间采集传感器,WiFi数据模块定时传数据
1000-1s中执行1000次,1ms
定时器的应用是在回调函数中完成一些功能
回调函数:作为参数传递(函数指针),函数的形参是函数指针,通过函数指针调用的这个函数,叫做回调函数
回调函数方便是因为操作系统写好之后,函数指针已经定义好了,函数可以自己命名之后,把函数地址赋给指针,就可以调用它,非常方便移植。如果没有回调函数,就不能自己创建函数了,不灵活
回调函数的调用
通过定时器的句柄可以识别到ID号,以辨别是哪个定时器的回调函数
通过句柄间接调用自己创建的函数
软件定时器通过任务来执行,如果优先级很低,定时器精度会受到影响
守护任务需要间隔一定的时间,检查有没有任务被定时到。还要去接收队列的指令
如果上面一个函数挂起,那就意味着下面那个函数就不执行了,会影响实时性
回调函数要跟中断一样,快进快出,不要长时间的停留
软件定时器的常用API函数
删除软件定时器不一定,如果用完了,可以释放资源。获取ID可要可不要
这里的定时器的创建是动态创建(一般都是)
启动可以在任务中启动,也可以在系统启动之前启动(中断中启动,用的很少)
中断中停止,用的很少
操作定时器通过句柄来实现,所以要先了解一下它的句柄
方便调试,打印的时候,可以把定时器的名称打印出来
所有定时器在链表里面进行排序,守护任务在链表里面读取信息
单次模式,运行一次就会自动删除
通过ID号识别是哪个定时器。通过名字(字符串)来识别,比较麻烦
ID号是指针,所以要取址,再把指针转换一下
不会立即执行的原因,执行是在守护任务里执行的,如果调度器还没开始调度,尽管发送队列成功了,但守护任务还没有执行
返回时指针
软件定时器的应用编程***
任务通知
不用的话,可以配置为0,每次创建任务时,可以减少5个字节的RAM
前面两个可以当作队列来使用,
只能一对一
通知不会等待,没有超时机制
任务通知的运行机制
比如说有个任务等待通知,如果没有通知就阻塞了,如果别的任务或中断向这个任务发通知,它就会被唤醒。如果别的任务没有发通知,它也会超时退出来
跟其他通信机制一致,可以在一定程度上替代
任务通知的常用API函数
任务通知值,别的任务或者中断可以发通知改变这个值
任务中的通知状态一般又三种。第一种是默认状态。没有等待通知。第二种是任务正在等待通知。第三种,如果一个任务向另一个任务发送通知,另一个任务表示已经接收到通知
状态的切换:比如说第一次发送通知给任务,发送完之后,任务的状态就会变成已经接收。再次发送,还是处于接收的状态。如果任务开始获取通知,现在没有通知值,就会进入阻塞(等待中),如果接收到通知了,处理完之后,又会变成没有在等待了。三个状态里面不断切换。
pdTRUE当作二值信号量使用,pdFALSE当作计数信号量使用
第一个参数,仅仅改变任务通知的状态,不会改变任务通知值
不懂的,可以适当看一看源码
返回值应该是返回pdFAIL
第三个参数保存通知值(获得),可以通过这个参数来回传通知值
可以节约5个字节,不是8个字节
任务通知代替二值信号量的应用编程
任务通知代替计数信号量的应用编程***
任务通知代替消息队列的应用编程***
任务通知代替事件组的应用编程***
综合应用1与代码讲解
LED:只是指示作用,所以优先级最低,代表系统正在运行状态
KEY:事件组的触发
Display:数码管的显示,软件定时器在函数里面发送任务通知,来刷新这个数据
Event_Sync:事件同步的任务
Modbus:单片机通过空闲中断+DMA功能产生一个中断,通过二值信号量同步过去,进行一个协议解析。二值信号量的同步应用
Queue_Receive:队列接收,Modbus收到的数据通过队列发送给任务,这个任务再把16进制数据转换成ASCII码,打印出来
操作系统就是管理任务的,任务就是用来管理裸机代码的,从而实现想要的功能
计数信号量和互斥信号量没用,在单片机中用的很少
动态内存管理
动态内存,内存可以申请,可以释放
内存碎片:比如说一开始是一百块内存,后面有很多申请释放(大小不一样),会出现很多内存小块(不方便使用,因为太小),造成资源的浪费
内存碎片的回收,通过链表实现,小的内存块通过链表的合并组成大的内存
不连续的内存区,比如单片机内部有SRAM(不够用),外部SRAM,方式五能够把这两个SRAM链接在一起,能够一起使用
动态内存管理的总结与应用
动态内存的编程测试(heap_4和heap_1)
动态内存的编程测试(heap_5)***
独立看门狗检测任务执行
看门狗检测多任务执行思路
监测系统死机(跑飞),由于某种原因:比如恶劣的工业环境
监测任务有没有长时间得到执行,比如低优先级任务长时间得不到执行
一对多。比如可以在一定时间内监测这个事件
如果任务出现挂起和删除,就不检测这个任务
看门狗检测多任务编程***
低功耗Tickless模式
STM32低功耗模式介绍
STM32低功耗的编程***
Tickless模式介绍
不能直接把睡眠模式放在空闲任务的原因,因为Tick是中断,如果直接在空闲任务睡眠,会反复被唤醒,低功耗不是最优的
Tick时钟不能关断,不然就没法执行一些堵塞任务。比如定时任务需要中断唤醒
Tickless模式源码分析
Tickless模式编程测试***
综合应用2
间隔1s一般是用软件定时器,再通过二值信号量同步
第二点可以用二值信号量,也可以用任务通知做同步,中断里面给同步信号,任务里面处理串口数据
第三点是第二点通过队列传输数据
数码管显示,可以通过二值信号量同步显示
综合应用3