1. 嵌入式系统概述:从一块板子到完整系统

大家好,我是你们这门课的老朋友。做了十几年嵌入式,从最早的 8 位单片机一路折腾到现在的多核 ARM,踩过的坑比走过的路还多。今天咱们先聊聊最基础的东西——到底什么是嵌入式 Linux 系统。

你想想看,我们平时用的电脑,装个 Windows 或者 Ubuntu,插上键盘鼠标就能用。但嵌入式设备不一样。它没有显示器,没有标准键盘,甚至可能连个像样的外壳都没有。它被“嵌入”到某个设备里,默默干活。

嵌入式 Linux 系统 = Linux 内核 + 根文件系统 + 应用软件,运行在资源受限的专用硬件上。

1.1 什么是嵌入式 Linux 系统

说白了,就是把 Linux 这套东西,裁剪到能在小存储、低功耗的板子上跑起来。我最早接触嵌入式 Linux 是在 2008 年,那时候用的还是 2.6 内核,Flash 只有 8MB,RAM 才 32MB。现在想想,那点资源连个微信都装不下。

嵌入式 Linux 和桌面 Linux 最大的区别在哪?

  • 资源受限:CPU 主频低,内存小,存储空间有限
  • 硬件固定:不像 PC 可以随便换显卡、加内存,嵌入式硬件是定死的
  • 功能专一:一个设备只干一件事,比如路由器只管转发数据包
  • 实时性要求:很多场景需要确定性的响应时间,比如工业控制

嗯,这里要注意。嵌入式 Linux 不等于“小 Linux”。它不是一个缩水版,而是一个定制版。你不需要的东西,统统砍掉。我见过有人把内核裁剪到不到 1MB,照样跑得稳稳的。

1.2 交叉编译的概念

这个概念,新手最容易搞混。什么是交叉编译?

你在你的电脑上写代码、编译,生成的可执行文件却要在另一台设备上运行。这台设备可能是个 ARM 架构的开发板,或者是个 MIPS 架构的路由器。你的电脑是 x86 架构,目标板是 ARM 架构,指令集都不一样,当然不能直接编译。

我的习惯:在宿主机上搭建交叉编译工具链,编译出目标板能运行的二进制文件。这个工具链包含交叉编译器、交叉链接器、交叉调试器等。

举个例子:

# 宿主机上编译一个简单的 hello.c
# 使用交叉编译器,目标架构是 ARM
arm-linux-gnueabihf-gcc -o hello hello.c

# 查看生成的文件类型
file hello
# 输出:ELF 32-bit LSB executable, ARM, version 1 (SYSV)

看到没?这个 hello 文件是 ARM 格式的,在 x86 电脑上跑不了。你得把它拷贝到开发板上才能执行。

我曾经犯过一个低级错误——在宿主机上编译了一个程序,直接拷贝到板子上,然后报错“Exec format error”。查了半天才发现,我用的编译器是 x86 的 gcc,不是交叉编译器。嗯,从那以后,我每次编译前都会确认一下工具链前缀。

3 目标板与宿主机的关系

这两个概念,我建议你从一开始就分清楚。

角色 定义 典型设备 主要任务
宿主机 你用来开发的电脑 PC、笔记本(x86) 编写代码、交叉编译、调试
目标板 最终运行系统的硬件 ARM 开发板、路由器、工控机 运行编译好的内核和应用程序

它们之间怎么通信?最常见的方式有几种:

  • 串口:最基础的方式,通过 UART 连接,用 minicom 或 putty 登录
  • 网络:通过以太网或 WiFi,用 SSH 登录,或者用 NFS 挂载根文件系统
  • JTAG/SWD:硬件调试接口,用来烧录 Bootloader 或调试内核
  • USB:有些板子支持 USB gadget,模拟成网卡或串口

避坑指南:我曾经在调试一块新板子时,串口线接反了 TX 和 RX,折腾了半小时没输出。后来用万用表一量,才发现线序不对。记住,串口线要交叉连接——TX 接 RX,RX 接 TX。

1.4 嵌入式系统的典型组成

一个完整的嵌入式 Linux 系统,从底层到上层,通常由三大部分组成。我画了一张图,帮你理清它们的关系。

嵌入式 Linux 系统分层架构 应用层 用户应用程序、守护进程、脚本 根文件系统层 /bin /sbin /etc /lib /usr /dev /proc /sys Linux 内核层 进程调度、内存管理、文件系统、设备驱动、网络协议栈 Bootloader 层 U-Boot、Barebox、GRUB——初始化硬件,加载内核 启动顺序:上电 → Bootloader → 内核解压 → 挂载根文件系统 → 运行 init 进程

从上图可以看得很清楚,系统启动是有顺序的。我一个个来讲。

1.4.1 Bootloader

板子上电后,CPU 会从某个固定地址开始执行代码。这个地址上放的就是 Bootloader。它的任务很简单:初始化硬件,加载内核到内存,然后跳转到内核入口。

最常见的 Bootloader 是 U-Boot。我习惯用 U-Boot 2018 以后的版本,支持设备树,配置起来方便很多。

# U-Boot 常用命令示例
# 查看内存信息
bdinfo

# 从网络加载内核
tftp 0x42000000 zImage

# 启动内核
bootz 0x42000000 - 0x43000000

我的经验:调试新板子时,先把 U-Boot 跑起来。只要能进 U-Boot 命令行,后面的事情就好办了。如果 U-Boot 都起不来,先检查时钟配置和 DDR 初始化参数。

1.4.2 Linux 内核

内核是系统的核心。它管理硬件资源,提供系统调用接口。嵌入式场景下,内核需要裁剪——去掉不需要的驱动和功能,减小体积,加快启动速度。

我见过有人把内核从 5MB 裁剪到 1.2MB,启动时间从 8 秒降到 2 秒。怎么做到的?

  • 去掉不用的文件系统支持(比如 XFS、Btrfs)
  • 去掉不用的网络协议(比如 IPX、DECnet)
  • 去掉调试选项和打印信息
  • 把驱动编译成模块,按需加载

1.4.3 根文件系统

内核启动后,需要挂载一个根文件系统。这里面放着所有用户空间的程序、库文件、配置文件、设备节点等。

根文件系统的选择,取决于你的需求:

类型 特点 适用场景
initramfs 集成在内核中,内存中解压 早期启动、调试、临时系统
JFFS2/UBIFS 支持 NAND Flash,有磨损均衡 产品量产,需要持久化存储
ext4 标准 Linux 文件系统 SD 卡、eMMC、硬盘
SquashFS 只读、高压缩比 固件升级、只读根文件系统
NFS 网络挂载,开发调试方便 开发阶段,频繁修改文件

避坑指南:我曾经在产品量产时用了 JFFS2,结果发现频繁写入导致 Flash 坏块增多。后来换成 UBIFS,配合 UBI 层的坏块管理,问题才解决。选文件系统时,一定要考虑 Flash 类型和写入频率。

1.5 本章小结

这一章我们聊了嵌入式 Linux 的基本概念。说白了,就是三件事:

  1. 交叉编译——在宿主机上编译,在目标板上运行
  2. 系统组成——Bootloader 引导,内核管理硬件,根文件系统提供用户空间
  3. 开发模式——宿主机开发,目标板运行,通过串口或网络通信

这些概念,后面每一章都会用到。尤其是交叉编译和系统组成,是整门课的基石。我建议你先把这些搞透,再往下走。