割り込み処理

2024年9月30日 (月)

1クロックでも速くしたい 割込を「ISR_NAKED」で

2024年9月27日:1クロックでも速くしたい DDS方式の2パルス発生器

この続き。 タイマー1のコンペアマッチA割り込みを
インライン・アセンブラで書いてみました。

/***************************/
/* DDSでパルス出力 */
/***************************/
// A相B相出力データ COM1A,COM1Bで出力
volatile uint8_t com1_ab; // 3bitでCW,CCW判断
// tbl_ab[]をアクセス
volatile uint8_t tbl_ab[]={
0b11110000, // 000 H H CW
0b10110000, // 001 L H
0b10100000, // 010 L L
0b11100000, // 011 H L
0b11100000, // 100 H L CCW
0b10100000, // 101 L L
0b10110000, // 110 L H
0b11110000, // 111 H H
}; // |||| ++---- CTC OCR1A モード
// ||++-------- COM1B PB2 (00:off,01トグル,10:L,11:H)
// ++---------- COM1A PB1

/***** タイマー1 : コンペアマッチA割り込み *****/
// DDS処理
// CW/CCW出力 A:PB1(OC1A) B:PB2(OC1B)
// COM1(A,B) 00:off,01トグル,10:L,11:H
// PB4_H; // (!!!)
// dds_cnt += dds_step; // DDSカウンタをカウントアップ
// if(pls_on){ // パルス出力中
// com1_ab = ([dds_cnt.cnt1+3] / 4) & 0x03; // 1/(65536*4096)
// com1_ab |= pls_dir; // bit2でCW,CCWを区分
// }
// TCCR1A = tbl_ab[com1_ab]; // COM1A,COM1B制御 offなら前のb値で
// PB4_L; // (!!!)
ISR(TIMER1_COMPA_vect, ISR_NAKED)
{
__asm__ volatile(
" sbi %0, 4 \n" // PB4 H
" push r0 \n"
" in r0, __SREG__ \n"
" push r28 \n"
" push r29 \n"
" push r30 \n"
" push r31 \n"
// dds_cnt += dds_step;
" lds r28, (dds_step) \n" // LSB
" lds r30, (dds_cnt) \n"
" add r30, r28 \n"
" sts (dds_cnt), r30 \n"
" lds r28, (dds_step+1) \n" // +1
" lds r30, (dds_cnt+1) \n"
" adc r30, r28 \n"
" sts (dds_cnt+1), r30 \n"
" lds r28, (dds_step+2) \n" // +2
" lds r30, (dds_cnt+2) \n"
" adc r30, r28 \n"
" sts (dds_cnt+2), r30 \n"
" lds r28, (dds_step+3) \n" // +3
" lds r30, (dds_cnt+3) \n"
" adc r30, r28 \n"
" sts (dds_cnt+3), r30 \n"
// if(pls_on){
" lds r28, (pls_on) \n"
" and r28, r28 \n"
" breq j1 \n"
// com1_ab = ([dds_cnt+3] / 4) & 0x03;
" lsr r30 \n" // 1/2
" lsr r30 \n" // 1/4
" andi r30, 0x03 \n" // & 0x03
// com1_ab |= pls_dir; // bit2でCW,CCWを区分
" lds r28, (pls_dir) \n"
" or r30, r28 \n"
" sts (com1_ab), r30 \n"
" j1: \n"
// TCCR1A = tbl_ab[com1_ab];
" lds r30, (com1_ab) \n" // 0~7
" ldi r31, 0 \n"
" ldi r28, lo8(tbl_ab) \n" // tbl_ab[]
" ldi r29, hi8(tbl_ab) \n"
" add r30, r28 \n"
" adc r31, r29 \n"
" ld r28, z \n"
" sts %1, r28 \n" // TCCR1A
// pop reg
" pop r31 \n"
" pop r30 \n"
" pop r29 \n"
" pop r28 \n"
" out __SREG__, r0 \n"
" pop r0 \n"
" cbi %0, 4 \n" // PB4 L
" reti \n"
:
: "n" (_SFR_IO_ADDR(PORTB)), // %0
"n" (_SFR_MEM_ADDR(TCCR1A)) // %1
:
);
}

「ISR_NAKED」で、使うレジスタを我が手に。
その結果、
  ・pushとpopするレジスタの数を吟味して減らせる
  ・タイミングを見るためのポート出力を先頭と最後に
   持ってこれるので、正確に見える。
問題はインライン・アセンブラの書式。
AVRマイコン特有の問題がまだ解決できていません。

「Z」レジスタにA相B相出力テーブルの先頭を持ってきているところ、
「c」だと「マイナスの値を引いて足し算」する処理が行われています。

   820:	e0 91 62 01   lds    r30, 0x0162
824: f0 e0 ldi r31, 0x00
826: ed 5f subi r30, 0xFD
828: fe 4f sbci r31, 0xFE
82a: 80 81 ld r24, Z  テーブル値を読み出し

これ、AVRマイコンの命令体系で、即値加算の命令が無いのです。
  即値加算:addiとかadci
そこで、即値減算命令 subi、sbci命令を使い、
  マイナスの値を引いて足し算に
ということで対処するのです。
  AVRマイコン・アセンブラの定石

