« Amazonベーシック単3(2000mAh) 60%(72分)放電 1200回で終了 | トップページ | Arduino-UNO 割り込み処理のミスあれこれ:パルス計数の抜けを確かめる その2 »

2022年3月16日 (水)

Arduino-UNO 割り込み処理のミスあれこれ:「cnt+n; n=0; 」での抜けを確かめる

Arduino-UNO 割り込み処理のミスあれこれ:ロータリーエンコーダー
の中で示した、重大なバグにつながる落とし穴の手順
~~~~~~~~~~~~~~~~~~~~~~~~~~
 if(m_nValue != 0){      ←(1) up/downパルスあり?
   R_count = R_count + m_nValue; ←(2)カウント値を+/-
   m_nValue = 0;      ←(3)up/downパルスをゼロに
 }
  もし、(2)と(3)の間にエンコーダカウント割り込みが入って
  m_nValueの値が+/-されたとすると・・・
  (3)でクリアされてしまって、パルス抜けが発生します。
  (2)と(3)は割り込み禁止状態で実行しなければなりません。
  short,、longデータを扱える16bit、32bitマイコンでも
  発生します。
~~~~~~~~~~~~~~~~~~~~~~~~~~  

この「パルス抜け」が発生する様子を確かめるスケッチを
書いてみました。
  ・・・ちょっと長いですが

