草庐IT

嵌入式Android系统增加新硬件支持

十六宿舍 2023-04-20 原文
根据Statcounter显示,截至2021年4月全球移动操作系统中 ,谷歌 Android占比高达72.2% , 苹果IOS占比26.99% ,其余移动操作系统占比之和低于1%。 包含桌面操作系统在内的全球操作系统占比中 , Android以40.66% 位列第一位, Windows 位列第二, 占比31.97% 。如此庞大的用户量,使得芯片厂商以及软件开发人员都不能无视它。

虽然早期的大多数嵌入式系统设备很少甚至没有用户界面,但还是有很多传统“嵌入式”设备确实是有用户界面的。现如今更多的嵌入式设备除了有纯粹的功能需求外,开发者还要为用户操作提供人机交互界面。所以,设计人员要么为用户提供一种其熟悉的界面体验,要么冒险为用户提供一种不熟悉甚至是全新的界面风格,再加上原先很多属于Java等语言开发的大型软件业务逻辑下移至嵌入式设备的需求。这么看来,在我们的板卡上运行Android系统,往往是一个不错的选择。🎭

如果您是基于安卓系统的应用开发,您不仅可以通过官方的渠道找到非常详细的指导文档,还有非常多书籍和博文,但是任何希望多Android框架开展工作的开发人员,不管是嵌入式系统移植还是修改Android框架的工程师,都会发现根本没有任何文档来阐述应该如何开展工作,官方AOSP(Android Open Source Project)网站的东西大部分都是说明性质,而且在很多情况下,你的嵌入式系统会包含Android所不支持的设备,增加对新类型硬件的支持是一件很棘手的事情,下面的博客内容会通过对姜饼系统的修改,增加一个具有"存储"功能的硬件支持,帮助我们理解Android框架各层之间的复杂关系。🐣

基础知识

Android系统跟“纯洁的Linux”相比,它需要的不仅仅是恰当的驱动程序来使得硬件能够正常工作。通常来说,每个Android支持的硬件类型都有一个系统服务HAL定义。比如说,GPIO驱动LED等的亮灭,就有一个灯光服务和灯光HAL的定义,这样基于安卓的应用就可以通过系统提供的灯光服务来控制LED灯。

本博文讲述的例子来源于《构建嵌入式Android系统》这本书籍,但显然这仅仅是一个简单的架构程序,注释为了帮你在增加新的硬件类型方面给予你些许灵感,你的硬件很可能具有完全不一样的接口,你的系统版本也提升了很多了。[1]

系统服务

我们首先在frameworks/base/services/java/com/android/server/目录下增加OpersysService.java,这个文件实现了OpersysServic类,提供了构造函数以及给上层的基本调用:

public OpersysService(Context context) {
    super();
    mContext = context;
    Log.i(TAG, "Opersys Service started");
    mNativePointer = init_native();  
    Log.i(TAG, "test() returns " + test_native(mNativePointer, 20));
}
public String read(int maxLength)
{
    int length;
    byte[] buffer = new byte[maxLength];
    length = read_native(mNativePointer, buffer);
    return new String(buffer, 0, length);
}

重要的工作是调用/write_native函数来完成的,它本身在OpersysServic类里的声明如下:

private static native int init_native();
private static native int read_native(int ptr, byte[] buffer);

该方法通过native关键字像编译器声明该函数不在任何Java代码中是实现,它需要Davik虚拟机运行时装载JNI的动态库文件。将这些本地方法的具体实现:com_android_server_OpersysService.cpp放到frameworks/base/services/jni目录下,并修改目录下的的Android.mk增加com_android_server_OpersysService.cpp为libandroid_servers.so动态库编译源文件,并在同目录下的onload.cpp中增加:

extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    ...
    register_android_server_OpersysService(env);
    ...
}

方法在com_android_server_OpersysService.cpp的实现如下:

static JNINativeMethod method_table[] = {
    { "init_native", "()I", (void*)init_native },
    { "finalize_native", "(I)V", (void*)finalize_native },
    { "read_native", "(I[B)I", (void*)read_native },
    { "write_native", "(I[B)I", (void*)write_native },
    { "test_native", "(II)I", (void*)test_native},
};

int register_android_server_OpersysService(JNIEnv *env)
{
    return jniRegisterNativeMethods(env, "com/android/server/OpersysService",
            method_table, NELEM(method_table));
};

