一篇写给嵌入式/车载/Android 底层工程师的实战指南,全程结合真实工程代码讲解。BSP开发圈子较小,如有感兴趣,欢迎私信交流~
一、引言:为什么是"配置BSP"而不是"移植BSP"?
很多同学第一次接触"基于 AOSP 定制 BSP"这个命题时,脑子里浮现的第一印象是"移植"——把代码拷过来、改改驱动、编出来能开机就算完事。但真正做过一两个量产项目的工程师都会告诉你:BSP 的功夫不在"移植",而在"配置"。
AOSP(Android Open Source Project)本身是一个高度工程化的"母版"。它把芯片无关的部分(框架、HAL 接口、构建系统、系统服务)和芯片/板卡相关的部分(内核、bootloader、分区表、设备树、vendor 驱动、HAL 实现)做了非常清晰的隔离。我们要做的事情,就是在这套母版里"填"上自己公司的产品信息,让它产出一套专属你产品的镜像。
本文以一套真实的 AOSP 派生车载工程(具体名字保密,产品形态是"车载智能网关 + 域控制器")为例,逐层拆解配置一套 BSP 的完整流程。
先给你一个整体地图,我们后面一步步展开:
AOSP 母版
│
├── build/ ← AOSP 派生构建系统(lunch/make 入口)
├── board/ ← 板级支持包(相当于官方 device/,产品定义都在这里)
├── kernels/ ← 内核 + bootloader + 安全固件
├── vendor/ ← 厂商预编译库 / 三方 SDK / 芯片驱动
├── hardware/ ← HAL 基础设施(libhardware 等)
├── system/ ← 系统核心组件(netd/adbd/lmkd 等)
├── prebuilt/ ← 预编译工具链
└── externals/ ← 第三方开源库(openssl/busybox 等)
二、先看懂 AOSP 构建系统的"三件套"
配置 BSP 之前,必须先理解 AOSP 构建系统是怎么"认识"一款产品的。不管你怎么定制,最终都逃不开这三件套:
-
lunch 入口:
vendorsetup.sh用add_lunch_combo注册产品,envsetup.sh加载环境,lunch <product>-<variant>选择产品与编译类型(eng/user/userdebug)。 -
BoardConfig.mk:定义板级硬件属性——CPU 架构、工具链、分区大小、内核路径、cmdline、SELinux 等。它回答的是"这块板子长什么样"。
-
Product.mk(产品 mk):定义产品软件组成——产品名、品牌、需要打进镜像的包和文件。它回答的是"这个产品装什么软件"。
我们的工程里,入口链是:
. build/envsetup.sh # 加载构建环境
lunch XXX-eng # 或者 brcm_rpi4-user
make # 默认目标是 nutshell,产出系统镜像
而 board/XXX/vendorsetup.sh 里只有两行,这就是产品"被 AOSP 认识"的注册动作:
add_lunch_combo XXX-eng
add_lunch_combo XXX-user
也就是说,你要新增一款产品,第一步就是写一个 vendorsetup.sh。这是整个定制的起点。
三、目录规划:一套企业级 BSP 该长什么样
很多公司失败在第一步——目录乱。AOSP 官方用 device/<oem>/<board> 和 vendor/<oem>/<board> 来分区,这套工程把它演进成了 board/ + vendor/ + kernels/ 的布局,原则是"谁来维护的东西放谁的地盘":
-
board/:板级支持包,放产品定义与板卡资源。例如
board/XXX/(XXX车规 SoC)和board/brcm/rpi4/(树莓派原型),board/common/放跨平台的公共资源(时区、TLS 证书、账号体系)。 -
vendor/:厂商预编译库与三方 SDK,按芯片平台分
vendor/XXX/、vendor/rpi3/、vendor/XXX/。里面全是"别人编译好、你只能集成"的二进制——比如博通 BCM89571 交换机 SDK、HSM 安全模块库、GmSSL/OpenSSL 预编译库。 -
kernels/:启动固件。
boot_XXX(车规多核多域启动)和boot_rpi(树莓派 u-boot/ATF/OP-TEE)。 -
hardware/:HAL 基础设施,以及车机定制 HAL 头文件(交换机、车辆电源、BLE)。
-
system/、prebuilt/、externals/:系统组件、工具链、三方源码。
关键经验:board/ 是"自己的地盘",vendor/ 是"供应商的地盘",kernels/ 是"芯片厂的地盘"。 三者通过 makefile 的 include 关系解耦,这样芯片换平台时,你只需要换 vendor/ 和 kernels/,board/ 里的产品逻辑基本不动。
四、第一步:搭产品骨架——三个文件定乾坤
4.1 产品信息 Product.mk
先看 board/XXX/g9q.mk 的开头,它定义了"你是谁":
(𝑐𝑎𝑙𝑙𝑖𝑛ℎ𝑒𝑟𝑖𝑡−𝑝𝑟𝑜𝑑𝑢𝑐𝑡,(SRC_TARGET_DIR)/product/generic.mk)
include board/XXX/BoardConfig.mk
PRODUCT_NAME := XXX # 产品名(lunch 时用的名字)
PRODUCT_DEVICE := XXX # 设备名
PRODUCT_BRAND := XXX # 品牌
PRODUCT_MODEL := XXX # 对外型号
PRODUCT_MANUFACTURER := XXX # 制造商
这些字段会被写进 system/build.prop,最终在运行时通过 ro.product.* 属性读取。注意第 2 行:产品 mk 主动 include 了 BoardConfig.mk,把硬件定义和软件定义耦合到一起——这是 AOSP 官方惯用写法(也可以在 lunch 脚本里组合)。
4.2 硬件属性 BoardConfig.mk
board/XXX/BoardConfig.mk 是配置的"重头戏"。它把芯片平台、CPU、工具链、内核、分区全部集中管理。抽几段核心的:
TARGET_ARCH := arm64
TARGET_ARCH_VARIANT := armv8-a
TARGET_CPU_VARIANT := XXX # XXX 四核 A55
# 交叉编译工具链前缀(用 Linaro 预编译的 aarch64 gcc)
TARGET_TOOLS_PREFIX := prebuilt/toolchains/g9x_gcc_linaro/gcc -linaro-7.3.1-2018.05-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu-
TARGET_KERNEL_PATH := kernels/kernel_g9x # 内核源码位置
TARGET_BOOT_PATH := $(NUTSHELL_BUILD_TOP)/kernels/boot_g9x # bootloader 位置
这里透露了两个 BSP 配置的核心技巧:
技巧一:工具链独立。 TARGET_TOOLS_PREFIX 指向 prebuilt/toolchains/ 下的 Linaro 预编译 gcc。这样你的构建机不用装芯片厂的 SDK,版本锁定、可复现、CI 里也好统一。
技巧二:用 TARGET_BOARD_VERSION 做多板卡变体管理。 看下面这段,一个产品定义里通过变量切换不同硬件版本:
#TARGET_BOARD_VERSION := g9x_evb
#TARGET_BOARD_VERSION := g9x_v03
#TARGET_BOARD_VERSION := g9q_evb
TARGET_BOARD_VERSION := g9q_v04 # 当前默认板卡
ifneq (
(𝑓𝑖𝑙𝑡𝑒𝑟𝑔9𝑞𝑣04,(TARGET_BOARD_VERSION)),)
TARGET_BOARD_DDRSIZE := 1GB
TARGET_KERNEL_DEFCONF := g9q_v04_defconfig # 对应内核 defconfig
INSTALLED_DTB_TARGET := g9q_v04.dtb # 对应设备树
TARGET_BOARD_PLATFORM := g9q
TARGET_PREBUILTS := G9Q_Ref_Linux_1G_2133 # 对应预编译固件集
endif
这是企业量产最常见的场景:同一颗 SoC,不同客户、不同内存配置、不同外设组合。你不可能为每个客户拉一个分支,正确做法就是像上面这样,用 ifneq(filter ...) 把"板卡版本 → DDR 大小 / 内核 defconfig / DTB / 预编译固件"做成一张配置表。改一行 TARGET_BOARD_VERSION,整套编译产物跟着变。
五、第二步:分区与文件系统规划——决定产品上限
BSP 工程师最容易被"坑"的就是分区规划。分区表一旦定错,后面想改就是伤筋动骨。看 BoardConfig.mk 里的定义:
TARGET_USERIMAGES_SPARSE_EXT_DISABLED := true
TARGET_USERIMAGES_USE_EXT4 := true
BOARD_SYSTEMIMAGE_PARTITION_SIZE := 128M # 系统分区只有 128M!
BOARD_CACHEIMAGE_PARTITION_SIZE := 536870912 # 512M
BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE := ext4
BOARD_USERDATAIMAGE_PARTITION_SIZE := 536870912
BOARD_FLASH_BLOCK_SIZE := 4096
看到 BOARD_SYSTEMIMAGE_PARTITION_SIZE := 128M 你会意识到:这是一个对镜像体积极其敏感的嵌入式产品。 128M 的系统分区意味着 system 镜像里放不下 Android 全家桶,只能放精简后的核心服务。这也是为什么这类 BSP 会大量使用"预编译小体积组件 + 按需裁剪"的策略。
配套的分区布局在 root/fstab.g9x 里定义,注意这几个关键字:
# 带 slotselect(A/B 槽位)和 verify(verity 校验)的典型车载配置
/dev/block/by-name/system_a /system ext4 ro,slotselect,verify ...
/dev/block/by-name/vendor_a /vendor ext4 ro,slotselect,verify ...
-
slotselect:A/B 分区冗余,配合
boot_controlHAL 做无缝升级。 -
verify:dm-verity 启动完整性校验,保证 system 分区没被篡改。
-
分区的
by-name别名来自 GPT 分区表(树莓派那边对应create_partition_table.sh里的 GPT 脚本)。
经验之谈:分区大小请按"最坏情况 + 20% 余量"来定。 128M 这种极限值虽然省 flash,但每次塞新功能都要动分区表,牵一发动全身。建议先做一版全量 build,用 du 摸清各分区真实占用,再倒推分区大小。
六、第三步:内核与 Bootloader 接入——最"芯片厂"的部分
内核和 bootloader 是 BSP 里最贴近芯片的部分,AOSP 不会帮你做,全靠 BoardConfig.mk 里的指针把它们"挂"进来。
6.1 内核
上面的 TARGET_KERNEL_PATH := kernels/kernel_g9x 指向内核源码,TARGET_KERNEL_DEFCONF 指定 defconfig,INSTALLED_DTB_TARGET 指定要打包的设备树。构建时 build/target/build-kernel.sh 会按这些配置编译内核、生成 boot.img。
这套工程还特意用了"按链接名解析构建目标"的技巧:build-kernel.sh + *.cfg 通过文件名判断该编哪个内核,把"多平台内核"和"单套构建系统"解耦。
6.2 Bootloader 与安全固件——多核多域的典型车规形态
这是本工程最有看点的部分。kernels/boot_g9x/ 不是一个普通 bootloader,而是一整套多核多域启动链:
AP1/AP2 应用域(跑 Android):ROM → SPL(DDR初始化) → dloader(preloader)
→ bootloader(AVB/A-B/dtbo) → AP1 内核
Safety 安全域(跑 FreeRTOS safety OS):ROM → t_loader(XIP 快速启动<100ms)
→ DDR_FW + DDR_INIT_SEQ → safety OS
Secure 域:sml.bin(安全监视器,配合 HSM)
整个启动链由 kernels/boot_g9x/nutshell.mk 驱动,产出 dil.bin / spl.bin / preloader.bin / ivi_bootloader.bin / safety.bin 等一堆固件,再汇编成 ospi_img。它传达了一个重要概念:车规 BSP 的"启动"不是单一链,而是多个核、多个域、多套 OS 并行引导。 你在配置自家产品时,至少要回答三个问题:
-
哪个核先起来?(主核引导顺序)
-
安全核和应用核怎么隔离?(内存隔离 / hypervisor / 独立核)
-
引导时间预算多少?(本工程 safety 域要求 <100ms,用 XIP 直接从 OSPI 启动)
对比树莓派分支 boot_rpi/,它则是标准的 u-boot + ARM Trusted Firmware(BL31) + OP-TEE 组合(nutshell.mk.atf 构建 armstub8.bin)。同一条构建系统,两种完全不同形态的 bootloader 都能挂——这正是 AOSP 把启动固件"外包"给 kernels/ 目录的好处。
七、第四步:HAL 定制——硬件抽象的"最后一公里"
AOSP 的软件层基本不用大动,真正要你写代码的是 HAL(硬件抽象层)。理解 HAL 从 hardware/libhardware 开始,它是所有 HAL 的"总开关"。
7.1 hw_get_module:HAL 的加载机制
hardware/libhardware/hardware.c 里核心就一个函数 hw_get_module(),它做的事是:按变体命名规则去指定目录 dlopen 一个 .so,然后 dlsym 出 HMI 符号。
/* 加载路径 */
/system/usr/lib/hw /vendor/lib/hw /vendor/lib64/hw /odm/lib/hw ...
/* 变体命名规则:<id>.<variant>.so */
ro.hardware.<id> → ro.hardware → ro.product.board → ro.board.platform → ro.arch → default
这段代码是整个 HAL 体系的灵魂:它意味着"只要 HAL 模块命名和放置路径对了,系统就能自动加载",不需要改任何框架代码。这也是 BSP 定制的核心理念——尽可能在 HAL 层做文章,别去动上层。
7.2 车机定制 HAL:你的产品差异化就藏在这里
这套工程在标准 HAL 之上,定制了这么几个"产品专属" HAL 头文件(都在 hardware/libhardware/include/hardware/ 下):
-
switch.h:车载以太网交换机 HAL。配合博通 BCM89571,暴露 VLAN / ATU 地址转发表 / 端口镜像 / 802.1Q / PHY XMDIO 寄存器 / 固件更新等接口。因为产品是"中央网关",交换机就是它的"心脏"。
-
vehicled.h:车辆电源/休眠守护 HAL。
enable / mode / sleep / wakeup_reason / battery / reboot,配合 UCOM 消息实现车辆上下电状态机。 -
ble.h:BLE HAL,带 CAN 响应回调。
-
boot_control.h:标准接口上追加了
setBootMode(mode),为 A/B OTA 定制。
启发: 拿到一颗新芯片,你不需要重写 Android HAL 框架,只需要:
-
按
hw_get_module的约定实现厂商 HAL(比如gps.你的平台.so); -
在
ro.hardware/ro.board.platform属性里填对平台名,让系统找到你的 .so; -
用
PRODUCT_PACKAGES把 .so 打进镜像。
八、第五步:vendor 预编译库与三方 SDK 集成
企业 BSP 里大量组件是供应商的闭源二进制——这是和开源社区开发最大的不同。看 board/semidrive/g9q/nutshell.mk,产品通过 include 把这些 vendor 组件"粘"进来:
- include vendor/g9x/openssl/openssl.mk # OpenSSL 预编译库
- include vendor/g9x/switchsdk2.6/libbcmutil_rpc.mk # BCM89571 交换机 RPC 客户端
- include vendor/g9x/power/power.mk # 电源管理 pmmng
- include vendor/g9x/hsm-rpmsg/hsm-rpmsg.mk # HSM 硬件安全模块
- include board/semidrive/g9q/security/nutshell.mk # 安全/签名
每个 .mk 里封装了三件事:库放哪、头文件放哪、安装到镜像的哪个位置。以 vendor/g9x/hsm-rpmsg 为例,它通过 RPMsg 与安全核上的 HSM 固件通信,对外提供 libhsm.so,API 长这样:
/* vendor/g9x/hsm-rpmsg/inc/hsm_lib.h */
hsm_key_install / hsm_aes_* / hsm_sm4_* / hsm_rsa_* / hsm_ecdsa_*
hsm_hash_* / hsm_cmac_* / hsm_hmac_* / hsm_aead_* / hsm_rnd_*
这套代码还清晰展示了一个量产痛点:同样的功能,不同供应商给不同 SDK。比如以太网交换机,switchsdk2.6 是博通 BCM89571(RPC 客户端 + 固件烧录),rtl904x 是瑞昱 RTL9040(T1 车用以太网,带 AVB/802.1Qbv/gPTP),iauto_mdc 是寄存器级 MDC/MDIO 直读。作为 BSP 架构师,职责就是给这些 SDK 包一层统一 HAL,让上层只看到 switch.h 一套接口。
给读者的建议: 给 vendor 组件写 .mk 时,务必做到"一个库一个 mk,路径全部相对化,头文件单独拷到 sysroot"。这样换版本、换平台时,你只需要动一个 mk 文件。
九、第六步:init 与开机服务编排
BSP 定制的最后一块拼图是开机流程。board/XXX/ 下有一组 rc 文件:
init.eng.rc / init.user.rc # 按 eng/user 变体区分的初始化脚本
init.g9.usb.configfs.rc # USB gadget(ADB)配置,基于 configfs
ueventd.rc # 设备节点权限管理
fstab.g9x # 分区挂载
system.prop # 系统属性
注意 init.$(TARGET_BUILD_VARIANT).rc 这个写法——它在 g9q.mk 里通过变量动态选择:
PRODUCT_COPY_FILES := \
(𝐿𝑂𝐶𝐴𝐿𝑃𝐴𝑇𝐻)/𝑖𝑛𝑖𝑡.(TARGET_BUILD_VARIANT).rc:root/init.rc \
$(LOCAL_PATH)/etc/init/system.rc:system/etc/init/system.rc \
...
同一个板卡,eng 版开 ADB、开调试服务、关 dm-verity;user 版关调试、开 SELinux enforcing、开安全校验。 这就是量产安全性和研发效率之间的平衡术——用构建变体自动切换,而不是手动改文件。
ueventd.rc 则管理 /dev 节点权限,车载里典型的是给交换机、HSM、RPMB 这些设备节点限定 uid/gid 和 mode,防止非授权进程访问。例如:
# ueventd.rc 片段(示意)
/dev/ncmq 0660 system system
/dev/hsm 0660 system system
十、第七步:SELinux 与安全策略——量产绕不过去的坎
BoardConfig.mk 里这两行决定了安全基调:
USE_SELINUX := true
BOARD_SEPOLICY_DIRS := board/semidrive/g9q/sepolicy
-
USE_SELINUX打开 SELinux; -
BOARD_SEPOLICY_DIRS把本产品的 sepolicy 目录并入构建,这样你可以给自家新增的 daemon(交换机守护进程、HSM、升级服务)写专属策略,而不影响公共策略。
配合的还有 security/ 目录里的整套 Android 签名密钥(platform/media/shared/testkey/verity),以及 g9q.mk 里拷贝的 verity_key。量产时,testkey 必须换成公司自签密钥,并妥善保管私钥。 这是很多团队在送测/量产阶段才发现的"天坑"——因为 testkey 是公开的,任何人都能签发一个同名的更新包。
另外提一句,这类车规 BSP 还会把安全延伸到启动链之外:vendor/g9x/hsm-rpmsg 的 HSM 负责密钥存储与加解密、RPMB 负责防回滚安全存储、kernels/boot_g9x 的 sml.bin 是安全监视器。安全不是某个组件,而是一条链:从 ROM → bootloader(AVB) → 内核(verified boot) → SELinux → HSM/RPMB。
十一、第八步:OTA 升级与量产烧录
一套 BSP 不能只"能开机",还必须"能升级、能量产"。这套工程给了完整的参考实现:
-
A/B 无缝升级:
fstab.g9x的slotselect+boot_controlHAL 的setBootMode,支持后台下载、切换槽位、重启生效。 -
升级包生成:
board/brcm/rpi4/sdk/makeUpdatePkg/提供完整流水线——img → zip(含 SoftwareCluster/SoftwarePackage 清单)→ RSA 签名 → Install/Remove.apk。升级包还融入了 UCM(升级管理)模型,动作类型分INSTALL/REMOVE、PRE-ACTIVATE-IREF、VERIFY-IREF。 -
签名校验:升级包用 RSA 私钥签名,设备端用
rsa_public_key.pem验签(etc/update/下)。注意:公钥在系统里,私钥在产线/公司手里。 -
工厂烧录:
build/target/release/mkromimg.sh用dd直接按块写镜像,kernels/boot_g9x/scripts/make_ospi_pac.sh / make_emmc_pac.sh生成工厂包。
启示: 配置自家 BSP 时,OTA 和量产方案要提前设计,而不是产品快上市了才补。特别是密钥体系(签名私钥、A/B 槽位、防回滚计数器)一旦定死,后期改动成本极高。
十二、常见"坑"与优化建议
基于对整套代码的复盘,给一份避坑清单:
-
分区容量是硬约束。
BOARD_SYSTEMIMAGE_PARTITION_SIZE := 128M这种极限值,务必用make后的du实测占用,留 20% 余量,别拍脑袋。 -
工具链版本必须锁定。
TARGET_TOOLS_PREFIX指向 prebuilt 工具链而不是系统 gcc——永远不要用构建机自带的 gcc 编 BSP,否则换台 CI 机器就编不过。 -
多板卡用变量表,不要用分支。学习
TARGET_BOARD_VERSION的ifneq(filter)写法,一个产品定义管住所有硬件变体。 -
HAL 优先,框架少动。能通过
hw_get_module变体规则解决的,绝不要去改 framework。框架层改动会导致后续升级 AOSP 时冲突爆炸。 -
vendor 组件 mk 化。每个供应商 SDK 一个
.mk,include进来,路径相对化,头文件拷 sysroot——这是可维护性的底线。 -
安全密钥提前换。testkey 只能用于开发,量产前必须整体换签,包括 boot、system、vendor、recovery、升级包。
-
SELinux 尽早开。很多团队开发期
USE_SELINUX := false省事,量产前才开,结果所有自研服务都因 sepolicy 缺失起不来,加班救火。正确做法是开发期就 enforcing + 补策略。 -
构建缓存与并行。AOSP 构建耗时以小时计,务必配好
ccache和-j,并在 CI 里固化构建环境(LXC 容器,见build/lxc.sh)。
十三、总结:从"能跑"到"可量产"的检查清单
最后,把整篇文章浓缩成一张"基于 AOSP 配置自家 BSP"的路线图。你可以拿它当 checklist:
1. 搭骨架 → vendorsetup.sh 注册产品,Product.mk 定义产品信息
2. 定硬件 → BoardConfig.mk:CPU/工具链/板卡版本变体表
3. 挂内核 → TARGET_KERNEL_PATH + defconfig + DTB
4. 挂启动 → kernels/ 接入 bootloader/安全固件,理清多核启动链
5. 划分区 → fstab + 分区大小(A/B、verify)
6. 编服务 → init.rc/ueventd.rc/system.prop,按 eng/user 变体分流
7. 写 HAL → 按 hw_get_module 约定实现/包装厂商 HAL
8. 集 vendor → 供应商 .so 全部 mk 化,include 进产品
9. 做安全 → SELinux 策略 + 换正式签名密钥 + HSM/AVB 打通
10. 通升级 → A/B 槽位 + 升级包生成/验签流水线
11. 能量产 → 工厂烧录脚本 + 产线密钥管理
12. 可验证 → 全量 build、开机冒烟、OTA 演练、安全审计
回到开头那句话:BSP 定制的本质,是"配置"而非"移植"。 AOSP 已经把 90% 的通用工程做完了,你要做的,是把剩下的 10%——那些真正属于你产品的东西(板卡变体、交换机 HAL、安全链、OTA 流水线)——以结构清晰、可维护、可量产的方式填进去。希望这篇基于真实工程代码的拆解,能帮你少踩几个坑,把自家的 BSP 从"能跑"做到"可量产"。
圈子不大,欢迎私信交流~
&spm=1001.2101.3001.5002&articleId=163943643&d=1&t=3&u=97f781381ca74fee955034a6c56b2bd5)

被折叠的 条评论
为什么被折叠?



