通过之前的学习,了解到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