1.4 嵌入式 C 与最小程序
教程目录
1. 平台与工具链
- 1.1 嵌入式系统与软件路线
- 1.2 MCU、MPU、SoC 与执行环境
- 1.3 芯片手册、开发板资料与引脚复用
- 1.4 嵌入式 C 与最小程序
- 1.5 开发环境与交叉编译
- 1.6 构建、烧录与调试
- 1.7 SWD、JTAG 与片上调试
- 1.8 复位、启动与链接
2. MCU 裸机开发
- 2.1 MCU、地址空间与外设
- 2.2 内存映射 I/O、寄存器与 volatile
- 2.3 时钟与复位
- 2.4 GPIO
- 2.5 中断与 NVIC
- 2.6 定时器、PWM 与系统节拍
- 2.7 UART
- 2.8 SPI 与 I²C
- 2.9 ADC、DAC 与 DMA
- 2.10 看门狗、低功耗与故障处理
3. 驱动、通信与中间件
- 3.1 驱动接口、CMSIS 与设备头文件
- 3.2 HAL、LL 与 BSP
- 3.3 轮询、中断、DMA 与非阻塞驱动
- 3.4 数据帧、环形缓冲区与状态机
- 3.5 CAN 与 Modbus
- 3.6 USB
- 3.7 以太网、TCP/IP 与 lwIP
- 3.8 BLE、Wi-Fi 与通信模块
- 3.9 Flash、EEPROM、SD 与文件系统
4. RTOS 与固件架构
- 4.1 临界区与 RTOS 边界
- 4.2 任务、调度与时间
- 4.3 队列、信号量、互斥量与事件
- 4.4 中断与任务协作
- 4.5 内存与资源管理
- 4.6 优先级反转、死锁与实时性
- 4.7 FreeRTOS
- 4.8 RT-Thread
- 4.9 RTOS 固件架构、跟踪与测试
5. 嵌入式 Linux
- 5.1 嵌入式 Linux 与系统编程边界
- 5.2 BootROM、U-Boot、内核与根文件系统
- 5.3 交叉工具链、sysroot 与根文件系统
- 5.4 Buildroot 与 Yocto
- 5.5 设备树
- 5.6 内核模块与驱动模型
- 5.7 字符设备与用户空间接口
- 5.8 platform、I²C 与 SPI 驱动
- 5.9 中断、DMA 与并发
- 5.10 用户空间服务、部署与调试
6. 质量、安全与交付
- 6.1 固件架构、配置与资源预算
- 6.2 日志、断言与故障诊断
- 6.3 测试、逻辑分析与硬件在环
- 6.4 Bootloader 与固件升级
- 6.5 实时性、性能与功耗测量
- 6.6 编码规范、静态分析与评审
- 6.7 安全启动、映像校验与密钥
- 6.8 发布、现场升级与维护
7. 综合实践
8. 参考
1. 习题
本篇以纯粹的工程实验与底层链接探索为主。在微控制器世界里,C 语言不仅关乎语法结构,更关乎生成的二进制机器码如何在没有操作系统托底的独立环境中实现自我立足。下列四道精心设计的阶梯练习,通过在宿主机上调用 arm-none-eabi-gcc 工具链,带领开发者层层剥离通用运行时库的外壳,逐步看清一个极简固件从源码到机器码的诞生机制、底层符号依赖与物理链接边界。前三道题仅依赖 1.2 MCU、MPU、SoC 与执行环境 与 1.8 复位、启动与链接 的知识储备,无需外部开发板即可直接在个人电脑上动手验证。
把「最小程序」从宿主环境里剥出来,先看清它需要什么。
(1) 写一个只有四行的 C 程序:
int main(void) {
return 0;
}2
3
先用 arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -O2 -Wall -Wextra min.c -o min.elf 链接。把链接器报出的全部 undefined reference 去重后列出来。
(2) 加上 -nostartfiles 再链接一次,记录结果。比较两次的报错,说明 crt0(_mainCRTStartup)自己引出了哪些桩。
(3) 用 arm-none-eabi-nm -u 检查 (2) 的产物:未定义符号只剩一个 _start,链接器同时打印 warning: cannot find entry symbol _start; defaulting to 00008000。用 arm-none-eabi-readelf -h 读入口地址,再用 arm-none-eabi-nm 把这个地址对到具体函数上。说明 _start 本来是给谁用的、为什么即使它缺失,这个 .elf 依然能被 readelf 与 size 正常解析。
用 -nostdlib 写一个完全自足的固件骨架。
写
bare_main.c,包含<stddef.h>与<stdint.h>,定义:- 一个 16 位与一个 32 位成员的结构体,用
offsetof与sizeof各算一遍; - 一个
_Static_assert断言sizeof(long) == 4、sizeof(void *) == 4、CHAR_BIT == 8(需要<limits.h>); - 一个
for (;;)里累加的volatile uint32_t。
- 一个 16 位与一个 32 位成员的结构体,用
用下面的命令链接:
arm-none-eabi-gcc -std=c17 -mcpu=cortex-m3 -mthumb -O2 -Wall -Wextra \
-ffreestanding -nostdlib -Wl,-e,main bare_main.c -o bare.elf2
验收:
arm-none-eabi-nm -u bare.elf无输出;arm-none-eabi-readelf -h bare.elf | grep Entry给出的地址以 1 结尾;arm-none-eabi-size bare.elf的text不超过 64 字节。把同一份源文件拿到主机上编译(
gcc -std=c17 -Wall -Wextra -c bare_main.c),记录_Static_assert的报错。解释为什么同一个sizeof(void *) == 4在 Cortex-M3 上成立、在 64 位主机上不成立,以及这件事对「把嵌入式代码拿到主机上跑单元测试」意味着什么。
写一个不依赖 printf 的十六进制与十进制格式化器,证明独立环境里可以完全没有 C 标准库。
- 实现两个函数,只允许使用
<stddef.h>与<stdint.h>:
size_t put_u32_hex(char *out, uint32_t value, int upper); /* 固定 8 位,不写结尾 '\0' */
size_t put_u32_dec(char *out, uint32_t value); /* 最短宽度,不写结尾 '\0' */2
- 在主机上写一个测试
main(可以用printf)穷举0、1、9、10、99、100、2147483648、4294967295八个值,与snprintf的结果逐字节比较,期望输出:
0 dec=0 OK hex=00000000 OK
1 dec=1 OK hex=00000001 OK
9 dec=9 OK hex=00000009 OK
10 dec=10 OK hex=0000000A OK
99 dec=99 OK hex=00000063 OK
100 dec=100 OK hex=00000064 OK
2147483648 dec=2147483648 OK hex=80000000 OK
4294967295 dec=4294967295 OK hex=FFFFFFFF OK2
3
4
5
6
7
8
把测试
main换成for (;;),用arm-none-eabi-gcc -std=c17 -mcpu=cortex-m3 -mthumb -O2 -Wall -Wextra -ffreestanding -nostdlib -Wl,-e,main fmt.c -o fmt.elf链接,确认arm-none-eabi-nm -u无输出。说明:为什么
put_u32_dec里的临时数组取char tmp[10],而不是char tmp[11]或char tmp[8]?把宽度算错会出什么问题,而编译器在这个位置不会替你拦下来?
把前几题的骨架拼成一个「最小程序生成器」,并说明它为什么还不是能烧进 F103 的固件。
写一个主机端程序
mkmin.c,从命令行读入一个输出路径、一个含%x的格式串与若干数值,生成一份完整的 C 源文件,内容包含:- 一个空的
main,以for (;;)结束; - 一段
volatile对象定义,用来占住.data与.bss; - 把格式串里的每个
%x依次换成0x加八位大写十六进制,写进const char g_banner[]; - 在
main里用strlen(g_banner)取长度并写进一个volatile对象,让生成的程序既依赖 C 标准库函数,也依赖自己的格式化代码。
例如
mkmin gen.c "magic=%x sp=%x" 0x103C8000 0x20005000生成的源文件里应当有一行:- 一个空的
const char g_banner[] = "magic=0x103C8000 sp=0x20005000";替换函数沿用上一题的 put_u32_hex,%x 的个数与数值个数不一致时退出码为 2。
- 生成的源文件用下面两条命令构建,两条都必须通过:
arm-none-eabi-gcc -std=c17 -mcpu=cortex-m3 -mthumb -O2 -Wall -Wextra \
-ffreestanding -nostdlib -Wl,-e,main gen.c -o gen.elf
arm-none-eabi-gcc -std=c17 -mcpu=cortex-m3 -mthumb -O2 -Wall -Wextra \
-ffreestanding -nostartfiles -Wl,-e,main gen.c -o gen_libc.elf --specs=nano.specs2
3
4
用
arm-none-eabi-nm,arm-none-eabi-size,arm-none-eabi-readelf -l比较两个产物,填一张表:text、data、bss、入口地址、符号总数、未定义符号集合、有没有.isr_vector、有没有_mainCRTStartup、第一个LOAD段的虚拟地址。第一条命令因为源码里用了
strlen而报undefined reference to strlen,先把这个现象连同 (4) 一起解释,再把源码里的strlen换成自写的长度循环,使第一条命令通过。做完对比会发现两个产物在
size与符号表上完全一样:-nostartfiles只是「不主动链接crt0」,而这个程序没有任何东西引用crt0提供的符号,链接器于是把它整个丢掉了。为了把差别做出来,给生成的文件再补一句printf。第一条命令这时会报undefined reference to printf(-nostdlib连 C 库都不链接);第二条命令能链上库,但会补报_write、_sbrk等七个桩(外加_kill、_getpid,取决于用哪份 libc)。把syscalls.c(正文 5.4)与它一起链接进第二条命令,记录两个构建在text与符号数量上的差距,说明-nostdlib与-nostartfiles各自省掉了哪一层。结论部分回答三个问题:
- 两个产物都没有向量表,上电后芯片从地址 0 取到的第一个字会被当成什么?后果是什么?
- 1.8 的
minimal比这两个产物多了哪两样东西,才使它成为能烧进 Flash 的固件? - 如果只有一块板子、没有调试器,怎么用
arm-none-eabi-objcopy -O binary与od验证上一条结论?