Linux内核是Linux操作系统的核心,它负责管理系统的硬件资源,如CPU、内存、磁盘等,同时为用户空间的应用程序提供接口;Linux内核的移植的过程,就是将Linux适配设备硬件,提供接口支持后续应用的功能。如果只是简单的运行,Linux Kernel的移植十分简单,只需要修改设备树文件和配置文件,就可以完成移植;不过构建完整适配硬件的Linux Kernel,则远比U-Boot复杂的多。对于U-Boot来说,只要实现必要的串口,网络,指示灯等功能,支持打印调试信息以及跳转内核,从应用场景来说基本就合格了。对于内核,则需要适配硬件上的各种外设功能,并确保都工作正常,才能保证系统的稳定和安全,这就需要进行全面的移植和适配。
之前讲述构建运行平台时,说到内核系统代码的三个来源。
Linux内核主干代码是整个系统代码的基础,由Linus领导的Linux开源社区维护;
芯片原厂基于特定分支的Linux内核,进行芯片硬件的适配,这里以ARM厂商居多,适配芯片的内核,时钟系统,中断系统,GPIO,串口,I2C,SPI等内部模块,以源码的方式提供给开发者使用。当然作为Linux开源社区的一员,同时也会定期将这些代码同步到内核主干中,作为主干内核代码的一部分,其中board和drivers目录下的很多驱动都是有厂商实现的。厉害的芯片厂商公司,往往也是Linux开源社区提供代码的主力部分。
方案商提供的Linux内核,一般是基于芯片原厂提供的内核,进行定制化的适配,包括设备树的修改,驱动的修改,配置文件的修改,以及部分bug的修复。最终会和U-Boot,Rootfs一起打包,以SDK的方式提供给开发者使用。
因为方案商提供的SDK基本上基于简单命令就可以编译,因此本节主要从主干代码和芯片原厂两部分说明移植方法,具体目录如下所示。
注意: 因为系统版本,安装环境和编译器版本不同,编译所面临问题也会有所差异,这里给出我验证本篇文章使用的编译环境信息。
这里提供Linux主干访问的地址代码
对于最新release的分支,一般会有一个tag,用于标识当前版本的代码,截至目前为v6.14-rc5,可以通过如下方式下载。
# 下载最新的Linux分支代码,国内
wget https://github.com/torvalds/linux/archive/refs/tags/v6.14-rc5.tar.gz
对于主干代码,我们以实现最基础的Linux Kernel打印输出功能为例进行测试。因为需要适配的芯片I.MX6ULL,选择适配NXP IMX6ull的版本,对应的配置文件和设备树如下所示。
因为只要实现最基础的打印内核启动信息,并执行文件系统的功能,我们并不需要大量修改代码,只需要适配我们的调试串口即可。很幸运,主干代码中使用的串口和我们使用的一致,都是uart1接口,参考设备树文件中的stdout配置。
当然如果uart不一致,可以参考这篇内容: 如何修改调试接口。
如果希望内核支持从emmc,nand,sd或者网络启动,则需要将相应节点修改或者添加到imx6ull-14x14-evk.dtsi中,这里以网络nfs启动为例,参考如下配置。
//修改imx6ull-14x14-evk.dtsi中的节点,支持网络通讯
&iomuxc_snvs {
pinctrl_fec1_reset: fec1_resetgrp {
fsl,pins = <
MX6ULL_PAD_SNVS_TAMPER7__GPIO5_IO07 0x79
>;
};
pinctrl_fec2_reset: fec2_resetgrp {
fsl,pins = <
MX6ULL_PAD_SNVS_TAMPER8__GPIO5_IO08 0x79
>;
};
};
&fec1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_enet1
&pinctrl_fec1_reset>;
phy-mode = "rmii";
phy-handle = <ðphy0>;
phy-reset-gpios = <&gpio5 7 GPIO_ACTIVE_LOW>;
phy-reset-duration = <26>;
// phy-supply = <®_peri_3v3>;
status = "okay";
};
&fec2 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_enet2
&pinctrl_fec2_reset>;
phy-mode = "rmii";
phy-handle = <ðphy1>;
// phy-supply = <®_peri_3v3>;
phy-reset-gpios = <&gpio5 8 GPIO_ACTIVE_LOW>;
phy-reset-duration = <26>;
status = "okay";
mdio {
#address-cells = <1>;
#size-cells = <0>;
ethphy0: ethernet-phy@0 {
compatible = "ethernet-phy-id0022.1560";
reg = <0>;
micrel,led-mode = <1>;
clocks = <&clks IMX6UL_CLK_ENET_REF>;
clock-names = "rmii-ref";
};
ethphy1: ethernet-phy@1 {
compatible = "ethernet-phy-id0022.1560";
reg = <1>;
micrel,led-mode = <1>;
clocks = <&clks IMX6UL_CLK_ENET2_REF>;
clock-names = "rmii-ref";
};
};
};
修改完成后,即可以配置内核和编译系统,使用如下命令。
# 解压代码
tar -zxvf v6.14-rc5.tar.gz
# 进入代码目录
cd linux-6.14-rc5/
# 配置内核
make imx_v6_v7_defconfig ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf-
# 编译内核
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- -j8
成功配置的显示如下所示。
成功编译完成的显示如下所示。
这时我们需要的内核镜像和设备树文件如下所示。
这是用nfs启动打印的信息。
上述内核代码在我们开发板是可以直接使用的,不过启动过程中会报大量的设备驱动错误。这是因为我们的开发板上并没有这些硬件设备,需要进行适配。如果进行全部的硬件适配,直接从主干代码开始,工作量是巨大的,所以我们需要一种更加简单的方式,就是使用芯片原厂提供的Linux内核进行适配。
使用芯片原厂的Linux芯片,厂商自然也是移植了芯片硬件的适配,已nxp为例,开源代码地址如下所示。
对于最新release的分支,一般会有一个tag,用于标识当前版本的代码,截至目前为止已经更新到6.6.y;不过我在测试的时候,使用的是6.1.y版本,因此以这个版本的移植为例。
# 下载Linux分支代码
wget https://github.com/nxp-imx/linux-imx/archive/refs/heads/lf-6.1.y.zip
# 解压代码
unzip lf-6.1.y.zip
# 进入代码目录
# 进入代码目录
cd cd linux-imx-lf-6.1.y/
重复上述的过程,继续配置内核和编译系统。
# 配置内核
make imx_v6_v7_defconfig ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf-
# 编译内核
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- -j8
这个版本的内核仍然可以使用,不过仍然会有大量报错,我们后续的操作就是解决这些报错,适配开发板。为什么使用芯片厂商的内核,而不是使用Linux主干呢?这是因为Linux主干维护主要应对通用的功能,版本的升级,部分芯片平台在新功能下可能因为兼容性问题缺失,个人开发者是很难解决这些问题的,如果是demo,自然无所谓,对于产品来说是不可接受的。厂商提供的内核,基本都是经过适配的,会有更多相关产品使用者进行测试,如果有问题,也会及时修复。下面讲解Linux内核的移植过程,可能比你想象的更加简单,主要流程如下。
设备树是Linux内核的核心组件之一,它描述了系统的硬件配置和资源分配。设备树文件通常以.dtb或.dts结尾,用于描述系统的硬件配置和资源分配。设备树文件是由设备树编译器(DTC)生成的,它将设备树源代码转换为二进制格式。
详细设备树的说明参考:设备树说明。
I.MX6ULL的设备树解析参考:I.MX6ULL设备树解析。
对于设备树的移植,我们需要修改设备树文件,适配开发板的硬件。
# 根据开发板的资源,建立板级的基础设备树
cp arch/arm/boot/dts/imx6ull-14x14-evk.dtsi arch/arm/boot/dts/imx6ull-14x14-rmk.dtsi
# 实现相应的设备树文件
# emmc对应的设备树
touch arch/arm/boot/dts/imx6ull-14x14-emmc-4.3-800x480-c
# nand对应的设备树
touch arch/arm/boot/dts/imx6ull-14x14-nand-4.3-800x480-c
设备树imx6ull-14x14-rmk.dtsi的修改,主要注释掉板级不存在的硬件,参考如下。
- regulator节点电压控制在板级不存在,移除
// reg_sd1_vmmc: regulator-sd1-vmmc {
// compatible = "regulator-fixed";
// regulator-name = "VSD_3V3";
// regulator-min-microvolt = <3300000>;
// regulator-max-microvolt = <3300000>;
// //gpio = <&gpio1 9 GPIO_ACTIVE_HIGH>;
// off-on-delay-us = <20000>;
// enable-active-high;
// };
// reg_peri_3v3: regulator-peri-3v3 {
// compatible = "regulator-fixed";
// pinctrl-names = "default";
// pinctrl-0 = <&pinctrl_peri_3v3>;
// regulator-name = "VPERI_3V3";
// regulator-min-microvolt = <3300000>;
// regulator-max-microvolt = <3300000>;
// gpio = <&gpio5 2 GPIO_ACTIVE_LOW>;
// /*
// * If you want to want to make this dynamic please
// * check schematics and test all affected peripherals:
// *
// * - sensors
// * - ethernet phy
// * - can
// * - bluetooth
// * - wm8960 audio codec
// * - ov5640 camera
// */
// regulator-always-on;
// };
// reg_can_3v3: regulator-can-3v3 {
// compatible = "regulator-fixed";
// regulator-name = "can-3v3";
// regulator-min-microvolt = <3300000>;
// regulator-max-microvolt = <3300000>;
// // gpios = <&gpio_spi 3 GPIO_ACTIVE_LOW>;
// };
- spi4节点挂载74hc595芯片,板级不存在,注释
// spi-4 {
// compatible = "spi-gpio";
// pinctrl-names = "default";
// //pinctrl-0 = <&pinctrl_spi4>;
// status = "disabled";
// gpio-sck = <&gpio5 11 0>;
// gpio-mosi = <&gpio5 10 0>;
// cs-gpios = <&gpio5 7 GPIO_ACTIVE_LOW>;
// num-chipselects = <1>;
// #address-cells = <1>;
// #size-cells = <0>;
// gpio_spi: gpio@0 {
// compatible = "fairchild,74hc595";
// gpio-controller;
// #gpio-cells = <2>;
// reg = <0>;
// registers-number = <1>;
// registers-default = /bits/ 8 <0x57>;
// spi-max-frequency = <100000>;
// enable-gpios = <&gpio5 8 GPIO_ACTIVE_LOW>;
// };
// };
- can2节点不存在,注释
// &can2 {
// pinctrl-names = "default";
// xceiver-supply = <®_can_3v3>;
// status = "okay";
// };
// &gpio_spi {
// eth0-phy-hog {
// gpio-hog;
// gpios = <1 GPIO_ACTIVE_HIGH>;
// output-high;
// line-name = "eth0-phy";
// };
// eth1-phy-hog {
// gpio-hog;
// gpios = <2 GPIO_ACTIVE_HIGH>;
// output-high;
// line-name = "eth1-phy";
// };
// };
- i2c挂载mag3310和fxls8471,不存在,注释
// &i2c1 {
// clock-frequency = <100000>;
// pinctrl-names = "default";
// pinctrl-0 = <&pinctrl_i2c1>;
// status = "okay";
// magnetometer@e {
// compatible = "fsl,mag3110";
// reg = <0x0e>;
// vdd-supply = <®_peri_3v3>;
// vddio-supply = <®_peri_3v3>;
// position = <2>;
// };
// fxls8471@1e {
// compatible = "fsl,fxls8471";
// reg = <0x1e>;
// position = <0>;
// interrupt-parent = <&gpio5>;
// interrupts = <0 8>;
// };
// };
- qspi外挂flash,硬件不存在,注释
// &qspi {
// pinctrl-names = "default";
// pinctrl-0 = <&pinctrl_qspi>;
// status = "okay";
// flash0: flash@0 {
// #address-cells = <1>;
// #size-cells = <1>;
// compatible = "micron,n25q256a", "jedec,spi-nor";
// spi-max-frequency = <29000000>;
// spi-rx-bus-width = <4>;
// spi-tx-bus-width = <1>;
// reg = <0>;
// };
// };
修改后的文件详细见设备树修改后的文件目录,将其中的dts,dtsi和Makefile文件复制到内核的arch/arm/boot/dts目录下,config文件添加到arch/arm/configs目录中。在emmc和nand对应的设备树文件,则增加相应的硬件信息,这部分在ch03.驱动模块开发中进行详细的添加说明。这里举个例子,例如硬件上i2c1下挂载ap3216c,修改适配网卡,添加和修改设备树节点如下。
// 适配i2c和器件设备树
&i2c1 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
ap3216@1e {
compatible = "rmk,ap3216";
reg = <0x1e>;
rmk,sysconf = /bits/ 8 <0x03>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ap3216_tsc>;
interrupt-parent = <&gpio1>;
interrupts = <1 IRQ_TYPE_EDGE_FALLING>;
int-gpios = <&gpio1 1 GPIO_ACTIVE_LOW>;
};
};
// 适配网口设备树
&iomuxc_snvs {
pinctrl_fec1_reset: fec1_resetgrp {
fsl,pins = <
MX6ULL_PAD_SNVS_TAMPER7__GPIO5_IO07 0x79
>;
};
pinctrl_fec2_reset: fec2_resetgrp {
fsl,pins = <
MX6ULL_PAD_SNVS_TAMPER8__GPIO5_IO08 0x79
>;
};
};
&fec1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_enet1
&pinctrl_fec1_reset>;
phy-mode = "rmii";
phy-handle = <ðphy0>;
phy-reset-gpios = <&gpio5 7 GPIO_ACTIVE_LOW>;
phy-reset-duration = <26>;
// phy-supply = <®_peri_3v3>;
status = "okay";
};
&fec2 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_enet2
&pinctrl_fec2_reset>;
phy-mode = "rmii";
phy-handle = <ðphy1>;
// phy-supply = <®_peri_3v3>;
phy-reset-gpios = <&gpio5 8 GPIO_ACTIVE_LOW>;
phy-reset-duration = <26>;
status = "okay";
mdio {
#address-cells = <1>;
#size-cells = <0>;
ethphy0: ethernet-phy@0 {
compatible = "ethernet-phy-id0022.1560";
reg = <0>;
micrel,led-mode = <1>;
clocks = <&clks IMX6UL_CLK_ENET_REF>;
clock-names = "rmii-ref";
};
ethphy1: ethernet-phy@1 {
compatible = "ethernet-phy-id0022.1560";
reg = <1>;
micrel,led-mode = <1>;
clocks = <&clks IMX6UL_CLK_ENET2_REF>;
clock-names = "rmii-ref";
};
};
};
其它模块类似。如果对设备树,芯片,器件和驱动都有深入了解,那么这部分就十分简单。在设备树中按照规则注释和添加节点即可。
详细的设备树文件如下所示。
在arch/arm/boot/dts/Makefile添加设备树的编译支持。
// arch/arm/boot/dts/Makefile
dtb-$(CONFIG_SOC_IMX6UL) += \
imx6ull-14x14-emmc-4.3-800x480-c.dtb \
imx6ull-14x14-nand-4.3-800x480-c.dtb \
imx6ul-14x14-evk.dtb \
....
配置文件主要根据官方的配置,拷贝个人的配置,在基础添加修改,具体流程如下。
# 拷贝文件
cp arch/arm/configs/imx_rmk_v7_defconfig imx_v6_v7_defconfig
# 添加配置选项
# ===================================================
# 支持usb串口驱动cp210x
CONFIG_USB_SERIAL_GENERIC=y
CONFIG_USB_SERIAL_FTDI_SIO=y
CONFIG_USB_SERIAL_OPTION=y
CONFIG_USB_SERIAL_CP210X=y
CONFIG_USB_SERIAL_CH341=y
CONFIG_IIO_BUFFER_CB=y
# 增加RTL 8192CU驱动
CONFIG_RTL_CARDS=y
CONFIG_RTL8192CU=y
CONFIG_RTLWIFI=y
CONFIG_RTLWIFI_USB=y
CONFIG_RTLWIFI_DEBUG=y
CONFIG_RTL8192C_COMMON=y
CONFIG_RTL8188EU=y
# ===================================================
详细的配置文件如下所示。
然后根据新添加的设备树和配置文件编译即可。
# 配置内核
make imx_rmk_v7_defconfig ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf-
# 编译内核
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- -j8
使用的zImage、imx6ull-14x14-emmc-4.3-800x480-c.dtb和imx6ull-14x14-nand-4.3-800x480-c.dtb即为可以使用的内核移植代码,具体执行流程如下。
至此,关于Linux内核的平台移植核适配完毕。可以看到随着统一设备模型的支持,通过设备树的修改基本可以完成大部分的移植工作。另外通过Kconfig配置选项的支持,也可以更方便的定制需要的内核,移植的工作目前可以说十分简化了。对于内核本身支持的硬件,只要在设备树中添加,在通过配置项修改,就可以快速支持;对于内核不支持的硬件,就需要通过驱动实现,这部分就可以参考章节03中各模块的驱动功能实现。对驱动的掌握越深,对于内核的移植就越简单,深入学习驱动相关的知识,也是掌握芯片bringup的必经之路。
直接开始下一小节: 内核执行流程分析