第三章 GPIO驱动开发:从寄存器到接口的实战之路
好,咱们今天来聊聊MTK8678平台上的GPIO驱动开发。说实话,GPIO这玩意儿看起来简单,不就是拉高拉低嘛?但实际做起来,坑还真不少。我当年刚接触MTK平台时,就被GPIO的复用功能折腾得够呛。今天我把这些经验掰开揉碎了讲给你听。
3.1 GPIO控制器寄存器映射
先说说寄存器映射。MTK8678的GPIO控制器,说白了就是一堆寄存器在控制。每个GPIO口都有对应的模式寄存器、数据寄存器、上拉下拉寄存器等等。
我个人习惯先把基地址搞清楚。在MTK8678上,GPIO控制器的基地址是 0x1000B000。嗯,这个地址你得记牢,后面所有操作都离不开它。
关键寄存器偏移量
| 寄存器名称 | 偏移地址 | 功能描述 |
|---|---|---|
| GPIO_MODE | 0x000 | 模式选择(GPIO/复用功能) |
| GPIO_DOUT | 0x100 | 数据输出寄存器 |
| GPIO_DIN | 0x200 | 数据输入寄存器 |
| GPIO_PULLEN | 0x300 | 上拉/下拉使能 |
| GPIO_PULLSEL | 0x400 | 上拉/下拉选择 |
你想想看,每个GPIO口占几位?MTK8678每个寄存器管理32个GPIO,所以每个GPIO占1位。但模式寄存器不一样,它每个GPIO占3位,用来选择不同的复用功能。
/* GPIO寄存器映射结构体 */
struct mtk_gpio_regs {
volatile uint32_t mode[8]; /* 8个寄存器,每个管理4个GPIO的模式 */
volatile uint32_t dout[8]; /* 数据输出 */
volatile uint32_t din[8]; /* 数据输入 */
volatile uint32_t pullen[8]; /* 上拉使能 */
volatile uint32_t pullsel[8]; /* 上拉选择 */
};
我的经验: 映射寄存器时一定要用 volatile 关键字。我曾经在一个项目里忘了加,结果编译器优化后,读到的永远是缓存值,排查了整整两天才找到问题。
3.2 DTS配置:设备树的正确打开方式
DTS配置这块,很多新手容易搞混。MTK8678的设备树里,GPIO节点长这样:
&pio {
/* 定义GPIO别名 */
gpio_led: gpio@10 {
label = "led_green";
gpios = <&pio 10 0>;
/* 0表示输出,1表示输入 */
};
gpio_key: gpio@20 {
label = "power_key";
gpios = <&pio 20 1>;
/* 1表示输入,带内部上拉 */
input;
debounce = <10000>; /* 10ms消抖 */
};
};
这里有个坑,我必须要说。DTS里的 gpios 属性,第二个参数是GPIO号,第三个参数是标志位。标志位可不是随便写的,它决定了驱动初始化的行为。
注意: MTK8678的GPIO号不是连续的!GPIO10和GPIO20之间可能隔着其他外设的引脚。我曾经在配置I2C的GPIO时,以为GPIO号是顺序的,结果死活调不通。后来查了芯片手册才发现,中间有几个GPIO被内部模块占用了。
我个人建议,在DTS里给每个GPIO起个有意义的别名。比如 led_green、power_key,这样后续维护时一看就懂。别学某些人,全用 gpio_0、gpio_1 这种名字,三个月后自己都忘了是干嘛的。
3.3 GPIO操作接口实现
好了,到了最核心的部分——写代码。MTK平台提供了标准的GPIO操作接口,但咱们得理解底层是怎么实现的。
3.3.1 初始化接口
static int mtk_gpio_request(struct gpio_chip *chip, unsigned int offset)
{
struct mtk_gpio_bank *bank = gpiochip_get_data(chip);
unsigned long flags;
spin_lock_irqsave(&bank->lock, flags);
/* 检查GPIO是否已被占用 */
if (bank->reserved_map & BIT(offset)) {
spin_unlock_irqrestore(&bank->lock, flags);
return -EBUSY;
}
/* 标记为已占用 */
bank->reserved_map |= BIT(offset);
spin_unlock_irqrestore(&bank->lock, flags);
return 0;
}
为什么要有 spin_lock_irqsave?你想想看,GPIO操作可能在中断上下文被调用,不加锁的话,并发访问会出大问题。我在做某个项目时,就因为忘了加锁,导致两个线程同时操作同一个GPIO,结果输出波形完全乱掉了。
3.3.2 方向设置与数据读写
static int mtk_gpio_direction_output(struct gpio_chip *chip,
unsigned int offset, int value)
{
struct mtk_gpio_bank *bank = gpiochip_get_data(chip);
void __iomem *base = bank->base;
uint32_t reg;
/* 先写输出值,再设方向 —— 这个顺序很重要 */
reg = readl(base + GPIO_DOUT_OFFSET);
if (value)
reg |= BIT(offset);
else
reg &= ~BIT(offset);
writel(reg, base + GPIO_DOUT_OFFSET);
/* 设置方向为输出 */
reg = readl(base + GPIO_DIR_OFFSET);
reg |= BIT(offset);
writel(reg, base + GPIO_DIR_OFFSET);
return 0;
}
避坑指南: 我曾经犯过一个错误——先设方向再写值。结果在GPIO从输入切换到输出时,会瞬间输出一个不确定的电平。如果这个GPIO控制的是电机或者继电器,那后果...嗯,你懂的。所以一定要先写值,再设方向。
3.3.3 中断配置
GPIO中断这块,MTK8678支持多种触发方式:上升沿、下降沿、高电平、低电平。配置代码如下:
static int mtk_gpio_set_irq_type(struct irq_data *data, unsigned int type)
{
struct mtk_gpio_bank *bank = irq_data_get_irq_chip_data(data);
unsigned int gpio = data->hwirq;
void __iomem *base = bank->base;
uint32_t reg;
/* 配置触发类型 */
reg = readl(base + GPIO_INT_TYPE_OFFSET);
switch (type) {
case IRQ_TYPE_EDGE_RISING:
reg |= BIT(gpio); /* 边沿触发 */
writel(reg, base + GPIO_INT_TYPE_OFFSET);
reg = readl(base + GPIO_INT_POLARITY_OFFSET);
reg |= BIT(gpio); /* 上升沿 */
writel(reg, base + GPIO_INT_POLARITY_OFFSET);
break;
case IRQ_TYPE_EDGE_FALLING:
reg |= BIT(gpio);
writel(reg, base + GPIO_INT_TYPE_OFFSET);
reg = readl(base + GPIO_INT_POLARITY_OFFSET);
reg &= ~BIT(gpio); /* 下降沿 */
writel(reg, base + GPIO_INT_POLARITY_OFFSET);
break;
/* 其他类型类似... */
}
return 0;
}
我的建议: 调试GPIO中断时,先用示波器看看引脚上实际的波形。很多时候不是代码问题,而是硬件上信号有毛刺。我曾经遇到一个案子,按键按下时波形抖动严重,最后加了硬件消抖电路才解决。
3.4 实战中的那些坑
最后,我总结几个实际项目中容易踩的坑:
- GPIO复用冲突:同一个引脚可能被多个模块使用。比如GPIO20既是UART的TX,又是PWM的输出。配置前一定要确认当前引脚没有被其他驱动占用。
- 休眠唤醒问题:系统进入休眠后,GPIO的状态可能会丢失。我建议在
suspend回调里保存GPIO状态,在resume里恢复。 - 电平转换:MTK8678的GPIO电压是1.8V,如果外接3.3V的设备,必须加电平转换芯片。别想着直接连,会烧芯片的。
- 驱动加载顺序:如果GPIO驱动依赖其他驱动(比如时钟、电源域),一定要在DTS里配置好
depends-on属性。
嗯,GPIO驱动开发大概就这些内容。说白了,就是寄存器操作加设备树配置,但细节决定成败。下一章咱们聊聊PWM驱动,那个更有意思,涉及到定时器和波形生成。到时候我会分享一个我做呼吸灯时踩过的坑,保证让你印象深刻。