自学习笔记-linux驱动开发

第一期:
linux驱动开发:跟裸机开发不同,使用框架开发不需要直接操作接触器
linux下一切皆文件,包括驱动(/dev/xxx)
linux驱动下支持设备树,这里面包括设备树文件(dts,device tree)里面记录了eg:记录io口,是io几,默认高低电平是什么,输入还是输出等,地址是多少等等。
linux内核会分析dts,知道有那些设备,然后通过匹配运行对应的驱动程序。
linunx驱动编写第一件事就是改写设备树。
linux驱动分类:字符设备驱动,块设备驱动,网络设备驱动。
驱动分类,先判断是否是块设备驱动,网络设备驱动,两者都不是就是字符设备驱动。
字符设备驱动是以不定长度的字元来传送资料,字符设备是一个顺序的数据流设备,对这种设备的读写是按字符进行的,而且这些字符是连续地形成一个数据流(iic,lcd,spi,蜂鸣器等等)
块设备驱动是以固定大小长度来传送和转移资料的,块设备能够随机,不需要按照顺序地访问固定大小的数据片。(flash,emmc,ssd,存储相关的)
网络设备驱动,能联网的及是(usb,wifi等)。
其中设备驱动可以是多类的,eg:usb既是网络驱动又是字符驱动。
第二期
内核空间和用户空间(又称内核态和用户态):
linux操作系统运行在内核空间,应用程序运行在用户空间。
应用程序调用外设(内核资源)有三种方法,调用系统调用(system call),异常处理(中断),陷入(软中断)。
其中调用系统调用,一般使用的是API函数进行间接调用,常用的就是POSIX,API和C库等,每个系统调用都有一个系统调用号(及软中断,触发一个软中断,然后陷入内核空间,然后指定系统调用编号)。
字符设备驱动开发流程,应用函数调用open函数打开一个设备(通过write函数向dev/led写入1,0表示led开闭,如果要关闭则调用close函数)
编写驱动的时候也需要编写驱动对应的open,close用户态(release内核态),write等等。
其中比较重要的一个函数是file_operationstruct(就是把系统调用和驱动程序关联起来的关键数据结构,这个结构的每一个成员都对应着一个系统调用,其中简写xx_fops)
第三期-第一个驱动实验
字符设备驱动框架
其实就是file_operations 的结构体的成员变量的实现。
开发流程:
1.编写驱动程序和测试app程序
2.编写makefile程序生成.ko文件,及模块文件
3.在已经拷贝好的linux系统sd卡中加载驱动模块
4.注册字符设备
5.加载模块(卸载模块)
驱动模块的加载与卸载
linux驱动程序有编译到kernel(内核)里面,但是需要重新烧写linux内核,也可以编译为模块及.ko文件,测试的时候只需要加载模块即可。
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
其中操作系统使用命令进行加载卸载驱动如下:
insmod drv.ko 加载驱动
rmmod drv.ko 卸载驱动
驱动加载完毕以后需要用lsmod查看一下。
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示:register_chrdev, unregister_chrdev,其中register_chrdev 函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:
major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分,主设备号代表一类设备,eg:iic下各个设备,其中设备号为32位,高12位为主设备号,低20位为次设备号。name:设备名字,指向一串字符串。fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。
ps:设备号的数据类型是dev_t 32位格式。
其中kdev.h定义了宏,MAJOR(dev-t)得到主设备号,MINOR(dev-t)得到次设备号,这样就不需要再移位得到,,,
MKDEV(ma,mi)知道主设备号和次设备号得到一个设备号。
设备打开和关闭是最基本的要求,几乎所有的设备都得提供打开和关闭的功能。因此我们需要实现 file_operations 中的 open 和 release(对应用户态的close) 这两个函数。
注册流程:
#define LED_MAJOR 200 /* 主设备号 /
#define LED_NAME “led” /
设备名字 /
/
设备操作函数 ,定义了fops函数及要自己编写实现功能的函数类*/
static struct file_operations led_fops = {
.owner = THIS_MODULE,//这段必写,代表这个是属于这个驱动函数的
.open = led_open,//这个后段是自己定义的函数名字,后面需要自己编写。
.read = led_read,
.write = led_write,
.release = led_release,
};
ret = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);

字符设备驱动模板:
static int chrtest_open(struct inode inode, struct file filp)
{
/
用户实现具体功能 /
return 0;
}

/
将上面两个函数指定为驱动的入口和出口函数 /
module_init(xxx_init);
module_exit(xxx_exit);
MODULE_LICENSE(“GPL”);/
LICENSE采用GPL协议
/
MODULE_AUTHOR(“alientek”);/alientek是作者名字随便定义/
编写驱动的时候注意事项:板子地址/lib/modules/4.14.0-xilinx-v2018.3
1.编译驱动的时候因为需要用到linux内核的源码,需要解压缩linux内核的源码,得到zlmage和.dts。需要使用编译后的zlmage和.dts启动系统。
2.makefile写法:kernel的绝对路径,我的是:/home/zynq/linux/kernel/linux-xlnx-xilinx-v2018.3
obj-m := led.o依据自己的改一遍即可
在当前目录命令行输入make,编译好.ko文件后
在编译app文件,同样输入:arm-linux-gnueabihf-gcc ledApp.c -o ledApp
剩下的按照教程改写即可。
3.如何将写好的驱动程序下载到开发板中呢,采用的方式是scp命令拷贝方法,首先ubuntu必须先开启ssh服务,命令行是sudo apt-get install openssh-server
然后在ubuntu中的驱动文件所在的文件夹中打开命令行(不是在开发板的系统中执行下列命令!!!)
scp chrdevbaseApp chrdevbase.ko root@172.18.170.30:/lib/modules/4.14.0-xilinx
scp命令格式:scp 本地文件 远程用户@远程ip:远程用户文件夹
远程用户即开发板上的系统(其ip可以在系统上输入ifconfig即可)
然后会弹出询问,先输入yes,然后再输入密码root即可
最后到下载的文件夹位置查看是否下载成功(一般要先创建一个文件夹命令行是:cd /lib/modules,再mkdir xxx,其中lib/modules是专门存放模块代码的地方)
加载驱动命令行为:
insmod chrdevbase.ko或者modprobe chrdevbase.ko(ps:如果modprobe挂载的时候提示没有.dep文件,则输入depmod生成即可)
输入lsmod可以查看挂载了多少驱动
cat /proc/devices | grep ‘chr’ //查看当前系统中的所有设备这个命令可以显示主设备号。(cat即连接文档并打印输出)
驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过
操作这个设备节点文件来完成对具体设备的操作。已知200是chrdevbase的主设备号,可以创建设备节点,操作如下
mknod /dev/chrdevbase c 200 0
mknod即make node创建节点,“/dev/chrdevbase”是要创建的节点文件,“c”表示这是个字符设备,“200”是设备的主设备号,“0”是设备的次设备号。创建完成以后就会存在/dev/chrdevbase 这个文件,可以使用“ls /dev/chrdevbase -l”命令查看,如果 chrdevbaseApp 想要读写 chrdevbase 设备,直接对/dev/chrdevbase 进行读写操作即可。相当于/dev/chrdevbase 这个文件是 chrdevbase 设备在用户空间中的实现。
使用app驱动设备命令行如下:
./chrdevbaseApp /dev/chrdevbase 1
在存放.ko和app程序的文件夹位置进行上述命令,./chrdevbaseApp代表使用驱动的文件位置,中间隔一个空格接驱动文件位置,再隔一个空格接输入的参数值。
卸载驱动命令行如下:rmmod chrdevbase.ko
删除文件:rm xxx即可

