中断

在嵌入式开发中,中断(Interrupt)是一种机制,可以使处理器暂停当前执行的任务,去处理一个紧急或高优先级的事件。中断机制能帮助嵌入式系统快速响应外部或内部的事件,提升实时性能和效率。

基本概念

中断是由硬件软件事件触发的一种信号,用于打断处理器的当前流程,使其快速跳转到指定的 中断服务程序(ISR),以便处理特定事件。当 ISR 执行完毕,处理器会返回并继续执行被打断的任务。

中断的分类

中断大体可以分为以下几类:

  • 外部中断:由外部硬件触发,例如按钮、传感器、计数器等。这类中断通常用于捕获外部事件。
  • 内部中断:由处理器或外设内部事件触发,如定时器溢出、ADC 转换完成、串口数据接收等。
  • 软件中断:通过软件指令触发的中断,用于实现特定的软件功能或系统调用。

中断的工作原理

中断的基本工作流程如下:

  1. 中断请求:中断源(如外设或软件)产生中断请求信号。
  2. 中断响应:处理器停止当前任务,并保存当前状态(如程序计数器和寄存器等)。
  3. 执行 ISR:处理器跳转到对应的中断服务程序,执行中断处理代码。
  4. 恢复执行:ISR 执行完成后,处理器恢复之前保存的状态,继续执行被打断的任务。

中断优先级和嵌套

在多中断系统中,中断的优先级决定了响应的先后顺序。中断的优先级可以进一步细分为不同的类型,以便更好地满足系统实时性和复杂性的需求。常见的中断优先级类型包括响应式优先级抢占式优先级自然优先级

  • 响应式优先级:响应式中断优先级指的是,中断的处理根据固定优先级顺序进行,不支持中断的嵌套或抢占;在多个中断发生时,优先响应高优先级的中断,直到 ISR 完成才响应下一个中断。
  • 抢占式优先级:抢占式中断优先级允许高优先级中断打断低优先级中断的执行;
  • 自然优先级:自然优先级是一种基于中断向量表的优先级机制,自然优先级实际上是中断向量表的序号,序号越小优先级越高。

无论是抢占优先级还是响应优先级,都是用数值表示的,数值越小,优先级越高。

中断向量表

中断向量表存储了各个中断服务程序的入口地址。每个中断源都有一个固定的入口地址。当发生中断时,处理器通过中断向量表找到相应 ISR 的地址并跳转执行。

中断服务程序(ISR)

中断服务程序用于处理中断事件的具体逻辑。设计 ISR 时需要注意以下几点:

  • 简短高效:ISR 应该尽量简洁,避免长时间运行,减少对主程序的影响。
  • 避免阻塞操作:ISR 中不适合使用延时、复杂计算或等待操作,以避免影响系统的实时性。
  • 使用中断标志位:很多中断事件触发后会设置特定的标志位,ISR 中通常需要检查和清除标志位,以防止重复触发。

事件

事件是一种简化的触发机制,通常用于在不需要中断服务程序(ISR)的情况下唤醒处理器或触发某些外设的操作。事件不会打断当前的程序执行,也不会跳转到一个特定的中断服务程序。因此,事件的处理方式更加轻量,也没有优先级之分。

NVIC

NVIC(Nested Vectored Interrupt Controller,嵌套向量中断控制器)是 ARM Cortex-M 系列微控制器内核中的一个重要模块,负责管理和控制中断请求的响应和优先级处理。

优先级分组

优先级分组机制是一种在嵌入式系统中管理中断优先级的方式,它允许系统在中断的抢占优先级子优先级(响应式优先级)之间进行分配,以更灵活地控制中断响应顺序。ARM Cortex-M 系列处理器通过嵌套向量中断控制器(NVIC)提供了优先级分组机制,使得在处理多种中断时可以实现更高的系统实时性和响应能力。

在 Cortex-M 架构中,中断优先级通过嵌套式向量型中断控制器(NVIC)进行配置;在 Cortex-M4 中使用 4 位来设置优先级;常见分组如下:

优先级分组 抢占优先级位数 子优先级位数 优先级分组类型描述
0 4 0 全抢占优先级,没有子优先级
1 3 1 3 位抢占优先级,1 位子优先级
2 2 2 2 位抢占优先级,2 位子优先级
3 1 3 1 位抢占优先级,3 位子优先级
4 0 4 没有抢占优先级,全部为子优先级

