Linux驱动开发初识
文章目录
- Linux驱动开发初识
- 一、驱动的概念
- 1.1 什么是驱动:
- 1.2 驱动的分类:
- 二、设备的概念
- 2.1 主设备号&次设备号:
- 2.2 设备号的作用:
- 三、设备驱动整体调用过程
- 3.1 上层用户操控设备的流程:
- 3.2 Linux驱动的运行方式:
- 四、基于框架编写驱动代码
- 4.1 基本字符设备驱动框架:
- 4.2 驱动代码的编译:
- 4.3 驱动的加载&卸载:
- 4.4 驱动的测试:
- 五、树莓派IO口驱动的编写
- 5.1 BCM2835芯片手册导读:
- 5.2 Pin4引脚定位:
- 5.3 根据驱动框架编写树莓派Pin4引脚驱动:
- 5.4 编译测试Pin4引脚驱动:
一、驱动的概念
1.1 什么是驱动:
Linux内核驱动:是指一段代码,这段代码可以驱动底层硬件,即驱动就是对底层硬件设备的操作进行封装,并向上层提供函数接口。
1.2 驱动的分类:
Linux驱动分为三个基础大类:字符设备驱动,块设备驱动,网络设备驱动。
- 字符设备(Char Device):
- 字符设备是个能够像字节流(类似文件)一样被访问的设备
- 对字符设备发出读/写请求时,实际的硬件I/O操作一般紧接着发生
- 字符设备驱动程序通常至少要实现
open
、close
、read
和write
系统调用 - 比如我们常见的lcd、触摸屏、键盘、led、串口等等,他们一般对应具体的硬件都是进行出具的采集、处理、传输
- 块设备(Block Device):
- 一个块设备驱动程序主要通过传输固定大小的数据(一般为512或1k)来访问设备
- 块设备通过buffer cache(内存缓冲区)访问,可以随机存取,即:任何块都可以读写,不必考虑它在设备的什么地方
- 块设备可以通过它们的设备特殊文件访问,但是更常见的是通过文件系统进行访问
- 只有一个块设备可以支持一个安装的文件系统
- 比如我们常见的电脑硬盘、SD卡、U盘、光盘等
- 网络设备(Net Device):
- 任何网络事务都经过一个网络接口形成,即一个能够和其他主机交换数据的设备
- 访问网络接口的方法仍然是给它们分配一个唯一的名字(比如eth0),但这个名字在文件系统中不存在对应的节点
- 内核和网络设备驱动程序间的通信,完全不同于内核和字符以及块驱动程序之间的通信,内核调用一套和数据包传输相关的函(
socket
函数)而不是read
、write
等 - 比如我们常见的网卡设备、蓝牙设备
二、设备的概念
- 在学习驱动和其开发之前,首先要知道所谓驱动,其对象就是设备。
2.1 主设备号&次设备号:
在Linux中,各种设备都以文件的形式存在**/dev目录下**,称为设备文件。最上层的应用程序可以打开,关闭,读写这些设备文件,从而完成对设备的操作。
为了管理这些设备,系统为设备编了号,每个设备都拥有主设备号和次设备号。主设备号用于区分不同种类的设备,而次设备号用于区分同一类型的多个设备。(对于常用的设备如硬盘,Linux赋予的主设备号一般是3)
- 在**
/dev
目录下输入ls -l
**,就可以看到设备文件对应的主次设备号:
2.2 设备号的作用:
在了解了什么是主次设备号之后,就要了解设备号的用处:
- 在用户态中:当用户调用了如
open
,read
,write
等函数想要操作设备文件时,需要两个参数,第一个是文件名,第二个就是设备号 - 在内核态中:存在着一个驱动链表,用于管理所有设备的驱动,而驱动在链表中的位置就由设备号来检索
三、设备驱动整体调用过程
3.1 上层用户操控设备的流程:
-
C语言上层调用
open
函数。open(“/dev/pin4”,O_RDWR);
调用/dev下的pin4以可读可写的方式打开。对于上层open
调用到内核时会发生一次软中断中断号是0X80,从用户空间进入到内核空间。 -
open
会调用到system_call(内核函数)
,system_call
会根据/dev/pin4
设备名,去找出需要的设备号。 -
再调到虚拟文件VFS ,调用VFS里的
sys_open
,sys_open
会找到在驱动链表里面,根据主设备号和次设备号找到引脚4里的open函数,引脚4里的open是对寄存器操作及对硬件的操作。
3.2 Linux驱动的运行方式:
- 将驱动编译进 Linux 内核中,当 Linux 内核启动的时就会自动运行驱动程序
- 将驱动编译成模块(Linux 下模块扩展名为.ko),并在Linux 内核启动以后使用相应命令加载驱动模块
四、基于框架编写驱动代码
4.1 基本字符设备驱动框架:
#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"; //模块名
//_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
printk("pin4_open\n"); //内核的打印函数和printf类似
return 0;
}
//_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
printk("pin4_write\n"); //内核的打印函数和printf类似
return 0;
}
static struct file_operations pin4_fops = { //结构体的类型是“file_operations”,名字可以自定义
//该结构体的成员就包含实现open和write的驱动函数
//当上层用户想要open或者write这个设备时,就会最终跳转到这个驱动代码中实现的open和write操作函数
//此处只赋值了该结构体中的三个成员变量(在keil中是不能这样写的,linux中可以),这个结构体其实有很多成员,如果想要实现更多的驱动函数,可以把更多的该结构体成员赋值并在这段代码中重写
.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); //注册驱动,告诉内核:把这个驱动加入到内核驱动的链表中
//以下两句代码目的是“生成设备文件”,也可以通过“mknod”命令手动生成,但是一般不会这样做
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"); //linux内核遵循GPL协议
4.2 驱动代码的编译:
- 进入Linux源码树目录下的驱动目录,因为驱动的是字符设备,所以进入的是驱动目录下的char目录。
/home/shiyahao/SYSTEM/linux-rpi-4.19.y/drivers/char
- 在这个路径下创建一个新的C文件:pin4driver.c,内容为我们刚刚的字符设备驱动:
- 修改当前路径(字符设备驱动)下的Makefile,确保这个新的驱动会被编译到:
- 回到linux内核源码的路径,运行以下指令尝试编译:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules
- 将编译好的驱动模块传到树莓派中:
scp ./drivers/char/pin4driver.ko pi@192.168.31.123:/home/pi
4.3 驱动的加载&卸载:
由于现在刚刚把驱动编译成了.ko的模块,所以需要运行以下指令来加载驱动模块:
sudo insmod pin4driver.ko //加载驱动模块
sudo rmmod pin4driver.ko //卸载驱动模块,此时驱动名字后不用加".ko"
运行成功后,就可以在**/dev**下看到生成的设备文件“pin4”了:
使用ls -l指令查看这个设备的主设备号&次设备号,和框架代码中的设置一样:
给pin驱动加权限:
sudo chmod 666 /dev/pin4
4.4 驱动的测试:
在树莓派下写一个测试驱动的C代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd;
fd = open("/dev/pin4", O_RDWR); //打开GPIO4口设备文件
if(fd < 0){
printf("open pin4 failed\n");
}else{
printf("open pin4 success\n");
}
write(fd, "1", 1); //输出高电平
return 0;
}
执行测试程序后用dmesg 查看内核打印信息发现打印了驱动函数的信息:
可见内核也按照框架代码中的printk成功打印了信息!驱动测试成功!
同时,结果也再次印证了:当用户在最上层对 驱动文件 调用C库的open函数后,最后的结果还是调用最底层 驱动文件里实现的open驱动函数
五、树莓派IO口驱动的编写
前面我们通过一个基本的字符设备驱动框架来测试了驱动的运行,但是在“pin4_open”和“pin4_write”这两个驱动函数的函数体里只写了一句内核打印的代码,作为一个真正的驱动文件这显然是不够的。
同时,在之前就提到过,驱动位于内核态的最底层,其下方就直接是硬件,所以驱动函数的目标就是直接操控硬件,也就是直接操控寄存器。在我的pin4驱动函数中应该添加的也就是根据函数功能,操作寄存器从而实现I/O口操控的代码。
5.1 BCM2835芯片手册导读:
明确了目标后,就产生了这个问题:我怎么知道应该使用哪些寄存器,又应该怎么使用呢?
答案是:根据开发平台的芯片手册/电路图来找到具体的描述,由于我是在树莓派3B上玩驱动的开发,所以我应该查阅这款树莓派的芯片,也就是BCM2835的芯片手册。
此处我只使用了芯片手册就定位了寄存器,而没有用电路图,原因是树莓派的这个芯片手册已经把用什么寄存器写的很清楚了
在BCM2835芯片手册的第六章描述了General Purpose I/O (GPIO)外设相关寄存器。这里驱动pin4引脚需要用到的寄存器有:
-
GPIO Function Select Registers (GPFSELn) 功能选择寄存器:
该寄存器共有五组,每个寄存器都有32位,以GPIO Alternate function select register 0为例,其中:
29-0位 :每三位对于一个引脚,比如29-27对应的是GPIO Pin 9,26-24对应的是GPIO Pin 8,且这三位取不同的值代表该三位对应的引脚选择不同的功能。比如,当29-27位为000时表示GPIO Pin 9是输入功能,29-27位为001时表示GPIO Pin 9是输出的功能。 -
GPIO Pin Output Set Registers (GPSETn) 置位寄存器:
该寄存器共两组,每个寄存器都有32位,将寄存器某一位置1即将对应的引脚置1。
-
GPIO Pin Output Clear Registers (GPCLRn) 清0寄存器:
与置位寄存器用法一至,将对应位数引脚置0。
-
所需寄存器的地址说明:
在编写驱动程序时,IO空间的起始地址位0X3F000000,加上GPIO的偏移量0X200000,因此GPIO的物理地址是从0X3F200000开始的,而编程所需的地址是虚拟地址,需要通过MMU内存虚拟化管理将地址映射到虚拟地址上。
5.2 Pin4引脚定位:
Pin4引脚指的是BCM4号,对应WiringPi库第7号,物理引脚的7脚:
5.3 根据驱动框架编写树莓派Pin4引脚驱动:
#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"; //模块名
//首先定义所要用的寄存器,为了防止地址被编译器优化需要用到volatile关键字
volatile unsigned int *GPFSEL0 = NULL;
volatile unsigned int *GPSET0 = NULL;
volatile unsigned int *GPCLR0 = NULL;
static int pin4_open(struct inode *inode,struct file *file)
{
printk("pin4_open\n"); //内核的打印函数和printf类似
//配置引脚4的寄存器,将其配置为输出模式,即将GPFSEL0寄存器的第14-12位配置成001
*GPFSEL0 &= 0XFFFF9FFF; //将第14,13位置0
*GPFSEL0 |= 0X00001000; //将第12位置1
return 0;
}
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
int usercmd;
printk("pin4_write\n");
copy_from_user(&usercmd,buf,count);//获取应用层write函数写入的内容
if(usercmd == 1){
printk("set 1\n");
*GPSET0 |=(0x1 << 4); //将Pin4引脚置1
}else if (usercmd == 0)
{
printk("set 0\n");
*GPCLR0 |=(0X1 << 4);//将Pin4引脚置0
}else{
printk("undo\n");
}
return 0;
}
static struct file_operations pin4_fops = {
.owner = THIS_MODULE,
.open = pin4_open,//当应用层调用open函数时,内核会调用pin4_open.
.write = pin4_write,//当应用层调用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");//由代码在dev下自动生成设备
pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name); //创建设备文件
GPFSEL0 = (volatile unsigned int *)ioremap(0X3f200000,4);//需要将物理地址映射位虚拟地址 ipremap第一个参数需要被映射的物理地址。第二个参数位映射的字节数
GPSET0 = (volatile unsigned int *)ioremap(0X3f20001C,4);//通过芯片手册可以看到该寄存器在基础地址上偏移了1C
GPCLR0 = (volatile unsigned int *)ioremap(0X3f200028,4);//通过芯片手册可以看到该寄存器在基础地址上偏移了28
return 0;
}
void __exit pin4_drv_exit(void)
{
iounmap(GPFSEL0);
iounmap(GPSET0);
iounmap(GPCLR0);
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");
然后在树莓派上编写测试代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
int fd,cmd;
fd = open("/dev/pin4",O_RDWR);
printf("input 0 ro 1 , 0 :Pin4 Set 0,1:Pin4 Set 1\n");
scanf("%d",&cmd);
printf("cmd = %d \n",cmd);
write(fd,&cmd,1);
return 0;
}
5.4 编译测试Pin4引脚驱动:
- 将驱动代码编译后生成驱动模块放置在树莓派上进行测试:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules
- 将生成的驱动模块拷贝至树莓派:
scp ./drivers/char/pin4driver.ko pi@192.168.31.123:/home/pi
- 在树莓派上安装驱动并给驱动权限:
sudo insmod pin4driver.ko
sudo chmod 666 /dev/pin4
运行测试程序: