everything i should kown about control
预备知识
单片机构成(mcu)
运算器:负责执行算术和逻辑运算。
控制器:计算机的大脑中枢,它协调和控制计算机各部件的工作。控制器从存储器中取出指令,分析指令的内容,并根据指令要求向其他部件发出控制信号,以确保整个计算机系统有条不紊地运行。
存储设备:用于存储数据和程序。它可以分为主存储器(如随机存取存储器RAM)和辅助存储器(如硬盘、固态硬盘等)。
主存储器:暂时存储正在运行的程序和数据。
辅助存储器:长期存储大量的数据和程序。
调试接口标准
- 为什么需要调试接口标准
电脑和单片机之间的一条“调试通道”,用来给芯片下载程序、调试程序、读取芯片内部状态。GDB / GDB Server / 这类硬件调试器很多就是通过 JTAG 或 SWD 这类调试接口和单片机通信的。
- JTAG:线更多,功能更完整,传统一些。
- SWD:线更少,STM32里非常常用,只能用于ARM内核的单片机。

boot 电路
为什么需要boot 电路
单片机一上电,CPU 必须先找一个起点去取第一条指令
这个起点可能有 3 个地方:
- 主 Flash
- 系统存储器
- 内部 SRAM
启动模式的选择
STM32 通过 BOOT0、BOOT1 的电平组合 (见表格)来决定选哪个区域启动