上面的结构体包含了三个域,第一个域是Java类中定义的函数名称,最后一个域是它对应的C语言实现的函数名。中间的参数括号里的字母是Java传递下来的参数类型描述,右括号后边的字母是返回值类型描述。例如init_native()函数不懈怠参数并返回一个整型值,而read_native()函数有两个参数,一个整型和一个字节数组,并且返回一个整型值。当你深入Android内部,你会经常跟这样的JNI打交道,建议你抽空系统的了解更多关于JNI使用的信息。

read_native()函数的实现如下:

static int read_native(JNIEnv *env, jobject clazz, int ptr, jbyteArray buffer)
{
    opersyshw_device_t* dev = (opersyshw_device_t*)ptr;
    jbyte* real_byte_array;
    int length;
    real_byte_array = env->GetByteArrayElements(buffer, NULL);

    if (dev == NULL) {
        return 0;
    }
    length = dev->read((char*) real_byte_array, env->GetArrayLength(buffer));
    env->ReleaseByteArrayElements(buffer, real_byte_array, 0);
    return length;
}

JNI调用函数的前两个隐藏入参一个叫做env的虚拟机句柄以及一个名为clazz的指向this对象的指针。在编写的时候千万不要忘记JNI函数是在C语言的世界里操作Java类型的对象,像是上面就需要使用env->GetByteArrayElements()来获得在C代码中可以使用的数组。

可以看出,其方法中实际的读取操作是通过dev->read来实现的,这个函数指针指向了哪里呢?我们需要看一下init_native()的实现:

static jint init_native(JNIEnv *env, jobject clazz)
{
    int err;
    hw_module_t* module;
    opersyshw_device_t* dev = NULL;  
    err = hw_get_module(OPERSYSHW_HARDWARE_MODULE_ID, (hw_module_t const**)&module);
    if (err == 0) {
        if (module->methods->open(module, "", ((hw_device_t**) &dev)) != 0)
       return 0;
    }
    return (jint)dev;
}

这个函数做了两件重要的事情。首先,调用hw_get_module()函数请求HAL装载支持OPERSYSHW_HARDWARE_MODULE_ID类型的硬件模块。然后,调用模块的open()函数。我们会在后边的章节再看这两个函数。但是需要在这里注意的是,第一个动作导致了驱动.so文件被加载到系统服务的地址空间,第二个动作使得那个库文件中的硬件相关函数,例如read()和write()被com_android_server_OpersysService.cpp调用,本质上也就是C实现的系统服务被增加到了系统中。

HAL模块管理

Hardware/目录下的HAL提供了前面提到的hw_get_module()函数。如果你进一步阅读代码,你会看到hw_get_module()函数是以经典的dlopen()来实现的,dlopen()函数的功能就是装载一个动态库到进程空间。[2]

HAL当然不仅仅装载一个动态库而已。当你请求一个特定硬件类型的时候,它会检查/system/lib/hw目录查找匹配的文件名。文件名由硬件类型和运行的设备决定,如我们现在在模拟器上实现的这个新的硬件类型,HAL会找一个叫做opersyshw.goldfish.so的文件,goldfish是我们模拟器的代码。动态库必须包含一个提供HAL硬件信息的结构体,结构体要命名HAL_MODULE_INFO_SYM_AS_STR。在下一个章节我们会看到一个例子。

对于一个新硬件类型本身的定义是放在hardware/libhardware/include/hardware/目录下的头文件,针对本例中即为opersyshw.h。

#ifndef ANDROID_OPERSYSHW_INTERFACE_H
#define ANDROID_OPERSYSHW_INTERFACE_H
#include <stdint.h>
#include <sys/cdefs.h>
#include <sys/types.h>
#include <hardware/hardware.h>

__BEGIN_DECLS
#define OPERSYSHW_HARDWARE_MODULE_ID "opersyshw"
struct opersyshw_device_t {
    struct hw_device_t common;

    int (*read)(char* buffer, int length);
    int (*write)(char* buffer, int length);
    int (*test)(int value);
};
__END_DECLS

#endif // ANDROID_OPERSYSHW_INTERFACE_H

这个文件除了定义了read和write原型之外,还定义了OPERSYSHW_HARDWARE_MODULE_ID,这个ID成为了在文件系统中查找HAL模块文件名的基础。

HAL模块

Android的思想是每个设备都需要一个相应的HAL模块来抽象特性的硬件类型。例如不同的硬件厂商的手机会使用不同的图形芯片,因此也就有不同的gralloc模块。通常来说,添加到AOSP源的HAL模块在device/<vendor>/<product>的lib*目录里,我们模拟器则放在sdk/emulator/目录下。我们的硬件驱动:opersyshw_qemu.c(环形缓冲驱动程序)就放在这个目录下。

