第1章.linux LED驱动:
init中:gpio相关寄存器内存映射ioremap-gpio配置-注册字符设备register-chrdev
exit中:注销字符设备unregister_chrdev------取消内存映射iounmap
write中:copy_from_user获取用户程序输入值------readl读取data寄存器值为val------根据用户程序传入的值将val的12位写0或者写1------writel写入data寄存器
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
/*#include <asm/mach/map.h>*/
#include <asm/uaccess.h>
#include <asm/io.h>
#define LED_MAJOR 200 /* 主设备号 */
#define LED_NAME "led" /* 设备名字 */
/*
* GPIO相关寄存器地址定义
*/
#define ZYNQ_GPIO_REG_BASE 0xFF0A0000
#define DATA_OFFSET 0x00000044
#define DIRM_OFFSET 0x00000244
#define OUTEN_OFFSET 0x00000248
#define INTDIS_OFFSET 0x00000254
#define APER_CLK_CTRL 0xFF5E00AC
/* 映射后的寄存器虚拟地址指针 */
static void __iomem *data_addr;
static void __iomem *dirm_addr;
static void __iomem *outen_addr;
static void __iomem *intdis_addr;
static void __iomem *aper_clk_ctrl_addr;
/*
* @description : 打开设备
* @param -- inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int led_open(struct inode *inode, struct file *filp)
{
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t led_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
{
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t led_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *offt)
{
int ret;
int val;
char kern_buf[1];
ret = copy_from_user(kern_buf, buf, cnt);// 得到应用层传递过来的数据
if(0 > ret) {
printk(KERN_ERR "kernel write failed!\r\n");
return -EFAULT;
}
val = readl(data_addr);
if(0 == kern_buf[0])
val &= ~(0x1U << 12); // 如果传递过来的数据是0则关闭led
else if (1 == kern_buf[0])
val |= (0x1U << 12); // 如果传递过来的数据是1则点亮led
writel(val, data_addr);
return 0;
}
/*
* @description : 关闭/释放设备
* @param -- filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
/* 设备操作函数 */
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release= led_release,
};
static int __init led_init(void)
{
u32 val;
int ret;
/* 1.寄存器地址映射 */
data_addr = ioremap(ZYNQ_GPIO_REG_BASE + DATA_OFFSET, 4);
dirm_addr = ioremap(ZYNQ_GPIO_REG_BASE + DIRM_OFFSET, 4);
outen_addr = ioremap(ZYNQ_GPIO_REG_BASE + OUTEN_OFFSET, 4);
intdis_addr = ioremap(ZYNQ_GPIO_REG_BASE + INTDIS_OFFSET, 4);
aper_clk_ctrl_addr = ioremap(APER_CLK_CTRL, 4);
/* 2.使能GPIO时钟 */
val = readl(aper_clk_ctrl_addr);
val |= (0x1U << 24);
writel(val, aper_clk_ctrl_addr);
/* 3.关闭中断功能 */
val |= (0x1U << 12);
writel(val, intdis_addr);
/* 4.设置GPIO为输出功能 */
val = readl(dirm_addr);
val |= (0x1U << 12);
writel(val, dirm_addr);
/* 5.使能GPIO输出功能 */
val = readl(outen_addr);
val |= (0x1U << 12);
writel(val, outen_addr);
/* 6.默认关闭LED */
val = readl(data_addr);
val &= ~(0x1U << 12);
writel(val, data_addr);
/* 7.注册字符设备驱动 */
ret = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
if(0 > ret){
printk(KERN_ERR "Rsgister LED driver failed!\r\n");
return ret;
}
return 0;
}
static void __exit led_exit(void)
{
/* 1.卸载设备 */
unregister_chrdev(LED_MAJOR, LED_NAME);
/* 2.取消内存映射 */
iounmap(data_addr);
iounmap(dirm_addr);
iounmap(outen_addr);
iounmap(intdis_addr);
iounmap(aper_clk_ctrl_addr);
}
/* 驱动模块入口和出口函数注册 */
module_init(led_init);
module_exit(led_exit);
MODULE_AUTHOR("DengTao <773904075@qq.com>");
MODULE_DESCRIPTION("Alientek ZYNQ GPIO LED Driver");
MODULE_LICENSE("GPL");
第2章.新字符设备驱动实验
改进点:(1)使用 Linux 内核推荐的新字符设备驱动API 函数注册和注销字符设备(2)在驱动模块加载的时候自动创建设备节点文件
1.创建设备结构体 newchrled_dev
struct newchrled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
static struct newchrled_dev newchrled; /* led设备 */
2.led_open 函数中设置文件的私有数据 private_data 指向 newchrdev。
init中:
(1)地址映射ioremap
(2)gpio配置
(3)注册设备号
if(newchrled.major) {
newchrled.devid = MKDEV(newchrled.major, 0);
ret = register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
if (ret)
goto out1;
} else {
ret = alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);
if (ret)
goto out1;
newchrled.major = MAJOR(newchrled.devid);
newchrled.minor = MINOR(newchrled.devid);
}
(4)注册cdev
newchrled.cdev.owner = THIS_MODULE;
cdev_init(&newchrled.cdev, &newchrled_fops);
/* 添加一个cdev */
ret = cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);
if (ret)
goto out2;
(5)创建类
newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
if (IS_ERR(newchrled.class)) {
ret = PTR_ERR(newchrled.class);
goto out3;
}
(6)创建设备
newchrled.device = device_create(newchrled.class, NULL,
newchrled.devid, NULL, NEWCHRLED_NAME);
if (IS_ERR(newchrled.device)) {
ret = PTR_ERR(newchrled.device);
goto out4;
}
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
/*#include <asm/mach/map.h>*/
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/cdev.h>
#define NEWCHRLED_CNT 1 /* 设备号个数 */
#define NEWCHRLED_NAME "newchrled" /* 名字 */
/*
* GPIO相关寄存器地址定义
*/
#define ZYNQ_GPIO_REG_BASE 0xFF0A0000
#define DATA_OFFSET 0x00000044
#define DIRM_OFFSET 0x00000244
#define OUTEN_OFFSET 0x00000248
#define INTDIS_OFFSET 0x00000254
#define APER_CLK_CTRL 0xFF5E00AC
/* 映射后的寄存器虚拟地址指针 */
static void __iomem *data_addr;
static void __iomem *dirm_addr;
static void __iomem *outen_addr;
static void __iomem *intdis_addr;
static void __iomem *aper_clk_ctrl_addr;
/* newchrled设备结构体 */
struct newchrled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
static struct newchrled_dev newchrled; /* led设备 */
/*
* @description : 打开设备
* @param -- inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &newchrled; /* 设置私有数据 */
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t led_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
{
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t led_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *offt)
{
int ret;
int val;
char kern_buf[1];
ret = copy_from_user(kern_buf, buf, cnt); // 得到应用层传递过来的数据
if (0 > ret){
printk(KERN_ERR "kernel write failed!\r\n");
return -EFAULT;
}
val = readl(data_addr);
if(0 == kern_buf[0])
val &= ~(0x1U << 12); // 如果传递过来的数据是0则关闭led
else if (1 == kern_buf[0])
val |= (0x1U << 12); // 如果传递过来的数据是1则点亮led
writel(val, data_addr);
return 0;
}
/*
* @description : 关闭/释放设备
* @param -- filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
static inline void led_ioremap(void)
{
data_addr = ioremap(ZYNQ_GPIO_REG_BASE + DATA_OFFSET, 4);
dirm_addr = ioremap(ZYNQ_GPIO_REG_BASE + DIRM_OFFSET, 4);
outen_addr = ioremap(ZYNQ_GPIO_REG_BASE + OUTEN_OFFSET, 4);
intdis_addr = ioremap(ZYNQ_GPIO_REG_BASE + INTDIS_OFFSET, 4);
aper_clk_ctrl_addr = ioremap(APER_CLK_CTRL, 4);
}
static inline void led_iounmap(void)
{
iounmap(data_addr);
iounmap(dirm_addr);
iounmap(outen_addr);
iounmap(intdis_addr);
iounmap(aper_clk_ctrl_addr);
}
/* 设备操作函数 */
static struct file_operations newchrled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
static int __init led_init(void)
{
u32 val;
int ret;
/* 1.寄存器地址映射 */
led_ioremap();
/* 2.使能GPIO时钟 */
val = readl(aper_clk_ctrl_addr);
val |= (0x1U << 24);
writel(val, aper_clk_ctrl_addr);
/* 3.关闭中断功能 */
val |= (0x1U << 12);
writel(val, intdis_addr);
/* 4.设置GPIO为输出功能 */
val = readl(dirm_addr);
val |= (0x1U << 12);
writel(val, dirm_addr);
/* 5.使能GPIO输出功能 */
val = readl(outen_addr);
val |= (0x1U << 12);
writel(val, outen_addr);
/* 6.默认关闭LED */
val =readl(data_addr);
val &= ~(0x1U << 12);
writel(val, data_addr);
/* 7.注册字符设备驱动 */
/* 创建设备号 */
if(newchrled.major) {
newchrled.devid = MKDEV(newchrled.major, 0);
ret = register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
if (ret)
goto out1;
} else {
ret = alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);
if (ret)
goto out1;
newchrled.major = MAJOR(newchrled.devid);
newchrled.minor = MINOR(newchrled.devid);
}
printk("newcheled major=%d,minor=%d\r\n", newchrled.major, newchrled.minor);
/* 初始化cdev */
newchrled.cdev.owner = THIS_MODULE;
cdev_init(&newchrled.cdev, &newchrled_fops);
/* 添加一个cdev */
ret = cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);
if (ret)
goto out2;
/* 创建类 */
newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
if (IS_ERR(newchrled.class)) {
ret = PTR_ERR(newchrled.class);
goto out3;
}
/* 创建设备 */
newchrled.device = device_create(newchrled.class, NULL,
newchrled.devid, NULL, NEWCHRLED_NAME);
if (IS_ERR(newchrled.device)) {
ret = PTR_ERR(newchrled.device);
goto out4;
}
return 0;
out4:
class_destroy(newchrled.class);
out3:
cdev_del(&newchrled.cdev);
out2:
unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);
out1:
led_ioremap();
return ret;
}
static void __exit led_exit(void)
{
/* 注销设备 */
device_destroy(newchrled.class, newchrled.devid);
/* 注销类 */
class_destroy(newchrled.class);
/* 删除cdev */
cdev_del(&newchrled.cdev);
/* 注销设备号 */
unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);
/* 取消地址映射 */
led_ioremap();
}
/* 驱动模块入口和出口函数注册 */
module_init(led_init);
module_exit(led_exit);
MODULE_AUTHOR("DengTao <773904075@qq.com>");
MODULE_DESCRIPTION("Alientek ZYNQ GPIO LED Driver");
MODULE_LICENSE("GPL");
第3章.linux设备树
3.1.优势
用于实现驱动代码与设备信息相分离,引入了设备树之后,驱动代码只负责处理驱动的逻辑,而关于设备的具体信息存放到设备树文件中,这样,如果只是硬件接口信息的变化而没有驱动逻辑的变化,驱动开发者只需要修改设备树文件信息,不需要改写驱动代码。这样编译只需要编译设备树,加快效率。
3.2.本篇驱动开发所使用的板级设备树文件
就是 arch/arm64/boot/dts/xilinx 目录下,包括 system-top.dts、pl.dtsi、pcw.dtsi、zyqnmp.dtsi、zynqmp-clk-conf.dtsi 和用户自定义设备树 system-user.dtsi 等。注意,在设备树中添加设备时,我们推荐放在 system-user.dtsi 中。(dts是设备树文件;dtsi文件其实就类似于 C 语言当中的.h 头文件,可以通过 C 语言中使用 include 来包含一个.dtsi 文件;dtb是dts编译后的二进制文件,我们需要这个,dtc是设备树文件.dts 的编译器)(zynqmp.dtsi 文件中的内容是 ZYNQ MPSoC 系列处理器相同的硬件外设配置信息(PS 端的),pl.dtsi 的内容是我们在 vivado 当中添加的 pl 端外设对应的配置信息,而 pcw.dtsi 则表示我们在 vivado 当中已经使能的 PS 外设,zynqmp-clk-ccf.dtsi 文件是 ZYNQ MPSoC 系列芯片通用的时钟相关的设备树文件)
3.3.使 用 make dtbs 命 令
在 内 核 源 码arch/arm64/boot/dts/xilinx/目录下有很多的 dts 文件,那我们编译的时候如何确定编译的是哪个或者说哪些 dts 文件的呢?大家可以打开 arch/arm64/boot/dts/xilinx/Makefile 文件,当运行 " make dtbs " 命令时,该makefile包含的设备树都会被编(我们要编译设备树"system-top.dts"文件,只要把"system-top.dtb"添加到Makefile 中就行。)
3.4.设备树结构特征
(1)一个 device tree 文件中只有一个 root node(根节点)(2)除了 root node,每个 node 都只有一个 parent node(父节点)(3)一般来说,开发板上的每一个设备都能够对应到设备树中的一个 node(4)每个 node 中包含了若干的 property-value(键-值对,当然也可以没有 value) 来描述该 node 的一些特性(5)每个 node 都有自己的 node name(节点名字)(6)node 之间可以是平行关系,也可以嵌套成父子关系,这样就可以很方便的描 述设备间的关系

