Skip to content

Commit 1b383dc

Browse files
feat: finish most main body
1 parent 04486cc commit 1b383dc

8 files changed

Lines changed: 475 additions & 8 deletions

File tree

tutorial/cross-compile/index.md

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,70 @@
11
---
22
title: 交叉编译
3-
description: 在我电脑上编译、在板子上运行——嵌入式的定义动作。
3+
description: 在我电脑上编译、在板子上运行——嵌入式的定义动作。两个前缀、newlib 对 glibc、硬浮点 ABI 的坑。
44
order: 6
5+
tags:
6+
- arm-none-eabi
7+
- cross-compile
8+
- intermediate
9+
difficulty: intermediate
10+
prerequisites:
11+
- "GCC 裸机、链接脚本与 objcopy(构建系统卷)"
512
---
613

7-
# 交叉编译
14+
# 交叉编译:嵌入式的定义动作
815

9-
交叉编译是嵌入式的定义动作:host 上编译、产出给 target 跑。这一卷讲透两个前缀(`arm-none-eabi-` 裸机 vs `arm-linux-gnueabihf-` Linux)、newlib 对 glibc、硬浮点 ABI 的坑。
16+
## 什么是"交叉",为什么嵌入式的命根子就是它
1017

11-
> 🚧 规划中。前置:[GCC 裸机与 objcopy](../build-system/gcc/03-bare-metal)[CMake 工具链文件](../build-system/cmake)
18+
我们先把这个词钉死。平时你在电脑上 `gcc main.c -o main`、编出来的程序就在你这台电脑上跑——编译器和运行程序的机器是同一台、同一种 CPU 架构,这叫**本地编译(native compile)**。嵌入式几乎永远不是这样:你在 x86 的电脑上写代码、编译,但编出来的程序要在一颗 ARM 的芯片上跑——编译器和目标机不是同一种架构,这种"跨架构"的编译就叫**交叉编译(cross compile)**。可以说,能不能顺畅地交叉编译,是嵌入式和普通软件开发最本质的分界线,所以这一篇我们把它彻底讲透。
19+
20+
交叉编译的关键是一套**前缀不同的工具链**。你电脑上那个普通 `gcc` 编的是 x86;要编 ARM,你得装一个名字带前缀的交叉编译器,比如 `arm-none-eabi-gcc`。这个前缀本身就是信息——我们拆开看:`arm` 是目标架构(ARM)、`none` 表示没有操作系统(裸机)、`eabi` 是 ARM 的嵌入式应用二进制接口(Embedded ABI)规范。所以 `arm-none-eabi-gcc` = "编 ARM 裸机、按 EABI 规约"的 gcc。
21+
22+
## 两个前缀,代表两条完全不同的路
23+
24+
嵌入式里你会撞上两种主流的交叉工具链,名字长得很像、但服务的场景天差地别,必须分清。第一种是 `arm-none-eabi-`,它编的是**裸机**——没操作系统、程序直接跑在金属上,配套的 C 库是 **newlib**(一个为裸机/资源受限环境精简的 libc)。第二种是 `arm-linux-gnueabihf-`,它编的是**跑在嵌入式 Linux 上**的程序,配套的是完整的 **glibc**(桌面 Linux 那套标准 C 库),末尾的 `hf` 表示硬浮点 ABI。
25+
26+
我们这台机器上装的是 `arm-none-eabi-gcc (Arch Repository) 14.2.0`,所以下面裸机相关的命令我们都是真跑、能看真实输出;而 `arm-linux-gnueabihf-gcc` 没装(它的用途是给跑 Linux 的板子编译用户态程序,得单独装 `gcc-arm-linux-gnueabihf`),涉及它的地方我会标注清楚、不伪造它的输出。一句话区分:**给 Cortex-M 这类裸机/RTOS 芯片编,用 `arm-none-eabi-`;给跑嵌入式 Linux 的板子(树莓派、i.MX 上的 Linux)编用户态程序,用 `arm-linux-gnueabihf-`**
27+
28+
## newlib 对 glibc:C 库的选择
29+
30+
C 库就是你程序里 `printf``malloc``strlen` 这些函数的来源。裸机世界用的是 newlib,它体积小、可裁剪,但有个前提——它的很多函数(尤其 `printf` 这种要输出的)依赖底层提供"字符怎么发出去"的系统调用桩(叫 `_write``_sbrk` 之类),你得自己实现这些桩、或者用半主机(semihosting)让调试器替你转发。newlib 还有个 nano 变体(`--specs=nano.specs`),进一步砍掉 printf 的体积,Cortex-M 项目几乎标配。而 glibc 是桌面/服务器 Linux 那套又大又全的 libc,跑嵌入式 Linux 的板子才用它。这个区别决定了你链接时报的库、写的系统调用桩都不一样,是你选工具链时第一个要想清楚的事。
31+
32+
## 硬浮点 ABI:一个能让你链接时炸开的坑
33+
34+
接下来这个坑,是新手交叉编译时最常见的"明明编过了、链接却报一堆 undefined"。它叫**浮点 ABI**。ARM 芯片有的带硬件浮点单元(FPU)、有的不带;浮点运算既可以由 FPU 直接算(硬浮点 hard),也可以用软件模拟(软浮点 soft)。问题在于:**库是按某种 ABI 编的,你的程序必须和它一致**——你用 `-mfloat-abi=hard` 编、却链了一个 soft 的库,链接器就找不到符号(比如硬浮点要 `__aeabi_dadd`,软浮点库给的是另一套),报一堆 undefined。
35+
36+
我们用真实输出看清楚两者的区别。写一个算圆面积的浮点函数,分别用软、硬浮点编:
37+
38+
```bash
39+
# 软浮点:浮点运算走软件库
40+
arm-none-eabi-gcc -c -mcpu=cortex-m4 -mthumb -mfloat-abi=soft -O2 fpu.c -o fpu_soft.o
41+
42+
# 硬浮点:浮点运算用 FPU 指令
43+
arm-none-eabi-gcc -c -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -O2 fpu.c -o fpu_hard.o
44+
```
45+
46+
我们反汇编硬浮点那个产物,能看到它用的是货真价实的 FPU 指令:
47+
48+
```text
49+
0: eddf 7a03 vldr s15, [pc, #12] ; 把常数装进浮点寄存器
50+
4: ee60 7a27 vmul.f32 s15, s0, s15 ; 硬件浮点乘法
51+
8: ee27 0a80 vmul.f32 s0, s15, s0
52+
```
53+
54+
`vldr``vmul.f32` 这些 `v` 开头的指令就是 FPU 在干活。而软浮点产物里你 grep 不到任何这类指令——它的浮点运算是靠调用 `__aeabi_fmul` 之类的软件函数实现的,又慢又占空间。所以只要芯片带 FPU(Cortex-M4F/M7 都带),就一定要用 `-mfloat-abi=hard -mfpu=<对应型号>`,把浮点交给硬件,性能和体积都更好。
55+
56+
> ⚠️ 注意,硬浮点不只是"编自己代码"时加个 flag 的事——**你链的所有库(newlib、HAL、第三方)都必须是同一个 ABI 编的**。混链是交叉编译第一杀手:症状就是链接阶段一堆 `undefined reference to __aeabi_xxx`,根因是你的程序是 hard、某个库是 soft。一旦撞上,先核对所有参与链接的东西是不是统一的 `-mfloat-abi`
57+
58+
## 用工具链文件把这套固化下来
59+
60+
每个目标芯片的 `-mcpu``-mthumb``-mfloat-abi``-mfpu` 这堆 flag 老是手敲很容易错,正经做法是写进构建系统里固化。如果你用 CMake,就写一个工具链文件(`CMAKE_TOOLCHAIN_FILE`)把这些 flag 跟编译器路径一起声明死,构建时 `-DCMAKE_TOOLCHAIN_FILE=arm-none-eabi.cmake` 传进去——这套我们在 CMake 那一篇讲过。如果你用 Makefile,就把它们放进 `CFLAGS` 变量。无论哪种,核心都是:**让"目标芯片的 ABI 参数"成为一份可复用的配置,而不是每次手敲**
61+
62+
## 小结
63+
64+
交叉编译是嵌入式的定义动作:在 host 上编、产出给 target 跑。两条路要分清——`arm-none-eabi-` 配 newlib 编裸机、`arm-linux-gnueabihf-` 配 glibc 编嵌入式 Linux 用户态。浮点 ABI 是最大的坑:硬浮点(`-mfloat-abi=hard`)用 FPU 指令(`vldr`/`vmul.f32`),但要求你链的所有库都是同一 ABI,否则链接报 undefined。把 `-mcpu/-mthumb/-mfloat-abi/-mfpu` 这套用工具链文件固化,别每次手敲。能把这些讲清楚、选对工具链和 ABI,你就跨过了嵌入式最关键的那道坎。
65+
66+
## 下一站
67+
68+
编译链搞定了,可没硬件怎么办?下一篇讲 QEMU——让你在电脑上模拟出一台 ARM 机器,没板子也能把程序跑起来、看输出。
69+
70+
> 下一卷:[模拟与复现 · QEMU 无硬件开发使能器](../simulation/qemu)

