前言
本文建立在上一篇笔记 的基础之上。记录了AT32F421实现GPIO触发外部中断的过程。代码由 AT官方的BSP(此处是AT32F421固件库BSP&Pack)
BSP\project\at_start_f421\examples\exint\exint_config
项目文件修改而来。主要测试AT32F4单片机的外部中断功能。
环境
- VScode ( EIDE + Cortex Debug )
- Open On-Chip Debugger 0.11.0+dev-snapshot
- GCC toolchain
- AT32F421C8T7系统板 & ATLink
- USB < - > TTL 模块
功能描述
使用外部中断的方式,检测PF7引脚外部输入的下降沿。
中断服务程序:串口打印PF7电平值、消抖后的按键状态,LED(PC13)闪烁一次。
注意事项
关于输入引脚的设置和处理
-
gpio_default_para_init()函数将传入的GPIOx port所有引脚默认初始化为输入模式、外部推挽、无上下拉、强驱动电流。这与实际情况只有out type一点不符——我板上的USRKEY_PIN 外部只有浮空(IDLE)和下拉(PRESS_DOWN)两种状态。out type可认为是开漏(GPIO_OUTPUT_OPEN_DRAIN),因此需要内部上拉。
-
使用GPIO作为中断源,需要配置复用外部中断配置寄存器 x(IOMUX_EXINTCx)或SCFG 外部中断配置寄存器x(SCFG_EXINTCx) 。但是AT32F421没有IOMUX_EXINTCx寄存器,这里调用scfg_exint_line_config()函数配置后者,需要指定GPIO中断源的端口和线号。
关于中断的初始化、设置、使能和处理
-
配置中断源——SCFG:系统配置寄存器SCFG是挂载在APB2上的外设,一定不要忘了使能其时钟!
crm_periph_clock_enable(CRM_SCFG_PERIPH_CLOCK, TRUE); scfg_exint_line_config(SCFG_PORT_SOURCE_USRKEY, SCFG_PINS_SOURCE_USRKEY);
- 中断(INT)/事件(EVT)初始化——EXINT:EXINT的初始化类似GPIO,在标准库中都是传入一个结构体给初始化函数。然后使用
初始化EXINT寄存器。具体需要指定:exint_init(&exint_init_struct);
- line mode,选择是INT还是EVT。它的值关系到两个使能寄存器;
- line select,这个指的就是中断线的号码了。来自GPIO的中断,号码就是PIN号,取值0~15之间。PF7就占用中断线line7,要注意的是,line x只能被ABCDEF端口中的一个PIN x所占用,因此这里PA7和PB7就无法再使用中断线了。它的值关系到置位EXINT寄存器的第几位。
- line polarity,指定外部中断的有效边沿类型。
- 设置中断优先级——SCB:用nvic_priority_group_config()设置。奇怪的是在用这个函数设置优先级时,只传入了参数NVIC_PRIORITY_GROUP_x (x=0,1,2,3,4),MCU怎么知道这是在给谁设置优先级呢?事实上确实如此,所以此函数需要配合其他函数一同使用。具体而言,nvic_priority_group_config()的底层为一个定义在core_cm4.h中的函数:
它把NVIC_PRIORITY_GROUP_x的信息处理后存放到SCB的AIRCR(应用中断和复位控制)寄存器中。而之后在中断使能的函数nvic_irq_enable() ,它会先调用void __NVIC_SetPriorityGrouping(uint32_t PriorityGroup)
读取SCB->AIRCR的值,重新获得优先级分组的信息,再结合该使能函数传入的抢占优先级和子优先级的信息合成一个32位的值,用于在底层的uint32_t __NVIC_GetPriorityGrouping(void)
中配置中断向量IQRn的优先级。所以,真正实现优先级设置的,是nvic_irq_enable()调用的__NVIC_SetPriority()!void __NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
- 使能中断——NVIC:用nvic_irq_enable()实现对中断向量IQRn的使能。但就如上文所说,该函数首先实现的是对IQRn优先级的设置。
这么多外设和寄存器,它们的关系和作用?
GPIO是挂载在AHB上的外设,SCFG/CMP和EXINT都是APB2上的外设,而NVIC是Cotex-M内核的控制器。设置中断优先级分组由内核的SCB(System Control Block)中的AIRCR寄存器指示。
最终决定外部中断优先级的是NVIC中的IP寄存器(可被__NVIC_SetPriority()设置)。
从__NVIC_SetPriority()的内容还可以看出,决定内部中断优先级的是SCB中的SHP寄存器。
使用一个GPIO外部中断需要多少“使能”?
需要2个与时钟有关的使能、2个与中断有关的使能:
使能GPIO时钟;
使能SFCG时钟;
在EXINT的INTEN寄存器中对应line x的位使能;
最后在__NVIC_EnableIRQ()中使能中断。
EXINT可以做到单独使能每一条中断线,但NVIC通常都是分组管理中断线(多个中断线共用一个中断向量,少数可以单独被NVIC控制)。
值得注意的是,EXINT作为要用到的APB2外设却不需要时钟使能。只能reset其时钟。
代码实现
几个核心文件和功能:
- main.c
/** ************************************************************************** * @file main.c * @version v2.0.5 * @date 2022-04-02 * @brief main program */ #include "at32f421_clock.h" #include "systick.h" #include "usart.h" #include "usrkey_exint.h" /** * @brief main function. * @param none * @retval none */ int main(void) { system_clock_config(); delay_init(); // user key init usrkey_init(); // LED init gpio_init_type gpio_init_struct; crm_periph_clock_enable(CRM_GPIOC_PERIPH_CLOCK, TRUE); gpio_default_para_init(&gpio_init_struct); gpio_init_struct.gpio_drive_strength = GPIO_DRIVE_STRENGTH_STRONGER; gpio_init_struct.gpio_out_type = GPIO_OUTPUT_PUSH_PULL; gpio_init_struct.gpio_mode = GPIO_MODE_OUTPUT; gpio_init_struct.gpio_pins = GPIO_PINS_13; gpio_init_struct.gpio_pull = GPIO_PULL_NONE; gpio_init(GPIOC, &gpio_init_struct); // exint init usrkey_exint_init(); //USART init uart_print_init(115200); /* output a message on hyperterminal using printf function */ printf("waiting for user key pressing down...\r\n"); // LED blink GPIOC->odt ^= GPIO_PINS_13; delay_sec(1); GPIOC->odt ^= GPIO_PINS_13; delay_ms(1); printf("current User Key pin value: %d\r\n", usrkey_state()); while (1) { // 轮询状态,等待中断发生 printf("Line 7 state bit: %d \t",exint_flag_get(EXINT_LINE_USRKEY_PIN)); printf("current User Key pin value: %d\t", usrkey_state()); printf("is User Key IDLE: %d\r\n", is_usrkey_pressed()); delay_ms(500); } }
- at32f421_int.c:由于在启动文件startup_at32f421.s中,对EXINT的_IRQHandler进行了弱定义[WEAK]。在配置完void nvic_irq_enable();函数后,在工程的任意位置编写同名的中断处理函数即可(建议统一置于at32f421_int.c方便代码管理和移植)
/** * @brief exint0 interrupt handler * @param none * @retval none */ void EXINT15_4_IRQHandler(void) { usrkey_isr(); }
- usrkey_exint.c:定义了初始化按键、查询状态、消抖的函数。定义了中断配置函数和中端服务程序。
#include "usrkey_exint.h" #include "systick.h" #include <stdio.h> // ===========================> 配置按键中断 <=========================== /** * @brief configure user key exint * @param none * @retval none */ void usrkey_exint_init(void) { exint_init_type exint_init_struct; crm_periph_clock_enable(CRM_SCFG_PERIPH_CLOCK, TRUE); // 程配置外部中断配置寄存器 SCFG_EXINTCx 以指定来自 GPIO 的中断源 scfg_exint_line_config(SCFG_PORT_SOURCE_USRKEY, SCFG_PINS_SOURCE_USRKEY); exint_default_para_init(&exint_init_struct); exint_init_struct.line_enable = TRUE; // 选择功能 线中断 或者 线事件 exint_init_struct.line_mode = EXINT_LINE_INTERRUPUT; // 使能 line0 上的中断请求和事件请求 exint_init_struct.line_select = EXINT_LINE_USRKEY_PIN; // 选择触发方式 exint_init_struct.line_polarity = EXINT_TRIGGER_FALLING_EDGE; exint_init(&exint_init_struct); nvic_priority_group_config(NVIC_PRIORITY_GROUP_4); nvic_irq_enable(EXINT15_4_IRQn, 1, 0); } /** * @brief user key handler function * @param none * @retval none */ void usrkey_isr(void) { /* check exint line x flag again */ if(exint_flag_get(EXINT_LINE_USRKEY_PIN) != RESET) { printf("An external user key interruption is detected.\r\n"); printf("current User Key pin value: %d\t", usrkey_state()); printf("is User Key IDLE: %d\r\n", is_usrkey_pressed()); // LED blink GPIOC->odt ^= GPIO_PINS_13; delay_sec(1); GPIOC->odt ^= GPIO_PINS_13; delay_ms(1); /* clear interrupt pending bit */ exint_flag_clear(EXINT_LINE_USRKEY_PIN); } } // ===========================> 配置用户按键 <=========================== /** * @brief configure user key gpio * @param none * @retval none */ void usrkey_init(void) { gpio_init_type gpio_init_struct; /* enable the user key gpio clock */ crm_periph_clock_enable(USRKEY_CRM_CLK, TRUE); /* set default parameter */ gpio_default_para_init(&gpio_init_struct); /* configure user key pin as input with pull-up/pull-down */ gpio_init_struct.gpio_drive_strength = GPIO_DRIVE_STRENGTH_STRONGER; gpio_init_struct.gpio_mode = GPIO_MODE_INPUT; gpio_init_struct.gpio_pins = USRKEY_PIN; // 在我的板上 USRKEY_PIN 外部只有浮空(IDLE)和下拉(PRESS_DOWN)两种状态。out type可认为是开漏,因此需要内部上拉 gpio_init_struct.gpio_out_type = GPIO_OUTPUT_OPEN_DRAIN; gpio_init_struct.gpio_pull = GPIO_PULL_NONE; gpio_init(USRKEY_PORT, &gpio_init_struct); } /** * @brief returns USRKEY_PIN value * @param none * @retval the USRKEY_PIN value */ uint8_t usrkey_state(void) { return gpio_input_data_bit_read(USRKEY_PORT, USRKEY_PIN); } /** * @brief returns whether the user key be pressed down or not * @param none * @retval the uer key state */ usrkey_state_type is_usrkey_pressed() { static uint8_t pressed = 0; /* get user key state */ if ((pressed == 0) && (usrkey_state() == RESET)) { /* debounce */ pressed = 1; delay_ms(5); if (usrkey_state() != SET) return PRESS_DOWN; } else if (usrkey_state() == SET) { pressed = 0; } return IDLE; }