【问题标题】:Any Faster RMS Value Calculation in C?C中任何更快的RMS值计算?
【发布时间】:2015-03-02 10:29:41
【问题描述】:

我正在用 C 语言为小型 8 位微控制器编写软件。部分代码是读取电流互感器 (ZCT) 的 ADC 值,然后计算 RMS 值。流过 ZCT 的电流是正弦的,但它可能会失真。我的代码如下:

float       adc_value, inst_current;
float       acc_load_current;           // accumulator = (I1*I1 + I2*I2 + ... + In*In)
double      rms_current;

// Calculate the real instantanous value from the ADC reading
inst_current = (adc_value/1024)*2.5;    // 10bit ADC, Voltage ref. 2.5V, so formula is: x=(adc/1024)*2.5V                           

// Update the RMS value with the new instananous value:
// Substract 1 sample from the accumulator (sample size is 512, so divide accumulator by 512 and substract it from the accumulator)
acc_load_current -= (acc_load_current / 512);       
inst_current *= inst_current;          // square the instantanous current
acc_load_current += inst_current;      // Add it to the accumulator

rms_current = (acc_load_current / 512);  // Get the mean square value. (sample size is 512)
rms_current = sqrt(rms_current);         // Get RMS value

// Now the < rms_current > is the real RMS current

但是,它有很多浮点计算。这给我的小型MCU增加了很大的负担。而且我发现sqrt() 函数在我的编译器中不起作用。

有没有可以运行得更快的代码?

【问题讨论】:

  • 它需要多准确(你能找到峰值然后做RMS = peak_adc_value/1024*2.5*0.707)吗?
  • 由于您似乎使用滑动窗口平均值(样本之间没有太大变化),您可以通过一步 Newton-Ralfston 实现 sqrt(),使用旧的 sqrt 作为起始值。
  • 一般使用查找表来存储平方根,而不是计算平方根。(在资源匮乏的环境中访问更快)
  • @Vagish 查找表意味着二进制搜索,即 O(log N)。 Newton-Ralfston 在 4 或 5 次迭代中收敛(但具有更大的大 O)
  • @joop,对不起,我的意思是查找表是指存储在 ROM 中的简单数组。

标签: c performance algorithm microcontroller


【解决方案1】:

当您需要在缺少 FPU 的处理器上提高速度时,您最好 赌注是用定点代替浮点计算。结合 这是 joop 的建议(一个 Newton-Raphson sqrt),你得到 像这样:

#define INITIAL 512  /* Initial value of the filter memory. */
#define SAMPLES 512

uint16_t rms_filter(uint16_t sample)
{
    static uint16_t rms = INITIAL;
    static uint32_t sum_squares = 1UL * SAMPLES * INITIAL * INITIAL;

    sum_squares -= sum_squares / SAMPLES;
    sum_squares += (uint32_t) sample * sample;
    if (rms == 0) rms = 1;    /* do not divide by zero */
    rms = (rms + sum_squares / SAMPLES / rms) / 2;
    return rms;
}

只需通过此过滤器运行您的原始 ADC 样本。你可以加几个 在这里和那里进行位移以获得更高的分辨率,但你必须 小心不要溢出你的变量。我怀疑你真的需要 额外的分辨率。

过滤器的输出与其输入的单位相同。在这种情况下, 它是您的 ADC 的单位: 2.5 V / 1024 ≈ 2.44 mV。如果你能保持 这个单位在随后的计算中,您将通过避免 不必要的转换。如果您真的需要以伏特为单位的值(它 可能是 I/O 要求),那么您将不得不转换为浮动 观点。如果你想要毫伏,你可以留在整数领域:

uint16_t rms_in_mV = rms_filter(raw_sample) * 160000UL >> 16;

