割り込み処理

2022年10月14日 (金)

8bitマイコンにも16bitのメモリ読み書き命令があった

8080のニーモニックでは
  LHLD nn と SHLD nn

Z-80なら
  LD HL,(nn)  LD (nn),HL

と、HLレジスタ(8bitレジスタのHとLを合わせて
16bitレジスタとする)を使っての16bitのメモリ
読み書き命令がありました。

割り込みで操作されるワード値を、読むだけ
あるいは書くだけなら、割り込み禁止操作が
不要だったのです。
  ※読んで処理して書き戻すときは
   当然アトミック操作が必要。

Z-80ではHLレジスタだけでなくBC、DEレジスタ
とIX、IYにもこの操作ができました。

タイマーなどのダウンカウンタのゼロチェック
(タイムアップチェック)が割り込み禁止にしなくても
できたのです。

この16bit値の直接読み書き機能、8bitマイコンでは
なかなかありがたかった。

6301にも LDD STD LDX STX命令があって、便利に
使って(アセンブラで)いました。
  6301はA,Bレジスタをペアにして。
  ただ、インデックスレジスタが1個というのがねぇ。
  メモリーの転送のように、あっちをこっちへという
  操作が面倒。

| | コメント (0)

2022年10月11日 (火)

何度も言うぞ! Arduino(8bitマイコン)の割り込みには気をつけろ!

過去、何度も言ってます。
8bitマイコンの割り込み処理で多バイトデータを扱う時は
アトミック操作」を!

  ※1バイトのデータならokということではなく、
   マイコンがどんな動きをするのかを想像して
   (マイコンになりきって)手順を考えなくてはなり
   ません。
   割り込み処理なら別の動きを考えるためにもう
   一つ頭がいるかも。

今月のトランジスタ技術2022年11月号

この「倒立振子 立つ」(著者:藤森 嵩人さん) の記事でも
手を抜かれて(cli、seiしていない)ます。

p.188~p.189のリスト。
volatile」が頭に付いた変数に注目。

  割り込みで読み書き。
  メインループでも読み書き。
  メイン側で「読む」だけでなく、処理したデータを
  「書いて」それを割り込み内で使っています。

  元はintのエンコーダ入力値。
  それを加工してあれこれのfloatデータに。
  モータ制御はMstimer2で駆動される1ms周期の
  タイマー割り込み内。

面倒でも、メイン側 ←→ 割り込み処理 を受け渡しする
データは「割り込み禁止・割り込み許可」を挟まなくては
なりません。

処理全体を割り込み禁止にするのではなく、受け渡しの
データだけで良いのです。

  cli、sei命令の動作は最短の1クロック。
  やりとりのための余分なデータ領域が増えますが、
  実行時間が遅くなることはありません。

  計算には一時的なデータを使って、それを本番データに
  コピーするだけ。 その時、割り込み禁止で。
  読み出すときも、割り込み禁止で本番データを一時データ
  にコピー。
  ただ、それだけ。

何度も言います。
  起きる可能性があるなら、いつかはアタリます。
  それがバグというものです。

最初からちゃんと考えておかないと、
  「忘れた頃におかしくなる」が発生します。


※この件、トラ技編集部に伝えています。

※公開されているダウンロードサービスでは
本誌に掲載されたのとは異なったスケッチ
(ちょっと手直し)が出てきます。
しかし、まだ不完全です。
面倒がらずに「cli、sei」の手順をお願い
したいところです。
「とりあえず動いているから」とか
「ちょっとした実験だから」という性質のもの
ではありません。

ネットに上がっているミス含有スケッチ、
「初心者だから許される」は言語道断。


※新たに「割り込み処理」というカテゴリを設けました。
これで、過去記事を追跡してもらえるかと。

ターゲットに「8bitマイコンを選んでしまった・・・」と
いうのが、今回のスカタンの根本原因じゃないでしょか。
32bitマイコンなら、現プログラムのままでOKかと思うのです。
電源やらエンコーダ入力割り込み処理の手軽さから
Arduino nanoを選ばれたんじゃないかと推測します・・・
8bitマイコンを使いこなすにはそれなりの覚悟が必須という
ことで・・・