为了这个文件产生的库文件可以被HAL管理识别,它需要以下面这样的代码段结尾:

static struct hw_module_methods_t opersyshw_module_methods = {
    .open = open_opersyshw
};

const struct hw_module_t HAL_MODULE_INFO_SYM = {
    .tag = HARDWARE_MODULE_TAG,
    .version_major = 1,
    .version_minor = 0,
    .id = OPERSYSHW_HARDWARE_MODULE_ID,
    .name = "Opersys HW Module",
    .author = "Opersys inc.",
    .methods = &opersyshw_module_methods,
};

由上面代码可得HAL_MODULE_INFO_SYM结构体和opersyshw_module_methods及其包含的open()函数指针。还记得前文提到的“调用模块的open函数"吗?指针经过传递最终调用open_opersyshw()。

static int open_opersyshw(const struct hw_module_t* module, char const* name,
        struct hw_device_t** device)
{
    struct opersyshw_device_t *dev = malloc(sizeof(struct opersyshw_device_t));
    memset(dev, 0, sizeof(*dev));

    dev->common.tag = HARDWARE_DEVICE_TAG;
    dev->common.version = 0;
    dev->common.module = (struct hw_module_t*)module;
    dev->read = opersyshw__read;
    dev->write = opersyshw__write;
    dev->test = opersyshw__test;
    *device = (struct hw_device_t*) dev;
    fd = open("/dev/circchar", O_RDWR);
    D("OPERSYS HW has been initialized");
    return 0;
}

这个函数主要将对设备的操作函数挂接到了device结构体,该结构体的类型就是opersyshw.h文件中定义的opersyshw_device_t,然后打开了/dev目录下的设备节点,建立了驱动程序与系统服务之间的连接。

最后,我们看一下opersyshw__read函数:

int opersyshw__read(char* buffer, int length)
{
    int retval;
    D("OPERSYS HW - read()for %d bytes called", length);
    retval = read(fd, buffer, length);    
    return retval;
}

这里我们没有做错误检测和处理,但这是你的驱动应该需要做的。现在,从系统服务为起点的调用的路径已经很清晰了。系统服务的read导致对JNI函数read_native()的调用,通过HAL,调用到了HAL模块中的opersyshw_read()。

调用系统服务

系统服务通过binder被其他系统和应用所调用,至少需要一个接口定义,我们依然以opersys服务为例,增加IOpersysService.aidl到frameworks/base/core/java/android/os中:

package android.os;
interface IOpersysService {
/**
* {@hide}
*/
String read(int maxLength);
int write(String mString);
}

这个修改使得ASOP中编译的代码可以调用到我们的系统服务。例如,我们可以在packages/apps目录下增加一个app,并在其onCreat()回调函数中这么做:

public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        IOpersysService om = IOpersysService.Stub.asInterface(ServiceManager.getService("opersys"));
        try {
            Log.d(DTAG, "Going to write to the \"opersys\" service");
            om.write("Hello Opersys");
            Log.d(DTAG, "Service returned: " + om.read(20));
        }
        catch (Exception e) {
            Log.d(DTAG, "FAILED to call service");
            e.printStackTrace();
        }
    }

我们这里使用了ServiceManager.getService()来得到Binder句柄,然后通过IOpersysService.Stub.asInterface()来把它转化成我们可以使用的IOpersysService对象,这对跟ASOP编译的app代码来说是可行的,但对于普通app来说就行不通了,因为ServiceManager.getService在SDK中是不存在的。

为了解决上边的问题,我们需要执行额外几个步骤来构建一个新的SDK。首先,我们需要创建一个manager类来进一步把binder可用服务包装起来,我们在frameworks/base/core/java/android/os目录中增加OpersysManager.java:

package android.os;
import android.os.IOpersysService;
public class OpersysManager
{
    public String read(int maxLength) 
    {
        try {
                return mService.read(maxLength);
            } catch (RemoteException e) {
                return null;
            }
    }
    public int write(String mString) 
    {
    try {
            return mService.write(mString);
         } catch (RemoteException e) {
                return 0;
         }
    }

    public OpersysManager(IOpersysService service) {
        mService = service;
    }
    IOpersysService mService;
}

