【问题标题】:Fast Hypotenuse Algorithm for Embedded Processor?嵌入式处理器的快速斜边算法?
【发布时间】:2011-03-31 04:51:08
【问题描述】:

是否有一种聪明/高效的算法来确定角度的斜边(即sqrt(a² + b²)),在没有硬件乘法的嵌入式处理器上使用定点数学?

【问题讨论】:

  • 你能避免sqrt吗?例如。只比较 vs lenSquared vs len?很多将取决于您的处理器。你能告诉我们它是什么吗?
  • 在这种情况下,sqrt 是必要的。该应用程序涉及处理来自加速度计的数据并通过非线性滤波器运行它。有问题的处理器有一个 8 位 RISC 指令集,Atmel ATtiny44A(数据表:atmel.com/dyn/resources/prod_documents/doc8183.pdf)。
  • 押注 PIC10-16 或 tinyAVR。
  • 顺便说一句,任何对嵌入式处理感兴趣的人都应该考虑加入Electronics and Roboticsstackexchange。
  • 这里有一些严重的术语问题。斜边是与直角三角形的直角相对的线段。您想要斜边的长度。此外,您可能(或可能不)实际上想要逆 而不是毕达哥拉斯表达式。

标签: c embedded avr


【解决方案1】:

如果结果不需要特别准确,你可以粗略 很简单的近似:

ab 的绝对值,并在必要时交换以得到a <= b。那么:

h = ((sqrt(2) - 1) * a) + b

要直观地了解其工作原理,请考虑在像素显示器上绘制浅角度线的方式(例如,使用 Bresenham 算法)。它看起来像这样:

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | | | | | | | | | | | | | | | |*|*|*|    ^
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+    |
| | | | | | | | | | | | |*|*|*|*| | | |    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+    |
| | | | | | | | |*|*|*|*| | | | | | | | a pixels
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+    |
| | | | |*|*|*|*| | | | | | | | | | | |    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+    |
|*|*|*|*| | | | | | | | | | | | | | | |    v
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 <-------------- b pixels ----------->

对于b 方向上的每一步,要绘制的下一个像素要么紧邻右侧,要么向右上方一个像素。

从一端到另一端的理想线可以近似为将每个像素的中心连接到相邻像素的中心的路径。这是一系列长度为sqrt(2)a 段和长度为1 的b-a 段(以像素为测量单位)。于是有了上面的公式。

这清楚地给出了a == 0a == b 的准确答案;但高估了两者之间的值。

误差取决于比率b/a;最大错误发生在b = (1 + sqrt(2)) * a 和结果为2/sqrt(2+sqrt(2)) 时,或比真实值高出约 8.24%。这不是很好,但是如果它对您的应用程序来说足够好,那么这种方法的优点是简单快速。 (乘以一个常数可以写成一系列的移位和加法。)

【讨论】:

  • 这是一个相当巧妙的近似值。
  • 不错;但是您可能应该提到同时采用 a 和 b 的绝对值。 +1 进行一些简短的错误分析
  • 上面的公式给出了一个八角形的轮廓,这对我有用。为了进一步打破它并坚持使用整数(没有浮点数!),将 sqr(2) 设为 0.414213562,将小数点与四舍五入:4142 并在稍后将其除以 (/10000) 非常快!没有浮点常量 h=4142*abs(a)/10000+abs(b) 的情况下要多很多倍@ 谢谢! ** 这个比率非常接近 1:2,所以只使用位移和 mul+add 我得到了一个非常好的传真h=(a&gt;&gt;1)+b
  • 好主意。不过,误差可能高达 10%。 (例如,长度为 231 的错误为 19。对于某些值,该错误也可能低于 1%,但通常为 5-7%。)
  • @MatthewSlattery 你能提供这个方法的参考吗?我知道这已经有一段时间了,但我迫切需要引用这一点。
【解决方案2】:

