翼度科技»论坛 云主机 LINUX 查看内容

掌握嵌入式Linux编程3引导程序

5

主题

5

帖子

15

积分

新手上路

Rank: 1

积分
15
3引导程序

引导程序是嵌入式Linux的第二个要素。它是启动系统和加载操作系统内核的部分。在这一章中,我们将研究引导程序的作用,特别是它如何使用一种叫做设备树的数据结构将控制权从自身传递给内核,设备树也被称为扁平化设备树或FDT(flattened device tree)。我将介绍设备树的基本知识,因为这将帮助你遵循设备树中描述的连接,并将其与真实的硬件联系起来。
我将研究流行的开源引导程序U-Boot的,并向你展示如何使用它来引导目标设备,以及如何通过使用BeagleBone Black作为例子来定制它,使它能够运行在新设备上。
在本章中,我们将介绍以下内容:

  • Bootloader是做什么的?
  • 启动顺序
  • 从引导程序转移到内核
  • 设备树
  • U-Boot
引导程序是做什么的?

在嵌入式Linux系统中,引导程序有两项主要工作:将系统初始化到基本level和加载内核。事实上,第一项工作在某种程度上是附属于第二项工作的,因为它只需要让系统工作到加载内核所需的程度。
当启动程序代码的第一行被执行时,在开机或复位之后,系统处于非常小的状态。DRAM控制器没有被设置,所以主存储器不能被访问。同样,其他接口也没有被配置,所以通过NAND闪存控制器、MMC控制器等访问的存储是不可用的。通常情况下,开始时唯一可以运行的资源是单个CPU核心、一些片上静态存储器和启动ROM。
系统引导由几个阶段的代码组成,每个阶段都会使更多的系统进入运行状态。引导程序的最终行为是将内核加载到RAM中并为其创造执行环境。引导程序和内核之间的接口细节是针对特定的架构的,但在每一种情况下,它都必须做两件事。首先,bootloader必须传递指针到包含硬件配置信息的结构。第二,它必须传递指向内核命令行的指针。
内核命令行是控制Linux行为的文本字符串。一旦内核开始执行,就不再需要bootloader了,它所使用的所有内存都可以被回收。
Bootloader 的一个附属工作是提供维护模式,用于更新 Boot 配置,将新的 Boot Image 加载到内存中,也可能用于运行诊断程序。这通常是由简单的命令行用户界面控制的,通常是通过串行控制台。
启动顺序

几年前,只需要在处理器的复位矢量处把引导程序放在非易失性存储器中。当时 NOR 闪存很普遍,由于它可以直接映射到地址空间,所以是理想的存储方法。下图显示了这样的配置,复位向量位于闪存区域的上端0xfffffffc处。Bootloader被连接起来,所以在这个位置有一条跳转指令,指向Bootloader代码的开始:

从那时起,在NOR闪存中运行的引导程序代码可以初始化DRAM控制器,使主存储器--DRAM--变得可用,然后将自己复制到DRAM中。一旦完全运行,引导程序可以将内核从闪存加载到DRAM中,并将控制权转移给它。
然而,一旦你离开了简单的可线性寻址的存储介质,如NOR闪存,启动序列就会变成复杂的多阶段程序。每个SoC的细节都是非常具体的,但它们一般都遵循以下各个阶段。
第1阶段--ROM代码

在没有可靠的外部存储器的情况下,复位或开机后立即运行的代码必须被存储起来。
复位或上电的过程必须存储在SoC的片上;这被称为ROM代码。它在制造时被加载到芯片中,因此,ROM代码是专有的,不能被开放源码的等同物所取代。通常,它不包括初始化存储器控制器的代码,因为DRAM的配置是高度特定于设备的,所以它只能使用静态随机存取存储器(SRAM Static Random Access Memory ),它不需要存储器控制器。
大多数嵌入式SoC设计都有少量的片上SRAM,大小从4KB到几百KB不等:

ROM代码能够从几个预编程的位置之一加载一小块代码到SRAM中。举例来说,TI OMAP和Sitara芯片试图从NAND闪存的前几页加载代码,或从通过串行外设接口(SPI Serial Peripheral Interface)连接的闪存,或从MMC设备(可能是eMMC芯片或SD卡)的第一个扇区,或从MMC设备的第一个分区上名为MLO的文件。如果它从所有这些存储器设备中读取失败,那么它就会尝试从以太网、USB或UART中读取字节流;后者主要是作为生产过程中向闪存加载代码的手段,而不是用于正常操作。大多数嵌入式SoC都有以类似方式工作的ROM代码。在SoC中,如果SRAM不够大,无法加载完整的引导程序,如U-Boot,就必须有一个中间的加载器,称为二级程序加载器(SPL)。
在ROM代码阶段结束时,SPL(secondary program loader)存在于SRAM中,ROM代码会跳到该代码的开头。
第2阶段--二级程序加载器

