1. 项目概述与整体设计思路1.1 Hi3516CV610的定位与开发场景Hi3516CV610是海思面向智能安防、智能门铃、USB摄像头等高性价比视频编解码场景推出的一颗SoC。它跟Hi3516系列的其他成员一样集成了ISP、视频编解码、智能加速单元以及丰富的对外接口典型工作是把传感器采集的RAW图抓进来经过ISP处理后编码成H.264/H.265码流输出同时还能跑轻量级智能算法。很多做IPC方案的团队第一颗量产芯片往往就是从它开始的。在这类嵌入式Linux开发里第一个卡住新手的问题往往不是写代码而是把编译环境搭起来。目标板是一颗ARM架构芯片算力和存储都很有限不可能直接在板子上编译整个系统所以我们要在x86的Linux主机上构建交叉编译环境用专门面向目标芯片的交叉编译器也就是工具链生成能在ARM上运行的uboot、内核和根文件系统最后再通过烧录工具写到板子上。这个过程是所有后续开发的地基基础不牢后面会遇到各种莫名其妙的问题。这篇文章适合谁我认为是三类人第一次接触海思平台想快速跑通SDK的嵌入式新人从其他MCU或应用层转来做IPC方案的工程师以及在Ubuntu等主机环境下被各种编译问题折腾过的开发者。整篇内容围绕从零搭建海思SDK编译环境和工具链部署这条主线展开把为什么要这么做、每一步在干什么、出问题怎么查都讲清楚。1.2 为什么手动搭建编译环境这件事值得重视很多人拿到SDK的第一反应是找个现成的docker镜像或者网上的教程照着抄。说实话现成镜像确实省事但我个人建议至少在第一次接触新平台时自己手动走一遍编译环境和工具链部署流程。原因很简单你会由此搞明白SDK的结构、脚本的工作方式、工具链和内核版本之间的匹配关系这些东西在后面的定制开发、驱动移植、系统裁剪中全都要用上。另外这也是不同嵌入式开发领域的一个共性话题。无论是做Android framework编译时配置的android ubuntu framework编译环境搭建还是C/C开发环境、autosar工具链、etas工具链本质上都是要处理目标平台和宿主平台之间的差异。嵌入式Linux交叉编译的这套逻辑如果打通了以后换芯片平台时你看到的就不再是陌生的一堆脚本而只是工具链名称不同、部分配置参数不同而已。这也是我在文中会频繁强调看官方Makefile的原因——芯片会换代但工程习惯是通用的。1.3 主机选型与基础依赖安装海思官方SDK通常推荐在Ubuntu 18.04/20.04的64位系统上运行我自己也一样。这里有个很现实的原因SDK里有些脚本和旧版编译工具比如一些老的mkimage、根文件系统制作工具依赖某些32位库新版Ubuntu默认不再支持装起来会麻烦不少。如果你的主力电脑是Windows建议装个虚拟机分配8GB以上内存、100GB以上硬盘跑Ubuntu如果你有单独的Linux服务器或者一台旧电脑装了Ubuntu Server体验会更好。在解压SDK之前先把基础依赖装好避免半途报错。Ubuntu/Debian系的安装命令大致如下sudo apt update sudo apt install -y build-essential git make gcc g \ libncurses5-dev libncursesw5-dev libssl-dev \ zlib1g-dev flex bison bc \ u-boot-tools device-tree-compiler \ lib32z1 lib32ncurses5 lib32ncurses6 \ dosfstools mtools mtd-utils squashfs-tools这里解释一下为什么需要这些包。build-essential提供make和gcc是编译宿主机侧工具用的libncurses系列是内核menuconfig界面需要的libssl-dev是内核和uboot编译时需要的flex和bison是解析设备树和配置文件的语法工具bc是脚本里的计算器依赖device-tree-compiler提供dtc用于编译设备树最后那一串lib32是和32位兼容相关的主要给老版本根文件系统制作工具用。装完之后用gcc --version和make --version确认一下基础工具版本只要不是太老的版本一般都没问题。2. SDK包的解压与目录结构深度解析2.1 SDK包的内容与解锁步骤从海思官网或者你所在的公司代码仓库拿到SDK后通常会看到一个类似Hi3516CV610_SDK_Vx.x.x.x.tgz的压缩包。这里要提醒一句SDK自带版本号最好在整个项目中保持统一因为uboot、内核、rootfs工具链之间是有版本匹配关系的混用不同版本SDK的组件编译出来的镜像往往在板子上跑不起来或者出现各种诡异问题。解压时用tar即可tar -xzf Hi3516CV610_SDK_Vx.x.x.x.tgz cd Hi3516CV610_SDK_Vx.x.x.x解压出来的目录里一般会有一个sdk.unpack脚本。这个脚本的作用不是简单解压缩它负责把SDK内的所有子压缩包依次展开同时恢复可执行权限、修复符号链接、做必要的文件系统检查。有些版本的脚本还会校验磁盘空间避免后续编译进行到一半磁盘满了。执行的时候直接./sdk.unpack如果提示权限不够先chmod x sdk.unpack再执行。建议在普通用户下执行不要一上来就用root因为后面编译时如果某些生成文件的属主是root再切换到普通用户继续操作会有一堆权限问题。sdk.unpack执行完成后会多出很多子目录和文件整个SDK才真正进入可用状态。2.2 SDK目录结构uboot、kernel、rootfs、tools解包完成后最关键的是两个目录一个是osdrv这是整个编译的核心另一个是docs里面放着官方文档包括《SDK开发指南》《Hi3516CV610工具链使用指南》《Hi3516CV610 U-boot开发指南》等。我个人的习惯是先花一小时把SDK目录结构和官方文档浏览一遍弄清哪里有什么而不是急着编译。因为嵌入式工程里最怕的不是问题难而是出了问题不知道去哪里找答案。osdrv目录的典型结构大致是osdrv/opensource/uboot存放uboot源码工程编译后在这里面生成uboot镜像。osdrv/opensource/kernel存放Linux内核源码编译后在这里面生成内核镜像和设备树文件。osdrv/rootfs_scripts根文件系统制作脚本负责将busybox、用户态库和应用打包成目标镜像。osdrv/toolchain交叉编译工具链的压缩包以及安装脚本。osdrv/tools各种辅助工具比如烧录工具、镜像打包工具、分区表工具等。osdrv/Makefile整个SDK编译的统一入口支持不同启动介质和不同模块的选择。对新手来说Makefile是最需要看懂的文件它就是整个编译流水线的中控台。里面会定义一系列变量比如启动介质的选择spi nor、spi nand、emmc、sdcard等、rootfs的类型squashfs、jffs2、ext4等、是否开启某个feature等。读懂这个文件几乎就掌握了整个编译流程的骨架。2.3 SDK整体编译流程概览在osdrv目录下执行编译时实际发生的工作链条是先编译工具链侧相关组件再依次编译uboot、内核最后制作根文件系统所有产物会输出到指定的镜像目录然后通过打包工具组织成最终可烧录的升级镜像。整个流程看似一个make就能完成但内部每一环节都对应独立的子工程可以单独编译和单独调试。这个设计的价值在于真实开发中你不会天天全量编译。比如只改了一个内核驱动就只需要重新编译内核并打包镜像只改了根文件系统里的某个应用脚本就只需要重新制作rootfs只改了uboot的启动参数也只需要单独重编uboot。明白每个子模块之间的关系能省下大量等待编译的时间这也是我在后面会细化讲解的原因。3. 工具链部署与交叉编译环境配置3.1 工具链在SDK里的位置海思不会让你自己去网上下载通用的arm gcc工具链而是会在SDK里自带一份经过验证的交叉编译工具链。打开osdrv/toolchain目录你会看到工具链压缩包和README文件压缩包的命名通常带有平台和版本信息例如aarch64-v01c02-linux-gnu-gcc_xxx.tar.gz或arm-himix100-linux_xxx.tar.gz具体名称以你的SDK版本为准。为什么要使用SDK自带的工具链而不是Ubuntu软件源里的gcc-aarch64-linux-gnu核心原因是版本匹配。内核、uboot和用户态程序对编译器的版本有一定要求某些版本的内核用太新的gcc编译会出现告警甚至直接报错某些用户态库依赖特定的glibc版本交叉编译器如果自带sysroot的glibc跟SDK里的rootfs不一致编译出来的应用拷到板子上会报GLIBC_XX not found。海思这颗SoC整体对外提供的调试、编译、烧录支持都是围绕官方SDK设计的自己换工具链属于给自己挖坑。3.2 工具链安装与环境变量配置工具链的安装方式以官方README为准一般有两种情况一是直接解压到某个固定目录比如/opt二是通过SDK里提供的安装脚本自动安装安装脚本通常会把工具链放到默认路径并写入环境变量。如果SDK给的是安装脚本例如.sh执行后一路按提示即可如果给的是压缩包手动解压时建议统一放到/opt下面sudo mkdir -p /opt sudo tar -xzf osdrv/toolchain/xxxx-linux-gcc.tar.gz -C /opt解压完成后工具链目录里会有一个bin子目录里面就是交叉编译器的可执行文件比如aarch64-linux-gnu-gcc或者arm-himix100-linux-gcc。接下来要把这个bin目录加入PATH。这里我强烈建议不要每次手动export而是写进shell配置文件里这样每次新开终端就不用重新输入。echo export PATH/opt/xxxx-linux/bin:$PATH ~/.bashrc source ~/.bashrc有一点要特别说明写在~/.bashrc里只对当前用户生效写在/etc/profile里会对所有用户生效。如果公司里多个人共用同一台编译服务器建议写进/etc/profile.d/下单独的文件里统一管理避免互相覆盖。写好后新开的终端会自动加载这条经验在多账号的主机上能省很多不必要的沟通成本。3.3 验证工具链是否可用的几个方法环境变量配好之后不要急着去编译整个SDK先用几个命令确认工具链本身是完好的。第一是查看版本号aarch64-linux-gnu-gcc -v正常情况下会输出gcc version并且target一栏显示的是类似aarch64-linux-gnu这说明工具链本体可执行。第二是做一个最小编译测试echo int main(){return 0;} /tmp/test.c aarch64-linux-gnu-gcc /tmp/test.c -o /tmp/test file /tmp/test如果file命令输出显示ELF 64-bit LSB executable, ARM aarch64就说明工具链已经能正常产出目标平台的可执行文件了。这个最小测试很重要因为它把工具链可执行和工具链能正确完成编译、链接这两件事分开验证了很多时候前者没问题、后者却因为sysroot路径打断而出错这类问题在后面的开发中会反复遇到。顺手再检查一下make是否识别到我们期望的交叉编译器。可以通过make -v | head -n 1确保make版本正常。很多新手容易忽略make的版本兼容性海思SDK在旧版本Ubuntu上是不挑make版本的但在Ubuntu 22.04上make 4.3的某些行为改变会触发内核Kbuild工程的告警虽然一般不影响最终产物但建议还是以官方支持的Ubuntu版本为准。4. SDK整体编译流程与核心环节实现4.1 osdrv的统一编译入口及必要参数进入osdrv目录最常用的命令是cd osdrv make all这是最基础的编译方式理论上它会自动完成uboot、内核、rootfs的全部构建并在完成后输出可烧录的镜像文件。但实际开发中我不会直接裸跑make all而是先看Makefile里支持的变量比如最常见的BOOT_MEDIA启动介质和CHIP芯片型号等变量。启动介质决定了uboot最终被烧在哪里、怎么加载内核一般要跟你的硬件方案保持一致。比如如果你的板子从SPI NOR Flash启动典型命令是make BOOT_MEDIAspi_nor如果从eMMC启动则是make BOOT_MEDIAemmc具体有哪些可选项打开osdrv根目录下的Makefile查看即可。为什么要在这里强调看Makefile因为不同版本的SDK支持的变量名略有差异写文章时我不能替你把所有可能的参数都列出来但告诉你去看哪里这一招是永远通用的。而且Makefile里一般会有详细的注释说明每个变量的取值和含义比任何博客都准确。4.2 uboot的编译与产物说明uboot是启动的第一段代码负责初始化DDR、时钟、外设然后把内核加载到内存。海思SDK对uboot已经做了大量板级适配大多数情况下你不需要动uboot源码直接编译即可。在osdrv目录下单独编译uboot通常是make boot编译完成后uboot的镜像产物一般位于uboot源码目录或输出目录中常见的是u-boot-hi3516cv610.bin这类命名。注意uboot镜像有几个不同形式原始的u-boot.bin、加了头信息的烧录镜像、以及用于uboot升级的镜像烧录时不要搞混。如果板子上已经跑了一个可启动的uboot想通过tftp或串口升级新的uboot要用带升级头格式的那个版本如果板子是完全空片第一次烧uboot则要用烧录工具直接写入的原始格式。4.3 内核的编译与配置方法内核是系统的主体也是大多数驱动开发的主战场。单独编译内核可以用make kernel编译之前可以先通过内核的menuconfig对配置做调整make kernel_menuconfigmenuconfig界面依赖ncurses库这也是我前面要求安装libncurses5-dev的原因。进入界面后你可以选中需要的驱动模块比如Sensor驱动、网络协议栈、文件系统选项等。对于第一次搭建环境的人来说不建议大改内核配置先用SDK默认配置把整条编译链路跑通确认板子能启动再逐步按需裁剪或增加功能这样排错范围会小很多。内核编译完成后输出物通常包括内核镜像比如uImage和若干dtb设备树文件。设备树是描述硬件信息的数据结构所有外设的地址、中断、时钟、GPIO配置都在里面。当你新增一个sensor或者修改了引脚复用很多时候只需要改设备树而不需要改C代码因此dtb编译是否成功对开发节奏影响很大。4.4 根文件系统的编译与镜像格式选择根文件系统是系统启动后挂载到/的文件系统里面包含busybox、动态库、配置文件、业务应用等。编译rootfs的典型命令是make rootfsSDK会先构建busybox再把必要的库和脚本拷贝到临时目录最后根据你指定的格式打包成镜像。常见格式有三种squashfs、jffs2和ext4。squashfs是只读压缩文件系统体积小、启动快适合只读系统或配合overlay使用jffs2主要用于nand flash支持磨损均衡ext4则是通用性最强的镜像格式方便调试时挂载修改。具体用哪种还是看你的启动方案和Makefile里的配置。我在实践中的一个建议是调试阶段优先使用ext4或jffs2这类可写文件系统这样在板子上可以直接增删文件、修改配置不需要每次改点东西都重新打包烧录等系统稳定、要量产时再切换成squashfsoverlay的方案来提升可靠性和安全性。4.5 整体编译产物与烧录镜像组织跑完上述步骤后osdrv目录下会生成一系列输出文件。通常最终打包好的烧录镜像会集中放在某个image或out目录里里面至少包含uboot镜像、内核镜像、dtb文件和rootfs镜像。有些版本的SDK还会额外生成一个升级包里面把多个分区镜像打包成一个文件专门用于在板子上通过uboot或系统内升级。拿到这些镜像之后接下来就是烧录环节。第一次烧录建议使用烧录工具通过串口或网口把镜像写入板子具体流程在官方《烧录工具使用指南》里写得很清楚先让板子进入烧录模式然后按uboot、内核、dtb、rootfs的顺序分别写入对应分区。这里要特别提醒烧录前一定要确认分区表分区dts里定义的起始地址和大小必须与烧录工具使用的分区配置一致否则启动时会因为分区重叠或内核加载地址错误直接起不来而且这种问题往往很难一眼看出来。5. 常见问题、避坑指南与排查思路5.1 依赖库不全导致的报错汇总搭建环境时最常见的报错就是编译过程中某条命令找不到某个库或工具。典型的有menuconfig打开时报Unable to find the ncurses libraries说明libncurses-dev没装mkimage命令找不到说明u-boot-tools没装好dtc命令找不到说明device-tree-compiler没装。这类问题解决思路基本一致看报错信息里缺少的命令名直接apt search找到对应包安装即可不要再重复解压SDK或者重新编译那样费时费力。还有一类隐蔽的问题是32位库缺失。某些老版本的rootfs制作工具和升级工具是32位程序在64位系统上直接报No such file or directory。如果你确认这个文件明明存在却还是提示找不到大概率就是缺少32位运行库。在Ubuntu 20.04上通常可以用sudo apt install lib32z1 lib32ncurses6解决如果还不行用file命令查一下这个可执行文件的架构信息再去搜索对应依赖库。5.2 环境变量、权限和路径这类软问题相比缺库环境变量、权限和路径问题更容易让人抓狂因为它们的报错往往不是那么直接。最常见的场景是新开一个终端后make提示找不到交叉编译器。这多半是环境变量没写到配置文件里只是临时export过换终端就失效了。另一个常见场景是在root用户下编译成功但普通用户下报权限错误或者反过来。这通常是之前用不同用户解压或编译导致文件属主混乱。我的习惯是选定一个编译用户后整个SDK的目录归属、编译操作都在这个用户下完成没有特殊情况不来回切换。路径问题也很典型有些人喜欢把SDK解压后移动到别处或者放在带空格的路径下比如/home/user/my sdk这会导致Makefile里很多脚本因为路径解析错误而失败。嵌入式工具链和Makefile对空格路径支持普遍不好建议所有工程路径都不含空格和中文解压后也不要随意移动目录。5.3 内核编译报错的一些实录与快速定位方法编译内核时我遇到过印象最深的报错是arch/arm64/Makefile: ... recipe for target Image failed这类信息量很少真正的错误在上面几百行里。面对这种编译失败我建议不要只看最后几行把完整日志保存下来之后用关键词搜索。比如make kernel 21 | tee build_kernel.log出了错之后再用grep -i error build_kernel.log把错误行摘出来许多真实的原因比如头文件缺失、配置项冲突、dts语法错误都会包含在error信息中。还有一个很实用的小技巧如果报错看起来跟某个.config选项有关检查一下源码根目录下是否有stale的.config文件把板级默认配置重新加载一次往往能解决配置漂移引发的编译问题。5.4 编译效率优化ccache、并行度与只编模块整条SDK链路第一次全量编译可能耗时很长所以实际开发中提升效率非常关键。我常用的办法有三个。第一使用ccache做编译缓存直接在编译命令前加ccache或者在环境变量里配置CCACHE_PREFIX内核这种反复修改配置的编译场景收益尤其明显。第二合理设置并行度make -j$(nproc)当然可以但如果编译服务器内存不大并行任务太多反而可能OOM建议根据CPU核数和内存大小平衡比如16核32G内存的机器用-j8通常比较稳。第三只编需要改的部分uboot、kernel、rootfs各自单独编译改哪里编哪里打包时再统一走流程这是开源工程里最常见的做法也是最高效的日常开发循环。6. 一些开发习惯上的经验之谈6.1 把官方文档当成第一参考资料我见过不少开发者遇到问题时第一反应是去搜索引擎找答案但嵌入式开发的很多坑在公开网络上根本没有现成答案靠谱的路径反而是先看官方文档。海思SDK里的docs目录覆盖了uboot、内核、rootfs、烧录、工具链等所有环节很多问题在文档的FAQ章节里都有明确解释。建议在开始开发前把最核心的几篇文档通读一遍尤其是跟编译环境和烧录相关的章节能避免后面大量踩坑。6.2 版本记录的纪律以可复现为目标工具链版本、SDK版本、内核版本、Ubuntu版本、关键补丁这些信息在项目初期就要记录清楚最好写进README或者维护一份环境说明文档。原因很简单嵌入式软件的生命周期很长一年后你大概率要重新搭建环境如果当时没有记录面对一堆细节变量很难还原。这个习惯也是我把环境搭建和版本固化当作完整工程的一部分的原因。6.3 建议从最简配置开始验证最后分享一个适用于所有嵌入式平台的经验第一次跑通系统尽量用最少的功能组合。先不裁减内核先不加复杂业务先确认uboot和最小根文件系统能启动再去叠加需求。很多团队第一次开发板起不来排查下来往往是一上来就试图同时搞定太多东西一旦出了问题就分不清是uboot的问题、内核的问题还是rootfs的问题。先把最小系统跑通后面的功能开发才谈得上效率。我在实际搭建这套环境时踩过最深的坑就是最初没有留意SDK版本和工具链版本的匹配关系结果编译出的内核在板子上启动到一半就卡死。后来翻官方文档才意识到不同SDK版本默认使用的工具链并不完全相同直接拿通用交叉编译器替代会埋下隐患。所以每次拿到新的开发板我都会先按照本文这套流程重新走一遍环境搭建确认全链路能产出可启动镜像后再开始往里面加自己的东西。这个过程确实琐碎但省下来的排错时间远比当初随手贴个教程、跳步骤省下的那几分钟要多得多。
