摘要 :零基础,学嵌入式,月薪过万,就关注果果小师弟。前面已经介绍了使用裸机点灯,今天使用驱动开发的方式点亮一个LED灯。看看两者有啥区别不?
一、先看原理图
首先查看原理图,看看我们的板子上的LED等接在哪一个IO口上面。
好了,看原理图我们知道LED灯接在芯片的GPIO1的第三个引脚上面,也就是GPIO1_IO03。
二、IMX6UL的GPIO操作方法
先掌握三个名词
- CCM: Clock Controller Module (时钟控制模块)
- IOMUXC : IOMUX Controller,IO复用控制器
- GPIO: General-purpose input/output,通用的输入输出口
2.1 GPIO模块结构
参考芯片手册《Chapter 26: General Purpose Input/Output (GPIO)》我们知道了IMX6UL一共有有5组GPIO(GPIO1~GPIO5),每组引脚最多有32个,但是可能实际上并没有那么多。
GPIO1有32个引脚:GPIO1_IO0~GPIO1_IO31;
GPIO2有22个引脚:GPIO2_IO0~GPIO2_IO21;
GPIO3有29个引脚:GPIO3_IO0~GPIO3_IO28;
GPIO4有29个引脚:GPIO4_IO0~GPIO4_IO28;
GPIO5有12个引脚:GPIO5_IO0~GPIO5_IO11;
我们知道IM6ULL有很多的引脚IO,但是并不是每一个引脚都能当做GPIO使用,它可以复用为其他模式的,比如作为I2C的时钟线I2C2_SCL等其他的用处。所以要向把某一IO当做GPIO使用需要将其复用,在linux中负责复用功能的寄存器IOMUXC_SW_MUX。还有要打开这个GPIO的时钟,在linux中叫做CCM,跟STM32一样还要设置它的IO口速度、上下拉电阻啊、驱动能力啊、压摆率(就是 IO 电平跳变所需要的时间,比如从0到1需要多少时间,时间越小波形就越陡,说明压摆率越高)啊等这些,在linux中是用IOMUXC_SW_PAD。
因此如果想要使用某一组GPIO,比如GPIO1_IO03。首先要打开GPIO1的时钟,然后将GPIO1_IO03设置为GPIO模式,而不是IIC模式。然后在设置一下GPIO1_IO03这个引脚的模式,速度、上下拉电阻、压摆率等。然后再设置GPIO1_IO03为输出模式。最后我们就可以向GPIO1_IO03的DR寄存器也就是数据寄存器写入0或者1,就可以输出高低电平来控制LED等的亮灭了。
2.2 打开的时钟
根据芯片手册我们可以看到,要想打开GPIO1_IO03的时钟就需要要去配置CCGR1这个寄存器的CG13这个位
而且我还知道了这个寄存器的地址是 20C406CH ,因此我们可以写一个宏定义。
#define CCM_CCGR1_BASE (0X020C406C)
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26);
val |= (3 << 26);
writel(val, IMX6U_CCM_CCGR1);
2.3 IOMUXC引脚复用和模式配置
参考资料:芯片手册《Chapter 32: IOMUX Controller (IOMUXC)》。对于某个/某组引脚,IOMUXC中有2个寄存器用来设置它。
IOMUXC_SW_MUX_CTL_PAD_pad-name
IOMUXC_SW_MUX_CTL_PAD_<PADNAME> :Mux pad xxx,选择某个pad的功能
IOMUXC_SW_MUX_CTL_GRP_<GROUP NAME>:Mux grp xxx,选择某组引脚的功能
某个引脚,或是某组预设的引脚,都有8个可选的模式(alternate (ALT) MUX_MODE),
比如我们要把这个GPIO1_IO03设置为GPIO模式,就要将这个寄存器的bit[0..3]设置为0101,也就是5.
然后也看到这个寄存器的地址位<span>Address: 20E_0000h base + 68h offset = 20E_0068h</span>
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
writel(5, SW_MUX_GPIO1_IO03);
IOMUXC_SW_MUX_CTL_GRP_group-name
IOMUXC_SW_PAD_CTL_PAD_<PAD_NAME>:pad pad xxx,设置某个pad的参数
IOMUXC_SW_PAD_CTL_GRP_<GROUP NAME>:pad grp xxx,设置某组引脚的参数
比如:
2.4 GPIO模块内部
框图如下:
我们暂时只需要关心3个寄存器:
① GPIOx_GDIR:设置引脚方向,每位对应一个引脚,1-output,0-input
② GPIOx_DR:设置输出引脚的电平,每位对应一个引脚,1-高电平,0-低电平
③ GPIOx_PSR:读取引脚的电平,每位对应一个引脚,1-高电平,0-低电平
三、怎么编程
3.1 读GPIO
① 设置CCM_CCGRx寄存器中某位使能对应的GPIO模块,默认是使能的。② 设置IOMUX来选择引脚用于GPIO。③ 设置GPIOx_GDIR中某位为0,把该引脚设置为输入功能。④ 读GPIOx_DR或GPIOx_PSR得到某位的值(读GPIOx_DR返回的是GPIOx_PSR的值)
3.2 写GPIO
① 设置CCM_CCGRx寄存器中某位使能对应的GPIO模块,默认是使能的。② 设置IOMUX来选择引脚用于GPIO。③ 设置GPIOx_GDIR中某位为1,把该引脚设置为输出功能。④ 写GPIOx_DR某位的值。
需要注意的是 ,你可以设置该引脚的loopback功能,这样就可以从GPIOx_PSR中读到引脚的有实电平;你从GPIOx_DR中读回的只是上次设置的值,它并不能反应引脚的真实电平,比如可能因为硬件故障导致该引脚跟地短路了,你通过设置GPIOx_DR让它输出高电平并不会起效果。
有了上面的知识,我们点亮led灯的流程基本就了解了。
四、GPIO寄存器操作方法
原则:不能影响到其他位。
4.1 直接读写
读出、修改对应位、写入
val = data_reg
val = val | (1<<n)
data_reg = val
val = data_reg
val = val & ~(1<<n)
data_reg = val
4.2 set-and-clear protocol
set_reg,clr_reg,data_reg 三个寄存器对应的是同一个物理寄存器
- 要设置 bit n:set_reg = (1<<n);
- 要清除 bit n:clr_reg = (1<<n);
五、编写驱动程序的套路
- 1、确定主设备号,也可以让内核分配。
- 2、定义自己的
<span>file_operations</span>
结构体。
- 3、实现对应的
<span>drv_open/drv_read/drv_write</span>
等函数,填入<span>file_operations</span>
结构体。
- 4、把
<span>file_operations</span>
结构体告诉内核:<span>register_chrdev</span>
。
- 5、谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数。
- 6、有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev。
- 7、其他完善:提供设备信息, 自动创建设备节点 :class_create,device_create。
驱动怎么操作硬件?
- 通过ioremap映射寄存器的物理地址得到虚拟地址,读写虚拟地址。
驱动怎么和APP传输数据?
- 通过
<span>copy_to_user</span>
、<span>copy_from_user</span>
这 2 个函数。
六、地址映射
在编写驱动之前,我们需要先简单了解一下 MMU 这个神器, MMU全称叫做 Memory Manage Unit,也就是内存管理单元 。在老版本的Linux中要求处理器必须有MMU,但是现在Linux内核已经支持无MMU的处理器了。MMU主要完成的功能如下:
- ①、完成虚拟空间到物理空间的映射。
- ②、内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。
我们重点来看一下第①点,也就是虚拟空间到物理空间的映射,也叫做地址映射。首先了解两个地址概念: 虚拟地址(VA,Virtual Address) 、 物理地址(PA,Physcical Address) 。对于 32 位的处理器来说,虚拟地址范围是2^32=4GB,我们的开发板上有512MB的DDR3,这512MB的内存就是物理内存,经过MMU可以将其映射到整个4GB的虚拟空间
内存映射
物理内存只有512MB,虚拟内存有4GB,那么肯定存在多个虚拟地址映射到同一个物理地址上去,虚拟地址范围比物理地址范围大的问题处理器自会处理。
Linux内核启动的时候会初始化MMU,设置好内存映射,设置好以后CPU 访问的都是虚拟地址 。比如 I.MX6ULL的<span>GPIO1_IO03</span>
引脚的复用寄存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03的地址为<span>0X020E0068</span>
。如果没有开启MMU的话直接向0X020E0068这个寄存器地址写入数据就可以配 GPIO1_IO03的复用功能。现在开启了MMU,并且设置了内存映射,因此就不能直接向0X020E0068这个地址写入数据了。我们必须得到 0X020E0068这个物理地址在Linux系统里面对应的虚拟地址,这里就涉及到了 物理内存和虚拟内存之间的转换,需要用到两个函数:ioremap 和 iounmap 。
6.1 ioremap函数
ioremap函 数用于获取指定物理地址空间对应的虚拟地址空间,定义在<span>arch/arm/include/asm/io.h</span>
文件中,定义如下:
#include<asm/io.h>
#define ioremap(cookie,size) __arm_ioremap((cookie), (size),MT_DEVICE)
void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size,unsigned int mtype)
{
return arch_ioremap_caller(phys_addr, size, mtype,__builtin_return_address(0));
}
ioremap 是个宏,有两个参数:cookie 和 size,真正起作用的是函数__arm_ioremap,此函数有三个参数和一个返回值,这些参数和返回值的含义如下:
- phys_addr :要映射给的物理起始地址。
- size :要映射的内存空间大小。
- mtype :ioremap 的类型,可以选择 MT_DEVICE、MT_DEVICE_NONSHARED、MT_DEVICE_CACHED 和 MT_DEVICE_WC,ioremap 函数选择 MT_DEVICE。
- 返回值:__iomem 类型的指针, 指向映射后的虚拟空间首地址 。
假如我们要获取I.MX6ULL的<span>IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03</span>
寄存器对应的虚拟地址,使用如下代码即可:
static void __iomem* SW_MUX_GPIO1_IO03;
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
宏<span>SW_MUX_GPIO1_IO03_BASE</span>
是寄存器物理地址,<span>SW_MUX_GPIO1_IO03</span>
是映射后的虚拟地址。 对于I.MX6ULL 来说一个寄存器是4 字节(32 位)的,因此映射的内存长度为 4 。映射完成以后直接对<span>SW_MUX_GPIO1_IO03</span>
进行读写操作即可。实际上,它是按页(4096 字节)进行映射的,是整页整页地映射的。所以说虽然映射的是4字节,实际上映射的是4096字节。
6.2 iounmap函数
卸载驱动的时候需要使用iounmap函数释放掉ioremap函数所做的映射,iounmap函数原型如下:
void iounmap (volatile void __iomem *addr)
<span>iounmap</span>
只有一个参数<span>addr</span>
,此参数就是要取消映射的虚拟地址空间首地址。假如我们现在要取消掉<span>IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03</span>
寄存器的地址映射,使用如下代码即可:
iounmap(SW_MUX_GPIO1_IO03);
6.3 volatile的使用
① 编译器很聪明,会帮我们做些优化,比如:
int a;
a = 0;
a = 1;
② 有时候编译器会自作聪明,比如:
int *p = ioremap(xxxx, 4);
*p = 0;
*p = 1;
③ 对于上面的情况,为了避免编译器自动优化,需要加上 volatile,告诉它这是容易出错的,别乱优化:
volatile int *p = ioremap(xxxx, 4);
*p = 0;
*p = 1;
七、I/O内存访问函数
这里说的I/O是输入/输出的意思,并不是我们学习单片机的时候讲的GPIO引脚。这里涉及到两个概念: I/O端口和I/O内存 。
当外部寄存器或内存映射到IO空间时,称为I/O端口。当外部寄存器或内存映射到内存空间时,称为I/O内存。
但是对于ARM来说没有 I/O 空间这个概念,因此ARM体系下只有I/O内存(可以直接理解为内存) 。 使用ioremap函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是Linux内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作 。
上面的话是啥意思呢?
我说通俗一点就是:我现在知道了GPIO1_IO03它的时钟寄存器地址是0X020C406C,但是你不能直接操作它
#define CCM_CCGR1_BASE (0X020C406C)
0X020C406C是它 实际存在的也就是物理地址 ,但是呢在Linux内核启动的时候会初始化MMU,设置好 内存映射 ,设置好以后CPU访问的都是虚拟地址,我们就不能操作实际的物理地址了。怎么办呢?不用怕,Linux提供了ioremap内存映射函数,我知道了实际的物理地址,只要通过这个函数我们就自动的获取到了这个物理地址对应的虚拟地址了
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4)
现在我们就得到了0X020C406C对应的虚拟地址IMX6U_CCM_CCGR1 ,但是呢,现在我们还不能直接操作这个虚拟地址。这又为啥呢?因为使用ioremap函数将寄存器的物理地址映射到虚拟地址以后,按说我们就可以直接通过指针访问这些地址,但是 Linux内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作 。好家伙,Linux内核它不建议这样做,它又提供了读写函数对这个虚拟地址进行操作。那么我们用户只能按照它建议的这样做了。比如我想操作这个地址后4个字节的某几个位,就需要下面这样做,先把这个地址对应的内存空间读出来,然后修改,最后再把修改好的数据写入就可以了。
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); /* 清楚以前的设置 */
val |= (3 << 26); /* 设置新值 */
writel(val, IMX6U_CCM_CCGR1);
具体的读操作和写操作函数如下:
1 、读操作函数
读操作函数有如下几个:
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)
readb、readw 和readl这三个函数分别对应 8bit、16bit 和 32bit读操作,参数addr就是要读取写内存地址,返回值就是读取到的数据。
2 、写操作函数
写操作函数有如下几个:
void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)
writeb、writew 和 writel 这三个函数分别对应 8bit、16bit 和 32bit 写操作,参数value是要写入的数值,addr是要写入的地址。
八、程序编写
8.1 编写驱动程序
#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"
#define LEDOFF 0
#define LEDON 1
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)
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);
}
}
static int led_open(struct inode *inode, struct file *filp)
{
return 0;
}
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
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);
} else if(ledstat == LEDOFF) {
led_switch(LEDOFF);
}
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)
{
int retvalue = 0;
u32 val = 0;
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26);
val |= (3 << 26);
writel(val, IMX6U_CCM_CCGR1);
writel(5, SW_MUX_GPIO1_IO03);
writel(0x10B0, SW_PAD_GPIO1_IO03);
val = readl(GPIO1_GDIR);
val &= ~(1 << 3);
val |= (1 << 3);
writel(val, GPIO1_GDIR);
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
if(retvalue < 0){
printk("register chrdev failed!\r\n");
return -EIO;
}
return 0;
}
static void __exit led_exit(void)
{
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
unregister_chrdev(LED_MAJOR, LED_NAME);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zhiguoxin");
有了上面的讲解,代码很简单就不用多说了,就是按照那7步来操作的。
8.2 编写测试程序
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#define LEDOFF 0
#define LEDON 1
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
unsigned char databuf[1];
if(argc != 3){
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename, O_RDWR);
if(fd < 0){
printf("file %s open failed!\r\n", argv[1]);
return -1;
}
databuf[0] = atoi(argv[2]);
retvalue = write(fd, databuf, sizeof(databuf));
if(retvalue < 0){
printf("LED Control Failed!\r\n");
close(fd);
return -1;
}
retvalue = close(fd);
if(retvalue < 0){
printf("file %s close failed!\r\n", argv[1]);
return -1;
}
return 0;
}
测试程序就很简单了,不用多说。
3 8.编写Makefile
KERNELDIR := /home/zhiguoxin/linux/IMX6ULL/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)
obj-m := led.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
$(CROSS_COMPILE)arm-linux-gnueabihf-gcc -o ledApp ledApp.c
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
- 第1行,KERNELDIR表示开发板所使用的Linux内核源码目录,使用绝对路径,大家根据自己的实际情况填写。
- 第2行,CURRENT_PATH表示当前路径,直接通过运行
<span>pwd</span>
命令来获取当前所处路径。
- 第3行,obj-m表示将
<span>led.c</span>
这个文件编译为<span>led.ko</span>
模块。
- 第8行,具体的编译命令,后面的modules表示编译模块,-C表示将当前的工作目录切换到指定目录中,也就是KERNERLDIR目录。M表示模块源码目录,
<span>make modules</span>
命令中加入M=dir以后程序会自动到指定的 dir 目录中读取模块的源码并将其编译为<span>.ko</span>
文件。
- 第9行,使用交叉编译工具链将
<span>ledApp.c</span>
编译成可以在arm板子上运行的<span>ledApp</span>
可执行文件。
Makefile 编写好以后输入<span>make</span>
命令编译驱动模块,编译过程如图所示
九、运行测试
9.1 上传程序到开发板执行
开发板启动后通过NFS挂载Ubuntu目录的方式,将相应的文件拷贝到开发板上。简单来说,就是通过NFS在开发板上通过网络直接访问ubuntu虚拟机上的文件,并且就相当于自己本地的文件一样。
因为我的代码都放在<span>/home/zhiguoxin/myproject/alientek_drv_development_source</span>
这个目录下,所以我们将这个目录作为NFS共享文件夹。
Ubuntu IP为192.168.10.100,一般都是挂载在开发板的mnt目录下,这个目录是专门用来给我们作为临时挂载的目录。
文件系统目录简介
然后使用MobaXterm软件通过SSH访问开发板。
ubuntu ip:192.168.10.100
windows ip:192.168.10.200
开发板ip:192.168.10.50
在开发板上执行以下命令就可以实现挂载了:
mount -t nfs -o nolock,vers=3 192.168.10.100:/home/zhiguoxin/myproject/alientek_drv_development_source /mnt
就将开饭的<span>mnt</span>
目录挂载在ubuntu的<span>/home/zhiguoxin/myproject/alientek_drv_development_source</span>
目录下了。这样我们就可以在Ubuntu下修改文件,然后可以直接在开发板上执行可执行文件了。当然我这里的<span>/home/zhiguoxin/myproject/</span>
和<span>windows</span>
之间是一个共享目录,我也可以直接在<span>windows</span>
上面修改文件,然后ubuntu和开发板直接进行文件同步了。
9.2 加载驱动模块
驱动模块<span>led.ko</span>
和<span>ledApp</span>
可执行文件都已经准备好了,接下来就是运行测试。这里我是用挂载的方式将服务端的项目文件夹挂载到arm板的mnt目录,进入到<span>/mnt/02_led</span>
目录输入如下命令加载<span>led.ko</span>
驱动文件:
insmod led.ko
9.3 创建设备节点文件
驱动加载成功需要在<span>/dev</span>
目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建<span>/dev/led</span>
这个设备节点文件:
mknod /dev/led c 200 0
其中<span>mknod</span>
是创建节点命令,<span>/dev/hello_drv<span> </span></span>
是要创建的节点文件,<span>c</span>
表示这是个字符设备,<span>200</span>
是设备的主设备号,<span>0</span>
是设备的次设备号。创建完成以后就会存在<span>/dev/led</span>
这个文件,可以使用<span>ls /dev/led-l</span>
命令查看。
9.3 led设备操作测试
一切准备就绪。使用<span>ledtest<span> </span></span>
软件操作<span>led</span>
这个设备,看看是否可以正常打开或关闭led。
./ledApp /dev/led 0 关闭LED
./ledApp /dev/led 1 打开LED
9.4 卸载驱动模块
如果不再使用某个设备的话可以将其驱动卸载掉,比如输入如下命令卸载掉<span>hello_drv</span>
这个设备:
rmmod led.ko
卸载以后使用<span>lsmod</span>
命令查看<span>led</span>
这个模块还存不存在:
可以看出,此时系统已经没有任何模块了,<span>led</span>
这个模块也不存在了,说明模块卸载成功。而且系统中也没有了<span>led</span>
这个设备。
至此,<span>led</span>
这个设备的整个驱动就验证完成了,驱动工作正常。以后的字符设备驱动实验基本都可以此为模板进行编写。
本文转自:果果小师弟公众号