【问题标题】:AVR/embedded: Normalizing vectors in a fast manner?AVR/嵌入式:以快速方式规范化向量?
【发布时间】:2014-08-13 18:20:14
【问题描述】:

我已经浪费了好几天时间与我的 16mhz 8 位 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;

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

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

我进行了很多研究并尝试了许多不同的近似值和想法,但无论我尝试了什么,代码的执行速度都太慢了。

也许还有另一种方法可以标准化我的传感器值..

针对我的特殊问题(加速度计大小)的人的更新: 没有浮点数和 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 = 没有额外的加速度,并且可以在不使用浮点数的情况下完成。
我没有检查性能差异,但它应该会快很多。

【问题讨论】:

  • 为什么要使用浮点数?可以改用定点(例如 Q15)吗?
  • 第一行需要更多的演员表。第一个乘法是 1nt32_t,但其他两个只是在 int 之间。可能会发生溢出。
  • UncleO:我怀疑会有溢出,编译器应该正确处理整个操作。我使用类似的代码,它可以正常工作而不会溢出。 @anatolyg:我目前正在将整个代码更改为 32 位整数,因为我无法让它更快地工作。我不确定定点有多少帮助,AVR-GCC afaik 上没有定点支持。
  • SCNR: +1 地震源 InvSqrt() 参考
  • @UncleO:所有的乘法都在int之间,而不是int16_t之间,由于促销。

标签: c performance normalization avr approximation


【解决方案1】:

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

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

  • 提取每个浮点数的指数(浮点数的第 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 的 oring 是由于浮点表示中的隐藏位。函数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 编译器认为或不认为的内容。第二步是用汇编写这个例程,但通常可以避免。

但您需要考虑一件事:绝对有必要使用浮点数吗?它们很慢。

【讨论】:

    【解决方案2】:

    使用定点算法。

    按合理数量的位数缩放您的输入并使用整数运算。 (也有整数平方根计算的算法)

    例如,如果您的值范围从 -10m 到 10m 并且需要至少毫米分辨率,我将添加 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;
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-09-12
      • 1970-01-01
      • 2013-06-30
      • 1970-01-01
      • 2015-12-08
      • 2015-01-10
      • 1970-01-01
      相关资源
      最近更新 更多