初入android驱动开发之字符设备(一)

大学毕业,初入公司,招进去的是android驱动开发工程师的岗位,那时候刚进去,首先学到的就是如何搭建kernel、android的编译环境,然后就是了解如何刷设备以及一些最基本的工具。如adb、fastboot、grep、minicom、kermit、svn、git、eclispe、ndk等相关的知识,记得那时候很挫,过去很多东西都不懂。到了那,一周,都是熟悉使用ubuntu,然后了解刷机的流程,了解uboot、kernel、ramdisk、recovery、system的作用以及相关的框架,印象最深的是,就搞定刷机这个问题,都折腾了很久,原因之前的文章也说了,usb id 没有配好,因为android设备在开机状态和fastboot的模式下,usb id是不一样的。在开机状态下,可以通过adb shell 进入android系统,但一切换fastboot模式,就发现无法找到设备。

当初,学习驱动开发的第一步,就是点亮一个LED灯,当然是基于android系统的,不是裸版上操作。正所谓初生牛犊不怕虎,先把百度,网上多的是例子,很高兴,马上copy一份代码,修改修改,试一试,编译通过,然后按着说明步骤,一步一步操作,发现insmod led.ko的时候,加载不成功,没办法,继续百度,搞了半天,没找到问题所在,然后尝试静态的编进去,别说,成功加载了,在/dev下找到自己的驱动,灯也亮了。当时,觉得完成任务,也没有多考虑什么,就向师父说,搞定了。就这样,一步一步的学下去,平台设备驱动模型,帧缓冲设备,输入子系统,中断,并态竞争,并开始慢慢解bug,调模块,UHF,nfc,rfid,电池,3G,音频,扫描头,wifi等一些,也许由于时间较紧,或者更可能也是因为得过且过,觉得在这家公司也能生存下去,对一些细节、原理性的东西并未深究。比如,内核层的数组越界表现为设备整个重启,JNI层数组越界,可能从andorid重启,app出现问题,表现应用挂掉。现在,事情不太多,想着把以前学的东西,从新梳理一下,并且深入的跟一下,毕竟不能浮于表面,应该多学习学习。但因中途电脑出现故障,所存资料全报废了,只能挑一些当时印象比较深刻的问题,重新学习一下。这次主要讲一下字符设备。

1. 什么是字符设备?

字符设备是 3 大类设备(字符设备、块设备和网络设备)中较简单的一类设备,提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取。相反,此类设备支持按字节/字符来读写数据。

其驱动程序中完成的主要工作是初始化、添加和删除 cdev 结构体,申请和释放设备号,以及填充file_operations 结构体中的操作函数,实现file_operations 结构体中的read()、write()和ioctl()等函数是驱动设计的主体工作。

2. 字符设备的框架模型:

初入android驱动开发之字符设备(一)

