3. 嵌入式硬件平台概览

GPIO驱动,说白了就是让CPU的引脚能听话地输出高低电平,或者准确读取外部信号。很多新手一上来就对着寄存器直接操作,觉得这样最“底层”、最“高效”。嗯,我早年也这么干过,结果项目一复杂,代码改得想哭。

这一章,我们就聊聊怎么把GPIO驱动写得既专业又省心。我会结合Linux内核的分层思想、设备树描述、子系统API,以及用户空间的操作方法,把这块讲透。

3.1 分层驱动设计思想:为什么要把简单的事情搞复杂?

你想想看,一个GPIO驱动,如果直接操作寄存器,换一颗SoC,所有代码都得重写。这谁受得了?

Linux内核的做法是分层。把驱动拆成三块:

  • 硬件层(Chip层):直接跟寄存器打交道。读、写、方向控制、中断触发。这部分是SoC相关的。
  • 核心层(GPIO Subsystem Core):内核帮你搭好的框架。提供统一的接口注册、中断映射、sysfs/chardev支持。这部分你不用动。
  • 用户层(Consumer层):其他驱动(比如LED、按键、SD卡检测)或者用户空间程序,通过标准API来用GPIO。这部分是业务逻辑。

核心思想: 硬件变化只改硬件层,上层代码纹丝不动。我在一个项目里,把主控从STM32MP1换成了i.MX8M,GPIO驱动只重写了chip层的注册函数,上层十几个驱动模块一行代码没改。这就是分层的力量。

分层之后,代码结构清晰了,调试也方便。哪一层出问题,就盯着哪一层查。

GPIO分层驱动架构图 GPIO分层驱动架构 用户层(Consumer) LED驱动 / 按键驱动 / 用户空间程序 核心层(GPIO Subsystem Core) gpiolib / 中断映射 / sysfs & chardev 接口 硬件层(Chip层) 寄存器操作 / 方向控制 / 中断处理 硬件变化只改硬件层,上层代码无需修改

3.2 设备树GPIO描述:让硬件配置“可读”

设备树(Device Tree)是嵌入式Linux里描述硬件信息的“说明书”。GPIO的信息,比如哪个引脚、什么功能、要不要上拉,都写在.dts文件里。

我个人习惯,写设备树GPIO节点时,先搞清楚三件事:

  1. GPIO控制器是谁? 比如 gpio0、gpio1,对应哪个寄存器基址。
  2. 引脚号是多少? 注意,设备树里引脚号是控制器内部的偏移,不是芯片物理引脚号。
  3. 有效电平是什么? 高有效还是低有效,用 GPIO_ACTIVE_HIGH 或 GPIO_ACTIVE_LOW 宏。

举个例子,一个LED节点:

/ {
    my_led {
        compatible = "my-led";
        led-gpios = &gpio1 15 GPIO_ACTIVE_HIGH;
        /* 含义:使用gpio1控制器,引脚偏移15,高电平点亮 */
    };
};

小技巧: 设备树里 *-gpios 属性名可以自定义,但内核推荐用 <功能>-gpios 的命名方式,比如 reset-gpios、irq-gpios。这样读起来一目了然。

我曾经在一个项目里,设备树里GPIO引脚号写错了,结果LED死活不亮。查了两天才发现,原来芯片手册里的引脚号和设备树里的偏移号差了4。嗯,从那以后,我每次写设备树都会对着原理图和芯片手册反复核对。

3.3 GPIO子系统API:内核里的“标准件”

内核提供了gpiolib,封装了一套标准API。你写驱动时,不需要再自己操作寄存器了。

常用的API就这几个:

API函数 功能 使用场景
gpio_request() 申请一个GPIO 使用前必须调用,防止冲突
gpio_direction_output() 设置为输出并设置初始电平 控制LED、蜂鸣器
gpio_direction_input() 设置为输入 读取按键、传感器状态
gpio_get_value() 读取当前电平 输入场景
gpio_set_value() 设置输出电平 输出场景
gpio_to_irq() 获取GPIO对应的中断号 注册中断处理函数
gpio_free() 释放GPIO 驱动卸载时调用

一个典型的驱动片段长这样:

#include <linux/gpio.h>

#define LED_GPIO 15