SPL必须设置内存控制器和系统的其他基本部分,以准备将三级程序加载器(TPL Tertiary Program Loader)加载到DRAM中。SPL的功能受限于SRAM的大小。它可以从存储设备的列表中读取程序,就像ROM代码一样,再次使用从闪存设备开始的预编程偏移量。如果SPL内置了文件系统驱动程序,它可以从磁盘分区中读取知名的文件名,如u-boot.img。SPL通常不允许任何用户互动,但它可以打印版本信息和进度信息,你可以在控制台看到。下图解释了第二阶段的结构:

前面的图显示了从ROM代码跳到SPL的过程。当SPL在SRAM内执行时,它将TPL加载到DRAM中。在第二阶段结束时,TPL存在于DRAM中,而SPL可以跳转到该区域。
SPL可能是开源的,如TI x-loader和Atmel AT91Bootstrap的情况,但它包含由制造商提供的二进制blob的专有代码是很常见的。
第三阶段--TPL

在这一点上,我们正在运行一个完整的引导程序,例如U-Boot,我们将在本章的后面了解一下它。通常,有简单的命令行用户界面,可以让你执行维护任务,比如将新的引导和内核映像加载到闪存中,以及加载和启动内核,还有一种方法是自动加载内核而不需要用户干预。
下图解释了第三阶段的架构:

前面的图显示了从SRAM中的SPL到DRAM中的TPL的跳跃过程。随着TPL的执行,它将内核加载到DRAM中。如果我们愿意,我们还可以选择在DRAM中的映像上附加一个FDT和/或初始RAM磁盘。无论哪种方式,在第三阶段结束时,内存中都有一个内核,等待被启动。
一旦内核开始运行,嵌入式引导程序通常会从内存中消失,不再参与系统的运行。在这之前,TPL需要把启动过程的控制权交给内核。
从 Bootloader 转移到 kernel

当引导程序将控制权交给内核时,它必须传递一些基本信息,其中包括以下内容:

  • 机器号,在不支持设备树的PowerPC和Arm平台上使用,用来识别SoC的类型。
  • 到目前为止已经检测到的硬件的基本细节,包括(最起码)物理RAM的大小和位置以及CPU的时钟速度。
  • 内核命令行。
  • 可选的,设备树二进制的位置和大小。
  • 可选,初始RAM磁盘的位置和大小,称为初始RAM文件系统(initramfs)。
内核命令行是一个普通的ASCII字符串,通过给出例如包含根文件系统的设备名称来控制Linux的行为。我们将在下一章讨论这个问题的细节。将根文件系统作为RAM盘提供是很常见的,在这种情况下,引导程序有责任将RAM盘镜像加载到内存中。我们将在第五章 "构建根文件系统 "中介绍如何创建初始RAM磁盘。
这种信息的传递方式取决于体系结构,并且在最近几年有所改变。例如,对于PowerPC,引导程序只是简单地传递一个指向电路板信息结构的指针,而对于Arm,它传递一个指向A标签列表的指针。在文档/arm/Booting的内核源中,对A标签的格式有一个很好的描述。
在这两种情况下,所传递的信息量都非常有限,大部分信息都是在运行时发现的,或者作为平台数据硬编码到内核中。平台数据的广泛使用意味着每个板子都必须有一个为该平台配置和修改的内核。我们需要一个更好的方法,这个方法就是设备树。在Arm世界中,随着Linux 3.8的发布,从A标签的转变开始认真进行。今天,几乎所有的Arm系统都使用设备树来收集硬件平台的具体信息,允许单一的内核二进制文件在广泛的这些平台上运行。
现在我们已经了解了引导程序的作用,引导序列的阶段是什么,以及它如何将控制权传递给内核,让我们学习如何配置引导程序,使其在流行的嵌入式SoC上运行。
介绍设备树

如果你正在使用Arm或PowerPC SoC,你几乎肯定会在某个时候遇到设备树。本节旨在让你快速了解它们是什么以及它们是如何工作的。在本书过程中,我们将反复讨论设备树的话题。
设备树是定义计算机系统硬件组件的一种灵活方式。请记住,设备树只是静态数据,不是可执行代码。通常,设备树由引导程序加载并传递给内核,尽管有可能将设备树与内核镜像捆绑在一起,以满足不能单独加载的引导程序。
这个格式来自于Sun Microsystems公司的OpenBoot的引导程序,它被正式确定为Open Firmware规范,也就是IEEE标准IEEE1275-1994。它被用于基于PowerPC的Macintosh电脑中,因此是PowerPC Linux端口的合理选择。从那时起,它已经被许多Arm Linux实现大规模采用,在较小的程度上,被MIPS、MicroBlaze、ARC和其他架构采用。
我建议访问https://www.devicetree.org,了解更多信息。
设备树基础知识