(备注:此图片来源于:http://my.oschina.net/u/1169027/blog/191538)

3. 字符设备的重要的数据结构

3.1 一个简单的字符设备的例子:

  1. #include <linux/module.h>
  2. #include <linux/kernel.h>
  3. #include <linux/init.h>
  4. #include <linux/input.h>
  5. #include <linux/platform_device.h>
  6. #include <linux/miscdevice.h>
  7. static int first_drv_open(struct inode *inode, struct file *file)
  8. {
  9. printk("first_drv_open\n");
  10. return 0;
  11. }
  12. static int first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
  13. {
  14. printk("first_drv_write\n");
  15. return 0;
  16. }
  1. /*3. 这个结构是字符设备驱动程序的核心
  2. * 当应用程序操作设备文件时所调用的open、read、write等函数,
  3. * 最终会调用这个结构中指定的对应函数
  1. static struct file_operations first_drv_fops = {
  2. .owner  =   THIS_MODULE,
  3. .open   =   first_drv_open,
  4. .write  =   first_drv_write,
  5. };
  6. static struct class *firstdrv_class;
  7. static struct device *firstdrv_class_dev;
  8. int major;
  1. // 执行insmod命令时就会调用这个函数
  1. static int first_drv_init(void)
  2. {
  3. major = register_chrdev(0, "first_drv", &first_drv_fops);
  4. firstdrv_class = class_create(THIS_MODULE, "firstdrv");
  5. firstdrv_class_dev = device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "myhello");
  6. printk("add ko,/dev/myhello \n");
  7. return 0;
  8. }
  1. /*
  2. * 执行rmmod命令时就会调用这个函数 ,注销函数,主要是释放你在注册是申请的资源,与你注册顺序相反,先注册的后释放。
  3. */
  4. static void first_drv_exit(void)
  5. {
  6. unregister_chrdev(major, "first_drv");
  7. device_unregister(firstdrv_class_dev);
  8. class_destroy(firstdrv_class);
  9. printk("del ko, \n");
  10. }
  1. //1. 我们一般从入口函数看起,先找到该字符设备的入口函数,
  2. module_init(first_drv_init);
  3. module_exit(first_drv_exit);
  4. MODULE_LICENSE("GPL");

 3.2. 主要看入口函数:

在入口函数中,有3个主要的函数:几个重要的结构体:firstdrv_class,firstdrv_class_dev,cdev

先看一下 字符设备注册函数:
static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)        
  参数说明:

major:cdev的主设备号,此为0;

name:cdev的名称,此为 first_drv;

file_operation: cdev的文件操作接口,非常主要,一般为open、close、read、write、ioctl等,此只有open、write。

看下此函数如何调下去的:
__register_chrdev(major, 0, 256, name, fops);    这里可以看出,它把major的主设备号下的256个次设备号都归为此字符设备
   下面三个函数,是注册字符设备的3步:
   cd = __register_chrdev_region(major, baseminor, count, name);    
   cdev = cdev_alloc();    
   err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);

此中重要的结构体:

struct cdev *cdev;    
struct cdev {
struct kobject kobj;//内嵌的kobject对象    
struct module *owner;所属模块,通常为THIS_MODULE
const struct file_operations *ops;//文件操作结构体 
struct list_head list;
dev_t dev; //设备号    
unsigned int count;
};

然后看device_create,这是创建一个类,然后在类下创建一个设备,这个其实就是帮你在proc/ 和 dev/下创建设备节点,赋予相应的属性,

我们跟下代码,看看是如何调用的:

__class_create(struct module *owner, const char *name,struct lock_class_key *key)        
    __class_register(cls, key);    
    error = kset_register(&cp->subsys);    
    kobject_uevent(&k->kobj, KOBJ_ADD);    
    kobject_uevent_env(kobj, action, NULL);    
    /* environment buffer *//* 分配保存环境变量的内存 */
        env = kzalloc(sizeof(struct kobj_uevent_env), GFP_KERNEL);
        /* complete object path */
        devpath = kobject_get_path(kobj, GFP_KERNEL);
/* default keys */  /*设置环境变量 */        
            retval = add_uevent_var(env, "ACTION=%s", action_string);
        retval = add_uevent_var(env, "DEVPATH=%s", devpath);
        retval = add_uevent_var(env, "SUBSYSTEM=%s", subsystem);
/* 调用应用程序: 比如mdev */
        /* 启动脚本 echo /sbin/mdev > /proc/sys/kernel/
        * 设置了uevent_helper为“/sbin/mdev“
        */    
        argv [0] = uevent_helper;
         argv [1] = (char *)subsystem;
        argv [2] = NULL;
        retval = add_uevent_var(env, "HOME=/");
       retva=add_uevent_var(env,"PATH=/sbin:/bin:/usr/sbin:/usr/bin");
        retval = call_usermodehelper(argv[0], argv,env->envp, UMH_WAIT_EXEC);

