目录
一、Linux驱动程序介绍
linux系统驱动程序分为三大类,字符设备驱动,块设备驱动和网络设备驱动。 一个设备可以属于多种设备驱动类型,比如USB WIFI,由于其使用USB 接口,所以属于字符设备,但是其又能上网,所以也属于网络设备驱动。
- 字符设备驱动: 是使用最多的一种,从点灯到IIC, SPI,音频设备等的驱动都是字符设备驱动。
- 块设备驱动: 所谓的块设备驱动就是存储器设备的驱动,比如 EMMC、NAND、 SD 卡和U 盘等存储设备,因为这些存储设备的特点是以存储块为基础,因此叫做块设备。
- 网络设备驱动: 就是网络驱动,不管是有线的还是无线的,都属于网络设备驱动的范畴。
二、字符设备驱动
1、字符设备驱动介绍
字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、 IIC、 SPI、 LCD等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
linux系统中万物皆文件,驱动程序加载后会在/dev目录下生成一个对应的文件,如/dev/led。应用程序就是先用open打开该文件,用write控制led的亮灭,用read读取led的亮灭,用完之后用close关闭该文件。这里需要注意的是,应用程序运行在用户空间,驱动程序运行在内核空间。应用程序必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。一个open函数执行的过程如下,明显地,驱动程序必须也得有个open函数才行,那么怎么写这个open函数?写在哪里?如何编译就是我们接下来要学习地内容。
2、编写驱动程序源码
linux源码中字符设备驱动程序存放在driver/char目录下,我们也可以将我们自己的驱动程序保存在该目录下,如下图所示。有了这个.c文件之后如何去编译呢?在此之前先介绍一个知识点——编译流程和Makefile编写。
3、编译流程和Makefile编写
(1)首先介绍一个编译和链接的问题,编写三个函数放在一个文件夹下面,分别为main.c、add.c、sub.c。
(2)用命令gcc -oapp main.c add.c sub.c 进行编译就会的到一个app的可执行文件,./app就输出结果。
(3)上述生成app的过程可以拆分为编译和链接两个过程。首先只编译不链接:gcc -c main.c -omain.o , 在这个过程中,会编译出目标文件,但是有个问题是:无法知道在main函数中的add和sub两个函数在哪找,就到了第二步——链接,这一步的目的就是把编译出来的.o文件中的空缺的add和sub跳转地址填补进去。
(4)如果该文件夹下面的程序多,那么如果只修改或者添加一个程序,使用上面的命令则会将修改的没修改的再编译一遍,导致效率降低。所以一般大型工程项目就会编写一个Makefile文件,该文件的目的就是编译只修改过的程序,搭建一个依赖和目标的关系, 提高开发效率。
(5)使用命令make进行编译,得到如下结果
4、编译驱动程序
此处有两种方法:修改Makefile和编写一个Makefile
(1)修改Makefile
- 使用vi drivers/char/Kconfig 进入配置文件中
- 找到config MINI2440_HELLO_MODULE进行参考并改写一个FIRST_DRIVER改写,如下图所示。
- 执行make menuconfig,进入到Device Drivers——Character devices,看到有个First driver,选择模式为M,进入Help,拷贝CONFIG_FIRST_DRIVER之后保存退出。
- 使用vi drivers/char/Makefile 进入Makefile,添加一行代码,保存退出。
- 输入make modules 进行编译,通过ls drivers/char/ 可以看到生成了.ko文件。
- 使用 cp drivers/char/first_driver.ko ~/nfs/rootfs 将生成的驱动程序拷贝到根文件系统下。
- 之后在2440中分别使用insmod、lsmod、rmmod加载、查看和卸载驱动程序。
(2)编写一个Makefile
5、完善驱动程序
在first_driver.c中只是编写了init和exit两个函数,但具体功能没有实现。所有驱动程序对应的外设都应该在/dev下面有一个具体的文件,通过open、read、write和close进行数据传递。
(1)字符设备驱动程序注册函数编写
目前我们编写的是一个字符设备驱动程序,字符设备驱动程序将来被linux加载的时候需要注册这个驱动程序。其实无论哪种驱动程序,按照linux的做法在加载时都需要注册。字符设备驱动程序注册函数为:
这个函数总共有三个参数,下面一一说明:
- unsigned int major:主设备号,一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的设备。简单来说, linux需要一个数来管理某个驱动程序和使用这个驱动程序的设备。很明显,这个设备号具有唯一性。我们可以使用cat /proc/devices命令即可查看当前系统中所有已经使用了的设备号。在接下来的程序中,我们可以设置一个静态的主设备号,比如200。设置时一定要注意不能使用已经用了的主设备号。
- *const char name:为驱动程序起一个名字
- const struct file_operations fops :是一个指向file_operations的结构体指针,这个结构体中变量如下图所示,包含打开、关闭、读和写*等等,这个结构体里面的成员绝大多数都是函数的指针。
① open 函数用于打开设备文件
② release 函数用于释放(关闭)设备文件,与应用程序中的 close函数对应
③ read 函数用于读取设备文件
④ write 函数用于向设备文件写入(发送)数据
⑤ poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写
⑥ owner 拥有该结构体的模块的指针,一般设置为THIS_MODULE
(2)完善后的代码
在这段代码中,首先定义了驱动的初始化函数first_driver_init和销毁函数first_driver_exit ,在初始化函数中需要对这个驱动进行注册register_chrdev,在销毁时需要注销unregister_chrdev。在注册函数中需要定义一个结构体,这个结构体中大多数为函数指针,指向要调用的函数,我们编写了first_driver_open、first_driver_close、first_driver_read和first_driver_write来初始化该指针。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
int first_driver_open(struct inode *p_node, struct file *fp)
{
printk("open\n");
return 0;
}
int first_driver_close(struct inode *p_node, struct file *fp)
{
printk("close\n");
return 0;
}
ssize_t first_driver_read(struct file *fp, char __user *user_buffer, size_t n, loff_t *offset)
{
printk("read\n");
return 0;
}
ssize_t first_driver_write(struct file *fp, char __user *user_buffer, size_t n, loff_t *offset)
{
printk("write\n");
return 0;
}
struct file_operations fops =
{
.owner = THIS_MODULE,
.open = first_driver_open,
.release = first_driver_close,
.read = first_driver_read,
.write = first_driver_write
};
static int __init first_driver_init(void)
{
int ret;
printk("init\n");
ret = register_chrdev(200, "first driver", &fops);
if(ret != 0)
{
return ret;
}
return 0;
}
static void __exit first_driver_exit(void)
{
unregister_chrdev(200, "first driver");
printk("exit\n");
}
module_init(first_driver_init);
module_exit(first_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ZY");
(3)字符设备和文件的关联
进行make编译,将生成的驱动程序.ko拷贝到2440中,之后insmod first_driver.c进行驱动安装,利用cat proc/devices 也可以看到注册的字符设备存在,但是发现/dev下面没有相关文件。
此时,需要手动创建一个字符设备和文件关联在一起,使用命令:mknod /dev/first c 200 0,表示创建一个结点,创建到/dev下面first文件,c表示创建的设备为字符设备,200表示主设备号,0表示子设备号。
(4)主函数调用
在nfs/rootfs中创建一个main.c,编写程序实现write调用。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(void)
{
int fd = open("/dev/first", O_RDWR);
if(fd < 0)
{
puts("error");
return -1;
}
while(1)
{
int i = 10;
write(fd, &i, 4);
sleep(1);
}
return 0;
}
之后arm-linux-gcc编译之后,在2440上面运行得到结果。
三、如何在linux中实现函数查看跳转
ctags -R .
(1)ctags 命令简介
ctags是一个用于生成索引文件(tags 文件)的工具。这个索引文件可以帮助文本编辑器(如 Vim)等工具实现快速的代码导航。例如,在一个大型的 C/C++ 或其他编程语言的代码库中,通过索引文件,编辑器可以快速地定位函数、类、变量等各种代码元素的定义位置。
(2) -R选项含义
-R是ctags命令的一个选项,它表示递归(recursive)。在命令ctags -R.中,.代表当前目录。所以ctags -R.的意思是从当前目录开始,递归地对所有子目录和文件进行扫描,生成一个包含所有代码元素(如函数、类等)索引信息的tags文件。
(3) 使用
编写好所有.c或者.h文件后,在当前文件夹输入上述命令。之后在main.c中将光标指在函数名的地方,ctrl+] 即可完成跳转,ctrl+t 即可恢复。