为了记录,这里还有一些近似值,大致在 复杂性和准确性的递增顺序。所有这些都假设 0 ≤ a ≤ b。

  • h = b + 0.337 * a // max error ≈ 5.5 %
  • h = max(b, 0.918 * (b + (a&gt;&gt;1))) // max error ≈ 2.6 %
  • h = b + 0.428 * a * a / b // max error ≈ 1.04 %

编辑:回答 Ecir Hana 的问题,这是我得出这些的方法 近似值。

第一步。逼近两个变量的函数可以是 复杂的问题。因此,我首先将其转化为问题 逼近 one 变量的函数。这可以通过选择来完成 最长边作为“比例”因子,如下:

h = √(b2 + a2)
= b √(1 + (a/b)2)
= b f(a/b)    其中 f(x) = √(1+x2)

添加约束 0 ≤ a ≤ b 意味着我们只关心 在区间 [0, 1] 中逼近 f(x)。

下面是相关区间中 f(x) 的图,以及 Matthew Slattery 给出的近似值(即 (√2−1)x + 1)。

第二步。下一步就是盯着这个情节,一边问 自己的问题是“我怎样才能便宜地近似这个函数?”。 由于曲线看起来大致为抛物线,我的第一个想法是使用 二次函数(第三近似)。但由于这仍然是 比较贵,我也看了线性和分段线性 近似值。以下是我的三个解决方案:

数值常数(0.337、0.918 和 0.428)最初是免费的 参数。选择特定值是为了最小化 近似值的最大绝对误差。最小化可以 当然可以通过某种算法完成,但我只是“手工”完成的, 绘制绝对误差并调整常数,直到达到 最小化。在实践中,这工作得非常快。将代码写入 自动化这将花费更长的时间。

第三步是回到最初的近似问题 两个变量的函数:

  • h ≈ b (1 + 0.337 (a/b)) = b + 0.337 a
  • h ≈ b max(1, 0.918 (1 + (a/b)/2)) = max(b, 0.918 (b + a/2))
  • h ≈ b (1 + 0.428 (a/b)2) = b + 0.428 a2/b

【讨论】:

  • 拜托,你能解释一下常数/近似值是如何得出的吗?
  • @EcirHana:查看扩展答案。
  • 非常有趣!非常感谢您的解释!
【解决方案3】:

考虑使用 CORDIC 方法。 Dobb 博士有一篇文章和相关的图书馆资源here。平方根、乘法和除法在文末处理。

【讨论】:

  • 注意:我用过这个库,发现log()函数有错误。这可以通过在 log_two_power_n_reversed[] 数组初始化程序的末尾添加 0x0LL 来纠正。我已与作者确认了此更正。
【解决方案4】:

一种可能性如下所示:

#include <math.h>

/* Iterations   Accuracy
 *  2          6.5 digits
 *  3           20 digits
 *  4           62 digits
 * assuming a numeric type able to maintain that degree of accuracy in
 * the individual operations.
 */
#define ITER 3

double dist(double P, double Q) {
/* A reasonably robust method of calculating `sqrt(P*P + Q*Q)'
 *
 * Transliterated from _More Programming Pearls, Confessions of a Coder_
 * by Jon Bentley, pg. 156.
 */

    double R;
    int i;

    P = fabs(P);
    Q = fabs(Q);

    if (P<Q) {
        R = P;
        P = Q;
        Q = R;
    }

/* The book has this as:
 *  if P = 0.0 return Q; # in AWK
 * However, this makes no sense to me - we've just insured that P>=Q, so
 * P==0 only if Q==0;  OTOH, if Q==0, then distance == P...
 */
    if ( Q == 0.0 )
        return P;

    for (i=0;i<ITER;i++) {
        R = Q / P;
        R = R * R;
        R = R / (4.0 + R);
        P = P + 2.0 * R * P;
        Q = Q * R;
    }
    return P;
}

这仍然会在每次迭代中进行几次除法和四次乘法,但每次输入很少需要超过三次迭代(通常两次就足够了)。至少对于我见过的大多数处理器来说,这通常比 sqrt 本身要快。