/*****  割り込み処理ミスの検証     *****/
// cnt + n; n = 0; 処理間での割り込みとの競合をチェック
// 【1】に割り込みが入ると「n2」がミスする
// PD2 パルス入力 ↓エッジ
// PB1 10kHz方形波出力
// PB3 125Hz方形波出力
// 1秒サイクルでカウント値を出力
// CR入力でクリア
// cnt_up, cnt_add1は割り込み禁止で処理
// cnt_add2を割り込み有効のままで計数
/***** I/O MACRO *****/
#define PB0_H (PORTB |= (1 << PB0)) // (!!!)PB0 H/L
#define PB0_L (PORTB &= ~(1 << PB0))
#define PB2_H (PORTB |= (1 << PB2)) // (!!!)PB2 H/L
#define PB2_L (PORTB &= ~(1 << PB2))
#define PB5_H (PORTB |= (1 << PB5)) // (!!!)PB5 H/L
#define PB5_L (PORTB &= ~(1 << PB5))
/***** データ *****/
// 加算データ
volatile long cnt_up; // 割込み内でカウントアップ
volatile int8_t cnt_n1; // 同じだが1バイトでカウントアップ
volatile int8_t cnt_n2; // 割り込み禁止なし用
long cnt_add1; // loop内でcnt_n1,n2を加算
long cnt_add2; // n1は割り込み禁止で
// n2は割り込み有効のままで処理
// タイマーデータ
volatile byte tm_4ms; // 4msタイマー割り込みでダウンカウント
// 250で1秒
// シリアル出力バッファ
char tx_bff[64]; // sprintfで出力文字を設定
/***** INT0(PD2) ↓エッジパルス割り込み *****/
ISR(INT0_vect){
PB0_H; // (!!!)
if(cnt_up < 99999999) cnt_up++; // 8桁maxで+1
cnt_n1++; // loop内でcnt_add1に加算
cnt_n2++; // cnt_add2に加算
PB0_L; // (!!!)
}
/***** タイマー2コンペアマッチA割込み *****/
// 250Hz 4mサイクル
ISR(TIMER2_COMPA_vect){
if(tm_4ms) tm_4ms--; // 4msタイマーダウンカウント
}
/***** カウント値シリアル出力 *****/
// cnt_up, add1, add2 ,差 の4データを出力
// 割り込み禁止でカウント値をコピー
void txcnt(void){
long d0, d1, d2;
PB5_H; // (!!!)
cli(); // ★いったん割込禁止に
d0 = cnt_up; // カウント値longデータ
cnt_add1 += (long)cnt_n1; // n1の残値をadd1に加算
cnt_n1 = 0; // 次に備えてゼロクリア
cnt_add2 += (long)cnt_n2; // n2をadd2に加算
cnt_n2 = 0; // ゼロクリア
d1 = cnt_add1; // 割込禁止でのカウントアップ
d2 = cnt_add2; // 割込禁止しないで
sei(); // ★割込再開
sprintf_P(tx_bff, PSTR("%8ld %8ld %8ld %8ld"), // 8桁で表示
d0, d1, d2, d1 - d2);
Serial.println(tx_bff); // 1行シリアル出力
PB5_L; // (!!!)
}
/***** セットアップ *****/
// ATmega328Pのレジスタを直接制御
void setup(){
cli(); // 割込禁止
// I/Oイニシャル
PORTB = 0b00000000; // data/pull up
DDRB = 0b00111111; // port指定
// |||||+---- PB0 IO8 out TEST 割り込みでパルス
// ||||+----- PB1 IO9 out OC1A出力 10kHz
// |||+------ PB2 IO10 out TEST カウントでパルス
// ||+------- PB3 IO11 out OC2A出力 125Hz
// |+-------- PB4 IO12 out
// +--------- PB5 IO13 out (LED) シリアル送信タイミング
PORTD = 0b00000111;
DDRD = 0b11111010;
// |||||||+---- PD0 RXD in
// ||||||+----- PD1 TXD out
// |||||+------ PD2 IO2 in INT0↓エッジをカウント
// ||||+------- PD3 IO3 out
// |||+-------- PD4 IO4 out
// ||+--------- PD5 IO5 out
// |+---------- PD6 IO6 out
// +----------- PD7 IO7 out
// INT0 ↓エッジ割り込み (PD2)
EICRA = 0b00000010; // 外部割り込み
// ||++---- ISC0 INT0 立ち下がりエッジ
// ++------ ISC1 INT1 Lレベル
EIMSK = 0b00000001;
// |+---- INT0 割り込み有効
// +----- INT1 割り込み未使用
// タイマー1 PB1に10kHz方形波出力
TCCR1A = 0b01000000;
// |||| ++---- WGM11,10 CTCモード
// ||++-------- COM1B
// ++---------- COM1A (PB1トグル出力)
TCCR1B = 0b00001001;
// || ||+++---- CS 1/1=16MHz
// || ++------- WGM13,12 CTC
// |+---------- ICES1
// +----------- ICNC1
TIMSK1 = 0b00000000; // 割り込み
// | ||+--- TOIE1
// | |+---- OCIE1A
// | +----- OCIE1B
// +-------- ICIE1
OCR1A = 800 - 1; // 20kHz PB1は1/2で10kHz
// タイマー2:4ms割り込みとPB3に125Hz方形波出力
TCCR2A = 0b01000010; // モード設定
// |||| ++--- WGM:CTC OCR2AがTOP値
// ||++------- COM2B (出力しない)
// ++--------- COM2A (PB3 トグル出力)
TCCR2B = 0b00000110;
// |+++--- CS:16MHz/256 16us
// +------ WGM02
TIMSK2 = 0b00000010;
// ||+---- TOIE2 オーバーフロー割り込み
// |+----- OCIE2A コンペアマッチA割り込み 有効
// +------ OCIE2B コンペアマッチB割り込み
OCR2A = 250 - 1; // 16us * 250 = 4ms
// シリアル
Serial.begin(9600);
sei(); // 割込許可
}
/***** LOOP *****/
// 【1】の間に割り込が入るとカウント読み出し値に抜けが発生
void loop()
{
char c;
sprintf_P(tx_bff, PSTR("%8s %8s %8s %8s"), // 8桁で表示
"cnt_up", "add1", "add2", "add1-2");
Serial.println(tx_bff); // 1行シリアル出力
while(1){
if(cnt_n1){ // n1割り込みあった
cli(); // ★割り込み禁止して
cnt_add1 += (long)cnt_n1; // n1をadd1に加算
cnt_n1 = 0; // 次に備えてゼロクリア
sei(); // ★割り込み有効に戻す
if(cnt_add1 > 99999999) cnt_add1 = 99999999; // max規制
}
if(cnt_n2){ // n2割り込みあった
PB2_H; // (!!!)
cnt_add2 += (long)cnt_n2; // ★1割り込み許可のまま加算
// 【1】この間に割り込みがあると
cnt_n2 = 0; // このゼロクリアで抜けが発生
PB2_L; // (!!!)
if(cnt_add2 > 99999999) cnt_add2 = 99999999; // max規制
}
// 1秒ごとにカウント値をシリアル出力
if(tm_4ms == 0){ // 1秒経過
tm_4ms = 250; // 1秒プリセット
txcnt(); // カウント値シリアル出力
}
// CR受信でカウント値をクリア
if(Serial.available()){ // 受信データあり
c = Serial.read(); // 1文字読み出し
if(c == '\r'){ // CR ?
cli(); // ★いったん割込禁止に
cnt_up = 0; // カウント値クリア
cnt_n1 = 0;
cnt_n2 = 0;
cnt_add1 = 0;
cnt_add2 = 0;
tm_4ms = 0; // タイマークリア
sei(); // ★割込再開
}
}
}
}

INT0入力で↓エッジのパルス割り込み
  long値cnt_upと1バイト値cnt_n1とcnt_n2を+1。
・1秒ごとに、4つの数値をシリアル出力します。

~~~~~~~~~~~~~~~~~~~~~~
【125Hz】
 cnt_up   add1   add2  add1-2
    0    0    0    0
   125   125   125    0
   250   250   250    0
   375   375   375    0
   500   500   500    0
   :