「どれだけ痛い目にあったか」の経験値がスカタン・バグの
予防になるのかと。

簡単なワクチンはありません。
自分で痛い目にあって「抗体」を作るしか、バグから逃れる術は
ありません。

| | コメント (6)

2022年9月11日 (日)

ロータリーエンコーダーのチャタリング波形

2022年8月24日:パルスジェネレータを作ってみた:箱に入れた
このスケッチ、
   ・ダウンロード - p_gen16c1.txt
では、ロータリーエンコーダの計数をINT0割り込みではなく
タイマー割り込みを使った周期的処理でチャタリングを
除去してA相の↓エッジを検出するようにしました。

そして、CRによるフィルタが不要になったので、エンコーダ接点に
入れたコンデンサを取り外してしまいました。

※タイマー処理によるチャタリング除去の考え方は
ここを参照。
  ・2020年9月15日:今度はチャタリング除去、その考え方
  ・2020年9月16日:ロータリーエンコーダーの2相パルスをタイマー割り込みで

コンデンサ無しだと、どんなチャタリングが出るのか、
オシロで観察しました。
デジタルオシロの無限残光モードが役立ちます。
こんなパルスが入ってきます。

Aa000

Aa001

0.5ms(2kHz)ごとにそのH/Lを記録して、HあるいはLが
4回以上連続してたらその信号は安定。
途中で変化があればチャタリングと判断します。
ですので、高速2相パルスは検出できません。
「クリック有り」ロータリーエンコーダ向けの
処理です。

ch3の波形が、A相信号のチャタリングを除去後、
A相↓エッジを検出して、計数処理をしているタイミングです。
その後に、新カウント値による液晶表示が始まっています。
周期や周波数データの液晶表示に10ms近くかかるので
loop()内でのA/B相処理は間に合いません。
割り込みでしか追いつかないのです。

けっこうなチャタリングが見えています。
全部のon/offでこれが発生するのではなく、たまにひどいのが出る
という感覚です。
そして、その場所(回転位置)は一定ではありません。
どこで出るかわからんなぁ~です。


| | コメント (0)

2022年9月 1日 (木)

8ビットマイコンの割り込み処理・・・言い足りないぞ

トランジスタ技術2018年5月号を何気なく見ていたら・・・
PICマイコンの大御所「後閑 哲也」さんが、
割り込み処理に関し
  「ちょっとこれはあかんで」
という表現をされているのを見つけました。

トラ技の特集内容が
  「Python発C行き」 micro:bit&新PIC入門

Pj2

その中の第4章
  高速IoT開発!
  Cプログラミング・ダイエット(1)
  高効率コーディング  著者:後閑 哲也

その73ページ。
~~~~~~~~~~~~~~~~~~~~~~~~~~
 「割り込み処理関数とメイン関数の両方から
  アクセスされる変数はchar型にしておく

Pp1_20220901103201
~~~~~~~~~~~~~~~~~~~~~~~~~~

そうなんですが、注意点が根本的に違う~。
1バイトならokということではなく「アトミック処理」を
しなくちゃならないんです。

このブログで何度も言ってます。
  「たまにおかしくなる」、
  「見つけにくいバグを生む」。

データを1バイトにしても割り込み禁止にしてないと
ミスする例がこれ。
Arduino-UNO 割り込み処理のミスあれこれ:「cnt+n; n=0; 」での抜けを確かめる

データのバイト数に関係なく、割り込みとの競合が生じる
プログラムの組み方が存在するわけです。

※通常は
  0か1か、onかoffかのフラグ処理の1バイトなら
  割り込み禁止にしなくても大丈夫。

  1バイトのダウンカウントタイマー的な処理なら
  割り込み禁止にしなくても大丈夫。

  メイン側は割り込みが書いた1バイトのデータを
  読むだけ、あるいはその逆なら割り込み禁止に
  しなくても大丈夫。

読んで処理して書き戻す(ゼロクリアかも)処理の時に
割り込みとぶつかったらどうなるかということを考え
なければなりません。
1バイトのデータでも、割り込みとの競合を忘れては
いけません。

