大学毕业,初入公司,招进去的是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. 字符设备的框架模型:
(备注:此图片来源于:http://my.oschina.net/u/1169027/blog/191538)
3. 字符设备的重要的数据结构
3.1 一个简单的字符设备的例子:
#include
#include
#include
#include
#include
#include
static int first_drv_open(struct inode *inode, struct file *file)
{
printk("first_drv_open\n");
return 0;
}
static int first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
printk("first_drv_write\n");
return 0;
}
/*3. 这个结构是字符设备驱动程序的核心
* 当应用程序操作设备文件时所调用的open、read、write等函数,
* 最终会调用这个结构中指定的对应函数
static struct file_operations first_drv_fops = {
.owner = THIS_MODULE,
.open = first_drv_open,
.write = first_drv_write,
};
static struct class *firstdrv_class;
static struct device *firstdrv_class_dev;
int major;
// 执行insmod命令时就会调用这个函数
static int first_drv_init(void)
{
major = register_chrdev(0, "first_drv", &first_drv_fops);
firstdrv_class = class_create(THIS_MODULE, "firstdrv");
firstdrv_class_dev = device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "myhello");
printk("add ko,/dev/myhello \n");
return 0;
}
/*
* 执行rmmod命令时就会调用这个函数 ,注销函数,主要是释放你在注册是申请的资源,与你注册顺序相反,先注册的后释放。
*/
static void first_drv_exit(void)
{
unregister_chrdev(major, "first_drv");
device_unregister(firstdrv_class_dev);
class_destroy(firstdrv_class);
printk("del ko, \n");
}
//1. 我们一般从入口函数看起,先找到该字符设备的入口函数,
module_init(first_drv_init);
module_exit(first_drv_exit);
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 模块时,会不成功。
KERN_DIR = /home/yl/workplace/svn4.0/urovo-sq39/samsung_android_kernel_3.0/
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += first_drv.o
编译:
只需要配置好内核的交叉编译环境即可,
编译: make
清除: make clean
4、 字符设备驱动的加载、卸载、测试
编译:
yl@yl-Lenovo:~/workplace/svn4.0/urovo-sq39/study$ make -j4
make -C /home/yl/workplace/svn4.0/urovo-sq39/samsung_android_kernel_3.0/ M=`pwd` modules
make[1]: Entering directory `/home/yl/workplace/svn4.0/urovo-sq39/samsung_android_kernel_3.0'
make[1]: warning: jobserver unavailable: using -j1. Add `+' to parent make rule.
CC [M] /home/yl/workplace/svn4.0/urovo-sq39/study/first_drv.o
Building modules, stage 2.
MODPOST 1 modules
CC /home/yl/workplace/svn4.0/urovo-sq39/study/first_drv.mod.o
LD [M] /home/yl/workplace/svn4.0/urovo-sq39/study/first_drv.ko
make[1]: Leaving directory `/home/yl/workplace/svn4.0/urovo-sq39/samsung_android_kernel_3.0'
获得root权限
yl@yl-Lenovo:~/workplace/svn4.0/urovo-sq39/study$ adb root
adbd is already running as root
加载驱动,并查看:
yl@yl-Lenovo:~/workplace/svn4.0/urovo-sq39/study$ adb push first_drv.ko system/
1288 KB/s (59300 bytes in 0.044s)
yl@yl-Lenovo:~/workplace/svn4.0/urovo-sq39/study$ adb shell
root@android:/ # insmod system/first_drv.ko
root@android:/ # ls dev/myhello -l
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. 硬件相关的代码