zl程序教程

您现在的位置是:首页 >  其他

当前栏目

linux驱动之LED驱动

2023-02-18 16:30:33 时间

通过之前的学习,了解到linux驱动编写的流程是:先通过注册函数注册我们编写的入口函数,然后在入口函数中获取设备号->注册字符设备->自动创建设备节点->获取设备树信息,最后通过销毁函数将出口函数中需要释放的资源进行释放,想知道具实现的小伙伴可以查看我之前的文章。完成之前的学习,这篇文章所涉及的知识就比较简单了,现在我们开始led驱动的学习。

一、准备材料

开发环境:VMware
操作系统:ubuntu
开发版:湃兔i2S-6UB
库文件:linux开发板或ubuntu的内核源码

二、GPIO原理图

我用的是i2C-6ULX-B开发版,想要了解更多开发版的信息可以查看i2C-6ULX-B开发套件,外观如下图所示:

通过湃兔官方提供的原理图,可以知道开发板上的两个LED,有一个是电源指示灯通电就亮,所以我们能使用的只有一个,具体如下图所示:

从原理图中可知led是低电平亮,高电平熄,然后接着查看湃兔核心板的引脚图,如下图所示:

最后在查看湃兔官方提供的引脚定义,具体如下图所以:

现在不用我多说小伙伴们都知道i2C-6ULX-B开发版上的led灯接的是芯片的gpio5.IO[5],在Linux中的GPIO计算方法是,GPIO_num = (<imx6ul_gpio_port> - 1) * 32 + <imx6ul_gpio_pin>,所以湃兔i2C-6ULX-B开发板的led接的是133引脚。

三、GPIO配置

了解led的硬件原理后,需要在设备树中进行配置,有需要的小伙伴可以了解湃兔官方的GPIO配置教程,好吧说得比较简单,没有学习过设备树的小伙伴可能看不懂,需要的可以百度搜索下相关教程。在配置之前我们还需要了解一下什么是GPIO子系统和pinctrl子系统,需要的朋友可以了解一下gpio子系统和pinctrl子系统(上)
好吧,赶紧回来扯远了,看不明白不要紧,我们主要是先实践再理论,只学理论知识可能很让人绝望啊,等用多了,再回头学习自然就明白了。现在我们开始在设备树中配置gpio,打开'arch/arm/boot/dts'目录下的'i2c6ulxb-i2s6ull-emmc.dtsi'文件,在跟节点中添加如下信息

dtsled{
		compatible = "i2som,gpioled";
		pinctrl-names = "default";
		pinctrl-0 = <&pinctrl_dtsled>;
		led-gpios = <&gpio5 5 GPIO_ACTIVE_LOW>;
		status = "okay";
	};

如下图所示:

细心的小伙伴可以已经看出来了,去注释了一行信息,因为湃兔的这个开发板只有一个led灯,然后被系统用于心跳灯使用,为了更好的验证,所以我们把系统使用的心跳灯给注释了,如下图所示:

最后在'iomuxc_snvs'这个节点中添加如下信息:

pinctrl_dtsled: dtsled {
			fsl,pins = <
				MX6ULL_PAD_SNVS_TAMPER5__GPIO5_IO05	0x1b0b0
			>;
		};

如下图所示:

到此我们的设备树已经更改完成了,接下来编写驱动程序。

四、led驱动程序

其他的函数我就不过多介绍了,有需要的小伙伴可以查看我直接的文章,我使用led的驱动函数是如下所示

/* 获取GPIO */
int of_get_named_gpio(struct device_node *np, const char *propname, int index)
/* 检查gpio number是否合法 */
int gpio_to_irq(unsigned gpio)
/* 申请IO */
int gpio_request(unsigned gpio, const char *label)
/* 释放IO */
void gpio_free(unsigned gpio)
/* 设置gpio 为输入*/
int gpio_direction_input(unsigned gpio)
/* 设置IO为输出模式 */
int gpio_direction_output(unsigned gpio, int value)
/* 设置IO输出电平 */
gpio_set_value(unsigned gpio, int value)
/* 设置gpio的消抖时间 */
int gpio_set_debounce(unsigned gpio, unsigned debounce)
/* 获取gpio对应的中断线路 */
int gpio_to_irq(unsigned gpio)
/* gpio中断 */
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char * name, void * dev)