举例:在 4 位抢占优先级、0 位子优先级的情况下,抢占优先级可以使用 0 到 15 之间的值进行设置;因为他占了四个字节。

回调函数

回调函数是一种将函数作为参数传递给另一个函数的编程技术。回调函数的作用是:允许一个函数在完成某些特定任务后,通过调用另一个“回调”函数来通知调用方任务完成,或执行特定的后续操作。

在中断处理中,我们不可以帮用户做决定,所以我们在中断中无法预知客户要做的事情是什么,这时候就可以使用回调函数;

中断

ARM Cortex-M 系列中断分为三类:

  • 系统异常(System Exceptions)
    • 内核自带的异常,例如复位(Reset)、不可屏蔽中断(NMI)、硬故障(Hard Fault)等。优先级较高,通常用于处理系统级的错误和状态。
  • 外部中断(External Interrupts)
    • 由外设(如 GPIO、UART、定时器等)产生的中断,通过 NVIC 进行管理。这些外部中断优先级由用户定义,响应外部事件,例如按钮按下、数据接收完成等。
  • 软件中断(Software Interrupts)
    • 由软件触发的中断,用于在代码中人为产生中断以实现特定功能,如任务调度或操作系统中的上下文切换。

EXTI(外部中断)

EXTI(External Interrupt/Event Controller,外部中断/事件控制器)主要作用是捕获外部中断/事件。

EXTI 负责检测和生成中断事件,而 NVIC 负责管理和调度中断的响应

EXTI 的结构与工作流程:

F407waibuzhonduanlc

  1. EXTI Line0~22
  • EXTI 控制器提供了多个外部中断线(Line0~Line22),这些线可以连接到外部设备或者 GPIO 引脚。
  • 每一条 EXTI 线可以设置为边沿检测,即检测上升沿或下降沿的信号变化。
  1. 极性控制
  • 用于设置信号的触发极性。可以选择触发上升沿、下降沿,或者两者都触发。
  • 极性控制模块通过设置不同的极性,控制边沿检测器检测信号变化的类型。
  1. 边沿检测
  • 边沿检测模块用于检测外部信号的变化。根据极性控制的设定,这里可以检测上升沿、下降沿,或者两者都检测。
  • 当检测到符合设定的边沿信号时,将触发后续的中断或事件。
  1. 软件触发
  • 软件触发模块允许通过编程来触发 EXTI 中断,而不依赖外部信号。
  • 通过软件触发,程序可以手动生成中断或事件,适用于某些特殊的控制场景。
  1. 中断屏蔽控制
  • 中断屏蔽控制模块用于控制 EXTI 信号是否生成中断。
  • 当中断屏蔽控制允许时,检测到的边沿信号将触发中断信号并传输给 NVIC。
  1. 事件屏蔽控制
  • 事件屏蔽控制模块用于控制 EXTI 信号是否生成事件。
  • 当事件屏蔽控制允许时,检测到的边沿信号将触发一个事件,用于系统内部的其他功能模块,如低功耗模式下的唤醒。
  1. 事件产生
  • 事件产生模块在满足边沿检测条件时产生事件。事件不同于中断,它不会直接引发 NVIC 的中断请求,而是用于系统的其他用途,比如唤醒单元等。
  1. 至 NVIC
  • 如果边沿检测模块检测到外部信号符合设定的条件,并且中断屏蔽控制允许,则通过 NVIC 触发中断服务程序。
  1. 至唤醒单元
  • 如果边沿检测满足条件且事件屏蔽控制允许,则可以触发事件产生,用于唤醒单元。
  • 该功能主要在低功耗模式下使用,用于处理待机或睡眠模式下的唤醒需求。

中断和事件是独立控制的,可以通过配置相应的寄存器来独立启用或屏蔽中断和事件。

  • 若只开启中断屏蔽而屏蔽事件,则只会产生中断,不会产生事件。
  • 若只开启事件屏蔽而屏蔽中断,则只会产生事件,不会产生中断。
  • 若同时开启中断和事件屏蔽,那么信号变化时会同时产生中断和事件。

EXTI 编号与触发源

F407EXTIchufayuan

NVIC

NVIC 是处理器核心中的一个模块,负责接收和管理中断请求。它可以将多个中断请求进行排序,按优先级分配中断,决定何时和以何种优先级响应中断。

中断向量表

F407NVICbiao

示例流程

