测试给andorid编写驱动

来源:互联网 发布:音频频谱分析软件 编辑:程序博客网 时间:2024/05/22 17:22

一、设备与驱动

本文中会创建一个字符设备(源代码),放在 /dev 下面,设备里面为一块内存,里面可以存储一定的内容。如果以写方式打开设备的话,可以往设备里面写入数据,当返回 -ENOSPC 时,表示设备已满,无法再写入。如果以读方式打开设备的话,可以从设备里面读取数据,每次都是从头开始读取,如果没有更多内容的时候,返回0,表示结束。

1. 设备驱动代码的基本框架

#include <linux/init.h>#include <linux/device.h>#include "membuff.h"static int __init membuff_init(void) {    return 0;   }static void __exit membuff_exit(void) {}MODULE_LICENSE("GPL")MODULE_DESCRIPTION("Memory Buffer Driver")module_init(membuff_init);module_exit(membuff_exit);

MODULE_LICENSE 用来告诉内核这个模块所使用的授权协议,如果没有这个声明的话,内核在加载这个模块的时候,就会报错。

MODULE_DESCRIPTION 用来展示一个人类可读的文本,描述该模块的用处。

module_initmodule_exit 用来指定模块被加载和被卸载时,内核需要调用的函数,__init__exit 用来表示这两个函数只能在加载或者卸载的时候使用,在其他场景下使用都会报错。

2. 初始化设备

① 注册主次设备号

主设备号用来关联该设备所使用的驱动程序,可以多个设备使用同一个驱动程序,但是一般情况下都是一个设备对应于一个驱动程序。而次设备号,驱动程序用来区分具体的设备。

主次设备号可以静态分配,也可以动态分配。静态分配的设备号,一般用于非常常见的设备,少数人使用的设备需要使用动态分配,否则容易和其他人分配的设备号冲突。

动态分配逻辑如下:

err = alloc_chrdev_region(&dev, 0, 1, MEMORY_BUFF_NODE_NAME);if (err < 0) {    printk(KERN_ALERT"Failed to alloc char dev region.\n");    goto fail;}devMajor = MAJOR(dev);devMinor = MINOR(dev);

alloc_chrdev_region 用来动态分配一定范围的设备号,原型如下:

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,                             unsigned int count, char *name);

dev 是一个输出参数,在成功的时候,返回设备号范围的第一个数。firstminor 用来指定 minor 的第一个数值,一般都为0。count 指定需要分配的范围。name 指定设备的名称。

② 添加设备

内核内部使用 struct cdev 来表示一个字符设备,因此添加设备之前需要先构造这样一个结构体,可以通过下面的方式分配及初始化:

struct cdev *my_cdev = cdev_alloc();my_cdev->ops = &my_fops;

除了使用动态分配 cdev,还可以将 cdev 放在其他结构体里面,但是这个时候,就得使用下面的函数来初始化了:

void cdev_init(struct cdev *cdev, struct file_operations *fops);

ops 待会介绍,初始化完了后,可以通过 cdev_add 将这个设备注册到系统中:

devno = MKDEV(membuf_device_major, membuf_device_minor);err = cdev_add(my_cdev, devno, 1);if (err) {    printk(KERN_ALERT"Failed to add char device.\n");    goto cleanup;}

cdev_add 的第二个参数是指定该设备对应的第一个设备号,count 指定该设备有多少个设备号,通常情况下是一个。调用 cdev_add 需要注意两点:

  1. cdev_add 可能会调用失败,所以需要检查返回值
  2. 一旦 cdev_add 调用成功,那么该设备就以及添加到系统中了,其对应的操作可以被调用,因此需要保证在此之前所有相关的逻辑都执行完了

③ 文件操作

目前,我们已经将设备准备好了,但是对文件的操作(比如 readwrite)是怎么与设备关联起来的呢?

应用程序调用 readwrite 访问文件的时候,会先进入系统调用,最后转到设备驱动的相关函数。系统调用是通过字符设备结构体 struct cdev 中的 ops 来找到对应的设备相关的操作。

ops 是一个 file_operations 类型,file_operations 结构体的大多数成员是一个函数指针,对应于这个设备中实现特定操作的函数,如果成员为空的话,表示不支持特定的操作。

介绍下本文中使用的几个字段:

struct module *owner

file_operations 的第一个字段不是一个操作,而是用来指定哪个模块用于该结构体,大多数情况下,这个成员被初始化为 THIS_MODULE

int (*open) (struct inode *, struct file *);

