普中STM32F103ZET6开发攻略(三)
接续上文:普中STM32F103ZET6开发攻略(二)-CSDN博客
点关注不迷路哟。你的点赞、收藏,一键三连,是我持续更新的动力哟!!!
目录
3.GPIO端口实验_中断控制:
3.1 实验目的:
3.2 实验环境:
3.3 实验原理
3.4 实验思路
3.5 实验代码
3.5.1 LED头、源
3.5.2 key头、源
3.5.3 beep头、源
3.5.4 exit头、源
3.5.5 实验现象
3.6 实验思考和拓展
3.6.1 如何增强中断服务函数中的按键消抖处理,使其更加可靠?
3.6.2 如何通过修改中断优先级,实现不同按键间的优先级控制?
3.6.3 如何在中断服务函数中实现按键长按和短按的区分?
3.6.4 如何实现组合按键功能(同时按下多个按键)?在中断方式下有何困难?
3.6.5 中断方式与轮询方式检测按键各有什么优缺点?在什么场景下选择中断方式更合适?
3.7 注意事项
3.GPIO端口实验_中断控制:
3.1 实验目的:
-
熟悉STM32F10x微控制器的外部中断(EXTI)系统结构和基本操作
-
掌握STM32标准库函数对外部中断的配置方法
-
学会使用中断方式处理按键输入,控制LED和蜂鸣器的工作状态
-
理解中断优先级设置和中断服务函数编写原则
3.2 实验环境:
-
开发板:STM32F103ZET6
-
IDE:Keil MDK 5 /Visual Studio
-
调试工具:CMSIS-DAP
3.3 实验原理
1. *外部中断基本原理*
STM32的外部中断系统可以监测IO口电平变化,并在符合预设触发条件时执行中断服务函数。触发条件包括:
上升沿触发:电平从低变高时触发中断
下降沿触发:电平从高变低时触发中断
双边沿触发:电平变化时均触发中断 本实验中KEY_UP按键使用上升沿触发,KEY0、KEY1、KEY2按键使用下降沿触发。
2. *中断优先级控制*
STM32微控制器通过嵌套向量中断控制器(NVIC)管理各种中断请求。中断优先级分为抢占优先级和子优先级:
抢占优先级:决定是否可以打断当前正在执行的中断服务函数
子优先级:决定同时发生的同级抢占优先级中断的执行顺序
当抢占优先级相同时比较子优先级谁的数字更小谁先执行
3. *中断消抖技术*
机械按键在中断模式下仍需要消抖处理,通常的做法是在中断服务函数中加入短时延时,再次检测按键状态以确认。
4. *LED和蜂鸣器控制原理*
本实验板上的LED采用共阳极连接方式,GPIO输出低电平时LED点亮;蜂鸣器则是GPIO输出高电平时发声。
3.4 实验思路
3.5 实验代码
3.5.1 LED头、源
led.h
#ifndef __LED_H #define __LED_H #include "stm32f10x.h" #define LED0_GPIO_PORT GPIOB #define LED0_GPIO_PIN GPIO_Pin_5 //LED0 #define LED1_GPIO_PORT GPIOE #define LED1_GPIO_PIN GPIO_Pin_5 //LED1 void LED_Init(void); void LED0_TOGGLE(void); void LED1_ON(void); void LED1_OFF(void); #endif
led.c
#include "led.h" void LED_Init(void) {GPIO_InitTypeDef GPIO_InitStructure;// 开启 GPIOB 和 GPIOE 的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOE, ENABLE); //配置PB5(LED0)GPIO_InitStructure.GPIO_Pin = LED0_GPIO_PIN;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(LED0_GPIO_PORT, &GPIO_InitStructure); //配置PE5(LED1)GPIO_InitStructure.GPIO_Pin = LED1_GPIO_PIN;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(LED1_GPIO_PORT, &GPIO_InitStructure); //低电平输出GPIO_ResetBits(LED0_GPIO_PORT, LED0_GPIO_PIN);GPIO_ResetBits(LED1_GPIO_PORT, LED1_GPIO_PIN); } void LED0_TOGGLE(void) {LED0_GPIO_PORT->ODR ^= LED0_GPIO_PIN; } void LED1_ON(void) {GPIO_ResetBits(LED1_GPIO_PORT, LED1_GPIO_PIN); } void LED1_OFF(void) {GPIO_SetBits(LED1_GPIO_PORT, LED1_GPIO_PIN); }
3.5.2 key头、源
key.h
#ifndef __KEY_H #define __KEY_H #include "stm32f10x.h" // GPIO 输入宏 #define PAin(n) (GPIO_ReadInputDataBit(GPIOA, (1 << (n)))) #define PEin(n) (GPIO_ReadInputDataBit(GPIOE, (1 << (n)))) // 按键 GPIO 宏定义 #define KEY0_PIN GPIO_Pin_4 #define KEY1_PIN GPIO_Pin_3 #define KEY2_PIN GPIO_Pin_2 #define KEY_UP_PIN GPIO_Pin_0 #define KEY_PORT GPIOE #define KEY_UP_PORT GPIOA #define KEY_UP PAin(0) #define KEY0 PEin(4) #define KEY1 PEin(3) #define KEY2 PEin(2) // 返回值宏定义 #define KEY_UP_PRESS 1 #define KEY0_PRESS 2 #define KEY1_PRESS 3 #define KEY2_PRESS 4 void KEY_Init(void); u8 KEY_Scan(u8 mode); #endif
key.c
#include "key.h" #include "delay.h" void KEY_Init(void) {GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOE, ENABLE); // KEY_UP: 下拉输入GPIO_InitStructure.GPIO_Pin = KEY_UP_PIN;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;GPIO_Init(KEY_UP_PORT, &GPIO_InitStructure); // KEY0, KEY1, KEY2: 上拉输入GPIO_InitStructure.GPIO_Pin = KEY0_PIN | KEY1_PIN | KEY2_PIN;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;GPIO_Init(KEY_PORT, &GPIO_InitStructure); } u8 KEY_Scan(u8 mode) {static u8 key_up = 1; if (mode) key_up = 1; if (key_up && (KEY_UP || !KEY0 || !KEY1 || !KEY2)){Delay_ms(10); // 消抖key_up = 0;if (KEY_UP) return KEY_UP_PRESS;if (!KEY0) return KEY0_PRESS;if (!KEY1) return KEY1_PRESS;if (!KEY2) return KEY2_PRESS;}else if (!KEY_UP && KEY0 && KEY1 && KEY2){key_up = 1;} return 0; }
3.5.3 beep头、源
bepp.h
#ifndef __BEEP_H #define __BEEP_H #include "stm32f10x.h" #define BEEP_GPIO_PORT GPIOB #define BEEP_GPIO_PIN GPIO_Pin_8 void BEEP_Init(void); void BEEP_ON(void); void BEEP_OFF(void); #endif
beep.c
#include "beep.h" void BEEP_Init(void) {GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //初始化各项参数:GPIO_InitStructure.GPIO_Pin = BEEP_GPIO_PIN;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(BEEP_GPIO_PORT, &GPIO_InitStructure); GPIO_ResetBits(BEEP_GPIO_PORT, BEEP_GPIO_PIN); // 默认关闭蜂鸣器 } void BEEP_OFF(void) {GPIO_ResetBits(BEEP_GPIO_PORT, BEEP_GPIO_PIN); } void BEEP_ON(void) {GPIO_SetBits(BEEP_GPIO_PORT, BEEP_GPIO_PIN); }
3.5.4 exit头、源
exit.h
#ifndef __EXTI_H #define __EXTI_H #include "stm32f10x.h" // 基础库头文件 void my_exti_int(void); // 外部中断初始化函数声明 #endif
exit.c
#include "exti.h" #include "key.h" void my_exti_int(void) {EXTI_InitTypeDef EXTI_InitStructure;NVIC_InitTypeDef NVIC_InitStructure;GPIO_InitTypeDef GPIO_InitStructure;// 开启 GPIO 时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // AFIO 复用功能RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);// 开启 GPIO 时钟:灯光RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE); // 配置 KEY_UP 为下拉输入模式(PA0)//GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Pin = KEY_UP_PIN;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; // 下拉输入GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置 KEY0~2 为上拉输入模式(PE2、PE3、PE4)GPIO_InitStructure.GPIO_Pin = KEY0_PIN | KEY1_PIN | KEY2_PIN;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;GPIO_Init(GPIOE, &GPIO_InitStructure); // GPIO 映射到 EXTI 通道GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); // KEY_UP -> EXTI0GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource4); // KEY0 -> EXTI4GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource3); // KEY1 -> EXTI3GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource2); // KEY2 -> EXTI2 // 配置 EXTI0(KEY_UP)EXTI_InitStructure.EXTI_Line = EXTI_Line0;EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; // 上升沿触发EXTI_InitStructure.EXTI_LineCmd = ENABLE;//使能特定的 EXTI 中断线EXTI_Init(&EXTI_InitStructure); // 配置 EXTI2(KEY2)EXTI_InitStructure.EXTI_Line = EXTI_Line2;EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发EXTI_InitStructure.EXTI_LineCmd = ENABLE;EXTI_Init(&EXTI_InitStructure); // 配置 EXTI3(KEY1)EXTI_InitStructure.EXTI_Line = EXTI_Line3;EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发EXTI_InitStructure.EXTI_LineCmd = ENABLE;EXTI_Init(&EXTI_InitStructure); // 配置 EXTI4(KEY0)EXTI_InitStructure.EXTI_Line = EXTI_Line4;EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发EXTI_InitStructure.EXTI_LineCmd = ENABLE;EXTI_Init(&EXTI_InitStructure); // 配置 NVIC 中断优先级// EXTI0_IRQnNVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;NVIC_Init(&NVIC_InitStructure); // EXTI2_IRQnNVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01;NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;NVIC_Init(&NVIC_InitStructure); // EXTI3_IRQnNVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03;NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;NVIC_Init(&NVIC_InitStructure); // EXTI4_IRQnNVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00;NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;NVIC_Init(&NVIC_InitStructure); }
分析:
在 STM32 系列微控制器中,GPIO 引脚映射到 EXTI(外部中断 / 事件控制器)通道时,不同的 EXTI 通道具有以下区别:
通道编号与引脚限制
STM32 的 EXTI 通道分为两种类型:
-
0-15 号通道:每个通道对应一组 GPIO 引脚中的
同一位号。
-
例如:
EXTI0
只能连接到所有 GPIO 端口的Pin0(如 PA0、PB0、PC0 等)。 -
同一时刻,每个通道只能映射一个 GPIO 引脚。
-
-
16-22 号通道:用于特殊功能(如 RTC 闹钟、USB 唤醒等),与 GPIO 无关。
总结
-
通道编号决定中断函数:不同通道对应不同的中断服务函数。
-
同一通道只能映射一个引脚:例如
EXTI0
不能同时连接 PA0 和 PB0。 -
触发方式和优先级独立配置:每个通道可单独设置触发条件和中断优先级。
通过合理分配 EXTI 通道,你可以高效处理多个按键的中断事件。
EXTI_InitStructure.EXTI_LineCmd = ENABLE;`EXTI_LineCmd = ENABLE`是启用外部中断功能必不可少的一步。只有完成了这一步,当 GPIO 引脚出现电平变化时,才会触发相应的中断服务函数。
3.5.5 实验现象
-
DS0指示灯会不断闪烁(200ms翻转一次)
-
按下KEY_UP键,DS1指示灯点亮
-
按下KEY2键,DS1指示灯熄灭
-
按下KEY1键,蜂鸣器发声
-
按下KEY0键,蜂鸣器停止发声
3.6 实验思考和拓展
3.6.1 如何增强中断服务函数中的按键消抖处理,使其更加可靠?
改进方案:
-
硬件消抖 + 软件消抖结合
-
硬件:添加 0.1μF 电容到按键两端,滤除高频抖动
-
软件:采用定时器消抖,避免阻塞中断服务函数
-
-
状态机消抖
-
将按键状态分为:按下抖动→稳定按下→释放抖动→稳定释放
-
使用定时器确认状态变化的稳定性
-
基本原理:
状态机消抖将按键的整个生命周期划分为多个稳定状态和过渡状态,利用定时器对每个状态的持续时间进行验证,只有当信号在足够长的时间内保持稳定时,才认为状态发生了有效变化。
-
典型状态划分:
-
IDLE(空闲状态):按键未被按下,引脚保持高电平。
-
PRESS_DEBOUNCE(按下抖动):检测到引脚电平下降,进入消抖验证。
-
PRESSED(稳定按下):确认按键已稳定按下。
-
RELEASE_DEBOUNCE(释放抖动):检测到引脚电平上升,进入释放消抖。
-
-
3.6.2 如何通过修改中断优先级,实现不同按键间的优先级控制?
实现步骤:
-
配置 NVIC 优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 2位抢占+2位子优先级
-
设置不同的抢占优先级
// 紧急按键(如复位) NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 最高抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 普通按键 NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 次高抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
-
优先级规则:
-
抢占优先级高的中断可打断正在执行的低优先级中断
-
抢占优先级相同的中断按子优先级排序,但不能互相打断。子优先级数字越小,优先级就越高~~~
-
3.6.3 如何在中断服务函数中实现按键长按和短按的区分?
方案设计:
-
时间阈值法:
-
按下时记录时间戳,释放时计算持续时间
-
超过 2 秒为长按,否则为短按
-
-
示例代码:
static uint32_t pressTime = 0; static bool longPressDetected = false; void EXTI0_IRQHandler(void) {if(EXTI_GetITStatus(EXTI_Line0) != RESET) {if(KEY_PIN == 0) { // 按下pressTime = HAL_GetTick();longPressDetected = false;HAL_TIM_Base_Start_IT(&htim2); // 启动长按检测定时器} else { // 释放HAL_TIM_Base_Stop_IT(&htim2);if(!longPressDetected) {ShortPressCallback(); // 短按处理}}EXTI_ClearITPendingBit(EXTI_Line0);} } // 长按检测定时器回调 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {if(htim == &htim2 && KEY_PIN == 0) {if(HAL_GetTick() - pressTime > 2000) { // 2秒长按longPressDetected = true;LongPressCallback();HAL_TIM_Base_Stop_IT(&htim2);}} }
3.6.4 如何实现组合按键功能(同时按下多个按键)?在中断方式下有何困难?
组合按键实现:
-
轮询方式:
void ScanKeyCombination(void) {static uint8_t keyState = 0;keyState = (KEY1 << 0) | (KEY2 << 1);switch(keyState) {case 0b00: Key1AndKey2Pressed(); break;case 0b01: Key1Pressed(); break;case 0b10: Key2Pressed(); break;} }
-
中断方式的困难:
-
时序同步问题:多个按键的中断可能不同时触发
-
抖动叠加:多个按键同时按下时抖动可能互相影响
-
解决方案:
-
检测到第一个按键中断时启动定时器,在定时窗口内检测其他按键
-
使用状态机记录各按键状态变化
-
-
3.6.5 中断方式与轮询方式检测按键各有什么优缺点?在什么场景下选择中断方式更合适?
特性 | 中断方式 | 轮询方式 |
---|---|---|
响应速度 | 即时响应,不受主程序影响 | 依赖扫描周期,可能有延迟 |
CPU 占用 | 空闲时不占用 CPU | 持续占用 CPU(即使无按键操作) |
资源消耗 | 需要配置 NVIC、EXTI 等资源 | 仅需 GPIO 读取 |
实现复杂度 | 较高(需处理中断优先级、抖动) | 较低(简单循环检测) |
多按键处理 | 适合少量紧急按键 | 适合大量按键同时检测 |
适用场景 | 实时性要求高(如安全关键系统) | 按键数量多、响应时间要求宽松 |
推荐使用中断的场景:
-
低功耗设计(仅在按键操作时唤醒 CPU)
-
紧急停机、复位等关键功能
-
要求立即响应的人机交互(如游戏控制器)
3.7 注意事项
(1) 外部中断的触发方式需要与按键接线方式一致(KEY_UP用上升沿触发,其他按键用下降沿触发)
(2) 中断服务函数中必须清除中断标志位,否则会造成中断反复触发
(3) 中断服务函数应尽量简短,避免长时间占用CPU
(4) 使用标准库函数时,需要注意头文件的包含和依赖关系
(5) GPIO和外部中断初始化前必须先使能相应的外设时钟
(6) 避免在中断服务函数中使用过长的延时函数
文章有写的不当的地方,欢迎在评论区中指正修改。如果感觉文章实用对你有帮助,欢迎点赞收藏和关注,你的点赞关注就是我动力,大家一起学习进步。
有不懂的可以在评论区里提出来哟,博主看见后会及时回答的。