2. 嵌入式开发环境搭建
做BSP开发,第一件事不是写代码,而是搭环境。我见过太多新手,上来就敲代码,结果编译报错半天找不到原因——最后发现是工具链版本不对。说白了,环境搭不好,后面全是坑。
这一章,我把嵌入式开发最核心的几个环境工具串起来讲。交叉编译工具链、调试器、串口终端、网络文件系统,一个一个来。
2.1 交叉编译工具链
嵌入式设备性能有限,没法直接在板子上编译代码。我们得在PC上编译,生成目标板能运行的二进制文件。这就是交叉编译。
工具链的组成:
- 编译器:gcc(针对ARM、RISC-V等架构)
- 链接器:ld
- 调试器:gdb(交叉编译版本)
- 库文件:glibc / uclibc / musl
- 头文件:内核头文件、C库头文件
我个人的习惯:用Linaro提供的预编译工具链,省事。比如针对ARM Cortex-A系列,我常用gcc-arm-none-eabi或aarch64-linux-gnu-gcc。
安装示例(Ubuntu 22.04):
sudo apt install gcc-arm-linux-gnueabihf
sudo apt install gcc-aarch64-linux-gnu
为什么推荐这个?因为Linaro的版本跟主流内核配合得好。我曾经自己编译过一次glibc,折腾了两天,最后发现某个补丁没打上,程序跑起来就段错误。从那以后,除非有特殊需求,我直接用官方预编译包。
验证工具链是否正常:
arm-linux-gnueabihf-gcc --version
arm-linux-gnueabihf-gcc -o hello hello.c
file hello # 查看生成的文件架构
嗯,这里要注意:交叉编译出来的文件,在PC上跑不了。你得用file命令确认架构对不对。我遇到过有人把ARM的二进制直接扔到x86上跑,然后问我为什么报错……
2.2 调试器:JTAG / SWD
调试嵌入式系统,光靠printf可不够。尤其是BSP开发,你要看寄存器、看内存、看PC指针。这时候就得用调试器。
JTAG vs SWD:
| 特性 | JTAG | SWD |
|---|---|---|
| 引脚数 | 5(TMS, TCK, TDI, TDO, nTRST) | 2(SWDIO, SWCLK) |
| 速度 | 较高 | 中等 |
| 适用场景 | FPGA、复杂SoC | ARM Cortex-M系列 |
| 调试能力 | 全功能(边界扫描、Flash编程) | 基本调试(断点、单步) |
我个人做BSP时,ARM Cortex-A系列用JTAG,Cortex-M系列用SWD。为什么?因为A系列要调试MMU、Cache这些复杂东西,JTAG的边界扫描功能很有用。M系列嘛,SWD两根线就够了,省引脚。
常用调试工具:
- OpenOCD:开源,支持JTAG/SWD,配合GDB使用
- J-Link:SEGGER出品,稳定,但贵
- ST-Link:ST官方,便宜,适合STM32
OpenOCD + GDB 调试示例:
# 启动OpenOCD(假设使用J-Link)
openocd -f interface/jlink.cfg -f target/stm32f4x.cfg
# 另一个终端启动GDB
arm-none-eabi-gdb my_program.elf
(gdb) target remote localhost:3333
(gdb) monitor reset halt
(gdb) load
(gdb) continue
我曾经在调试一个DMA驱动时,发现数据总是传错。用JTAG单步跟踪,发现DMA的源地址寄存器写错了——因为结构体对齐问题。要是没有调试器,这种bug靠看代码根本看不出来。
2.3 串口终端
串口是嵌入式开发者的「眼睛」。板子启动时打印的日志、内核的早期输出、甚至交互式shell,都靠它。
常用工具:
- minicom:经典,配置稍复杂
- screen:轻量,一行命令搞定
- putty:Windows用户最爱
- picocom:我最近常用,简洁
快速启动串口(Linux):
sudo picocom -b 115200 /dev/ttyUSB0
# 或
sudo screen /dev/ttyUSB0 115200
这里有个坑:串口波特率一定要跟uboot或内核设置的一致。我遇到过有人板子uboot设的是115200,但内核里改成了57600,结果uboot能打印,内核启动后全是乱码。嗯,这种问题排查起来很烦人。
串口参数速查表:
| 参数 | 常见值 | 说明 |
|---|---|---|
| 波特率 | 115200 / 57600 / 9600 | 越高越快,但线长时容易丢数据 |
| 数据位 | 8 | 几乎都是8位 |
| 停止位 | 1 | 偶尔用2位 |
| 校验位 | 无 | 大多数场景不用 |
2.4 TFTP / NFS 网络文件系统搭建
开发阶段,你不可能每次改个驱动都重新烧写整个镜像。太慢了。用网络加载内核和设备树,用NFS挂载根文件系统,这才是高效的做法。
TFTP(Trivial File Transfer Protocol):
说白了,就是通过网络把文件从服务器拉到板子上。uboot里常用。
搭建TFTP服务器(Ubuntu):
sudo apt install tftpd-hpa
sudo mkdir -p /srv/tftp
sudo chmod 777 /srv/tftp
# 编辑配置文件 /etc/default/tftpd-hpa
# 确保 TFTP_DIRECTORY="/srv/tftp"
然后在uboot里:
setenv serverip 192.168.1.100
setenv ipaddr 192.168.1.10
tftp 0x82000000 zImage
tftp 0x83000000 myboard.dtb
bootz 0x82000000 - 0x83000000
NFS(Network File System):
根文件系统挂在NFS上,板子启动后直接读写服务器上的文件。改代码、编译、重启验证,一气呵成。
搭建NFS服务器:
sudo apt install nfs-kernel-server
sudo mkdir -p /srv/nfs/rootfs
# 编辑 /etc/exports,添加:
/srv/nfs/rootfs *(rw,sync,no_subtree_check,no_root_squash)
sudo exportfs -a
内核启动参数里加上:
root=/dev/nfs nfsroot=192.168.1.100:/srv/nfs/rootfs rw ip=dhcp
注意:NFS的no_root_squash选项很重要。如果不加,板子上的root用户会被映射成nobody,写文件会权限不足。我曾经因为这个,调试一个驱动时死活写不了日志文件,折腾了一下午才发现是NFS权限问题。
知识体系总览
下面这张图,把本章的核心逻辑串起来了。你想想看,从编译到调试,再到运行和文件传输,每一步都有对应的工具。
这张图里,开发机通过交叉编译生成目标板能跑的二进制,然后用JTAG/SWD烧写或调试,串口看输出,网络传文件。四个工具配合起来,开发效率能翻好几倍。
我的建议:刚开始学BSP开发,先把串口和TFTP/NFS搭好。这两个能让你快速迭代代码。调试器可以等遇到复杂bug时再深入研究。别一上来就搞OpenOCD+GDB,容易劝退。