第四章:GPIO与基础外设控制

各位同学,欢迎来到第四章。这一章我们开始真正动手了——控制硬件。GPIO是嵌入式开发里最基础、也最常用的外设接口。说白了,它就是芯片和外部世界对话的“手脚”。

我个人习惯把GPIO比作“数字世界的开关”。你给它高电平,它就输出3.3V;你给它低电平,它就输出0V。反过来,你也能读它上面的电压是高还是低。就这么简单,但几乎所有智能家居设备都离不开它。

4.1 GPIO原理:从寄存器到引脚

GPIO的全称是General Purpose Input/Output,通用输入输出口。每个GPIO引脚都可以独立配置为输入或输出模式。

在鸿蒙系统里,GPIO的操作被封装成了标准的HDF(Hardware Driver Foundation)接口。你不需要直接操作寄存器,但理解背后的原理很重要。

核心概念:

  • 输出模式:芯片内部通过推挽电路驱动引脚,输出高电平或低电平
  • 输入模式:引脚处于高阻态,读取外部电平状态
  • 上拉/下拉:内部电阻将引脚默认拉到VCC或GND,防止浮空
  • 中断模式:电平变化时触发CPU中断,不用轮询

我在项目中遇到过一个问题:某个按键按下后偶尔没反应。查了半天,发现是GPIO内部上拉电阻没使能,引脚处于浮空状态,电平乱跳。嗯,这种坑踩过一次就记住了。

4.2 LED灯控制:你的第一个硬件程序

控制LED灯,就是GPIO输出的“Hello World”。我们以鸿蒙Hi3861开发板为例,点亮板载LED。

先看硬件连接:LED正极通过限流电阻接到GPIO9,负极接地。输出高电平,灯亮;输出低电平,灯灭。

鸿蒙的GPIO操作接口在//device/soc/hisilicon/hi3861v100/sdk_liteos/include/hi_gpio.h里。我习惯用这套API:

#include "hi_gpio.h"
#include "hi_io.h"

// 初始化GPIO
hi_io_set_func(HI_IO_NAME_GPIO_9, HI_IO_FUNC_GPIO_9_GPIO);
hi_gpio_set_dir(HI_GPIO_IDX_9, HI_GPIO_DIR_OUT);

// 点亮LED
hi_gpio_set_ouput_val(HI_GPIO_IDX_9, HI_GPIO_VALUE1);

// 熄灭LED
hi_gpio_set_ouput_val(HI_GPIO_IDX_9, HI_GPIO_VALUE0);

你想想看,这三步其实对应了GPIO使用的完整流程:

  1. 复用功能配置:告诉芯片这个引脚用作GPIO,而不是I2C或UART
  2. 方向设置:输入还是输出
  3. 电平写入:输出高或低

小技巧:调试时可以用示波器看GPIO波形。如果没有示波器,用万用表测电压也行。我早期做项目时,经常靠万用表“听”代码跑没跑到某一行。

4.3 按键输入检测:从轮询到中断

按键检测是GPIO输入的典型应用。按键按下时,引脚电平变化,我们读取这个变化就知道按键被按下了。

最简单的做法是轮询——CPU不断读取引脚电平。但这样太浪费资源了,就像你一直盯着门口等人,啥事也干不了。

更好的方案是使用中断。鸿蒙里注册一个GPIO中断,当电平变化时自动调用回调函数。

#include "hi_gpio.h"
#include "hi_io.h"
#include "hi_interrupt.h"

// 中断回调函数
static void gpio_isr_func(unsigned int param)
{
    printf("按键被按下!\n");
}

// 初始化按键中断
hi_io_set_func(HI_IO_NAME_GPIO_8, HI_IO_FUNC_GPIO_8_GPIO);
hi_gpio_set_dir(HI_GPIO_IDX_8, HI_GPIO_DIR_IN);
hi_io_set_pull(HI_IO_NAME_GPIO_8, HI_IO_PULL_UP);  // 内部上拉

