« Arduino-UNO 割り込み処理のミスあれこれ:割り込み禁止にして読まないとダメよ | トップページ | 「ペコ」、7年目 »

2022年3月21日 (月)

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

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

前記事、
Arduino-UNO 割り込み処理のミスあれこれ:割り込み禁止にして読まないとダメよ
これ↑は、読み出し時のお話しでしたが、今回は
書き込みで発生するミスを検証してみます。

Arduino-UNOのINT0を↓エッジ検出の割り込み入力にして
PD5のPWM出力(約980Hz)をつなぎます。
「読み出し」と同じ接続です。
Aa1_20220321093001
INT0割り込み処理では、long:4バイトのカウントデータを
デクリメント(-1)します。
メインループでこれを「ゼロ・クリアー」する処理を走ら
せます。
そして、ゼロにした直後、この値を読み取ってチェックし
ます。
こんな手順です。

  (1) 割り込み有効のままカウント値を
    ゼロクリア。
  (2) その直後、割り込み禁止にして
    カウント値を読み取る。
  (3) カウント値は、
      ゼロのままになっている。
        あるいは、
      直後に-1されてマイナスの値に
      なるかもしれない。
  (4) ゼロかマイナスなのでプラスには
    ならないはず。

問題はこの(4)。 ・・・ほんとでしょうか?
それを確かめます。

こんなスケッチです。

/*****  割り込み処理ミスの検証     *****/
// "intr_miss_dncnt1.ino" 2022-03-20 / JH3DBO
// 割り込みでカウントダウンされるデータをゼロクリアするとき
// 割り込み禁止にしていないとミスすることがあるのを検証
// 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_down; // 割込み内でカウントダウン
long loop_cnt; // メイン処理をloopするごとに+1
// シリアル出力バッファ
char tx_bff[64]; // sprintfで出力文字を設定
/***** INT0(PD2) ↓エッジパルス割り込み *****/
void countdn(void){
PB0_H; // (!!!)
cnt_down--; // カウント値 -1
PB0_L; // (!!!)
}
/***** セットアップ *****/
void setup(){
analogWrite(5, 128); // D5に980Hz出力
pinMode(2, INPUT_PULLUP); // D2は入力
attachInterrupt(0, countdn, FALLING); // D2をINT0割り込みに
pinMode(8, OUTPUT); // PB0 テストパルス
pinMode(12, OUTPUT); // PB4
pinMode(13, OUTPUT); // PB5
Serial.begin(9600); // シリアル出力
}
/***** LOOP *****/
void loop(){
char c;
long d0; // 割り込み禁止でcnt_downをコピー
uint32_t tm_1ms; // 1msタイマー
Serial.println(F("test(ZeroClr + DnCnt)")); // タイトル
tm_1ms = millis() - 1000; // タイマーすぐ表示で
// 実行loop
while(1){
loop_cnt++; // ループカウンタを+1
if(loop_cnt > 99999999) loop_cnt = 0; // max8桁
delayMicroseconds(random(100)); // 0~99us停止
// 一定周期でループカウンタを出力
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_down = 0; // カウント値クリア
interrupts(); // 割込再開
loop_cnt = 0; // ループカウンタもゼロに
tm_1ms = millis() - 1000; // タイマーすぐ表示で
}
}
// カウント値をゼロクリアしたあと読み出してチェック
PB4_H; // (!!!) データコピータイミング
cnt_down = 0; // ★1 割り込み禁止にしないでゼロクリア
PB4_L; // (!!!)
noInterrupts(); // いったん割込禁止に
d0 = cnt_down; // カウント値longデータ
interrupts(); // 割込再開
if(d0 > 0){ // ゼロかマイナスのはず
PB5_H; // (!!!)
sprintf_P(tx_bff, PSTR("0x%08lX"), d0); // 8桁16進で
Serial.println(tx_bff); // シリアル出力
PB5_L; // (!!!)
}
}
}

・setupで、PWM出力とIN0を↓エッジ割り込みに。
  他、テストポートを出力に。
・PWMはD5ポート。 約980Hzを出力。
  これをINT0入力につなぐ。
・1秒周期でループカウンタ(何回loopを回ったか)
 を出力。
・ゼロクリア後のカウント値を割り込み禁止で
 読み取り、「プラス」ならその値を16進で出力。
・ゼロクリア+読み取りの処理が高速で続くので、
 loopを回るとき、
   delayMicroseconds(random(100))
 で、ちょっとだけ(max99us)時間待ちを入れる。

