Automotive Grade Linux是一套开源的车载系统平台。本文是对该平台的整体介绍。
介绍
Automotive Grade Linux (简称AGL)是一个由Linux基金会主导的一个开源项目。这是一个由汽车制造商,供应商,技术公司共同组建的开发团体开发的项目。
AGL是一个为车载环境开发的操作系统,我们通常称这种系统叫做车载信息娱乐系统(In-Vehicle Infotainment,简称IVI)。
大家熟悉的Android Auto和Apple CarPlay也都属于IVI系统。
我之前写过一篇文章介绍Android IVI系统,有兴趣的可以移步:《Android与汽车》。
AGL项目于2012年启动,最初的成员包括:捷豹路虎,日产,丰田,富士通,英伟达,三星等公司。
截至目前(2019年5月)该项目包含的成员已经超过100家,具体成员列表可以见这里:AGL Members。
IVI市场现状
这里,我们大致了解一下整个汽车行业的IVI市场现状。
下表整理了几大汽车厂商的IVI系统以及底层系统的信息:
品牌 | 专属IVI系统 | 底层系统 |
---|---|---|
福特 | SYNC 3 | QNX |
奔驰 | COMAND/MBUX | QNX |
奥迪 | MMI | QNX |
宝马 | iDriver | QNX |
大众 | Composition Meida | QNX |
沃尔沃 | Sensus | QNX |
丰田 | G-Book | Linux |
特斯拉 | Version | Linux |
雪佛兰 | MyLink | Linux |
本田 | Honda Connect | Android |
蔚来 | NOMI | Android |
到目前为止,黑莓的QNX在市场占有率上有绝对的优势。下面是几个主流系统的对比:
操作系统 | 占有率 | 优势 | 劣势 | 合作厂商与供应商 |
---|---|---|---|---|
QNX | 约50% | 安全性,稳定性极高,符合车规级要求 | 商业软件,需要授权费用,只应用在较高端车型上 | 通用,克莱斯勒,凯迪拉克,雪佛兰,雷克萨斯,路虎,保时捷,奥迪,宝马,大陆,博士等 |
Linux | 约20% | 免费,灵活 | 应用生态不完善,技术支持差 | 丰田,日产,特斯拉 |
Android | 目前较低 | 开源,有强大的移动生态环境 | 安全性较差,无法适配仪表盘等安全要求高的部件 | 奥迪,通用,蔚来,小鹏,吉利,比亚迪,博泰,英伟达等 |
WinCE | 约16% | Windows应用开发便利 | 即将退出历史舞台 | 福特Sync 1, Sync2等 |
就目前来说,AGL的市场影响力还很小。仅仅在以下两款车型上使用:
不过,随着越来越多的车厂以及供应商加入AGL,以Linux免费和开源的特性,也会吸引更多的开发者。
相信在未来会逐步扩大市场份额。
AGL版本历史
AGL以一年两个版本的节奏进行开发。每个版本包含3~4个月的开发时间和一段时间的功能冻结阶段。
AGL以统一代码库(Unified Code Base)的形式对外发布版本。这是结合了汽车制造商以及设备供应商的Linux发行版本。
详细的版本历史如下:
- 2014年6月30日,初始版本发布
- 2016年1月,Agile Albacore (UCB 1.0)发布
- 2016年7月,Brilliant Blowfish Release (UCB 2.0)发布
- 2017年1月,Charming Chinook Release (UCB 3.0)发布
- 2017年8月, Daring Dab Release(UCB 4.0)发布
- 2018年1月, Electric Eel Release(UCB 5.0)发布
- 2018年10月,Funky Flounder Release (UCB 6.0)发布
最新版本的特性可以访问这里:Unified Code Base。
系统构建
AGL系统的构建流程如下:
- 准备开发的宿主系统
- 下载AGL编译环境
- 初始化编译环境
- 编译系统镜像
下面我们就按这个流程来进行构建。
准备开发的宿主系统
AGL的开发环境需要使用Yocto Project编译系统,所以需要以此为基础来准备开发环境。
目前,Yocto项目支持的Linux版本包括下面这些:
- Ubuntu 14.10
- Ubuntu 15.04
- Ubuntu 15.10
- Ubuntu 16.04 (LTS)
- Fedora release 22
- Fedora release 23
- Fedora release 24
- CentOS release 7.x
- Debian GNU/Linux 8.x (Jessie)
- Debian GNU/Linux 9.x (Stretch)
- openSUSE 13.2
- openSUSE 42.1
考虑到Ubuntu Linux的使用的人群最多,因此笔者建议大家以这个系统来进行准备。
在安装好Ubuntu系统之后,还需要安装如下的开发软件包以便编译AGL系统:
sudo apt-get install gawk wget git-core diffstat unzip texinfo gcc-multilib \
build-essential chrpath socat cpio python python3 python3-pip python3-pexpect \
xz-utils debianutils iputils-ping libsdl1.2-dev xterm
下载AGL编译环境
AGL项目中包含了很多子项目,因此建议大家创建一个新的文件夹来存放相关文件。
这里以workspace_agl
文件夹为例来构建编译环境。
export AGL_TOP=$HOME/workspace_agl
mkdir -p $AGL_TOP
AGL使用repo
工具来管理源码,所以需要先下载这个工具:
mkdir -p ~/bin
export PATH=~/bin:$PATH
curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
chmod a+x ~/bin/repo
获取工具之后就可以根据需求来下载不同版本的环境。
如果是希望获取稳定版源码,可以通过下面这个命令:
cd $AGL_TOP
repo init -b flounder -u https://gerrit.automotivelinux.org/gerrit/AGL/AGL-repo
repo sync
如果是希望获取最新的版本,可以使用下面这个命令:
cd $AGL_TOP
repo init -u https://gerrit.automotivelinux.org/gerrit/AGL/AGL-repo
repo sync
初始化编译环境
下载完成之后,需要通过一个脚本来初始化编译环境,脚本位于下面这个路径:
$AGL_TOP/meta-agl/scripts/aglsetup.sh
通过这个脚本可以设置编译的参数,例如:目标硬件,编译产出的目录等等。
可以通过下面这个命令来查看帮助:
source meta-agl/scripts/aglsetup.sh -h
在执行aglsetup.sh
脚本之前,需要先确定想要编译出AGL系统的哪些特性。
特性的选择可以通过指定分层来确定,目前主要使用的分层有下面三个:
meta-agl
:包含AGL发行版的最小集合。meta-agl-demo
:包含了参考UI实现以及演示平台和应用。meta-agl-devel
:包含了开发中的模块。这个也会包含OEM需要的但不在AGL中的软件包。
关于特性的详细说明请看这里。
这里我们只要编译出模拟器中可以运行的演示系统即可,因此执行下面这条命令:
source meta-agl/scripts/aglsetup.sh -f -m qemux86-64 agl-demo agl-devel
执行完这条命令之后,脚本便会生成编译需要的配置文件。
编译系统镜像
接下来输入下面这条命令:
bitbake agl-demo-platform
这个步骤会比较耗时,可能需要若干个小时,因为其中包含了下载和编译的过程。具体时间长度取决于你的网络环境和开发机器的硬件配置。
编译完成之后会自动生成系统镜像。
笔者在Ubuntu系统上搭建了编译环境,使用 gcc 4.9.2 编译失败。升级到 gcc 6.5.0之后编译成功。
启动编译产物
编译出的镜像位于 build/tmp/deploy/images/
目录下。根据编译时的设置会存在于不同的子文件夹中。镜像的文件名以 vmdk
为后缀。
例如,根据上面的配置,本次编译出的镜像是:
build/tmp/deploy/images/qemux86-64/agl-demo-platform-qemux86-64-20190529011851.rootfs.wic.vmdk
这个文件可以通过QEMU启动,也可以通过VirtualBox启动。
笔者将上面这个vmdk文件拷贝的MacBook Pro上之后,通过VirtualBox新建了虚拟机,配置如下:
启动之后界面如下:
如果你没有自己的编译环境,也可以到这里:AGL/release 直接下载官方编译好的镜像。
系统架构
AGL系统的完整功能说明见这里:《Automotive Grade Linux Requirements Specification》。
这份文档中描述了AGL的整体系统架构,并对每一个模块都做了说明。
系统架构图如下所示:
从这幅图中我们可以看出,整个AGL系统可以分为四层。从上至下以此是:
- 应用和界面层:包含各种类型的应用程序,以及用户接触到的操作界面。
- 应用程序框架层:提供接口来管理运行中的应用程序。这一层又分为:本地应用,AGL应用框架,Web应用三个部分。
- 服务层:包含了位于用户空间的所有应用程序可以访问的服务,这些服务分为平台服务和汽车服务两大类。
- 操作系统层:包含了系统内核以及驱动设备。
浏览源码
事实上,通过repo
工具获取的并非是系统的源码,仅仅是项目的元数据。真正的源码在编译的过程中才会拉取。拉取后,源码便位于build/tmp/work/
目录下。
通过浏览源码会发现,AGL项目使用CMake做为编译的配置工具,因此可以通过查看每个工程的CMakeLists.txt
来了解工程的目标产物。
对于没有自己搭建编译环境的读者,也可以到这里:AGL Gerrit 浏览所有工程。每个工程的描述页面都会包含git
命令来获取相应的源码。
由于篇幅所限,下文选取系统中最核心应用程序框架和人机界面框架做一些介绍。
应用程序框架
应用程序框架(Application Framework)负责管理所有的应用程序。管理主要分为两类:
- 静态的:包括应用程序的安装和卸载。
- 动态的:应用程序的运行时管理,例如:应用启动和退出,应用间的切换等。
应用程序框架最核心的就是afm-system-daemon
和 afm-user-daemon
两个模块。这是两个独立的进程,它们通过D-Bus进行通信。如下图所示:
其中,
afm-system-daemon
负责应用的安装和卸载afm-user-daemon
负责应用程序生命周期的管理,包括:启动,退出,暂停和恢复等。
应用程序框架的项目地址见这里:src/app-framework-main。
可以通过下面这条命令直接获取其源码:
git clone "https://gerrit.automotivelinux.org/gerrit/src/app-framework-main"
AGL使用systemd来管理系统进程的启动。
afm-system-daemon
和afm-user-daemon
的启动配置分别如下:
[Unit]
Description=Application Framework Master, system side
Requires=afm-system-setup.service
[Service]
#User=afm
#Group=afm
SyslogIdentifier=afm-system-daemon
ExecStart=/usr/bin/afb-daemon --name=afm-system-daemon --no-httpd --no-ldpaths --binding=@afm_libexecdir@/afm-binding.so --ws-server=sd:afm-main
Restart=on-failure
RestartSec=5
CapabilityBoundingSet=CAP_DAC_OVERRIDE CAP_MAC_OVERRIDE
[Install]
WantedBy=multi-user.target
[Unit]
Description=Application Framework Master, User side
[Service]
Type=dbus
BusName=org.AGL.afm.user
ExecStart=/usr/bin/afm-user-daemon --user-dbus=unix:path=%t/bus unix:@afm_platform_rundir@/apis/ws/afm-main
Environment=AFM_APP_INSTALL_DIR=%%r
EnvironmentFile=-@afm_confdir@/unit.env.d/*
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
从上面这个配置可以看出:
afm-system-daemon
由/usr/bin/afb-daemon
可执行文件启动。afm-user-daemon
由/usr/bin/afm-user-daemon
可执行文件启动。
afb-daemon
是Application Framework Binder Daemon的简写。
AGL系统进程通过D-Bus 暴露接口。关于这两个服务暴露的接口说明见这里:The afm daemons: The D-Bus interface。
afm-system-daemon
暴露的接口包括下面这些
- install
- uninstall
afm-user-daemon
暴露的接口包括下面这些
- runnables
- detail
- start
- once
- terminate
- pause
- resume
- runners
- state
- install
- uninstall
其中,afm-user-daemon
的install和uninstall接口最终还是会请求afm-system-daemon
。
afm-user-daemon
afm-user-daemon
的main
函数位于afm-user-daemon.c
文件中。
这是一个守护进程,守护进程在启动之后通常都会一直在loop上循环等待消息的处理。
/* start servicing */
if (jbus_start_serving(user_bus) < 0) {
ERROR("can't start server");
return 1;
}
/* run until error */
for(;;)
sd_event_run(evloop, (uint64_t)-1);
return 0;
对外接口名称和内部实现的绑定在 afm-binding.c
文件中完成:
static const afb_verb_t verbs[] =
{
{.verb=_runnables_, .callback=runnables, .auth=&auth_detail, .info="Get list of runnable applications", .session=AFB_SESSION_CHECK },
{.verb=_detail_ , .callback=detail, .auth=&auth_detail, .info="Get the details for one application", .session=AFB_SESSION_CHECK },
{.verb=_start_ , .callback=start, .auth=&auth_start, .info="Start an application", .session=AFB_SESSION_CHECK },
{.verb=_once_ , .callback=once, .auth=&auth_start, .info="Start once an application", .session=AFB_SESSION_CHECK },
{.verb=_terminate_, .callback=terminate, .auth=&auth_kill, .info="Terminate a running application", .session=AFB_SESSION_CHECK },
{.verb=_pause_ , .callback=pause, .auth=&auth_kill, .info="Pause a running application", .session=AFB_SESSION_CHECK },
{.verb=_resume_ , .callback=resume, .auth=&auth_kill, .info="Resume a paused application", .session=AFB_SESSION_CHECK },
{.verb=_runners_ , .callback=runners, .auth=&auth_state, .info="Get the list of running applications", .session=AFB_SESSION_CHECK },
{.verb=_state_ , .callback=state, .auth=&auth_state, .info="Get the state of a running application", .session=AFB_SESSION_CHECK },
{.verb=_install_ , .callback=install, .auth=&auth_install, .info="Install an application using a widget file", .session=AFB_SESSION_CHECK },
{.verb=_uninstall_, .callback=uninstall, .auth=&auth_uninstall, .info="Uninstall an application", .session=AFB_SESSION_CHECK },
{.verb=NULL }
};
这是一个数组,每一项对应了一个接口的绑定。.callback
指定的是逻辑实现的回调函数。
afm-user-daemon
中对应用程序生命周期的管理实现在以下这些函数中:
extern int afm_urun_start(struct json_object *appli, int uid);
extern int afm_urun_once(struct json_object *appli, int uid);
extern int afm_urun_terminate(int runid, int uid);
extern int afm_urun_pause(int runid, int uid);
extern int afm_urun_resume(int runid, int uid);
extern struct json_object *afm_urun_list(struct afm_udb *db, int all, int uid);
extern struct json_object *afm_urun_state(struct afm_udb *db, int runid, int uid);
extern int afm_urun_search_runid(struct afm_udb *db, const char *id, int uid);
例如,下面这段代码负责应用程序的启动:
int afm_urun_once(struct json_object *appli, int uid)
{
const char *udpath, *state, *uscope, *uname;
int rc, isuser;
/* retrieve basis */
rc = get_basis(appli, &isuser, &udpath, uid);
if (rc < 0)
goto error;
/* start the unit */
rc = systemd_unit_start_dpath(isuser, udpath);
if (rc < 0) {
j_read_string_at(appli, "unit-scope", &uscope);
j_read_string_at(appli, "unit-name", &uname);
ERROR("can't start %s unit %s for uid %d", uscope, uname, uid);
goto error;
}
state = wait_state_stable(isuser, udpath);
if (state == NULL) {
j_read_string_at(appli, "unit-scope", &uscope);
j_read_string_at(appli, "unit-name", &uname);
ERROR("can't wait %s unit %s for uid %d: %m", uscope, uname, uid);
goto error;
}
if (state != SysD_State_Active) {
j_read_string_at(appli, "unit-scope", &uscope);
j_read_string_at(appli, "unit-name", &uname);
ERROR("start error %s unit %s for uid %d: %s", uscope, uname, uid, state);
goto error;
}
rc = systemd_unit_pid_of_dpath(isuser, udpath);
if (rc <= 0) {
j_read_string_at(appli, "unit-scope", &uscope);
j_read_string_at(appli, "unit-name", &uname);
ERROR("can't getpid of %s unit %s for uid %d: %m", uscope, uname, uid);
goto error;
}
return rc;
error:
return -1;
}
如果你浏览这些源码就会发现,这里面还有些逻辑没有实现完全:
int afm_urun_pause(int runid, int uid)
{
return not_yet_implemented("pause");
}
int afm_urun_resume(int runid, int uid)
{
return not_yet_implemented("resume");
}
afb-daemon
afb-daemon
可执行文件的源码位于另外一个工程中:src/app-framework-binder
可以通过下面这条命令获取其源码:
git clone "https://gerrit.automotivelinux.org/gerrit/src/app-framework-binder"
binder提供了将应用程序连接到所需服务的方法。利用binder,可以安全地为各种语言编写并几乎可在任何地方运行的应用程序提供服务。
这里的binder和Android中的binder不是一回事。
afb-daemon
与应用程序的关系如下图所示:
afb-daemon 提供了同时提供了http接口和WebSocket接口。
afb-daemon
的main
函数位于main-afb-daemon.c
文件中。其main函数中主体逻辑做了四件事情:
- 解析传入的参数。
- 根据
name
参数设置进程名称。回顾一下上文,启动afm-system-daemon
的时候就是通过systemd的配置文件指定了name参数:--name=afm-system-daemon
。 - 设置为守护进程。
- 开始服务。
考虑到守护进程在同一时刻可能被多个客户端并发请求,所以afb-daemon
以多线程的方式工作。具体是通过下面这个接口实现的:
int jobs_start(int allowed_count, int start_count,
int waiter_count, void (*start)(int signum, void* arg), void *arg)
这里的几个参数说明如下:
allowed_count
最大支持的线程数量start_count
初始启动的线程数量waiter_count
最多等待的请求start
处理请求的函数指针
main
函数中对于这几个参数设定的值如下:
jobs_start(3, 0, 50, start, NULL);
afm-util
应用程序框架提供了一个命令行工具来方便开发和测试,这个工具的名称叫做afm-util
。
提供的功能包含下面这些:
list
runnables list the runnable widgets installed
add wgt
install wgt install the wgt file
remove id
uninstall id remove the installed widget of id
info id
detail id print detail about the installed widget of id
ps
runners list the running instance
run id
start id start an instance of the widget of id
kill rid
terminate rid terminate the running instance rid
status rid
state rid get status of the running instance rid
人机界面框架
人机界面(Human-Machine Interface,简称HMI)框架主要包含下面几个模块:
- WindowManager
- HomeScreen
- SoundManager
- InputManager
最新版本(2019年1月)的HMI架构文档见这里:AGL HMI Framework Architecture Document。
2018年的演示幻灯片见这里:Proposal for AGL HMI-Framework。
WindowManager
WindowManager正如其名称所示,该模块负责系统的窗口管理。通过下面的命令可以获取其源码:
git clone https://gerrit.automotivelinux.org/gerrit/apps/agl-service-windowmanager
WindowManager需要管理的硬件包括:显示设备,GPU,输入设备和显存。它的主要职责包括:
- 绘制窗口
- 管理好窗口的层级
- 处理可视化效果动画
- 显示帧率管理
- 有效利用各种硬件,并减少硬件依赖
- 多窗口,多屏幕管理
- 兼容性管理
官网上该模块的文档 Window Manager Application Guide 已经有很详细的说明。
下面仅对该模块的做一个简单介绍。
WindowManager的整体架构如下图所示:
WindowManager模块包含四个部分:
afb-binder
- 服务绑定库
- 用于策略管理的共享库
- 配置文件
应用程序可以利用 libwindowmanager.so
来使用WindowManager的接口。接口包括下面这些:
int requestSurface(const char* role);
int requestSurfaceXDG(const char* role, unsigned ivi_id);
int activateWindow(const char* role, const char* area);
int activateWindow(const char* role);
int deactivateWindow(const char* role);
int endDraw(const char* role);
struct Screen getScreenInfo();
int getAreaInfo(const char* role, Rect *out_rect);
void setEventHandler(const WMHandler& wmh);
HomeScreen
桌面模块的实现包含在下面几个项目中:
- homescreen:HomeScreenGUI,这是一个Qt的应用程序
- launcher:LauncherGUI,这是一个Qt的应用程序
- gl-service-homescreen:HomeScreenBinder的绑定库
- libhomescreen:提供来给应用程序与HomeScreenBinder通讯的库
- libqthomescreen:提供给Qt应用程序与HomeScreenBinder通讯的库,基于libhomescreen
libhomescreen
中提供了一组接口用来管理桌面:
int init(const int port, const std::string& token);
int tapShortcut(const char* application_id);
int onScreenMessage(const char* display_message);
int onScreenReply(const char* reply_message);
void set_event_handler(enum EventType et, handler_func f);
void registerCallback(
void (*event_cb)(const std::string& event, struct json_object* event_contents),
void (*reply_cb)(struct json_object* reply_contents),
void (*hangup_cb)(void) = nullptr);
int call(const std::string& verb, struct json_object* arg);
int call(const char* verb, struct json_object* arg);
int subscribe(const std::string& event_name);
int unsubscribe(const std::string& event_name);
int showWindow(const char* application_id, json_object* json);
int hideWindow(const char* application_id);
int replyShowWindow(const char* application_id, json_object* json);
int showNotification(json_object* json);
int showInformation(json_object* json);
int getRunnables(void);
这些接口的说明请参见
libhomescreen.cpp
文件。
例如,当有应用被点击的时候,被会调用showWindow接口显示影响的应用:
void HomescreenHandler::tapShortcut(QString application_id)
{
HMI_DEBUG("Launcher","tapShortcut %s", application_id.toStdString().c_str());
struct json_object* j_json = json_object_new_object();
struct json_object* value;
value = json_object_new_string("normal.full");
json_object_object_add(j_json, "area", value);
mp_hs->showWindow(application_id.toStdString().c_str(), j_json);
}
AGL应用开发
AGL系统既支持Web应用,也支持Native应用。系统框架会处理好不同类型应用的管理。
很显然,在进行AGL应用开发之前必须先获取相应的系统镜像。如果没有自己的编译环境,可以从官网上下载编译好的镜像。
AGL应用开发有两种模式:
- 使用Software Development Kit(简称SDK)
- 使用Cross Development System(简称XDS)
XDS是官方推荐的方式,XDS支持通过XDS dashboard 或XDS命令行在目标系统上构建,部署和运行个人项目。
关于如何开发AGL的应用程序请参见这里:Developer Guides。
QNX Hypervisor
前面已经提到,目前IVI市场上占有率最高是黑莓的QNX系统。这主要是因为其安全稳定的特性。
不过,QNX本身是收费的商业软件,并不开源,因此对其深度化定制就比较难了。
好在QNX系统了Hypervisor功能,这个功能类似于电脑上的虚拟机功能:在QNX的基础上运行其他的操作系统,例如Linux或者Android系统。如下图所示:
借助这个功能,在使用QNX的基础上,进行AGL的深度定制或许是一个不错的方案。