假设一个按键连接到 GPIO 引脚,用来触发 EXTI 中断,流程如下:

  1. EXTI 监测按键状态,检测到按键按下,生成一个中断请求并将其发送到 NVIC。
  2. NVIC 检查优先级:如果该中断优先级足够高,NVIC 将立即响应此请求,暂停其他任务。
  3. 执行中断服务程序:系统进入按键中断服务程序(ISR)进行处理,如记录按键状态或执行相应的功能。
  4. 清除中断标志:在 ISR 中处理完毕后,清除 EXTI 的中断标志,以便下一次按键按下时可以重新触发中断。
  5. 恢复正常运行:NVIC 将控制权交还给主程序或其他正在运行的任务。s

SYSCFG

SYSCFG(系统配置控制器)是 ARM Cortex-M 微控制器(如 STM32 系列)中的一个重要外设,负责配置系统的各种功能和引导外设的资源管理。

外部中断配置

  • 引脚映射:SYSCFG 允许将外部 GPIO 引脚映射到 EXTI(外部中断)线。用户可以指定哪些引脚用于外部中断触发。
  • 中断触发条件:支持设置中断触发条件(上升沿、下降沿、双边沿),以满足不同应用的需求。

MISC

MISC 是对嵌套向量中断控制器(NVIC)和系统定时器(SysTick)操作的软件包。

实现

功能:通过外部中断监听按钮按下和抬起的动作

硬件触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
// GPIO 配置
void GPIO_EXTI_Config(void) {
//启用GPIOA外部时钟
rcu_periph_clock_enable(RCU_GPIOA);
// 配置GPIO工作模式为复用模式
gpio_mode_set(GPIOA, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN_0);
// 选择复用哪一个模式,这时候就需要去查看 中断不需要进行复用选择
//gpio_af_set(GPIOA, uint32_t alt_func_num, uint32_t pin);
}

void EXTI_Config(void) {

// 我们要使用外部中断功能,但是在端口复用配置中,却没有外部中断这个复用选项
// 所以我们需要用其他的方法将这个引脚映射到 EXTI 上
// 这里就要使用系统提供的配置工作:SYSCFG(系统配置控制器)

// 任何配置前我们先要配置时钟,我们用到的功能是 EXTI,但是 EXTI 的功能依赖于 SYSCFG 的配置
// 所以我们要先配置 SYSCFG 的时钟,我们通过 SYSCFG 将 GPIO 映射到 EXTI

// 启用RCU_SYSCFG外部时钟
rcu_periph_clock_enable(RCU_SYSCFG);
// 将引脚映射到 EXTI
syscfg_exti_line_config(EXTI_SOURCE_GPIOA, EXTI_SOURCE_PIN0);

// 中断处理需要用到 NVIC 所以还需要配置 NVIC
// 使能 NIVC 请求,是来自什么的中断处理,优先级是什么样的
nvic_irq_enable(EXTI0_IRQn, 2, 2);
// 配置 EXIT,那个外部中断寄存器、是中断还是事件、触发方式是什么
// 中断触发源
exti_init(EXTI_0, EXTI_INTERRUPT, EXTI_TRIG_BOTH);
// 使能外部中断
exti_interrupt_enable(EXTI_0);
}

// 编写外部中断处理函数
// 查找初始化文件,找到 EXTI0 的中断处理函数
void EXTI0_IRQHandler(void) {
if(SET == exti_interrupt_flag_get(EXTI_0)) {
exti_interrupt_flag_clear(EXTI_0);
printf("Hi,chulihanshu\n");
}
}

int main(void) {
systick_config();
GPIO_EXTI_Config();
EXTI_Config();
while(1){
}
}
============================================================================
// 回调函数
// main.c
// 这是用户要做的事情
void EXTI0_Callback(USART0_KEY_STATE state) {
if(state) {
printf("DOWN\n");
} else {
printf("UP\n");
}
}
// 将用户的操作作为参数传递过去
bsp_EXTI0_Callback(EXTI0_Callback);

// bsp_exti0.c
// 我们定义一个函数指针类型的变量,并初始化为 NULL;
void (*p_Callback)(USART0_KEY_STATE state) = NULL;
// 我们通过上面的这个变量接收用户传递过来的参数
void bsp_EXTI0_Callback(void (*p)(USART0_KEY_STATE state)) {

p_Callback = p;
}

