4、GPIO与数字信号采集:LED控制、按键输入、数字信号读取、中断基础
各位同学,欢迎来到第四章。这一章我们聊聊GPIO——也就是通用输入输出口。说白了,它就是芯片和外界打交道的最基本方式。你想想看,一个单片机要控制世界,总得有个“手脚”吧?GPIO就是那个手脚。
我个人习惯把GPIO比作一个“智能开关”。它既能输出高低电平去点亮LED、驱动继电器,也能读取外部信号是高还是低。嗯,这里要注意:GPIO虽然基础,但坑也不少。我在项目中遇到过因为GPIO配置不当导致整块板子电流倒灌烧芯片的事,所以咱们得认真对待。
核心概念:GPIO(General Purpose Input/Output)是微控制器最基础的引脚功能。每个引脚可以独立配置为输入或输出模式。输出模式下,我们可以控制引脚输出高电平(通常3.3V或5V)或低电平(0V);输入模式下,我们可以读取外部信号的电平状态。
4.1 LED控制:从点亮到PWM调光
先来个最简单的——点亮LED。这几乎是每个嵌入式工程师的“Hello World”。
电路上,LED正极通过一个限流电阻接到GPIO引脚,负极接地。为什么需要电阻?我曾经见过新手直接把LED怼到引脚上,结果LED瞬间烧了。因为LED内阻很小,不加限流的话电流会很大。一般用220Ω到1kΩ的电阻就行。
代码实现其实就几行:
// 配置GPIO为输出模式
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0; // 引脚0
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL; // 不上拉也不下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 点亮LED
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);
// 熄灭LED
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
// 翻转LED状态
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);
这里有个小细节:推挽输出和开漏输出的区别。推挽输出能主动拉高和拉低电平,驱动能力强;开漏输出只能拉低,拉高要靠外部上拉电阻。我建议初学者一律用推挽输出,除非你明确知道要用开漏(比如I2C总线)。
再进阶一点,用PWM(脉冲宽度调制)控制LED亮度。说白了就是快速开关LED,通过调整占空比让人眼感觉亮度变化。STM32的定时器可以硬件生成PWM,不需要软件去翻转引脚,效率高得多。
// 初始化定时器PWM
TIM_OC_InitTypeDef sConfigOC = {0};
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 500; // 占空比,范围0-999
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
// 动态调整亮度
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 750); // 75%亮度
经验之谈:PWM频率不要低于100Hz,否则人眼能感觉到闪烁。我一般用1kHz,既省电又平滑。如果驱动电机,频率要更高,通常10kHz以上。
4.2 按键输入:去抖是必修课
按键输入看起来简单——读引脚电平就行。但实际项目中,按键按下和松开时会产生机械抖动,电平会在几毫秒内反复跳变。如果不处理,一次按键可能被误判成十几次。
我曾经在一个工业控制面板上吃过这个亏。操作员按一次“启动”,机器抖动了七八次,差点出事故。从那以后,我所有按键都加了去抖处理。
去抖有两种方式:硬件去抖(加RC滤波电路)和软件去抖(延时采样)。嵌入式开发中,软件去抖更常用:
// 按键去抖检测
uint8_t Key_Scan(void)
{
static uint8_t last_state = 1; // 上次状态,默认高电平(未按下)
static uint16_t count = 0;
uint8_t current_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0);
if (current_state != last_state) {
// 状态变化,开始计数
count++;
if (count >= 5) { // 连续5次采样一致,认为稳定
last_state = current_state;
count = 0;
if (current_state == 0) { // 低电平表示按下
return 1; // 有效按键
}
}
} else {
count = 0; // 状态一致,清零计数
}
return 0;
}
这个代码的核心思想:不立即相信引脚电平变化,而是连续采样多次,确认电平稳定后才认为按键有效。采样间隔一般用10ms,5次就是50ms的去抖时间,足够滤掉机械抖动。
注意:按键电路要接上拉电阻(内部或外部),确保未按下时引脚为高电平。如果悬空,引脚电平不确定,会随机触发。我见过有人省掉上拉电阻,结果按键完全没法用。
4.3 数字信号读取:不只是读电平
数字信号读取,听起来就是读0和1。但实际项目中,信号可能来自各种传感器:霍尔开关、光电传感器、编码器等等。这些信号往往有特定的时序要求。
举个例子,读取一个旋转编码器的AB相脉冲。A相和B相输出有90度相位差,通过判断哪个信号先变化,就能知道旋转方向。这需要同时读取两个引脚,并且快速判断。
// 读取编码器方向
int8_t Encoder_ReadDir(void)
{
uint8_t a = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
uint8_t b = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1);
static uint8_t last_ab = 0;
uint8_t current_ab = (a << 1) | b; // 合并为2位值
// 查表法判断方向
const int8_t dir_table[4][4] = {
{0, 1, -1, 0},
{-1, 0, 0, 1},
{1, 0, 0, -1},
{0, -1, 1, 0}
};
int8_t dir = dir_table[last_ab][current_ab];
last_ab = current_ab;
return dir;
}
这里用了查表法,比if-else判断快得多。我在做电机控制时,这个函数要在中断里每10微秒调用一次,查表法省下的时间很可观。
4.4 中断基础:别让CPU空等
前面讲的都是轮询方式——CPU不断去读引脚状态。但你想过没有,如果按键10分钟才按一次,CPU这10分钟就一直在空转,啥也干不了。中断就是解决这个问题的。
中断的原理很简单:外部信号变化时,硬件自动暂停当前程序,跳到一个专门的中断服务函数(ISR)去处理,处理完再回来继续执行。这样CPU平时可以干别的事,有事件才响应。
配置一个外部中断的步骤:
- 使能GPIO时钟和SYSCFG时钟
- 配置GPIO为输入模式
- 将GPIO连接到外部中断线(EXTI)
- 配置中断触发方式(上升沿、下降沿或双边沿)
- 使能中断并编写ISR
// 配置PA0为外部中断,下降沿触发
void EXTI_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_SYSCFG_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发
GPIO_InitStruct.Pull = GPIO_PULLUP; // 内部上拉
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
// 中断服务函数
void EXTI0_IRQHandler(void)
{
if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET) {
// 清除中断标志位
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
// 处理按键事件
// 注意:ISR里不要做耗时操作,只设置标志位
key_pressed_flag = 1;
}
}
中断编程铁律:ISR要短小精悍,只做最必要的事。我一般只在ISR里设置一个标志位或计数,具体处理放到主循环里。如果在ISR里做延时、打印、复杂计算,整个系统会卡死。
4.5 知识体系总览
下面这张图把本章的核心逻辑串起来了。从GPIO的基本配置,到输出控制LED,再到输入读取按键和数字信号,最后用中断提升效率。每一步都是递进的。
4.6 避坑指南与实用技巧
最后,分享几个我踩过的坑,希望能帮你少走弯路:
- 引脚复用冲突:有些GPIO引脚同时连接着调试接口(如SWD)或晶振。我曾在某个项目里把SWD的时钟引脚配置成普通GPIO,结果程序烧录一次后就再也连不上调试器了。配置前一定先看数据手册的引脚功能表。
- 电流驱动能力:单个GPIO引脚一般只能输出几毫安到20毫安。如果需要驱动继电器、电机这类大电流设备,必须加三极管或MOS管驱动。直接接上去,芯片会过热保护甚至烧毁。
- 中断优先级:多个中断同时触发时,优先级低的会被打断。我建议把时间敏感的中断(如编码器读取)设高优先级,按键这类可以设低一些。
- 浮空输入:输入引脚如果不接上拉或下拉电阻,电平会随电磁环境飘忽不定。内部上拉电阻一般在40kΩ左右,如果要求更稳定的电平,外部加一个10kΩ上拉电阻更可靠。
调试小技巧:用逻辑分析仪观察GPIO波形,比用万用表直观得多。我调试PWM时,一眼就能看出占空比和频率对不对。几十块钱的USB逻辑分析仪就够用。
好了,这一章的内容就到这里。GPIO看似简单,但它是整个嵌入式系统的基石。把LED控制、按键输入、中断这些基本功练扎实了,后面学传感器通信、外设驱动就会轻松很多。记住:越是基础的东西,越值得花时间吃透。
公众号:蓝海资料掘金营,微信deep3321