继上一篇驱动入门的进一步深入
*********************
注册字符设备驱动新接口
老接口:
ret = register_chrdev(MYMAJOR,MYNAME,&test_fops);
包括主设备号、名字、file_operations
新接口:
register_chrdev_region/alloc_chrdev_region + cdev_xx
register_chrdev_region:
需要指定设备号
alloc_chrdev_region :
内核分配设备号
cdev_xx:
包括cdev_init 、 cdev_add 、cdev_del等操作
设备号:
MKDEV、MAJOR、MINOR三个宏
MKDEV 返回值类型:dev_t类型
***************************************
指定设备号方法:
*
首先,定义static dev_t dev_id;
先用MKDEV申请一个设备号:
dev_id = MKDEV(MYMAJOR, 0); //设备号
MYMAJOR是指定的主设备号
后面的是次设备号
然后注册:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
retval = register_chrdev_region(dev_id, MYCNT,MYNAME);
if (retval) {
printk(KERN_ERR "Unable to register minors for usb_device\n");
return retval;
}
用返回的dev_id
MYCNT:注册的数量
MYNAME:注册的名字
retval:返回值成功是0 ,失败返回负数
然后,注册驱动:
static struct cdev test_cdev;
static const struct file_operations test_fops
cdev_init(&test_cdev, &test_fops); //绑定两个结构体
retval = cdev_add(&test_cdev, dev_id, 1); //添加驱动
if (retval)
cdev_del(&test_cdev);
最后,注销
unregister_chrdev_region(dev_id, 1);
**************************************
上面就是完整的用新接口注册驱动的步骤
***********************
用内核自动分配设备号:
自动分配的话就不需要先用MKDEV申请设备号:
dev_id = MKDEV(MYMAJOR, 0); //设备号
而是直接用定义的结构体变量加上定义定义的子设备号,加上名字
就可以完成自动分配设备号
typedef u_long dev_t;(源结构体)
static dev_t mydev;
// 第1步:分配主次设备号
retval = alloc_chrdev_region(&mydev, 12, MYCNT, MYNAME);
int alloc_chrdev_region(dev_t *dev, u[nsigned baseminor, unsigned count,
const char *name)
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}
*****************
这里对led灯的驱动操作
在模块层次代码中,应该要简洁,及对read进来的相关指令,这个指令应该是
简单的,例如1、2、3这样的,可以用宏定义
就是说,较为复杂的一些操作,例如对输入的指令的判断,用到的
strcmp等,应该在应用层完成
**********************
这里是对设备注册的一个操作
之前注册驱动时用到的是一个全局变量的结构体,
占据固定内存
static struct cdev test_cdev;
现在,用到
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
if (p) {
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj, &ktype_cdev_dynamic);
}
return p;
}
这一个函数,可以设置一个struct cdev类型的指针
struct cdev *cdev;
static struct cdev *test_cdev;
//注册驱动,使用cdev_alloc
test_cdev = cdev_alloc();
if (!test_cdev)
return -EINVAL;
test_cdev->owner = THIS_MODULE;
test_cdev->ops = test_fops;
//cdev_init(&test_cdev, &test_fops); //绑定两个结构体
这里使用的*指针的意义就是,用全局变量指定一个指针,只占一个指针大小的内存,
当需要使用时,就用cdev_alloc为指针动态分配一个堆内存,指向用的结构体,
需要注意的就是,在内核中,堆内存的释放一般都不是简单的对应关系,
它是内部有一个数的增加机制,有用到申请堆内存的就加一,用完就减一,
目前还不太了解其中机制
释放函数:
kfree(cls);
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
这个就是用到的结构体,里面有指向模块、文件操作、链表、设备等
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
这里不使用cdev_init,
而是直接实例化结构体其中的几个,
实际上也是和上面的函数是一样的,只不过是少实例几个而已
还有就是,上面实例化的owner
是在file_operation中的那个,
.owner = THIS_MODULE,
********************
总结:
以上的就是关于设备号的新旧使用,手动/自动分配使用,
关于设备注册的函数使用,
一个是全局变量直接定义,一个是定义一个全局变量的指针使用
关于设备的初始化,
可以用init函数,也可以直接实例化
*************
解决每次需要手动增添设备文件的不足:
手动添加设备文件:
[root@sugar /root]# mknod /dev/test c 250 0
解决方案:
udev(嵌入式中使用mdev)
udev 是 Linux 系统中的一个子系统,用于管理设备事件。
mdev :
BusyBox 提供的轻量级设备管理工具,相当于精简版的 udev。
rootfs中Etc/init.d/rcS文件
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
作用:初始化 /dev 目录下的设备节点。
原理:
-s 参数让 mdev 扫描 /sys 文件系统,根据当前已连接的硬件信息,
在 /dev 目录下动态创建设备节点(如 /dev/sda、/dev/ttyUSB0 等)
作用:将 /sbin/mdev 设置为内核的 热插拔事件处理程序。
补充:
/proc/sys/kernel/hotplug 的特殊性:
该文件存在于内存中(procfs),重启后失效,
需在启动脚本(如 /etc/rc.local)中持久化配置
完整 Linux 系统通常使用 udev 或 systemd-udevd,
但 mdev 更适合资源受限的嵌入式环境
总结:
这两条命令共同实现了 设备节点的动态创建/删除,
是嵌入式 Linux 自动识别硬件的基础
udev是应用层的一个应用程序
内核驱动和应用层udev之间有一套信息传输机制(netlink协议)
应用层启动udev,内核驱动中使用相应接口
驱动注册和注销时信息会传给udev,由udev在应用层进行设备
文件的创建和删除
使用相关函数添加设备文件:
1、class_create
//源定义源函数
static struct class *adb_dev_class;
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
owner:fos中的owner,指定类的所有者是哪个模块
name:自定义的class名字
//创建类
test_dev_class = class_create(THIS_MODULE, "sugar_class");
if (IS_ERR(test_dev_class))
return -EINVAL;
//结束时销毁
void class_destroy(struct class *cls)
{
if ((cls == NULL) || (IS_ERR(cls)))
return;
class_unregister(cls);
}
class_destroy(test_dev_class);
2、device_create
//设备文件创建,就是mknod 创建的文件名
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
{
va_list vargs;
struct device *dev;
va_start(vargs, fmt);
dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
va_end(vargs);
return dev;
}
device_create(test_dev_class, NULL, dev_id, NULL, "test");
void device_destroy(struct class *class, dev_t devt)
{
struct device *dev;
dev = class_find_device(class, NULL, &devt, __match_devt);
if (dev) {
put_device(dev);
device_unregister(dev);
}
}
device_destroy(test_dev_class,dev_id);
************************
上面的使用就是一个设备类的创建,
类中的一个设备文件的创建(就是mknod创建的设备文件的名字)
结合:
创建:
class_create()
device_create()
销毁:
device_destroy()
class_destroy()
*********************
成功现象:
[root@sugar /root]# insmod module_test.ko
[ 39.180515] major = 250, minor = 0.
[root@sugar /root]# lsmod
Not tainted
module_test 2102 0 - Live 0xbf000000
[root@sugar /root]# ls /dev/test -l
crw-rw---- 1 root 0 250, 0 Jan 1 12:00 /dev/test
[root@sugar /root]# ./app
直接安装执行,不需要再手动mknod创建设备文件
insmod后直接查看也可以查看到device_destroy中输入的test文件
******************
查看创建的class
[root@sugar /sys]# cd class/
[root@sugar class]# ls
sugar_class
[root@sugar class]# cd sugar_class/
[root@sugar sugar_class]# ls
test
[root@sugar sugar_class]# cd test/
[root@sugar test]# ls
dev power subsystem uevent
用cat查看,可以查看里面的相关信息
[root@sugar test]# cat uevent
MAJOR=250
MINOR=0
DEVNAME=test
[root@sugar test]# cat dev
250:0
函数代码分析:
class_create()
__class_create(owner, name, &__key); //实际调用
cls = kzalloc(sizeof(*cls), GFP_KERNEL);//申请堆内存
__class_register //注册
cp = kzalloc(sizeof(*cp), GFP_KERNEL);
kset_register
kobject_uevent
struct kobject *kobj //输出型参数,作为返回值
返回的就是kobject结构体的相应参数
struct kobject {
const char *name;
struct list_head entry;
struct kobject *parent;
struct kset *kset;
struct kobj_type *ktype;
struct sysfs_dirent *sd;
struct kref kref;
unsigned int state_initialized:1;
unsigned int state_in_sysfs:1;
unsigned int state_add_uevent_sent:1;
unsigned int state_remove_uevent_sent:1;
unsigned int uevent_suppress:1;
};
kfree(cls);
device_create()
device_create_vargs
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
dev->devt = devt;
dev->class = class;
dev->parent = parent;
dev->release = device_create_release;
dev_set_drvdata(dev, drvdata);
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
bus_add_device
dpm_sysfs_add
device_pm_add //power management
kobject_uevent //输出结构体相关参数
kfree(dev->p);
*************************
以上就是两个类和设备文件的函数分析
***********************
新旧字符设备号的注册:
register_chrdev
register_chrdev_region/alloc_chrdev_region
register_chrdev
__register_chrdev(major, 0, 256, name, fops);//实际调用
__register_chrdev_region(major, baseminor, count, name);
if (major == 0) {
for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
if (chrdevs[i] == NULL)
break;
}//这里的处理就是解释到为什么设备号的分配从后往前分配
register_chrdev_region
__register_chrdev_region(MAJOR(n), MINOR(n),
next - n, name);
alloc_chrdev_region
__register_chrdev_region(0, baseminor, count, name);
总结:
实际上,三个都是调用的__register_chrdev_region这个函数,
只不过是里面的传进去的参数的定义不同
新的这个两个主要是设备号定义,为0
旧的是无法定义子设备号
*************************
静态页表的分析:
之前对寄存器的操作有:
静态映射、动态映射两种方法
静态映射:
使用的就是自定义好的寄存器地址相关的虚拟地址
其中,涉及到的一个就是静态映射表,
有了这个表才能实现一段物理地址到虚拟地址的映射关系
1、映射表涉及到的物理地址和虚拟地址的值的相关宏定义
2、映射表的建立函数
/kernel/arch/arm/mach-s5pv210/mach-smdkc110.c
smdkc110_map_io
s5p_init_io(NULL, 0, S5P_VA_CHIPID);
iotable_init(s5p_iodesc, ARRAY_SIZE(s5p_iodesc));
create_mapping
上面函数涉及到的就是io表的初始化,创建映射
iotable_init中涉及到的映射有s5p_iodesc
这是一个结构体数组,数组中的每一个元素就是一个映射,
描述了一段物理地址到虚拟地址之间的映射
表中记录的这些映射关系被iotable_init调用,这个函数负责将这个结构体
数组格式的表建立称MMU所能识别的页表映射关系,这样开机后就可以直接
使用对应的虚拟地址访问对应的物理地址
其中,页表显示都是4k、8k、16k...
arch/arm/plat-s5p/cpu.c
映射表对应的结构体数组:
/* Initial IO mappings */
static struct map_desc s5p_iodesc[] __initdata = {
{
{
.virtual = (unsigned long)S5P_VA_GPIO,
.pfn = __phys_to_pfn(S5P_PA_GPIO),
.length = SZ_4K,
.type = MT_DEVICE,
},
这块就是我们要操作的gpio对应的映射关系转换模块
还有一个问题,就是开机时调用映射表建立函数
开机时(kernel启动时)smdkc110_map_io怎么被调用的
start_kernel(main.c)
setup_arch(573)
paging_init
devicemaps_init
/*
* Ask the machine support to map in the statically mapped devices.
*/
if (mdesc->map_io)
mdesc->map_io();
************************
动态映射
这里解决一个问题,就是动态映射对多个寄存器需要多次映射的问题
解决:
仿效真实驱动中,用结构体封装的方式来进行单次多个寄存器的地址映射
这样就可以不用多次进行单个寄存器的映射了
//定义一个结构体包含两个寄存器
typedef struct GPJ0REG{
volatile unsigned int gpj0_con;
volatile unsigned int gpj0_dat;
}gpj0_refs;
注意:
定义结构体不能用static,
如果我们在文件作用域内使用`static`关键字修饰一个结构体定义,
这是不允许的,编译器会报错。
因为结构体定义本身并不占用存储空间,它只是一个类型描述
还有,结构体内定义的变量不要与外部变量名字有冲突,否则有错
#define GPJ0_PA_BASE 0xe0200240
gpj0_refs *pGPJ0_REG;
//用两步完成动态映射
if (!request_mem_region(GPJ0_PA_BASE, sizeof(gpj0_refs), "GPJ0REG"))
return -EINVAL;
pGPJ0_REG=ioremap(GPJ0_PA_BASE, sizeof(gpj0_refs));
//销毁
iounmap(pGPJ0_REG);
release_mem_region(GPJ0_PA_BASE, sizeof(gpj0_refs));
**********************************
最后,是一个writel、readl函数
用来操作寄存器(当完成映射后)
writel()
#define writel(b,addr) __raw_writel(__cpu_to_le32(b),addr)
__raw_writel
#define __raw_writel(v,a) (__chk_io_ptr(a), *(volatile unsigned int __force *)(a) = (v))
这个就是实际工作的函数
实质就是我们之前设置的一个定义即操作的寄存器地址+要进行的操作
#define rGPJ0CON *((volatile unsigned int *)S5PV210_GPJ0CON)
#define rGPJ0DAT *((volatile unsigned int *)S5PV210_GPJ0DAT)
#define GPJ0CON_PA 0xe0200240
static void __iomem *baseaddr; // 寄存器的虚拟地址的基地址
通过ioremap 得到虚拟地址
writel(0x11111111, baseaddr+0);
writel(((0<<3) | (0<<4) | (0<<5)), baseaddr+4);
iowrite32()
#define iowrite32(v, addr) writel((v), (addr))
writel
实际就是上面的函数
************************
至此,可以完成一个led的驱动
其中,包括使用
1、新旧设备号注册函数、驱动的alloc申请(alloc_chrdev_region)
2、对test_cdev的一个操作,一个是用全局变量做全部的初始化,
一个使用一个全局指针,后面对齐实例化操作(替换一个cdev_init()函数)
//static struct cdev test_cdev;
static struct cdev *test_cdev;
3、类的创建
目的就是不用手动mknod取创建设备文件
class_create
device_create
4、对动态映射的申请优化,使用一个指针接收虚拟地址映射,
可以用结构体定义其中的寄存器,亦可以自己在地址中加上相应的偏移量
request_mem_region
ioremap
5、对寄存器的操作
iowrite32
writel