解释:例如 node2 表示 ZYNQ PS 的一个 I2C 控制器,而 node3 则表示挂在该 I2C 总线下的某个设备,例如 eeprom、RTC 等。


3.5.设备树节点属性
(1)compatible 属性("<制造商>,<型号>")
compatible = "xlnx,xuartps", "cdns,uart-r1p8";
compatible 属性用于将设备和驱动绑定起来,例如该设备首先使用第一个兼容值 (xlnx,xuartps)在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值(cdns,uart-r1p8)查找,直到找到或者查找完整个 Linux 内核也没有找到对应的驱动。(一般驱动程序文件都会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设备树中的节点的 compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动)
(2)model 属性值
model ="Alientek Zynq MPSoC Development Board";
model 属性值是一个字符串描述信息,内核在解析设备树的时候会把这个属性对应的字符串信息打印出来。
(3)status 属性
status 标识了设备的状态,使用 status 可以去禁止设备或者启用设备,看下设备树规范中的 status 可选值。

(4),#address-cells 和#size-cells
(5)reg属性
reg 属性的值一般是(address,length),其中 address 是起始地址,length 是地址长度。
(5)根节点 compatible 属性
针对 Xilinx Zynq MPSoC 系列的板卡,根节点"/"的 compatible 属性内容则为"xlnx,zynqmp"。当我们修改了根节点 compatible 属性内容以后,因为 Linux 内核找不到对应的硬件平台,因此 Linux 内核无法启动。
Linux 内核通过根节点 compatible 属性找到对应的 machine_desc 结构体的函数调用过程。

