Arduino

2022年5月 8日 (日)

「ダイソーの観覧車」を回せ! 回路図

緊急指令! 「ダイソーの観覧車」を回せ!
こんな回路で回します。

Dd12
ステップパルスを出すだけですので、もっと小さなマイコン
で良いのですが、「汎用性」ということでArduino-UNOを
(乗っているチップ ATmega328P)をベースにしました。

ドライバは「A4988」。
ゆっくり回すので1/16のマイクロステップで使っています。
1回転が32768パルス。
最大速度と加減速を半固定ボリュームで設定できるように
しておきました。

そうそう。
24Vから5Vを作っているロームのDC-DCコンバータBP5293-50
発熱しないんで小物の工作で便利に使っているのですが、
どうやらディスコンのようです。
秋月電子の在庫はまだ大丈夫のようですが、検索すると
 「終息品」「製造終了」「注文不可」
の案内が出てきます。

※追記
ユニバーサル基板に手組みした回路。
B01
Arudino-UNOとつなぐのではなく、28ピン・ソケットに実装した
Atmega328Pで動かしています。
左下の緑色基板が「A4988」ドライバ回路です。

※制御波形
Bb000_20220509145801

空いたポートにテスト用のパルスを出していますので
オシロを使えば処理時間が「見え」ます。

1msのタイマー割り込みで加速・減速処理をしています。
一定速度になると、周波数からOCR1A設定値を計算する
処理が無くなり、割り込みの処理時間が短くなります。
Bb001_20220509150101

24V電源がオフした時の「減速」処理です。
Bb002_20220509150201
電源ラインに入れたコンデンサに残ったエネルギー
を使って、急停止ではなくスローダウンできればという
ことで試してみました。
パルス周波数を4kHzまで上げると、先にコンデンサの
電圧が落ちてしまいます。

マイコンを使って制御しましたが、これくらいの処理なら
ステッピングモータを「ゆっくり回す」 回路のように、
「VCO:4046」を使ってパルス周波数を制御するほうが
簡単でしょう。
  ・電源オンで勝手に走り出せばよい。
  ・アナログ的にスローアップ、スローダウンできる。
  ・正逆回転制御は不要。

こしらえた制御プログラム、けっこう複雑です。
  ・16bitタイマーのOC1Aでパルス出力。
  ・タイマー2で1ms割り込み。
     この中で加減速制御
  ・速度、加減速設定ボリューム値はA/D割り込みで。
  ・割り込みと競合するwordデータの扱い。

※スケッチ: ・ダウンロード - wheel1.txt
   (ファイルタイプ、inoではなくtxtにしています)

※出窓に設置 (5月10日)
B02_20220510091701
音は静か。
モーターのドライブ周波数も気になりません。
周波数は約2kHz。
一回転32768パルスなんで、15~16秒で1回転。
A4988ドライバ基板の半固定抵抗、24V電源の電流と
して40mAちょいに調整。
  (制御回路の電流も含めて)
これで駆動トルクは十分。
長時間運転しても、モータの発熱は気になりません。


※youtubeに動画をアップ
  ・https://www.youtube.com/watch?v=G9pLAnvU-uU
音声がザワザワしているのは、再開したご近所の居酒屋さん。
(我がガレージじゃない)


| | コメント (1)

2022年4月22日 (金)

Arduino UNOのA/D入力:PWMでD/A出力してA/D入力を試す

A/DとD/A(PWMを使って)、同じ基準電圧で出力・入力すると
スケールが同じになります。
ラズパイ・ピコで試したのが
  ・Arduino IDEでRaspberry Pi Pico:PWMでD/A出力してA/D入力を試す
結果・・・
 「こりゃあかんで」
 「まともに使われへんやん」
ということに。
ピコでまともなA/Dしたい時は外付けICを使うのが定石
になるかと思います。

で、同じ考え方をArduino UNOで試してみました。
UNOに乗っているマイコンチップはATmega328P。
過去、いろんなツールに使ってきましたが、ピコの
ようないびつなA/D変換結果には出くわしていません。
外部から与えた電圧(などの信号)で2点校正すれば、
安定した読みが得られるぞ、ということで、回路と
ソフトを作ってきました。

