一、驱动认知
1.画了一张图片:
由上至下
用户空间:它分为app和c库,我们之前写的很多关于app的开发和ftp项目都是在app也就是上层做的,然后通过调用c库提供的api支配内核干活的接口,比如open ,read, write, fork, pthread, socket, 由此处c库封装实现,不需要管内核当中发生了什么事情。
那么在应用层调用驱动就需要用到wiringPi库,但某些平台厂家不一定提供wiringPi库,它不像c库,c库是Linux的标准库,只要能运行Linux就有c库。所以这时我们要自己实现wiringPi库。
内核空间:Linux一切皆文件,用户空间调用open会产生一个软终端,终端号是0x80,代表发生了一个系统调用,来到系统调用(sys_call),然后调用虚拟文件系统的sys_open,然后来到内核空间找到被调用的驱动文件,驱动文件存在于驱动链表中,所以是通过驱动的设备名和设备号,然后通过设备驱动函数(操作寄存器来驱动I/O口)
添加驱动:
1)设备名
2)设备号:主设备号和此设备号
3)设备驱动函数(操作寄存器来驱动I/O口)
2.主设备号和次设备号
Linux的设备管理是和文件系统紧密结合的,各种设备都以文件的形式存放在/dev目录下,称为设备文件。应用程序可以打开、关闭和读写这些设备文件,完成对设备的操作,就像操作普通的数据文件一样。为了管理这些设备,系统为设备编了号,每个设备号又分为主设备号和次设备号。主设备号用来区分不同种类的设备(类似于某设备的品牌,比如华为是手机品牌吧),而次设备号用来区分同一类型的多个设备(比如华为品牌的mate 50手机)。对于常用设备,Linux有约定俗成的编号,如硬盘的主设备号是3。
一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各设备。例如一个嵌入式系统,有两个LED指示灯,LED灯需要独立的打开或者关闭。那么,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1和2。这里,次设备号就分别表示两个LED灯。
让我们来查看下/dev目录下的设备文件:
红包框内,第一个数是主设备号,第二个是次设备号。
二、编写驱动代码的基本框架
1)手动生成设备
sudo mknod freepapa c 8 1
2)如果在上层写了一个驱动程序,通过设备名打开了某个设备,如果没有内核中没有这个驱动的话,执行一定会报错。所以要在内核中添加这个驱动程序,并且生成了我们要调用的驱动设备名
意思就是:当上层用户空间用户调用open打开一个设备时,会通过open的设备名来触发软中断(中断号0x80),系统调用(sys_call)来调用虚拟文件系统(VFS)的(sys_open)通过主设备号在内核驱动链表中找到驱动并运行驱动内的程序。
下面是设备驱动的基本代码框架:
#include <linux/fs.h> //file_operations声明
#include <linux/module.h> //module_init module_exit声明
#include <linux/init.h> //__init __exit 宏定义声明
#include <linux/device.h> //class devise声明
#include <linux/uaccess.h> //copy_from_user 的头文件
#include <linux/types.h> //设备号 dev_t 类型声明
#include <asm/io.h> //ioremap iounmap的头文件
static struct class *pin4_class;
static struct device *pin4_class_dev;
static dev_t devno; //设备号
static int major =231; //主设备号
static int minor =0; //次设备号
static char *module_name="pin4"; //模块名
//led_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
printk("pin4_open\n"); //内核的打印函数和printf类似
return 0;
}
//led_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
printk("pin4_write\n");
return 0;
}
static struct file_operations pin4_fops = {
.owner = THIS_MODULE,
.open = pin4_open,
.write = pin4_write,
};
int __init pin4_drv_init(void)
{
int ret;
devno = MKDEV(major,minor); //创建设备号
ret = register_chrdev(major, module_name,&pin4_fops); //注册驱动 告诉内核,把这个驱动加入到内核驱动的链表中
pin4_class=class_create(THIS_MODULE,"myfirstdemo");
pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name); //创建设备文件
return 0;
}
void __exit pin4_drv_exit(void)
{
device_destroy(pin4_class,devno);
class_destroy(pin4_class);
unregister_chrdev(major, module_name); //卸载驱动
}
module_init(pin4_drv_init); //入口
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");
一开始看到这个驱动代码框架肯定有些懵逼,但是没关系,习惯就好。因为我们是要基于这个基本的驱动代码框架来添加我们需要的驱动设备,并且注册到内核中的驱动链表中去,所以我们只要遵循内核中驱动程序的规则去编写驱动代码就好了。看代码注释*
三、驱动代码编译和测试
1)要在模块目录下写入驱动代码文件,我们把上述驱动基本框架代码写入pin4driver2.c里面
2)那么怎样才能让编译到我们加进去的这个驱动代码文件呢?修改Makefile,在Makefile文件下添加上,把pin4driver2.c编译成模块的意思:
3) 然后回到内核源码目录进行模块编译:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules
4)会看到生成了一个.ko文件
5)我们把这个.ko文件远程拷贝到树莓派:
scp pin4driver2.ko pi@192.168.4.106:/home/pi
6)编译测试代码拿到树莓派去
测试代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd;
fd=open("/dev/pin4",O_RDWR);
write(fd,"1",1);
return 0;
}
交叉编译测试代码:
arm-linux-gnueabihf-gcc text.c -o pin4text
把生成的可执行程序pin4text拿到树莓派去:
scp pin4text pi@192.168.4.106:/home/pi
7)打开树莓派
加载内核驱动:
sudo insmod pin4driver2.ko
在/dev目录下生成设备名和主次设备号:
给pin4驱动添加权限后,才可以执行
sudo chmod 666 /dev/pin4
//666:所有用户都可以读写
发现啥也没有。因为上层我们看不到任何信息,信息是在内核态打印的
查看内核态打印信息:
dmesg
四、驱动阶段性总结
1)内核驱动基本框架:
驱动代码编写:
参考pin4driver2.c:
2)内核驱动编译:
把驱动代码拷贝至 driver/char
修改Makefile ,告诉编译器,要编译该驱动文件,驱动代码文件放在哪个目录下就修改哪个目录下的Makefile文件
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules
3)驱动测试步骤:
内核驱动装载:
sodu insmod xxx.ko
内核驱动卸载:
sodu rmmod xxx 不需要写ko
查看内核模块:
lsmod
4)验证步骤:
装载驱动
驱动装载后生成设备,比如:/dev/pin4,通过
sudo chmod 666 /dev/pin4 添加访问权限
运行测试程序pin4text调用驱动
内核的printk是内核层的printf,通过dmesg查看打印信息。