嵌入式Linux学习笔记
将物理内存通过虚拟内存管理机制映射到进程的虚拟内存地址上的过程。对于32位的系统,一旦进程运行起来,操作系统就会为每个进程分配4G的虚拟内存。前面3G是用户空间,后面1G是内核空间。针对全局变量肯定在【.bss或 .data】段,初始化好的肯定在【.data】,未初始化的肯定在【.bss】可执行程序的大小 = .data的大小+ .text的大小。
第1章 linux基础
1.1 内存映射
内存映射是一个过程。
内存映射的定义: 将物理内存通过虚拟内存管理机制映射到进程的虚拟内存地址上的过程。
对于32位的系统,一旦进程运行起来,操作系统就会为每个进程分配4G的虚拟内存。
前面3G是用户空间,后面1G是内核空间。

针对全局变量肯定在【.bss或 .data】段,初始化好的肯定在【.data】,未初始化的肯定在【.bss】
可执行程序的大小 = .data的大小 + .text的大小。
1.2 进程与线程

1.3 进程间通讯
1.3.1 有名管道和无名管道
1.3.2 信号
1.3.3 共享内存
共享内存是进程间进行通讯的一种方式。
- 共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝。
- 为了在多个进程间交换信息,内核专门留出了一块内存区可以由需要访问的进程将其映射到自己的私有地址空间。
- 进程就可以直接读写这一内存区而不需要进行数据的拷贝:从而大大提高的效率。
- 由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等。

1.3.4 消息队列
1.3.4.1 链表
链表的核心思想

