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 或其他一些功率测量方法吗? (例如,你能用一个简单的框均值算法逃脱吗?)
divisions/multiplications 的 2 次方
可以通过仅通过位掩码操作和 +,-
更改 指数 来完成,因此 mask/extract 指数 到 integer
值,然后应用 偏差 。之后 add/sub
值 log2(operand)
并编码回您的 double
值
平方
它应该有多快和多准确?您可以在固定点上使用二进制搜索或使用 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 操作仅将尾数作为整数进行比较。此外,乘法本身是累积的,因此您可以使用之前的子结果。
[注释]
由于您的平方和值 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 位控制器
速度非常快,只需 9 次循环就可以找到平方根
我正在用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 或其他一些功率测量方法吗? (例如,你能用一个简单的框均值算法逃脱吗?)
divisions/multiplications 的 2 次方
可以通过仅通过位掩码操作和
+,-
更改 指数 来完成,因此 mask/extract 指数 到integer
值,然后应用 偏差 。之后add/sub
值log2(operand)
并编码回您的double
值平方
它应该有多快和多准确?您可以在固定点上使用二进制搜索或使用 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 操作仅将尾数作为整数进行比较。此外,乘法本身是累积的,因此您可以使用之前的子结果。
[注释]
由于您的平方和值 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 位控制器
速度非常快,只需 9 次循环就可以找到平方根