Arduino-UNO 割り込み処理のミスあれこれ:「cnt+n; n=0; 」での抜けを確かめる
この続きです。
こんな回路にして、ボリュームで試験用パルス出力の
L時間/H時間を変えられるようにして「パルス抜け」を
Arduino自身で確認できるようにしました。
Arduino-UNO基板に10kΩのボリュームを3つ
付けたサブ基板を装着します。
小基板に乗せて、直接挿入。
そして、テストプログラムのINT0パルス割り込みを
↓↑両エッジに変更します。
L側の時間を短くしたり長くすると、入力パルスに
対する割り込み応答の様子が見えてきます。
まず、最短クロック幅での割込応答。
両エッジモードのままでも、ちゃんと割り込みがかかりました。
(両エッジそれぞれに対する応答は無理ですが)
両エッジを捉えてくれる最小パルス幅がこんな感じ。

「割り込み禁止にしてない」のが理由で、2発目のパルスを
落としている様子です。
(1)まず、正常な場合。
(2)スカタンすると・・・

(3)忘れたころに「抜け」が出る様子です。

この「メイン処理でパルス数を積算する」方式では、
読み出し時に「抜け」が発生すると、それがどんどん
積もっていきます。
※基本、1発でも抜けがあるとアウト!!。
パルスに対する割り込み応答はハードウェアの
制限ですが、このパルス抜けはソフトウェアの
ミス(スカタン)ですんで・・・
新しいスケッチです。 ・・・ずいぶん長いけど
/***** 割り込み処理ミスの検証 *****/
// "intr_miss2.ino" 2022-03-18 / JH3DBO
// cnt + n; n = 0; 処理間での割り込みとの競合をチェック
// 【1】に割り込みが入ると「n2」がミスする
// PD2 パルス入力 ↓↑両エッジ
// PB1 PWM周期方形波出力
// PB2 PWM出力
// PB3 125Hz方形波出力
// 1秒サイクルでカウント値を出力
// CR入力でクリア
// cnt_up, cnt_add1は割り込み禁止で処理
// cnt_add2を割り込み有効のままで計数
// VR1,VR2でL幅可変 VR3でH幅可変
// VR1は1clk単位で微調
/***** 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; // 割込み内でカウントアップ
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; // 1msタイマー割り込みでダウンカウント
// 250で1秒
// シリアル出力バッファ
char tx_bff[64]; // sprintfで出力文字を設定
/***** INT0(PD2) ↓↑エッジパルス割り込み *****/
// cnt_upはlongデータ n1,n2は1バイト
// メイン処理でn1,n2をadd_n1,n2に加算する時に違いが出る
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割込み *****/
// 1kHz 1msサイクル
ISR(TIMER2_COMPA_vect)
{
static byte cnt4 = 0; // 4msカウント
cnt4++;
if(cnt4 >= 4){
cnt4 = 0;
if(tm_4ms) tm_4ms--; // 4msタイマーダウンカウント
}
ADCSRA |= (1 << ADSC); // A/D変換開始
}
/**** A/D データ *****/
// A/D変換チャンネル (ADMUX)
const byte ad_mpx[] PROGMEM ={
0b01000000, // ch 0 VR1
0b01000001, // ch 1 VR2
0b01000010, // ch 2 VR3
}; // ||| ++++---- MPX 0,1
// ||+--------- ADLAR 右そろえ
// ++---------- REFS1,0 AVCC接続
#define AD_SU sizeof(ad_mpx) // A/D ch数
// AD ch0,1,2 : VRデータ
volatile word ad_avr[AD_SU]; // A/D変換データ 0~1023
// 平均処理された結果
volatile word ad_add[AD_SU]; // A/D平均処理用加算データ
volatile byte f_adok; // A/D変換完了フラグ
// 192msごと
volatile byte adok_cnt; // A/D変換完了回数
// max255で+1
/***** A/D割り込み処理 *****/
// タイマー割り込みでA/D変換開始
// 1msに1回, 3msで3ch, 64回(192ms)で平均値算出
// 10bitを64回なら16bit内に入る
ISR(ADC_vect)
{
word d;
static byte ch = 0; // A/D変換チャンネル
static byte cnt = 0; // A/D変換平均回数
d = (word)ADCL; // A/Dデータ
d |= (ADCH & 0x03) << 8; // 符号なしで 0~3FF
// 測定値
ad_add[ch] += d; // 平均用に加算
// 次変換チャンネル
ch++; // ch0,1,2繰り返し
if(ch >= AD_SU) ch = 0;
ADMUX = pgm_read_byte(&ad_mpx[ch]); // MPX切り替え
// 平均加算回数
if(ch == 0){
cnt++; // 変換回数+1
if(cnt >= 64){ // 64回?
cnt = 0;
ad_avr[0] = ad_add[0] / 64; // ch0 平均値 10bit
ad_avr[1] = ad_add[1] / 64; // ch1
ad_avr[2] = ad_add[2] / 64; // ch2
ad_add[0] = 0; // 次加算データクリア
ad_add[1] = 0;
ad_add[2] = 0;
f_adok = 1; // 変換完了フラグon
if(adok_cnt < 255) adok_cnt++; // 変換回数+1
}
}
}
/***** 2つのshort値の差を求める *****/
short sdiff(short a, short b)
{
short d;
d = a - b; // ±差
if(d < 0) d = -d; // 絶対値で
return d;
}
/***** PWM出力設定 *****/
// A/D変換タイミングでタイマー1のPWMを設定
// 変動するので8bitのA/D値で設定
// VR1 L幅微調 16MHzクロックで 1~256clk : 62.5ns~16us
// VR2 L幅 5usクロック 0~1275us
// VR3 H幅 8usクロック 0~2040us + 50us (VR1~3足して16bit越えないよう)
void pwm1set(void)
{
static byte f1 = 0; // 初めてフラグ
static word d[AD_SU]; // A/D変化チェック用 前回値
word a[AD_SU]; // A/D値一時データ
if(f_adok){ // 変換完了?
f_adok = 0; // 次を待つ
cli(); // 割込禁止でコピー
a[0] = ad_avr[0]; // 10bit A/D値を保存
a[1] = ad_avr[1];
a[2] = ad_avr[2];
sei(); // 割込許可
if(f1 == 0){ // はじめて?
d[0] = a[0]; // チェックデータを保存
d[1] = a[1];
d[2] = a[2];
}
// 変化があればPWM値を設定 2以上の変動で変化ありと判断
if((sdiff(d[0], a[0]) >= 2) || // どちらかA/D値変化あり?
(sdiff(d[1], a[1]) >= 2) || // 3つのうちどれか
(sdiff(d[2], a[2]) >= 2) ||
(f1 == 0)){ // はじめてか?
f1 = 1; // はじめて処理済みに
d[0] = a[0]; // 新値をコピー
d[1] = a[1]; // 有効桁は10bit
d[2] = a[2];
// A/D値は8bit(0~255)で設定
a[0] = (d[0] / 4); // VR1 62.5ns
a[1] = (d[1] / 4) * 80; // VR2 5us
a[2] = ((d[2] / 4) * 128) + 799; // VR3 8us + 50us
OCR1B = a[0] + a[1]; // PWM L区間
OCR1A = a[0] + a[1] + a[2]; // PWM周期
}
}
}
/***** カウント値シリアル出力 *****/
// cnt_up, add1, add2 ,差 の4データを出力
// 割り込み禁止でカウント値をコピー
void txcnt(void)
{
long d0, d1, d2;
word a0, a1;
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.print(tx_bff); // シリアル出力
sprintf_P(tx_bff, PSTR(" %4dus %dus"), // L,Hパルス幅
(OCR1B + 1) / 16, // PWM L区間
(OCR1A - OCR1B + 1) / 16); // PWM H区間
Serial.println(tx_bff); // シリアル出力
PB5_L; // (!!!)
}
/***** カウント値クリア *****/
void clrcnt(void)
{
cli(); // いったん割込禁止に
cnt_up = 0; // カウント値クリア
cnt_n1 = 0;
cnt_n2 = 0;
cnt_add1 = 0;
cnt_add2 = 0;
sei(); // 割込再開
}
/***** セットアップ *****/
// ATmega328Pのレジスタを直接制御
void setup()
{
cli(); // 割込禁止
// I/Oイニシャル
PORTB = 0b00000000; // data/pull up
DDRB = 0b00111111; // portI/O
// |||||+---- PB0 IO8 out TEST 割り込みでパルス
// ||||+----- PB1 IO9 out OC1A出力 PWM周期でトグルする方形波
// |||+------ PB2 IO10 out OC1B出力 PWM出力
// ||+------- PB3 IO11 out OC2A出力 500Hz方形波
// |+-------- PB4 IO12 out TEST カウントでパルス
// +--------- PB5 IO13 out (LED) シリアル送信タイミング
PORTC = 0b00000000; // data/pull up
DDRC = 0b00111000; // portI/O
// |||||+---- PC0 AD0 in VR1入力
// ||||+----- PC1 AD1 in VR2入力
// |||+------ PC2 AD2 out VR3入力
// ||+------- PC3 AD3 out
// |+-------- PC4 AD4 out
// +--------- PC5 AD5 out
PORTD = 0b00000111; // data/pull up
DDRD = 0b11111010; // portI/O
// |||||||+---- 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 = 0b00000001; // 外部割り込み
// ||++---- ISC0 INT0 ↓↑両エッジで
// ++------ ISC1 INT1 Lレベル
EIMSK = 0b00000001;
// |+---- INT0 割り込み有効:アップカウント実行
// +----- INT1 割り込み未使用
// タイマー1 : PWM出力
TCCR1A = 0b01110011;
// |||| ++---- WGM11,10 PWM TOP=OCR1A
// ||++-------- COM1B PB2 PWM反転出力
// ++---------- COM1A PB1トグル出力
TCCR1B = 0b00011001;
// || ||+++---- CS 1/1=16MHz
// || ++------- WGM13,12 PWM
// |+---------- ICES1
// +----------- ICNC1
TIMSK1 = 0b00000000; // 割り込み 使わない
// | ||+--- TOIE1
// | |+---- OCIE1A
// | +----- OCIE1B
// +-------- ICIE1
OCR1A = 1023 + 4000; // PWM周期
OCR1B = 1023; // L区間
// タイマー2 : 1ms割り込み
TCCR2A = 0b01000010; // モード設定
// |||| ++--- WGM:CTC OCR2AがTOP値
// ||++------- COM2B 出力しない
// ++--------- COM2A PB3 トグル出力 500Hz
TCCR2B = 0b00000101;
// |+++--- CS:16MHz/128 8us
// +------ WGM02
TIMSK2 = 0b00000010;
// ||+---- TOIE2 オーバーフロー割り込み
// |+----- OCIE2A コンペアマッチA割り込み 有効
// +------ OCIE2B コンペアマッチB割り込み
OCR2A = 125 - 1; // 8us * 125 = 1ms
// A/D変換 割り込みでデータを受ける
ADMUX = 0b01000000; // 内蔵A/D
// ||| ++++---- ch0 VR1
// ||+--------- ADLAR
// ++---------- AVCC接続
ADCSRA = 0b10001111;
// |||||+++--- ADPS 16MHz/128=125kHz
// ||||+------ ADIE A/D割込有効
// |||+------- ADIF 変換完了フラグ
// ||+-------- ADATE A/D自動起動
// |+--------- ADSC 変換開始
// +---------- ADEN A/D有効
DIDR0 = 0b00000111; // デジタル入力禁止
// |||+++--- A/D ch2,1,0 A/Dに
// +++------ A/D ch5~3 I/Oポートに
// シリアル
Serial.begin(9600);
sei(); // 割込許可
}
/***** LOOP *****/
// 【1】の間に割り込が入るとカウント読み出し値に抜けが発生
void loop()
{
char c;
sprintf_P(tx_bff, PSTR("%8s %8s %8s %8s %6s %6s"), // データ区分
"cnt_up", "add1", "add2", "1-2", "PWM-L", "PWM-H");
Serial.println(tx_bff); // 1行シリアル出力
adok_cnt = 0; // A/D変換回数
while(adok_cnt < 5){ // 5回 0.96秒
pwm1set(); // A/D値でPWM出力
}
clrcnt(); // カウント値クリア
// 実行loop n1,n2カウントアップ処理
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割り込みあった
PB4_H; // (!!!)
cnt_add2 += (long)cnt_n2; // ★1割り込み許可のまま加算
// 【1】この間に割り込みがあると
cnt_n2 = 0; // このゼロクリアで抜けが発生
PB4_L; // (!!!)
if(cnt_add2 > 99999999) cnt_add2 = 99999999; // max規制
}
// 1秒ごとにカウント値をシリアル出力
if(tm_4ms == 0){ // 1秒経過
tm_4ms = 250; // 1秒プリセット
txcnt(); // カウント値シリアル出力
}
// A/D値でPWM出力を設定
pwm1set(); // 256msサイクルで
// CR受信でカウント値をクリア
if(Serial.available()){ // 受信データあり
c = Serial.read(); // 1文字読み出し
if(c == '\r'){ // CR ?
clrcnt(); // カウント値クリア(割込禁止で)
tm_4ms = 0; // タイマークリア
}
}
}
}
・前のと同じで、★1のところ、【1】に割り込みが入ったら
パルス抜けが生じます。
だから、ちゃんと割り込み禁止にして処理しなくちゃいけません。
※参:パルス抜けが生じるかも
・ピンチェンジ割込みを使ってロータリーエンコーダーを読む (Arduino)
・ロータリーエンコーダテスト 割り込みを使う場合
・タイマー1でPWMパルスを出力。
TOP値(全体の周期)はOCR1Aで。
OCR1Aを使うと、周期を変えた時にギクシャクしません。
TOP値をICR1でセットできるモードもあるのですが、
これは固定周期用になります。
OCR1Aが2重バッファになっているので、更新時、
TCNT1より小さくなっても、一週回りの0xFFFFを待つ
ということがありません。
ICR1を使うと、この一周回りが起こってしまい、ギクシャク
するのです。
・このPWM波のL区間とH区間のパルス幅をボリュームで設定します。
VR1でL区間のパルス幅を微調するようにして、1clk単位で設定
できます。
・3つのVRの読み込みは1msタイマー割り込みでA/Dを起動。
呼ぶたびに待たされるanalogRead()はキライです。
64回加算して平均値を算出。
これも割り込み内で処理しちゃうんで、メイン側は勝手に
出てくる平均値を読み出すだけ。
ただし、割り込み禁止にして。
・シリアル出力の様子
■徐々にLパルス幅を短く
cnt_up add1 add2 1-2 PWM-L PWM-H
0 0 0 0 300us 2090us
836 836 836 0 300us 2090us
1674 1674 1674 0 285us 2090us
2524 2524 2524 0 235us 2090us
3398 3398 3398 0 175us 2090us
4286 4286 4286 0 135us 2090us
5196 5196 5196 0 80us 2090us
6124 6124 6124 0 65us 2090us
7054 7054 7054 0 50us 2090us
7990 7990 7990 0 45us 2090us
8930 8930 8930 0 30us 2090us
9876 9876 9876 0 20us 2090us
10824 10824 10821 3 15us 2090us
11774 11774 11767 7 15us 2090us
12724 12724 12709 15 15us 2090us
13674 13674 13653 21 15us 2090us
14624 14624 14599 25 15us 2090us
15574 15574 15538 36 14us 2090us
16526 16526 16475 51 13us 2090us
17476 17476 17353 123 12us 2090us
18428 18428 18056 372 11us 2090us
19380 19380 18755 625 10us 2090us
20332 20332 19453 879 10us 2090us
↑ ↑ ↑ ↑
+-同じ-+ パルス抜け 差
cnt_up : longで加算
add1 : n1を割り込み禁止で加算
cnt_upとadd1は同じ値
add2 : n2を割り込み許可のまま加算
競合するとパルス抜けが生じる
※続き 「読み」と「書き」に分けて検証
・Arduino-UNO 割り込み処理のミスあれこれ:割り込み禁止にして読まないとダメよ
・Arduino-UNO 割り込み処理のミスあれこれ:割り込み禁止にして書かないとダメよ
↑
この2つは8bitマイコン特有の注意点
最近のコメント