あらためて試してみました。
まずは外部から基準電圧を与えるため、こんな回路で
試します。
A11_20220422130301
使った基準電圧ICはMCP1525とMCP1541。
それぞれの実値は、2.4976Vと4.0777Vでした。

10bitのPWMを0~1023、step=1で可変。
A/Dの平均回数は16回。
その結果。
Cc1_20220422130401

3LSBほどの差が生じましたが、「段」や「飛び」
はありません。
  ※0V付近に不感帯が発生。
   オペアンプによるものは1.0~1.5mVです。

次がチップに内蔵された1.1V基準電圧源。
AREF端子から外に出し、オペアンプU1Bで受けて
D/Aの基準電圧にします。
こんな回路。
A12_20220422130501

AIN0入力に入れるCRフィルタの影響も見てみました。
結果のグラフ。
Cc2_20220422130601
ノイズ除去用として良く用いる「10KΩ + 0.1uF」の
フィルタ。
大きな変換値への影響は無さそうです。

他のArduino UNO基板ではどうかと試しました。
グラフはスムージング処理を加えて描きました。
Cc3_20220422130701

2点校正:線形補間すれば、10bitの分解能を十分に
生かせる特性です。

※関連
線形補間って「LERP」って言うんだ!
Arduino 10bit A/D値をmap関数でスケーリングする例
ミスが広まる 1/1023 vs 1/1024
Arduino なんとかして誤用を正したい:A/Dの1/1023とmap関数
Arduino、analogWriteは捨てちゃえ。ちゃんとしたPWMを使おう

/*****  PWM出力とA/D入力のテスト *****/
// Arduino UNOで
// A0でA/D入力
// PWM周波数は1.95kHz, 10bit分解能 D9(PB1) OC1A出力
// Vrefに2.5V基準電圧ICを接続 → 内部1.1Vで ★1
/***** 送信データ *****/
char tx_str[64]; // 送信文字列
/***** シリアル1行入力 *****/
#define RXBF_SIZ 32 // 文字バッファ文字数
char rx_bff[RXBF_SIZ+1]; // 受信文字バッファ (+null)
byte f_rxok; // 受信データありフラグ
/***** シリアル1行受信 *****/
// CRでターミネート f_rxokを1に
// エコーバックなしに
// なし→BSで1文字戻す
void rxbff(void)
{
static byte cnt = 0; // 受信文字数
char c;
if(Serial.available()){ // 受信データあり
if(f_rxok == 0){ // 前データ受信処理した
c = Serial.read(); // 1文字読み出し
if(c == '\r'){ // CR?
rx_bff[cnt] = '\0'; // nullを最後に
// Serial.println(); // 改行
f_rxok = 1; // 受信成功
cnt = 0; // 最初から
}
// else if(c == '\x08'){ // BS? (処理なしに)
// if(cnt > 0){
// cnt--; // 1文字戻す
// Serial.print("\b \b"); // BS,space,BS
// }
// }
else{ // 文字
if((cnt < RXBF_SIZ) && // バッファサイズ内
(isprint(c))){ // 表示可能文字0x20~0x7E
// Serial.write(c); // エコーバック
rx_bff[cnt] = c; // バッファに入れる
cnt++; // 1文字進める
}
}
}
}
}
/***** Break処理付のdelay *****/
// シリアル受信あればdelayを中断
void bkdelay(word dly)
{
word tn, t;
tn = (word)millis(); // now
while(1){
t = (word)millis() - tn; // 経過時間
if(t >= dly) break;
if(Serial.available()) break; // 受信データ
if(f_rxok) break; // CR入力
}
}
/***** 平均処理付A/D入力 *****/
// avr:平均回数
// Raspberry Pi Picoは12bit
// Arduino UNOは10bit
// D2出力は処理タイミングチェック用
// ノイズ減のためコメントアウト
word adavr(word avr)
{
word ad;
word i;
uint32_t add; // 平均加算値
if(avr == 0) avr = 1; // 0なら1回に
add = 0;
for(i = 0; i < avr; i++){
// digitalWrite(2, HIGH); // D2 H
ad = analogRead(A0); // 生A/D値
// digitalWrite(2, LOW); // D2 L
add += (uint32_t)ad; // 加算
}
ad = (word)(add / avr); // 平均値
return ad; // 10bit A/D値
}

/***** D9:PB1:OC1Aへのアナログ出力 *****/
// 10bit値 0~1023
#define PWMTOP 1023 // 10bit max値
void analogWrite9(word d)
{
if(d > PWMTOP) d = PWMTOP;
OCR1A = PWMTOP - d; // PB1設定
}

/***** SETUP *****/
void setup() {
Serial.begin(9600); // 通信
pinMode(2, OUTPUT); // D2
pinMode(9, OUTPUT); // D9 PC1A PWM出力
pinMode(LED_BUILTIN, OUTPUT); // LED
// PWMでD/A出力
// タイマー1 PWM出力 (A:PB1,B:PB2)
TCCR1A = 0b11000010;
// |||| ++---- PWM ICR1 モード
// ||++-------- Port動作
// ++---------- PWM A (Negモード)
TCCR1B = 0b00011010;
// || ||+++---- クロックセレクト 16MHz/8=2MHz
// || ++------- PWM ICR1 モード
// |+---------- ICES1
// +----------- ICNC1
ICR1 = PWMTOP; // 10bit
OCR1A = 0xFFFF; // PWM off 0Vに
// A0入力 Vrefは外部 → 内部に
// analogReference(EXTERNAL); // 外部 ★1
analogReference(INTERNAL); // 内部1.1V
analogRead(A0); // 1回変換しておく
}

// 繰り返しデータ
word ad; // A/D値
word da; // D/A:PWM設定値
char *s; // 入力文字列 最初の
word n; // 文字列番号 5つまで
char *s_in[5]; // 文字列5つ
word d_in[5]; // 入力 start,end,step,avr,wait
byte j_exc; // 実行区分

/***** LOOP *****/
void loop() {
byte f_xLED = 0; // LED点滅用
Serial.println("# A/D test out:PWM D9 in:AD0");
delay(500);
while(1){
f_xLED ^= 1; // LED トグル
digitalWrite(LED_BUILTIN, f_xLED); // LED出力
rxbff(); // 1行受信処理
switch(j_exc){ // 実行区分
case 0: // コマンドプロンプト表示
Serial.print("#Start End Step Avr Wait >");
d_in[2] = 16; // step値を16に
d_in[3] = 1; // 平均回数=1で
d_in[4] = 200; // wait時間を200msに
j_exc = 1;
break;
case 1: // コマンド入力
if(f_rxok){
f_rxok = 0;
Serial.println(); // 改行
n = 0;
s = strtok(rx_bff, " ,"); // 最初の文字分離
while(s != NULL){
s_in[n] = s;
d_in[n] = atoi(s_in[n]); // 数字に
s = strtok(NULL, " ,"); // 次文字分離
n++;
if(n >= 5) break; // 5つまで
}
if(n == 0){ // 数値入力なし
j_exc = 0; // プロンプトから
}
else if(n == 1){ // start一つだけ
da = d_in[0]; // D/A値
analogWrite9(da); // PWM出力して
delay(1000); // wait
j_exc = 2; // A/D入力繰り返しへ
}
else{ // end,step,waitで
j_exc = 3; // PWM繰り返しへ
}
}
break;
case 2: // A/D入力表示続行
ad = analogRead(A0); // A/D値
sprintf(tx_str, "D/A:%4d A/D:%4d",
da, ad);
Serial.println(tx_str);
bkdelay(1000); // 1秒
if(f_rxok){ // break?
f_rxok = 0;
Serial.println("#Break");
j_exc = 0;
}
break;
case 3: // start,end,step,avr,wait
da = d_in[0]; // start値
analogWrite9(da); // PWM出力
Serial.println("#D/A A/D diff");
bkdelay(1000); // 1秒
j_exc = 4; // A/D入力繰り返しへ
break;
case 4: // loop
analogWrite9(da); // PWM出力
bkdelay(d_in[4]); // wait ミリ秒
ad = adavr(d_in[3]); // avr A/D平均処理
sprintf(tx_str, "%4d %4d %5d",
da, ad, (ad - da)); // 差も出力
Serial.println(tx_str);
if(da >= d_in[1]){ // endと比較 おわり?
Serial.println(); // 改行x2
Serial.println();
j_exc = 3; // A/D入力繰り返しへ
}
da += d_in[2]; // +step
if(f_rxok){ // break?
f_rxok = 0;
Serial.println("#Break");
j_exc = 0;
}
break;
}
}
}

PWM出力、8bitしか扱えないanalogWrite()は使っていません。
タイマー1を使ったPWM出力、その初期化手順はsetup()を、
出力方法はanalogWrite9()を見てください。

12_20220422154401
ブレッドボードに2つあるIC、上のほうがオペアンプ。
下側がアナログマルチプレクサ。

| | コメント (0)

2022年4月19日 (火)

Arduino IDEでRaspberry Pi Pico:millisをwordで処理した時の異常

2022年4月14日:Arduino IDEでRaspberry Pi Pico:32bitマイコンがバグを生む
この検証用スケッチを書いてみました。

/*****  millisを使った時間待ち  *****/
// uint32_tではなくwordで
word ms_0;
word ms_1;
void delay2(word dly)
{
ms_0 = (word)millis(); // 開始値
while(1){
digitalWrite(2, HIGH); // テスト用パルス出力
ms_1 = (word)millis(); // 現在値
if((ms_1 - ms_0) >= dly) break; // ★1 時間経過
// if((word)(ms_1 - ms_0) >= dly) break; // ★2
digitalWrite(2, LOW);
}
}

/***** SETUP *****/
void setup() {
Serial.begin(9600); // TX
pinMode(2, OUTPUT); // PD2
pinMode(3, OUTPUT); // PD3
pinMode(LED_BUILTIN, OUTPUT); // LED
}

/***** LOOP *****/
void loop() {
byte f_xLED = 0; // LED点滅用
word cnt = 0; // loopカウンタ
char tx_bff[64]; // 送信文字列
while(1){
f_xLED ^= 1; // LED トグル
digitalWrite(LED_BUILTIN, f_xLED);
delay2(1000); // 1000ms wait
sprintf(tx_bff, "#%4d %5u %5u",
cnt, ms_1, ms_0);
Serial.println(tx_bff); // 改行
cnt++;
if(cnt > 9999) cnt = 0;
}
}

★1 と ★2の違いが重要。
★2は引き算結果を(word)でキャストしています。

Arduino UNOで動いていた<★1>の処理、
これをRaspberry Pi Picoに持ってくると、
  cnt ms_1 ms_0
 # 61 62002 61002
 # 62 63002 62002
 # 63 64002 63002
 # 64 65002 64002 ←ここまで動いて停止

数値の比較ができず(32bitのマイナス値になる)、
時間待ち関数から抜け出せません。

これを<★2>のようにすると、Pi PicoでもArduino UNO
でもオーバーフロー発生部分を無事に通過できます。
こんな具合。
  cnt ms_1 ms_0
 # 63 64002 63002
 # 64 65002 64002 ←ここで止まっていたのが、
 # 65   466 65002  (65536+466=66002) 通過
 # 66  1466  466
 # 67  2466 1466
 # 68  3466 2466

符号なし16bit値同士の減算、オーバーフローが起こっても
16bitならうまく「差」が算出されます。
「466 - 65002」は16進だと「01D2 - FDEA」。
「01D2 - FDEA = FFFF・03E8」となり、不要な上位の
「FFFF」は捨てられて下位の16bit値、「03E8」が出てきます。
16進の03E8は10進で1000。
ちゃんと1000ms待ちが動きます。

それが32bitマイコンだと「暗黙の型変換と符号拡張」 が悪さをして、
「01D2 - FDEA = FFFF・03E8 = -64536」とマイナスの値になって
しまいます。
  ※符号なし16bit = word = uint16_t で比較されるつもりが
   符号付の long = int32_t (32bitマイコンでは int だ) で
   比較が行われるのです。
結果、1000msの経過がチェックできません。
今回の例では、減算値ms_0は65002と固定されています。
ms_1が0~65535の間を変化しても、1000を越える値は出てきません
いつまでたってもこのdelayルーチンから抜け出せないのです。

このスカタンの場合、65秒ほど経てば「あれ? なんかおかしい
と気が付くわけですが、もっと複雑な処理で、異常の出現まで
時間がかかるような場合や、条件が重なった時だけ発生する
ものだと、むちゃ怖いバグになってしまいます。

8bitマイコンで正しく動いていたルーチンを32bitマイコンに
持ってくると、転けてしまって動かない。
よく考えておかないと、こんなことが現実に起こります。
「暗黙の型変換と符号拡張」 、昔のルーチンを借用する時は
どうぞ気をつけてください。

