STM32
第一章:从 GPIO 到中断系统
本节总结
这一节从 STM32F103 最基础、也最容易反复用到的 GPIO 开始,一路串到外部中断系统。核心目标不是背标准库函数,而是建立一条稳定的外设初始化思路:先用 RCC 给外设开时钟,再用 GPIO_Init 配置引脚模式;如果要让外部信号触发 CPU 响应,就继续配置 AFIO、EXTI 和 NVIC,最后在固定名字的 IRQHandler 里处理事件并清除中断挂起标志。
和 51 单片机相比,STM32 进入主循环后的读输入、写输出、计数、判断逻辑并不陌生,真正增加的是外设使用前的配置链条。GPIO 输出对应 LED 闪烁和 LED 模块封装;GPIO 输入对应按键读取、上拉输入和软件消抖;外部中断则把 PB14 这样的输入引脚,经 AFIO 映射到 EXTI14,再交给 NVIC 管理优先级和 CPU 响应。
本章四份文档分别承担不同任务:整合补充稿负责系统讲清主线;补充讲述稿负责用上课口吻把概念讲顺;代码链与模块对应底稿负责整理模板和调用链;学习复盘与问答底稿负责课后自测。后面学习定时器、PWM、串口、ADC 时,RCC、GPIO 模式、NVIC 和 IRQHandler 这些思想会不断复用。
整合补充稿
STM32 第一组笔记:从 GPIO 到中断系统
> 本文参考本地 STM32F103 资料、江科大 STM32 标准库源码示例,以及 CSDN 汇总学习路线整理。目标不是背 API,而是把“为什么要开时钟、为什么要配置模式、为什么外部中断还要 AFIO 和 NVIC”讲清楚。
0. 这一组到底要学会什么
从 51 过渡到 STM32,最容易卡住的地方不是 C 语言,而是外设使用前的初始化链条。
51 里点灯经常像这样:
P2_0 = 0;STM32 里不能一上来就写引脚,因为 GPIO 本身也是一个外设。使用外设之前,至少要解决三件事:
1. 这个外设有没有时钟?
2. 这个引脚工作在输入还是输出?推挽还是开漏?上拉还是下拉?
3. 如果要中断,信号从 GPIO 引脚如何一路送到 CPU?本组主线:
GPIO 输出
-> LED 闪烁 / LED 模块封装
GPIO 输入
-> 按键读取 / 软件消抖
外部中断
-> GPIO 输入引脚
-> AFIO 选择 EXTI 线路来源
-> EXTI 判断边沿并产生中断请求
-> NVIC 管理中断优先级和开关
-> IRQHandler 中断服务函数1. STM32 和 51 的核心差异
你说“很多代码和 51 一样”,这个判断很对。进入 `while(1)` 之后,很多业务逻辑确实类似:
读一个输入
判断状态
改一个输出
计数
显示真正不同的是外设初始化阶段。51 的外设比较少,很多端口上电后就能直接用;STM32 外设多、模式多、总线多,所以 ST 把大部分外设都做成“默认不工作,先开时钟再配置”。
可以粗略类比:
51:
引脚更像默认可用的简单开关
STM32:
引脚是挂在总线上的可配置外设
不开时钟,寄存器配置通常不会生效这就是为什么 STM32 第一行经常是:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);2. RCC:为什么要先开时钟
RCC 是 Reset and Clock Control,复位和时钟控制模块。STM32 为了省电,很多外设默认时钟关闭。GPIOA、GPIOB、AFIO 都挂在 APB2 总线上,所以使用它们时要开 APB2 外设时钟。
常见写法:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);含义:
RCC_APB2Periph_GPIOA
打开 GPIOA 外设时钟
RCC_APB2Periph_GPIOB
打开 GPIOB 外设时钟
RCC_APB2Periph_AFIO
打开复用功能/外部中断映射相关外设时钟一句话记忆:
凡是要配置某个外设,先找它挂在哪条总线,再打开对应 RCC 时钟。3. GPIO 输出:点灯的完整链条
LED 闪烁示例的核心流程:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
while (1)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_0);
Delay_ms(500);
GPIO_SetBits(GPIOA, GPIO_Pin_0);
Delay_ms(500);
}拆开看:
1. 开 GPIOA 时钟
2. 定义 GPIO 初始化结构体
3. 设置模式为推挽输出
4. 选择 PA0
5. 设置输出速度
6. 调 GPIO_Init 写入底层寄存器
7. 主循环里设置高低电平这里的 `GPIO_InitTypeDef` 本质是把多个寄存器配置项打包成一个结构体,让库函数统一写寄存器。
4. 推挽输出和开漏输出
GPIO 输出最常见的是:
GPIO_Mode_Out_PP
通用推挽输出
GPIO_Mode_Out_OD
通用开漏输出
GPIO_Mode_AF_PP
复用推挽输出
GPIO_Mode_AF_OD
复用开漏输出初学阶段先抓两个:
推挽输出:
高电平能主动输出 1,低电平能主动输出 0
适合普通 LED、蜂鸣器控制等
开漏输出:
低电平能主动拉低,高电平依赖上拉电阻
适合 I2C、多设备线与、需要电平转换的场景LED 常用推挽输出,因为它需要明确输出高低电平。
5. GPIO_SetBits、ResetBits、WriteBit 的含义
标准库常用输出函数:
GPIO_SetBits(GPIOA, GPIO_Pin_0);
GPIO_ResetBits(GPIOA, GPIO_Pin_0);
GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET);
GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET);含义:
SetBits
把指定引脚输出高电平
ResetBits
把指定引脚输出低电平
WriteBit
根据第三个参数写高或写低注意:很多开发板 LED 是低电平点亮。也就是:
GPIO_ResetBits -> LED 亮
GPIO_SetBits -> LED 灭这不是函数反了,而是 LED 接法决定的。
6. GPIO 输入:按键读取
按键示例使用 PB1 和 PB11,上拉输入:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);`GPIO_Mode_IPU` 是上拉输入。它的意思是:
按键未按下:
引脚被内部上拉电阻拉到高电平,读到 1
按键按下:
引脚接地,读到 0所以按键判断常写:
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
// 按键按下
}7. 软件消抖
机械按键按下和松开时,电平不会理想地从 1 直接跳到 0,而会抖动几毫秒。
常见消抖流程:
1. 发现按下
2. 延时 20ms
3. 等待松手
4. 再延时 20ms
5. 返回键码示例逻辑:
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
Delay_ms(20);
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0);
Delay_ms(20);
KeyNum = 1;
}这是一种阻塞式按键扫描。优点是简单,缺点是按键按住时程序会卡在 `while` 等待松手。
8. 从轮询到中断
按键扫描属于轮询:
CPU 一直主动问:
按了吗?
按了吗?
按了吗?外部中断则是:
外部信号变化
-> 硬件自动通知 CPU
-> CPU 暂停主程序
-> 进入中断服务函数
-> 处理完再回到主程序这和 51 的外部中断思想一样,只是 STM32 中间多了 EXTI、AFIO、NVIC 几层配置。
9. EXTI 外部中断的完整链条
以 PB14 下降沿计数为例:
PB14 引脚电平变化
-> AFIO 把 PB14 映射到 EXTI14
-> EXTI14 检测下降沿
-> EXTI 产生中断请求
-> NVIC 接收 EXTI15_10_IRQn
-> CPU 进入 EXTI15_10_IRQHandler
-> 软件判断 EXTI_Line14
-> CountSensor_Count++
-> 清除中断挂起标志关键代码:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);
EXTI_InitStructure.EXTI_Line = EXTI_Line14;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);10. 为什么要 AFIO
EXTI 只有线路编号,没有天然绑定某个具体端口。
比如 EXTI14 可以来自:
PA14
PB14
PC14
...但是同一时间 EXTI14 只能选择一个来源。所以必须用 AFIO 告诉芯片:
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);含义:
把 EXTI14 这条外部中断线连接到 GPIOB 的 14 号引脚,也就是 PB14。一句话记忆:
GPIO 负责电平输入,AFIO 负责把哪个端口的哪个脚接到哪条 EXTI 线。11. EXTI 和 NVIC 的分工
EXTI 和 NVIC 很容易混在一起,分清它们很重要。
EXTI:
关心外部中断线
负责选择 Line
负责上升沿/下降沿触发
负责产生挂起标志
NVIC:
关心 Cortex-M3 内核中断管理
负责使能某个 IRQ 通道
负责优先级
负责决定 CPU 是否响应、谁先响应可以类比:
EXTI 是门口的门铃线路。
NVIC 是保安调度中心,决定哪个门铃优先处理。12. EXTI 线路和 IRQHandler 名字
STM32F103 的 EXTI 线路和中断通道不是一一独立到 16 个函数。
常见分组:
EXTI0 -> EXTI0_IRQHandler
EXTI1 -> EXTI1_IRQHandler
EXTI2 -> EXTI2_IRQHandler
EXTI3 -> EXTI3_IRQHandler
EXTI4 -> EXTI4_IRQHandler
EXTI5~9 -> EXTI9_5_IRQHandler
EXTI10~15 -> EXTI15_10_IRQHandlerPB14 属于 EXTI14,所以中断函数必须写:
void EXTI15_10_IRQHandler(void)
{
...
}函数名不能随便改,因为启动文件的中断向量表里已经写好了这些名字。
13. 中断服务函数里必须清标志
外部中断服务函数中常见写法:
void EXTI15_10_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line14) == SET)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0)
{
CountSensor_Count++;
}
EXTI_ClearITPendingBit(EXTI_Line14);
}
}最后一行非常关键:
EXTI_ClearITPendingBit(EXTI_Line14);如果不清除挂起标志,CPU 会认为中断一直存在,可能反复进入中断服务函数,主程序像“卡死”一样无法正常运行。
14. 中断优先级:抢占优先级和响应优先级
NVIC 有两个优先级概念:
抢占优先级:
决定一个中断能不能打断另一个正在执行的中断
响应优先级:
当多个中断同时等待时,决定谁先被响应先配置分组:
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);然后配置具体中断:
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;初学时先记住:
工程里优先级分组通常只配置一次。
数字越小,优先级越高。15. 本组最终理解
GPIO 到中断系统,本质是从“CPU 主动查”走到“硬件主动报”。
GPIO 输出:
CPU 写 ODR/BSRR 等寄存器,让引脚输出高低电平
GPIO 输入:
CPU 读 IDR 等寄存器,获得引脚当前电平
EXTI 中断:
引脚电平变化被硬件捕获
EXTI 产生中断请求
NVIC 通知 CPU
CPU 自动跳到中断服务函数如果用一句话讲给别人听:
> STM32 的 GPIO 不难,难的是它比 51 多了一套严谨的外设管理流程:先用 RCC 给外设供时钟,再用 GPIO_Init 配模式;如果要外部中断,还要用 AFIO 把具体引脚接到 EXTI 线,再用 EXTI 配触发条件,最后用 NVIC 允许 CPU 响应这个中断。真正进入主循环之后,读输入、写输出、计数这些逻辑仍然和 51 很像。
补充讲述稿
STM32 第一组补充讲述稿:GPIO、EXTI、NVIC 怎么连起来
> 这份稿子按“上课讲给自己听”的口吻写。它适合复习时顺着念一遍,目标是把 API 背后的硬件链路讲顺。
0. 先建立总图
我们现在学的是 STM32F103 标准库。标准库不是魔法,它只是把寄存器配置包装成函数和结构体。
GPIO 到中断系统可以先画成:
RCC
-> 给 GPIO/AFIO 等外设开时钟
GPIO
-> 配置引脚输入/输出模式
-> 读输入或写输出
AFIO
-> 外部中断时,选择 EXTIx 来自哪个 GPIO 端口
EXTI
-> 配置哪条外部中断线
-> 配置上升沿/下降沿
-> 产生中断挂起标志
NVIC
-> 使能 CPU 侧中断通道
-> 管理优先级
IRQHandler
-> 真正写中断发生后要做什么这条链路非常重要。很多 STM32 bug 都不是 C 语法错,而是链条漏了一环。
1. 为什么 51 可以直接写口,STM32 要先初始化
51 学习时,我们很容易形成一个印象:端口就是变量,写 0 写 1 就行。
STM32 的端口更复杂。一个引脚可以当普通 GPIO,也可以当串口 TX/RX、定时器通道、SPI、I2C、ADC 等。既然一个引脚能承担多种功能,芯片就必须让你先说明:
这个引脚现在要干什么?
是输入还是输出?
输出时是推挽还是开漏?
输入时要不要上拉/下拉?
速度是多少?
如果做中断,触发方式是什么?所以 STM32 的初始化不是多余模板,而是在告诉硬件“这根引脚这次扮演什么角色”。
2. 点灯:最小 GPIO 输出模型
点灯这件事,本质是让一个 GPIO 引脚输出高低电平。
完整步骤:
1. 打开 GPIOA 时钟
2. 设置 PA0 为推挽输出
3. 调 GPIO_Init 写入配置
4. 用 SetBits/ResetBits 控制电平为什么要推挽?因为普通 LED 控制希望引脚能主动输出高电平,也能主动输出低电平。
推挽输出可以想成引脚内部有两个“开关”:
输出 1:
上管导通,把引脚接到 VCC
输出 0:
下管导通,把引脚接到 GND所以推挽输出驱动能力强,适合普通数字输出。
3. LED 低电平点亮为什么容易迷惑
江科大示例里常见:
GPIO_ResetBits(GPIOA, GPIO_Pin_1); // LED1_ON
GPIO_SetBits(GPIOA, GPIO_Pin_1); // LED1_OFF初看会觉得反了。其实是 LED 接法导致的。
很多板子是:
VCC -> 电阻 -> LED -> GPIO 引脚当 GPIO 输出低电平时,电流从 VCC 流过 LED 到 GPIO,LED 亮。
当 GPIO 输出高电平时,LED 两端电压差很小,LED 灭。
所以要养成一个习惯:
函数写的是引脚电平,不一定等于外设的“开/关”。
外设开关逻辑由电路连接决定。4. 按键:GPIO 输入和上拉输入
按键读取用的是 GPIO 输入。常见接法是一端接地,另一端接 GPIO。为了避免没按时引脚悬空,使用内部上拉:
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;这时状态是:
没按:
内部上拉让引脚读到 1
按下:
引脚被接到 GND,读到 0所以代码判断按下时常写 `== 0`。
这和 51 里“按键按下读 0”的很多开发板接法很像。
5. 软件消抖为什么要等待松手
按键函数里常见:
Delay_ms(20);
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0);
Delay_ms(20);这段不是为了让程序慢,而是为了得到一个清晰的“一次按键事件”。
如果没有等待松手,主循环跑得很快,一次长按可能被识别成很多次按下。
这套写法的效果是:
按下以后只返回一次键码
松手后才允许下一次按键被识别代价是阻塞。后面学定时器和中断后,可以做非阻塞按键扫描。
6. 为什么需要中断
轮询的思路是 CPU 一直主动检查:
while (1) {
if (按键按下) {
处理;
}
}中断的思路是硬件有事再叫 CPU:
主程序正常做自己的事
外部信号变化
CPU 自动跳到中断服务函数
处理完再回到主程序在计数传感器、编码器、红外对射等场景里,中断比轮询可靠,因为脉冲可能很短,主循环不一定刚好读到。
7. 外部中断为什么要 GPIO + AFIO + EXTI + NVIC
这是今天最关键的地方。
51 里外部中断入口比较固定,比如 INT0、INT1。STM32 引脚多,中断线也更灵活。它要解决两个问题:
1. 哪个引脚作为中断输入?
2. 这个中断请求送到 CPU 后,CPU 是否响应、优先级如何?所以分成几层:
GPIO:
负责引脚电平输入
AFIO:
负责把 PB14 这类具体引脚映射到 EXTI14
EXTI:
负责检测 EXTI14 的边沿变化
NVIC:
负责 Cortex-M3 内核是否接收这个中断把 PB14 配成下降沿中断,就是:
PB14 输入
-> AFIO 选择 PB14 接 EXTI14
-> EXTI14 配成下降沿触发
-> NVIC 使能 EXTI15_10_IRQn
-> 写 EXTI15_10_IRQHandler8. 为什么 PB14 的函数叫 EXTI15_10_IRQHandler
EXTI 线有 0~15,对应外部引脚编号。但 NVIC 的中断通道会把某些线合并。
EXTI0 单独一个 IRQ
EXTI1 单独一个 IRQ
EXTI2 单独一个 IRQ
EXTI3 单独一个 IRQ
EXTI4 单独一个 IRQ
EXTI5~9 共用 EXTI9_5_IRQn
EXTI10~15 共用 EXTI15_10_IRQnPB14 属于 EXTI14,所以它进入 `EXTI15_10_IRQHandler`。
因为 10~15 共用一个中断函数,所以函数里要判断到底是哪条线触发:
if (EXTI_GetITStatus(EXTI_Line14) == SET)
{
...
}9. 中断服务函数的两个规矩
第一,中断函数名不能乱写。
名字来自启动文件的中断向量表。CPU 不是按 C 函数调用规则找你随便起的名字,而是中断向量表里绑定了固定符号。
第二,中断处理完要清 pending bit。
EXTI_ClearITPendingBit(EXTI_Line14);否则 EXTI 会一直认为“这条线还有中断没处理”,CPU 就会不断重新进入中断。
10. 今天这组怎么和后面连接
GPIO 和中断是后面所有外设的基础。
OLED:
本质还是 GPIO 模拟通信或外设通信
定时器:
会用到中断、优先级、IRQHandler
PWM:
引脚要配置为复用推挽输出
串口:
TX/RX 是 GPIO 复用功能,也会用 NVIC 接收中断
ADC/DMA:
仍然需要 RCC、GPIO 模式、外设初始化所以你现在觉得“STM32 主要麻烦在时钟、模式、使能”,这个感觉非常准。只要把初始化链条练熟,后面的外设就会像拼积木。
代码链与模块对应底稿
STM32 GPIO 到中断系统:代码链与模块对应底稿
> 本文专门整理代码链、模块职责和模板。正式复习时先看整合稿,写代码前看这一份。
1. 本地参考源码
本组主要参考:
程序源码/程序源码/STM32Project-有注释版/3-1 LED闪烁
程序源码/程序源码/STM32Project-有注释版/3-4 按键控制LED
程序源码/程序源码/STM32Project-有注释版/5-1 对射式红外传感器计次对应模块:
| 示例 | 主要文件 | 学习目标 | |---|---|---| | 3-1 LED闪烁 | User/main.c | GPIO 输出最小模板 | | 3-4 按键控制LED | Hardware/LED.c, Hardware/Key.c | GPIO 模块封装、输入读取、消抖 | | 5-1 对射式红外传感器计次 | Hardware/CountSensor.c | EXTI、AFIO、NVIC、中断函数 |
2. GPIO 输出最小代码链
main
-> RCC_APB2PeriphClockCmd(GPIOA)
-> GPIO_InitTypeDef 填参数
-> GPIO_Init(GPIOA, &GPIO_InitStructure)
-> while(1)
-> GPIO_ResetBits / GPIO_SetBits模板:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_ResetBits(GPIOA, GPIO_Pin_0);
GPIO_SetBits(GPIOA, GPIO_Pin_0);对应寄存器思想:
RCC_APB2PeriphClockCmd
-> 打开 GPIOA 外设时钟
GPIO_Init
-> 配置 CRL/CRH 中的 MODE 和 CNF 位
GPIO_SetBits / ResetBits
-> 写 BSRR/BRR 或相关输出控制寄存器3. LED 模块封装代码链
main
-> LED_Init()
-> LED1_ON()
-> LED1_OFF()
-> LED1_Turn()`LED_Init`:
void LED_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_1 | GPIO_Pin_2);
}模块化意义:
main.c 不直接关心 LED 接在哪个引脚
main.c 只调用 LED1_ON / LED1_OFF / LED1_Turn
硬件细节收进 LED.c这和后面写 `Key.c`、`OLED.c`、`CountSensor.c` 是同一套路。
4. GPIO 输入代码链
main
-> Key_Init()
-> while(1)
-> Key_GetNum()
-> 根据键码控制 LED`Key_Init`:
void Key_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}`Key_GetNum` 核心:
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
Delay_ms(20);
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0);
Delay_ms(20);
KeyNum = 1;
}输入模式对应:
GPIO_Mode_IPU
上拉输入,默认读 1,按下接地读 0
GPIO_Mode_IPD
下拉输入,默认读 0,按下接高电平读 1
GPIO_Mode_IN_FLOATING
浮空输入,外部必须给稳定电平
GPIO_Mode_AIN
模拟输入,ADC 常用5. 外部中断初始化代码链
CountSensor_Init
-> 开 GPIOB 时钟
-> 开 AFIO 时钟
-> PB14 配为上拉输入
-> GPIO_EXTILineConfig(GPIOB, Pin14)
-> EXTI_Line14 配置下降沿中断
-> NVIC_PriorityGroupConfig
-> NVIC 使能 EXTI15_10_IRQn完整模板:
void CountSensor_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line14;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitStructure);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
}6. 中断服务函数代码链
外部下降沿
-> EXTI14 pending = 1
-> NVIC 响应 EXTI15_10_IRQn
-> CPU 查向量表
-> EXTI15_10_IRQHandler
-> 判断 EXTI_Line14
-> Count++
-> 清除 pending模板:
void EXTI15_10_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line14) == SET)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0)
{
CountSensor_Count++;
}
EXTI_ClearITPendingBit(EXTI_Line14);
}
}注意:
1. 函数名必须和启动文件里的中断向量表一致
2. 共用 IRQHandler 时必须判断是哪条 EXTI Line
3. 处理结束必须清除 pending bit
4. 中断函数里尽量短,不要长延时,不要写阻塞逻辑7. EXTI 线路与 IRQ 通道速查
| EXTI 线 | NVIC IRQChannel | 中断函数 | |---|---|---| | EXTI0 | EXTI0_IRQn | EXTI0_IRQHandler | | EXTI1 | EXTI1_IRQn | EXTI1_IRQHandler | | EXTI2 | EXTI2_IRQn | EXTI2_IRQHandler | | EXTI3 | EXTI3_IRQn | EXTI3_IRQHandler | | EXTI4 | EXTI4_IRQn | EXTI4_IRQHandler | | EXTI5~9 | EXTI9_5_IRQn | EXTI9_5_IRQHandler | | EXTI10~15 | EXTI15_10_IRQn | EXTI15_10_IRQHandler |
8. 最容易漏的配置
GPIO 输出没反应:
可能没开 GPIO 时钟
可能端口/引脚写错
可能 LED 是低电平点亮
GPIO 输入乱跳:
可能浮空输入没有稳定电平
按键需要上拉/下拉
机械按键需要消抖
外部中断不进:
可能没开 AFIO 时钟
可能 GPIO_EXTILineConfig 端口/引脚不匹配
可能 EXTI_Line 写错
可能 NVIC_IRQChannel 写错
可能 IRQHandler 函数名写错
可能触发边沿和实际电路相反
中断一直进:
可能忘记 EXTI_ClearITPendingBit
可能信号本身抖动严重9. 本组背诵模板
GPIO 输出模板:
开时钟
填 GPIO_InitTypeDef
GPIO_Init
SetBits/ResetBitsGPIO 输入模板:
开时钟
填 GPIO_InitTypeDef 为 IPU/IPD/FLOATING
GPIO_Init
ReadInputDataBitEXTI 外部中断模板:
开 GPIO 时钟
开 AFIO 时钟
GPIO 配输入
AFIO 选 EXTI 来源
EXTI 配 Line/Mode/Trigger
NVIC 配分组和通道
写 IRQHandler
判断 Line
处理事件
清 pending学习复盘与问答底稿
STM32 第一组学习复盘与问答底稿
> 这份用于课后自测。先不要看答案,自己试着说出来;说不顺的地方,再回整合稿补。
1. 本组一句话总结
STM32 的 GPIO 和 51 的端口控制在业务逻辑上很像,但 STM32 外设更复杂,所以使用前必须先开时钟、配置模式;外部中断则需要 GPIO、AFIO、EXTI、NVIC 和 IRQHandler 多层配合。
2. 我现在应该掌握的链路
GPIO 输出:
RCC -> GPIO_Init -> SetBits/ResetBits
GPIO 输入:
RCC -> GPIO_Init -> ReadInputDataBit -> 消抖
外部中断:
RCC(GPIO/AFIO)
-> GPIO 输入
-> AFIO 映射 EXTI 线
-> EXTI 配触发边沿
-> NVIC 使能 IRQ 通道
-> IRQHandler
-> 清除 pending bit3. 自测问题
Q1:为什么 STM32 使用 GPIO 前要先开 RCC 时钟?
因为 GPIO 是挂在总线上的外设。为了省电,外设默认可能没有时钟;没有时钟时,外设寄存器无法正常工作,配置和读写就可能无效。
Q2:`GPIO_Mode_Out_PP` 是什么?
通用推挽输出。引脚可以主动输出高电平,也可以主动输出低电平,适合 LED、蜂鸣器等普通数字输出。
Q3:为什么 `GPIO_ResetBits` 可能表示 LED 亮?
因为很多开发板 LED 是低电平点亮。`GPIO_ResetBits` 只是把引脚输出低电平,LED 是否亮取决于电路接法。
Q4:按键为什么常用 `GPIO_Mode_IPU`?
如果按键一端接地,使用上拉输入可以让未按下时读到 1,按下时接地读到 0,避免引脚悬空。
Q5:软件消抖为什么要延时 20ms?
机械按键按下/松开时会有短时间电平抖动。延时可以跳过不稳定阶段,让程序只识别稳定的按下和松手。
Q6:轮询和中断有什么区别?
轮询是 CPU 主动不断检查输入;中断是外部事件发生后硬件主动通知 CPU。中断更适合短脉冲、计数、需要及时响应的场景。
Q7:外部中断为什么要开 AFIO 时钟?
因为 AFIO 负责把具体 GPIO 端口引脚映射到 EXTI 线路。例如 PB14 要作为 EXTI14 输入,就要通过 AFIO 配置。
Q8:`GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14)` 的含义是什么?
把 EXTI14 这条外部中断线的输入来源选择为 GPIOB 的 14 号引脚,也就是 PB14。
Q9:EXTI 和 NVIC 的区别是什么?
EXTI 管外部中断线和触发条件,比如哪条 Line、上升沿还是下降沿。NVIC 管 CPU 内核侧中断响应,比如是否使能某个 IRQ、优先级是多少。
Q10:PB14 为什么使用 `EXTI15_10_IRQHandler`?
因为 EXTI10 到 EXTI15 共用一个 NVIC 中断通道和中断服务函数,PB14 对应 EXTI14,所以进入 `EXTI15_10_IRQHandler`。
Q11:中断函数里为什么要判断 `EXTI_GetITStatus(EXTI_Line14)`?
因为 `EXTI15_10_IRQHandler` 可能由 EXTI10~15 任意一条线触发。需要判断是不是 EXTI14 触发的,再执行对应处理。
Q12:为什么中断服务函数最后必须清除 pending bit?
如果不清除,EXTI 会一直认为中断未处理,CPU 可能反复进入同一个中断服务函数,导致主程序无法正常运行。
Q13:抢占优先级和响应优先级区别是什么?
抢占优先级决定一个中断能不能打断另一个正在执行的中断;响应优先级决定多个同级等待中断谁先执行。
Q14:中断函数里为什么不建议写长延时?
中断执行期间会影响主程序和其他中断响应。中断函数应该短小,只做必要记录或置标志,复杂逻辑放到主循环处理。
4. 典型错误复盘
错误 1:LED 不亮
检查:
GPIO 时钟是否打开
GPIO 端口是否正确
引脚是否正确
模式是否是输出
LED 是高电平亮还是低电平亮错误 2:按键一直乱跳
检查:
是否使用上拉/下拉
电路是否悬空
是否做消抖
读取逻辑是否与电路按下电平一致错误 3:外部中断不进入
检查:
GPIO 时钟是否开
AFIO 时钟是否开
GPIO_EXTILineConfig 端口和 PinSource 是否匹配
EXTI_Line 是否匹配
EXTI_Trigger 是否和实际边沿一致
NVIC_IRQChannel 是否正确
IRQHandler 函数名是否正确错误 4:中断进入后程序卡死
检查:
是否清除 EXTI pending bit
中断输入是否抖动严重
中断函数里是否写了长延时或阻塞 while5. 我能讲给别人听的版本
STM32 点灯不是直接写端口,而是先打开 GPIO 所在总线的时钟,再配置引脚模式,最后写输出寄存器。按键输入也是一样,先配置输入模式,再读取输入电平。外部中断比普通输入多了几层:GPIO 只负责引脚电平,AFIO 负责把某个端口引脚映射到 EXTI 线,EXTI 负责检测边沿并产生中断请求,NVIC 负责让 Cortex-M3 内核响应这个中断,最后 CPU 根据中断向量表进入固定名字的 IRQHandler。中断处理完后必须清除挂起标志,否则会一直重复进入中断。
6. 下一轮学习预告
这一组之后,最自然的下一步是定时器:
定时器定时中断
-> 再次用到 NVIC 和 IRQHandler
PWM
-> GPIO 复用推挽输出
-> 定时器输出比较
输入捕获 / 编码器接口
-> GPIO 输入
-> 定时器硬件计数所以本组的 GPIO、EXTI、NVIC 是后面定时器、PWM、串口、ADC 的地基。
