9. GPIO子系统

1. 基础知识

什么是GPIO?

定义

  • GPIO(General-Purpose Input/ Output,通用输入输出)是一种灵活的硬件接口,允许开发者通过编程控制其工作模式(输入或输出),实现以下功能:

  • 输出模式:主动控制引脚电平(高/低),用于驱动外部设备(如点亮LED、控制继电器)。

  • 输入模式:读取外部信号的电平状态(如检测按键是否按下、传感器触发信号)。

典型应用场景

  • 控制简单外设:LED、蜂鸣器、继电器。

  • 读取外部信号:按键、开关、传感器(如红外、温湿度)。

  • 实现通信协议:作为I2C、SPI的片选(CS)信号,或自定义低速协议。

核心概念

1. 方向(Direction)

输入模式(Input)

  • 引脚作为“信号接收端”,用于检测外部设备的状态。

  • 示例:读取按键是否被按下(按键按下时引脚电平变化)。

输出模式(Output)

  • 引脚作为“信号发送端”,主动控制外部设备。

  • 示例:设置引脚电平为高,点亮LED。


2. 电平(Level)

物理电平
高电平:实际电压值(如3.3V或5V,取决于硬件设计)。
低电平:接近0V的电压(如0V)。
逻辑电平
逻辑1:对应高电平(1)。
逻辑0:对应低电平(0)。

逻辑值 物理电平(典型值) 应用场景
1 3.3V / 5V 点亮LED、使能设备
0 0V 关闭LED、禁用设备

3. 有效极性(Active Polarity)

作用:定义“有效状态”对应的逻辑电平,简化硬件设计。

  • GPIO_ACTIVE_HIGH(高有效):
    有效状态 = 逻辑1(高电平)。
    示例:LED正极接GPIO,负极接地。当GPIO输出1时,LED点亮。

  • GPIO_ACTIVE_LOW(低有效):
    有效状态 = 逻辑0(低电平)。
    示例:LED正极接电源,负极接GPIO。当GPIO输出0时,LED点亮。

关键区别

// 设备树中定义极性:  
gpios = <&gpio0 5 GPIO_ACTIVE_HIGH>; // 高有效  
gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;  // 低有效  

// 驱动代码中设置电平:  
gpiod_set_value(led, 1); // 无论极性如何,均表示“激活设备”  

• 若极性为GPIO_ACTIVE_LOWgpiod_set_value(1)会将物理电平设为0,从而点亮LED。

为什么需要有效极性?
硬件设计灵活性
某些电路需要低电平触发(如共阳极LED),通过配置极性可避免修改代码逻辑。
代码统一性
驱动代码只需关注“激活/关闭设备”的逻辑值(10),无需关心具体电平。

总结

GPIO是连接软件与硬件的桥梁,通过方向、电平、极性控制外部设备。
输入模式用于检测信号,输出模式用于驱动设备。
有效极性将逻辑值与物理电平解耦,简化开发和硬件设计。

4. 设备树配置

4.1 关闭系统默认的GPIO占用

在设备树中禁用系统自带的心跳灯(避免GPIO冲突):

// 文件:rk3568-evb.dtsi
&leds {
    work_led: work {
        status = "disabled";  // 关闭心跳灯
    };
};

4.2 添加LED设备节点

在根节点下添加自定义LED配置:

// 文件:rk3568-atk-evb1-ddr4-v10.dtsi
/ {
    gpioled {
        compatible = "alientek,led";       // 驱动匹配标识
        led-gpio = <&gpio0 RK_PC0 GPIO_ACTIVE_HIGH>;  // GPIO0_C0引脚,高电平有效
        status = "okay";
    };
};

参数解析

  • &gpio0:GPIO控制器为GPIO0组。

  • RK_PC0:引脚号为C0(对应索引号16)。

  • GPIO_ACTIVE_HIGH:高电平激活LED。


4.3 验证设备树

编译并烧写设备树后,检查节点是否生效:

cat /proc/device-tree/gpioled/status  # 应输出"okay"

5. 驱动代码实现


5.1 完整驱动代码(gpioled.c)

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>

#define GPIOLED_NAME   "gpioled"
#define LED_ON  1
#define LED_OFF 0

