前言
1、中断简介
中断:程序在运行过程中发生了外部或内部事件时,中断了正在执行的程序,转到外部或内部事件中去执行别的任务。
(1)中断的作用
- 中断的作用是大量节约CPU资源,提高程序的效率,即避免重要事件被错过。
- 中断使用地方:没办法确定什么时候需要处理数据时,采用中断方式处理。
以之前学习的串口接收数据为例,比较使用中断与否的效果:
①查询方式:能够实现功能代码,CPU一直在查询是否接收数据标志,如果程序中有延时就会产生丢数据的现象。
②中断方式:能够实现功能代码,当产生接收数据中断时,立马告诉MCU,也就是将CPU拉到中断中执行中断任务。(如果中断没有发生变化,MCU可以做其他工作)。
(2)中断入口
在单片机内部有一块空间专门用来存放地址,该地址已经和相应的中断绑定在一起了(芯片厂商处理的)。当中断发生时,CPU会跑到该内存空间中查找中断入口地址,该地址就是中断入口。该地址对于开发人员而言,可读性较差,所以芯片厂商在芯片设计时将该地址重新取名(便于记忆和理解),即中断服务函数名,在stm32里,中断服务函数名是不可自定义,所有的中断服务函数名都已经被设定好了。因此在编写中断服务函数时绝对不能写错,如果写错,CPU就找不到中断的入口。如中断地址0x233947hff取中断服务函数名为USART1_IRQHandler,这对应着同一个中断事件。
中断服务函数在XXX_md.s文件。
主函数和中断服务函数的关系:主函数和中断服务函数之间并不是主函数调用中断服务函数的关系,二者属于同一个级别,即本质是中断服务函数和主函数一起抢占CPU的使用权。所以中断服务函数不是写在主函数中。
(3)中断优先级
把事件的紧急情况或执行事件的先后顺序称为优先级。使用到优先级的目的:为了区分这些事情的重要程度,即事件执行时的先后顺序。优先级就是一个数值,数值越小,优先级越高。每个中断优先级可以分为自然(固定不变的)和可控(程序员可以配置)两种。中断自带的优先级编号称为自然优先级。可控优先级就是可以由程序员自行修改的优先级。
- 可控优先级分为抢占优先级和响应优先级。
- 自然优先级的本质就是一些硬件编号。数字越小优先级越高。
- 优先级比较顺序:先比较抢占优先级,再比较响应优先级,最后比较自然优先级。
(4)中断嵌套
中断嵌套只发生在抢占优先级不一样的情况下。
中断嵌套:就是中断在执行的时候在中断中又出现了一个中断。
中断嵌套目的:处理更紧急的事情。中断嵌套中需要将嵌套的中断优先级设置的更高。因为高优先级的任务可以打断或抢占低优先级的任务。
2、Cortex-M4——中断体系
中断体系就是管理中断的一套机制。
(1)Cortex-M4-中断架构
在芯片的内核里专门有一个管理中断机制的模块——NVIC(嵌套向量中断控制器)控制器。M4内核专门负责处理中断相关问题的机构,专门做中断管理相关的事务。
注意:NVIC控制器属于内核级的模块,所以在中文手册找不到,到芯片内核编程手册:《Cortex M3与M4权威指南.pdf》在第7章异常和中断中详细讲中断体系中查看。
所有的中断都是在NVIC控制器中控制的,如下图所示。
- NVIC控制器中的中断来源:包括来自GPIO口外部中断,片上外设,系统核心,SysTick,非掩蔽中断。
- Cortex-M3和Cortex-M4支持多达240个IRQ(中断请求)、一个非掩蔽中断(NMI)、一个SysTick(系统滴答)定时器中断和一些系统异常。NMI通常是由外围设备(如看门狗定时器)生成的,其余的异常来自处理器核心。大部分 IRQ由外围设备(如定时器、I/O端口和通信接口(例如UART、I2C)生成。
(2)Cortex-M4-NVIC控制器中断来源分析
NVIC控制器总共提供了255个中断入口。具体见《Cortex M3与M4权威指南.pdf》
①系统及SysTick中断入口共15个(内核固定的)。
注意:这是内核级中断,中断控制器必须响应,内核中断不能够被打断也不能被设置优先级,具有固定的优先级。这15个中断源是由ARM设计的。
②片上外设或IO口中断入口16到255总共240个。
注意:该部分中断由芯片厂商决定(不是内核极的),由芯片生产者决定。
(3) Cortex-M4中断管理方式
Cortex-M4 NVIC控制器中1 ~ 15个系统中断不能被打断也不可以设置优先级,他们的优先级是固定的,且发生事件时必须要响应。剩下的16~255总共240个中断属于外部或片上外设中断,他们可以自由的设置中断优先级,且每一种中断都具有三种类型的优先级:可控优先级(抢占优先级,响应优先级),自然优先级。
①抢占优先级:当其中一个中断正在执行时,其他的中断是否可以打断正在执行的中断。
②响应优先级:决定当多个抢占优先级都相同且多个中断同时到来的时候,先执行哪一个中断。
③自然优先级(硬件固定):按照自带的优先级编号(硬件固定)在抢占和响应优先级相同的情况下决定先执行哪个中断,数字越小级别越高。
- 优先级大小:抢占优先级>响应优先级>自然优先级。
- 每个中断源的抢占优先级和响应优先级由用户决定(软件设置),而自然优先级已经被硬件固定, 不可更改。优先级越高其对应的值越低。数字越小,优先级越高。
M4 NVIC控制器通过分组来设置各个中断的优先级的方式来管理各个中断。
在CM4里面,系统会给每一个中断源都分配一个8位寄存器来存放它的优先级(抢占优先级和响应优先级)。在这8位寄存器里面,一部分用于存放抢占优先级,另一部分存放响应优先级。8位寄存器的理想情况下的分布情况如下(抢占优先级位数+响应优先级位数=8bit):
Cortex-M4-NVIC 中得知抢占优先级+响应优先级最多占8位,最少3位。(一个范围,具体占几位取决于芯片生产商)
M4内核中的中断可分为8组,不同的组抢占和响应的位数不一样,可以设置的优先级的范围也不一样。如下表所示。
注意:抢占优先级最大有128级别,响应优先级最大有256级别。
往SCB(系统控制块)->AIRCR寄存器的PRIGROUP3位(8 ~ 10)中写入不同值(分组值0~7)即可决定系统中,中断的抢占优先级位数和响应优先级位数。
一个系统在使用中断之前,必须确定优先级分组,也就是确定抢占优先级和响应优先级的位数,从而决定抢占优先级和响应优先级的可设置范围。
(4)Cortex-M4-NVIC控制器中断相关函数介绍
3、STM32——中断体系
(1)STM32-NVIC控制器简介
STM32-NVIC控制器结构和内核的一样,只是中断入口或者来源发生了变化。见《STM32F4xx中文参考手册.pdf》中断章节。
1)设置分组&优先级
M4内核的NVIC控制器抢占+响应优先级设置总共占用8bit表示。而ARM公司规定:不是所有的芯片厂商都要使用8位。最多可以使用8bit,最少不少于3bit。可以根据需要使用(比如自己芯片功能等)。比如ST公司就是使用4位,NXP(恩智浦)公司使用5位。
这里讲的是STM32F40X的中断分组设置(使用4bit)。抢占优先级位数+响应优先级位数=4bit。
知识总结:
- 由上表得知STM32优先级使用4~7共4个位控制。 如下所示:
- 由上表得知STM32分组值范围:3 ~ 7,上表使用的是二进表示形式,是写入寄存器的值,一般对分组使用组编号的形式,组编号范围:0 ~ 4。组编号中0对应于分组值的7,而组编号中4对应于分组值的3。组编号=抢占优先级位数。
问:将某外设分组为第5组,程序如何实现?
答:抢占优先级是几位就7-几,即(7-n),剩下的是响应优先级位数。
注意:设置组编号(5)的目的是为了更清楚的知道抢占优先级和响应优先级的大小,其本身并不存在。
(2)STM32-NVIC控制器相关函数及配置方法
在NVIC控制器中打开相应中断功能叫做:核心级中断使能。
(3)STM32外设中断原理
经过核心级中断使能的中断并未真正的打开,还需要打开相关外设的中断,即模块级中断。即要真正使用中断功能要实现核心级中断使能+模块级中断使能。
真正的开启一个外设的中断需要以下步骤:
①开启核心级中断使能,即在NVIC控制器当中打开该外设对应的中断(比如 串口接受中断)。
②开启模块级中断使能(打开在外设中当前(比如串口接受中断)中断的开关)。也可以理解为想要真正的使用中断需完成以下几步:
①设置分组; (可以在中断函数对应的源文件处设置,也可以在main.c文件设置)
②设置抢占和响应的优先级级别;
③给中断设置中断功能;
④打开中断在NVIC控制器当中的开关;
⑤打开中断在模块(外设)中的开关。
中断过程分析:
书写中断服务函数注意事项:
① 中断服务函数名是固定的,在启动代码里面已经定下来了,不可以由程序员自己更改。
② 在STM32中,中断服务函数名在相应的汇编文件(.s文件)中有,如STM32F407VGT6→startup_stm32f40_41xxx.s。 中断编号即中断源在相应的头文件(.h文件)中有,如STM32F4XX.h。
③ 中断服务函数名尽量用复制,不要自己写,写错就进不了中断。
④(如果中断服务函数是公共入口即如果是全局中断)进入到中断服务函数后先要查询是哪种中断。
⑤ 先清中断标志,然后再做中断处理,不要把清中断标志放在函数的最后。(如果把清除中断标志放在中断服务函数的最后,会出现当发出清中断标志指令后,硬件还没有把相关标志清除掉,程序就已经跳出了中断服务函数,这个时候NVIC又会识别到标志是1,出现重复中断)。可以清除中断标志命令发出后,等待清除成功再往下执行。
⑥ 中断服务函数应该尽量简短,一般是做一些标识,不要在中断中做延时之类的占用CPU很长时间的工作。(快进快出)
⑦ 中断服务函数不会被任何一个函数调用,当中断条件满足后,NVIC控制把CPU拉到中断服务函数中执行,执行完毕后在回到主函数。
⑧ 中断服务函数写法是:void 中断服务函数 (void),即不可以有参数和返回值。
⑨ 中断服务函数不需要声明,也不需要调用,它的本质和main函数一样,其工作原理是和main函数抢CPU的使用权。
4、说明
本博客设置 USART1接收中断为例。
串口1模块级中断寄存器(USART_CR1):
一、单片机利用中断接收字符串
现象:串口助手发送字符串到单片机,单片机将接收到的字符串又发送到串口助手。
1、在usart.c文件中编写中断接收字符串函数
先初始化,再编写接收函数。
void Usart1_Init(u32 brr)
{
//1、开串口USART1和GPIOA组时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1 ,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct; //定义结构体变量
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; //配置为复用推挽输出
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz; //输出速度为2MHz
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9; //选定管脚PA9
GPIO_Init(GPIOA,&GPIO_InitStruct); //调用初始化函数
//2、初始化USART1
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = brr; //波特率
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //禁止硬件流控
USART_InitStruct.USART_Mode = USART_Mode_Tx|USART_Mode_Rx; //全双工通信
USART_InitStruct.USART_Parity = USART_Parity_No; //禁止奇偶校验
USART_InitStruct.USART_StopBits = USART_StopBits_1; //1个停止位
USART_InitStruct.USART_WordLength = USART_WordLength_8b; //8bit数据位
USART_Init(USART1, &USART_InitStruct);
//中断
USART_ITConfig(USART1,USART_IT_RXNE, ENABLE); //使能USART1接收中断
USART_ITConfig(USART1,USART_IT_IDLE, ENABLE); //使能USART1空闲中断 判断通信什么时候结束
NVIC_SetPriority (USART1_IRQn, NVIC_EncodePriority (7-2, 1, 2)); //设置抢占和响应的优先级级别,并将合成的优先级设置给USART1中断源
NVIC_EnableIRQ (USART1_IRQn); //使能NVIC控制器中断开关,即打开串口1在NVIC控制器当中的开关,这一步必须要
//3、使能串口1
USART_Cmd( USART1, ENABLE);
}
//使用中断方式接收字符串。
u8 buff[50];
u8 rev_ok;
/*
1.中断服务函数不可以有返回值,也不可以有形参
2.中断服务函数名名字已经固定好,不可以由程序员自己更改
3.中断服务函数,不需要声明,也不需要调用,它的本质和main函数一样,其工作原理是和main函数抢CPU的使用权
*/
void USART1_IRQHandler(void)
{
static u8 i = 0;//static 修饰的静态局部变量只执行初始化一次
if( USART_GetITStatus(USART1, USART_IT_RXNE) ) //接收中断
{
USART_ClearFlag(USART1,USART_FLAG_RXNE); //清除接收中断标志位
buff[i] = USART_ReceiveData(USART1);
i++;
}
if( USART_GetITStatus(USART1, USART_IT_IDLE) ) //空闲中断
{
if(USART1->DR) //读DR寄存器 清空闲标志位
{
;
}
buff[i] = '\0'; //字符串以字符'\0'结尾
i = 0; //数组下标清零
rev_ok = 1; //表示一次通信结束
}
}
2、编写main.c文件,引用usart.c文件的中断接收函数。
#include "main.h"
int main()
{
NVIC_SetPriorityGrouping (7-2); //分组一般同一个工程只设置一次
Usart1_Init(115200);
while(1) //死循环
{
//中断方式
if(rev_ok == 1) //这个标志位置1代表一次通信结束
{
rev_ok = 0;
Usart1_Send_Str(buff);
}
}
return 0;
}
二、单片机利用中断控制LED灯
现象:串口助手发送命令到单片机,单片机将接收到命令后做出响应。如串口助手发送LED_ON,单片机将打开LED灯。
1、编写main.c文件,引用usart.c文件的中断接收函数。
使用strcmp需包含#include “string.h”。在串口助手里的“发送新行”要取消,否则控制不了。因为它是以“\r\n”结尾。所以也可以在代码上加上“\r\n”就不用取消。
#include "main.h"
int main()
{
NVIC_SetPriorityGrouping (7-2); //分组一般同一个工程只设置一次
Usart1_Init(115200);
Led_Pin_Init();
while(1) //死循环
{
if(rev_ok == 1) //这个标志位置1代表一次通信结束
{
rev_ok = 0;
Usart1_Send_Str(buff);
if(strcmp("LED_ON",(const char *)buff)==0)
{
LED1_ON;
LED2_ON;
LED3_ON;
}
if(strcmp("LED_OFF",(const char *)buff)==0)
{
LED1_OFF;
LED2_OFF;
LED3_OFF;
}
}
}
return 0;
}
控灯部分也可以放在usart.c文件的中断接收函数里。