(6)引用节点
节点的命名格式如下所示,引入 label 的目的就是为了方便访问节点,可以直 接通过&label 来访问这个节点。
label:\]node-name\[@unit-address
比如节 点"intc: interrupt-controller@00a01000",节点 label 是 intc,而节点名字就很长了,为 "interrupt-controller@00a01000"。很明显通过&intc 来访问"interrupt-controller@00a01000" 这个节点要方便很多。
3.6.添加新设备到设备树:
任务:假设在 Zynq7000 系列的开发板上有一个 eeprom 器件(24c64)和一个 rtc 器件(pcf8563),它俩都 是挂在 ZYNQ 的 i2c0 总线下的。那么现在要把这两个设备添加到 i2c0 总线下。
方法:我们需要在 system-user.dtsi 文件中通过 &label 来访问节点完成数据追加的内容如下。

使用"&i2c0"引用i2c0,第 34~48 行就是向 i2c0 节点添加/修改数据,比如 35 的属性"clock-frequency = <100000>" 就表示将 i2c0 的时钟设置为 100KHz,"clock-frequency"就是新添加的属性。 第 36 行,将 status 属性的值由原来的 disabled 改为 okay,这是修改节点的属性值。 第 38~47 行,我们向 i2c0 子节点追加了两个子节点,"24c64@50"和"rtc@51"。
3.7.特殊节点
在根节点"/"中有那么几个特殊的子节点:aliases、chosen 以及 memory。
(1)aliases节点

aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。例如前面说到的使用"&label"的方式访问设备树中的节点,而是内核当中方便定位节点,例如在内核中通过 ethernet0 就可以定位到 gem0 节点(&gem0 引用的节点),再例如内核通过 serial0 就可以找到 uart0 节点。
(2)chosen 节点
chosen 节点一般会有两个属性,"bootargs"和"stdout-path"。

在 chosen 节点当中,属性 stdout-path = "serial0:115200n8",表示标准输出设备使用串
口 serial0,在 system-top.dts 文件当中,serial0 其实是一个别名,指向的就是 uart0;"115200"
则表示串口的波特率为 115200,"n"表示无校验位,"8"则表示有 8 位数据位。
console=ttyPS0,115200,ttyPS0 其实指的就是根文件系统下的/dev/ttyPS0 这个设备文
件,那么它对应的硬件设备其实就是板子的 uart0,root就是指定根文件系统路径。

对于我们使用这个 U-Boot 来说,它并没有定义 bootargs 环境变量,所以使用的就是内核设备树 chosen 节点下的 bootargs 属性,也就是说 U-Boot 的环境变量 bootargs 拥有最高的优先级。U-Boot 的 bootcmd 命令最终会执行 bootz 命令,而 bootz命令启动 Linux 内核的时候会运行 do_bootm_linux 函数,do_bootm_linux 函数会通过一系列复杂的调用,最终通过 fdt_chosen 函数在内核设备树chosen 节点中添加 bootargs 属性。
(3)memory 节点

memory 节点描述了系统内存的基地址以及系统内存大小,"reg = <0x0 0x20000000>"
就表示系统内存的起始地址为 0x0,大小为 0x20000000,也就是 512MB,该节点一般只有这
两个属性,device_type 属性的值固定为"memory"。
3.8 设备树在系统中的体现
Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的
/proc/device-tree 目录下根据节点名字创建不同文件夹。
3.9 设备树详细文档
:Linux 源码目录/Documentation/devicetree/bindings
3.10设备树常见of操作函数
设备都是以节点的形式"挂"到设备树上的,因此要想获取这个设备的属性信息,必须
先获取到这个设备的节点。Linux 内核使用 device_node 结构体来描述一个节点,此结构体定
义在文件 include/linux/of.h 中,定义如下
示例代码 24.8.1.1 device_node 节点
49 struct device_node {
50 const char *name;
/* 节点名字 */
51 const char *type;
/* 设备类型 */
52 phandle phandle;
53 const char *full_name;
/* 节点全名 */
54 struct fwnode_handle fwnode;
55
56 struct property *properties;
/* 属性 */
57 struct property *deadprops;
/* removed 属性 */
58 struct device_node *parent;
/* 父节点 */
59 struct device_node *child;
/* 子节点 */
60 struct device_node *sibling;
61 struct kobject kobj;
62 unsigned long _flags;
63 void *data;
64 #if defined(CONFIG_SPARC)
65 const char *path_component_name;
66 unsigned int unique_id;
67 struct of_irq_controller *irq_trans;
68 #endif
69 };
所以在驱动代码中需要定义一个device_node类型的变量。
/* dtsled设备结构体 */struct dtsled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
};
static struct dtsled_dev dtsled; /* led设备 */
3.10.1 of_find_node_by_path****函数

