首页 / 资讯中心 / 文章详情

Ubuntu下HPM6750 RISC-V开发环境与CMake构建实战

Ubuntu下HPM6750 RISC-V开发环境与CMake构建实战 ★ FEATURED ARTICLE
拿到一块 HPM6750 的评估板插上 USB 线屏幕亮了但接下来该干什么很多做单片机出身的人第一反应是找个 IDE、新建工程、点编译、点下载。放到 HPM6750 RISC-V 开发这套体系里这条路走不通——原厂给的主线是一套基于 CMake 的 SDK 构建系统Ubuntu 环境下从工具链安装到最终烧录中间隔着工具链路径、板级配置、链接脚本、调试探头配置等好几道坎。这篇就把我在 Ubuntu 上把 HPM6750 RISC-V 开发环境跑通、并用 CMake 构建完整工程的全过程摊开讲包括每一步为什么要这么做、参数到底管什么、以及那些文档里不会写的翻车点。适合刚拿到板子、或者在 Linux 下做嵌入式开发想换个平台试试的朋友基础薄一点也没关系关键步骤我都会把原理补上。1. 动手装环境之前先把这块芯片的构建逻辑摸清楚1.1 双核 RISC-V 对构建系统提出的真实要求HPM6750 是一颗双核 RISC-V 的 MCU两个核各自带浮点和 DSP 扩展主频跑得很高片上 SRAM 也大外设从以太网、USB 高速口、CAN FD 到 RGB 显示接口、摄像头接口都有。这个配置意味着两件事第一工程里的代码量不会小随随便便一个带协议栈和图形库的例程就是几百个源文件第二双核意味着你迟早要面对一份代码编译出两个镜像、两个镜像怎么合并、谁先启动、共享内存怎么划的问题。如果还沿用那种一个 .c 文件列表手工维护、一个 Makefile 从头写到尾的方式光是维护两套目标文件就够喝一壶。构建系统必须能处理同一份源码用不同的编译选项、不同的链接脚本、生成两个 elf然后再合成一个烧录文件这种流程。这就是为什么原厂主线选择了 CMake 而不是给你一堆现成的 IDE 工程文件——CMake 天生适合描述多目标、多配置、参数化的构建逻辑改板子、改核、改优化等级只需要换几个变量而不是复制一整个工程目录。理解这一点很关键因为它决定了你后面调 bug 的思路遇到编译报错先想的是哪个 CMake 变量没传对而不是我哪个文件没加进工程。这两种思路的排查路径完全不一样前者去翻 CMake 缓存里的变量值后者去翻 IDE 的工程树方向错了会浪费大量时间。1.2 hpm_sdk 的目录分工boards、soc、samples、cmake 各管什么SDK 解压出来第一眼看上去目录不少但真正需要你关心的就四块搞清它们的分工后面所有操作都有据可依。boards/目录下是按板子型号分的文件夹一块板一个目录。每个目录里通常包含板级引脚定义、时钟配置、链接脚本、调试探头配置文件OpenOCD 用的那种 .cfg以及一个板级的 CMakeLists。你在构建命令里传的-DBOARDxxx就是让构建系统去这个目录下找对应板子的配置。换句话说换板子只需要换这个参数其他都不动。soc/目录下是芯片级别的定义包括启动文件、向量表、外设寄存器头文件、系统初始化代码。这一层跟板子无关同一颗芯片的所有板子共用。你基本不需要改这里但排查启动问题时需要来这里看启动流程。samples/是例程集合每个例程一个独立目录自带 CMakeLists。这是最好的学习材料也是你新建工程的模板来源。很多人第一次用会直接把 samples 里的某个例程当自己的工程改这没问题但要按后文说的方法复制不要原地改。cmake/目录是整个构建系统的核心里面是各种 .cmake 脚本工具链定义、编译选项、链接参数、配置宏生成逻辑都在这里。这个目录平时不需要动但出问题时值得翻一翻因为很多看起来莫名其妙的编译行为答案就写在这些脚本里。提示SDK 根目录通常还会带一个环境初始化脚本名字类似 env.sh它做的事情就是把HPM_SDK_BASE和工具链路径这两个变量设好并导出。你可以直接 source 它也可以按自己的目录结构手工设后者更可控尤其是在你同时装了多个版本 SDK 的时候。1.3 为什么原厂主线用 CMake 而不是直接发 IDE 工程有人会问直接给一套 IDE 工程多省事双击打开就能编。这话在单核、小工程时代成立放到现在不成立。原因有三个。第一IDE 工程文件是二进制或者带大量绝对路径的 XML一旦你的 SDK 装在不同路径工程就得重新生成一遍。多人协作时这种每人一份绝对路径的工程极其痛苦。第二双核工程需要编译两次、合并一次的流程IDE 的图形化配置很难干净地表达这种依赖关系而 CMake 用add_custom_command加一个自定义目标就能搞定。第三持续集成。你要做自动化构建、自动化测试命令行一条 cmake 命令跑完全流程比在服务器上装个图形 IDE 现实得多。所以我的建议是前期花两三个小时把命令行构建跑通后面所有的 IDE 都只是锦上添花。事实上 SDK 里通常也提供了从 CMake 工程反向生成 IDE 工程的脚本你把命令行跑通了生成 IDE 工程也是一条命令的事反过来则很别扭。2. Ubuntu 侧的基础环境依赖、工具链和那条最容易设错的环境变量2.1 系统依赖与版本对齐Ubuntu 装嵌入式工具链很多人第一反应是apt install gcc-riscv64-unknown-elf之类的包。这条路我不推荐原因很直接发行版仓库里的交叉工具链版本通常落后不少而且riscv64和riscv32是两套不同的目标HPM6750 是 32 位核虽然 64 位工具链加-marchrv32也能编但 newlib 的库文件配套容易出问题链接阶段报找不到-lc、-lnosys是家常便饭。稳妥做法是用官方预编译的 RISC-V GNU 工具链32 位版本版本号按 SDK 文档里标注的要求来别自己乱升。至于系统层面的依赖Ubuntu 这边主要需要这几类构建工具本身cmake、make 或 ninja、编译器基础组件build-essential、以及调试烧录链路上用到的工具openocd 相关的依赖、gdb 的 python 支持、串口工具。另外有几个库容易被忽略libncurses系列gdb 要、libusb调试探头要、libftdiFT2232 类探头要。这些库在 Server 版 Ubuntu 上默认没装缺了之后现象是工具装上了但一跑就报 shared library 找不到看着像权限问题实际是缺库。我的习惯是先把依赖一次性装齐再动工具链因为工具链下载解压是个慢活儿中间发现缺依赖再回头补很打断节奏。检查方式很简单把常用命令挨个--version跑一遍报错的就是缺的。2.2 RISC-V GNU 工具链的安装与路径规划工具链的放置位置我建议统一到/opt/下比如/opt/riscv32-toolchain/这样多个项目、多个 SDK 版本都能共用不会出现换个项目就要重装工具链的尴尬。解压后目录里会有一个bin/里面是riscv32-unknown-elf-gcc、-g、-objcopy、-size、-gdb这一整套。路径规划上有个坑值得单独说工具链的绝对路径里不要带空格、中文、以及太深的层级。CMake 的很多变量在传递过程中会被引号处理路径里一旦有空格某些脚本会把它拆成两个参数报错信息还特别隐晦比如找不到编译器 xxx 的前半段。我最早把工具链放在一个带空格的目录下排查了快两个小时才反应过来。另一个建议是不要把工具链的bin/加到全局PATH里。原因是你机器上可能同时有别的 RISC-V 项目在用另一套工具链全局 PATH 一冲突which riscv32-unknown-elf-gcc出来的结果和你以为的不一样。正确做法是通过环境变量指定让构建系统自己去用那个路径全局 PATH 保持干净。2.3 环境变量设置与三步自检SDK 构建系统认的环境变量主要就两个一个是 SDK 根目录HPM_SDK_BASE一个是工具链路径GNURISCV_TOOLCHAIN_PATH。这两个变量如果设错现象是cmake 能跑完配置阶段但一编译就全线报错或者配置阶段直接告诉你找不到工具链。设完之后做三步自检能挡掉八成低级错误工具链自检直接调工具链里的 gcc 编译一个空文件确认-marchrv32imafdc这类参数能被接受。这一步验证的是工具链本身完整、目标架构支持。SDK 路径自检ls $HPM_SDK_BASE/cmake和ls $HPM_SDK_BASE/boards两个都能列出来说明路径指对了。很多人把路径指到了 SDK 的子目录看着像对的其实差一层。板子名自检ls $HPM_SDK_BASE/boards里能找到你手上那块板的目录名这就是你-DBOARD该填的值一字不差地抄过去大小写敏感。注意环境变量只在当前 shell 生效。如果你开新终端后突然编不过了先想想是不是忘了重新 source。我的做法是把这几行写进一个自己的env.sh放在项目根目录每次开工先 source 一遍比改.bashrc更可控——改.bashrc的问题是它会污染所有终端你哪天想临时换个工具链版本会发现改不过来。3. 把 cmake 那几行命令拆开看从源码到可烧录文件3.1 一次完整构建命令的参数含义标准流程是进到某个例程目录建一个build子目录进去执行 cmake再执行 make。命令大致长这样mkdir -p build cd build cmake -DBOARD你的板子名 -DCMAKE_BUILD_TYPEdebug .. make -j8三行命令每一行都有讲究。mkdir build cd build是所谓的外部构建把生成物和源码分开。这不是习惯问题而是硬要求SDK 的 CMake 脚本里有相对路径引用在源码目录里直接 cmake 会生成一堆中间文件污染源码树而且清理起来极麻烦。清缓存就是删 build 目录干净利落。-DBOARD是板级选择前面说过它决定了链接脚本、引脚定义、时钟配置用哪一套。这里填错的现象很有意思能编译能链接甚至能下载但程序一跑就复位或者串口没输出因为引脚定义和实际硬件对不上。-DCMAKE_BUILD_TYPEdebug控制优化等级和调试信息。debug 是-O0 -g方便单步release 是-O2或-O3代码小、跑得快但你打断点会发现行号跳来跳去。我的建议是前期一律用 debug等逻辑稳定了再切 release 验证性能和体积。切换的时候有个坑同一个 build 目录来回切构建类型缓存里的编译选项不一定完全刷新遇到改了类型没生效的情况直接删 build 目录重来最省事。make -j8里的核数按你机器来一般取 CPU 核数或略少。并行编译在 SDK 这种几百个文件的工程上能省一半以上时间但要留意报错信息会交错出了错在最后看第一个 error 就行。3.2 toolchain 文件与链接脚本的配合关系CMake 交叉编译的核心是 toolchain 文件它在配置阶段一开始就被读入负责告诉 CMake用什么编译器、什么目标架构、什么系统。SDK 的cmake/目录里就有这么一份它通常会根据GNURISCV_TOOLCHAIN_PATH拼出编译器路径再根据芯片型号拼出-march、-mabi参数。链接脚本是另一回事它决定代码放在哪块存储器、栈放在哪、堆多大。SDK 的boards/板名/linker/下一般会有多个 .ld 文件对应不同的运行方式有的把代码直接放在片上 SRAM 里跑调试方便断电丢失有的把代码放在外部 Flash 里跑掉电保存但启动时要先从 Flash 搬运或者配成 XIP 执行。这两个文件的关系是toolchain 文件决定怎么编链接脚本决定放哪里。它们的配合点在链接阶段如果-march里开了某个扩展、但链接脚本里没预留对应的库空间或者芯片本身不带 F 扩展却链了浮点库就会在链接阶段报一堆undefined reference。提示如果你不确定该用哪个链接脚本去看boards/板名/CMakeLists.txt里的默认设置那里通常写了默认选哪个、以及怎么用变量覆盖。别自己猜文件名。3.3 构建产物与 map 文件的阅读方法编译完成后输出一般在build/output/下会有.elf、.bin、.hex和.map这几个。elf 是带符号和调试信息的完整镜像下载和 gdb 调试用它bin 是纯二进制用于串口 ISP 之类的烧录方式hex 是带地址信息的文本格式map 是链接映射表。map 文件是最容易被忽略但价值极高的东西。它告诉你每个函数、每个变量被放到了哪个地址段、占了多大空间。我最常用的两个场景一是编译通过了但烧进去不跑去 map 里看入口地址和向量表地址对不对二是 RAM 不够用了去 map 里按大小排序找哪个大数组吃的空间。具体怎么看map 文件里通常有 Memory Configuration 段列出每块存储器的起始地址和大小有 Linker script and memory map 段逐个符号列出地址末尾还有 Discarded input sections 段被丢弃的段都在这里。有时候你以为加进去的代码没生效去这一看原来是被链接器当无用段丢了原因可能是没被引用也可能是编译选项里的-ffunction-sections -Wl,--gc-sections把它回收了。4. 从示例工程到自己的工程app 目录该怎么改4.1 复制示例前先看 SDK 里 app 的 CMakeLists 约定几乎所有人都是从samples/里挑一个例程复制出来改成自己的工程。这一步本身没问题但复制之前要先读懂例程目录里的CMakeLists.txt。SDK 的例程 CMakeLists 一般遵循固定套路先find_package或者include一个 SDK 提供的公共 cmake 模块声明这是个 SDK 应用工程然后用一个宏或者函数把自己的源码列表注册进去最后调用一个生成目标函数。这套封装的好处是你不需要手写交叉编译参数坏处是这些宏的名字和用法在不同 SDK 版本之间可能有变化所以别凭记忆写照抄例程。复制的时候有几个细节要注意。第一目录名不要带空格和中文前面说过。第二复制出来的工程目录里如果有build目录先删掉那是旧路径的缓存。第三例程的 CMakeLists 里可能有对上级目录的相对引用比如../../..指向 SDK 根你如果把工程复制到 SDK 之外的目录这些相对路径就断了。我一般的做法是把自建工程放在 SDK 包的samples/同级或者干脆放在 SDK 外但通过HPM_SDK_BASE引用——后者更干净但要确认例程里的引用方式都是通过环境变量而不是相对路径。4.2 增删源文件、头文件路径与编译宏工程里加自己的代码涉及三件事源文件、头文件路径、编译宏。三件事在不同的 SDK 封装里有不同的写法但逻辑相通。源文件方面SDK 的宏一般是接受一个列表你可以一次给多个也可以调用多次累加。注意源文件路径一般是相对于当前 CMakeLists 的位置用${CMAKE_CURRENT_SOURCE_DIR}拼比较稳。新增一堆文件时我习惯按模块分目录然后在 CMakeLists 里用file(GLOB ...)或者显式列出来。这两者各有取舍显式列出更可控新增文件忘了加会立刻报错GLOB 方便但要重新跑 cmake 才能感知新文件而且有些规范不建议用。SDK 例程一般用显式列表建议跟着用。头文件路径用target_include_directories或者 SDK 封装的对应宏。这里有个常见错误是路径写成了文件路径而不是目录路径编译器会找不到但报错信息说找不到 xxx.h你盯着那个 .h 看了半天其实问题是路径多写了一层。编译宏用target_compile_definitionsSDK 更常见的做法是通过-DCONFIG_xxx1这种方式在 cmake 命令行传然后由 SDK 的脚本统一收集生成一个配置文件类似hpm_config.h。这种设计的意图是功能开关由构建配置决定代码里只#include这个生成的头文件然后#if判断。好处是同一份代码可以编出不同功能组合的固件坏处是你改宏之后必须重新跑 cmake只 make 是不会更新的。4.3 双核工程的组织方式HPM6750 是双核第二个核要跑代码就得单独编一份镜像。SDK 里通常会有多核的例程目录结构是把两个核的代码分开放各自一个 CMakeLists最外层一个 CMakeLists 用add_subdirectory分别引入再加一个自定义目标把两份产物合并。这里的关键概念是内存布局。两个核共用片上 SRAM但不能用同一块否则一个核写的数据被另一个核冲掉。所以链接脚本里必须给两个核划分不同的区域而这个划分又必须和启动代码里的约定一致。SDK 的多核例程里一定有类似共享内存段的定义改的时候要三处对齐核 0 的链接脚本、核 1 的链接脚本、以及两边代码里对共享区的声明。启动顺序上一般是主核先跑初始化时钟和外设然后把从核的入口地址写到一个约定位置并触发启动。这个流程在 SDK 的启动代码里已经实现你要做的只是确认从核镜像被正确加载到了它该在的地址。我踩过一次坑从核镜像加载地址和链接地址差了一个偏移现象是从核完全不执行也不报错查了两天才发现是合并脚本里的偏移写错了。5. 烧录与调试把 openocd 这条链跑通5.1 调试探头与连接配置Ubuntu 下烧录调试主流方案是 OpenOCD 加一个调试探头。探头类型市面上常见的有几类基于双通道 USB 转串口芯片的方案、CMSIS-DAP 方案、以及商用高速探头。SDK 的板级目录里一般会带对应的 OpenOCD 配置文件你按自己手上的探头选一个。用探头之前有个绕不开的坑Linux 下的 USB 设备权限。默认情况下普通用户没有权限直接访问 USB 设备OpenOCD 会报找不到设备或者权限不足。解决办法是加一条 udev 规则把探头厂商和产品 ID 对应的设备权限放开然后重新插拔。很多人卡在这一步以为是驱动问题其实 Linux 内核早就认出来了就是权限没给。另一个需要确认的是探头固件版本。商用探头的固件和 OpenOCD 版本之间有兼容性要求固件太老或者 OpenOCD 太新都可能连不上。SDK 文档一般会写推荐组合照着来。实在连不上先换个 OpenOCD 版本试试这比怀疑硬件快得多。5.2 用 openocd 命令行直接下载调试链路打通之后最简单的下载方式就是用 OpenOCD 直接 programopenocd -f 探头配置.cfg -f 板级配置.cfg \ -c program build/output/你的工程.elf verify reset exit这条命令做四件事加载配置、下载 elf、校验、复位并退出。verify建议保留它会读回 Flash 内容比对能挡住下载成功但内容错了这类问题。去掉exit则进入交互模式可以配合 gdb 做源码级调试。gdb 那边用工具链自带的riscv32-unknown-elf-gdb先target remote :3333连上 OpenOCD 的调试端口再load和continue。第一次连的时候记得monitor reset halt让核停在复位向量否则你连上去的时候程序已经在跑了断点打不上。串口 ISP 是另一条路不依赖调试探头通过 UART 进芯片内置的引导程序烧写。适合产线批量也适合手头没有探头的场景。代价是你需要手动让板子进 ISP 模式一般靠拨码或者按键改变启动引脚电平而且烧录速度慢调试更谈不上。日常开发我还是建议用探头ISP 留作后备方案。5.3 下载失败的五类原因与排查顺序下载失败的现象五花八门但成因基本能归到五类按这个顺序排查效率最高排查顺序现象可能原因验证方式1OpenOCD 启动即报找不到设备USB 权限、探头未识别lsusb看设备在不在检查 udev 规则2能找到设备但连接超时探头固件与 OpenOCD 版本不匹配、线序接错换 OpenOCD 版本检查 SWD/JTAG 接线3能连上但 halt 不住核芯片处于低功耗或安全状态、复位引脚被拉住手动复位一次再连检查复位电路4下载过程报校验失败Flash 未解锁、时钟配置不对、链接脚本地址超出 Flash 范围看 map 文件确认地址段检查 Flash 驱动配置5下载成功但不运行启动模式引脚不对、向量表位置不对、链接脚本选了 RAM 版对照板子手册确认启动模式换链接脚本重编这个表是我自己排出来的实际用的时候从上往下走绝大多数问题在前两步就能定位。特别提醒第五类下载成功但不运行很多人第一反应是代码有 bug其实八成是启动模式或者链接脚本的问题——RAM 版镜像烧到 Flash 里当然跑不起来因为启动时它期望代码已经在 RAM 里了。6. 我在 Ubuntu 上踩过的几个坑以及它们的真实成因6.1 CMake 缓存里残留的旧工具链路径这个坑我踩过不止一次。现象是明明环境变量已经改成新工具链了cmake配置阶段也没报错但编译出来的东西行为就是不对甚至链接阶段报一堆奇怪的符号错误。成因在于 CMake 的缓存机制。CMake 第一次配置时会把编译器路径、工具链相关变量写进CMakeCache.txt后续再跑 cmake 时这些变量不会被环境变量覆盖——缓存优先级高于环境变量。所以你换了工具链但 build 目录还是旧的CMake 依然用缓存里那个旧路径。验证方法很直接grep -i toolchain CMakeCache.txt看看里面的路径是不是你以为的那个。修复方法也简单删掉 build 目录重新配置。这件事教育我养成一个习惯——只要动了工具链、SDK 路径、板子名这三样中的任何一样一律删 build 重来不要试图在旧缓存上打补丁省那两分钟不值当。顺带一提同样的坑也出现在CMAKE_BUILD_TYPE上。debug 和 release 来回切有时候编译选项没完全刷新表现是我明明改了优化等级怎么代码行为没变。一样的处理方式删缓存重来。6.2 链接脚本选错能编译能下载就是跑不起来这个坑的迷惑性最强因为整条链路前面全是绿的编译通过、链接通过、下载校验通过唯独程序不跑串口一个字节都不输出调试器 halt 住看到的 PC 值也不在预期的复位向量上。我遇到的那次原因是链接脚本用了 RAM 版本但我把镜像烧到了 Flash 区域。芯片上电后从 Flash 启动而 Flash 里的内容是按 RAM 布局链接的入口地址对不上程序自然跑飞。排查方法先看 map 文件里 Memory Configuration 的地址段再对照板子手册里 Flash 和 SRAM 的地址范围两边一对就知道对不对。另外 elf 文件可以用readelf -h看入口地址如果入口地址落在 RAM 段而你是从 Flash 启动的那就是这个问题。这个坑还有一个变种板子上的启动模式引脚设置错了。有些板子支持从 Flash 启动、从 USB 启动、从串口启动等多种模式靠拨码或者跳线选择。如果拨到了串口启动模式芯片上电后就在等串口数据你烧到 Flash 里的程序根本不会被执行。这种情况下程序、链接脚本全对就是启动模式不对查起来最费劲因为从软件侧看不到任何异常。6.3 权限与串口设备名那些小事最后说几个小但高频的问题。串口设备名在 Ubuntu 上一般是/dev/ttyUSB0或/dev/ttyACM0但插多个 USB 设备时编号会变。我一般用ls -l /dev/serial/by-id/看稳定的设备标识按 id 连接避免昨天是 ttyUSB0 今天是 ttyUSB1的尴尬。串口工具方面minicom、picocom、screen都行我个人习惯picocom参数简单picocom -b 115200 /dev/ttyUSB0。注意波特率要和代码里的配置一致SDK 例程默认一般是 115200但也有用别的看代码里的宏定义。权限问题同样适用于串口。普通用户默认不在dialout组里加进去之后要重新登录才生效。这个提示很多教程会写但真正被卡住的人不少因为现象是打开串口失败报权限不足新手容易误以为是串口坏了。还有一个细节make -j并行编译时如果出现莫名其妙的编译错误先试试单线程跑一遍。并行编译有时会把真正的第一个错误信息淹没在大量输出里单线程跑一遍反而一眼看到根因。这个技巧在 SDK 这种大工程上特别有用。我个人在实际操作中的体会是HPM6750 这套开发环境真正的门槛不在技术难度而在信息对齐——工具链版本、SDK 版本、板子配置、调试探头固件、链接脚本这五样东西必须严丝合缝地对上任何一处偏差都会以看起来完全不相关的方式表现出来。所以我现在养成一个习惯每换一个环境先花十分钟把这五项写在一张纸上一项项核对比事后花两小时排查划算得多。另外建议你把跑通的那套命令原封不动地记成脚本包括环境变量、cmake 参数、openocd 命令下次换机器直接跑脚本省下来的时间足够你把外设驱动多调通两个。
阅读完成 · 觉得有帮助?
咨询建站