1.3.4.2 队列
#ifndef QUEUE_H_
#define QUEUE_H_
#include <stddef.h>
/* 内部链表节点结构体 */
struct uv_queue {
struct uv_queue* next;
struct uv_queue* prev;
};
/* 从链表节点指针获取包含它的结构体指针 */
#define uv_queue_data(pointer, type, field) \
((type*) ((char*) (pointer) - offsetof(type, field)))
/* 遍历链表(不包括头节点) */
#define uv_queue_foreach(q, h) \
for ((q) = (h)->next; (q) != (h); (q) = (q)->next)
/* 初始化链表头(形成循环) */
static inline void uv_queue_init(struct uv_queue* q) {
q->next = q;
q->prev = q;
}
/* 判断链表是否为空(只有头节点) */
static inline int uv_queue_empty(const struct uv_queue* q) {
return q == q->next;
}
/* 获取链表第一个节点 */
static inline struct uv_queue* uv_queue_head(const struct uv_queue* q) {
return q->next;
}
/* 获取下一个节点 */
static inline struct uv_queue* uv_queue_next(const struct uv_queue* q) {
return q->next;
}
/* 将队列 n 添加到 h 后面(拼接两个链表)*/
static inline void uv_queue_add(struct uv_queue* h, struct uv_queue* n) {
h->prev->next = n->next;
n->next->prev = h->prev;
h->prev = n->prev;
h->prev->next = h;
}
/* 拆分链表:将 [n, q) 段从 h 链表中分离 */
static inline void uv_queue_split(struct uv_queue* h,
struct uv_queue* q,
struct uv_queue* n) {
n->prev = h->prev;
n->prev->next = n;
n->next = q;
h->prev = q->prev;
h->prev->next = h;
q->prev = n;
}
/* 移动整个链表 h 到 n 前面 */
static inline void uv_queue_move(struct uv_queue* h, struct uv_queue* n) {
if (uv_queue_empty(h))
uv_queue_init(n);
else
uv_queue_split(h, h->next, n);
}
/* 插入到头部 */
static inline void uv_queue_insert_head(struct uv_queue* h,
struct uv_queue* q) {
q->next = h->next;
q->prev = h;
q->next->prev = q;
h->next = q;
}
/* 插入到尾部 */
static inline void uv_queue_insert_tail(struct uv_queue* h,
struct uv_queue* q) {
q->next = h;
q->prev = h->prev;
q->prev->next = q;
h->prev = q;
}
/* 从链表中移除节点 */
static inline void uv_queue_remove(struct uv_queue* q) {
q->prev->next = q->next;
q->next->prev = q->prev;
}
#endif /* QUEUE_H_ */
使用实例
- 给定路径:
F:/home/user/workspaces/test - 拆分为:
F:/homeF:/home/userF:/home/user/workspacesF:/home/user/workspaces/test
- 将这些路径插入到
uv_queue链表中(按顺序) - 使用
uv_queue提供的接口操作 - 实现“前进”和“后退”逻辑(即在链表中前后移动)
- 到头或尾时再次点击按钮发出提示
#ifndef MAIN_WINDOW_H
#define MAIN_WINDOW_H
#include <QMainWindow>
#include <QString>
// 包含你提供的 uv_queue 定义
#include "queue.h"
// 路径节点结构(queue 必须是第一个成员)
struct PathNode {
uv_queue queue;
QString path;
};
QT_BEGIN_NAMESPACE
class QLabel;
class QPushButton;
QT_END_NAMESPACE
class MainWindow : public QMainWindow {
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void onBackwardClicked();
void onForwardClicked();
private:
void buildPathList(const QString& fullPath);
void updateUI();
QLabel* m_statusLabel;
QPushButton* m_btnBack;
QPushButton* m_btnForward;
uv_queue m_pathListHead; // 链表头
PathNode* m_currentNode; // 当前节点指针
};
#endif // MAIN_WINDOW_H
// main_window.cpp
#include "mainwindow.h"
#include <QVBoxLayout>
#include <QWidget>
#include <QMessageBox>
#include <QDebug>
#include <QtWidgets>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, m_currentNode(nullptr)
{
// 初始化链表头
uv_queue_init(&m_pathListHead);
// 创建 UI
QWidget* centralWidget = new QWidget(this);
QVBoxLayout* layout = new QVBoxLayout(centralWidget);
m_statusLabel = new QLabel("加载中...");
m_btnBack = new QPushButton("← 后退");
m_btnForward = new QPushButton("前进 →");
layout->addWidget(m_statusLabel);
layout->addWidget(m_btnBack);
layout->addWidget(m_btnForward);
setCentralWidget(centralWidget);
// 连接信号槽
connect(m_btnBack, &QPushButton::clicked, this, &MainWindow::onBackwardClicked);
connect(m_btnForward, &QPushButton::clicked, this, &MainWindow::onForwardClicked);
// 构建路径链表
buildPathList("F:/workspace/Test/04_demo20250822/Test");
// 设置当前为第一个节点
if (!uv_queue_empty(&m_pathListHead)) {
m_currentNode = uv_queue_data(uv_queue_head(&m_pathListHead), PathNode, queue);
} else {
m_currentNode = nullptr;
}
updateUI();
}
MainWindow::~MainWindow() {
// 释放所有 PathNode 节点
while (!uv_queue_empty(&m_pathListHead)) {
uv_queue* q = uv_queue_head(&m_pathListHead);
uv_queue_remove(q);
PathNode* node = uv_queue_data(q, PathNode, queue);
delete node;
}
}
void MainWindow::buildPathList(const QString& fullPath) {
// 清空旧链表
while (!uv_queue_empty(&m_pathListHead)) {
uv_queue* q = uv_queue_head(&m_pathListHead);
uv_queue_remove(q);
PathNode* node = uv_queue_data(q, PathNode, queue);
delete node;
}
// 分割路径,跳过空字符串
QStringList parts = fullPath.split("/", Qt::SkipEmptyParts);
if (parts.isEmpty()) return;
// 第一步:提取盘符,构建根路径 F:/
QString drive = parts[0] + "/"; // "F:/"
// 插入根节点 F:/
PathNode* root = new PathNode{ {}, drive };
uv_queue_insert_tail(&m_pathListHead, &root->queue);
// 第二步:从 parts[1] 开始,逐步拼接子目录
QString currentPath = drive;
for (int i = 1; i < parts.size(); ++i) {
currentPath += parts[i];
PathNode* node = new PathNode{ {}, currentPath };
uv_queue_insert_tail(&m_pathListHead, &node->queue);
currentPath += "/"; // 补回 '/',用于下一级拼接
}
// 设置当前节点为第一个(即 F:/)
m_currentNode = uv_queue_data(uv_queue_head(&m_pathListHead), PathNode, queue);
}
void MainWindow::onBackwardClicked() {
if (!m_currentNode) return;
uv_queue* prev_q = m_currentNode->queue.prev;
if (prev_q == &m_pathListHead) {
QMessageBox::information(this, "提示", "已经到达最顶层目录。");
return;
}
m_currentNode = uv_queue_data(prev_q, PathNode, queue);
updateUI();
}
void MainWindow::onForwardClicked() {
if (!m_currentNode) return;
uv_queue* next_q = m_currentNode->queue.next;
if (next_q == &m_pathListHead) {
QMessageBox::information(this, "提示", "已经到达最底层目录。");
return;
}
m_currentNode = uv_queue_data(next_q, PathNode, queue);
updateUI();
}
void MainWindow::updateUI() {
m_statusLabel->setText("当前路径: " + (m_currentNode ? m_currentNode->path : QString("无")));
}
1.3.5 信号量
1.4 桥接和Nat连接
1.4.1 桥接
桥接的通俗理解:假设有A和B两个房间。A房间使用了一个交换机将所有电脑连接在了一起;B房间使用了一个交换机将所有电脑连接在一起。现在将A房间和B房间的交换机用网线连接起来,并且将A房间的交换机在通过网线和路由器连接到一起(上互联网)。
这样组合起来就是桥接,桥接最大的现象是,A房间的电脑和B房间的电脑在同一个网段。

1.4.2 Nat连接


第2章 嵌入式系统框架

2.0 嵌入式系统构成
- 嵌入式微处理器(MPU);
- 外围硬件设备;
- 嵌入式操作系统(可选);
- 应用软件。
2.1 Bootloader的作用
- 将内核从存储器(NorFlash或EMMC)加载到内存(虚拟内存)。
- 将加载到内存中的内核启动起来。
注意:Bootloader使用的实实在在的物理地址。因为操作系统还没起来,是不可能完成物理地址到虚拟地址的映射,所以Bootloader使用的是器件的物理地址。
操作系统起来之后,操作系统中的MMU(内存管理单元)才将物理地址映射到虚拟地址。
Bootloader是一个裸机程序,由汇编和C语言组成。
第3章 硬件
3.0 芯片
三星生产了一颗芯片

3.1 地板+核心板
粤嵌拿着三星的核心板,做了一块开发板。

3.1 核心板

-
gec6818芯片;
- 以太网芯片;
- 电源管理芯片;
- EMMC(掉电不丢失的),8G的EMMC;
- DDR(两块DDR,每块是512M,组合起来是1G);
3.2 芯片的地址划分
三星公司生成出gec6818这颗芯片,并且围绕着这颗芯片做出了一块核心板,并且会给出一个芯片手册。gec6818芯片的芯片手册会给出地址划分示意图。粤嵌拿着这个芯片手册,找到地址划分示意图做二次开发出底板。

- 粤嵌在【0xC000 0000 ~0xE000 0000】这块区域上,设置LED灯,设置外设等等。
3.3 硬件的启动过程
3.3.1 分区和扇区的区别
一个图书馆的的某一层楼有三个房间,第一个房间放置数学相关的数据,第二个房间放置物理相关的书籍,第三个房间放置化学相关的书籍。
第一个放置数学书籍的房间内部有18和书架,每个书架上放置分类更精细的数学相关书籍。

每一个房间就是一个分区,每一个房间内部的书架就是一个扇区。
3.3.2 启动流程

- ARM上电之后,从片上的flash(IROM)中执行三星公司出厂就固化好的20K程序;
- 20K的程序主要作用是:由于Uboot的典型结构设计为2阶段,所以20K的程序是将扇区1的stage1的uboot代码拷贝到片上内存中(SRAM),并且从IROM跳转到SRAM上开始执行stage1上的代码;
- 由于stage1的uboot是56K左右,该56K的代码会完成1G的DDR3内存初始化。初始化完成之后会将从eMMC将stage2的uboot代码拷贝到DDR3中,并且开始执行。
- stage2的uboot代码开始运行之后,会将存储在eMMC中的内核代码拷贝到DDR3中进行解压。解压之后,启动内核。
- 内核一旦启动机会加载eMMC中的根文件系统。
- 根文件系统挂载完毕之后,就开始执行用户程序。

3.3.3 启动过程重要参数分析


通过板载设备信息验证

第4章 微处理器(MPU)

-
ARM公司生产内核
- 芯片厂商在内核上加外设,制作成芯片。(例如:瑞芯微,三星,海思);
- 设备厂商拿着芯片做板卡。(例如,正点原子,或者其他企业)
- 企业拿着芯片做产品(例如:昂立公司的继保测试仪)
4.0 ARM内核
ARM公司只是做红色部分的内核。

4.1 ARM内核中的寄存器

4.2 寄存器的组织结构
ARM处理器有37个32位的寄存器,其中31个为通用寄存器,6个为状态寄存器。
寄存器是在ARM内核中。

第一列用户模式和第二列的系统模式下都是使用这ARM啮合中的相同寄存器。
- R13是堆栈指针(SP)
- R14是链接寄存器(LR)
- R15程序计数器(PC)
- CPSR是当前程序状态寄存器
4.3 存储介绍