vim使用学习
Vim可以分为三种模式,分别为:命令行模式(Command mode)插入模式(Insert mode)底行模式(Lastline mode)
其中底行模式跟命令行模式差不多,进入vim直接进入命令行模式,输入a或者i或者o切换到插入模式(文本编辑模式),esc退出插入模式到命令行模式,在命令行模式按住shift+:进入底部模式(其实就是输入:)。
输入模式:
跟正常word没区别,但是不能ctr+c,ctr+v(Ctrl+S 快捷键不是用来完成保存的功能的,而是暂停该终端!所以你一旦在使用终端的时候按下 Ctrl+S 快捷键,那么你的终端肯定不会再有任何反应,如果你按下 Ctrl+S 关闭了当前终端的话可以按下 Ctrl+Q 来重新打开终端)
命令行模式:
一般翻页采用ctr+f向后翻,ctr+b向前翻。
cc 删除整行,并且修改整行内容。
dd 删除改行,不提供修改功能。
底行模式:
输入:进入,也可以在命令行模式输入/进入(英文模式下/)
进入/之后直接在“/”底行模式下我们可以在文本中搜索指定的内容
字符设备驱动编写:
显示输出:printk 相当于 printf 的孪生兄妹,printf 运行在用户态,printk 运行在内核态。在内核中想要向控制台输出或显示一些内容,必须使用 printk 这个函数。

采用需要自己写驱动定义的函数有至少四个,open,read,write,release,然后在操作设备结构体中定义,如下.owner = THIS_MODULE,是必须要写的不能改变。

0 static struct file_operations chrdevbase_fops = {
101 .owner = THIS_MODULE,
102 .open = chrdevbase_open,
103 .read = chrdevbase_read,
104 .write = chrdevbase_write,
105 .release = chrdevbase_release,
106 };

read函数如下(write也差不多)

static char readbuf[100]; // 读缓冲区
22 static char writebuf[100]; // 写缓冲区
23 static char kerneldata[] = {"kernel data!"}
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
47 {
48 int retvalue = 0;
49 
50 /* 向用户空间发送数据 */
51 memcpy(readbuf, kerneldata, sizeof(kerneldata));
52 retvalue = copy_to_user(buf, readbuf, cnt);
53 if(retvalue == 0){
54 printk("kernel senddata ok!\r\n");
55 }else{
56 printk("kernel senddata failed!\r\n");
57 }
58 
59 //printk("chrdevbase read!\r\n");
60 return 0;
61 }

filp为要打开的设备,buf是用户空间的数据缓冲区,cnt是读取的数据的长度。
memcpy是复制函数,及使kerneldata(前面定义的变量)赋值给readbuf,长度就是kerneldatae的长度。
然后同样跟memcpy一样赋值,copy_to_user 函数来完成内核空间的数据到用户空间的复制。copy_to_user 函数原型如下:
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)
参数 to 表示目的,参数 from 表示源,参数 n 表示要复制的数据长度。如果复制成功,返回值为 0,如果复制失败则返回负数。(同理还有一个函数 copy_from_user 将用户空间的数据复制到 writebuf 这个内核空间中)
release函数在应用程序调用 close 关闭设备文件的时候此函数会执行,一般会在此函数里面执行一些释放操作。如果在 open 函数中设置了 filp 的private_data 成员变量指向设备结构体(及私有变量,有点像清理手机的运存),那么在 release 函数最终就要释放掉。
第一步:写完定义函数和fops结构体定义后
第二步:开始注册设备(卸载),包括驱动入口函数 chrdevbase_init( register_chrdev 来注册字符设备)和驱动出口函数 chrdevbase_exit(unregister_chrdev来注销字符设备)。
在通过 module_init 和 module_exit 这两个函数来指定驱动的入口和出口函
数。

3 static int __init chrdevbase_init(void)
114 {
115 int retvalue = 0;
116
117 /* 注册字符设备驱动 */
118 retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
119 if(retvalue < 0){
printk("chrdevbase driver register failed\r\n");
121 }
122 printk("chrdevbase_init()\r\n");
123 return 0;
124 }
static void __exit chrdevbase_exit(void)
132 {
133 /* 注销字符设备驱动 */
134 unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
135 printk("chrdevbase_exit()\r\n");
136 }
module_init(chrdevbase_init);
 module_exit(chrdevbase_exit);

其中register_chrdev返回值小于0即注册失败。
第三步:最后添加 LICENSE 和作者信息。
MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“alientek”);