static int __init my_led_init(void)
{
    int ret;

    ret = gpio_request(LED_GPIO, "my-led");
    if (ret) {
        pr_err("Failed to request GPIO %d\n", LED_GPIO);
        return ret;
    }

    gpio_direction_output(LED_GPIO, 0); /* 初始低电平 */
    gpio_set_value(LED_GPIO, 1);        /* 点亮LED */

    return 0;
}

static void __exit my_led_exit(void)
{
    gpio_set_value(LED_GPIO, 0);
    gpio_free(LED_GPIO);
}

注意: gpio_request() 一定要检查返回值。我见过有人不检查,结果两个驱动同时申请同一个GPIO,系统没报错,但行为完全不可控。调试起来非常痛苦。

3.4 用户空间GPIO操作:不写内核代码也能控制

有时候,我们只是想快速验证一个GPIO功能,或者做一些简单的控制逻辑,没必要写内核驱动。Linux提供了用户空间操作GPIO的方式。

早期是 sysfs接口,现在推荐用 chardev接口(libgpiod)。

3.4.1 sysfs接口(传统方式)

操作步骤很简单:

  1. 导出GPIO:echo 15 > /sys/class/gpio/export
  2. 设置方向:echo out > /sys/class/gpio/gpio15/direction
  3. 写值:echo 1 > /sys/class/gpio/gpio15/value
  4. 读值:cat /sys/class/gpio/gpio15/value
  5. 用完释放:echo 15 > /sys/class/gpio/unexport

提醒: sysfs接口虽然简单,但每个操作都要读写文件,性能差。而且多个进程同时操作同一个GPIO时,容易出问题。所以,新项目建议用chardev接口。

3.4.2 chardev接口(libgpiod,推荐)

libgpiod是内核官方推荐的用户空间GPIO操作库。它通过字符设备(/dev/gpiochipX)与内核通信,性能好,接口也清晰。

常用命令行工具:

  • gpiodetect:列出系统中的GPIO控制器
  • gpioinfo:查看某个控制器的引脚信息
  • gpioget:读取引脚值
  • gpioset:设置引脚值
  • gpiomon:监控引脚中断

举个例子,用gpioset点亮LED:

# 设置gpiochip0的第15脚为高电平
gpioset gpiochip0 15=1

用C代码操作也很简洁:

#include <gpiod.h>

struct gpiod_chip *chip;
struct gpiod_line *line;

chip = gpiod_chip_open_by_name("gpiochip0");
line = gpiod_chip_get_line(chip, 15);

gpiod_line_request_output(line, "my-app", 0);
gpiod_line_set_value(line, 1);  /* 输出高电平 */

gpiod_line_release(line);
gpiod_chip_close(chip);

我的建议: 如果你在做产品原型验证,或者写一些简单的测试脚本,用libgpiod的CLI工具就够了。但如果是正式产品里的用户空间控制逻辑,还是用C API更可靠。我在一个工业控制项目里,就是用libgpiod的C接口实现了GPIO的实时控制,跑了两年没出过问题。

3.5 避坑指南:我踩过的那些GPIO坑

做GPIO驱动开发,有些坑是共通的。我把自己踩过的列出来,你遇到了可以少走弯路。

  • 引脚复用冲突: 同一个引脚被多个外设功能占用。比如你想用GPIO,但芯片默认把它配成了UART。解决办法:检查芯片手册的引脚复用表,在设备树或bootloader里正确配置。
  • 电平不匹配: 1.8V的SoC接了3.3V的外设,直接烧引脚。或者反过来,电平不够,外设不工作。解决办法:加电平转换芯片,或者确认SoC引脚是否容忍5V。
  • 中断触发抖动: 机械按键按下时,电平会抖动好几次,导致一次按下触发多次中断。解决办法:硬件加RC滤波,软件做去抖(延迟采样或状态机)。
  • 忘记释放GPIO: 驱动卸载时没调用 gpio_free(),下次加载时就申请失败。解决办法:在驱动退出路径里,确保每个 gpio_request() 都有对应的 gpio_free()。

特别提醒: 调试GPIO时,别只盯着软件。拿万用表或示波器量一下引脚电平,往往比看代码更快定位问题。我有个习惯,调试桌上永远放一台示波器,抓GPIO波形比猜代码靠谱多了。

好了,这一章的内容就这些。分层设计让你代码更健壮,设备树让硬件配置更清晰,子系统API让你开发更高效,用户空间操作让你调试更灵活。把这些吃透了,GPIO驱动这块基本就稳了。