#Arduino Unoは遅い?
Arduinoはマイコンプログラミングの取っつきにくい所を上手く隠し、手軽にガジェット等を作成出来る優れものですが、見えない所で色々と初期設定されて思った様な速度で動いてくれない事があります(特にシリアル受信とか使った時)
Arduino UNOでYMF825FM音源ボードをMIDI音源化した時に使った、ヒントになりそうな事をまとめてみました。
###ditigalWrite()は使わない
これは有名、設定エラーの処理等余分なコードが一杯
digitalWrite(10,LOW); over 40 clock
PORTB &= ~(0x04); over 3 clock
asm("cbi 5,2"); 2clock
下の方が高速です。
###Timer0の停止
Timer0 Overflowの割り込みがデフォルトで設定されているので時間管理が不要なら停止させます、多分こいつがSerialが115,200以上使いものに成らない犯人。
setup()の中ぐらいで止めときましょう。
TIMSK0= 0;
###loop()の中でループ
Arduinoの場合隠れmain()からloop()がコールされるのですが、Serial.begin()をコールすると、ユーザ設定関数まで呼びに行くので呼び出しのオーバーヘッド節減のため素直にwhileで明示的にループさせます。
void loop() {
while (1) {
・・・・・・・・・メインの繰り返し処理
}
}
*.inoの中でmain()を記述する事も出来ます、この場合は初期設定のinit()を呼ばないので一切の初期設定はユーザが行います。
###Serial.available()を使わない
1バイトずつ処理するのであればint Serial.read()は受信バッファに文字が無い場合-1を返すので、いちいちSerial.available()をcallする必要はありません。
char c;
if(Serial.available()>0){
c = Serial.read()
.........
.........
signed char c;
if( (c = Serial.read()) != -1){
..........
..........
###出来るだけ小さいsizeの変数の型を使う
型 | size | 範囲 |
---|---|---|
int | 16 | -32768~32767 |
uint8_t | 8 | 0~255 |
char | 8 | -128~127 |
8bitマイコンは一部の例外を除き16bitの演算を複数の命令に分けて行います、8bitと16bitの差は加減算で倍、乗除算なんかえらいちがいになります、取りうる値の最大値を考えて型を決めましょう。 | ||
ループカウンタの変数とか取りあえずintにしてませんか? | ||
######変数を使いまわす | ||
関数内で宣言された変数は可能な限りマイコンのレジスタに振り分けられています、速度重視の時は可読性を犠牲にして変数を使いまわすと速くなる場合があります(変数を一杯使っている時とか)。 |
###関数呼び出しはオーバーヘッドを伴う
関数のコールと復帰で8clock使います、実際は引数の受け渡し等もっとかかるので見にくくなりますが速度の必要なループの中では同じ処理があってもだらだら書いた方が良い時も。
###コンパイルオプションの変更
windowsの場合
Arduino\hardware\arduino\avr\platform.txtでgccのコンパイルオプションが設定されています
optimizeオプションのデフォルトはOs(サイズ優先)になっているのでO3(速度優先、ループ展開したりしてサイズが大きくなる、手元のプログラムで1.6倍ぐらいにサイズが増えました)だめならO2にしてみましょう。
##プログラム用メモリは余っても意味が無いからどんどん使う。
大概のプログラムでは結構残ると思います、スタックエリアじゃ無いし他のプロセス、ましてや他人様が使う訳ないのでギリギリまで利用しましょう。
###あらかじめ計算できるものはテーブルで持つ(特に除算が入る場合)
PROGMEMで定数をプログラムエリアへ追い出してワーク用のメモリを確保するのはよく使うのですが
変数が2つぐらいまでで、値の取りうる範囲が分かっている場合には除算とか実数を扱う場合はかなり効果があります。
例)int a,bともに値が 0~31
int a,b,x;
x = a*b/31; とかの場合
PROGMEM int ans[32][32] = {
{0*0/31, 0*1/31, 0*2/31, 0*3/31, .... 0*31/31},
{1*0/31, 1*1/31, 1*2/31, 1*3/31, .... 2*31/31},
{2*0/31, 2*1/31, 2*2/31, 2*3/31, .... 3*31/31},
...
..
{31*0/31, 31*1/31, 31*2/31, 31*3/31, ... 31*31/31},
x = pgm_read_word(&(ans[a][b])); /* 計算しない */
データが1バイトなら1024byte,2バイトなら2048,float(4バイト)で4096byte、Arduino Unoで使ってるAtmega328Pは乗算はハードウェアで出来ますが除算は出来ません、可能な限りビットシフト使うかテーブル処理で除算を省きましょう。
###同じ処理を続けて書く
ループするのにカウンタデクリメントして比較ブランチさせるとアセンブラで8bitカウンタでも最低でも3clockは掛ります、計算がintだともっと掛るので繰り返し部のコードが少ないならループを展開します。
int life[30];
int i
int damage;
for(i = 0; i<30;i++){
life[i] -= damage;
if(life[i] <= 0){
dead(i); //貴方のLifeはゼロよ
}
}
i = -1;
life[++i] -= damage; <- 1回目
if(life[i] <= 0){
dead(i);
}
life[++i] -= damage; <-2回目
if(life[i] <= 0){
dead(i);
}
…
life[++i] -= damage; <-30回目
if(life[i] <= 0){
dead(i);
}
これを30回繰り返すとループ処理の回数だけ速くなります、他にする事が無くなってプログラムメモリが余ってたらメモリの許す限りやっちゃいましょう。
##アセンブラで書き直す
1からアセンブラを学習するのならスケッチそのままでArduino Mega使った方が良いと思いますが利用できるライブラリ等があるのなら有用です。
私はYMF825boardMIDIではピッチベンドの計算処理、シリアルの受信SPI送信をアセンブラで書き直しました。
##例
YMF825-MIDIで2byteの固定少数点(2.14形式)のピッチベンド値の計算を、先に計算できる値のテーブルをプログラムエリアに置いて、計算処理のループを展開したのをアセンブラで書いた例。
YMF825boardMIDI CalcExpo.S
同じプログラムでSPIへ480バイトのデータを出来るだけ早く書き込むため16Clockかかる書き込みに対し17clockで復帰するサブルーチンコールをマクロで60個ずつ480個のCall命令置いて処理した例。
送信終了を調べる命令が最悪のタイミングになっていたので1Kbyteプログラムエリア使って6clock×480で2,880clock(0.18ms)速くなりました。
###ここ間違ってるよ~、こんなのあるよ~てのがありましたらどんどんお教え願います。
2020/3/22 「出来るだけ小さいsizeの変数の型を使う」追加