21、自定义产品开发:创建新设备、配置产品、添加驱动模块

好,咱们今天聊点实在的。前面讲了那么多理论,什么Makefile语法、Soong构建规则、BoardConfig配置……说白了,都是为了这一刻——真正动手创建一个属于你自己的设备

我记得刚入行那会儿,公司要做一个基于MTK平台的平板。领导丢给我一份参考代码,说“照着改就行”。结果我折腾了两天,系统死活起不来。后来才发现,是Product配置里少了一个关键模块。嗯,从那以后,我对自定义产品开发这件事,就多了一份敬畏。

今天这一章,我会带你完整走一遍流程:创建设备目录、编写产品配置、添加硬件驱动模块。你跟着做一遍,基本就能掌握Android Build系统里“自定义产品”的核心套路。

核心要点:Android Build系统通过 device/ 目录下的 BoardConfig.mkdevice.mkAndroidProducts.mk 三个文件,来定义一台“设备”的完整构建配置。驱动模块则通过 PRODUCT_PACKAGESLOCAL_MODULE 机制加入。

21.1 创建设备目录:从零开始搭架子

Android源码里,所有设备定义都放在 device/ 目录下。通常的命名规则是:

device/<厂商名>/<设备名>/

比如,Google的Pixel设备在 device/google/ 下,高通的参考板在 device/qcom/ 下。我个人习惯,把公司名缩写作为厂商名,产品代号作为设备名。

假设我们要做一个叫“Neptune”的智能音箱,厂商叫“OceanTech”。那目录结构就是:

device/oceantech/neptune/
├── BoardConfig.mk        # 板级配置(芯片、内存、分区等)
├── device.mk             # 设备产品配置(预装应用、权限等)
├── AndroidProducts.mk    # 产品列表(告诉系统有哪些产品变体)
├── system.prop           # 系统属性覆盖
├── overlay/              # 资源覆盖目录
└── kernel/               # 内核相关(可选)

你可能会问:“这三个mk文件到底各管什么?”我简单解释一下:

  • BoardConfig.mk:定义硬件相关的参数。比如CPU架构是arm64还是x86,分区大小是多少,是否支持AB分区。说白了,这是给“板子”看的。
  • device.mk:定义产品相关的配置。比如要预装哪些APK,要加入哪些系统服务,默认语言是什么。这是给“用户”看的。
  • AndroidProducts.mk:把上面两个文件“绑定”在一起,并声明产品名称。这是给“构建系统”看的。

小技巧:如果你只是做原型验证,可以直接复制一份Google的参考设备(比如 device/generic/arm64)来改。但正式项目,我建议从头写,这样你能清楚每一行配置的含义。

21.2 配置产品:编写BoardConfig.mk和device.mk

好,架子搭好了,咱们开始填内容。先看 BoardConfig.mk

# device/oceantech/neptune/BoardConfig.mk

# 指定芯片架构
TARGET_ARCH := arm64
TARGET_ARCH_VARIANT := armv8-a
TARGET_CPU_VARIANT := generic
TARGET_CPU_ABI := arm64-v8a

# 分区大小(单位:MB)
BOARD_BOOTIMAGE_PARTITION_SIZE := 67108864
BOARD_SYSTEMIMAGE_PARTITION_SIZE := 2147483648
BOARD_USERDATAIMAGE_PARTITION_SIZE := 5368709120

# 启用AB分区
TARGET_NO_RECOVERY := true
BOARD_USES_RECOVERY_AS_BOOT := true

# 内核相关
TARGET_PREBUILT_KERNEL := device/oceantech/neptune/kernel
BOARD_KERNEL_CMDLINE := console=ttyS0,115200n8
BOARD_KERNEL_BASE := 0x80000000

这里要注意几个坑。我曾经在项目里把 BOARD_SYSTEMIMAGE_PARTITION_SIZE 设小了,结果编译出来的system.img塞不进去,报了个莫名其妙的“out of space”错误。排查了半天才发现是分区大小没对齐。所以,分区大小一定要根据实际镜像大小来设,别拍脑袋写个数字。

再看 device.mk

# device/oceantech/neptune/device.mk

# 继承基础产品配置
$(call inherit-product, $(SRC_TARGET_DIR)/product/core_64_bit.mk)
$(call inherit-product, $(SRC_TARGET_DIR)/product/full_base.mk)

# 产品名称和厂商
PRODUCT_NAME := neptune
PRODUCT_DEVICE := neptune
PRODUCT_BRAND := OceanTech
PRODUCT_MODEL := Neptune Smart Speaker

# 预装应用
PRODUCT_PACKAGES += \
    Launcher3 \
    Settings \
    Bluetooth \
    WifiService

# 添加驱动模块(后面会细说)
PRODUCT_PACKAGES += \
    audio.primary.neptune \
    lights.neptune \
    sensors.neptune

# 系统属性
PRODUCT_PROPERTY_OVERRIDES += \
    ro.sf.lcd_density=160 \
    ro.product.manufacturer=OceanTech

