打印
[G32R]

这样够快么?G32R501 与 Zidian 不得不说的秘密!

[复制链接]
100|1
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主

1. 背景

在传统的嵌入式开发中,如果我们需要在一颗小小的 MCU 上跑一些高强度的运算,常规做法是:

  • 借助库函数或手写汇编进行手动优化;
  • 或是在硬件设计层面,通过额外的外设或协处理器(crypto, DSP) 助力,完成特定功能加速。

但随着对性能需求越来越苛刻,硬件资源又不想失去 Arm 生态的广泛支持,怎么办?  这时候,“自定义指令” (或称自定义加速单元) 就闪亮登场了:

  1. 具备灵活性:  允许根据应用需求把常用的算法指令“写入”CPU,以极低延迟操作处理器寄存器。
  2. 兼容生态:  不必魔改编译器、无须大改调试器;保持与现有 Arm 工具的融洽相处。

zidian 就是这样一个在 G32R501 MCU 上基于 ACI 机制打造的“加速引擎”,主要目标是——极速飙升运算能力,让原本需要几十条甚至上百条指令才能搞定的运算,缩短到寥寥几条。

2. 不得不说ACI:一把开启硬件定制的大门

2.1 ACI 的“前世今生”

ACI 全称 Arm Custom Instructions,它让芯片厂商能够在 Cortex-M33、Cortex-M55、Cortex-M52 等处理器上,自定义数据处理指令集。

  • 传统做法中,也有借助“Coprocessor Interface (协处理器接口)”实现硬件加速,但它往往需要独立管理寄存器、内存访问,适合批量处理或在后台执行的场景。
  • ACI 则进一步贴近 CPU 寄存器流水线,“紧耦合”地完成特定操作,非常适合短周期、高频调用的计算需求。

简单来说,ACI 就是为 MCU 打开了一扇“能深度定制指令的门”,保留了标准 Arm 生态的同时,提供了更细粒度的指令级优化。

image-20250707211719864.png

2.2 ACI 的实现方式

在硬件层面,Arm 针对协处理器指令编码区,预留了部分空间让厂商编写自己的操作:

  1. 指令解码阶段:  CPU 内部会检测该指令是否隶属某个 coprocessor number;若与 ACI 相关,则交由“定制数据通路”进行计算;
  2. 数据读写:  CPU 读取需要的寄存器 (整数寄存器或浮点寄存器),然后将数据送给 ACI 硬件模块;
  3. 获取结果:  结果再写回 CPU 寄存器;若支持更新 APSR 的某些标志位 (如NZCV) 也可以在此阶段完成。

2.3 ACI 指令风格

在 Arm v8-M 架构里,ACI 以类似于 “CXn” 或 “VCXn” 的形式呈现:

  • “CX” 前缀常对应整数指令;
  • “VCX” 前缀常对应浮点 (FPU) 或向量 (MVE);
  • n 可取 1, 2, 3 等,表示几个输入寄存器;
  • 后面可能有 “A” 表示累加模式 (accumulate),或 “D” 表示双寄存器/双精度。

image-20250707211752173.png

指令后面依次跟的是

  • Coprocessor #,协处理器编号;
  • Destination,目的寄存器;
  • Source,源寄存器;
  • Immediate data value,一个立即数值,可以说是指令的编号。

工程师只需在编译器中,通过类似 “-mcpu=cortex-m52+cdecp0” 的编译选项告诉工具链:

“哥们,我要用 coprocessor 0 做我的自定义指令啦~”

然后我们就能在 C 里写类似内置函数:

// Example in C, with English comments
uint32_t custom_operation(uint32_t input) 
{
    // Coprocessor number=0, immediate=0, e.g. simple operation
    return __arm_cx1(0, input, 0);
}

这段看似普通的 C 函数,在编译后就会生成一条“CX1”形式的自定义指令,最终交由硬件中的 ACI 通路执行。这就是 ACI 的魅力之处:紧贴标准工具链,却能完成极具针对性的硬件加速。

2.4 zidian登场

了解了 ACI,我们就能理解 zidian 是如何工作的:

  • 在 G32R501 中,zidian 将常见的数学计算 (如 sin、cos、atan、sqrt 甚至一些 CRC/FFT 相关) 封装为“专用指令”,让应用层只需调用 zidian_math.h 提供的函数,即可调用这些定制指令;
  • 原理上,zidian 就是利用了 Cortex-M 架构中的 ACI 能力,结合自家的协处理器号 (coproc ID),并封装到一系列“__arm_cxX()”等 intrinsics 中。