例子:dtsled.nd = of_find_node_by_path("/led");
3.10.2 of_property_read_string****函数

例1:ret = of_property_read_string(dtsled.nd, "status", &str);
if(!ret) {
if(strcmp(str, "okay"))
return -EINVAL;
}
例2:ret = of_property_read_string(dtsled.nd, "compatible", &str);
if(0 > ret)return -EINVAL;
if(strcmp(str, "alientek,led"))
return -EINVAL;
3.10.3 of_iomap函数

例子
static inline void led_ioremap(void)
{
data_addr = of_iomap(dtsled.nd, 0);
dirm_addr = of_iomap(dtsled.nd, 1);
outen_addr = of_iomap(dtsled.nd, 2);
intdis_addr = of_iomap(dtsled.nd, 3);
aper_clk_ctrl_addr = of_iomap(dtsled.nd, 4);
}
第4章.设备树下LED驱动实验
(1)优势:原先"在驱动文件 newchrled.c 中定义有关寄存器物理地址,然后使用 io_remap 函数进行内存映射,得到对应的虚拟地址,最后操作寄存器对应的虚拟地址完成对 GPIO 的初始化",本章我们在"新字符设备驱动实验"基础上完成,本章我 们使用设备树来向 Linux 内核传递相关的寄存器物理地址,Linux 驱动文件使用上一章讲解的 OF 函数从设备树中获取所需的属性值,然后使用获取到的属性值来初始化相关的 IO。
(2)添加LED这个设备对应的设备树节点,打开 linux 内核源码目录下的arch/arm64/boot/dts/xilinx/system-user.dtsi 文件,在根节点"/"下创建一个名为"led"的子节点
led**{**
compatible**="alientek,led";**
status**="okay";**
default**-state="on";**
reg = <0x0 0XFF0A0044 0x0 0x4
0x0 0XFF0A0244 0x0 0x4
0x0 0XFF0A0248 0x0 0x4
0x0 0XFF0A0254 0x0 0x4
0x0 0XFF5E00AC 0x0 0x4>;
};
因为MPsoc是64位,所以前面的0x0是补了32个0。
(3)驱动代码编写(基于newchrled.c)
添加1:在设备结构体 dtsled_dev 中添加了成员变量 nd,nd 是 device_node 结构体类型指针,表示设备节点。如果我们要读取设备树某个节点的属性值,首先要先得到这个节点,一般在设备结构体中添加 device_node 指针变量来存放这个节点。
struct dtsled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
};
static struct dtsled_dev dtsled; /* led设备 */
修改1:通过使用 of_iomap 函数替换之前使用 ioremap 函数来实现物理地址到虚拟地址的映射,它能够直接解析给定节点的 reg 属性,并将 reg 属性中存放的物理地址和长度进行映射,使用不同的下标依次对 reg 数组中记录的不同组"物理地址-长度"地址空间进行映射。
static inline void led_ioremap(void)
{
data_addr = of_iomap(dtsled.nd, 0);
dirm_addr = of_iomap(dtsled.nd, 1);
outen_addr = of_iomap(dtsled.nd, 2);
intdis_addr = of_iomap(dtsled.nd, 3);
aper_clk_ctrl_addr = of_iomap(dtsled.nd, 4);
}
添加2:设备相关代码:
(1)通过 of_find_node_by_path 函数获取设备树根节点下的 led 节点
(2)通过 of_property_read_string 函数获取 led 节点的"status"属性的内容
(3)通过 of_property_read_string 函数获取 led 节点的"compatible"属性的内容
(4)通过 of_property_read_string 函数获取 led 节点的"default-state"属性的内容
/* 1.寄存器地址映射 *///根据路径获取设备节点
dtsled.nd = of_find_node_by_path("/led");
if(NULL == dtsled.nd) {
printk(KERN_ERR "led node can not found!\r\n");
return -EINVAL;
}
/* 2.读取 status 属性 */
ret = of_property_read_string(dtsled.nd, "status", &str);
if(!ret) {
if(strcmp(str, "okay"))
return -EINVAL;
}
/* 3、获取 compatible 属性值并进行匹配 */
ret = of_property_read_string(dtsled.nd, "compatible", &str);
if(0 > ret)
return -EINVAL;
if(strcmp(str, "alientek,led"))
return -EINVAL;
printk(KERN_ERR "led device matching successful!\r\n");
//适应设备树中设置的初始状态
ret = of_property_read_string(dtsled.nd, "default-state", &str);
if(!ret){
if(!strcmp(str, "on"))
val |= (0x1U << 12);
else
val &= ~(0x1U << 12);
} else
val &= ~(0x1U << 12);
writel(val, data_addr);
第5章.gpio子系统介绍
(1)内核维护者在内核中设计了一些统一管控系统资源的体系(gpio子系统,input子系统等),这些体系让内核能够对系统 资源在各个驱动之间的使用统一协调和分配,保证整个内核的稳定健康运行。例如系统中所有的 GPIO 就属于系统资源,每个驱动模块如果要使用某个 GPIO 就要先调用特殊的接口先申请,申请到后使用,使用完后要释放 。又譬如中断号也是一种资源,驱动在使用前也必须去 申请,只能申请成功之后才能使用,否则不能使用。
(2)gpio 子系统是 linux 内核当中用于管理 GPIO 资源的一套系统,它提供了很多 GPIO 相关的 API 接口。驱动程序中使用 GPIO 之前需要向 gpio 子系统申请,申请成功之后才可以使用,例如设置 GPIO 的输入、输出方向,设置 GPIO 输出高或低电平、读取 GPIO 输入电平等等。(本质是:寄存器开发------>库函数开发)
(3)设备树中的gpio信息
在zynqmp.dtsi,在里面找到如下所示内容:gpio 节点信息描述了 ZYNQ MPSoC 器件 PS GPIO 控制器的所有信息。
示例代码 20.1.2 gpio0 节点
648 gpio**:gpio@ff0a0000 {
649 compatible ="xlnx,zynqmp-gpio-1.0";**
650 status ="disabled";
651 #gpio**-cells = <0x2>;
652 interrupt-parent = <&gic>;
653 interrupts = <0 16 4>;
654 interrupt-controller;**
655 #interrupt**-cells = <2>;
656 reg = <0x0 0xff0a0000 0x0 0x1000>;
657 gpio-controller;**
658 power**-domains = <&zynqmp_firmware 46>;
659 };
"#gpio-cells"属性和"#address-cells"类似,在 gpio 节点中#gpio-cells 的值等于 0x2,表示一共有两个 cell,大家可以这样理解,使用 gpio 的时候,需要传递 2 个参数过去,第一个参数为 GPIO 编号,比如"&gpio 38"就表示 GPIO MIO38。第二个参数表示GPIO 极 性 , 如 果 为0(GPIO_ACTIVE_HIGH) 的 话 表 示 高 电 平 有 效 , 如 果 为1(GPIO_ACTIVE_LOW)的话表示低电平有效。所以在 led 节点中"led-gpio = <&gpio 38 GPIO_ACTIVE_HIGH>;"就表示使用了 GPIOBank1 MIO38 这个管脚,并且是高电平有效。
gpio-controller;表示 gpio 节点是个 GPIO 控制器,表示这个节点对应驱动程序是 gpio 驱动。
示例代码的gpio信息如下(我个人感觉下面的gpios和上文说的led-gpio是一个意思)
示例代码 20.1.1 gpio 信息
11 gpio-led0 {
12 label ="PS_LED0";**
13 gpios = <&gpio 38 GPIO_ACTIVE_HIGH>;
14 linux**,default-trigger ="timer";**
15 };
......
41 gpio**-key1 {
42 label ="PS_KEY0";**
43 gpios = <&gpio 40 GPIO_ACTIVE_LOW>;
44 linux**,**code = <KEY_UP>;
45 };
1.gpio子系统的API函数
1.1gpio_request****函数

