第4章 GPIO驱动基础

GPIO,说白了就是芯片上那些可以编程控制的引脚。你可以让它输出高电平或低电平,也可以读取它当前的电平状态。在嵌入式系统里,GPIO 是最基础也最常用的外设接口——控制 LED、读取按键、模拟时序,哪样都离不开它。

这一章,我带你从 Linux GPIO 子系统的整体框架入手,然后深入到寄存器层面,最后再聊聊怎么在用户空间通过 sysfs 接口直接操作 GPIO。嗯,这些都是我这些年做驱动开发天天打交道的玩意儿。

Linux GPIO 子系统框架 用户空间 (sysfs / libgpiod) GPIO 子系统核心 (gpiolib) GPIO 控制器 (硬件寄存器) 关键组件 • gpio_chip: 每个控制器对应一个 • gpio_desc: 每个引脚的描述符 • 操作函数: .get, .set, .direction_input/output • 中断支持: .to_irq, 边沿触发 • sysfs 接口: /sys/class/gpio/ • 设备树: 描述引脚与控制器映射 用户空间 → 内核 → 硬件寄存器

4.1 GPIO 子系统框架

Linux 内核的 GPIO 子系统,我习惯叫它 gpiolib。它把不同芯片的 GPIO 控制器抽象成统一的接口。你写驱动的时候,不用关心底层寄存器具体怎么配,调用 gpiolib 提供的 API 就行。

框架分三层:

我记得刚接触 gpiolib 时,总觉得它很绕。后来发现,你只要理解两个核心结构体就行:gpio_chip 和 gpio_desc。前者代表一个控制器,后者代表一个引脚。

核心结构体 gpio_chip(简化版):
struct gpio_chip {
    const char *label;
    int base;              // 引脚编号起始
    int ngpio;             // 本控制器管理的引脚数
    int (*direction_input)(struct gpio_chip *chip, unsigned offset);
    int (*direction_output)(struct gpio_chip *chip, unsigned offset, int value);
    int (*get)(struct gpio_chip *chip, unsigned offset);
    void (*set)(struct gpio_chip *chip, unsigned offset, int value);
    // ... 还有中断、请求、释放等回调
};

你写一个 GPIO 控制器驱动,主要就是填充这个结构体,然后调用 gpiochip_add_data() 注册到内核。剩下的,gpiolib 帮你搞定。

4.2 GPIO 控制寄存器

说完了框架,咱们得落地到硬件。Atlas 200 DK 用的海思芯片,GPIO 模块的寄存器布局其实很典型。每个 GPIO 引脚通常对应这么几类寄存器:

寄存器类型 作用 位宽
GPIO_SWPORTA_DR 数据输出寄存器(写引脚电平) 32位,每bit对应一个引脚
GPIO_SWPORTA_DDR 方向寄存器(0=输入,1=输出) 32位,每bit对应一个引脚
GPIO_EXT_PORTA 数据输入寄存器(读引脚电平) 32位,只读
GPIO_INTEN 中断使能寄存器 32位
GPIO_INTTYPE_LEVEL 中断触发类型(电平/边沿) 32位
GPIO_INT_POLARITY 中断极性(高/低电平,上升/下降沿) 32位

举个例子,你想让 GPIO12 输出高电平。先设置方向寄存器对应位为 1(输出),再写数据寄存器对应位为 1。就这么简单。

我的经验: 调试 GPIO 时,我最喜欢用示波器或者逻辑分析仪直接量引脚。有时候代码看起来没问题,但寄存器没写进去,或者被别的驱动复用了,一量就知道。别光靠猜。

4.3 在用户空间操作 GPIO(sysfs 接口)

对于快速原型验证,我强烈推荐先用 sysfs 接口。它不需要写内核模块,直接在 shell 里 echo 和 cat 就能控制 GPIO。虽然现在内核社区更推荐 libgpiod,但 sysfs 在调试阶段依然很顺手。

基本步骤就四步:

  1. 导出引脚:echo 12 > /sys/class/gpio/export
  2. 设置方向:echo out > /sys/class/gpio/gpio12/direction
  3. 写值:echo 1 > /sys/class/gpio/gpio12/value
  4. 读值:cat /sys/class/gpio/gpio12/value

用完之后记得 echo 12 > /sys/class/gpio/unexport 释放引脚。

注意: 我曾经在项目里犯过一个低级错误——导出引脚后忘了设置方向,直接写 value。结果写不进去,查了半天才发现方向默认是输入。所以,顺序别搞反。

下面是一个完整的 shell 示例,控制 GPIO12 闪烁 5 次:

#!/bin/sh
GPIO=12
# 导出
echo $GPIO > /sys/class/gpio/export
# 设为输出
echo out > /sys/class/gpio/gpio$GPIO/direction

for i in 1 2 3 4 5; do
    echo 1 > /sys/class/gpio/gpio$GPIO/value
    sleep 0.5
    echo 0 > /sys/class/gpio/gpio$GPIO/value
    sleep 0.5
done

# 清理
echo $GPIO > /sys/class/gpio/unexport

你想想看,就这么几行,就能让一个 LED 闪起来。对于硬件 bring-up 阶段,这种快速验证方式特别管用。

sysfs 接口的路径结构:
/sys/class/gpio/
├── export          # 写引脚号导出
├── unexport        # 写引脚号释放
├── gpiochip0/      # 第一个GPIO控制器
│   ├── base        # 起始编号
│   ├── ngpio       # 引脚数量
│   └── label       # 控制器名称
├── gpiochip64/     # 第二个控制器
└── gpio12/         # 导出后出现的目录
    ├── direction   # in / out / high / low
    ├── value       # 0 或 1
    ├── edge        # none / rising / falling / both
    └── active_low  # 0 或 1

我个人习惯在调试时先 cat 一下 /sys/kernel/debug/gpio,看看当前所有 GPIO 的状态。这个 debugfs 接口会列出每个引脚的申请者、方向、当前值,排查冲突特别好用。

嗯,这一章的内容差不多就这些。从框架到寄存器,再到用户空间操作,GPIO 驱动的基础脉络你应该清楚了。下一章我们会深入 GPIO 中断,那才是真正体现驱动工程师水平的地方。