rk3588 驱动开发(三)第五章 新字符设备驱动实验
register_chrdev 和 unregister_chrdev 这两个函数是老版本驱动使用的函数,现在新的字符设备驱动已经不再使用这两个函数,而是使用 Linux 内核推荐的新字符设备驱动 API 函数。本节我们就来学习一下如何编写新字符设备驱动,并且在驱动模块加载的时候自动创建设备节点文件
设备节点文件:
设备节点文件(Device Node 或 Device File),也叫设备文件,是 Linux 系统中用来访问硬件设备的一种特殊文件,通常位于 /dev 目录下。
🧠 一句话解释:
设备节点文件是用户空间访问内核驱动和硬件设备的接口桥梁。
5.1 新字符设备驱动原理
5.1.1分配和释放设备号
使用 register_chrdev 函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会
带来两个问题:
①、需要我们事先确定好哪些主设备号没有使用。
②、会将一个主设备号下的所有次设备号都使用掉,比如现在设置 LED 这个主设备号为
200,那么 0~1048575(2^20-1)这个区间的次设备号就全部都被 LED 一个设备分走了。这样太
浪费次设备号了!一个 LED 设备肯定只能有一个主设备号,一个次设备号。
解决这两个问题最好的方法就是在使用设备号的时候向 Linux 内核申请,需要几个就申请
几个,由 Linux 内核分配设备可以使用的设备号。
如果没有指定设备号的话就使用如下函数来申请设备号:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
参数 from 是要申请的起始设备号,也就是给定的设备号;参数 count 是要申请的数量,一般都是一个;参数 name 是设备名字。
注销字符设备之后要释放掉设备号,不管是通过 alloc_chrdev_region 函数还是
register_chrdev_region 函数申请的设备号,统一使用如下释放函数:
void unregister_chrdev_region(dev_t from, unsigned count)
新字符设备驱动下,设备号分配示例代码如下:
1 int major; /* 主设备号 */
2 int minor; /* 次设备号 */
3 dev_t devid; /* 设备号 */
4
5 if (major) { /* 定义了主设备号 */
6 devid = MKDEV(major, 0); /* 大部分驱动次设备号都选择 0 */
7 register_chrdev_region(devid, 1, "test");
8 } else { /* 没有定义设备号 */
9 alloc_chrdev_region(&devid, 0, 1, "test"); /* 申请设备号 */
10 major = MAJOR(devid); /* 获取分配号的主设备号 */
11 minor = MINOR(devid); /* 获取分配号的次设备号 */
12 }
devid是设备号是由主设备号major 和此设备号minor 合成得到的
这里注意设备名和设备节点文件的区别
概念 | 举例 | 用途 | 关系
设备名 | “rk3588-led” | 设备/驱动内部识别和匹配 | 属于内核驱动的部分
设备节点文件名 | /dev/platled | 用户空间访问设备接口 | 可以根据设备名生成
设备名是内核态区分识别匹配设备/驱动的设备节点文件是用户态和内核态沟通的桥梁
如果要注销设备号的话,使用如下代码即可:
1 unregister_chrdev_region(devid, 1); /* 注销设备号 */
5.1.2 新的字符设备注册方法
1、字符设备结构
在 Linux 中使用 cdev 结构体表示一个字符设备,cdev 结构体在 include/linux/cdev.h 文件中的定义如下
示例代码 5.1-3 cdev 结构体
1 struct cdev {
2 struct kobject kobj;
3 struct module *owner;
4 const struct file_operations *ops;
5 struct list_head list;
6 dev_t dev;
7 unsigned int count;
8 } __randomize_layout;
在 cdev 中有两个重要的成员变量:ops 和 dev,这两个就是字符设备文件操作函数集合file_operations 以及设备号 dev_t。编写字符设备驱动之前需要定义一个 cdev 结构体变量,这个变量就表示一个字符设备,如下所示:
struct cdev test_cdev;
2、cdev_init 函数
定义好 cdev 变量以后就要使用 cdev_init 函数对其进行初始化,cdev_init 函数原型如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合。
使用 cdev_init 函数初始化 cdev 变量的示例代码如下:
示例代码 5.1-4 cdev_init 函数使用示例代码
1 struct cdev testcdev;
2
3 /* 设备操作函数 */
4 static struct file_operations test_fops = {
5 .owner = THIS_MODULE,
6 /* 其他具体的初始项 */
7 };
8
9 testcdev.owner = THIS_MODULE;
10 cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */
3、cdev_add 函数
cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数
完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。
cdev_add 函数原型如下:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参数 count 是要添加的设备数量。完善上面的示例代码,加入 cdev_add 函数,内容如下所示:
示例代码 5.1-5 cdev_add 函数使用示例
1 struct cdev testcdev;
2
3 /* 设备操作函数 */
4 static struct file_operations test_fops = {
5 .owner = THIS_MODULE,
6 /* 其他具体的初始项 */
7 };
8
9 testcdev.owner = THIS_MODULE;
10 cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */
11 cdev_add(&testcdev, devid, 1); /* 添加字符设备 */
示例:
dev_t devid;
alloc_chrdev_region(&devid, 0, 2, "mydev");
这时候 devid 表示的是:主设备号 + 起始次设备号为 0。
cdev_add(&mycdev, devid, 2);
你这里添加了 2 个次设备号,也就是:
/dev/mydev0(主设备号 + 次设备号 0)
/dev/mydev1(主设备号 + 次设备号 1)
3、cdev_del 函数
卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备,cdev_del
函数原型如下:
void cdev_del(struct cdev *p)
5.2 自动创建设备节点
在前面的 Linux 驱动实验中,当我们使用 modprobe 加载驱动程序以后还需要使用命令“mknod”手动创建设备节点。本节就来讲解一下如何实现自动创建设备节点,在驱动中实现自动创建设备节点的功能以后,使用 modprobe 加载驱动模块成功的话就会自动在/dev 目录下创建对应的设备文件。
5.2.1mdev 机制
udev 是一个用户程序,在 Linux 下通过 udev 来实现设备文件的创建与删除,udev 可以检
测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。比如使用
modprobe 命令成功加载驱动模块以后就自动在/dev 目录下创建对应的设备节点文件,使用
rmmod 命令卸载驱动模块以后就删除掉/dev 目录下的设备节点文件。
开发板启动的时候会启动 udev,如下图所示
5.2.2 创建和删除类
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。首先要创建一个 class 类,class 是个结构体,定义在文件include/linux/class.h 里面。class_create 是类创建函数,class_create 是个宏定义,内容如下:
示例代码 5.2-1 class_create 函数
546 extern struct class * __must_check __class_create(struct module
547 *owner,const char *name,
548 struct lock_class_key *key);
549 extern void class_destroy(struct class *cls);
550
551 /* This is a #define to keep the compiler from merging different
552 * instances of the __key variable */
553 #define class_create(owner, name) \
554 ({ \
555 static struct lock_class_key __key; \
556 __class_create(owner, name, &__key); \
557 })
根据上述代码,将宏 class_create 展开以后内容如下:
struct class *class_create (struct module *owner, const char *name)
class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。
返回值是个指向结构体 class 的指针,也就是创建的类。
卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy,函数原型如下:
void class_destroy(struct class *cls);
参数 cls 就是要删除的类
5.2.3 创建设备
上一小节创建好类以后还不能实现自动创建设备节点,我们还需要在这个类下创建一个设
备。使用 device_create 函数在类下面创建设备,device_create 函数原型如下:
struct device *device_create(struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...)
device_create 是个可变参数函数,参数 class 就是设备要到创建哪个类下面;参数 parent
是父设备,一般为 NULL,也就是没有父设备;参数 devt 是设备号;参数 drvdata 是设备可能
会使用的一些数据,一般为 NULL;参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成
/dev/xxx 这个设备文件。返回值就是创建好的设备。
同样的,卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy,函数
原型如下:
void device_destroy(struct class *cls, dev_t devt)
参数 classs 是要删除的设备所处的类,参数 devt 是要删除的设备号。
5.2.4参考示例
在驱动入口函数里面创建类和设备,在驱动出口函数里面删除类和设备,参考示例如下:
示例代码 5.2-2 创建/删除类/设备参考代码
1 struct class *class; /* 类 */
2 struct device *device; /* 设备 */
3 dev_t devid; /* 设备号 */
4
5 /* 驱动入口函数 */
6 static int __init xxx_init(void)
7 {
8 /* 创建类 */
9 class = class_create(THIS_MODULE, "xxx");
10 /* 创建设备 */
11 device = device_create(class, NULL, devid, NULL, "xxx");
12 return 0;
13 }
14
15 /* 驱动出口函数 */
16 static void __exit led_exit(void)
17 {
18 /* 删除设备 */
19 device_destroy(newchrled.class, newchrled.devid);
20 /* 删除类 */
21 class_destroy(newchrled.class);
22 }
23
24 module_init(led_init);
25 module_exit(led_exit);
5.3 设置文件私有数据
每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device)、开关状态
(state)等等,在编写驱动的时候你可以将这些属性全部写成变量的形式,如下所示
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
这样写肯定没有问题,但是这样写不专业!对于一个设备的所有属性信息我们最好将其做
成一个结构体。编写驱动 open 函数的时候将设备结构体作为私有数据添加到设备文件中,如
下所示:
示例代码 5.3-2 设备结构体作为私有数据
/* 设备结构体 */
1 struct test_dev{
2 dev_t devid; /* 设备号 */
3 struct cdev cdev; /* cdev */
4 struct class *class; /* 类 */
5 struct device *device; /* 设备 */
6 int major; /* 主设备号 */
7 int minor; /* 次设备号 */
8 };
9
10 struct test_dev testdev;
11
12 /* open 函数 */
13 static int test_open(struct inode *inode, struct file *filp)
14 {
15 filp->private_data = &testdev; /* 设置私有数据 */
16 return 0;
17 }
在 open 函数里面设置好私有数据以后,在 write、read、close 等函数中直接读取private_data 即可得到设备结构体。
5.5 实验程序编写
5.5.1 LED 灯驱动程序编写
新建名为“03_newchrled”文件夹,创建好以后新建 newchrled.c 文件,在 newchrled.c 里
面输入如下内容:
#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/uaccess.h>
#include <asm/io.h>
#include <linux/cdev.h>
#include <linux/device.h>
#define NEWCHRLED_CNT 1 /* 设备号个数 */
#define NEWCHRLED_NAME "newchrled"
/* 主设备号 设备名称 */
#define LED_MAJOR 200 /* 主设备号 */
#define LED_NAME "led" /* 设备名字 */#define LEDOFF 0 /* 关灯 */
#define LEDON 1 /* 开灯 */
/* PMU_GRF_BASE 外设的基地址,为 0xFD5F8000 */
#define PMU_GRF_BASE (0xFD5F8000)
#define BUS_IOC_BASE (0xFD5F8000)
/* IO功能复用寄存器 */
#define BUS_IOC_GPIO1A_IOMUX_SEL_L (BUS_IOC_BASE + 0x0020)
/* 驱动能力设置寄存器基地址 */
#define VCCIO1_4_BASE (0xFD5F9000)
/* GPIO1_A3 的驱动能力设置寄存器地址 */
#define VCCIO1_4_IOC_GPIO1A_DS_L (VCCIO1_4_BASE + 0x0020)
/* GPIO1组的基地址 */
#define GPIO1_BASE (0xFEC20000)
/* 输出输入模式寄存器 */
#define GPIO_SWPORT_DR_L (GPIO1_BASE + 0X0000)
/* 设置输出高低电平寄存器*/
#define GPIO_SWPORT_DDR_L (GPIO1_BASE + 0X0008)static void __iomem *BUS_IOC_GPIO1A_IOMUX_SEL_L_PI;
static void __iomem *VCCIO1_4_IOC_GPIO1A_DS_L_PI;
static void __iomem *GPIO_SWPORT_DR_L_PI;
/*
static表示这个指针的作用域限制在当前文件中(即文件内全局变量)。void *指针类型为 void,可以转换为任意数据类型,表示一个未定具体类型的地址。__iomem是一个内核宏,表示指针指向 I/O 内存(而非普通的 RAM)。目的是让 sparse(Linux 的静态分析工具)在编译阶段检测可能的非法操作。*/
static void __iomem *GPIO_SWPORT_DDR_L_PI;
struct newchrled_dev /* 定义led设备结构体 */
{/* 在 Linux 内核中,dev_t 是一个用于表示设备号的类型,通常用于存储主设备号和次设备号的组合。
定义dev_t 是一个内核定义的类型,通常是一个 32 位无符号整数,定义在内核头文件中 */dev_t devid; /* 设备号 */struct cdev cdev; /* cdev 结构体 */struct class *class; /* 类 */struct device *device; /* 设备 */int major; /* 主设备号 */int minor; /* 次设备号 *//* data */
};
struct newchrled_dev newchrled; /* led设备 */
/* @description : LED 灯控制函数@param - sta : LEDON(0) -- 打开 LED 灯LEDOFF(1) -- 关闭 LED 灯@return : 无*/
void led_switch(u8 sta)
{u32 val = 0;if(sta == LEDON) {val = readl(GPIO_SWPORT_DR_L_PI);val &= ~(0X8 << 0); /* bit3 清零*/val |= ((0X8 << 16) | (0X8 << 0)); /* bit19 置 1,允许写 bit3 bit3,高电平*/writel(val, GPIO_SWPORT_DR_L_PI); }else if(sta == LEDOFF) {writel(val, GPIO_SWPORT_DR_L_PI);val = readl(GPIO_SWPORT_DR_L_PI);val &= ~(0X8 << 0); /* bit3 清零*/val |= (0X8 << 16); /* bit19 置 1,允许写 bit3,bit3,低电平*/writel(val, GPIO_SWPORT_DR_L_PI);}
}
/*
* @description : 物理地址映射 将物理地址映射到虚拟地址上
* @return : 无
*/
void led_remap(void)
{/* GPIO的复用功能寄存器 */BUS_IOC_GPIO1A_IOMUX_SEL_L_PI = ioremap(BUS_IOC_GPIO1A_IOMUX_SEL_L,4);/* GPIO输出能力设置寄存器 */VCCIO1_4_IOC_GPIO1A_DS_L_PI = ioremap( VCCIO1_4_IOC_GPIO1A_DS_L ,4);/* 输出输入模式寄存器 */ GPIO_SWPORT_DR_L_PI = ioremap(GPIO_SWPORT_DR_L, 4);/* 输出电平寄存器 */ GPIO_SWPORT_DDR_L_PI = ioremap(GPIO_SWPORT_DDR_L, 4);
}/* @description : 取消映射@param : 无@return : 无*/
void led_unmap(void)
{/* 取消映射 */iounmap(BUS_IOC_GPIO1A_IOMUX_SEL_L_PI);iounmap(VCCIO1_4_IOC_GPIO1A_DS_L_PI);iounmap(GPIO_SWPORT_DR_L_PI);iounmap(GPIO_SWPORT_DDR_L_PI);
}
/** @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 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;//返回错误 }ledstat = databuf[0]; /* 获取状态值 */if(ledstat == LEDON) {led_switch(LEDON); /* 打开 LED 灯 */}else if(ledstat == LEDOFF) {led_switch(LEDOFF);}return 0;
}/** @description : 关闭/释放设备* @param - filp : 要关闭的设备文件(文件描述符)* @return : 0 成功;其他 失败*/static int led_release(struct inode *inode, struct file *filp){return 0;}
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)
{int retvalue = 0;u32 val = 0;/* 初始化LED *//* 1、映射寄存器地址 */led_remap();/* 2、设置GPIO1_A3的功能 */val = readl(BUS_IOC_GPIO1A_IOMUX_SEL_L_PI);val &= ~(0XF000 << 0); /* bit15:12,清零 */val |= ((0XF000 << 16) | (0X0 << 0)); /* bit31:28 置 1,bit15:12:0,用作 GPIO1_A3 */writel(val, BUS_IOC_GPIO1A_IOMUX_SEL_L_PI);/* 3、设置 GPIO0_C0 驱动能力为 40ohm */val = readl(VCCIO1_4_IOC_GPIO1A_DS_L_PI);val &= ~(0X7000 << 0); /* bit14:12 清零*/val |= ((0X7000 << 16) | (0X6000 << 0));/* bit30:28 置 1,允许写 bit14:12,bit14:12:110, 用作 GPIO1_A3 */writel(val, VCCIO1_4_IOC_GPIO1A_DS_L_PI);/* 4、设置 GPIO1_A3 为输出 */val = readl(GPIO_SWPORT_DDR_L_PI);val &= ~(0X8 << 0); /* bit3 清零*/val |= ((0X8 << 16) | (0X8 << 0)); /* bit19 置 1,允许写 bit3,bit3,高电平 */writel(val, GPIO_SWPORT_DDR_L_PI);/* 5、设置 GPIO1_A3 为低电平,关闭 LED 灯。*/val = readl(GPIO_SWPORT_DR_L_PI);val &= ~(0X8 << 0); /* bit3 清零*/val |= (0X8 << 16); /* bit19 置 1,允许写 bit3,bit3,低电平 */writel(val, GPIO_SWPORT_DR_L_PI);/* 注册字符设备驱动 *//* 1、创建设备号 */if(newchrled.major) {/* #define MKDEV(major, minor) (((major) << 20) | (minor))MKDEV是 Linux 内核提供的一个宏,用于将主设备号和次设备号组合成一个设备号。*/newchrled.devid = MKDEV(newchrled.major, 0);//将主次设备号转换为dev_t类型/* int register_chrdev_region(dev_t from, unsigned count, const char *name); *//* 向内核注册一个字符设备 devt 1连续注册的设备数量 LED_NAME字符设备名*/retvalue = register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);//静态注册字符设备 if(retvalue < 0) {pr_err("cannot register %s char driver [ret=%d]\n",NEWCHRLED_NAME, NEWCHRLED_CNT);goto fail_map;}} else {/* int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name); *//* dev_t *dev:指向一个 dev_t 类型的变量,用于存储分配到的设备号(主设备号和次设备号的组合)。 0:次设备号开始分配1:分配设备号的数量LED_NAME:设备名*/retvalue = alloc_chrdev_region(&newchrled.devid, 0, 1, LED_NAME);//动态注册字符设备if(retvalue < 0) {pr_err("cannot register %s char driver [ret=%d]\n",NEWCHRLED_NAME, NEWCHRLED_CNT);goto fail_map;}newchrled.major = MAJOR(newchrled.devid);//获取主设备号newchrled.minor = MINOR(newchrled.devid);//获取次设备号}printk("newchrled major=%d,minor=%d\r\n",newchrled.major,newchrled.minor);/* 2、初始化 cdev 结构 */newchrled.cdev.owner = THIS_MODULE;/* cdev_init 是一个内核提供的函数,用于初始化 struct cdev 结构体。它会将字符设备与文件操作函数(struct file_operations)绑定*/cdev_init(&newchrled.cdev, &newchrled_fops);/* 3、向 Linux 内核添加 cdev 结构 */retvalue = cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);if(retvalue < 0) {goto del_unregister;}/* 4、自动创建设备节点 -- 创建类 *//* 设备类 是 Linux 内核中的一个抽象概念,用于将具有相似功能或用途的设备分组管理。设备类的主要作用是为设备提供一个逻辑上的分类,并在 class 文件系统中创建一个对应的目录,方便用户空间查看和管理设备。设备类的作用逻辑分组:将功能相似的设备归为一类。例如:所有 LED 设备可以归为 led 类。所有串口设备可以归为 serial 类。设备信息的组织:在 class 文件系统中为设备创建一个类目录,便于用户查看设备信息。例如:/sys/class/led:存放所有 LED 设备的信息。/sys/class/serial:存放所有串口设备的信息。设备节点的自动创建:配合 device_create 函数,设备类可以在 dev 目录下自动创建设备文件(设备节点),方便用户空间访问设备*/newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);if(IS_ERR(newchrled.class)) {goto del_cdev;}/* 5、创建设备 */newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME);if(IS_ERR(newchrled.device)) {goto destory_class;}return 0;destory_class:class_destroy(newchrled.class); //删除类del_cdev:cdev_del(&newchrled.cdev);//删除cdevdel_unregister:unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT); //注销设备号fail_map:led_unmap();return -EIO;}/* 出口函数实现 */
static void __exit led_exit(void)
{/* 取消映射 */led_unmap();/* 注销字符设备驱动 */cdev_del(&newchrled.cdev);unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);device_destroy(newchrled.class, newchrled.devid);class_destroy(newchrled.class);
}/* 声明入口函数 */
module_init(led_init);
/* 声明出口函数 */
module_exit(led_exit);
/* 开源协议 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("LK");
/* 骗过内核该模块属于设备树 */
MODULE_INFO(intree, "Y");
APP程序沿用上一章的内容
modprobe newchrled //加载驱动
驱动加载成功以后会自动在
/dev 目录下创建设备节点文件/dev/newchrdev