第5章 驱动
5.1 驱动开发步骤
以点亮LED灯为例。
- 第1步,看原理图寻找GPIOx口;
- 第2步,在芯片手册中寻找GPIOx的寄存器地址;
- 第3步,编写汇编代码;
- 第3.1步,配置GPIOx的时钟寄存器;
- 第3.2步,配置GPIOx的复用功能寄存器;
- 第3.3步,配置GPIOx的输入或者输出模型;
- 第3.4步,配置GPIOx的数据寄存器;
- 第4步,编写C语言代码;
- 第5步,编写Makefile脚本,进行交叉编译。
- 第6步,代码下载运行调试。
5.1.1 汇编编写
LedSC.S
#define WTCON 0xC0019000 // 看门狗的物理地址
#define GPIOxALTFN 0xC001E020 // 复用功能寄存器的物理地址
#define GPIOEOUTENB 0xC001E004 // GPIOE的输出使能寄存器的物理地址
#define GPIOEOUT 0xC001E000 // GPIOE的输出数据寄存器的物理地址
.global _start
_start:
// 关闭看门狗
LDR R0, =WTCON
LDR R1, [R0] // R1获取0xC0019000地址内的值
BIC R1, R1, #(1<<5) // WTCON[5] = 0; 取出R1的值,设置立即数,最后放回R1中
STR R1, [R0] // 0xC0019000地址内的值 = R1
// 复用功能寄存器设置 GPIOEALTFN[27:26] = 00
LDR R0, =GPIOxALTFN
LDR R1, [R0]
BIC R1, R1 #(3<<26)
STR R1, [R0]
// GPIOE的输出使能寄存器设置 GPIOEOUTENB[13] = 1
LDR R0, =GPIOEOUTENB
LDR R1, [R0]
ORR R1, R1 #(1<<13)
STR R1, [R0]
loop:
// GPIOE的输出数据寄存器设置 GPIOEOUT[13] = 1
LDR R0, =GPIOEOUT
LDR R1, [R0]
ORR R1, R1 #(1<<13)
STR R1, [R0]
// 延时
MOV R0, #0xFF00000
MOV R1, #50000
bl cdelay // 跳到c语言的cdelay函数去执行,执行完毕在跳转回来
// GPIOE的输出数据寄存器设置 GPIOEOUT[13] = 0
LDR R0, =GPIOEOUT
LDR R1, [R0]
BIC R1, R1 #(1<<13)
STR R1, [R0]
// 延时
MOV R0, #0xFF00000
MOV R1, #50000
bl cdelay // 跳到c语言的cdelay函数去执行,执行完毕在跳转回来
// 跳转到循环
b loop
5.1.2 C语言编写
cdelay.c
# 接收来至汇编语言的两个变量, 做延时处理
void cdelay(int val1, int val2){
int i;
for(i = 0; i < (val1 + val2); i++);
}
5.1.3 Makefile编写
Makefile
# 目标: 依赖
ledSC.bin: LedSC.o cdelay.o
arm-linux-Id -Ttext 0x40000000 -o ledSC.elf $^
arm-linux-objcopy -O binary ledSC.elf ledSC.bin
arm-linux-objdump -D ledSC.elf > ledSC_elf.dis
%.o: %.S
arm-linux-gcc -o $@ $< -c -nostdlib
%.o: %.c
arm-linux-gcc -o $@ $< -c -nostdlib
clean:
rm *.o *.elf *.bin *.dis -f
5.1.4 交叉编译
make
5.1.5 程序下载
(1)使用loadb下载程序【串口线下载】
- 将编译好的二进制文件【ledSC.bin】放置在win10的任意位置。
- 令开发板进入到下载模式。
- 使用如下命令,下载程序。
loadb 0x4000000 ledSC.bin
使用SecureCRT-->Transfer--->send Kermit 选中你要下载的二进制文件。

4. 再次使用如下命令开始执行二进制文件。让uboot跳转到地址【0x40000000】处去执行代码
go 0x40000000
现在可以观察到Led闪缩。
(2)使用tftp下载程序【网线下载】
- 将编译好的二进制文件【ledSC.bin】放置在linux的【/home/scholar/tftp】下。
- 令开发板进入到下载模式。
- 使用如下命令,下载程序。
tftp 0x40000000 ledSC.bin
4. 再次使用如下命令开始执行二进制文件
go 0x40000000
5.2 GPIO口
绘制树状图
5.3 驱动开发的4种经典步骤
- mmap映射型设计方法。【不推荐】
- 将芯片上的物理地址映射到用户空间的虚拟地址上,用户操作虚拟地址来操作硬件。
- 使用文件操作集(file_operatiopns)设计方法。【极致推荐】
- platfrom总线型设置方法。【比较流行】
- 设备树。【推荐】
5.4 Linux设备分类
- 字符设备
- LED
- 显卡
- 声卡
- USB
- 鼠标
- 键盘
- 触摸屏
- 块设备
- 硬盘
- nand flash
- SD卡
- U盘
- eMMC卡
- 网络设备
- 无线网卡
- 有线网卡
5.4.1 字符设备特点
1、应用程序和驱动程序交互数据时,是以字节为单位进行访问的。
2、访问的数据是连续的,而且实时的。
3、字符设备驱动不带缓存,而块设备驱动带有缓存。
5.4.2 块设备特点
5.4.3 网络设备的特点
第6章 文件系统

- 一个linux操作系统支持多个文件系统并存。
- 本地文件系统,ext4;
- 网络文件系统,NFS;
6.1 文件系统架构

实时细节:

6.2 NFS文件系统框架
6.3 文件下载
6.3.1 在裸机情况下下载文件
1.在未启动内核之前,进入uboot模型下(也就是没有操作系统的裸机情况下)进行文件下载。
2. 文件下载是指将服务器[ubuntu]中的文件下载到开发板。

(1) 使用串口下载文件
GEC6818# loadb 0x40000000 ledSC.bin
GEC6818# go 0x40000000
- loadb使用的是串口;
- loadb需要配合SecureCRT软件使用,并且需要下载的文件需要放置在win10的指定位置;
- go是跳转到DDR3的指定位置去运行代码。
(2)使用网口下载文件
GEC6818# tftp 0x40000000 ledSC.bin
GEC6818# go 0x40000000
- tftp使用的是网口,但是要保证服务器Ubuntu上安装了服务进程Vstftp;
- tftp相当于客户端,Vstftp相当于服务端,需要下载的文件需要放置在服务端的指定位置下;
- go是跳转到DDR3的指定位置去运行代码。
6.3.2 在操作系统情况下下载文件