ところが、インライン・アセンブラの書式で、テーブルの先頭アドレスを
マイナスにする方法が分からないのです。
  (- tbl_ab)とか(0 - tbl_ab)とかあれこれ
  試しましたが、エラーが出ちゃいます。
そこで仕方なく、レジスタ同士の加算命令を使って、アドレスを
得ています。
  これで、2命令を損

 " lds r30, (com1_ab) \n" // 0~7
 " ldi r31, 0 \n"
 " ldi r28, lo8(tbl_ab) \n" // tbl_ab[]
 " ldi r29, hi8(tbl_ab) \n"
 " add r30, r28 \n"
 " adc r31, r29 \n"
 " ld r28, z \n"  テーブル値の読み出し

これ、なんとかできれば良いのですが。

push、popするレジスタを5つまで減らせました。
   元のは13個。
これでダイブとスピードアップ。

「union」を使った32bit値と8bit値のまぜこぜ処理も
無くなったし、ある意味、すっきりと。

※実行時間 割り込み処理およそ5μ秒に。
Scope_1126_memo1
  前は8μ秒ほど

| | コメント (0)

2024年9月27日 (金)

1クロックでも速くしたい DDS方式の2相パルス発生器

DDS方式の2相パルス発生回路、現在は80kHz(12.5μs)の
タイマー割り込み内で処理しています。
こんな感じ。

/******************************/
/* DDS制御 */
/******************************/
// DDS制御データ
// dsiz(1024,2048,4096)の上位2bitでA,B相出力を決定
// タイマー1割り込みで処理 80kHz=12.5us(=TM1_FRQ)
int32_t dds_frq; // 0.01Hz単位の周波数設定値 ★1
// 0.10Hz~9999.99Hz
volatile uint32_t dds_step; // frqから計算したdds制御1step ★2
// frq * dsiz * 65536 / TM1_FRQ
// DDS設定カウンタ (uint32_t dds_cnt)
// タイマー割り込みでdds_stepを加算 上位ビットで波形出力
union u_dc{
volatile uint32_t cnt4; // 4byte DDS設定カウンタ ★3
volatile uint8_t cnt1[4]; // 1byt DDS設定カウンタ ★4
};
u_dc udds_cnt; // カウントデータ共用体
// cnt4に加算 cnt1で読み出し
// --- 3--- --- 2--- --- 1--- --- 0--- 4バイト
// 10987654 32109876 54321098 76543210
// xxxxHHbb bbbbbbbb aaaaaaaa aaaaaaaa
// || 下位26bitのところにstepを加算
// ++--- CW,CCW判定bit これを読み出したい
// xxxx----- 上位は無視してok

/***************************/
/* DDSでパルス出力 */
/***************************/
/***** タイマー1 : コンペアA割り込み *****/
// DDS処理
// CW/CCW出力 A:PB1(OC1A) B:PB2(OC1B)
// COM1(A,B) 00:off,01トグル,10:L,11:H
ISR(TIMER1_COMPA_vect)
{
static uint8_t b; // 3bitでCW,CCW判断
static uint8_t com1ab[]={
0b11110000, // 000 H H CW
0b10110000, // 001 L H
0b10100000, // 010 L L
0b11100000, // 011 H L
0b11100000, // 100 H L CCW
0b10100000, // 101 L L
0b10110000, // 110 L H
0b11110000, // 111 H H
}; // |||| ++---- CTC OCR1A モード
// ||++-------- COM1B PB2 (00:off,01トグル,10:L,11:H)
// ++---------- COM1A PB1
PB4_H; // (!!!)
udds_cnt.cnt4 += dds_step; // DDSカウンタをカウントアップ ★3+★2
if(pls_on){ // パルス出力中
b = (udds_cnt.cnt1[3] >> 2) & 0x03; // 1/(65536*4096)
b |= pls_dir; // bit2でCW,CCWを区分
}
TCCR1A = com1ab[b]; // COM1A,COM1B制御 offなら前のb値で
PB4_L; // (!!!)
}


アセンブル結果がこれ。