tutorial/debugging/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ order: 5
99
固件跑飞、行为异常,靠 `printf` 不够用。这一卷讲 GDB(从 host 段错误一路到连硬件探针的远程调试)和串口(嵌入式世界里几乎唯一的输出观测手段)。
1010

1111
- [GDB:从段错误到远程调试](./gdb) —— 断点/单步/backtrace、core dump、`target remote` + openocd、硬件断点、`-O2` 变量消失
12-
- 串口 = 嵌入式 stdout(规划中)—— 波特率、stty/minicom、dialout 权限、QEMU 讲协议 + 物理 bring-up
12+
- [串口:嵌入式的 stdout](./serial) —— 波特率/8N1/电平、QEMU 无硬件验证、USB-TTL + minicom 物理 bring-up

tutorial/debugging/serial.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
title: "串口:嵌入式的 stdout"
3+
description: "为什么串口是嵌入式世界里几乎唯一的输出观测手段、协议(波特率/8N1/电平)怎么理解,怎么用 QEMU 无硬件验证、再用 USB-TTL + minicom 在真板上抓日志。"
4+
order: 2
5+
tags:
6+
- host
7+
- serial
8+
- qemu
9+
- intermediate
10+
difficulty: intermediate
11+
prerequisites:
12+
- "终端与 Shell 进阶(权限、/dev 设备文件)"
13+
- "GDB(本卷)"
14+
---
15+
16+
# 串口:嵌入式的 stdout
17+
18+
## 为什么串口是嵌入式的命脉
19+
20+
我们写普通程序,想看个变量直接 `printf`、终端就出来了——因为底下有操作系统、有终端驱动替你把字送到屏幕上。可嵌入式不一样:你的程序跑在裸机芯片上,没有屏幕、没有操作系统、更没有 `printf` 默认就去的那条路。那你怎么知道程序跑到哪了、变量是多少、有没有崩?答案几乎只有一条——**串口(UART)**。芯片上有串口硬件、你拿一根 USB-TTL 线把芯片的 TX/RX 接到电脑,程序往串口寄存器里写字节,电脑这边用工具收下来显示,这就是嵌入式版的 `printf`、也就是你的"stdout"。可以这么说:嵌入式开发离了串口几乎寸步难行,它是你观测板子状态最主要的窗口。
21+
22+
这一篇我们先讲清楚串口的协议(波特率、数据帧、电平),再用 QEMU 在**没有真硬件**的情况下把"程序写字节 → 屏幕显示"这条路真跑通给你看,最后讲真板子怎么用 USB-TTL 和 minicom 抓日志。
23+
24+
## 协议:波特率、8N1、电平
25+
26+
串口通信的核心概念其实就三个。**波特率(baud rate)**是双方约定的"每秒传多少位"的速度,必须收发两端设成一样,常见的有 9600、115200(嵌入式最常用)。**数据帧格式**最常见的是"8N1"——8 个数据位、无校验位(No parity)、1 个停止位,一个字节加上起始位停止位一共 10 位在线上跑。**电平**是另一个新手最容易烧板子的点:芯片那边的串口是 TTL 电平(3.3V 表示高),而你电脑老式串口是 RS232 电平(±12V,方向相反),**两者绝不能直连**,直连轻则通信不通、重则烧芯片。我们日常用 USB-TTL 转换器(一头 USB、一头 3.3V TTL),它在内部把 USB 转成 3.3V TTL,正好跟芯片对接;接的时候 TX 接 RX、RX 接 TX(交叉接),GND 共地,别忘了 VCC 看芯片是 3.3V 还是 5V 别接错。
27+
28+
> ⚠️ 注意,最常见的"串口收到一堆乱码"故障,九成是波特率两端不一致——芯片按 115200 发、你这边按 9600 收,解出来的全是乱码。看到乱码第一反应不是怀疑程序,而是核对接收端的波特率跟程序里设的是否一致。
29+
30+
## 没有硬件?QEMU 帮你把这条路跑通
31+
32+
我们不依赖真板子,先在 QEMU 里把"程序往串口写、屏幕显示出来"这条路完整验证一遍——这正好也是后面 QEMU 那一篇的预热。QEMU 的 `virt` 机器模拟了一个 PL011 串口控制器,它的数据寄存器固定在地址 `0x09000000`,我们往这个地址逐字节写字符,QEMU 用 `-nographic` 启动时会把这条串口路由到你电脑的 stdout。先写一个最小裸机程序:
33+
34+
```c
35+
/* serial_hello.c —— 往 QEMU virt 机器的 PL011 串口数据寄存器逐字节写 */
36+
volatile unsigned int *const UART0_DR = (unsigned int *)0x09000000;
37+
void _start(void) {
38+
const char *s = "Hello from QEMU bare-metal!\r\n";
39+
while (*s) *UART0_DR = (unsigned int)*s++;
40+
for (;;); /* 写完死循环停住 */
41+
}
42+
```
43+
44+
这个程序没有任何依赖——没有 libc、没有启动文件,`_start` 就是入口,干的事就是把字符串里每个字符写到那个固定的串口寄存器地址。我们用交叉编译器编成 ARM 裸机 ELF、链接到 `virt` 机器的内存起始地址 `0x40000000`:
45+
46+
```bash
47+
arm-none-eabi-gcc -nostdlib -ffreestanding -mcpu=cortex-a15 -O2 \
48+
-Wl,-Ttext=0x40000000 serial_hello.c -o serial_hello.elf
49+
```
50+
51+
然后用 QEMU 跑起来(`-M virt` 选机型、`-nographic` 把串口接到 stdout、`-kernel` 加载我们的 ELF):
52+
53+
```bash
54+
qemu-system-arm -M virt -cpu cortex-a15 -m 128M -nographic -kernel serial_hello.elf
55+
```
56+
57+
跑出来的结果是真真切切的一行字:
58+
59+
```text
60+
Hello from QEMU bare-metal!
61+
```
62+
63+
(注:这个程序在 QEMU 11.0.1、`virt` 机型上实测通过,输出就是上面这行;写完它会进死循环挂住,按 `Ctrl+A X` 退出 QEMU。)
64+
65+
这条链路打通的意义在于:**它用零硬件证明了一个嵌入式程序的输出是怎么从芯片流到屏幕的**——程序写字节到串口寄存器 → 串口控制器按 8N1、约定波特率把位打到线上 → 接收端(这里是 QEMU)按同样的约定把位拼回字节 → 显示。真板子上唯一多了的,是中间那根 USB-TTL 线和一个 minicom 之类的接收工具。
66+
67+
## 真板子:USB-TTL + minicom 抓日志
68+
69+
到了真板子,我们用 USB-TTL 线把芯片串口接到电脑 USB,插上后设备出现在 `/dev/ttyUSB0`(Linux)。接收这头我们用 minicom 或 picocom——它们就是"串口版的终端",按你设的波特率收字节、显示成字符。先确认设备权限(终端那一卷讲过,串口设备属 `dialout`/`uucp` 组,没权限会 permission denied):
70+
71+
```bash
72+
ls -l /dev/ttyUSB0
73+
groups # 看自己在不在 dialout(uucp) 组
74+
sudo usermod -aG dialout $USER # 不在就加上,然后重新登录生效
75+
```
76+
77+
然后用 minicom 打开串口、设好波特率(比如 115200、8N1)。minicom 是交互式的,进去之后一般 `Ctrl+A Z` 调菜单、`Ctrl+A Q` 退出;打开时可以直接 `-b` 指定波特率、`-D` 指定设备:
78+
79+
```bash
80+
minicom -b 115200 -D /dev/ttyUSB0
81+
```
82+
83+
> 说明:minicom / picocom 这类工具和 `/dev/ttyUSB0` 设备需要真实硬件——本机没有 USB-TTL 和板子,所以这一节的命令是你在有硬件时跑的,我们没法在这里贴真实的串口接收输出。如果你只想快速看一眼串口、又不想装 minicom,Linux 自带的 `stty` 也能配串口参数、再 `cat /dev/ttyUSB0` 直接读,够应急用。
84+
85+
想存日志的话,minicom 里能 `Ctrl+A Z` 开 log 到文件;或者命令行直接 `cat /dev/ttyUSB0 | tee serial.log` 把收到的字节同时显示和存盘,跟我们在终端篇讲的 `tee` 一回事。
86+
87+
## 小结
88+
89+
串口是嵌入式的 stdout——裸机上没有操作系统的 `printf`,程序往串口寄存器写字节、电脑这头用工具收下来显示,就是你看板子状态的主要窗口。协议三件事:波特率两端必须一致(不一致就乱码)、最常见 8N1 帧、TTL 电平(3.3V)绝不能跟 RS232 直连、接法是 TX 交叉接 RX。没硬件时用 QEMU 的 `virt` 机型(PL011 串口在 `0x09000000`)能零成本把"写字节→显示"这条路真跑通。真板子上 USB-TTL + minicom(`-b 波特率 -D /dev/ttyUSB0`),记得先把串口设备权限(dialout/uucp 组)搞定。串口是你和板子之间最基础也最可靠的那条观测通道。
90+
91+
## 下一站
92+
93+
串口讲完了,调试这一卷(GDB + 串口)就齐了。接下来如果你手里暂时没板子、又想跑 ARM 程序,下一篇 QEMU 是你的无硬件开发使能器——这一篇里那个 `virt` 机型的程序,下一篇会展开讲。
94+
95+
> 下一卷:[模拟与复现 · QEMU 无硬件开发使能器](../simulation/qemu)

tutorial/environment/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ order: 2
1010

1111
- [WSL2 + Linux + VS Code 远程开发](./wsl) —— Windows 上跑真 Linux、装交叉工具链、Remote 开发
1212
- [终端与 Shell 进阶](./terminal) —— PATH / 环境变量 / 管道重定向 / 权限 / 进程 / 设备文件(串口、dd 烧写)
13-
- VS Code 与交叉编译器(规划中)
13+
- [VS Code:指向交叉编译器](./vscode) —— compilerPath 指向 arm-none-eabi-gcc、Cortex-Debug + openocd 一键调试

0 commit comments

Comments
 (0)