目前它是为doubles 编写的,但假设您已经实现了基本操作,将其转换为使用定点应该不会很困难。

关于“相当稳健”的评论引发了一些疑问。至少正如最初所写的那样,这基本上是一种相当反常的说法,“它可能并不完美,但至少比直接实现勾股定理要好很多。”

特别是,当您对每个输入进行平方时,表示平方结果所需的位数大约是表示输入值所需位数的两倍。添加后(只需要一个额外的位),您取平方根,这使您回到需要与输入大致相同的位数。除非您的类型比输入的精度高得多,否则很容易产生非常糟糕的结果。

此算法不直接对任一输入求平方。中间结果仍然有可能下溢,但它的设计是这样的,当它这样做时,结果仍然会出现以及使用的格式支持。基本上,发生这种情况的情况是您有一个 非常 锐角三角形(例如,90 度、0.000001 度和 89.99999 度)。如果它足够接近 90、0、90,我们可能无法表示两条长边之间的差异,因此它会将斜边计算为与另一条长边的长度相同。

相比之下,当勾股定理失败时,结果通常是 NaN(即,什么都不告诉我们),或者,根据使用的浮点格式,很可能看起来像一个合理的答案,但实际上是大错特错。

【讨论】:

  • 相当稳健”是什么意思?这是否意味着它并非在所有情况下都有效!?
  • math.h 和 double 类型不可能适合 ATTiny。你得到 4k 的程序空间,最大值,除法和乘法都将在软件中。但是,这对于具有硬件乘法(或除法)指令的处理器来说效果很好。
  • @reemevnivek:是的,但是尝试以未知的定点格式呈现它几乎是不可能的。 &lt;math.h&gt; 仅用于fabs,因此它的包含(单独)几乎没有任何意义(包括标题通常只声明事物;只有您使用的内容进入可执行文件)。不过最终你是对的:正如最后一句话所表明的那样,我当然不希望在微控制器上按原样使用它。
  • 同意;我只是认为没有资格或量化的“合理稳健”作为评论有点弱,并且需要用户进行分析以确定使用此代码的后果。这可能意味着它会以非确定性的方式失败,而不仅仅是确定性的输入范围,或者它可能只是指在所有情况下的精度而不是在某些情况下的正确性。例如,如果这是一个对安全至关重要的应用程序,那么单独的评论就会敲响警钟。
  • @JerryCoffin 哇,10 年过去了,你复活了?我认为编辑答案以量化您的意思是我所追求的 - 10 年前。不是为了我自己的利益;我可以自己解决这个问题。相反,这是一个改进已经很好的答案的建议,并消除了该声明可能会灌输给任何新手的任何疑问。评论是不够的。
【解决方案5】:

您可以从重新评估您是否需要 sqrt 开始。很多时候,您计算斜边只是为了将其与另一个值进行比较 - 如果您将要比较的值平方,则可以完全消除平方根。

【讨论】:

    【解决方案6】:

    除非您以 >1kHz 的频率执行此操作,否则即使在没有硬件的 MCU 上进行乘法 MUL 也不可怕。更糟糕的是sqrt。我会尝试修改我的应用程序,使其根本不需要计算。

    如果您确实需要标准库,它可能是最好的,但您可以考虑使用牛顿方法作为一种可能的替代方法。但是,它需要几个乘法/除法周期才能执行。

    AVR 资源

    【讨论】:

    • 您不需要除以近似平方根。您可以轻松地使用二进制搜索类型的算法,该算法每位仅进行一次乘法运算,并且没有除法运算。
    【解决方案7】:

    也许您可以使用一些 Elm Chans Assembler Libraries 并将 ihypot 功能调整到您的 ATtiny。您需要更换 MUL 并且可能(我还没有检查)一些其他说明。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-02-04
      • 2018-03-04
      • 2013-06-30
      • 1970-01-01
      • 2012-09-21
      • 2015-06-22
      • 1970-01-01
      • 2020-01-27
      相关资源
      最近更新 更多