第一章:麒麟芯片GPIO驱动开发
各位同学,咱们今天聊聊GPIO驱动开发。说实话,GPIO在嵌入式开发里就像人的手脚——看似简单,但没了它啥也干不了。我刚开始接触麒麟芯片时,第一个练手的模块就是GPIO。为啥?因为它够基础,又能把整个驱动框架串起来。
1.1 GPIO控制器硬件原理
先说说GPIO控制器到底是个啥。说白了,它就是芯片上的一堆引脚,每个引脚可以独立配置成输入或输出。你想想看,一个芯片少则几十个引脚,多则上百个,总不能每个引脚都单独控制吧?所以就有了GPIO控制器这个"管家"。
麒麟芯片的GPIO控制器,内部结构其实挺清晰的。每个GPIO端口(比如GPIO0、GPIO1)都对应一组寄存器。我习惯把GPIO控制器想象成一个"开关矩阵"——每个引脚就是一个开关,你可以控制它输出高电平还是低电平,也可以读取外部信号的状态。
这里有个关键点:GPIO引脚通常不是"裸奔"的。它们内部有上拉电阻和下拉电阻。我在项目中遇到过一个问题:某个引脚在悬空时电平不稳定,导致误触发。后来查了手册才发现,默认的上拉电阻没使能。嗯,这个坑我踩过,你们要记住。
核心概念:GPIO控制器本质是一个寄存器组,通过读写这些寄存器来控制引脚的电平状态、方向(输入/输出)、上下拉配置等。
1.2 GPIO寄存器映射
寄存器映射,说白了就是给这些硬件寄存器分配内存地址。ARM架构下,外设寄存器通常映射到内存的高地址区域。麒麟芯片也不例外。
我举个例子。假设GPIO控制器的基地址是0xE010_0000,那么每个寄存器的偏移量是固定的。比如:
- 数据寄存器(DATA):偏移0x000,用来读写引脚电平
- 方向寄存器(DIR):偏移0x004,配置输入还是输出
- 上拉寄存器(PU):偏移0x008,使能内部上拉
- 下拉寄存器(PD):偏移0x00C,使能内部下拉
你可能会问:为啥要分这么多寄存器?其实是为了灵活控制。每个寄存器里的每一位对应一个引脚,比如DIR寄存器的bit0控制GPIO0_0的方向,bit1控制GPIO0_1的方向,以此类推。
我记得第一次写GPIO驱动时,直接用了物理地址去操作寄存器。结果发现不同芯片版本,基地址居然不一样!从那以后,我养成了一个习惯:永远从设备树获取寄存器基地址,而不是硬编码。
个人经验:写驱动时,建议用ioremap()把物理地址映射到虚拟地址空间。这样既安全又方便调试。我曾经因为忘记取消映射,导致系统崩溃,后来加了iounmap()才解决。
1.3 基于platform驱动的GPIO驱动实现
好了,理论说完了,咱们上代码。麒麟芯片的GPIO驱动,我推荐用platform驱动框架。为啥?因为它和设备树天然配合,而且Linux内核已经帮你处理了很多繁琐的事情。
一个典型的platform驱动,需要实现两个核心函数:probe()和remove()。probe函数在设备匹配时被调用,remove在设备移除时被调用。
static int my_gpio_probe(struct platform_device *pdev)
{
struct resource *res;
void __iomem *base;
// 从设备树获取寄存器基地址
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base))
return PTR_ERR(base);
// 初始化GPIO控制器
writel(0xFFFFFFFF, base + GPIO_DIR_OFFSET); // 全部设为输出
writel(0x00000000, base + GPIO_DATA_OFFSET); // 全部输出低电平
dev_info(&pdev->dev, "GPIO driver initialized\n");
return 0;
}
static int my_gpio_remove(struct platform_device *pdev)
{
dev_info(&pdev->dev, "GPIO driver removed\n");
return 0;
}
static const struct of_device_id my_gpio_of_match[] = {
{ .compatible = "hisilicon,kirin-gpio" },
{ }
};
MODULE_DEVICE_TABLE(of, my_gpio_of_match);
static struct platform_driver my_gpio_driver = {
.probe = my_gpio_probe,
.remove = my_gpio_remove,
.driver = {
.name = "kirin_gpio",
.of_match_table = my_gpio_of_match,
},
};
module_platform_driver(my_gpio_driver);
这段代码看着简单,但有几个坑要注意。我曾经在probe函数里忘了检查platform_get_resource的返回值,结果内核直接panic了。还有,devm_ioremap_resource会自动处理内存释放,比手动ioremap安全得多。
避坑指南:千万不要在probe函数里做耗时操作!比如睡眠、等待中断等。我曾经因为初始化时读了一个慢速外设,导致系统启动延迟了好几秒。后来改成用工作队列才解决。
1.4 用户空间GPIO控制
驱动写好了,怎么让应用程序用呢?两种主流方式:sysfs和libgpiod。
1.4.1 sysfs方式(传统)
sysfs是Linux内核暴露给用户空间的接口。操作GPIO就像操作文件一样简单:
# 导出GPIO引脚
echo 100 > /sys/class/gpio/export
# 设置方向
echo out > /sys/class/gpio/gpio100/direction
# 输出高电平
echo 1 > /sys/class/gpio/gpio100/value
# 读取输入
cat /sys/class/gpio/gpio100/value
# 取消导出
echo 100 > /sys/class/gpio/unexport
这种方式简单粗暴,但有个问题:每个操作都要读写文件,效率低。我做过测试,用sysfs控制GPIO翻转,频率最多到几百kHz。对于高速应用,这显然不够。
1.4.2 libgpiod方式(推荐)
libgpiod是新一代的GPIO控制库,通过字符设备接口操作,效率高得多。我个人强烈推荐这种方式。
# 安装libgpiod工具
sudo apt-get install gpiod libgpiod-dev
# 查看GPIO芯片信息
gpiodetect
# 设置GPIO输出高电平
gpioset gpiochip0 10=1
# 读取GPIO输入
gpioget gpiochip0 10
# 监控GPIO变化
gpiomon gpiochip0 10
用C代码调用libgpiod也很方便:
#include <gpiod.h>
struct gpiod_chip *chip;
struct gpiod_line *line;
int ret;
// 打开GPIO芯片
chip = gpiod_chip_open_by_name("gpiochip0");
if (!chip) {
perror("Open chip failed");
return -1;
}
// 获取GPIO引脚
line = gpiod_chip_get_line(chip, 10);
if (!line) {
perror("Get line failed");
gpiod_chip_close(chip);
return -1;
}
// 配置为输出,初始值高电平
ret = gpiod_line_request_output(line, "myapp", 1);
if (ret < 0) {
perror("Request output failed");
gpiod_chip_close(chip);
return -1;
}
// 输出低电平
gpiod_line_set_value(line, 0);
// 清理
gpiod_line_release(line);
gpiod_chip_close(chip);
我的建议:新项目一律用libgpiod。sysfs虽然简单,但已经被内核标记为"过时"了。而且libgpiod支持事件监听、批量操作,性能好得多。我在一个项目中用libgpiod实现了GPIO中断响应,延迟只有几十微秒。
总结
这一章咱们从硬件原理讲到驱动实现,再到用户空间控制。GPIO看似简单,但每个环节都有值得深挖的地方。记住三点:
- 硬件层面:理解寄存器映射和引脚复用
- 驱动层面:用好platform框架和设备树
- 应用层面:优先选择libgpiod
下一章,咱们会深入GPIO中断处理,讲讲怎么用中断方式响应外部事件。到时候我会分享一个实际项目中的调试经历,保证让你少走弯路。
好了,今天就到这里。有问题随时交流。