第1章 GPIO与基础外设驱动

各位同学,咱们今天聊聊窗帘电机里最基础、也最绕不开的部分——GPIO与基础外设驱动。说白了,就是让单片机跟外面的世界打交道。你想想看,电机转不转、按键按没按、灯亮不亮,这些都得靠GPIO来管。

我个人习惯把GPIO比作单片机的「手脚」。手脚不灵活,整个系统就瘫了。我在项目中遇到过不少次,明明逻辑写对了,硬件就是不干活,最后查出来是GPIO配置出了问题。嗯,这里要注意,配置错了,神仙也救不了。

1.1 GPIO输入输出模式配置

GPIO的模式,说白了就两种:输入和输出。但实际用起来,花样还挺多。

模式 典型用途 注意事项
推挽输出 驱动LED、蜂鸣器 电流驱动能力有限,别直接带大负载
开漏输出 I2C总线、电平转换 需要外部上拉电阻
浮空输入 按键检测(外部有上拉) 容易受干扰,不推荐单独使用
上拉/下拉输入 按键检测(省外部电阻) 内部上拉约40kΩ,弱上拉
模拟输入 ADC采样 关闭数字功能,省电

配置代码其实很简单,但容易踩坑。我贴一段常用的初始化代码:

void GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    // 使能时钟——这一步忘了,后面全白搭
    __HAL_RCC_GPIOB_CLK_ENABLE();
    
    // 配置PB0为推挽输出,驱动LED
    GPIO_InitStruct.Pin = GPIO_PIN_0;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    
    // 配置PB1为上拉输入,接按键
    GPIO_InitStruct.Pin = GPIO_PIN_1;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
我曾经犯过的错: 忘了使能GPIO时钟。结果折腾了半天,以为硬件坏了。记住,STM32的GPIO必须先开时钟,否则写寄存器也没用。

1.2 按键消抖

按键消抖,这话题老生常谈了。但每次做项目,总有人在这上面翻车。为什么会这样?因为机械按键按下和松开的时候,触点会弹跳,产生一连串的毛刺信号。单片机跑得飞快,一个毛刺就能触发一次中断。

消抖有两种路子:硬件消抖和软件消抖。

硬件消抖

最简单的方法,就是加个RC低通滤波器。电阻选10kΩ,电容选0.1μF,时间常数约1ms。这个组合我用了好多年,效果稳定。不过要注意,电容别太大,否则按键响应会变慢。

软件消抖

我个人更推荐软件消抖,省成本、调参数也方便。核心思路就一句话:检测到电平变化后,等一段时间再确认。

uint8_t Key_Scan(void)
{
    static uint8_t last_state = 1;  // 假设上拉,默认高电平
    static uint16_t count = 0;
    uint8_t current_state;
    
    current_state = HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin);
    
    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;
}
我的小技巧: 消抖延时别用死等(比如HAL_Delay),会阻塞系统。用定时器轮询或者状态机来做,既高效又优雅。

1.3 LED指示灯驱动

LED驱动,看起来简单,但里面也有门道。直接拿GPIO推挽输出,串个限流电阻就完事了。电阻怎么选?我一般按这个公式算:

R = (Vcc - Vf) / If

Vcc是3.3V,Vf是LED正向压降(红色约2V,蓝色约3V),If取5~10mA。算下来,红色LED用220Ω,蓝色用100Ω。嗯,差不多就这样。

实际项目中,我习惯把LED做成呼吸灯效果,让窗帘电机的工作状态更直观。用PWM控制亮度,代码也不复杂:

void LED_Breath(void)
{
    static uint16_t pwm_val = 0;
    static int8_t direction = 1;
    
    __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pwm_val);
    
    pwm_val += direction * 10;
    if (pwm_val >= 1000) direction = -1;
    if (pwm_val == 0) direction = 1;
}
重点: LED驱动电流别超过GPIO的极限(通常20mA)。如果需要更亮,加个三极管或者MOS管扩流。

1.4 蜂鸣器驱动

蜂鸣器分两种:有源和无源。有源的给电就响,无源的得给PWM波才能发声。窗帘电机里,我一般用有源的,简单粗暴,用来提示操作成功或故障报警。

驱动方式跟LED差不多,但要注意:蜂鸣器是感性负载,关断时会产生反向电动势。直接接GPIO,容易把引脚烧了。我建议加个续流二极管,或者用三极管驱动。

void Buzzer_On(void)
{
    HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_SET);
}

void Buzzer_Off(void)
{
    HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_RESET);
}

// 短促鸣叫,用于按键反馈
void Buzzer_Beep(uint16_t duration_ms)
{
    Buzzer_On();
    HAL_Delay(duration_ms);
    Buzzer_Off();
}
我曾经踩过的坑: 蜂鸣器直接并联在GPIO上,结果一关断,GPIO被高压打坏了。后来老老实实加了NPN三极管和续流二极管,再也没出过问题。

1.5 外部中断(EXTI)应用

外部中断,说白了就是让单片机「被动」响应外部事件。按键按下、电机堵转、限位开关触发,这些都可以用EXTI来处理。

配置EXTI有几个关键点:

  • 选择触发边沿:上升沿、下降沿、或者双边沿
  • 配置中断优先级:别让不重要的事件抢了CPU
  • 中断服务函数里别干重活:只设标志位,具体处理放主循环
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == KEY_Pin)
    {
        // 设置按键标志,不要在中断里做消抖
        key_press_flag = 1;
    }
    else if (GPIO_Pin == LIMIT_SWITCH_Pin)
    {
        // 限位开关触发,立即停止电机
        motor_stop_flag = 1;
    }
}
我的经验: 中断服务函数越短越好。我见过有人把HAL_Delay放在中断里,结果系统直接卡死。记住,中断里只做三件事:读数据、清标志、设标志。

好了,这一章的内容就这些。GPIO和基础外设驱动,看似简单,但它是整个嵌入式系统的根基。你想想看,如果连灯都点不亮、按键都读不准,后面的电机控制、通信协议还怎么玩?

下一章,咱们聊聊定时器和PWM,窗帘电机调速的核心就靠它了。


公众号:蓝海资料掘金营,微信deep3321