单片机裸机系统,通常又被称为前后台系统。
百度百科中,对前后台系统有一段解释:
前后台系统,即计算机前后台系统,早期的嵌入式系统中没有操作系统的概念,程序员编写嵌入式程序通常直接面对裸机及裸设备,在这种情况下,通常把嵌入式程序分成两部分,即前台程序和后台程序。
实现模式如下:
应用程序是一个无限的循环,循环中调用相应的函数完成相应的操作,这部分可以看成后台行为,也就是while主循环;前台程序通过中断来处理事件,也就是我们常见的中断。
一般情况下,后台程序也叫事件处理任务,前台程序也叫中断级任务。在程序运行时,后台程序检查每个任务是否具备运行条件,通过一定的调度算法来完成相应的操作(按照先后顺序依次调度)。对于实时性要求特别严格的操作通常由中断来完成,仅在中断服务程序中标记事件的发生,不再做任何工作就退出中断,这样就不会造成在中断服务程序中处理费时的事件而影响后续和其他中断。
实际上,前后台系统的实时性比预计的要差。这是因为前后台系统认为所有的任务具有相同的优先级别,即是平等的,而且任务的执行又是通过FIFO队列排队,因而对那些实时性要求高的任务不可能立刻得到处理。另外,由于后台程序是一个无限循环的结构,一旦在这个循环体中正在处理的任务崩溃,使得整个任务队列中的其他任务得不到机会被处理,从而造成整个系统的崩溃。由于这类系统结构简单,几乎不需要RAM/ROM的额外开销,因而在简单的嵌入式应用被广泛使用。
可以看出,前后台系统是在轮询系统的基础上加入了中断。
外部事件的响应在中断里面完成,事件的处理还是回到轮询系统中完成。
中断在这里称为前台,main()函数中的无限循环称为后台。
示例说明:
int flag1 = 0; int flag2= 0; int flag3 = 0; int main(){ hardwareInit();//硬件初始化 for(;;){ if(flag1){ doSomething1();//处理事件 flag1=0;//清除标志位 } if(flag2){ doSomething2();//处理事件 flag2=0;//清除标志位 } if(flag3){ doSomething3();//处理事件 flag3=0;//清除标志位 } } } //中断处理程序 void ISR1(void ){ flag1=1//置位标志位 } //中断处理程序 void ISR2(void ){ flag2=1//置位标志位 } //中断处理程序 void ISR3(void ){ flag3=1//置位标志位 }
在顺序执行后台程序时,如果有中断,那么中断会打断后台程序的正常执行流,转而去执行中断服务程序,在中断服务程序中标记事件。当然,并不是说不能在中断里处理程序,如果事件要处理的事情很简短,则可在中断服务程序里面处理,如果事件要处理的事情比较多,则建议返回后台程序处理。虽然事件的响应和处理分开了,但是事件的处理还是在后台顺序执行的,但相比轮询系统,前后台系统确保了事件不会丢失,再加上中断具有可嵌套的功能,这可以大大提高程序的实时响应能力。
通常,我们会在中断里执行一些紧急的任务,对实时性要求比较高的任务,最直观的就是,速度很快的任务,需要及时处理的任务。
举个例子说明
直接参考:前后台系统的精髓
总之就是
紧急的事务一定要用中断处理!中断只处理紧急事务!
比如底层的数据收发,速度太快,放在后台处理太慢了。对于数据的处理,就没有那么着急了。可以在中断里接收完成后设置标志位,然后在主循环里根据标志位完成处理,之后手动开启相应中断去发送。
这其中,注意全局变量的使用。
主循环和中断的关系梳理
首先要明确的是,嵌入式程序中必须要有主循环,这样才能保证程序的正确反复执行。
之前学习时讲的是,不要在中断里做过多的操作,以免中断时间过长,影响其他中断和主循环的程序执行。
关于这点可参考:
1,中断处理时间太长或导致正常的应用程序不能按时被执行;
2,不能嵌套的cpu中,执行中断时不能响应其他中断,中断执行时间太长容易丢其他中断;
3,能嵌套的cpu中,某个中断服务程序执行时间太长,可能导致中断嵌套非常深,容易导致栈溢出,也容易出现逻辑问题;
ISR会阻断正常的CPU调度机制。
在最简单的嵌入式系统上,可能没有操作系统,所谓CPU调度就是状态轮询加上各种中断响应。如果一个ISR占用时间太长,势必影响轮询的及时性稳定性。如果ISR一直关中断,不允许本身再入或者响应其他中断,那麻烦就更多了。所以正确的ISR应该只做那些必须立即完成的事情,然后把不必立即处理的数据保存下来,设置一个状态指示,然后退出,让轮询机制去处理。
在有操作系统的情况下,ISR怎么写,必须符合操作系统规定的规范,不能因为ISR运行特权级别高就乱来。
注意:
并非所有的任务都不能放在中断里,一些紧急任务,比如数据收发等,就需要放在中断里,如果放在主循环里很可能来不及处理。
我的理解:
主循环和中断本身是独立的,即CPU和外设是可以同步工作的。
二者的配合就是,外设的工作完成后,会通知CPU,这时候,主循环里可以根据标志位情况进行相应的处理。
这样一来,二者的分工就相对明确了,中断里处理数据传输的紧急任务或者完成标志位,然后主循环里根据标志位去处理不紧急的任务。
这样一来,中断和主循环的某个任务在某种程度上就是一种先后的顺序结构,只有中断完成了,主循环里才能根据标志位来执行任务。
因为主循环是循环执行的,所以不管一个任务放在主循环的哪个位置,都是会被循环执行到的,这就是CPU的轮询调度。
中断是外设在硬件层面上的实时性任务。
注意:
以最近学到的一种思路来举例。
之前,我用定时器时,都是一个任务一个定时器,这样,有几个任务就需要几个定时器,每个任务时间改变时,就去改变初始化的内容。
后来学到一种思路,定时器只用一个,然后设定1ms的基准值,接着在对应的中断里面计数,并且设置计数完成标志位,然后在主循环里面,根据完成标志位再去执行相应的业务代码。这样一来,中断里就只处理了标志位,而且,因为定时器里几乎没有业务逻辑,所以一个定时器中断里可以执行判断好多个任务的定时,然后主循环里依次执行各业务逻辑。
中断最大的优势就是及时响应。
主循环里是顺序执行的,所以,没办法及时响应,但是可以处理多任务。
因此二者需要配合使用。
主循环优先级是最低的,会被所有中断打断。
参考:
这里面可能会涉及到一些原子操作的概念。
如果中断函数没执行完成时,又来了该中断源的中断请求,会发生什么情况?
参考:
如果中断函数没执行完成时,又来了该中断源的中断请求,会发生什么情况? - STM32/STM8单片机论坛 - ST MCU意法半导体官方技术支持论坛 - 21ic电子技术开发论坛
参考:
前后台系统中,中断只进行硬件层面的数据处理以及置标志位,其他的事情让主循环去做。
任务要有执行条件
裸机开发时,主循环里的任务都需要有执行条件,只有满足执行条件,才会进入执行。
而这个执行条件,直观来说其实就是判断标志位。
如果没有执行条件,就会不断地循环进入程序执行,可能会导致程序卡顿。
另外一方面,如果没有执行条件,则不管什么情况都会执行,那么在不需要执行的时候也会执行,那么就加大了主循环中的执行时间,实时性更加无法得到保证。
关于标志位补充
在裸机编程中,会大量用到各种状态标志位。
一方面,这些标志位是表示机器的某些状态,另一方面,因为主程序和中断的执行,很多时候是有先后顺序的,所以,需要通过一些标志位来判断某个任务当前是否执行。
比如某个任务有些地方实时性较高,有些地方实时性要求没那么高,此时,就可以将实时性要求高的放在中断里,实时性要求不高的放在主循环里。
这种情况下,就需要有个标志位,让中断里先执行,执行后置个标志位,主循环里,当标志位被置位时才会开始执行接下来的程序部分。
另外要注意的是,当判断标志位并且执行了相关代码后,就需要复位标志位,防止每次都会进入执行。
其实,中断里的标志位判断正确后需要复位标志位,就是为了防止条件还没满足时也能不断地进入执行。
比如,接收数据然后处理,接收数据就需要放在中断里,因为这个对实时性要求较高,但是对数据的处理就没那么急了,可以放在主循环里,此时,就有先后顺序的要求,
需要先接收完数据,然后才能处理。所以,就需要一个标志位。接收完数据后,标志位置位,然后在主循环里面,根据标志位置位去执行处理程序,同时,需要复位标志位,
要不然,就会不断进入主循环程序,而不会按照既定的顺序去执行,自然就会出错了。有个问题就是,在一个1秒中断里写10个任务,和把10个任务分别写在10个1秒中断里,有区别吗?
前者的好处是,只需要一个定时器就能搞定,而后者需要10个定时器。
后者看起来貌似可以让程序执行更快,但仔细想一想,他10个定时器也不是并行执行的,就算10个定时器可以同时触发,但是CPU一次也只能
执行一个中断,中断的执行也是要排队的。
所以,前者在一个中断里排队,后者分成了好多个中断在排队,二者的执行实时度并没有明显区别。
定时器的使用技巧
有时候,需要很多时间定时,比如100ms,200ms,300ms,500ms,1s,5s,1min等等,试想一下,如果每个时间都定个定时器,那单片机仅有的定时器肯定不够用。
怎么办呢?
那就是开启一个定时器时基,比如定个1ms定时器,然后可以在中断处理函数中,对每个定时任务都进行计数,只要是1ms的整数倍,都可以计数到达目标之后才执行任务,这样一个时基定时器的处理函数中就可以处理多个任务。
比如:
这里示例的时基设置的是10ms
但是要注意,中断里只进行标志位的设置,不要进行任何业务操作。
标志位的设计
一开始,我在设计标志位的时候,都是用一个字节的0和1来表示的,这样太浪费了。
其实有更好的方式,这种方式也是单片机设计中的一种设计思想,那就是每个标志位用1个位来表示,如果一个位不够表示,就用多个位来表示。
比如:
#define FLG_UART 0x01
#define FLG_TMR 0x02
#define FLG_EXI 0x04
#define FLG_KEY 0x08
然后由此就有一些计算方式
定义u8FlgTmp为这个标志位字节的表示变量
那么u8FlgTmp & FLG_UART位与就能表示u8FlgTmp里是不是置位了 FLG_UART这个标志位
if(u8FlgTmp & FLG_UART)
{
action_uart(); /*处理串口事件*/
}
g_u8EvntFlgGrp |= FLG_UART; /*设置 UART 事件标志*/位或就可以将FLG_UART标志位叠加到g_u8EvntFlgGrp上。
以下举例说明:
g_u8EvntFlgGrp一开始为0
volatile INT8U g_u8EvntFlgGrp = 0; /*事件标志组*/
然后接连发生了UART/TMR/EXI/KEY中断,那么:
g_u8EvntFlgGrp |= FLG_UART,此时g_u8EvntFlgGrp为0x01
g_u8EvntFlgGrp |= FLG_TMR,此时g_u8EvntFlgGrp为0x03
g_u8EvntFlgGrp |= FLG_EXI,此时g_u8EvntFlgGrp为0x07
g_u8EvntFlgGrp |= FLG_KEY,此时g_u8EvntFlgGrp为0x0F
此时g_u8EvntFlgGrp就记录了所有事件的触发标志,然后如何判断当前事件有没有发生?
那就是用g_u8EvntFlgGrp去位与对应的标志位
g_u8EvntFlgGrp & FLG_UART
g_u8EvntFlgGrp & FLG_TMR
g_u8EvntFlgGrp & FLG_EXI
g_u8EvntFlgGrp & FLG_KEY
标准库举例
程序进行位操作的效率也更高。
不过,这样用1位表示1个标志,一个字节也只能表示8个,数量利用不大够。
如果标志较多,也可以直接使用一个字节从0—255的数值来表示各种标志。
需要注意的是,这种方式一个变量同一时间只能表示一种类型的状态,而位方式一次能表示8种类型的状态,相当于8个变量。
实际中根据具体需要去进行设计。
仔细想了想,以上更适合驱动层面的设计,业务层面复杂且不固定,设计起来相对困难。
如果标志位太多,就用位操作,前提是事件标志,逻辑或可以传入多个标志位。
标志位引起的隐蔽BUG
遇到一个很隐蔽的问题。
我有个需求,是这样的,我需要每隔一段时间就往屏幕发数据,于是我在定时器里计数,然后在主循环根据计数值来判断是否执行发送函数。
我以为这样在满足条件后只会执行一次,其实会进入执行很多次。
为什么?上面图片中,我理想情况下,当计数值templateTimeCount为5的整数倍时才会进入,0和5都会进入,但是因为这个是放在主循环里循环执行的,当templateTimeCount=0的时候,因为等待templateTimeCount=1之前,会进入这个函数很多很多次,而且每次判断都是满足条件的。
这里其实就是1个定时周期内能执行这个程序多少次,这里就会执行多少次。
这就是为什么主循环中以及中断中的每个任务函数都要有判断条件,而且,进入后要马上清标志位,要不然会不断进入,所以,裸机中主函数中判断标志位和清除标志位都是必须的操作,如果没有这个环节,就要注意很有可能会出错。
任务同步
用标志位实现的,两个程序先后执行的情况,可以称之为任务同步。也就是说,两个任务必须按照既定的先后顺序来执行,而不是随机地谁先谁后。
为什么中断里不建议有耗时较长的操作?
就以直接延时来考虑这个问题。
如果中断里延时了,那么因为主循环和相同优先级中断没法打断延时的中断,所以其他中断和主循环里的其他任务都得不到及时执行。
主循环优先级最低,主循环不会阻塞中断,只有中断会阻塞中断。
所以中断里不能时间过长的最大原因是,会阻碍其他同优先级的中断执行。
不管是主循环还是中断,都不能有长时间延时,都会阻碍其他主循环任务的执行,中断里更严重的会影响其他中断的执行。
那对于较耗时的工作,为什么不推荐放在中断里,但是可以放在主循环里?
因为中断本来就是为了响应实时性要求高的任务而设定的,执行的任务实时性要求都很高,所以不能有较长时间的阻塞,但是主循环里的任务优先级没有那么高,有稍微的耗时也问题不大,这个耗时有时也不要想得太夸张,一般也就是从ns级别转到了us或者ms级别罢了。
所以
为什么中断里不建议有耗时较长的操作?
因为影响实时性。
有个疑惑?
中断能打断死循环吗?
分情况,如果优先级低,比如是软件级别的死循环,那就可以打断,因为中断优先级永远高于软件优先级。比如中断就可以打断main函数里的死循环。
如果是中断里的死循环,那么只有更高优先级的中断能打断。
这样的话,那HardFault是不是优先级最高的中断呢?因为程序进入HardFault里的死循环就彻底死了。