测试APP程序的编写:
编写测试 APP 就是编写 Linux 应用,需要用到 C 库里面和文件操作有关的一些函数,比如open、read、write 和 close 这四个函数。
类似c一样,一个main函数打天下。
open:
int open(const char *pathname, int flags)
pathname:要打开的设备或者文件名。
flags:文件打开模式,以下三种模式必选其一:
O_RDONLY 只读模式
O_WRONLY 只写模式
O_RDWR 读写模式
返回值:如果文件打开成功的话返回文件的文件描述符
文件描述符理解:(文件描述符是内核为了高效的管理已经被打开的文件所创建的索引,它是一个非负整数,用于指代被打开的文件,所有执行I/O操作的系统调用都是通过文件描述符完成的,所谓的文件描述符是一个低级的正整数。最前面的三个文件描述符(0,1,2)分别与标准输入(stdin),标准输出(stdout)和标准错误(stderr)对应(0、1、2 这三个特殊的文件,类似于前台应用后面的4到。。。类似于后台应用)一个 Linux 进程可以打开成百上千个文件,为了表示和区分已经打开的文件,Linux 会给每个文件分配一个编号(一个 ID),这个编号就是一个整数,被称为文件描述符。
具体理解参考:http://c.biancheng.net/view/3066.html
read:(write同理)
ssize_t read(int fd, void *buf, size_t count)
fd:要读取的文件描述符(file descriptor)
返回值:读取成功的话返回读取到的字节数;如果返回 0 表示读取到了文件末尾;如果返回负值,表示读取失败。
close:
int close(int fd);
返回值为0 表示关闭成功,负值表示关闭失败。

int main(int argc, char *argv[])
30 {
31 int fd, retvalue;
32 char *filename;
char readbuf[100], writebuf[100];
34
35 if(argc != 3){
36 printf("Error Usage!\r\n");
37 return -1;
38 }
39
40 filename = argv[1];
41
42 /* 打开驱动文件 */
43 fd = open(filename, O_RDWR);
44 if(fd < 0){
45 printf("Can't open file %s\r\n", filename);
46 return -1;
47 }

这一段是判断运行测试 APP 的时候输入的参数是不是为 3 个,main 函数的 argc 参数表示参数数量,argv[]保存着具体的参数,如果参数不为 3 个的话就表示测试 APP 用法错误。比如,现在要从 chrdevbase 设备中读取数据,需要输入如下命令:

./chrdevbaseApp /dev/chrdevbase 1

上述命令一共有三个参数“./chrdevbaseApp”、“/dev/chrdevbase”和“1”,这三个参
数分别对应 argv[0]、argv[1]和 argv[2]。第一个参数表示运行 chrdevbaseAPP 这个软件,第二个参数表示测试 APP 要打开/dev/chrdevbase 这个设备。第三个参数就是要执行的操作,1表示从 chrdevbase 中读取数据,2 表示向 chrdevbase 写数据。
如果输入的参数不是三个表示输入格式错误,第二个参数表示测试 APP 要打开/dev/chrdevbase 这个设备,使用open打开,如果返回值小于0则表示打不开。

if(atoi(argv[2]) == 1){ /* 从驱动文件读取数据 */
50 retvalue = read(fd, readbuf, 50);
51 if(retvalue < 0){
52 printf("read file %s failed!\r\n", filename);
53 }else{
54 /* 读取成功,打印出读取成功的数据 */
55 printf("read data:%s\r\n",readbuf);
56 }
57 }
58
59 if(atoi(argv[2]) == 2){
60 /* 向设备驱动写数据 */
61 memcpy(writebuf, usrdata, sizeof(usrdata));
62 retvalue = write(fd, writebuf, 50);
63 if(retvalue < 0){
64 printf("write file %s failed!\r\n", filename);
65 }
66 }
67
68 /* 关闭设备 */
69 retvalue = close(fd);
70 if(retvalue < 0){
71 printf("Can't close file %s\r\n", filename);
72 return -1;
73 }

判断 argv[2]参数的值是 1 还是 2,因为输入命令的时候其参数都是字符串格式
的,因此需要借助 atoi 函数将字符串格式的数字转换为真实的数字。当 argv[2]为 1 的时候表示要从 chrdevbase 设备中读取数据,一共读取 50 字节的数据,读取到的数据保存在 readbuf 中,读取成功以后就在终端上打印出读取到的数据,当 argv[2]为 2 的时候表示要向 chrdevbase 设备写数据。对 chrdevbase 设备操作完成以后就关闭设备。
第四期-led驱动编写
地址映射:
MMU 全称叫做 Memory ManageUnit,也就是内存管理单元,主要实现虚拟空间到物理空间的映射。虚拟地址(VA,Virtual Address仅仅只是编号罢了,32位cpu代表一次性处理32位数据,及编号只能有32位)、物理地址(PA,Physcical Address,类似于单片机写寄存器)。对于 32 位的处理器来说,虚拟地址范围是2^32=4GB,我们的 7010 核心板搭配的是 512MB 的 DDR3(7020 是 1GB),这 512MB 的内存就是物理内存,经过 MMU 可以将其映射到整个 4GB 的虚拟空间(及多个虚拟地址可以映射同一个物理地址)。这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数:ioremap 和 iounmap。
ioremap 函 数 用 于 获 取 指 定 物 理 地 址 空 间 对 应 的 虚 拟 地 址 空 间
ioremap(cookie,size),假如我们要获取 ZYNQ 的 APER_CLK_CTRL 寄存器对应的虚拟地址

#define APER_CLK_CTRL 0xF800012C/*定义物理地址宏*/
static void __iomem *aper_clk_ctrl_addr;/*定义变量*/
aper_clk_ctrl_addr = ioremap(APER_CLK_CTRL, 4);/*返回映射的虚拟地址*/

宏定义 APER_CLK_CTRL 是寄存器物理地址,aper_clk_ctrl_addr 是该物理地址映射后的虚拟地址。对于 ZYNQ 来说一个寄存器是 4 字节(32 位)的,因此映射的内存长度为 4。映射完成以后直接对 aper_clk_ctrl_addr 进行读写操作即可。
iounmap 函数 :卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射。
void iounmap (volatile void __iomem *addr),iounmap 只有一个参数 addr,此参数就是要取消映射的虚拟地址空间首地址。假如我们现在要取消掉 APER_CLK_CTRL 寄存器的地址映射。
得到虚拟地址后进行操作,使用一组操作函数来对映射后的内存进行读写操作。

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 就
是要读取写内存地址,返回值就是读取到的数据。

void writeb(u8 value, volatile void __iomem *addr)
2 void writew(u16 value, volatile void __iomem *addr)
3 void writel(u32 value, volatile void __iomem *addr)

同理有readb、readw 和 readl 这三个函数分别对应 8bit、16bit 和 32bit 读操作,参数 addr 就
是要读取写内存地址,返回值就是读取到的数据。
写寄存器操作之一

val = readl(dirm_addr);
val |= (0x1U << 7);

0X1U中的U代表的是无符号unsigned的意思,先令val赋值为其虚拟地址,然后再向左移七位,再进行或操作,等效于使能val的虚拟地址地七位置高。
自动分配设备号函数:

 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 }

dev-t是设备号数据类型,如果主设备号给了就按照前面的注册,如果没有给设备号,则使用alloc_chrdev_region自动注册设备号,alloc_chrdev_region(&devid, 0, 1, “test”);中的0是指次设备号为0,自动赋值设备号后,会传递值到devid中,所以后面可以获取其主次设备号。
注销依旧使用前面的:unregister_chrdev_region(devid, 1);
字符设备结构:
其中字符设备结构有新的定义,及cdev结构体表示一个字符设备,在 cdev 中有两个重要的成员变量:ops 和 dev,这两个就是字符设备文件操作函数集合file_operations 以及设备号 dev_t。
定义好 cdev 变量以后就要使用 cdev_init 函数对其进行初始化,void cdev_init(struct cdev *cdev, const struct file_operations *fops),参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合。
cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init函数完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。int cdev_add(struct cdev *p, dev_t dev, unsigned count)参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参数 count 是要添加的设备数量。
卸载驱动的时候一定要使用cdev_del函数从Linux内核中删除相应的字符设备void cdev_del(struct cdev *p)参数 p 就是要删除的字符设备。cdev_del 和unregister_chrdev_region 这 两 个 函 数 合 起 来 的 功 能 相 当 于unregister_chrdev 函数。(unregister_chrdev注销并且删除设备号)
自动创造节点
我们使用 mdev 来实现设备节点文件的自动创建与删除,自动创建设备节点的工作是在驱动程序的入口函数(如static int __init led_init(void))中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。
首先先为设备创造其指定的类(不同的设备不同的类,后续方便分别创造节点)

struct class *class_create (struct module *owner, const char *name)
class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。
返回值是个指向结构体 class 的指针,也就是创建的类。
void class_destroy(struct class *cls);
参数 cls 就是要删除的类。
	第二步,创建设备,需要在这个类下创建一个设备。使用 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 *class, dev_t devt)参数 classs 是要删除的设备所处的类,参数 devt 是要删除的设备号。

 struct class *class; /* 类 */
