嵌入式Linux驱动开发 - GPIO LED驱动

发布于:2025-08-30 ⋅ 阅读:(24) ⋅ 点赞:(0)

嵌入式Linux驱动开发 - GPIO LED驱动

一、项目概述

本项目实现了基于GPIO子系统的LED驱动程序,展示了一种更现代、更灵活的GPIO驱动开发方法。相比直接操作寄存器的方式,本项目使用Linux内核提供的GPIO子系统来控制LED,这种方式更加简洁、安全且易于维护。

二、开发环境

  • 开发板:i.MX6ULL阿尔法开发板
  • 内核版本:Linux 4.1.15
  • 开发工具链:交叉编译工具链
  • 硬件平台:NXP i.MX6ULL处理器

三、代码结构

gpioled/
├── gpioled.c          // 内核模块驱动代码
├── gpioledAPP.c       // 用户空间测试程序
├── Makefile          // 编译规则
└── imx6ull-alientek-emmc.dts  // 设备树文件

四、核心组件详解

1. Makefile分析

KERNERDIR := /home/ubuntu2004/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENTDIR := $(shell pwd)

obj-m := gpioled.o
build : kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) modules

clean:
	$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) clean
  • KERNERDIR:内核源码路径
  • CURRENTDIR:当前工作目录
  • obj-m:声明编译成内核模块
  • kernel_modules:编译内核模块的目标规则
  • clean:清理编译生成的文件

2. 设备树配置

&iomuxc {
    ...
    pinctrl_gpioled: ledgrp {
        fsl,pins = <
            MX6UL_PAD_GPIO1_IO03__GPIO1_IO03	0x10b0
        >;
    };
};

gpioled{
    compatible = "alientek,gpioled";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_gpioled>;
    states = "okay";
    /* led-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>; */
};
设备树关键配置:
  • pinctrl_gpioled:定义LED使用的GPIO引脚配置
  • gpioled节点
    • compatible:匹配驱动的兼容字符串
    • pinctrl-namespinctrl-0:指定引脚控制配置
    • states:设备状态
    • led-gpios:指定GPIO引脚和激活电平(注释状态)

3. 内核模块代码分析 (gpioled.c)

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>

#define GPIOLED_CNT 1
#define GPIOLED_NAME "gpioled"
#define LEDON 1
#define LEDOFF 0

struct gpioled_dev
{
    dev_t devid;
    int major;
    int minor;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    struct device_node *nd;
    int led_gpio;
};
struct gpioled_dev gpioled;

static int gpioled_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &gpioled;
    return 0;
}

static int gpioled_release(struct inode *inode, struct file *filp)
{
    return 0;
}

static ssize_t gpioled_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    struct gpioled_dev *dev = filp->private_data;
    unsigned char data[1];
    if (copy_from_user(data, buf, cnt))
        return -EFAULT;
    if (data[0] == LEDON)
        gpio_set_value(dev->led_gpio, 0);
    else if (data[0] == LEDOFF)
        gpio_set_value(dev->led_gpio, 1);
    return 0;
}

static const struct file_operations gpioled_fops = {
    .owner = THIS_MODULE,
    .open = gpioled_open,
    .release = gpioled_release,
    .write = gpioled_write,
};

