« Arduino-UNO 割り込み処理のミスあれこれ:パルス計数の抜けを確かめる その2 | トップページ | Arduino-UNO 割り込み処理のミスあれこれ:割り込み禁止にして書かないとダメよ »

2022年3月21日 (月)

Arduino-UNO 割り込み処理のミスあれこれ:割り込み禁止にして読まないとダメよ

何度も言ってるんですが・・・
「8ビットマイコンで割り込みで処理される多バイトデータを
 読み書きする時は割り込み禁止にしなくちゃダメ」

今回は読み出し時のミスを検証してみます。

Arduino-UNOのINT0を↓エッジ検出の割り込み入力にして
PD5のPWM出力(約980Hz)をつなぎます。
こんな接続です。
Aa1_20220321093001

テストしたスケッチを示します。
ハードウェアの直叩きはポートのH/L操作だけにして
Arduinoの作法でsetup()などを記しています。
cli()、sei()も noInterrupts()とinterrupts()で
記述してます。
  ※アセンブラからAVRマイコンに入った人は、
   3文字のCLI、SEIでエエやんっと。
   8080なら2文字でDIとEIだ。

/*****  割り込み処理ミスの検証     *****/
// "intr_miss_upcnt1.ino" 2022-03-20 / JH3DBO
// 割り込みでカウントアップされるデータをprintするとき
// 割り込み禁止にしていないとミスすることがあるのを検証
// PD2 <D2> パルス入力 ↓エッジ
// PD5 <D5> テスト用パルス analogWriteで980Hzを出力
// 1秒サイクルでループした回数を出力
// ミスを検出した時に16進で値を出力
// CR入力でクリア
/***** I/O MACRO *****/
#define PB0_H (PORTB |= (1 << PB0)) // (!!!)PB0 H/L
#define PB0_L (PORTB &= ~(1 << PB0))
#define PB4_H (PORTB |= (1 << PB4)) // (!!!)PB4 H/L
#define PB4_L (PORTB &= ~(1 << PB4))
#define PB5_H (PORTB |= (1 << PB5)) // (!!!)PB5 H/L
#define PB5_L (PORTB &= ~(1 << PB5))
/***** データ *****/
// カウントデータ
volatile long cnt_up; // 割込み内でカウントアップ
long loop_cnt; // メイン処理をloopするごとに+1
// シリアル出力バッファ
char tx_bff[64]; // sprintfで出力文字を設定
/***** INT0(PD2) ↓エッジパルス割り込み *****/
void countup(void){
PB0_H; // (!!!)
cnt_up++; // カウント値 +1
PB0_L; // (!!!)
}
/***** セットアップ *****/
void setup(){
analogWrite(5, 128); // D5に980Hz出力
pinMode(2, INPUT_PULLUP); // D2は入力
attachInterrupt(0, countup, FALLING); // D2をINT0割り込みに
pinMode(8, OUTPUT); // PB0 テストパルス
pinMode(12, OUTPUT); // PB4
pinMode(13, OUTPUT); // PB5
Serial.begin(9600); // シリアル出力
}
/***** LOOP *****/
void loop(){
char c;
long d1; // 割り込み有効のままcnt_upをコピー
long d2; // 割り込み禁止でcnt_upをコピー
uint32_t tm_1ms; // 1msタイマー
Serial.println(F(" (EI) (DI) dif")); // タイトル
tm_1ms = millis() - 1000; // タイマー1秒前に
noInterrupts(); // いったん割込禁止に
cnt_up = 0; // カウント値クリア
interrupts(); // 割込再開
// 実行loop
while(1){
loop_cnt++; // ループカウンタを+1
if(loop_cnt > 99999999) loop_cnt = 0; // max8桁
// 一定周期でループカウンタを出力
if((millis() - tm_1ms) >= 1000){ // 1秒経過
tm_1ms = millis(); // 現在値保存
sprintf_P(tx_bff, PSTR("%8ld"), // ループカウンタを出力
loop_cnt);
Serial.println(tx_bff); // シリアル出力
}
// CR受信でカウント値をクリア
if(Serial.available()){ // 受信データあり
c = Serial.read(); // 1文字読み出し
if(c == '\r'){ // CR ?
noInterrupts(); // いったん割込禁止に
cnt_up = 0; // カウント値クリア
interrupts(); // 割込再開
loop_cnt = 0; // ループカウンタもゼロに
tm_1ms = millis() - 1000; // タイマーすぐ表示で
}
}
// カウント値を読み出してチェック
PB4_H; // (!!!) データコピータイミング
d1 = cnt_up; // 【1】割り込み状態でコピー
PB4_L; // (!!!)
noInterrupts(); // ★いったん割込禁止に
d2 = cnt_up; // 【2】カウント値longデータ
interrupts(); // ★割込再開
if(abs(d2 - d1) > 100){ // 差が大きければ
PB5_H; // (!!!)
sprintf_P(tx_bff, PSTR("0x%08lX 0x%08lX 0x%04lX"), // 8桁16進で
d1, d2, abs(d2 - d1));
Serial.println(tx_bff); // シリアル出力
PB5_L; // (!!!)
}
}
}