(1)临时修改客户端IP(修改开发板IP)
开发板当前IP地址为:192.168.31.205

1. 寻找一个未被占用的IP
[root@GEC6818 /IOT]#ping 192.168.31.200
2. 修改IP地址为【192.168.31.200】
[root@GEC6818 /IOT]#ifconfig eth0 192.168.31.200

(2) 永久修改IP
vim /etc/profile
编辑上述文件,将如下命令添加到文件的最后一行。
ifconfig eth0 192.168.31.200
(3)使用网口下载文件
[root@GEC6818 /IOT]#tftp -g -r buttons_drv.ko 192.168.31.63
选项位置不要换。
-g get文件-r remote地址
(2)使用串口下载文件
- 需要借助SecureCRT的XModem协议下载。
- 将要下载的文件【ledSC.bin】放置在win10的任意位置,然后执行命令【rx ledSC.bin】,点击SecureCRT send Xmodem协议下载文件。
rx xxxxx.bin
6.4 挂载nfs网络文件系统
6.4.1 文件系统的加载方式一
通过OTG方式将文件系统镜像固化到eMMC的分区3中。
当内核启动之后就会去eMMC中加文件系统挂载到内核的根文件系统下,以此来完成文件系统的加载。
6.4.2 文件系统的加载方式二
- 应用场景
- 即使eMMC没有文件系统,以及eMMC中文件系统损坏。都可以采用下面的方式实现文件系统的挂载。
- 如果eMMC中的文件系统已经损坏,可以将开发板采集到的数据通过nfs【网口】传递到ubuntu上的文件系统。
当内核启动之后,通往nfs【网口】去挂载Ubuntu中的文件系统。
总结
- eMMC的分区0是存放uboot,分区1存放内核,分区2存放根文件系统;
- 系统启动之后,会将eMMC识别为块设备0【blk0】
- 当内核启动之后,会去将flash(eMMC)分区2中的根文件系统挂载到内核的根结点上,形成根文件系统;
- 制作根文件系统步骤
- 按照根文件系统的目录名称,制作目录;
- 根据目录名称填充相应的内容到对应的目录;
- 例如/bin目录下存放的操作系统命令,可以从BusyBox软件中拷贝到该目录下,也可以从交叉编译工具目录下拷贝到该目录下。
- 将所有目录利用某一种工具制作成某一个文件格式的文件。
Mkcramfs /home/zs/myroot myroot.img # 将指定目录制作为镜像文件
- 将该格式的文件固化到eMMC的分区中。
第7章 内核
7.0 linux系统结构

7.1 Linux操作系统内核

- 系统调用接口(系统IO)
- open()
- write()
- close()
- 函数库(标准IO)【C 标准库】
- fopen()
- fwrite()
- fclose()
7.2 微内核和宏内核
7.3 Linux内核目录结构
Linux内核的源代码目录中含有 14 个子目录,总共包括 102 个代码文件。下面逐个对这些子目录中的内容进行描述。
linux/ 目录是源代码的主目录,在该主目录中除了包括所有的 14 个子目录以外,还含有唯一的一个 Makefile 文件。该文件是编译辅助工具软件 make 的参数配置文件。make 工具软件的主要用途是通过识别哪些文件已被修改过,从而自动地决定在一个含有多个源程序文件的程序系统中哪些文件需要被重新编译。因此,make 工具软件是程序项目的管理软件。
linux/ 目录下的这个 Makefile 文件还嵌套地调用了所有子目录中包含的 Makefile 文件,这样,当 linux/ 目录(包括子目录)下的任何文件被修改过时,make 都会对其进行重新编译。因此为了编译整个内核所有的源代码文件,只要在 linux 目录下运行一次make 命令即可。