//在中断处理函数里面调用客户传过来的函数
void EXTI0_IRQHandler(void) {
if(SET == exti_interrupt_flag_get(EXTI_0)) {
exti_interrupt_flag_clear(EXTI_0);
// 我们不去帮用户做决定,让用户自己决定要干什么
if(SET == gpio_input_bit_get(GPIOA, GPIO_PIN_0)) {

if(p_Callback != NULL) {
p_Callback(DOWN);
}
} else {
if(p_Callback != NULL) {

p_Callback(UP);
}
}
}
}
=========================================================================
// 软件消抖
//1. 延时,手动减少中断函数响应的次数
void EXTI0_IRQHandler(void) {
if(SET == exti_interrupt_flag_get(EXTI_0)) {
exti_interrupt_flag_clear(EXTI_0);
// 我们不去帮用户做决定,让用户自己决定要干什么
if(SET == gpio_input_bit_get(GPIOA, GPIO_PIN_0)) {

if(p_Callback != NULL) {
p_Callback(DOWN);
}
} else {
if(p_Callback != NULL) {

p_Callback(UP);
}
}
}
delay_1ms(50);
}
===============================================================
// 2.通过计数的方式来进行消抖
// 我们知道系统是通过滴答时钟来进行计数的
void delay_1ms(50)
{
delay = count * 1;

while(0U != delay) {
}
}
void delay_decrement(void) {
if(0U != delay) {
delay--;
}
}
// 通过上面这个不难看出,他的 50ms 记时是通过在 delay_decrement 中 delay-- 实现的;
// 也就是说 delay_decrement 这个函数调用一次就是 1ms,那我们可以通过这个函数的调用来计时
// 但是 ms 的时间太长了,我们需要 us 的时间,这里我们就需要将时间进行细化
// 我们在 main() 中通过 systick_config() 函数可以看到:
// SystemCoreClock 就是我们之前的外部高速晶振 8000000Hz
// 通过 SysTick_Config(SystemCoreClock / 1000U) 这个函数可以定时触发异常
// 这里我们看到它默认是 1000Hmz 也就是 1ms,
// 我们需要将其改为 1us 也就是除以 1000000u
void systick_config(void) {
/* setup systick timer for 1000Hz interrupts */
if(SysTick_Config(SystemCoreClock / 1000U)) {
/* capture error */
while(1) {
}
}
/* configure the systick handler priority */
NVIC_SetPriority(SysTick_IRQn, 0x00U);
}
// 改为=============================================
// 这样它就会在 1us 触发一次他的中断库函数 SysTick_Handler
void systick_config(void) {
/* setup systick timer for 1000Hz interrupts */
if(SysTick_Config(SystemCoreClock / 1000000U)) {
/* capture error */
while(1) {
}
}
/* configure the systick handler priority */
NVIC_SetPriority(SysTick_IRQn, 0x00U);
}
// 可以看到他的函数就是调用了 delay_decrement 也就是 1us 调用一次
void SysTick_Handler(void)
{
// led_spark();
delay_decrement();
}
//这时候我们就可以进行如下修改:
uint64_t delay = 0;
// 这样 cnt 就是 us
void delay_decrement(void) {
cnt ++;
if(0U != delay) {
delay--;
}
}
// 通过函数返回 us 的时间
uint64_t get_us() {
return cnt;
}
// 1us
void delay_1us(uint32_t count) {
delay = count * 1;

while(0U != delay) {
}
}
// 1ms
void delay_1ms(uint32_t count){
delay = count * 1000;

while(0U != delay) {
}
}
==============================================================
// 这样就可以将中断函数改为如下
// 该函数只会在 100us 触发一次
uint64_t pre_cnt 0;
void EXTI0_IRQHandler(void) {
if(SET == exti_interrupt_flag_get(EXTI_0)) {
// 只能放在上面,否则不管是放在 if里面还是后面都会产生 bug
exti_interrupt_flag_clear(EXTI_0);
// 100us 让他执行一次
// 函数不会阻塞,让他 100us 执行一次
// 按键物理抖动通常在 5ms 到 50ms 之间,
uint64_t cur_cnt = get_us();
if(cur_cnt - pre_cnt <= 100) {
return ;
}
pre_cnt = cur_cnt;

// 我们不去帮用户做决定,让用户自己决定要干什么
if(SET == gpio_input_bit_get(GPIOA, GPIO_PIN_0)) {

if(p_Callback != NULL) {
p_Callback(DOWN);
}
} else {
if(p_Callback != NULL) {

p_Callback(UP);
}
}
}
}