| | コメント (0)

2022年4月14日 (木)

Arduino IDEでRaspberry Pi Pico:32bitマイコンがバグを生む

Arduino UNOの「int」は16ビット
それがRaspberry Pi Picoになると「int」は32ビットに

Arduino UNOだとついついRAMをケチりたく
なって、255までの数値でOKなら「byte」でと
「スケッチ」を書いてしまいます。
8ビットで済むところをwordやintにするのは
もったいない・・・ はい。ケチです。

そんな考えで作ったプログラムをPicoに持って
くると思わぬ「バグ」が生じます。
  ・Arduino IDEでRaspberry Pi Pico:PWMでD/A出力してA/D入力を試す

このスケッチ中で使っている時間待ちルーチン。
bkdelay」という名で、1ms単位の時間待ちをしている途中、
シリアル入力があったら時間待ちを中断するという機能を
持たせています。
1秒待ちなんかしてる時に、「もぅエエからはよ終わろうぜ
に対処します。

さて、この時間待ち。
割り込みを使わずに管理するとなると、millisを呼び出して
時間経過をチェックするという処理になります。

millisは32bit値で最大1193時間、50日ほどを計時できます。
「そんな長いこと待たへんのんで16bitでエエやん」と
ケチるわけです。  (16bitだと65秒)
その「ケチな考え」が32bit CPUでバグを生んでしまいます。

今回のルーチン。
/***** Break処理付のdelay *****/
// シリアル受信あればdelayを中断
void bkdelay(word dly)
{
word tn, t;
  tn = (word)millis();    // now
  while(1){
   t = (word)millis();    // 経過
   if((word)(t - tn) >= dly) break;
   if(Serial.available())  break; // 受信データ
  }
}

目標待ち時間dlyと経過時間(t - tn)を比較している
んですが、すべて16bit値で計算していました。
  ※Arduino UNOだから。
  ※UNOの時は(word)を付けてなかった。
  ※(t - tn)、符号無しwordだとtとtnの大小が逆に
   なっても差の値は正しく出てきますんで。
ところが、Picoは32bitで内部処理。
16bitで減算して比較するつもりが、32bit値に型変換され
てから比較されるのです。
  ※「暗黙の型変換と符号拡張」というヤツです。
tとtnの大小が逆になったらマイナスになってしまいます。
tnの値によっては、いつまでたってもdlyを越えることが
できません。

ということで、減算式(t - tn)の前に付けた
キャスト演算子(word)
これを忘れると動かないわけです。

※教訓:32bitマイコンはRAMをケチるな!

ちゃうちゃう。
 「暗黙の型変換と符号拡張」に注意!ということで。


※millis時間判定の異常、検証用スケッチの紹介
Arduino IDEでRaspberry Pi Pico:millisをwordで処理した時の異常


| | コメント (1)

2022年4月 4日 (月)

ラズパイ・ピコのmillisとmicros、なぜか遅い!?

ラズパイ・ピコをArduino IDEで動かそうとしたけど のコメントで、
なんとなくloop時間が長いぞ」っと記しましたが、
millismicros処理時間をテストしてみました。

void setup() {
pinMode(2, OUTPUT); // GP2
pinMode(3, OUTPUT); // GP3
}

uint32_t t;

void loop() {
if(millis() != t){ // 1ms変化?
digitalWrite(2, HIGH); // GP2
t = millis();
digitalWrite(2, LOW);
}
digitalWrite(3, HIGH); // GP3 H/L
digitalWrite(3, LOW); // loopでパルス
}

まずmillis
B000_20220404175301
処理に10us以上かかっています。

microsはちょっと速くなりました。
こんな具合に1ms経過をチェック。
  if((micros() - t) >= 1000){ // 1ms変化
    :
B001_20220404175401

どんな処理をしているのか探してみましたが、探し切れていません。

<common.h>
 unsigned long millis(void);
 unsigned long micros(void);
 void delay(unsigned long);
 void delayMicroseconds(unsigned int us);

<wiring.cpp>
 unsigned long millis(){
  return duration_cast<milliseconds>(lowPowerTimer.elapsed_time()).count();
 }

 unsigned long micros() {
  return timer.elapsed_time().count();
 }

この中の、
 lowPowerTimer.elapsed_time()).count();
 timer.elapsed_time().count();