000007a6 <__vector_11>:
7a6: 1f 92 push r1
7a8: 0f 92 push r0
7aa: 0f b6 in r0, 0x3f ; 63
7ac: 0f 92 push r0
7ae: 11 24 eor r1, r1
7b0: 4f 93 push r20
7b2: 5f 93 push r21
7b4: 6f 93 push r22
7b6: 7f 93 push r23
7b8: 8f 93 push r24
7ba: 9f 93 push r25
7bc: af 93 push r26
7be: bf 93 push r27
7c0: ef 93 push r30
7c2: ff 93 push r31
7c4: 2c 9a sbi 0x05, 4 ; 5
7c6: 40 91 74 01 lds r20, 0x0174
7ca: 50 91 75 01 lds r21, 0x0175
7ce: 60 91 76 01 lds r22, 0x0176
7d2: 70 91 77 01 lds r23, 0x0177
7d6: 80 91 63 01 lds r24, 0x0163
7da: 90 91 64 01 lds r25, 0x0164
7de: a0 91 65 01 lds r26, 0x0165
7e2: b0 91 66 01 lds r27, 0x0166
7e6: 84 0f add r24, r20
7e8: 95 1f adc r25, r21
7ea: a6 1f adc r26, r22
7ec: b7 1f adc r27, r23
7ee: 80 93 63 01 sts 0x0163, r24
7f2: 90 93 64 01 sts 0x0164, r25
7f6: a0 93 65 01 sts 0x0165, r26
7fa: b0 93 66 01 sts 0x0166, r27
7fe: 80 91 7e 01 lds r24, 0x017E
802: 88 23 and r24, r24
804: 69 f0 breq .+26 ; 0x820 <__vector_11+0x7a>
806: 80 91 66 01 lds r24, 0x0166   ★A
80a: 90 e0 ldi r25, 0x00 ;★B
80c: 95 95 asr r25  ★B
80e: 87 95 ror r24  ★A
810: 95 95 asr r25  ★B
812: 87 95 ror r24  ★A
814: 83 70 andi r24, 0x03 ; 3
816: 90 91 7c 01 lds r25, 0x017C
81a: 89 2b or r24, r25
81c: 80 93 62 01 sts 0x0162, r24
820: e0 91 62 01 lds r30, 0x0162
824: f0 e0 ldi r31, 0x00 ; 0
826: ed 5f subi r30, 0xFD ; 253
828: fe 4f sbci r31, 0xFE ; 254
82a: 80 81 ld r24, Z
82c: 80 93 80 00 sts 0x0080, r24
830: 2c 98 cbi 0x05, 4 ; 5
832: ff 91 pop r31
834: ef 91 pop r30
836: bf 91 pop r27
838: af 91 pop r26
83a: 9f 91 pop r25
83c: 8f 91 pop r24
83e: 7f 91 pop r23
840: 6f 91 pop r22
842: 5f 91 pop r21
844: 4f 91 pop r20
846: 0f 90 pop r0
848: 0f be out 0x3f, r0 ; 63
84a: 0f 90 pop r0
84c: 1f 90 pop r1
84e: 18 95 reti

アセンブラのソースになった「C」のコードを見ると、
  ここ、どうにかでけへんのか?
というところが見えてきます。

32bitのDDSカウンタ、その上位バイトを取り出して
A相B相出力をアウトプット・コンペアレジスタにセット
するためのテーブルを読み出しているところ、
  1バイトでのシフトで良いのに2バイトで処理してる
  ★Aだけでエエのに。 ★Bはイラんねん。
アセンブラだと、好きに書けるんですが・・・
Cコンパイラの内部処理まで手が出せません。

dds_cntにdds_stepを加算しているところも、使用レジスタを
ケチれば、push/popも減らせます。

dds_cntにunionを使い、32bitのところを8ビットでアクセス
できるようにしたのも高速化のため。
32bitのまま 「dds_cnt >> 26」としたら、えらく時間
がかかってしまいます。

現在の処理時間。
Scope_1123_memo1

パルスだけを見ると「3.5μs」使って処理して
いるように見えますが、ch4のトグルパルスを
見ると、「8μs」ほどかかっています。
割り込みサイクルが12.5μsなので割り込みの
処理時間は65%ほど。
push/popの数を減らせればだいぶと時間稼ぎ
できるんですが。


※追記 : 試してみるものです。

2ビット右シフトのところ、
   b = (udds_cnt.cnt1[3] >> 2) & 0x03; 
>> 2」を「 /4 」にしたら、
   b = (udds_cnt.cnt1[3] / 4) & 0x03; 
1バイトで処理してくれました。

806: 80 91 66 01 lds r24, 0x0166
80a: 86 95 lsr r24
80c: 86 95 lsr r24
80e: 83 70 andi r24, 0x03 ; 3
810: 90 91 7c 01 lds r25, 0x017C
814: 89 2b or r24, r25
816: 80 93 62 01 sts 0x0162, r24

3命令=3クロックだけですが、Cのままで高速化成功!


※補足 今回のDDS処理

・目標周波数 fo (Hz)
・基準周波数 fr = 80kHz
・DDSカウンタ 上位12bit + 下位16bitの28bit
       4096 × 65536
・DDSステップカウンタ step 26bit(max)

step = (fo × (4096×65536)) ÷ fr

1.00Hz出力だと
 step = (1 * (4096×65536)) ÷ 80000
    = 3355
この値を80kHzでDDSカウンタに加算する。

1秒後には80000回加算されて0.999868…Hzの
パルスが出る。

目標周波数が最大の9999.99Hzだと
 step = (9999.99 * (4096×65536)) ÷ 80000
    = 33554398 = 0x01FF FFDE : 25bit
実周波数は、9999.9899…Hzとなる。

A相・B相パルスを4バイトあるDDSカウンタの
上位バイトのbit3とbit2を使って発生させている。
  28bitの上位2ビット

「fr ÷ fo」が「2のn乗」になると、(4096×65536)が
割り切れて加算時の端数が出なくなる。
  例えば 5kHz → 16
     2.5kHz → 32
     1250Hz → 64
     625Hz → 128
     312.5Hz → 256
     156.25Hz → 512
これ以外の時は、特定の周期で出力パルスに
ジッターが生じる。

※ジッターの例
Jj1_20240928090201
0.1秒間とか1秒間の平均なら一定周波数が
カウントされるが、瞬間を見ると主クロック
(今回は80kHz)とのズレが生じている。