步骤:

  1. GPIO 的配置
  • 主要外部中断不在 GPIO的复用选项中,我们需要通过其他方式将 GPIO 映射到 EXIT。
  1. EXTI 配置
  • GPIO 配置完成后,查看用户手册了解所使用的引脚属于那个中断源
  • 使能 SYSCFG 外部时钟,将 GPIO 引脚映射到 EXTI
  • 配置 NVIC
  • 配置 EXIT,并使能
  1. 编写中断处理函数
  • NVIC 所对应的中断处理函数要到启动文件(.s)文件中去找
  • 在编写时候注意查看用户手册,了解我们所使用的的中断源是否需要手动清除;

软件触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include "gd32f4xx.h"
#include "systick.h"
#include <stdio.h>
#include "Usart0.h"

// 该函数在 USART 的中断处理函数中进行调用
void Usart0_recv(uint8_t *data, uint32_t len) {
printf("recv: %s\r\n", data);

exti_software_interrupt_enable(EXTI_0);
}

static void EXTI_config() {
uint32_t extix = EXTI_0; // 哪个中断
uint32_t extix_irq = EXTI0_IRQn;

/********************* EXTI config *********************/
// 时钟配置
rcu_periph_clock_enable(RCU_SYSCFG);
// 中断初始化 因为是软件触发所以触发源就不需要选择上升或者下降沿了
exti_init(extix, EXTI_INTERRUPT, EXTI_TRIG_NONE);
// 配置中断优先级
nvic_irq_enable(extix_irq, 1, 1);
// 使能中断
exti_interrupt_enable(extix);
// 清除中断标志位
exti_interrupt_flag_clear(extix);
}

// 中断函数
void EXTI0_IRQHandler(void) {
printf("IRQ \r\n");
if(SET == exti_interrupt_flag_get(EXTI_0)) {
printf("exti 0 \r\n");
// 清除中断标志位
exti_interrupt_flag_clear(EXTI_0);
}
}

// UART0 中断处理函数
void USART0_IRQHandler(void) {
// 有数据了
if(SET == usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE)) {
usart_interrupt_flag_clear(USART0, USART_INT_FLAG_RBNE);
int8_t data = usart_data_receive(USART0);
dataBuf[index] = data;
index++;
}

if(SET == usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE)) {
// 清除 LDLE 中断标志位
// 软件先读USART_STAT0,再读USART_DATA可清除该位。
// usart_interrupt_flag_get 读取的就是 USART_STAT0 寄存器中的标志位
// USART_STAT0 是状态寄存器,USART_DATA 为数据寄存器
usart_data_receive(USART0); // 读数据寄存器,以清除 LDLE 中断标志位
// 通过这个函数去调用 EXTI 的软件触发函数
Usart0_recv(dataBuf, index);
index = 0;
}
}

int main(void) {
systick_config();
Usart0_init();
EXTI_config();

while(1) {
}
}

问题

一·、在使用 EXTI 与 NVIC 时为什么不需要显示的开启外部时钟

因为这两个模块的工作原理和时钟管理与外部时钟的启用无关。

EXTI 是用于处理外部硬件事件(如按键、传感器、外部信号等)触发的中断。它本质上是一个由外部事件触发的信号,依赖于微控制器的输入引脚(如 GPIO)上的状态变化。

NVIC 本身并不依赖于外部时钟。NVIC 只是处理从不同外设或外部事件(如 EXTI)发出的中断请求。它的作用是管理中断的优先级、响应时间以及是否允许中断等,所有这些操作都基于内部时钟(通常是系统时钟或 APB 时钟)。所以,NVIC 与外部时钟并无直接关系。

二、通过计数消抖这时间如何确定

以波特率为 115200 bps 为例,假设数据帧格式为1 位起始位 + 8 位数据位 + 1 位停止位,总共 10 位

F407RTCfpjissf

也就是说我们延时的时间比这个大就可以了;

三、exti_interrupt_flag_clear(EXTI_0);,可能会产生的bug

当凑巧我们按下的时候 EXTI0_IRQHandler 中的条件 cur_cnt - pre_cnt <= 100 为真时,函数会直接 return,而 在返回前没有清除 EXTI 的中断标志位。在这种情况下,该中断标志位依然保持为 1,因此后续也不会再触发相同的中断。

因此我们应该在进入中断后第一时间清除标志位;