我理解的linux驱动:封装对底层硬件的操作,向上层应用提供操作接口
文中有些地方没贴出相应的函数原型,请自行查阅,或者用SouceInsight搜索自己的内核源码树(本人就是用该方式查阅函数的使用)
简单设备驱动开发基础知识,暂不考虑驱动框架。文章根据GFM排版https://github.com/TongxinV
开发环境的搭建:内核源码树、nfs挂载的roofs、开发配置好相应的bootcmd
和bootargs
驱动开发的步骤:1.驱动源码代码的编写、Makefile
文件编写、编译得到;2.insmod装载模块、测试,rmmod卸载模块。
bootcmd和bootargs
1.设置bootcmd使开发板能够通过tftp下载自己建立的内核源码树编译得到的zImage
set bootcmd 'tftp 0x30008000 zImage;bootm 0x30008000'
(注:bootcmd=movi read kernel 30008000; movi read rootfs 30B00000 300000; bootm 30008000 30B00000 这样的bootcmd是从inand启动内核的时候用的)
2.设置bootargs使开发板从nfs去挂载rootfs(内核配置记得打开使能nfs形式的rootfs)
setenv bootargs root=/dev/nfs nfsroot=192.168.1.141:/root/x210_porting/rootfs/rootfs ip=192.168.1.10:192.168.1.141:192.168.1.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC2,115200
编译驱动源码的Makefile文件
#ubuntu的内核源码树,如果要编译在ubuntu中安装的模块就打开这2个
#KERN_VER = $(shell uname -r)
#KERN_DIR = /lib/modules/$(KERN_VER)/build
# 开发板的linux内核的源码树目录,根据自己在源码树存放的目录修改
KERN_DIR = /root/driver/kernel
obj-m += module_test.o //-m 表示我们要将module_test.c编译成一个模块
//-y表示我们要将module_test.c编译链接进zImage
all:
make -C $(KERN_DIR) M=`pwd` modules
//-C 表示进入到某一个目录下去编译
//`pwd`:表示把两个`号中间的内容当成命令执行
//M=`pwd`则表示把pwd打印的内容保存起来,目的是为了编译好了之后能够返回原来的目录
//modules就是真正用来编译模块的命令,在内核的其他地方定义过了
cp:
cp *.ko /root/porting_x210/rootfs/rootfs/driver_test
.PHONY: clean //把clean当成一个伪目标
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
总结:模块的makefile非常简单,本身并不能完成模块的编译,而是通过make -C进入到内核源码树下借用内核源码的体系来完成模块的编译链接的。
#include // module_init module_exit
#include // __init __exit
// 模块安装函数
static int __init chrdev_init(void)
{
printk(KERN_INFO "chrdev_init helloworld init\n");
return 0;
}
// 模块卸载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
}
module_init(chrdev_init);
module_exit(chrdev_exit);
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
(1)使用printk打印调试信息,printk可以设置打印级别。常见的KERN_DBUG-8\KERN_INFO-7,当前系统也有一个打印信息的级别0-7(比如当前系统打印信息的级别为4,则printk打印小于级别4)。
查看当前系统打印信息的级别:cat /proc/sys/kernel/printk;修改:echo 8 > /proc/sys/kernel/printk
(2)驱动源代码中包含的头文件和原来应用编程程序中包含的头文件不是一回事。应用编程中包含的头文件是应用层的头文件,是应用程序的编译器带来的(譬如gcc的头文件路径在/usr/include下,这些东西是和操作系统无关的)。驱动源码属于内核源码的一部分,驱动源码中的头文件其实就是内核源代码目录下的include目录下的头文件。
(3)函数修饰符__init
(前面加下划线的表示这是给内核使用的函数),本质上是个宏定义,在内核源代码中就有#define __init xxxx
。这个__init
的作用就是将被他修饰的函数放入.init.text
段中去(本来默认情况下函数是被放入.text
段中)。
#define __init __section(.init.text) __cold notrace
├──#define __section(S) __attribute__ ((__section__(#S)))
整个内核中的所有的这类函数都会被链接器根据链接脚本放入.init.text
段中,所以所有的内核模块的__init
修饰的函数其实是被统一放在一起的。
内核启动时统一会加载.init.text
段中的这些模块安装函数,加载完后就会把这个段给释放掉以节省内存。__exit
同理。
可以理解模块是一种机制,驱动使用了模块这种机制来实现
系统整体工作原理:(1)应用层->API->设备驱动->硬件;(2)API:open、read、write、close等;(3)驱动源码中提供真正的open、read、write、close等函数实体
file_operations结构体(另外一种为attribute方式后面再讲):(1)元素主要是函数指针,用来挂接实体函数地址;(2)每个设备驱动都需要一个该结构体类型的变量;(3)设备驱动向内核注册时提供该结构体类型的变量。
注册字符设备驱动register_chrdev
:
static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
(1)作用,驱动向内核注册自己的file_operations结构体,注册的过程其实主要是将要注册的驱动的信息存储在内核中专门用来存储注册的字符设备驱动的数组中相应的位置
(2)参数:设备号major–major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备,内核如果成功分配就会返回分配的主设备号;如果分配失败会返回负数
(3)inline和static
inline:当把函数定义在头文件里面的时候,如果你这个头文件被两个及两个以上的函数包含的时候,在链接的时候就会出错。inline的作用就是解决这个问题,原地展开并能够实现静态检查。另外一个原因是函数本身就比较短。
内核如何管理字符设备驱动
(1)内核中用一个数组来存储注册的字符设备驱动;(2)register_chrdev内部将我们要注册的驱动的信息(fops结构体地址)存储在数组中相应的位置;(3)cat /proc/devices查看内核中已经注册过的字符设备驱动(和块设备驱动)
核心工作:定义file_operations类型变量及其元素填充、注册驱动
简单的驱动程序示例
module_test.c
├── 模块安装函数xxx
│ └── 注册字符设备驱动register_chrdev(MYNMAJOR, MYNAME, &test_module_fops)
├── 模块安装函数yyy
│ └── 注销字符设备驱动unregister_chrdev(MYNMAJOR, MYNAME)
│
├── module_init(模块安装函数xxx);
├── module_exit(模块卸载函数yyy);
│
└── MODULE_LICENSE("GPL");
#include // module_init module_exit
#include // __init __exit
#include // file_operations 没写会报错:xxx has initializer but incomplete type
#define MYNMAJOR 200
#define MYNAME "test_chrdev"
//file_operations结构体变量中填充的函数指针的实体,函数的格式要遵守
static int test_chrdev_open(struct inode *inode, struct file *file)
{
//这个函数中真正应该放置的是打开这个设备的硬件操作代码部分
//但是现在我们暂时写不了那么多,所以就就用一个printk打印个信息来做代表
printk(KERN_INFO "test_module_open\n");
return 0;
}
static int test_chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "test_chrdev_release\n");
return 0;
}
//自定义一个file_operations结构体变量,并填充
static const struct file_operations test_module_fops = {
.owner = THIS_MODULE, //惯例,所有的驱动都有这一个,这也是这结构体中唯一一个不是函数指针的元素
.open = test_chrdev_open, //将来应用open打开这个这个设备时实际调用的函数
.release = test_chrdev_release, //对应close,为什么不叫close呢?详见后面release和close的区别的讲解
};
/*********************************************************************************/
// 模块安装函数
static int __init chrdev_init(void)
{
printk(KERN_INFO "chrdev_init helloworld init\n");
//在module_init宏调用的函数中去注册字符设备驱动
int ret = -1; //register_chrdev 返回值为int类型
ret = register_chrdev(MYNMAJOR, MYNAME, &test_module_fops);
//参数:主设备号major,设备名称name,自己定义好的file_operations结构体变量指针,注意是指针,所以要加上取地址符
//完了之后检查返回值
if(ret){
printk(KERN_ERR "register_chrdev fial\n"); //注意这里不再用KERN_INFO
return -EINVAL; //内核中定义了好多error number 不都用以前那样return -1;负号要加 !!
}
printk(KERN_ERR "register_chrdev success...\n");
return 0;
}
// 模块卸载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
//在module_exit宏调用的函数中去注销字符设备驱动
//实验中,在我们这里不写东西的时候,rmmod 后lsmod 查看确实是没了,但是cat /proc/device发现设备号还是被占着
unregister_chrdev(MYNMAJOR, MYNAME); //参数就两个
//检测返回值
......
return 0;
}
/*********************************************************************************/
module_init(chrdev_init); //insmod 时调用
module_exit(chrdev_exit); //rmmod 时调用
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
驱动设备文件的创建:(1)何为设备文件:用来索引驱动;(2)设备文件的关键信息是:设备号 = 主设备号 + 次设备号;(3)使用mknod创建设备文件:mknod /dev/xxx c 主设备号 次设备号
(c表示要创建的设备文件类型为字符设备);(4)使用ls xxx -l
去查看设备文件,就可以得到这个设备文件对应的主次设备号。
注:不可能总用mknod来创建设备文件,能否自动生成和删除设备文件?linux内核有一种机制–udev(嵌入式中用的是mdev)后面细讲
一个简单的应用程序示例app.c
#include
#include
#include
#include //man 2 open 查看头文件有哪些
#define FILE "/dev/test" // 刚才mknod创建的设备文件名 双引号不要漏
int main(void)
{
int fd = -1;
fd = open(FILE, O_RDWR);
if (fd < 0){
printf("open %s error.\n", FILE);
return -1;
}
printf("open %s success..\n", FILE);
// 读写文件
...
// 关闭文件
close(fd);
return 0;
}
照猫画虎
在驱动程序中添加:
ssize_t test_chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)//struct file *file:指向我们要操作的文件;const char __user *buf:用户空间的buf
{
printk(KERN_INFO "test_chrdev_read\n");
......
static ssize_t test_chrdev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
printk(KERN_INFO "test_chrdev_write\n");
......
在应用程序中添加:
//读写文件
read(fd,buf,100);//最后一个参数为要读取的字节数
write(fd,"helloworld",10);
......
测试:测试前先rmmod 把之前实验的模块卸载掉,lsmod确认下cat /proc/devices
再insmod;还有设备文件也要rm /dev/xxx
删设备文件,安装完模块后再mknod重新建立设备文件。然后执行应用程序查看打印信息(在后面我们会讲怎么弄才不会那么麻烦)
应用和驱动之间的数据交换:
写函数的本质就是将应用层传递的过来的数据先复制到内核中,然后将之以正确的方式写入硬件,完成操作
目前接触到的就两种:copy_from_user、copy_to_user和mmap,这里只讲第一种
完成write和read函数:
copy_from_user函数的返回值定义,和常规有点不同。返回值如果成功复制则返回0,如果不成功复制则返回尚未成功复制剩下的字节数。
module_test.c:
char kbuf[100];//内核空间的一个buf
......
static ssize_t test_chrdev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_write\n");
//使用该函数将应用层的传过来的ubuf中的内容拷贝到驱动空间(内核空间)的一个buf中
//memcpy(kbuf,ubuf); //不行,因为2个不在一个地址空间中
menset(kbuf, 0, sizeof(kbuf));
ret = copy_from_user(kbuf,ubuf,count);
if(ret){
printk(KERN_ERR "copy_from_user fail\n");
return -EINVAL;//在真正的的驱动中没复制成功应该有一些纠错机制,这里我们简单点
}
printk(KERN_ERR "copy_from_user success..\n");
//到这里我们就成功把用户空间的数据转移到内核空间了
//真正的驱动中,数据从应用层复制到驱动中后,我们就要根据这个数据去写硬件完成硬件的操作
//所以下面就应该是操作硬件的代码
......
return 0;
}
ssize_t test_chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_read\n");
ret = copy_to_user(ubuf,kbuf,size);
if(ret){
printk(KERN_ERR "copy_to_user fail\n");
return -EINVAL;//在真正的的驱动中没复制成功应该有一些纠错机制,这里我们简单点
}
printk(KERN_ERR "copy_to_user success..\n");
return 0;
}
app.c
......
//读写文件
write(fd, “helloworld”, 10);
read(fd,buf,100);
printf(“读出来的内容是:%s \n”,buf);
打印结果:...
在PowerPC、m68k和ARM等体系中,外设I/O端口具有与内存一样的物理地址,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。Linux的驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将物理地址映射到内核虚地址空间。
还是那个硬件:
(1)硬件物理原理不变;(2)硬件操作接口(寄存器)不变;(3)硬件操作代码不变
哪里不同:
(1)寄存器地址不同。原来是直接用物理地址,现在需要用该物理地址在内核虚拟地址空间相对应的虚拟地址。寄存器的物理地址是CPU设计时决定的,从datasheet中查找到的。
(2)编程习惯不同。裸机中习惯直接用函数指针操作寄存器地址,而kernel中习惯用封装好的io读写函数来操作寄存器,以实现最大程度可移植性。
内核的虚拟地址映射方法:
(1)为什么需要虚拟地址映射:内核运行在自己的虚拟地址空间中
(2)内核中有2套虚拟地址映射方法:动态和静态
(3)静态映射方法的特点:
内核移植时以代码的形式硬编码,如果要更改必须改源代码后重新编译内核
在内核启动时建立静态映射表,到内核关机时销毁,中间一直有效
对于移植好的内核,你用不用他都在那里
(4)动态映射方法的特点:
驱动程序根据需要随时动态的建立映射、使用、销毁映射
映射是短期临时的
如何选择虚拟地址映射方法:
(1)2种映射并不排他,可以同时使用
(2)静态映射类似于C语言中全局变量,动态方式类似于C语言中malloc堆内存
(3)静态映射的好处是执行效率高,坏处是始终占用虚拟地址空间;动态映射的好处是按需使用虚拟地址空间,坏处是每次使用前后都需要代码去建立映射&销毁映射(还得学会使用那些内核函数的使用)
没有绝对好绝对坏
在ARM 存储系统中,使用内存管理单元(MMU)实现虚拟地址到实际物理地址的映射。MMU的实现过程实际上就是一个查表映射的过程。建立页表是实现MMU功能不可缺少的一步。页表位于系统的内存中,页表的每一项对应于一个虚拟地址到物理地址的映射。每一项的长度即是一个字的长度(在32位ARM中,一个字的长度被定义为4B)。页表项除完成虚拟地址到物理地址的映射功能之外,还定义了访问权限和缓冲特性等。
由于篇幅有限,这里只分析如何使用(以s5pv210为例),具体如何实现点击这里Linux内核静态映射表建立过程分析
关于静态映射要说的:
(1)不同版本内核中静态映射表位置、文件名可能不同
(2)不同SoC的静态映射表位置、文件名可能不同
(3)所谓映射表其实就是头文件中的宏定义
三星版本内核中的静态映射表:
(1)主映射表位于:arch/arm/plat-samsung/include/plat/map-base.h
和arch/arm/plat-s5p/include/plat/map-s5p.h
map-base.h
...
#define S3C_ADDR_BASE (0xFD000000)//三星移植时确定的静态映射表的基地址,表中的所有虚拟地址都是以这个地址+偏移量来指定的
#ifndef __ASSEMBLY__
#define S3C_ADDR(x) ((void __iomem __force *)S3C_ADDR_BASE + (x))
#else
#define S3C_ADDR(x) (S3C_ADDR_BASE + (x))
#endif
#define S3C_VA_IRQ S3C_ADDR(0x00000000) /* irq controller(s) */
#define S3C_VA_SYS S3C_ADDR(0x00100000) /* system control */
#define S3C_VA_MEM S3C_ADDR(0x00200000) /* memory control */
...
#define S5P_VA_GPIO S3C_ADDR(0x00500000)
...
map-base.h和map-s5p.h中定义的是各模块的寄存器基地址的虚拟地址。(后面那个可能是九鼎根据自己的硬件自己移植)
CPU在安排寄存器地址时不是随意乱序分布的,而是按照模块去区分的。每一个模块内部的很多个寄存器的地址是连续的。所以内核在定义寄存器地址时都是先找到基地址,然后再用基地址+偏移量来寻找具体的一个寄存器。(map-s5p.h中定义的就是要用到的几个模块的寄存器基地址。并没有全,三星只写了自己要用的。将来实际工作如果要用到的这里没有就自己添加)
(2)GPIO各个端口相关的主映射表位于:arch/arm/mach-s5pv210/include/mach/regs-gpio.h
表中是GPIO的各个端口的基地址的定义。GPIO还分成GPA0、GPA1、GPB0、GPC、E、F、G、H等
regs-gpio.h
/* Base addresses for each of the banks */
#define S5PV210_GPA0_BASE (S5P_VA_GPIO + 0x000)
#define S5PV210_GPA1_BASE (S5P_VA_GPIO + 0x020)
#define S5PV210_GPB_BASE (S5P_VA_GPIO + 0x040)
#define S5PV210_GPC0_BASE (S5P_VA_GPIO + 0x060)
...
(3)每一个GPIO的具体寄存器定义位于:arch/arm/mach-s5pv210/include/mach/gpio-bank.h
gpio-bank.h
...
#define S5PV210_GPA0CON (S5PV210_GPA0_BASE + 0x00)
#define S5PV210_GPA0DAT (S5PV210_GPA0_BASE + 0x04)
#define S5PV210_GPA0PUD (S5PV210_GPA0_BASE + 0x08)
#define S5PV210_GPA0DRV (S5PV210_GPA0_BASE + 0x0c)
#define S5PV210_GPA0CONPDN (S5PV210_GPA0_BASE + 0x10)
#define S5PV210_GPA0PUDPDN (S5PV210_GPA0_BASE + 0x14)
...
Q:为什么给个虚拟地址就能找到对应的物理地址?—-MMU的存在,Linux内核静态映射表建立过程分析
驱动中添加相应代码:
#include //虚拟地址映射表
#include
...
#define rGPJ0CON *((volatile unsigned int *)GPJ0CON) //强制类型转化为指针类型,再解引用
#define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)
...
//写函数的本质就是将应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件
static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_write\n");
// 使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间中的一个buf中
//memcpy(kbuf, ubuf); // 不行,因为2个不在一个地址空间中
memset(kbuf, 0, sizeof(kbuf));
ret = copy_from_user(kbuf, ubuf, count);
if (ret){
printk(KERN_ERR "copy_from_user fail\n");
return -EINVAL;
}
printk(KERN_INFO "copy_from_user success..\n");
if (kbuf[0] == '1'){
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
}else if (kbuf[0] == '0'){
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
}
return 0;
}
...
完整源代码module_test.c
注:我们驱动这么写既是正确的又是不正确的,正确的是说它能实现功能,不正确是说它写法不符合常规,常规的写法就是我们在驱动里只负责单纯的操作硬件,而应该把一些判断啊跟用户相关的业务逻辑写到应用里而不应该把它写到驱动里。
如何建立动态映射:
(1)request_mem_region,向内核申请(报告)需要映射的内存资源。参数:寄存器物理地址,寄存器占用字节数,name
(2)ioremap,真正用来实现映射,传给他物理地址他给你映射返回一个虚拟地址。参数:寄存器物理地址,寄存器占用字节数;返回值:映射到虚拟地址的地址的指针。
如何销毁动态映射
(1)iounmap
(2)release_mem_region
注意:映射建立时,是要先申请再映射;然后使用;使用完要解除映射时要先解除映射再释放申请。(倒影式结构)
驱动中添加相应代码:
在模块安装中去申请资源和实现映射;在模块卸载中去解除映射和释放资源
...
#define GPJ0CON_PA 0xe0200240
#define GPJ0DAT_PA 0xe0200244
unsigned int *pGPJ0CON;
unsigned int *pGPJ0DAT;
...
// 模块安装函数
static int __init chrdev_init(void)
{
printk(KERN_INFO "chrdev_init helloworld init\n");
// 在module_init宏调用的函数中去注册字符设备驱动
mymajor = register_chrdev(0, MYNAME, &test_fops);//分配就会返回分配的主设备好;如果分配失败会返回负数
if (mymajor < 0){
printk(KERN_ERR "register_chrdev fail\n");
return -EINVAL;
}
printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);
// 使用动态映射的方式来操作寄存器
if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
return -EINVAL;
if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0CON"))
return -EINVAL;
pGPJ0CON = ioremap(GPJ0CON_PA, 4);
pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);
/***后面就可以通过pGPJ0CON、pGPJ0DAT来操作相应寄存器从而控制硬件了***/
*pGPJ0CON = 0x11111111;
*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮
return 0;
}
// 模块下载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
*pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
// 解除映射
iounmap(pGPJ0CON);
iounmap(pGPJ0DAT);
release_mem_region(GPJ0CON_PA, 4);
release_mem_region(GPJ0DAT_PA, 4);
// 在module_exit宏调用的函数中去注销字符设备驱动
unregister_chrdev(mymajor, MYNAME);
}
实现同时映射多个寄存器:
因为地址是挨着的,所以可以一次映射4n个字节的内存长度。怎么访问呢?–*(p+1)
,真实驱动中其实现方式为用结构体进行封装,具体点击这里动态映射之结构体方式操作寄存器
老接口:
register_chrdev
新接口:register_chrdev_region/alloc_chrdev_region + cdev
int register_chrdev_region(dev_t from, unsigned count, const char *name)
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
include/linux/cdev.h
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops; //fops结构体
struct list_head list;
dev_t dev; //设备号(包含主设备号和次设备号);dev_t类型:typedef u_long dev_t;
unsigned int count;
};
(1)对于这个结构体我们有很多函数来操作他,相关函数:cdev_alloc
、cdev_init
、cdev_add
、cdev_del
(2)设备号:主设备号+次设备号
MKDEV:由一个主设备号和次设备号算出设备号
MAJOR:从设备号提取主设备号
MINOR:从设备号提取次设备号
代码示例:
新的接口注册字符设备驱动需要两步
module_test.c
├── static struct cdev xxx_cdev;(定义一个全局cdev结构体)
│
├── 模块安装函数xxx
│ ├── 第一步:注册/分配主次设备号 | 用register_chrdev_region/alloc_chrdev_region
│ └── 第二步:注册字符设备驱动,cdev_init关联file_operations结构体 + cdev_add完成真正的注册
├── 模块卸载函数yyy
│ ├── 第一步:真正注销字符设备驱动 | 用cdev_del
│ └── 第二步:注销申请的主次设备号unregister_chrdev_region
│
├── module_init(模块安装函数xxx);
├── module_exit(模块卸载函数yyy);
│
└── MODULE_LICENSE("GPL");
#define TEST_DEV MKDEV(MYMAJOR,0)
#define TEST_MAX 1
#define MYNAME "test_chrdev"
...
static struct cdev test_cdev; //定义一个全局cdev结构体
...
// 模块卸载函数
static void __exit chrdev_exit(void)
{
...
//第一步:注册/分配主次设备号 | 用register_chrdev_region/alloc_chrdev_region
int retval;
retval = register_chrdev_region(TEST_DEV, TEST_MAX, MYNAME);
if (retval) {
...
//第二步:注册字符设备驱动
cdev_init(&test_cdev, &test_fops); //关联file_operations结构体
retval = cdev_add(&test_cdev, TEST_DEV, TEST_MAX);//cdev_add完成真正的注册
if (retval) {
...
...
register_chrdev_region是在事先知道要使用的主、次设备号时使用的;要先cat /proc/devices去查看哪些可以使用的
完整代码module_test.c,注意每个函数的参数的意义
return -EINVAL 这种错误处理方式是不合适的,譬如上面代码中第一步分配设备号执行正确了,第二步错了直接return,那第一步申请到的设备号就得不到注销,一直占着位置
从内存角度体会cdev_alloc用与不用的差别
module_test.c
├── static struct cdev *pcdev;(定义一个全局cdev结构体类型指针)
│
├── 模块安装函数xxx
│ ├── 第一步:注册/分配主次设备号 | 用register_chrdev_region/alloc_chrdev_region
│ └── 第二步:注册字符设备驱动,cdev_alloc分配内存 + cdev_init关联file_operations结构体 + cdev_add完成真正的注册
├── 模块卸载函数yyy
│ ├── 第一步:真正注销字符设备驱动 | 用cdev_del
│ └── 第二步:注销申请的主次设备号unregister_chrdev_region
│
├── module_init(模块安装函数xxx);
├── module_exit(模块卸载函数yyy);
│
└── MODULE_LICENSE("GPL");
代码示例:
...
//static struct cdev test_cdev; //简单粗暴不够灵活
static struct cdev *pcdev; //灵活
...
// 模块卸载函数
static void __exit chrdev_exit(void)
{
...
//第一步:注册/分配主次设备号 | 用register_chrdev_region/alloc_chrdev_region
int retval;
retval = register_chrdev_region(TEST_DEV, TEST_MAX, MYNAME);
if (retval) {
...
//第二步:注册字符设备驱动
pcdev = cdev_alloc(); //给pcdev分配内存,指针实例化,内部实际上是调用了内核自己的malloc--> kmalloc
cdev_init(pcdev, &test_fops);
retval = cdev_add(pcdev, dev_id, TEST_COUNT);
if (retval) {
...
...
cdev_init有时候被下面两句代替:
pcdev->owner = THIS_MODULE;
pcdev->ops = &test_fops;
因为cdev_init内部的一些事情在你使用cdev_alloc时已经做了
但如果你使用全局变量的方式定义cdev结构体变量就一定要用cdev_init了
完整代码module_test.c,相对上一次改动:系统分配设备号 | 倒影式错误处理 | 使用cdev_alloc
register_chrdev
和register_chrdev_region
/alloc_chrdev_region
分析方法:看内核源码,RTFSC : Read The Fucking Source Code,工具SourceInsight
register_chrdev
__register_chrdev
__register_chrdev_region
cdev_alloc
cdev_add
register_chrdev_region
__register_chrdev_region
alloc_chrdev_region
__register_chrdev_region
__register_chrdev_region分析:点击这里
目前为止的理解
之前讲的驱动一直需要使用mknod创建设备文件,能否自动生成和删除设备文件?
解决方案:udev(嵌入式中用的是mdev),应用层启用udev,内核驱动中使用相应接口
(1)什么是udev?应用层的一个应用程序;制作根文件系统时:/etc/init.d/rcS文件中有一行语句就是用来启用udev
(2)内核驱动和应用层udev之间有一套信息传输机制(netlink协议)
(3)驱动注册和注销时信息会被传给udev,由udev在应用层进行设备文件的创建和删除
内核驱动中使用相应接口:内核驱动设备类相关函数
(1)class_create
(2)device_create
编程示例:(以下为图片形式,因为自带的高亮并不能满足我想表达的,源码点击这里)
module_test.c
├── static struct cdev *pcdev; (定义一个全局cdev 结构体类型指针)
├── static struct class *test_class;(定义一个全局class结构体类型指针)
│
├── 模块安装函数xxx
│ ├── 第一步:注册/分配主次设备号 | 用register_chrdev_region/alloc_chrdev_region
│ ├── 第二步:注册字符设备驱动,cdev_alloc分配内存 + cdev_init关联file_operations结构体 + cdev_add完成真正的注册
│ └── 添加设备类操作:第一步创建类class_create | 第二步创建用户空间下的设备文件device_create
├── 模块卸载函数yyy
│ ├── 删除用户空间下的设备文件和类文件:第一步device_destroy | 第二步class_destroy
│ ├── 第一步:真正注销字符设备驱动 | 用cdev_del
│ └── 第二步:注销申请的主次设备号unregister_chrdev_region
│
├── module_init(模块安装函数xxx);
├── module_exit(模块卸载函数yyy);
│
└── MODULE_LICENSE("GPL");
class_create
和device_create
内核源码分析
目前还不能够完全分析下来,这里只列出程序框架。日后有时间分析再贴上链接,自己也可以去内核看源码分析
class_create
__class_create
__class_register
kset_register
kobject_uevent
device_create
device_create_vargs
kobject_set_name_vargs
device_register
device_initialize
device_add
kobject_add
device_create_file
device_create_sys_dev_entry
devtmpfs_create_node
device_add_class_symlinks
device_add_attrs
device_pm_add
kobject_uevent
sys 文件系统是内核提供的一种调试方式,
/sys
、/proc
都属于我们的虚拟文件系统。虚拟文件系统有很多东西,是内核的数据结构,变量等,以文件的形式展示给我们。那么我们可以在应用层来跟这些文件进行交互实现跟内核的一些文件进行交互。
譬如用户空间/dev/..
下的文件是通过内核的mknod
或者设备类操作
来添加上去,用户通过设备访问内核的驱动程序。
关于sys文件系统更详细的描述可以参考这篇文章使用 /sys 文件系统访问 Linux 内核
arm是IO与内存统一编址,其他平台如x86是IO与内存独立编址访问方式不一样,使用内核提供的寄存器读写接口具有可移植性
在文章随笔–Linux字符设备驱动开发基础前面写的驱动在静态映射操作寄存器,都用#define rGPJ0CON *((volatile unsigned int *)GPJ0CON)
的方式来访问寄存器,这样的做法在驱动中并不是很好,因为这样的做法在不同平台的情况下不具有可移植性。现在写的驱动是在ARM平台下去写的,ARM属于内存和IO统一编址的,在读写寄存器的时候即为进行IO操作,进行IO操作是和读写内存是一样的(IO也有个地址),这就叫统一编址。但是还有另外一些CPU(像x86)是非统一编址的,这种CPU在进行IO操作时的方法跟进行内存的读写的方法是不一样的。那么在这种情况下就有一种问题,如果写的驱动不仅要求在ARM下能够运行,还要求在X86下也要能够运行,如果还用#define rGPJ0CON *((volatile unsigned int *)GPJ0CON)
的方式显然是不合适的,需要进行比较大的修改。我们要怎样才能够使他能够具有很强的移植性呢?——内核已经帮我们想好了办法,即内核提供访问寄存器的读写接口(函数),使用这些函数具有可移植性。其实现的原理就是用条件编译,如下比较:
代码示例(静态映射):
...
#include //虚拟地址映射表
#include
#include
#include
#define GPJ0CON S5PV210_GPJ0CON
#define GPJ0DAT S5PV210_GPJ0DAT
...
...
writel(0x11111111, GPJ0CON);
writel(((0<<3) | (0<<4) | (0<<5)), GPJ0DAT);
...
代码示例(动态映射):
#include
#include
...
#define GPJ0CON_PA 0xe0200240
#define S5P_GPJ0REG(x) (x)
#define S5P_GPJ0CON S5P_GPJ0REG(0)
#define S5P_GPJ0DAT S5P_GPJ0REG(4)
static void __iomem *baseaddr; // 寄存器的虚拟地址的基地址,用来保存 ioremap的返回值
...
...
if (!request_mem_region(GPJ0CON_PA, 8, "GPJ0BASE"))
return -EINVAL;
baseaddr = ioremap(GPJ0CON_PA, 8);
writel(0x11111111, baseaddr + S5P_GPJ0CON);
writel(((0<<3) | (0<<4) | (0<<5)), baseaddr + S5P_GPJ0DAT);
...
简单驱动一般框架(基础知识,不考虑驱动框架)
module_test.c
├── MKDEV(MYMAJOR,0)或dev_t dev_id
│
├── static struct cdev *pcdev; (定义一个全局cdev 结构体类型指针)
├── static struct class *test_class;(定义一个全局class结构体类型指针)
│
├── 自定义一个file_operations结构体变量,并且去填充
│
├── 模块安装函数xxx
│ ├── 第一步:注册/分配主次设备号 | 用register_chrdev_region/alloc_chrdev_region
│ ├── 第二步:注册字符设备驱动,cdev_alloc分配内存 + cdev_init关联file_operations结构体 + cdev_add完成真正的注册
│ └── 添加设备类操作:第一步创建类class_create | 第二步创建用户空间下的设备文件device_create
├── 模块卸载函数yyy
│ ├── 删除用户空间下的设备文件和类文件:第一步device_destroy | 第二步class_destroy
│ ├── 第一步:真正注销字符设备驱动 | 用cdev_del
│ └── 第二步:注销申请的主次设备号unregister_chrdev_region
│
├── module_init(模块安装函数xxx);
├── module_exit(模块卸载函数yyy);
│
└── MODULE_LICENSE("GPL");
还需要掌握1.添加读写接口(应用和驱动之间的数据交换);2.静态映射和动态映射(多个同时);3.倒影式结构;4.使用内核提供的读写寄存器接口
示例代码:module_test.c