上に記した「fr ÷ foが2のn乗」の時だけ、
このズレはなくなる。

※過去記事
  割り込みの高速応答、こんな手法を使ってました。
2020年8月18日:Arduino デューティー計測のためのインプットキャプチャータイミング
2020年8月19日:Arduino インプットキャプチャー割り込みを「ISR_NAKED」で (デューティー比測定)


| | コメント (0)

2024年6月 6日 (木)

最適化処理のせいで悩んだぞ 呪文volatile再び

割り込みに絡む処理で、「C」が良かれと思って
やった最適化処理のせいで悩んでしまいました。

簡略化して説明するとこんな流れ。
  intr_aは割り込み内で処理されるデータ。
  2バイト値なんでメイン側では割込禁止で書き込む
  必要がある。
  hoge()という関数の中でそれを実行する。

~~~~~~~~~~~~~~~~~~~~~~~~~
// 割り込みで読み出し処理される2バイトデータ
volatile int intr_a;

// aをあれこれ計算してintr_aを更新
void hoge(int a)
{
int d;      ★1 一時変数
  d = a * なんたら (1)
  d += あれこれ   (2) そこそこ時間のかかる計算
  cli();       // 割込禁止にして
  intr_a = d;  (3)  // intr_aに計算結果を書いて
  sei();       // 割込有効に戻す
}            // リターン
~~~~~~~~~~~~~~~~~~~~~~~~~

走らせてみると、思いのほか割り込みが禁止されて
いる時間が長いのです。
割り込み禁止期間は計算結果を書く(3)の一瞬だけ
のはず。
しかし、禁止時間から考えて計算途中も割り込み
禁止になっている様子でなのです。

その原因は最適化処理。
別行に書いている(1)と(2)の処理を、(3)のところに
コンパイラがまとめてしまっていたのです。

Cコンパイラの気持ち:
  「一時変数なんか使わんでも処理できるぜい」と

そのため、計算が始まる直前に割込禁止にして
しまっていて、時間のかかる計算が割り込み
禁止状態で行われていたのです。

で、対策。
・その1
  一時変数int dの前に「volatile」を前置。
  これで(1)(2)(3)が記述どうりの順序で進む
  ので、(3)だけが割り込み禁止になります。

・その2
  hoge()を戻り値を持たせるようにして、
  その値を割り込み禁止でintr_aに書く。
  int hoge(int a) とし、hoge()の中で
  intr_aを書くのではなく、外の処理として
    b = hoge(a);
    cli();
    intr_a = b; (4)
    sei();
  とすると、(4)だけが割り込み禁止になって
  思惑どうりに処理が流れます。
    ひょっとしたらこのbにもvolatileが
    必要かもしれない。

※過去記事
2018年10月11日:魔法の言葉「volatile」

割り込みで直接操作されるintr_aはvolatileを付けなけりゃ
と注意します。
しかし、処理の流れが重要なら、計算途中で使う一時変数に
も付けておかないと、ということです。

「割り込み禁止時間が長いぞ」・・・この発見は
テストパルスをオシロで見てしかわかりません。
答えは合ってるし。


※スペルのミスを訂正 (恥ずかしいぞ)

| | コメント (0)

2024年4月12日 (金)

I2C液晶のアクセス、割り込みで処理しないようにすると

I2C液晶のアクセス、割り込み処理で遅れる原因らしきもの

I2C液晶の処理と高レートのタイマー割り込みとの競合、
その原因がI2C液晶アクセスでの割り込み処理だという
ことがわかりました。

I2Cアクセスでの最後、I2Cバスを待機状態に戻す
「ストップコンディション」の処理に時間待ちが
入れられていました。
  割り込み処理の中での時間待ちはちょっとなぁ。
そのせいで、こんな具合に抜けや遅れが出たのです。
Aa004

Aa000_20240408135301

そこで、I2C液晶の表示ルーチンを割り込みを使わない方法で
書いてみました。
   詳細(スケッチ例)は、また報告します。

普通は液晶アクセスでの遅れなんて問題にしないでしょうが、
特殊な場合はアカンでぇ!ということで、その解決方法の
一例と見てください。

10μ秒割り込みルーチンの抜けや遅れが無くなりました。
Ff000
Ff001
Ff002

割り込みを使うことで処理が早くなったり便利(次の処理に
早く移れる)になるんなら良いのですが、現状の液晶表示
手順の場合は、割り込み処理にするメリットがありません。

I2Cでの割り込み処理、I2Cアクセスでのエラーが生じた時に
対応しやすいという面は否定しません。
でも、液晶表示の場合はエラーが生じても、一連の表示ルーチン
さえ抜ければ次に進めるという処理でかまわないでしょう。
  ロックせずに「STOP」まで進めれば、なんとかなるかと。

I2Cでのエラー解除、本来はSDA=Hの状態(マスター側がHにする
つもりで)でダミークロックを流し込むという手法を使います。
  ※昔のI2C解説本ではよく見たけれど
現状、そういった処理はしていませんので、せっかくの割り込みが
生かされていません。

I2C簡単そうだけどトラブったときの対策、けっこうやっかいです。
  ノイズが多い環境でスカタンしたら
  そのリカバリーがたいへんなんです。
