« シリコン樹脂とリミットスイッチの接点 | トップページ | アナログ計器 »

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」で (デューティー比測定)


|

« シリコン樹脂とリミットスイッチの接点 | トップページ | アナログ計器 »

Arduino」カテゴリの記事

割り込み処理」カテゴリの記事

コメント

コメントを書く



(ウェブ上には掲載しません)




« シリコン樹脂とリミットスイッチの接点 | トップページ | アナログ計器 »