Cで移動平均を計算するための時間とメモリの効率的なソリューションを探しています。専用の除算ユニットを持たないPIC 16にいるため、除算を避ける必要があります。
現時点では、すべての値をリングバッファに保存し、新しい値が到着するたびに合計を保存および更新します。これは本当に効率的ですが、残念ながら利用可能なメモリのほとんどを使用します...
Cで移動平均を計算するための時間とメモリの効率的なソリューションを探しています。専用の除算ユニットを持たないPIC 16にいるため、除算を避ける必要があります。
現時点では、すべての値をリングバッファに保存し、新しい値が到着するたびに合計を保存および更新します。これは本当に効率的ですが、残念ながら利用可能なメモリのほとんどを使用します...
回答:
他の人が述べたように、現在使用しているFIR(有限インパルス応答)フィルターではなく、IIR(無限インパルス応答)フィルターを検討する必要があります。それだけではありませんが、一見すると、FIRフィルターは明示的な畳み込みと方程式を含むIIRフィルターとして実装されます。
マイクロコントローラーでよく使用する特定のIIRフィルターは、単極ローパスフィルターです。これは、単純なRCアナログフィルタのデジタル版です。ほとんどのアプリケーションでは、これらは使用しているボックスフィルターよりも優れた特性を備えています。私が遭遇したボックスフィルターのほとんどの用途は、特定の特性を必要とする結果ではなく、デジタル信号処理のクラスで誰かが注意を払っていない結果です。ノイズであることがわかっている高周波のみを減衰させる場合は、単極ローパスフィルターの方が適しています。マイクロコントローラにデジタルで実装するための最良の方法は、通常:
FILT <-FILT + FF(NEW-FILT)
FILTは永続的な状態です。これは、このフィルターを計算するために必要な唯一の永続変数です。NEWは、この反復でフィルターが更新される新しい値です。FFはフィルターの割合で、フィルターの「重さ」を調整します。このアルゴリズムを見て、FF = 0の場合、出力が変化しないため、フィルターが無限に重いことがわかります。FF = 1の場合、出力は入力の直後になるため、実際にはフィルターはまったくありません。間に有用な値があります。小規模なシステムでは、FFを1/2 Nに選択しますFFによる乗算は、Nビットの右シフトとして実行できます。たとえば、FFは1/16で、FFを乗算すると4ビットの右シフトになります。それ以外の場合、このフィルターは1つの減算と1つの加算のみを必要としますが、通常、数値は入力値よりも広くする必要があります(以下の別のセクションで数値精度について詳しく説明します)。
私は通常、A / Dの読み取りを必要な速度よりも大幅に高速化し、これらのフィルターのうちの2つをカスケード接続します。これは、直列の2つのRCフィルターに相当するデジタルであり、ロールオフ周波数より12 dB /オクターブ上で減衰します。ただし、A / D測定では、通常、ステップ応答を考慮して、時間領域でフィルターを確認する方が適切です。これは、測定対象が変化したときに、システムがどれだけ速く変化を見るかを示します。
これらのフィルターの設計を容易にするため(FFを選択し、カスケードする数を決定することを意味するだけです)、プログラムFILTBITSを使用します。カスケードされた一連のフィルターの各FFのシフトビット数を指定すると、ステップ応答とその他の値が計算されます。実際、私は通常、ラッパースクリプトPLOTFILTを介してこれを実行します。これにより、FILTBITSが実行され、CSVファイルが作成され、CSVファイルがプロットされます。たとえば、「PLOTFILT 4 4」の結果は次のとおりです。
PLOTFILTの2つのパラメーターは、上記のタイプの2つのフィルターがカスケード接続されることを意味します。値4は、FFによる乗算を実現するためのシフトビットの数を示します。したがって、この場合、2つのFF値は1/16です。
赤いトレースはユニットのステップ応答であり、注目すべき主なものです。たとえば、これは、入力が瞬間的に変化した場合、結合されたフィルターの出力が60回の反復で新しい値の90%に落ち着くことを示しています。約95%の整定時間を気にする場合は、約73回の反復を待つ必要があり、50%の整定時間では26回の反復のみです。
緑色のトレースは、単一の最大振幅スパイクからの出力を示しています。これにより、ランダムノイズ抑制のアイデアが得られます。単一のサンプルが出力に2.5%以上の変化を引き起こすことはないようです。
青いトレースは、このフィルターがホワイトノイズで行うことの主観的な感覚を与えるためのものです。PLOTFILTの今回の実行でホワイトノイズ入力として選択された乱数の内容が正確に何であるかは保証されないため、これは厳密なテストではありません。どれだけつぶされるか、どれだけ滑らかであるかを大まかに感じさせるだけです。
PLOTFILT、多分FILTBITS、およびその他の多くの有用なもの、特にPICファームウェア開発用のソフトウェアは、ソフトウェアダウンロードページのPIC開発ツールソフトウェアリリースで入手できます。
私はコメントから、このフィルターを実装するのに必要なビット数を議論することに関心があるという新しい答えを見つけました。FFで乗算すると、バイナリポイントの下にLog 2(FF)の新しいビットが作成されることに注意してください。小規模システムでは、FFは通常1/2 Nに選択されるため、この乗算は実際にはNビットの右シフトによって実現されます。
したがって、FILTは通常、固定小数点整数です。これは、プロセッサの観点から数学を変更しないことに注意してください。たとえば、10ビットのA / D測定値とN = 4(FF = 1/16)をフィルタリングする場合、10ビット整数のA / D測定値の下に4つの小数ビットが必要です。ほとんどのプロセッサでは、10ビットのA / D測定値のために16ビット整数演算を実行します。この場合でも、まったく同じ16ビット整数操作を実行できますが、A / Dの読み取り値を左に4ビットシフトして開始します。プロセッサは違いを知らず、その必要もありません。16ビット整数全体で計算を行うと、それらを12.4固定小数点と見なす場合でも、真の16ビット整数(16.0固定小数点)と見なす場合でも機能します。
一般に、数値表現のためにノイズを追加したくない場合は、各フィルターポールにNビットを追加する必要があります。上記の例では、2番目のフィルターは、情報を失わないために10 + 4 + 4 = 18ビットでなければなりません。実際には、8ビットマシンでは、24ビット値を使用することになります。技術的には、2番目の極だけがより広い値を必要としますが、ファームウェアの単純化のために、通常はフィルターのすべての極に対して同じ表現を使用し、それにより同じコードを使用します。
通常、1つのフィルターポール操作を実行するサブルーチンまたはマクロを記述し、それを各ポールに適用します。サブルーチンまたはマクロは、サイクルまたはプログラムメモリがその特定のプロジェクトでより重要であるかどうかに依存します。いずれにせよ、サブルーチン/マクロにNEWを渡すためにいくつかのスクラッチ状態を使用して、FILTを更新しますが、NEWが入っていた同じスクラッチ状態にロードします。次の新機能。サブルーチンの場合、途中でFILTを指すポインターを持つと便利です。これは、途中でFILTの直後に更新されます。このようにして、複数回呼び出された場合、サブルーチンはメモリ内の連続したフィルターで自動的に動作します。マクロを使用すると、各反復で動作するアドレスを渡すため、ポインターは必要ありません。
PIC 18について上記で説明したマクロの例を次に示します。
///////////////////////////////////////////////// /////////////////////////////// // //マクロフィルターフィルト // // NEWVALの新しい値で1つのフィルターポールを更新します。NEWVALが更新されました //新しいフィルタリングされた値が含まれます。 // // FILTはフィルター状態変数の名前です。24ビットと想定されます //広く、地元の銀行。 // //フィルタを更新するための式は次のとおりです。 // // FILT <-FILT + FF(NEWVAL-FILT) // // FFによる乗算は、FILTBITSビットの右シフトによって実行されます。 // /マクロフィルター /書きます dbankif lbankadr movf [arg 1] +0、w; NEWVAL <-NEWVAL-FILT subwf newval + 0 movf [引数1] + 1、w subwfb newval + 1 movf [引数1] + 2、w subwfb newval + 2 /書きます / loop n filtbits;各ビットにつき1回、NEWVALを右にシフトします rlcf newval + 2、w; NEWVALを右に1ビットシフト rrcf newval + 2 rrcf newval + 1 rrcf newval + 0 / endloop /書きます movf newval + 0、w;シフトした値をフィルターに追加し、NEWVALに保存する addwf [引数1] + 0、w movwf [引数1] +0 movwf newval + 0 movf newval + 1、w addwfc [arg 1] + 1、w movwf [arg 1] +1 movwf newval + 1 movf newval + 2、w addwfc [arg 1] + 2、w movwf [arg 1] +2 movwf newval + 2 / endmac
そして、これはPIC 24またはdsPIC 30または33の同様のマクロです:
///////////////////////////////////////////////// /////////////////////////////// // //マクロフィルターffbits // // 1つのローパスフィルターの状態を更新します。新しい入力値はW1:W0にあります //そして、更新されるフィルタ状態はW2によってポイントされます。 // //更新されたフィルター値もW1:W0で返され、W2は //フィルタ状態を過ぎた最初のメモリへ。したがって、このマクロは //一連のカスケードローパスフィルターを更新するために連続して呼び出されます。 // //フィルタ式は次のとおりです。 // // FILT <-FILT + FF(NEW-FILT) // // FFによる乗算は、算術右シフトによって実行されます // FFBITS。 // //警告:W3はゴミ箱に捨てられます。 // /マクロフィルター / var new ffbits integer = [arg 1];シフトするビット数を取得 /書きます / write ";単極ローパスフィルタリングを実行、シフトビット=" ffbits /書きます " ;" sub w0、[w2 ++]、w0; NEW-FILT-> W1:W0 subb w1、[w2--]、w1 lsr w0、#[v ffbits]、w0;結果をW1:W0で右にシフト sl w1、#[-16 ffbits]、w3 ior w0、w3、w0 asr w1、#[v ffbits]、w1 w0、[w2 ++]、w0を追加し、FILTを追加してW1:W0に最終結果を作成します。 addc w1、[w2--]、w1 mov w0、[w2 ++];フィルター状態に結果を書き込み、ポインターを進める mov w1、[w2 ++] /書きます / endmac
これらの例は両方とも、PICアセンブラプリプロセッサを使用してマクロとして実装されています。これは、組み込みマクロ機能のいずれよりも優れています。
平均するアイテム数の2のべき乗(つまり、2,4,8,16,32など)の制限で生活できる場合、除算は専用の除算なしで低パフォーマンスのマイクロで簡単かつ効率的に実行できます。ビットシフトとして実行できます。各右シフトは、2のべき乗の1つです。例:
avg = sum >> 2; //divide by 2^2 (4)
または
avg = sum >> 3; //divide by 2^3 (8)
等
そこで少ないメモリ要件を持つ真の移動平均フィルタ(別名「ボックスカーフィルタ」)のための答えが、あれば、あなたがダウンサンプリング気にしません。カスケード積分櫛型フィルター(CIC)と呼ばれます。考え方としては、ある期間にわたって差分を取る積分器があり、重要なメモリ節約デバイスは、ダウンサンプリングによって、積分器のすべての値を保存する必要がないということです。次の擬似コードを使用して実装できます。
function out = filterInput(in)
{
const int decimationFactor = /* 2 or 4 or 8 or whatever */;
const int statesize = /* whatever */
static int integrator = 0;
static int downsample_count = 0;
static int ringbuffer[statesize];
// don't forget to initialize the ringbuffer somehow
static int ringbuffer_ptr = 0;
static int outstate = 0;
integrator += in;
if (++downsample_count >= decimationFactor)
{
int oldintegrator = ringbuffer[ringbuffer_ptr];
ringbuffer[ringbuffer_ptr] = integrator;
ringbuffer_ptr = (ringbuffer_ptr + 1) % statesize;
outstate = (integrator - oldintegrator) / (statesize * decimationFactor);
}
return outstate;
}
有効な移動平均の長さはdecimationFactor*statesize
異なりますが、statesize
サンプルを保持するだけで十分です。statesize
and decimationFactor
が2の累乗である場合、除算演算子と剰余演算子がシフトとマスクアンドに置き換えられるため、明らかにパフォーマンスが向上します。
追記:移動平均フィルターの前に、単純なIIRフィルターを常に考慮する必要があることは、Olinに同意します。ボックスカーフィルターの周波数ヌルが不要な場合は、1極または2極のローパスフィルターがおそらく正常に機能します。
一方、デシメーションの目的でフィルタリングする場合(高サンプルレートの入力を取得し、低レートプロセスで使用するために平均化する場合)、CICフィルターはまさにあなたが探しているものです。(特に、statesize = 1を使用して、リングバッファを以前の単一のインテグレーター値のみで回避できる場合)
Olin Lathropがデジタル信号処理スタック交換で既に説明した1次IIRフィルターを使用した数学の詳細な分析があります(多くのきれいな写真を含みます)。このIIRフィルターの式は次のとおりです。
y [n] =αx[n] +(1-α)y [n-1]
これは整数のみを使用して実装でき、次のコードを使用した除算は使用できません(メモリから入力しているため、デバッグが必要になる場合があります)。
/**
* @details Implement a first order IIR filter to approximate a K sample
* moving average. This function implements the equation:
*
* y[n] = alpha * x[n] + (1 - alpha) * y[n-1]
*
* @param *filter - a Signed 15.16 fixed-point value.
* @param sample - the 16-bit value of the current sample.
*/
#define BITS 2 ///< This is roughly = log2( 1 / alpha )
short IIR_Filter(long *filter, short sample)
{
long local_sample = sample << 16;
*filter += (local_sample - *filter) >> BITS;
return (short)((*filter+0x8000) >> 16); ///< Round by adding .5 and truncating.
}
このフィルターは、アルファの値を1 / Kに設定することにより、最後のKサンプルの移動平均を近似します。前のコードでこれを行うには、LOG2(K)に#define
ing BITS
します。つまり、K = 16 BITS
を4に設定し、K = 4 BITS
を2に設定します。
(変更を取得したらすぐにここにリストされたコードを確認し、必要に応じてこの回答を編集します。)
単極ローパスフィルターを次に示します(移動平均、カットオフ周波数= CutoffFrequency)。非常にシンプルで、非常に高速で、優れた機能を発揮し、メモリオーバーヘッドはほとんどありません。
注:newInputで渡されるものを除き、すべての変数のスコープはフィルター関数を超えています
// One-time calculations (can be pre-calculated at compile-time and loaded with constants)
DecayFactor = exp(-2.0 * PI * CutoffFrequency / SampleRate);
AmplitudeFactor = (1.0 - DecayFactor);
// Filter Loop Function ----- THIS IS IT -----
double Filter(double newInput)
{
MovingAverage *= DecayFactor;
MovingAverage += AmplitudeFactor * newInput;
return (MovingAverage);
}
注:これは単一ステージのフィルターです。複数のステージをカスケード接続して、フィルターのシャープネスを高めることができます。複数のステージを使用する場合は、補正するためにDecayFactorを調整する必要があります(カットオフ周波数に関連して)。
そして、明らかに必要なのは、どこにでも配置された2行だけで、独自の機能は必要ありません。このフィルターには、移動平均が入力信号の移動平均を表すまでのランプアップ時間があります。そのランプアップ時間をバイパスする必要がある場合は、MovingAverageを0ではなくnewInputの最初の値に初期化し、最初のnewInputが異常値ではないことを期待できます。
(CutoffFrequency / SampleRate)の範囲は0〜0.5です。DecayFactorは0から1までの値で、通常は1に近い値です。
ほとんどの場合、単精度の浮動小数点数で十分であり、倍精度浮動小数点数型を好みます。整数に固執する必要がある場合は、DecayFactorとAmplitude Factorを分数整数に変換できます。分子は整数として格納され、分母は2の整数乗です(したがって、右にビットシフトできますフィルタループ中に分割する必要はなく、分母です)。たとえば、DecayFactor = 0.99で、整数を使用する場合、DecayFactor = 0.99 * 65536 = 64881に設定できます。その後、フィルターループでDecayFactorを掛けるたびに、結果を16にシフトします。
この詳細については、オンラインの優れた本、再帰フィルターに関する第19章:http : //www.dspguide.com/ch19.htm
PS移動平均パラダイムの場合、DecayFactorとAmplitudeFactorを設定する別のアプローチで、ニーズに関連する可能性があります。以前の約6個のアイテムを一緒に平均し、個別に実行して、6個のアイテムを追加して除算します6、AmplitudeFactorを1/6に、DecayFactorを(1.0-AmplitudeFactor)に設定できます。
他の誰もが、IIR対FIRの有用性、および2のべき乗の除算について徹底的にコメントしています。実装の詳細を説明したいと思います。以下は、FPUのない小型マイクロコントローラーでうまく機能します。乗算はありません。Nを2のべき乗にすると、すべての除算はシングルサイクルのビットシフトになります。
基本的なFIRリングバッファー:最後のN個の値の実行中のバッファーと、バッファー内のすべての値の実行中のSUMを保持します。新しいサンプルが入るたびに、SUMからバッファー内の最も古い値を減算し、それを新しいサンプルで置き換え、新しいサンプルをSUMに追加し、SUM / Nを出力します。
unsigned int Filter(unsigned int sample){
static unsigned int buffer[N];
static unsigned char oldest = 0;
static unsigned long sum;
sum -= buffer[oldest];
sum += sample;
buffer[oldest] = sample;
oldest += 1;
if (oldest >= N) oldest = 0;
return sum/N;
}
変更されたIIRリングバッファー:最後のN個の値の実行中のSUMを保持します。新しいサンプルが入るたびに、SUM-= SUM / N、新しいサンプルを追加し、SUM / Nを出力します。
unsigned int Filter(unsigned int sample){
static unsigned long sum;
sum -= sum/N;
sum += sample;
return sum/N;
}
以下のようmikeselectricstuffは言った、あなたが本当にあなたの記憶のニーズを削減する必要がある、とあなたがあなたのインパルス応答が(代わりに、矩形パルスの)指数関数的であることを気にしないならば、私はのために行くだろう指数移動平均フィルタ。私はそれらを広範囲に使用しています。このタイプのフィルターを使用すると、バッファーは必要ありません。N個の過去のサンプルを保存する必要はありません。一つだけ。したがって、メモリ要件はN倍に削減されます。
また、そのための分割は必要ありません。乗算のみ。浮動小数点演算にアクセスできる場合は、浮動小数点乗算を使用してください。それ以外の場合は、整数の乗算を行い、右にシフトします。ただし、2012年です。浮動小数点数を操作できるコンパイラ(およびMCU)を使用することをお勧めします。
より効率的なメモリと高速であることに加えて(循環バッファ内のアイテムを更新する必要はありません)、より自然であると言えます。なぜなら、ほとんどの場合、指数インパルス応答は自然の振る舞いによりよく一致するからです。
@olinと@supercatにほとんど触れられているが、明らかに他の人に無視されているIIRフィルターの問題の1つは、切り捨てによって不正確さ(および潜在的にバイアス/切り捨て)が導入されることです:Nが2のべき乗であり、整数演算のみが使用すると、右シフトは体系的に新しいサンプルのLSBを削除します。これは、シリーズがどれくらい長くなる可能性があるかを意味しますが、平均ではそれらが考慮されません。
たとえば、徐々に減少する系列(8,8,8、...、8,7,7,7、... 7,6,6、)を想定し、平均が最初は実際に8であると仮定します。最初の「7」サンプルは、フィルター強度に関係なく、平均を7にします。1つのサンプルのみ。6などと同じ話です。今度は反対のことを考えてください。セリエが上がります。サンプルが変化するのに十分な大きさになるまで、平均は永遠に7のままになります。
もちろん、1/2 ^ N / 2を追加することで「バイアス」を修正できますが、それでも精度の問題は実際には解決しません。その場合、減少するシリーズは、サンプルが8-1になるまで8のままになります/ 2 ^(N / 2)。たとえば、N = 4の場合、ゼロを超えるサンプルは平均値を変更しません。
そのための解決策は、失われたLSBのアキュムレーターを保持することを意味すると思います。しかし、私はコードの準備が十分にできていなかったので、シリーズの他のいくつかのケースでIIRの力を害しないかどうかはわかりません(たとえば、7、9、7、9が平均して8になるかどうか) 。
@ Olin、2段カスケードも説明が必要です。各反復で最初の結果を2番目の結果に2つの平均値を保持することを意味しますか?これの利点は何ですか?