昔々、マイコンに搭載されたI2C機能を使うのが面倒くさくって、
H/Lパルスを自分で出してI2Cを制御したこともありました。
エラー発生時の手順が自由になりますので。

※参考
ROHMのBR24Lxxx EEPROMデータシートより
https://fscdn.rohm.com/jp/products/databook/datasheet/ic/memory/eeprom/br24lxxx-w-j.pdf

・基本のタイミング
Ep11
・リセット方法
Ep12
・コマンドのキャンセル
   SCLをHにしてSDAをL・H
Ep13

※テストプログラムをアップしておきます。
  ・ダウンロード - test_i2c_lcd_acm0802_2b.zip

ちなみに、超低速2相パルス発生回路 のI2C液晶表示を
wireライブラリを使わないようにしたら(wireの中からtwi
を呼んでいる)、プログラムのサイズが1.8kバイトほど減少。
RAMエリアも200バイトほど減りました。
単一の液晶表示のように、単純に「書くだけ」だとずいぶん
処理が簡単になるというのが理由かと。



| | コメント (0)

2024年4月 7日 (日)

I2C液晶のアクセス、割り込み処理で遅れる原因らしきもの

超低速2相パルス発生回路・ケース入れでの処理遅れ、
I2Cの処理をしているソース「twi.c」をざっと追いか
けてみると、

・TWI(I2C)の割り込み処理
  ISR(TWI_vect)
 この中にtwi_stop()というルーチンが出てくる。

・twi_stop()の処理を見ると、
  _delay_us(10); という時間待ちがある。
     ※実際にこれが動いているのかどうかは不明

・_delay_us()がどこにあるのか探してみると
  util\delay.hの中
・関数のヘッダは void _delay_us(double __us)
  「double __us」って何よ。

・もし、_delay_us(10)で10us遅らせているとしたら、
 オシロでの観察結果と合っている気がする。

割り込み処理の中では時間待ちはするなぁ~
  っと叫びたくなるゾ!

※追記
波形観察できるよう、簡単にテスト。
・タイマー0を停止
   じゃまされないよう
・タイマー1で10us割り込みを作る
   割り込み期間、Hパルスを出力
・I2C液晶を使い1秒間隔で2文字を表示
   カーソル位置指定+2文字書込みの3データ
・10usパルスはいつも見えるはず
・I2Cの割り込みと重なった時にズレが出る

ところが・・・そのズレの量が大。
I2Cのストップコンディションと重なった部分の
割り込みパルスが1つ消失して、その後の1つ
が遅れて出現しました。

Aa000_20240408135301
終端部を拡大 (時間経過でSDA波形が↑のと異なります)
Aa004

I2C(TWI)の割り込みが悪さをしてるとしか考えられない。
割り込みを使わないでI2Cを処理するプログラムを
書かないといけないなぁ。
マスターだけならそんなに難しくないけどついつい
ライブラリを頼ってしまうんで。

もう一度言います!
  『割り込み処理の中では時間待ちはするなぁ~

Wire.cはI2C処理の手順を示すだけで実際の
ハードウェアアクセスはutility/twi.cの中でごそごそ。

※さらに追記
メインループの先頭でLEDポート(PB5)をトグル出力させると
いろんな処理時間が見えてきます。

今回の10usタイマー割り込みと液晶アクセスは
こんな具合。
A1000

一番下のch4の波形がトグル出力。
なにもせず(メインループがヒマ)グルグル回っていると
フルスピードでパルスが出ます。
しかし、何かの処理が長引くとトグル出力が一次停止
します。

液晶アクセス中にメインが回っていないということは、
何のための割り込み処理なの?
っと、思っちゃいます。
割り込み処理してるんだったら、ひとかたまりを
書き終わったら、メインに戻ってきて欲しいところ。
I2Cの送出完了でバッファから読み出し、無くなるまで
順次転送ということをしてくれたら良いのになぁっと。
  シリアル送信なんかはそうなっている。

10us割り込みのところを拡大すると。
A1001

割り込み処理そのものに加えて、その前準備と後始末に
けっこう時間が食われているのが見えます。
割り込み内で出しているパルスの前後に見えない
時間があるということが、メインループで出す
トグルパルスで見えてきます。

| | コメント (0)

2024年4月 3日 (水)

超低速2相パルス発生回路・ケース入れ

2024年3月25日:超低速2相パルス発生回路 の続き。
こんな回路に落ち着きました。
Cw_ccw1b
・電池3本運用。
・秋月の昇圧DC-DCモジュール
   AE-XCL102D503CR-G で5Vを発生。
   ただし、ちょい改造(EN端子のプルアップを外す)
・電源スイッチと操作スイッチを共用。1つだけ。
   長押しで電源のon/off操作。
   短押しでCW/CCW/stop選択。
   このためにあれこれややこしい回路に。(黄色部)
・最低周波数が0.1Hz。
 最高周波数を(無理やり)9990.0Hzに。
・周波数によって設定できる最小桁が変化。
 150Hzまでは0.1Hz。2800Hzまでが1Hz。それ以上が10Hz。
