4. 开发环境搭建

GPIO 驱动,说白了就是让芯片的引脚听话。你想让它输出高电平就输出高电平,想让它检测按键按下就检测按键按下。在 R-Car 平台上,GPIO 控制器是挂在内部总线上的一个外设,驱动开发的核心就是搞定三件事:寄存器映射、中断处理、用户空间接口。

我个人习惯把 GPIO 驱动分成两层:底层是硬件操作层,负责读写寄存器;上层是框架层,对接内核的 GPIO 子系统。今天我们就从寄存器映射开始,一步步把整个驱动搭起来。

4.1 GPIO控制器寄存器映射

R-Car 的 GPIO 控制器,每个端口有一组寄存器。以 R-Car H3 为例,GPIO 基地址在 0xE605_0000 附近(具体看芯片手册)。每个引脚的控制寄存器布局如下:

偏移地址 寄存器名 功能
0x00 GPSR GPIO 引脚功能选择(输入/输出)
0x04 GPDR GPIO 方向控制(0=输入,1=输出)
0x08 GPSR GPIO 输出数据寄存器
0x0C GPCR GPIO 输出清零寄存器
0x10 GPDR GPIO 输入数据寄存器(只读)
0x14 INTEDS 中断使能设置寄存器
0x18 INTEDC 中断使能清除寄存器
0x1C INTFLG 中断标志寄存器
0x20 INTCLR 中断清除寄存器

嗯,这里要注意:R-Car 的 GPIO 寄存器是 32 位的,每个引脚对应一个 bit。比如 GPDR 寄存器的 bit 0 控制 GPIO0_0 的方向,bit 1 控制 GPIO0_1,以此类推。

在驱动里,我们通过 ioremap 把物理地址映射到内核虚拟地址空间:

static void __iomem *gpio_base;

static int gpio_probe(struct platform_device *pdev)
{
    struct resource *res;

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    gpio_base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(gpio_base))
        return PTR_ERR(gpio_base);

    // 现在可以通过 gpio_base + offset 访问寄存器了
    // 例如:设置 GPIO0_0 为输出
    u32 val = ioread32(gpio_base + GPDR);
    val |= BIT(0);
    iowrite32(val, gpio_base + GPDR);

    return 0;
}
我的经验: 在 R-Car 上,devm_ioremap_resource 比直接 ioremap 更安全。它自动处理了资源冲突检查,而且设备移除时会自动释放映射。我在一个项目里因为用了 ioremap 忘记释放,导致第二次 insmod 时系统直接挂掉……从那以后我就只用 devm_ 系列函数了。

4.2 基于HAL的GPIO驱动框架

R-Car 的 HAL(硬件抽象层)把 GPIO 操作封装成了标准接口。说白了,就是让你不用每次都去读写寄存器,而是调用一组 API。HAL 层通常放在 drivers/pinctrl/renesas/ 目录下。

HAL 的核心结构体是 struct gpio_chip,内核的 GPIO 子系统通过它来统一管理:

static struct gpio_chip rcar_gpio_chip = {
    .label          = "rcar-gpio",
    .owner          = THIS_MODULE,
    .request        = rcar_gpio_request,
    .free           = rcar_gpio_free,
    .direction_input  = rcar_gpio_direction_input,
    .direction_output = rcar_gpio_direction_output,
    .get            = rcar_gpio_get,
    .set            = rcar_gpio_set,
    .set_config     = rcar_gpio_set_config,
    .base           = -1,  // 动态分配
    .ngpio          = 32,  // 每个端口32个引脚
};

每个回调函数背后,其实就是在操作我们刚才映射的寄存器。比如 direction_output:

static int rcar_gpio_direction_output(struct gpio_chip *chip, unsigned offset, int value)
{
    void __iomem *base = gpiochip_get_data(chip);
    u32 reg;

    // 先设置输出值
    reg = ioread32(base + GPSR);
    if (value)
        reg |= BIT(offset);
    else
        reg &= ~BIT(offset);
    iowrite32(reg, base + GPSR);

    // 再设置方向为输出
    reg = ioread32(base + GPDR);
    reg |= BIT(offset);
    iowrite32(reg, base + GPDR);

    return 0;
}

你想想看,为什么先设值再设方向?因为如果先设方向为输出,此时输出寄存器里可能是随机值,引脚上会瞬间跳变。我在项目中遇到过这个问题——LED 在初始化时闪了一下,后来调整了顺序才解决。

4.3 实现GPIO中断处理