INT0の割り込み処理では、long:4バイトのカウントデータを
インクリメント(+1)します。
メインループでこれを読み取って、シリアル出力します。
この時、
  (1)割り込み有効のままカウント値を
    読み出した値:d1。
  (2)その直後、割り込み禁止にして
    読み出した値:d2。
という2つのデータを得ます。
d2のほうが後なので、途中でカウントアップされたら、
d2のほうが大きくなります。

ところが・・・
(1)の読み出し処理の途中でカウントアップ割り込みが
入って、カウント値が桁上がり直前だと、こんなミス
が生じるかもしれないのです。

データをシリアル出力してミスが生じる様子を観察できるように
しています。

  (EI)   (DI)   dif
    1
 137345           ←loopカウンタ
  :
2197306           ←カウントが進む
0x00003EFF 0x00003E00 0x00FF ←異常発生時 16進で出力
  |     |    +--差分 (差の絶対値)
  |     +-------割り込み禁止で読んだ値
  +-------------割り込み有効のままで読み出し

解説:(1)「3DFF」の時、下位がFFの時に読み出し。
   (2)そのタイミングでカウントアップ割り込みが入る。
   (3)「3E00」になる。
   (4)カウントアップされた2桁目の「3E」を読み出し。
   (5)結果、「3DFF」あるいは「3E00」なのに
    「3EFF」になる。 ※読み出しミス発生。
   (6)割り込み禁止にしておくと、途中のカウントアップが
     待たされて正しい値が読める。
  :
0x00006DFF 0x00006D00 0x00FF
3982559
  :
4806511
0x000088FF 0x00008800 0x00FF
4943813
5081068
0x00008FFF 0x00008F00 0x00FF
  :
0x0000AAFF 0x0000AA00 0x00FF
  :
0x0000CCFF 0x0000CC00 0x00FF
  :
0x0000DAFF 0x0000DA00 0x00FF
  :
0x0000E4FF 0x0000E400 0x00FF
  :
0x0000FFFF 0x0000FF00 0x00FF
9200547
0x0001FFFF 0x00010000 0xFFFF ★1:2桁目でミス 珍しい
9337847
  :
9887149
0x000114FF 0x00011400 0x00FF
10024449
  :

ミスが生じたときの下位桁は「FF」か「00」ですので、
正しいカウント値との差は「FF」になります。

また、4バイトの下位2桁が「FFFF」のときにうまく(!)
割り込みが入ると、正しい値との差が「FFFF」という
大きなもの生じてしまい、これはもう大事件です。
★1のように「たまたま」出現しました。

カウント値の下位が「FF」になるのは、割り込みの
256回に1回です。
異常値が出るのはこのときにたまたま読み出してと
いうタイミングです。
うまく条件が合わないとこのミスは出ません。

でも・・・  何度も言います。
  起きる可能性があるなら、いつかはアタリます。

この読み出しミスは、
 ・8ビットマイコン特有の問題
 ・特定の値の時に割り込みと競合した
  場合だけミスが発生。
 ・次の読み出しでは正常に戻る
という特徴があります。

解決方法は、
 ・割り込み禁止にして読み出して、
  その後、すぐ割り込み有効に戻す。
です。

  ※割り込みの応答速度への影響はほとんどありません。
   割り込み禁止にするのは、数値を読み出す一瞬だけ
   です。
   バックグランドで動いているタイマー割り込み処理
   の影響やトロくさいdigitalWriteなんかのほうが問題
   でしょう。

  ※割り込みで処理される値を扱っている関数全体を
   割り込み禁止で動かすのではなく、割り込み禁止に
   してから数値をローカル変数にコピーした後、割り込み
   有効に戻し、関数ではそのコピーした値を使うという
   ふうにして、割り込み禁止している時間を短くする
   ということで速度低下(ちょっとだけだ)に対処します。

表示だけだと実害は無いかもしれません。
しかし、カウント値を何かの制御に使っていたら
「たまにおかしくなる」が発生して、
見つけにくいバグになってしまいます。
気付きにくいから、最初からちゃんと考えておか
ないと、という次第です。

※関連
割り込みで処理させるwordデータの扱い
Arduino-UNO 割り込み処理のミスあれこれ:ロータリーエンコーダー
Arduino-UNO 割り込み処理のミスあれこれ:計時処理
『アトミック操作』・・・8bitマイコンに限り何か別の言い方なかったか?

タイマー割り込みを使ったLEDのダイナミック点灯処理
Arduinoのタイマー処理
toneパルスの異常 もうちょっと掘り下げて

※続き
Arduino-UNO 割り込み処理のミスあれこれ:割り込み禁止にして書かないとダメよ

|

« Arduino-UNO 割り込み処理のミスあれこれ:パルス計数の抜けを確かめる その2 | トップページ | Arduino-UNO 割り込み処理のミスあれこれ:割り込み禁止にして書かないとダメよ »

Arduino」カテゴリの記事

重箱の隅」カテゴリの記事

コメント

コメントを書く



(ウェブ上には掲載しません)




« Arduino-UNO 割り込み処理のミスあれこれ:パルス計数の抜けを確かめる その2 | トップページ | Arduino-UNO 割り込み処理のミスあれこれ:割り込み禁止にして書かないとダメよ »