X表示无所谓,0 或 1 都行。
BOOT0 = 0,BOOT1 = X芯片复位后,直接从 Flash 里执行你的程序(也就是你写的
main()前面的启动代码,最终都会从这里开始跑。启动地址:0x08000000 是STM32内置的Flash,一般我们使用JTAG或者SWD模式下载程序时,就是下载到这里,重启后也直接从这启动程序。基本上都是采用这种模式。
BOOT0 = 1,BOOT1 = 0系统存储器一般放的是 STM32 官方出厂烧好的 BootLoader。
作用是:
- 你可以不靠 JTAG / SWD。
- 而是通过串口、USB、CAN 等某些方式下载程序。
BOOT0 = 1,BOOT1 = 1启动到内部 SRAM也就是从 RAM 启动。
这个模式平时很少用,更多用于:
- 调试
- 特殊测试
因为 RAM 掉电就没了,通常不会拿来做正常程序启动。
电路原理

该电路图本质上是在给 BOOT0 / BOOT1 选择高低电平
你可以看到:
- 上面接了 VCC3V3,表示高电平
- 下面接了 GND,表示低电平
- 中间通过排针跳帽来选择连到哪边
也就是说:
- 跳帽接到 3.3V → 这个 Boot 引脚为 1
- 跳帽接到 GND → 这个 Boot 引脚为 0
电阻 R3、R4 是 100k,一般起到:
- 限流
- 上拉 / 下拉辅助
- 保证引脚状态稳定
时钟
时钟树
STM32 里通常不是“一个时钟管全部”,而是有一套时钟树
也就是说:
- 先有几个“原始时钟源”
- 再经过选择、分频、倍频
- 最后分给 CPU 和各个外设
这就叫 时钟树
时钟控制器(RCC)
它主要管两大类事情:
时钟管理
它负责:
- 选择时钟源
- 打开某个外设的时钟
- 设置分频、倍频
- 让系统时钟分配给各总线和外设
比如:
- 你要用 GPIOA
- 先得开 GPIOA 的时钟
- 这个动作通常就是 RCC 在管
如果时钟没开,你去操作 GPIO 寄存器,往往就不生效。
这就是为什么很多 STM32 程序都会先写类似:
1 | __HAL_RCC_GPIOA_CLK_ENABLE(); |
这句话本质就是:
通过 RCC 把 GPIOA 的时钟打开。
复位管理
RCC 还可以对某些外设进行复位。
比如一个外设状态乱了,可以:
- 给它复位一下
- 再重新配置
所以 RCC 不只是时钟模块,它还带“复位控制”。
常见时钟源
HSI(High Speed Internal):高速内部时钟,芯片内部自带的 RC 振荡器
- 不需要外部晶振
- 上电就能用
- 成本低,方便
- 但精度一般不如外部晶振
HSE(High Speed External):高速外部时钟,通常来自板子上的外部晶振。
- 精度更高
- 更稳定
- 常用于需要较准频率的场景
- 但要外接晶振电路
LSI(Low Speed Internal):低速内部时钟。
- 频率低
- 精度一般
- 常给独立看门狗、RTC 等低速模块使用
LSE(Low Speed External):低速外部时钟,一般是 32.768kHz 晶振。
这个频率很经典,因为适合做:
- RTC 实时时钟
- 日历计时
Crystal/ceramic resonator(晶体/陶瓷振荡器)
一种外部谐振元件
开发板上常见有一个小小的晶振器件,它不是 MCU 内部自己生时钟,而是:
- 接在 STM32 的时钟引脚上
- 配合 MCU 内部振荡电路
- 产生稳定时钟
晶体振荡器
一般说“晶振”,通常指石英晶体。
特点:
- 精度高
- 稳定性好
- 很常用
比如板子上常见:
- 8MHz 晶振
- 12MHz 晶振
- 25MHz 晶振
陶瓷振荡器
也能用来提供振荡源,但一般精度会比石英晶体差一些。
Bypass clock source(旁路时钟源)
由外部其他设备主动给出该时钟信号,不经过 MCU 内部时钟电路驱动,可直接作为单片机时钟
系统时钟(SYSCLK)
芯片当前最核心的主工作时钟(是 CPU 和很多总线时钟的基础来源
系统时钟可以由这些来源之一提供:
- HSI
- HSE
- PLL 输出
通常 STM32 不会直接一直拿原始时钟跑,而是常常通过 PLL 倍频 之后得到更高的系统时钟。
例如:
- 外部晶振 HSE = 8MHz
- 经过 PLL 倍频
- 得到 SYSCLK = 72MHz
这样芯片主频就提高了。
PLL
PLL 就是 锁相环,一个“倍频器”或者“调频器”
例如:
- 输入 8MHz
- PLL 倍频后输出 72MHz
这样就能让芯片从一个较低频率的时钟源,得到一个更高的主频。
所以很多 STM32 的典型配置都是:
- HSE → PLL → SYSCLK
分频
意思就是把时钟变慢。
例如:
- 系统时钟 72MHz
- 分频 2
- 得到 36MHz
为什么要分频?
因为不是所有模块都能跑一样快,也不是所有模块都需要那么快。
所以 STM32 会把系统时钟再分给不同总线和外设。
AHB/APB
AHB:高速总线时钟
通常给:
- 内核
- DMA
- 内存接口
- 高速外设
APB1 / APB2:低速 / 高速外设总线时钟
常给:
- USART
- SPI
- I2C
- TIM
- ADC
- GPIO 等外设
不同系列分法略有差异,但核心思想一样:
系统时钟不会直接一股脑送到所有外设,而是先分到不同总线。
比如常见一种理解方式:
- SYSCLK → AHB
- AHB → APB1
- AHB → APB2
然后外设再挂在 APB1 或 APB2 上。
与时钟树相关的参数
串口波特率
定时器定时:定时器溢出时间 = 预分频 × 自动重装值 × 时钟周期
ADC采样速度
USB、CAN 等外设
GPIO
MCU与外部元件沟通的接触点,一般沟通的是电压信息俗称引脚
工作模式
输入模式
下拉输入
意思是:
- 引脚默认通过一个电阻接地
- 所以在没人管它时,默认是 低电平
如果外部给它高电平,它才会变成 1。
用途:
- 防止引脚悬空
- 让默认状态明确
上拉输入
意思是:
- 引脚默认通过一个电阻接到 VDD
- 所以默认是 高电平
如果外部把它拉低,它才会变成 0。
这个在按键电路里特别常见。
比如:
- 默认高电平
- 按键按下接地
- 读到低电平表示按下
浮空输入
意思是:
- 不上拉,也不下拉
- 引脚完全“飘着”
这样如果外部没明确驱动,就可能:
- 一会高
- 一会低
- 很容易受干扰
所以浮空输入一般只在:
- 外部已经有明确驱动
- 或特殊场景
才会使用。
模拟输入
这个和前面几种数字输入不一样。
它不是判断 0 / 1,而是读取一个连续变化的电压值。
比如:
- 0.3V
- 1.2V
- 2.7V
这种通常给:
- ADC
- 模拟传感器输入
用了模拟输入模式后,GPIO 的数字输入那一套比较器之类通常会被关掉,减少干扰和功耗。
输出模式
推挽输出
这是最常用的输出模式。
特点是:
- 想输出高电平,就能主动拉高
- 想输出低电平,就能主动拉低
也就是:
高低电平都由单片机主动驱动。
适合:
- 点亮 LED
- 输出普通数字信号
- 控制一般器件
开漏输出
这个模式下:
- 只能主动拉低
- 不能主动拉高
当它“输出高”时,本质上不是主动输出高电平,而是:
- 把上面的管子关掉
- 引脚处于“放开”的状态
- 需要靠外部上拉电阻把它拉高
所以开漏输出通常要配合:
- 外部上拉
- 或内部上拉
复用推挽输出、复用开漏输出
这里的“复用”意思是:
这个引脚不再只是普通 GPIO,而是交给某个外设使用。
比如这个脚可能不再单纯输出 0 / 1,而是变成:
- USART_TX
- SPI_SCK
- I2C_SCL
- TIM_PWM
所以:
- 复用推挽输出:给外设输出信号,底层输出结构是推挽
- 复用开漏输出:给外设输出信号,底层输出结构是开漏
比如:
- UART TX 常常用复用推挽
- I2C 常常用复用开漏
总示例图讲解
顺序:从右到左

I/O 引脚和保护二极管
这是 GPIO 引脚真正连到芯片外部的地方
保护二极管的作用主要是:
- 防止静电冲击
- 防止电压过高或过低对内部电路造成损伤
输入电路 (中上部分)
上拉电阻、下拉电阻
图里标了大约 50K。
这表示 STM32 内部通常集成了弱上拉 / 弱下拉电阻。
所以配置成:
- 上拉输入
- 下拉输入
其实就是在控制内部这个电阻是否接上。
注意这个电阻值通常比较大,是“弱上拉/弱下拉”,不是特别强的驱动。
TTL 施密特触发器
把外部不是特别干净的电压信号,转换成芯片内部更稳定的数字 0 / 1 判断
因为外部输入电压可能不是一下子从 0V 跳到 3.3V,而是慢慢变化,还可能有噪声。
施密特触发器可以提高抗干扰能力,让芯片更稳定判断:
- 这是高电平
- 这是低电平
IDR:输入数据寄存器
这个寄存器里保存的是当前引脚读到的输入状态。
比如:
- 引脚现在是高电平 → 对应位是 1
- 引脚现在是低电平 → 对应位是 0
所以你程序里读取 GPIO 输入,本质上就是在读这个寄存器对应位
模拟输入 / 复用功能输入
模拟输入
这个脚可以接到 ADC 等模拟模块,不走数字输入判断。
复用功能输入
这个脚也可以送给某个片上外设当输入。
比如:
- 串口 RX
- 定时器输入捕获
- 外部中断输入
- SPI MISO
也就是说,GPIO 虽然叫 GPIO,但它还经常是很多外设功能的“入口”。
输出电路(中下部分)
ODR:输出数据寄存器
你给某个 GPIO 输出 1 或 0,本质上就是改这个寄存器里的某一位。
比如:
- 写 1 → 希望该引脚输出高
- 写 0 → 希望该引脚输出低
然后这个值再去控制后面的输出驱动电路。
BRR / BSRR
- BSRR:位设置 / 复位寄存器
- BRR:位复位寄存器
它们的作用是:
可以更方便、更原子地对某一位 GPIO 置 1 或清 0。
为什么不用直接改 ODR?
因为直接改 ODR 可能涉及:
- 先读
- 再改
- 再写回
在某些场景下不够安全,尤其在中断或并发访问时。
而 BSRR 可以做到:
- 写某位到 set 区 → 置 1
- 写某位到 reset 区 → 清 0
所以很多底层库都喜欢用它来快速控制引脚。
输出控制
这块逻辑会根据你配置的模式决定:
- 是普通 GPIO 输出
- 还是复用输出
- 是推挽
- 还是开漏
也就是控制下面那两个 MOS 管怎么工作。
P-MOS 和 N-MOS
这两个是输出驱动核心。
你可以简单理解成:
- P-MOS 负责把引脚往 高电平 拉
- N-MOS 负责把引脚往 低电平 拉
推挽输出时
- 输出高:P-MOS 开,N-MOS 关
- 输出低:P-MOS 关,N-MOS 开
所以推挽可以主动输出高和低。
开漏输出时
通常上面的拉高能力被禁掉,只保留下拉能力:
- 输出低:N-MOS 开,把引脚拉到低电平
- 输出高:N-MOS 关,引脚悬空,由外部上拉拉高
这就是为什么开漏不能主动输出高。
复用功能输出
意思是:
这个引脚输出的数据不一定来自 ODR,也可能来自某个外设模块。
比如:
- 定时器输出 PWM
- USART 输出发送数据
- SPI 输出时钟/数据
这时候你虽然看到的是这个 GPIO 脚在变化,但其实背后是外设在控制它,不是你手动写 ODR 了。
分示例图讲解
输入模式

下拉输入(右上角)
- PC13 这个引脚通过一个 10k 电阻接地
- 按键另一端接 VCC
- 按键没按下时,引脚通过电阻被拉到地,所以是 低电平
- 按键按下时,引脚直接接到 VCC,所以变成 高电平
所以它叫:
下拉输入
因为“默认状态”被电阻拉向 GND。
结论:
- 松开按键:0
- 按下按键:1
上拉输入(右下角,标号3)
- PC13 通过一个 10k 电阻接 VCC
- 按键另一端接地
- 按键没按下时,引脚被电阻拉到 VCC,所以是 高电平
- 按键按下时,引脚直接接地,所以是 低电平
所以它叫:
上拉输入
因为“默认状态”被电阻拉向 VCC。
结论:
- 松开按键:1
- 按下按键:0
浮空输入(标号2)
- PC13 接一个按键
- 按键一端接地
- 但是 没有上拉电阻,也没有下拉电阻
这时如果按键没按下,引脚其实谁都没接,相当于“悬空着”。
按下时:
- 引脚接地,明确是 低电平
没按下时:
- 引脚没有明确高低,电平可能乱跳
所以浮空输入最大的问题就是:
不稳定,容易受干扰。
为什么要加电阻,不能直接接 VCC 或 GND?
因为按键按下时,电路状态会改变。
比如上拉输入:
- 平时通过电阻接 VCC
- 按下时引脚直接接 GND
如果没有那个电阻,而是直接把 VCC 和引脚硬连在一起,那么一按键就等于:
VCC 直接短接到 GND
这就短路了。
所以电阻的作用有两个:
提供默认电平,让引脚在松开按键时不悬空。
限流保护,避免按键按下时电源和地直接短路。
输出模式
推挽输出


可以输出高电平和低电平
电平由芯片内部直接提供
只能提供3.3V电平(GPIO 高电平 ≈ 3.3V,GPIO 低电平 ≈ 0V)
无法提供大电流
开漏输出


只能提供低电平
Q2 导通时,输出点被拉到地 → 低电平
Q2 截止时,输出点不会被主动拉高,而是变成 高阻态
需要外部提供上拉电路
就像图里那个 R1 接到 3.3V:
- Q2 关断 → GPIO 不管这根线了
- 这时 R1 把线拉到 3.3V → 线变高电平
所以开漏输出通常都要配:
外部上拉电阻
没有上拉的话,Q2 关断时这根线就会悬空。
可以提供除3.3V之外的电平
可以提供大电流
开漏结构更容易配合外部电源和器件实现更强的驱动能力。
真正的大电流通常还是由外部电源和外部器件承担,GPIO 本身只是控制“是否导通到地”。
可以实现“线与”的功能
所谓 线与,意思是多个器件的输出端可以直接连到同一根线上,再接一个上拉电阻
这时逻辑规则是:
- 只要有任意一个器件把线拉低,整根线就是低电平
- 只有当所有器件都“松手”(都不拉低)时,这根线才会被上拉成高电平
所以表现出来就像逻辑“与”:
- 都为 1,线才是高
- 有一个为 0,线就是低
这就是“线与”。
操作GPIO常用的函数
HAL_GPIO_ReadPin()
读取某个 GPIO 引脚当前的电平状态
1 | GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); |
返回类型:GPIO_PinState
通常就是两种:
GPIO_PIN_RESET:低电平GPIO_PIN_SET:高电平
例子:
1 | if(HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET) |
这里的意思就是读取 PC13 的状态
HAL_GPIO_WritePin()
给某个 GPIO 引脚写入高电平或低电平
1 | void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState); |
GPIOx:哪个 GPIO 组,比如GPIOA、GPIOB、GPIOCGPIO_Pin:哪个引脚,比如GPIO_PIN_5PinState:写成高还是低
例子:
1 | HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // 输出高电平 |
HAL_GPIO_TogglePin()
把当前引脚电平翻转一次
1 | void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); |
例子:
1 | HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); |
NVIC(Nested Vectored Interrupt Controller)
嵌套向量中断控制器
它是 Cortex-M 内核 里专门管理中断的硬件模块,用来完成“哪个中断先响应、能不能打断别的中断、CPU 响应后跳到哪里执行”这些事情。
你可以把它理解成一个“中断调度中心”:
- 外设产生中断请求,比如按键 EXTI、定时器 TIM、串口 USART
- 这些请求送到 NVIC
- NVIC 根据优先级判断:
谁先执行,谁后执行,当前中断能不能被更高优先级中断打断 - 然后 CPU 根据中断向量表,跳到对应的中断服务函数里执行
省流版:外设提请求,NVIC 排队和裁决,CPU 去执行。
some definitions
程序计数器(PC)
处理器系统在执行代码的时候,会从存储器依次取出指令和数据,这种能力需要在处理器里保存一个存储器的地址,就是所谓的程序计数器(Program Counter,PC),也叫程序指针。
EXTI(外部中断)
当连接到某个GPIO引脚的中断线检测到预设的电平变化或边沿变化时,就会产生一个中断请求,使CPU暂停当前任务,转而去执行相应的中断服务程序。这样,单片机就能够及时地处理外部事件,提高了系统的实时性和灵活性。