その結果です。

test(ZeroClr + DnCnt)
1
0x000000FF
0x0000FFFF
0x0000FFFF
6808
0x00FFFFFF
0x000000FF
0x0000FFFF
0x00FFFFFF
13592
0x0000FFFF
0x000000FF
0x0000FFFF
0x000000FF
20388
0x000000FF
0x000000FF
0x0000FFFF
27190
0x000000FF
0x000000FF
0x000000FF
33980
0x0000FFFF
0x000000FF
0x00FFFFFF
40792
0x0000FFFF
0x0000FFFF
0x0000FFFF
0x000000FF
47590
  :

  ※時間待ちを入れないと「ミス」の出現が
   多過ぎて見にくいので、割り込みと重なる
   タイミングを少なくしています。

割り込みを有効にしたままでのゼロクリア、
多バイトのゼロ書き込み途中に入り込んだ割り込み
(カウントダウン処理)のタイミング(どの桁の時に)
によって、結果が変わります。

ミスしたデータとして出てくるのはこの3種類。
  0x000000FF
  0x0000FFFF
  0x00FFFFFF
いずれもプラスの値です。
ダウンカウントしているはずなのに、です。

本来は「0x00000000」か、-1された「0xFFFFFFFF」。
もう少し進むと-2されて「0xFFFFFFFE」となるはず
です。
ゼロクリア中に割り込みが入ると、上位にだけ
「00」が残ってしまい(最後に書かれる)「プラス」
の値となってしまうのです。

この書き込みミスは、
 ・8ビットマイコン特有の問題
 ・ゼロクリアした途中のカウントダウン
  だけでなく、数値のプリセットでも
  桁上がり・桁下がりが生じるとミスが
  発生する。
 ・一度生じたミスは残ってしまい、正常には
  戻らない。
    ※これが読み出しと違って怖いところです。
     読み出しは、次の読み出しでは正常に戻
     りますんで。
対策は、
 ・割り込み禁止にしてから、ゼロクリアー
  あるいはカウント値のプリセット処理を
  行って、その後割り込み有効に戻す。
です。

うまく条件が合わないことにはこのミスは出ません。
しかし、読み出しと違って、ミスが出たときは
致命的な影響が残ってしまいます。

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

それがプログラムの「バグ」というものです。

|

« Arduino-UNO 割り込み処理のミスあれこれ:割り込み禁止にして読まないとダメよ | トップページ | 「ペコ」、7年目 »

Arduino」カテゴリの記事

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

コメント

補足です。
INT0割り込みでのダウンカウント・パルスですが、「analogWrite(5, 128);」とPWM出力(この場合はタイマー0)を使いました。
ですので、一定周波数になります。
「tone()」を使えば、任意のピンに任意の周波数(範囲はあるけど)を出力することができます。
  参:http://igarage.cocolog-nifty.com/blog/2022/01/post-73e040.html
しかし問題が。
tone()はタイマー2のコンペアマッチA割り込みでポートをトグル(方形波を出すため)しています。
つまり、tone方形波の変化点は割り込みがかかっているのです。
その波形の↓エッジでINT0割り込みをかけてしまうと、割り込み起動のタイミングが固定化してしまいます。
tone割り込みが終わった直後にINT0割り込みが起動してしまうわけです。
ゼロクリア処理とのタイミングをバラけさせたいので、割り込みが二重になってしまうtoneは使いませんでした。

しかし、analogWrite()にも気をつけなければなりません。
タイマー0のanalogWrite()は「8bit高速PWM+非反転出力モード」を使っています。
そして、内部のタイマー処理(millis()など)のためにオーバーフロー割り込みを有効にしています。
  参:http://act-ele.c.ooco.jp/blogroot/igarage/article/4365.html
非反転出力モードではオーバーフローでPWM出力がL→Hになります。
つまり出力PWM波形の↑エッジでオーバーフロー割り込みがかかるのです。
  参:http://igarage.cocolog-nifty.com/blog/2020/08/post-0d2777.html
ですので、今回は割り込みのかからない↓エッジをINT0に入れて、重複を避けました。

投稿: 居酒屋ガレージ店主(JH3DBO) | 2022年3月23日 (水) 14時31分

コメントを書く



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




« Arduino-UNO 割り込み処理のミスあれこれ:割り込み禁止にして読まないとダメよ | トップページ | 「ペコ」、7年目 »