2 struct device *device; /* 设备 */
3 dev_t devid; /* 设备号 */
4 
5 /* 驱动入口函数 */
6 static int __init xxx_init(void)
7 {
/* 7.注册字符设备驱动 */
185 /* 创建设备号 */
186 if (newchrled.major) {
187 newchrled.devid = MKDEV(newchrled.major, 0);
188 ret = register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
189 if (ret)
190 goto out1;
191 } else {
192 ret = alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT,
NEWCHRLED_NAME);
193 if (ret)
194 goto out1;
195
196 newchrled.major = MAJOR(newchrled.devid);
197 newchrled.minor = MINOR(newchrled.devid);
198 }
/* 初始化 cdev */
203 newchrled.cdev.owner = THIS_MODULE;
204 cdev_init(&newchrled.cdev, &newchrled_fops);
205
206 /* 添加一个 cdev */
207 ret = cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);
208 if (ret)
209 goto out2;
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);

自动注册字符设备驱动具体步骤:
1.创建设备号

if (newchrled.major) {
187 newchrled.devid = MKDEV(newchrled.major, 0);
188 ret = register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
189 if (ret)
190 goto out1;
191 } else {
192 ret = alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT,
NEWCHRLED_NAME);
193 if (ret)
194 goto out1;
195
196 newchrled.major = MAJOR(newchrled.devid);
197 newchrled.minor = MINOR(newchrled.devid);
198 }

2.初始化 cdev(编写字符设备驱动之前需要定义一个 cdev 结构体变量)

newchrled.cdev.owner = THIS_MODULE;
 cdev_init(&newchrled.cdev, &newchrled_fops);

3.添加一个 cdev

ret = cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);

4.创建类

newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);

5.创建设备

newchrled.device = device_create(newchrled.class, NULL,
 newchrled.devid, NULL, NEWCHRLED_NAME);

解决没有执行权限的文件:
解决方法是先使用chmod命令对shell脚本赋予权限,再执行

[root]# chmod 777 ./start.sh
[root]# ./start.sh

