5、Linux内核移植(下):GPIO与PinCtrl子系统、I2C/SPI总线驱动移植、Display驱动(MIPI DSI/HDMI)、内核启动优化
好,咱们接着往下聊。上一节我们把内核启动流程和设备树讲透了,这一节要啃的,都是硬骨头——GPIO与PinCtrl、I2C/SPI总线、显示驱动,还有启动优化。这几个模块,说白了就是嵌入式Linux的“四肢”和“脸面”。四肢不灵,板子动不了;脸面不亮,用户看不见。
我个人习惯,做移植时先搞定GPIO和PinCtrl,因为这是所有外设的基础。你想想看,一个引脚复用不对,后面的I2C、SPI、UART全得翻车。我在项目中遇到过好几次,板子启动后I2C设备死活枚举不到,查了两天才发现是PinCtrl配置里把SCL和SDA的电气特性设错了。
5.1 GPIO与PinCtrl子系统:引脚管理的“交通警察”
GPIO子系统管的是“拉高拉低、读电平”,PinCtrl管的是“这个脚现在当GPIO用,还是当I2C的SCL用”。两者配合,才能让SoC的引脚按你的意愿工作。
核心要点:PinCtrl负责引脚复用(muxing)和电气特性配置(驱动强度、上拉/下拉),GPIO负责数字信号的输入输出。两者在设备树中通过pinctrl-0、pinctrl-names等属性绑定。
以i.MX8M为例,设备树中配置一个UART引脚的典型写法:
&uart1 {
pinctrl-names = "default";
pinctrl-0 = &pinctrl_uart1;
status = "okay";
};
&iomuxc {
pinctrl_uart1: uart1grp {
fsl,pins = <
MX8M_IOMUXC_UART1_RXD_UART1_DCE_RX 0x140
MX8M_IOMUXC_UART1_TXD_UART1_DCE_TX 0x140
>;
};
};
看到那个 0x140 了吗?这就是电气特性配置。我刚开始做移植时,经常忽略这个值,结果串口能发不能收。后来养成习惯,每次配完PinCtrl,先拿示波器看波形——高电平是不是3.3V,上升沿够不够陡。
避坑指南:我曾经在某个项目里,把GPIO的驱动强度设得太低,导致外接的LED亮度不够。后来查手册才发现,i.MX的GPIO驱动强度分好几档,默认值往往偏保守。建议根据外设的电流需求,适当调高驱动强度。
5.2 I2C/SPI总线驱动移植:外设的“高速公路”
I2C和SPI是嵌入式系统里最常用的两种总线。I2C适合接传感器、EEPROM这类低速设备,SPI适合接显示、ADC这类高速设备。移植时,重点在时钟频率、时序参数和DMA支持。
先看I2C。设备树里配I2C,核心是配好时钟频率和引脚:
&i2c1 {
clock-frequency = <100000>; /* 100kHz标准模式 */
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
/* 挂载在I2C总线上的设备 */
touch@38 {
compatible = "edt,edt-ft5406";
reg = <0x38>;
interrupt-parent = <&gpio1>;
interrupts = <9 IRQ_TYPE_EDGE_FALLING>;
};
};
这里有个细节:clock-frequency 不是设得越高越好。我遇到过一块板子,把I2C频率设到400kHz,结果接的触摸屏偶尔丢数据。降到200kHz后,稳如老狗。嗯,有时候“慢”就是“稳”。
SPI的移植类似,但多了个 spi-max-frequency 和 cs-gpios:
&ecspi1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ecspi1>;
cs-gpios = <&gpio5 9 GPIO_ACTIVE_LOW>;
status = "okay";
spidev@0 {
compatible = "spidev";
reg = <0>;
spi-max-frequency = <20000000>; /* 20MHz */
};
};
注意:SPI的片选(CS)信号,很多SoC支持硬件自动控制,也支持GPIO模拟。我个人建议用GPIO模拟,因为硬件CS有时序问题,尤其是多设备共享SPI总线时。我曾经被硬件CS的“毛刺”坑过,换了GPIO模拟后问题消失。
5.3 Display驱动移植:MIPI DSI与HDMI
显示驱动是移植中最“显眼”的部分——搞好了,用户能看到你的成果;搞砸了,黑屏一片。i.MX8M系列支持MIPI DSI(接LCD屏)和HDMI(接显示器),两者驱动框架不同,但核心逻辑一致:配好时钟、配好时序、配好背光。
先看MIPI DSI。设备树里需要配置panel的时序参数:
&mipi_dsi {
status = "okay";
panel@0 {
compatible = "boe,tv080wum-nl0";
reg = <0>;
reset-gpios = <&gpio1 8 GPIO_ACTIVE_LOW>;
backlight = <&backlight_lcd>;
port {
panel_in: endpoint {
remote-endpoint = <&mipi_dsi_out>;
};
};
};
};
/* 时序参数在panel驱动里定义 */
static const struct drm_display_mode boe_tv080wum_mode = {
.clock = 160000, /* 像素时钟 160MHz */
.hdisplay = 1200,
.hsync_start = 1200 + 40,
.hsync_end = 1200 + 40 + 20,
.htotal = 1200 + 40 + 20 + 40,
.vdisplay = 1920,
.vsync_start = 1920 + 10,
.vsync_end = 1920 + 10 + 5,
.vtotal = 1920 + 10 + 5 + 10,
};
时序参数怎么来的?看屏厂给的datasheet。我见过很多工程师直接抄参考设计,结果屏闪、花屏。我的习惯是:拿到屏后,先看datasheet里的“Timing Characteristics”表格,把HBP、HFP、VBP、VFP一个个对清楚。
HDMI的移植相对简单,因为HDMI是标准接口,驱动层已经封装好了。主要工作是配好PHY的时钟和电源:
&hdmi {
status = "okay";
ddc-i2c-bus = <&i2c2>; /* DDC通道用于读取EDID */
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hdmi>;
};
经验之谈:HDMI移植最常见的坑是“无显示”。90%的原因是DDC(I2C通道)没配好,导致系统读不到显示器的EDID信息。先确认I2C总线能正常通信,再用 cat /sys/class/drm/card0-HDMI-A-1/edid | edid-decode 验证。
5.4 内核启动优化:从5秒到1秒的“瘦身”
启动优化,说白了就是让内核从“慢吞吞”变成“嗖的一下”。我做过一个项目,客户要求冷启动时间小于2秒。嗯,原厂的内核启动要5秒多,怎么办?一个字:砍。
优化的核心思路:减少不必要的初始化,并行化必要的初始化。具体手段包括:
- 裁剪内核配置:关掉不需要的驱动、文件系统、网络协议栈。用
make menuconfig一个个过,能关就关。 - 减少打印信息:把
loglevel从默认的7降到3,甚至1。启动时少打印一行,能省几十毫秒。 - 使用initramfs:把根文件系统做成cpio归档,直接打包进内核。省去挂载块设备的时间。
- 异步探测:让多个驱动同时初始化,而不是串行等待。在设备树里加
probe-type = "async"。
我常用的优化手段是“分阶段测量”:
- 在
arch/arm64/kernel/setup.c里加printk时间戳,看每个阶段耗时。 - 找出最耗时的模块——通常是USB、网络、显示驱动。
- 针对性地优化:USB可以延迟初始化,网络可以只配IP不配路由,显示可以提前背光点亮。
一个真实案例:我曾经优化一块i.MX8M板子,启动时间从4.2秒降到1.8秒。主要做了三件事:把USB控制器驱动改成模块(省了0.8秒)、关掉内核的帧缓冲控制台(省了0.6秒)、用initramfs替代ext4根文件系统(省了1.0秒)。
最后,放一张我总结的“内核移植知识体系图”,帮你理清这几个模块的关系:
好了,这一节的内容就到这里。GPIO和PinCtrl是地基,I2C/SPI是管道,Display是门面,启动优化是装修。每一步都踩实了,你的板子才能跑得稳、跑得快。
公众号:蓝海资料掘金营,微信deep3321