在程序中,CPU对外界突发事件进行处理的方式又两种:
1》轮询系统:(在main中,使用while循环,进行循环判断外界事物是否发生)
while(1){
}
2》前后台系统:(此时main中的while中的程序是在处理其它事务,当中断来到时,就处理中断服务函数)
轮询系统+中断
中断的过程:
在主程序执行的过程中,中断突然发生,此时主程序停止往下执行,并将CPU的当前状态保持在内核栈中(即:现场保护)。然后跳转到中断服务函数的入口,并执行中断服务函数,当中断服务函数执行完后,再将之前保存在内核栈中的状态全部进行出栈,将状态恢复到发生中断之前(即:现场恢复),此时对于CPU来说,就好像从来没有发生过中断一样。
中断管理器——异常处理器NVIC
我们可以在这里寻找到,NVIC的中断优先级分组。
中断异常处理器-----NVIC---专门用于管理中断的片上外设
管理中断的方式:(注意:整个程序,管理中断的方式,只能五选其一,如果设置多个,那么只有最后一个设置生效)
*NVIC_PriorityGroup_0:0 bits for pre-emption priority //没有主优先级,即:0个位来表示主优先级
* 4 bits for subpriority //16种次优先级,即:4个位来表示次优先级
*NVIC_PriorityGroup_1:1 bits for pre-emption priority//2种主优先级,即:1个位来表示主优先级
*3 bits for subpriority //8种次优先级,即:3个位来表示主优先级
NVIC_PriorityGroup_1的分配如下图:
*NVIC_PriorityGroup_2:2 bits for pre-emption priority
*2 bits for subpriority
*NVIC_PriorityGroup_3:3 bits for pre-emption priority
*1 bits for subpriority
*NVIC_PriorityGroup_4:4 bits for pre-emption priority
*0 bits for subpriority
NVIC给每个中断源都配置两种优先级,分别是:(数值越小优先级越高)
抢占优先级(主优先级):具备抢占功能,优先级高的中断可以抢占优先级低的中断,但是一个中断不能抢占另外一个相同优先级的中断。由此可知,一个中断不能抢占它自己,因为自己与自己的优先级相同。
执行优先级(次优先级):不具备抢占功能,只有在两个主优先级相同的中断,并且它们同时发生的情况下,那么次优先级高的先执行。
总结:配置中断的3个步骤(不管什么时候,中断配置都是这四步)
1.配置中断源
2.配置中断源的抢占和执行优先级
3.编写中断服务函数
4.如果是程序的第一个中断,需要设置优先级分组(最好写在主程序中)
配置中断的4步细节描述:(以UART中断为例)
配置一个中断需要做以下几件事情:
1,配置中断源
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState)
2,给这个中断源配置相应的抢占优先级和执行优先级
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct)
typedef struct
{
uint8_t NVIC_IRQChannel; //中断通道号 refer to stm32f10x.h
//typedef enum IRQn
uint8_t NVIC_IRQChannelPreemptionPriority; //主优先级
uint8_t NVIC_IRQChannelSubPriority;//次优先级
FunctionalState NVIC_IRQChannelCmd; //使能中断通道
} NVIC_InitTypeDef;
3,编写中断服务函数----中断服务函数的函数名,已经在启动文件中定义,所以中断服务函数的函数名在启动文件中找
注意:中断服务函数写在,stm32f10x_it.c文件中,该文件在apps文件夹下
void USART1_IRQHandler(void)
{
//判断标志位
//清除中断标志位
}
4,如果是程序的第一个中断,需要设置优先级分钟(最好写在main函数中,写在子函数中会被子函数重复调用)
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)
怎样查看中断源的通道号:
我们可以在stm32f10x.h文件中找到,在与我们所使用的启动文件名字(即:STM32F10X_HD)一样的那部分定义。再到需要使用的中断源的名字,它的右边便是中断通道号。我们还注意到了这部分的通道号是从18开始的,是因为这些通道号是这款芯片专用的通道号,0~17通道号在该文件的前面,它是通用的通道号。
注意:中断服务函数的函数名不能乱写。必须在启动文件中寻找到中断服务函数的名字。因为函数名就是函数的入口地址,这个地址在启动文件中就定义好了的。中断服务函数的函数名如果写错了会导致中断服务函数无法执行。
UART中断配置函数
UART中断源配置函数。
USART_IT_RXNE:表示当UART串口接收到数据的时候,就触发UART中断。
中断标志位,如果相应的事件发生中断,则该标志位置1
NVIC中断管理器配置函数。
UART接收数据函数,即UART_RX接收数据进CPU来。
UART发送数据函数,即UART_TX发送数据出去。
编写程序
void USART1_Config(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
USART_InitTypeDef USART_InitStruct;
NVIC_InitTypeDef NVIC_InitStruct;
// 1,打开时钟---GPIOA,串口1,AFIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO|RCC_APB2Periph_USART1,ENABLE);
// 2,GPIO初始化
GPIO_InitStruct.GPIO_Pin =GPIO_Pin_9;
GPIO_InitStruct.GPIO_Mode =GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed =GPIO_Speed_2MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin =GPIO_Pin_10;
GPIO_InitStruct.GPIO_Mode =GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA,&GPIO_InitStruct);
// 3,串口初始化
USART_InitStruct.USART_BaudRate =115200;
USART_InitStruct.USART_HardwareFlowControl =USART_HardwareFlowControl_None;
USART_InitStruct.USART_Mode =USART_Mode_Rx|USART_Mode_Tx;
USART_InitStruct.USART_Parity =USART_Parity_No;
USART_InitStruct.USART_StopBits =USART_StopBits_1;
USART_InitStruct.USART_WordLength=USART_WordLength_8b;
USART_Init(USART1,&USART_InitStruct);
USART_ClearFlag(USART1, USART_FLAG_TC|USART_FLAG_TXE);
// 1,配置中断源
USART_ITConfig(USART1,USART_IT_RXNE, ENABLE); //当UART串口接收到数据的时候,就触发UART中断。
// 2,给这个中断源配置相应的抢占优先级和执行优先级
NVIC_InitStruct.NVIC_IRQChannel =USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority =0;
NVIC_InitStruct.NVIC_IRQChannelSubPriority=0;
NVIC_InitStruct.NVIC_IRQChannelCmd =ENABLE;
NVIC_Init(&NVIC_InitStruct);
// 4,使能串口
USART_Cmd(USART1,ENABLE);
}
//中断服务函数(写在stm32f10x_it.c文件中的指定位置,这个位置如下图)
该文件的这个位置是专门写外设中断的地方,其中USART1_IRQHandler为中断函数名
/*中断服务函数:功能:pc端的串口助手通过UART_RX发送数据给MCU,接收到数据后,MCU通过UART_TX发送数据给PC端的串口助手*/
void USART1_IRQHandler(void) //
{
if(SET==USART_GetITStatus(USART1,USART_IT_RXNE)){ //如果UART串口接收到了数据,那么该标志位置1
USART_ClearITPendingBit(USART1,USART_IT_RXNE); //手动清空标志位,一定要清空!
USART_SendData(USART1,USART_ReceiveData(USART1)); //通过UART_TX发送数据给PC端的串口助手
}
}
注意:无论什么中断,在进入中断后必须进行中断标志位的判断,即:
if(SET==USART_GetITStatus(USART1,USART_IT_RXNE)).....,因为由上面可知,触发USART1中断的事件有很多种,我们并不知道是哪一种事件触发了USART1中断,所以,我们需要对相应的中断触发事件标志位进行判断,然后做相应的中断程序处理。
注意:中断服务函数越短越好。如果是定时器中断,那么定时器中断服务函数执行的时间大于定时的时间的话,每当执行完定时器中断函数后,就立马又加入该中断。因为定时器中断在进入中断后,就立马重新开始计数,当计数完后,触发下一次中断,但是如果此时中断函数还没有执行完,那么定时器不会马上执行下一次中断(因为自己不能抢占自己),并且计数器此时不会再重新计数,而是等待中断函数执行后,立马又进入定时中断,然后计算器重新开始计数。这样的话,其他中断就没机会抢到cpu的控制权了。就算不是定时器中断,那么如果中断执行时间长的话,main函数就没时间去执行了,而且其他中断也没有机会抢到cpu的控制权。所以,在中断中不要加mS级的延时(中断执行时间都是uS级的)!!!。
注意:外部中断触发也是如此,如果在触发中断的时候,上一次中断还没执行完,由于自己不能抢断自己,它就会等待。等到上一次中断执行完后,它就马上执行这一次的中断。
int main(void)
{
RCC_ConfigTo72M();//将系统时钟配置成72MHZ
Systick_Config(72);
USART1_Config();
// 4,如果是程序的第一个中断,需要设置优先级分钟
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
while(1){
LED_CTRL(LEDR,LED_CLOSE);
Systick_NmsDelay(3000);
LED_CTRL(LEDR,LED_OPEN);
Systick_NmsDelay(3000);
}
}
串口助手给MCU发送字符,控制LED的亮/灭
功能:串口助手给MCU发送open字符串,控制LED的亮,发送close字符串,控制LED的灭
方法一:(接收标志位中断)
//串口中断服务函数
/*每次MCU接收到从串口助手发送来的一个字节,MCU就会进入一次USART1中断服务函数。*/
void USART1_IRQHandler(void)
{
if(SET==USART_GetITStatus(USART1,USART_IT_RXNE)){
USART_ClearITPendingBit(USART1,USART_IT_RXNE);//手动清空标志位,一定要清空!
USART_SendData(USART1,USART_ReceiveData(USART1));
USART1_Data[indexs]=USART_ReceiveData(USART1); //将接收到的一个字节数据,保存在数组USART1_Data[]中
indexs++; //数组下标后移
USART1_Data[indexs]='\0'; //让接收到的最末尾字符的后面为'\0'
if((USART1_Data[0]!='o')&&(USART1_Data[0]!='c')){ /*如果串口助手发送过来的第一个字符既不是字母‘o’也不是字母‘c’,那么就没必要再进行下去了,数组USART1_Data[]的索引回到0*/
indexs=0;
}else{
if(indexs>3){ //open为4个字符,如果串口助手发送过来4个字符,那么有可能发送过来的是open
if(0==strcmp("open",(const char *)USART1_Data)){ //数组中的内容与open字符串进行对比
LED_CTRL(LEDR,LED_OPEN); //开灯
indexs=0;
memset(USART1_Data,0,sizeof(USART1_Data));
}
if(0==strcmp("close",(const char *)USART1_Data)){ //数组中的内容与close字符串进行对比
LED_CTRL(LEDR,LED_CLOSE); //关灯
indexs=0;
memset(USART1_Data,0,sizeof(USART1_Data));
}
}
}
if(indexs==5){ /*如果前面对比失败,说明串口助手发送过来的即不是open也不是close。又因为close为5个字符,串口助手发送过来的字符数量为5个,数组索引回归0*/
indexs=0;
memset(USART1_Data,0,sizeof(USART1_Data));
}
}
}
方法二:(空闲总线中断)
//串口中断服务函数
void USART1_IRQHandler(void)
{
if(SET==USART_GetITStatus(USART1,USART_IT_RXNE)){
USART_ClearITPendingBit(USART1,USART_IT_RXNE);
USART_SendData(USART1,USART_ReceiveData(USART1));
USART1_Data[indexs]=USART_ReceiveData(USART1);
if(USART1_Data[indexs]==0x0D || USART1_Data[indexs]==0x0A) /*0XOD是回车,0XOA是换行,串口调试助手会在每帧数据末尾,发回车+换行符,我们需要把它两个清除*/
USART1_Data[indexs] = '\0';
indexs++;
}
if(SET==USART_GetITStatus(USART1,USART_IT_IDLE)){ //空闲总线中断
USART_ReceiveData(USART1);//清除空闲总线标志位
if(0==strcasecmp("open",(const char *)USART1_Data)){
LED_CTRL(LEDR,LED_OPEN);//开灯
}
if(0==strcasecmp("close",(const char *)USART1_Data)){
LED_CTRL(LEDR,LED_CLOSE);//关灯
}
indexs = 0;
memset(USART1_Data,0,sizeof(USART1_Data));
}
}
我们可以看到方法二更加的方便和简洁。那么空闲总线中断是什么?
USART1,USART_IT_IDLE:空闲总线中断标志位,当MCU的UART接收完一帧数据后,该标志位就会置1。也就是,串口助手发送一串数据给MCU。MCU则是一个字节一个字节的接收。当接收完这一串数据后,过一小段时间,如果没有数据发送过来,那么空闲总线中断标志位就会自动置1,表示MCU将这一帧数据接收完成。
单片机串行通信里面的数据帧是怎么理解?一帧数据的位数可以改变吗?
串行通信中,帧信息一般是根据需要自己约定而确定的。其内容一般是由多个8位单字节数据组成,比如你所说的传感器,需要采集电压值,电流值等信息,假设这些信息需要10个字节,那么你的一帧信息最少需要10个字节,也就是收发两方都需要计数,计数到10时才能说明通讯完成。这是最简单的,但大多数应用中规范的做法一帧信息都会包含帧头标识符、帧长度、信息内容及校验信息。
注意:在中断配置中如果打开了中断源,那么一定要在中断服务函数中手动将中断标志位清除。否则,中断触发的时候,标志置1,当再次进入到中断后,由于上一次中断没有清除标志位,那么程序就可能会卡在中断服务函数中。