第五期设备树
在旧版本(大概是 3.x 以前的版本)的 linux 内核当中,ARM 架构的板级硬件设备信息被硬编码在 arch/arm/plat-xxx 和 arch/arm/mach-xxx 目录下的文件当中,例如板子上的platform 设备信息、设备 I/O 资源 resource、板子上的 i2c 设备的描述信息信息i2c_board_info、板子上 spi 设备的描述信息 spi_board_info 以及各种硬件设备的
platform_data 等 , 所 以 就 导致在 Linux 内 核 源 码 中 大 量 的 arch/arm/mach-xxx 和arch/arm/plat-xxx 文件夹,这些文件夹里面的文件就描述了对应平台下的板级硬件设备信息。
引入了设备树之后,驱动代码只负责处理驱动的逻辑,而关于设备的具体信息存放到设备树文件中,这样,如果只是硬件接口信息的变化而没有驱动逻辑的变化,驱动开发者只需要修改设备树文件信息,不需要改写驱动代码。使用设备树之后,许多硬件设备信息可以直接通过它传递给 Linux,而不需要在内核中堆积大量的冗余代码。
在这里插入图片描述
zynq的板级设备树文件就是 arch/arm/boot/dts/system-top.dts,其次 zynq7000.dtsi文件中的内容是zynq-7000系列处理器相同的硬件外设配置信息(PS端的),pl.dtsi的内容是我们在 vivado 当中添加的 pl 端外设对应的配置信息,而 pcw.dtsi 则表示我们在vivado 当中已经使能的 PS 外设,这两个文件都是由 petalinux 根据 hdf 硬件描述自动生成的。
dtc 其实就是 device-tree-compiler,也就是设备树文件.dts 的编译器。将.c 文件编译为.o 文件需要用到 gcc 编译器,那么将.dts 文件编译为相应的二进制文件则需要 dtc 编译器。
设备树结构:设备树用树状结构描述设备信息,组成设备树的基本单元是 node(设备节点),这些 node被组织成树状结构,有如下一些特征:
➢ 一个 device tree 文件中只有一个 root node(根节点);
➢ 除了 root node,每个 node 都只有一个 parent node(父节点);
➢ 一般来说,开发板上的每一个设备都能够对应到设备树中的一个 node;
➢ 每个 node 中包含了若干的 property-value(键-值对,当然也可以没有 value)来描述该 node 的一些特性;
➢ 每个 node 都有自己的 node name(节点名字);
➢ node 之间可以是平行关系,也可以嵌套成父子关系,这样就可以很方便的描述设备间的关系;

 1/{ // 根节点
 2 node1{ // node1 节点
 3 property1=value1; // node1 节点的属性 property1
 4 property2=value2; // node1 节点的属性 property2
 5 ...
 6 }; 
 7 
 8 node2{ // node2 节点
 9 property3=value3; // node2 节点的属性 property3
10 ...
11 node3{ // node2 的子节点 node3
12 property4=value4; // node3 节点的属性 property4
13 ...
14 }; 
15 }; 
16 };
	第 1 行当中的’/’就表示设备树的 root node(根节点),所以可知 node1 节点和 node2

节点的父节点都是 root node,而 node3 节点的父节点则是 node2。
node命名格式:

[label:]node-name[@unit-address] {
[properties definitions]
[child nodes]};
	其中引入 label 的目的就是为了方便访问节点,可以直接通过&label 来访问这个节点

clocks = <&clkc 3>//引用节点形式
compatible 属性也叫做“兼容性”属性,这是非常重要的一个属性!compatible 属性的值可以是一个字符串,也可以是一个字符串列表;一般该字符串使用”<制造商>,<型号>”这样的形式进行命名,当然这不是必须要这样,这是要求大家按照这样的形式进行命名,目的是为了指定一个确切的设备,并且包括制造商的名字,以避免命名空间冲突,我们的compatible 的内容为“xlnx,zynq-7000”。
model 属性值也是一个字符串描述信息,它指定制造商的设备型号,model 属性一般定义在根节点下,一般就是对板子的描述信息,没啥实质性的作用,内核在解析设备树的时候会把这个属性对应的字符串信息打印出来。板子的model 的内容是“Alientek ZYNQ Development Board”。
status 属性看名字就知道是和设备状态有关的,device tree 中的 status 标识了设备的状态,使用 status 可以去禁止设备或者启用设备,看下设备树规范中的 status 可选值:在这里插入图片描述
注意如果节点中没有添加 status 属性,那么它默认就是“status = okay”。
#address-cells 和#size-cells 这两个属性可以用在任何拥有子节点的设备节点中,用于描述子节点的地址信息。(这两个属性的值都是无符号 32 位整形)
➢ #address-cells,用来描述子节点"reg"属性的地址表中首地址 cell 的数量;
➢ #size-cells,用来描述子节点"reg"属性的地址表中地址长度 cell 的数量。
eg:

