外部中断实验
% \7 T: o6 a* [$ a" ?2 i本章我们将介绍如何把STM32H7的IO口作为外部中断输入来使用,在本章中,我们将以中断的方式,实现我们在第十五章所实现的功能。
5 _$ D. Z5 }" x1 ?7 r
. I9 z! P9 n$ n* v& ~16.1 NVIC和EXTI简介
0 @% Z: a6 Y$ g16.1.1 NVIC简介
1 @! T0 x' w2 J2 q- ~+ E0 p什么是NVIC?NVIC即嵌套向量中断控制器,全称Nested vectored interrupt controller。它是内核的器件,所以它的更多描述可以看内核有关的资料。M3/M4/M7内核都是支持256个中断,其中包含了16个系统中断和240个外部中断,并且具有256级的可编程中断设置。然而芯片厂商一般不会把内核的这些资源全部用完,如STM32H750的系统中断有10个,外部中断有150个。这个在讲解启动文件的时候有提到过,可以回顾一下。下面我们看看系统中断部分:
& q6 a/ ]+ v, X6 p0 G0 N7 P" ]. t7 W9 h, }* |
! F$ o* {6 m; N( _4 _( Z0 B! K; c* v5 g% \, y8 {4 c8 k
表16.1.1.1 中断向量表-系统中断部分
: a% q0 |+ t" r) e/ f关于150个外部中断部分在《STM32H7xx参考手册_V3(中文版).pdf》的9.1.2小节有详细的列表,这里就不列出来了。STM32H750的中断向量表在stm32h750xx.h文件中被定义。* k9 K/ c8 u1 T" i7 F% g5 C6 R" |
16.1.1.1 NVIC寄存器
& W( X. M9 O. d8 ^NVIC相关的寄存器定义可以在core_cm7.h文件中找到,还可以在《STM32H7编程手册.pdf》的4.2小节找到其描述。我们直接通过程序的定义来分析NVIC相关的寄存器,其定义如下:
3 x7 } M' ?9 |' a" ]- }% i5 }7 }# w$ k
- typedef struct% M3 l5 i9 H& o Q
- {& C" e. r8 ~; i$ U
- __IOM uint32_t ISER[8U]; /* 中断使能寄存器 */
, _) n+ ^7 A$ `8 n - uint32_t RESERVED0[24U];- c `* y- H1 j; \+ P
- __IOM uint32_t ICER[8U]; /* 中断除能寄存器 */2 i1 X e- R* j5 `
- uint32_t RSERVED1[24U];1 W1 s' e- S- {5 B2 f2 y3 t8 @& D
- __IOM uint32_t ISPR[8U]; /* 中断使能挂起寄存器 */, V2 u: Q; a5 B# Y L3 f# E
- uint32_t RESERVED2[24U];
9 R' e, \& V3 {- T! ~( g8 y3 Y - __IOM uint32_t ICPR[8U]; /* 中断解挂寄存器 */0 I6 l. t' r/ K0 V# f
- uint32_t RESERVED3[24U];* k2 B3 j: c) U+ x' o5 \7 W
- __IOM uint32_t IABR[8U]; /* 中断有效位寄存器 */
" Y( n L; [+ w a( I4 m - uint32_t RESERVED4[56U];
4 ^) k" L* N% ^8 e$ D - __IOM uint8_t IP[240U]; /* 中断优先级寄存器(8Bit 位宽) */8 C- C0 @. C5 Q: G$ ], H: x( F
- uint32_t RESERVED5[644U];
# g8 r7 N0 L; i, I - __OM uint32_t STIR; /* 中断触发中断寄存器 */
' O; h1 y! V2 W4 w h - } NVIC_Type;
" A2 B; |# D) U8 s4 p ]7 x
复制代码 2 d$ L5 u: b( C, J% o0 F9 E
STM32H750的中断在这些寄存器的控制下有序的执行的。只有了解这些中断寄存器,才能方便的使用STM32H750的中断。下面重点介绍这几个寄存器:
8 V: f/ Y9 Y4 SISER[8]:ISER全称是:Interrupt Set Enable Registers,这是一个中断使能寄存器组。上面说了CM7内核支持256个中断,这里用8个32位寄存器来控制,每个位控制一个中断。但是STM32H750的可屏蔽中断最多只有150个,所以对我们来说,有用的就是四个(ISER[04]]),总共可以表示160个中断。而STM32H750只用了其中的150个。ISER[0]的bit031分别对应中断031;ISER[1]的bit031对应中断32~63;其他以此类推,这样总共150个中断就可以分别对应上了。你要使能某个中断,必须设置相应的ISER位为1,使该中断被使能(这里仅仅是使能,还要配合中断分组、屏蔽、IO口映射等设置才算是一个完整的中断设置)。具体每一位对应哪个中断,请参考STM32H750xx.h里面的第62行到201行,共150个。" c" k& o- t7 \9 I
ICER[8]:全称是:Interrupt Clear Enable Registers,是一个中断除能寄存器组。该寄存器组与ISER的作用恰好相反,是用来清除某个中断的使能的。其对应位的功能,也和ICER一样。这里要专门设置一个ICER来清除中断位,而不是向ISER写0来清除,是因为NVIC的这些寄存器都是写1有效的,写0是无效的。- [' m: X8 j3 n, K' S$ u, N* Q9 C, i
ISPR[8]:全称是:Interrupt Set Pending Registers,是一个中断使能挂起控制寄存器组。每个位对应的中断和ISER是一样的。通过置1,可以将正在进行的中断挂起,而执行同级或更高级别的中断。写0是无效的。
[6 i7 D, I+ xICPR[8]:全称是:Interrupt Clear Pending Registers,是一个中断解挂控制寄存器组。其作用与ISPR相反,对应位也和ISER是一样的。通过设置1,可以将挂起的中断解挂。写0无效。6 W& q9 o2 x+ Q" Q+ b# O
IABR[8]:全称是:Interrupt Active Bit Registers,是一个中断激活标志位寄存器组。对应位所代表的中断和ISER一样,如果为1,则表示该位所对应的中断正在被执行。这是一个只读寄存器,通过它可以知道当前在执行的中断是哪一个。在中断执行完了由硬件自动清零。 I i9 ]* o2 V/ ?2 L ]8 G4 l6 l
IP[240]:全称是:Interrupt Priority Registers,是一个中断优先级控制的寄存器组。这个寄存器组相当重要!STM32H750的中断分组与这个寄存器组密切相关。IP寄存器组由240个8bit的寄存器组成,每个可屏蔽中断占用8bit,这样总共可以表示240个可屏蔽中断。而STM32H750只用到了其中的150个。IP[149]IP[0]分别对应中断1490。而每个可屏蔽中断占用的8bit并没有全部使用,而是只用了高4位。这4位,又分为抢占优先级和子优先级。抢占优先级在前,子优先级在后。而这两个优先级各占几个位又要根据SCB->AIRCR中的中断分组设置来决定。关于中断优先级控制的寄存器组我们下面详细讲解。* R& Q: c' g, C' J: @9 D
, d' t/ O" X( d; P' N6 ]+ I
16.1.1.2 中断优先级
8 Y! w7 \! y, }8 D O/ N: X# TSTM32中的中断优先级可以分为:抢占式优先级和响应优先级,响应优先级也称子优先级,每个中断源都需要被指定这两种优先级。抢占式优先级和响应优先级的区别:
4 h0 t7 H7 y1 k7 W5 w' m抢占优先级:抢占优先级高的中断可以打断正在执行的抢占优先级低的中断。
8 z- p. N4 Y6 f0 ?9 e8 o响应优先级:抢占优先级相同,响应优先级高的中断不能打断响应优先级低的中断。 C+ Y# Z z' J; F2 \' `! L% ]
还有一种情况就是当两个或者多个中断的抢占式优先级和响应优先级相同时,那么就遵循自然优先级,看中断向量表的中断排序,数值越小,优先级越高。& v" V# G* O, C7 r5 s4 K' G* V
在NVIC中由寄存器NVIC_IPR0-NVIC_IPR59共60个寄存器控制中断优先级,每个寄存器的每8位又分为一组,可以分4组,所以就有了240组宽度为8bit的中断优先级控制寄存器,原则上每个外部中断可配置的优先级为0~255,数值越小,优先级越高。但是实际上M3 /M4 /M7 芯片为了精简设计,只使用了高四位[7:4],低四位取零,这样以至于最多只有16级中断嵌套,即2^4=16。
* g$ N: |$ }! B* b4 ^9 A对于NVCI的中断优先级分组:STM32H750将中断分为5个组,组04。该分组的设置是由SCB->AIRCR寄存器的bit108来定义的。具体的分配关系如表16.1.1.2.1所示:: d' V {' d' s; O
' r3 Q5 J" T8 i
( w) X6 u; [! j" F. ~: W
" W! ?) T/ y" P4 N8 W; S8 ]表16.1.1.2.1 AIRCR中断分组设置表0 `* l. m( p; c6 v) m5 P+ k# e3 F
通过这个表,我们就可以清楚的看到组04对应的配置关系,例如优先级分组设置为3,那么此时所有的150个中断,每个中断的中断优先寄存器的高四位中的最高3位是抢占优先级,低1位是响应优先级。每个中断,你可以设置抢占优先级为07,响应优先级为1或0。抢占优先级的级别高于响应优先级。而数值越小所代表的优先级就越高。0 P G3 n0 D. b9 l8 p, V6 L
结合实例说明一下:假定设置中断优先级分组为2,然后设置中断3(RTC_WKUP中断)的抢占优先级为2,响应优先级为1。中断6(外部中断0)的抢占优先级为3,响应优先级为0。中断7(外部中断1)的抢占优先级为2,响应优先级为0。那么这3个中断的优先级顺序为:中断7>中断3>中断6。
) @ P5 b; `" a上面例子中的中断3和中断7都可以打断中断6的中断。而中断7和中断3却不可以相互打断!
& e+ v# N; \2 }) y$ M
, o0 J) C( L& W+ [9 }16.1.1.3 NVIC相关函数( d3 ^ K8 T# A2 |2 U
ST公司把core_cm7.h文件的NVIC相关函数封装到stm32h7xx_hal_cortex.c文件中,下面列出我们较为常用的函数进行,想了解更多其他的函数请自行查阅。
3 ?* W) K7 i+ S1 A1.HAL_NVIC_SetPriorityGrouping函数
1 r; v7 _ ~* }: J# WHAL_NVIC_SetPriorityGrouping是设置中断优先级分组函数。其声明如下: _! k# G2 q3 w
void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup);% g$ d. m* L; { U& k
函数描述:
; e% [1 @( q' r用于设置中断优先级分组。
4 S) f0 B" J/ d- o# `/ {- p函数形参:
6 I0 s# o* L3 H- D0 _! c形参1是中断优先级分组号,可以选择范围:NVIC_PRIORITYGROUP_0到+ M1 X' Y/ ?4 {5 E
NVIC_PRIORITYGROUP_4(共5组)。
0 x, M" a+ u0 p) {% ]" J) s7 h函数返回值:无
% x5 `7 W; l% W1 C' J* u注意事项:
. f0 O/ G( {' K这个函数在一个工程里基本只调用一次,而且是在程序HAL库初始化函数里面已经被调用,后续就不会再调用了。因为当后续调用设置成不同的中断优先级分组时,有可能造成前面设置好的抢占优先级和响应优先级不匹配。0 A z9 Z Z* D+ k+ g/ _
2.HAL_NVIC_SetPriority函数4 l# ^" T' w- F* k$ B k
HAL_NVIC_SetPriority是设置中断优先级函数。其声明如下:
?8 X, r; _0 o1 [2 Zvoid HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority,
' |2 g4 J; s$ f6 Uuint32_t SubPriority);1 M( |% f' l6 m+ L Q: `
函数描述:# V3 U; S2 X" w+ }$ [9 z+ G
用于设置中断的抢占优先级和响应优先级(子优先级)。
e" _4 c/ R5 f函数形参:/ t; t3 F0 \" T2 p) J4 @. y) T
形参1是中断号,可以选择范围:IRQn_Type定义的枚举类型,定义在stm32h750xx.h。
8 N; D' p5 a2 a形参2是抢占优先级,可以选择范围:0到15。" X* s8 A# B9 U
形参3是响应优先级,可以选择范围:0到15。0 g, F7 n% i& X
函数返回值:无" [: {2 b- I" B r
3.HAL_NVIC_EnableIRQ函数, U. L& R- y0 b
HAL_NVIC_EnableIRQ是中断使能函数。其声明如下:
4 \9 Z3 \6 ?$ u8 `$ ~' S5 gvoid HAL_NVIC_EnableIRQ(IRQn_Type IRQn);
8 ]3 ]' G; V) H$ w& [4 b函数描述:6 W" [' O3 \ \' j; l" a/ K
用于使能中断。
5 r, [; t1 j Q% {3 }) S; S函数形参:
/ M1 S: [, @; ?: F6 A( Q形参1是中断号,可以选择范围:IRQn_Type定义的枚举类型,定义在stm32h750xx.h。1 x r/ h$ s" y. H v" G( w
函数返回值:无
% h& o& n; E3 C1 a/ p4 V! y3 i: J- I. h4.HAL_NVIC_DisableIRQ函数
8 u$ t: U! P9 `' z$ X Z0 NHAL_NVIC_DisableIRQ是中断除能函数。其声明如下:0 h" r1 `# @; ]& A
void HAL_NVIC_disableIRQ(IRQn_Type IRQn);: O3 `* W, A# H# _7 P
函数描述:
( A) C: J3 S1 A) R; m" {; x用于中断除能。7 H7 o9 Y8 z( A" `( T; m
函数形参:无形参# J* g' |9 T; J p) N
函数返回值:无) R6 V* W& r! z4 A, d
5.HAL_NVIC_SystemReset函数+ |. \* G. x7 k8 t. f& ?" W: @
HAL_NVIC_SystemReset是系统复位函数。其声明如下:6 Z! r: g4 F0 o5 @& B" l! [
void HAL_NVIC_SystemReset(void);/ k) C, c2 _# t' d5 w( Q
函数描述:
' i2 I' `+ G3 d" }" y# \7 a用于软件复位系统。
2 t- }4 |" B* U9 e: h# t函数形参:无形参9 |( I) f- ]' I: p$ \
函数返回值:无% W4 g. O6 D2 N e3 I! ^. R4 I
其他的NVIC函数用得较少,我们就不一一列出来了。NVIC的介绍就到这,下面介绍外部中断。
$ s. }& n* m! l/ l+ V% b; f
$ I/ q0 j$ t' ?. l3 J. [/ A16.1.2 EXTI简介0 V& \5 N( E. _8 y; n
EXTI即是外部中断和事件控制器,它由三个部分组成:APB接口访问的寄存器模块、事件输入触发模块和屏蔽模块。其中寄存器块包含了所有EXTI寄存器,事件输入触发模块提供事件输入边沿触发逻辑,屏蔽模块为不同的唤醒、中断和事件输出及其屏蔽功能提供事件分配。EXTI的功能框图如下图所示:
* j9 N8 ~, R9 |* l% c9 D9 ]1 ^( N1 l; I5 d
, n) q9 p% L3 {
( N; n/ `* X8 Y& x0 L8 R! u- T
图16.1.2.1 EXTI功能框图
4 B. h) [9 a! V6 o- z由外设对EXTI产生的事件分为可配置事件和直接事件。
+ [1 K! `0 p- m: p- R可配置事件是来自能够生成脉冲的 IO 或外设的信号,这类事件具有多种特性:可选择的有效触发边沿、中断挂起状态寄存器位、单独的中断和事件生成屏蔽、支持软件触发、可配置系统 D3 域唤醒事件。+ \1 g. C1 n9 Z9 R4 W
直接事件是来自其它外设的中断和唤醒源,需要在外设中清除,这类事件具备特性:固定上升沿有效触发、EXTI 中无中断挂起状态寄存器位、单独的中断和实际生产屏蔽、不支持软件触发、直接系统 D3 域唤醒事件包含一个D3域挂起屏蔽和状态寄存器。
3 h' J3 W0 B, U9 ^EXTI支持88个中断/事件请求,即包括可配置事件和直接事件。每个中断设有状态位,每个中断/事件都有独立的屏蔽设置,其中26个可配置的输入事件。前23个中断/事件如下表:1 g$ d' ]& \2 u8 O
" @0 @- U$ r% r2 G' B; |
6 C' o& N" z# L* F: S, N+ V& q4 Z% V; e+ v
表16.1.2.1 EXTI事件输入映射
1 ?; e4 A& B+ D7 O/ f, C由于表格太长,其余的请大家参考《STM32H7xx参考手册_V3(中文版).pdf》20.4小节的EXTI事件输入映射表格。$ k3 {- C$ P0 |5 k& m4 E
从上表可以看出,普通的 GPIO 口事件输入,属于可配置事件,唤醒目标为任意,比如 CPU NVIC 中断控制器,或者连接至 D3 低功耗域,产生唤醒中断。
% m0 u3 `5 H7 O- ` P% m, \事件输入0-15对应外部IO口的输入中断,一共是16个外部中断线。STM32H7供IO口使用的中断线只有16个,但是STM32H7的IO口却远远不止16个,那么STM32H7是怎么把16个中断线和IO口一一对应起来的呢?于是STM32就这样设计,GPIO的引脚GPIOx.0 ~ GPIOx.15(x=A,B,C,D,E,F,G,H,I,J,K)分别对应中断线0~15。这样每个中断线对应了最多11个IO口,以线0为例:它对应了GPIOA.0、GPIOB.0、GPIOC.0、GPIOD.0、GPIOE.0、GPIOF.0、GPIOG.0、GPIOH.0、GPIOI.0、GPIOJ.0、GPIOK.0。而中断线每次只能连接到1个IO口上,这样就需要通过配置来决定对应的中断线配置到哪个GPIO上了。
% _$ y6 a2 T7 J V: Z) T8 SGPIO和中断线映射关系是在寄存器SYSCFG_EXTICR1 ~ SYSCFG_EXTICR4中配置的。
, D. N1 R! z) H0 ]6 e
5 |3 ^: J8 o; Y- J4 w$ Y
5 u" K! D; o9 U
2 O) L( z4 r9 [ h; u图16.1.2.2 SYSCFG_EXTICR1寄存器3 i) |- f3 A5 {
SYSCFG_EXTICR1寄存器配置EXTI0到EXTI3线,包含的外部中断的引脚包括PAx到PKx,x=0到3。SYSCFG_EXTICR2寄存器配置EXTI4到EXTI7线,包含的外部中断的引脚包括PAx到PGx,x=4到7,SYSCFG_EXTICR2寄存器请打开参考手册查看(这里没有截图出来)。SYSCFG_EXTICR3和SYSCFG_EXTICR4以此类推。
$ X( H+ [' Z) T/ q* k. ~4 ~" Y另外要注意的是,我们配置SYSCFG相关寄存器前,还需要打开SYSCFG时钟。
1 g- }, w+ o2 [5 G# G3 j4 W7 c/ C# M; I* e' K
16.2 硬件设计
: f& }) D+ k6 L' X2 w1.例程功能
4 V* b; [0 t7 |' F% T/ H通过外部中断的方式让开发板上的三个独立按键控制RGB灯:KEY_UP控制LED0翻转,KEY1控制LED1翻转,KEY0控制LED2翻转。' ~3 @2 F3 r" K# M; e
2.硬件资源: ]6 r. I# J- Q% u- O
1)RGB灯: @8 @# p# o# |3 n" ^
RED :LED0 - PB4
8 l- L( G9 |5 o" h; K4 X0 wGREEN :LED1 - PE6
; T: K8 u% N6 I2 _& S% VBLUE :LED2 - PE5
' U3 m! L8 Y4 s( n2)独立按键
2 V+ b& A0 a6 G/ SKEY0 - PA1
1 k v) r: N, m& {6 U! O# lKEY1 - PA15
8 A( S/ W I/ `0 y% S/ ^7 bWK_UP - PA09 J2 _4 N8 v2 S
3.原理图; M+ t' }- R0 \( T1 H
独立按键硬件部分的原理图,如图16.2.1所示:8 ?3 D3 f! p: Z7 q/ N9 {) O
# i& W. R* ]8 D" J9 X& v( g0 S
$ S; T7 u. D8 k( y/ v
$ M# a7 I0 L$ x图16.2.1 独立按键与STM32H7连接原理图
. z: s* U9 K5 c! G& B这里需要注意的是:KEY0和KEY1是低电平有效的,而KEY_UP是高电平有效的,并且外部都没有上下拉电阻,所以,需要在STM32H750内部设置上下拉。
+ V9 w. s" Z t+ h
; A- k, }# z" R% C- W16.3 程序设计5 i6 K9 l; j& z* }. v
16.3.1 EXTI的HAL库驱动
6 k, g& v, H) P5 u8 `4 ^2 |前面讲解HAL_GPIO_Init函数的时候有提到过:HAL库的EXTI外部中断的设置功能整合到HAL_GPIO_Init函数里面,而不是单独独立一个文件。所以我们的外部中断的初始化函数也是用HAL_GPIO_Init函数。9 H1 e) G! n5 P- p
既然是要用到外部中断,所以我们的GPIO的模式要从下面的三个模式中选中一个:0 [6 }, T# E4 n! x
% L! Z& `6 ?5 _/ F- #define GPIO_MODE_IT_RISING (0x11110000U) /* 外部中断,上升沿触发检测 */* V, w/ B ?; J6 Q
- #define GPIO_MODE_IT_FALLING (0x11210000U) /* 外部中断,下降沿触发检测 */" w" h5 P7 o6 A' p
- /* 外部中断,上升和下降双沿触发检测 */
6 A& T+ _3 R% I& ^* S - #define GPIO_MODE_IT_RISING_FALLING (0x11310000U)
复制代码
4 g X) q. f* e* A+ C& b& ]KEY0和KEY1是低电平有效的,所以我们要选择下降沿触发检测,而KEY_UP是高电平有效的,那么就应该选择上升沿触发检测。
: Z7 \. L5 L# `+ S& NEXTI外部中断配置步骤
6 l6 X/ _% y% N% X" Z( g3 M1)使能IO口时钟。
A# F* r/ Y* V本实验用到的GPIO和按键输入实验是一样的,因此GPIO时钟使能也是一样的,请参考上一章代码。8 [5 D4 Q( \, ?5 e6 |0 K
2)设置IO口模式,触发条件,开启SYSCFG时钟,设置IO口与中断线的映射关系。- ~3 L' m* o. ]4 e: X9 j4 J
这些步骤HAL库全部封装在HAL_GPIO_Init函数里面,我们只需要设置好对应的参数,再调用HAL_GPIO_Init函数即可完成配置。
9 l/ p: d) A- `$ p) v; e V! O3)配置中断优先级(NVIC),并使能中断。
& a' C- |# C; L+ P8 C配置好GPIO模式以后,我们需要设置中断优先级和使能中断,中断优先级我们使用HAL_NVIC_SetPriority函数设置,中断使能我们使用HAL_NVIC_EnableIRQ函数设置。
: w0 c, Q9 T; N# V& O4)编写中断服务函数。
7 e. t/ { p4 p; X每开启一个中断,就必须编写其对应的中断服务函数,否则将导致死机(CPU将找不到中断服务函数)。中断服务函数接口厂家已经在startup_stm32h750xx.s中做好了。STM32H7的IO口外部中断函数只有7个,分别为:6 Y! |9 e8 H. Y3 T3 @1 q1 ?
) G9 G; V* @. G4 K! H
- void EXTI0_IRQHandler();
8 [; F/ G0 k7 {0 c% A5 | - void EXTI1_IRQHandler(); 2 A. g l8 R' M+ t$ O/ x
- void EXTI2_IRQHandler(); ' S) k e9 ]9 N* V0 F& J
- void EXTI3_IRQHandler();
# X, H5 z5 w6 G4 L6 l; [! R - void EXTI4_IRQHandler();
8 I* u) O% Q+ t d% a; Z' n' L4 X - void EXTI9_5_IRQHandler();
" v$ x9 f* P* q. F - void EXTI15_10_IRQHandler();
复制代码 , _& |6 V. l: d# v s0 {& r& H
中断线0-4,每个中断线对应一个中断函数,中断线5-9共用中断函数EXTI9_5_IRQHandler,中断线10-15共用中断函数EXTI15_10_IRQHandler。一般情况下,我们可以把中断控制逻辑直接编写在中断服务函数中,但是HAL库把中断处理过程进行了简单封装,请看下面步骤5讲解。
" s1 b+ w; H" O, `5)编写中断处理回调函数HAL_GPIO_EXTI_Callback3 } j. I5 S6 \7 B
HAL库为了用户使用方便,提供了一个中断通用入口函数HAL_GPIO_EXTI_IRQHandler,在该函数内部直接调用回调函数HAL_GPIO_EXTI_Callback。5 f3 E# z: A+ J* d
我们先来看一下HAL_GPIO_EXTI_IRQHandler函数定义:7 L2 V: {( ]' y8 T0 I: q
- ?' U+ y6 \% V9 w* K
- void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
5 K, J+ t4 [ E3 R) W. E - {
6 L. E7 i: i: A - if(__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00U)
( \4 p, H: ]% ]" W7 B1 y: ^4 Y0 E2 H% k - {' U' t; h1 [" d' z/ m6 z
- __HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin); /* 清中断标志位 */
" n6 Z3 ^. a7 C - HAL_GPIO_EXTI_Callback(GPIO_Pin); /* 外部中断回调函数 */
/ Z+ j' p' y5 r3 X' A - }$ T8 f, F/ K7 ^ g
- }
复制代码 ! V; W. a- T; {2 j* x$ i5 I1 J! Y) ?
该函数实现的作用非常简单,通过入口参数GPIO_Pin判断中断来自哪个IO口,然后清除相应的中断标志位,最后调用回调函数HAL_GPIO_EXTI_Callback()实现控制逻辑。在所有的外部中断服务函数中直接调用外部中断共用处理函数HAL_GPIO_EXTI_IRQHandler,然后在回调函数HAL_GPIO_EXTI_Callback中通过判断中断是来自哪个IO口编写相应的中断服务控制逻辑。
& j9 V6 w- F! f" @* G因此我们可以在HAL_GPIO_EXTI_Callback里面实现控制逻辑编写,详见本实验源码。9 @6 z8 A- L6 X: _4 J
/ B3 e; c% u, c16.3.2 程序流程图
# p0 G, k! x" G% j/ U) r8 p; o- @1 q. f% \# O* E1 A4 N
; n0 U5 R. A5 E0 T3 \+ t& \% l( R
( ~7 j9 Y- x& ~8 t, u: X图16.3.2.1外部中断实验程序流程图$ b# _6 b, p# k' Q9 N; M
主程序初始外设,在按键初始化时初始化按键的采样边缘。
# |) o+ }! p8 c7 l( N/ Y+ `8 P2 {+ U- L
16.3.3 程序解析) X* C' A4 P' z7 y
1.外部中断驱动代码
2 ]' a; u4 T, M8 V) x下面我们先解析exti.h的程序。1 O( o8 r1 ~# Y$ J1 n( ~+ |" ?# t6 ^- G
由硬件设计小节,我们知道KEY0、KEY1和KEY_UP分别来连接到PA1、PA15和PA0上,我们做了下面的引脚定义。
! P7 `8 w: V9 q; |8 S- /*****************************************************************************/3 w) Y* Y" l7 h' k) k8 m. U) N
- /* 引脚 和 中断编号 & 中断服务函数 定义 */
3 ?) b v# G$ _/ u
' O9 I, Z; O% a- #define KEY0_INT_GPIO_PORT GPIOA s/ o. h0 ?* P/ M! }
- #define KEY0_INT_GPIO_PIN GPIO_PIN_1, Z4 G- |0 d7 M% e# l
- /* PA口时钟使能 */
' D3 w. O, G9 Q& c7 ^5 K1 {( i - #define KEY0_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)+ C9 y: r0 c9 R$ X& c
- #define KEY0_INT_IRQn EXTI1_IRQn
) V; g" V: u! i* Y' T - #define KEY0_INT_IRQHandler EXTI1_IRQHandler8 @3 ]5 X. v' P1 B1 v3 o
2 A3 u9 q! [" U- Q' }- #define KEY1_INT_GPIO_PORT GPIOA6 o. l: Z4 `4 x T3 \
- #define KEY1_INT_GPIO_PIN GPIO_PIN_15" ]; C( C4 w# m b
- /* PA口时钟使能 */
! r( ]4 V" Q$ v5 g - #define KEY1_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) . z+ S! U' @3 q, B) { a; n8 `
- #define KEY1_INT_IRQn EXTI15_10_IRQn4 D& L1 M; O" S4 Z) t
- #define KEY1_INT_IRQHandler EXTI15_10_IRQHandler
7 R2 |; ~& b. P5 B
' _7 m9 W. ^6 E- #define WKUP_INT_GPIO_PORT GPIOA
; U* h; o9 O0 Q - #define WKUP_INT_GPIO_PIN GPIO_PIN_0
; x( o# ~0 E7 |- P. o4 j/ b - /* PA口时钟使能 */
% M5 Y/ P& O7 t! m; q - #define WKUP_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) $ m# ]1 \7 K% C( O8 l4 {1 q
- #define WKUP_INT_IRQn EXTI0_IRQn
" ]" U9 S* T0 A. p& l( [8 M - #define WKUP_INT_IRQHandler EXTI0_IRQHandler& f% V0 a% N; _
- /*****************************************************************************/
复制代码
4 q- e/ J' c3 r. q8 _KEY0、KEY1和WK_UP分别连接PA1、PA15和PA0,即对应了EXTI1、EXTI15和EXTI0这三条外部中断线。这里需要注意的是EXTI0到EXTI4都是有单独的中断向量,EXTI5到EXTI9是公用EXTI9_5_IRQn,EXTI10到EXTI15是公用EXTI15_10_IRQn。
# j; M0 O* P" c g$ @/ P下面我们再解析exti.c的程序,先看外部中断初始化函数,其定义如下:: v4 O. w2 I" M) Y/ |
N \6 H: ]) ], F$ r( I* ?9 m- /**
$ a3 f2 c8 y5 B& i - * @brief 外部中断初始化程序" ?- m# Y; }; Z) o7 l; t2 o
- * @param 无2 b) J8 k' K8 q! x
- * @retval 无
! f$ J0 i$ Q% g& c% ^* v5 n - */
; n4 i! X; X" L. Q. z% @+ ?- a8 Z$ j - void extix_init(void)* c5 A# W: z8 w& z
- {
7 c$ O; q" k( v9 d/ _0 f( K: y9 | - GPIO_InitTypeDef gpio_init_struct;8 \9 m% s. T9 L9 g9 }. @6 `7 y
0 b- Z0 z4 e; ?& p- KEY0_INT_GPIO_CLK_ENABLE(); /* KEY0时钟使能 */' g5 X7 N; |8 D. P, Q' ]1 o2 L2 A
- KEY1_INT_GPIO_CLK_ENABLE(); /* KEY1时钟使能 */0 C+ \. n4 v" @$ Q: u( P0 v- N2 M6 E
- WKUP_INT_GPIO_CLK_ENABLE(); /* WKUP时钟使能 */
5 H, k7 G2 a" D1 x. D - ( M+ T" p4 f; U/ ?0 F/ V
- gpio_init_struct.Pin = KEY0_INT_GPIO_PIN; /* KEY0引脚 */
% V: `) q( _( S: c* Q; L( i7 O3 h - gpio_init_struct.Mode = GPIO_MODE_IT_FALLING; /* 下升沿触发 */
- C1 u6 \+ G! D$ E: f9 f% B& k - gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */2 a h6 q9 ^) i4 u, M, o, g# ^
- HAL_GPIO_Init(KEY0_INT_GPIO_PORT, &gpio_init_struct); /* KEY0引脚初始化 */( m! ~# B- G2 L' h
k$ e r5 x6 v* n7 P! J& Q5 V- gpio_init_struct.Pin= KEY1_INT_GPIO_PIN; /* KEY1引脚 */
. _9 S7 i- p$ K" M - gpio_init_struct.Mode = GPIO_MODE_IT_FALLING; /* 下升沿触发 */
( q7 P; [' t) }: c5 J - gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
# U7 g# S/ ^/ w& @) j+ {, Q - HAL_GPIO_Init(KEY1_INT_GPIO_PORT, &gpio_init_struct); /* KEY1引脚初始化 */
; g6 ^" O' P' {; d8 ~0 C+ Z4 o
/ W8 N G$ E `- gpio_init_struct.Pin = WKUP_INT_GPIO_PIN; /* WKUP引脚 */5 R: D v% x+ }! [% O/ H# Y
- gpio_init_struct.Mode = GPIO_MODE_IT_RISING; /* 上升沿触发 */, u3 r2 w( |- p$ Y3 I# W
- gpio_init_struct.Pull = GPIO_PULLDOWN; /* 下拉 */
1 k3 S1 q- o! Q- | - HAL_GPIO_Init(WKUP_INT_GPIO_PORT, &gpio_init_struct); /* WKUP引脚初始化 */# P! _- A. k; L- {: `
- ' M/ b; b* J: C+ H- S8 ?0 J
- HAL_NVIC_SetPriority(WKUP_INT_IRQn, 2, 0); /* 抢占优先级为2,子优先级为0 */5 ]9 C0 J [7 _7 u/ I) i6 F4 n
- HAL_NVIC_EnableIRQ(WKUP_INT_IRQn); /* 使能中断线0 */
$ R' P+ @9 K* `9 v. ^$ o' V
4 L; K0 _: g5 A F6 G8 q- HAL_NVIC_SetPriority(KEY0_INT_IRQn, 2, 1); /* 抢占优先级为2,子优先级为1 */1 |- ]! Y, L- G7 z
- HAL_NVIC_EnableIRQ(KEY0_INT_IRQn); /* 使能中断线1 */' P: X6 z! y' Y7 F
- # p8 w0 a( A! `& ^4 W/ E& S
- HAL_NVIC_SetPriority(KEY1_INT_IRQn, 2, 2); /* 抢占优先级为2,子优先级为2 */
. ^5 T1 H7 i2 ^3 ~/ f* @ - HAL_NVIC_EnableIRQ(KEY1_INT_IRQn); /* 使能中断线15 */4 ^7 [: H* F) ?! S+ G3 h" t- E
- }
2 h) q2 `) q$ Y" z) V1 F& L! Z; c- S& ^
复制代码 3 o$ e e6 ^6 d$ }8 p! y y
外部中断初始化函数主要做了两件事情,先是调用IO口初始化函数HAL_GPIO_Init来初始化IO口,然后设置中断优先级并使能中断线。
* s% s2 R$ Q. p/ B3个外部中断服务函数,其定义如下:
/ q. m. B8 F8 R( r. k1 T
- t2 I6 r' G( ~5 U- n+ T+ B- /**3 j( a+ F( o6 F7 b/ ~
- * @brief 外部中断服务程序# A* a: u. Q3 B1 J* X7 G: K- I) V1 S0 o
- * @param 无3 B4 g& K/ b! s* s
- * @retval 无
' l( {7 m3 I0 k& \/ u/ n' @! o2 f2 r - */
+ |) {. L# r/ W1 v - void EXTI0_IRQHandler(void)& [- e, k; `5 H- v9 E# R
- {1 X% b+ Z7 L- ~7 _$ Q
- HAL_GPIO_EXTI_IRQHandler(WKUP_INT_GPIO_PIN); /* 调用中断处理公用函数 */
% n2 m$ m/ K( O U. u1 M+ ^ - }' J& L! i6 U0 H) d. l# g+ [; o* S
1 z% n4 S' x0 r9 {# Q6 T$ u, L- void EXTI1_IRQHandler(void)
b. I3 ?3 ~. K - {3 W r7 w: f( I1 z0 J- J
- HAL_GPIO_EXTI_IRQHandler(KEY0_INT_GPIO_PIN); /* 调用中断处理公用函数 */4 L' d# G! g1 f% u& V
- }: t. a6 C3 x' _
( q! c# l6 O2 [2 Z; S. H1 e0 l- void EXTI15_10_IRQHandler(void)2 v8 w4 ?4 g) [7 C' o" y% _* F0 T
- {
7 S1 c$ V, v' B! z5 i4 \ - HAL_GPIO_EXTI_IRQHandler(KEY1_INT_GPIO_PIN); /* 调用中断处理公用函数 */
) c+ ^3 t4 }) M* y - }
复制代码
5 V) L4 k7 Q3 [) [- r$ c w+ }! M所有的外部中断服务函数里都只调用了同样一个函数HAL_GPIO_EXTI_IRQHandler,该函数是外部中断共用入口函数,函数内部会进行中断标志位清零, 并且调用中断处理共用回调函数HAL_GPIO_EXTI_Callback。但是它们的形参不同,我们的回调函数也是根据形参去判断是哪个IO口的外部中断线被触发。9 c5 P1 e) }0 t
外部中断回调函数,其定义如下:! }9 @( t' p6 a4 |% x! Q7 O
- C: D4 h3 h& U) L+ C, q* I" A6 ]3 c- /**: s( L% k& d! x- Y: }3 ~* Q
- * @brief 外部中断回调函数$ o; ^8 V5 U: s5 F$ h
- * @param GPIO_Pin: 中断引脚号
0 S8 ~+ ?& N0 u: t" S% W% y6 ~ - * @note 在HAL库中所有的外部中断服务函数都会调用此函数
/ L' w' C; _( k6 t, i' s - * @retval 无$ S! L9 ~& P) B$ B/ I0 e7 x; T
- */. v2 ]6 l, E. \- J, u
- void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)5 Y# c8 d- r+ B% \9 b( a2 A; E
- {
' F& h# ]6 D ^ - /* 消抖,此处为了方便使用了延时函数,实际代码中禁止在中断服务函数中调用9 k( d* ]+ j2 G5 T& \& _6 c
- 任何delay之类的延时函数!!! */
# U# w( @5 b' ?" C- u# A. y+ [9 W - delay_ms(20); + r. {& s1 G1 Q
- switch (GPIO_Pin)
1 b9 s: ^9 ` d - {
* b, p6 r M# z6 c; ~. g4 l) j - case WKUP_INT_GPIO_PIN:/ B3 H7 Z! w+ q, u- q
- if (WK_UP == 1) /* WK_UP中断 */8 K, I. s2 F9 o" ~) D% E
- {* _% ], J" Z# i L6 e7 F' G
- LED2_TOGGLE(); /* LED2 状态取反 */ 6 ^3 n( ~$ a0 I. d( w$ N
- }: c3 S4 a: |6 j/ _& |% }! {
- break;
7 e; \( F0 p6 }0 d. Y - case KEY0_INT_GPIO_PIN:" G1 q3 p8 y# c% g. q0 N8 ^# U
- if (KEY0 == 0) /* KEY0U中断 */
X$ F, x, U6 W3 g1 q - {; z1 l3 n9 A1 F& A
- LED0_TOGGLE(); /* LED0 状态取反 */7 F& n- _- O% L0 u8 l
- }
( z2 q2 t3 Y2 ~, k* z% r - break;1 z6 n9 n; ^+ D% u
- case KEY1_INT_GPIO_PIN:
7 w7 Q$ Q8 L; s: C - if (KEY1 == 0) /* KEY1中断 */
' ^$ [2 h- V2 A" z - {+ x9 U" G: }$ v
- LED1_TOGGLE(); /* LED1 状态取反 */) |5 W0 c. s; M% o8 d$ y
- } n2 `& |+ T1 ^& c+ w+ N& l( o
- break;
$ p) ?! e% X" L- \' } - }
0 g' N& X5 e9 _& d' L - }
复制代码 4 ]: j$ w* S. W
外部中断回调函数HAL_GPIO_EXTI_Callback是用来编写真正的外部中断控制逻辑。该函数有一个形参就是IO引脚号。所以我们在该函数内部,一般通过判断IO引脚号来确定中断是来自哪个IO口,也就是哪个中断线,然后编写相应的控制逻辑。所以在该函数内部,我们通过switch语句判断IO口来源,例如是来自GPIO_PIN_0,那么一定是来自PA0,因为中断线一次只能连接一个IO口,而三个IO口中引脚号为0的IO口只有PA0,所以中断线0一定是连接PA0,也就是外部中断由PA0触发。其他的引脚号的逻辑类似。: O X; T; W- R. Z2 Y- w0 G) z
2. main.c代码
\+ i9 u3 \; F! b在main.c里面编写如下代码:( K4 t8 b; o0 c9 Z3 m% |0 y
0 n, m6 m! ~. K) ?
- #include "./SYSTEM/sys/sys.h": n z- t/ d( a
- #include "./SYSTEM/usart/usart.h"
) ], _9 e2 O \0 r3 G0 v' N4 \ - #include "./SYSTEM/delay/delay.h"2 q- q2 Z0 ^4 Z4 m2 I4 {. ^5 E
- #include "./BSP/LED/led.h"
8 K+ x, U4 A; `/ P9 Y; R - #include "./BSP/EXTI/exti.h"3 d( R2 O& u3 \. q \$ ?/ d- [& }
- 7 O7 @: b* }6 k* R* q; R
- int main(void)
2 k' J! ?4 `! D: D% |+ ~ - {
- K" z, P8 b, ~ - sys_cache_enable(); /* 打开L1-Cache */% }+ s f* y8 i, j1 e- T9 Q. a
- HAL_Init(); /* 初始化HAL库 */
5 }5 r' V r/ l6 a/ G - sys_stm32_clock_init(240, 2, 2, 4); /* 设置时钟, 480Mhz */. y" V* ^- \6 v5 f& @, D* ]
- delay_init(480); /* 延时初始化 */, Z1 K) k6 y* b: C- l
- led_init(); /* 初始化LED */
5 S, d8 W$ A6 N0 z4 v - extix_init(); /* 初始化外部中断输入 */
' _9 W& c# ], |9 I e - LED0(0); /* 先点亮红灯 */
" v; u* ^6 j* K* K7 t - while (1), R5 ~; N0 o$ E# t' d
- {
- n H) N" N2 ?9 E( x B9 N - delay_ms(1000);; m* Y: e. N9 E' c9 h
- }
" r5 |2 x. p( m& P+ K+ j, O! w+ Q - }
复制代码 ; B2 b0 @; I9 t9 @
首先是调用系统级别的初始化:sys_cache_enable函数使能I-Cache和D-Cache,然后初始化 HAL库、系统时钟和延时函数。接下来,调用led_init来初始化RGB灯,调用extix_init函数初始化外部中断,点亮红灯。最后在无限循环里面执行1000ms的延时函数。逻辑控制代码都在中断回调函数中完成。$ A* K2 Z. @7 k5 i# ]2 T& Q" m6 k
6 s8 [& |% Z, }! S5 L* T7 l16.4 下载验证8 v! H# h* f- l
在下载好程序后,我们可以按KEY0、KEY1和KEY_UP来看看RGB灯的变化,是否和我们预期的结果一致?
1 R+ d. ^; _, w0 C z————————————————
Y/ ]/ T8 T9 k. b+ c: E/ Y版权声明:正点原子+ d \; [( P* K+ \, y
% R7 H1 l7 s5 d( I4 N5 U( ]* a9 a7 z& L( Z6 v ^
|