・1.5kHz程度までは、設定値から0.01%内の誤差。
 10Hzステップになる2.8kHzあたりだと0.016%。
 5kHzあたりまで上がると0.03%。
 最高周波数に近づくと分周比が小さくなり誤差が目立ちます。
 0.06%ほどの誤差が生じることがあります。
・例えば9810Hzの設定なら分周比が815で
 出てくる周波数が9815.95Hzとなって+0.061%の差。
 また、9970Hzも9980Hzも同じ分周比802になり
 出てくるのはどちらも9975.06Hz。
 でも、8000Hzの設定は誤差ゼロで出てきます。

バックアップがわりのスケッチ。
  ・ダウンロード - cw_ccw2k1b.txt
    ※inoではなくtxtにしてます。

箱はダイソーで買ったプラケース。
  (電池保管用のものだったか・・・)
Cc11_20240404142901

Cc12_20240404142901

LowBat警報表示
Cc14_20240404142901
Cc15
電池電圧が3Vを切ると、空電池マークと半分電池
マークを交互に点滅表示します。
  重要アラームは点滅させて欲しい
   なんて記事も書いてますし。

内部処理でキツいのが周波数を上げたとき。
最高が9990Hzですので、その2倍の周波数
(トグルさせるので2倍に)でタイマー1の
コンペエマッチ割り込みが発生します。
周期が50μ秒ほどとなって、16MHzのATmega328P
にはなかなかキツい。
この割り込みの他に、システムタイマーであるタイマー0の
オーバーフロー割り込みを止めて、別の周期、
3.90625kHz=0.256msのタイマー割り込みを
使っています。
これの主目的はロータリーエンコーダのチャタリング除去。

2つの割り込み処理のタイミングを見てみます。
   オシロの無限残光モードがありがたい
Cs000

Cs001

B相出力が変化する前にタイマー1のコンペエマッチ
割り込みを終えなければ、2つのパルスを正しい
タイミングで出力できません。

タイマー割り込みの処理時間がけっこう長いので、
これが問題。
シリアル出力すると、この割り込み処理による
遅延も入ってきます。
  ※I2C液晶制御でも割り込み絡むのか?
最高周波数になると、タイミングはなかなか
キビシイです。

ロータリーエンコーダの読み取りタイミング。
Cs003
I2C液晶での文字表示(xxxx.xHzの8文字)に
いがいと時間がかかります。

| | コメント (2)

2024年3月25日 (月)

超低速2相パルス発生回路

実験中の回路の動作検証用のジグで、
ジッタの無い2相パルスが欲しいという
のに答えた回路とスケッチです。

過去、あれこれ2相パルス発生器を作っていますが、
その原発振に使っているのはVCO。
手軽に周波数を可変できますが、安定度は
もう一つ。
周波数カウンタで発振周波数を読むのも面倒だし。
ということで、Arduino UNO R3を使って作って
みました。

タイマー1をCTCモードで使います。
OCR1AレジスタでA相周期を決めて、OC1Aを
トグル出力。
これがA相。

B相は、OCR1Aの半分の値をOCR1Bに設定して
(90度位相のため)OC1Bの出力極性をOC1A割り込み
のたびにトグル(CW、CCWに合わせて)します。

こんな回路です。
タイマー1の「ハード」でパルスを作っているので
ジッタはありませんし、安定した周波数が得られます。

Cw_ccw0
タイマー1のクロック源は、設定周波数が150Hz以上
だと内蔵の16MHzクロック。
周波数が低くなると、外部クロックに切り替えて
タイマー2で作った方形波をクロック源にします。

ケースへの組み込みはまだ。
バラックでの試運転。
C21_20240325151801

設定できる最低周波数が「0.1Hz」。
つまり周期が10秒。
150.0Hzまでは0.1Hzステップで可変できます。
越えると1Hzステップに。

設定できる最高周波数は5kHzまでにしていますが、
2.8kHzを越えたあたりから1Hzステップの可変では
分周器での誤差が目立ってきます。

こんなスケッチです。
  ・ダウンロード - cw_ccw2k1.zip
I2C液晶のライブラリを同梱しています。

ソースファイルだけ。 (.inoを.txtにして)
  ・ダウンロード - cw_ccw2k1.txt

パルスジェネレータ では周期を設定していましたが、
今回のは周波数
  (入力周波数に反応する回路の検証用です)
DDS機能なんかは無いので、
 クロック周波数 ÷ 設定周波数
で、タイマー1の分周比を得ています。
そのため、設定Hzと実際のHzに少々の狂いが
生じます。 (Arduino 発振子の誤差とは別に)
実験中のArduino UNO R3では1000.0Hzに設定
したら1000.074Hzが出てきます。
この末尾の74は発振子の誤差。
999.9Hzにすると999.0753となって、分周比が
整数になることによる誤差が積まれます。
1000.0Hzなら8MHz÷1000で8000.0000が分周比。
999.0Hzなら8008.008008に。
目標周波数で少数以下が変わります。
そして周波数が上がると飛んでしまう設定も
出てきます。
3007Hzも3008Hzも3007.742Hzが出て、
3009Hzになると3008.873Hzになります。
  (実物の試験回路で)
周波数が高くなると分周比が小さくなって、
微妙なところが出せなくなるのです。