// 注册中断:下降沿触发(按键按下时电平从高变低)
hi_gpio_register_isr_function(HI_GPIO_IDX_8, HI_INT_TYPE_EDGE, 
                               HI_GPIO_EDGE_FALLING_LEVEL_LOW, 
                               gpio_isr_func, 0);

避坑指南:我曾经在量产阶段发现按键偶尔触发两次。原因是机械按键有抖动——按下瞬间电平会弹跳几次。解决方案是加10ms的软件消抖:在中断里启动一个定时器,10ms后再读取一次电平确认。

消抖代码示例:

// 简单的延时消抖
if (hi_gpio_get_input_val(HI_GPIO_IDX_8, &val) == 0) {
    hi_sleep_ms(10);  // 延时10ms
    if (hi_gpio_get_input_val(HI_GPIO_IDX_8, &val) == 0) {
        if (val == 0) {
            // 确认按键按下
        }
    }
}

4.4 PWM调光:让LED呼吸起来

PWM(脉冲宽度调制)是通过快速切换高低电平,改变占空比来模拟不同电压。说白了,就是让LED快速亮灭,人眼因为视觉暂留效应,看到的是平均亮度。

鸿蒙的PWM接口也很清晰。以Hi3861为例,PWM0对应GPIO9:

#include "hi_pwm.h"

// 初始化PWM
hi_pwm_init(HI_PWM_PORT_PWM0);

// 设置PWM参数:频率1000Hz,占空比50%
hi_pwm_set_clock(HI_PWM_PORT_PWM0, 1000);  // 设置时钟分频
hi_pwm_set_duty(HI_PWM_PORT_PWM0, 50);     // 占空比50%

// 启动PWM输出
hi_pwm_start(HI_PWM_PORT_PWM0);

要实现呼吸灯效果,只需要不断改变占空比:

for (int duty = 0; duty <= 100; duty += 5) {
    hi_pwm_set_duty(HI_PWM_PORT_PWM0, duty);
    hi_sleep_ms(20);
}
for (int duty = 100; duty >= 0; duty -= 5) {
    hi_pwm_set_duty(HI_PWM_PORT_PWM0, duty);
    hi_sleep_ms(20);
}

实际应用场景

  • 智能灯:PWM调光调色,配合RGB LED实现1600万色
  • 风扇调速:通过PWM控制MOS管,调节直流电机转速
  • 蜂鸣器:不同频率的PWM驱动蜂鸣器发出不同音调

我记得有个智能台灯的项目,客户要求调光时不能有频闪。我一开始用100Hz的PWM,结果用手机摄像头一拍,全是条纹。后来把频率提高到1kHz以上,人眼和摄像头都看不出来了。嗯,这里要注意:PWM频率太低会被人眼感知到闪烁,一般建议500Hz以上。

4.5 综合实战:智能小夜灯

把上面三个知识点串起来,做一个智能小夜灯:按键控制开关,PWM调节亮度。

// 伪代码逻辑
void main() {
    // 初始化LED(GPIO9,PWM模式)
    // 初始化按键(GPIO8,中断模式)
    
    while (1) {
        if (按键按下) {
            toggle_light();  // 切换开关状态
        }
        if (需要调光) {
            set_brightness(target_duty);  // 设置PWM占空比
        }
        hi_sleep_ms(10);  // 主循环延时
    }
}

这个例子虽然简单,但涵盖了GPIO输出、输入、中断、PWM四个核心技能。你想想看,智能家居里那些灯光控制、窗帘电机、传感器检测,底层都是这些东西。

我的建议:初学者先拿一块开发板,把LED点亮、熄灭、闪烁、呼吸灯都跑一遍。然后接个按键,做中断检测。最后把两者结合。这个过程走通了,GPIO这块你就入门了。

下一章我们会讲更复杂的I2C和SPI通信协议,用来连接温湿度传感器、OLED屏幕这些外设。但GPIO是基础中的基础,一定要练扎实。


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