void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct)
{
uint32_t tmp = 0;
/* Check the parameters */
assert_param(IS_EXTI_MODE(EXTI_InitStruct->EXTI_Mode));
assert_param(IS_EXTI_TRIGGER(EXTI_InitStruct->EXTI_Trigger));
assert_param(IS_EXTI_LINE(EXTI_InitStruct->EXTI_Line));
assert_param(IS_FUNCTIONAL_STATE(EXTI_InitStruct->EXTI_LineCmd));
tmp = (uint32_t)EXTI_BASE;
if (EXTI_InitStruct->EXTI_LineCmd != DISABLE)
{
/* Clear EXTI line configuration */
EXTI->IMR &= ~EXTI_InitStruct->EXTI_Line;
EXTI->EMR &= ~EXTI_InitStruct->EXTI_Line;
tmp += EXTI_InitStruct->EXTI_Mode;
*(__IO uint32_t *) tmp |= EXTI_InitStruct->EXTI_Line;
/* Clear Rising Falling edge configuration */
EXTI->RTSR &= ~EXTI_InitStruct->EXTI_Line;
EXTI->FTSR &= ~EXTI_InitStruct->EXTI_Line;
/* Select the trigger for the selected external interrupts */
if (EXTI_InitStruct->EXTI_Trigger == EXTI_Trigger_Rising_Falling)
{
/* Rising Falling edge */
EXTI->RTSR |= EXTI_InitStruct->EXTI_Line;
EXTI->FTSR |= EXTI_InitStruct->EXTI_Line;
}
else
{
tmp = (uint32_t)EXTI_BASE;
tmp += EXTI_InitStruct->EXTI_Trigger;
*(__IO uint32_t *) tmp |= EXTI_InitStruct->EXTI_Line;
}
}
else
{
tmp += EXTI_InitStruct->EXTI_Mode;
/* Disable the selected external lines */
*(__IO uint32_t *) tmp &= ~EXTI_InitStruct->EXTI_Line;
}
}
如果没记错的话,笔者好像写过关于EXTI的文章,应该也分析过这个,不过年事高了容易忘,而且每次看应该感悟不同,分析的思路也不同,所以再写写换个脑子也可以。
如上,是外部中断/事件初始化的代码。
要理解这个代码首先得知道要初始化一个外部中断/事件,其实是在对外部中断/事件这个外设的对应寄存器的设置。那么首先介绍的肯定是这个外设对应的寄存器。
这个图截自《stm32f10x-中文参考手册》的目录,因为这样比较明显的看的出来EXTI这个外设有哪些寄存器。一目了然,6个。不一一打字,看代码里面如何定义的(stm32f10x.h文件中):
/**
* @brief External Interrupt/Event Controller
*/
typedef struct
{
__IO uint32_t IMR;
__IO uint32_t EMR;
__IO uint32_t RTSR;
__IO uint32_t FTSR;
__IO uint32_t SWIER;
__IO uint32_t PR;
} EXTI_TypeDef;
#define EXTI ((EXTI_TypeDef *) EXTI_BASE)
#define EXTI_BASE (APB2PERIPH_BASE + 0x0400)
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
/*!< Peripheral base address in the alias region */
#define PERIPH_BASE ((uint32_t)0x40000000)
笔者将外设基地址,APB2基地址,EXTI基地址和EXTI地址都列出来,相信读者会更清晰的知道代码在阐释一个外设的时候是怎么来做的。其实就是一个地址的操作,定义成那样一个结构,是因为在寄存器都是按32bit,也就是4字节间隔的地址来映射的。uint32_t就是四字节,那么在数据结构中,IMR EMR RTSR FTSR SWIER PR这6个寄存器刚好就是4字节的间隔,刚好和手册中寄存器映射的寄存器地址偏移对应上。
另外笔者将基地址列出来,也就是手册中列出的EXTI的基地址是:
0x40010400这个地址。
#define EXTI ((EXTI_TypeDef *) EXTI_BASE)
这个宏定义以及下面的哪些宏,数学计算不差的可以算出来,EXTI的基地址就是:
0x40000000+0x10000+0x0400 = 0x40010400,
正好就是手册里的EXTI外设的基地址了,然后根据刚才说的数据结构的偏移,操作这个EXTI就是在操作各个寄存器了。
这里花时间介绍这个,是因为每一个外设都有自己的一套寄存器,以后只要想用对应外设的寄存器就是这个方法,找到他的外设基地址的定义,再找到他的寄存器的定义就知道要操作的是啥了。
当然,我们操作寄存器就是为了往里面写数据,那么写什么数据呢?stm32的处理就是对所有对应的外设的再定义一个初始化的结构体,比如说EXTI,我要写里面的六个寄存器,写什么样的数据,肯定都是差不多的,那我就定义一个叫初始化数据的结构体,用这个结构体来先把数据写到结构体变量中,然后通过这个结构体中的各个变量来给EXTI的寄存器赋值不就好了。因此EXTI_InitTypeDef应运而生,从结构体的命名就知道它的意思,EXTI是外部中断/事件外设的名称,Init就是初始化的简称,然后就是类型定义:
typedef struct
{
uint32_t EXTI_Line; /*!< Specifies the EXTI lines to be enabled or disabled.
This parameter can be any combination of @ref EXTI_Lines */
EXTIMode_TypeDef EXTI_Mode; /*!< Specifies the mode for the EXTI lines.
This parameter can be a value of @ref EXTIMode_TypeDef */
EXTITrigger_TypeDef EXTI_Trigger; /*!< Specifies the trigger signal active edge for the EXTI lines.
This parameter can be a value of @ref EXTIMode_TypeDef */
FunctionalState EXTI_LineCmd; /*!< Specifies the new state of the selected EXTI lines.
This parameter can be set either to ENABLE or DISABLE */
}EXTI_InitTypeDef;
我们看着结构体来反推,这个结构体成员为啥是这四个。
第一,EXTI_Line,也就是EXTI的线,啥意思?
看图,外部中断/事件的框图中,那个输入线的意思,外部中断/事件寄存器是由20个外部线连接着的,要用这个外设来监测外部中断,得有个来源吧,物理上得有个线吧,就是这个线。
那有20根线,那这个线的定义是不是就是1~20这20个数呢,并不是,因为你想想,我们操作的是寄存器,寄存器的形式是什么样的,是32位的一个硬件,每个bit只能是0和1,以IMR为例,我们假设它是32个框框:
每一个框框对应一个中断的线,那么我一个IMR的前20个框就对于国内没一根线,框图里中断屏蔽寄存器就是IMR,那个斜杠20的意思就是IMR这个寄存器控制着20个来自于输入线的中断,事件屏蔽寄存器控制着20个来自输入线的事件,当确定了是哪个线,比如线5,那就是框框中的5,我们程序员眼里5可不是5啊,这个32bit 的每个bit都只能表示0和1两个数,当是线5的时候,只有5是1,其他的31位都是0啊,用二进制表示就是:
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0
也就是0x00000020了,我们看下面的代码。
#define EXTI_Line0 ((uint32_t)0x00001) /*!< External interrupt line 0 */
#define EXTI_Line1 ((uint32_t)0x00002) /*!< External interrupt line 1 */
#define EXTI_Line2 ((uint32_t)0x00004) /*!< External interrupt line 2 */
#define EXTI_Line3 ((uint32_t)0x00008) /*!< External interrupt line 3 */
#define EXTI_Line4 ((uint32_t)0x00010) /*!< External interrupt line 4 */
#define EXTI_Line5 ((uint32_t)0x00020) /*!< External interrupt line 5 */
#define EXTI_Line6 ((uint32_t)0x00040) /*!< External interrupt line 6 */
#define EXTI_Line7 ((uint32_t)0x00080) /*!< External interrupt line 7 */
#define EXTI_Line8 ((uint32_t)0x00100) /*!< External interrupt line 8 */
#define EXTI_Line9 ((uint32_t)0x00200) /*!< External interrupt line 9 */
#define EXTI_Line10 ((uint32_t)0x00400) /*!< External interrupt line 10 */
#define EXTI_Line11 ((uint32_t)0x00800) /*!< External interrupt line 11 */
#define EXTI_Line12 ((uint32_t)0x01000) /*!< External interrupt line 12 */
#define EXTI_Line13 ((uint32_t)0x02000) /*!< External interrupt line 13 */
#define EXTI_Line14 ((uint32_t)0x04000) /*!< External interrupt line 14 */
#define EXTI_Line15 ((uint32_t)0x08000) /*!< External interrupt line 15 */
#define EXTI_Line16 ((uint32_t)0x10000) /*!< External interrupt line 16 Connected to the PVD Output */
#define EXTI_Line17 ((uint32_t)0x20000) /*!< External interrupt line 17 Connected to the RTC Alarm event */
#define EXTI_Line18 ((uint32_t)0x40000) /*!< External interrupt line 18 Connected to the USB Device/USB OTG FS
Wakeup from suspend event */
#define EXTI_Line19 ((uint32_t)0x80000) /*!< External interrupt line 19 Connected to the Ethernet Wakeup event */
看到EXTI_Line5是不是正好就是0x00020这个数值,只不过代码中省去了20~31位的0.
当然这样说还没说清楚,这样写的目的是因为,我们这个EXTI_Line成员这样赋值,是因为这个值到了EXTI_Init这个初始化函数中去用的时候,都是和32bit的寄存器进行与或非加运算的,比如:IMR寄存器是EXTI基地址上的那个寄存器,它一定是一个32bit的寄存器吧,你这时候用的是线5,你如果直接用这个32bit的寄存器的地址加5,二进制的5是0x5,实际就是将寄存器的0bit和2bit给置1了,这肯定就是错的。其实再换个说法就是不管是IMR EMR RTSR FTSR SWIER PR这其中哪一个寄存器,它们的0~19bit对应是对应着输入线的1~20根线的,而对应的0~19bit用32bit的数来表示的时候并不是1~20这些数,而是以上的EXTI_Linex对应的十六进制数。
第二,有了物理线,那信号来了是要产生中断还是产生事件呢?这个EXTI外设的名字不是外部中断/事件控制器吗?那肯定功能有中断也有事件嘛,这里就需要有个变量来记录下这两个东西。当然我们知道了是中断还是事件是需要记录的,那么这个区分的操作是怎么完成的呢,我们这个变量去怎么定义呢?先看这个变量的定义:
typedef enum
{
EXTI_Mode_Interrupt = 0x00,
EXTI_Mode_Event = 0x04
}EXTIMode_TypeDef;
看完之后你会奇怪,为什么是0x00和0x04,可能我们会说0和1来区分一下两个的区别不就行了。这里我们想想,我们这个数据是给谁的,是给EXTI外设的寄存器的,EXTI里面有两个寄存器一个叫IMR(中断屏蔽寄存器)一个叫EMR(事件屏蔽寄存器),只看他们的前两个字,中断屏蔽,那也就是说我要用EXTIMode_TypeDef给的数据来确定是这两个中的一个,就能确定这个外部来的是中断还是事件了吧,因为IMR和EMR这两个寄存器说白了就是框图里面的两个正方框框,他们的操作其实都一样。再回到0x00和0x04,说了很多了,寄存器的偏移都是4,我们根据这两个数据的差就可以想象到,到初始化的时候,如果是中断肯定是在EXTI基地址上加0x00,如果是事件肯定就是在EXTI基地址上加0x04,这也就确定了操作的寄存器,也就是这个结构体所说的EXTI_Mode_xxx的意思。我们看下面三个语句,可以验证我们上面的分析:
tmp = (uint32_t)EXTI_BASE;
tmp += EXTI_InitStruct->EXTI_Mode;
*(__IO uint32_t *) tmp |= EXTI_InitStruct->EXTI_Line;
第一句拿下EXTI的基地址,就是为了操作EXTI这个外设的寄存器;
第二句通过加上初始化结构体中EXTI_Mode变量的0x00或者0x04加上EXTI的基地址,确定操作的事IMR还是EMR寄存器,因为IMR和EMR与EXTI外设的地址偏移就是0x00和0x04;
第三句因为外部中断有20根线,又要确定是IMR或者EMR中的对应控制哪个输入线就需要用到上面解释的EXTI_Line的数值,来置位那个对应的bit。
第三确定的是触发中断或者事件的是上升沿还是下降沿还是上下沿均可触发。这个数据是要去置位RTSR或者FTSR或者二者均置位的,所以这个变量应该有三个值可以选择:
/**
* @brief EXTI Trigger enumeration
*/
typedef enum
{
EXTI_Trigger_Rising = 0x08,
EXTI_Trigger_Falling = 0x0C,
EXTI_Trigger_Rising_Falling = 0x10
}EXTITrigger_TypeDef;
0x08和0x0C很好理解,和EXT_Mode是同理,为了找对相对EXTI基地址的偏移,从而找到对应的寄存器,那这个0x10呢,难道是在上下沿都触发的时候有一个上下沿均触发的寄存器?答案是没有,因为如果是上下沿触发的时候就将RTSR和FTSR这两个寄存器对应的输入线的bit均置位,而且基地址加0x10这个寄存器是SWIER寄存器,跟上下沿没关系。
直接看EXTI_Init代码吧,看它是怎么处理的:
/* Select the trigger for the selected external interrupts */
if (EXTI_InitStruct->EXTI_Trigger == EXTI_Trigger_Rising_Falling)
{
/* Rising Falling edge */
EXTI->RTSR |= EXTI_InitStruct->EXTI_Line;
EXTI->FTSR |= EXTI_InitStruct->EXTI_Line;
}
else
{
tmp = (uint32_t)EXTI_BASE;
tmp += EXTI_InitStruct->EXTI_Trigger;
*(__IO uint32_t *) tmp |= EXTI_InitStruct->EXTI_Line;
}
原来这个0x10不是地址偏移,只是一个if else语句的判断条件,取初始化结构体中边沿触发的数据,如果是上下沿触发就进去if语句,将RTSR和FTSR这两个寄存器中对应的输入线的bit置位;如果不是上下沿,那就根据基地址加偏移找到对应的RTSR和FTSR寄存器,然后再用EXTI_Line来置位相应的bit。
最后就是那个EXTI_LineCmd了,它就是ENABLE和DISABLE,在代码中如果是ENABLE,就先把IMR EMR RTSR FTSR这几个寄存器中的对应的输入线bit给清零,以确保之前的值不会影响现在的设置,清完零以后再来置位相应的位,就是真正的初始化了。相反如果是DISABLE,那么就一个操作,找到对应的中断或者事件屏蔽寄存器(IMR或者EMR),就去把对应的位清零就行了,也就是不让中断或者事件发生,屏蔽掉就行了。
以上就外部中断设计及代码的过程了。
补充:
还说漏了一点,就是一直在说的输入线问题。一直说输入线输入线,这个输入线是哪个,就好比说我们这里一直说的EXTI_Line5,应该是GPIOA~GPIOG这些个端口中的pin5吧,我们在芯片上选择了GPIOA5用,但是在代码中如果没做这个配置,那芯片怎么知道是这个脚被配置成了输入线的来源呢?所以在最开始我们就应该要做这样一个配置。
这里又来了一个AFIO寄存器组,这个寄存器组中用来设置输入线源的寄存器就是图中的:
AFIO_EXTICRx寄存器组了。
可以看到从EXTICR1~EXTICR4这四个寄存器,只有低16位有效,每四位设置一个端口,也就是GPIOA~GPIOG,因为A~G一共是7个端口,用四位表示足矣。
而16除以4是4,也就是说每个寄存器可以配置的引脚是四个,比如CR1,它的0~3bit对应的是引脚0,而0~3bit表示的数字用来选择A~G端口,也就是说如果选择了EXTICR1寄存器的0~3bit,那么就确定了是引脚0了,引脚0对应的EXTI_Line就是Line0,端口是多少不影响Line的数据,但是必须要确认是那个端口,物理连接才能通,所以还得确认端口,端口就得看0~3bit的数值了。同理,如果用EXTICR2寄存器的4~7bit,那么就是确定了引脚是5引脚,也就确认可Line是Line5,也就确认了EXTI寄存器组中应该去置位哪个bit,具体端口确认就看4~7bit的实际值了。
库函数里选择端口和引脚的函数如下:
/**
* @brief Selects the GPIO pin used as EXTI Line.
* @param GPIO_PortSource: selects the GPIO port to be used as source for EXTI lines.
* This parameter can be GPIO_PortSourceGPIOx where x can be (A..G).
* @param GPIO_PinSource: specifies the EXTI line to be configured.
* This parameter can be GPIO_PinSourcex where x can be (0..15).
* @retval None
*/
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource)
{
uint32_t tmp = 0x00;
/* Check the parameters */
assert_param(IS_GPIO_EXTI_PORT_SOURCE(GPIO_PortSource));
assert_param(IS_GPIO_PIN_SOURCE(GPIO_PinSource));
tmp = ((uint32_t)0x0F) << (0x04 * (GPIO_PinSource & (uint8_t)0x03));
AFIO->EXTICR[GPIO_PinSource >> 0x02] &= ~tmp;
AFIO->EXTICR[GPIO_PinSource >> 0x02] |= (((uint32_t)GPIO_PortSource) << (0x04 * (GPIO_PinSource & (uint8_t)0x03)));
}
可以看到是在操作AFIO->EXTICR这个寄存器,也就可以确定是在确定那个输入线的来源了。
笔者用stm32cubemx来设置外部中断的时候也查看了一下HAL库的代码,在进行中断配置的时候的确也需要对输入线的来源进行配置,以确定Line值,具体的步骤及程序不一一列出,只把用到AFIO->EXTICR寄存器的代码截图以供参考:
以上就是所有的外部中断配置的理解。