AVR / embedded:快速标准化向量?

时间:2014-06-23 16:08:38

标签: c performance normalization avr approximation

我浪费了几天与我的16mhz 8bit AVR(mega 2560)作战。

目标是规范我接收的值(加速度计,磁力计等) 这些值是16位有符号(int16),之后我想要一个0.0f-1.0f的浮点数 我用这个用于3D IMU。

常用方法:

int32_t tmp = (int32_t)a*a+b*b+c*c;
float magnitude = sqrt(tmp);
float a_v = a / magnitude;
float b_v = b / magnitude;
float c_v = c / magnitude;

更快的方法:

int32_t tmp = (int32_t)a*a+b*b+c*c;
float imagnitude = InvSqrt(tmp); // like the 'tricky' one for ID software quake source
float a_v = a * imagnitude;
float b_v = b * imagnitude;
float c_v = c * imagnitude;

第二个有一些优点,因为它使用aproximiation而不是1 / sqrt(但也有aproximated sqrts)并且它需要3次乘法而不是分段,这是好的,因为AVR支持MUL而不是DIV。 另一方面,由于浮点和32位计算,它无论如何都非常慢。

这样的函数通常需要1-2毫秒,这对我的代码产生了巨大的影响,它试图在一个应该持续最多2.4ms的周期中解决许多额外的任务和规范化

我经常挖掘并尝试了许多不同的近似和想法,但无论我尝试过什么,代码执行起来都太慢了。

也许还有另一种方法来规范我的传感器值。

有特殊问题(加速计量级)的人更新 没有floting point和sqrt我现在正在这个工作:(忽略额外的演员:) int16 cal []保存3轴的校准加速度计值。

int16 average_sq_1g = CONST_1G / 256;
uint32_t work = (int32_t)((int32_t)cal[0]*cal[0] + (int32_t)cal[1]*cal[1] + (int32_t)cal[2]*cal[2])/256;
work = work * 100L / average_sq_1g;
attitude.acc_magnitude = work;

这对我的事业非常专业,因为我正在努力获得加速度,我知道1G(大约16000)的价值 因此,公式(X ^ 2 + Y ^ 2 + Z ^ 2)* 100 / 1G ^ 2返回幅度(100 =没有额外的加速度,可以不使用浮点数。) 我没有检查性能差异,但它应该快得多。

2 个答案:

答案 0 :(得分:3)

固定点肯定是要走的路。唯一的困难是保持精度。

在这种情况下 - 如果精度很重要 - 我将使用以下相当快的算法:

  • 提取每个浮点数的指数(浮点数的第23..30位)
  • 保存每个值(第31位)的符号以供进一步使用
  • 提取尾数的15个最高有效位(位8..22),向左添加一个'1'

这可能听起来很复杂,但它可以实现,例如,

sign = b3 & 0x80;
exponent = b3 << 1;
if (b2 & 0x80)
  exponent |= 1;
else
  b2 |= 0x80;
mantissa = join_to_word(b1, b2);

其中b0 .. b3是浮点数的单个八位字节(b3是带符号的那个,请参阅IEEE754浮点结构)。 b2乘以0x80是由浮点表示中的隐藏位引起的。函数join_to_word是将两个字节组合成一个单词的东西。这不应该导致机器代码中的单个指令,因为只有编译器需要知道两个八位字节所在的位置。 (实现这一目标的一种方法是使用工会。)

现在我们知道了指数:

  • 找到最大的指数
  • 计算该数字尾数的平方
  • 向右移动2
  • 为另外两个尾猫
    • 向右移动指数差(即如果最大指数为17,则指数14的数字需要右移17-14 = 3)
    • 如果差异> = 7,请忘记这个数字(两者都优化一下并处理零指数)
    • 否则,将尾数平方,向右移动2,然后加到上面计算的平方
  • 取平方和的平方根

所以,此时我们有:

  • 15-16有效位的标准
  • 具有16位有效位的组件的绝对值

整数域中剩下的是通过将绝对值向左移15位并将它们除以范数来执行除法。然后将得到的矢量按2 ^ 16缩放。

然后我们转换到左边的浮点数。每个数字的基本步骤

  • 如果数字为零,则用零填充fp,否则执行以下步骤
  • 指数为127 + 16 = 142
  • 如果最高有效位不是1,则将尾数向左移动直到最高位为1,在每次移位时递减指数
  • 重设尾数的最高位
  • 如果指数的最低位是1,或者它是尾数的最高位
  • 将指数右移一个
  • 或将符号返回指数八位字节
  • 汇总指数,尾数和零字节
  • 的fp编号

整个算法应该在几百个时钟周期内运行。

如果你真的很匆忙,首先要看的是汇编代码。可能存在不必要的库调用,不必要的零字节等,具体取决于您编写代码的方式以及C编译器认为或不认为的内容。第二步是在汇编中编写这个例程,但通常可以避免。

但有一点需要考虑:是否绝对有必要使用浮点数?它们很慢。

答案 1 :(得分:2)

使用修复点算术。

按合理的位数缩放输入并使用整数运算。 (还有整数sqare根计算的算法)

例如,如果你的值范围从-10m到10m并且要求至少mm分辨率,我会添加11位(用2048缩放)

#define VEC_SHIFT 11
#define VEC_SCALE (1 << (VEC_SHIFT))
int16_t a =  7 * VEC_SCALE;
int16_t b =  3 * VEC_SCALE;
int16_t c = 10 * VEC_SCALE;

// calculations have to be done in larger data type so they do not overflow
int32_t snorm = (int32_t)a * a + (int32_t)b*b + (int32_t)c*c;  // snorm now is scaled by VEC_SCALE*VEC_SCALE (2*VEC_SHIFT)
int16_t norm = intsqrt(snorm); // norm is scaled with VEC_SCALE

// since norm and a,b,c is in VEC_SCALE, you have to scale up the divident so that one VEC_SCALE is chanceled out by division 
int16_t as =  (((int32_t)a) * VEC_SCALE )/norm;
int16_t bs =  (((int32_t)b) * VEC_SCALE )/norm;
int16_t cs =  (((int32_t)c) * VEC_SCALE )/norm;