嵌入式Linux驱动开发 - DTS LED驱动
一、项目概述
本项目实现了基于设备树(Device Tree)的LED驱动程序,包含内核模块和用户空间测试程序。驱动程序通过设备树获取硬件信息,实现了LED的开关控制功能。
二、开发环境
- 开发板:i.MX6ULL阿尔法开发板
 - 内核版本:Linux 4.1.15
 - 开发工具链:交叉编译工具链
 - 硬件平台:NXP i.MX6ULL处理器
 
三、代码结构
dtsled/
├── dtsled.c          // 内核模块驱动代码
├── dtsledAPP.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 := dtsled.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
        >;
    };
};
dtsled{
    compatible = "gpio-leds";
    led0{
        label = "red";
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_gpioled>;
        gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
        default-state = "on";
        linux,default-trigger = "heartbeat";
    };
};
 
设备树关键配置:
- pinctrl_gpioled:定义LED使用的GPIO引脚配置
 - dtsled节点: 
  
compatible:匹配驱动的兼容字符串label:LED标签gpios:指定GPIO引脚和激活电平default-state:默认状态linux,default-trigger:默认触发器
 
3. 内核模块代码分析 (dtsled.c)
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/io.h>
#include <linux/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>
#define DRIVER_NAME "dtsled"
#define DTSLED_CNT 1
#define LEDON 1
#define LEDOFF 0
/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
void led_switch(u8 sta)
{
    u32 val = 0;
    if (sta == LEDON)
    {
        val = readl(GPIO1_DR);
        val &= ~(1 << 3);
        writel(val, GPIO1_DR);
    }
    else if (sta == LEDOFF)
    {
        val = readl(GPIO1_DR);
        val |= (1 << 3);
        writel(val, GPIO1_DR);
    }
}
struct dtsled_dev
{
    dev_t devid;
    int major;
    int minor;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    struct device_node *nd;
};
static struct dtsled_dev dtsled;
static int dtsled_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &dtsled;
    return 0;
}
static int dtsled_release(struct inode *inode, struct file *filp)
{
    struct dtsled_dev *dev = (struct dtsled_dev *)filp->private_data;
    return 0;
}
static ssize_t dtsled_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    struct dtsled_dev *dev = (struct dtsled_dev *)filp->private_data;
    int retvalue;
    unsigned char databuf[1];
    unsigned char ledstat;
    retvalue = copy_from_user(databuf, buf, cnt);
    if (retvalue < 0)
    {
        printk("kernel write failed!\r\n");
        return -EFAULT;
    }
    led_switch(buf[0]);
    return 0;
}
struct file_operations dtsled_fops =
    {
        .owner = THIS_MODULE,
        .open = dtsled_open,
        .release = dtsled_release,
        .write = dtsled_write,
};
static int __init dtsled_init(void)
{
    int ret = 0;
    const char *str;
    u32 regdata[10], val;
    u8 i = 0;
    dtsled.major = 0; // Dynamic major number
    if (dtsled.major)
    {
        dtsled.devid = MKDEV(dtsled.major, 0);
        ret = register_chrdev_region(dtsled.devid, DTSLED_CNT, DRIVER_NAME);
    }
    else
    {
        ret = alloc_chrdev_region(&dtsled.devid, 0, DTSLED_CNT, DRIVER_NAME);
        dtsled.major = MAJOR(dtsled.devid);
        dtsled.minor = MINOR(dtsled.devid);
    }
    if (ret < 0)
    {
        goto fail_devid;
    }
    printk("major: %d, minor: %d\r\n", dtsled.major, dtsled.minor);
    dtsled.cdev.owner = THIS_MODULE;
    cdev_init(&dtsled.cdev, &dtsled_fops);
    ret = cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_CNT);
    if (ret < 0)
    {
        goto fail_cdev;
    }
    dtsled.class = class_create(dtsled.cdev.owner, DRIVER_NAME);
    if (IS_ERR(dtsled.class))
    {
        ret = PTR_RET(dtsled.class);
        goto fail_create_class;
    }
    dtsled.device = device_create(dtsled.class, NULL, dtsled.devid, NULL, DRIVER_NAME);
    if (IS_ERR(dtsled.device))
    {
        ret = PTR_RET(dtsled.device);
        goto faid_device_create;
    }
    dtsled.nd = of_find_node_by_path("/alphaled");
    if (dtsled.nd == NULL)
    {
        ret = -EIO;
        goto fail_findnd;
    }
    ret = of_property_read_string(dtsled.nd, "status", &str);
    if (ret < 0)
    {
        goto fail_rs;
    }
    else
    {
        printk("status=%s", str);
    }
    ret = of_property_read_string(dtsled.nd, "compatible", &str);
    if (ret < 0)
    {
        goto fail_rs;
    }
    else
    {
        printk("compatible=%s", str);
    }
    ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 10);
    if (ret < 0)
    {
        goto fail_rs;
    }
    else
    {
        printk("\r\n");
        for (i = 0; i < 10; i++)
        {
            printk("%#x ", regdata[i]);
        }
        printk("\r\n");
    }
    IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0);
    SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1);
    SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2);
    GPIO1_DR = of_iomap(dtsled.nd, 3);
    GPIO1_GDIR = of_iomap(dtsled.nd, 4);
    /* 2、使能GPIO1时钟 */
    val = readl(IMX6U_CCM_CCGR1);
    val &= ~(3 << 26); /* 清楚以前的设置 */
    val |= (3 << 26);  /* 设置新值 */
    writel(val, IMX6U_CCM_CCGR1);
    /* 3、设置GPIO1_IO03的复用功能,将其复用为
     *    GPIO1_IO03,最后设置IO属性。
     */
    writel(5, SW_MUX_GPIO1_IO03);
    writel(0x10B0, SW_PAD_GPIO1_IO03);
    /* 4、设置GPIO1_IO03为输出功能 */
    val = readl(GPIO1_GDIR);
    val &= ~(1 << 3); /* 清除以前的设置 */
    val |= (1 << 3);  /* 设置为输出 */
    writel(val, GPIO1_GDIR);
    /* 5、默认关闭LED */
    val = readl(GPIO1_DR);
    val |= (1 << 3);
    writel(val, GPIO1_DR);
    return 0;
