1.?背景
在传统的嵌入式开发中,如果我们需要在一颗小小的?MCU?上跑一些高强度的运算,常规做法是:
- 借助库函数或手写汇编进行手动优化;
- 或是在硬件设计层面,通过额外的外设或协处理器(crypto,?DSP)?助力,完成特定功能加速。
但随着对性能需求越来越苛刻,硬件资源又不想失去?Arm?生态的广泛支持,怎么办?? 这时候,“自定义指令”?(或称自定义加速单元)?就闪亮登场了:
- 具备灵活性:? 允许根据应用需求把常用的算法指令“写入”CPU,以极低延迟操作处理器寄存器。
- 兼容生态:? 不必魔改编译器、无须大改调试器;保持与现有?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?生态的同时,提供了更细粒度的指令级优化。

2.2?ACI?的实现方式
在硬件层面,Arm?针对协处理器指令编码区,预留了部分空间让厂商编写自己的操作:
- 指令解码阶段:? CPU?内部会检测该指令是否隶属某个?coprocessor?number;若与?ACI?相关,则交由“定制数据通路”进行计算;
- 数据读写:? CPU?读取需要的寄存器?(整数寄存器或浮点寄存器),然后将数据送给?ACI?硬件模块;
- 获取结果:? 结果再写回?CPU?寄存器;若支持更新?APSR?的某些标志位?(如NZCV)?也可以在此阶段完成。
2.3?ACI?指令风格
在?Arm?v8-M?架构里,ACI?以类似于?“CXn”?或?“VCXn”?的形式呈现:
- “CX”?前缀常对应整数指令;
- “VCX”?前缀常对应浮点?(FPU)?或向量?(MVE);
- n?可取?1,?2,?3?等,表示几个输入寄存器;
- 后面可能有?“A”?表示累加模式?(accumulate),或?“D”?表示双寄存器/双精度。

指令后面依次跟的是
- 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):
- SIMD?相关运算?(如复数乘法、FFT)
- CRC?算法加速
- 一些特殊位操作
-
浮点?(FCAU):
- sin,?cos,?atan,?atan2?等三角函数
- 平方根?sqrtf32
- 除法、复数比值等
在头文件“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
函数:
- 开启zidian后,将调用相关的
vcx
指令;
- 关闭zidian时,使用的是arm的
hardfp_sinf
函数

3.2?使用?zidian?的小贴士
-
在编译器启用?ACI:? MDK-ARM:?加入 -mcpu=cortex-m52+cdecp0
?? IAR:?在?Extra?Options?里加?--cdecp=0
-
在预编译宏中,定义?__ZIDIAN_FCAU__
?(若需要对浮点库进行替换)。

-
在?C?代码中?#include?"zidian_math.h"
,然后大胆调用?sinf()
,?cosf()
,?sqrtf()
,?atan2f()
?等。
-
编译后,一旦检测到宏开关及指令支持,就会将这些函数替换为?zidian?自定义指令的版本。
4.?来跑个分吧:加速前后的对比
接下来就是见证奇迹的时刻啦~咱们设计一个场景,对比在未使用?zidian?和?使用?zidian?两种情况下的耗时表现。
4.1?设计“超级复杂”的测试驱动
所谓“超级复杂”,可以理解为“重复调用多种函数?+?大循环”。当函数被调用几千几万次后,任何一丁点优化都可能带来大量时间差。CRC?和浮点运算?(sinf/cosf/sqrtf/atanf)?都是嵌入式常见场景,此例就足以说明大规模调用带来的性能落差。
在?G32R501?的?ITCM?区,我们放置了代码,并借助?DWT?计数器测量执行周期数。
整体思路包括:
- test_data[ ] 用于?CRC32?计算;
- float_data[ ] 存放?0?~?(FLOAT_COUNTS-1)?的弧度数;
- 在循环中分别调用?dsp_crc32_test()?和?dsp_trig_test(),用?GET_DWT_CYCLE_COUNT()?宏记录执行前后的周期值;
- 最后打印计算时间与结果值,以便观测差异。
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中:

仿真环境下运行代码:

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

??未启用?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(…)
?指令的距离。
参考代码:
附件:zidian_ex2_test.rar,请解压至 G32R501_SDK_V1.1.0\driverlib\g32r501\examples\eval\zidian\
参考文档:
- arm-custom-instructions-wp.pdf
- arm-custom-instructions-without-fragmentation-whitepaper.pdf
- AN1132_G32R501 zidian应用笔记 V1.0.pdf
以上便是本次分享的全部内容啦,欢迎各位在评论区留下你的想法吧!
@21小跑堂 :感谢资持,Thanks?(?ω?)?
在G32R501?上使用zidian?,极速飙升MCU的运算能力。作者将原理和使用方式均进行详细介绍,感兴趣的童鞋可以阅读品鉴。