#address-cells = <1>;
45 #size-cells = <1>;
46 spi-max-frequency = <50000000>;
47 partition@0x00000000 {
48 label = "boot";
49 reg = <0x00000000 0x00500000>;
50 ;

设置 flash0:flash@0 节点#address-cells = <1>,#size-cells = <1>,说明 flash0:flash@0 的子节点起始地址长度所占用的字长为 1,地址长度所占用的字长也为 1。flash0:flash@0 的子节点 partition@0x00000000 的 reg 属性值为 reg = <0x00000000 0x00500000>,因为父节点设置了#address-cells = <1>,#size-cells = <1>,所以 address使用一个 32bit 数据来表示,也就是 address=0x00000000,而 length 也使用一个 32bit 数据来表示,也就是 length=0x00500000,相当于设置了起始地址为 0x00000000,地址长度为0x00500000。
ranges 是地址转换表,其中的每个项目是一个子地址、父地址以及在子地址空间的大小的映射。ranges 属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字矩阵。如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换。

soc {
2 compatible = "simple-bus";
3 #address-cells = <1>;
4 #size-cells = <1>;
5 ranges = <0x0 0xe0000000 0x00100000>;
6 
7 serial {
8 device_type = "serial";
9 compatible = "ns16550";
10 reg = <0x4600 0x100>;
11 clock-frequency = <0>;
12 interrupts = <0xA 0x8>;
13 interrupt-parent = <&ipic>;
14 };
15 };

节点 soc 定义的 ranges 属性,值为<0x0 0xe0000000 0x00100000>,此属性值指定了一个 1024KB(0x00100000)的地址范围,子地址空间的物理起始地址为 0x0,父地址空间的物理起始地址为 0xe0000000。
第 10 行,serial 是串口设备节点,reg 属性定义了 serial 设备寄存器的起始地址为0x4600,寄存器长度为 0x100。经过地址转换,serial 设备可以从 0xe0004600 开始进行读写操作,0xe0004600=0x4600+0xe0000000。
设备树效验过程
在没有使用设备树以前的方法:
uboot 会向 Linux 内核传递一个叫做 machine id 的值,machine id 可以认为就是一个机器 ID 编码,告诉 Linux 内核自己是个什么硬件平台,看看 Linux 内核是否支持。
Linux 内核用MACHINE_START 和 MACHINE_END 来定义一个 machine_desc 结构体来描述这个硬件平台。其中该结构体会包含一个:.nr = MACH_TYPE_MX35_3DS,就是“Freescale MX35PDK”这个板子的 machine id。然后mach-types.h 中,此文件定义了大量的 machine id,将 machine id 与的这些 MACH_TYPE_XXX 宏进行对比,看看有没有相等的,如果相等的话就表示 Linux 内核支持这个硬件平台,如果不支持的话就没法启动 Linux 内核。
使用设备树以后的设备匹配方法:
当 Linux 内 核 引 入 设 备 树 以 后 就 不 再 使 用 MACHINE_START 了 , 而 是 换 为 了DT_MACHINE_START。DT_MACHINE_START 和 MACHINE_START 基本相同,只是.nr 的设置不同,DT_MACHINE_START 里面直接将.nr 设置为~0。说明引入设备树以后不会再根据 machine id 来检查 Linux 内核是否支持某个硬件平台了。

static const char * const zynq_dt_match[] = {
192 "xlnx,zynq-7000",
193 NULL
194 };
DT_MACHINE_START(XILINX_EP107, "Xilinx Zynq Platform")
.dt_compat = zynq_dt_match,
...
 MACHINE_END
	1.machine_desc 结构体中有个.dt_compat 成员变量,此成员变量保存着本硬件平台的兼容属性。
	2.zynq_dt_match 数组的定义在第 191~194 行中,可以看到它匹配的字符串是“xlnx,zynq-7000”。
	3.只要某个板子的设备树根节点“/”的 compatible 属性值与 zynq_dt_match 表中的任何一个值相等,那么就表示 Linux内核支持这个开发板、支持这个硬件平台。

Linux 内核是如何根据设备树根节点的 compatible 属性来匹配出对应的 machine_desc,Linux 内核调用 start_kernel 函数来启动内核,start_kernel 函数会调 用 setup_arch 函数来匹配 machine_des。在这里插入图片描述
向节点追加或修改内容
直接在 zynq-7000.dtsi 文件添加子节点即可,然后需要在 system-top.dts 文件中完成数据追加的内容。参考p670(文档)。
特殊节点
aliases 节点 :单词 aliases 的意思是“别名”,因此 aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。
chosen 节点:一般会有两个属性,“bootargs”和“stdout-path”。

chosen {
16 bootargs = "console=ttyPS0,115200 earlyprintk root=/dev/mmcblk0p2 rw rootwait";
17 stdout-path = "serial0:115200n8";
18 };

serial0 其实是一个别名,指向的就是 uart0;“15200”则表示串口的波特率为 115200,“n”表示无校验位,“8”则表示有 8 位数据位

memory 节点:描述了系统内存的基地址以及系统内存大小,“reg = <0x0 0x20000000>”就表示系统内存的起始地址为 0x0,大小为 0x20000000,也就是 512MB.
设备树下led驱动实验
1.修改设备树文件,添加led子节点。
在memory节点后面加入

led {
34 compatible = "alientek,led";
35 status = "okay";
36 default-state = "on";
37
38 reg = <0xE000A040 0x4/data地址
39 0xE000A204 0x4/dirm地址
40 0xE000A208 0x4/outen地址
41 0xE000A214 0x4/intdis地址
42 0xF800012C 0x4/aper_clk_ctrl地址  长度为4
43 >;
44 };

2.修改驱动代码
首先在dtsled设备结构体中加入节点struct device_node nd; / 设备节点 */如果我们要读取设备树某个节点的属性值,首先要先得到这个节点,一般在设备结构体中添加 device_node 指针变量来存放这个节点。
其次跟前面的映射不同的是这里采用不同的映射方式:

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);}

通过使用 of_iomap 函数替换之前使用 ioremap 函数来实现物理地址到虚拟地址的映射,它能够直接解析给定节点的 reg 属性,并将 reg 属性中存放的物理地址和长度进行映射,使用不同的下标依次对 reg 数组中记录的不同组“物理地址-长度”地址空间进行映射。
3.验证led设备节点属性是否正确:
通过 of_find_node_by_path 函数获取设备树根节点下的 led 节点,这里我们用的是绝对路径“/led”,因为 led 节点就在根节点“/”下;

dtsled.nd = of_find_node_by_path("/led");
ret = of_property_read_string(dtsled.nd, "status", &str);
	if(!ret) {
		if (strcmp(str, "okay"))
			return -EINVAL;
	}/* 2、获取compatible属性值并进行匹配 */
	ret = of_property_read_string(dtsled.nd, "compatible", &str);
	if(0 > ret)
		return -EINVAL;
	if (strcmp(str, "alientek,led"))/相同返回0
		return -EINVAL;
	printk(KERN_ERR "led device matching successful!\r\n");
	/* 4.寄存器地址映射 */
	led_ioremap();
ret = of_property_read_string(dtsled.nd, "default-state", &str);/设置led的初始状态
	if(!ret) {
		if (!strcmp(str, "on"))
			val |= (0x1U << 7);
		else
			val &= ~(0x1U << 7);
	} else
		val &= ~(0x1U << 7);
	writel(val, data_addr);

第六期GPIO子系统
gpio 子系统是 linux 内核当中用于管理 GPIO 资源的一套系统,它提供了很多 GPIO 相关的 API 接口。驱动程序中使用 GPIO 之前需要向 gpio 子系统申请,申请成功之后才可以使用。