按键没按下时
- GPIO 通过 10K 电阻被拉到
VCC - 所以此时引脚读到的是 高电平 1
按键按下时
- 开关闭合
- GPIO 被直接接到地
- 此时引脚读到的是 低电平 0

三种触发模式
External Interrupt Mode with Rising edge trigger detection只在 0 变 1 时触发中断。
比如这个按键电路里:
- 按键松开那一瞬间:0 变 1
- 会触发中断
External Interrupt Mode with Falling edge trigger detection只在 1 变 0 时触发中断。
比如:
- 按键按下那一瞬间:1 变 0
- 会触发中断
这个在按键检测里很常用,因为一般更关心“按下”的那一刻。
External Interrupt Mode with Rising/Falling edge trigger detection上升沿和下降沿都触发
也就是:
- 按下触发一次
- 松开再触发一次
适合既想知道“按下”,又想知道“松开”的场景。
EXTI 线路复用关系
STM32 的 EXTI 线是怎么和 GPIO 引脚对应起来的。

从图中可见:相同编号的引脚,共用同一条 EXTI 线
中间的“选择器”是什么意思
图里像多路开关一样的那个结构,表示:
同一条 EXTI 线只能从若干候选 GPIO 里选一个。
例如 EXTI0:
- 候选有 PA0、PB0、PC0、PD0…
- 但最终只能选其中一个接到 EXTI0
不能同时让:
- PA0 触发 EXTI0
- PB0 也触发 EXTI0
因为它们抢的是同一条线。
所以一个特别重要的结论
PA0 和 PB0 不能同时作为独立外部中断源使用
因为它们都占 EXTI0
同理:
PA1和PC1不能同时独立用 EXTI1PA13和PB13不能同时独立用 EXTI13
AFIO_EXTICR 是什么
图上写了:
- 在
AFIO_EXTICR1寄存器的 EXTI0[3:0] 位 - 在
AFIO_EXTICR1寄存器的 EXTI1[3:0] 位 - …
- 在
AFIO_EXTICR4寄存器的 EXTI15[3:0] 位
这说明:
STM32 通过这些寄存器来决定:
EXTI0 到底接 PA0 还是 PB0 还是 PC0
EXTI1 到底接 PA1 还是 PB1 还是 PC1
也就是说,这些寄存器就是“选择器的控制开关”。
如果你在 CubeMX 里配置了某个引脚为外部中断,CubeMX 最终就是帮你改这些寄存器。
中断事件的优先级
抢占优先级,高的可以直接中断掉低优先级的中断服务函数直接处理
子优先级,在多中断事件发生时,高的可以插队到低的前面先处理,决定同级中断的先后顺序
中断优先级分组

