点灯的总结
属于D.Willam的第一篇博客...自己写的,如有雷同,那就雷同
2016年3月23日15:43:09
零.写在开始的屁话
我记得,我在念书的时候拿到的第一块开发板LPC2400,上面有几个led灯,然后,老师的要求就是,点一个灯,然后,很无奈,之后LPC11C14,LPC1800,LPC3250,S3C6410。。。Blabla开始了没完没了的电灯路。。。。。
当然,当初从裸机系统开始点灯,然后到linux,终于,现在到了android。
屁话说够了,开始描述如何在android手机上,除开厂商写的东西,然后去点一盏灯。
如果按照某些人的说法,点个灯不就是上个电,拉高个io口就完了么,好吧,我表示大部分的学习处于软件部分,正是因为灯的硬件简单,才选择去点灯,排除不必要的麻烦,毕竟自己也是个初学者。
简述一下,所用到的平台和代码,硬件平台是MT6735M,机器用的是BKR1,使用的代码是MTK厂商释放的android 5.1版本的代码。
壹.再来一罐老汤
首先,上来一张看到无数遍的图片
本篇文档将要描述的就是通过点灯的动作沿着这幅图片做纵向的分析,从APP到framework到HAL,然后到linux内核,最后控制到硬件,也就是那颗led灯。
从代码的角度,一般是先写linux kernel driver,控制硬件,然后在extern 目录下添加一个led_app.c编译成一个bin文件,在手机运行的时候可以通过这个led_app.bin去测试具体的driver是不是ok,然后,稍加改动之后把代码加入hal层文件中,把hal层文件编译为so库,然后透过jni将native方法送给framework层,最后用aidl文件将接口暴露给app,app通过反射得到操作的方法,最后再一路向下控制到硬件。
当然,非常感谢android的分层思想,可以先写app,用打印的方式完成界面的业务逻辑,然后在framework加上Service,加上aidl,然后用先前的app进行测试,然后接着做jni的工作,接着用app+framework测试jni,完成jni之后接着来hal层,用app+framework+jni测试hal层,最后实现kernel层的文件节点,然后用app+framework+jni+hal层测试driver。
当然,也可以用当初詹天佑先生修铁路开凿隧道的方法,两头一起开工。。。
恩,我选择的是,詹天佑先生的方法。
无所谓手段怎样,只需要完成目的,按照小平爷爷的话就是,不管白猫黑猫,抓到老鼠的就是好厨师。正式因为分层思想,保证我们可以低耦合的做这样那样的事情。
说了这么一段话,作用有两个,一个是为了装,另一个是为了让看的人看得云里雾里不知所云,这样目的就达到了,但是如果在大牛面前,就会死的很惨,所以保持谦逊低调的作风,继续往下。
贰.立足于硬件的linux kernel driver
关于kernel的driver怎么写,宋宝华先生的《linux设备驱动详解第二版》,里面给出了各种各样的驱动模板,我们用最简单的字符设备驱动来控制硬件就好。用简单的模板,然后用最简单的操作做最简单的事情。
static struct file_operations led_fops = {
/* .owner = THIS_MODULE, */
.open = led_open,
.release = led_release,
.unlocked_ioctl = led_unlocked_ioctl,
};
static struct platform_device led_platform_device = {
.name = LED_DEVICE_NAME,
};
static struct platform_driver led_platform_driver = {
.remove = led_platform_driver_remove,
.shutdown = NULL,
.probe = led_platform_driver_probe,
.driver = {
.name = LED_DEVICE_NAME,
},
};
static int led_open(struct inode *inode, struct file *file)
{
printk("%s\n", __func__);
...
return 0;
}
static int led_release(struct inode *inode, struct file *file)
{
printk("%s\n", __func__);
return 0;
}
static long led_unlocked_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
printk("%s, cmd = %d, arg = %ld\n", __func__, cmd, arg);
...
return 0;
}
static int led_platform_driver_probe(struct platform_device *pdev)
{
int ret = 0;
printk("%s\n", __func__);
/* Allocate char driver no. */
if (alloc_chrdev_region(&led_devno, 0, 1, LED_DEVICE_NAME))
{
printk("%s, alloc_chrdev_region error\n", __func__);
return -EAGAIN;
}
/* Allocate driver */
g_led_CharDrv = cdev_alloc();
if (NULL == g_led_CharDrv)
{
unregister_chrdev_region(led_devno, 1);
return -ENOMEM;
}
/* Attatch file operation. */
cdev_init(g_led_CharDrv, &led_fops);
/* Add to system */
if (cdev_add(g_led_CharDrv, led_devno, 1)) {
printk("%s Attatch file operation failed\n", __func__);
unregister_chrdev_region(led_devno, 1);
return -EAGAIN;
}
/* sys/class/led/led */
led_class = class_create(THIS_MODULE, LED_DEVICE_NAME);
if (IS_ERR(led_class)) {
int ret = PTR_ERR(led_class);
printk(" %s Unable to create class, err = %d\n", __func__, ret);
return ret;
}
led_device = device_create(led_class, NULL, led_devno, NULL, LED_DEVICE_NAME);
if (NULL == led_device) {
return -EIO;
}
printk("%s ok....\n", __func__);
return 0;
}
static int led_platform_driver_remove(struct platform_device *pdev)
{
printk("%s\n", __func__);
return 0;
}
static int __init led_driver_init(void)
{
printk("%s\n", __func__);
if(platform_device_register(&led_platform_device)!=0) {
printk("unable to register led_platform_device.\n");
return -1;
}
if (platform_driver_register(&led_platform_driver) != 0) {
printk("unable to register led_platform_driver.\n");
return -1;
}
return 0;
}
static void __exit led_driver_exit(void)
{
printk("%s\n", __func__);
}
module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_DESCRIPTION("Flash driver for led");
MODULE_AUTHOR("dengjinzhe <[email protected]>");
MODULE_LICENSE("GPL v2");
实际上,这个驱动并没有当初想的那样,用最简单的操作做最简单的事情,实际上,ioctl可以更简单,然后字符设备驱动也没必要嵌入到平台驱动里面,class_create和device_create也没有必要去做,这么做,只是为了看起来比较像样,堆积代码量,最后生成的字符设备节点位于/dev/myled,通用的模板是/dev/XXX,因为这个XXX被定义成LED_DEVICE_NAME,然后这个宏被我弄成了myled...
按照字符设备驱动那一套流程,需要编写一个简单的用户空间程序来测试driver是否ok。
测试程序的代码很简单,无非就是一些open,ioctl什么的,所以,还是写下来吧。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#define LED_DEVICE_NAME "/dev/myled"
int main(int argc, char ** argv)
{
int fd = -1;
int cmd = 0;
int arg = 0;
if (argc != 3)
{
printf("usage : ./led_app cmd status\n");
return -1;
}
fd = open(LED_DEVICE_NAME, O_RDWR);
if (fd == -1)
{
printf("failed to open device %s.\n", LED_DEVICE_NAME);
return -1;
}
cmd = atoi(argv[1]);
arg = atoi(argv[2]);
printf("cmd = %d, arg = %d\n", cmd, arg);
ioctl(fd, cmd, arg);
close(fd);
return 0;
}
请忽略掉那些参数的判断和返回值的判断,一切只是为了流程去干活,砍掉细枝末节,最终达到共同富裕。
如果log显示open节点成功,然后ioctl里面的值正常,那么这个driver算是可以工作起来了。
叁.传说中的.so库
其实很纠结,因为说到so库的时候,总觉得挺神秘的,首先,这个所谓的so库应该怎样被理解?然后怎样被使用?So库就是传输中的动态链接库,其中的库函数的连接被推迟到程序运行时进行,恩,好像有一个比较如雷贯耳的名称,叫做动态连接技术。所谓人无利而不往,大致说一下这样的好处,好,大致跳出来开始说了:好处一,牺牲少许动态连接的额外开销,节省系统内存资源;好处二,程序易升级;好初三,本着有需求才调入的原则,可以由程序员在代码中控制实现。
本片作文不讨论android中的legacy的方式,毕竟,现在已经二十一世纪了,我们讨论比较现代化的动作,也就是用hmi那一套,就是类似于hw_get_module那一组函数。编译成库文件的源码大都位于hardware\libhardware\modules\目录下,不过,例如mtk这样的厂家会把相关的代码移动到vendor\mediatek\proprietary\hardware\目录下,然后再引用hardware\libhardware\include\hardware\下的头文件。我们看hardware\libhardware\modules\目录就好,毕竟,后续的代码也是加在这个路径下面的。
对于程序员来说,有一句话非常经典,模仿是程序员的第一技能,我们可以看一下hardware\libhardware\modules\vibrator\下的文件,包含一个vibrator.c和一个android.mk
这里很明白的事情就是vibrator.c就是将要编译成so文件的源代码,所以,我们应该看android.mk文件,虽然我很厌恶作文被大片大片的代码占据,但是,还是代码拿出来吧。
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := vibrator.default
# HAL module implementation stored in
# hw/<VIBRATOR_HARDWARE_MODULE_ID>.default.so
LOCAL_MODULE_RELATIVE_PATH := hw
LOCAL_C_INCLUDES := hardware/libhardware
LOCAL_SRC_FILES := vibrator.c
LOCAL_SHARED_LIBRARIES := liblog
LOCAL_MODULE_TAGS := optional
include $(BUILD_SHARED_LIBRARY)
里面的大致意思就是,将vibrator.c文件编译成vibrator.default.so库文件,需要的动态链接库是liblog,头文件放在hardware/libhardware/include/hardware,你说我怎么知道的,注释上面有写到了,具体VIBRATOR_HARDWARE_MODULE_ID在vibrator.h中定义成“vibrator”。所以,如果要依样话葫芦,做成我们的模块的话,android.mk文件应该改成如下,
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := led.default
# HAL module implementation stored in
# hw/<VIBRATOR_HARDWARE_MODULE_ID>.default.so
LOCAL_MODULE_RELATIVE_PATH := hw
LOCAL_C_INCLUDES := hardware/libhardware
LOCAL_SRC_FILES := led_hal.c
LOCAL_SHARED_LIBRARIES := liblog
LOCAL_MODULE_TAGS := optional
include $(BUILD_SHARED_LIBRARY)
这种时候,我觉得,对比一下就可以看得出区别了,就像看看别人的女朋友,就知道自己的女朋友有多好了。
好,假设你的vibrator.c或者说是自己搞进去的led_hal.c没有语法错误,那么应该会编译出来一个vibrator.default.so或者led_hal.default.so文件被加载。额,似乎感觉顺序有点跑偏,这里只是为了说明,如果这个mk文件没有搞定的话,so文件没有办法生成,后续加载so的时候会出错,所以还是踏实点,先把趟过的坑写出来,这样似乎也许会比较好一些。
肆. 传说中的so库如何被加载
其实,所谓的加载是后面的事情,首先如何去整一个源码出来,这个比较实在。首先,我们需要借鉴一下kernel中driver module的一些思想,例如在字符设备中,只需要填充一个file_operations结构体,然后注册到系统中,再例如在一个framebuffer设备驱动中,只需要填充一个fb_info结构体然后注册到系统中就可以了。对于当前需要去分析的so库,我们需要去填充的是hw_module_t结构体,我们需要在代码中查看一下这个结构体。
typedef struct hw_module_t {
/** tag must be initialized to HARDWARE_MODULE_TAG */
uint32_t tag;
/**
* The API version of the implemented module. The module owner is
* responsible for updating the version when a module interface has
* changed.
*
* The derived modules such as gralloc and audio own and manage this field.
* The module user must interpret the version field to decide whether or
* not to inter-operate with the supplied module implementation.
* For example, SurfaceFlinger is responsible for making sure that
* it knows how to manage different versions of the gralloc-module API,
* and AudioFlinger must know how to do the same for audio-module API.
*
* The module API version should include a major and a minor component.
* For example, version 1.0 could be represented as 0x0100. This format
* implies that versions 0x0100-0x01ff are all API-compatible.
*
* In the future, libhardware will expose a hw_get_module_version()
* (or equivalent) function that will take minimum/maximum supported
* versions as arguments and would be able to reject modules with
* versions outside of the supplied range.
*/
uint16_t module_api_version;
#define version_major module_api_version
/**
* version_major/version_minor defines are supplied here for temporary
* source code compatibility. They will be removed in the next version.
* ALL clients must convert to the new version format.
*/
/**
* The API version of the HAL module interface. This is meant to
* version the hw_module_t, hw_module_methods_t, and hw_device_t
* structures and definitions.
*
* The HAL interface owns this field. Module users/implementations
* must NOT rely on this value for version information.
*
* Presently, 0 is the only valid value.
*/
uint16_t hal_api_version;
#define version_minor hal_api_version
/** Identifier of module */
const char *id;
/** Name of this module */
const char *name;
/** Author/owner/implementor of the module */
const char *author;
/** Modules methods */
struct hw_module_methods_t* methods;
/** module's dso */
void* dso;
#ifdef __LP64__
uint64_t reserved[32-7];
#else
/** padding to 128 bytes, reserved for future use */
uint32_t reserved[32-7];
#endif
} hw_module_t;
对于这个结构体,注释写的都听明白的,被漆成蓝色的成员需要重点关注,按照注释而言,tag这个变量必须要被初始化为HARDWARE_MODULE_TAG,id用来标示这个模块,和我们的身份证号一个意思。
所以,实际上的代码我写成如下的样子
/*
* The led Module
*/
struct hw_module_t HAL_MODULE_INFO_SYM = {
.tag = HARDWARE_MODULE_TAG,
.id = LED_HARDWARE_MODULE_ID,
.name = "dengjinzhe led Module",
.author = "dengjinzhe",
.methods = &led_module_methods,
};
这里,LED_HARDWARE_MODULE_ID被定义成”led”,简单点比较好~。但是,很遗憾,似乎这个结构体怎么被调用?似乎走进了一个死胡同?恩,source insight是一个利器,具体怎么使用这里不再赘述,用”hw_module_t”作为关键字进行搜索,大部分都是一些具体的模块对hw_module_t的操作,当然,每个模块都有定义一个hw_module_t,最终,我们可以找到Hardware.c,这个文件位于./hardware/libhardware/hardware.c,我们开始跟踪这个文件以及其中的内容。
里面只实现了几个函数,我们一一列出:
int hw_get_module(const char *id, const struct hw_module_t **module)
int hw_get_module_by_class(..)
static int hw_module_exists(...)
static int load(...)
大致看一下hardware.c,入口函数为hw_get_module,我们搜索这个函数
我们可以参考灯光系统,文件位置如下:com_android_server_lights_LightsService.cpp(frameworks\base\services\core\jni)中对hw_get_module的调用。
实际上,这个.cpp文件是一个jni文件,我们暂时不去关注jni的过程,我们先去看到init_native函数中有对hw_get_module进行调用,所以,我们从这里作为切入点进行描述,开始hw_get_module的过程。
int hw_get_module(const char *id, const struct hw_module_t **module)
{
ALOGE("%s, %d\n", __func__, id);
return hw_get_module_by_class(id, NULL, module);
}
这里没有什么好看的,直接调用hw_get_module_by_class,这些代码片段都不算太长,直接贴出来,希望看到的人不要打我。。。。
int hw_get_module_by_class(const char *class_id, const char *inst,
const struct hw_module_t **module)
{
int i;
char prop[PATH_MAX];
char path[PATH_MAX];
char name[PATH_MAX];
char prop_name[PATH_MAX];
if (inst)
snprintf(name, PATH_MAX, "%s.%s", class_id, inst);
else
strlcpy(name, class_id, PATH_MAX);
ALOGE("%s, %s", __func__, name);
/*
* Here we rely on the fact that calling dlopen multiple times on
* the same .so will simply increment a refcount (and not load
* a new copy of the library).
* We also assume that dlopen() is thread-safe.
*/
/* First try a property specific to the class and possibly instance */
snprintf(prop_name, sizeof(prop_name), "ro.hardware.%s", name);
ALOGE("%s, %s", __func__, prop_name);
if (property_get(prop_name, prop, NULL) > 0) {
if (hw_module_exists(path, sizeof(path), name, prop) == 0) {
goto found;
}
}
/* Loop through the configuration variants looking for a module */
for (i=0 ; i<HAL_VARIANT_KEYS_COUNT; i++) {
if (property_get(variant_keys[i], prop, NULL) == 0) {
continue;
}
if (hw_module_exists(path, sizeof(path), name, prop) == 0) {
ALOGE("%s, path = %s, name = %s, prop = %s", __func__, path, name, \
prop);
goto found;
}
}
/* Nothing found, try the default */
if (hw_module_exists(path, sizeof(path), name, "default") == 0) {
ALOGE("%s, found default\n", __func__);
goto found;
}
ALOGE("%s, hw_module_exists not found\n", __func__);
return -ENOENT;
found:
/* load the module, if this fails, we're doomed, and we should not try
* to load a different variant. */
return load(class_id, path, module);
}
按照函数名来说,大致可以猜测是通过传下来的id这个变量来找对应的模块,好,我们不喜欢猜测,直接走代码。
1. 很明显,传下来的inst是个NULL,所以把class_id存入name中,例如class_id为”led”,那么name的值就是”led”。
2. 用snprintf将字符串格式化,拼接”led”和”ro.hardware.”最终,将ro.hardware.led存入prop_name变量。
3. property_get取出属性值,然后走hw_module_exists继续用snprintf拼接so库的名字并判断这个文件是否在/system/lib/hw路径和/vendor/lib/hw下存在,当然,这个属性值应该是先前就已经扔进系统里面的了,hw_module_exists继续去根据拼接的路径去查找。
4. 如果都找不到,那么从variant_keys这个数组里逐个将值取出来拼接路径继续找
5. 如果再找不到,那么用一个default作为拼接的元素进行查找
6. 再找不到,那么系统就认为没有这个库存在,综上,一般来说,应该搜索这些库名:
ro.hardware.led
/vendor/lib/hw/led.mt6735.so
/vendor/lib/hw/led.blueangel.so
/vendor/lib/hw/led.default.so
/system/lib/hw/led.mt6735.so
/system/lib/hw/led.led.blueangel.so
/system/lib/hw/led.default.so
其实这里有一些小小的疑惑,实际上按照variant_keys的数组而言,打印出来的并不应该只有这些so的路径,但是最终打印到了default,也就是说走到了最后才找到的。好,暂且不管,往下走。
7. 调用load函数,load函数,从名字上看就很明显,用来装载找到的so库,我们把代码放出来。
static int load(const char *id,
const char *path,
const struct hw_module_t **pHmi)
{
int status;
void *handle;
struct hw_module_t *hmi;
/*
* load the symbols resolving undefined symbols before
* dlopen returns. Since RTLD_GLOBAL is not or'd in with
* RTLD_NOW the external symbols will not be global
*/
handle = dlopen(path, RTLD_NOW);
if (handle == NULL) {
char const *err_str = dlerror();
ALOGE("load: module=%s\n%s", path, err_str?err_str:"unknown");
status = -EINVAL;
goto done;
}
/* Get the address of the struct hal_module_info. */
const char *sym = HAL_MODULE_INFO_SYM_AS_STR;
hmi = (struct hw_module_t *)dlsym(handle, sym);
if (hmi == NULL) {
ALOGE("load: couldn't find symbol %s", sym);
status = -EINVAL;
goto done;
}
/* Check that the id matches */
if (strcmp(id, hmi->id) != 0) {
ALOGE("load: id=%s != hmi->id=%s", id, hmi->id);
status = -EINVAL;
goto done;
}
hmi->dso = handle;
/* success */
status = 0;
done:
if (status != 0) {
hmi = NULL;
if (handle != NULL) {
dlclose(handle);
handle = NULL;
}
} else {
ALOGV("loaded HAL id=%s path=%s hmi=%p handle=%p",
id, path, *pHmi, handle);
}
*pHmi = hmi;
return status;
}
如果熟悉linux c编程,那么很容易可以看到一些重要的函数,这两个重要的函数我都漆成蓝色,dlopen和dlsym,dlopen用来打开动态链接库,dlsym根据动态链接库操作句柄与符号,返回符号对应的地址。最后返回的hw_module_t类型的指针送回参数中,完成动态链接库的装载。
伍.承下启上的jni
关于jni,先做一个简单的描述,jni全程为java native interface,作用是作为java语言调用C/C++这样的native语言实现的native方法的一种手段,通过JNINativeMethod这样的数组将java方法和native方法映射起来,从而达到java调用c/c++函数的目的。
Jni有一些重要的概念,例如jni描述符,用于描述方法的参数和返回值,具体的使用方法请参考jni官方文档,就是那个117页的jni.pdf。。。
好,回到最原先的hw_get_module,我们在搜索这个函数的时候,可以发现,除了实现的hardware.c文件之外,大部分都在.cpp文件中,我们可以先参考com_android_server_lights_LightsService.cpp (frameworks\base\services\core\jni) 这一支文件,
一般看这样的jni文件,我们先找到注册的函数
int register_android_server_LightsService(JNIEnv *env)
{
return jniRegisterNativeMethods(env, "com/android/server/lights/LightsService",
method_table, NELEM(method_table));
}
然后,想都不要想的去找到这个函数表
static JNINativeMethod method_table[] = {
{ "init_native", "()J", (void*)init_native },
{ "finalize_native", "(J)V", (void*)finalize_native },
{ "setLight_native", "(JIIIIII)V", (void*)setLight_native },
};
漆成蓝色的字体就是所谓的jni描述符,描述符左边就是本地函数的函数名,右边是给java用的jni方法,所以我们先看左边,毕竟这是一个承下启上,而不是承上启下的过程,好吧,无所谓了。先看左边的实现,我们发现hw_get_module被init_native调用。
所以,我们开始分析这个init_native方法
static jlong init_native(JNIEnv *env, jobject clazz)
{
int err;
hw_module_t* module;
Devices* devices;
devices = (Devices*)malloc(sizeof(Devices));
err = hw_get_module(LIGHTS_HARDWARE_MODULE_ID, (hw_module_t const**)&module);
if (err == 0) {
devices->lights[LIGHT_INDEX_BACKLIGHT]
= get_device(module, LIGHT_ID_BACKLIGHT);
devices->lights[LIGHT_INDEX_KEYBOARD]
= get_device(module, LIGHT_ID_KEYBOARD);
devices->lights[LIGHT_INDEX_BUTTONS]
= get_device(module, LIGHT_ID_BUTTONS);
devices->lights[LIGHT_INDEX_BATTERY]
= get_device(module, LIGHT_ID_BATTERY);
devices->lights[LIGHT_INDEX_NOTIFICATIONS]
= get_device(module, LIGHT_ID_NOTIFICATIONS);
devices->lights[LIGHT_INDEX_ATTENTION]
= get_device(module, LIGHT_ID_ATTENTION);
devices->lights[LIGHT_INDEX_BLUETOOTH]
= get_device(module, LIGHT_ID_BLUETOOTH);
devices->lights[LIGHT_INDEX_WIFI]
= get_device(module, LIGHT_ID_WIFI);
} else {
memset(devices, 0, sizeof(Devices));
}
return (jlong)devices;
}
1. 先通过hw_get_module得到hw_module_t结构体
2. 调用get_device
3. 调用module->methods->open(module, name, &device);
所以,我们自己的cpp文件中的代码段可以这样写
static void ledClose(JNIEnv *env, jobject clazz)
{
ALOGE("native_ledClose\n");
}
static jint ledOpen(JNIEnv *env, jobject clazz)
{
jint err;
hw_module_t *module;
hw_device_t *device;
ALOGE("native_ledOpen\n");
#if 1
/* 1. hw_get_module */
err = hw_get_module(LED_HARDWARE_MODULE_ID, (hw_module_t const**)&module);
if (err == 0)
{
ALOGE("native_ledOpen hw_get_module ok.....\n");
/* 2. get device : module->methods->open */
err = module->methods->open(module, NULL, &device);
if (err == 0)
{
/* 3. call led_open */
ALOGE("native_ledOpen module->methods->open ok.....\n");
led_device = (led_device_t *)device;
return led_device->led_open(led_device);
}
else
{
ALOGE("native_ledOpen error, err = %d\n", err);
return -1;
}
}
ALOGE("native_ledOpen, hw_get_module error, err = %d\n", err);
return -1;
#else
return 0;
#endif
}
static jint ledCtrl(JNIEnv *env, jobject clazz, jint which, jint status)
{
ALOGE("native_ledCtrl, which = %d, status = %d\n", which, status);
#if 1
return led_device->led_ctrl(led_device, which, status);
#else
return 0;
#endif
}
static JNINativeMethod method_table[] = {
{ "native_ledCtrl", "(II)I", (void*)ledCtrl },
{ "native_ledOpen", "()I", (void*)ledOpen },
{ "native_ledClose", "()V", (void*)ledClose },
};
int register_android_server_LedService(JNIEnv *env)
{
return jniRegisterNativeMethods(env, "com/android/server/LedService",
method_table, NELEM(method_table));
}
重点来了,我们之前只描述了so库是怎么生成并且加载的,但是具体的hw_module_t结构体怎么填充,我们并没有说明,现在来说一下。
按照之前的led_hal.c中所说的hw_module_t中的三个属性必须要填充,其他的随他去吧
.tag = HARDWARE_MODULE_TAG,
.id = LED_HARDWARE_MODULE_ID,
.methods = &led_module_methods,
当然,这里为了装一下,把自己的名字也填充进去
.author = "dengjinzhe",
==》形成了这个样子
struct hw_module_t HAL_MODULE_INFO_SYM = {
.tag = HARDWARE_MODULE_TAG,
.id = LED_HARDWARE_MODULE_ID,
.name = "dengjinzhe led Module",
.author = "dengjinzhe",
.methods = &led_module_methods,
};
重要的是led_module_methods,我们只实现了一个open函数
static struct hw_module_methods_t led_module_methods = {
.open = led_device_open,
};
static int led_device_open(const struct hw_module_t* module, char const* name,
struct hw_device_t** device)
{
ALOGE("native %s\n", __func__);
*device = &led_device;
return 0;
}
led_device是一个结构体,具体的定义如下
static struct led_device_t led_device = {
.common = {
.tag = HARDWARE_DEVICE_TAG,
.close = led_close,
},
.led_open = led_open,
.led_ctrl = led_ctrl,
};
习惯把重要的事情漆成蓝色的字体,led_device_t这个hmi规范要求的,必须要定义一个模块的结构体,这个结构体我定义成下面的样子,
struct led_device_t {
struct hw_device_t common;
int (*led_open)(struct led_device_t* dev);
int (*led_ctrl)(struct led_device_t* dev, int which, int status);
};
第一个成员必须要是hw_device_t,不要问为什么,当做是规定的就好,当然为什么是这样的,我个人认为是因为hw_device_t放在第一个元素可以保证对应到led_device_open中的第一个元素,可以实现地址的对齐,方面做赋值操作。
下面的函数指针就是要填充的操作,具体为什么要这样设计,因为这样设计的好处可以实现一种所谓的stub思想,从结构体引出操作,可以封装掉底层,再次体现分层的思想。
这里,open,ctrl和close可以操作kernel生成的字符设备节点,就像最开始的led_app一样,操纵驱动控制硬件。
static int led_open(struct led_device_t* dev)
{
ALOGE("native %s\n", __func__);
#if 1
fd = open(FILE_PATH, O_RDWR);
if (fd >= 0)
{
ALOGE("native %s ok...\n", __func__);
return 0;
}
else
{
ALOGE("native %s failed...\n", __func__);
return -1;
}
#else
return 0;
#endif
}
int led_ctrl(struct led_device_t* dev, int which, int status)
{
ALOGE("native %s, which = %d, status = %d\n", __func__, which, status);
int ret = ioctl(fd, status, which);
return ret;
}
/** Close the lights device */
static int led_close(struct led_device_t *dev)
{
ALOGE("native %s\n", __func__);
//close(fd);
return 0;
}
从硬件开始,一层一层的叠上去,目前叠到了jni,既然jni是承下启上,那么jni的上面是java,当然,在java之前,我们先看一下jni是怎样加载的。
我们可以查阅jni的官方文档,加载jni本地方法用到的是jniRegisterNativeMethods,当然,这个函数被register_android_server_LightsService封装起来,那么这个函数在哪里被调用?使用source insight来搜索一下,
我们找到onload.cpp这个文件,可以到这个文件下看一下,最终被入口函数JNI_OnLoad加载,所以我们也仿造将自己写的register_android_server_LedService放入其中,用在注册jni。
这样,java层便可以使用JNINativeMethod 中声明的方法了,也就是描述符右边的方法名。
陆.Java的世界
Jni网上,便是java的世界了,对于android而言,一般来说,需要在framework中实现一个硬件访问服务。因为android系统的硬件访问服务通常运行在系统进程System中,而使用这些硬件访问服务的应用程序运行在其他的进程中,这个时候会使用到进程间通信。Android提供了一个高效的进程间通信机制,这个机制叫做binder进程间通信机制。实现这个机制,我们需要提供服务的一方实现binder的接口,那么,我们需要定义这个服务的接口。
看起来挺复杂的,实际上,android提供了一种所谓的android接口描述语言,也就是AIDL,编译的事情扔给编译器就好。我们可以参考振动器的例子,恩,振动器,就是它,一个神奇的设备。。。IVibratorService.aidl,位置在frameworks\base\core\java\android\os\
感觉每次在framework下找东西都很痛苦,经常搜个后缀然后在傻瓜的去目录下找吧、、、
文件内容很简单
package android.os;
/** {@hide} */
interface IVibratorService
{
boolean hasVibrator();
void vibrate(int uid, String opPkg, long milliseconds, int usageHint, IBinder token);
void vibratePattern(int uid, String opPkg, in long[] pattern, int repeat, int usageHint, IBinder token);
void cancelVibrate(IBinder token);
}
这样就定义完了,编译出来个什么玩意呢?
out\target\common\obj\JAVA_LIBRARIES\framework_intermediates\src\core\java\android\os\IVibratorService.java
会生成这样的一个文件,我们看一下这个文件做了什么,文件还是挺长的,只列出部分重要的代码段:
1. IVibratorService继承了接口,并实现了一个内部类stub,这个类继承了binder并且实现说了IVibratorService接口
2. 声明了aidl文件中的声明的函数
public boolean hasVibrator() throws android.os.RemoteException;
public void vibrate(int uid, java.lang.String opPkg, long milliseconds, int usageHint, android.os.IBinder token) throws android.os.RemoteException;
public void vibratePattern(int uid, java.lang.String opPkg, long[] pattern, int repeat, int usageHint, android.os.IBinder token) throws android.os.RemoteException;
public void cancelVibrate(android.os.IBinder token) throws android.os.RemoteException;
3. 这个文件的作用,在定义的服务类中继承stub类,并通过stub的方式向app提供服务,app可以通过java的反射机制最终获服务中的方法,实际上是app借助继承了bind的stub类,并且通过反射得到这个这个stub类的外部类,也就是服务类,最终调用服务类中的jni送上来的native方法达到最终目的。
4. 我们参考振动器的例子,实现自己的ILedService.aidl和LedService.java代码如下
Aidl文件:
package android.os;
/** {@hide} */
interface ILedService
{
int ledCtrl(int which, int status);
}
LedService.java
package com.android.server;
import android.os.ILedService;
import android.util.Slog;
public class LedService extends ILedService.Stub {
private static final String TAG = "LedService";
/* call native c function to access hardware */
public int ledCtrl(int which, int status) throws android.os.RemoteException
{
Slog.e(TAG, "ledctrl which " + which + "status " + status);
return native_ledCtrl(which, status);
}
public LedService() {
native_ledOpen();
Slog.e(TAG, "LedService......");
}
public static native int native_ledOpen();
public static native void native_ledClose();
public static native int native_ledCtrl(int which, int status);
}
哈~简单吧~~,因为本来就很简单。
既然是一个服务,那么怎样让这个服务注册到系统中,并且让这个服务跑起来呢?我们依然参考振动器的例子,我们搜索VibratorService.java文件中的构造方法VibratorService
很明显,在SystemServer.java,中被实例化,额,补充一下,jni过程中需要明确jni方法应该注册到哪一个类,淡然在jniRegisterNativeMethod中可以看到,其实第二个参数就是注册到的位置,每个斜杠代表一级目录,和cpp文件名神同步。。。。
好,回到SystemServer.java,具体SystemServer怎么被实例化的,我们先不管,我们看到SystemServer.java中有这样的描述
实际上,SystemServer是zygote进程实例化出来的服务,属于android系统初始化过程的一部分,好,我们看到这里实例化了一个SystemServer,然后调用了run方法。
对于代码而言,写注释是一种美德,我们找到run方法,这个方法很长,我们截取其中和我们这部分有关系的一些代码
当然,注释上都注明了每一个步骤都做了些什么事情。
这里调用了loadLibrary方法,然后调用nativeInit,用来初始化本地服务。
最后开启服务,我们亲爱的振动器就是在startOtherServices中被实例化的
所以,我们依样画葫芦,做同样的事情,在这里实例化我们的LedService。按照先前对LedService.java的代码描述,在LedService的构造方法中会调用open的native方法,便可以按照之前的方法一路追溯到jni->hal->kernel->hardware了
柒. Apk,一个测试的apk
说实话,这是一个惨淡的apk,只有checkbox和button,功能只是checkbox和button被触碰之后会一路向下操纵到硬件。会涉及到java中的反射操作。
下面贴出一些重要的代码段并做一些简单的分析。
看apk我都先从onCreate开始,作为入口。
protected void onCreate(Bundle savedInstanceState) {
button = (Button) findViewById(R.id.button);
...//略过控件初始化
Method getService = Class.forName("android.os.ServiceManager").getMethod("getService", String.class); //取得ServiceManager的getService 方法
Object ledService = getService.invoke(null, "led"); //取得LedService的服务
Method asInterface = Class.forName("android.os.ILedService$Stub").getMethod("asInterface", IBinder.class);//取得asInterface方法
proxy = asInterface.invoke(null, ledService); //使用asinterface得到proxy代理
ledCtrl = Class.forName("android.os.ILedService$Stub$Proxy").getMethod("ledCtrl", int.class, int.class); //得到LedService中的ledCtrl方法
***********************************************************************
public void onCheckboxClicked(View view) {
ledCtrl.invoke(proxy, 0, 1);//调用ledCtrl 的方法,并把参数传进去
}
我本人并不是很擅长apk,所以也没什么好说的,还是附上测试的apk源码。
package com.example.dengjinzhe.leddemo;
import android.app.Activity;
import android.os.Bundle;
import android.os.IBinder;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.Toast;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class MainActivity extends Activity {
private boolean ledon = false;
private Button button = null;
private CheckBox checkBoxLed1 = null;
private CheckBox checkBoxLed2 = null;
private CheckBox checkBoxLed3 = null;
private CheckBox checkBoxLed4 = null;
Object proxy = null;
Method ledCtrl = null;
//IBinder ibinder = null;
class MyButtonListener implements View.OnClickListener {
@Override
public void onClick(View v) {
ledon = !ledon;
if (ledon) {
button.setText("ALL OFF");
checkBoxLed1.setChecked(true);
checkBoxLed2.setChecked(true);
checkBoxLed3.setChecked(true);
checkBoxLed4.setChecked(true);
try {
for (int i = 0; i < 4; i++)
//iLedService.ledCtrl(i, 1);
ledCtrl.invoke(proxy, i, 1);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
else {
button.setText("ALL ON");
checkBoxLed1.setChecked(false);
checkBoxLed2.setChecked(false);
checkBoxLed3.setChecked(false);
checkBoxLed4.setChecked(false);
try {
for (int i = 0; i < 4; i++)
//iLedService.ledCtrl(i, 0);
ledCtrl.invoke(proxy, i, 0);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
public void onCheckboxClicked(View view) {
// Is the view now checked?
boolean checked = ((CheckBox) view).isChecked();
try {
// Check which checkbox was clicked
switch(view.getId()) {
case R.id.checkBox_LED1:
if (checked) {
// Put some meat on the sandwich
Toast.makeText(getApplicationContext(), "LED1 on", Toast.LENGTH_SHORT).show();
//iLedService.ledCtrl(0, 1);
ledCtrl.invoke(proxy, 0, 1);
}
else {
// Remove the meat
Toast.makeText(getApplicationContext(), "LED1 off", Toast.LENGTH_SHORT).show();
//iLedService.ledCtrl(0, 0);
ledCtrl.invoke(proxy, 0, 0);
}
break;
case R.id.checkBox_LED2:
if (checked) {
// Put some meat on the sandwich
Toast.makeText(getApplicationContext(), "LED2 on", Toast.LENGTH_SHORT).show();
//iLedService.ledCtrl(1, 1);
ledCtrl.invoke(proxy, 1, 1);
}
else {
// Remove the meat
Toast.makeText(getApplicationContext(), "LED2 off", Toast.LENGTH_SHORT).show();
//iLedService.ledCtrl(1, 0);
ledCtrl.invoke(proxy, 1, 0);
}
break;
case R.id.checkBox_LED3:
if (checked) {
// Put some meat on the sandwich
Toast.makeText(getApplicationContext(), "LED3 on", Toast.LENGTH_SHORT).show();
//iLedService.ledCtrl(2, 1);
ledCtrl.invoke(proxy, 2, 1);
}
else {
// Remove the meat
Toast.makeText(getApplicationContext(), "LED3 off", Toast.LENGTH_SHORT).show();
//iLedService.ledCtrl(2, 0);
ledCtrl.invoke(proxy, 2, 0);
}
break;
case R.id.checkBox_LED4:
if (checked) {
// Put some meat on the sandwich
Toast.makeText(getApplicationContext(), "LED4 on", Toast.LENGTH_SHORT).show();
//iLedService.ledCtrl(3, 1);
ledCtrl.invoke(proxy, 3, 1);
}
else {
// Remove the meat
Toast.makeText(getApplicationContext(), "LED4 off", Toast.LENGTH_SHORT).show();
//iLedService.ledCtrl(3, 0);
ledCtrl.invoke(proxy, 3, 0);
}
break;
// TODO: Veggie sandwich
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = (Button) findViewById(R.id.button);
checkBoxLed1 = (CheckBox) findViewById(R.id.checkBox_LED1);
checkBoxLed2 = (CheckBox) findViewById(R.id.checkBox_LED2);
checkBoxLed3 = (CheckBox) findViewById(R.id.checkBox_LED3);
checkBoxLed4 = (CheckBox) findViewById(R.id.checkBox_LED4);
button.setOnClickListener(new MyButtonListener());
try {
//iLedService = ILedService.Stub.asInterface(ServiceManager.getService("led"));
Method getService = Class.forName("android.os.ServiceManager").getMethod("getService", String.class);
if (getService == null){
System.out.println("######### 111 ##############");
}
Object ledService = getService.invoke(null, "led");
System.out.println("######### 222 ##############" + ledService);
if (ledService == null){
System.out.println("######### 222 ##############");
}
Method asInterface = Class.forName("android.os.ILedService$Stub").getMethod("asInterface", IBinder.class);
if (asInterface == null){
System.out.println("######### 333 ##############");
}
proxy = asInterface.invoke(null, ledService);
System.out.println("######### 4444 ##############" + proxy);
ledCtrl = Class.forName("android.os.ILedService$Stub$Proxy").getMethod("ledCtrl", int.class, int.class);
if (ledCtrl == null){
System.out.println("######### 5555 ##############");
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
}
捌. 写在最后的话
整个工作实际上还是有很多问题,例如返回值的判断,容错,代码规范,还有kernel driver的逻辑控制,但是本文旨在研究如何从apk一路顺下到hardware的控制,所以会有一些出入,因为当初的硬件设定和代码并不算太多匹配,实际上也可以用模拟器来代替实体的硬件,所以如果在apk操作控件,最终让kernel能打印一些预期的语句也是可以的,led只是一个硬件的载体,我们在试验的时候可以省去这个实体硬件的载体,换做其他的硬件也可以,只是对于初学者的我而言,先操作一个简单的硬件会对流程分析更有帮助。
另外,在MTK平台的android 5.1代码上,会碰到selinux的问题,如果为了方便,可以先把selinux相关的东西关掉,也就是做enforceing的动作
bootable/bootloader/lk/platform/mt6735/rules.mk
SELINUX_STATUS := 3 --> SELINUX_STATUS := 2
system/core/init/Android.mk加上
feq ($(strip $(TARGET_BUILD_VARIANT)),user)
LOCAL_CFLAGS += -DALLOW_DISABLE_SELINUX=1
endif
或者,按照selinux修改
/device/mediatek/common/sepolicy/device.te
/device/mediatek/common/sepolicy/file_contexts
/device/mediatek/common/sepolicy/service_contexts
/device/mediatek/common/sepolicy/system_server.te
/device/mediatek/common/sepolicy/untrusted_app.te
这些文件吧。
整个过程走了很久,经常出现这样那样的问题,分析流程还是先借鉴已经做好的硬件服务来,不断的打磨基本功,遇到问题需要进行log分析,弄清楚流程。
感谢曾昊前辈和陈鹏前辈对小弟我的指导,在我学习这个过程中遇到困惑的时候给予指导。
参考的书籍:
1.《Java编程思想第4版》
2.《LinuxC编程一站式学习》
3.《Android系统源代码情景分析》
4.《Linux设备驱动程序》