5、Android Automotive内核与设备树:Linux内核在车载场景下的定制、设备树(Device Tree)语法与实战、内核模块编译与加载
说实话,做车载系统这几年,我最大的感触就是——内核这玩意儿,真不是拿来就能用的。你从高通或者NXP拿到的BSP,里面塞了一大堆用不上的驱动,车载场景下要的是精简、稳定、实时。我见过不少团队,直接把公版内核怼上去,结果启动慢、功耗高,最后被客户怼回来改。
这一章,我就带你走一遍车载内核定制的核心流程。咱们不扯虚的,直接上手。
5.1 车载场景下的内核定制思路
车载Linux内核和手机、服务器上的内核,区别在哪?说白了就三点:启动速度、实时性、外设管控。
- 启动速度:车机冷启动,用户希望5秒内看到Logo。内核必须裁剪,不必要的驱动全干掉。
- 实时性:仪表盘、ADAS这些,延迟超过10ms就可能出问题。所以PREEMPT_RT补丁几乎是标配。
- 外设管控:车载外设多——CAN、LIN、以太网AVB、摄像头、显示屏。每个都得在内核里配好。
我个人的习惯是,拿到一块新板子,先跑一遍make menuconfig,把不用的架构、驱动、文件系统支持全关掉。你想想看,一个车载系统要什么虚拟化?要什么NFS服务器?统统关掉。
5.2 设备树(Device Tree)语法与实战
设备树这玩意儿,刚接触的人会觉得它啰嗦。但做久了你会发现,它其实是硬件描述的最佳实践。尤其是车载平台,同一款SoC可能用在十几个车型上,每个车型的外设引脚都不一样。没有设备树,你改一个GPIO就得重新编译内核,那得多痛苦?
5.2.1 设备树基本结构
一个典型的设备树节点长这样:
/dts-v1/;
/ {
model = "Qualcomm SA8155P Automotive Board";
compatible = "qcom,sa8155p";
chosen {
bootargs = "console=ttyMSM0,115200n8 root=/dev/mmcblk0p2 rw";
};
soc {
#address-cells = <2>;
#size-cells = <2>;
uart0: serial@a84000 {
compatible = "qcom,msm-uartdm-v1.4";
reg = <0x0 0x00a84000 0x0 0x1000>;
interrupts = <0 113 0x4>;
clocks = <&gcc GCC_BLSP1_UART1_APPS_CLK>;
status = "okay";
};
i2c0: i2c@a88000 {
compatible = "qcom,i2c-qup-v2.2.1";
reg = <0x0 0x00a88000 0x0 0x1000>;
interrupts = <0 115 0x4>;
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
touch@38 {
compatible = "goodix,gt911";
reg = <0x38>;
interrupt-parent = <&tlmm>;
interrupts = <65 2>;
reset-gpios = <&tlmm 64 GPIO_ACTIVE_LOW>;
};
};
};
};
这里有几个关键点,我重点说一下:
- compatible:驱动匹配的关键。内核通过这个字符串找到对应的驱动。命名规范是"厂商,设备型号"。
- reg:描述地址和大小。注意#address-cells和#size-cells决定了地址和长度的位数。
- status:我见过有人把不用的设备也配成"okay",结果内核去初始化一个不存在的硬件,直接卡死。不用的设备,要么删掉节点,要么设成"disabled"。
5.2.2 设备树实战:添加一个车载摄像头
假设我们要在SA8155P平台上添加一个OV10640摄像头,通过CSI接口连接。设备树里需要这样配:
&cam_sensor0 {
cell-index = <0>;
compatible = "ovti,ov10640";
reg = <0x60>;
clocks = <&camcc CAM_CC_MCLK0_CLK>;
clock-names = "xvclk";
clock-frequency = <24000000>;
vdda-supply = <&pm8150_l7>;
vddd-supply = <&pm8150_l8>;
reset-gpios = <&tlmm 104 GPIO_ACTIVE_LOW>;
powerdown-gpios = <&tlmm 105 GPIO_ACTIVE_HIGH>;
status = "okay";
port {
sensor0_ep: endpoint {
remote-endpoint = <&csiphy0_ep>;
data-lanes = <1 2 3 4>;
link-frequencies = /bits/ 64 <480000000>;
};
};
};
这里要注意的是data-lanes和link-frequencies。我遇到过有人把data-lanes配成<1 2 3 4 5>,但CSI接口只支持4 lane,结果内核直接报错。还有link-frequencies,这个值必须和传感器实际输出的MIPI时钟匹配,否则图像全是雪花。
5.3 内核模块编译与加载
虽然我前面说能静态编译就静态编译,但有些场景下,模块化是必须的。比如:
- 调试阶段的驱动,频繁修改,不想每次都烧写整个内核
- 第三方驱动,比如某些车载以太网芯片,厂商只提供.ko文件
- 按需加载,比如USB摄像头驱动,不是每次启动都用
5.3.1 编译一个内核模块
假设我们要写一个简单的车载GPIO按键驱动,编译成模块:
// gpio_key.c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
pr_info("Key pressed!\n");
return IRQ_HANDLED;
}
static int gpio_key_probe(struct platform_device *pdev)
{
struct gpio_desc *gpio;
int irq, ret;
gpio = devm_gpiod_get(&pdev->dev, "key", GPIOD_IN);
if (IS_ERR(gpio))
return PTR_ERR(gpio);
irq = gpiod_to_irq(gpio);
ret = devm_request_irq(&pdev->dev, irq, key_irq_handler,
IRQF_TRIGGER_FALLING, "gpio_key", NULL);
return ret;
}
static const struct of_device_id gpio_key_of_match[] = {
{ .compatible = "custom,gpio-key" },
{ }
};
MODULE_DEVICE_TABLE(of, gpio_key_of_match);
static struct platform_driver gpio_key_driver = {
.probe = gpio_key_probe,
.driver = {
.name = "gpio_key",
.of_match_table = gpio_key_of_match,
},
};
module_platform_driver(gpio_key_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Car GPIO Key Driver");
Makefile这样写:
obj-m += gpio_key.o
KDIR := /path/to/your/kernel/source
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
编译出来就是gpio_key.ko。注意KDIR一定要指向你实际运行的内核源码树,版本必须完全一致。我见过有人用Ubuntu自带的headers编译,然后放到嵌入式板子上,insmod直接报Invalid module format。
5.3.2 模块加载与卸载
加载模块:
insmod gpio_key.ko
查看模块信息:
modinfo gpio_key.ko
卸载模块:
rmmod gpio_key
如果模块之间有依赖关系,可以用modprobe,它会自动处理依赖。但车载系统里,我建议你明确加载顺序,不要依赖modprobe的自动解析。为什么?因为自动解析依赖modules.dep文件,这个文件如果没更新,或者依赖链里有循环,系统就挂了。
/vendor/lib/modules/目录下。系统启动时,init进程会根据init.rc中的配置加载模块。如果你要添加自己的模块,记得修改init.rc或者在vendor/etc/init/下添加一个自定义的rc文件。
5.4 车载内核定制实战流程
好了,理论说完了,咱们来走一遍实际流程。假设我们要为某款车型定制一个基于SA8155P的内核:
- 获取BSP:从芯片厂商拿到内核源码和设备树。通常是Linux 5.4或5.10版本。
- 配置内核:
make sa8155p_defconfig,然后make menuconfig裁剪。 - 修改设备树:根据车型的硬件原理图,修改dts文件。比如调整GPIO、I2C地址、时钟频率。
- 编译内核和设备树:
make -j8 Image dtbs。 - 编译模块:
make modules,然后把.ko文件拷贝到目标系统。 - 烧写验证:把Image和dtb文件烧写到板子,启动后检查
dmesg,确认所有设备都正确初始化。
这里我画了一张流程图,帮你理清整个定制流程:
这个流程看起来简单,但实际做起来,每一步都可能踩坑。我举个例子,有一次我在裁剪内核时,把CONFIG_DRM关掉了,结果显示屏死活不亮。查了半天才发现,显示驱动依赖DRM框架。所以,裁剪之前,先搞清楚依赖关系。
5.5 车载内核调试技巧
调试内核,说白了就是三板斧:dmesg、/sys、/proc。但车载场景下,有些技巧你可能不知道:
- 动态调试:如果驱动里用了
pr_debug,可以在运行时通过echo 'file gpio_key.c +p' > /sys/kernel/debug/dynamic_debug/control打开调试信息,不用重新编译。 - 设备树覆盖:调试阶段,可以用
configfs动态加载设备树覆盖文件,不用每次都烧写整个dtb。这个功能在Android Automotive里默认是开启的。 - 内核崩溃转储:如果系统挂了,
kdump可以帮你保存现场。车载系统里,这个功能一定要打开,否则出了问题你连日志都拿不到。
CONFIG_DEBUG_GPIO和CONFIG_DEBUG_PINCTRL打开。这两个选项能帮你看到GPIO的当前状态和复用配置。有一次客户说某个外设不工作,我一看/sys/kernel/debug/gpio,发现某个引脚被配置成了输出,但硬件上它应该是输入。这种问题,没有调试信息,你根本想不到。
好了,这一章的内容就到这里。内核定制和设备树,是车载驱动开发的基本功。你把这些搞透了,后面做音频、显示、传感器驱动,都会轻松很多。
公众号:蓝海资料掘金营,微信deep3321