4、GPIO驱动开发:从寄存器到用户空间
GPIO,说白了就是芯片的“手脚”。你想让某个引脚输出高电平,或者读取外部设备的状态,都得靠它。在嵌入式Linux下,GPIO驱动开发是入门必修课。今天我就带你走一遍完整流程:从GPIO子系统框架,到英伟达平台的寄存器映射,再到写一个简单的驱动,最后用sysfs在用户空间操作它。
4.1 GPIO子系统框架:Linux怎么管这些引脚?
Linux内核的GPIO子系统,其实分三层。我画个简单的逻辑图给你看:
- 最底层:硬件相关层 —— 直接操作寄存器,由芯片厂商提供。
- 中间层:GPIO核心层 —— 提供统一的API,比如gpio_request()、gpio_direction_output()。
- 最上层:用户接口层 —— 通过sysfs或字符设备暴露给用户空间。
为什么要这么分层?你想想看,如果每个驱动都直接操作寄存器,那代码就乱套了。核心层帮你做了“引脚编号到寄存器地址”的映射,你只需要调用标准接口就行。
核心数据结构:每个GPIO控制器对应一个struct gpio_chip。里面定义了方向设置、读写值等回调函数。
我在项目中遇到过一个问题:某个GPIO在驱动里死活拉不高。查了半天,发现是另一个驱动抢先注册了同一个引脚。嗯,这就是没用好gpio_request()的后果。
4.2 英伟达平台GPIO控制器寄存器映射
英伟达的Tegra系列,GPIO控制器通常挂在APB总线上。每个GPIO bank有32个引脚,寄存器偏移是固定的。我以Tegra X1为例:
| 寄存器名称 | 偏移地址 | 描述 |
|---|---|---|
| GPIO_CNF | 0x00 | 方向配置(1=输出,0=输入) |
| GPIO_OE | 0x04 | 输出使能 |
| GPIO_OUT | 0x08 | 输出值寄存器 |
| GPIO_IN | 0x0C | 输入值寄存器 |
| GPIO_INT_STA | 0x10 | 中断状态 |
每个bank的基地址怎么算?设备树里会给出。比如:
gpio@6000d000 {
compatible = "nvidia,tegra210-gpio";
reg = <0x0 0x6000d000 0x0 0x1000>;
interrupts = <0 32 0x04>;
#gpio-cells = <2>;
gpio-controller;
};
这里reg属性告诉内核:寄存器基地址是0x6000d000,范围4KB。我个人习惯在驱动里用ioremap()把物理地址映射到虚拟地址空间,然后直接读写。
注意:英伟达的GPIO寄存器是32位的,但有些引脚可能只用了低16位。写寄存器时千万别把高16位搞乱了,否则会影响其他bank。
4.3 编写简单的GPIO控制驱动
好,理论说完了,咱们动手写一个。这个驱动要做的事很简单:注册一个GPIO,然后提供文件操作接口,让用户空间能读写它。
先看核心代码:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/gpio.h>
#include <linux/platform_device.h>
#define GPIO_PIN 184 // 假设是某个GPIO
static int gpio_drv_open(struct inode *inode, struct file *file) {
return 0;
}
static ssize_t gpio_drv_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos) {
char val;
if (copy_from_user(&val, buf, 1))
return -EFAULT;
if (val == '1')
gpio_set_value(GPIO_PIN, 1);
else
gpio_set_value(GPIO_PIN, 0);
return count;
}
static struct file_operations gpio_fops = {
.owner = THIS_MODULE,
.open = gpio_drv_open,
.write = gpio_drv_write,
};
static int __init gpio_drv_init(void) {
int ret;
ret = gpio_request(GPIO_PIN, "my-gpio");
if (ret) {
pr_err("Failed to request GPIO %d\n", GPIO_PIN);
return ret;
}
gpio_direction_output(GPIO_PIN, 0);
// 注册字符设备...
return 0;
}
static void __exit gpio_drv_exit(void) {
gpio_free(GPIO_PIN);
// 注销字符设备...
}
module_init(gpio_drv_init);
module_exit(gpio_drv_exit);
MODULE_LICENSE("GPL");
这段代码里,我用了gpio_request()来申请引脚。为什么要申请?防止别的驱动乱用。我曾经在调试时忘了这步,结果两个驱动同时操作同一个引脚,电平乱跳,排查了一下午。
小技巧:调试时可以用gpio_set_value()配合示波器看波形。如果波形不对,先检查gpio_direction_output()有没有调用。
4.4 用户空间通过sysfs操作GPIO
其实,很多时候你不需要写内核驱动。Linux的GPIO子系统已经提供了sysfs接口,用户空间可以直接操作。步骤很简单:
- 导出GPIO:echo 184 > /sys/class/gpio/export
- 设置方向:echo out > /sys/class/gpio/gpio184/direction
- 写值:echo 1 > /sys/class/gpio/gpio184/value
- 读值:cat /sys/class/gpio/gpio184/value
- 释放:echo 184 > /sys/class/gpio/unexport
为什么会有这套机制?说白了,是为了让应用层工程师也能控制硬件,不用碰内核代码。你想想看,如果每次改个引脚电平都要重新编译内核,那得多麻烦。
不过要注意,sysfs接口在较新的内核里已经被标记为“过时”,推荐用libgpiod或字符设备接口。但作为学习,sysfs依然是最直观的入门方式。
避坑指南:我曾经在用户空间用shell脚本循环写GPIO,频率一高就卡死。后来发现是每次写文件都要经过VFS层,开销很大。如果要做高频操作,还是得写内核驱动或用libgpiod的bulk操作。
4.5 总结与思考
这一章我们走完了GPIO驱动的完整链路。从内核框架到寄存器映射,从驱动代码到用户空间操作。我个人觉得,理解GPIO子系统的分层思想,比记住具体寄存器地址更重要。因为换一个芯片平台,寄存器地址就变了,但框架思想是通用的。
下一章,我们会深入中断子系统。到时候你会发现,GPIO和中断是密不可分的——毕竟,光会拉电平还不够,还得能响应外部事件。