你发现没有?PRODUCT_PACKAGES 这个变量贯穿始终。无论是预装APK,还是添加驱动模块,都靠它。这也是Android Build系统最核心的机制之一——通过变量声明,让构建系统自动去查找并编译对应的模块

21.3 添加驱动模块:从HAL到内核

驱动模块,说白了就是让Android系统能“认识”你的硬件。比如音频芯片、传感器、显示屏等。在Android里,驱动通常分为两层:

  • 内核驱动:在Linux内核里,通过设备树或模块加载。
  • HAL模块:在用户空间,通过 hardware/libhardware 框架与上层交互。

咱们重点讲HAL模块的添加。假设我们要加一个音频驱动,名字叫 audio.primary.neptune。首先,在源码里创建模块目录:

hardware/oceantech/audio/
├── Android.mk
├── audio_hw.c
├── audio_hw.h
└── audio_policy.conf

然后写 Android.mk

# hardware/oceantech/audio/Android.mk

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := audio.primary.neptune
LOCAL_MODULE_RELATIVE_PATH := hw
LOCAL_SRC_FILES := audio_hw.c
LOCAL_SHARED_LIBRARIES := liblog libcutils libtinyalsa
LOCAL_MODULE_TAGS := optional

include $(BUILD_SHARED_LIBRARY)

注意 LOCAL_MODULE_RELATIVE_PATH := hw 这一行。它告诉构建系统,编译出来的 .so 文件要放到 /vendor/lib/hw/ 目录下。Android的HAL框架会从这个路径加载驱动。

最后,回到 device.mk,把模块名加到 PRODUCT_PACKAGES 里。这样,编译时系统就会自动去 hardware/oceantech/audio/ 目录下找这个模块并编译。

注意:驱动模块的命名有讲究。比如音频驱动必须是 audio.primary.<设备名> 的格式,传感器驱动必须是 sensors.<设备名>。这是Android HAL框架的约定,不能乱改。我曾经因为把模块名写成了 audio.neptune,少了 primary,结果系统死活加载不上,查了一下午才找到原因。

21.4 构建与验证:跑起来看看

配置写好了,模块也加了,接下来就是编译和验证。执行:

source build/envsetup.sh
lunch oceantech_neptune-userdebug
make -j16

如果一切顺利,你会看到编译系统输出类似这样的信息:

Target system fs image: out/target/product/neptune/system.img
Target vendor fs image: out/target/product/neptune/vendor.img
Installed: out/target/product/neptune/audio.primary.neptune.so

看到 audio.primary.neptune.so 被正确安装,说明驱动模块已经成功加入。你可以用 fastboot flash 把镜像烧录到设备上,然后通过 adb shell 检查:

adb shell ls /vendor/lib/hw/
# 应该能看到 audio.primary.neptune.so

如果看不到,别慌。先检查 PRODUCT_PACKAGES 里有没有拼写错误,再确认 Android.mkLOCAL_MODULE 名字是否一致。我遇到过最奇葩的一次,是文件名大小写写错了——Linux是区分大小写的,但编译系统没报错,只是默默跳过了这个模块。

21.5 知识体系总览

为了让你更直观地理解整个流程,我画了一张图:

自定义产品开发流程 1. 创建设备目录 device/oceantech/neptune/ 2. 编写配置文件 BoardConfig.mk device.mk AndroidProducts.mk 3. 添加驱动模块 HAL模块 内核驱动 4. 构建与验证 lunch & make fastboot flash ✅ 自定义产品镜像

这张图把整个流程串起来了。从创建设备目录开始,到编写三个核心配置文件,再到添加驱动模块,最后构建验证。每一步都有对应的文件和操作,你照着这个流程走,基本不会迷路。

21.6 避坑指南

最后,分享几个我踩过的坑,希望能帮你省点时间:

  • 分区大小不匹配BoardConfig.mk 里的分区大小,必须和实际烧录的分区表一致。否则刷机时会报错,甚至变砖。
  • 模块名拼写错误PRODUCT_PACKAGES 里的模块名,必须和 Android.mk 里的 LOCAL_MODULE 完全一致,包括大小写。
  • 缺少依赖库:驱动模块如果依赖了其他共享库,记得在 LOCAL_SHARED_LIBRARIES 里声明。否则运行时会出现 dlopen failed 错误。
  • 权限问题:HAL模块通常需要 systemroot 权限。如果发现驱动加载了但无法访问硬件,检查一下 device.mk 里的权限配置。

嗯,这一章的内容就到这里。自定义产品开发,说白了就是“搭架子、填配置、加模块”三步走。你只要理解了这三个mk文件的作用,以及 PRODUCT_PACKAGES 的用法,剩下的就是熟练工种了。

一句话总结:Android Build系统通过 device/ 目录下的三个mk文件定义设备,通过 PRODUCT_PACKAGES 添加驱动模块。掌握这个套路,你就能创建任意自定义产品。


公众号:蓝海资料掘金营,微信deep3321