虽然对于文件来说,这个操作通常是第一个执行的,但是驱动并不是必须实现该操作。如果这个成员为 NULL,打开这个设备的话,就一定是成功的,但是对应的驱动程序并不会得到通知。

int (*release) (struct inode *, struct file *);

这个操作在 file 结构体被释放的时候被调用,和 open 一样,这个成员可以为 NULL

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

用来从设备中获取数据。如果这个成员为空的话,read 系统调用将会失败,并返回 -EINVAL。返回非负数代表本次调用成功读到的字节数。

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

将数据写入到设备中。如果为空的话,write 系统调用也返回 -EINVAL。返回值如果为非负数,表示成功写入的字节数。

membuff 的设备驱动只实现了几个重要的操作,它的 file_operations 结构体初始化代码如下:

static struct file_operations membuf_ops = {    .owner = THIS_MODULE,    .open = membuf_open,    .release = membuf_release,    .read = membuf_read,    .write = membuf_write};

④ file 结构体

struct file,定义在 <linux/fs.h> 中,在设备驱动程序中是相当重要的数据结构。需要注意的是,struct fileFILE 是不一样的,FILE 是定义在C语言库中,不会出现在内核代码中。而 struct file 是内核的数据结构,不会出现在用户程序中。

struct file 代表一个被打开的文件,它与设备驱动无特定关联。它是内核在调用 open 的时候创建的,在所有和文件相关的函数中作为参数使用,直到调用 close

介绍下几个比较重要的成员:

mode_t f_mode;

通过设置 FMODE_READFMODE_WRITE 表示该文件的打开模式,你可能会需要在 open 或者 ioctl 函数里面检查是否有读写的权限,但是在调用 readwrite 的时候不需要检查,因为内核会在调用你的函数之前检查。尝试读写没有对应权限的文件的时候,会被直接拒绝,驱动程序根本不会知道。

loff_t f_pos;

当前的读写位置,loff_t 在所有平台上都是一个64位的值。驱动程序如果需要知道当前位置的话,可以读该值,但是正常情况下,不能去修改它;readwrite 通过设置它们最后一个参数的值来更新文件位置,而不应该直接修改 f_pos。这一点对于 llseek 方法的话不适用,因为它就是用来修改文件位置的。

struct file_operations *f_op;

这个文件对应的操作。内核在它的 open 实现中对该值进行了赋值,当内核在派发操作的时候读取该字段。内核后续的引用该成员不会再修改它,也就意味着可以修改该文件关联的操作。这一点可以用在同一个主设备号,不同次设备号定义不同的行为。

void *private_data;

open 系统调用在调用驱动程序的 open 函数的时候,会将这个指针赋值为 NULL。这个指针可以忽略不用,也可以用来保存分配的内存,但是必须记得在 release 的时候将其释放。

⑤ inode 结构体

inode 结构体是内核用来表示一个文件。因此,它和 file 结构体是不一样的,file 结构体是用来表示一个打开的文件描述符。同一个文件可以有很多 file 结构,来表示被打开的文件描述符,但是它们都指向都一个 inode 结构。

inode 结构体里面包含很多文件相关的信息。通常情况下,写设备驱动的时候,只对两个成员有兴趣:

dev_t i_rdev;

对于描述设备文件的 inode,这个成员包含实际的设备号。

struct cdev *i_cdev;

struct cdev 是内核内部用来描述字符设备的结构;当 inode 描述的是一个字符设备文件时,该成员指向内核中的结构。

⑥ 构造驱动所使用的结构体

由于 membuf 使用内存来表示一个文件,所以需要在某些地方将内存的指针保存起来。通常的做法是构造一个结构体,用来保存和设备相关的信息:

struct membuf_dev {    char* pBuffer;    size_t bufLen;    size_t wroteLen;    struct cdev dev;    struct semaphore sema;};

inode 结构中的 i_cdev 成员指向的是内核中的 cdev 结构,而内核中的 cdev 是由驱动在调用 cdev_add 的时候设置的,所以通常做法是在设备初始化的时候,分配一个 membuf_dev 结构体,然后调用 cdev_add 的时候,传入的是该结构体的成员的地址:

pMembufDev = kmalloc(sizeof(struct membuf_dev), GFP_KERNEL);...memset(pMembufDev, 0, sizeof(struct membuf_dev));...cdev_init(&(pMembufDev->dev), &membuf_ops);pMembufDev->dev.owner = THIS_MODULE;pMembufDev->dev.ops = &membuf_ops;devno = MKDEV(devMajor, devMinor);err = cdev_add(&(pMembufDev->dev), devno, 1);

