定点数运算是一种在整数处理器上高效模拟浮点运算的技术,它通过预定义的小数点位置(称为q格式)来存储和操作分数。这种技术在嵌入式系统和实时处理应用中尤为重要,因为ARM等RISC架构通常不包含硬件浮点运算单元(FPU)。
定点数的核心表示方法是将数值编码为两个部分:
数学表达式为:值 = n × 2^(-q)
例如,在q=14的格式中:
q值的选择直接影响数值表示的动态范围和精度:
在32位系统中,常见的选择包括:
乘法操作需要特别注意,因为两个q格式数相乘会产生2q格式的结果。例如:
经验法则:
c复制// 加法:相同q格式直接相加
#define FADD(a,b) ((a)+(b))
// 减法:相同q格式直接相减
#define FSUB(a,b) ((a)-(b))
// 乘法:结果右移q位回到原格式
#define FMUL(a,b,q) (((a)*(b))>>(q))
// 除法:被除数左移q位后除
#define FDIV(a,b,q) (((a)<<(q))/(b))
当操作数具有不同q格式时,需要先进行格式转换:
c复制// 将a从q1格式转换为q2格式
#define FCONV(a, q1, q2) \
(((q2)>(q1)) ? (a)<<((q2)-(q1)) : (a)>>((q1)-(q2)))
// 通用加法:自动处理不同q格式
#define FADDG(a,b,q1,q2,q3) \
(FCONV(a,q1,q3)+FCONV(b,q2,q3))
c复制// 使用泰勒级数近似计算exp(x),x<1
double q_exp(double x) {
int q = 14;
int a = (int)(x * (1<<q)); // 浮点转定点
int result = 1<<q; // 初始化为1.0
int term = 1<<q; // 当前项值
int n = 1; // 当前阶乘
for(int i=1; i<10; i++) {
term = FMUL(term, a, q); // a^i
n *= i; // i!
result += FDIVI(term, n); // 累加项
}
return (double)result / (1<<q); // 转回浮点
}
ARM的桶形移位器可以在单条指令中完成移位操作,极大提升定点数效率:
assembly复制; q=14格式的乘法实现
MUL r0, r1, r2 ; r0 = r1 * r2 (结果在低32位)
MOV r0, r0, ASR #14 ; 右移14位得到正确q格式
对于需要更高精度的场合(如q=30),使用64位乘法指令:
assembly复制; 64位乘法保持精度
SMULL r0, r1, r2, r3 ; [r1:r0] = r2 * r3
; 提取q=30格式结果
MOV r4, r0, LSR #30
ORR r4, r4, r1, LSL #2
计算3D向量的长度(q=8格式):
assembly复制; 输入:r0=x, r1=y, r2=z (均为q=8)
SMULL r3, r4, r0, r0 ; x²
SMLAL r3, r4, r1, r1 ; +y²
SMLAL r3, r4, r2, r2 ; +z²
; 开平方(q=8结果)
BL isqrt_q8
; 结果在r0中
在音频处理中,典型的滤波器实现:
c复制// 二阶IIR滤波器
int16_t iir_filter(int16_t input, struct iir_state *s) {
int32_t acc = FMUL(s->a1, s->x1, 14);
acc += FMUL(s->a2, s->x2, 14);
acc += FMUL(s->b0, input, 14);
acc += FMUL(s->b1, s->y1, 14);
acc += FMUL(s->b2, s->y2, 14);
// 更新状态
s->x2 = s->x1;
s->x1 = input;
s->y2 = s->y1;
s->y1 = acc >> 14; // 转回q=14
return (int16_t)(s->y1);
}
3D图形中的矩阵变换:
assembly复制; 4x4矩阵乘向量 (q=8)
; 输入:r0=矩阵指针, r1=向量指针
LDMIA r1, {r2-r5} ; 加载向量[x,y,z,w]
MOV r6, #4 ; 循环计数器
loop:
LDMIA r0!, {r8-r11} ; 加载矩阵行
SMULL r12, r14, r8, r2
SMLAL r12, r14, r9, r3
SMLAL r12, r14, r10, r4
SMLAL r12, r14, r11, r5
MOV r7, r12, LSR #8 ; 提取q=8结果
ORR r7, r7, r14, LSL #24
STR r7, [r1], #4 ; 存储结果
SUBS r6, r6, #1
BNE loop
乘法顺序优化:
c复制// 不佳:连续乘法导致精度损失
result = (a * b * c) >> (2*q);
// 优化:分步进行中间归一化
temp = (a * b) >> q;
result = (temp * c) >> q;
除法优化:
c复制// 不佳:直接除法损失精度
result = a / b;
// 优化:先将被除数左移
result = (a << q) / b;
中间结果使用更大位宽:
c复制int64_t temp = (int64_t)a * b;
result = (int32_t)(temp >> q);
自动缩放技术:
c复制// 检测乘法是否可能溢出
if((abs(a) > INT32_MAX/abs(b)) >> q) {
// 执行安全路径
a >>= 1;
q -= 1;
}
定点-浮点转换工具:
c复制void debug_fixed(int32_t val, int q) {
printf("Fixed: 0x%08X (%d) ≈ %f\n",
val, val, (double)val/(1<<q));
}
边界条件测试:
c复制void test_range(int q) {
int32_t max = INT32_MAX >> q;
int32_t min = INT32_MIN >> q;
printf("q=%d range: [%f, %f]\n",
q, (double)min, (double)max);
}
c复制void matrix_mult(int32_t *out, const int32_t *a,
const int32_t *b, int n, int q) {
for(int i=0; i<n; i++) {
for(int j=0; j<n; j++) {
int64_t sum = 0;
for(int k=0; k<n; k++) {
sum += (int64_t)a[i*n+k] * b[k*n+j];
}
out[i*n+j] = (int32_t)(sum >> q);
}
}
}
使用SIMD指令并行处理4个q=16的乘法:
assembly复制; 假设q=16,输入在q0-q3
VMULL.S16 q4, d0, d2 ; a0*b0, a1*b1
VMULL.S16 q5, d1, d3 ; a2*b2, a3*b3
VQSHRN.S32 d8, q4, #16 ; 右移并窄化
VQSHRN.S32 d9, q5, #16
乘法指令差异:
assembly复制; ARMv5 (需要多条指令)
MUL r0, r1, r2
MOV r0, r0, ASR #14
; ARMv7 (单条指令)
SMMUL r0, r1, r2 ; 直接得到高32位结果
除法优化:
assembly复制; ARMv5需要软件除法
BL __aeabi_idiv
; ARMv7支持硬件除法
SDIV r0, r1, r2
Thumb-2指令集下的特殊考虑:
assembly复制; 需要显式移位指令
MULS r0, r1, r2
ASRS r0, r0, #14
混合精度策略:
测试覆盖率要点:
性能分析技巧:
c复制#define PROFILE_START() unsigned _cycles = get_cycle_count()
#define PROFILE_END(msg) \
printf("%s: %u cycles\n", msg, get_cycle_count()-_cycles)
GCC编译提示:
makefile复制CFLAGS += -Wconversion -Wsign-conversion
CFLAGS += -fno-strict-aliasing
调试符号扩展:
c复制// 查看符号扩展问题
printf("0x%08X -> %d\n", val, val);
仿真器支持:
定点数运算在ARM架构上的高效实现需要深入理解整数运算特性、合理选择q格式、并充分利用ARM的移位和乘法指令。通过本文介绍的技术,开发者可以在没有硬件FPU的情况下实现高性能的分数运算,满足实时信号处理、图形计算等场景的严苛性能要求。