4. GPIO驱动开发与LED控制

任务目标

在TI Jacinto平台(以J721E为例)上实现一个基础GPIO字符设备驱动,将特定引脚配置为输出模式并连接板载LED,通过应用层控制LED以1Hz频率闪烁,以此验证GPIO寄存器的读写操作与驱动框架的完整工作链路。

硬件连接与引脚定义

本案例以J721E EVM板载用户LED(通常连接至WKUP_GPIO0_12,对应控制器的第12号引脚)为目标对象。若实际板卡引脚不同,需根据原理图修改设备树中的gpios属性。驱动通过memory-mapped I/O直接操作GPIO控制器寄存器,不依赖现有GPIO子系统,以展示底层寄存器读写过程。

核心实现

驱动实现分为三部分:寄存器定义与操作函数、文件操作接口、平台驱动注册。LED闪烁通过定时器在驱动内部完成,应用层只需打开设备节点即可触发,无需反复写入。关键点在于GPIO输出方向寄存器的设置与数据寄存器的位操作。

// gpio_led_drv.c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/timer.h>
#include <linux/slab.h>
#include <linux/miscdevice.h>

#define GPIO_DIR_REG      0x0
#define GPIO_DATA_REG     0x8
#define GPIO_SETDATA_REG  0x10
#define GPIO_CLEARDATA_REG 0x18

struct gpio_led_dev {
    void __iomem *base;
    int gpio_num;
    struct timer_list timer;
    int led_state;
};

static struct gpio_led_dev *led_dev;

static void led_timer_cb(struct timer_list *t) {
    struct gpio_led_dev *dev = from_timer(dev, t, timer);
    u32 val;

    dev->led_state = !dev->led_state;
    if (dev->led_state)
        writel(BIT(dev->gpio_num), dev->base + GPIO_SETDATA_REG);
    else
        writel(BIT(dev->gpio_num), dev->base + GPIO_CLEARDATA_REG);

    mod_timer(&dev->timer, jiffies + msecs_to_jiffies(500));
}

static ssize_t led_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) {
    char state = led_dev->led_state ? '1' : '0';
    if (copy_to_user(buf, &state, 1))
        return -EFAULT;
    return 1;
}

static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) {
    char kbuf[4];
    int on;

    if (count > 3) count = 3;
    if (copy_from_user(kbuf, buf, count))
        return -EFAULT;
    kbuf[count] = '\0';

    if (kstrtoint(kbuf, 10, &on) == 0) {
        if (on == 1) {
            led_dev->led_state = 1;
            writel(BIT(led_dev->gpio_num), led_dev->base + GPIO_SETDATA_REG);
        } else if (on == 0) {
            led_dev->led_state = 0;
            writel(BIT(led_dev->gpio_num), led_dev->base + GPIO_CLEARDATA_REG);
        }
    }
    return count;
}

static const struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .read = led_read,
    .write = led_write,
};

static struct miscdevice led_miscdev = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "gpio_led",
    .fops = &led_fops,
};

static int gpio_led_probe(struct platform_device *pdev) {
    struct device *dev = &pdev->dev;
    struct resource *res;
    int ret;

    led_dev = devm_kzalloc(dev, sizeof(*led_dev), GFP_KERNEL);
    if (!led_dev)
        return -ENOMEM;

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    led_dev->base = devm_ioremap_resource(dev, res);
    if (IS_ERR(led_dev->base))
        return PTR_ERR(led_dev->base);

    ret = of_property_read_u32(dev->of_node, "gpio-offset", &led_dev->gpio_num);
    if (ret < 0) {
        dev_err(dev, "missing gpio-offset property\n");
        return -EINVAL;
    }

    // 配置为输出模式:方向寄存器对应位写1
    writel(BIT(led_dev->gpio_num), led_dev->base + GPIO_DIR_REG);

    // 初始关闭LED
    writel(BIT(led_dev->gpio_num), led_dev->base + GPIO_CLEARDATA_REG);
    led_dev->led_state = 0;

    timer_setup(&led_dev->timer, led_timer_cb, 0);
    mod_timer(&led_dev->timer, jiffies + msecs_to_jiffies(500));

    ret = misc_register(&led_miscdev);
    if (ret) {
        dev_err(dev, "misc_register failed\n");
        return ret;
    }

    dev_info(dev, "GPIO LED driver probed, gpio=%d\n", led_dev->gpio_num);
    return 0;
}

static int gpio_led_remove(struct platform_device *pdev) {
    del_timer_sync(&led_dev->timer);
    misc_deregister(&led_miscdev);
    return 0;
}

static const struct of_device_id gpio_led_of_match[] = {
    { .compatible = "ti,j721e-gpio-led-demo" },
    { }
};
MODULE_DEVICE_TABLE(of, gpio_led_of_match);

static struct platform_driver gpio_led_driver = {
    .probe = gpio_led_probe,
    .remove = gpio_led_remove,
    .driver = {
        .name = "gpio_led_demo",
        .of_match_table = gpio_led_of_match,
    },
};
module_platform_driver(gpio_led_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("TI Jacinto GPIO LED control demo");

设备树节点配置

在J721E EVM的设备树文件中,需在根节点下添加以下节点,指定GPIO控制器基地址与引脚偏移。地址值0x02940000对应WKUP_GPIO0控制器,具体需查阅TRM确认。

/ {
    gpio_led_demo {
        compatible = "ti,j721e-gpio-led-demo";
        reg = <0x00 0x02940000 0x00 0x100>;  // WKUP_GPIO0 寄存器基地址
        gpio-offset = <12>;                  // 对应WKUP_GPIO0_12
    };
};

编译与加载

  1. 将驱动源码放入内核drivers/misc/目录,并在Kconfig/Makefile中添加对应条目,或使用外部模块编译方式:
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- M=$PWD modules
  1. 将生成的gpio_led_drv.ko拷贝至目标板,加载并验证:
insmod gpio_led_drv.ko
ls /dev/gpio_led

验证方法

加载驱动后观察板载LED是否以1Hz频率闪烁(亮500ms灭500ms)。若LED未闪烁,执行以下检查:

  • dmesg | tail查看驱动probe是否成功,确认打印的gpio编号与硬件一致;
  • 使用示波器或万用表测量对应引脚电平是否在0V与3.3V间切换;
  • 向设备节点写入控制状态:echo 0 > /dev/gpio_led应使LED常灭,echo 1 > /dev/gpio_led应使LED常亮,读取cat /dev/gpio_led返回当前状态字符。

通过标准:LED按预期频率闪烁,且echo/cat操作能正确控制与读取状态,说明GPIO寄存器读写与驱动框架工作正常。