linux-5.10/
├── arch/ # Architecture-specific code: 不同CPU架构的核心实现
│ ├── arm/ # ARM 架构支持(32位)
│ ├── arm64/ # AArch64 架构支持(64位)
│ ├── x86/ # x86 架构支持(包括 32/64位 PC 平台)
│ ├── mips/ # MIPS 架构支持
│ ├── riscv/ # RISC-V 架构支持
│ ├── powerpc/ # PowerPC 架构支持
│ └── ... # 其他架构(如 sh, sparc, tile 等)
│ ├── boot/ # 启动代码(如汇编启动文件、设备树处理)
│ ├── kernel/ # 架构相关的内核核心代码(中断、上下文切换等)
│ ├── mm/ # 架构相关的内存管理(页表、TLB 操作)
│ └── libs/ # 架构特定的底层库函数(如 memcpy, memset 优化实现)
├── include/ # 全局头文件目录
│ ├── linux/ # 内核通用头文件(核心数据结构、API 声明)
│ ├── asm/ # 架构相关头文件的符号链接(指向 arch/$(ARCH)/include/asm)
│ ├── uapi/ # 用户空间 API 头文件(供用户程序包含,如系统调用接口)
│ └── asm-generic/ # 通用汇编头文件模板(可被多种架构复用)
├── init/ # 内核初始化代码
│ ├── main.c # 内核启动主函数 start_kernel() 所在文件
│ ├── do_mounts.c # 根文件系统挂载逻辑
│ ├── version.c # 内核版本信息定义(如 KERNEL_VERSION)
│ └── calibrate.c # BogoMIPS 校准(用于延时循环估算)
├── kernel/ # 内核核心子系统(进程、调度、时间、系统调用等)
│ ├── sched/ # 进程调度器实现(CFS、实时调度等)
│ ├── time/ # 时间子系统(时钟源、定时器、jiffies)
│ ├── fork.c # 进程创建(fork, clone 系统调用)
│ ├── exit.c # 进程退出处理
│ ├── sys.c # 系统调用实现(如 sys_getpid)
│ ├── kthread.c # 内核线程管理
│ └── params.c # 内核参数(module_param、boot param 解析)
├── mm/ # Memory Management: 内存管理子系统
│ ├── page_alloc.c # 物理内存页分配器(buddy system)
│ ├── slab.c # Slab 分配器(小对象内存池)
│ ├── vmalloc.c # 非连续虚拟内存分配
│ ├── memory.c # 内存热插拔支持
│ └── swap.c # 交换(Swap)机制实现
├── fs/ # File System: 文件系统实现
│ ├── ext4/ # ext4 文件系统
│ ├── proc/ # proc 虚拟文件系统(/proc)
│ ├── sysfs/ # sysfs 虚拟文件系统(/sys)
│ ├── binfmt_script.c # 脚本解释器支持(如 #!/bin/sh)
│ └── super.c # 超级块管理(mount/umount 核心逻辑)
├── net/ # Networking: 网络协议栈
│ ├── core/ # 网络核心(sk_buff, net_device 管理)
│ ├── ipv4/ # IPv4 协议实现
│ ├── ipv6/ # IPv6 协议实现
│ ├── unix/ # Unix 域套接字
│ └── netlink/ # Netlink 套接字(内核与用户通信)
├── drivers/ # 设备驱动程序(占源码最大比例)
│ ├── char/ # 字符设备驱动(如串口、mem)
│ ├── block/ # 块设备驱动(如 IDE、RAM disk)
│ ├── net/ # 网络设备驱动(如以太网卡、WiFi)
│ ├── usb/ # USB 子系统及设备驱动
│ ├── input/ # 输入设备驱动(键盘、鼠标、触摸屏)
│ ├── gpio/ # GPIO 子系统
│ ├── pinctrl/ # 引脚控制(Pin Control)
│ └── ... # 其他(如 spi, i2c, mtd, video 等)
├── sound/ # ALSA 音频子系统(Advanced Linux Sound Architecture)
│ └── core/ # 音频核心层、PCM、Mixer 等
├── firmware/ # 固件加载机制(如 request_firmware)
│ # 部分二进制固件也可存放于此(但通常在用户空间)
├── security/ # 安全模块框架(如 SELinux, Smack, AppArmor 支持)
├── crypto/ # 加密算法实现(如 AES, SHA, MD5)
│ └── algapi.c # 加密API接口
├── lib/ # 内核通用库函数
│ ├── string.c # 字符串操作(strcpy, strcmp 等)
│ ├── vsprintf.c # 格式化输出(printk 基础)
│ └── bitmap.c # 位图操作
├── scripts/ # 编译脚本与工具(Makefile 辅助脚本、kconfig 工具)
│ ├── kconfig/ # menuconfig、xconfig 等配置系统
│ └── Makefile.build # 模块编译规则
├── tools/ # 用于开发和调试的用户空间工具(如 perf, bpf)
│ # 可独立编译使用
├── usr/ # initramfs 支持(早期用户空间)
│ └── initramfs_list.sh # 构建 initramfs 文件列表
├── Kconfig # 根配置文件,包含顶层菜单和主配置项
├── Makefile # 根 Makefile,定义内核编译规则(如 ARCH, CROSS_COMPILE)
├── .config # (编译生成)当前配置选项(由 make menuconfig 生成)
├── vmlinux # (编译生成)未压缩的内核镜像(ELF 格式)
└── arch/$(ARCH)/boot/ # (编译生成)如 zImage, Image, Image.gz 等可启动镜像
7.4 Linux中配置文件
- Kconfig强调的是,我是什么。例如我是面粉,我是辣椒油。
- makefile强调的是,我要做什么。我要将面粉做成包子,我要将面粉和辣椒油做成兰州拉面。
- menuconfig强调的是,你需要哪些选项。我需要面粉,我不需要辣椒油。
- 我需要面粉,我不需要辣椒油。这些需要的信息和不需要的信息会被记录到【.config】文件中。
- make 就会拿着【.config】按照makefile上的做法做成想要的实物。
7.4.1 Kconfig
# =====================================================
# 食材配置(Ingredients Configuration)
# 本文件定义厨房可用的基础食材
# =====================================================
# 【核心食材】面粉支持
# 说明:面粉是制作面食的基础原料,如油条、拉面、饺子皮等
# 提示:不启用面粉,将无法制作任何面食!
config HAS_WHEAT_FLOUR # 我是面粉!这是我的“配置名”
bool "支持面粉 (Wheat Flour)" # 用户在菜单中看到的名字
default y # 默认:开启(我们是面馆,当然要有面粉!)
help
选择此项表示厨房具备高筋面粉供应。
面粉用于制作:
- 油条
- 拉面
- 饺子
- 煎饼
如果关闭此项,所有依赖面粉的菜品将无法制作。
推荐保持启用。
# 可选:是否使用全麦面粉?
# 依赖于“HAS_WHEAT_FLOUR”被启用
config USE_WHOLE_WHEAT
bool "使用全麦面粉"
depends on HAS_WHEAT_FLOUR # 只有有面粉,才能选全麦
default n
help
如果你希望提供更健康的全麦面食,请启用此项。
注意:口感略有不同。
7.5 内核移植
1. 修改内存配置文件
/home/scholar/test/6818GEC/kernel/arch/arm/plat-s5p6818/GEC6818/include/cfg_mem.h
修改DDR的起始地址和总共大小。