【讨论】:

  • 感谢您的回复。我将研究不动点计算。我想如果我将原始 ADC 值输入到这个过滤器,这个过滤器的返回值就是 ADC 数据的 RMS 值,对吧?因此,我仍然需要通过定点法从 RMS ADC 结果中计算出真正的 RMS 电流/电压值(例如 rms_voltage = (rms / 1024) * 2.5V),是真的吗?
  • @eepty:你猜对了(答案已更新)。您无需计算实际 RMS 电压,因为滤波器的输出是实际电压,仅采用非传统单位。你是否需要改变单位,我不能说,这完全取决于你的应用程序的要求:如果你想显示结果,你会想要一个更常规的单位。如果是某种过程控制,你可以保留任何便于计算的单位。
  • @EdgarBonet 为什么 sum_squares -= sum_squares / SAMPLES;?我不明白这是如何减去一个样本,我的意思是数学证明。它实际上是减去平均总和,不是吗?
  • @lalamer:它是一个exponential moving average,按系数SAMPLES 缩放,时间常数为SAMPLES 采样次数。使用链接的维基百科文章的符号:S_t = Y_t + (1−α)⋅S_{t−1}α = 1/SAMPLES
  • @EdgarBonet 你能看看相关的问题吗? stackoverflow.com/questions/61698417/rms-calculation-dc-offset
【解决方案2】:

由于您的平方和值acc_load_current 在迭代之间变化不大,因此它的平方根几乎是恒定的。 Newton-Raphson sqrt() 函数通常只在几次迭代中收敛。通过每步使用 一次 迭代,计算会被抹掉。

static double one_step_newton_raphson_sqrt(double val, double hint)
{
double probe;
if (hint <= 0) return val /2;
probe = val / hint;
return (probe+hint) /2;
}

static double      acc_load_current = 0.0;           // accumulator = (I1*I1 + I2*I2 + ... + In*In)
static double      rms_current = 1.0;
float       adc_value, inst_current;
double      tmp_rms_current;

// Calculate the real instantanous value from the ADC reading
inst_current = (adc_value/1024)*2.5;    // 10bit ADC, Voltage ref. 2.5V, so formula is: x=(adc/1024)*2.5V                           

// Update the RMS value with the new instananous value:
// Substract 1 sample from the accumulator (sample size is 512, so divide accumulator by 512 and substract it from the accumulator)
acc_load_current -= (acc_load_current / 512);
inst_current *= inst_current;          // square the instantanous current
acc_load_current += inst_current;      // Add it to the accumulator

tmp_rms_current = (acc_load_current / 512);
rms_current = one_step_newton_raphson_sqrt(tmp_rms_current, rms_current);         // Polish RMS value

// Now the <rms_current> is the APPROXIMATE RMS current

注意事项:

  • 我将一些数据类型从float 更改为double(这在通用机器/台式机上很正常)如果double 在您的微型计算机上非常昂贵,您可以将它们改回来。
  • 我还添加了static,因为我不知道代码是来自函数还是来自循环。
  • 我创建了函数static 来强制它被内联。如果编译器没有内联静态函数,则应手动内联。

【讨论】:

  • 不错。但是:拉尔夫斯顿? ;-)
  • 哎呀ph,我猜...(我是荷兰人,我们没有 ph 值;大部分单词都使用 f
  • 好的,但是这个人的名字实际上是 Raphson(没有 S,没有 T)。 ;)
  • 我懒得查了。现在我还是得查一下。考虑完成。
【解决方案3】:

希望您的项目是用于测量“大”交流电压(而不是像 9v 电机控制之类的东西。)如果发生这种情况,那么您可以作弊,因为您的错误可以在可接受的范围内。