例では単純なカウントアップ処理でしたが、シリアル通信の
送受割り込み処理などだと、文字抜けが生じるかもしれません。

  ※シリアル割り込み、1バイトの読み書きポインタと
   1バイトの読み書きデータ数で処理すると
   アトミック処理は不要? そんなことはありません。

メモリーのデータだけじゃありません。
8bit単位でアクセスする出力ポートも、割り込みでポートを
操作していたら、メインからの操作と衝突するかもしれません。
メイン側でのアクセスは割込禁止状態でというのが基本。

  ※1bit単位でポートの操作できるマイコンがどんだけ
   ありがたいか。

スカタンなプログラムを書いたら、「たまにおかしくなる」とい
うのがやっかいなのです。

| | コメント (0)

2022年7月26日 (火)

割り込みと絡むクリチカルな制御 変数にはVolatileを!

魔法の言葉「Volatile」、これ大事です。
先日の Atmega328P タイマー/カウンタ1の高速PWM動作
このスケッチ、「IR_FRQ_SCAN3.ino」。
  ※たくさん空いている出力ポートにパルスを出して、
   動きをオシロでチェックできるようにしています。

大まかな処理の流れは、

・5msごとに、LED駆動周波数であるPWM周波数と
 デューティ比を計算して、変数に保存。
・タイマー1割り込み内(PWM周波数と同じ)でその値を
 タイマー1レジスタに書き込み。

これを繰り返し、22kHz~54kHzの周波数で赤外線LEDを
光らせています。

スケッチから関連部分をプックアップ。

【関連する変数】
メインルーチンでセットして割り込みでハードウェアを制御

volatile byte f_frqset; // 周波数設定フラグ
volatile word led_div; // LED駆動 分周比 ICR1に設定
volatile word led_duty; // 駆動デューティ PWM設定値 OCR1Aに設定

【関連する処理:loop内】