この処理がどうなっているのか不明っす。


※追記 Arduino-UNOと比較
それじゃっと、16MHzクロックのArduino-UNO(ATmega328p)と
比較してみました。
こんなスケッチ。

//  ポート出力 H/L (digitalWriteだと遅いので)
#define PD2_H (PORTD |= (1 << PD2))
#define PD2_L (PORTD &= ~(1 << PD2))
#define PD3_H (PORTD |= (1 << PD3))
#define PD3_L (PORTD &= ~(1 << PD3))

void setup() {
pinMode(2, OUTPUT); // D2
pinMode(3, OUTPUT); // D3
}

uint32_t t;

void loop() {
if(millis() != t){ // 1ms変化
PD2_H; // PD2 H/L
t = millis(); // millis処理の時間
PD2_L;
}
PD3_H; // PD3 H/L
PD3_L;
}

確認用のパルス出力はdigitalWriteを使わず、ポートの直叩きで
スピードアップします。
結果の波形。
A000_20220405085301

millisの処理が1.3usほど
約2usでloopを回っています。

loopで出しているパルス(PD3)。
それが、いったん止まります。
その原因がタイマー0のオーバーフロー割り込み。
millisはタイマー0のオーバーフロー割り込みでカウントアップ
していますんで、その直後のloopで1ms経過を検知している
様子が見えています。


ラズパイ・ピコの速度、はてさて・・・

※関連
割り込みで処理させるwordデータの扱い
   Arduino-UNOでのmillisの中味を解説しています。


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

2022年3月11日 (金)

Arduino-UNO 割り込み処理のミスあれこれ:ロータリーエンコーダー

Arduino-UNO 割り込み処理のミスあれこれ:計時処理に続いて、
今度はロータリーエンコーダーの処理をあれこれと。

「Arduinoのライブラリを使ってカウントしよう」という場合は
問題ないでしょう。
割り込みを使ってのカウント、そのデータ読み出しのルーチンを
見るとちゃんと割り込み禁止状態で4バイトのカウントデータを
取り出しています。
   (端折って)
inline int32_t read(){  読み出し処理
 noInterrupts();   割り込み禁止で 
 int32_t ret = encoder.position; カウント値
 interrupts();     割り込み有効に戻して
 return ret;  カウント値4バイトを持ってリターン
}

ところが、エンコーダの処理を自前でされているスケッチが
気になるのです。

割り込みでカウント値を増減するのはかまいません。
問題はその読み出しです。
割り込みで処理させるwordデータの扱いに引っかかるのです。

Arduino-UNOのマイコンATmega328Pは、RAM領域にある
データを1バイト単位でしか読み書きできません。
   (8bitマイコンですから)
wordデータなら2回、longデータなら4回に分けて読み書きが
行われます。
多バイトデータ読み出しの間に割り込みが入ると、読み出し途中
の値が変わってしまうことがあります。

例えば、単純なカウントアップだと、
「0x12FF」の下位バイト「FF」を読み出した次のタイミングで
割り込みが入ると、データが+1されて「0x1300」になります。
次のステップの上位バイトの読み出しでは「13」が現れます。
0x12FF」あるいは「0x1300」が出て欲しいのに
0x13FF」という間違った値になってしまいます。

連続して読み出しているはずですが、1バイト単位でしか読み出し
できなので、こんなことが起こるのです。
割り込みで多バイトデータを扱う時は、読み書きのシーケンスが
分断されないよう割り込み禁止にしなければなりません。
  ※この場合↑、割り込みがない状態での次の読み出しでは
   「0x1300」と正しい値が出てきます。
   しかし、一瞬だけ出現する誤った「0x13FF」が
   命取りになる制御もあるのです。

書き込む場合も同じです。  (カウントダウンにして書き直し)
現カウント値が「0x1234」だったのを「ゼロクリアー」してみます。
先に下位バイトを「00」にした瞬間、カウントダウン割り込みが入ると、
いったん「0x1200」になったカウント値が「0x11FF」になります。
その直後、上位バイトに「00」を書くので、結果は「0x00FF」と
なり、思っていたプリセット値(0x0000)になりません。
この場合も割り込み禁止の手順は省けません。