相信这些函数都不用过多的介绍,小伙伴们应该都知道怎么使用了,如想了解具体的参数含义可以查看Linux 驱动学习笔记 - gpio 子系统 (八)这篇文章,接下来开始编写源码。

五、程序源码

驱动dtsled.c文件

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

#define DTSLED_NAME     "dtsled"
#define DTSLED_COUNT    1
#define LEDOFF          0
#define LEDON           1

/*设备结构体*/
struct dtsled_dev{
    dev_t devid;                /* 设备号 */
    int major;                  /* 主设备号 */
    int minor;                  /* 次设备号 */
    struct cdev cdev;           /* 字符设备 */
    struct class *class;        /* 类结构体 */
    struct device *device;      /* 设备 */
    struct device_node *nd;     /* 设备节点 */
    int gpio_number;            /* gpio的编号 */
};

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)
{
    return 0;
}

static ssize_t dtsled_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
    int ret = 0;
    unsigned char databuf[1];
    struct dtsled_dev *dev = filp->private_data;

    ret = copy_from_user(databuf, buf, count);
    if (ret < 0) {
        return -EINVAL;
    }

    if (databuf[0] == LEDON) {
        gpio_set_value(dev->gpio_number, 0);
    } else if (databuf[0] == LEDOFF) {
        gpio_set_value(dev->gpio_number, 1);
    }

    return 0;
}

/*
 * 字符设备操作集合
 */
static const 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;

    printk("dtsled_init\r\n");

    /* 申请设备号 */
    dtsled.major = 0;   /* 设置设备号由内存分配 */
    if (dtsled.major){
    	dtsled.devid = MKDEV(dtsled.major, 0);
    	ret = register_chrdev_region(dtsled.devid, DTSLED_COUNT, DTSLED_NAME);
    } else {
    	ret = alloc_chrdev_region(&dtsled.devid, 0, DTSLED_COUNT, DTSLED_NAME);
    	dtsled.major = MAJOR(dtsled.devid);
    	dtsled.minor = MINOR(dtsled.devid);
    }
    if (ret < 0) {
    	printk("dtsled chrdev_region err!\r\n");
    	goto fail_devid;
    }
    
    /* 注册字符设备 */
    dtsled.cdev.owner = dtsled_fops.owner;
    cdev_init(&dtsled.cdev, &dtsled_fops);
    ret = cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_COUNT);
    if (ret < 0) {
        goto fail_cdev;
    }

    /* 自动创建设备节点 */
    dtsled.class = class_create(dtsled_fops.owner, DTSLED_NAME);
    if (IS_ERR(dtsled.class)) {
        ret = PTR_ERR(dtsled.class);
        goto fail_class;
    }

    dtsled.device = device_create(dtsled.class, NULL, dtsled.devid, NULL, DTSLED_NAME);
    if (IS_ERR(dtsled.device)) {
        ret = PTR_ERR(dtsled.device);
        goto fail_device;
    }

    /* 获取设备树的属性内容 */
    dtsled.nd = of_find_node_by_path("/dtsled");
    if (dtsled.nd == NULL) {
        ret = -EINVAL;
        goto fail_findnd;
    }

    /* 获取GPIO */
    dtsled.gpio_number = of_get_named_gpio(dtsled.nd, "led-gpios", 0);
    if (dtsled.gpio_number < 0) {
        printk("can't find led gpio");
        ret = -EINVAL;
        goto fail_rs;
    } else {
        printk("led gpio num = %d\r\n", dtsled.gpio_number);
    }

    /* 申请IO */
    ret = gpio_request(dtsled.gpio_number, "led-gpio");
    if (ret < 0) {
        printk("failde to request the led gpio\r\n");
        ret = -EINVAL;
        goto fail_rs;
    }

    /* 设置IO为输出模式 */
    ret = gpio_direction_output(dtsled.gpio_number, 1);
    if (ret < 0) {
        ret = -EINVAL;
        goto fail_setoutput;
    }

    /* 设置IO默认输出低电平 */
    gpio_set_value(dtsled.gpio_number, 0);

    return 0;