Linux内核在arch/$ARCH/boot/dts中包含了大量的设备树源文件,这是学习设备树的好的起点。在arch/$ARCH/dts的U-boot源代码中也有少量的源文件。如果你的硬件是从第三方购买的,dts文件是板卡支持包的一部分,所以你应该期望和其他源文件一起收到。
设备树将计算机系统表示为以层次结构连接在一起的组件的集合,如一棵树。设备树以根节点开始,根节点用正斜杠/表示,它包含代表系统硬件的后续节点。每个节点都有一个名称,并包含一些名称="值 "形式的属性。下面是简单的例子:
  1. /dts-v1/;/{model = "TI AM335x BeagleBone";compatible = "ti,am33xx";#address-cells = ;#size-cells = ;cpus {#address-cells = ;#size-cells = ;cpu@0 {compatible = "arm,cortex-a8";device_type = "cpu";reg = ;};};memory@0x80000000 {device_type = "memory";reg = ; /* 512 MB */};};
复制代码
这里,我们有一个根节点,它包含一个cpus节点和一个内存节点。cpus节点包含一个名为cpu@0的CPU节点。这些节点的名字通常包括一个@,后面是一个地址,用来区分该节点和其他同类型的节点。如果节点有reg属性,@是必须的。
根节点和CPU节点都有一个兼容属性。Linux内核使用这个属性,通过与每个设备驱动程序在of_device_id结构中输出的字符串进行比较,找到一个匹配的设备驱动程序(更多信息请参见第11章,与设备驱动程序的接口)。
按照惯例,兼容属性的值由制造商名称和组件名称组成,以减少不同制造商制造的类似设备之间的混淆;因此,ti,am33xx和arm,cortex-a8。如果有一个以上的驱动程序可以处理这个设备,那么兼容属性有一个以上的值也是很常见的。它们被列在第一位,最合适的被提到。
CPU节点和内存节点有一个device_type属性,它描述了设备的类别。节点名称通常从device_type派生。
reg属性

前面显示的内存和CPU节点有一个reg属性,它指的是寄存器空间中的单位范围。reg属性由两个值组成,分别代表实际物理地址和范围的大小(长度)。这两个值都被写成零或多个32位整数,称为单元。因此,前面的内存节点指的是从0x80000000开始、长度为0x20000000字节的单组内存。
当地址或大小值不能用32位表示时,理解寄存器的属性就变得更加复杂。例如,在具有64位寻址的设备上,你需要为每个单元格设置两个:
  1. /{#address-cells = ;#size-cells = ;memory@80000000 {device_type = "memory";reg = ;};};
复制代码
关于所需单元格数量的信息被保存在祖先节点的#address-cells和#size_cells属性中。换句话说,要理解一个reg属性,你必须在节点层次结构中向后看,直到找到#address-cells和#size_cells。如果没有的话,每一个的默认值都是1--但是对于设备树编写者来说,依赖默认值是不好的做法。
现在,让我们回到cpu和cpus的节点上。CPU也有地址;在一个四核设备中,它们可能被编为0、1、2和3。这可以被认为是没有任何深度的一维数组,所以大小为零。因此,你可以看到我们在cpus节点中有#address-cell = 和#size-cells = ,而在子节点cpu@0中,我们为reg属性分配了一个值,reg = 。
标签和中断

到目前为止,我们所描述的设备树的结构假定有单一的组件层次,而事实上,有几个。除了组件与系统其他部分之间明显的数据连接外,它还可能与中断控制器、时钟源和电压调节器相连接。为了表达这些连接,我们可以给节点添加一个标签,并从其他节点引用该标签。这些标签有时被称为phandles,因为当设备树被编译时,从另一个节点引用的节点被分配了一个独特的数值,这个属性称为phandle。如果你反编译设备树的二进制文件,你可以看到它们。
以一个包含可以产生中断的LCD控制器和一个中断控制器的系统为例:
  1. /dts-v1/;{intc: interrupt-controller@48200000 {compatible = "ti,am33xx-intc";interrupt-controller;#interrupt-cells = ;reg = ;};lcdc: lcdc@4830e000 {compatible = "ti,am33xx-tilcdc";reg = ;interrupt-parent = ;interrupts = ;ti,hwmods = "lcdc";status = "disabled";};};
复制代码
在这里,我们有一个interrupt-controller@48200000节点,标签为intc。interrupt-controller属性将其标识为中断控制器。像所有的中断控制器一样,它有一个#interrupt-cell属性,它告诉我们需要多少个单元来表示中断源。在这种情况下,只有一个表示中断请求(IRQ)号码。其他中断控制器可能会使用额外的单元来描述中断的特征;例如,指示它是边缘触发还是电平触发。中断单元的数量和它们的含义在每个中断控制器的绑定中都有描述。设备树的绑定可以在Linux内核源文件/devicetree/bindings/目录下找到。
看一下lcdc@4830e000节点,它有interrupt-parent属性,使用标签引用它所连接的中断控制器。它还有一个interrupts属性,在这种情况下是36。注意,这个节点有它自己的标签,lcdc,它被用在其他地方:任何节点都可以有一个标签。
设备树包含文件

很多硬件在同一家族的SoC之间和使用同一SoC的板子之间是通用的。这反映在设备树中,就是将共同的部分分割成包含文件,通常以.dtsi为扩展名。开放固件标准将/include/定义为要使用的机制,如vexpress-v2p-ca9.dts中的这个片段:
/include/ "vexpress-v2m.dtsi"
不过,翻看内核中的.dts文件,你会发现一个从C语言中借用的替代性包含语句;例如,在am335x-boneblack.dts中:
  1. #include "am33xx.dtsi"#include "am335x-bon-common.dtsi"
复制代码
下面是am33xx.dtsi的另一个例子:
  1. #include #include #include .
复制代码
最后,include/dt-bindings/pinctrl/am33xx.h包含正常的C语言宏:
  1. #define PULL_DISABLE (1 。输入help会打印出所有在这个版本的U-Boot中配置的命令;输入help 会打印出关于某个特定命令的更多信息。
  2. BeagleBone Black的默认命令解释器是非常简单的。按左键或右键不能进行命令行编辑;按Tab键没有命令完成;按up键没有命令历史。按这些键中的任何一个都会打乱你目前正在尝试输入的命令,你将不得不输入Ctrl + C,然后重新开始。你唯一可以安全使用的行编辑键是退格键。作为一个选项,你可以配置一个不同的命令外壳,叫做Hush,它有更复杂的交互式支持,包括命令行编辑。
  3. 默认的数字格式是十六进制。考虑以下命令作为例子:</p>[code]=> nand read 82000000 400000 200000
复制代码
这将从NAND闪存开始的偏移量0x400000读取0x200000字节到RAM地址0x82000000。
U-Boot广泛地使用环境变量来存储和传递函数之间的信息,甚至用来创建脚本。环境变量是简单的name=value对,被存储在内存的一个区域。变量的初始数量可以在电路板配置头文件中编码,像这样:
  1. #define CONFIG_EXTRA_ENV_SETTINGS"myvar1=value1""myvar2=value2"[…]
复制代码
你可以使用setenv从U-Boot命令行创建和修改变量。例如,setenv foo bar用bar值创建foo变量。注意,在变量名称和数值之间没有=符号。你可以通过将一个变量设置为空字符串来删除它,setenv foo。你可以用printenv把所有的变量打印到控制台,或者用printenv foo把单个变量打印出来。
如果U-Boot已经配置了存储环境的空间,你可以使用saveenv命令来保存它。如果有原始的NAND或NOR闪存,那么可以为这个目的保留擦除块,通常另被用作冗余拷贝以防止损坏。如果有eMMC或SD卡存储,它可以被存储在保留的扇区阵列中,或存储在磁盘分区中名为uboot.env的文件中。其他选择包括将其存储在通过I2C或SPI接口连接的串行EEPROM或非易失性RAM中。
U-Boot没有文件系统。相反,它用64字节的头来标记信息块,这样它就可以跟踪内容。我们使用mkimage命令行工具为U-Boot准备文件,它与Ubuntu的u-boot-tools软件包捆绑在一起。你也可以通过在U-Boot源代码树中运行make tools来获得mkimage,然后以tools/mkimage的形式调用它。
下面是对该命令用法的简要总结:
  1. $ mkimageError: Missing output filenameUsage: mkimage -l image-l ==> list image header informationmkimage [-x] -A arch -O os -T type -C comp -a addr -e ep -n name -d data_file[:data_file...] image-A ==> set architecture to 'arch'-O ==> set operating system to 'os'-T ==> set image type to 'type'-C ==> set compression type 'comp'-a ==> set load address to 'addr' (hex)-e ==> set entry point to 'ep' (hex)-n ==> set image name to 'name'-d ==> use image data from 'datafile'-x ==> set XIP (execute in place)mkimage [-D dtc_options] [-f fit-image.its|-f auto|-F] [-b  [-b ]] [-i ] fit-image file is used with -f auto, it may occur multiple times.-D => set all options for device tree compiler-f => input filename for FIT source-i => input filename for ramdisk fileSigning / verified boot options: [-E] [-B size] [-k keydir] [-K dtb] [ -c ] [-p addr] [-r] [-N engine]-E => place data outside of the FIT structure-B => align size in hex for FIT structure and header-k => set directory containing private keys-K => write public keys to this .dtb file-c => add comment in signature node-F => re-sign existing FIT image-p => place external data at a static position-r => mark keys used as 'required' in dtb-N => openssl engine to use for signingmkimage -V ==> print version information and exitUse '-T list' to see a list of available image types
复制代码
例如,要为Arm处理器准备一个内核镜像,你可以使用
下面的命令:
  1. $ mkimage -A arm -O linux -T kernel -C gzip -a 0x80008000 \-e 0x80008000-n 'Linux' -d zImage uImage
复制代码
在这个例子中,架构是arm,操作系统是linux,图像类型是kernel。此外,压缩方案是gzip,加载地址是0x80008000,入口点与加载地址相同。最后,图像
名称是Linux,图像数据文件被命名为zImage,正在生成的图像被命名为uImage。
通常情况下,你会从可移动的存储器中加载镜像,比如SD卡或网络。SD卡在U-Boot中是由MMC驱动处理的。典型的顺序是用来加载图像到内存中的,如下:
  1. => mmc rescan=> fatload mmc 0:1 82000000 uimagereading uimage4605000 bytes read in 254 ms (17.3 MiB/s)=> iminfo 82000000## Checking Image at 82000000 ...Legacy image foundImage Name: Linux-3.18.0Created: 2014-12-23 21:08:07 UTCImage Type: ARM Linux Kernel Image (uncompressed)Data Size: 4604936 Bytes = 4.4 MiBLoad Address: 80008000Entry Point: 80008000Verifying Checksum ... OK
复制代码
mmc rescan命令重新初始化了MMC驱动,也许是为了检测最近是否有SD卡被插入。接下来,fatload被用来从SD卡上FAT格式的分区中读取文件。其格式如下:
  1. fatload  [ [ [ [bytes [pos]]]]]
复制代码
如果是mmc,就像我们的例子一样,dev:part是MMC接口的设备号,从0开始计算,分区号从1开始计算。因此,是第一个设备上的第一个分区,也就是microSD卡的mmc 0(板载eMMC是mmc 1)。内存位置,0x82000000,被选择在RAM的一个区域,这个区域目前还没有被使用。如果我们打算启动这个内核,我们必须确保当内核映像被解压缩并位于运行时位置0x80008000时,RAM的这个区域不会被覆盖。
要通过网络加载映像文件,你必须使用琐碎的文件传输协议(TFTP)。这需要你在你的开发系统上安装一个TFTP守护程序,tftpd,并开始运行它。你还必须配置你的PC和目标板之间的任何防火墙,以允许UDP 69端口的TFTP协议通过。TFTP的默认配置只允许访问/var/lib/tftpboot目录。下一步是将你想传输到目标板的文件复制到该目录。然后,假设你使用的是一对静态IP地址,这样就不需要进一步的网络管理,加载一组内核镜像文件的命令序列应该是这样的:
  1. => setenv ipaddr 192.168.159.42=> setenv serverip 192.168.159.99=> tftp 82000000 uImagelink up on port 0, speed 100, full duplexUsing cpsw deviceTFTP from server 192.168.159.99; our IP address is 192.168.159.42Filename 'uImage'.Load address: 0x82000000Loading:########################################################################################################################################################################################################################################################################################################################3 MiB/sdoneBytes transferred = 4605000 (464448 hex)
复制代码
最后,让我们看看如何将图像编程到NAND闪存中并读回,这由nand命令来处理。这个例子通过TFTP加载一个内核镜像,并将其编程到闪存中:
  1. => tftpboot 82000000 uimage=> nandecc hw=> nand erase 280000 400000NAND erase: device 0 offset 0x280000, size 0x400000Erasing at 0x660000 -- 100% complete.OK=> nand write 82000000 280000 400000NAND write: device 0 offset 0x280000, size 0x4000004194304 bytes written: OK
复制代码
现在,你可以使用nand read命令从闪存中加载内核:
  1. => nand read 82000000 280000 400000
复制代码
一旦内核被加载到RAM中,我们就可以启动它。
引导Linux

bootm命令启动一个正在运行的内核镜像。其语法如下:
  1. bootm [address of kernel] [address of ramdisk] [address of dtb].
复制代码
内核映像的地址是必须的,但是如果内核配置不需要它们,ramdisk和dtb的地址可以省略。如果有dtb但没有initramfs,第二个地址可以用破折号(-)代替。这看起来就像这样:
  1. => bootm 82000000 – 83000000
复制代码
很明显,每次开机时输入一长串命令来启动你的板子是不能接受的。让我们来看看如何使启动过程自动化。
U-Boot将一连串的命令储存在环境变量中。如果名为bootcmd的特殊变量包含一个脚本,它就会在开机时经过bootdelay秒的延迟而运行。如果你在串行控制台观察,你会看到延迟倒计时为零。你可以在这期间按任何一个键来终止倒计时,并进入与U-Boot的交互式会话。
创建脚本的方法很简单,虽然不容易读懂。你只需将命令用分号隔开,分号前面必须有一个/转义字符。因此,举例来说,要从闪存的偏移量中加载一个内核镜像并启动它,你可以使用下面的命令:
  1. setenv bootcmd nand read 82000000 400000 200000\;bootm 82000000
复制代码
我们现在知道如何使用U-Boot在BeagleBone Black上启动内核了。但是我们如何将U-Boot移植到一个没有BSP的新板子上呢?我们将在本章的其余部分介绍这个问题。
移植U-Boot到新板

让我们假设你的硬件部门已经创建了一个新的板子,叫做Nova,是基于BeagleBone Black的,你需要把U-Boot移植到它上面。你将需要了解U-Boot代码的布局以及板子的配置机制是如何工作的。在本节中,我将向你展示如何创建现有板子的变体--BeagleBone Black--你可以把它作为进一步定制的基础。有相当多的文件需要被修改。我把它们放在代码档案中的MELP/Chapter03/0001-BSP-for-Nova.patch中。你可以简单地把这个补丁应用到2021.01版的U-Boot的一个干净的拷贝上,像这样:
  1. $ cd u-boot$ patch -p1 < MELP/Chapter03/0001-BSP-for-Nova.patch
复制代码
如果你想使用不同版本的U-Boot,你必须对补丁做一些修改,才能干净地应用。
本节的其余部分将描述如何创建该补丁。如果你想按部就班,你将需要一个没有Nova BSP补丁的U-Boot 2021.01的干净拷贝。我们要处理的主要目录如下:

  • arch
包含arm、mips和powerpc目录中每个支持的架构的特定代码。在每个架构中,每个家族成员都有一个子目录;例如,在arch/arm/cpu/中,有架构变体的目录,包括amt926ejs、armv7和armv8。

  • 板卡
包含特定于某个板卡的代码。如果有几个来自同一供应商的板子,它们可以被收集到一个子目录里。因此,对BeagleBone所基于的am335x EVM板的支持就在board/ti/am335x中。

  • common
包含核心功能,包括命令外壳和可以从中调用的命令,每个都在一个名为cmd_[命令名称].c的文件中。

  • doc
包含了几个README文件,描述了U-Boot的各个方面。如果你想知道如何进行你的U-Boot移植,这是一个好的开始。

  • include
除了许多共享的头文件之外,这里还包含了非常重要的include/configs/子目录,在这里你可以找到大部分的板子配置设置。
Kconfig从Kconfig文件中提取配置信息并将全部系统配置存储在一个名为.config的文件中的方式将在第四章 "配置和构建内核 "中详细描述。每块板都有一个默认的配置,存储在configs/[板名]_defconfig中。对于Nova板,我们可以先为EVM制作一个配置的副本:
  1. $ cp configs/am335x_evm_defconfig configs/nova_defconfig
复制代码
现在,编辑configs/nova_defconfig并在CONFIG_AM33XX=y之后插入CONFIG_TARGET_NOVA=y,如图所示:
  1. CONFIG_ARM=yCONFIG_ARCH_CPU_INIT=yCONFIG_ARCH_OMAP2PLUS=yCONFIG_TI_COMMON_CMD_OPTIONS=yCONFIG_AM33XX=yCONFIG_TARGET_NOVA=yCONFIG_SPL=y[…]
复制代码
注意 CONFIG_ARM=y 会导致 arch/arm/Kconfig 的内容被包含在内,而 CONFIG_AM33XX=y 会导致 arch/arm/mach-omap2/am33xx/Kconfig 被包含在内。
接下来,将CONFIG_SYS_CUSTOM_LDSCRIPT=y和CONFIG_SYS_LDSCRIPT=="board/ti/nova/u-boot.lds "插入到CONFIG_DISTRO_DEFAULTS=y之后的同一个文件,如图所示:
  1. [...]CONFIG_SPL=yCONFIG_DEFAULT_DEVICE_TREE="am335x-evm"CONFIG_DISTRO_DEFAULTS=yCONFIG_SYS_CUSTOM_LDSCRIPT=yCONFIG_SYS_LDSCRIPT="board/ti/nova/u-boot.lds"CONFIG_SPL_LOAD_FIT=y[...]
复制代码
现在我们已经完成了对configs/nova_defconfig的修改。
每个板子都有一个名为board/[板名]或board/[供应商]/[板名]的子目录,它应该包含以下内容:

  • Kconfig: 包含电路板的配置选项。
  • MAINTAINERS: 包含板子目前是否被维护的记录,如果是的话,由谁维护。
  • Makefile: 用来构建板子的特定代码。
  • README: 包含任何关于U-Boot这个端口的有用信息;例如,包括哪些硬件变体。
    此外,还可能有针对板卡功能的源文件。
    我们的Nova板是基于BeagleBone的,而BeagleBone又是基于TI am335x EVM的。因此,我们应该复制am335x板的文件:
  1. $ mkdir board/ti/nova$ cp -a board/ti/am335x/* board/ti/nova
复制代码
接下来,编辑board/ti/nova/Kconfig,将SYS_BOARD设置为 "nova",这样它就会在board/ti/nova中建立文件。然后,将SYS_CONFIG_NAME也设为 "nova",这样它就会使用include/configs/nova.h作为配置文件:
  1. if TARGET_NOVAconfig SYS_BOARD        default "nova"config SYS_VENDOR        default "ti"config SYS_SOC        default "am33xx"config SYS_CONFIG_NAME        default "nova"[…]
复制代码
这里还有一个文件,我们需要修改。链接器脚本被放置在board/ti/nova/u-boot.lds,包含一个硬编码的引用,指board/ti/am335x/built-in.o:
  1. {*(.__image_copy_start)*(.vectors)CPUDIR/start.o (.text*)board/ti/nova/built-in.o (.text*)}
复制代码
现在,我们需要将Nova的Kconfig文件链接到Kconfig文件链中。首先,编辑arch/arm/Kconfig,在source "board/tcl/sl50/Kconfig "之后插入source "board/ti/nova/Kconfig",如下图所示:
  1. {    *(.__image_copy_start)    *(.vectors)    CPUDIR/start.o (.text*)    board/ti/nova/built-in.o (.text*)}
复制代码
然后,编辑arch/arm/mach-omap2/am33xx/Kconfig,在TARGET_AM335X_EVM后面紧接着添加一个TARGET_NOVA的配置选项,如图所示:
  1. […]source "board/st/stv0991/Kconfig"source "board/tcl/sl50/Kconfig"source "board/ti/nova/Kconfig"source "board/toradex/colibri_pxa270/Kconfig"source "board/variscite/dart_6ul/Kconfig"[…]
复制代码
然后,编辑 arch/arm/mach-omap2/am33xx/Kconfig,在 TARGET_AM335X_EVM 之后添加 TARGET_NOVA 的配置选项,如图所示:
  1. […]config TARGET_NOVA        bool "Support the Nova! board"        select DM        select DM_GPIO        select DM_SERIAL        select TI_I2C_BOARD_DETECT        imply CMD_DM        imply SPL_DM        imply SPL_DM_SEQ_ALIAS        imply SPL_ENV_SUPPORT        imply SPL_FS_EXT4        imply SPL_FS_FAT        imply SPL_GPIO_SUPPORT        imply SPL_I2C_SUPPORT        imply SPL_LIBCOMMON_SUPPORT        imply SPL_LIBDISK_SUPPORT        imply SPL_LIBGENERIC_SUPPORT        imply SPL_MMC_SUPPORT        imply SPL_NAND_SUPPORT        imply SPL_OF_LIBFDT        imply SPL_POWER_SUPPORT        imply SPL_SEPARATE_BSS        imply SPL_SERIAL_SUPPORT        imply SPL_SYS_MALLOC_SIMPLE        imply SPL_WATCHDOG_SUPPORT        imply SPL_YMODEM_SUPPORT        help          The Nova target board[…]
复制代码
所有 imply SPL_ 行都是必要的,这样 U-Boot 才能顺利编译,不会出错。
现在,我们已经复制并修改了 Nova 板的特定文件,接下来就看头文件了。
每块板都有一个头文件,位于 include/configs/ 中,其中包含大部分配置信息。该文件以电路板 Kconfig 文件中的 SYS_CONFIG_NAME 标识命名。该文件的格式在 U-Boot 源代码树顶层的 README 文件中有详细描述。对于我们的 Nova 板,只需将 include/configs/am335x_evm.h 复制到 include/configs/nova.h,然后做一些修改,如图所示:
  1. […]#ifndef __CONFIG_NOVA_H#define __CONFIG_NOVA_Hinclude #include #undef CONFIG_SYS_PROMPT#define CONFIG_SYS_PROMPT "nova!> "#ifndef CONFIG_SPL_BUILD# define CONFIG_TIMESTAMP#endif[…]#endif  /* ! __CONFIG_NOVA_H */
复制代码
除了将 __CONFIG_AM335X_EVM_H 替换为 __CONFIG_NOVA_H,唯一需要做的改动是设置一个新的命令提示符,以便在运行时识别这个引导加载器。
这样我们就可以在运行时识别这个引导加载器。
完全修改了源代码树之后,我们现在就可以为定制板构建 U-Boot 了。
参考资料

构建和测试

要为 Nova 板构建 U-Boot,请选择刚刚创建的配置:
  1. $ source ../MELP/Chapter02/set-path-arm-cortex_a8-linux-gnueabihf$ make distclean$ make nova_defconfig$ make
复制代码
将 MLO 和 u-boot.img 复制到之前创建的 microSD 卡启动分区,然后启动主板。你应该会看到这样的输出(注意命令提示符):
  1. U-Boot SPL 2021.01-dirty (Feb 08 2021 - 21:30:41 -0800)Trying to boot from MMC1U-Boot 2021.01-dirty (Feb 08 2021 - 21:30:41 -0800)CPU  : AM335X-GP rev 2.1Model: TI AM335x BeagleBone BlackDRAM:  512 MiBWDT:   Started with servicing (60s timeout)NAND:  0 MiBMMC:   OMAP SD/MMC: 0, OMAP SD/MMC: 1Loading Environment from FAT... *** Warning - bad CRC, using default environment not set. Validating first E-fuse MACNet:   eth2: ethernet@4a100000, eth3: usb_etherHit any key to stop autoboot:  0nova!>
复制代码
您可以使用 git format-patch 命令为所有这些更改创建补丁:
  1. $ git add .$ git commit -m "BSP for Nova"[detached HEAD 093ec472f6] BSP for Nova12 files changed, 2379 insertions(+)create mode 100644 board/ti/nova/Kconfigcreate mode 100644 board/ti/nova/MAINTAINERScreate mode 100644 board/ti/nova/Makefilecreate mode 100644 board/ti/nova/READMEcreate mode 100644 board/ti/nova/board.ccreate mode 100644 board/ti/nova/board.hcreate mode 100644 board/ti/nova/mux.ccreate mode 100644 board/ti/nova/u-boot.ldscreate mode 100644 configs/nova_defconfigcreate mode 100644 include/configs/nova.h$ git format-patch -10001-BSP-for-Nova.patch
复制代码
生成此补丁后,U-Boot 作为 TPL 的覆盖范围就结束了。U-Boot 也可以配置为完全绕过启动过程中的 TPL 阶段。接下来,让我们来看看启动 Linux 的另一种方法。
Falcon模式

我们习惯于认为,启动现代嵌入式处理器需要 CPU 引导 ROM 加载 SPL,SPL 加载 u-boot.bin,u-boot.bin 再加载 Linux 内核。你可能想知道是否有办法减少这些步骤,从而简化并加快启动过程。答案就是 U-Boot Falcon 模式。这个想法很简单:让 SPL 直接加载内核映像,省去 u-boot.bin。没有用户交互,也没有脚本。它只是将内核从闪存或 eMMC 中的已知位置加载到内存中,然后将预先准备好的参数块传递给它,并启动它运行。配置猎鹰模式的细节超出了本书的范围。如果你想了解更多信息,请查看 doc/README.falcon。
重要提示
猎鹰模式是以游隼命名的,游隼是所有鸟类中速度最快的一种,在俯冲中能达到每小时 200 英里以上的速度。
小结

每个系统都需要一个引导加载器来启动硬件和加载内核。U-Boot 支持各种有用的硬件,而且很容易移植到新设备上,因此受到许多开发者的青睐。在本章中,我们学习了如何通过串行控制台的命令行交互式检查和驱动 U-Boot。这些命令行练习包括使用 TFTP 通过网络加载内核,以实现快速迭代。最后,我们学习了如何通过为 Nova 板生成补丁,将 U-Boot 移植到新设备上。
在过去几年中,由于嵌入式硬件的复杂性和多样性不断增加,引入了设备树作为描述硬件的方法。设备树只是系统的文字表述,它被编译成设备树二进制文件(DTB),并在加载时传递给内核。内核负责解释设备树,并为其找到的设备加载和初始化驱动程序。
在使用中,U-Boot 非常灵活,可以从大容量存储器、闪存或网络加载映像,然后启动。在介绍了启动 Linux 的一些复杂之处后,我们将在下一章介绍该过程的下一阶段,即嵌入式项目的第三个元素--内核--的作用。

来源:https://www.cnblogs.com/testing-/p/17525233.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

举报 回复 使用道具