それと・・・
周波数を低くした(周期を長く)ときの応答。
0.1Hz→0.2Hzに変えた時、実際の波形が変わるのは
最大で5秒後。
A相波形をトグルするOC1A割り込みの中で次の周期を
設定しているのでこんな動作になります。

トラ技2005年10月号:2相パルス発生器
29年前に製作したツール 2相パルスカウンタ
アナログ入力で2相パルスの周波数と正転逆転を制御する発振器
高速2相パルス発生回路


※アナログコンパレータ入力
Low Batのチェックをしてますんで
  (液晶右下に電池マーク点滅表示)
AIN1/PD7がオープンだとチラチラするかもです。

※なんでこんなめんどくさいことを
タイマーで計時してポートをH/Lさせれば、どんなマイコンでも
2相パルスを作れます。
今回の要望は2相パルスの安定性。
ジッターが出て欲しくなかったのです。
ATmega328の場合、タイマー1(16biit)でのハード的な
波形出力が使えました。

割り込みの中でコンペアマッチの値を書き換えているのは、
出力波形を崩さないため。
OCR1Aをメイン側で書き換えたとき、タイミングとその値で
TCNT > OCR1Aになってしまうと、コンペアマッチの発生
が1周(65536パルス)遅れてしまう可能性があるからです。
低周波ではその影響が目立ちます。
コンペアマッチの直後だと、TCNTはゼロからちょいと進んだ
値に留まっているはずなので、それより大きな値だと
1周抜けは避けられます。

これ、データシートでは「2重緩衝」という表現が使われています。
CTCモードでのOCR1Aはその機能が無効になるのです。
PWMモードでのOCR1Aは2重緩衝されますので、割り込み処理
しなくても自由に周期を変えることができます。
  ただし、ICR1でのTOP値設定では2重緩衝されないでの、
  単純な操作ではコンパエマッチのミスが発生します。
タイマー周期を自由に変えたいときは、データシートよく読ま
ないとスカタンします。

※続き
2024年4月3日:超低速2相パルス発生回路・ケース入れ

| | コメント (0)

2024年3月14日 (木)

割り込みの中でSerial.printを使うと・・・

割り込み処理の中でのdelayやSerila.print,lcd.print
  ↑この続き

割り込み処理の中でSerial.printを使ったら
どうなるのか、
具体的に見てもらいましょう。

こんなスケッチで確認

//   割り込みの中でSerial.printを使うと・・・
//  割込元はINT1 (D3)の↓エッジ
// テストパルス
#define D12_H (PORTB |= (1 << PB4)) // PB4 D12 H/L
#define D12_L (PORTB &= ~(1 << PB4)) // テストパルス
#define D13_X (PINB |= (1 << PB5)) // PB5(LED)トグル出力

/***** INT1↓エッジ *****/
void ISRint1(void)
{
char s[80]; // sprintf用文字バッファ
static uint32_t t, t0, t1, cnt;
D12_H;
t1 = micros(); // us時間
t = t1 - t0; // 経過時間
t0 = t1;
cnt++; // 割り込み回数
if(cnt > 99999999) cnt = 0L; // 8桁max
sprintf(s, "%08ld %8ldus", // 20文字
cnt, t);
Serial.println(s); // CR,LF付加で22文字(約23ms)
D12_L;
}

/***** SETUP *****/
void setup()
{
pinMode(3, INPUT_PULLUP); // INT1
pinMode(12, OUTPUT); // テストパルス
pinMode(13, OUTPUT); // LED
Serial.begin(9600); // シリアル
delay(100);
Serial.println("Test Serial Print");
attachInterrupt(1, ISRint1, FALLING); // INT1 ↓エッジ
}

/***** LOOP *****/
void loop()
{
while(1){
delay(5); // 5msでH/L
D13_X; // LEDをトグル
}
}

・INT1入力の↓エッジで割り込み。
・割り込み内では、
  割り込み回数をカウント。
  micros()を使って、パルス間隔を取得。
  回数と間隔をシリアル出力。
  CRとLFを入れて22文字。 22.92ms
  D12ポートをH/L。 割り込み処理時間が分かる。
・loopでは
  delayで5ms待ち。
  LEDポート(D13)をトグル出力。
  割り込み処理が長引くとこのパルスが止まる。
  正常なら100Hzのパルス。

出力文字数22文字で9600BPSだから約23ms。
この周期以上のINT1パルス入力なら、Serial.printでの
待ちは発生しません。

INT1の周期が23msを切ると、徐々にFIFOが詰まり出し、
割り込みの応答がおかしくなってきます。
割り込み処理にかかる時間が長くなると、他の割り込み、
(今回は計時用のタイマー0のオーバーフロー割り込み)
も影響を受け、正しい計時ができなくなってしまいます。

INT1の周期を22.8msにすると、パルス間隔はこんな具合に
変化します。

~~~~~~~~~~~~~~~~~~~~~
◆22.8ms周期のINT1パルス
 22文字なので文字列は約23msの長さ
Test Serial Print
割り込み回数 間隔
00000001 100172us (初回は不定)
00000002 22788us
00000003 22800us 最初は正常
00000004 22800us
 :
00000335 22800us
00000336 21776us FIFOが詰まり出して
00000337 22804us microsが正常に
00000338 22800us カウントできなくなる
00000339 21776us
00000340 21780us
 :
