目录
本篇主要以串口中断来展开学习
一、什么是中断
1、中断机制执行过程
当CPU执行主程序时,接收到处理紧急事件的请求,CPU中断当前操作,去执行紧急事件(就是中断服务函数内的程序),执行完紧急事件后再返回到主程序继续执行。
2、中断的控制系统 NVIC
2.1 NVIC是什么
首先NVIC的全称是中断嵌套向量控制器,位于M4内核中,属于核心外设。NVIC的作用:监控紧急事件是否发生、判断紧急事件的优先级、调度内核执行紧急事件
2.2 NVIC内部结构
嵌套向量中断控制器 NVIC具有82个中断源
中断仲裁主要解决的问题是多个中断同时发生时,确定哪个中断应优先处理。中断仲裁的核心在于中断的优先级设置,高优先级的中断能够在低优先级的中断之前被处理,从而确保了系统对紧急情况的快速响应和处理。
STM32的中断仲裁机制包括以下几点:
中断优先级:STM32支持设置不同的中断优先级,允许用户根据需要调整各个中断的优先级。
中断嵌套:如果一个高优先级的中断发生,它会立即打断当前正在处理的中断(如果其优先级较低),并首先处理这个高优先级的中断,这就是所谓的中断嵌套。
中断执行流程:当中断发生时,STM32的执行流程包括外设发出中断请求、处理器暂停当前执行的任务、跳转到对应的中断服务程序(ISR)并执行、恢复现场后返回到被中断的位置继续执行等步骤。
2.3 中断协作模型
片上外设产生中断源,除了GPIO
3、中断服务函数跟平时调用的函数的区别
首先,中断服务函数不需要调用;其次,中断服务函数跟普通函数执行的程序点不同,普通函数需要CPU执行到此函数调用的位置才能执行相应的功能,执行程序的地点是固定的;而中断服务函数,无论CPU执行到哪里,只要有紧急事件发生就会中断当前操作去执行中断服务函数,执行程序的地点是随时,不固定的。
4、什么时候用中断
首先,需要有特定的条件触发紧急事件,并且紧急事件不需要一直运行能瞬间完成此事件;最后,还需要实时的响应,需要的时候触发,不需要的时候不触发
二、如何配置中断
1、中断优先级
优先级分类:
抢占优先级: 通过设置优先级寄存器设置对应的值 //程序设置
响应优先级: 通过设置优先级寄存器设置对应的值 //程序设置
自然优先级: 是固定,不能通过软件进行配置;
说明:优先级的级别值越小,优先级越高。
1.1 优先级设置
在CM4中的NVIC控制器,每一个中断源都分配了一个8位的寄存器来放置该中断的优先级(抢占、响应)
注意:在STM32中只用到其中的高四位,低四位为固定值。
高四位分为两部分,一部分用来设置中断源的抢占优先级,另一部分用来设置中断源的响应优先级
具体抢占优先级和响应优先级各占用多少位--->需要根据设置的优先级分组决定
1.2 优先级分组
在地址为0xE000ED0C这个寄存器的 10~8 位设置优先级分组(应用程序中断及复位控制 寄存器(AIRCR))
通过设置这个值来确定-->设置优先级寄存器的高四位中抢占优先级占几位,响应优先级占几位
精简ST芯片分组表:
写入值/分组号 占先位数 次级位数 占先取值范围 次级取值范围
3 4 0 0~15 0
4 3 1 0~7 0~1
5 2 2 0~3 0~3
6 1 3 0~1 0~7
7 0 4 0 0~15
串口中断过程:
2、中断配置函数
由于NVIC属于CM4内核级的外设,所有芯片厂家在寄存器上是完全相同的(配置方式,作用),所以ARM公司提供了一份通用的NVIC配置函数。
配置NVIC所需要的函数
NVIC_SetPriorityGrouping(uint32_t PriorityGroup);//通过对应的寄存器的 8~10 位写值(7 6 5 4 3---分组号)--->设置优先级分组
u32 pri = NVIC_EncodePriority (uint32_t PriorityGroup, uint32_t PreemptPriority, uint32_t SubPriority);//计算优先级编码值,设置抢占和响应的级别值
NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority);//设置具体某个中断源的优先级
NVIC_EnableIRQ(IRQn_Type IRQn);//NVIC模块响应片上外设中断源的开关--->使能NVIC响应通道
3、中断服务函数
进入中断服务函数的条件:
- 状态寄存器SR的第5位/第4位置1(接收中断与空闲中断)
- 中断使能打开
分析:
void 中断服务函数(void)
{
//紧急事件
}
由于状态寄存器SR的第5位/第4位一直置1,所以一直执行中断服务函数,故需要清除中断标志位(状态寄存器SR的第5位/第4位清0)
所以得到以下框架:
void 中断服务函数(void)
{
//清除中断标志位
//紧急事件
}
又由于每个中断源下面有多个中断信号可以触发,触发了都会进入同一个中断服务函数
但是每个中断信号触发执行紧急事件是不一样的,所以要进入中断服务函数后要判断到底是哪个中断信号触发
所以最终得到以下框架:
void 中断服务函数(void)
{
//判断某个中断信号触发
{
//清除中断标志位
//紧急事件
}
}
三、具体使用中断
需求1:串口接收字符控制蜂鸣器
流水灯运行,随时可以通过串口接收字符,控制蜂鸣器。O 开 N 关闭
分析:
打开接收中断,关闭空闲中断
/*
函数名: USART1_IRQHandler
函数功能:中断服务函数
返回值:void
形参:void
函数说明:
*/
u8 data;
void USART1_IRQHandler(void)
{
//是否产生接收中断
if(USART1->SR & (1 << 5))
{
//清除接收中断的标志位
USART1->SR &= ~(1 << 5);
//紧急事件
data = USART1->DR;
if(data == 'O')
{
LED_ON;
}
else if(data == 'N')
{
LED_OFF;
}
}
}
需求2: 串口接收字符串控制流水灯
流水灯运行,随时可以通过串口接收字符串,控制蜂鸣器。open 开 close 关
分析:
打开接收中断和空闲中断
中断服务函数:
NVIC.h
#ifndef _NVIC_H
#define _NVIC_H
#include "stm32f4xx.h"
typedef struct usart1_rec
{
u8 usart1_buff[50];//接收字符串
u8 usart1_idle_flag;//空闲中断接收字符串完毕标志位
u8 len;//数组索引值
}USART1_REC;
extern USART1_REC usart1_val;
#endif
NVIC.c
/*
函数名: USART1_IRQHandler
函数功能:中断服务函数
返回值:void
形参:void
函数说明:
*/
USART1_REC usart1_val;//接收数据结构体变量
void USART1_IRQHandler(void)
{
//是否产生接收中断
if(USART1->SR & (1 << 5))
{
//清除接收中断的标志位
USART1->SR &= ~(1 << 5);
//紧急事件 以一字节的方式接受,接收完后就到下一次中断
usart1_val.usart1_buff[usart1_val.len] = USART1->DR;
usart1_val.len++;
}
//是否产生空闲中断
if(USART1->SR & (1 << 4))
{
//清除空闲标志位
USART1->SR;
USART1->DR;
//紧急事件
usart1_val.usart1_buff[usart1_val.len] = '\0';
usart1_val.len = 0;
//字符串接收完毕
usart1_val.usart1_idle_flag = 1;
}
}
主函数:
u8 led_flag = 0;
u8 speed = 5;
while(1)
{
//字符串接收完毕
if(usart1_val.usart1_idle_flag == 1)
{
usart1_val.usart1_idle_flag = 0;//为下次再次接收字符串
if(strcmp((const char*)usart1_val.usart1_buff,"Open") == 0) //存在BUG Open123也会亮 解决方案:空闲中断内设置标志位
{
led_flag = 1;
}
else if(strcmp((const char*)usart1_val.usart1_buff,"Close") == 0)
{
led_flag = 0;
LED_OFF;
}
}
if(led_flag == 1)
{
LED_flash(speed);
}
}
为什么要在空闲中断中设置接收字符串完成标志位?
为了防止接收到其他字符也会打开或关闭流水灯。比如:发送open123流水灯一定概率能运行,为什么?因为每进一次中断接收一个字符,直到接收到open后,又因为全局变量在open后面添加‘\0’从而形成了一个完整的字符串,所以还没接收到后面的“123”,流水灯就能运行了。这是个小BUG,只要加在空闲中断中设置接收字符串完成标志位就能解决了
需求3:PC通过串口发送命令控制流水灯的开关和调速
open 开
close 关
sp1~9
按键1控制蜂鸣器开关
分析:
打开接收中断和空闲中断
//字符串接收完毕
if(usart1_val.usart1_idle_flag == 1)
{
usart1_val.usart1_idle_flag = 0;//为下次再次接收字符串
if(strcmp((const char*)usart1_val.usart1_buff,"Open") == 0)
{
led_flag = 1;
}
else if(strcmp((const char*)usart1_val.usart1_buff,"Close") == 0)
{
led_flag = 0;
LED_OFF;
}
//方法1:strcmp函数先把SP这两个字符限制住,再比较字符1~9,最后再限制住字符串的
长度从而达到除了SP1~9以外的也能调速的Bug
else if(strcmp((char *)usart1_val.usart1_buff,"SP1") >= 0 && strcmp((char *)usart1_val.usart1_buff,"SP9") <= 0 && strlen((char *)usart1_val.usart1_buff) == 3 )
{
speed = usart1_val.usart1_buff[2] - 48;
}
//方法2:strncmp函数
// else if(strlen((char *)usart1_val.usart1_buff) == 3 && strncmp((char *)usart1_val.usart1_buff,"SP",2) == 0)//限制长度是为了防止除了SP1~9以外的也能调速
// {
// if(usart1_val.usart1_buff[2] >= '1' && usart1_val.usart1_buff[2] <= '9')
// {
// speed = usart1_val.usart1_buff[2] - 48;
// }
// }
}
if(led_flag == 1)
{
LED_flash(speed);
}
}
需求4:开发板1控制开发板2的流水灯
按键1 板2的流水灯开
按键2 板2的流水灯关
按键3 板2的流水灯加速
按键4 板2的流水灯减速
分析:
打开接收中断,关闭空闲中断
用USART2通信
PA2-------Tx
PA3-------Rx
#ifdef Send //发送板
Keynum = Key_scan();
if(Keynum != 0xff)
{
Usart2_sendbyte(Keynum);
}
#else //接收板
switch(usart2_recdata)
{
case 1:flag = 1;break;//按键1打开流水灯
case 2:flag = 0;LED_OFF;break;//按键2关闭流水灯
case 3:if(flag){usart2_recdata = 0;speed -= 1;if(speed < 1) speed = 5;}break;//按键3加速 usart2_recdata = 0为了只执行一次这个选择
case 4:if(flag){usart2_recdata = 0;speed += 1;if(speed > 10) speed = 5;}break;//按键4减速
}
if(flag == 1)
{
LED_flash(speed);
}
#endif
注意:需要设定接收字符完成标志位,要不然速度一减到底或者一加到顶
需求5:PC通过串口发送命令控制两个板子的流水灯
PC发送open_1 板1的流水灯运行
PC发送open_2 板2的流水灯运行
PC发送close_1 板1的流水灯关闭
PC发送close_2 板2的流水灯关闭
分析:
打开接收中断和空闲中断
PC通过串口1发送给板1,如果是所需的字符串则实现相应的功能,如果不是则板1通过串口2发给板2;板2用串口2接收到字符串,如果是所需的字符串则实现相应的功能,如果不是则不实现相应的功能
while(1)
{
#ifdef send
//发送板
if(usart1_val.usart_idle_flag == 1)
{
usart1_val.usart_idle_flag = 0;
if(strcmp((const char *)usart1_val.usart_buff ,"open_1") == 0)
{
led_flag = 1;
}
else if(strcmp((const char *)usart1_val.usart_buff ,"close_1") == 0)
{
led_flag = 0;
LED_OFF;
}
else
{
Usart2_sendstr(usart1_val.usart_buff);
}
}
#else
if(usart2_val.usart_idle_flag == 1)
{
usart2_val.usart_idle_flag = 0;
if(strcmp((const char *)usart2_val.usart_buff ,"open_2") == 0)
{
led_flag = 1;
}
else if(strcmp((const char *)usart2_val.usart_buff ,"close_2") == 0)
{
led_flag = 0;
LED_OFF;
}
}
#endif
if(led_flag)
{
LED_flash(1);
}
}
总结:字符只需要用到接收中断,别打开空闲中断
字符串需要用到接收中断和空闲中断(添加个’\0’为了形成一个完整的字符串)
在使用之前想清楚需要打开哪个串口的接收和空闲中断