build_embed_linux_system

嵌入式Linux启动流程说明

通过上一章节的学习,掌握了如何基于虚拟机安装Linux系统,修改软件源,安装常用软件应用;并讲述了如何交叉编译库和程序应用到嵌入式平台。从本章节开始,就需要在上述平台上完成嵌入式Linux系统运行的环境,用于真正的嵌入式设备执行,让芯片运行起来,构建驱动和应用执行的平台,这也就是嵌入式Linux开发中最困难的芯片的bringup移植流程实现。

对于系统启动的移植和调试,不仅包含对于SOC启动流程和硬件资源的掌握,还包含U-Boot、Kernel、Rootfs、库移植、init/systemd操作、shell脚本、gdb调试,也依赖于对驱动开发的精通情况。对于本章节的学习先建议大致浏览,如果遇到不理解的内容有概念即可,可以在驱动学习完之后再深入学习。

本章节涉及的相关知识点如下。

本节目录如下所示。

system_startup

嵌入式Linux平台一般由U-Boot,Kernel和Rootfs三部分组成,对于最新的ARM还支持U-Boot SPL用于实现两段式加载(U-Boot SPL主要用于初始化ddr,进行安全相关的处理,并加载U-Boot)。目前能够运行嵌入式Linux系统的平台主流都属于复杂的SOC系统,像单片机一样由用户直接从启动的入口函数去开发是不现实的。为了解决这个问题,技术开发人员维护了基于U-Boot,Kernel,Rootfs构成的框架,来完成对于运行平台的软件支持,让我们可以更轻松的构建嵌入式产品应用。如果使用过开发板,那么一定知晓可以通过拨码开关实现从SDCard,EMMC或者Nand上电,这是因为芯片内部有厂商维护的ROM程序,支持串行下载,上电Uboot加载等功能。

对于不需要U-Boot SPL的Linux芯片,启动流程如下。

BootRom => U-Boot => Linux Kernel => System Rootfs

其中SOC ROM支持根据外部的引脚状态选择启动方式和加载U-Boot的地址。对于最新的芯片,支持通过U-Boot SPL多段启动方式,另外也支持OPTEE安全核管理系统,启动流程则更加复杂。

以RK3568为例,启动流程如下所示。

BOOTROM => [ TPL => SPL(MiniLoader) ] => [ TRUST => U-Boot(uboot.img) ] => Linux Kernel => System Rootfs

uboot_feature

U-Boot(Universal BootLoader)开源引导加载程序,类似PC端的BIOS引导,在启动时开始加载;主要包含功能如下。

U-Boot支持多种功能,但在应用开发中,U-Boot最重要的功能仍然是加载内核,跳转执行。无论是网络服务,命令行管理,都是提供机制支持在运行状态下控制加载内核的方式,如从NFS/SD/EMMC/NAND加载等。事实上,传统的U-Boot对于Linux启动流程也不是必须的,通过简化的SPL实现直接跳转内核也可以实现,不过这部分只有芯片厂商对芯片资源足够了解才能够实现,一般开发者很难实现此功能。

kernel_feature

Linux Kernel是系统平台的核心组件,负责管理系统的硬件和软件资源,为应用程序提供服务。

Linux提供上述功能和接口用于具体应用的实现。表现在具体的应用中,就以文件的方式,通过访问相应的文件接口,可以实现对于硬件的操作和管理。

rootfs_feature

文件和操作访问这些文件的命令和库的集合就是我们常提到的Rootfs(文件系统),简单来说,文件系统就是目录和文件的合集,里面包含两部分。

无论是busybox,buildroot,yocto,debian还有ubuntu,都是提供上述目录和功能,并没有什么本质区别,只是对应不同的启动服务配置和软件,从而支持不同的功能。

  1. 复制和创建文件系统需要的目录,如bin,lib,tmp,usr,etc,dev,root,home等。
  2. 安装系统执行需要的命令,如bash shell,tar,unzip,ls,cd等。
  3. 安装程序运行需要的库,一般是编译时调用的gcc中的lib库,以及一些软件执行需要的库,如libc.so.6。
  4. 添加启动和配置文件或服务,加载一系列命令完成调用。

本小节中,大致描述嵌入式Linux平台中最重要的三个部分,有个基础概念,在后续将进一步深入,详细去实现移植,构建和运行各组件。

soc_bringup_design

对于构建Linux启动运行的平台,从易到难有三个层次。

core_board_bringup