// 5msサイクルloop
while(1){
if(f_5ms){ // 5ms経過
f_5ms = 0;
// LED PWM周波数とデューティ
PD4_H; // (!!!)
if(led_frq == FRQ_LO){ // LO周波数のとき
PD2_H; // (!!!) パルスH/L
nop(); nop(); nop(); nop();
PD2_L; // (!!!)
}
a = (word)(160000L / (long)led_frq); // 分周比 (1)
b = (2 * a) / 3; // デューティ 1/3で (2)
cli(); // いったん割り込み禁止にして (3)
led_div = a; // LED駆動周分周比
led_duty = b; // デューティ比
f_frqset = 1; // 設定on タイマー1割り込みで処理
sei(); // 割込再開
// 周波数スキャン D/A PWM出力 220~540を0~320にして1/2
OCR2A = 255 - ((led_frq - FRQ_LO) / 2); // PWM 0~
PD4_L; // (!!!)
}

【割り込み処理】

ISR(TIMER1_OVF_vect) // タイマー1割り込み TOP値で
{
static byte cyc = 0; // LEDonサイクル
PD7_H; // (!!!)
switch(f_frqset){ // 実行区分
case 1: // PWM 設定指令
PD3_H; // (!!!)
ICR1 = led_div - 1; // LED周波数分周比 ★
OCR1A = led_duty - 1; // LED駆動デューティ 1/3 ★
cyc = 8; // 8サイクルだけLEDをon
f_frqset = 2;
PD3_L; // (!!!)
break;
case 2: // サイクル数をチェックしてオフに
if(cyc) cyc--;
if(cyc == 0){ // ダウンカウントしてゼロになった
PD3_H; // (!!!)
if(INP_SENS){ // センサー入力 H/L ?
SENS_H; // ポート出力でH保持
f_senshl = 1;
}
else{
SENS_L; // ポート出力でL保持
f_senshl = 0;
}
OCR1A = 0xFFFF; // LED駆動オフに
f_sensok = 1; // センサー状態確定
f_frqset = 0; // 次の起動設定を待つ
PD3_L; // (!!!)
}
break;
}
PD7_L; // (!!!)
}

(1) 5msごとに分周比=PWMの周波数とデューティ比を計算。
(2) 結果をいったん a と b に入れておいて。
(3) 割り込み禁止状態で変数に保存。

この部分の処理が、変数に付けた「volatileの有無」で変わって
しまうのです。

まずvolatileを付けたとき。 オシロでタイミングを見ます。
Vl09

PWMを処理しているタイマー1割り込み、この場合は
一定の周期で繰り返しています。

ところがvolatileを外すと・・・

Vl07

ch1波形、約65uほどのHレベル区間が「(1)(2)(3)」を
処理している時間です。
  ※ch2の1ms割り込みが5ms周期を知らせている。

ch3のタイマー1割り込み、新周期と新デューティ値を
変数に書いているのが「★★★」のタイミングです。
どういうわけか、割り込みが待たされてしまい、
処理の開始が遅れています。

これ、volatileを取ってしまったため、コンパイラが
  「いちいち仮変数の a と b に入れんでもエエやん」
  「直接 led_div と led_dutyに書いたろ」
  「ちょっとは早なるで
  「いちおう書き込み前に cli() はしとこ」
  「書き終わったら sei() ね」
と判断したせいです。

コンパイラの「ちょっとでも速よう実行できるほうがエエやろ」
というおせっかい。 (最適化の指示)
ただ、計算の前から割り込み禁止としたんで、時間のかかる除算
割り込み禁止の中に入ってしまい、処理が長くなってタイマー1割り込み
が待たされてしまいました。
  ※(2)の除算が割り込み禁止の中に入ってました。
   (1)は禁止前の実行でした。
割り込みが待たされるということは・・・
ICR1でPWM周波数を決めている方法では、ちょっと怖いこと
(PWM処理の一周抜け)が起こるかもしれません。

※関連
2018年10月11日:魔法の言葉「volatile」
2016年02月19日:Arduinoのタイマー処理

※追記
コンパイルされたソースファイルで比較してみると。

★volatile有

7b4: 20 91 21 01 lds r18, 0x0121 ▲led_frq
7b8: 30 91 22 01 lds r19, 0x0122
7bc: 40 e0 ldi r20, 0x00 ; 0
7be: 50 e0 ldi r21, 0x00 ; 0
7c0: c5 01 movw r24, r10
7c2: b4 01 movw r22, r8
7c4: 0e 94 94 07 call 0xf28 ; 0xf28 <__divmodsi4>
7c8: 29 01 movw r4, r18
7ca: 3a 01 movw r6, r20
7cc: c9 01 movw r24, r18
7ce: 88 0f add r24, r24
7d0: 99 1f adc r25, r25
7d2: b6 01 movw r22, r12
7d4: 0e 94 80 07 call 0xf00 ; 0xf00 <__udivmodhi4>
7d8: f8 94 cli
7da: 50 92 20 01 sts 0x0120, r5 ▲led_div
7de: 40 92 1f 01 sts 0x011F, r4
7e2: 70 93 1e 01 sts 0x011E, r23 ▲led_duty
7e6: 60 93 1d 01 sts 0x011D, r22
7ea: 30 92 1c 01 sts 0x011C, r3 ▲f_frq_set
7ee: 78 94 sei
★volatile無

7b2: 20 91 21 01 lds r18, 0x0121 ▲led_frq
7b6: 30 91 22 01 lds r19, 0x0122
7ba: 40 e0 ldi r20, 0x00 ; 0
7bc: 50 e0 ldi r21, 0x00 ; 0
7be: c5 01 movw r24, r10
7c0: b4 01 movw r22, r8
7c2: 0e 94 92 07 call 0xf24 ; 0xf24 <__divmodsi4>
7c6: f8 94 cli
7c8: 30 93 20 01 sts 0x0120, r19 ▲led_div
7cc: 20 93 1f 01 sts 0x011F, r18
7d0: c9 01 movw r24, r18
7d2: 88 0f add r24, r24
7d4: 99 1f adc r25, r25
7d6: b6 01 movw r22, r12
7d8: 0e 94 7e 07 call 0xefc ; 0xefc <__udivmodhi4> ←★
7dc: 70 93 1e 01 sts 0x011E, r23 ▲led_duty
7e0: 60 93 1d 01 sts 0x011D, r22
7e4: 70 92 1c 01 sts 0x011C, r7 ▲f_frq_set
7e8: 78 94 sei

「★volatile無」だと、「cli()割り込み禁止~sei()割り込み許可」内に
割り算が入っています。
「★volatile有」だと割り算は外に。
これが割り込み処理の遅れにつながっています。

| | コメント (0)

2022年4月12日 (火)

Arduino IDEでラズパイ・ピコ:1msタイマー割り込み

Arduino IDEでラズパイ・ピコ:Earle Philhower版
の導入で、Raspberry Pi Pico C/C++ SDKの関数がちゃんと
動き出したようです。

アラーム割り込みとPWM割り込みの二つで
1msタイマー割り込みを試してみました。
こんなスケッチ。

/*****  1msタイマー割り込みのテスト *****/
// タイマーとPWMで
// GP2 1msタイマー割込処理時間
// GP3 タイマー割込でトグル この周期が2.00598ms
// GP4 loopでトグル 割り込みでパルスが停止
// GP5 1kHz PWM出力
// GP6 PWM割込処理時間
// LED 1Hzで点滅
// millisによル送信データ間隔が1003ms
#include "hardware/pwm.h" // PWM処理に必要
#define nop() asm volatile ("nop\n\t") // nopコード
// ※参考
// https://raspberrypi.github.io/pico-sdk-doxygen/group__hardware__pwm.html
// https://github.com/raspberrypi/pico-examples/blob/master/pwm/led_fade/pwm_led_fade.c
// https://plaza.rakuten.co.jp/sorriman/diary/202103080000/

// データ
volatile word tm_1ms; // 1msダウンカウントタイマー
volatile byte f_1sec; // 1秒フラグ
word cnt_1sec; // 秒カウンタ
char tx_str[64]; // 送信文字列

/***** 1ms割り込み処理 *****/
struct repeating_timer st_tm1ms;
// 割り込み処理
bool tm1ms(struct repeating_timer *t)
{
static word cnt1000 = 0;
static byte f_x3 = 0;
gpio_put(2, HIGH); // GP2 H
f_x3 ^= 1;
gpio_put(3, f_x3); // GP3 トグル
if(tm_1ms) tm_1ms--;
cnt1000 ++;
if(cnt1000 >= 1000){
cnt1000 = 0;
f_1sec = 1;
}
gpio_put(2, LOW); // GP2 L
return true;
}

/***** 1kHz PWM割り込み *****/
word slice_num5; // GP5 PWMスライス番号
// 割り込み処理
void pwm1ms(void){
//static byte f_x6 = 0;
pwm_clear_irq(slice_num5);
gpio_put(6, HIGH); // GP6 H
nop(); nop(); nop(); nop(); // 時間待ち
nop(); nop(); nop(); nop();
gpio_put(6, LOW); // GP6 L
// gpio_put(6, f_x6); // GP6 トグル
// f_x6 ^=1;
}

/***** SETUP *****/
void setup() {
Serial1.begin(115200); // TX0
pinMode(2, OUTPUT); // GP2 1msタイマー割込時間
pinMode(3, OUTPUT); // GP3 1ms割り込みでトグル
pinMode(4, OUTPUT); // GP4 loopでトグル
pinMode(6, OUTPUT); // GP6 PWM割り込み
pinMode(25, OUTPUT); // LED
// GP5 PWM:1kHz 割り込み有効に
gpio_set_function(5, GPIO_FUNC_PWM); // GP5 PWMに
slice_num5 = pwm_gpio_to_slice_num(5); // PWMスライス番号
pwm_clear_irq(slice_num5);
pwm_set_irq_enabled(slice_num5, true);
irq_set_exclusive_handler(PWM_IRQ_WRAP, pwm1ms);
irq_set_enabled(PWM_IRQ_WRAP, true);
pwm_set_clkdiv(slice_num5, 100); // 1.25MHz
pwm_set_wrap(slice_num5, 1250-1); // 1kHz
pwm_set_chan_level(slice_num5, PWM_CHAN_B, 625); // duty50%
pwm_set_enabled(slice_num5, true); // PWMスタート
// 1msサイクルでタイマー割り込み
add_repeating_timer_us(1000, tm1ms, NULL, &st_tm1ms);
}

/***** LOOP *****/
void loop() {
byte f_x4 = 0;
byte f_x25 = 0;
uint32_t ms, t;
ms = millis();
while(1){
if(f_1sec){ // 1秒ごとにシリアル出力
f_1sec = 0;
cnt_1sec++;
if(cnt_1sec > 9999) cnt_1sec = 0;
t = millis() - ms;
ms = millis();
sprintf(tx_str, "%4dsec %4ldms",
cnt_1sec, t);
Serial1.println(tx_str);
}
f_x4 ^= 1; // loopでトグル
gpio_put(4, f_x4); // GP4 トグル
if(tm_1ms == 0){ // 0.5秒経過
tm_1ms = 500;
f_x25 ^= 1;
gpio_put(25, f_x25); // LED トグル
}
}
}

・シリアル出力はTX0に。
・実行の様子はポートにパルスとして出力。
・GP2とGP3がアラーム割り込みによるパルス。
 GP3が500Hzでトグルします。
・GP4は処理loopでトグルパルスを出力。
 割り込みなどが入ると、このパルスが止まります。
・GP5が1kHzのPWM出力。
 50%デューテューに設定。
・GP6がPWM割り込みの処理時間。

問題なのがアラーム割り込み。
2022年4月8日にコメントしましたが、アラーム割り込み
を使うとこの中での処理遅れが加算されるのです。
  ※一定周期の時間が欲しい時はアウトです。

間違いなく一定周期が得られるPWMと比べて、どのくらい
違うかというと・・・
 アラーム割り込み(500Hzでトグル)が
   周期 2.005983ms
 1KHzのPWM出力が
   周期 0.9999975ms

1kHzに換算すると「1.00299倍」。
millisで間隔「1003ms」と出てくるので、
方形波パルスの周波数は正しいようです。
やはりアラーム割り込みによる繰り返しはダメ。

1msアラーム割り込みの処理時間はこんなのです。
Aa1_20220412142701
GP2をH/Lしている割り込み処理の区間は約0.3us。
しかし、loopパルスが途絶えている区間が8usほど
になっています。
これが実際の割り込み処理時間になるわけです。
毎回の1ms割り込みで「3us」が積み重なると、
1秒で3msの遅れとなり、だいたいのつじつまが
合ってきます。

※重要
・一定周期の発生にはアラーム割り込みは適さない。
・利用できるのはPWM割り込み。
・PWM __↑ ̄ パルスエッジの直前で割り込みがかかる。
・PWM割り込み、実質的に一つしか使えない。
    どのPWMチャンネルから割り込みが入ったの
    チェックがむつかしいかと。


| | コメント (0)

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

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

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

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

| | コメント (1)

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 割り込み処理のミスあれこれ:割り込み禁止にして書かないとダメよ

| | コメント (0)

2022年3月18日 (金)

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

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

こんな回路にして、ボリュームで試験用パルス出力の
L時間/H時間を変えられるようにして「パルス抜け」を
Arduino自身で確認できるようにしました。

Bb01

Arduino-UNO基板に10kΩのボリュームを3つ
付けたサブ基板を装着します。
Bb11
Bb10
小基板に乗せて、直接挿入。

そして、テストプログラムのINT0パルス割り込み
↓↑両エッジに変更します。
L側の時間を短くしたり長くすると、入力パルスに
対する割り込み応答の様子が見えてきます。

まず、最短クロック幅での割込応答。
Bb21
両エッジモードのままでも、ちゃんと割り込みがかかりました。
  (両エッジそれぞれに対する応答は無理ですが)

両エッジを捉えてくれる最小パルス幅がこんな感じ。
Bb22

「割り込み禁止にしてない」のが理由で、2発目のパルスを
落としている様子です。

(1)まず、正常な場合。
Bb23

(2)スカタンすると・・・
Bb24

(3)忘れたころに「抜け」が出る様子です。
Bb25
この「メイン処理でパルス数を積算する」方式では、
読み出し時に「抜け」が発生すると、それがどんどん
積もっていきます。
  ※基本、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マイコン特有の注意点

| | コメント (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

| | コメント (0)