从这一步开始,正式进入如何构建系统运行平台的工作。
本节以正点原子的I.MX6ull阿尔法开发板为基础,移植目前NXP提供的最新u-boot和kernel,并在其上构建支持busybox,bootroot和debian系统的平台功能实现。本篇修改涉及硬件,设备树,驱动加载和系统执行过程等知识,从过程和结果去总结如何完成U-Boot的移植和构建。当然,真正的理解移植流程需要些前置的知识积累,详细如下。
对于uboot的移植,具体目录如下所示。
在前面提到过,构建系统运行平台根据来源不同如下所示。
本篇以芯片厂商提供的U-Boot源码方案进行说明。
以NXP的系列芯片为例(包含I.MX6ull),其U-Boot源码放置在github上,地址如下。
# 下载U-Boot源码
git clone https://github.com/nxp-imx/uboot-imx.git
不过这种方式除了uboot代码外,也会下载git的管理文件,体积大,速度慢,建议github网页选择特定分支下载。目前默认的稳定分支为If_v2022.04(If_v2023.04已经发布,不过在我写此节文章时不是default,所以等稳定后再迁移到此版本)。这里选择分支(lf_v2022.04)。选择Code>Download ZIP即可下载,当然也可以直接使用如下命令下载。
# 下载指定分支的U-Boot
wget https://codeload.github.com/nxp-imx/uboot-imx/zip/refs/heads/lf_v2022.04
#下载并解压U-Boot文件
unzip lf_v2022.04
cd uboot-imx-lf_v2022.04/
如此,便完成了U-Boot的下载。
此时拥有的是NXP实现的,适配官方开发板的U-boot源码;对于I.MX6ULL适配的就是官方的MCIMX6ULL-EVK开发板,相应的资料如下:
此地址下有提供的原理图和相关资料,自行设计的开发板在硬件上当然和这块板子有差异,因此需要相应的移植修改动作。不过这里先不要做修改,验证下我们的环境能否完成U-Boot的编译,如果能够编译成功,后续的修改才有意义,也可以判断是否下载了完整固件包。对于U-Boot的编译,就涉及到了前面讲述的内容。U-Boot通过Kconfig进行管理配置项,然后在通过Makefile完成控制编译,最终生成可执行固件。理论上基础的U-Boot需要配置大量的选项来进行平台的适配,不过庆幸的是芯片厂商已经完成大部分的配置,我们只要在基础上修改即可。对于适配好的平台配置信息,在uboot/configs目录下提供,以I.MX6ull为例,官方开发板对应的配置文件为
可以使用如下命令进行开始界面的修改(以emmc版本为例)。不过在编译前,选择合适版本的编译器,并添加到系统环境中。这里选择测试的版本为gcc-arm-11.2-2022.02-x86_64-arm-none-linux-gnueabihf,详细下载和安装方式参考编译环境构建小节说明。
#step1:选择emmc版本作为配置输入,生成配置界面
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- mx6ull_14x14_evk_emmc_defconfig
#step2:编译U-Boot
make -j8 ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf-
流程解释如下所示。
SHIPPED dts/dt.dtb
CAT u-boot-dtb.bin
COPY u-boot.dtb
COPY u-boot.bin
CFGS u-boot-dtb.cfgout
MKIMAGE u-boot-dtb.imx
编译完成后,目录下的u-boot-dtb.imx即为编译后的可执行文件。如果使用官方的开发板,可以直接下载使用,如果是方案商或者自己设计的开发板,上面只表示我们验证了U-Boot的编译环境,不能够直接用于我们的开发板(直接下载部分不匹配的外设驱动无法正常工作,不过如果使用相同的电源和通讯串口,打印一般没有问题)。
注意:U-Boot编译如果出错,很大可能是缺少某些库问题,因此编译前执行编译环境构建中的Linux平台库安装中提供的命令。
按照上面步骤,去官网下载源码,然后编译成固件使用并不困难,不过把U-Boot源码移植到适配自己的开发板并不简单。我看到的教程都是只告诉如何修改,并没有告知为什么这样修改,我在学习过程中,按照方法也移植成功了,对于移植的理解仍然是知其然不知其所以然。如何系统的有效理解U-Boot的移植过程,并能够推广适配到其它SOC芯片,这是我在学习和理解移植过程面对的最大难题。我也是在多次移植调试,验证不同芯片,不同版本的U-Boot源码,思考了许久后,才有了一些自己的见解。
U-Boot的功能总结起来就一句话:根据启动配置项区别,从对应存储中加载内核和设备树到内存中,然后跳转执行,其它硬件的检查,执行都属于附加的功能。那么我们移植的过程,总结起来就是:根据硬件的差异化,进行U-Boot软件适配的过程。对于早期的版本,因为没有使用设备树支持,需要通过软件开发适配不同的硬件差异,也就是更多的代码修改,而较新的版本因为设备树的支持,则相对来说简单清晰。
备注: 随着嵌入式的版本迭代,同一个功能的实现支持多种接口和方法实现,无论是U-Boot,Linux的移植,还是驱动的实现,都要面临这类问题的困扰. 所有接口都学习当然可以,但本身难度就高,接口多了更容易迷惑。从我的经验来说,选新不选旧。当然如果不确定,可以看看内核driver/目录下的驱动实现,加入主干的驱动基本上就是最佳实践了。
基于单片机开发的经验,应该知道要开发支持某个硬件,从实现上包含如下要点。
对于SOC来说,因为芯片的复杂度原因,由开源的U-Boot和Linux源码提供对于硬件的抽象以及系统集成的功能。不过这只能解决软件上的差异. 对于具体的硬件,差异仍然是客观存在的。那么移植U-Boot,Kernel时,其思路和上诉并没有不同,但是因为U-Boot和Linux源码已经实现了完整的框架和接口,所以移植只需要进行适配硬件,实现少量的代码开发即可,具体流程如下所示。
下面按照这个流程去讲解分析。
对于硬件差异的确认。在硬件设计时,一般会有与官方开发板硬件差异性的对比表格(类似BOM表),来避免设计问题,如果有,那么这一步直接拿来用即可。对于使用方案商的开发板,没有I/O对照表时,就需要根据原理图去比对,整理出表格,然后基于硬件差异确定对设备树的修改。虽然初始比较耗时间,但这可以让你清晰了解板级的资源,也可以避免发生I/O或者器件地址被占用,导致后期实际开发中出问题。
对于目前我手中的开发板和官方开发板,硬件差异还是很多的。项目如果用于实际产品中,那么这一步建议是直接全部修改适配。不存在的器件就注释,删除,新增的硬件则扩展支持。这个难度不低,不过这里主要是入门学习,可以先提供基础的U-Boot功能支持。
硬件模块 | 功能说明 | 官方开发板 | 用户开发板 |
---|---|---|---|
fec1 phy_addr(ENET1) | 网口1的地址 | 0x02 | 0x0 |
fec2 phy_addr(ENET2) | 网口2的地址 | 0x01 | 0x1 |
fec1 reset | 网口1的Reset引脚 | 74LVN595(IO-1) | MX6UL_PAD_SNVS_TAMPER7__GPIO5_IO07 |
fec2 reset | 网口2的Reset引脚 | 74LVN595(IO-2) | MX6UL_PAD_SNVS_TAMPER8__GPIO5_IO08 |
CAN2 | CAN2接口引脚 | MX6UL_PAD_UART2_RTS_B__FLEXCAN2_RX MX6UL_PAD_UART2_CTS_B__FLEXCAN2_TX | CAN2未使用 |
i2c1 chip | i2c1外部器件 | FXAS21002CQ MAG3310 FXLS8974CFR3 | AP3216C |
i2c2 chip | i2c2外部器件 | OV5640 TSC | AP3216C |
LCD | LCD屏幕分辨率 | 480*272 | 800*480 |
LCD RESET | LCD复位引脚 | (LCD_RESET)MX6UL_PAD_SNVS_TAMPER9__GPIO5_IO09 | UM805RE,RESET when Power On |
QSPI | QSPI引脚和器件 | MT25QL256 | 未使用 |
SPI3 | SPI3引脚和器件 | SPI3未使用 | ICM20608 |
SIM2 | SIM2卡连接引脚 | SIM2接口 | 未使用 |
SPI软件模拟口 | SPI软件扩展引脚 | 74LV595 | 未使用 |
触摸键盘引脚 | I/O扩展引脚 | GPIO1-01 ~ GPIO01-04 | 未使用 |
UART2功能 | UART2功能引脚 | TTL功能 | 未使用 |
USDHC1 Extern | SdCard引脚功能 | 选择和复位脚 | 未使用 |
I/O | 普通I/O引脚 | 未使用 | key,led,beep |
在文件embed_linux_design.xlsx列出了我整理的官方开发板和正点原子阿尔法开发板的差异。理论上对于U-Boot的移植,是需要把上面所有的设备都进行匹配,实现满足本地硬件的驱动代码。不过这里只要得出可用的设备树,相对来说就比较简单了,在这里先要讲讲U-Boot的主要功能。
为了支持U-Boot的上述功能,就需要进行硬件适配,下面基于此进行说明,首先根据硬件的差异对比,影响U-Boot功能相关的如下所示。
其中串口打印硬件一致,不需要修改,那么主要适配的就是LCD,网口,存储相关接口。
LCD的分辨率和复位引脚差异,修改如下。
pinctrl_lcdif_ctrl: lcdifctrlgrp {
fsl,pins = <
MX6UL_PAD_LCD_CLK__LCDIF_CLK 0x79 //lcd引脚复用
MX6UL_PAD_LCD_ENABLE__LCDIF_ENABLE 0x79
MX6UL_PAD_LCD_HSYNC__LCDIF_HSYNC 0x79
MX6UL_PAD_LCD_VSYNC__LCDIF_VSYNC 0x79
/* used for lcd reset */
// MX6UL_PAD_SNVS_TAMPER9__GPIO5_IO09 0x79
>;
};
&lcdif {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_lcdif_dat
&pinctrl_lcdif_ctrl>;
display = <&display0>;
status = "okay";
display0: display@0 {
bits-per-pixel = <24>;
bus-width = <24>;
display-timings {
native-mode = <&timing0>;
timing0: timing0 {
clock-frequency = <33000000>;
hactive = <800>; /* 分辨率修改为800x400 */
vactive = <480>;
hfront-porch = <40>;
hback-porch = <88>;
hsync-len = <48>;
vback-porch = <32>;
vfront-porch = <13>;
vsync-len = <3>;
hsync-active = <0>;
vsync-active = <0>;
de-active = <1>;
pixelclk-active = <0>;
};
};
};
};
存储部分SDCard不需要复位和选择引脚。
pinctrl_usdhc1: usdhc1grp {
fsl,pins = <
MX6UL_PAD_SD1_CMD__USDHC1_CMD 0x17059
MX6UL_PAD_SD1_CLK__USDHC1_CLK 0x10071
MX6UL_PAD_SD1_DATA0__USDHC1_DATA0 0x17059
MX6UL_PAD_SD1_DATA1__USDHC1_DATA1 0x17059
MX6UL_PAD_SD1_DATA2__USDHC1_DATA2 0x17059
MX6UL_PAD_SD1_DATA3__USDHC1_DATA3 0x17059
MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059 /* SD1 CD */
// MX6UL_PAD_GPIO1_IO05__USDHC1_VSELECT 0x17059 /* SD1 VSELECT */
// MX6UL_PAD_GPIO1_IO09__GPIO1_IO09 0x17059 /* SD1 RESET */
>;
};
以太网部分比较复杂,主要包含复位引脚和地址的变化,修改如下。
&iomuxc_snvs {
pinctrl_enet1_reset: enet1grp {
fsl,pins = <
MX6ULL_PAD_SNVS_TAMPER7__GPIO5_IO07 0x10B0
>;
};
pinctrl_enet2_reset: enet1grp {
fsl,pins = <
MX6ULL_PAD_SNVS_TAMPER8__GPIO5_IO08 0x10B0
>;
};
};
&fec1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_enet1
&pinctrl_enet1_reset>; //fec1复位引脚连接配置
phy-mode = "rmii";
phy-handle = <ðphy0>;
phy-reset-gpios = <&gpio5 7 GPIO_ACTIVE_LOW>; //fec1复位引脚是否逻辑翻转配置
phy-reset-duration = <200>;
status = "disabled";
};
&fec2 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_enet2
&pinctrl_enet2_reset>; //fec2复位引脚连接配置
phy-mode = "rmii";
phy-handle = <ðphy1>;
phy-reset-gpios = <&gpio5 8 GPIO_ACTIVE_LOW>; //fec2复位引脚是否逻辑翻转配置
phy-reset-duration = <200>;
status = "okay";
mdio {
#address-cells = <1>;
#size-cells = <0>;
ethphy0: ethernet-phy@0 { //phy1寄存器地址
reg = <0>;
micrel,led-mode = <1>;
clocks = <&clks IMX6UL_CLK_ENET_REF>;
clock-names = "rmii-ref";
};
ethphy1: ethernet-phy@1 { //phy2寄存器地址
reg = <1>;
micrel,led-mode = <1>;
clocks = <&clks IMX6UL_CLK_ENET2_REF>;
clock-names = "rmii-ref";
};
};
};
上述的修改可以直接在arch/arm/boot/dts/imx6ul-14x14-evk.dtsi中修改,后续直接使用mx6ull_14x14_evk_emmc_defconfig配置项编译即可。不过这种修改会影响到所有相关的文件,如果出问题,会丢失原始的数据,这里建议复制一份imx6ul-14x14-[name].dtsi,然后再进行的文件修改。在本例中,相关的文件如下。
上一节我们通过对比硬件,更新了设备树,这一节就要把设备树导入u-boot中,来实现用户功能,主要分为以下步骤。
# copy用户自定义的配置文件
cp configs/mx6ull_14x14_evk_emmc_defconfig configs/mx6ull_14x14_rmk_emmc_defconfig
cp configs/mx6ull_14x14_evk_emmc_defconfig configs/mx6ull_14x14_rmk_sd_defconfig
cp configs/mx6ull_14x14_evk_nand_defconfig configs/mx6ull_14x14_rmk_nand_defconfig
# 修改和设备树相关的选项
CONFIG_TARGET_MX6ULL_14X14_EVK=y => CONFIG_TARGET_MX6ULL_14X14_RMK=y
## sd或emmc
CONFIG_DEFAULT_DEVICE_TREE="imx6ull-14x14-evk-emmc" => CONFIG_DEFAULT_DEVICE_TREE="imx6ull-14x14-rmk-emmc"
## nand
CONFIG_DEFAULT_DEVICE_TREE="imx6ull-14x14-evk-emmc" => CONFIG_DEFAULT_DEVICE_TREE="imx6ull-14x14-rmk-gpmi-weim"
# 硬件区分(与USDHC连接有关,USDHC1连接SDCard,对应0,USDHC2连接EMMC,对应1)
# emmc(mx6ull_14x14_rmk_emmc_defconfig)
CONFIG_SYS_MMC_ENV_DEV=1
# sdcard(mx6ull_14x14_rmk_sd_defconfig)
CONFIG_SYS_MMC_ENV_DEV=0
复制board相关的文件,定义自己的板级代码实现。
#board相关的头文件
cp include/configs/mx6ullevk.h cp include/configs/mx6ullrmk.h
#board相关的文件目录
mkdir board/freescale/mx6ullrmk
cp board/freescale/mx6ullevk/* board/freescale/mx6ullrmk/*
对于board相关的文件,主要更改如下。
imximage_lpddr2.cfg文件。
#board/freescale/mx6ullrmk/imximage_lpddr2.cfg
PLUGIN board/freescale/mx6ullevk/plugin.bin 0x00907000 => PLUGIN board/freescale/mx6ullrmk/plugin.bin 0x00907000
imximage.cfg文件。
#board/freescale/mx6ullrmk/imximage.cfg
PLUGIN board/freescale/mx6ullevk/plugin.bin 0x00907000 => PLUGIN board/freescale/mx6ullrmk/plugin.bin 0x00907000
Kconfig文件,用于显示板级信息的界面管理文件。
#board/freescale/mx6ullrmk/Kconfig
if TARGET_MX6ULL_14X14_RMK || TARGET_MX6ULL_9X9_EVK
config SYS_BOARD
default "mx6ullrmk"
config SYS_VENDOR
default "freescale"
config SYS_CONFIG_NAME
default "mx6ullrmk"
config IMX_CONFIG
default "board/freescale/mx6ullrmk/imximage.cfg"
config SYS_TEXT_BASE
default 0x87800000
endif
MAINTAINERS文件,定义board相关的配置和访问文件。
#board/freescale/mx6ullrmk/MAINTAINERS
MX6ULLEVK BOARD
M: Peng Fan <peng.fan@nxp.com>
S: Maintained
F: board/freescale/mx6ullrmk/
F: include/configs/mx6ullrmk.h
F: configs/mx6ull_14x14_rmk_defconfig
F: configs/mx6ull_14x14_rmk_plugin_defconfig
F: configs/mx6ulz_14x14_evk_defconfig
Makefile文件。
#board/freescale/mx6ullrmk/Makefile
# SPDX-License-Identifier: GPL-2.0+
# (C) Copyright 2016 Freescale Semiconductor,Inc.
obj-y := mx6ullrmk.o
mk6ullrmk.c文件
//board/freescale/mx6ullrmk/mk6ullrmk.c
int checkboard(void)
{
if (is_mx6ull_9x9_evk())
puts("Board: MX6ULL 9x9 RMK\n");
else if (is_cpu_type(MXC_CPU_MX6ULZ))
puts("Board: MX6ULZ 14x14 RMK\n");
else
puts("Board: MX6ULL 14x14 RMK\n");
return 0;
}
plugin.S文件
#CONFIG_USE_IMXIMG_PLUGIN宏未定义,plugin.S未导入
#board/freescale/mx6ullrmk/plugin.S
no modify(plugin.bin)
include/configs/mx6ullrmk.h文件,这里包含默认的环境变量配置,这部分可以自己修改维护,或者使用boot.scr管理都可以,因为官方使用为USDHC2,因此主要修改如下。
//include/configs/mx6ullrmk.h
/* environment organization */
#if CONFIG_SYS_MMC_ENV_DEV == 0
#define CONFIG_MMCROOT "/dev/mmcblk0p2" /* USDHC1 */
#else
#define CONFIG_MMCROOT "/dev/mmcblk1p2" /* USDHC2 */
#endif
环境变量可以在此文件修改,也可以在BootLoader命令行模式下修改,详细参考文件:uboot-env。
另外需要将Board文件导入到U-Boot源码中,修改文件arch/arm/mach-imx/mx6/Kconfig,将新的Board配置信息导入。
# 增加RMK的编译选项(在界面里增加编译选项选择)
config TARGET_MX6ULL_14X14_RMK
bool "Support mx6ull_14x14_rmk"
depends on MX6ULL
select BOARD_LATE_INIT
select DM
select DM_THERMAL
select IMX_MODULE_FUSE
select OF_SYSTEM_SETUP
imply CMD_DM
# 将增加的板级文件导入到menuconfig中
source "board/freescale/mx6ullrmk/Kconfig"
因为phy芯片的自适应配置需要在芯片复位重连时才能进行配置,因此在自适应配置前需要复位一次芯片,具体代码如下。
//drivers/net/phy/phy.c
int genphy_config_aneg(struct phy_device *phydev)
{
int result;
/* 复位phy,增加 */
phy_reset(phydev);
if (phydev->autoneg != AUTONEG_ENABLE)
return genphy_setup_forced(phydev);
result = genphy_config_advert(phydev);
if (result < 0) /* error */
return result;
if (result == 0) {
/*
* Advertisment hasn't changed,but maybe aneg was never on to
* begin with? Or maybe phy was isolated?
*/
int ctl = phy_read(phydev, MDIO_DEVAD_NONE, MII_BMCR);
if (ctl < 0)
return ctl;
if (!(ctl & BMCR_ANENABLE) || (ctl & BMCR_ISOLATE))
result = 1; /* do restart aneg */
}
/*
* Only restart aneg if we are advertising something different
* than we were before.
*/
if (result > 0)
result = genphy_restart_aneg(phydev);
return result;
}
将上述修改完成后,即可执行编译命令。
make mx6ull_14x14_rmk_emmc_defconfig ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf-
make -j6 ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf-
成功后如下所示,u-boot-dtb.imx即为编译好的U-Boot固件,如下图所示。
之后将其下载到开发板中运行即可。
直接开始下一小节: uboot环境和命令