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 镜像、再到远程调用的完整数据流与工具链关系,注意区分“编译期产物”和“运行期通道”两类对象。

编译期(Host 上执行) vec_add.c DSP 侧 C 实现 vec_add.idl 接口定义 hexagon-clang -mv65 -G0 -fPIC qaic / idl 编译器 生成 stub + skel hexagon-ld 链接 libs 与 skel gcc (host) 编译 CPU 侧 stub libvec_skel.so DSP 镜像(Hexagon ELF) libvec_stub.so CPU 侧调用库 运行期(设备上执行) CPU 应用进程 vec_add_stub() libadsprpc.so FastRPC 通道 共享内存 + 消息队列 /dev/fastrpc-* Hexagon DSP libvec_skel.so vec_add() 执行 返回结果 / 状态码

二、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 会完成镜像加载、符号解析和远程过程绑定,因此首次调用延迟明显高于后续调用,这是正常现象。

判断远程调用是否真正落到 DSP 上,可以观察 /sys/kernel/debug/adsprpc/ 下的统计信息,或使用 fastrpc 的日志开关。如果 vec_add 返回 AEE_SUCCESS 但结果全为 0,优先检查 IDL 中 out 是否被标为 rout,以及长度参数是否与数组正确绑定。

五、常见失败点与排查顺序

第一个 Hexagon 程序最容易卡在三个地方:一是架构不匹配,用 -mv60 编译的镜像推到 v65 设备上会加载失败;二是 IDL 方向标注错误,导致 out 缓冲区没有被回传;三是路径问题,skel 没有放到 rfsa/adsp 目录下,FastRPC 找不到镜像。建议按“先确认镜像架构 → 再确认 IDL 生成物 → 最后确认设备路径”的顺序排查,而不是一上来就怀疑 DSP 代码逻辑。