后续在调用 open 的时候,通过 inode 参数里面的 i_cdev 的地址,并进行一定的位移,计算出 struct membuf_dev 的地址,然后将其保存到 struct file 中的 private_data 成员,后续的 readwrite 操作通过 private_data 就可以拿到设备相关的信息,具体逻辑见打开文件。

由于对地址进行偏移,看起来是比较取巧的方法,有时候可能会产生误解,因此内核提供了 container_of 宏,其实质也就是对地址进行偏移来拿到整个结构体的地址,原型如下:

container_of(pointer, container_type, container_field);

⑦ 创建设备

大多数情况下,都是给硬件设备做驱动,因此设备是已经存在的,但是这里的设备需要自己来创建。创建设备也很简单,可以通过 mknod 来创建字符设备,但是需要在命令行下来创建,如果是在 android 平台,还不一定有这个命令。

这里通过函数来直接创建,在设备驱动初始化的时候创建设备,反初始化的时候移除设备。

pMembufClass = class_create(THIS_MODULE, MEMORY_BUFF_CLASS_NAME);if (IS_ERR(pMembufClass)) {    err = PTR_ERR(pMembufClass);    goto destroy_cdev;}temp = device_create(pMembufClass, NULL, devno, "%s", MEMORY_BUFF_FILE_NAME);if (IS_ERR(temp)) {    err = PTR_ERR(temp);    goto destroy_class;}

3. 释放设备

释放设备的逻辑和初始化逻辑相对应,但是没有初始化那么复杂。只需要将分配的内存和设备号都释放掉即可,代码如下:

static void __exit membuff_exit(void) {    dev_t devno = MKDEV(devMajor, devMinor);    if (pMembufClass) {        device_destroy(pMembufClass, devno);        class_destroy(pMembufClass);    }    if (pMembufDev) {        cdev_del(&(pMembufDev->dev));        kfree(pMembufDev->pBuffer);        kfree(pMembufDev);    }    unregister_chrdev_region(devno, 1);}

4. 打开设备

应用程序在打开一个文件的时候,传入的是一个路径,经过转换后,最终传入驱动的时候,是一个 inode 参数。由于可能存在一个驱动程序对应于多个设备,所以在 open 的时候需要将设备相关的信息关联到 file,这样后续的读写操作能够通过 private_data 拿到文件所关联的设备。

static int membuf_open(struct inode* inode, struct file* filp) {    struct membuf_dev* pMembufDev;    pMembufDev = container_of(inode->i_cdev, struct membuf_dev, dev);    filp->private_data = pMembufDev;    return 0;}

5. 编译

为了将新写的驱动编进到android中,需要先将android系统及内核都编译一遍,建议使用ubuntu进行编译,编译流程见这里。

编译完成后,假设内核源代码目录名为 common ,在 common/drivers 下面创建一个目录 membuff,将源代码放到里面。

增加 Makefile 文件,内容为:

obj-$(CONFIG_MEMBUFF) += membuff.o

增加 Kconfig 文件,内容为:

config MEMBUFF    tristate "memory buffer driver"    default n    help    This is a memory buffer driver.

编译内核的原则是 TANSTAAFL(There Ain’t No Such Thing As A Free Lunch),任何编译进内核的内容都会增加它的大小,就算选的是以模块方式编译也是如此。所以编译内核的时候,需要配置需要编译哪些内容。

上面的 Kconfig 就是用来配置 membuff 的,第一行定义下面的内容是定义哪一项的,前缀 CONFIG_ 是省略了。 tristate 表示它可以编译进内核(Y),也可以作为模块编译(M),也可以不编译(N)。第三行指定了默认值为不编译。

除此之外还需要修改 arch/arm/Kconfigdrivers/kconfig 两个文件,在 menu “Device Drivers”endmenu 之间添加一行:

source "drivers/hello/Kconfig"

然后在 drivers/Makefile 文件中,添加一行:

obj-$(CONFIG_HELLO) += hello/

添加完成后,就可以通过修改 common/.config,将里面的

# CONFIG_MEMBUFF is not set

改为

CONFIG_MEMBUFF=y

如果没有 .config 文件,说明之前没有编译过内核,应该需要按照上面的指引编译一个与android系统相匹配的内核。比如如果要给 ARMv7 的模拟器编译内核,可以通过 make goldfish_armv7_defconfig 生成 ARMv7 对应的内核编译配置文件 .config

编译完成后,就可以在启动模拟器的时候,指定使用新的内核了:

emulator -kernel common/arch/arm/boot/zImage
0 0
原创粉丝点击