00000609 1296us
00000610 1324us
00000611 352us 割り込みが間に合わず
00000612 -672us 完璧にアウト
00000613 352us
00000614 348us
00000615 -668us
~~~~~~~~~~~~~~~~~~~~~

最初は22.8msを計測していますが、それが徐々に
詰まってきて(タイマー0が動かないので)、
612回目を過ぎるとアウトになっている様子
が捉えられました。

とうぜん、メインループ側も動かないのでD13パルス
(正常なら100Hz)も出てきません。

オシロでの観察波形です。
 上から(ch1→ch4)
  ch1:INT1入力 ↓エッジで割り込み
  ch2:LEDポート D13出力
  ch3:テスト用D12出力 割り込みでパルス
  ch4:TXD出力 アイドル中はHレベル

・シリアル出力が間に合って正常な時 
I000

・INT1にチャタリングを与えると・・・
I003

・その拡大
I004

Serial.printの処理がエライというのか、FIFOがちゃんと
働いて、文字の抜けはありません。
しかし、メインの処理が止まるのはやはり致命的。

割り込みの中でのSerial.print、いかがでしょうか。
「間に合えばエエやん」というのもご意見。
しかし、基本は書いたらアカんプログラムです。

※チャタリングの発生はこの回路で
「チャタリング除去回路」じゃなくって「チャタリング発生回路」をどうぞ

| | コメント (0)

2024年3月12日 (火)

割り込み処理の中でのdelayやSerila.print,lcd.print

(1)割り込みの中でのdelay()

そもそも割り込みの中で時間待ちするという発想が
おかしいが、delay()の動きの解説を。
   delay()のソースはwiring.cの中。
   millis()やmicros()もこの中。
   delayMicroseconds()もそばにある。
   delay()内では割込許可フラグを
   操作していない。

Arduino UNOのdelayはタイマー0のオーバーフロー割り
込みでカウントされる値を使って処理されている。
その周波数は16MHz÷64÷256で約976Hzで周期1.024ms。

delayの単位は1msだが、micros()でμ秒単位のスタート
時間を得て、そこからの経過ミリ秒をチェックして
時間待ちを行っている。

そのmicros()もタイマー0のオーバーフロー割り込みで
カウントされるtimer0_overflow_countの値をベースと
して、8bitのタイマー0カウンタの値(1カウント4usで
255が最大)を加算して経過時間を得ている。

割り込み禁止状態(割り込み処理の中)ではmicros()の
カウントが進まないため、delay(10)としても、実際に
遅延するのは1~2msとなる。
  ※delay()は割込禁止でも抜けるように
   なっているのがエライ。
delay(1)だとは1msほどは待ってくれるが、時間設定を
長くしても1~2msしかならない。

割り込み内での時間待ちは、割り込みの有効・禁止に
左右されないdelayMicroseconds()が使える。
ただし設定できる時間の最大は16383μ秒まで。
65535は設定できない。


(2)割り込みの中でのSerila.print()

Arduino UNO R3でのシリアル出力は割り込みで
処理される。
送出文字列は64文字のFIFOバッファに入れられて、
送信完了割り込みで順次出力される。

割り込みの中でSerial.printを使った時、割り込みの
頻度が大きくなって(周期が短い割り込み要因が入力
されるなどして)FIFOがいっぱいになったら、送信が
終わるまで割り込みの中で待たされることになって
しまい、割り込み処理から抜けるのが遅れてしまう。
そのため、割り込みだけでなく、メイン側の流れも
滞ってしまう。
  ※9600BPSなら1文字の送出に約1msかかる。
   FIFOに空きがあれば一瞬で抜けてくるが、
   いっぱいだと空くのを待つ。
シリアル受信を処理していたら、割り込みが遅れると
受信文字を落とすかもしれない。
タイマーへも影響する。


(3)割り込みの中でのlcd.print() 液晶表示

カーソル位置を指定して文字を表示するだけですぐに
数ミリ秒かかってしまうので、Serila.print()より
たちが悪い。
  ※Serila.print()はFIFOに空きがあればすぐの
   処理が終わる。

液晶モジュール制御のEパルスを出すだけで0.1msほど。
画面クリアで2ms待ち。
  ※busy読みしたら早くなるかとR/W線をつない
   でも、液晶ライブラリLiquidCrystaはlR/W=L
   にして無視してしまっているので、つなぐのは
   無意味。

割り込みで液晶をアクセスしてしまったら、メイン側での
表示と衝突してうまく表示できなくなってしまうし。

間違っても割り込みの中で使う関数じゃない。


(4)float計算

16MHzで走っているArduinoのATmega328、floatの加減算
だとそんなに重くならない。
float(4バイト)とlong(4バイト)で実測してみると、単純な
加減算だと10倍も違わない。
乗算も思いのほか早い。(乗算命令が活躍)
しかし、整数でも除算が遅い。
割り込み内での除算はしたくない。

 

割り込みの中では、時間がかかる(かもしれない)処理は
避けるのが基本。
   もたもたしない!
どうしてもというときは、他に影響が無いことを
確認して。


※実際に検証
2024年3月14日:割り込みの中でSerial.printを使うと・・・


| | コメント (0)

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)

より以前の記事一覧