这里可以看到将IOpersysService用OpersysManager包了一层。接下来要使得这个manager能通过getSystemService()调用获得,还需要两个步骤要做。首先,需要修改frameworks/base/core/java/android/content/Context.java文件,将其增加到应用上下文环境中:

// ***** <ADDED-BY-OPERSYS> *****
    /**
     * Use with {@link #getSystemService} to retrieve a
     * {@link android.os.OpersysManager} for using Opersys Service.
     *
     * @see #getSystemService
     */
    public static final String OPERSYS_SERVICE = "opersys";
    // ***** </ADDED-BY-OPERSYS> *****

然后,我们需要修改frameworks/base/core/java/android/content/ContextImpl.java以使得getSystemService()函数能够识别这个新服务:

public Object getSystemService(String name){
    if(WINDOW_SERVICE.equals(name)){
        return WindowManagerImpl.getDefault();
    }else if(LAYOUT_INFLATER_SERVICE.equals(name)){
        synchronized(mSync){
    ...
    }else if(NFC_SERVICE.equals(name)){
        return getNFCManager();

    }else if(OPERSYS_SERVICE.equals(name)){
        return getOpersysManager();
    ...

getSystemService类的实现在android不同版本之间差异较大,您可以参考一下ContextImpl类如何声明新的manager,理一下POWER_SERVICE是怎么利用getSystemService得到要用的PowerManager对象,就可以照葫芦画瓢了。

到此,我们就可以通过这个ASOP构建一个新的SDK,然后基于这个SDK编译的APP就可以像调用其他预定义服务一样调用这个新的系统服务:

public void onCreat(Bundle savedInstanceState){
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    
    OpersysManager om = (OpersysManager)getSystemService(OPERSYS_SERVICE);
    
    Log.d(DTAG, "Going to write to the \"opersys\" service");
    om.write("Hello Opersys");
    Log.d(DTAG, "Service returned: " + om.read(20));

结语

这个例子还有一点没有介绍,就是这个系统服务是怎么启动的。基于Java的系统服务是在frameworks/base/services/java/com/android/server/SystemServer.java中启动的。所以,我们修改这个文件来实例化我们的系统服务并向Service Manager注册:

...
 // ***** <ADDED-BY-OPERSYS> *****
        try {
                Slog.i(TAG, "Opersys Service");
                ServiceManager.addService(Context.OPERSYS_SERVICE, new OpersysService(context));
            } catch (Throwable e) {
                Slog.e(TAG, "Failure starting OpersysService Service", e);
            }
// ***** </ADDED-BY-OPERSYS> *****
...

上面为AOSP添加新类型硬件的方法和给出的代码仅仅就是能工作而已。因为你需要修改一些ASOP的文件,所以它是依赖版本的。定制化的扩展最好还是添加到device/<manufacturer/product_name>/目录,这样你就可以把它复制到任意的AOSP代码树里面去。但是尽管有些缺点,但是这种方法你可以以ASOP中大量的HAL模块作为例子,并且可以简单的复制这些代码来用,因为你增加的代码位置跟其他的内建组件是一样的。

要使得新类型的硬件对app和其他系统服务可用,还有很多种路线。我们可以把系统服务添加到产品相关的目录device/中,我们也可以采用Java创建一个独立的系统服务,甚至可以使用C这样的语言创建本地服务。虽然程序可以直接运行在CPU上,但是你就不能像Java一样使用aidl这样的工具来产生封装和解封装代码,与Binder通信的数据也都只能手工进行封装和解封装,这是相当繁琐的过程。


[1]亚荷毛尔.构建嵌入式Android系统[DB/CD].北京:中国电力出版社,2015.

[2]王振丽.底层开发技术实战详解-内核、移植和驱动[DB/CD].北京:电子工业出版社,2012.

十六宿舍 原创作品,转载必须标注原文链接。
©2023 Yang Li. All rights reserved.
欢迎关注 『十六宿舍』 ,大家喜欢的话,给个 👍 ,更多关于嵌入式相关技术的内容持续更新中。

有关嵌入式Android系统增加新硬件支持的更多相关文章

  1. ruby - 检查数组是否在增加 - 2

    这个问题在这里已经有了答案:Checktoseeifanarrayisalreadysorted?(8个答案)关闭9年前。我只是想知道是否有办法检查数组是否在增加?这是我的解决方案,但我正在寻找更漂亮的方法:n=-1@arr.flatten.each{|e|returnfalseife

  2. 电脑0x0000001A蓝屏错误怎么U盘重装系统教学 - 2

      电脑0x0000001A蓝屏错误怎么U盘重装系统教学分享。有用户电脑开机之后遇到了系统蓝屏的情况。系统蓝屏问题很多时候都是系统bug,只有通过重装系统来进行解决。那么蓝屏问题如何通过U盘重装新系统来解决呢?来看看以下的详细操作方法教学吧。  准备工作:  1、U盘一个(尽量使用8G以上的U盘)。  2、一台正常联网可使用的电脑。  3、ghost或ISO系统镜像文件(Win10系统下载_Win10专业版_windows10正式版下载-系统之家)。  4、在本页面下载U盘启动盘制作工具:系统之家U盘启动工具。  U盘启动盘制作步骤:  注意:制作期间,U盘会被格式化,因此U盘中的重要文件请注

  3. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  4. kvm虚拟机安装centos7基于ubuntu20.04系统 - 2

    需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc

  5. 安卓apk修改(Android反编译apk) - 2

    最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路

  6. ruby - 在没有基准或时间的情况下用 Ruby 测量用户时间或系统时间 - 2

    因为我现在正在做一些时间测量,我想知道是否可以在不使用Benchmark类或命令行实用程序time的情况下测量用户时间或系统时间。使用Time类只显示挂钟时间,而不显示系统和用户时间,但是我正在寻找具有相同灵active的解决方案,例如time=TimeUtility.now#somecodeuser,system,real=TimeUtility.now-time原因是我有点不喜欢Benchmark,因为它不能只返回数字(编辑:我错了-它可以。请参阅下面的答案。)。当然,我可以解析输出,但感觉不对。*NIX系统的time实用程序也应该可以解决我的问题,但我想知道是否已经在Ruby中实

  7. ruby - 以毫秒为单位获取当前系统时间 - 2

    在Ruby中,以毫秒为单位获取自纪元(1970)以来的当前系统时间的正确方法是什么?我试过了Time.now.to_i,好像不是我想要的结果。我需要结果显示毫秒并且使用long类型,而不是float或double。 最佳答案 (Time.now.to_f*1000).to_iTime.now.to_f显示包含十进制数字的时间。要获得毫秒数,只需将时间乘以1000。 关于ruby-以毫秒为单位获取当前系统时间,我们在StackOverflow上找到一个类似的问题:

  8. ruby-on-rails - 如何构建复杂的 Rails 系统 - 2

    关闭。这个问题需要更多focused.它目前不接受答案。想改进这个问题吗?更新问题,使其只关注一个问题editingthispost.关闭8年前。Improvethisquestion我们有以下(以及更多)系统,我们将数据从一个应用推送/拉取到另一个:托管CRM(InsideSales.com)Asterisk电话系统(内部)横幅广告系统(openx,我们托管)潜在客户生成系统(自行开发)电子商务商店(spree,我们托管)工作板(本土)一些工作网站抓取+入站工作提要电子邮件传送系统(如Mailchimp,自主开发)事件管理系统(如eventbrite,自主开发)仪表板系统(大量图表和

  9. ruby-on-rails - Rails 3,在RAILS_ROOT上方显示来自本地文件系统的jpg图片 - 2

    我正在尝试找出一种方法来显示来自不在RAILS_ROOT下(在RedHat或Ubuntu环境中)的已安装文件系统的图像。我不想使用符号链接(symboliclink),因为这个应用程序实际上是通过Tomcat部署的,而当我关闭Tomcat时,Tomcat会尝试跟随符号链接(symboliclink)并删除挂载中的所有图像。由于这些文件的数量和大小,将图像放在public/images下也不是一种选择。我查看了send_file,但它只会显示一张图片。我需要在一个格式良好的页面中显示6个请求的图像。由于膨胀,我宁愿不使用Base64编码,但我不知道如何将图像数据与呈现的页面一起传递下去。

  10. ruby - 如何使用 readline 支持重新安装 ruby​​? - 2

    我已经按照https://github.com/wayneeseguin/rvm#installation上的说明通过RVM安装了Ruby.有关信息,我有所有文件(readline-5.2.tar.gz、readline-6.2.tar.gz、ruby-1.9.3-p327.tar.bz2、rubygems-1.8.24.tgz、wayneeseguin-rvm-stable.tgz和yaml-0.1.4.tar.gz)在~/.rvm/archives目录中,我不想在任何目录中重新下载它们方式。当我这样做时:sudo/usr/bin/apt-getinstallbuild-essent

随机推荐