led {
 2 compatible = "alientek,led";
 3 status = "okay";
 4 default-state = "on";
 5 led-gpio = <&gpio0 7 GPIO_ACTIVE_HIGH>;
 6 };
 7
 8 beeper {
 9 compatible = "alientek,beeper";
10 status = "okay";
11 default-state = "off";
12 beeper-gpio = <&gpio0 60 GPIO_ACTIVE_HIGH>;
13 };
	#gpio-cells的值等于 2,表示一共有两个 cell,大家可以这样理解,使用 gpio0 的时候,需要传递 2 个参数过去,第一个参数为 GPIO 编号,比如“&gpio0 7”就表示 GPIO0_IO07。第二个参数表示 GPIO极性,如果为 0(GPIO_ACTIVE_HIGH)的话表示高电平有效,如果为 1(GPIO_ACTIVE_LOW)的话表示低电平有效。
	led-gpio = <&gpio0 7 GPIO_ACTIVE_HIGH>;就表示使用了GPIO0_IO07 这个管脚,并且是高电平有效。
	几个gpio的api函数:
gpio_request 函数用于申请一个 GPIO 管脚,返回值为0表示申请成功。int gpio_request(unsigned gpio, const char *label),label为自己命名gpio。
如果不使用某个 GPIO 了,那么就可以调用 gpio_free 函数进行释放。void gpio_free(unsigned gpio)
此函数用于设置某个 GPIO 为输入,函数原型如下所示:int gpio_direction_input(unsigned gpio)
此函数用于设置某个 GPIO 为输出,并且设置默认输出值,函数原型如下:int gpio_direction_output(unsigned gpio, int value)
	此函数用于获取某个 GPIO 的值(0 或 1),此函数是个宏,定义所示:#define gpio_get_value __gpio_get_value                          int __gpio_get_value(unsigned gpio)
	此函数用于设置某个 GPIO 的值,此函数是个宏,定义如下#define gpio_set_value __gpio_set_value                          void __gpio_set_value(unsigned gpio, int value)
	int of_gpio_named_count(struct device_node *np, const char *propname)

np:设备节点。propname:要统计的 GPIO 属性。返回值:正值,统计到的 GPIO 数量;负值,失败。
int of_get_named_gpio(struct device_node *np, const char *propname, int index)
函数参数和返回值含义如下:np:设备节点。propname:包含要获取 GPIO 信息的属性名。index:GPIO 索引,因为一个属性里面可能包含多个 GPIO,此参数指定要获取哪个 GPIO的编号,如果只有一个 GPIO 信息的话此参数为 0。返回值:正值,获取到的 GPIO 编号;负值,失败。
pinctrl 子系统又是做什么的呢?pinctrl 其实就是 PIN control 的一个缩写形式。对于大多数的 32 位 SOC 而言,PIN 都是需要设置复用功能和电气特性(IO 速率、上下拉)zynq不需要这块fsbl已经完成了这部分。

gpio 子系统下的 LED 驱动实验
1.修改设备树文件,跟前面设备树led实验不同(前者类似裸机开发,操作寄存器reg等,这里需要更改为)

 #define GPIO_ACTIVE_HIGH 0
 8 #define GPIO_ACTIVE_LOW 1
led {
36 compatible = "alientek,led";
37 status = "okay";
38 default-state = "on";
39 led-gpio = <&gpio0 7 GPIO_ACTIVE_HIGH>;
40 };

这里的&gpio0 7代表MIO7及gipo中的一些编号,这些编号都是已经定义好了的,直接使用即可,比如beeper蜂鸣器是emio60及&gpio0 60等等。
这些都写在 zynq-7000.dtsi

gpio0: gpio@e000a000 {
111 compatible = "xlnx,zynq-gpio-1.0";
112 #gpio-cells = <2>;
113 clocks = <&clkc 42>;
114 gpio-controller;
115 interrupt-controller;
116 #interrupt-cells = <2>;
117 interrupt-parent = <&intc>;
118 interrupts = <0 20 4>;
119 reg = <0xe000a000 0x1000>;
120 };

在这里插入图片描述

