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-frequencycs-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"

我常用的优化手段是“分阶段测量”:

  1. arch/arm64/kernel/setup.c 里加 printk 时间戳,看每个阶段耗时。
  2. 找出最耗时的模块——通常是USB、网络、显示驱动。
  3. 针对性地优化:USB可以延迟初始化,网络可以只配IP不配路由,显示可以提前背光点亮。

一个真实案例:我曾经优化一块i.MX8M板子,启动时间从4.2秒降到1.8秒。主要做了三件事:把USB控制器驱动改成模块(省了0.8秒)、关掉内核的帧缓冲控制台(省了0.6秒)、用initramfs替代ext4根文件系统(省了1.0秒)。

最后,放一张我总结的“内核移植知识体系图”,帮你理清这几个模块的关系:

Linux内核 GPIO & PinCtrl 引脚复用 / 电气特性 I2C / SPI 总线 时钟频率 / 时序 / DMA Display 驱动 MIPI DSI / HDMI 启动优化 裁剪 / 并行 / initramfs 依赖 依赖 图:Linux内核移植核心模块关系图 GPIO/PinCtrl是基础,I2C/SPI是桥梁,Display是输出,启动优化是目标 💡 移植顺序建议:先GPIO/PinCtrl → 再I2C/SPI → 然后Display → 最后做启动优化 每一步都验证通过后,再进入下一步,避免问题叠加

好了,这一节的内容就到这里。GPIO和PinCtrl是地基,I2C/SPI是管道,Display是门面,启动优化是装修。每一步都踩实了,你的板子才能跑得稳、跑得快。


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