dsPIC30F/33EPからdsPIC33CKへ移行しようとしています。「sinテーブルを演算させるルーチンで意外なほど演算時間がかかった」、という投稿を最初にしたものの、完全に自分のミスであることが判明したので、投稿を全面改版し、ドタバタ顛末記として再投稿です。
事の発端は、パワーオン時にsinテーブルの計算をさせたこと
これは「パワーオンですぐに動作させる回路」プログラミング時、冒頭で、
void calc_sintbl(void) {
double dval0;
int i;
for(i=0;i<SINDIV;i++) {
dval0 = sin((double)i/SINDIV*3.1415926*2.0);
sinbuf[i] = (int)(dval0*10000.0);
}
}
という関数でRAM上に1サイクルsinカーブのint配列を作らせています。SINDIV:200で1サイクル200個のデータです。
これを50MIPSのdsPIC33CK256MP503に実装してみたところ、なんと300ms近くもかかった、ということです。
目的の機器はスイッチを押して動作開始に300msタイムラグがあるので、なんとも「もっさり」した体感になってしまいます。
原因は、クロック初期化の前にグローバル変数初期化してたこと
その中に、sinテーブル作成が入っていたのである。
PICは、PLL有効化等を、コンフィグレーションではなくプログラム起動後に行うことになっています。今回は内蔵8MHzFRCを×12.5して50MIPSにしています。maxの100MIPSにしないのは、そこまで必要ないのと消費電流が半分ぐらいだからです。
int main(void) {
initIO();
initParam(); // この中でsinテーブル作成を呼んでいた
initCLK();
・
・
したがって、このクロック初期化の前の処理は4MIPSで実行されることになり、sinテーブル計算もその速度になっていた、というのが原因です。
<<<判明した経緯>>>
まず初期化をいじらず、sinテーブル演算ルーチンでパルスを出しオシロで観測。メインループでもsinテーブル演算を繰り返したら、なんと最初の一回目だけ長いのが判明した。・・・なぜだ?。思い込みもありコンフィグレーションをいじるなど迷走しながらここから数時間悩んでしまった。そして解決。
改善して実測したら実行時間は19.3msだった!
「とにかくsinテーブル作成を先頭でやらせよう」という思いが強かったための単純ミスです。まあ、これまでも「ポート設定だけは最初にしたい」だの、「変数初期化だけは先頭で・・・」となりがちなところが確かにありました。これからは何があってもクロック初期化再優先で行きます。
原因が判明する前やったこと、armと比較
昨今ベクトル制御で20KHzで当たり前のように三角関数演算する時代ですが、それでもsin演算って意外とかかるものだなぁ、と思いながらarmと比較するため実測してみました
・STM32F446RE 180MHz(Nucleo-F446RE):2.99ms
・STM32F308K8 72MHz(Nucleo-F303K8):10.14ms
環境はmbedオンライン開発環境です。カリカリに最適化されたコンパイラです。
実行時間の差は単純にクロック差と、M4とM3の違いのようです。F446REはFPU持っていますが単精度なのでmbed開発環境でFPUは使用していないと思われます。
この時は、クロックの違いあれど、armは早いなぁ、と勝手に思い込んでいました。
原因特定前の解決策→ROMテーブル化
ROMテーブルにして解決。dsPIC33CK256MP503は256KBもフラッシュ持つので余裕です。ただ、波形を台形や矩形にアレンジしたいときにテーブルを用意しなければならないのが欠点といえば欠点です。
結局、今回のアプリでは解決後もRAMテーブル方式に戻さずROMテーブル方式で解決、ということにしてしまいました。
ちなみに、Excelで計算した値をCソースに埋め込むには、
- Excelのその行を選択してクリップボードへコピー
- 秀丸エディタに貼りつけ。改行で区切られた数値が並ぶ。
- 正規表現で「\r」を「,」に全置換
- 20とかで改行するようにキーマクロで操作。例えば”,”を検索しておきカーソルを先頭に持っていく→マクロ記録開始→F3を20回→右カーソル”→”を一回→改行→マクロ記録終了。つぎにShift+F2を必要回数押す。です。
数百データ程度なら、この方法でやっています。
IF誌の「軽量sin計算アルゴリズム」を試してみる
せっかくなので、高速sinアルゴリズムを試して今回の顛末とします。2017.8号のIF誌の三上直樹氏の記事、ミニマックス多項式近似のサンプルをつかわせていただき、dsPIC33CKへ実装。
// 三上氏作成のミニマックス多項式近似によるsin計算式をつかわせてもらう int32_t labs(int32_t x) {
if (x<0) return -x; else return x;
}
// 丸めのために使う
inline float ForRound(float x) {
return (x > 0) ? 0.5f : -0.5f;
}
// float 型の引数を int16_t 型の Q14 に変換する
inline int16_t ToFixed14(float x) {
return (int16_t)(x*16384.0f + ForRound(x));
}
// float 型の引数を int16_t 型の Q15 に変換する
inline int16_t ToFixed15(float x) {
return (int16_t)(x*32768.0f + ForRound(x));
}
// float 型の引数を int16_t 型の Q29 に変換する
inline int32_t ToFixed29(float x) {
return (int32_t)(x*32768.0f*16384.0f);
}
// 下位ビットを丸めて右に 14 ビットシフトする
inline int32_t Round14(int32_t x) {
return (x + 0x2000) >> 14;
}
// 下位ビットを丸めて右に 154 ビットシフトする
inline int32_t Round15(int32_t x) {
return (x + 0x4000) >> 15;
}
inline int32_t Sin16(int32_t x) {
int32_t SIN_A1_ = ToFixed29( 0.57079101); // a1 - 1
int32_t SIN_A3_ = ToFixed29(-0.64589285); // a3
int32_t SIN_A5_ = ToFixed29( 0.07943434); // a5
int16_t SIN_A7_ = ToFixed15(-0.00433310); // a7
if (labs(x) > 0x4000) x = 0x8000 - x; // x ← 2 - x ※1
int32_t x2 = Round14(x*x);
int32_t acc = Round14(Round14(Round14 (SIN_A7_*x2 + SIN_A5_)*x2 + SIN_A3_)*x2 + SIN_A1_)*x;
return Round15(acc) + x;
}
void calc_sintbl_fix(void) {
int i,ival;
long lval;
long th;
th=0; for(i=0;i<SINDIV;i++) {
lval = Sin16(th);
sinbuf_f[i] = lval*28508/32768; // 87% th += 327L;
}
}
変数のレンジをそのまま使ったので2πが65536です。16ビットMCUなのでint16_tの挙動でうまくいかず※1でオーバーフロー起こすので、やむなく各所にint32_tを多用しました。
それでも、実行時間は約半分の8.68msに短縮されました。また誤った情報を拡散しないためにも、確認ためdsPICからUART経由でデータをPCに転送しグラフ化してsinカーブを確認ました。
UARTからUSB-UARTモジュールでPCのTeraTermで取り込み、excelへ貼り付けて波形を確認 最初のパルス幅は今回のsinテーブル演算(19.3ms)、2番目が軽量sinによるテーブル演算(8.68ms)
厳密にいえば、1サイクル分解能65536を200分割するのは上記コードでは無理で、327を200回加算すると135ほどずれています。256にするとか、ジッタ覚悟でfloatで加算する必要があります。あくまで速度差の参考用です
まとめ
dsPIC33CKは符号付乗算器を持ち、32ビット乗算をサポートしています。コンパイラでどこまでサポートするかは未確認ですが、演算性能も上がっているようです。と、手のひらを返したように持ち上げることにします。
sin演算に限れば、dsPIC33CK はクロック相対で比較すると32ビットMCU Cortex-M3の70%ぐらいのパフォーマンスということになります。
dsPIC30F/33EPからの移行は粛々と進めることにします
以上