2. 配置菜单
/home/scholar/test/6818GEC/kernel
在上述目录下执行
make menuconfig
修改需要的配置项。

修改完配置项之后,会在目录下生成【.config】文件,文件中记录了需要编译的项目。
执行上述内核编译时,配置工具【mk】会使用自己内部设计好的配置项,也就是加载【GEC6818_defconfig】文件中的内容进行文件编译。然而我们已经使用menuconfig生成了新的配置项【.config】文件,所以我们需要将新的【.config】覆盖默认的【GEC6818_defconfig】文件内容。因此要先覆盖默认的配置之后,才能进行内核编译。
cp ./.config /home/scholar/test/6818GEC/kernel/arch/arm/configs/GEC6818_defconfig
覆盖默认配置之后,在执行内核编译。
./mk -k
7.6 zImage、uImage和boot.img的区别
根据上述的步骤会生成三个文件
- uImage适合调试使用,他比zImage多64byte。也就是说uImage = 64byte头部信息 + zImage;
64Byte记录了内核的起始地址和入口地址等信息。uImage可以通过串口或者tftp直接下载到内存中进行调试。
2. boot.img只是对uImage进行了一些格式转换,更适合存放到eMMC中和SD卡中。
当uImage调试没问题时,说明boot.img也没有问题。
7.6.1 串口下载uImage到内存
1. 将uImage拷贝win10的指定目录
使用xftp。或者其他软件
2. 在ARM端进入下载模式
- 下载服务器上的指定文件到指定位置
loadb 0x48000000 uImage
- 利用SecureCRT软件,将指定目录下的的指定文件【uImage】下载到指定位置

- 启动内核
bootm 0x48000000
这样就可以观察到启动的信息了
7.6.2 tftp下载uImage到内存
1. 拷贝到指定tftp文件目录下
cp /home/scholar/test/6818GEC/kernel/arch/arm/boot/uImage /home/scholar/tftp/
2. 在ARM端进入下载模式
- 查看环境变量
printenv
-
联通tftp服务器
ping 192.168.31.63
- 下载服务器上的指定文件到指定位置
tftp 0x48000000 uImage
- 启动内核
bootm 0x48000000
这样就可以观察到启动的信息了

能够进如操作系统,说明已经启动成功了。
7.6.3 OTG下载boot.img
7.7 总结

-
mk是一个编译脚本;
-
prebuilts是放置交叉编译链工具的目录;
-
配置好内核之后,进行编译。编译之后会产生zImage,uImage和boot.img。
-
uImage = 64Byte的头部信息 + zImage; uImage这个内核文件大概只有5M左右。
-
boot.img是uImage的文件变体,boot.bin稍微有一点点大,16.4M。
-
-
通过串口或者tftp可以将uImage内核文件下载到DDR中的0x48000000这个地址上去调试。
-
当uImage在DDR上调试没问题之后,说明boot.img也没有问题了。毕竟boot.img只是uImage内核文件的一个文件变体。
-
boot.img可以通过远程OTG技术将其固化到eMMC或者SD卡上,更适合生成时候使用。
-
如何误将zImage或者boot.img通过串口或者tftp下载到DDR的0x48000000,启动内核会失败。
-
启动内核的uboot指令是【bootm 0x48000000】。
-
内核移植是一个复杂的过程,需要丰富的工程经验。建议初学者还是关注驱动或者Qt开发。等时间到了或者时间成熟之后,你在回来看看内核移植和uboot移植会轻松很多。
第8章 Bootloader
- Bootloader是操作系统没有启动之前运行的一段裸机程序。
- Uboot是Bootloader的一种,嵌入式ARM端通常都使用它。win10一般使用BIOS。
- Uboot一般有两种模式,启动加载模式(自主模式),一种是下载模式。
8.1 Bootloader的文件传输
8.1.1 串口
采样以太网传输时,利用xmodem/ymodem/zmodem/协议进行下载。
SecureCRT软件中天然就有这些协议。