例子:
/* 4.获取设备树中的led-gpio属性,得到LED所使用的GPIO编号 */
gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0);
if(!gpio_is_valid(gpioled.led_gpio)) {
printk(KERN_ERR "gpioled: Failed to get led-gpio\n");
return -EINVAL;
}
printk(KERN_INFO "gpioled: led-gpio num = %d\r\n", gpioled.led_gpio);
/* 5.向gpio子系统申请使用GPIO */
ret = gpio_request(gpioled.led_gpio, "LED-GPIO");
if (ret) {
printk(KERN_ERR "gpioled: Failed to request led-gpio\n");
return ret;
}
1.2 gpio_free****函数

/* 释放GPIO */
gpio_free(gpioled.led_gpio);
1.3 gpio_direction_input****函数

1.4 gpio_direction_output****函数

/* 6.将led gpio管脚设置为输出模式 */
gpio_direction_output(gpioled.led_gpio, 0);
1.5 gpio_get_value****函数

1.6 gpio_set_value****函数

/* 7.初始化LED的默认状态 */
ret = of_property_read_string(gpioled.nd, "default-state", &str);
if(!ret) {
if (!strcmp(str, "on"))
gpio_set_value(gpioled.led_gpio, 1);
else
gpio_set_value(gpioled.led_gpio, 0);
} else
gpio_set_value(gpioled.led_gpio, 0);
2.gpio相关of函数
2.1 of_get_named_gpio函数
获取设备树中的led-gpio属性,得到LED所使用的GPIO编号。

