3. 第一个 Hexagon 程序
在上一节中我们已经理解了 Hexagon DSP 在 Snapdragon 平台中的角色:它是一颗独立运行、拥有自己指令集、内存空间和实时操作系统的协处理器。要让这颗 DSP 真正“跑起来”一段我们写的逻辑,需要走完一条完整的工具链路径——从一份普通的 C 源文件,经过交叉编译、链接、签名打包,最终变成一个可以被 CPU 侧加载并远程调用的 DSP 镜像。本节我们就用一个最小的“向量加法”示例,把这条路径完整走一遍。
一、先明确我们要交付的产物
Hexagon 程序最终交付的并不是一个普通的 ELF 可执行文件,而是一个可以被 Qualcomm 的 FastRPC 框架加载的共享对象(.so),通常称为 skel(skeleton,DSP 侧桩)。CPU 侧还会有一个对应的 stub(.so),负责把应用层的函数调用序列化后通过 FastRPC 通道转发给 DSP。因此一个完整的“第一个程序”实际上包含三个部分:
- DSP 侧实现:用 C 写的计算函数,编译成 Hexagon 架构的
.so; - IDL 接口定义:描述 CPU 与 DSP 之间函数的参数、方向(in/out)和长度,是生成 stub/skel 胶水代码的依据;
- CPU 侧调用:应用通过生成的 stub 头文件调用函数,就像调用本地函数一样。
下面这张图展示了从 C 源码到 DSP 镜像、再到远程调用的完整数据流与工具链关系,注意区分“编译期产物”和“运行期通道”两类对象。
二、DSP 侧 C 源码与 IDL 定义
先写 DSP 侧的计算函数。为了聚焦流程,我们只做两个 int32 数组的逐元素相加,长度由参数传入。注意 Hexagon 上指针参数需要明确方向,否则 IDL 编译器无法生成正确的序列化代码。
// vec_add.c —— DSP 侧实现
#include "AEEStdDef.h"
#include "vec_add.h" // 由 IDL 生成
AEEResult vec_add(const int32 *a, int aLen,
const int32 *b, int bLen,
int32 *out, int outLen)
{
if (aLen != bLen || aLen != outLen) return AEE_EBADPARM;
for (int i = 0; i < aLen; i++) {
out[i] = a[i] + b[i];
}
return AEE_SUCCESS;
}
对应的 IDL 文件描述接口契约。in 表示 CPU 传给 DSP,out 表示 DSP 回传给 CPU,rout 表示返回值。长度参数用 aLen 这种形式与数组绑定,编译器据此生成正确的缓冲区分配与拷贝逻辑。
// vec_add.idl
#include "AEEStdDef.idl"
interface vec {
AEEResult add(
in sequence<int32> a,
in sequence<int32> b,
rout sequence<int32> out
);
};
三、编译与链接命令
假设使用 Hexagon SDK 的 HEXAGON_SDK_ROOT 环境变量,目标架构为 v65(对应较新的 Snapdragon 8 系)。关键编译选项有三个:-mv65 指定 DSP 架构,-G0 关闭小数据区(避免跨模块访问问题),-fPIC 生成位置无关代码,因为 DSP 镜像会被动态加载到任意地址。
# 1. 生成 stub 与 skel 胶水代码
qaic -mdll -o gen vec_add.idl
# 2. 编译 DSP 侧实现
hexagon-clang -mv65 -G0 -fPIC -I$HEXAGON_SDK_ROOT/incs \
-Igen -c vec_add.c -o vec_add.o
# 3. 链接成 DSP 镜像
hexagon-ld -shared -o libvec_skel.so vec_add.o gen/vec_add_skel.o \
-L$HEXAGON_SDK_ROOT/libs -ladsprpc
# 4. 编译 CPU 侧 stub
gcc -fPIC -shared -Igen -o libvec_stub.so gen/vec_add_stub.c \
-L$HEXAGON_SDK_ROOT/libs -ladsprpc
执行完成后,libvec_skel.so 就是我们要推到 DSP 上的镜像。可以用 hexagon-objdump -h libvec_skel.so 检查它是否包含 .text、.data 等段,以及入口符号 vec_add 是否存在。如果链接时报 undefined reference to AEE_SUCCESS,通常是 -ladsprpc 或 SDK 头文件路径没配对。
四、加载与远程调用
在设备上,DSP 镜像并不是由应用直接 dlopen 的,而是由 libadsprpc.so 通过 FastRPC 驱动把 .so 推送到 DSP 的地址空间并完成重定位。应用侧只需要调用生成的 stub 函数:
#include "vec_add.h"
int main() {
int32_t a[4] = {1, 2, 3, 4};
int32_t b[4] = {10, 20, 30, 40};
int32_t out[4] = {0};
int32_t outLen = 4;
AEEResult r = vec_add(a, 4, b, 4, out, &outLen);
if (r != AEE_SUCCESS) {
printf("remote call failed: 0x%x\n", r);
return -1;
}
for (int i = 0; i < 4; i++) printf("%d ", out[i]);
// 期望输出: 11 22 33 44
return 0;
}
运行前需要确保 libvec_skel.so 被放到 DSP 可访问的路径(通常通过 adb push 到 /vendor/lib/rfsa/adsp/),并且 libvec_stub.so 在 CPU 侧可被链接到。第一次调用时,FastRPC 会完成镜像加载、符号解析和远程过程绑定,因此首次调用延迟明显高于后续调用,这是正常现象。
/sys/kernel/debug/adsprpc/ 下的统计信息,或使用 fastrpc 的日志开关。如果 vec_add 返回 AEE_SUCCESS 但结果全为 0,优先检查 IDL 中 out 是否被标为 rout,以及长度参数是否与数组正确绑定。
五、常见失败点与排查顺序
第一个 Hexagon 程序最容易卡在三个地方:一是架构不匹配,用 -mv60 编译的镜像推到 v65 设备上会加载失败;二是 IDL 方向标注错误,导致 out 缓冲区没有被回传;三是路径问题,skel 没有放到 rfsa/adsp 目录下,FastRPC 找不到镜像。建议按“先确认镜像架构 → 再确认 IDL 生成物 → 最后确认设备路径”的顺序排查,而不是一上来就怀疑 DSP 代码逻辑。