先日の「H8/3664でIICを使う」で検証した、ルネサステクノロジのH8/3664のI2C(以下、IIC)通信機能について、さらなるステージを目指すべく、アセンブラによる高速化を行ったので紹介しておく。一応復習しておくが、H8/3664の内蔵機能によるIICインターフェースは ダメダメなので使えない 。したがって、電子工作に興じる諸先輩方に習って、ソフトウェア・インプリメントした。ただし、C言語で視認性をよくするために少々冗長であるがゆえ、スピードが遅いという結果に終わっていた。アプリケーションによってはこれで問題ないのだが、 組み込みシステムにおけるプログラミングでは、その実行性能をハードウェアの限界近くまで引き上げることにこそ喜びがあるというものだ。
現在ターゲットとして使用しているのはルネサステクノロジのH8/3664に20MHzのクリスタル、IIC仕様のEEPROMがアトメルのAT24C512である。評価基板は秋月電商のもので、これはCQ出版がトランジスタ技術誌で過去におまけでつけてくれた基板とほとんど同じものだ。今回は(昨日の今日だけど…)C言語をアセンブラ言語にポーティングして、ハードの限界に迫るような高速化をしてみようと思う。
プログラムを確認しながらオシロスコープで何度も眺めていたら、SDAのヒゲやらSCLのパルス幅がバラバラだったりと、あまり美しい波形(=制御)だとはいえなかった。だから、少しずつコードの順序を入れ替えたりして、ロジックを壊さないように最適化と冗長性のバランスをとったものが、以下のコードである。基本的なロジックは変わっていない。IICではSDAラインを入出力に使用するため、マスタ側もスレーブ側もバスを入力にすると、プルアップされている都合でHレベルに引っ張られてしまう。IICの送信では8ビット送信後に入力に切り替えてスレーブからのACKを受信するのだが、このタイミングにわずかなバスの開放があって、ヒゲ(0.5V)が出てしまう。もちろん実用上は問題ない。SCLがHレベルの期間はマスタ側出力であってもスレーブ出力であっても、SDAに出力されるデータは保障されているからだ。そうは言っても、オシロスコープで観察される ヒゲは剃るべきだと思う 。
このソースを見てもらえばわかるとおり、SCLのパルス前後で出力や入力のビット情報をハンドリングしているが、それがパルスの脱調を助長しているようだった。これはC言語レベルで解決できるものではないかもしれない。C言語などの高級言語ではよくも悪くも、スタックからレジスタへ転送する処理とメモリ間のデータ転送が乱用?されていて、レジスタを効率的に使っていない気がする。だから、同じ値や変数を使っているにもかかわらず、何度もスタックから引っ張ってきたりするし、条件判定になぜかわざわざMOV命令を使ったりと、不思議な最適化がかかることがある。これはルネサステクノロジが提供するHEWや、Best Technologyが提供しているGDL、そして秋月電商のキットにも付属したりするCygwinのGCCなどと比較すると、また違った結果が出るかもしれない。ただし、H8/Tinyのようなベーシックなインストラクションを持つCPUでは、コンパイラの最適化処理よりも人間が直接最適化した方が効果的な場合が多いのは仕方がないかもしれない。昔話になるが、自分がPlayStationでプログラムを作っていたとき、MIPSのR3000でアセンブル言語のライブラリを書いた。ブランチ命令直後の命令が実行されるなど、いままで体験したことがないCPUだったものだから少々困惑したものだ。その後、PlayStation 2のGPU(Graphics Synthesizer)《他にベクトルユニット(=VU)とも呼んでいた》の描画エンジンをアセンブラで書き、CPU(Emotion Engine)との間で通信を行うDMA転送のライブラリを書いていたときは、ちょっと発狂するかと思ったよ…。なぜなら、アセンブラソースの中に、命令リストが2列並ぶのである。左がVU1、右がVU2、みたいな感じで。命令はどちらもほとんど同じだが、順序などによってストールする(=GPUが同期を取るために一一時停止する)ため、高速化するにはその順序を最適化しなければならないのだ。それに加えて、DMAから順次転送されてくるデータのハンドリングもしなければならない。話がよくわからないでしょ?
最適化したC言語版IIC関数群
VOID readIIC(UB* adrs, UB* data, SIZE datasz); // IICデータ列読込 VOID writeIIC(UB* adrs, UB* data, SIZE datasz); // IICデータ列書込 UB controlIIC(ID iicid, UB data); // IICソフトウェア制御 // readIIC : IICデータ列読込 VOID readIIC(UB* adrs, UB* data, SIZE datasz) { // IICデータ列読込(1) controlIIC(IIC_START, 0); controlIIC(IIC_SETBYTE, 0xa0); controlIIC(IIC_SETBYTE, ((VP_INT)adrs >> 8) & 0xff); controlIIC(IIC_SETBYTE, (VP_INT)adrs & 0xff); // IICデータ列読込(2) controlIIC(IIC_START, 0); controlIIC(IIC_SETBYTE, 0xa1); for(; datasz; datasz --) *(data ++) = controlIIC(IIC_GETBYTE, ((datasz == 1) ? TRUE : FALSE)); controlIIC(IIC_STOP, 0); } // writeIIC : IICデータ列書込 VOID writeIIC(UB* adrs, UB* data, SIZE datasz) { // IICデータ列書込 controlIIC(IIC_START, 0); controlIIC(IIC_SETBYTE, 0xa0); controlIIC(IIC_SETBYTE, ((VP_INT)adrs >> 8) & 0xff); controlIIC(IIC_SETBYTE, (VP_INT)adrs & 0xff); for(; datasz; datasz --) controlIIC(IIC_SETBYTE, *(data ++)); controlIIC(IIC_STOP, 0); } #define iic_read_sda() PCR5 &= ~0x40 // IIC:SDA受入モード #define iic_write_sda() PCR5 |= 0x40 // IIC:SDA送出モード #define iic_check_sda() PDR5 & 0x40 // IIC:SDAビット判定 #define iic_off_sda() PDR5 &= ~0x40 // IIC:SDAビットオフ(=0) #define iic_on_sda() PDR5 |= 0x40 // IIC:SDAビットオン(=1) #define iic_write_scl() PCR5 &= ~0x80 // IIC:SCL送出モード #define iic_off_scl() PDR5 &= ~0x80 // IIC:SCLビットオフ(=0) #define iic_on_scl() PDR5 |= 0x80 // IIC:SCLビットオン(=1) // controlIIC : IICソフトウェア制御 UB controlIIC(ID iicid, UB data) { UB cnt; // カウンタ BOOL flg; // フラグ switch(iicid) { case IIC_INIT : // IIC初期化 iic_write_sda(); iic_write_scl(); iic_on_scl(); iic_on_sda(); break; case IIC_START : // スタート送出 iic_on_sda(); iic_write_sda(); iic_on_scl(); iic_off_sda(); break; case IIC_STOP : // ストップ送出 iic_off_sda(); iic_write_sda(); iic_on_scl(); iic_on_sda(); break; case IIC_SETBYTE : // バイト送出 iic_write_sda(); for(cnt = 0x80; cnt; cnt >>= 1) { iic_off_scl(); if(data & cnt) iic_on_sda(); else iic_off_sda(); iic_on_scl(); } iic_off_scl(); iic_off_sda(); iic_on_scl(); iic_read_sda(); data = iic_check_sda() ? 1 : 0; iic_write_sda(); iic_off_scl(); return data; case IIC_GETBYTE : // バイト受入 flg = (BOOL)data; iic_read_sda(); for(data = 0, cnt = 0x80; cnt; cnt >>= 1) { iic_on_scl(); if(iic_check_sda()) data |= cnt; iic_off_scl(); } if(flg) iic_on_sda(); else iic_off_sda(); iic_write_sda(); iic_on_scl(); iic_off_scl(); iic_read_sda(); return data; } return 0; }
ちなみに、このC言語版では転送レートが 約179KHz であった。やっぱり遅いよねぇ。組み込みアプリケーションが使う設定データの退避と復旧ぐらいなら問題ないと思うけど、高速にデータ転送したい場合には、これだとそのアプリケーションがストールする可能性がある。インタラクション(表示器やボタン操作)が伴うとすれば、0.1秒でも惜しい。ただし、割込み対しては問題は発生していない。H8/3664のIICの不具合は割込み時のデータ取りこぼしに起因するというレポートがウェブでは挙がっていたようだ。
C言語版は遅い!ということでポーティングしたアセンブラ版が下のコードである。基本的にC言語のロジックを踏襲していて、ラン・テストを繰り返して気になる部分を微調整してある。特にNOPが挿入されたりしている部分や、SDAラインの入出力切替の順序は、C言語版とは異なっているのだ。アセンブラ化でもっとも恩恵を受けるのは、スタックからレジスタへの転送など、メモリアクセスの煩雑さから開放される点にある。アドレッシングモードが比較的豊富なH8/300シリーズとはいえ、メモリアクセスに貴重なクロックが使われてしまうのは惜しい。このコードでは最低限のメモリアクセスで、あとはレジスタ上で事を済ませようという戦略で作成したつもりである。
高速化したアセンブラ版IIC関数群
; IIC Software Interface for H8/3664,3694 1.1.0 2009/05/13 Daisukeh ; IIC3664.asm - H8/3664用ソフトウェアIICインターフェース PDR5 EQU H'FFD8 ; ポート5データレジスタ PCR5 EQU H'FFE8 ; ポート5方向レジスタ SDA EQU 6 ; IIC:SDAビット SCL EQU 7 ; IIC:SCLビット SEGMENT TEXT ATR_CODE ; IICデータ列読込 ; VOID readIIC(UB* adrs, UB* data, SIZE datasz) ; adrs 4(ER6) ; data 6(ER6) ; datasz 8(ER6) PUBLIC _readIIC _readIIC: PUSH.W R6 ; ER6退避 MOV.W R7,R6 ; スタックポインタ退避 SUBS #2,ER7 ; スタックポインタ補正 PUSH.L ER4 ; ER4退避 PUSH.L ER5 ; ER5退避 MOV.L #PDR5,ER4 ; ER4=ポート5データレジスタ MOV.L #PCR5,ER5 ; ER5=ポート5方向レジスタ BSR cntrlIIC_INIT ; IIC制御:初期化 BSR cntrlIIC_START ; IIC制御:スタート送出 MOV.B #H'A0,R0L ; 書込コマンド BSR cntrlIIC_SET ; IIC制御:バイト送出/ACK(NAK)受入 MOV.W @(+4,ER6),R2 ; adrs取得 MOV.B R2H,R0L ; adrs上位バイト BSR cntrlIIC_SET ; IIC制御:バイト送出/ACK(NAK)受入 MOV.B R2L,R0L ; adrs下位バイト BSR cntrlIIC_SET ; IIC制御:バイト送出/ACK(NAK)受入 BSR cntrlIIC_START ; IIC制御:スタート送出 MOV.B #H'A1,R0L ; 読込コマンド BSR cntrlIIC_SET ; IIC制御:バイト送出/ACK(NAK)受入 MOV.W @(+6,ER6),R2 ; data取得 EXTU.L ER2 ; dataロングワード化 MOV.L @(+8,ER6),ER3 ; datasz取得 read_iic_loop: XOR.B R1L,R1L ; 最終データフラグクリア DEC.L #1,ER3 ; datasz更新 BLT read_iic_exit ; データ列読込終了判定 BNE read_iic_data ; 最終データ判定 DEC.B R1L ; 最終データフラグセット read_iic_data: BSR cntrlIIC_GET ; IIC制御:バイト受入/ACK(NAK)送出 MOV.B R0L,@ER2 ; データストア INC.L #1,ER2 ; data更新 BRA read_iic_loop ; データ列読込繰返 read_iic_exit: BSR cntrlIIC_STOP ; IIC制御:ストップ送出 POP.L ER5 ; ER5復旧 POP.L ER4 ; ER4復旧 MOV.W R6,R7 ; スタックポインタ復旧 POP.W R6 ; ER6復旧 RTS ; 復帰 ; IICデータ列書込 ; VOID writeIIC(UB* adrs, UB* data, SIZE datasz) ; adrs 4(ER6) ; data 6(ER6) ; datasz 8(ER6) PUBLIC _writeIIC _writeIIC: PUSH.W R6 ; ER6退避 MOV.W R7,R6 ; スタックポインタ退避 SUBS #2,ER7 ; スタックポインタ補正 PUSH.L ER4 ; ER4退避 PUSH.L ER5 ; ER5退避 MOV.L #PDR5,ER4 ; ER4=ポート5データレジスタ MOV.L #PCR5,ER5 ; ER5=ポート5方向レジスタ BSR cntrlIIC_INIT ; IIC制御:初期化 BSR cntrlIIC_START ; IIC制御:スタート送出 MOV.B #H'A0,R0L ; 書込コマンド BSR cntrlIIC_SET ; IIC制御:バイト送出/ACK(NAK)受入 MOV.W @(+4,ER6),R2 ; adrs取得 MOV.B R2H,R0L ; adrs上位バイト BSR cntrlIIC_SET ; IIC制御:バイト送出/ACK(NAK)受入 MOV.B R2L,R0L ; adrs下位バイト BSR cntrlIIC_SET ; IIC制御:バイト送出/ACK(NAK)受入 MOV.W @(+6,ER6),R2 ; data取得 EXTU.L ER2 ; dataロングワード化 MOV.L @(+8,ER6),ER3 ; datasz取得 write_iic_loop: DEC.L #1,ER3 ; datasz更新 BLT write_iic_exit ; データ列書込終了判定 MOV.B @ER2+,R0L ; データロード/data更新 BSR cntrlIIC_SET ; IIC制御:バイト送出/ACK(NAK)受入 BRA write_iic_loop ; データ列読込繰返 write_iic_exit: BSR cntrlIIC_STOP ; IIC制御:ストップ送出 POP.L ER5 ; ER5復旧 POP.L ER4 ; ER4復旧 MOV.W R6,R7 ; スタックポインタ復旧 POP.W R6 ; ER6復旧 RTS ; 復帰 ; IICソフトウェア制御処理群 ; IIC制御:初期化 cntrlIIC_INIT: BSET #SCL,@ER5 ; SCL=出力 BSET #SDA,@ER5 ; SDA=出力 BSET #SCL,@ER4 ; SCL=1 BSET #SDA,@ER4 ; SDA=1 RTS ; 復帰 ; IIC制御:スタート送出 cntrlIIC_START: BSET #SDA,@ER4 ; SDA=1 BSET #SDA,@ER5 ; SDA=出力 BSET #SCL,@ER4 ; SCL=1 NOP ; ウェイト BCLR #SDA,@ER4 ; SDA=0 NOP ; ウェイト RTS ; 復帰 ; IIC制御:ストップ送出 cntrlIIC_STOP: BCLR #SDA,@ER4 ; SDA=0 BSET #SDA,@ER5 ; SDA=出力 BSET #SCL,@ER4 ; SCL=1 NOP ; ウェイト BSET #SDA,@ER4 ; SDA=1 NOP ; ウェイト RTS ; 復帰 ; IIC制御:バイト送出/ACK(NAK)受入 cntrlIIC_SET: BSET #SDA,@ER5 ; SDA=出力 MOV.B #8,R0H ; ループカウント設定 iic_loop_set: BCLR #SCL,@ER4 ; SCL=0 SHLL.B R0L ; MSBビットテスト&シフト BST #SDA,@ER4 ; SDA=0/1 BSET #SCL,@ER4 ; SCL=1 DEC.B R0H ; ループカウント更新 BNE iic_loop_set ; ループ判定 BCLR #SCL,@ER4 ; SCL=0 XOR.L ER0,ER0 ; 返値初期化 BCLR #SDA,@ER4 ; SDA=0 BCLR #SDA,@ER5 ; SDA=入力 BSET #SCL,@ER4 ; SCL=1 MOV.B @ER4,R0L ; ポート5データ取得 BCLR #SCL,@ER4 ; SCL=0 BSET #SDA,@ER5 ; SDA=出力 AND.B #(1<<SDA),R0L ; SDAビットマスク BEQ iic_exit_set ; ACK→スキップ MOV.B #1,R0L ; NAK→ER0=1 iic_exit_set: RTS ; 復帰 ; IIC制御:バイト受入/ACK(NAK)送出 cntrlIIC_GET: XOR.L ER0,ER0 ; 返値初期化 BCLR #SDA,@ER4 ; SDA=0 BCLR #SDA,@ER5 ; SDA=入力 MOV.B #8,R0H ; ループカウント設定 iic_loop_get: SHLL.B R0L ; ビット列シフト BSET #SCL,@ER4 ; SCL=1 MOV.B @ER4,R1H ; ポート5データ取得 BCLR #SCL,@ER4 ; SCL=0 AND.B #(1<<SDA),R1H ; SDAビットマスク BEQ iic_next_get ; SDA=0→スキップ BSET #0,R0L ; LSBビットセット iic_next_get: DEC.B R0H ; ループカウント更新 BNE iic_loop_get ; ループ判定 BSET #SDA,@ER5 ; SDA=出力 SHLR.B R1L ; 最終データ確認 BST #SDA,@ER4 ; ACK(NAK)設定 BSET #SCL,@ER4 ; SCL=1 NOP ; ウェイト BCLR #SCL,@ER4 ; SCL=0 BCLR #SDA,@ER4 ; SDA=0 RTS
このコード、なんと 約630KHz の転送レートでEEPROMの読み書きができた。使っているEEPROM(AT24C512)の公証最高転送レートが +5V時に1MHz だから、まずまずといったところだ。しかも、H8/3664のIICインターフェースはクロック 20MHz でも 最大571KHz である。非同期通信ができないという点では不利だが、スピード勝負では負けていないのだ。ただし、アセンブラ化したとはいってもやはりハードロジックではないため、オシロスコープの波形をみるとクロックのデューティーがギリギリだったりしている。おそらく事実上、これが最速だと(僕は)思う。コーディングに際してBST命令を多用してみたのだが、おわかりだろうか?これはキャリーフラグを指定ビット位置に転送する命令だが、バイトデータをシフトしてキャリーにMSBを送り出し、それをSDAのビットに反映する仕組みは、とても美しいシリアライズ法だと思う。
アセンブラの世界を久しぶりに体験した。もうすっかり「デジタル洞窟の囚人」なのである。
failed to fetch data: unkown error
failed to fetch data: unkown error
failed to fetch data: unkown error
failed to fetch data: unkown error
掲示板
soda. Quanto browse originale does medicare pay for 2014
5 mg prezzo farmacia 2012.
viagra generic baking soda. Quanto browse originale does medicare pay for 2014 5 mg prezzo farmacia 2012.