第二章:GPIO驱动开发
各位同学,今天我们来聊聊GPIO驱动。说实话,GPIO是嵌入式开发里最基础、也最常用的外设之一。我刚开始做MTK平台时,第一个调试的外设就是GPIO。那时候踩了不少坑,今天我把这些经验都分享给你们。
2.1 GPIO硬件原理
GPIO,全称General Purpose Input Output,通用输入输出口。说白了,就是芯片上那些可以编程控制的引脚。你可以让它输出高电平或低电平,也可以读取外部信号的电平状态。
在MTK芯片内部,每个GPIO引脚都连接着多个功能模块。比如一个引脚,既可以当普通GPIO用,也可以配置成UART的TX、I2C的SCL,或者PWM的输出。这就是所谓的「引脚复用」。我在项目中遇到过,有同事把本该当I2C用的引脚配成了GPIO,结果传感器死活读不到数据。嗯,这种问题排查起来很头疼。
GPIO的硬件结构其实不复杂,核心就三个部分:
- 输出驱动器:控制引脚输出高/低电平
- 输入缓冲器:读取引脚上的电平状态
- 上拉/下拉电阻:引脚悬空时保持确定电平
你想想看,如果引脚悬空,电平是不确定的。这时候就需要内部上拉或下拉电阻来固定电平。我建议你在设计电路时,尽量使用外部上拉电阻,因为内部电阻的精度和驱动能力都有限。
2.2 MTK GPIO控制器寄存器
MTK的GPIO控制器,说白了就是一堆寄存器。每个GPIO引脚对应几个关键的寄存器位。我整理了一下,常用的寄存器有这些:
| 寄存器名称 | 功能描述 | 位宽 |
|---|---|---|
| GPIO_MODE | 引脚功能选择(GPIO/复用功能) | 3-4位 |
| GPIO_DIR | 方向控制(输入/输出) | 1位 |
| GPIO_DOUT | 输出数据寄存器 | 1位 |
| GPIO_DIN | 输入数据寄存器(只读) | 1位 |
| GPIO_PULL_EN | 上拉/下拉使能 | 1位 |
| GPIO_PULL_SEL | 上拉/下拉选择 | 1位 |
这里要注意,不同MTK芯片的寄存器偏移地址可能不一样。比如MT6765和MT6785,GPIO基地址就不同。我习惯在调试时先看芯片手册的「GPIO Register Map」章节,确认基地址和偏移量。
2.3 DTS配置
在MTK平台上,GPIO的配置主要通过设备树(DTS)来完成。DTS文件里,每个GPIO引脚都有对应的节点。我给你们看一个典型的配置:
/* 在dts文件中配置GPIO */
&pio {
/* 定义GPIO引脚 */
gpio_led_pins: gpio_led {
pins_cmd_dat {
pinmux = <MTK_PINMUX(GPIO10, 0)>; /* GPIO10,功能0即GPIO模式 */
slew-rate = <1>; /* 1:输出,0:输入 */
bias-pull-up; /* 内部上拉 */
drive-strength = <4>; /* 驱动能力,单位mA */
};
};
};
/* 在设备节点中引用 */
led_device: led {
pinctrl-names = "default", "sleep";
pinctrl-0 = <&gpio_led_pins>;
pinctrl-1 = <&gpio_led_sleep>;
};
这里有几个关键点:
- pinmux:指定引脚号和功能模式。0表示纯GPIO模式
- slew-rate:控制方向。1是输出,0是输入
- bias-pull-up/down:内部上拉或下拉
- drive-strength:驱动能力。我一般设4mA,除非有特殊需求
我个人习惯把不同状态的引脚配置分开定义。比如「default」状态和「sleep」状态,这样在系统休眠时可以自动切换引脚配置,省电又安全。
2.4 GPIO操作API
在驱动代码里,我们通过内核提供的API来操作GPIO。MTK平台完全兼容标准的Linux GPIO子系统。常用的API有这些:
2.4.1 gpio_request
使用GPIO前,必须先申请。就像你住酒店要先登记一样:
int gpio_request(unsigned gpio, const char *label);
/* 示例 */
int ret;
ret = gpio_request(10, "my_led");
if (ret < 0) {
pr_err("Failed to request GPIO10: %d\n", ret);
return ret;
}
这里要注意,label参数最好起个有意义的名字。我在项目中遇到过,系统里同时有多个驱动使用GPIO,如果label都是"gpio",排查冲突时根本分不清是谁在用。
2.4.2 gpio_set_value
设置GPIO输出电平:
void gpio_set_value(unsigned gpio, int value);
/* 示例:点亮LED */
gpio_set_value(10, 1); /* 输出高电平 */
mdelay(100); /* 延时100ms */
gpio_set_value(10, 0); /* 输出低电平 */
你想想看,如果GPIO配置成了输入模式,调用gpio_set_value会怎样?嗯,不会有任何效果,但也不会报错。所以一定要先确认方向。
2.4.3 gpio_get_value
读取GPIO输入电平:
int gpio_get_value(unsigned gpio);
/* 示例:读取按键状态 */
int val = gpio_get_value(20);
if (val == 0) {
pr_info("Button pressed\n");
}
2.4.4 gpio_direction_output / gpio_direction_input
设置GPIO方向:
int gpio_direction_output(unsigned gpio, int value);
int gpio_direction_input(unsigned gpio);
/* 示例 */
gpio_direction_output(10, 0); /* 设置为输出,初始低电平 */
gpio_direction_input(20); /* 设置为输入 */
2.4.5 gpio_free
使用完后释放GPIO:
void gpio_free(unsigned gpio);
/* 示例 */
gpio_free(10);
释放GPIO这个操作,很多人会忘记。尤其是在驱动卸载时,如果不释放,下次加载驱动就会申请失败。我曾经调试过一个bug,就是驱动反复加载卸载,最后GPIO资源耗尽,系统直接panic了。
2.5 完整示例
最后,我给你们一个完整的GPIO驱动示例。这个例子控制一个LED闪烁:
#include <linux/module.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#define LED_GPIO 10
static int __init led_init(void)
{
int ret;
/* 申请GPIO */
ret = gpio_request(LED_GPIO, "led_gpio");
if (ret) {
pr_err("gpio_request failed: %d\n", ret);
return ret;
}
/* 设置为输出,初始低电平 */
gpio_direction_output(LED_GPIO, 0);
/* 闪烁3次 */
for (int i = 0; i < 3; i++) {
gpio_set_value(LED_GPIO, 1);
mdelay(500);
gpio_set_value(LED_GPIO, 0);
mdelay(500);
}
return 0;
}
static void __exit led_exit(void)
{
/* 关闭LED并释放GPIO */
gpio_set_value(LED_GPIO, 0);
gpio_free(LED_GPIO);
pr_info("LED driver removed\n");
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("MTK GPIO LED Example");
这个例子虽然简单,但包含了GPIO开发的核心流程:申请、配置方向、读写、释放。你把这个流程吃透了,其他外设驱动开发也就触类旁通了。
好了,GPIO驱动开发就讲到这里。下一章我们讲UART驱动,那个比GPIO稍微复杂一点,但原理是相通的。有什么问题,欢迎在课程群里交流。