再看一下设备创建:     
device_create()
    device_destroy(struct class *cls, dev_t devt);    
    dev = class_find_device(class, NULL, &devt, __match_devt);

编译的Makefile 文件:

# 下面这个很重要,指向你的内核路径,在编译完成后,会出现Module.symvers
#在内核源码树根目录中,其中的Module.symvers文件就包含了内核所有的导出符号以及所有编译后模块的导出符号。
#在编译内核时,根目录下会生成Module.symvers文件,它包含了内核以及编译后的模块导出的所有符号。对于每一个符号,相应的CRC校验值也被保存,
#当内核编译选项CONFIG_MODVERSIONS关闭时,所有的CRC值都为0x00000000。
#Module.symvers文件主要有以下用途:
#1.列出vmlinux和所有模块的导出函数
#2.列出所有符号的CRC校验值
#若不指向,则insmod 模块时,会不成功。

  1. KERN_DIR = /home/yl/workplace/svn4.0/urovo-sq39/samsung_android_kernel_3.0/
  1. all:
  2. make -C $(KERN_DIR) M=`pwd` modules
  3. clean:
  4. make -C $(KERN_DIR) M=`pwd` modules clean
  5. rm -rf modules.order
  6. obj-m   += first_drv.o

编译:

只需要配置好内核的交叉编译环境即可,

编译: make

清除: make clean

4、 字符设备驱动的加载、卸载、测试

编译:

  1. yl@yl-Lenovo:~/workplace/svn4.0/urovo-sq39/study$ make -j4
  2. make -C /home/yl/workplace/svn4.0/urovo-sq39/samsung_android_kernel_3.0/ M=`pwd` modules
  3. make[1]: Entering directory `/home/yl/workplace/svn4.0/urovo-sq39/samsung_android_kernel_3.0'
  4. make[1]: warning: jobserver unavailable: using -j1.  Add `+' to parent make rule.
  5. CC [M]  /home/yl/workplace/svn4.0/urovo-sq39/study/first_drv.o
  6. Building modules, stage 2.
  7. MODPOST 1 modules
  8. CC      /home/yl/workplace/svn4.0/urovo-sq39/study/first_drv.mod.o
  9. LD [M]  /home/yl/workplace/svn4.0/urovo-sq39/study/first_drv.ko
  10. make[1]: Leaving directory `/home/yl/workplace/svn4.0/urovo-sq39/samsung_android_kernel_3.0'

推送到设备的目录下,并加载设备:

获得root权限

  1. yl@yl-Lenovo:~/workplace/svn4.0/urovo-sq39/study$ adb root
  2. adbd is already running as root

加载驱动,并查看:

  1. yl@yl-Lenovo:~/workplace/svn4.0/urovo-sq39/study$ adb push first_drv.ko system/
  2. 1288 KB/s (59300 bytes in 0.044s)
  3. yl@yl-Lenovo:~/workplace/svn4.0/urovo-sq39/study$ adb shell
  4. root@android:/ # insmod system/first_drv.ko
  5. root@android:/ # ls dev/myhello -l
  6. crw------- root     root     248,   0 2015-08-04 15:31 myhello

查看kernel日志,adb shell cat proc/kmsg ,会发现加载驱动时,打印的log。

这一部分,主要涉及到一些基本的命令,如adb push ,adb pull,adb root,

或 lsmod 查看系统加载的动态模块

insmod 加载模块

rmmod  删除模块

至于如何编译android文件系统下的字符设备测试程序,字符设备的高阶写法,如具体去操作某一个设备(led、按键),加入中断、并发、定时器等,留在下一编讲解,不过,字符设备的框架基本这样:

1. 分配
2. 设置
3. 注册
4. 硬件相关的代码

版权声明:本文为博主原创文章,未经博主允许不得转载。
上一篇:linux设备驱动第四篇:驱动调试方法


下一篇:.NET MVC 插件化框架支持原生MVC的Area和路由特性