第2章:STM32基础与GPIO控制
各位同学,欢迎来到第二章。上一章我们搭好了开发环境,这一章咱们来点实在的——让芯片真正“动”起来。
GPIO控制,说白了就是让芯片的引脚输出高电平或低电平。听起来简单吧?但这里面的门道可不少。我刚开始学STM32时,以为GPIO就是写个1或0完事,结果项目里吃了不少亏。嗯,咱们今天就把这些坑一个个填上。
2.1 时钟树配置——芯片的“心跳”
先问大家一个问题:为什么STM32上电后,GPIO不能直接用?
答案很简单——时钟没开。STM32的每个外设都有独立的时钟开关,这是为了省电。你想想看,如果所有外设一上电就全速运行,那电池能撑多久?
时钟树,就是芯片内部时钟信号的分配网络。我个人习惯把时钟树想象成一个自来水系统:
- 晶振:水源(8MHz外部晶振或内部RC振荡器)
- PLL:增压泵(倍频到更高频率)
- AHB/APB分频器:阀门(降低频率给低速外设)
- 外设时钟使能:水龙头(用哪个开哪个)
我在项目中遇到过一件事:有个同事调试了整整两天,I2C就是不通。最后发现是APB1总线的时钟没配置对,I2C的时钟源压根没工作。这种问题,查起来真要命。
核心要点:STM32的GPIO挂在APB2总线上,操作GPIO前必须先使能对应端口的时钟。
以STM32F103为例,时钟配置的典型流程:
// 1. 使能GPIOB时钟(挂在APB2上)
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
// 2. 或者用HAL库的方式
__HAL_RCC_GPIOB_CLK_ENABLE();
为什么是APB2?因为GPIO属于高速外设。APB1最高36MHz,APB2最高72MHz。这个细节,面试时经常考。
2.2 GPIO模式详解——不只是输入输出
很多新手以为GPIO只有“输入”和“输出”两种模式。错了。STM32的GPIO有8种模式,但常用的就这几种:
| 模式 | 用途 | 典型场景 |
|---|---|---|
| 推挽输出 | 强驱动高低电平 | 驱动LED、普通IO输出 |
| 开漏输出 | 只能拉低,不能拉高 | I2C总线、电平转换 |
| 浮空输入 | 高阻抗,电平不确定 | 外部中断输入 |
| 上拉/下拉输入 | 内部电阻固定电平 | 按键检测、防抖动 |
2.2.1 推挽输出
推挽输出,说白了就是芯片内部有两个MOS管:一个推(输出高),一个挽(输出低)。输出高电平时,上管导通;输出低电平时,下管导通。这样驱动能力强,LED可以直接点亮。
但要注意:推挽输出不能“线与”。如果两个推挽输出引脚直接连在一起,一个输出高一个输出低,那就短路了。我曾经见过有人把两个推挽输出短接,芯片直接冒烟——嗯,那味道挺难忘的。
2.2.2 开漏输出
开漏输出就聪明多了。它只有下管,没有上管。输出低电平时,下管导通;输出高电平时,下管断开,引脚处于高阻态。
所以开漏输出必须外接上拉电阻。为什么要有这种模式?因为开漏输出可以实现“线与”——多个设备可以共用一根线,谁拉低谁说话。I2C总线就是典型应用。
我的经验:做I2C通信时,开漏输出的上拉电阻一般选4.7kΩ。如果总线电容大,可以降到2.2kΩ。但别太小,否则功耗会上去。
2.2.3 上拉/下拉输入
按键检测时,最怕什么?引脚悬空。浮空输入的引脚电平是不确定的,可能受电磁干扰乱跳。
解决办法就是内部上拉或下拉。上拉输入:按键没按下时读1,按下后读0。下拉输入则相反。
STM32内部的上拉电阻约40kΩ,下拉也是40kΩ。这个值不算小,但对付按键足够了。
2.3 寄存器操作 vs HAL库
这是个老生常谈的话题。我两种都用过,说说我的感受。
寄存器操作:
- 直接读写芯片的寄存器地址
- 代码量小,执行速度快
- 可读性差,移植困难
- 适合对时序要求苛刻的场景
HAL库:
- 封装好的函数接口
- 可读性好,移植方便
- 代码量大,执行效率略低
- 适合快速开发、复杂外设
举个例子,点亮一个LED:
// 寄存器方式
GPIOB->BSRR = GPIO_Pin_0; // 置位PB0
// HAL库方式
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
哪个好?我个人建议:学习阶段两种都写一遍。寄存器操作能帮你理解芯片底层原理,HAL库能提高开发效率。工作中,我一般用HAL库做原型,优化时再改寄存器。
避坑指南:我曾经在批量产品中用了HAL库的延时函数HAL_Delay(),结果发现它依赖SysTick中断。如果某个中断服务函数执行时间过长,延时就不准了。后来我改用了定时器硬件延时,问题才解决。
2.4 实战:点亮LED与按键消抖
理论说完了,咱们动手。这个实战项目很简单:按下按键,LED亮;松开按键,LED灭。
但这里有个经典问题——按键抖动。机械按键按下和松开时,触点会弹跳几次,持续5-20ms。如果不处理,一次按下可能被误判成多次。
消抖方法有两种:硬件消抖(加RC滤波)和软件消抖(延时采样)。咱们用软件消抖,因为成本低、灵活。
2.4.1 硬件连接
- LED:PB0,串联220Ω电阻到GND
- 按键:PA0,一端接GND,另一端接PA0
- PA0配置为上拉输入(默认高电平,按下低电平)
2.4.2 代码实现
// 初始化GPIO
void GPIO_Init(void)
{
// 使能时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN;
// PB0推挽输出,50MHz
GPIOB->CRL &= ~(0xF << 0); // 清零
GPIOB->CRL |= (0x3 << 0); // 推挽输出,50MHz
// PA0上拉输入
GPIOA->CRL &= ~(0xF << 0); // 清零
GPIOA->CRL |= (0x8 << 0); // 上拉输入
GPIOA->ODR |= (1 << 0); // 使能上拉
}
// 按键消抖检测
uint8_t Key_Scan(void)
{
if((GPIOA->IDR & (1 << 0)) == 0) // 检测到按下
{
delay_ms(10); // 延时10ms消抖
if((GPIOA->IDR & (1 << 0)) == 0) // 再次确认
{
return 1; // 按键有效
}
}
return 0;
}
// 主循环
while(1)
{
if(Key_Scan())
{
GPIOB->BSRR = (1 << 0); // LED亮
}
else
{
GPIOB->BRR = (1 << 0); // LED灭
}
}
关键点:消抖延时10-20ms是经验值。太短消不掉抖动,太长影响响应速度。我一般取15ms,这是大多数按键的抖动周期。
2.4.3 为什么用BSRR和BRR?
你可能注意到,我设置LED时用了BSRR和BRR,而不是直接写ODR。为什么?
因为BSRR是“原子操作”。如果直接写ODR,在多任务或中断环境下,可能发生“读-改-写”冲突。比如你读ODR想置位PB0,但此时中断改了ODR,你再写回去就把中断的修改覆盖了。
BSRR则不同:写1到对应位就置位,写0不影响。BRR同理,写1就复位。这样就不会有冲突。
这个细节,我在一个多中断的项目里吃过亏。从那以后,我设置GPIO只用BSRR和BRR。
2.5 本章小结
这一章内容不少,咱们捋一捋:
- 时钟树是芯片的“心跳”,用外设前必须先开时钟
- GPIO有8种模式,推挽输出驱动强,开漏输出能“线与”
- 上拉/下拉输入解决引脚悬空问题
- 寄存器操作快但难读,HAL库方便但略慢
- 按键消抖用10-20ms延时,设置GPIO用BSRR/BRR
下一章,咱们要讲中断系统。这可是嵌入式开发的灵魂——没有中断,CPU就得一直轮询,效率低得可怜。到时候我会分享一个中断优先级配置的翻车经历,保证让你印象深刻。
好了,今天的课就到这里。动手把代码跑一遍,有问题随时交流。