3. zidian:Geehy家族的“小宇宙”

3.1 zidian 能加速哪些运算?

针对 G32R501,zidian 提供了典型的整数和浮点加速:

  • 整数 (ICAU):

    1. SIMD 相关运算 (如复数乘法、FFT)
    2. CRC 算法加速
    3. 一些特殊位操作
  • 浮点 (FCAU):

    1. sin, cos, atan, atan2 等三角函数
    2. 平方根 sqrtf32
    3. 除法、复数比值等

在头文件“zidian_math.h”里,你会看到类似这样的函数:

  • __sinpuf32()
  •  __cos()
  • __atan2puf32()
  • __sqrtf32()
  • __divf32() 

这些看似普通的函数背后,其实就是 zidian 对 ACI 的进一步封装,令开发者无缝使用。

在头文件“zidian_cde.h”里,你会看到G32R501支持的全部指令:

// CX2
#define __tas(x)       __arm_cx2(0x0, x, 0x1)
#define __rstatus(x)   __arm_cx2(0x0, x, 0x2)
#define __wstatus(x)   __arm_cx2(0x0, x, 0x3)
#define __neg(x)       __arm_cx2(0x0, x, 0x4)

// CX2A
#define __crc8l(y, x)      __arm_cx2a(0x0, y, x, 0x8)
#define __crc16p1l(y, x)   __arm_cx2a(0x0, y, x, 0x9)
#define __crc16p2l(y, x)   __arm_cx2a(0x0, y, x, 0xa)
#define __crc32l(y, x)     __arm_cx2a(0x0, y, x, 0xb)
#define __crc8h(y, x)      __arm_cx2a(0x0, y, x, 0xc)
#define __crc16p1h(y, x)   __arm_cx2a(0x0, y, x, 0xd)
#define __crc16p2h(y, x)   __arm_cx2a(0x0, y, x, 0xe)
#define __crc32h(y, x)     __arm_cx2a(0x0, y, x, 0xf)

// CX2DA
#define __tast(y, x)   __arm_cx2da(0x0, y, x, 0x40)

// CX3
#define __sh1add(x1, x2)       __arm_cx3(0x0, x1, x2, 0x8)
#define __sh1addi(x1, x2)      __arm_cx3(0x0, x1, x2, 0x18)
#define __sh1addexh(x1, x2)    __arm_cx3(0x0, x1, x2, 0xa)
#define __sh1addexhi(x1, x2)   __arm_cx3(0x0, x1, x2, 0x1a)
#define __sh1addexl(x1, x2)    __arm_cx3(0x0, x1, x2, 0xb)
#define __sh1addexli(x1, x2)   __arm_cx3(0x0, x1, x2, 0x1b)
#define __sh1sub(x1, x2)       __arm_cx3(0x0, x1, x2, 0xc)
#define __sh1subi(x1, x2)      __arm_cx3(0x0, x1, x2, 0x1c)
#define __sh1subexh(x1, x2)    __arm_cx3(0x0, x1, x2, 0xe)
#define __sh1subexhi(x1, x2)   __arm_cx3(0x0, x1, x2, 0x1e)
#define __sh1subexl(x1, x2)    __arm_cx3(0x0, x1, x2, 0xf)
#define __sh1subexli(x1, x2)   __arm_cx3(0x0, x1, x2, 0x1f)

// CX3D
#define __hstas(x1, x2)   __arm_cx3d(0x0, x1, x2, 0x20)
#define __hstsa(x1, x2)   __arm_cx3d(0x0, x1, x2, 0x21)
#define __lstas(x1, x2)   __arm_cx3d(0x0, x1, x2, 0x22)
#define __lstsa(x1, x2)   __arm_cx3d(0x0, x1, x2, 0x23)
#define __max(x1, x2)     __arm_cx3d(0x0, x1, x2, 0x25)

// CX3DA
#define __hbitsel(y, x1, x2)   __arm_cx3da(0, y, x1, x2, 0x2)

// VCX2
#define __rd_scr(x)    __arm_vcx2_u32(0x0, x, 0x6)
#define __wr_scr(x)    __arm_vcx2_u32(0x0, x, 0x7)

...

举个栗子,对于 sinf函数:

  1. 开启zidian后,将调用相关的 vcx指令;
  2. 关闭zidian时,使用的是arm的 hardfp_sinf函数

image-20250707222441857.png