例子:gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0);
第6章.gpio子系统下的LED实验
(1)我们在 dtsled.c 驱动代码中获取到了 GPIO 有关寄存器物理地址,然 后使用 of_iomap 函数进行内存映射,得到对应的虚拟地址,最后操作寄存器对应的虚拟地址 完成对 GPIO 的初始化,但本质上还是跟裸机没啥区别,还非常的麻烦。所以本章就带大家 完成一个 gpio 子系统下的 led 驱动程序。
(2)本章实验重点内容如下:
1、修改第十九章创建的 led 节点,在节点中指定 led 使用的 GPIO。
2、去掉 led 节点中的 reg 属性,因为不需要用到了。
3、在第十九章驱动实验的基础上修改驱动代码,获取设备树中传入的 gpio,调用 gpio
子系统提供的接口函数对 gpio 进行操控。
(3)修改设备树:打开 linux 内核源码目录下的 arch/arm64/boot/dts/xilinx/system-user.dtsi 文件,对 led 节点内容进行修改,去掉之前 reg 属性,添加 led-gpio 节点指定 LED 所使用的 GPIO 管脚。
1 #define GPIO_ACTIVE_HIGH 0
2 #define GPIO_ACTIVE_LOW 1
......
19 led**{**
20 compatible**="alientek,led";**
21 status**="okay";**
22 default**-state="on";**
23
24 led**-**gpio = <&gpio 38 GPIO_ACTIVE_HIGH>;
25 };
(4)修改驱动代码:
第 26 行,使用 include 包含 of_gpio.h 头文件,因为我们的代码中使用到了与 gpio 相关的
OF 函数。
第 28~29 行,定义 led 设备的设备号数量以及 led 设备的名字。
第 32~41 行,定义了 struct gpioled_dev 结构体,在第十九章的基础上加入 led_gpio 用来
表示 GPIO 的编号。
第 80~98 行,使用 gpio_set_value 函数替代第十九章直接操作 GPIO 寄存器的做法,那么
这个函数我们在前面一张给大家介绍过了。
第 125~129 行,跟之前是一样的,从设备树中获取 led 节点,包括后面读取"status"属
性以及"compatible"属性。
第 153~157 行,重点在这里,使用 of_get_named_gpio 函数获取设备树中 led 节点指定的
gpio 管脚,得到一个 GPIO 编号,那么这个编号是 linux 内核 GPIO 子系统对 gpio 的编号,一
个编号就对应一个 gpio,那么我们这里得到的这个编号就对应设备树中 led-gpio 指定的那个
管脚(gpio_38---MIO38)。得到 GPIO 编号之后使用 gpio_is_valid 函数判断编号是否是有效
编号。
第 162~166 行,通过 gpio_request 函数去申请对该 gpio 管脚的使用权。
第 169 行,通过 gpio_direction_output 函数将 gpio 设置为输出模式。
第 172~179 行,根据 led 节点的"default-state"属性初始化 LED 的状态,使用
gpio_set_value 函数设置 gpio 输出高或低电平。
后面跟第十九章就是一样的,需要注意的是,第 240~256 行,驱动模块的出口函数中一
定要把申请的 gpio 管脚给释放,通过 gpio_free 函数释放 gpio。
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
/* #include <asm/mach/map.h> */
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/cdev.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#define GPIOLED_CNT 1 /* 设备号个数 */
#define GPIOLED_NAME "gpioled" /* 名字 */
/* dtsled设备结构体 */
struct gpioled_dev {
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
int led_gpio; /* LED所使用的GPIO编号 */
};
static struct gpioled_dev gpioled; /* led设备 */
/*
* @description : 打开设备
* @param -- inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &gpioled; /* 设置私有数据 */
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t led_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
{
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t led_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *offt)
{
int ret;
char kern_buf[1];
ret = copy_from_user(kern_buf, buf, cnt); // 得到应用层传递过来的数据
if(0 > ret) {
printk(KERN_ERR "kernel write failed!\r\n");
return -EFAULT;
}
if (0 == kern_buf[0])
gpio_set_value(gpioled.led_gpio, 0); // 如果传递过来的数据是0则关闭led
else if (1 == kern_buf[0])
gpio_set_value(gpioled.led_gpio, 1); // 如果传递过来的数据是1则点亮led
return 0;
}
/*
* @description : 关闭/释放设备
* @param -- filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
/* 设备操作函数 */
static struct file_operations gpioled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
static int __init led_init(void)
{
const char *str;
int ret;
/* 1.获取led设备节点 */
gpioled.nd = of_find_node_by_path("/led");
if(NULL == gpioled.nd) {
printk(KERN_ERR "gpioled: Failed to get /led node\n");
return -EINVAL;
}
/* 2.读取status属性 */
ret = of_property_read_string(gpioled.nd, "status", &str);
if(!ret) {
if (strcmp(str, "okay"))
return -EINVAL;
}
/* 3、获取compatible属性值并进行匹配 */
ret = of_property_read_string(gpioled.nd, "compatible", &str);
if(0 > ret) {
printk(KERN_ERR "gpioled: Failed to get compatible property\n");
return ret;
}
if (strcmp(str, "alientek,led")) {
printk(KERN_ERR "gpioled: Compatible match failed\n");
return -EINVAL;
}
printk(KERN_INFO "gpioled: device matching successful!\r\n");
/* 4.获取设备树中的led-gpio属性,得到LED所使用的GPIO编号 */
gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0);
if(!gpio_is_valid(gpioled.led_gpio)) {
printk(KERN_ERR "gpioled: Failed to get led-gpio\n");
return -EINVAL;
}
printk(KERN_INFO "gpioled: led-gpio num = %d\r\n", gpioled.led_gpio);
/* 5.向gpio子系统申请使用GPIO */
ret = gpio_request(gpioled.led_gpio, "LED-GPIO");
if (ret) {
printk(KERN_ERR "gpioled: Failed to request led-gpio\n");
return ret;
}
/* 6.将led gpio管脚设置为输出模式 */
gpio_direction_output(gpioled.led_gpio, 0);
/* 7.初始化LED的默认状态 */
ret = of_property_read_string(gpioled.nd, "default-state", &str);
if(!ret) {
if (!strcmp(str, "on"))
gpio_set_value(gpioled.led_gpio, 1);
else
gpio_set_value(gpioled.led_gpio, 0);
} else
gpio_set_value(gpioled.led_gpio, 0);
/* 8.注册字符设备驱动 */
/* 创建设备号 */
if (gpioled.major) {
gpioled.devid = MKDEV(gpioled.major, 0);
ret = register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);
if (ret)
goto out1;
} else {
ret = alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME);
if (ret)
goto out1;
gpioled.major = MAJOR(gpioled.devid);
gpioled.minor = MINOR(gpioled.devid);
}
printk("gpioled: major=%d,minor=%d\r\n",gpioled.major, gpioled.minor);
/* 初始化cdev */
gpioled.cdev.owner = THIS_MODULE;
cdev_init(&gpioled.cdev, &gpioled_fops);
/* 添加一个cdev */
ret = cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT);
if (ret)
goto out2;
/* 创建类 */
gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);
if (IS_ERR(gpioled.class)) {
ret = PTR_ERR(gpioled.class);
goto out3;
}
/* 创建设备 */
gpioled.device = device_create(gpioled.class, NULL,
gpioled.devid, NULL, GPIOLED_NAME);
if (IS_ERR(gpioled.device)) {
ret = PTR_ERR(gpioled.device);
goto out4;
}
return 0;
out4:
class_destroy(gpioled.class);
out3:
cdev_del(&gpioled.cdev);
out2:
unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);
out1:
gpio_free(gpioled.led_gpio);
return ret;
}
static void __exit led_exit(void)
{
/* 注销设备 */
device_destroy(gpioled.class, gpioled.devid);
/* 注销类 */
class_destroy(gpioled.class);
/* 删除cdev */
cdev_del(&gpioled.cdev);
/* 注销设备号 */
unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);
/* 释放GPIO */
gpio_free(gpioled.led_gpio);
}
/* 驱动模块入口和出口函数注册 */
module_init(led_init);
module_exit(led_exit);
MODULE_AUTHOR("DengTao <773904075@qq.com>");
MODULE_DESCRIPTION("Alientek ZYNQ GPIO LED Driver");
MODULE_LICENSE("GPL");
第7章 前六章代码总结
过程:"使用老的字符注册函数+手动创建设备节点+物理地址放在驱动代码"
|
|
"使用新的字符驱动函数+使用cdev+主动创建设备节点+物理地址放在驱动代码"
|
|
"使用设备结构体+使用新的字符驱动函数+使用cdev+主动创建设备节点+物理地址放在设备树"
|
|
"除了物理地址放在设备树不同,其他同上,不定义物理地址,使用gpio子系统和相关API函数"
(1)定义设备的结构体,之中包含了需要的私有设备
/* dtsled设备结构体 */
struct gpioled_dev {
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
int led_gpio; /* LED所使用的GPIO编号 */
};
static struct gpioled_dev gpioled; /* led设备 */
(2)实现open,read,write,release函数
/*
* @description : 打开设备
* @param -- inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &gpioled; /* 设置私有数据 */
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t led_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
{
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t led_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *offt)
{
int ret;
char kern_buf1;
ret = copy_from_user(kern_buf, buf, cnt); // 得到应用层传递过来的数据
if(0 > ret) {
printk(KERN_ERR "kernel write failed!\r\n");
return -EFAULT;
}
if (0 == kern_buf0)
gpio_set_value(gpioled.led_gpio, 0); // 如果传递过来的数据是0则关闭led
else if (1 == kern_buf0)
gpio_set_value(gpioled.led_gpio, 1); // 如果传递过来的数据是1则点亮led
return 0;
}
/*
* @description : 关闭/释放设备
* @param -- filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
(3)定义设备操作函数
/* 设备操作函数 */
static struct file_operations gpioled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
(4)驱动模块入口
配置设备树相关:
1.获取led设备节点:gpioled.nd = of_find_node_by_path("/led");
2.读取status属性:ret = of_property_read_string(gpioled.nd, "status", &str);
3.获取compatible属性值并进行匹配:ret = of_property_read_string(gpioled.nd, "compatible", &str);
4.获取设备树中的led-gpio属性,得到LED所使用的GPIO编号:gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0);
------》使用gpio子系统配置gpio
1.向gpio子系统申请使用GPIO:ret = gpio_request(gpioled.led_gpio, "LED-GPIO");
2.将led gpio管脚设置为输出模式:gpio_direction_output(gpioled.led_gpio, 0);
3.初始化LED的默认状态:
ret = of_property_read_string(gpioled.nd, "default-state", &str);
if(!ret) {
if (!strcmp(str, "on"))
gpio_set_value(gpioled.led_gpio, 1);
else
gpio_set_value(gpioled.led_gpio, 0);
} else
gpio_set_value(gpioled.led_gpio, 0);
------》创建设备号
if (gpioled.major) {
gpioled.devid = MKDEV(gpioled.major, 0);
ret = register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);
if (ret)
goto out1;
} else {
ret = alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME);
if (ret)
goto out1;
gpioled.major = MAJOR(gpioled.devid);
gpioled.minor = MINOR(gpioled.devid);
}
------》注册cdev并添加一个cdev
gpioled.cdev.owner = THIS_MODULE;
cdev_init(&gpioled.cdev, &gpioled_fops);
ret = cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT);
if (ret)
goto out2;
------》创建类和设备
gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);
gpioled.device = device_create(gpioled.class, NULL,
gpioled.devid, NULL, GPIOLED_NAME);
------》解决报错
out4:
class_destroy(gpioled.class);
out3:
cdev_del(&gpioled.cdev);
out2:
unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);
out1:
gpio_free(gpioled.led_gpio);