以整数形式进行所有数学运算,并使用简单的查找映射进行 sqrt 操作。 (您可以在启动时预先计算,如果您正在执行 3 阶段,您将只真正需要大约 600 个奇数值..

这也引出了一个问题,您实际上需要 VAC RMS 或其他一些功率测量方法吗? (例如,你能用一个简单的盒子均值算法逃脱吗?)

【讨论】:

  • 如果输入电压很小,只需将“二进制点”向右移动几个位置(即将输入向左移动几个位置)进行定点运算,然后再将其移回最后(记得在乘除之后调整)。
【解决方案4】:

要找到平方根,请使用以下来自 microchip 的 8 位控制器应用说明

Fast Integer Square Root

速度非常快,只需 9 次循环即可找到平方根

【讨论】:

    【解决方案5】:
    1. 2 次方的除法/乘法

      可以通过仅通过位掩码操作和+,- 更改 exponent 来完成,因此将 exponent 掩码/提取为integer 值,然后应用 bias 。之后add/sublog2(operand) 并编码回您的double

    2. sqrt

      它应该有多快和多准确?您可以在定点上使用二进制搜索或使用sqrt(x)=pow(x,0.5)=exp2(0.5*log2(x))。同样在定点上,它很容易实现。您可以通过取尾数并将其位移到您使用的值周围的某个已知指数来临时使固定点翻倍

      计算sqrt,然后存储回double。如果您不需要太大的精度,那么您可以留在操作数指数上,只在尾数上直接进行二进制搜索。

    [edit1] C++ 中的二进制搜索

    //---------------------------------------------------------------------------
    double f64_sqrt(double x)
        {
        const int h=1;      // may be platform dependent MSB/LSB order
        const int l=0;
        DWORD b;            // bit mask
        int e;              // exponent
        union               // semi result
            {
            double f;       // 64bit floating point
            DWORD u[2];     // 2x32 bit uint
            } y;
        // fabs
        y.f=x;
        y.u[h]&=0x7FFFFFFF; x=y.f;
        // set safe exponent (~ abs half)
        e=((y.u[h]>>20)&0x07FF)-1023;
        e/=2;               // here can use bit shift with copying MSB ...
        y.u[h]=((e+1023)&0x07FF)<<20;
        // mantisa=0
        y.u[l] =0x00000000;
        y.u[h]&=0xFFF00000;
        // correct exponent
        if (y.f*y.f>x) { e--; y.u[h]=((e+1023)&0x07FF)<<20; }
        // binary search
        for (b =0x00080000;b;b>>=1) { y.u[h]|=b; if (y.f*y.f>x) y.u[h]^=b; }
        for (b =0x80000000;b;b>>=1) { y.u[l]|=b; if (y.f*y.f>x) y.u[l]^=b; }
        return y.f;
        }
    //---------------------------------------------------------------------------
    

    它返回sqrt(abs(x)) 结果与我的C++ IDE (BDS2006 Turbo C++) 中的“math.h”实现匹配。指数从 x 值的一半开始,如果需要,对值 x&gt;1.0 进行 1 校正。其余的都很明显

    代码在 C++ 中,但仍未优化,它肯定可以改进...如果您的平台不知道DWORD,请改用unsigned int。如果您的平台不支持 32 位整数类型,则将其分块为 4 x 16 位值或 8 x 8 位值。如果你有 64 位,那么使用单个值来加快进程

    不要忘记也将指数处理为 11 位 .... 所以对于 8 位寄存器仅使用 2 ... 如果您将尾数作为整数相乘和比较,则可以避免 FPU 操作.此外,乘法本身是累积的,因此您可以使用以前的子结果。

    [注释]

    位的位置见wiki double precision floating point format

    【讨论】:

    • 要除以 512,如果使用的是 IEEE-732 FP 表示,我很确定您可以将内存中的值视为整数并直接从中减去适当移位的常量.
    • pow() 可能会比 sqrt() 更昂贵; (可能例外:2 的幂,但这取决于编译器检测这些特殊情况)
    • @joop pow 可以实现比定点二进制搜索更高的精度,所以在不知道我提到的目标需要的情况下,编译器与此无关,因为:1.大多数 MCU 没有 FPU 模块所以一切是通过函数计算的软件... 2. 编译器无法检测通用函数操作数的特殊情况(除非 sqrt 直接集成在语言中而不是这种情况)
    • @j_random_hacker 我没有想到......但它似乎应该为好主意工作(+1)
    • @joop 已经尝试实现二进制搜索,并且在玩了一点指数之后,精度与 pow 匹配,所以你是对的 pow 即使在这种情况下以相同的精度也会变慢
    猜你喜欢
    • 2013-04-01
    • 2016-04-05
    • 1970-01-01
    • 2020-08-25
    • 1970-01-01
    • 2013-03-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多