第2章:嵌入式开发环境搭建

说实话,很多初学者觉得开发环境搭建是件小事。我当年也这么想,结果第一个项目就栽了大跟头——交叉编译链版本不对,折腾了整整三天。所以这一章,咱们把环境搭得扎扎实实。

2.1 交叉编译工具链安装

嵌入式设备资源有限,你不可能在ARM芯片上直接编译Linux内核。怎么办?在PC上编译,生成目标平台能运行的代码。这就是交叉编译。

工具链的选择,我个人习惯用Linaro提供的版本。为什么?因为它更新及时,社区活跃。举个例子,针对ARM Cortex-A系列,我常用这个:

# 下载ARM GCC工具链
wget https://releases.linaro.org/components/toolchain/binaries/latest-7/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz

# 解压到指定目录
tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt/

# 配置环境变量
export PATH=$PATH:/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin

这里有个坑,我踩过好几次——环境变量要写进~/.bashrc,否则新开终端就失效了。你想想看,每次都要手动export,多烦人。

小技巧:验证工具链是否安装成功,运行 arm-linux-gnueabihf-gcc --version。如果显示版本号,说明OK了。

我在项目中遇到过一个问题:编译出来的程序在目标板上跑不起来。查了半天,原来是工具链的浮点运算选项不匹配。嗯,这里要注意——目标CPU是否支持硬件浮点,决定了你用gnueabihf还是gnueabi。

2.2 TFTP/NFS服务器配置

开发阶段,你不可能每次修改代码都重新烧写Flash。太慢了。TFTP和NFS就是用来解决这个问题的。

2.2.1 TFTP服务器

TFTP用于网络启动内核和设备树。说白了,就是U-Boot通过网线从PC下载文件。

# 安装TFTP服务器(Ubuntu)
sudo apt-get install tftpd-hpa

# 配置文件 /etc/default/tftpd-hpa
TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/tftpboot"    # 文件存放目录
TFTP_ADDRESS="0.0.0.0:69"
TFTP_OPTIONS="-l -c -s"

# 创建目录并设置权限
sudo mkdir /tftpboot
sudo chmod 777 /tftpboot

# 重启服务
sudo systemctl restart tftpd-hpa

我曾经犯过一个低级错误——忘记关闭防火墙。U-Boot那边一直timeout,我还以为是网线坏了。所以,记得检查:

sudo ufw disable   # 或者开放69端口

2.2.2 NFS服务器

NFS用于挂载根文件系统。这样你修改了应用程序,目标板重启后就能直接用新版本,不用重新烧写。

# 安装NFS服务器
sudo apt-get install nfs-kernel-server

# 编辑 /etc/exports,添加共享目录
/opt/rootfs *(rw,sync,no_subtree_check,no_root_squash)

# 重启服务
sudo systemctl restart nfs-kernel-server
注意:no_root_squash这个选项很重要。如果不加,目标板上的root用户会被映射成nobody,导致权限问题。我刚开始做BSP时就被这个坑过。

在U-Boot中设置启动参数时,大概是这样:

setenv bootargs 'console=ttyS0,115200 root=/dev/nfs nfsroot=192.168.1.100:/opt/rootfs rw ip=192.168.1.101'
setenv bootcmd 'tftp 0x80000000 zImage; tftp 0x83000000 imx6ull.dtb; bootz 0x80000000 - 0x83000000'
boot

你想想看,有了这套流程,开发效率能提升多少?改个驱动,重新编译,重启目标板,搞定。

2.3 JTAG/SWD调试器使用

调试器是嵌入式开发的「显微镜」。没有它,你只能靠printk和LED灯来猜问题。我建议每个BSP工程师都备一个J-Link或ST-Link。

2.3.1 硬件连接

JTAG需要4根线:TMS、TCK、TDI、TDO。SWD更简单,只要SWDIO和SWCLK两根线。现在的调试器大多同时支持两种模式。

信号 JTAG SWD
时钟 TCK SWCLK
数据 TMS/TDI/TDO SWDIO
GND GND

连接时要注意——复位信号最好也接上。我遇到过目标板死机,调试器连不上的情况。这时候按住复位键再连接,往往能救回来。

2.3.2 OpenOCD + GDB调试

开源方案我推荐OpenOCD + GDB。配合起来用,功能不比商业调试器差。

# 启动OpenOCD,连接ST-Link调试器
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg

# 在另一个终端启动GDB
arm-none-eabi-gdb my_program.elf

# 在GDB中连接OpenOCD
(gdb) target remote localhost:3333
(gdb) monitor reset halt
(gdb) load
(gdb) continue

核心命令速记:

  • monitor reset halt — 复位并暂停CPU
  • load — 下载程序到Flash或RAM
  • break main — 在main函数设置断点
  • info registers — 查看寄存器状态

我记得有一次调试USB驱动,设备枚举总是失败。用JTAG单步跟踪,发现是DMA描述符地址没对齐。这种问题,靠看日志根本看不出来。

2.3.3 调试技巧

说几个实战中常用的技巧:

  • 硬件断点有限 — ARM Cortex-M系列通常只有6个硬件断点。别设太多,否则调试器会报错。
  • 用Watchpoint监控变量 — 比如怀疑某个全局变量被意外修改,可以用 watch my_var 来监控。
  • 读芯片ID — 连接成功后,先读一下芯片的ID寄存器。如果读出来全是0xFF或0x00,说明连接有问题。
避坑指南:我曾经在调试时发现程序跑飞了,怎么都找不到原因。后来用JTAG读PC指针,发现指向了0xFFFFFFFC。嗯,这是典型的栈溢出。从此以后,我写代码都会给每个任务分配足够的栈空间。

好了,环境搭建这部分就讲到这里。下一章咱们开始写第一个BSP程序——点亮LED。别小看这个,它背后涉及GPIO控制器、时钟树、设备树,是理解整个BSP框架的入口。