2.用api中的一些函数代替前面使用的一些直接对寄存器操作的过程。
eg:使用 gpio_set_value 函数替代第二十五章直接操作 GPIO 寄存器的做法,
使用 of_get_named_gpio 函数获取设备树中 led 节点指定的gpio 管脚,得到一个 GPIO 编号,那么这个编号是 linux 内核 GPIO 子系统对 gpio 的编号,一个编号就对应一个 gpio,那么我们这里得到的这个编号就对应设备树中 led-gpio 指的那个(gpio0_7—MIO7)。得到 GPIO 编号之后使用 gpio_is_valid 函数判断编号是否是有效编号。
在这里插入图片描述
应用层和内核层,硬件层之间的地址有隔离没办法直接访问,应用层到内核层需要用copy_to_user,内核层到硬件层需要用ioremap进行虚拟映射(或者直接使用GPIO接口)。内核模块三要素,.ko,模块初始化,免费开源声明GAL
内核层提高机制能力(基本功能),应用层提供策略。
设备树上面对应的就是物理地址:cpu1: cpu@1 {//物理地址1 39 compatible = "arm,cortex-a9"; 40 device_type = "cpu"; 41 reg = <1>; 42 clocks = <&clkc 3>; 43 };
后面可以包括偏移地址和其节点,但是需要通过datasheet查找基地址。
一、地址映射、地址取消映射
ioremap():地址映射,将返回值赋值给定义好的地址指针,以后操作该指针指向的位置就是操作该地址。
iounmap():地址取消映射
示例
二、IO内存操作函数
2.1 读操作函数
readb(),readw(),readl(),分别对应8/16/32位机器。参数 addr 就是要读取写内存地址,返回值就是读取到的数据。u8 readb(const volatile void __iomem *addr)
2.2 写操作函数
writeb(),writew(),writel(),void writeb(u8 value, volatile void __iomem addr)
三、设备号、主设备号、设备号操作宏
MKDEV(xxx.major, 0)
MAJOR(xxx.devid)
MINOR(xxx.devid)
四、【旧字符设备驱动框架】常用函数总结
4.1 注册、注销设备驱动
register_chrdev():注册设备驱动
unregister_chrdev():注销设备驱动
五、【新字符设备驱动框架】
5.1 申请、注册、注销设备号
alloc_chrdev_region()
register_chrdev_region()
unregister_chrdev_region()
5.2 初始化/删除设备结构体
cdev_init()
cdev_add()
cdev_del()
5.3 创建类、删除类
class_create():创建类,xxx.class = class_create();自动创建设备节点代码一般在cdev_add()函数之后完成,创建之前首先要创建一个class类。
class_destroy():删除类
5.4 创建设备、摧毁设备
device_create():创建设备
device_destroy():摧毁设备
六、短延时函数
6.1 休眠函数(应用层函数):
sleep(unsigned int seconds)
usleep(useconds_t usec)
6.2 延时函数(内核层函数):
mdelay(unsigned long msecs)
udelay(unsigned long usecs)
ndelay(unsigned long nsecs)
七、GPIO常用函数
gpio_direction_output(unsigned gpio, int value)
gpio_set_value(unsigned gpio, int value)
gpio_direction_input(unsigned gpio)
gpio_get_value(unsigned gpio)
gpio_request(unsigned gpio, const char
label)
gpio_free(unsigned gpio)
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在Linux中一切皆是文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx”的文件进行相应的操作即可实现对硬件的操作。如现在有个叫做/dev/led 的驱动文件,此文件是 led 灯的驱动文件。应用程序使用 open 函数来打开文件/dev/led,使用完成以后使用 close 函数关闭/dev/led 这个文件。open和 close 就是打开和关闭 led 驱动的函数,如果要点亮或关闭 led,那么就使用 write 函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开 led 的控制参数。如果要获取led 灯的状态,就用 read 函数从驱动中读取相应的状态。
应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。
open、close、write 和 read 等这些函数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分。
在这里插入图片描述

1588 struct file_operations {
1589 struct module *owner;
1590 loff_t (*llseek) (struct file *, loff_t, int);
1591 ssize_t (*read) (struct file *, char __user *, size_t, loff_t 
	  *);
1592 ssize_t (*write) (struct file *, const char __user *, size_t,
	  loff_t *);
1593 ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
1594 ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
1595 int (*iterate) (struct file *, struct dir_context *);
1596 unsigned int (*poll) (struct file *, struct poll_table_struct 
	  *);
1597 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned
	  long);
1598 long (*compat_ioctl) (struct file *, unsigned int, unsigned
	  long);
1599 int (*mmap) (struct file *, struct vm_area_struct *);
1600 int (*mremap)(struct file *, struct vm_area_struct *);
1601 int (*open) (struct inode *, struct file *);
1602 int (*flush) (struct file *, fl_owner_t id);
1603 int (*release) (struct inode *, struct file *);
1604 int (*fsync) (struct file *, loff_t, loff_t, int datasync);
1605 int (*aio_fsync) (struct kiocb *, int datasync);
1606 int (*fasync) (int, struct file *, int);
1607 int (*lock) (struct file *, int, struct file_lock *);
1608 ssize_t (*sendpage) (struct file *, struct page *, int, size_t,
	  loff_t *, int);
1609 unsigned long (*get_unmapped_area)(struct file *, unsigned long,
	  unsigned long, unsigned long, unsigned long);
1610 int (*check_flags)(int);
1611 int (*flock) (struct file *, int, struct file_lock *);
1612 ssize_t (*splice_write)(struct pipe_inode_info *, struct file *,
	  loff_t *, size_t, unsigned int);
1613 ssize_t (*splice_read)(struct file *, loff_t *, struct
	 pipe_inode_info *, size_t, unsigned int);
1614 int (*setlease)(struct file *, long, struct file_lock **, void
	 **);
1615 long (*fallocate)(struct file *file, int mode, loff_t offset,
1616 loff_t len);
1617 void (*show_fdinfo)(struct seq_file *m, struct file *f);
1618 #ifndef CONFIG_MMU
1619 unsigned (*mmap_capabilities)(struct file *);
1620 #endif
1621 };

第 1589 行,owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。
第 1590 行,llseek 函数用于修改文件当前的读写位置。
第 1591 行,read 函数用于读取设备文件。
第 1592 行,write 函数用于向设备文件写入(发送)数据。
第 1596 行,poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
第 1597 行,unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
第 1598 行,compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。
第 1599 行,mmap 函数用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
第 1601 行,open 函数用于打开设备文件。
第 1603 行,release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。
第 1604 行,fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
第 1605 行,aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的
数据。
字符驱动开发中常用的就是:open、release、write、read函数。
Linux驱动的两种运行方式:
①、将驱动编译进Linux内核中,当内核启动时就会自动运行驱动程序;
②、将驱动编译成模块(Linux下模块扩展名位.ko),在Linux内核启动以后使用“insmod”命令加载驱动模块。
在调试阶段一般将驱动编译成模块,有利于驱动代码的更新,不需要编译整个Linux代码,调试时只需要加载或者卸载驱动代码,不需要重启整个系统;当确定驱动功能完备后再编译进内核中。
其中驱动编译完成以后扩展名为.ko;有两种命令可以加载驱动模块:insmod和 modprobe;insmod命令的缺点:insmod 命令不能解决模块的依赖关系,比如 drv.ko 依赖 first.ko 这个模块,就必须先使用insmod 命令加载 first.ko 这个模块,然后再加载 drv.ko 这个模块。而modprobe 就不会存在insmod的问题,modprobe 会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中,因此modprobe 命令相比 insmod 要智能一些。
Platform总线
在这里插入图片描述
从图中我们可以很清楚的看出Linux platform总线设备驱动模型的整体架构。在总线设备驱动模型中,需关心总线、设备和驱动这3个实体,总线将设备和驱动绑定。当向内核注册驱动程序时,要调用platform_driver_register函数将驱动程序注册到总线,并将其放入所属总线的drv链表中,注册驱动的时候还会调用所属总线的match函数寻找该总线上与之匹配的设备,如果找到与之匹配的设备则会调用相应的probe函数将相应的设备和驱动进行绑定;同样的当向内核注册设备时,要调platform_device_register函数将设备注册到总线,并将其放入所属总线的dev链表中,注册设备的时候同样也会调用所属总线的match函数寻找该总线上与之匹配的驱动程序,如果找到与之匹配的驱动程序时会调用相应的probe函数将相应的设备和驱动进行绑定;这一匹配过程是由总线自动完成的。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值