示例解释:
NVIC_PriorityGroup_0
- 0 bit 给抢占优先级
- 4 bit 给子优先级
所以结果是:
- 没有抢占优先级
- 子优先级范围 0~15
这意味着:
所有中断都不能互相打断。
因为压根没有“抢占”这一说。
只能在“大家同时等着处理”时,按子优先级排先后。
NVIC_PriorityGroup_2
- 2 bit 给抢占优先级
- 2 bit 给子优先级
所以:
- 抢占优先级 0~3
- 子优先级 0~3
这是一种比较均衡的分法。
意思是:
- 有 4 档“打断能力”
- 每档里再分 4 档先后顺序
这个分组很常见,因为比较平衡。
假设现在有三个中断:
- 串口中断:抢占 1,子 2
- 定时器中断:抢占 0,子 3
- 按键中断:抢占 1,子 0
情况1:按键中断正在执行,定时器中断来了
- 按键抢占优先级 = 1
- 定时器抢占优先级 = 0
因为 0 比 1 高,所以定时器中断可以打断按键中断。
情况2:串口中断和按键中断同时到来
- 串口:抢占 1,子 2
- 按键:抢占 1,子 0
它们抢占优先级一样,所以谁也不能打断谁。
但如果这两个中断都在等待响应,那么子优先级更高的先执行。
通常 STM32 里是 数字越小优先级越高,所以:
- 子优先级 0 比 2 高
- 先执行按键中断
需要用到的函数
1 | 外部引脚变化 |
HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup)
设置 中断优先级分组
例:
1 | HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2); |
表示通常把优先级位分成:
- 2 位抢占优先级
- 2 位子优先级
一般在系统初始化时设置一次就行。
HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority)
设置 某个具体中断 的优先级。
IRQn:中断号PreemptPriority:抢占优先级SubPriority:子优先级
例:
1 | HAL_NVIC_SetPriority(EXTI9_5_IRQn, 1, 0); |
- 设置
EXTI9_5_IRQn这个中断 - 抢占优先级 = 1
- 子优先级 = 0
HAL_NVIC_EnableIRQ(IRQn_Type IRQn)
使能某个中断。
例:
1 | HAL_NVIC_EnableIRQ(EXTI9_5_IRQn); |
意思是把 EXTI9_5_IRQn 这个中断在 NVIC 里打开
为什么需要它:
即使:
- GPIO 配好了
- EXTI 配好了
- 边沿也来了
如果 NVIC 里没使能,CPU 还是不会进去执行中断函数。
所以你可以把它理解成:
给这个中断发“通行证”
EXTI9_5_IRQHandler(void)
这是 真正的中断服务函数入口
当 EXTI5 ~ EXTI9 这些外部中断线触发时,CPU 会进入这个函数。
例如如果你用的是 PA6 做外部中断,那一般就会进这个函数,因为 6 属于 5~9 这一组。
为什么叫 EXTI9_5_IRQHandler
因为 STM32 的 EXTI 中断不是每条线都单独一个 IRQ。
常见分组是:
EXTI0_IRQn→ EXTI0EXTI1_IRQn→ EXTI1EXTI2_IRQn→ EXTI2EXTI3_IRQn→ EXTI3EXTI4_IRQn→ EXTI4EXTI9_5_IRQn→ EXTI5 ~ EXTI9EXTI15_10_IRQn→ EXTI10 ~ EXTI15
所以:
- GPIO5~9 的外部中断,共用
EXTI9_5_IRQHandler - GPIO10~15 的外部中断,共用
EXTI15_10_IRQHandler
例:
你通常不会在这个函数里直接写业务代码, 而是调用 HAL 提供的处理函数:
1 | void EXTI9_5_IRQHandler(void) |
HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
这是 HAL 给你预留的回调函数,当 HAL 确认某个 EXTI 线触发后,会调用它。
例:
1 | void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) |
- 如果是 6 号引脚触发了中断
- 就翻转一次 LED
为什么要判断 GPIO_Pin:
因为一个回调函数可能服务多个引脚, 所以通常要用 if 或 switch 区分。
这几个函数的调用关系
一个“按下按键翻转 LED”的简单例子
假设:
- 按键接在
PA6 - LED 接在
PC13 - 按键为上拉输入,按下产生下降沿
- 使用 EXTI6,所以会进
EXTI9_5_IRQHandler
1 | // 1. 初始化 |
虚函数(_weak)
弱函数
__weak 的作用,就是给库函数留一个“默认版本”,你如果自己写了同名函数,就优先用你写的那个。
比如 HAL 库里有这样一个函数(弱定义:空实现)
1 | //弱函数 |
库先给你放了一个空壳版本。你需要时,可以自己在用户文件里写一个同名函数把它顶掉。
实函数
1 | void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) |
- 优先级更高
- 一旦存在,就会替代弱定义版本
- 一般只能有一个同名定义,否则链接报错
编译器最终怎么选
- 只有弱函数,没有你自己写的普通函数
那就用库里那个弱函数。
也就是执行空壳版本,通常什么都不做。
- 你自己写了一个同名普通函数
那就用你写的普通函数。也就是说:
你的函数把 HAL 库那个 __weak 版本覆盖了。
实验
按键控制LED亮灭
把PB9设置为GPIO_EXTI模式
设置上拉电阻
设置为下降沿触发
设置中断优先级分组
设置EXTI优先级
1 | /* main.c */ |
1 | /* stm32f1xx_it.c */ |
按键消抖
按键抖动指的是在机械按键在按下或释放时由于机械元件的特性,导致开关多次闭合或断开,从而在单片机IO口上检测到电压信号出现抖动的现象

