C 中有任何更快的 RMS 值计算吗?

Any Faster RMS Value Calculation in C?

我正在用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() 函数在我的编译器中不起作用。

是否有任何代码可以 运行 更快?

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

以整数形式完成所有数学运算,并使用简单的查找映射进行 sqrt 运算。 (你可以在启动时预先计算,如果你正在做 3 阶段,你只 REALLY 需要大约 ~600 个奇数..

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

  1. divisions/multiplications 的 2 次方

    可以通过仅通过位掩码操作和 +,- 更改 指数 来完成,因此 mask/extract 指数 integer 值,然后应用 偏差 。之后 add/sublog2(operand) 并编码回您的 double

  2. 平方

    它应该有多快和多准确?您可以在固定点上使用二进制搜索或使用 sqrt(x)=pow(x,0.5)=exp2(0.5*log2(x))。同样在固定点上,它很容易实现。您可以通过获取尾数并将其移位到您使用的值周围的某个已知指数+处理偏移量或 2^0 如果您有足够的位...

    计算 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;
    }
//---------------------------------------------------------------------------

它 returns sqrt(abs(x)) 结果匹配 "math.h" 我的 C++ IDE (BDS2006 Turbo C++) 的实现。指数从 x 值的一半开始,如果需要,对值 x>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

由于您的平方和值 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(这在一般用途上是正常的machine/desktop)如果double在你的微型计算机上很昂贵你可以改回来。
  • 我还添加了static,因为我不知道代码是来自函数还是来自循环。
  • 我创建了函数 static 来强制它被内联。如果编译器没有内联静态函数,你应该手动内联。

当您需要在缺少 FPU 的处理器上获得更快的速度时,您最好 bet是用定点代替浮点计算。结合 这与 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;

要求平方根,请使用 Microchip 的以下应用笔记,用于 8 位控制器

Fast Integer Square Root

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