ロータリーエンコーダのカウント値、いろんな例では
intあるいはlongにしている場合がほとんどなので、
この注意が必要です。

ただ、
カウント値をシリアル出力しているだけ」や
液晶に表示しているだけ」なら、計数割り込みと輻輳しても
次のタイミングで正しい値が出力、表示されるので、
異常があっても気付かないわけです。

しかし、カウント値を位置情報として制御に使った時は
たまにおかしくなる」というバグの原因になります。

位置制御だと「ここで止まるはずが・・・あれれ?」でしょうか?

わずかなことですが、最初からこれを頭に入れてプログラムを
組まなければ、見つけにくいバグを混入させてしまいます。

  ※プリセットの場合は、割り込みよるカウント操作を
   いったん止める処理でしょうか。

  ※また、プリセット処理の場合、割り込みとの競合で生じた
   結果はずっと残ってしまいます。
   読み出しだと、次のサイクルには正常値が出てくるんで、
   気付きにくいかもしれませんが、書き込む場合はミスした
   結果が残るので「あれれ?」っとミスを発見できる可能性
   が出てきます。
   しかし、このタイミングが合ってミスが生じるのは
   ほんとに「まれ」。
   「まれ」でも起きる可能性があるなら、いつかはアタリま
   すんで。   (マーフィーの法則っぽいお話し)
   気付きにくいから、最初からちゃんと考えておかないと、
   という次第です。   


▲割り込み処理のカウント値をそのまま読み出している例
 <カウントをミスるかも>
ArduinoUNOでロータリエンコーダを読む 立ち上がりエッジ読み取り(2021-03-22)
  (コメント書き込み)
Arduinoでロータリーエンコーダを使う(つなぎ方&スケッチ)(2019.12.05)
  (コメント書き込み)
arduinoでロータリーエンコーダ値の読み取り(2016/09/28)
第二十一項 ロータリーエンコーダとノイズ対策・割り込み(電子工作創作表現 2019/12/04)
モーター制御にも通じる!Arduinoとエンコーダーを使ったオモシロIoT工作&プログラムまとめ(2020年10月6日)
  (コメント書き込み、返信頂戴しました)
Arduinoでロータリーエンコーダー (じわじわ進む 2016年12月28日)
  (コメント書き込み、返信頂戴しました)

▲カウント抜けが生じる処理
ロータリーエンコーダテスト 割り込みを使う場合
   ※↑カウント抜けの可能性。
    「m_nValue = 0;」の直前に入ったカウントパルスは
     捨てられてしまいます。
 ※解説
  int R_count0;  ←エンコーダカウント値
  volatile int m_nValue; ←割り込みでup/down
               パルスが無ければ0
  m_nValueに値が入ったらR_countを+/-という処理
  ~~~~~~~~~~~~~~~~~~~~~~~
   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マイコンでも
  発生します。

  (1)と(2)の間に割り込みが入って、m_nValueの値が
  変わっても問題なしです。ゼロになってもOK。
  (2)(3)間が割り込み禁止状態なら、その間のカウントは
  (3)の次でm_nValueが確定して抜けることはありません。


▲割り込み禁止処理を入れて正しく解説
ロータリーエンコーダ(PIC AVR 工作室)


▲補足「カウント抜けが生じる処理」に関して
これ、カウンタup/down計数だけでなく、「割り込みでのデータ転送」
にも絡んできます。 (例えばシリアル通信)

(1)は「新たなデータが来たか?」のチェック。
 割り込みで受けたデータ数をチェックして、データが来てたら、
 その数だけ別のバッファにデータをコピーするような処理を
 想像してください。

(2)は受けたデータの数だけ、受信データをコピーする操作。
  割り込み用のバッファから、受けた数だけ処理用のバッファへ
  転送します。
  
(3)は次のデータを待つためにデータ数をゼロにという処理。

もし、(2)と(3)の間に割り込みが入って新たなデータが来てたら・・・
割り込み禁止にしていないと(3)でデータ数がクリアされ、せっかく
受けたデータを喪失してしまいます。
これは、8bitマイコン特有の問題ではありません。

割り込みとの競合、ちゃんと考えておかないと、見つけにくいバグが
生じてしまいます。

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


| | コメント (1)

より以前の記事一覧