8.1.2 以太网
采样以太网传输时,利用TFTP协议。
TFTP,全称是 Trivial File Transfer Protocol(简单文件传输协议),基于 UDP 的69端口实现,是最简单的文件传输网络协议,该协议只能从远程服务器读取文件或向远程服务器上传文件。
虽然 TFTP 不具备 FTP 的许多功能,但是实现简单,内存占用很小,在uboot等小型平台上也能实现。

ARM板字相当于客户端,宿主机相当于服务器。
客户端只需在uboot源码中实现TFTP功能,服务器端只是需要下载TFTP协议服务器。宿主机(服务器)可以是WIn10, 可以是虚拟机中的Ubuntu。如果是Win10下载Tftpd32/64.exe

Ubuntu中只需要安装【vsftpd】作为服务器端就好。
8.1.3 USB
进入Uboot的下载模式,在将开发板作为安卓端,利用adb,采样USB-OTG就可以下载。
1. 将开发板设置为安卓设备
GEC6818# fastboot # 将开发板设置为安卓设备
2. 执行win10上的bat脚本。
./auto.bat
执行完上述的两步之后,就会将uboot,内核,文件系统中的任意镜像固化到eMMC中。
8.2 Bootloader的典型结构
Bootloader一般分为两阶段。阶段1采用汇编语言编写,阶段2采用C语言编写。
需要强调的是:Bootloader也可以设置为单阶段。
8.2.1 stage1
-
设置处理器模式
- 关闭中断、FIQ。
- 设置 CPU 为 SVC(管理模式)。
-
CPU 及核心寄存器初始化
- 初始化 MMU(可选,通常 Stage 2 才开启)。
- 配置异常向量表。
- 清除数据和指令缓存。
-
初始化关键外设
- 初始化 时钟系统(Clock):设置 PLL,为主控提供稳定时钟。
- 初始化 内存控制器(SDRAM Controller):这是最关键的一步,为外部 DDR 内存建立访问通道。
-
复制 Stage 2 到 DDR 内存
- 将 U-Boot 的第二阶段代码(Stage 2)从存储设备(如 Flash、eMMC、SPI NOR/NAND)复制到已初始化的 DDR 内存中。
-
设置堆栈(Stack)
- 在 SRAM 或已初始化的内存中建立堆栈,为 C 语言执行做准备。
-
跳转到 Stage 2 的入口函数
- 使用
bl或b指令跳转到 Stage 2 的 C 语言入口(通常是start_armboot)。
- 使用
8.2.2 stage2
-
继续硬件初始化
- 初始化 GPIO、UART(用于串口调试输出)。
- 初始化看门狗(Watchdog)、定时器等。
-
设置堆(Heap)和栈(Stack)
- 为 U-Boot 运行时使用
malloc等函数准备内存空间。
- 为 U-Boot 运行时使用
-
初始化设备驱动
- NAND/NOR Flash、eMMC/SD 卡、网络(Ethernet)、USB 等。
- 使 U-Boot 能从多种设备加载内核镜像。
-
环境变量(Environment)初始化
- 从 Flash 中加载
env(环境变量),如bootcmd、ipaddr、serverip等。 - 若无则使用默认环境变量。
- 从 Flash 中加载
-
打印启动信息
- 输出 U-Boot 版本、CPU 信息、内存大小、环境变量等。
-
设备重新定位(Relocation)
- 将 U-Boot 自身从加载地址复制到最终运行地址(通常是 DDR 高地址)。
- 更新
gd(global data)和bd(board info)结构体指针。
-
启动内核前的准备
- 加载 Linux 内核镜像(如
zImage、uImage)和设备树(.dtb)到内存。 - 可选:加载 initramfs。
- 设置内核启动参数(
bootargs)。
- 加载 Linux 内核镜像(如
-
启动 Linux 内核
- 调用
do_bootm_linux()或类似函数,跳转到内核入口。 - 关闭 CPU,将控制权交给 Linux 内核。
- 调用
8.3 Uboot的命令
- 显示当前环境变量
printenv
2. 设置或修改环境变量
setenv ipaddr 192.168.1.50
setenv serverip 192.168.1.1
setenv bootargs console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait
3. 将当前环境变量保存到非易失性存储(如 Flash、EEPROM)
saveenv
4. 测试与 TFTP 服务器的网络连通性(需支持 NETPING)
ping 192.168.1.100
5. 通过 TFTP 协议从服务器下载文件到内存
tftp 0x80000000 LedSC.bin
6. 通过串口使用 ymodem 协议加载文件
loadb 0x80000000
7. 启动一个已加载的 Linux 内核镜像
bootm 0x80000000
8. 启动压缩的 Linux 内核镜像(zImage / Image) + 设备树(dtb)
bootz 0x80000000 - 0x81000000
bootz <内核地址> <initrd地址,- 表示无> <dtb地址>
9. 跳转到指定地址执行代码(常用于运行裸机程序)
go 0x80000000
更多推荐







所有评论(0)