Arduino:関数ポインタを使ったテーブルジャンプ ROMにテーブルを置く
2020年4月10日:JIS C8708:2019対応ニッケル水素電池充放電実験回路(回路図とプログラム)
の中の「batcyc3b.zip」を見ていただければ良いかと
思うんですが、連続実行させるプログラムの処理に
「関数ポインタ」を使っています。
いろんな処理の流れを場面に分けて記述。
それをテーブルにして、したい処理だけを実行させる
という手順です。
「batcyc3b」では二つの処理を並列に稼働させています。
こんな具合です。
/***** 充放電実行 *****/
void (*cycexc[])(void)={
cycstby, // 0 待機
cycstart, // * 1 充放電開始
cyc0dchg, // 2 CYC 0 最初の放電開始
cyc0dchg1, // 3 CYC 0 最初の放電完了待ち
cyc0dwait, // * 4 CYC 0 放電完了後の時間待ち
cyc49chg, // * 5 CYC 1~49 充電開始
cyc49chg1, // * 6 CYC 1~49 充電完了待ち
cyc49chg2, // 7 CYC 1~49 充電後の時間待ち
cyc49dchg, // * 8 CYC 1~49 放電開始
cyc49dchg1, // 9 CYC 1~49 放電完了待ち
cyc49dchg2, // 10 CYC 1~49 放電後の時間待ち
cyc50chg, // * 11 CYC 50 充電開始
cyc50chg1, // 12 CYC 50 充電完了待ち
cyc50chg2, // 13 CYC 50 充電後の時間待ち
cyc50dchg, // * 14 CYC 50 放電開始
cyc50dchg1, // * 15 CYC 50 放電完了待ち
};
/***** 測定実行 *****/
void (*mesexc[])(void)={
mesttl, // * 0 タイトル表示
mesttl1, // 1 タイトル表示続き
messtby, // * 2
messtby1, // 3
mesmenu, // * 4
mesmenu1, // 5
mesmenu2, // 6
mespwm, // * 7 PWM電流テスト
mespwm1, // 8 PWM電流テスト#1
mespwm2, // 9 PWM電流テスト#2
mespwm3, // 10 PWM電流テスト#3
mespara, // * 11 パラメータ設定
mespara1, // 12
mespara2, // 13
mescyc, // * 14 充放電サイクル開始
};
※それぞれの実行ルーチン
cycexc[cyc_exc](); // 充放電サイクル実行
mesexc[mes_exc](); // 測定実行区分で処理
このテーブルですが、置かれているのは「RAM」領域。
関数のアドレスを示していますんで、固定的な値です。
※実行中にこれを変更する必要はありません。
これが合わせて30コほど。
一つが2バイトですので合わせて60バイト。
固定データにRAMを使うのって、Arduino(ATmega328P)では
ちょっともったいない。 (RAMが少ないので)
ROMに置ければ、60バイトのRAM領域が浮いてきます。
しかし・・・
AVRマイコンでのC言語、ROM領域にデータを置くには
「PROGMEM」を使って記述するという、独特の制限が
あります。
そしてROM領域からの読み出しは、pgm_read_byte()や
pgm_read_word()を使わなくてはなりません。
RAMに置いたように記述しても、ROMだと直接実行してくれない
のです。
そこで、こんなテストプログラムを書いてみました。
まずは、いつものようにRAMでの実行。
/***** テーブルジャンプのテスト *****/
// テーブルをROMに入れたい
// まずはいつも使っているRAMのテーブルで
byte ex_t = 0; // 実行番号 0~3
/***** 実行ルーチン *****/
void t0(void){
Serial.println(" test0");
}
void t1(void){
Serial.println(" test1");
}
void t2(void){
Serial.println(" test2");
}
void t3(void){
Serial.println(" test3");
}
void t4(void){ // これは動作確認用おまけ
Serial.println(" test4");
}
/***** RAMでの実行 *****/
// 配列に実行ルーチンを入れる
void (*ramexc[])(void)={
t0, // 0
t1, // 1
t2, // 2
t3, // 3
};
/***** 実験ルーチン *****/
void setup() {
Serial.begin(9600); // 9600BPSで
}
/***** LOOP *****/
void loop() {
Serial.print("RAM ");
delay(100);
ramexc[ex_t](); // RAMでテーブルジャンプ
ex_t++;
if(ex_t >= 4) ex_t = 0; // 0に戻す
delay(1000); // ちょい待ち
}
関数(サブルーチン)をテーブルに置いて、
ramexc[ex_t]();
とすれば、「ex_t」の値で該当サブルーチンを実行し
てくれます。
次はROMの場合。
まず、関数の置く方法。
「typedef」で型宣言してから「PROGMEM」でROM内の
固定データ(関数のアドレス)の配列にします。
関数の名前の置き方はRAMといっしょ。
問題はこの関数テーブルの実行方法です。
アドレスは2バイトデータですので、pgm_read_word()で
取り出します。
それをプログラムとして実行する本体、関数ポインタに与え
ます。
この時、ちょいとややっこい「キャスト」をしないと
warningが出てしまいます。
最終的に落ち着いたキャストの方法がこれ。
p = (void (*)(void))a;
p=a; とか
p=(void *)a;
だとwarningが出ちゃうのです。
実行は
romexc(ex_t);
でok。
ex_tの番号で指定した関数(サブルーチン)が実行されます。
具体的にはこんなスケッチです。
/***** テーブルジャンプのテスト *****/
// テーブルをROMに入れたい
// ROMでの実行を試す
byte ex_t = 0; // 実行番号 0~3
/***** 実行ルーチン *****/
void t0(void){
Serial.println(" test0");
}
void t1(void){
Serial.println(" test1");
}
void t2(void){
Serial.println(" test2");
}
void t3(void){
Serial.println(" test3");
}
void t4(void){ // これは動作確認用おまけ
Serial.println(" test4");
}
/***** ROMでの実行 *****/
typedef void(*romtt)(void); // typedefで形態を指定
// テーブルの実態
romtt const romtbl[] PROGMEM ={ // PROGMEMでROMに配置
t0, // 0
t1, // 1
t2, // 2
t3, // 3
};
// 実行ルーチン
void romexc(byte n){
word a; // 読み出しアドレス
void (*p)(void); // 実行する本体
a = pgm_read_word(&romtbl[n]); // アドレスリード
// p = a; // warning
// p = (void *)a; // warning
p = (void (*)(void))a; // ★ここのキャストどうにかしたいが
p(); // 実行
}
/***** 実験ルーチン *****/
void setup() {
Serial.begin(9600); // 9600BPSで
}
/***** LOOP *****/
void loop() {
Serial.print("ROM ");
delay(100);
romexc(ex_t); // ROMでテーブルジャンプ
ex_t++;
if(ex_t >= 4) ex_t = 0; // 0に戻す
delay(1000); // ちょい待ち
}
ROMでのテーブルジャンプ、もうちょい簡単に記述でき
れば良いのですが・・・
RAMやI/Oの読み書き場所とROMの読み出し場所、方法
が異なるというAVRマイコンのアーキテクチャに起因し
ますんでどうにもできません。
機械語レベルの話になりますが、AVRマイコン、プログラムを
入れるROMが16ビットなんで、ROM内のテーブルを読む時の
アドレス指定がちょいとややこしい。(2倍しなくちゃならない)
※この話、もうちょい続きます。
どうやら、ramexc()とromexc()を同時に使うと
コンパイラがバグる(最適化のミス)ようなのです。
RAM用とROM用、それぞれのテーブルの中を
t0~t3を同じ順で並べると、RAMのほうの実行が
暴走。
「同じ値のテーブルやから処理を省略したろ」と
しているっぽいのです。
t0~t3の順番を変える、t4を入れるなどして異なった
テーブルにすると、ちゃんと動くのです。
※暴走するスケッチ
ダウンロード - test_progmem1.c
ファイルタイプを「.c」にしています。
★マークの実行サブルーチンの配列、例えばt3をt4に
変えるとちゃんと走るようになります。
| 固定リンク
「Arduino」カテゴリの記事
- 初めて買ったArduino UNO・・・今は(2023.05.25)
- 液晶表示コントローラ HD44780で迎撃(2023.05.16)
- Arduino UNOで3相モーターを回す(2023.05.01)
- Arduino サーミスタを使った温度測定で 【ゼロ除算問題】(2023.03.23)
- A/Dコンバータでサーミスタの抵抗値を読む サーミスタをつなぐ場所は?(2023.03.21)
コメント