前回、参照テーブルを使わずに減衰曲線を生成する方法を考察しましたが、その方法をマイクロプロセッサで試してみました。やはり一筋縄ではいかないようです。
実際のプログラムは記事に載せるには長すぎますが、関係のある部分を抜粋して、こんな感じのプログラムを組みました。一見動きそうだし、これを普通に PC で実行すれば動作するはずですが、AVR ではうまく動作しません。
volatile uint32_t g_value; // value holder, Q10.22 fixed point number
volatile uint32_t g_decay_factor; // amount of decay, Q0.32 fixed point
volatile uint8_t g_update_ready;
ISR(Time1_OVF_vect) {
OCR1A = g_value >> 22; // reflect the value to PWM、Q10.22 to integer
g_update_ready = 1;
}
void UpdateValue() {
if (!update_ready) {
return;
}
// reduce the value by multiplying the decay factor
uint64_t temp = g_value;
temp *= g_decay_factor;
g_value = temp >> 32;
g_update_ready = false;
}
int main() {
g_value = 1023 << 22; // integer to Q10.22
g_decay_factor = 0.99309249543703590153321021688807 * 0x100000000;
g_update_ready = 0;
while (true) {
UpdateValue();
}
}
Cどうも AVR 用のコンパイラ avr-gcc は、C 言語標準に厳密には従っていないようです。どこかにドキュメントがあるかもしれませんが見つけていません。レジスタを細かく制御したいための仕様なのかもしれません。レジスタを意識して、データ領域からレジスタにどんな風にデータをロードしてレジスタ上でどんな演算をするのか意識して書くと動くようです。上記のプログラムは以下のように書き換えれば動くはずです。
volatile uint32_t g_value; // value holder, Q10.22 fixed point number
volatile uint32_t g_decay_factor; // amount of decay, Q0.32 fixed point
volatile uint8_t g_update_ready;
ISR(Time1_OVF_vect) {
register uint32_t temp = g_value; // copy the Q10.22 value to a variable
temp >>= 22; // Q10.22 to integer
OCR1A = temp; // reflect the value to PWM
g_update_ready = 1;
}
void UpdateValue() {
if (!update_ready) {
return;
}
register uint64_t temp = g_value;
// copy the Q10.22 value to a 64-bit int
temp *= g_decay_factor;
// multiply by the Q0.32 decay factor
temp >>= 32; // shift 32 bit to adjust scale, now it's Q10.22 again
g_value = temp;
// copy back to the value holder
g_update_ready = false;
}
int main() {
register uint32_t temp = 1023 << 22; // this is calculated by the compiler
g_value = temp;
g_decay_factor = 0.99309249543703590153321021688807 * 0x100000000;
g_update_ready = 0;
while (true) {
UpdateValue();
}
}
Ctemp は register 変数として宣言しなくても動くようです。コンパイラが良きに計らってくれるようです。
要点は、ビットを伸ばしたり縮めたりする演算をするときには、以下の点を気を付けると動くようです
- データをどこにロードさせるかはっきりさせる (一時変数を宣言する)
- 演算がどこで行われるかはっきりさせる
- 二項演算子 +, -, *, >>, << のかわりに +=, -=, *=, >>=, <<= を使う。
- 一行一演算、一行に二つの演算を入れない
「ようです」が多いのが恐縮です。このことが書かれているドキュメントを読んだわけではないので、コンパイラの挙動から推測しています。