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;
}
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 控制器支持边沿触发和电平触发两种模式。中断处理的流程如下:
- 配置中断触发方式(上升沿、下降沿、高电平、低电平)
- 使能中断(写 INTEDS 寄存器)
- 注册中断处理函数(
request_irq) - 在中断处理函数中读取 INTFLG 判断哪个引脚触发
- 处理完后写 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 注册到内核。
测试步骤:
- 导出 GPIO:
echo 16 > /sys/class/gpio/export - 设置方向:
echo out > /sys/class/gpio/gpio16/direction - 设置值:
echo 1 > /sys/class/gpio/gpio16/value - 读取值:
cat /sys/class/gpio/gpio16/value - 测试中断:
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
dmesg 有没有错误。常见问题:gpiochip_add 返回 -EBUSY,说明基地址冲突。设置 .base = -1 让内核自动分配就能解决。
下面这张图总结了 GPIO 驱动的整体架构和数据流:
从这张图可以清楚看到,用户空间的 sysfs 操作经过内核 GPIO 子系统,再通过 HAL 层的回调函数,最终落到寄存器读写上。每一层都有明确的职责,这也是 Linux 驱动设计的精髓——分层解耦。
好了,GPIO 驱动开发的核心内容就这些。从寄存器映射到 HAL 框架,再到中断处理,最后用 sysfs 验证,一条完整的链路都走通了。你在实际项目中遇到 GPIO 相关的问题,欢迎随时交流。