3.2 使用 zidian 的小贴士

  1. 在编译器启用 ACI:  MDK-ARM: 加入 -mcpu=cortex-m52+cdecp0   IAR: 在 Extra Options 里加 --cdecp=0

  2. 在预编译宏中,定义 __ZIDIAN_FCAU__ (若需要对浮点库进行替换)。

    image-20250707212418159.png

  3. 在 C 代码中 #include "zidian_math.h",然后大胆调用 sinf()cosf()sqrtf()atan2f() 等。

  4. 编译后,一旦检测到宏开关及指令支持,就会将这些函数替换为 zidian 自定义指令的版本。

4. 来跑个分吧:加速前后的对比

接下来就是见证奇迹的时刻啦~咱们设计一个场景,对比在未使用 zidian 和 使用 zidian 两种情况下的耗时表现。

4.1 设计“超级复杂”的测试驱动

所谓“超级复杂”,可以理解为“重复调用多种函数 + 大循环”。当函数被调用几千几万次后,任何一丁点优化都可能带来大量时间差。CRC 和浮点运算 (sinf/cosf/sqrtf/atanf) 都是嵌入式常见场景,此例就足以说明大规模调用带来的性能落差。

在 G32R501 的 ITCM 区,我们放置了代码,并借助 DWT 计数器测量执行周期数。

整体思路包括:

  1. test_data[ ] 用于 CRC32 计算;
  2. float_data[ ] 存放 0 ~ (FLOAT_COUNTS-1) 的弧度数;
  3. 在循环中分别调用 dsp_crc32_test() 和 dsp_trig_test(),用 GET_DWT_CYCLE_COUNT() 宏记录执行前后的周期值;
  4. 最后打印计算时间与结果值,以便观测差异。

4.2 参考代码

以下是一整套 zidian_ex1_math.c 文件示例,你可以直接参照使用。

  • 其中文件开头包含了相关头文件 driverlib、zidian_math.h 等;
  • 代码中用到 GET_DWT_CYCLE_COUNT() 这一宏来简化测量流程;
  • dsp_crc32_test() 和 dsp_trig_test() 会根据宏判断走硬件还是纯软件逻辑。

请留意,示例使用了 SECTION_DTCM_DATA,将数组放入 DTCM(ITCM) 中,以减少访问延迟。

实际项目中可根据需要放在合适的内存区域。

//
// Globals
//
SECTION_DTCM_DATA
volatile uint32_t dwtCycleCounts[2];


// 定义测试规模
#define TEST_SIZE        512
#define FLOAT_COUNTS     5000

// CRC32 的常量
#define CRC32_POLY   0x04C11DB7U
#define CRC32_INIT   0xFFFFFFFFU
#define CRC32_XOROUT 0xFFFFFFFFU

// 全局数组 (放在DTCM中)
SECTION_DTCM_DATA
static uint8_t test_data[TEST_SIZE];

SECTION_DTCM_DATA
float float_data[FLOAT_COUNTS];

// 函数声明
uint32_t dsp_crc32_test(const uint8_t *data, uint32_t length);
float dsp_trig_test(void);

//
// Main
//
void example_main(void)
{
    printf("\nG32R501 EVAL zidian test!\n");

#if defined(__zidian_FCAU__)
    printf("zidian : ENABLED (Hardware Acceleration)\n");
#else
    printf("zidian : DISABLED (Pure Software)\n");
#endif

    //
    // 初始化测试数据
    //
    for(int i = 0; i < TEST_SIZE; i++)
    {
        test_data[i] = (uint8_t)i;
    }
    for(int i = 0; i < FLOAT_COUNTS; i++)
    {
        // 用 (i / 180.0f) * PI 模拟不同弧度
        float_data[i] = (float)(i) / 180.0f * 3.1415926f; 
    }

    //
    // 测试
    //

    // 1) 测量 CRC32
    uint32_t crcResult;
    GET_DWT_CYCLE_COUNT(dwtCycleCounts[0], 
        crcResult = dsp_crc32_test(test_data, TEST_SIZE));

    // 2) 测量 三角函数测试
    volatile float trigResult = 0.0f;
    GET_DWT_CYCLE_COUNT(dwtCycleCounts[1],
        trigResult = dsp_trig_test());

    printf("CRC Cycles  : %u\n", dwtCycleCounts[0]);
    printf("Trig Cycles : %u\n", dwtCycleCounts[1]);
    printf("CRC Value   : 0x%X\n", crcResult);
    printf("Trig Value  : %.6f\n", trigResult);


    //
    // Loop
    //
    for(;;)
    {
    }
}
// 硬件加速 还是 软件版本 取决于编译宏
#if defined(__zidian_FCAU__)
//------------------------------
// 硬件版CRC
//------------------------------
uint32_t zidian_crc32(const uint8_t *data, uint32_t length)
{
    uint32_t crc = CRC32_INIT;
    for(uint32_t i = 0; i < length; i++)
    {
        // 专用zidian指令
        crc = __crc32l(crc, data[i]);
    }
    crc ^= CRC32_XOROUT;
    return crc;
}
#else
//------------------------------
// 纯软件版CRC
//------------------------------
uint32_t zidian_crc32(const uint8_t *data, uint32_t length)
{
    uint32_t crc = CRC32_INIT;
    for(uint32_t i = 0; i < length; i++)
    {
        crc ^= ((uint32_t)data[i]) << 24;
        for(int bit = 0; bit < 8; bit++)
        {
            if(crc & 0x80000000U)
                crc = (crc << 1) ^ CRC32_POLY;
            else
                crc <<= 1;
        }
    }
    crc ^= CRC32_XOROUT;
    return crc;
}
#endif

