版权说明
本书为作者精校开源版,版权归作者所有。
作者允许读者在注明来源后引用内容,并通知作者。
前 言
近年来,随着硬件性能的快速提高,嵌入式Linux系统已经得到越来越多的应用。要想用好嵌入式Linux系统,首先要会启动嵌入式Linux系统。
本电子书围绕学会启动嵌入式Linux系统这个关键话题展开,利用ZYNQ7000开发板作为载体,深入介绍了ZYNQ7000开发板嵌入式Linux系统各种启动方式的启动过程、特点以及制作方法。
全书采用学以致用的方式编写,图文并茂地讲解了各项操作的具体过程,务求读者可以亲自实践。
本书面向对计算机操作比较熟悉,乐于动手实操的读者,具体要求如下:
- 安装以及使用过虚拟机;
- 有Linux命令的使用基础;
- 了解嵌入式Linux系统的基本概念;
符合要求的读者只要学完本书并完成相关操作,应该都能:
- 了解ZYNQ7000开发板嵌入式Linux系统启动的过程;
- 了解ZYNQ7000开发板嵌入式Linux系统启动过程涉及的文件;
- 了解ZYNQ7000开发板嵌入式Linux系统启动过程的关键参数;
- 学会使用与启动相关的Linux命令;
- 学会使用启动加载器U-Boot的主要命令;
最终学会启动ZYNQ7000开发板的嵌入式Linux系统。
本书由孙宇彤、孙启凡执笔,感谢康宇哲提供的技术支持,本书编写时还参考了DeepSeek提供的信息。
2026.4
第1章 概述
1.1 嵌入式Linux系统简介
单片机,以其高集成、强可控的特点,一直以来都在产业界和人们的日常生活中扮演着重要角色,发挥着越来越大的作用。
这些年,随着芯片的集成程度不断提高,单片机的功能越发强大,单片机系统已经逐步从传统的裸机应用向嵌入式Linux系统转换,很像之前手机从功能机向智能机的跨越,有种改朝换代的感觉。
使用嵌入式Linux系统的最大好处,就是智能机与功能机的差别:应用与硬件解耦,系统引入了更多的开放性。这样,无论是开发者还是用户,都会受益匪浅。
比如,得益于Linux系统的高度开放性,嵌入式Linux系统可以部署到各种各样的硬件架构上,包括树莓派、香橙派这样的准PC,或者采用openwrt的路由器,又或者是本文的主角:基于ZYNQ7000系列芯片的开发板。
在Linux操作系统之上,我们就可以开发和安装各式各样的应用软件,灵活满足用户的需求。
当然,由于硬件架构众多,部署嵌入式Linux系统就成为极具挑战性的任务。在部署任务中,上电后的第一项任务——启动,尤为关键。大家都知道,良好的开端是成功的一半,只有设备正常启动了,才能取得开门红,为后续的应用开个好头。
俗话说得好:“开门七件事,柴米油盐酱醋茶”。嵌入式Linux系统的启动并不是简单的两个字,启动是一个过程,经历很多的步骤,是一环扣一环的。
另外,启动工作的具体环节与硬件架构密切相关,后续的内容将基于ZYNQ7000系列芯片开发板,详细介绍开发板的启动过程以及如何去操控启动过程。
附带讲一下,嵌入式Linux系统的启动过程还是有很多共通之处,学会了基于ZYNQ7000系列芯片的启动过程,其他硬件架构的启动过程也就豁然开朗了。
1.2 ZYNQ7000系列芯片介绍
ZYNQ7000系列芯片是赛灵思(Xilinx,目前已经被AMD收购)研发的产品,赛灵思以研发高性能的FPGA产品而知名。
ZYNQ7000系列芯片是赛灵思研发的一款具有里程碑意义的产品,首次将高性能的ARM处理器系统(PS,Processing System)与FPGA可编程逻辑(PL,Programmable Logic)紧密集成在单颗芯片,完美地实现了处理器的顺序执行能力和FPGA的并行处理能力的融合。
ZYNQ7000系列芯片具有相同的ZYNQ芯片架构,系列中包含多种型号的芯片,对应不同的硬件规模和芯片性能,以满足不同应用的需求和成本考量。
1.2.1 芯片特色
ZYNQ-7000系列芯片的本质是一个以处理器为中心的可编程SoC,因此芯片首先是一个强大的应用处理器,可以运行像Linux这样的复杂操作系统。其次芯片拥有一个FPGA,可以作为定制硬件加速器或专用外设。
传统的“FPGA+外挂CPU”方案存在通信延迟大、功耗高、PCB设计复杂等缺点。ZYNQ的设计者将FPGA与处理器两者置于同一芯片上,通过高速内部总线互联,实现了高带宽、低延迟的协同工作,特别适用于需要大量数据预处理、实时控制、硬件加速的应用场景。
1.2.2 体系结构:PS + PL
ZYNQ芯片由PS和PL两大体系结合而成,这是理解ZYNQ芯片的关键,其结构可以清晰地分为两大部分:
1. 处理器系统 - PS
PS是一个完整的、独立的ARM Cortex-A9处理器系统,即使不使用PL部分,它也能作为一颗标准的应用处理器工作。其主要组成部分包括:
- 中央处理器:
- 单核或双核ARM Cortex-A9 MPCore,主频最高可达1GHz;
- 每个核心拥有32KB指令和32KB数据一级缓存,共享512KB二级缓存;
- Cortex-A9是一款经典的32位处理器核心,使用ARMv7-A架构指令集;
- 集成了NEON协处理器和FPU浮点单元,支持硬件加速计算。
- 存储器接口:
- 支持多种标准的DDR控制器(如DDR3, DDR2, LPDDR2);
- 静态存储器控制器,支持NAND Flash、NOR Flash、SRAM等。
- 自带丰富的外设接口:
- 通用外设(SPI, I2C, CAN, UART, GPIO等);
- 高速接口(USB 2.0 (OTG), Gigabit Ethernet, SD/SDIO)。
- 这些外设大大降低了设计复杂度,无需在PL中重新实现。
- 互联基础架构:
- 包括AMBA总线、存储器交换网络、外设交换网络等,用于连接PS内部的各个模块。值得一提的是AMBA总线,这是ARM公司提出的一套开放的、用于连接芯片内部模块的通信标准,定义了这些模块之间如何相互通信和数据传输。
另外,ZYNQ支持多种启动方式,包括利用Flash、SD、JTAG启动。
2. 可编程逻辑 - PL
PL部分本质上就是一个传统的Xilinx 7系列FPGA,其逻辑资源根据具体型号有所不同。主要包括:
- 可配置逻辑块:CLB,由查找表(LUT)和触发器(Flip-Flop)组成,实现基本的组合和时序逻辑功能;
- 块RAM:BRAM,每个36Kb的分布式RAM模块,可用于数据缓存;
- 数字信号处理单元:用于实现高性能的乘法、乘加操作,是数学运算和信号处理算法的关键;
- 时钟管理单元:CMT,包含PLL和MMCM,用于时钟生成、去偏斜和频率综合;
- 可编程I/O:支持多种I/O标准,如LVCMOS, LVDS, SSTL等。
3. PS与PL的互联:关键优势
PS和PL之间通过多种高性能的AXI(Advanced eXtensible Interface)总线连接,这是ZYNQ架构的精髓。主要包括:
- 4个高性能AXI_HP接口:用于PL高速访问PS端的DDR存储器。
- 1个加速器一致性ACP端口:允许PL以缓存一致的方式访问PS,使得PL作为加速器时效率极高。
- 4个通用AXI_GP接口:用于PS和PL之间的一般性控制信号传输,例如处理器配置PL中的寄存器,或者PL向处理器发送中断信号。
这种丰富的互联结构保证了数据在处理器和可编程逻辑之间能够高效、低延迟地流动。
1.2.3 芯片系列
ZYNQ-7000系列芯片提供了多种硬件规模和性能的型号,以满足不同应用的需求和成本考量。
首先看PS处理器,分为如下两类:
- Zynq-7000S:单核Cortex-A9,较小规模PL,主打低成本、低功耗市场。
- Zynq-7000:双核Cortex-A9,PL规模从少到多,性能强,是系列中的主流产品,应用范围最广。
因此我们一般选用双核的ZYNQ-7000系列芯片。
其次看PL,也就是FPGA部分。
双核ZYNQ-7000系列芯片主要有Z-7010、Z-7015、Z-7020、Z-7030、Z-7035、Z-7045和Z-7100等型号,最后的数字代表了PL部分的规模大小。数字越大,逻辑资源(CLB、DSP、BRAM)越多,性能越强,价格也越高。
下表列出了三种典型型号Z-7010、 Z-7020和Z-7045的硬件规模:
| 资源类型 | Z-7010 | Z-7020 | Z-7045 |
|---|---|---|---|
| 逻辑单元 | 28K | 85K | 350 |
| 查找表 (LUTs) | 17,600 | 53,200 | 218,600 |
| 触发器 (FFs) | 35,200 | 106,400 | 437,200 |
| 块RAM (BRAM) | 60块 | 140块 | 545块 |
| 数字信号处理单元 | 80 | 220 | 900 |
| 最大I/O 引脚数 | 100 | 200 | 362 |
初学者推荐选择Z-7010或者Z-7020,性价比高。
1.2.4 开发板
市面上有很多采用ZYNQ-7010或者ZYNQ-7020的芯片的开发板,这里我们推荐两款:
1. EBAZ4205
EBAZ4205 是一款基于Xilinx Zynq-7010芯片的矿机主控板,由于矿机退役,被大量拆机流入市场,经过商家改造后,售价通常在百元级别,成为性价比极高的嵌入式学习开发板。
该板卡配备了256MB的DDR3 内存和128MB的NAND Flash,还配备了100M 的以太网口以及SD卡座。另外,该板卡还有多个插座,用于引出ZYNQ-7010的各种管脚,主要是PL端的GPIO端口。
该板卡支持运行 Linux 系统及自定义 FPGA 逻辑设计。
EBAZ4205的缺点是需要外加扩展板,才能实现供电以及提供USB接口。另外一个缺点是无法切换启动方式,在学习嵌入式Linux系统的启动时很不方便。
在本项目中,EBAZ4205主要作为验证板使用,用于验证学习的成果,不作为学习板使用,学习板使用下面介绍的R210。
2. R210
R210是一款基于Xilinx Zynq-7020芯片的软件无线电开发板,主要由Zynq-7020芯片和AD9361芯片组成,与ADI公司的ADALM Pluto基本兼容,价格在千元级别,是一款性价比较高的软件无线电开发板。
作为软件无线电的开发板,R210提供了2 收2发的射频SMA端口,其工作频率范围可达70MHz~6GHz,满足各种双通道无线电系统的需要。这一点,也是R210显著优于Pluto之处。
R210的其他优点包括提供了更为丰富的外部接口,例如千兆的网口、SD卡座以及两个TypeC的USB接口。这两个USB接口都可以作为供电端口,通常我们选择右侧这个USB接口供电。由于R210的功耗较大,因此电源输出需要5V/1A以上。
R210的硬件配置为512MB的DDR3 内存和32MB的NOR Flash,因此可以支持嵌入式Linux系统的启动。
在R210的最右侧,有一个2位的拨码开关,可以灵活地切换启动模式:SD启动、Flash启动或者JTAG启动,这为学习嵌入式Linux系统的启动提供了非常大的方便。
后续的章节我们就在R210开发板上,开始嵌入式Linux系统启动的学习过程。
1.3 全书内容简介
本书后续内容将围绕R210开发板,详细介绍SD启动以及Flash启动的具体步骤、工作原理以及制作方法。
其中第二章介绍基于分区的SD启动;第三章介绍RAMdisk的SD启动;第四章介绍Flash启动;第五章,介绍统一启动。
第2章 深度研究基于分区的SD启动
2.1 概述
R210作为基于ZYNQ7000系列的SDR板卡,支持SD、QSPI和JTAG等3种启动模式,QSPI就是Flash启动。
其中SD启动是R210的出厂标配,这种启动方式借鉴了树莓派的启动特点,将嵌入式Linux系统存放在TF卡上,可以把TF卡看成一个硬盘。
在树莓派系统中,TF卡做成两个分区,一个分区是启动分区,放置了启动文件。这个分区采用FAT的文件系统,可以通过windows系统访问,方便进行配置;另外一个分区是EXT4文件分区,运行树莓派的Raspberry Pi OS,也就是一种Linux系统。
R210的TF卡也是两个分区,一个FAT分区放置启动文件,当然这些启动文件与树莓派的完全不同;另外一个分区是EXT文件分区,运行嵌入式Linux系统。
本章将详细讲解R210基于分区的SD启动,并介绍相关的操作步骤。以下列出的是操作步骤用到的工具与命令
【本章windows工具】
树莓派镜像烧写工具、xshell、bootinfo
2.2 制作TF启动卡
前面说了,R210的TF启动源自树莓派,所以可以采用与树莓派一样的启动卡制作方式,也就是通过烧写镜像来制作TF启动卡。
为此,我们首先到https://www.raspberrypi.com/software/下载树莓派的镜像烧写工具:Raspberry Pi imager,然后安装Raspberry Pi imager,选择I accept the agreement,指定安装路径,点击next进行安装。
接下来下载R210的系统镜像: 2019_R1-2020_06_22.zip。
厂商提供的下载链接:https://pan.baidu.com/s/17vCbHScepADLSbk17y5MIQ 提取码:7H4M
随后找一张16GB或更大的空闲TF卡,插入电脑,运行树莓派的镜像烧写工具,设置R210系统的烧录参数。
下面3张图片展示了烧录参数的设置过程,其中“树莓派设备”项不用选择,“操作系统”项先选择“使用自定义镜像”,再选择刚刚下载的R210的系统镜像,“存储设备”选择对应的TF卡。
当出现如下图的界面时,点击下一步。
点击最右边的否,然后启动烧录过程。
烧录过程需要运行5到10分钟。烧录完成后,将TF卡重新插拔,就可以在windows系统中看到boot分区。将boot分区NeptuneSDR P210目录下的全部文件复制到根目录,就完成了TF启动卡的制作。
2.3 启动后查看TF卡的分区
制作完TF启动卡后,在启动R210之前,我们还要安装终端工具xshell,通过电脑远程操作R210的嵌入式Linux系统。
启动R210时先将R210的拨码开关打到SD启动,插入做好的TF启动卡,再用Type-C数据线连接R210的UART口与电脑的USB口,R210开始上电启动。
接着在电脑中找到新增的串口,记录串口的端口号。在xshell中增加一个会话,采用串口连接,波特率设为115200。
会话建好后,点击连接,就会出现终端界面。
在终端界面中输入回车键,如果看到如下的提示符,代表连接r210嵌入式Linux系统成功,已经以root用户成功连接到了R210:
root@analog: ~#
接下来查看分区情况:
root@analog:~# lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT mmcblk0 179:0 0 14.9G 0 disk ├─mmcblk0p2 179:2 0 6.4G 0 part / ├─mmcblk0p3 179:3 0 1M 0 part └─mmcblk0p1 179:1 0 1G 0 part root@analog:~# df -h Filesystem Size Used Avail Use% Mounted on /dev/root 6.2G 3.1G 2.8G 53% /
不难看出TF卡的第二分区挂载为系统的根目录,这也就是分区启动的含义。我们还可以输入mount命令,将第一分区挂载到/boot目录:
root@analog:~# mount /dev/mmcblk0p1 /boot
查看/boot目录的内容,不难发现与windows系统中看到的BOOT分区根目录的内容是一致的。
root@analog:/boot# cd /boot root@analog:/boot# ls -l total 9212 -rwxr-xr-x 1 root root 4681552 Sep 11 2022 BOOT.BIN drwxr-xr-x 2 root root 4096 Mar 11 2023 NeptuneSDR K210 drwxr-xr-x 2 root root 4096 Mar 11 2023 NeptuneSDR P210 drwxr-xr-x 2 root root 4096 Aug 8 2025 System Volume Information -rwxr-xr-x 1 root root 16492 Sep 11 2022 devicetree.dtb -rwxr-xr-x 1 root root 392 Feb 4 2020 uEnv.txt -rwxr-xr-x 1 root root 4704936 Oct 29 2024 uImage
附带说一下,输入Linux命令需要区分大小写,命令与参数之间需要有空格。
2.4 启动相关文件
在TF卡BOOT分区的根目录中,有4个文件与启动相关:BOOT.BIN、uEnv.txt、devicetree.dtb和uImage,以下分别介绍:
2.4.1 BOOT.BIN
BOOT.BIN是ZYNQ启动的第一个文件,其内容是多个分区镜像(二进制文件)的组合,这些分区镜像包括fsbl、FPGA比特流和u-boot,其中fsbl是ZYNQ启动过程的关键代码,FPGA比特流用于初始化ZYNQ的PL部分(也就是FPGA),u-boot用于加载嵌入式Linux系统。
BOOT.BIN利用文件头来定义多个分区镜像的结构,下图展示了文件头的结构,其中比较重要的数据结构有启动头(Boot Header)、镜像头表(IMAGE HEADER TABLE)、分区头表(PARTITON HEADER TABLE)。
可见BOOT.BIN中镜像与分区一一对应,构成镜像/分区对,镜像头表给出了镜像文件的名称,分区表给出了镜像文件的存放位置、大小以及加载位置。
BOOT.BIN可以有多个镜像/分区对,利用bootinfo工具可以解析BOOT.BIN的结构参数。将bootinfo.exe和BOOT.BIN两个文件复制到合适的目录,在命令提示符中输入bootinfo BOOT.BIN,就可以看到BOOT.BIN的结构参数:
其中的分区头表展示了镜像文件的关键信息,摘录如下:
PARTITION HEADER TABLE (fsbl.elf)
--------------------------------------------------------------------------------
encrypted_length (0x00) : 0x00006002 unencrypted_length (0x04) : 0x00006002
total_length (0x08) : 0x00006002 load_addr (0x0c) : 0x00000000
exec_addr (0x10) : 0x00000000 partition_offset (0x14) : 0x000005c0
attributes (0x18) : 0x00000010 section_count (0x1C) : 0x00000001
checksum_offset (0x20) : 0x00000000 iht_offset (0x24) : 0x00000240
ac_offset (0x28) : 0x00000000 checksum (0x3c) : 0xfffed7e8
attribute list -
trustzone [non-secure] el [el-0]
exec_state [aarch-32] dest_device [none]
encryption [no] core [none]
--------------------------------------------------------------------------------
PARTITION HEADER TABLE (system_top.bit)
--------------------------------------------------------------------------------
encrypted_length (0x00) : 0x000f6ec0 unencrypted_length (0x04) : 0x000f6ec0
total_length (0x08) : 0x000f6ec0 load_addr (0x0c) : 0x00000000
exec_addr (0x10) : 0x00000000 partition_offset (0x14) : 0x000065d0
attributes (0x18) : 0x00000020 section_count (0x1C) : 0x00000001
checksum_offset (0x20) : 0x00000000 iht_offset (0x24) : 0x00000250
ac_offset (0x28) : 0x00000000 checksum (0x3c) : 0xffd14b7e
attribute list -
trustzone [non-secure] el [el-0]
exec_state [el-0] dest_device [none]
encryption [no] core [none]
--------------------------------------------------------------------------------
PARTITION HEADER TABLE (u-boot.elf)
--------------------------------------------------------------------------------
encrypted_length (0x00) : 0x0001fbc8 unencrypted_length (0x04) : 0x0001fbc8
total_length (0x08) : 0x0001fbc8 load_addr (0x0c) : 0x04000000
exec_addr (0x10) : 0x04000000 partition_offset (0x14) : 0x000fd490
attributes (0x18) : 0x00000010 section_count (0x1C) : 0x00000001
checksum_offset (0x20) : 0x00000000 iht_offset (0x24) : 0x00000260
ac_offset (0x28) : 0x00000000 checksum (0x3c) : 0xf7ea35a6
attribute list -
trustzone [non-secure] el [el-0]
exec_state [aarch-32] dest_device [none]
encryption [no] core [none]
--------------------------------------------------------------------------------
这其中,total_length(代码长度)、load_addr(加载地址)、exec_addr(执行地址)和partition_offset(代码偏移)几个字段的内容最为重要。
我们可以从分区头表中提取分区文件如下表的关键参数,并作出BOOT.BIN文件的结构表(注意代码偏移和长度在分区头表中都是以字为单位,转换为字节数要乘以4)
| 镜像文件 | 代码偏移 | 代码长度(Bytes) | 加载地址 | 执行地址 |
|---|---|---|---|---|
| fsbl.elf | 5888 (4*0x5c0) | 98312 (4*0x6002) | 0x0000000 | 0x0000000 |
| system_top.bit | 104256 | 4045568 | 0x0000000 | 0x0000000 |
| u-boot.elf | 4149824 | 531728 | 0x4000000 | 0x4000000 |
因此,boot.bin中包含了3大功能模块,2.5节将详细介绍这三大功能模块的具体作用,这里只简单描述一下名称:
- FSBL.elf的FSBL是第一阶段引导加载器的缩写;
- system_top.bit 是FPGA的比特流;
- u-boot.elf 的u-boot 是一个通用的引导加载器;
最后,我们得到如下的BOOT.BIN文件的结构图:
2.4.2 uEnv.txt
uEnv.txt是个文本文件,存放有u-boot使用的环境变量,具体内容如下:
uenvcmd=run adi_sdboot
adi_sdboot=echo Copying Linux from SD to RAM... && fatload mmc 0 0x3000000 ${kernel_image} && fatload mmc 0 0x2A00000 ${devicetree_image} && if fatload mmc 0 0x2000000 ${ramdisk_image}; then bootm 0x3000000 0x2000000 0x2A00000; else bootm 0x3000000 - 0x2A00000; fi
bootargs=console=ttyPS0,115200 root=/dev/mmcblk0p2 rw earlycon rootfstype=ext4 rootwait cpuidle.off=1
2.4.3 devicetree.dtb
devicetree.dtb是一个二进制文件,是系统设备树信息的编译结果。设备树指的是嵌入式Linux系统所支持的硬件设备,相当于R210的硬件信息。
devicetree.dtb由u-boot加载。
2.4.4 uImage
uImage是一个二进制文件,包括一个64字节长度的头以及一个压缩的嵌入式Linux内核镜像。
文件头中包含操作系统类型、CPU 架构、镜像类型、压缩类型和镜像名称等字段内容。
内核镜像包含了各种硬件设备的驱动,同样由u-boot加载。
2.5 基于分区的启动过程
2.5.1 启动过程图
ZYNQ芯片的嵌入式Linux系统启动过程如下图所示,分为ROM启动→FSBL→U-Boot→内核运行等四个阶段,以下就按照启动过程的顺序,详细介绍每一阶段的运行机制。
2.5.2 ROM启动阶段
ROM启动阶段是启动的第一步,所谓的ROM指的是固化在ZYNQ芯片内部的不可修改代码,负责最基础的硬件初始化和加载FSBL(第一阶段引导加载器),以下用BootROM来描述。
BootROM完成以下功能:
- 检测启动模式(SD启动):上电后,BootROM根据拨码开关(对应MIO3-5引脚)配置启动模式(如QSPI、SD、JTAG等模式)。由于我们将拨码开关打到了SD启动,BootROM进入SD启动模式。
- 初始化SD卡设备:BootROM使能SD卡控制器的时钟,复位TF卡,并进行读取卡唯一标识等工作。
- 读取MBR分区表:BootROM加载TF卡第0扇区的MBR(主引导记录)并解析出TF卡BOOT分区的参数。
- 访问文件系统:BootROM根据BOOT分区的参数,读取TF卡BPB(引导参数块),获取根目录信息;BootROM遍历TF卡根目录,搜索BOOT.BIN。
- 读取BOOT.BIN:BootROM找到BOOT.BIN后,读取BOOT.BIN最前面的内容到片上内存OCM(On-Chip Memory)中,最前面的内容必须包含完整的FSBL(第一阶段启动加载器)。
- 校验BOOT.BIN:BootROM在OCM中搜索有效的BOOT.BIN文件头。有效文件头的关键字段包括:
- 0x020:固定值0xAA995566(宽度检测)。
- 0x024:镜像ID 0x584C4E58(ASCII为"XLNX")。
- 解析分区:找到有效文件头后,BootROM解析BOOT.BIN头部信息,得到类似于2.4.1节的表格。
- 搬迁FSBL:根据表格的内容,BootROM将R210的BOOT.BIN自第5888字节起,总长度为98312字节的内容搬迁到OCM的地址0x0000000处。
- 加载FSBL:BootROM将跳转到OCM的地址0x0000000处执行FSBL。
既然提到了ZYNQ的OCM,这里就展开讲一下。
ZYNQ同时使用两种内存,一种是片上内存OCM(On-Chip Memory),共256KB,可用的范围为192kB。OCM的优点是高速、低延迟:CPU可直接访问,无需初始化,上电即可使用,主要用于引导过程。
另外一种是片外的DDR内存,容量最大可达1GB,R210配备了512MB,这是操作系统工作的内存。
这两种内存占用同样的地址空间,其中OCM为低端0x00000000 ~ 0x0003FFFF,DDR是0x00100000以上,也就是1MB以上。
2.5.3 FSBL阶段
BOOTROM结束后,就进入了FSBL阶段。FSBL是用户可修改的启动加载程序,主要完成更复杂的硬件初始化和后续镜像加载,主要功能如下所示:
- 初始化PS端:通过ps7_init()函数配置MIO、PLL、时钟和DDR控制器等。
- 检测启动设备:读取BOOT_MODE寄存器确认启动方式(如SD启动),并初始化对应的外设。
- 解析BOOT.BIN:FSBL进一步解析BOOT.BIN中的分区表(Partition Header Table),获取U-Boot以及FPGA比特流的地址、大小等信息。
- 加载FPGA比特流:将FPGA比特流从BOOT.BIN拷贝到DDR内存中,并通过PCAP接口将FPGA的比特流(.bit文件)配置到PL端。
- 加载U-Boot:将U-Boot镜像从BOOT.BIN拷贝到DDR内存中,并跳转到U-Boot的入口地址。
2.5.4 U-Boot阶段
FSBL结束后,就进入了U-Boot阶段,这里详细一下介绍前面多次遇到过的U-boot。
首先,这个U代表通用的意思。引入U-Boot这个中间件后,可以增强启动的灵活性,这样就可以支持各种芯片架构、各种操作系统以及各种外设的启动了。比如openwrt,就采用了U-Boot;ZYNQ芯片,同样也采用了U-Boot。
具体而言,为了加载Linux操作系统内核,U-Boot需要完成如下工作:
- 外设驱动:初始化串口、网络、文件系统等,为内核启动做准备。
- 环境变量:读取环境变量文件uEnv.txt的内容,覆盖默认环境变量。
- 加载内核:从存储介质(TF卡)加载Linux内核(uImage)和设备树(.dtb)。
- 设置启动参数:启动参数即bootargs,关键参数包括控制台和根文件系统的设置。
- 启动内核:跳转到内核入口,完成启动过程。
U-Boot启动过程与环境变量文件uEnv.txt密切相关,文件中定义了环境变量以及命令脚本。以R210的uEnv.txt为例,以下两行是环境变量的定义:
uenvcmd=run adi_sdboot bootargs=console=ttyPS0,115200 root=/dev/mmcblk0p2 rw earlycon rootfstype=ext4 rootwait cpuidle.off=1
uenvcmd代表U-Boot运行后需要执行的命令脚本;bootargs代表U-Boot加载系统内核时,向系统内核传递的启动参数,其中列出了根系统所在的启动分区以及分区的文件系统。
而adi_sdboot则定义了命令脚本的具体任务,这些任务要通过U-Boot的命令来执行:
adi_sdboot=echo Copying Linux from SD to RAM... && fatload mmc 0 0x3000000 ${kernel_image} && fatload mmc 0 0x2A00000 ${devicetree_image} && if fatload mmc 0 0x2000000 ${ramdisk_image}; then bootm 0x3000000 0x2000000 0x2A00000; else bootm 0x3000000 - 0x2A00000; fi
命令脚本中需执行多个u-Boot命令,里面的&&连结这些命令,代表只有前一个命令执行成功,才执行下一个命令。
其中一个关键的U-Boot命令是fatload,这个命令用于加载FAT文件系统的文件到DDR内存中,因此:
fatload mmc 0 0x3000000 ${kernel_image}就是将mmc设备(TF卡)第0分区上的kernel_image这个内核镜像文件加载到DDR内存地址0x3000000。
kernel_image的缺省文件名为uImage,这是在U-Boot中已经预先设定了,当然我们也可以在uEnv.txt中进行更换,比如加一行:kernel_image=uImage_r210。
由于TF启动卡的第0分区也就是BOOT分区,该分区格式化为FAT32的文件系统,因此只要kernel_image 文件在根目录,fatload就可以顺利读取并加载。
同样地,adi_sdboot中还加载了devicetree_image(缺省文件名为devicetree.dtb)到DDR内存地址0x2A00000。
加载完内核镜像以及设备树之后,就可以用bootm命令启动内核了。
bootm的命令有三个参数,分别是内核镜像地址、ramdisk镜像地址和设备树地址。因此,脚本最后还做了一个逻辑判断,如果成功加载了ramdisk_image(缺省文件名为uramdisk.image.gz),就用bootm的三个参数启动内核;如果没有加载成功,就用bootm的两个参数启动内核。
由于基于分区的SD启动没有uramdisk.image.gz文件,因此R210启动时只需要加载uImage和devicetree.dtb两个文件,启动内核的命令是:bootm 0x3000000 - 0x2A00000。
总结一下,在bootm执行前,DDR内存中程序的分布如下图所示:
注意R210的DDR内存为512MB,对应的地址空间为0x00100000~0x1FFFFFFF。
2.5.5 内核运行阶段
内核运行也分为多个阶段进行,包括内核解压与启动、挂载根文件系统和运行系统服务。
2.5.5.1 内核解压与启动
- U-Boot将控制权转交给内核uImage
- uImage解压自身
- 初始化基本硬件:CPU、内存、中断控制器等
- 加载解析设备树
- 解析并展开设备树
- 识别PS端设备和PL端IP核
- 根据设备树注册硬件设备
2.5.5.2 挂载根文件系统
- 内核根据启动参数bootargs中的root=参数挂载文件系统。R210是/dev/mmcblk0p2,也就是TF卡的第2个分区。
- 挂载后执行根文件系统中的/sbin/init脚本。R210的init脚本链接到了/lib/systemd/systemd,也就是运行systemd。
- systemd启动系统服务。
2.5.5.3 运行系统服务
- systemd从/etc/systemd/system.conf获取systemd的配置
- systemd从/usr/lib/systemd/system/获取系统默认服务
这样,嵌入式Linux系统的启动过程就结束了。
最后,我们可以输入lsb_release -a命令,查看发行版信息:
root@analog:/ # lsb_release -a
No LSB modules are available.
Distributor ID:Raspbian
Description:Kuiper GNU/Linux 11.2 (bullseye)
Release:11.2
Codename:bullseye
说明R210的嵌入式Linux系统还是版本比较新的,相当于Ubuntu20.04的版本。
2.6 系统配置
嵌入式Linux系统第一次启动后,通常我们还需要进行系统配置,包括设置IP地址、主机名以及登录的欢迎词等工作。
Linux通过修改系统文件的方式来配置系统,我们用vi来修改文件内容。
2.6.1 设置IP
- 查看以太网卡名称
- 修改interfaces文件
- 修改DNS服务器地址
输入ifconfig命令,可以查看以太网卡名称。
图中,以太网卡的名称为eth0。
输入命令 vi /etc/network/interfaces,按insert键进行修改。修改后的内容如下图所示。修改完毕后,按esc键退出编辑模式,输入:wq保存并退出编辑器。
DNS服务器地址由/etc目录下的resolv.conf文件存储,但这个文件在重启后会恢复默认设置,且该文件无法用下文提到的保护手段防止覆盖。为保证DNS配置永久生效,我们可以采用下面的方法:
输入命令rm /etc/resolv.conf删除原有的resolv.conf文件。
输入命令 vi /etc/resolv.conf,按insert键进行修改。修改后的内容如下图所示。修改完毕后,按esc键退出编辑模式,输入:wq保存并退出编辑器。
输入命令chattr +i /etc/resolv.conf,锁定/etc/resolv.conf防止意外修改。
现在重启后,DNS服务器配置仍能生效,下图可见重启后能ping通外网。
2.6.2 设置主机名
输入命令:hostnamectl set-hostname r210,这里的r210为新的主机名。
修改/etc/hosts文件,使其配置与新的主机名一致。输入命令 vi /etc/hosts,按insert键进行修改。修改后的内容如下图所示。修改完毕后,按esc键退出编辑模式,输入:wq保存并退出编辑器。
重启后修改生效。可通过命令hostnamectl查看主机名。
2.6.3 设置欢迎词
登录的欢迎词由/etc/motd文件来配置。输入命令 vi /etc/motd,按insert键进行修改。修改后的内容如下图所示。修改完毕后,按esc键退出编辑模式,输入:wq保存并退出编辑器。
重启后可见欢迎词。
2.7 小结
本章详细介绍了ZYNQ芯片嵌入式Linux系统基于分区的SD启动过程,详细分析了启动相关文件的功能,并重点解析了BOOT.BIN文件的结构。
本章还详细讲解了使用树莓派的镜像烧写工具制作TF启动卡的过程,并介绍了用终端工具xshell访问嵌入式Linux系统的方法。
本章最后介绍了使用vi更改系统文件,进行系统配置的方法。
【本章Linux命令】
| 命令名称 | 说明 |
|---|---|
| cd | 改变目录 |
| ls -l | 查看目录文件详细列表 |
| mount | 挂载分区 |
| vi | 文本文件编辑器 |
| df -h | 查看文件系统磁盘空间使用情况 |
| lsblk | 查看块设备信息 |
| lsb_release -a | 查看系统发行版信息 |
第3章 深度研究基于Ramdisk的SD启动
3.1 概述
学会了采用基于分区的TF卡来启动R210后,接下来我们介绍利用Ramdisk系统来启动R210。本章首先介绍了基于Ramdisk系统的启动过程,然后重点讲解了如何定制Ramdisk系统。定制过程在虚拟机的Linux系统上进行,还需要使用大量的Linux命令。
【windows工具】
虚拟机软件Virtualbox、FTP工具winscp
3.2 Ramdisk系统的特点
用过基于分区的TF启动卡后,我们会发现这种启动方式有如下几个特点:
一个是优点:操作系统比较完整,功能全面,甚至还安装了编译器,还可以直接编译源代码,这与树莓派的操作系统很类似。
剩下的就是缺点了:
一个缺点是启动比较慢,这是由于系统加载了很多服务。如果用于人机交互,有windows做借鉴,大家也无所谓。但如果是单一用途的设备,比如电视机、智能音箱、网关之类的设备,或者像软件无线电SDR的应用,启动慢就很难接受了。
另外一个缺点是文件系统不稳定,经常会报错。这与操作系统在TF卡上工作有很大的关系,也与操作系统的版本有很大的关系。
为了解决这两个问题,我们可以采用Ramdisk方式启动系统。所谓Ramdisk,就是用内存模拟磁盘,这样就能大幅提升系统的读写性能。
之前我们操作系统采用的是磁盘Disk系统,也就是操作系统安装在磁盘的分区上,绝大多数人的PC机也是这样使用的,也就是将windows系统或者Linux系统安装在系统分区上。
除了将分区用于系统外,我们还可以采用文件充当操作系统,也就是文件即系统的概念,大大简化了系统的安装与备份工作。
文件即系统中有大家熟悉的虚拟机系统、VHD虚拟磁盘系统,也有本章介绍的内存磁盘系统Ramdisk。
这三者都有一个共同的特点,就是用一个文件作为一套操作系统,文件存放在磁盘上。
这三者也有明显的差别:
虚拟机系统和VHD虚拟磁盘系统可以看成是一个大箱子,里面存放了操作系统的各种文件,但不进行压缩,操作系统运行时直接针对磁盘上的文件内容进行操作。
Ramdisk系统可以看成是个压缩袋,将操作系统的各种文件压缩在一起,打包成一个镜像image。每次开机时,镜像里面的文件被释放到内存中。Ramdisk系统把内存当作磁盘使用,操作系统运行时直接针对内存操作。
由于这样的特点,虚拟机系统、VHD系统一般体积比较大,都要在10GB以上,操作系统的功能完整而全面;Ramdisk系统一般体积在100MB以下,操作系统的功能比较精简。
对于嵌入式Linux系统,人机交互简单,处理的任务明确且简单,响应速度要快,这种情况下,Ramdisk系统除了消耗内存和无法持久化等少数缺点外,剩下的都是优点了。
特别是采用Ramdisk系统后,安装和维护也变得格外简单,接下来大家就可以体会了。
3.3 安装Ramdisk系统
说了这么多,怎么把我们现有的启动卡升级到Ramdisk系统呢?
很简单,我们只需要将Ramdisk系统的相关启动文件复制到TF卡上。
Ramdisk系统的启动文件包括BOOT.BIN、uEnv.txt、devicetrree.dtb、uImage和uramdisk.image.gz共5个文件,如下图所示:
不难看出,与分区启动相比,Ramdisk系统多了一个uramdisk.image.gz镜像文件,另外BOOT.BIN需要升级,以支持Ramdisk系统;uEnv.txt需要更新,以支持Ramdisk系统以及分区启动。
文件复制和替换完成后,再插卡上电启动,输入用户名root,密码是analog,就进入了Ramdisk系统。启动后的界面如下图所示。
整个安装过程不到1分钟,非常简单、快捷,比之前制作基于分区的TF启动卡要方便很多。
另外,Linux系统启动过程也明显快于基于分区的启动。
接下来,就来解答大家的疑问,Ramdisk系统是如何启动的。
3.4 Ramdisk系统的启动过程
下图是从ZYNQ7000技术手册中摘录的Ramdisk系统的启动过程,从下图不难看出,Ramdisk系统的启动与基于分区的启动过程是一模一样的,只是最后加载根文件系统时,由从分区加载改为从uramdisk.image.gz镜像文件加载。
那么,R210是怎么知道要修改root文件系统的加载方式呢?
关键在uEnv.txt这个文件的内容发生了变化。
从uEnv.txt的内容不难看出,与基于分区启动的uEnv.txt一样,基于Ramdisk系统的uEnv.txt也定义了一个环境变量uenvcmd和一个脚本函数adi_sdboot:
与基于分区的adi_sdboot脚本相比,基于Ramdisk的adi_sdboot脚本在判断语句中增加了bootargs启动参数的设置,这样如果TF卡上有uramdisk.image.gz文件,就会设置基于Ramdisk的启动参数(“root=/dev/ram0 rootfstype=ramfs”)并执行Ramdisk系统启动;如果没有uramdisk.image.gz文件,就会设置基于分区的启动参数(“root= root=/dev/mmcblk0p2 rootfstype= ext4”)并执行基于分区的启动。
if fatload mmc 0 0x2000000 ${ramdisk_image}; then setenv bootargs console=ttyPS0,115200 root=/dev/ram0 rootfstype=ramfs rw earlycon rootwait bootm 0x3000000 0x2000000 0x2A00000; else setenv bootargs "console=ttyPS0,115200 root=/dev/mmcblk0p2 rw earlycon rootfstype=ext4 rootwait bootm 0x3000000 - 0x2A00000; fi>
另外一方面, uboot的bootm指令支持从Ramdisk文件中加载root文件系统并启动,从而实现了Ramdisk镜像文件的加载。
当然,也不是所有版本的uboot都能支持启动Ramdisk系统,这也是为什么要替换掉原厂提供的BOOT.BIN的原因。
根据bootm指令,我们不难做出Ramdisk镜像文件在DDR内存中的加载位置,如下图所示。
加载了uramdisk.image.gz镜像文件后,R210根据启动参数bootargs的定义,首先会对镜像文件进行解压和解包,解包后得到的根系统释放到DDR内存中,形成了Ramdisk系统。
接下来,R210调用Ramdisk系统中的初始化脚本init,进行系统的初始化。
之后,R210查找/etc/inittab并执行,完成基础文件系统挂载、设备节点初始化和执行系统服务脚本。
这样,启动过程就完成了。
3.5 定制Ramdisk系统
前面也讲了,Ramdisk系统的缺点是无法持久化,这是由于启动的时候,系统是从镜像文件uramdisk.image.gz解压而来的,因此每次启动的系统都相同,这也就意味着系统运行时对系统的更改无法保存。
这很容易理解,要想保存更改,每次关机时就必须保存系统的内容并进行打包和压缩,重新生成镜像文件,势必消耗消耗大量的时间。用户不能忍受开机时间过长,同样也不能忍受关机时间过长。
但是,如果不能保存系统的更改,如果每次启动后都需要去重新配置系统,也是很麻烦的。
为此,我们可以在Ramdisk镜像中预先设置好相关的配置,比如IP地址、主机名等,生成一个定制后的Ramdisk镜像文件,这样每次开机后就不用再调整配置,节省了大量的时间。
3.5.1 工作环境
为了保证配置的正确性,我们需要在Linux系统上定制Ramdisk镜像文件。考虑到我们的工作电脑是Windows系统,因此简单的办法是安装VirtualBox软件,使用VirtualBox软件生成Ubuntu虚拟机,在Ubuntu虚拟机上定制Ramdisk镜像文件。
关于VirtualBox软件的安装,已经有大量的参考资料,这里就不赘述了。
我们需要生成的Ubuntu虚拟机,版本为18.04。之所以不用最新的版本,是因为我们的任务比较简单,没有必要使用更高的版本。
Ubuntu虚拟机需要安装openssh以及ftp,这样才能作为我们定制镜像的环境。
完成Ubuntu18.04安装工作,并能通过xshell访问Ubuntu虚拟机后,接下来我们需要安装FTP工具WinSCP。
下图是WinSCP的界面,镜像文件传入用户home目录下的Downloads目录。
接下来我们通过xshell连接虚拟机,在命令行中操作Ramdisk镜像文件。
定制的过程分为如下几个步骤:去头并解压、解包并设置权限、修改镜像、重新打包、重新压缩和重新加头。
3.5.2 去头并解压
镜像文件uramdisk.image.gz有一个64字节的头,供U-Boot加载时使用。不去掉这个头,镜像文件就无法解压。因此我们需要先通过dd命令去掉这64字节的头,以得到标准的gz压缩文件,具体操作步骤参考下面的命令:
sdr@Vbox:~/$ cd Downloads sdr@Vbox:~/Downloads$ dd if=uramdisk.image.gz of=ramdisk.image.gz bs=64 skip=1 sdr@Vbox:~/Downloads$ ls -l *.gz -rw-rw-r-- 1 sdr sdr 3303042 8月 13 16:54 uramdisk.image.gz -rw-rw-r-- 1 sdr sdr 3302978 8月 13 22:24 ramdisk.image.gz
不难看出ramdisk.image.gz比uramdisk.image.gz少了64个字节。
得到标准的gz压缩文件ramdisk.image.gz后,我们可以用gunzip命令解压ramdisk.image.gz:
sdr@Vbox:~/Downloads$ gunzip ramdisk.image.gz sdr@Vbox:~/Downloads$ ls -l *.image -rw-rw-r-- 1 sdr sdr 6860288 8月 13 22:24 ramdisk.image
解压后得到ramdisk.image,这就是打包的镜像文件ramdisk.image
3.5.3 解包并设置权限
接下来,我们创建工作目录~/work/zynq/ramfs,将ramdisk.image解包到工作目录。
sdr@Vbox:~/$ cd .. sdr@Vbox:~/$ mkdir -p work/zynq/ramfs sdr@Vbox:~/$ cd work/zynq/ramfs
由于镜像采用CPIO格式打包,因此解包采用cpio命令:
sdr@Vbox:~/work/zynq/ramfs$ cpio -idv < ~/Downloads/ramdisk.image
cpio命令选项说明:
-i:解包模式
-d:自动创建子目录
-v:显示详细信息
< ../ramdisk.image:从上级目录读取归档文件
解包后,还需要修改文件的权限,全部置为root用户。如果不修改权限,重置的镜像文件启动将会报错。
sdr@Vbox:~/ work/zynq/ramfs$ cd .. sdr@Vbox:~/work/zynq/ $ sudo chown -R root:root ramfs
3.5.4 修改镜像
3.5.4.1 修改IP地址
镜像文件uramdisk.image.gz设置的IP地址是192.168.1.189,我们可以修改这个IP地址以适应运行环境。
假定我们需要把IP地址修改为192.168.1.186,具体做法是修改工作目录下etc/network下的interfaces文件。
我们采用系统自带的文本编辑工具nano修改文件,比起vi更简单,命令如下:
sdr@Vbox:~/work/zynq/ramfs/etc/network$ sudo nano interfaces
可以看到如下的nano界面:
在nano界面中,我们找到192.168.1.189,将其改为192.168.1.186。修改后直接输入Ctrl+O,保存修改的结果。
3.5.3.2 设置自动挂接分区
Ramdisk系统使用了TF卡,TF卡的分区可以通过mount命令挂接到系统中,方便进行文件管理。我们可以设置自动挂接分区,就不用每次启动后输入mount命令了。
设置自动挂接分区的具体做法是修改工作目录下etc下的fstab文件,还是采用nano工具,输入如下命令:
sdr@Vbox:~/work/zynq/ramfs/ $ sudo nano etc/fstab
上图是已经设置自动挂接分区的fstab文件,不难看出挂载了TF卡的两个分区。
3.5.3.3 设置启动自动运行脚本
设置启动自动运行脚本的目的是简化系统的配置,避免反复打包镜像文件。
简单地说,就是把自动运行脚本init.sh放到了TF卡的BOOT分区中,方便修改和调整。
设置自动挂接分区的具体做法分两步,先是修改工作目录下etc下的inittab文件,还是采用nano工具,输入如下命令:
sdr@Vbox:~/work/zynq/ramfs/ $ sudo nano etc/inittab
然后在inittab中增加一行,如下图最后一行所示:
接下来修改工作目录下etc下的rc.local文件,还是采用nano工具,输入如下命令:
sdr@Vbox:~/work/zynq/ramfs/ $ sudo nano etc/rc.local
然后在rc.local中增加如下图所示的内容:
5.4.3.4 更新主机名、登录提示
如果我们需要交付产品,那么主机名、登录提示都是需要设置的。
设置主机名的第一步是修改工作目录下etc下的hostname文件,将其改为所需的主机名,如R210,如下图所示:
接下来修改工作目录下etc下的hosts文件,将第二行的内容改成R210,如下图所示:
设置登录提示是修改工作目录下etc下issue文件,将其改为所需要提示的内容:
3.5.5. 重新打包
完成了系统的定制,接下来可以重新打包Ramdisk系统。
打包还是利用cpio这个命令,先回到工作目录,然后运行如下命令:
sdr@Vbox:~/work/zynq/ramfs $ find . | sudo cpio -ov -H newc > ../ramdisk.image
cpio选项说明:
-o:创建归档
-v:显示详细信息
-H newc:指定为 SVR4 格式(兼容 ZYNQ 启动要求)
> ../ramdisk.image:输出到上级目录的新文件
打包过程如下图所示,打包后得到了镜像文件ramdisk.image:
3.5.6. 重新压缩
接下来,我们用压缩命令gzip压缩镜像文件ramdisk.image,参数-9代表使用最大压缩率。
sdr@Vbox:~/work/zynq$ gzip -9 ramdisk.image sdr@Vbox:~/work/zynq$ ls -l -rw-rw-r-- 1 sdr sdr 3312157 8月 14 11:01 ramdisk.image.gz
3.5.7. 重新加头
最后,我们要给镜像压缩文件ramdisk.image.gz加头,这样uboot才能加载镜像压缩文件。
加头用到了专门的mkimage命令,使用这个命令需要先安装u-boot-tools。
sdr@Vbox:~/work/zynq$ sudo apt install u-boot-tools
安装完毕后输入如下的加头命令:
sdr@Vbox:~/work/zynq$ mkimage -A arm -T ramdisk -C gzip -n Ramdisk -d ramdisk.image.gz
输出如下图所示:
3.5.8. 更新Ramdisk系统
得到了定制过uramdisk.image.gz文件后,我们有两种方法部署到TF卡上。
一种是在R210断电后,拔出TF卡再接入Windows系统,用第3.3节讲到的文件复制的方法更新uramdisk.image.gz。
另外一种是R210开机状态,如果已经将TF卡的分区挂接了到了/boot,用FTP工具就可以把uramdisk.image.gz文件上传到/boot,直接更新了。
更新uramdisk.image.gz后,下次重启时就会进入到更新的系统,可以看到修改的效果。
3.6 小结
本章介绍了Ramdisk系统的特点以及安装方法,详细介绍了ZYNQ芯片嵌入式Linux系统基于Ramdisk系统的SD启动过程,详细分析了启动相关文件的功能。
本章重点讲解了Ramdisk系统的定制方法,利用虚拟机更新Ramdisk系统文件,从而实现了系统配置。
【Linux命令】
| 命令名称 | 说明 |
|---|---|
| ip a | 查看IP配置 |
| dd | 数据复制与转换 |
| chown | 改变文件所有者 |
| mkdir | 创建目录 |
| cpio | 创建和提取归档文件 |
| gunzip | 解压文件 |
| gzip | 压缩文件 |
| apt install | 安装软件包 |
第4章 深度研究QSPI启动
4.1 概述
R210作为基于ZYNQ7000系列的SDR板卡,支持SD、QSPI和JTAG等启动方式。前面两章已经介绍了SD启动方式,这一章就来讲解QSPI启动方式。
首先介绍一下QSPI这个术语。QSPI是从SPI发展而来的,SPI(Serial Peripheral Interface,串行外设接口)是摩托罗拉公司在1970年代研发的串行接口技术,特点是用4线(CLK、CS、MOSI和MISO)实现了高达10Mbps的串行高速数据传输,遥遥领先于传统的RS232串行接口,因此在单片机领域得到了广泛的应用。
QSPI是QUAD SPI,四路串行外设接口的缩写。QSPI接口从SPI接口的4线扩展到了6线,数据线从2根扩充到了4根。QSPI采用半双工的模式,单向最高吞吐率可达40Mbps,从而进一步了提升传输速率。
在R210中,QSPI接口用于连接Flash芯片W25Q256,一款容量为32MB的可擦写的非易失存储芯片。
Flash芯片有NOR和NAND两种技术,intel公司1988年开发出了NOR flash技术。NOR的特点是芯片内执行(XIP, eXecute In Place),这样应用程序可以直接在flash 闪存内运行,不必再把代码读到系统RAM中。NOR的传输效率很高,在MB级的小容量时具有很高的成本效益,但是其较低的写入和擦除速度大大影响了它的性能。
相反地,1989年东芝公司发表了NAND flash技术。NAND内部采用非线性宏单元模式,容量大,改写速度快,为固态大容量内存的实现提供了廉价有效的解决方案,手机、平板的GB级别Flash一般都采用NAND技术。
W25Q256这块Flash芯片显然是采用了NOR技术,我们可以在Flash芯片中烧录嵌入式Linux系统,以支持QSPI启动。
本章将围绕Flash,介绍QSPI的启动过程以及定制QSPI启动的方法。
4.2 Flash的结构
Flash芯片类似于硬盘,可以被分区。
在嵌入式linux系统,Flash的分区称为MTD(Memory Technology Device)分区,而MTD是Linux系统用来管理Flash的子系统。
接下来我们来了解R210的Flash芯片的具体分区情况。
我们首先把R210的拨码开关打到QSPI启动,再用Type-C数据线连接R210的UART口与电脑的USB口,R210开始上电启动。
我们登入嵌入式linux系统后,输入如下命令:
可以看到R210有4个MTD分区,还可以看到每个MTD分区的编号、名称以及大小。
我们再输入如下命令,还可以看到R210每个MTD分区的起始地址:
这样,我们就可以做出MTD分区的分布图,如下图所示:
我们还可以输入mtdinfo命令,查看mtd分区的详细信息:
总之,从分区名称可以看到,qspi-fsbl-uboot相当于SD启动的BOOT.BIN,qspi-uboot-env相当于SD启动的uEnv.txt。至于内核以及根文件系统对应另外哪个分区,似乎还很难判断。
接下来,我们读取Flash各个MTD分区,解析其内容,来分析出各个MTD分区的功能。
4.3 导出MTD分区
4.3.1 导出分区
我们使用dd命令导出MTD分区的内容,分4次得到4个MTD分区的内容:
4.3.2 查看内容
首先查看mtd0分区的内容,也就是mtd0.bin的内容。
我们采用xxn与head两个命令的组合,用来以十六进制显示mtd0.bin的内容。不难看出,mtd0.bin与BOOT.BIN的文件头是一致的,等价于BOOT.BIN:
接着查看mtd1.bin的内容,我们还是采用xxn与head两个命令的组合,用来以十六进制显示mtd1.bin的内容。不难看出,mtd1.bin就是环境变量,等价于uEnv.txt。
4.3.3 复制内容
接下来,我们把MTD分区的内容复制到TF卡上,方便进行后续的分析。
首先插入TF卡,然后输入如下命令,将4个分区bin文件复制到TF卡上:
mkdir /boot mount /dev/mmcblk0p1 /boot cd /boot cp /root/*.bin . umount /boot cd / umount /boot
之后,我们在windows系统中读取TF卡,用winscp工具,将这4个文件传入虚拟机中。
4.4 解析分区镜像
得到了各个分区的镜像内容,接下来就进行镜像内容的解析。
4.4.1 mtd0.bin
mtd0.bin与boot.bin一样,也是多个分区镜像的组合,这些分区镜像包括fsbl和uboot,其组成方式与用于SD启动的boot.bin完全一致,也就是利用文件头来定义分区结构。
利用bootinfo工具可以解析mtd0.bin的格式,其输出如下:
Image Header Table:
Partition Count: 2
PARTITION HEADER TABLE (fsbl.elf)
--------------------------------------------------------------------------------
encrypted_length (0x00) : 0x00006002 unencrypted_length (0x04) : 0x00006002
total_length (0x08) : 0x00006002 load_addr (0x0c) : 0x00000000
exec_addr (0x10) : 0x00000000 partition_offset (0x14) : 0x000005c0
attributes (0x18) : 0x00000010 section_count (0x1C) : 0x00000001
checksum_offset (0x20) : 0x00000000 iht_offset (0x24) : 0x00000240
ac_offset (0x28) : 0x00000000 checksum (0x3c) : 0xfffed7e8
attribute list -
trustzone [non-secure] el [el-0]
exec_state [aarch-32] dest_device [PS]
encryption [no] core [none]
--------------------------------------------------------------------------------
PARTITION HEADER TABLE (u-boot.elf)
--------------------------------------------------------------------------------
encrypted_length (0x00) : 0x00019699 unencrypted_length (0x04) : 0x00019699
total_length (0x08) : 0x00019699 load_addr (0x0c) : 0x04000000
exec_addr (0x10) : 0x04000000 partition_offset (0x14) : 0x000065d0
attributes (0x18) : 0x00000010 section_count (0x1C) : 0x00000001
checksum_offset (0x20) : 0x00000000 iht_offset (0x24) : 0x00000250
ac_offset (0x28) : 0x00000000 checksum (0x3c) : 0xf7fad403
attribute list -
trustzone [non-secure] el [el-0]
exec_state [aarch-32] dest_device [PS]
encryption [no] core [none]
--------------------------------------------------------------------------------
从解析的内容可以看到,与SD启动的boot.bin不同, mtd0.bin只有fsbl和uboot,不包含FPGA比特流。
根据解析的结果,可以做出mtd0.bin的结构,如下图所示:
4.4.2 mtd1.bin
mtd1.bin存放4字节的头和uboot使用的环境变量。
4个字节的头固定为0xb56eb45b(小端序存储为 5b b4 6e b5),头之后就是环境变量的内容。
4.4.3 mtd2.bin
在虚拟机中运行file命令,可以查看文件的格式:
file mtd2.bin mtd2.bin: Linux jffs2 filesystem data little endian
从显示的内容不难看出,MTD2分区安装了jffs2系统。
jffs2的全称是Journalling Flash File System Version 2,这是由Red Hat在JFFS基础上开发的第二代日志结构文件系统,专为NOR闪存设备设计。
简单地说,jffs2文件系统相当于把Flash变成了TF卡,实现了文件的存储与读写。
4.4.4 mtd3.bin
最后我们再来看mtd3.bin。mtd3.bin与mtd0.bin类似,也是各种部件打包的组合,这些部件包括kernel、FPGA比特流、设备树以及ramdisk文件系统。
打包采用了FIT(Flattened Image Tree,扁平化镜像树)打包格式,其特点有:
- 多镜像支持:可打包多套内核(kernel)、设备树(fdt)、文件系统(ramdisk)等。
- 设备树语法:使用 .its(Image Source File)文件描述镜像结构。
- 校验与签名:支持哈希校验(SHA)和 RSA 签名,确保固件安全。
- 动态选择配置:可在启动时选择不同的配置(如不同硬件型号)。
查看打包结构前需要先在虚拟机安装uboot-tools,如果已经安装了可以忽略这一步:
sudo apt install uboot-tools
接下来输入dumpimage -l mtd3.bin,就可以看到mtd3.bin对应的FIT的组成:
FIT description: R210 Firmware with FPGA, Kernel, Ramdisk Created: Tue Aug 5 13:50:10 2025 Image 0 (fdt@1) Description: r210 Created: Tue Aug 5 13:50:10 2025 Type: Flat Device Tree Compression: uncompressed Data Size: 22489 Bytes = 21.96 KiB = 0.02 MiB Architecture: ARM Hash algo: sha1 Hash value: ab1edbc6f1790377782192efd80cba8b0e687532 Image 1 (fpga@1) Description: FPGA Bitstream Created: Tue Aug 5 13:50:10 2025 Type: FPGA Image Compression: uncompressed Data Size: 2363432 Bytes = 2308.04 KiB = 2.25 MiB Load Address: 0x0f000000 Hash algo: sha1 Hash value: b4e566833796ab1a6466f373e79715654ef4baaf Image 2 (kernel@1) Description: Linux Kernel Created: Tue Aug 5 13:50:10 2025 Type: Kernel Image Compression: uncompressed Data Size: 4783832 Bytes = 4671.71 KiB = 4.56 MiB Architecture: ARM OS: Linux Load Address: 0x00008000 Entry Point: 0x00008000 Hash algo: sha1 Hash value: 2639b862ee70c89a1e94ab330d4540d4f4ccd9ed Image 3 (ramdisk@1) Description: RootFS Created: Tue Aug 5 13:50:10 2025 Type: RAMDisk Image Compression: gzip compressed Data Size: 5429282 Bytes = 5302.03 KiB = 5.18 MiB Architecture: ARM OS: Linux Load Address: unavailable Entry Point: unavailable Hash algo: sha1 Hash value: 8b7e1746af5bc80a438ecc0f08bb1bd1ccc5840f Default Configuration: 'config@1' Configuration 0 (config@1) Description: R210 Hardware Kernel: kernel@1 Init Ramdisk: ramdisk@1 FDT: fdt@1 FPGA: fpga@1
不难看出这是个简化版本的FIT镜像,只有一套配置,里面的部件包括kernel、FPGA比特流、设备树以及ramdisk文件系统。
我们还可以根据FIT的信息,用dumpimage命令提取出FIT各个部件:
dumpimage -T flat_dt -p 0 -o devicetree.dtb ./mtd3.bin dumpimage -T flat_dt -p 1 -o fpga.bit ./mtd3.bin dumpimage -T flat_dt -p 2 -o zImage ./mtd3.bin dumpimage -T flat_dt -p 3 -o ramdisk.image.gz ./mtd3.bin
其中,devicetree.dtb为设备树,fpga.bit为FPGA比特流,zImage为Linux内核,ramdisk.image.gz为ramdisk文件系统。
值得注意的是:
- zImage与uImage类似,少了64字节的头。
- ramdisk.image.gz与uramdisk.image.gz类似,少了64字节的头。
4.5 QSPI启动过程
讲解了Flash的结构,接下来我们来了解QSPI的启动过程。QSPI的启动过程如下图所示,不难看出与SD启动的过程是一致的,也是分为ROM启动→FSBL→U-Boot→内核运行等四个阶段:
4.5.1 BootROM阶段
BootROM是固化在ZYNQ芯片内部的不可修改代码,负责最基础的硬件初始化和FSBL加载:
- 硬件初始化:上电后,BootROM根据MIO3-5引脚配置的启动模式(如QSPI、SD卡等)初始化对应的外设(如QSPI控制器)。
- 搜索BOOT.BIN:在QSPI Flash的0x00到0x1000000地址范围内,以0x8000为间隔搜索有效的BOOT.BIN文件头。头部的关键字段包括:
- 0x020:固定值0xAA995566(宽度检测)。
- 0x024:镜像ID 0x584C4E58(ASCII为"XLNX")。
- 0x030:FSBL代码在FLASH中的偏移地址。
- 0x034:FSBL代码的长度。
- 0x038:FSBL加载到片内SRAM(OCM)的目标地址(通常为0x0)。
- 0x03C:FSBL加载到片内SRAM(OCM)的执行地址(通常为0x0)。
- 加载FSBL:解析头部后,BootROM将FSBL代码从QSPI拷贝到OCM的目标地址,并跳转到FSBL的入口地址执行
以下是R210Flash中的mtd0.bin的头部内容
xxd mtd0.bin | head 00000000: feff ffea feff ffea feff ffea feff ffea ................ 00000010: feff ffea feff ffea feff ffea feff ffea ................ 00000020: 6655 99aa 584e 4c58 0000 0000 0000 0101 fU..XNLX........ 00000030: 0017 0000 0880 0100 0000 0000 0000 0000 ................
可见BOOTROM中与加载fsbl相关字段的内容如下:
| 字段 | 代码偏移地址 | 代码长度 | 加载OCM地址 | 执行地址 |
|---|---|---|---|---|
| 字节数 | 0x1700(5888) | 0x18008(98312) | 0 | 0 |
4.5.2 FSBL阶段
FSBL是用户可修改的启动加载程序,主要完成更复杂的硬件初始化和后续镜像加载:
- 初始化PS端:通过ps7_init()函数配置MIO、PLL、时钟和DDR控制器等。
- 检测启动设备:读取BOOT_MODE寄存器确认启动方式(如QSPI启动),并初始化对应的外设。
- 解析BOOT.BIN:FSBL进一步解析BOOT.BIN中的分区表(Partition Header Table),获取U-Boot以及FPGA比特流的地址、大小等信息。
- 加载U-Boot:将U-Boot镜像从QSPI拷贝到DDR内存中,并跳转到U-Boot的入口地址。
fsbl通常还包括加载FPGA比特流的工作,也就是通过PCAP接口将FPGA的比特流(.bit文件)配置到PL端。但是R210的BOOT.BIN中并没有包含FPGA的比特流,这项工作挪到后续阶段进行。
fsbl根据BOOT.BIN的镜像表,找到u-boot.elf所在的分区,得到偏移量为0x330。
000008c0: 0000 0201 0200 0000 2003 0000 4002 0000 ........ ...@... 000008d0: 0000 0000 ffff ffff ffff ffff ffff ffff ................ 000008e0: ffff ffff ffff ffff ffff ffff ffff ffff ................ 000008f0: ffff ffff ffff ffff ffff ffff ffff ffff ................ 00000900: 5002 0000 2003 0000 0000 0000 0100 0000 P... ........... 00000910: 6c62 7366 666c 652e 0000 0000 0000 0000 lbsffle......... 00000920: ffff ffff ffff ffff ffff ffff ffff ffff ................ 00000930: ffff ffff ffff ffff ffff ffff ffff ffff ................ 00000940: 0000 0000 3003 0000 0000 0000 0100 0000 ....0........... 00000950: 6f62 2d75 652e 746f 0000 666c 0000 0000 ob-ue.to..fl....
根据偏移量,提取出U-Boot分区表的信息:
00000cc0: b195 0100 b195 0100 b195 0100 0000 0004 ................ 00000cd0: 0000 0004 d065 0000 1000 0000 0100 0000 .....e.......... 00000ce0: 0000 0000 5002 0000 0000 0000 0000 0000 ....P........... 00000cf0: 0000 0000 0000 0000 0000 0000 bbd6 faf7 ................
可见fsbl加载U-Boot的关键字段的内容如下:
| 字段 | 代码偏移 | 代码长度 | 加载DDR地址 | 执行地址 |
|---|---|---|---|---|
| 数值 | 104356(4*0x65d0) | 415428(4*0x195b1) | 0x4000000 | 0x4000000 |
4.5.3 U-Boot阶段
U-Boot作为第二阶段的Bootloader,负责加载操作系统内核:
- 外设驱动:初始化串口、网络、文件系统等,为内核启动做准备。
- 环境变量:读取环境变量,获取启动参数(如bootargs),确定内核加载方式。
- 加载内核:从存储介质加载Linux内核(zImage)、设备树(.dtb)、FPGA的比特流和根文件系统。
- 启动内核:跳转到内核入口,完成启动过程。
U-Boot启动过程与环境变量uenv密切相关,环境变量包括变量以及命令脚本两种类型。
在R210中,QSPI的启动过程执行如下工作:
初始化SPI接口:
下面这个命令的作用是初始化QSPI Flash控制器,准备进行后续的Flash操作。Flash控制器是由SPI接口连接的,所以需要先初始化SPI接口
sf probe 0:0 50000000 0
- sf: SPI Flash命令前缀
- probe: 探测并初始化SPI Flash设备
- 0:0: 第一个参数表示SPI控制器编号(0),第二个参数表示片选(CS)线编号(0)
- 50000000: SPI时钟频率(50MHz)
- 0: SPI模式(0表示模式0,即CPOL=0, CPHA=0)
加载Flash的内容:
下面这个命令的作用是从QSPI Flash的2MB偏移处读取一个FIT镜像到DDR内存中。参考4.2节的内容,2MB的地址正好是MTD3分区(qspi-linux)的起始位置。
sf read ${fit_load_address} 0x200000 ${fit_size}
- sf read: 从QSPI Flash读取数据到DDR内存
- ${fit_load_address}: 目标内存地址(从环境变量获取)
- 0x200000: QSPI Flash中的源偏移地址(2MB处)
- ${fit_size}: 要读取的数据大小(从环境变量获取)
设置内核启动参数:
内核启动参数启动参数如下:
console=ttyPS0,115200 maxcpus=${maxcpus} rootfstype=ramfs root=/dev/ram0 rw quiet loglevel=4 clk_ignore_unused uboot="${uboot-version}"
从内核启动参数不难看出QSPI启动采用Ramdisk的系统。
启动内核:
bootm ${fit_load_address}#${fit_config}
通过从环境变量获取fit_load_address和fit_config,其中fit_config=config@1,fit_load_address=0x2080000
4.5.4 内核运行阶段
这一段的运行与基于Ramdisk的SD启动的内核运行阶段是一样的,这里就不赘述了。
4.6 定制系统参数
与之前两章类似,我们可以通过修改镜像文件的内容来定制系统参数,这就涉及到修改MTD3的内容。
4.6.1 解包Flash
首先我们用4.3.1节的方法导出MTD3的内容,用4.4.4节的方法提取出ramdisk.image.gz文件。
接下来用前一章的方法将ramdisk.image.gz文件并解包到工作目录。
修改工作目录中的系统配置。
4.6.2 打包根系统
修改完成后,还是采用上一章的方法将工作目录重新打包压缩成ramdisk.image.gz。
4.6.3 创建FIT配置
在打包FIT镜像前,需要创建配置文件r210.its,完整内容如下:
/dts-v1/;
/ {
description = "R210 Firmware with FPGA, Kernel, Ramdisk";
#address-cells = <1>;
images {
fdt@1 {
description = "R210 Device Tree";
data = /incbin/("./devicetree.dtb");
type = "flat_dt";
arch = "arm";
compression = "none";
hash@1 {
algo = "sha1";
};
};
fpga@1 {
description = "FPGA Bitstream";
data = /incbin/("./fpga.bit");
type = "fpga";
arch = "arm";
compression = "none";
load = <0x0f000000>;
hash@1 {
algo = "sha1";
};
};
kernel@1 {
description = "Linux Kernel";
data = /incbin/("./zImage");
type = "kernel";
arch = "arm";
os = "linux";
compression = "none";
load = <0x00008000>;
entry = <0x00008000>;
hash@1 {
algo = "sha1";
};
};
ramdisk@1 {
description = "RootFS";
data = /incbin/("./ramdisk.image.gz");
type = "ramdisk";
arch = "arm";
os = "linux";
compression = "gzip";
hash@1 {
algo = "sha1";
};
};
};
configurations {
default = "config@1";
config@1 {
description = "R210 Hardware";
kernel = "kernel@1";
fdt = "fdt@1";
ramdisk = "ramdisk@1";
fpga = "fpga@1";
};
};
};
4.6.4 打包FIT镜像
如果FIT镜像的部件都准备好了,打包是非常快的,只需要输入如下命令:
mkimage -f r210.its r210fit.bin
将打包后得到的r210fit.bin利用winscp工具拷到TF卡的根目录下。
4.6.5 烧录FIT镜像
r210启动后,输入如下命令:
cd /boot flashcp -v r210fit.bin /dev/mtd3
就实现了烧录镜像的工作。
4.7 文件的持久化
QSPI启动方式的最大优点是摆脱了TF卡,简化了系统硬件设备,系统也变得更可靠了,特别适合嵌入式设备。
但是“成也萧何,败也萧何”,没有了TF卡,一旦掉电,运行数据就会丢失,不方便进行分析和维护。
为此,可以采用前面介绍的jffs2文件系统,利用jffs2文件系统的非易失性,实现关键文件的持久化保存。
在我们的系统中,jffs2文件系统挂接到了/mnt/jffs2目录。不过由于NOR芯片擦写次数以及分区大小的限制,建议只用于保存最关键的动态配置以及最新的应用程序,而不是用于保存日志文件。
4.8 小结
本章介绍了Flash芯片的数据结构,详细介绍了ZYNQ芯片嵌入式Linux系统的QSPI启动过程,详细分析了各个分区镜像的结构。
本章重点讲解了Flash芯片的定制方法,通过打包FIT镜像并进行烧录,从而实现了QSPI启动。
【Linux命令】
| 命令名称 | 说明 |
|---|---|
| xxd | 查看文件二进制内容 |
| head | 查看文件前部内容 |
| cat | 查看文件内容 |
| dmesg | 查看系统启动日志 |
| mtdinfo | 查看MTD分区信息 |
| umount | 取消分区挂载 |
| cp | 复制文件 |
| dumpimage | 导出镜像内容 |
第5章 用FIT镜像实现统一启动
5.1 概述
经过前面三章的学习和实操,相信大家对ZYNQ7000开发板的各种启动方式已经很熟悉了,并且也能实现自定义启动效果了。
接下来我们就对前面的内容进行一次大综合,同时也是对前面所学内容的一次小结。
综合的主角就是FIT镜像,所谓FIT,就是扁平化镜像树,类似于linux的tar命令,把kernel、FPGA比特流、设备树以及ramdisk文件系统打包到一起。
我们需要做的是利用TF卡上的FIT镜像启动Ramdisk系统,这样TF卡上的启动文件数量大幅减少,只有boot.bin、uEnv.txt以及r210fit.bin三大文件。
另外,由于QSPI启动方式使用的也是FIT镜像,两种方式的FIT镜像可以实现统一,使用同一个r210fit.bin镜像文件,这样就可以在烧录Flash前充分验证FIT镜像的启动效果,避免反复烧录的麻烦。
5.2 FIT镜像的SD启动
要想实现FIT镜像的SD启动,需要做两件事:一件是制作FIT镜像,另外一件是修改uEnv.txt。
制作FIT镜像的过程就是4.6节中介绍的打包FIT镜像过程,最后生成r210fit.bin文件。
接下来,我们需要修改第三章用到的uEnv.txt,将其内容替换为:
bootcmd=run fitboot
fit_load_address=0x2080000
fit_config=config@1
fit_image= r210fit.bin
fitboot=echo Copying fit image from SD to RAM... && fatload mmc 0 ${fit_load_address} ${fit_image} && setenv bootargs "console=ttyPS0,115200 maxcpus=2 rootfstype=ramfs root=/dev/ram0 rw earlyprintk " && bootm ${fit_load_address}#${fit_config}
替换后的uEnv.txt首先定义了三个参数fit_load_address 、fit_config和fit_image,用来指定FIT镜像加载到DDR内存的地址、FIT镜像选用的配置以及FIT镜像的文件名。
接下来定义了启动脚本fitboot,主要完成三项工作:
1. 将TF卡上的FIT镜像加载到DDR内存中。
fatload mmc 0 ${fit_load_address} ${fit_image}
2. 设置启动参数bootargs,传递给内核。
setenv bootargs "console=ttyPS0,115200 maxcpus=2 rootfstype=ramfs root=/dev/ram0 rw earlyprintk "
3. 用内存中的FIT镜像的指定配置启动,对应kernel、FPGA比特流、设备树以及ramdisk文件系统的组合。
bootm ${fit_load_address}#${fit_config}
最后,把r210fit.bin和uEnv.txt复制到TF的boot分区,将r210的拨码开关打到SD启动,上电后就可以实现FIT镜像的启动了。
5.3 FIT镜像的启动过程
FIT镜像的启动过程与第三章介绍的基于RAMdisk的启动过程基本相同,这里就不赘述了。
需要强调一点,尽管SD启动与QSPI启动使用了同一个FIT镜像,但是启动过程还是有差别的,突出表现在对Flash设备的处理上:
SD启动时只需要初始化Flash设备的驱动器,也就是SPI的控制器;QSPI启动不光要初始化Flash设备的驱动器,还要使用Flash设备的驱动器读取Flash的内容。虽然看起来差别不大,但是会带来不同的效果。
5.4 小结
本章介绍了利用TF卡上FIT镜像文件来启动嵌入式Linux系统的方法。