1.注册字符设备驱动新接口
1.1、新接口与老接口
(1)老接口:register_chrdev
(2)新接口:register_chrdev_region/alloc_chrdev_region + cdev
register_chrdev_region向内核申请一个主设备号,alloc_chrdev_region让内核自动分配一个主设备号给我,cdev结构体,包含注册驱动的函数指针。
(3)为什么需要新接口
1.2、cdev介绍
(1)结构体
(2)相关函数:cdev_alloc、cdev_init、cdev_add、cdev_del
1.3、设备号
(1)主设备号(就是内核管理驱动的数组下标)和次设备号
(2)dev_t类型
(3)MKDEV:用主设备号和次设备号算出来一个设备号、MAJOR:根据设备号得到主设备号、MINOR:根据设备号来得到次设备号。三个宏(用来在主次设备号之间进行换算,根据内核的架构决定主次设备号在内存中怎么存储)
1.4、编程实践
(1)使用register_chrdev_region + cdev_init + cdev_add注册
//int register_chrdev_region(dev_t from, unsigned count, const char *name)
//form:从第一个设备号开始:比方说主设备号是200,次设备号是0,则from就是MKDEV(200,0),MKDEV的类型是dev_t类型的数字,对应存储主设备号和次设备号的规则
//比如这样一套规则:
/*
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) //取出dev_t中的某几位,作为主设备号
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) //取出dev_t中的某几位,作为次设备号
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) //将主次设备号按一定规则存储
*/
//count:分配几个次设备号
(2) cdev_del+unregister_chrdev_region注销
2.注册字符设备驱动新接口
2.1、使用alloc_chrdev_region自动分配设备号
(1)register_chrdev_region是在事先知道要使用的主、次设备号时使用的;要先查看cat /proc/devices去查看没有使用的。
(2)更简便、更智能的方法是让内核给我们自动分配一个主设备号,使用alloc_chrdev_region就可以自动分配了。
(3)自动分配的设备号,我们必须去知道他的主次设备号,否则后面没法去mknod创建他对应的设备文件。
2.2、得到分配的主设备号和次设备号
(1)使用MAJOR宏和MINOR宏从dev_t得到major和minor
(2)反过来使用MKDEV宏从major和minor得到dev_t。
(3)使用这些宏的代码具有可移植性
2.3、中途出错的倒影式错误处理方法
(1)内核中很多函数中包含了很多个操作,这些操作每一步都有可能出错,而且出错后后面的步骤就没有进行下去的必要性了。我们之前是直接return,但是这样导致我可能上一步执行的任务没法释放,所以在出错时我们要逆序将错误之前的运行正确的函数释放,这就是倒影式错误处理方法。c语言中用goto语句跳转到错误处理函数。也称为无条件转移语句,其一般格式如下: goto 语句标号; 其中语句标号是按标识符规定书写的符号, 放在某一语句行的前面,标号后加冒号(:) 各标号不得重名。语句标号起标识语句的作用,与goto 语句配合使用。注意goto后面的代码都会被执行,无论他有没有标号。C语言中goto的标号不能用数字,汇编可以。
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <mach/regs-gpio.h>
#include <mach/gpio-bank.h> // arch/arm/mach-s5pv210/include/mach/gpio-bank.h
#include <linux/string.h>
#include <linux/io.h>
#include <linux/ioport.h>
#include <linux/cdev.h>
//#define MYMAJOR 200
#define MYCNT 1
#define MYNAME "testchar"
#define GPJ0CON S5PV210_GPJ0CON
#define GPJ0DAT S5PV210_GPJ0DAT
#define rGPJ0CON *((volatile unsigned int *)GPJ0CON)
#define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)
#define GPJ0CON_PA 0xe0200240
#define GPJ0DAT_PA 0xe0200244
unsigned int *pGPJ0CON;
unsigned int *pGPJ0DAT;
//int mymajor;
static dev_t mydev;
static struct cdev test_cdev;
char kbuf[100]; // 内核空间的buf
// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops;
// 模块安装函数
static int __init chrdev_init(void)
{
int retval;
printk(KERN_INFO "chrdev_init helloworld init\n");
// 使用新的cdev接口来注册字符设备驱动
// 新的接口注册字符设备驱动需要2步
// 第1步:分配主次设备号
retval = alloc_chrdev_region(&mydev, 12, MYCNT, MYNAME);
if (retval < 0)
{
printk(KERN_ERR "Unable to alloc minors for %s\n", MYNAME);
goto flag1;
}
printk(KERN_INFO "alloc_chrdev_region success\n");
printk(KERN_INFO "major = %d, minor = %d.\n", MAJOR(mydev), MINOR(mydev));
// 第2步:注册字符设备驱动
cdev_init(&test_cdev, &test_fops);
retval = cdev_add(&test_cdev, mydev, MYCNT);
if (retval) {
printk(KERN_ERR "Unable to cdev_add\n");
goto flag2;
}
printk(KERN_INFO "cdev_add success\n");
// 使用动态映射的方式来操作寄存器
if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
goto flag3;
if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0CON"))
goto flag3;
pGPJ0CON = ioremap(GPJ0CON_PA, 4);
pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);
*pGPJ0CON = 0x11111111;
*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮
return 0;
// 如果第4步才出错跳转到这里来
flag4:
release_mem_region(GPJ0CON_PA, 4);
release_mem_region(GPJ0DAT_PA, 4);
// 如果第3步才出错跳转到这里来
flag3:
cdev_del(&test_cdev);
// 如果第2步才出错跳转到这里来
flag2:
// 在这里把第1步做成功的东西给注销掉
unregister_chrdev_region(mydev, MYCNT);
// 如果第1步才出错跳转到这里来
flag1:
return -EINVAL;
}
// 模块下载函数
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);
// 使用新的接口来注销字符设备驱动
// 注销分2步:
// 第一步真正注销字符设备驱动用cdev_del
cdev_del(&test_cdev);
// 第二步去注销申请的主次设备号
unregister_chrdev_region(mydev, MYCNT);
}
module_init(chrdev_init);
module_exit(chrdev_exit);
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("aston"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
2.4、使用cdev_alloc
(1)cdev_alloc的编程实践:之前定义的是结构体变量:static struct cdev test_cdev;被分配在数据段中。(结构体本身不占内存,但是结构体变量占内存,结构体变量是用结构体这个模子印出来的,在面向对象的语言中,结构体就是一个类,结构体变量就是类的一个对象,对象就是类的一个实例,定义结构体变量就是对结构体进行实例化),占了sizeof(struct cdev)这么大的内存,在注册驱动后一直存在,只要驱动没有卸载,这段内存就仍然被占用。真正能按需分配的方法就是使用堆内存。
现在我们要定义指针:static struct cdev *pdev;
所以用pcdev = cdev_alloc(); // 给pcdev分配内存,指针实例化
cdev_alloc()内部调用malloc函数在堆中申请内存。
在这里用了cdev_alloc()申请堆内存,就要在模块卸载函数中去释放堆内存,我们释放堆内存的函数和删除结构体变量的函数是同一个函数 :
void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev, p->count); //此函数用来释放内存
kobject_put(&p->kobj); //此函数用来删除结构体变量
}
//这个函数会看是否有内存被申请了,如果有,就释放,然后将结构体删除,如果没有,
//就直接删除。那怎么知道是否有内存被申请了呢,因为cdev_alloc内部在申请堆内存
//时做了个记号,cdev_alloc和cdev_del协商好,两相呼应,cdev_del去找是否有这
//个记号就行。
(2)从内存角度体会cdev_alloc用与不用的差别
(3)这就是非面向对象的语言和面向对象的代码
2.5、cdev_init的替代
有时候会用 cdev->owner = THIS_MODULE;
cdev->ops = fops; 替代此函数
因为cdev_init主要做的事情就是这两个,而头插链表,清数据等在cdev_alloc也做过,所以这里不用做了。
3.字符设备驱动注册代码分析
3.1、老接口分析
register_chrdev
__register_chrdev
__register_chrdev_region
cdev_alloc
cdev->owner = fops->owner;
cdev->ops = fops;
cdev_add //(干活的是kobj_map函数,cdev_del干活的是 kobj_unmap)
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
} //在老接口中,第二个参数就是0,所以只能指定主设备号,次设备号为0
//追到__register_chrdev_region函数内部去
static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
int minorct, const char *name) //如果第一个参数传的是0,就自动分配设备号
3.2、新接口分析
_chrdev_region
__register_chrdev_region
alloc_chrdev_region
__register_chrdev_region
4.自动创建字符设备驱动的设备文件
4.1、问题描述:
(1)整体流程回顾
(2)使用mknod创建设备文件的缺点(mknod也是应用程序,是一个命令)
(3)能否自动生成和删除设备文件
4.2、解决方案:udev(嵌入式中用的是mdev)
什么是udev?应用层的一个应用程序(busybox中的一个命令)
(2)内核驱动和应用层udev之间有一套信息传输机制(netlink协议)
insmod和mdev是隔离的,insmod向内核安装一个驱动,然后内核就会告诉mdev有个驱动被安装了,然后就会进行创建和分配设备文件,传给内核。
步骤:
(1) 应用层启用udev,在rcS文件中。 echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
内核驱动中使用相应接口(class_create和device_create)
(4)驱动注册和注销时信息会被传给udev,由udev在应用层进行设备文件的创建和删除
4.3、内核驱动设备类相关函数
(1)class_create
(2)device_create
4.4、编程实践
// 模块安装函数
static int __init chrdev_init(void)
{
int retval;
printk(KERN_INFO "chrdev_init helloworld init\n");
// 第1步:分配主次设备号
retval = alloc_chrdev_region(&mydev, 12, MYCNT, MYNAME);
if (retval < 0)
{
printk(KERN_ERR "Unable to alloc minors for %s\n", MYNAME);
goto flag1;
}
printk(KERN_INFO "alloc_chrdev_region success\n");
printk(KERN_INFO "major = %d, minor = %d.\n", MAJOR(mydev), MINOR(mydev));
// 第2步:注册字符设备驱动
pcdev = cdev_alloc(); // 给pcdev分配内存,指针实例化
//cdev_init(pcdev, &test_fops);
pcdev->owner = THIS_MODULE;
pcdev->ops = &test_fops;
retval = cdev_add(pcdev, mydev, MYCNT);
if (retval) {
printk(KERN_ERR "Unable to cdev_add\n");
goto flag2;
}
printk(KERN_INFO "cdev_add success\n");
// 注册字符设备驱动完成后,添加设备类的操作,以让内核帮我们发信息
// 给udev,让udev自动创建和删除设备文件
test_class = class_create(THIS_MODULE, "aston_class");
if (IS_ERR(test_class))
return -EINVAL;
// 最后1个参数字符串,就是我们将来要在/dev目录下创建的设备文件的名字
// 所以我们这里要的文件名是/dev/test
device_create(test_class, NULL, mydev, NULL, "test111");
// 使用动态映射的方式来操作寄存器
if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
// return -EINVAL;
goto flag3;
if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0CON"))
// return -EINVAL;
goto flag3;
pGPJ0CON = ioremap(GPJ0CON_PA, 4);
pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);
*pGPJ0CON = 0x11111111;
*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮
return 0;
release_mem_region(GPJ0CON_PA, 4);
release_mem_region(GPJ0DAT_PA, 4);
flag3:
cdev_del(pcdev);
flag2:
unregister_chrdev_region(mydev, MYCNT);
flag1:
return -EINVAL;
}
// 模块卸载函数
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);
device_destroy(test_class, mydev);
class_destroy(test_class);
// 使用新的接口来注销字符设备驱动
// 注销分2步:
// 第一步真正注销字符设备驱动用cdev_del
cdev_del(pcdev);
// 第二步去注销申请的主次设备号
unregister_chrdev_region(mydev, MYCNT);
}
5.设备类相关代码分析
5.1、sys文件系统简介
(1)sys文件系统的设计思想
让应用层可以和内核交互
发现class,bus中的文件是devices的软链接文件
(2) 设备类的概念
cd /sys/class有很多的设备类型,我们运行写好的程序在开发板中,就有一个我们新建的类,aston_class,进入这个类,有我们新建的设备文件test,进入test会有4个文件夹,cat每一个都会有不同的效果。
(3)/sys/class/xxx/中的文件的作用
5.2、
(1)class_create
__class_create
__class_register
kset_register
kobject_uevent(内核发生一个事件后向udev发送一个uevent事件,要么是装了一个驱动,要么是卸载一个驱动,udev会根据这个情况创建或者删除一个设备)
(2)device_create(支持可变参数,主要目的是形成设备后面的数字)
device_create_vargs
kobject_set_name_vargs
device_register
device_add
kobject_add(以下函数对应着dev、power、subsystem、uevent文件)
device_create_file
device_create_sys_dev_entry
devtmpfs_create_node
device_add_class_symlinks
device_add_attrs
device_pm_add
kobject_uevent
(3)在设备下添加文件的函数详解
1.
device_create_file(dev, &devt_attr); //给dev文件添加属性:
static struct device_attribute devt_attr =
__ATTR(dev, S_IRUGO, show_dev, NULL);
追到static ssize_t show_dev(struct device *dev, struct device_attribute *attr,
char *buf)
{
return print_dev_t(buf, dev->devt);
}
#define print_dev_t(buffer, dev) sprintf((buffer), "%u:%u\n", MAJOR(dev), MINOR(dev))
我们cat dev的时候最终就是执行了print_dev_t这个宏。
2.
device_add_class_symlinks(dev)
sysfs_create_link(&dev->kobj, &dev->class->p->class_subsys.kobj, "subsystem");
6.静态映射表建立过程分析
6.1、建立映射表的三个关键部分
(1)映射表具体物理地址和虚拟地址的值相关的宏定义
(2)映射表建立函数。该函数负责由(1)中的映射表来建立linux内核的页表映射关系。
在kernel/arch/arm/mach-s5pv210/mach-smdkc18.c中的smdkc110_map_io函数
smdkc110_map_io
s5p_init_io
iotable_init
结论:经过分析,真正的内核移植时给定的静态映射表在arch/arm/plat-s5p/cpu.c中的s5p_iodesc,本质是一个结构体数组,(将来要添加其他的地址映射就在这个结构体添加)数组中每一个元素就是一个映射,这个映射描述了一段物理地址到虚拟地址之间的映射。这个结构体数组所记录的几个映射关系被iotable_init所使用,该函数负责将这个结构体数组格式的表建立成MMU所能识别的页表映射关系,这样在开机后可以直接使用相对应的虚拟地址来访问对应的物理地址。
(3)开机时调用映射表建立函数
问题:开机时(kernel启动时)smdkc110_map_io怎么被调用的?
start_kernel
setup_arch
paging_init
devicemaps_init
if (mdesc->map_io)
mdesc->map_io();
MACHINE_START(SMDKV210, "SMDKV210")
/* Maintainer: Kukjin Kim <kgene.kim@samsung.com> */
.phys_io = S3C_PA_UART & 0xfff00000,
.io_pg_offst = (((u32)S3C_VA_UART) >> 18) & 0xfffc,
.boot_params = S5P_PA_SDRAM + 0x100,
//.fixup = smdkv210_fixup,
.init_irq = s5pv210_init_irq,
.map_io = smdkc110_map_io,
.init_machine = smdkc110_machine_init,
.timer = &s5p_systimer,
MACHINE_END
所以map_io()就是smdkc110_map_io。
7.动态映射结构体方式操作寄存器
7.1、问题描述
(1)仿效真实驱动中,用结构体封装的方式来进行单次多寄存器的地址映射。
7.2、实践编码
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <mach/regs-gpio.h>
#include <mach/gpio-bank.h> // arch/arm/mach-s5pv210/include/mach/gpio-bank.h
#include <linux/string.h>
#include <linux/io.h>
#include <linux/ioport.h>
typedef struct GPJ0REG
{
volatile unsigned int gpj0con;
volatile unsigned int gpj0dat;
}gpj0_reg_t;
#define MYMAJOR 200
#define MYNAME "testchar"
#define GPJ0CON S5PV210_GPJ0CON
#define GPJ0DAT S5PV210_GPJ0DAT
#define rGPJ0CON *((volatile unsigned int *)GPJ0CON)
#define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)
#define GPJ0_REGBASE 0xe0200240
gpj0_reg_t *pGPJ0REG;
int mymajor;
char kbuf[100]; // 内核空间的buf
static int test_chrdev_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "test_chrdev_open\n");
rGPJ0CON = 0x11111111;
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮
return 0;
}
static int test_chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "test_chrdev_release\n");
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
return 0;
}
// 模块安装函数
static int __init chrdev_init(void)
{
printk(KERN_INFO "chrdev_init helloworld init\n");
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);
// 2步完成了映射
if (!request_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t), "GPJ0REG"))
return -EINVAL;
pGPJ0REG = ioremap(GPJ0_REGBASE, sizeof(gpj0_reg_t));
// 映射之后用指向结构体的指针来进行操作
// 指针使用->结构体内元素的方式来操作各个寄存器
pGPJ0REG->gpj0con = 0x11111111;
pGPJ0REG->gpj0dat = ((0<<3) | (0<<4) | (0<<5)); // 亮
return 0;
}
// 模块下载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
pGPJ0REG->gpj0dat = ((1<<3) | (1<<4) | (1<<5));
// 解除映射
iounmap(pGPJ0REG);
release_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t));
// 在module_exit宏调用的函数中去注销字符设备驱动
unregister_chrdev(mymajor, MYNAME);
}
7.3、分析和总结
8.内核提供的读写寄存器接口
8.1、前面访问寄存器的方式
(1)行不行
(2)好不好
因为ARM架构是内存于IO统一编址,但是内核希望有可移植性,比方说能兼容x86这种内存与IO独立编址的架构。
8.2、内核提供的寄存器读写接口
(1)writel和readl
(2)iowrite32和ioread32
8.3、代码实践
static int __init chrdev_init(void)
{
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);
// *pGPJ0CON = 0x11111111;
// *pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮
// 测试1:用2次ioremap得到的动态映射虚拟地址来操作,测试成功
// writel(0x11111111, pGPJ0CON);
// writel(((0<<3) | (0<<4) | (0<<5)), pGPJ0DAT);
// 测试2:用静态映射的虚拟地址来操作,测试成功
// writel(0x11111111, GPJ0CON);
// writel(((0<<3) | (0<<4) | (0<<5)), GPJ0DAT);
// 测试3:用1次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);
return 0;
}
8.4、分析和总结