一、基础概念

1.1 系统移植

系统

操作系统(本质一个程序一堆代码;管理对应的资源和任务

移植

将源代码(Linux操作系统原码)从一种环境(开发环境)放在另一种环境(产品环境)下也能运行叫做移植

内核裁剪

make menuconfig;选择对应的功能

linux文件系统的瘦身

给动态库瘦身:删静态库加上调试信息

1.2 启动流程

pc机

1、BIOS:基本输入输出系统

初始化时钟;初始化内存;基本硬件的初始化

判断启动方式(USB;硬盘;光驱)

2、引导程序:

固化在硬盘(存储数据)最前面的部分

识别对应的操作系统

加载对应的操作系统

3、操作系统(没有图形化界面的程序)

4、文件系统(NTFS  FAT(16/32)EXT(2、3、4);window系统常见的文件系统格式  日志文件系统)

5、应用程序(桌面运用程序)

PC机启动时,首先启动BIOS(负责系统时钟和内存),然后选择启动方式(USB、硬盘、光驱)。启动盘中最开始的程序是引导程序(在内存中运行),引导程序会识别系统数量:如果只有一个系统则直接加载;如果有多个操作系统,则让用户选择启动哪个系统。系统启动后,挂载文件系统并启动应用程序。操作系统本身是一个非图形化界面的程序,桌面是对应的应用程序。

嵌入式设备启动流程

1、厂家固化在IROM里面的代码

基本硬件初始化(需要的时钟和设备)

判断对应的启动方式(emmc;sd卡)

把bootloader(给操作系统准备环境)第一阶段的代码加载到IRAM(运行内存)

问:为什么不在IROM初始化内存

答:因为IROM是厂家过来的;并不知道你开发板采用的什么内存;无法初始化

2、在IRAM里运行对应的bootloader第一阶段的代码(SPL:第二段程序加载)

初始化整个系统的时钟;

初始化内存

把完整的bootloader搬移到内存中

问:为什么bootloader要分两个阶段运行;不直接在IRam里面运行还要搬运到内存里

答:因为IRAM在芯片内部;存储空间有限;如果内存太大很浪费资源

3、在内存里运行bootloader第二个阶段的代码

初始化了一些硬件设备(根据开发板去定义(网卡;串口;USB)

加载对应的操作系统(内存)

4、运行操作系统(没有图形化界面)

5、挂载文件系统(EXT(2、3、4))

6、启动应用程序

PC机和嵌入式设备的联系

bootloader=BIOS+引导程序

在所有的操作系统中苹果的任务调度、进程线程调度是最好的;用几年都不卡

linux集成的通信协议栈是最丰富的

1.3 交叉编译

不同的架构涉及到不同的编译器

arm-----arm-linux-gcc;     x86------gcc

大部分都是arm架构

课上用到交叉编译器是x86的;但是这个程序编译出来的可执行文件是arm架构

APP想要运行要有相对应的架构(X86;arm;RISC-V);32/64;库

ELF 是 Executable and Linkable Format(可执行与可链接格式),是 Linux/Unix 系统中目标文件、共享库和可执行文件的标准文件格式。

readelf hello_x86 -a:显示对应的头部信息(Arm去头);把这些信息给删除

./arm-linux-gcc -v:运行当前目录下的 ARM 交叉编译器,并显示它的详细版本和配置信息

-v : verbose 的缩写,意思是显示详细信息

which gcc:可以直接查找gcc在哪

1.4 修改环境变量

1.echo $PATH:查找所有环境变量;

环境变量在执行命令运行代码的时候,在对应的路径下去查找对应的命令;类似于把指令搞成了全局变量

export PATH=$PATH:加路径:增加新的环境变量;注:只适用于当前终端。 关闭终端后设置就失效了。

2.想要所有终端都有效用.bashrc(当前linux用户登录后自动执行)

sudo vi .bashrc进入到脚本中

加入

TOOL_CHAIN=/home/hqyj/fs4412/tool_chain/gcc-4.6.4/bin
export PATH=$PATH:$TOOL_CHAIN

就能对当前用户下所有的终端都有效

3.对全部用户都有效

sudo vi /etc/bash.bashrc

然后在文件中输入方法二的添加环境变量

source /etc/bash.bashrc

执行脚本

sudo su切换用户

1.5 开发板和PC机进行文件传输

在linux下搭建一个服务器

sudo apt-get install tftp-hpa(客户端程序)(用来进行对应测试)
sudo apt-get install tftpd-hpa(服务端程序)

桥接模式是将虚拟机的虚拟网卡直接连接到物理主机的物理网卡上,使其在局域网中拥有独立的 IP 地址,像一台真实电脑一样与局域网中的其他设备通信。

创建一个文件夹;这个文件夹用来保存tftp服务器的文件

先创建好文件夹(当成服务器的数据文件夹)后;给他对应的权限;然后再文件夹中加入文件

mkdir tf tpboot
sudo chmod 777 -R tf tpboot

配置服务器的配置文件

sudo vi /etc/default/tftpd-hpa

重启tftp对应的服务

sudo service tftpd-hpa restart

测试(本地回环)linux运行了服务器;

tftp 本地回环地址:和tftp进行数据交换

get 文件名;获取文件(通过tftp服务器下载文件)

quit:退出交互

pc和板子链接方式

方法一:串口传输(速度太慢)

┌─────────────────┐                    ┌─────────────────┐
│       PC 机      │                    │    开发板       │
│                  │                    │                 │
│   MobaXterm      │                    │   UART 串口     │
│   (COM3 端口)    │◄──      网线     ──►│   调试口        │
│                  │                    │                 │
└─────────────────┘                    └─────────────────┘

方法二:网络通信(tftp:端口号69)

┌─────────────────┐         ┌─────────────┐         ┌─────────────────┐
│       PC 机      │         │  交换机     │         │    开发板       │
│                  │         │   (或路由器) │         │                 │
│   TFTP/NFS 服务器│────────►│  192.168.1.1│◄────────│   网口           │
│   192.168.1.100 │         │             │         │   192.168.1.10   │
│                  │         └─────────────┘         │                 │
└─────────────────┘                                 └─────────────────┘

交换机会通过DHCP自动分配IP;但是不会给板子指定IP;所以板子需要自己配置IP;可能会IP重复

nfs服务器配置

 NFS 服务器(/etc/exports)的共享配置,作用是:把主机上的 /home/linux/fs4412/rootfs 目录共享给网络中的所有设备,并设定访问权限。

# 1. 安装 NFS 服务器
sudo apt-get install nfs-kernel-server

# 2. 解压文件系统
tar -xvf rootf.tar.gz

# 3. 修改 NFS 配置文件(/etc/exports)
# 添加以下内容:
/home/linux/fs4412/rootfs *(rw,sync,no_root_squash)

# 4. 重启 NFS 服务
sudo service nfs-kernel-server restart

# 5. 测试挂载
sudo mount 127.0.0.1:/home/linux/fs4412/rootfs <空目录路径>

# 6. 卸载
sudo umount --lazy ./Public

通过网络挂载对应的文件系统;就不需要再下载对应的文件系统镜像;只需要下载内核的镜像和设备树

setenv bootcmd tftp 0x41000000 uImage\;tftp 0x42000000 exynos4412-fs4412.dtb\;bootm 0x41000000 - 0x42000000

没找到文件系统会卡死;linux系统启动后会找相对应的文件系统

把文件从 TFTP 服务器加载到内存(RAM)(临时的),然后从内存启动

setenv bootcmd tftp 0x41000000 uImage\;tftp 0x42000000 exynos4412-fs4412.dtb\;tftp  0x43000000 ramdisk.img\;bootm 0x41000000 0x43000000 0x42000000

板子上创建的文件没有办法进行保存;因为运行在内存中;断电就消失

在开发阶段如果采用这个方式,代码没办法保存

让虚拟机的linux和开发板上的Linux共享一个文件系统(nfs);在虚拟机上写代码;在开发板上进行测试

最后把整个文件系统打包成镜像文件固化到emmc中

nfs下可以被远程访问的文件目录

/home/hqyj/fs4412/rootfs *(rw,sync,no_root_squash)

nfs和传统方式区别

用 NFS 可以直接挂载并编辑文件,修改即时生效,无需打包镜像和烧录;否则,每次修改文件系统都需要重新制作镜像、下载到开发板,整个流程繁琐且低效。

具体对比:

  • NFS 方式:把宿主机上的目录通过网络挂载为开发板的根文件系统,可以直接在宿主机上修改文件,开发板重启即生效,适合开发阶段。

  • 传统方式:每次修改内容后,都要重新制作文件系统镜像(如 ramdisk.img),再通过 TFTP 下载或烧录到开发板,反复操作耗时且不利于调试。

对比维度 命令一(传统烧录) 命令二(网络开发)
完整命令 setenv bootcmd movi read kernel 0x41000000;movi read dtb 0x42000000;movi rootfs 0x43000000 30000;bootm 0x41000000 - 0x42000000 setenv bootcmd tftp 41000000 Image\;tftp 42000000 exynos4412-fs4412.dtb\;tftp 43000000 ramdisk.img\;bootm 41000000 43000000 42000000
数据来源 板载存储(eMMC/NAND) 网络服务器(TFTP)
依赖条件 无需网络,文件已烧录到板载存储 必须联网,依赖 TFTP 服务器
修改文件系统 需重新制作镜像并烧录,过程繁琐 替换服务器文件即可,无需烧录
修改内核/设备树 需重新烧录到存储分区 替换 TFTP 服务器文件即可
启动速度 快(从本地存储读取) 相对慢(需网络传输下载)
适用阶段 量产/产品交付阶段 开发调试阶段
核心特征 从存储分区读取并启动 从网络下载内存并启动

1.6 基础命令解析

U-boot只是bootloader一种

print:打印对应的参数信息

setenv:设置对应参数信息;

serverip:服务器IP即linux的IP

ipaddr:板子IP

gatewayip:网关IP:192.168.2.1

ethaddr:mac地址

saveenv:保存设置

先ping网关再ping服务器

角色 U-Boot 环境变量 bootargs 中设置
开发板 IP ipaddr=192.168.2.98 ip=192.168.2.98
NFS 服务器 IP serverip=192.168.2.101 nfsroot=192.168.2.1
网关 gatewayip=192.168.2.1 未设置

 U-Boot 支持自动将 ipaddr 的值填入 bootargs

下载(手动)

1. 下载 Linux 镜像文件

tftp 0x41000000 uImage

将 Linux 内核镜像文件下载到内存地址 0x41000000(运行内存ARM)

2. 下载文件系统镜像文件

tftp 0x43000000 ramdisk.img

将文件系统镜像文件下载到内存地址 0x43000000(运行内存ARM)

3. 下载设备树文件

tftp 0x42000000 exynos4412-fs4412.dtb

将设备树文件下载到内存地址 0x42000000

4. 启动内核

bootm 0x41000000 0x43000000 0x42000000

从指定内存地址启动:内核地址 0x41000000、文件系统地址 0x43000000、设备树地址 0x42000000

bootcmd:uboot启动完成读秒后,自动执行从参数(命令)

1.7 bootargs参数讲解

bootargs告诉文件系统在哪里找;本质uboot(bootloader)给linux系统传递的-参数

setenv bootargs root=/dev/nfs nfsroot=192.168.2.101:/home/hqyj/fs4412/rootfs,proto=tcp,nfsvers=3,no_lock rw console=ttySAC2,115200 init=/linuxrc ip=192.168.2.98
参数 含义
root=/dev/nfs 指定根文件系统(rootfs)不是从本地块设备(如 emmc/SD 卡)挂载,而是使用 NFS 网络文件系统。内核会因此启用 NFS 根文件系统支持。
nfsroot=192.168.2.101:/home/hdyj/fs4412/rootfs NFS 服务器的 IP 和导出路径:
- 服务器 IP:192.168.2.101
- 共享目录:/home/hdyj/fs4412/rootfs(主机上存放根文件系统的位置)
proto=tcp NFS 使用 TCP 协议(而非 UDP),TCP 更可靠,适合大文件传输和长距离网络。
nfsvers=3 使用 NFS 版本 3(常见于嵌入式开发,兼容性好)。
noLock 禁用 NFS 文件锁(lockd)。避免在无锁服务时出现警告或延迟,常用于简单开发环境。
rw 根文件系统以 读写(read-write) 方式挂载,允许在开发过程中修改文件。
console=ttySAC2,115200 内核控制台输出到 串口 2(ttySAC2),波特率 115200。这是开发板与主机间调试信息的输出通道。
init=/linuxrc 内核启动后执行的第一个用户态程序是 /linuxrc(通常是 BusyBox 提供的初始化脚本或软链接到 /sbin/init)。
ip=192.168.2.98 给开发板自身指定静态 IP 地址192.168.2.98),用于 NFS 挂载及网络通信。若不设,也可用 DHCP(ip=dhcp)。

1.8 Linux内核支持的传递格式


<1>通过struct parm struct结构体来传递(Linux 2.6之前使用的方式,已经过时,不便于扩展

<2>通过tag列表来传递(Linux 2.6开始使用的方式)

不是链式的;地址是连续的

参数格式固定且不够灵活,增加或修改参数需要修改内核和 U‑Boot 双方的代码,扩展性差,难以支持硬件差异越来越大的新设备

<3>通过设备树来传递(Linux 3.0开始使用的方式)

u_boot修改了设备树的节点(bootargs参数节点和内存信息节点),把需要传递的信息放在设备树中。在r2寄存器中记录了设备树在内存中的地址

注意:

  1. 在Linux 3.0之后的内核,这些方式都支持。

  2. 可以参看u-boot-2013-learn/arch/arm/lib/bootm.c文件了解参数传递过程

1.9 EMMC 的分区结构

EMMC 存储空间被划分成多个分区,每个分区有特定用途,类似电脑硬盘的 C 盘、D 盘:

分区名 作用 存储内容 存储介质 启动中的作用
uboot U-Boot 分区 存放 U-Boot 本身(u-boot.bin EMMC / NAND Flash 第一级启动:CPU 上电后首先从这里加载 U-Boot 到内存运行
kernel 内核分区 存放 Linux 内核镜像(uImage / zImage EMMC / NAND Flash 第二级启动:U-Boot 从这里读取内核到内存,然后启动内核
dtb 设备树分区 存放设备树文件(exynos4412-fs4412.dtb EMMC / NAND Flash 内核启动时读取:内核根据设备树信息识别硬件设备
ramdisk 临时根文件系统分区 存放 ramdisk.img(压缩的内存文件系统镜像) EMMC / 内存(TFTP加载) 早期启动:内核先加载 ramdisk 作为临时根文件系统,然后切换到真正的 rootfs
system / rootfs 永久根文件系统分区 存放完整的根文件系统(bin/etc/lib/usr/home/ 等) EMMC / NAND Flash / NFS 最终根文件系统:内核最终挂载这里作为系统的根目录 /
命令 数据来源 大小参数
movi write kernel 41000000 完整文件(uImage) 不需要(U-Boot 自动识别文件大小)
movi write rootfs 41000000 300000 原始数据(ramdisk.img 或 rootfs 镜像) 需要(手动指定写入大小,单位是字节)

二、内核

2.1 内核目录讲解

芯片厂家给到默认linux内核配置:arch/arm/configs

arch/arm/boot/compressed:linux对应的压/解;linux内核编译程序所占的内存大;但开发板的内存有限;所以需要把程序进行压缩;取出来的时候还要自解压

arch/arm/boot/dts:设备信息,源文件(给工程师看)dts相当于.c文件;dtsi相当于.h文件

设备树:.dtb:可执行文件(给操作系统用)

arch/arm/kernel/head.s:linux最先开始启动的代码

drivers/net/ethernet/davicom:物理网卡;dirvers:放各种驱动文件

net:网络协议栈对应的代码

2.2 makefile

1. 导入默认配置

make ARCH=arm exynos_defconfig

2. 编译内核

make ARCH=arm CROSS_COMPILE=arm-linux-

源码 (.c/.h/.S)
    ↓ 编译
目标文件 (.o)
    ↓ 链接
vmlinux(ELF 格式,完整内核)
    ↓ 处理
vmlinux.bin(纯二进制)
    ↓ 压缩
Image / zImage / uImage

编译后在同目录下生成vmlinux ;是编译生成的原始内核文件(未压缩、带调试信息),用于调试和分析,不能直接启动,最终会被加工成 uImage 或 zImage 才能烧录到开发板运行。

是ELF格式;包含头部信息;readelf -a vmlinux可以读头部信息

在arch/arm/boot 生成Image:去掉elf头和调试信息后的linux内核镜像;

和zImage (2.7M)(自解压代码 + gzip压缩后的去掉elf头之后的Linux内核镜像)

uImage是uboot专属镜像;就在zImage基础上多了64kb;多的信息给uboot用的;uboot去加载对应的操作系统

arch/arm/boot/compressed里的vmlinux:头部信息的linux镜像压缩文件+自解压代码

位置 文件 本质
根目录 /vmlinux 原始内核 未压缩、ELF格式、带调试符号,是内核的主体
arch/arm/boot/compressed/vmlinux 自解压程序 包含 压缩后的内核数据 + 自解压代码不是内核本身

2.3 内核配置

另外两个支持都是固定的;其中网卡驱动是根据原理图上面的网卡芯片去找的,如果在menuconfig里面没有找到芯片的驱动;找对应的厂家要驱动适配。

2.4 添加新的驱动文件

1、把文件复制到drivers/char/文件;下

2、配置Kconfig和Makefile文件

3、在上级目录的Kconfig里添加source路径;在Makefile里加

4、在菜单里面使能

obj-$(CONFIG_HELLO) += hello/

注:

CONFIG_HELLO 是全局的

一旦在任何 Kconfig 中定义了 CONFIG_HELLO 并且被 source 包含进来,它就存在于整个内核配置系统中,所有 Makefile 都能使用它。

所以要删掉source "drivers/char/test/Kconfig"

2.5 编辑设备树

设备树将硬件信息从内核代码中解耦出来,以独立文件描述设备配置,内核只需读取设备树即可动态识别硬件,无需重新编译内核,大大提升了移植性和灵活性。

.dts    ==    源文件
.dtsi   ==    头文件
.dtb    ==    生成的文件(可执行文件)

在/arch/arm/boot/dts找到抄的母版;cp成自己的;

在Makefile里面添加设备树

在linux顶层的makefile里编辑设备树:make ARCH=arm CROSS_COMPILE=arm-linux- dtbs

gedit (以文本的形式打开)设备树文件添加网卡设备信息(添加设备节点)

三、Linux 内核配置与编译关系

  • menuconfig:Linux内核提供的菜单选项界面,表示内核支持哪些功能,好比去餐馆看到的菜单

  • .config:Linux内核当前选择的功能配置,记录了你具体点了哪些菜,好比点菜的凭证

  • Kconfig:为Linux内核提供菜单选项的定义,好比当前餐馆所有厨师会做的菜

  • Makefile:管理Linux内核的编译过程,根据.config里面的宏配置决定编译哪些代码,好比厨师根据点菜单做菜

Kconfig 定义了所有可选功能,menuconfig 读取 Kconfig 显示图形化菜单让你选择,你选好后保存到 .config 文件中,最后 Makefile 读取 .config 根据配置编译对应的代码。

3.1 Kconfig 配置选项语法

config 选项名
    属性1
    属性2
    ...

[1] 选项名

config HELLO

展现形式:CONFIG_HELLO,在 .config 文件中定义


[2] 属性

(1) 选择类型

类型

取值

含义

menuconfig 显示

tristate(三态)

y

编译进内核

< > 或 <M> 或 <*>

m

编译成模块(find -name *.ko

n

不编译

bool(两态)

y

编译进内核

[ ] 或 [×]

n

不编译

string

字符串

如 CONFIG_选项名="字符串"

( )

int

整数

如 CONFIG_选项名=整数

hex

十六进制数

如 CONFIG_选项名=十六进制数


(2) 提示字符串

prompt "提示字符串"

在配置菜单中显示的名称


(3) range

指定值的范围(用于 int 或 hex 类型)


(4) help

帮助信息,在 menuconfig 中按 ? 显示

help
"test help ..."


(5) default

当用户没有手动配置时,默认的选择值

config HELLO
    tristate
    prompt "hello support"
    default y

(6) depends on — 依赖关系

depends on 配置选项名
depends on 配置选项名1(m) || 配置选项名2(y)
depends on 配置选项名1(y) && 配置选项名3(m)

依赖逻辑:

  • y = 2,m = 1,n = 0

  • && → 取最小值

  • || → 取最大值

注意:

  • 如果依赖结果为 0:选项不可见

  • 如果依赖结果为 2:选项为三态(tristate)

  • 如果依赖结果为 1:选项为两态(bool)


(7) select — 自动选择

当前配置选项被选中的时候,同时自动选中 select 指定的配置选项

config DM9000
    tristate "DM9000 support"
    select CRC32
    select MII

选 DM9000 时,自动选中 CRC32 和 MII。


(8) menuconfig — 菜单

menuconfig 选项名

表示这是一个菜单项,可以包含子选项。

menu "Test menu support"

config HELLO1
    tristate
    prompt "support HELLO1"

config HELLO2
    tristate
    prompt "support HELLO2"

endmenu

menuconfig TEST_MENUCONFIG
    tristate
    prompt "support menconfig"

if TEST_MENUCONFIG
    config HELLO0
        tristate
        prompt "support HELLO0"

    config HELLO0y
        tristate
        prompt "support HELLO0y"
endif
  • menu:纯组织作用,把选项归组显示(文件夹一样)

  • menuconfig + if:父选项可以对菜单里的权限控制(y/m/n),父项什么态,子选项有相对应的权限


(9) choice — 二选一

代表多个选项中只能选一个。

choice
    prompt "support choice"
    optional
    default CONFIG_TEST_C1

config CHOICE1
    tristate "support choice1"

config CHOICE2
    tristate "support choice2"

endchoice

(10) comment — 提示信息

在菜单中显示纯文本提示信息。

comment "以下为网络配置选项"


(11) source — 包含另一个 Kconfig

source "路径/另一个Kconfig"

将这个路径下的 Kconfig 文件内容包含进来,实现 Kconfig 文件的拆分和组织。


注意

(1) 和 (2) 必须有,其它可选择

config HELLO
    tristate
    prompt "Hello support"
    help
        "Compile hello.c"

config HELLO
    tristate "Hello support"
    ---help---
        "Compile hello.c" //可以不加引号

config HELLO1
    bool
    prompt "Hello1 support"
    help
        "Compile hello.c "

config HELLO2
    string
    prompt "Hello2 string"
    help
        "Compile hello.c "

config HELLO3
    int
    prompt "Hello3 int"
    help
        "Compile hello.c "

config HELLO4
    int "Hello4 int" //03的简写
    help
        "Compile hello.c "

config HELLO5
    int
    prompt "Hello5 int rang[0-5]"
    range 0 5
    help
        "Compile hello.c "

config HELLO6
    hex
    prompt "Hello6 hex"
    help
        "Compile hello.c "

config HELLO7
    hex
    prompt "Hello7 hex"
    range 0 5
    help
        "Compile hello.c "

四、文件系统

制作的工具:

  • BusyBox — 文件系统(BusyBox 是一个集成了三百多个最常用Linux命令和工具的软件)

  • Buildroot — 文件系统、Linux内核、bootloader

  • Yocto — 文件系统、Linux内核、bootloader

BusyBox 编译安装流程

# 1. 配置(选择静态编译、交叉编译前缀等)
make menuconfig
Busybox Settings → Build Options(编译相关的一些选项)
  • [*] Build BusyBox as a static binary (no shared libs)
    静态编译方式,这样就不需要采用动态库,直接拷贝过去即可运行

  • [ ] Force NOMMU build

  • [ ] Build with Large File Support (for accessing files > 2 GB)

  • (arm-linux-) Cross Compiler prefix
    选择对应的编译器前缀

  • () Additional CFLAGS

编译方式 命令 特点
静态编译 gcc hello.c -static -o hello 库代码直接嵌入可执行文件,文件大,但独立运行,不依赖外部 .so
动态编译 gcc hello.c -o hello(不加 -static 可执行文件小,但运行时需要系统中有对应的动态库(.so)
# 2. 编译(生成 busybox 可执行文件)
make

# 3. 安装(打包/部署)
make install

把你之前的文件系统改名;创建一个rootfs;进入rootfs进行拷贝——install所有文件

linux操作系统跑起来后需要有一些文件夹进行资源的挂载

mkdir dev etc mnt proc var tmp sys root

板子跑起来还需要这些目录

寻找动态库

ldd查_x86架构涉及到的动态库;

找arm架构的动态库就readelf -a hello_arm在头部信息里面找

五、Busybox  init启动过程

init === busybox === 会去读取 inittab(初始化表),并加载和运行里面的内容

inittab在etc文件夹里面

inittab 文件格式:

id编号:权限等级:动作:进程

即以什么编号、什么等级、什么动作去加载进程。

id编号和权限等级可以省略。

动作 说明
sysinit 表示进程在系统启动后最先执行,只执行一次,init 进程等待它结束才继续执行其他动作
wait 表示进程在执行 sysinit 后执行,只执行一次,init 进程等待它结束才继续执行其他动作
once 表示进程在执行 wait 后执行,只执行一次,init 进程不等待它结束
respawn 表示进程在启动 once 后执行,init 进程一旦发现该进程死掉,就会重新启动
askfirst 表示进程在启动完 respawn 后执行,与 respawn 类似,不过 init 进程会先输出提示 "Please press Enter to activate this console",等用户输入回车后才启动子进程
restart 如果 BusyBox 中配置了 CONFIG_FEATURE_USE_INITTAB,并且 init 进程收到 SIGHUP 信号时,先重新读取/解析 /etc/inittab 文件,再执行该进程
shutdown 表示进程在系统收到重启、关闭系统命令时运行
ctrlaltdel 表示进程在按下 Ctrl+Alt+Del 组合键时运行

rcS

1. 内核启动
       ↓
2. 执行 /sbin/init(BusyBox 提供的 init)
       ↓
3. 读取 /etc/inittab
       ↓
4. 执行 ::sysinit:/etc/init.d/rcS
       ↓
5. rcS 脚本执行 /bin/mount -a
       ↓
6. mount -a 读取 /etc/fstab
       ↓
7. 挂载 fstab 中定义的所有文件系统
   (proc、sysfs、tmpfs、dev 等)

proc — Linux内核的资源(进程资源) → /proc 这个目录

tmpfs — 临时存储的空间 → /tmp 这个目录

sysfs — Linux子系统(GPIO、PWM、I2C) → /sys 这个目录

tmpfs — Linux的设备挂载 → /dev 目录

挂载与创建

挂载设备是把存储设备或虚拟文件系统(如 /proc)挂到 Linux 目录树上,让系统能通过路径访问其内容,对应配置文件是 /etc/fstab,由 mount -a 读取执行。

创建设备是在 /dev 下生成设备节点文件,让应用程序通过该文件与硬件或内核驱动通信,由 /etc/init.d/rcS 中的 mdev -s 扫描 /sys 动态生成。

两者分工明确:挂载解决“怎么访问文件系统”,创建设备解决“怎么访问硬件设备”。挂载依赖 /etc/fstab,创建设备依赖 mdev

sh

sh 就是给你提供一个能输入命令、与系统交互的命令行环境(Shell)。

init

通过看门狗机制监控 init 进程,若 init 因异常卡死而无法喂狗,系统将自动复位,避免设备永久锁死。

init(PID=1)
    ↓
读取 /etc/inittab
    ↓
执行 ::sysinit:/etc/init.d/rcS(初始化脚本)
    ↓
执行 ::askfirst:-/bin/sh(启动 Shell)
    ↓
/bin/sh 启动后自动读取 /etc/profile
    ↓
进入命令行交互界面(提示符出现)

系统启动时自动执行程序方法

(1) 在 bootargs 参数中指定自己的程序

  • init=/linuxrc 默认写法

  • init=我们自己程序的路径

例如,需要执行的程序是 hello,路径为 /test/hello

init=/test/hello

缺点:没有启动 init 进程


(2) 默认启动的是 init 进程,init 进程启动过程中读取 /etc/inittab 文件,并将该文件指定的程序启动起来,所以可以在此文件中添加自己的程序。

例如,需要执行的程序是 hello,路径为 /test/hello,并且该程序不能被杀死:

::respawn:/test/hello


(3) init 进程启动时,会执行 /etc/init.d/rcS 脚本,可以在此脚本中添加自己的程序。

例如:

/test/hello

或(后台运行):

/test/hello &


(4) init 进程启动时,会执行 /bin/sh,这是一个 shell 程序,shell 程序执行时会执行 /etc/profile 脚本文件,所以可以在该脚本中添加自己的程序。

例如:

/test/hello

或(后台运行):

/test/hello &


注意

很多时候会选择在 (2) 或 (4) 中添加:

方式 特点
(2) /etc/inittab 可以让自己的进程死掉后自动重新启动respawn
(4) /etc/profile 可以让自己的程序在执行时,执行环境比较好(已加载环境变量)

五、ramdisk文件系统(镜像文件)

1.拷贝镜像文件

dd 命令说明

dd:用指定大小的块拷贝一个文件,并在拷贝的同时进行指定的转换。

参数

说明

if=文件名

输入文件名,缺省为标准输入,即指定源文件(input file)

of=文件名

输出文件名,缺省为标准输出,即指定目的文件(output file)

bs=bytes

同时设置读入/输出的块大小为 bytes 个字节

cbs=bytes

一次转换 bytes 个字节,即指定转换缓冲区大小


示例命令

cd ~
dd if=/dev/zero of=ramdisk bs=1k count=8192

(ramdisk 为 8M)

/dev/zero 是 Linux 中的一个特殊设备文件(不是普通文件),它的特点是:

  • 读取它时,永远返回 0(空字节)

  • 大小是无限的——你可以一直读,它一直输出 0。


ramdisk 为 8M

8192 × 1k = 8192KB = 8MB

它是根据对应的文件系统大小来决定的,一定要比对应的文件系统的大小大。这里文件系统大小约为 5.2M,所以选择了 8M 来确保有足够的空间容纳文件系统内容。

2.格式化镜像为 ext2

目的:添加文件系统的信息,操作系统起来之后才知道是什么文件系统。

mkfs.ext2 -F ramdisk

3.拷贝对应的文件系统文件到镜像中
之前的步骤中我们创建了一个文件系统,里面是空的,所以现在需要拷贝对应文件系统的文件到镜像中。

mkdir FileSystem          ← 创建挂载点目录
sudo mount ramdisk FileSystem  ← 将镜像挂载到目录

cd FileSystem/

sudo cp ../fs4412/rootfs/* ./ -a  ← 拷贝根文件系统内容到镜像中

文件系统是“内容”(数据的组织方式),镜像文件是“载体”(存储这个内容的文件)。


用生活比喻来理解

概念 比喻
文件系统(如 ext2) 图书的分类规则(书怎么编号、怎么摆放)
镜像文件(ramdisk) 一个空的纸箱(容器)
格式化(mkfs.ext2) 在纸箱里画好格子、贴好标签(写入文件系统规则)
拷贝内容(cp) 把书按规则放进纸箱里
最终产物 一个装好书、并按规则整理好的纸箱 = 可用的镜像文件

4.卸载

卸载是为了“封箱”——把镜像文件从系统挂载点断开,确保所有数据完整地写入到镜像文件中,这样才能得到一个可用的、一致的根文件系统镜像。 sudo umount FileSystem/

5.压缩 ramdisk 为 ramdisk.gz

目的:通过压缩可以把对应的 ramdisk 文件系统变得更小。

gzip --best -c ramdisk > ramdisk.gz

6.让U-Boot能识别

mkimage -n "ramdisk" -A arm -O linux -T ramdisk -C gzip -d ramdisk.gz ramdisk.img

这是一个 mkimage 命令(用于 U-Boot 制作镜像),用于将压缩后的 ramdisk 文件封装成 U-Boot 可识别的格式。

参数 含义
-n "ramdisk" 设置镜像名称(Image Name)为 ramdisk
-A arm 指定架构(Architecture)为 ARM
-O linux 指定操作系统(OS)为 Linux
-T ramdisk 指定镜像类型(Type)为 ramdisk
-C gzip 指定压缩方式(Compression)为 gzip
-d ramdisk.gz 指定数据文件(Data file)为 ramdisk.gz
ramdisk.img 指定输出文件名为 ramdisk.img

7.把镜像复制到目录中

cp ramdisk.img fs4412/tftpboot/

六、Linux 系统启动的三种部署策略

1.全部下载到内存执行(纯内存运行)

核心原理
U-Boot 通过 TFTP 协议,把所有需要的组件(内核、设备树、根文件系统)从网络服务器下载到开发板的内存(RAM)中,然后直接跳转到内存地址启动系统。整个过程不涉及板载存储,系统完全在内存中运行。

典型 U-Boot 命令

setenv bootcmd 'tftp 41000000 Image; tftp 42000000 exynos4412-fs4412.dtb; tftp 43000000 ramdisk.img; bootm 41000000 43000000 42000000'

优点

  • 调试效率最高:修改内核、驱动或文件系统后,只需替换 TFTP 服务器上的文件,重启开发板即可生效。

  • 零损耗:不会磨损板载 Flash。

缺点

  • 依赖网络:每次启动都必须连接 TFTP 服务器。

  • 断电即失:文件系统在内存中,修改不会保存(除非手动挂载存储)。

适用场景驱动开发、内核移植、频繁修改文件系统的早期调试阶段。


2. 下载内核/设备树 + NFS 挂载文件系统(网络挂载)

核心原理
U-Boot 只通过 TFTP 下载内核和设备树到内存,但根文件系统不下载,而是通过 NFS(网络文件系统) 协议,将宿主机上的一个目录直接挂载为开发板的根目录 /。这意味着开发板的文件系统实际上存储在宿主机上。

典型 U-Boot 命令

setenv bootcmd 'tftp 41000000 Image; tftp 42000000 exynos4412-fs4412.dtb; bootm 41000000 - 42000000'

(在 bootargs 环境变量中指定 NFS 根目录)

setenv bootargs 'root=/dev/nfs nfsroot=192.168.1.100:/home/xxx/rootfs ip=192.168.1.10:192.168.1.100::255.255.255.0::eth0:off init=/linuxrc console=ttySAC0,115200'

优点

  • 文件修改即存:在宿主机上修改文件,保存后开发板立即可用,无需重启。

  • 存储空间大:不受开发板 Flash 容量限制,可以挂载数十 GB 的根文件系统。

  • 节省开发板资源:不占用板载存储空间。

缺点

  • 强依赖网络:网络不稳定时系统会卡死。

  • 启动速度最慢:需要从网络加载大量小文件。

适用场景应用程序开发、根文件系统内容频繁调整的软件调试阶段。


3.全部固化到 eMMC 并从 eMMC 读取(本地存储启动)

核心原理
这是量产阶段的最终形态。所有组件(内核、设备树、根文件系统)都提前烧录到开发板的 eMMC/NAND Flash 中。U-Boot 启动时直接从存储介质的分区读取到内存,然后启动。完全不依赖网络

典型 U-Boot 命令(eMMC 版)

setenv bootcmd 'movi read kernel 0x41000000; movi read dtb 0x42000000; movi read rootfs 0x43000000 30000; bootm 0x41000000 - 0x42000000'

(注:movi 是三星系列芯片读取存储的命令,其他平台可能用 mmc read 或 nand read

优点

  • 开机即用:上电无需外设,独立启动。

  • 启动速度最快:从本地 Flash 读取速度远快于网络传输。

  • 生产标准:符合产品出厂要求。

缺点

  • 修改麻烦:任何文件变更都需要重新制作分区镜像并烧录(dd 或烧录工具)。

  • 磨损风险:Flash 有写入寿命限制,不适合频繁刷写。

适用场景产品定型后的量产阶段、用户现场部署。

终极对比(三张表总结)

对比维度

方式一:全 TFTP

方式二:TFTP + NFS

方式三:eMMC 固化

内核获取方式

TFTP 下载到内存

TFTP 下载到内存

eMMC 读取到内存

设备树获取方式

TFTP 下载到内存

TFTP 下载到内存

eMMC 读取到内存

根文件系统方式

TFTP 下载 ramdisk 到内存

NFS 网络挂载宿主机目录

eMMC 分区读取

修改是否方便

需替换镜像文件、重启

直接改,即改即用

需重新烧录

启动速度

中等(下载镜像)

慢(小文件多)

最快

网络依赖

强(启动全程依赖)

极强(运行时也依赖)

开发阶段

前期(内核/驱动调试)

中期(应用/文件系统调试)

后期(量产交付)


一句话区分它们

  • 方式一:最像“一次性”运行,所有文件从网络搬进内存跑,调试内核最快。

  • 方式二:最像“本地电脑”,文件系统在服务器上,改完就能用,调试应用最爽。

  • 方式三:最像“家电产品”,上电即用,不依赖任何外部环境,用户拿到的就是它。

七、U‑Boot

7.1 目录讲解

board目录

board/samsung/origen存放的是三星 Origen 开发板(Exynos 4210)的板级初始化代码和硬件配置文件

  • lowlevel_init.s:开发板上电后最先执行的汇编代码,负责关闭看门狗、设置栈指针、关闭中断等最基础的CPU环境。(时钟初始化)

  • mem_setup.S:内存初始化汇编代码,负责配置DRAM控制器,让开发板能够正常使用外部内存(DDR)。(内存初始化)

common目录

存放的是 U-Boot / Linux 内核中与具体硬件无关的通用核心代码

common/ = 存放命令的“菜谱”(源代码)

drivers目录

驱动

net目录

网络协议栈;初始化网卡

arch目录
  arch/arm/cpu/armv7/start.S  这个是uboot首先执行的代码 
  arch/arm/cpu/u-boot.lds     这个是连接脚本文件(告诉编译在连接生成可执行文件的时候,一些段存放位置) 

Linux一个可执行文件
objcopy
|       |
|elf 头 | ----------> 给Linux使用
|-------|
|.data  |
|-------|
|.bss   |
|-------|
|.ro    |
|-------|
|.txt   |

存储内容 是否占用可执行文件空间 是否占用内存空间(运行时)
.text 机器码(你的代码) 是(文件大小增加)
.rodata 字符串常量、只读数据 是(文件大小增加)
.data 已初始化的全局/静态变量  是(有初始值,得存) 是(程序加载时复制到内存)
.bss 未初始化的全局/静态变量  不占用(只记个长度) 是(加载时由系统分配并清零)

include目录

存放的是uboot相关代码需要头文件
  include/configs/origen.h

origen开发板对应的头文件,它决定了origen所需要的一些代码的宏开关

它也是开发板配置头文件,开发板所需要的代码宏开关和相关的宏定义的

参数都应该在此文件中定义    

boards.cfg目录
u-boot 支持的板子的配置信息,u-boot编译系统就是根据此配置文件,来识别当前板子的信息

U-Boot 编译生成文件说明

文件 说明
u-boot.bin raw binary 镜像(bootloader 第二阶段代码)
u-boot ELF 二进制格式的镜像
u-boot.sec Motorola S-Record 格式的镜像
u-boot.map 记录了生成 u-boot 代码所需文件的路径
System.map 记录了 u-boot 中标签名和函数名对应的地址

7.2 两个最先开始的程序

对比维度 linux/arch/arm/kernel/head.S u-boot/arch/arm/cpu/armv7/start.S
所属软件 Linux 内核 U-Boot 引导程序
运行时机 U-Boot 加载内核后,跳转到这里 开发板上电后,最先执行这里
主要任务 使能 MMU、建立内核页表、跳转到 start_kernel() 关闭看门狗、设置中断向量、初始化 DRAM、搬移自身到内存、跳转到 board_init_f()
最终目标 启动 Linux 操作系统 加载并启动 Linux 内核
运行环境 已由 U-Boot 初始化好 DRAM 刚上电,CPU 处于原始状态(Flash/ROM 中执行)
汇编/ C 混合 先汇编(head.S)后 C(start_kernel) 先汇编(start.S)后 C(board_init_f/r)
开发板上电
    ↓
U-Boot 最先执行
    ├── start.S(U-Boot 入口)
    │   └── 初始化 CPU、DRAM、时钟,搬移 U-Boot 自身
    ↓
U-Boot 加载 Linux 内核和设备树到内存
    ↓
U-Boot 执行 bootm,跳转到内核入口
    ↓
Linux 内核执行
    ├── head.S(Linux 入口)
    │   └── 使能 MMU、建立页表、准备 C 环境
    ↓
start_kernel()(Linux 内核 C 入口)
    ↓
挂载根文件系统,启动 init 进程

start.S 是 U-Boot 的第一行代码(硬件初始化),head.S 是 Linux 内核的第一行代码(CPU 运行环境准备)。一个是"给芯片穿衣服",一个是"让系统站起来"。

7.3 配置和编译 U-Boot 引导程序

指定架构

make origen_config

将 U-Boot 源码配置为针对三星 Origen 开发板(Exynos 4210)的编译环境

会在u-boot-2013.01/include/config.mk里生成配置的板子信息;config.mk会在总的Makefile里加载

指定编译器

起始地址在System.map查看

阶段 文件 说明
第一阶段 u-boot-spl.bin SPL(Secondary Program Loader),负责初始化最基础的硬件(如时钟、DRAM),将第二阶段的 u-boot.bin 加载到内存中
第二阶段 u-boot.bin 完整的 U-Boot,提供命令行、环境变量、网络、Flash 烧录等丰富功能,最终加载 Linux 内核
开发板上电
    ↓
ROM 固化代码(BL0)执行
    ↓
加载 SPL(u-boot-spl.bin)到 SRAM  ← 第一阶段
    ↓
SPL 初始化 DRAM、时钟
    ↓
SPL 从存储介质(eMMC/SD/NAND)加载 u-boot.bin 到 DRAM  ← 第二阶段
    ↓
跳转到 u-boot.bin 执行
    ↓
U-Boot 提供命令行,加载 Linux 内核

7.4 启动流程

ARM核的设置

  1. 设置CPU为SVC模式

  2. 设置异常向量表

  3. 关闭Cache和MMU(内存管理单元:A. 内存访问权限设置 B. 物理地址和虚地址的映射)

SOC芯片的设置

  1. 处理唤醒的条件

  2. 判断是否在内存中运行

  3. 如果不在内存中运行,则初始化系统时钟和内存控制器(第一阶段
    如果在内存中运行,则不需要初始化系统时钟和内存控制器(第二阶段

  4. 初始化 UART

如果通过 go 命令启动 u-boot 的时候,没有看到任何输出效果?

可能原因:

[1] u-boot 在内存中运行的时候,重新初始化了系统时钟和内存?
[2] 没有初始化串口?
[3] 指定内存基地址不对?
[4] ?(点灯大法:通过在一些位置加亮灯代码,确定导致 u-boot 死掉的代码)
[5] TrustZone 代码有问题?

在 u-boot-2013.01 for fs4412 上面,是由于 TrustZone 的代码有问题,我们只需要将
board/samsung/origen/lowlevel_init.s
文件中的 bl tzpc_init 代码注释掉就可以了。

八、硬件底层代码调试方法

  1. 遇到硬件底层代码有问题,首先通信(打印信息)

  2. 如果通信有问题,只有点灯

找相关功能在哪;grep "board_eth_init" * -nR  

 在当前目录及其子目录下递归搜索 "board_eth_init",并在结果中显示行号(-n)

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