驱动入门的进一步深入

继上一篇驱动入门的进一步深入

*********************

注册字符设备驱动新接口

老接口:

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 (chrdevsi == 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

相关推荐
A小辣椒2 天前
TShark:Wireshark CLI 功能
linux
A小辣椒2 天前
TShark:基础知识
linux
AlfredZhao2 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao3 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334663 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪3 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠4 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush44 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5204 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩4 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言