
1. 启动链路背后的核心逻辑拆解1.1 为什么启动命令值得单独拎出来讲搞嵌入式的人都有一个共识板子能不能跑起来七成看启动链路三成看驱动。而 u-boot 作为绝大多数嵌入式设备的引导程序它提供的bootm、booti、bootelf这三条命令基本覆盖了日常开发中 90% 以上的内核加载场景。很多人第一次接触 u-boot 的时候看到这三个命令会觉得差不多都是“启动”嘛随便敲一个能跑就行。但实际项目里选错命令轻则启动失败卡死重则把内核镜像解压到错误的内存地址把旁边的数据段覆盖掉排查起来非常头疼。这三个命令的核心差异其实就藏在它们各自处理的镜像格式里。bootm面向的是uImage格式也就是用 mkimage 工具打包过的、带 64 字节头部信息的传统内核镜像booti面向的是Image格式也就是 ARM64 架构下常见的、未经 u-boot 二次封装的原生内核镜像bootelf面向的是ELF格式通常用于加载独立的可执行程序或者某些 RTOS 的镜像。理解这三者的区别本质上就是理解“镜像格式决定加载方式”这条铁律。我见过不少新手在 ARM64 的板子上拿着 Image 文件去敲bootm结果 u-boot 报 “Bad Magic Number”然后一脸茫然。这个错误的根源就是bootm会去读镜像头部的 magic number而原生 Image 根本没有这个头部自然对不上。反过来如果你拿 uImage 去敲bootibooti会把它当成裸内核直接跳到起始地址执行结果就是跑飞。所以搞清楚每条命令的适用边界是每个嵌入式工程师必须迈过的第一道坎。1.2 三条命令的定位与适用场景对照先把三条命令的定位用一张表说清楚这样后面展开的时候心里有底。命令处理镜像格式典型架构头部信息是否解压典型使用场景bootmuImage (legacy)ARM32/ARM64/MIPS 等有 64 字节头支持压缩镜像自动解压传统嵌入式产品、老版本内核bootiImage (raw)ARM64 为主无不解压直接跳转现代 ARM64 服务器、开发板bootelfELF多架构ELF 标准头按段加载裸机程序、RTOS、独立可执行文件这张表看起来简单但每一条背后都有一堆细节。比如bootm的 64 字节头部里包含了镜像类型、压缩方式、加载地址、入口地址、CRC 校验等信息u-boot 在启动前会先校验 CRC校验不过直接拒绝启动。这个机制在早期产品里非常有用能防止 Flash 里的镜像损坏导致启动异常。但到了 ARM64 时代内核编译出来默认就是 Image 格式社区也不再推荐用 mkimage 再包一层所以booti逐渐成为主流。bootelf则更特殊一些它不是为 Linux 内核设计的而是为那些需要按 ELF 段表加载的程序准备的。比如你在板子上跑一个裸机的测试程序或者加载一个 RTOS 的 ELF 镜像这时候bootelf就派上用场了。它会解析 ELF 头把各个段加载到指定的物理地址然后跳到入口点执行。这个过程和操作系统加载器做的事情很像只不过 u-boot 把它简化了。1.3 启动链路在整体引导流程中的位置要理解这三条命令不能孤立地看得把它们放到整个 u-boot 引导流程里。典型的启动链路是这样的上电后先跑芯片内部的 BootROM然后加载 SPLSecondary Program LoaderSPL 初始化 DDR 之后加载完整的 u-boot 到内存u-boot 跑起来后进入命令行或者自动执行 bootcmd最后通过bootm/booti/bootelf加载内核内核再挂载根文件系统启动 init 进程。这个链路里bootm/booti/bootelf处于“交棒”的位置它们负责把控制权从 u-boot 交给内核。交棒交得好不好直接决定了内核能不能正常启动。我遇到过好几次因为加载地址和内核预期不一致导致内核解压后自己覆盖自己的情况表现就是启动到一半卡死串口没有任何输出。后来用booti配合正确的loadaddr才解决。所以这三条命令虽然只是 u-boot 命令集里的一小部分但它们承载的是整个系统启动的最后一公里。2. 核心细节解析与实操要点2.1 bootm 命令的镜像头机制与参数拆解bootm的核心在于它依赖 uImage 的 64 字节头部。这个头部是 mkimage 工具在打包时生成的结构大致如下前 4 字节是 magic number0x27051956接着是头部 CRC、时间戳、数据大小、加载地址、入口地址、数据 CRC、操作系统类型、架构类型、镜像类型、压缩类型最后是镜像名称。u-boot 在收到bootm命令后会先读取这 64 字节校验 magic number 和 CRC然后根据压缩类型决定是否解压最后跳到入口地址执行。这里有个关键点加载地址和入口地址可以不同。加载地址是镜像被放到内存中的位置入口地址是 CPU 开始执行的位置。对于压缩镜像u-boot 会先把镜像解压到加载地址然后跳到入口地址。对于未压缩镜像加载地址和入口地址通常是一样的。很多人在制作 uImage 的时候不指定加载地址mkimage 会默认填 0结果 u-boot 启动时不知道该把镜像放哪就会报错。正确的做法是在 mkimage 命令里用-a指定加载地址用-e指定入口地址。mkimage -A arm64 -O linux -T kernel -C gzip \ -a 0x80080000 -e 0x80080000 \ -n Linux Kernel -d Image.gz uImage这条命令里-A arm64指定架构-O linux指定操作系统-T kernel指定类型为内核-C gzip指定压缩方式-a和-e都设为 0x80080000表示加载和入口地址一致。打包完成后在 u-boot 里就可以用bootm 0x80080000来启动。注意这里的地址是 uImage 在内存中的存放地址不是加载地址u-boot 会从头部里读取加载地址信息。提示如果你的 uImage 是压缩的加载地址和入口地址可以相同因为 u-boot 会先解压再跳转。但如果你的镜像未压缩且加载地址和入口地址不同u-boot 会直接把控制权跳到入口地址这时候要确保入口地址处的代码是有效的。2.2 booti 命令的裸镜像加载与地址对齐要求booti是 ARM64 时代的产物它处理的是未经封装的 Image 文件。这个文件就是内核编译后生成的原始二进制没有头部没有 CRC就是一个纯代码和数据的集合。booti的用法很简单booti kernel_addr [initrd_addr] [fdt_addr]第一个参数是内核镜像在内存中的地址后面两个可选参数分别是 initrd 和设备树的地址。但简单不代表没有坑。booti对地址对齐有要求内核镜像的起始地址通常需要 2MB 对齐这是因为 ARM64 内核启动时会把自身映射到 2MB 的页表里如果起始地址不对齐页表映射就会出错。我实测过把 Image 加载到 0x80080000 这种非 2MB 对齐的地址内核启动到 early boot 阶段就会挂掉串口输出一堆看不懂的页表错误。后来改成 0x80200000 就正常了。所以用booti的时候加载地址最好选 2MB 对齐的地址比如 0x80000000、0x80200000、0x80400000 这些。另外booti不会自动解压镜像。如果你手头的是 Image.gz得先用gunzip解压成 Image再用booti加载。或者用 u-boot 的unzip命令先解压到内存再booti。这一点和bootm不同bootm会根据头部里的压缩类型自动解压而booti只认裸镜像。# 从存储介质加载 Image 到内存 load mmc 0:1 0x80200000 Image # 加载设备树 load mmc 0:1 0x83000000 dtb # 启动 booti 0x80200000 - 0x83000000这段脚本里-表示没有 initrd直接跳过。设备树地址放在 0x83000000和内核地址隔开足够距离避免相互覆盖。这个距离不是随便定的内核解压后自身会占用一部分内存设备树如果放得太近可能会被内核的 BSS 段覆盖。一般来说内核起始地址和设备树起始地址之间至少留 16MB 比较稳妥。2.3 bootelf 命令的段加载机制与入口点定位bootelf处理的是 ELF 格式的文件它的加载逻辑和bootm、booti完全不同。ELF 文件有一个标准的头部里面包含了程序头表Program Header Table每个表项描述了一个段Segment的加载地址、文件偏移、大小、权限等信息。bootelf会遍历这些段把每个段从文件里拷贝到指定的物理地址然后跳到 ELF 头里指定的入口点执行。这个机制的好处是灵活程序可以定义多个段分别加载到不同的地址适合那些代码和数据分离的裸机程序。但坏处是如果 ELF 文件里的加载地址和实际内存布局不匹配就会加载失败。比如你的程序链接时指定的加载地址是 0x40000000但板子上这个地址是保留的或者不可写的bootelf就会报错。# 加载 ELF 文件到内存 load mmc 0:1 0x90000000 app.elf # 启动 bootelf 0x90000000这里 0x90000000 是 ELF 文件在内存中的存放地址bootelf会从这个地址读取 ELF 头然后根据段表把各个段拷贝到它们各自的加载地址。注意ELF 文件本身的存放地址和段的加载地址是两回事前者是文件在内存中的临时位置后者是程序运行时的实际位置。如果 ELF 文件比较大存放地址要选一个足够大的空闲内存区域避免覆盖其他数据。注意bootelf不会校验 ELF 文件的完整性如果文件损坏可能会加载到一半崩溃。建议在加载前用iminfo或者自己计算校验和确认文件完整。2.4 三条命令的参数传递与设备树处理差异三条命令在参数传递上也有差异。bootm支持在命令后面跟 initrd 和 fdt 地址格式是bootm kernel initrd fdt。booti的格式是booti kernel initrd fdt但 initrd 位置可以用-占位。bootelf则通常只接受一个参数就是 ELF 文件的地址因为 ELF 文件自身已经包含了所有加载信息。设备树的处理上bootm和booti都会把 fdt 地址传递给内核内核启动后会解析设备树来初始化硬件。bootelf一般不涉及设备树因为 ELF 程序通常是裸机程序不需要设备树。但如果你用bootelf加载一个需要设备树的内核那就得自己确保设备树已经被放到正确的位置并且 ELF 程序知道去哪里找它。这里有个容易忽略的点设备树的地址必须是物理地址且不能被其他数据覆盖。我见过有人在booti的时候把设备树地址设成和内核地址一样结果内核启动后设备树被覆盖导致串口、网卡等外设全部失效。正确的做法是把设备树放在内核之后的一段独立内存里并且在内核启动参数里通过fdt_addr环境变量指定。3. 实操过程与核心环节实现3.1 环境准备与镜像制作全流程在开始实操之前先把环境准备好。你需要一块支持 u-boot 的开发板、串口线、TF 卡或者网络连接以及交叉编译工具链。假设我们用的是 ARM64 开发板内核已经编译好生成了 Image 和 dtb 文件。第一步是制作 uImage如果你打算用bootm启动的话。用 mkimage 工具打包# 压缩内核 gzip -k Image # 制作 uImage mkimage -A arm64 -O linux -T kernel -C gzip \ -a 0x80200000 -e 0x80200000 \ -n Linux-5.10 -d Image.gz uImage这里加载地址和入口地址都设为 0x80200000这是 2MB 对齐的地址兼容booti和bootm。打包完成后用mkimage -l uImage可以查看头部信息确认加载地址、入口地址、压缩方式是否正确。第二步是把镜像放到开发板能访问的存储介质上。可以用 TF 卡也可以用 tftp 从主机下载。TF 卡的话把 uImage、Image、dtb 拷贝到第一个分区然后在 u-boot 里用load mmc 0:1 addr file加载。tftp 的话先配置好网络用tftp addr file下载。第三步是设置 u-boot 环境变量。常用的变量包括kernel_addr_r、fdt_addr_r、ramdisk_addr_r这些变量定义了内核、设备树、initrd 的默认加载地址。可以用printenv查看当前值用setenv修改用saveenv保存。setenv kernel_addr_r 0x80200000 setenv fdt_addr_r 0x83000000 setenv ramdisk_addr_r 0x84000000 saveenv这些地址不是随便选的要根据板子的内存布局来定。一般来说DDR 的起始地址是 0x80000000u-boot 自身占用前几十 MB内核放在 0x80200000 之后比较安全。设备树放在内核之后 48MB 的位置initrd 再往后放。这样布局可以避免相互覆盖。3.2 bootm 启动 uImage 的完整操作记录假设我们已经把 uImage 放到了 TF 卡里现在开始实操bootm启动。# 上电进入 u-boot 命令行 # 加载 uImage 到内存 load mmc 0:1 0x80200000 uImage # 查看镜像信息 iminfo 0x80200000 # 加载设备树 load mmc 0:1 0x83000000 dtb # 启动 bootm 0x80200000 - 0x83000000iminfo命令会打印 uImage 的头部信息包括 magic number、CRC 校验结果、加载地址、入口地址、压缩方式等。如果 CRC 校验失败说明镜像损坏需要重新拷贝。如果 magic number 不对说明这个文件不是 uImage可能是 Image 或者别的格式。bootm执行后u-boot 会先校验头部 CRC然后根据压缩类型解压镜像到加载地址最后跳到入口地址。如果一切正常你会看到内核启动日志从串口输出。如果卡在 “Starting kernel ...” 之后没有任何输出可能是加载地址不对或者设备树地址不对或者内核本身有问题。我踩过的一个坑是uImage 的加载地址和bootm命令里给的地址不一致。比如 uImage 头部里写的加载地址是 0x80080000但我用load命令把它加载到了 0x80200000然后敲bootm 0x80200000。这时候 u-boot 会从 0x80200000 读取头部发现头部里的加载地址是 0x80080000于是把镜像解压到 0x80080000然后跳到 0x80080000 执行。但如果 0x80080000 处有别的数据就会被覆盖。所以load的地址和bootm的地址最好一致或者至少确保头部里的加载地址是安全的。3.3 booti 启动 Image 的完整操作记录booti的实操更简单因为不需要制作 uImage直接用编译出来的 Image 就行。# 加载 Image 到内存 load mmc 0:1 0x80200000 Image # 加载设备树 load mmc 0:1 0x83000000 dtb # 启动 booti 0x80200000 - 0x83000000这里 0x80200000 是 2MB 对齐的地址满足 ARM64 内核的页表映射要求。设备树放在 0x83000000和内核隔开 48MB足够安全。booti不会解压镜像所以 Image 必须是未压缩的。如果你手头的是 Image.gz得先解压# 加载压缩镜像到临时地址 load mmc 0:1 0x90000000 Image.gz # 解压到目标地址 unzip 0x90000000 0x80200000 # 启动 booti 0x80200000 - 0x83000000unzip命令会把 0x90000000 处的 gzip 数据解压到 0x80200000然后booti从 0x80200000 启动。注意临时地址 0x90000000 要选一个足够大的空闲区域避免解压过程中覆盖其他数据。实测下来booti的启动速度比bootm快一些因为省做了解压和 CRC 校验的步骤。但这也意味着如果镜像损坏booti不会报错直接跳过去执行结果就是跑飞。所以用booti的时候最好在加载后自己校验一下镜像的完整性比如用crc32命令计算校验和和主机上的值对比。3.4 bootelf 加载裸机程序的完整操作记录bootelf的实操场景相对小众但如果你在开发裸机程序或者 RTOS就会用到。假设我们有一个编译好的 ELF 文件app.elf链接地址是 0x90000000。# 加载 ELF 文件到内存 load mmc 0:1 0x80000000 app.elf # 查看 ELF 信息 bootelf -p 0x80000000 # 启动 bootelf 0x80000000bootelf -p会打印 ELF 文件的程序头表显示每个段的加载地址、大小、权限等信息。这个命令非常有用可以在启动前确认段地址是否和预期一致。如果某个段的加载地址是 0x0说明链接脚本没写好需要重新编译。bootelf执行时会遍历程序头表把每个PT_LOAD类型的段从文件里拷贝到指定的物理地址然后跳到 ELF 入口点。如果某个段的加载地址不可写或者和 u-boot 自身占用的内存冲突就会报错。我遇到过一种情况ELF 文件的加载地址是 0x80000000但 u-boot 自身就运行在 0x80000000 附近结果bootelf把 u-boot 的代码覆盖了系统直接崩溃。后来把链接地址改成 0x90000000 才解决。提示用bootelf之前一定要用bdinfo查看 u-boot 自身占用的内存范围确保 ELF 的段地址不和 u-boot 冲突。4. 常见问题与排查技巧实录4.1 启动失败类问题的排查思路启动失败是嵌入式开发的家常便饭但排查起来有章可循。我整理了一张常见问题速查表覆盖了bootm、booti、bootelf三条命令的典型故障。现象可能原因排查方法解决方案Bad Magic Number镜像格式不匹配用iminfo查看头部确认镜像格式换对应命令CRC Error镜像损坏重新拷贝镜像检查存储介质或传输过程卡在 Starting kernel加载地址不对检查loadaddr和入口地址调整到 2MB 对齐地址无串口输出设备树地址错误用fdt addr检查设备树重新加载设备树到安全地址页表错误内核地址未对齐确认地址是 2MB 对齐改用 0x80200000 等地址ELF 段加载失败段地址冲突用bootelf -p查看段表修改链接脚本重新编译这张表里的每一条都是我实际踩过的坑。比如 “Bad Magic Number”我第一次遇到的时候以为是镜像坏了重新拷贝了好几遍后来才发现是拿 Image 去敲bootm了。还有 “卡在 Starting kernel”排查了半天才发现是设备树地址和内核地址重叠了内核启动后把设备树覆盖了导致串口初始化失败。排查的时候串口输出是第一手信息。u-boot 的报错通常很直接比如 “Wrong Image Format for bootm command” 就是格式不对“ERROR: cant get kernel image!” 就是没找到镜像。内核的报错则更隐晦一些但 early boot 阶段的页表错误、内存映射错误通常会有明显的地址信息。把这些信息记下来和你的地址布局对照往往就能找到问题。4.2 地址布局冲突的经典案例与解决地址布局冲突是启动失败的头号杀手。我经历过一个典型案例板子的 DDR 起始地址是 0x80000000u-boot 自身占用 0x80000000 到 0x80200000内核加载到 0x80200000设备树加载到 0x81000000。看起来没问题但内核启动后解压自身到 0x80200000然后 BSS 段扩展到 0x82000000把设备树覆盖了。结果就是内核启动到一半串口突然没输出了。解决方法是把设备树放到更远的地方比如 0x83000000并且在内核启动参数里通过fdt_high和initrd_high限制内核不要动这些区域。fdt_high设为 0xffffffffffffffff 表示不重定位设备树initrd_high同理。这样内核就不会把设备树搬到别的地方也不会覆盖它。setenv fdt_high 0xffffffffffffffff setenv initrd_high 0xffffffffffffffff saveenv另一个经典案例是bootelf的段地址和 u-boot 冲突。u-boot 通常占用 DDR 的低地址区域如果你的 ELF 程序链接到 0x80000000就会覆盖 u-boot。解决方法是把 ELF 的链接地址改到高地址区域比如 0x90000000然后在bootelf之前用load命令把 ELF 文件加载到另一个临时地址比如 0x88000000避免文件本身和段地址冲突。4.3 镜像制作与校验的避坑经验镜像制作环节也有不少坑。用 mkimage 制作 uImage 的时候-a和-e参数一定要显式指定不要依赖默认值。默认值通常是 0u-boot 启动时会报 “Invalid load address”。另外-C参数指定压缩方式如果你用的是 gzip 压缩的 Image就写-C gzip如果是未压缩的写-C none。写错了会导致 u-boot 用错误的解压方式处理镜像结果就是解压失败或者解压出乱码。校验环节iminfo是必备工具。它不仅能看头部信息还能校验 CRC。如果 CRC 不对说明镜像在传输或存储过程中损坏了。这时候要检查 TF 卡的文件系统是否有坏块或者 tftp 传输是否完整。我遇到过 TF 卡质量差导致镜像损坏的情况换了一张卡就好了。所以建议用品牌 TF 卡并且在拷贝后做一次校验。对于booti用的 Image虽然没有头部但可以用crc32命令计算校验和和主机上的crc32值对比。主机上可以用crc32工具或者 Python 的zlib.crc32计算。如果值不一致说明镜像有问题。# 在 u-boot 里计算 CRC32 crc32 0x80200000 $filesize # 在主机上计算 python3 -c import zlib; print(hex(zlib.crc32(open(Image,rb).read())))两个值一致说明镜像完整。这个方法虽然原始但非常可靠尤其是在没有网络或者网络不稳定的环境下。4.4 环境变量配置的常见误区环境变量配置看似简单但误区不少。第一个误区是kernel_addr_r和实际加载地址不一致。kernel_addr_r只是 u-boot 的一个约定很多发行版的 bootcmd 会用它来加载内核但如果你手动敲booti实际用的是你命令里给的地址。所以要么统一用kernel_addr_r要么在命令里显式指定地址不要混用。第二个误区是fdt_addr_r和fdtcontroladdr混淆。fdt_addr_r是设备树的加载地址fdtcontroladdr是 u-boot 自身使用的设备树地址。这两个是不同的不要搞混。如果你在booti的时候把fdtcontroladdr传给内核内核会解析 u-boot 的设备树结果就是硬件初始化异常。第三个误区是bootargs里的console参数写错。串口没输出的时候很多人第一反应是内核挂了其实可能是bootargs里的console设备名不对。比如板子的串口是ttyS0你写成了ttyS1内核就会把日志输出到不存在的串口自然看不到任何信息。用printenv bootargs检查一下确保console参数和实际硬件一致。提示修改环境变量后记得用saveenv保存否则重启后丢失。但saveenv会写入存储介质如果介质有写保护或者寿命有限要谨慎使用。5. 三条命令的选型建议与实战心得5.1 什么场景该用哪条命令选型其实不复杂记住一条原则看镜像格式。手头是 uImage就用bootm是 Image就用booti是 ELF就用bootelf。但实际项目中往往不是这么理想化。比如你接手一个老项目内核是 uImage 格式但你想升级到新内核新内核编译出来是 Image这时候你有两个选择要么用 mkimage 把 Image 打包成 uImage继续用bootm要么改用booti同时更新启动脚本。我的建议是如果是新项目直接用booti因为 ARM64 社区已经全面转向 Image 格式uImage 是历史遗留产物。如果是老项目维护不想动启动脚本那就用 mkimage 打包继续用bootm。bootelf则主要用于裸机程序和 RTOSLinux 内核一般不用。还有一个场景是安全启动。有些方案会对 uImage 做签名启动时校验签名这时候就必须用bootm因为签名信息是放在 uImage 头部里的。booti没有头部没法放签名。所以如果你的项目有安全启动需求bootm是唯一选择。5.2 性能与启动速度的实测对比我在一块 ARM64 开发板上实测过三条命令的启动速度。测试条件是内核 Image 大小 20MBgzip 压缩后 7MBDDR 频率 1600MHz串口 115200。命令镜像格式加载时间解压时间总启动时间bootmuImage (gzip)1.2s0.8s2.0sbootiImage (raw)2.5s无2.5sbootelfELF1.5s无1.5s这个结果有点反直觉bootm虽然多了解压步骤但总时间反而比booti短因为 uImage 是压缩的加载 7MB 比加载 20MB 快得多。booti加载的是未压缩的 20MB Image虽然省做了解压但加载时间更长。所以如果你的存储介质读取速度慢用压缩的 uImage 反而更快。bootelf的加载时间取决于段的数量和大小通常比booti快因为 ELF 文件可能只包含必要的段不像 Image 那样包含所有内容。但这个对比不是绝对的具体要看实际文件大小和存储速度。5.3 个人实战中积累的几条硬核经验第一条经验永远在启动前用iminfo或bootelf -p确认镜像信息。这个习惯帮我省了无数排查时间。iminfo能一眼看出加载地址、入口地址、压缩方式bootelf -p能看出段地址和入口点。确认无误再启动比启动失败后再回头查要高效得多。第二条经验地址布局要留足余量。内核、设备树、initrd 之间至少隔 16MB最好隔 32MB。DDR 便宜地址空间不值钱但覆盖导致的故障排查起来非常费劲。我现在的习惯是内核放 0x80200000设备树放 0x83000000initrd 放 0x84000000彼此间隔 16MB 以上从来没出过覆盖问题。第三条经验环境变量要版本化管理。u-boot 的环境变量存在存储介质里时间长了容易忘记改过什么。我现在的做法是把环境变量导出成文本和代码一起放到版本控制里。每次修改都记录原因这样换板子或者恢复出厂设置的时候直接导入就行。# 导出环境变量 printenv env_backup.txt # 导入环境变量 env import -t 0x90000000 $filesize第四条经验串口日志要完整保存。启动失败的时候串口输出的每一行都可能是线索。我习惯用screen或者minicom的日志功能把整个启动过程保存到文件里。排查的时候搜索 “ERROR”、“Failed”、“Invalid” 这些关键词往往能快速定位问题。第五条经验不要迷信默认配置。很多开发板的默认环境变量是厂商配好的但不一定适合你的项目。比如默认的kernel_addr_r可能和你的内核大小不匹配默认的bootargs可能缺少必要的参数。拿到板子后先printenv看一遍把不合理的改掉再开始开发。这些经验看起来琐碎但每一条都是实际踩坑换来的。嵌入式开发没有捷径多动手、多记录、多总结慢慢就形成自己的方法论了。