基于方案商平台的芯片启动是大部分产品应用面对的环境,使用稳定的工业原型板,大部分情况都可以不做修改直接进行开发。即使有新的硬件要求,也只是在基础上增加或者裁剪相应的外设驱动就可以。如Yocto,Debian构建的系统,各类lib库都已经支持,大部分情况都是提供编译好的SDK做到开箱即用。

嵌入式的主要工作就是应用开发,做到少量甚至完全不修改驱动。就可以满足大部分场景的要求。而且理论上方案商的工业开发板使用人员多,测试往往更加充分,系统会更加稳定。一般小公司或者大公司内小团队如果没有足够的人员维护稳定的嵌入式Linux环境,可能还不如方案商的平台稳定安全,效率高,这也是很多产品直接采用方案商的平台开发的原因。不过这类平台也有价格高,定制和专业性一般,功能更新需要额外付费,技术支持及时性的问题,需要开发者去平衡。

chip_bringup

基于芯片厂商方案的芯片启动实现,这类工作主要是方案商或培训机构,以及能够负担专门维护定制化Linux团队的企业负责。在确定嵌入式芯片后,芯片厂商会提供相应的编译工具,支持维护的U-Boot和Linux分支,以及基于官方开发板维护软硬件平台方案。因为官方开发板一般只展示厂商主推的功能,I/O暴露较少,作为应用平台一般需要二次的软硬件设计。

方案商的主要工作,就是基于芯片厂商的方案,二次设计硬件,并维护自己的配置,提供打包的环境和烧录工具的资源包。基于芯片厂商提供的已经适配芯片的U-Boot,kernel和文件系统,裁剪和修改维护一套SDK资源包,然后以整机或方案的模式提供开发者。这部分主要工作内容包含如下:

backbone_bringup

系统主干到芯片的适配是由芯片原厂完成,需要以某个版本的U-Boot和Linux发布主干为基础,增加芯片级的硬件支持,包括不限于存储,寄存器,设备树和硬件驱动接口,特殊模块如GPU,NPU驱动的支持。不过除非全新的模块,大部分也是在原驱动基础上迭代,进行新的功能的支持。这些工作是伴随着芯片存在的整个周期进行的,而且同一系列的芯片也会有继承,这也是芯片原厂中嵌入式开发主要的工作之一,对于大部分嵌入式Linux开发者来说是接触不到的。从技术上需要对芯片内核,设计框架,驱动,U-Boot,Linux内核都要有深入认知。这部分国外的芯片厂商如NXP,TI做的比较好,官网都会提供全套的资料,即使个人用户也很方便去查找使用,国内厂商则基本只对商业用户开放,客观上是人为设置壁垒,对于芯片的推广也是不利的,除非价格差异大,学习阶段建议选择资料更全的芯片

综上可知,如果是希望往嵌入式Linux驱动方向发展,那么需要向第二种方向努力,第三种则看能否去原厂的机会在提高。如果从事嵌入式Linux应用开发,只要满足第一种水平,能够在原型板基础上完成U-Boot,Linux和文件系统的编译,进行部分外设的修改,就算入门了。对于增加新的器件如RS485,摄像头,i2c接口的温度传感器,I/O扩展芯片,SPI扩展屏也能够实现驱动并正常工作,在遇到的软硬件问题能够有思路并解决,那么已经可以应对大部分产品开发遇到的问题。对于系统BSP工程师的要求,肯定听过对于u-boot和kernel的配置和裁剪,芯片的启动工作也是如此。配置和裁剪u-boot和kernel,主要为了满足以下需求。

  1. 减少体积,通过裁剪删除不需要的配置项,可以减少编译后u-boot和kernel固件的体积,不过比起文件系统动辄几兆的库文件,这部分体积杯水车薪,基本可以忽略
  2. 优化启动时间,嵌入式Linux平台启动时间是关键指标,从打开电源到正式进入UI主界面,涉及u-boot启动功能,boot-delay时间,kernel驱动加载时间,文件系统加载,应用启动时间共同组成,优化启动时间对于经常开关机的设备是很重要的需求
  3. 适配硬件,实现芯片的启动流程和系统正常运行。设备的功能需要相应的驱动支持,配置相应选项和设备树是必须需求
  4. 裁剪硬件,降低不存在器件的驱动工作导致的功耗浪费和可能的异常问题

带着这些需求,去理解芯片bringup的流程可以事半功倍。

bringup_analysis

imx6ull_bringup

i.MX6Ull时基于Cortex-A7的32位平台,启动流程比较简单,执行顺序如下所示。