GPIO 中断是驱动里最常用的功能之一。R-Car 的 GPIO 控制器支持边沿触发和电平触发两种模式。中断处理的流程如下:

  1. 配置中断触发方式(上升沿、下降沿、高电平、低电平)
  2. 使能中断(写 INTEDS 寄存器)
  3. 注册中断处理函数(request_irq)
  4. 在中断处理函数中读取 INTFLG 判断哪个引脚触发
  5. 处理完后写 INTCLR 清除中断标志

中断处理函数示例:

static irqreturn_t rcar_gpio_irq_handler(int irq, void *dev_id)
{
    struct rcar_gpio_priv *priv = dev_id;
    u32 flg = ioread32(priv->base + INTFLG);
    int pin;

    // 遍历所有引脚,找到触发中断的引脚
    for_each_set_bit(pin, (unsigned long *)&flg, 32) {
        // 调用该引脚注册的中断处理函数
        generic_handle_irq(irq_find_mapping(priv->irq_domain, pin));
    }

    // 清除所有中断标志
    iowrite32(flg, priv->base + INTCLR);

    return IRQ_HANDLED;
}
注意: 中断处理函数里不要做耗时操作。我曾经在中断里直接调用 printk 调试,结果在高频中断下系统直接死锁。正确的做法是使用 tasklet 或 workqueue 把耗时操作推到下半部执行。

注册中断的代码:

static int rcar_gpio_irq_setup(struct platform_device *pdev, struct rcar_gpio_priv *priv)
{
    int irq = platform_get_irq(pdev, 0);
    int ret;

    // 创建 irq_domain,用于将硬件引脚号映射到 Linux IRQ 号
    priv->irq_domain = irq_domain_add_linear(NULL, 32, &irq_domain_ops, priv);
    if (!priv->irq_domain)
        return -ENOMEM;

    ret = devm_request_irq(&pdev->dev, irq, rcar_gpio_irq_handler,
                           IRQF_SHARED, "rcar-gpio", priv);
    if (ret) {
        dev_err(&pdev->dev, "failed to request irq\n");
        return ret;
    }

    return 0;
}

4.4 在用户空间通过sysfs测试GPIO

驱动写完了,怎么验证?最简单的方法就是用 sysfs。内核的 GPIO 子系统会在 /sys/class/gpio/ 下暴露接口。前提是你的驱动调用了 gpiochip_add 注册到内核。

测试步骤:

  1. 导出 GPIO:echo 16 > /sys/class/gpio/export
  2. 设置方向:echo out > /sys/class/gpio/gpio16/direction
  3. 设置值:echo 1 > /sys/class/gpio/gpio16/value
  4. 读取值:cat /sys/class/gpio/gpio16/value
  5. 测试中断:echo in > /sys/class/gpio/gpio16/direction,然后用示波器或按键触发

我习惯写一个简单的 shell 脚本做自动化测试:

#!/bin/sh
# GPIO 测试脚本
GPIO=16

# 导出 GPIO
if [ ! -d /sys/class/gpio/gpio$GPIO ]; then
    echo $GPIO > /sys/class/gpio/export
    sleep 0.1
fi

# 测试输出
echo out > /sys/class/gpio/gpio$GPIO/direction
echo 1 > /sys/class/gpio/gpio$GPIO/value
echo "GPIO$GPIO set to 1"
sleep 1
echo 0 > /sys/class/gpio/gpio$GPIO/value
echo "GPIO$GPIO set to 0"

# 测试输入
echo in > /sys/class/gpio/gpio$GPIO/direction
val=$(cat /sys/class/gpio/gpio$GPIO/value)
echo "GPIO$GPIO input value: $val"

# 释放 GPIO
echo $GPIO > /sys/class/gpio/unexport
调试技巧: 如果 sysfs 里看不到你的 GPIO,先检查 dmesg 有没有错误。常见问题:gpiochip_add 返回 -EBUSY,说明基地址冲突。设置 .base = -1 让内核自动分配就能解决。

下面这张图总结了 GPIO 驱动的整体架构和数据流:

GPIO驱动架构图 GPIO驱动架构与数据流 用户空间 (sysfs / devfs) 内核 GPIO 子系统 (gpiolib) HAL 抽象层 (gpio_chip 回调) 寄存器操作 (ioread32 / iowrite32) R-Car GPIO 硬件控制器 关键数据结构 • struct gpio_chip • struct irq_domain • struct platform_device • struct resource

从这张图可以清楚看到,用户空间的 sysfs 操作经过内核 GPIO 子系统,再通过 HAL 层的回调函数,最终落到寄存器读写上。每一层都有明确的职责,这也是 Linux 驱动设计的精髓——分层解耦。

好了,GPIO 驱动开发的核心内容就这些。从寄存器映射到 HAL 框架,再到中断处理,最后用 sysfs 验证,一条完整的链路都走通了。你在实际项目中遇到 GPIO 相关的问题,欢迎随时交流。