fail_setoutput:
    gpio_free(dtsled.gpio_number);
fail_rs:
fail_findnd:
    device_destroy(dtsled.class, dtsled.devid);
fail_device:
    class_destroy(dtsled.class);
fail_class:
    cdev_del(&dtsled.cdev);
fail_cdev:
    unregister_chrdev_region(dtsled.devid, DTSLED_COUNT);
fail_devid:
    return ret;

}

/*
 * 模块出口
 */
static void __exit dtsled_exit(void)
{
    printk("dtsled_exit\r\n");

    /* 关闭led */
    gpio_set_value(dtsled.gpio_number, 1);

    /* 删除字符设备 */
    cdev_del(&dtsled.cdev);

    /* 释放字符设号 */
    unregister_chrdev_region(dtsled.devid, DTSLED_COUNT);

    /* 摧毁设备 */
    device_destroy(dtsled.class, dtsled.devid);

    /* 摧毁类 */
    class_destroy(dtsled.class);

    /* 释放IO */
    gpio_free(dtsled.gpio_number);
}

/*
 * 模块注册入口
 */
module_init(dtsled_init);
/*
 * 模块注册出口
 */
module_exit(dtsled_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("jiaozhu");

Makefile文件

KERNELDIR := /home/xfg/linux/imx_7ull/i2x_6ub/system_file/i2SOM-iMX-Linux
 
CURRENT_PATH := $(shell pwd)
obj-m := dtsled.o

build: kernel_modules

kernel_modules:
    $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
    $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

ledApp.c应用测试文件

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


/*
 *argc:应用程序参数个数
 *argv[]:具体的参数内容,字符串形式
 *./ledApp <filename> <0:1> 0表示关,1表示开 
 *./ledApp /dev/dtsled 0    关闭led灯
 *./ledApp /dev/dtsled 1    打开led灯
 * */

#define LEDOFF      0
#define LEDON       1

int main(int argc, char *argv[])
{
    int ret = 0;
    int fd = 0;
    char *filename;
    unsigned char databuf[1];

    if(argc !=3) {
        printf("Instruction usage error!!!\r\n");
        printf("./ledApp <filename> <0:1> 0表示关,1表示开\r\n");
        printf("./ledApp ./dev/dtsled 1 \r\n");
        return -1;
    }

    filename = argv[1];

    fd = open(filename, O_RDWR);
    if(fd < 0) {
        printf("open file %s failed\r\n", filename);
    }

    databuf[0] = atoi(argv[2]);
    ret = write(fd, databuf, sizeof(databuf));
    if(ret < 0) {
        printf("write file %s failed\r\n", filename);
    }

    ret =close(fd);
    if(ret <0) {
        printf("close file %s falied!\r\n", filename);
    }

    return 0;
}

六、测试

1.设备树测试

回到内核源码的根目录下重新编译设备树文件,编译命令

make dtbs

编译完成后将/arch/arm/boot/dts目录下的i2c6ulxb-i2s6ull-emmc.dtb拷贝至tftp服务器下,然后重启开发版即可,我使用的是nfs挂载根文件系统的方式进行测试的,当然也可以直接更新开发板中的设备树,只是这样比较麻烦,开发中不建议这么操作。
重新启动后,进入/proc/device-tree/目录下查看我们更改的节点信息是否存在,如下图所示:

可知我们添加的dtsled节点信息是确定的,接下来测试驱动。

1.驱动测试

将应用文件和驱动文件编译后拷贝到开发版的/lib/modules/4.1.43/目录下,挂载led驱动,如下图所示:

挂载成功后可以看到我们申请的gpio引脚编号是133,最后通过应用测试led是否能打开和关闭,如下图所示:

如果能正常打开和关闭led灯,说明我们的驱动和应用程序没有问题,若有写得不好的地方,望各位大佬指出。

参考文献

gpio子系统和pinctrl子系统(上):https://www.cnblogs.com/rongpmcu/p/7662751.html
Linux 驱动学习笔记 - gpio 子系统 (八):https://blog.csdn.net/tyustli/article/details/105484666