struct gpioled_dev {
    struct device_node *nd;     // 设备树节点
    int led_gpio;               // GPIO编号
};

static struct gpioled_dev gpioled;

// 文件操作函数:写操作控制LED
static ssize_t led_write(struct file *filp, const char __user *buf, 
                        size_t cnt, loff_t *offt) {
    u8 val;
    if (copy_from_user(&val, buf, cnt))  // 从用户空间获取值
        return -EFAULT;

    gpio_set_value(gpioled.led_gpio, val);  // 设置GPIO电平
    return cnt;
}

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .write = led_write,
};

// 驱动初始化
static int __init led_init(void) {
    int ret;

    // 1. 获取设备树节点
    gpioled.nd = of_find_node_by_path("/gpioled");
    if (!gpioled.nd) {
        printk("Failed to find gpioled node!\n");
        return -ENODEV;
    }

    // 2. 解析GPIO编号
    gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0);
    if (gpioled.led_gpio < 0) {
        printk("Invalid GPIO: %d\n", gpioled.led_gpio);
        return -EINVAL;
    }

    // 3. 申请GPIO资源
    ret = gpio_request(gpioled.led_gpio, "led-gpio");
    if (ret) {
        printk("GPIO %d request failed!\n", gpioled.led_gpio);
        return ret;
    }

    // 4. 配置为输出模式,初始低电平
    gpio_direction_output(gpioled.led_gpio, LED_OFF);

    // 5. 注册字符设备
    ret = register_chrdev(0, GPIOLED_NAME, &fops);
    if (ret < 0) {
        gpio_free(gpioled.led_gpio);
        printk("Failed to register chrdev!\n");
        return ret;
    }

    printk("LED driver loaded. GPIO: %d\n", gpioled.led_gpio);
    return 0;
}

// 驱动卸载
static void __exit led_exit(void) {
    gpio_set_value(gpioled.led_gpio, LED_OFF);  // 关闭LED
    gpio_free(gpioled.led_gpio);                // 释放GPIO
    unregister_chrdev(0, GPIOLED_NAME);         // 注销设备
    printk("LED driver unloaded.\n");
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

5.2 关键代码解析

  1. 设备树交互

    • of_find_node_by_path:通过路径查找设备树节点。

    • of_get_named_gpio:从设备树属性解析GPIO编号。

  2. GPIO操作

    • gpio_request:申请GPIO资源。

    • gpio_direction_output:设置为输出模式并初始化电平。

  3. 用户空间接口

    • led_write:接收用户指令(10)控制LED亮灭。


6. 编译与测试


6.1 编译驱动

使用Makefile编译驱动:

obj-m += gpioled.o
all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

执行编译:

make

6.2 加载驱动并测试

insmod gpioled.ko          # 加载驱动
echo 1 > /dev/gpioled      # 点亮LED
echo 0 > /dev/gpioled      # 熄灭LED
rmmod gpioled              # 卸载驱动

6.3 验证GPIO状态

gpioinfo gpiochip0         # 查看GPIO0所有引脚状态
gpioget gpiochip0 16       # 读取GPIO0_C0电平(应为0或1)

7. 常见问题排查

  1. GPIO申请失败

    • 检查设备树是否禁用系统默认占用(如心跳灯)。

    • 使用dmesg查看内核日志,确认冲突信息。

  2. 电平反向问题

    • 修改设备树的GPIO_ACTIVE_HIGH/LOW属性。

    • 硬件检查:确认LED正负极接线正确。

  3. 驱动加载失败

    • 确保设备树节点compatible值与驱动匹配。

    • 检查GPIO编号是否正确(of_get_named_gpio返回值)。


总结

通过设备树配置GPIO引脚并编写对应驱动,可实现灵活的硬件控制。关键步骤包括:

  1. 设备树配置:禁用冲突节点,定义GPIO属性。

  2. 驱动开发:解析设备树、申请GPIO、实现用户接口。

  3. 调试验证:使用gpioinfo/gpioget工具确保硬件与软件一致。

下一步可扩展功能:添加中断处理、PWM控制或结合其他外设(如按键)实现复杂交互。 可参考https://blog.guoxiaozhong.cn/archives/1706491900645#2-2-gpio%E5%AD%90%E7%B3%BB%E7%BB%9FAPI%E5%87%BD%E6%95%B0