static int __init gpioled_init(void)
{
    u8 ret = 0;
    gpioled.major = 0;
    if (gpioled.major)
    {
        gpioled.devid = MKDEV(gpioled.major, 0);
        ret = register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);
    }
    else
    {
        ret = alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME);
        gpioled.major = MAJOR(gpioled.devid);
        gpioled.minor = MINOR(gpioled.devid);
    }
    if (ret < 0)
    {
        goto fail_devid;
    }

    gpioled.cdev.owner = THIS_MODULE;
    cdev_init(&gpioled.cdev, &gpioled_fops);
    ret = cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT);
    if (ret < 0)
    {
        goto fail_cedv_add;
    }

    gpioled.class = class_create(gpioled.cdev.owner, GPIOLED_NAME);
    if (IS_ERR(gpioled.class))
    {
        ret = PTR_RET(gpioled.class);
        goto fail_class;
    }

    gpioled.device = device_create(gpioled.class, NULL, gpioled.devid, NULL, GPIOLED_NAME);
    if (IS_ERR(gpioled.device))
    {
        ret = PTR_RET(gpioled.device);
        goto fail_device;
    }
    gpioled.nd = of_find_node_by_path("/gpioled");
    if (!gpioled.nd)
    {
        ret = -EINVAL;
        goto fail_nd;
    }
    gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpios", 0);
    if (gpioled.led_gpio < 0)
    {
        ret = -EINVAL;
        goto fail_gpio;
    }
    ret = gpio_request(gpioled.led_gpio, "label");
    if (ret)
    {
        ret = -EINVAL;
        goto fail_gpio_req;
    }
    ret = gpio_direction_output(gpioled.led_gpio, 1);
    if (ret)
    {
        ret = -EINVAL;
        goto fail_direction_output;
    }
    gpio_set_value(gpioled.led_gpio, 0);
    return 0;
fail_direction_output:
    gpio_free(gpioled.led_gpio);
fail_gpio_req:
    printk("err gpio_request\r\n");
fail_gpio:
    printk("err get named gpio\r\n");
fail_nd:
    device_destroy(gpioled.class, gpioled.devid);
fail_device:
    class_destroy(gpioled.class);
fail_class:
    cdev_del(&gpioled.cdev);
fail_cedv_add:
    unregister_chrdev(gpioled.major, GPIOLED_NAME);
fail_devid:
    return ret;
}

static void __exit gpioled_exit(void)
{
    gpio_set_value(gpioled.led_gpio, 1);

    gpio_free(gpioled.led_gpio);
    device_destroy(gpioled.class, gpioled.devid);
    class_destroy(gpioled.class);
    cdev_del(&gpioled.cdev);
    unregister_chrdev(gpioled.major, GPIOLED_NAME);
}

module_init(gpioled_init);
module_exit(gpioled_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("alientek");
模块初始化流程:
  1. 字符设备注册

    • 动态分配主设备号
    • 初始化并添加字符设备
    • 创建设备类和设备文件
  2. 设备树解析

    • 查找设备树节点/gpioled
    • 获取GPIO描述符:of_get_named_gpio
  3. GPIO初始化

    • 请求GPIO:gpio_request
    • 设置为输出模式:gpio_direction_output
    • 默认关闭LED:gpio_set_value
  4. 文件操作接口

    • open:简单的文件打开处理
    • release:资源释放
    • write:接收用户空间的LED控制命令并调用gpio_set_value
使用的GPIO子系统API:
  • of_get_named_gpio():从设备树中获取GPIO编号
  • gpio_request():申请GPIO
  • gpio_direction_output():设置GPIO为输出模式
  • gpio_set_value():设置GPIO值
  • gpio_free():释放GPIO

4. 用户空间测试程序 (gpioledAPP.c)

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
    if (argc != 3)  // Expecting the program name and one argument
    {
        fprintf(stderr, "Usage: %s <led_device> <0|1>\n", argv[0]);
        return -1;
    }

    char* fileanme;
    unsigned char databuf[1];
    fileanme = argv[1];
    databuf[0] = atoi(argv[2]);

    int fd = 0;
    int ret = 0;

    fd = open(fileanme, O_RDWR);
    if (fd < 0)
    {
        perror("open led device error");
        return -1;
    }
    ret = write(fd, databuf, 1);
    if (ret < 0)
    {
        perror("write led device error");
        close(fd);
        return -1;
    }

    close(fd);
    return 0;
}
使用说明:
# 编译
arm-linux-gnueabi-gcc -o gpioledAPP gpioledAPP.c

# 运行示例 - 打开LED
./gpioledAPP /dev/gpioled 1

# 运行示例 - 关闭LED
./gpioledAPP /dev/gpioled 0

五、驱动工作原理

1. 设备树机制

  • 使用设备树传递硬件信息,避免硬编码GPIO地址
  • 通过of_find_node_by_path获取设备树节点
  • 使用of_get_named_gpio获取GPIO编号
  • 支持设备树热插拔