BOOTROM => U-BOOT => KERNEL => Rootfs/init

  1. BOOTROM: 芯片内部固化的程序,通过检测boot引脚加载U-BOOT执行,当然在BOOTROM也会初始化对应的硬件,从而确保能够成功加载。
  2. U-BOOT: 引导加载程序,完成必要硬件的初始化(如外挂的DDR),进行自检动作,然后将内核和设备树加载到内存中,跳转执行
  3. KERNEL: 解析设备树,加载驱动,完成初始化动作,加载文件系统
  4. Rootfs/init: 文件系统加载后,开始顺序执行文件系统内的程序,脚本和服务

当文件系统执行后,既可以提供命令行或者图形化的服务。无论是简单的busybox环境,还是复杂的debian系统(Ubuntu),以及图形化的Android系统,其实原理上都是一致的。了解了这一点,对于nand,emmc,sdcard和网络启动就比较好理解了。这里面bootloader的加载是由芯片内部的rom程序完成的,根据相应的启动选项(检测I/O引脚决定启动加载地址),可以实现从选择存储地址中加载U-Boot并执行,之后由U-Boot进一步从地址中(如通过网络,SDCard,USB等)加载Linux内核到内存中。了解了这个流程,对于arm 32位系统的启动,可以按照相同的流程去实现。

关于I.MX6ULL的存储内固件架构,如下所示。

Boot stage number Address Actual program name Image Name
1 1k offset U-Boot U-Boot.bin
2 /dev/mmc0:part1 Kernel zImage/*.dtb
3 /dev/mmc0:part2 Rootfs Rootfs

rk3568_bringup

RK3568是基于Cortex-A53的64位平台,支持ARM TrustZone机制,因此启动流程复杂,启动顺序如下所示。

BOOTROM => MiniLoader.bin(SPL+TPL) => Trust.img => Uboot => Kernel => Rootfs

#安装debian基础结构
RELEASE=bullseye TARGET=desktop ARCH=arm64 ./mk-base-debian.sh

#安装必要组件
RELEASE=bullseye ARCH=arm64 ./mk-Rootfs.sh

#打包生成固件linaro-roofs.img
./mk-image.sh

关于RK3568镜像内部数据介绍。

镜像名称 说明
uboot.img uboot.img 是一种 FIT 格式镜像,它由多个镜像合并而成, 其中包括trust 镜像(ARM Trusted Firmware + OP-TEE OS)、 u-boot 镜像、 uboot dtb; 编译 U-Boot 时会将这些镜像打包成一个 uboot.img。uboot.img 会烧录到开发板 uboot 分区
MiniLoaderAll.bin 该镜像是运行在 RK3568 平台 U-Boot 之前的一段 Loader 代码(也就是比 U-Boot 更早阶段的 Loader), MiniLoaderAll.bin 由 TPL 和 SPL两部分组成, TPL 用于初始化 DDR,运行在 SRAM; 而 SPL 运行在DDR,主要负责加载、 引导 uboot.img。
boot.img boot.img 也是一种 FIT 格式镜像, 它也是由多个镜像合并而成, 其中包括内核镜像、 内核 DTB、 资源镜像resource.img。boot.img 会烧录到开发板 boot 分区
misc.img 包含 BCB(Bootloader Control Block) 信息,该镜像会烧写到开发板misc 分区。misc 分区是一个很重要的分区,其中存放了 BCB 数据块,主要用于Android/Linux 系统、 U-Boot 以及 recovery 之间的通信
oem.img 给厂家使用,用于存放厂家的 APP 或数据,该镜像会烧写至开发板oem 分区,系统启动之后会将其挂载到/oem 目录
parameter.txt 一个 txt 文本文件,是 RK3568 平台的分区表文件(记录分区名以及每个分区它的起始地址、结束地址);烧写镜像时,并不需要将parameter.txt 文件烧写到 Flash, 而是会读取它的信息去定义分区
recovery.img recovery 模式镜像, recovery.img 用于进入 recovery 模式, recovery.img 会烧录到开发板 recovery 分区。recovery 模式是一种用于对设备进行修复、升级更新的模式。 recovery.img 也是 FIT 格式镜像, 也是由多个镜像合并而成,其中包括ramdisk(进入 recovery 模式时挂载该根文件系统)、内核镜像(进入recovery 模式时启动该内核镜像)、 内核 DTB 以及 resource.img。
Rootfs.img 正常启动模式下对应的根文件系统镜像, 包含有大量的库文件、可执行文件等。Rootfs.img 会烧录到开发板 Rootfs 分区
userdata.img 给用户使用,可用于存放用户的 App 或数据; 该镜像会烧写至开发板 userdata 分区,系统启动之后, 会将其挂载到/userdata 目录

next_chapter

返回目录

直接开始下一章节:Makefile脚本语法