fail_rs:
fail_findnd:
    device_destroy(dtsled.class, dtsled.devid);
faid_device_create:
    class_destroy(dtsled.class);
fail_create_class:
    cdev_del(&dtsled.cdev);
fail_cdev:
    unregister_chrdev_region(dtsled.devid, DTSLED_CNT);
fail_devid:
    return ret;
}
static void __exit dtsled_exit(void)
{
    u32 val = readl(GPIO1_DR);
    val |= (1 << 3);
    writel(val, GPIO1_DR);
    /* 取消映射 */
    iounmap(IMX6U_CCM_CCGR1);
    iounmap(SW_MUX_GPIO1_IO03);
    iounmap(SW_PAD_GPIO1_IO03);
    iounmap(GPIO1_DR);
    iounmap(GPIO1_GDIR);
    device_destroy(dtsled.class, dtsled.devid);
    class_destroy(dtsled.class);
    cdev_del(&dtsled.cdev);
    unregister_chrdev_region(dtsled.devid, DTSLED_CNT);
}
module_init(dtsled_init);
module_exit(dtsled_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Alientek");
 
模块初始化流程:
-  
字符设备注册
- 动态分配主设备号
 - 初始化并添加字符设备
 - 创建设备类和设备文件
 
 -  
设备树解析
- 查找设备树节点
/alphaled - 读取属性:
status,compatible,reg - 获取寄存器地址并进行IO映射
 
 - 查找设备树节点
 -  
GPIO初始化
- 使能GPIO1时钟
 - 配置GPIO1_IO03为GPIO功能
 - 设置GPIO1_IO03为输出模式
 - 默认关闭LED
 
 -  
文件操作接口
open:简单的文件打开处理release:资源释放write:接收用户空间的LED控制命令
 
4. 用户空间测试程序 (dtsledAPP.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 dtsledAPP dtsledAPP.c
# 运行示例 - 打开LED
./dtsledAPP /dev/dtsled 1
# 运行示例 - 关闭LED
./dtsledAPP /dev/dtsled 0
 
五、驱动工作原理
1. 设备树机制
- 使用设备树传递硬件信息,避免硬编码GPIO地址
 - 通过
of_find_node_by_path获取设备树节点 - 使用
of_property_read_*系列函数读取节点属性 - 通过
of_iomap进行寄存器地址映射 
2. 字符设备驱动框架
- 分配和注册设备号
 - 初始化字符设备结构体
 - 创建设备类和设备文件
 - 实现文件操作接口
 
3. GPIO控制流程
- 使能GPIO时钟
 - 配置引脚复用功能
 - 设置引脚电气属性
 - 配置为输出模式
 - 通过写DR寄存器控制LED状态
 
4. 用户空间通信
- 通过
write系统调用传递LED状态 - 内核空间接收数据后调用
led_switch函数 - 直接操作GPIO寄存器实现LED控制
 
六、编译与测试流程
1. 编译驱动
make -C /path/to/kernel/source M=$(PWD) modules
 
2. 加载驱动
insmod dtsled.ko
 
3. 测试LED
# 打开LED
./dtsledAPP /dev/dtsled 1
# 关闭LED
./dtsledAPP /dev/dtsled 0
 
七、调试技巧
1. 内核日志查看
dmesg
 
2. 设备节点检查
ls -l /dev/dtsled
 
3. 寄存器读写验证
- 使用
devmem2工具直接读写寄存器验证配置 
4. 错误处理
- 检查模块加载日志
 - 验证设备树配置
 - 检查GPIO引脚配置
 - 查看文件权限设置
 
八、扩展与优化
1. 支持多个LED
- 修改设备树配置多个LED子节点
 - 修改驱动支持多个GPIO控制
 
2. 支持亮度调节
- 添加PWM控制功能
 - 在设备树中配置PWM相关属性
 
3. 添加sysfs接口
- 创建sysfs节点提供更友好的用户接口
 
4. 支持设备树热插拔
- 实现
of_device的probe和remove函数 - 支持设备树动态更新
 
九、常见问题与解决
1. 模块加载失败
- 检查内核版本匹配
 - 验证交叉编译工具链
 - 检查内核配置是否支持模块
 
2. 设备节点未创建
- 检查class和device创建是否成功
 - 查看dmesg日志中的错误信息
 
3. LED不响应
- 验证设备树配置是否正确
 - 检查GPIO引脚是否被其他功能占用
 - 使用devmem2直接操作寄存器测试
 
4. 权限问题
- 使用chmod修改设备节点权限
 - 或者使用root权限运行测试程序
 
十、总结
本项目完整实现了基于设备树的LED驱动程序,展示了嵌入式Linux驱动开发的核心技术:
- 设备树的使用与配置
 - 字符设备驱动框架
 - GPIO操作与寄存器访问
 - 用户空间与内核空间通信
 - 模块化开发与调试技巧
 
                  
                  
                  
                  
                            
                    
      
          
                
                
                
                
              
                
                
                
                
                
              
                
                
              
            
                  
					352
					
被折叠的  条评论
		 为什么被折叠?
		 
		 
		
    
  
    
  
            


            