一、Linux设备的分类
字符设备、块设备、网络设备,三种设备之间的区别是数据的交互模式,分别为:
字节流、数据块、数据包。
二、VFS核心结构体
VFS核心结构体定义在"linux/fs.h"头文件中。
1、struct inode结构体
记录文件的属主、访问时间等信息。当第一次打开文件的时候由VFS创建并初始化。当文件的所有引用都退出后,释放inode; 如果用户态有多个人同时打开一个文件,则VFS只需要分配一个inode。
2、struct file结构体
对应用户态的open操作。如果多次打开同一个文件,内核会生成多个file。file中记录文件的打开方式,文件内部指针等。当文件彻底关闭时,释放file。
3、struct file_operations结构体
该结构体包含若干函数指针,这些函数由驱动来实现,并集中到file_operations中,注册到VFS。
驱动一般要实现的函数有:
open
release
read
write
unlocked_ioctl
驱动可能会实现的有:
poll
mmap
fasync
flush
llseek
三、字符设备驱动开发流程
(1)确定硬件信息
要确定硬件的数量,物理地址,中断号等信息;
(2)为要支持的设备准备一个私有结构体
内核并不要求必须有私有结构体,但如果驱动支持多个设备,最好设计一个。私有结构体完全由驱动人员自行设计,一般来说,会把和设备相关的信息写入该结构体,比如设备的地址等。
(3)为要支持的每个设备分配对应的设备号
设备号由char驱动分配,要求唯一。一般来说,如果char驱动可支持多个类似的设备,则应该为这些设备选择一个主设备号,然后为每个设备选择一个特定的次设备号。尽量挑选和其他驱动不一样的主设备号。可以看/proc/devices,文件中记录了其他驱动选择的主设备号;也可以向内核申请,由内核分配一个主设备号。
#define DEV_MAJOR 50
...
dev_id = MKDEV(DEV_MAJOR, 0);
(4)准备file_operations结构体
函数集中包括open/release/read/write/unlocked_ioctl等函数,如果驱动支持多个设备,在函数中必须能区分自己访问的是哪个设备。
static struct file_operations mem_fops = {
.owner = THIS_MODULE,
.open = mem_open,
.release = mem_release,
.read = mem_read,
.write = mem_write,
.unlocked_ioctl = mem_ioctl,
};
(5)注册设备
方法一:
利用cdev结构体,将设备号和file_operations注册到VFS。一般来说,将cdev结构体包含到私有结构体中。采用cdev注册的设备,不会自动创建设备文件。
cdev_init(&mem_cdev, &mem_fops);
cdev_add(&mem_cdev, dev_id, 1);
cdev_del(&mem_cdev); //注销cdev
方法二:
注册miscdevice(用misc代替cdev注册,可以自动创建设备文件)
这种情况下不需要定义主设备号,即省去#define DEV_MAJOR 50,同时需要用头文件"linux/miscdevice.h"代替"linux/cdev.h"头文件。
static struct miscdevice mem_miscdev = {
.minor = MISC_DYNAMIC_MINOR,
.name = "mem", //对应/dev/mem设备文件
.fops = &mem_fops,
};
ret = misc_register(&mem_miscdev); //注册miscdevice
misc_deregister(&mem_miscdev); //注销miscdevice
cdev和miscdevice比较:
(1)cdev可以实现同一个驱动对应多个设备,而miscdevice只能实现一个驱动对应一个设备;
(2)cdev不能实现设备文件的自动创建,而miscdevice可以实现设备文件的自动创建。
上述的过程比较适合较简单的设备,比如看门狗,led灯,各种传感器等。较复杂设备的char驱动,常常要利用内核提供的驱动子系统代码进行设计。
四、如何在linux驱动中访问寄存器(SFR)
1、片内外设(pripheral)
(1)基于三总线访问
(2)用寄存器控制
(3)寄存器有物理地址,可通过手册查到,不可更改
2、片外外设
(1)很少通过三总线相连,一般是通过UART,CAN,I2C,USB,SPI,MIPI,I2S,AC97等总线相连。主芯片内部必须提供对应的控制器:内部的控制器 <–> 外部的外设
(2)片外外设基本都是智能设备(不但有硬件,还有软件)
(3)有些片外外设通过寄存器控制,有些则通过命令控制
(4)如果用寄存器控制,寄存器没有物理地址,只有偏移。
(5)要控制片外外设,需要首先了解对应的总线
3、访问寄存器的流程
由于linux使能了MMU,因此对于驱动来说,不能直接使用寄存器的物理地址,必须将其映射为虚拟地址才可以使用。
(1)定义寄存器物理基地址以及寄存器的偏移
#define GPIO_BASE 0x11000000
#define GPIO_SIZE 0x1000 //0x8
#define GPM4CON 0x2E0 //偏移地址
#define GPM4DAT 0x2E4 //偏移地址
GPIO_SIZE为寄存器的范围,可以按照使用的寄存器的总大小进行计算,比如用了两个寄存器,范围是0x8;但由于地址映射的最小单位是4K,因此小于4K的值都是可以的。
(2)将寄存器物理地址映射到虚拟地址,如果映射不成功,则无法访问寄存器
static void __iomem *vir_base;
vir_base = ioremap(GPIO_BASE, GPIO_SIZE);
if (!vir_base) {
printk("Cannot ioremap\n");
return -EIO;
}
(3)访问寄存器,一般采用基地址加偏移的模式,内核根据寄存器的大小,提供了一系列函数
8位寄存器的访问
char value;
value = readb(vir_base + offset);
writeb(value, (vir_base + offset));
16位寄存器的访问
short value;
value = readw(vir_base + offset);
writew(value, (vir_base + offset));
32位寄存器的访问
int value;
value = readl(vir_base + offset);
writel(value, (vir_base + offset));
64位寄存器的访问
u64 value;
value = readq(vir_base + offset);
writeq(value, (vir_base + offset));
(4)取消寄存器的映射
iounmap(vir_base);