硬件消抖原理
硬件消抖通常通过RC电路实现。在RC消抖电路中,电阻起到限流的作用,电容则用来储存电荷。当输入信号发生变化时,电容会通过电阻进行充放电,从而实现对信号的平滑处理。通过合理选择电阻和电容的数值,可以达到最佳的消抖效果。

RC 电路:由电阻 R(Resistor)和电容 C(Capacitor)组成的电路
软件消抖方法
延时消抖法
在检测到按键按下时延时一段时间(比如10ms)再次读取按键IO的电平,检测两次读取的结果是否相同,相同则认为按键被按下。
按下按键进入中断回调函数
在函数中把某个变量置1
在主函数里开始延时
延时后再读取引脚电平,如果发现是低电平即认为按键被按下
计数消抖法
周期执行按键扫描程序在检测到按键变化时备份输入状态,清零计数器并开始计数,直到检测到按键弹起,如果计数值大于某个设定值则认为按键动作有效,否则按键动作无效
TIM(Timer定时器)
分类

3个重要的参数
PSC (定时器分频系数)
它的作用就是:把输入给定时器的时钟先“降速”
比如定时器输入时钟是 72 MHz,说明 1 秒钟来 7200 万个计数脉冲。 这个速度太快了,直接计数不方便,所以先用 PSC 分频。
STM32 里通常可以理解成:
计数频率 = 定时器时钟 / (PSC + 1)
例:
如果定时器时钟是 72 MHz:
- PSC = 71
- 那么计数频率 = 72 MHz / (71 + 1) = 1 MHz
这就表示:计数器每 1 微秒加 1 次
所以 PSC 决定的是:定时器“走得多快”
ARR (自动重装载值)
它的作用是:规定计数器最多数到多少
计数器不是无限加下去的。 它从 0 开始数,数到 ARR 之后,就会产生一次更新事件,然后重新开始。
通常可以理解成:
- 从 0 数到 ARR
- 一共会经历 ARR + 1 个计数
所以定时器溢出周期一般是:定时时间 = (PSC + 1) × (ARR + 1) / 定时器时钟
一个完整例子
假设:
- 定时器时钟 = 72 MHz
- PSC = 7199
- ARR = 9999
先算分频后频率:
- 72 MHz / (7199 + 1) = 10 kHz
也就是计数器每 0.1 ms 加 1。
再数到 ARR=9999:
- 一共 10000 次
所以总时间:
- 10000 × 0.1 ms = 1000 ms = 1 s
也就是说:
这个定时器每 1 秒触发一次更新事件/中断
Counter Mode(计数模式 )
计数模式决定的是:计数器 CNT 按什么方式变化

边沿对齐模式
一般就是:
- 从 0 一直加到 ARR
- 然后回到 0
- 再继续加
这叫 向上计数
也可能反过来:
- 从 ARR 往下减到 0
这叫 向下计数
这种模式因为计数过程只朝一个方向走,所以叫“边沿对齐”。

中心对齐模式
- 先从 0 数到 ARR
- 再从 ARR 数回 0
- 然后再上去
周期计算公式
定时器输出的频率取决于:
- 主频:即 MCU 的主时钟频率,通常是 72 MHz
- PSC:定时器的预分频器,它控制定时器的输入频率
- ARR:定时器的自动重载寄存器,它决定计数器达到多少就会触发事件
例
假设我们有以下信息:
- MCU 主频 = 72 MHz
- PSC = 3600 - 1,即 PSC = 3599
- ARR = 10000 - 1,即 ARR = 9999
我们想让 LED 闪烁 1 次每 0.5 秒。
计算定时器的主频
首先,定时器的输入时钟频率:
主频=72 MHz=72,000,000 Hz
计算定时器分频后的频率
根据公式:
代入数值:
结果分析
定时器的频率是 2 Hz,这意味着每 0.5 秒触发一次更新事件。
相关函数
流程
开启定时器 → 定时器溢出 → 进入中断入口函数 → HAL 处理中断 → 调用用户回调函数
HAL_TIM_Base_Start()
启动定时器基础计数功能,但不开中断
HAL_TIM_Base_Start_IT()
启动定时器,同时使能更新中断
TIM3_IRQHandler(void)
中断服务函数
它是定时器 TIM3 对应的中断入口函数。
当 TIM3 真的发生中断时,CPU 会先跳到这里来。
比如 TIM3 更新中断来了,执行流程首先到:
1 | void TIM3_IRQHandler(void) |
这个函数通常在 stm32xx_it.c 里。
特点:
- 这是 硬件中断入口
- 名字基本是固定的,和中断向量表对应
- 一般不要在里面写太多自己的业务逻辑
- 通常只调用 HAL 提供的中断处理函数
HAL_TIM_PeriodElapsedCallback()
定时器更新中断回调函数
当 HAL 帮你处理完底层中断标志以后,如果确认是“周期到达/溢出更新中断”,就会调用这个回调函数。
也就是说:真正写你自己定时任务代码的地方,通常就在这里
常见写法例如:
1 | void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) |
- 如果当前触发回调的是 TIM3
- 那就翻转一次 LED