// 浮点测试函数
float dsp_trig_test(void)
{
    volatile float result = 0;
    for(int j = 0; j < FLOAT_COUNTS; j++)
    {
        // 连续调用一下各种函数
        result += sinf(float_data[j]);
        result += cosf(float_data[j]);
        result += sqrtf(float_data[j]);
        result += atanf(float_data[j]);
        // 还来一次 atanf
        result += atanf(float_data[j]);
    }
    return result;
}

4.3 实测结果

将代码放置至ITCM RAM中:

image-20250707212605731.png

仿真环境下运行代码:

image-20250707212731122.png

在此上面的代码下,我们采集到如下跑分数据(截取自典型测量结果):

image-20250707212849364.png

• 未启用 zidian(纯软件模式,zidian : DISABLED)

  • CRC Cycles : 114234
  • Trig Cycles : 2522328
  • CRC Value : 0x2F728526
  • Trig Value : 46191.281250

• 启用 zidian(硬件加速模式,zidian : ENABLED):

  • CRC Cycles : 12319
  • Trig Cycles : 1250023
  • CRC Value : 0x2F728526
  • Trig Value : 46191.285156

从结果中可以清晰看到,开启 zidian 后的 CRC 计算从 114234 个周期骤降至 12319 个,性能提升近 9 倍;浮点函数测算也从约 252 万周期降低到约 125 万周期,缩短了一半以上的执行时间。  更值得注意的是,CRC 结果在两种模式下完全一致,Trig 结果也仅在浮点小数位上有细微差异,可见 zidian 并不会破坏运算精度。

综上可知,zidian 的硬件“外挂”确实能大大缓解 CPU 在处理大规模 CRC 和浮点函数时的性能压力,让 G32R501 整体算力上“飞”了一大截。这正是 zidian 的魅力所在:在几乎不改变业务逻辑的情况下,给你的 MCU 算力打了一个“超神 Buff”。

5. 结语:迈向高效与优雅的新世界

通过 ACI 接口,zidian 在 G32R501 上为我们打开了一扇“更高效”的大门。无论是 CRC 还是三角函数,都只要在编译时简单切换一个宏,就能把重复的“负担”挪给硬件,让 MCU 的主吃力大大减轻。更妙的是,这一切并不需要你手动改写繁琐的汇编,也不必妥协精度——堪称“一步到位”的加速方案。

当然,zidian 能做的事情或许不止这些,如果你的项目中大量使用了 DSP 之类的运算,也可以挖掘更多潜力。毕竟,当你尝到硬件加速的甜头后,就会发现那些曾经令人头疼的性能瓶颈,或许只是一段 __crc32l() 或 __arm_cx2(…) 指令的距离。

参考代码:

upload 附件:zidian_ex2_test.rar,请解压至 G32R501_SDK_V1.1.0\driverlib\g32r501\examples\eval\zidian\

参考文档:

  1. arm-custom-instructions-wp.pdf
  2. arm-custom-instructions-without-fragmentation-whitepaper.pdf
  3. AN1132_G32R501 zidian应用笔记 V1.0.pdf

以上便是本次分享的全部内容啦,欢迎各位在评论区留下你的想法吧!

使用特权

评论回复
沙发
kai迪皮|  楼主 | 2025-7-7 22:35 | 只看该作者

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

41

主题

261

帖子

11

粉丝