【10kHz】
 cnt_up   add1   add2  add1-2
    5    5    5    0
  10000  10000  10000    0
  20000  20000  20000    0
  30000  30000  30000    0
  40000  40000  40000    0
   :
~~~~~~~~~~~~~~~~~~~~~~

「cnt_up」はINT0割り込みでカウントアップするlong値。
「add1」と「add2」は、INT0割り込みでカウントアップする
1バイト値を、メインループ内で加算して得られるlong値。
加算処理時、add1は割り込み禁止で加算
add2は割り込み有効のまま加算
このため、add2ではパルス抜けが生じるかも、となります。
「add1-2」はadd1とadd2の差です。

ちゃんと処理してるcnt_upとadd1は同じ値になりますが、
割り込み禁止にしていないadd2はパルス抜けが生じて、
その差「add1-2」の値がどんどん大きくなっていきます。

リストの【1】部分がそれ。
ここに割り込みが入るとパルス抜けが発生します。
  ★の所で割り込み禁止と再許可

ハード的には、Arduino-UNOをこんな具合に。
A1_20220316112801
PB1に10kHzの方形波、PB3に125Hzの方形波を出しているので、
これを使ってパルス入力します。

125Hzも10kHzも、ふつうにつないだだけでは割り込み処理が
十分に追いついているので安定してカウントが進みます。
カウント値のシリアル出力やシリアル出力割り込み、タイマー
割り込みなどで、メインループでのカウント処理が遅れること
はありますが、カウント値のミスは出てきません。

ところが・・・
INT0入力クリップを離したり付けたりして
チャタリングを発生させると、一発でアウトに。

チャタリングがあってもcnt_upとadd1は同じ値を維持
していますが、add2がパルス抜けを起こして、
差の値「add1-2」がどんどん大きくなってきます。

~~~~~~~~~~~~~~~~~~~~~~
【チャタリングあり】
 cnt_up   add1   add2  add1-2
    0    0    0    0
  1397   1397   1384    13
  2805   2805   2781    24
  4209   4209   4169    40
  5616   5616   5562    54
   :
~~~~~~~~~~~~~~~~~~~~~~

オシロで見ると、パルス抜けのタイミングはこんなふうになっ
ています。
   ランダムなチャタリングでなく、見やすいような
   パルスにしています。
A2_20220316121301

2発目のパルスエッジ、割込は応答してちゃんとカウント
していますがメインループでの加算処理と重なってしまい、
「cnt_n2」がゼロクリアされ、結果的にパルス抜けが
生じています。

cnt_upとadd1が一致しているということで、
割り込みを禁止してのあれこれ処理、重要なのがお分かり
いただけたかと。。。


※チャタリングへの応答
ランダムなチャタリングパルスへの応答、パルス全部を捉えられる
わけではありません。
そこは割り込み処理の応答速度です。


※補足
Arduinoでの割り込み禁止と割り込み許可は、
noInterrupts()」と「interrupts()」を使えという
のが標準です。 が・・・
「Arduino.h」を見ると、こんな記述が
  #define interrupts()  sei()
  #define noInterrupts() cli()
「sei」と「cli」はAVRマイコンでのアセンブラ・ニーモニック。
Arduino-UNOのATmega328Pを制御するんであれば・・・
使ってヨシ!。 ということで。

※補足
テスト用パルスを出している
  #define PB0_H (PORTB |= (1 << PB0))
  #define PB0_L (PORTB &= ~(1 << PB0))
この記述で、ポートに対するビット操作命令を生んでくれます。
ポートをダイレクトにH/LするSBI」「CBI」命令に置き換わるので、
最高速です。
割り込み処理確認に出しているテストパルスに「digitalWrite()」
なんて、まどろっこしくて使えません。
Arduino-UNOでtone()の挙動を調べる
toneパルスの異常 もうちょっと掘り下げて
digitalWriteの秘密をもうちょっと

※補足  これ↓、何度も言っておきます。
Arduinoのタイマー OCRレジスタは「n」じゃなく「n - 1」の値を設定せよ

※関連
ロータリーエンコーダーの2相パルスをピン変化割り込みで取り込む
Arduinoでロータリーエンコーダーを使う:ラジオペンチ
ピンチェンジ割込みを使ってロータリーエンコーダーを読む (Arduino):ラジオペンチ

※続き
Arduino-UNO 割り込み処理のミスあれこれ:パルス計数の抜けを確かめる その2

|

« Amazonベーシック単3(2000mAh) 60%(72分)放電 1200回で終了 | トップページ | Arduino-UNO 割り込み処理のミスあれこれ:パルス計数の抜けを確かめる その2 »

Arduino」カテゴリの記事

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

割り込み処理」カテゴリの記事

コメント

コメントを書く



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




« Amazonベーシック単3(2000mAh) 60%(72分)放電 1200回で終了 | トップページ | Arduino-UNO 割り込み処理のミスあれこれ:パルス計数の抜けを確かめる その2 »