2. 字符设备驱动框架

  • 分配和注册设备号
  • 初始化字符设备结构体
  • 创建设备类和设备文件
  • 实现文件操作接口

3. GPIO子系统控制流程

  1. 从设备树获取GPIO编号
  2. 申请并配置GPIO
  3. 设置GPIO为输出模式
  4. 通过gpio_set_value控制LED状态

4. 用户空间通信

  • 通过write系统调用传递LED状态
  • 内核空间接收数据后调用gpio_set_value
  • 利用标准GPIO子系统API实现安全的GPIO操作

六、与传统寄存器操作对比

功能 传统寄存器操作 GPIO子系统方法
寄存器映射 需要手动映射GPIO相关寄存器 不需要直接操作寄存器
引脚复用配置 需要在驱动中配置 通过设备树配置,驱动中自动获取
电平控制 读取DR寄存器 -> 修改 -> 写入DR 直接调用gpio_set_value
安全性 无内核保护 内核提供安全检查
可移植性 与硬件强相关 更加抽象,便于移植
可维护性 修改配置需要修改驱动 修改配置只需修改设备树
驱动代码复杂度 更复杂 更简洁、易维护

七、编译与测试流程

1. 编译驱动

make -C /path/to/kernel/source M=$(PWD) modules

2. 加载驱动

insmod gpioled.ko

3. 测试LED

# 打开LED
./gpioledAPP /dev/gpioled 1

# 关闭LED
./gpioledAPP /dev/gpioled 0

八、调试技巧

1. 内核日志查看

dmesg

2. 设备节点检查

ls -l /dev/gpioled

3. 设备树验证

  • 检查/gpioled节点是否存在
  • 验证led-gpios属性是否正确
  • 确认pinctrl配置是否匹配

4. 错误处理

  • 检查模块加载日志
  • 验证设备树配置
  • 查看GPIO引脚配置
  • 查看文件权限设置

九、扩展与优化

1. 支持多个LED

  • 修改设备树配置多个LED节点
  • 修改驱动支持多个GPIO控制

2. 支持亮度调节

  • 添加PWM控制功能
  • 在设备树中配置PWM相关属性

3. 添加sysfs接口

  • 创建sysfs节点提供更友好的用户接口
  • 通过文件操作实现LED控制

4. 支持异步通知

  • 实现fasync机制
  • 支持信号驱动的异步IO

5. 添加设备树动态绑定

  • 实现of_device的probe和remove函数
  • 支持设备树动态更新

十、常见问题与解决

1. 模块加载失败

  • 检查内核版本匹配
  • 验证交叉编译工具链
  • 检查内核配置是否支持模块

2. 设备节点未创建

  • 检查class和device创建是否成功
  • 查看dmesg日志中的错误信息

3. LED不响应

  • 验证设备树配置是否正确
  • 检查GPIO引脚是否被其他功能占用
  • 使用gpioinfo工具检查GPIO状态

4. 权限问题

  • 使用chmod修改设备节点权限
  • 或者使用root权限运行测试程序

5. GPIO请求失败

  • 检查GPIO是否被其他驱动占用
  • 验证设备树中GPIO配置是否正确
  • 确认GPIO编号是否有效

十一、总结

本项目完整实现了基于GPIO子系统的LED驱动程序,展示了现代Linux设备驱动开发的最佳实践:

  • 使用设备树传递硬件信息
  • 基于字符设备框架的驱动开发
  • 使用GPIO子系统实现安全的GPIO操作
  • 用户空间与内核空间通信
  • 模块化开发与调试技巧

相比传统的寄存器操作方式,使用GPIO子系统具有以下优势:

  • 更高的抽象层次,简化开发
  • 更好的可移植性
  • 更安全的GPIO操作
  • 更清晰的代码结构
  • 更容易的维护和扩展

十二、参考资料

  • Linux内核文档:https://www.kernel.org/doc/
  • NXP i.MX6ULL参考手册
  • Linux设备驱动程序开发指南
  • 项目源码仓库:https://gitee.com/dream-cometrue/linux_driver_imx6ull