割り込み処理

2023年5月25日 (木)

初めて買ったArduino UNO・・・今は

Arduino初体験 が2012年12月。 (10年以上前だわ)

その基板の現在がこれ。
 ・どこかから拾ったプラケースをベースに。
 ・16MHzセラミック発振子を水晶発振子に交換。
 ・ブートローダ書き込み済みのATmega328Pと
  通信(スケッチアップロード)するための
  ケーブルを付加。
 ・乗っているチップは何度も交換。

Aa21_20230525110601

ちょっとした実験や試運転で便利なのが
アナログ入力ポートにつなぐ4つのボリューム。
スイッチも2つ付けてます。
こんな回路です。
Aa1_20230525110701

ボリュームのスライダーに0.1uFのコンデンサを
入れておくと安心。

発端はこんな3つのボリューム。
Aa22_20230525110801

+5VとGNDを取れるようにピンヘッダをハンダして
います。

ケチって片面のユニバーサル基板なんで、配線は
ちょっと面倒。

AnalogReadで読んだA/D値(0~1023)を使って
Delay値を設定したり、動作パラメータを変えたりと
リアルタイムでできますんで、なかなか便利です。

使ったボリュームは秋月電子通商のつまみ付半固定抵抗
  (足の1・2・3の順に注意:刻印あり)
  似てるけど3386K-EY5-103TRじゃない!


※応用のためのスケルトン・スケッチ
4つのボリュームと2つのスイッチを入力するための
応用基本スケッチを示しておきます。
 ・ボリューム値の入力は1ms割り込みで処理。
 ・64回平均。
 ・4つあるので256msごとにデータが確定。
 ・勝手にスキャンするのでAnalogReadのように
  100us待たされるということがない。
 ・割込禁止にしなくてもいつでも読めるよう
  VR値は0~255の8bitで。
    ad_avr[ch]を読めばok。

起動すると256msごとに値をシリアル出力します。
  A/D 8bit:VR1,2,3,4 SW1,2
  105 178 123 249 0 0
  105 178 123 255 0 0
   :
  0 64 128 255 0 0
  0 64 128 255 0 0
  0 64 128 255 1 0 SWはオンで1
  0 64 128 255 1 1
  0 64 128 255 0 1
  0 64 128 255 0 0

ダウンロード -  ad_vr4.txt
   .inoではなくUTF8Nのテキストです。


※タイマー割り込みとADC変換完了割り込み

ソースを見てもらえれば、その手順がわかるかと。
さまざまなライブラリ、確かに便利です。
でも、Arduino UNOのATmega328Pマイコン
あたりなら、レジスタの直接操作はそんなに
難しくはありません。
マイコンに備わっているさまざまな機能を引き
出すには、データーシートをにらみながらの
プログラミングをしなければなりません。

| | コメント (0)

2023年3月 1日 (水)

8ビットマイコンの割り込み処理・・・1バイトに収まるなら1バイトに

2022年9月1日:8ビットマイコンの割り込み処理・・・言い足りないぞ
ここ↑では、トランジスタ技術2018年5月号での記述を
「ちょっと違うけどなぁ」っと話題にしました。

この内容↓です。
 「割り込み処理関数とメイン関数の両方から
  アクセスされる変数はchar型にしておく」

Pp1_20220901103201

確かに、1バイトにしておくと安全側にはなるのですが、
間違いなく割り込みを動かそうとすると、アトミック処理
(いったんn割り込み禁止にしてごそごそ)が必須です。

で、またまた何気なく古いトラ技を見ていたらの話
になります。
今度は2019年4月号
Tr1904  

この連載:宇宙ロケットMOMO 開発深堀り体験<2>

ブロック図を記したp.129の図1を見ると、ジャイロや
サーボの制御に8bitマイコンATmega328が乗った
Arduino Pro Miniが使われています。

Tr1904a

p.135に制御ソフトが載っていたんで、ちょっと
追いかけてみました。
T11a_20230301162201

まず、目に入ったのが割り込みで処理されるであろう
変数です。
Volatileが前置されたint値(2バイト値)です。

T11b  

これを使うのが0.1ms周期のタイマー割り込みの中

T11c

pwm_cntがアップカウンタで、200になったら
出力ピンをHにしてゼロクリア。
そして、pwm_cntがpwm_h_periodeになったら
出力ピンをLにという制御。
200がPWM周期でpwm_h_periodでPWMの
パルス幅を決めています。
pwm_cntはこの割り込みの中だけで使われて
いるようなので、割り込みとの競合は問題なし。

比較するデータpwm_h_periodはどうかと追いかけ
ますと、gimbal_agl_to_pwmという関数で値を
出していました。
これはメイン側の処理です。

T11d

gimbal_agl_to_pwmがint値を算出して、それを
pwm_h_periodeに書く時に割り込みが入ると
どうなるか・・・

2バイト値ですので、2回に分けて値が書き込まれます。
もしその中間で割り込みが入り、なおかつ、
0x00FF → 0x01000xFFFF(-1) → 0x0001(+1)の
ような2バイトにまたがる数値の変化が発生したら、
割り込み処理での数値判定をミスするかもしれません。

しかし・・・このプログラムでは
  ・pwm_cntは200が最大値なんで、int値(2バイト)の
   下位側しか変化しない。
  ・関数gimbal_abl_to_pwmが返す値は
   +8~+20の範囲で1バイトの範囲内。
   負にもならない。

ということで、2バイトデータと割り込み処理との競合は
大丈夫でした。
でも、もし上位バイト変化するようなint値なら
割り込み禁止にしてデータを更新して割り込み処理に
知らせるというアトミック処理が必須です。

このプログラムでは、変数が1バイトに収まるならデータの
宣言は1バイトでということにしておくと、処理も速くなるし
でエエんじゃないでしょか。

ただ、昔人間からすると、
  char符号有り符号無しかどっちやねん?
     コンパイラはどう処理するんや?
  Arduino環境なら符号無しは  byte やろ。
  今ふうにちゃんと書くなら uint8_tint8_t で宣言か。
こんなところが気になります。

| | コメント (0)

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)

より以前の記事一覧