説明するのは、『AVR互換コア(その6 デバイス)』の記事の rtavr-wk07f あたり。
2倍速 動作タイミング
当初 postedge のみとか 1 クロックのなかで固定のタイミングだけで動かそうとしていた。が、全然無理だった。理由は、分散メモリ・ブロックメモリを同期型として使うため。
それ以外に、2倍速のGPR もある。この 2倍速のクロックと 標準のクロックの間に若干のスキューがあることを前提にしている。GPR は、ちょっとばかりややこしいことになった。まとめておかないとマズそうな気がする。
まず、標準速の CLK の生成について。
2倍速のクロックを外部から供給して、1X の 標準のクロックを生成することにした。だから、2倍速のクロックの方のエッジが必ず先行する。
______ ______ ______ ______
CLK2X ___| |______| |______| |______| |______
(1) (1) (1) (1)
_____________ ______________
CLK ____________| |_____________| |___
(2)--------------------------(2)
(t1) (t2) (t3) (t4)
____________ _____________
r_clk ____________| |______________|
おおげさに書くとこう。CLK2X の negedge で CLK を駆動している (1)。で 標準クロックで動作する回路とインターフェイスする場合は、CLK の negedge でラッチする (2)。
標準クロックで動作する回路から CLK2X 側が受け取る場合、基本どのエッジでも良い。標準クロック側は 基本 negedge で動作している。(t1)-(t4) 。
ただし、演算結果とか遅延のあるものは、(t1) (t2) で受け取らない。
逆に、CLK2X 側から信号を渡す場合、(2) のタイミングで確定していないといけない。(t4) で reg の値が確定しさらに遅延が加わるものは渡せない。
ちなみに、フェーズを知るために posedge CLK2X で駆動する r_clk というのも使っている。冗長のような気もするが、現状はこうなっている。
さて GPR からみて、実際どのようになっているか見てみよう。
(t1) posedge r_clk==0 ADDRAH,ADDRBH,(ADDRAL)
(t3) posedge r_clk==1 ADDRAL,ADDRBL, DI
(2) CLK negedge ADDRBL,ADDRBH
基本 (2) で変化する信号を 最初に受け取れるのは、(t1)。2X だからそれに (t3) を加えた posedge のタイミングが基本。(DI 以外の) これらの信号は、reg の出力そのものになるようにしている。(2) のタイミングのものは、標準クロックで動作する回路の延長とみなしている。
DI は、演算結果。標準クロックの 3/4 で受け取るから、CLK2X 側の条件は厳しい。逆に 演算自体の 遅延の条件は緩くなっている。
ちなみに、GPR を 標準クロックだけで組むと受け取るタイミングは、標準クロックの 1/2 である posedge CLK になるはずだ。演算側の遅延条件が厳しくなる。
(t1) posedge r_clk==0 INDEX(lo)
(t3) posedge r_clk==1 INDEX(hi),JA(hi)
(t2) negedge r_clk==0 JA(lo)
(t4) negedge r_clk==1 DOBH
(2) DOBL
s2_execute での Rd/Rr 入力である DOBH/DOBL はこのタイミング。DOBL は、ラッチしていて 標準クロックに同期済み。DOBH は (t4) だからスキュー分前にずれている。そうなると、遅延のない回路では negedge CLK では受け取れない。演算の入力や posedge CLK でラッチする書き込みデータに使うので問題ない。
INDEX は S1 で実行アドレス(EA)として使う。JA は icall などの ジャンプアドレス(EA)。
これらは、(2) のタイミングでラッチする。タイミングがさまざまだが、(t4) 以外なので問題ない。
標準クロック 動作タイミング
ここから先は 2倍速のことは考えなくて良い。(2倍速と比べれば)簡単なのだが、注意すべきことはある。
基本 negedge で動かすわけだが、ブロックメモリは、posedge 。あと周辺装置である IOR も posedge 。SREG や スタックポインタ(SP)も posedge 。
| S0 | S1 | S2 |
______ ______ ______
CLK _____| |______| |______| |______|
| | | | | |
PC INST | | STORE |
DECODE EXECUTE |
EA GPR STORE
GPR LOAD
1つの命令の流れをかくとこんな感じ。PC は早くから決まっているが、1/2 CLK 後でようやく 使われて、一定遅延後 INST が出てくる。
S0 は、ここからスタートして、デコードの一部 LD/ST の判断と PREDEC/POSTINC の指示を作る。
S1 は司令塔で、それ以外の指示を全部作る。GPR のロードもこのとき(S1 の期間)。
S2 では、EA は最初から決まっているが、アドレスデコードはそこから。RAM も 1/2 CLK 遅れで動作する。STORE の場合、用意したデータをこのタイミングで書くだけだから問題ない。LOAD の場合、1/2 CLK でロードを開始して 3/4 CLK で GPR に書き戻す。
結構厳しいタイミングなので、RAM も 2X を使って 1/4 CLK で読み込み 3/4 CLK で書き込みとしたいところだが、対処していない。
ブロックRAMは、200MHz で動作するはずで、ロードもかなり高速。分散RAM は普通のFF と同等でこちらも速い。だから大丈夫だとタカを括っているわけだ。対応策も検討しているわけで、対応は実際に困ったことになってからで良い。
だが、標準クロックのGPR を作ってみようとしたとき、ここがネックになる。忘れないようにしないと。
追記: ちょっとやってみた。試行錯誤したが、結局 CLK のかわりに CLK2X を使って、書き込みは CLK が H のときだけ というつくりにするのが良いという結果になった。
読み込みを無駄に 2 回するわけだが、2回目の読み込み -- 3/4 のタイミングで違う値になったとしても その結果を反映するタイミングがそもそも存在しない。これで問題なさそうだ。
この変更をすると、6 スライス減る。なぜなのだろう? (バグではなさそう)
ついでに、SPH (スタックポインタの 上位 8bit) の bit 数を設定できるようにしてみた。変更するビット数を制限することで 最適化で消すやりかた。4 にすると 7 スライスも減った。2 だと 15 スライスも減る。0 に固定すると 19スライス。ぎりぎりになったときのために仕込んでおこう。
ただ、こういう対処をするなら、標準クロックのGPR を作っても CLK2X 自体は必要ということになる。CLK2X をなくせないなら 標準クロックのGPR を作るのは あまり意味がなさそうな気がしてきた。
ついでに、ブロックRAM について補足
ブロックRAM を 標準の Verilog の記述だけで定義するには、かなりの制限がある。そして 失敗すると 分散RAM で実装しようとして いつまでも終わらない。18bit 分あるから 2bit 分を属性に使おうとしてみたがそれすら失敗して保留している。また 2 つのポートを 違うbit 幅にすることが可能だが 標準的な記述方法では定義できなかった。
あと ブロックRAM は 同期式で posedge で駆動する以外のことはできない。だからタイミングを変える方法ば 2X で駆動するぐらいしかない。
ブロックRAM は 初期値を与えて ROM として使える。初期値を与える方法は 2 通りあるが、データファイルを用意して $readmemh() 等で読み込むのがベスト。initial で値を代入する方法もあるが、最初に分散RAM で構成しようとするので 時間がかかる。
IOR の方は、非同期動作で、S2 の先頭から読み込みを開始する。だから SBI/CBI で使う read-modfy-write ができる。
フラグ値の先取り(フォワーディング)
SREG も IOR の一部であり、書き込むタイミングは postedge (STORE と同じ)。
だが、SREG_IN / SREG_OUT は別パスで出ていて、バス経由で書き込む時以外は SREG_IN で常に更新している。
SREG_IN を使って更新するのは、s2_execute 。参照するのは、条件分岐命令とスキップ命令。
スキップ命令は、現在の s2_execute の結果で現在実行中の s1_decode を 無効化する。条件分岐命令は、現在の s2_execute の結果で 次のサイクルの s1_decode を 無効化する。(それに加えて分岐も実行)。
演算結果の先取り(フォワーディング2)
s2_execute の後半 で 演算結果を書き戻すが、それと平行して GPR はレジスタ値(Rd/Rd)を 読み出そうとしている。
GPR は 2倍速であり 読み出しの 1 つは、前半で終わっている。順序が逆だから間にあうはずもない。
どうやるかというと、いま書き込むデータが 読み込んだレジスタのものならば セレクタで 書き込んだデータに置き換える。これが使えるのは、DOBL/DOBH 。これは、Rd/Rr と icall などの ジャンプアドレス(JA)。あと PREDEC/POSTINC なしのときの インデックスレジスタ(X/Y/Z) に使っている。
PREDEC/POSTINC ありの場合は、どうやっても無理で、命令自体を止める(パイプラインストール)。
止めれば競合が解消するわけで、問題を回避できる。
POSTDEC/PREINC とそのキャンセル(パイプラインストール)
- s0_fetch では、部分的にデコードしているが、PREDEC/POSTINCの LD/ST だと分かると S0_WB_INST=1 として s1_decode に知らせる。
- s1_decode では、今デコードしている命令が GPR への 書き込みをするものならば、パイプラインストールすることに決めて s0_fetch には S0_VALID=0 で通知する。
- s0_fetch では、PREDEC/POSTINC を S0_VALID でマスクしていて、GPR への指示をとりやめる。s0_fetch の副作用がある動作はこれだけなので、他は気にする必要はない。
そもそも PREDEC/POSTINC と 演算結果の書き戻しは同時にできない設計になっている。更新できるのは、2 レジスタまでで、PREDEC/POSTINC は 2 レジスタを更新する。
PREDEC/POSTINC では 1 レジスタしか更新しない場合が 圧倒的に多いので、当初の計画では、演算結果の書き戻しをまず行って、その後 2 レジスタを更新するケースでのみ、パイプラインストールさせようとしていた。が、無理があった。たとえ計画どおり作れてもそれでクロックを上げられないことになるなら本末転倒なのでやめた。
このしくみは、次のようにして実装している。
ついでに補足
AVR 互換コア命令の所要クロック数はおおむねオリジナルの AVR8L と同じにしている。AVR8L は単純化されていて mega や tiny よりクロック数が少ないものがある。
そして LD/ST 命令の所要クロック数は、オリジナルよりさらにすくなく 基本1クロック。2 クロックかかる場合があるが、それは上で説明したケースのみ。
追加: POSTDEC/PREINC と SKIP 命令
- SKIP 命令 での判断を S1 でやってしまう。-- (1)
- SKIP 命令 であれば SKIP する/しないに関わらず パイプラインストールさせる。-- (2)
- POSTDEC/PREINC のキャンセルを S1 でできるようにする。-- (3)
テストの段階で設計ミスしていたことに気がついた。
条件分岐命令や JUMP/CALL などは、S1 実行中に 次の命令の S0 を無効化している。これに対して SKIP 命令は、S2 実行中に 次の命令の S1 を無効化する設計になっている。(それすらバグっていたが)
設計ミスというのは、POSTDEC/PREINC があっても S0 が実行されてしまうこと。
対処する方法は色々考えられる。
(1) について:
SKIP 命令の SKIP 条件には次の 3 つがある。レジスタの 指定ビット , 2 つのレジスタの一致 , IOレジスタの指定ビット。
現状どれも S2 でしかアクセスできない。レジスタに関しては もともとフォワードする仕組みがあるので 信号を引き出すだけで対処できる。だが、 IOレジスタは(擬似)バスアクセスであり、 アクセスパスがない。1 bit のパスを付けて dual-port 化 する方法はあるが規模が増える。時分割で dual-port 化 する方法もある -- IOレジスタに対する アドレスを 1/4 クロック 前にずらせば良い。ただ IOレジスタのアクセスは セレクタの塊だから ボトルネックになる可能性がある。フォワードしなくとも 前にずらしたいところだ。
(2)について:
SKIP 命令 + POSTDEC/PREINC 付き LD/ST命令 の組み合わせだけが 1 クロック余計にかかる。出現確率からいうと 性能にほとんど影響がない。
パイプラインストールについては、仕組みはあるので条件追加で済む。ただ、SKIP する場合 2 クロック分 SKIP させなければならない。これは、2 word 命令の SKIP と同じようなもので、(後述の)2word LDS/STS 命令用の処理に条件を追加すれば良い。
(3)について:
POSTDEC/PREINCの処理自体は S1 である。すみやかに(1/4 クロック以内)条件が確定するのであれば S1 に入って からのキャンセルも可能だ。(1) が可能ならば、(3) は当然可能でありむしろ条件が緩い。
対処方法としては (2)が一番簡単であり これをまず選択する。だが、(3) も 魅力がある。(3) という選択をした上で IOレジスタに対する アドレスを 1/4 クロック 前にずらしたり、レジスタの条件を S2 開始時にラッチしてしまうのも (遅延)性能的に有利になるかも知れない。回路規模も増えるとはかぎらない。
他の方法もいずれ検討したい。
2word LDS/STS 命令
avr-gcc (というか binutils) で 1word LDS/STS 命令に対応できていないことが分かったので、急遽 2word LDS/STS 命令を作って バグ付きの gcc に対応できるようにしている。
2word 命令は、これだけ。で、条件スキップ命令では、2word 目もスキップしなければならない。後で気がついて対応コードを入れたが未テスト。
その後、AVR_Toolchain を改造していくことにした。まだ出来てはいないが 1word LDS/STS に対応する。
対応すると、全部 1word LDS/STS にしてしまうので、2word LDS/STS 命令 は使えなくなる。だから この追加を enable にするのは 推奨しない。
ちなみに LDS/STS できるのは、RAM の 下位 128B のみ。それより後ろに変数を置くことはできるが、ポインタを使ったアクセスしか出来なくなる。ポインタへの代入は LDI 命令になるので シンボルも使える。
縮小 LDD/STD 命令
avr-gcc の命令生成部を 直しているのだが、LDD/STD 命令 が便利そうに見える。AVR8L はレジスタが少なく スタックに変数を置くことが多くなるが、ロード・ストアに 5 命令かかる。フレームポインタの Y に offset を加算してアクセスして 元に戻すために減算するわけだ。ADIW/SBIW 命令も使えないから ポインタの加減算 は 2 命令。
アクセス可能な範囲が 0 - 63 までという条件があるが、LDD/STD 命令 だと 1 命令でできる。AVR8L は命令効率が悪いからこれだけでも欲しい。
LDD/STD 命令 と 1word LDS/STS 命令 は命令スペースが重なっているが、検討したところ、オフセットを 0 - 31 に制限すれば 両立可能なことが分かった。
なので、2word LDS/STS 命令などより、こっちを積極的にサポートすることにした。
実装してみたが、基本的な動作はできている。あと実際の AVR は 2 クロックかかるが、こいつは (無条件で) 1 クロック。
サポートするための規模の増加は、17 スライス。50A にも入る。
あと 自製 avr-gcc でこの命令を使うには、オプションが必要。
avr-gcc -mmcu=rtavr40 -mreduced-lddstd
とやって使う。
avr-objdump での逆アセンブルには、
avr-objdump -mavr201 -d xxx.o
とする。そうしないと 1word lds/sts まで ldd/std として出力される。
タイムチャート
どの期間信号が有効で、いつ取り込むかについてのタイムチャートを作ったので添付しておく。
取り込むタイミングが、なかなか複雑なことになっている。
0/4 1/4 2/4 3/4 0/4 1/4 2/4 3/4 0/4 1/4 2/4 3/4 0/4
| S0 | S1 | S2 |
__ _____ _____ _____ _____ _____ _____
|_____| |_____| |_____| |_____| |_____| |_____| |___
__ ___________ ___________ ___________
|_____________| |____________| |____________| |
PM_OUT =========================
ADDRAL =========================
ADDRBL =========================
@
ADDRAH ========================
ADDRBH ========================
@
INST ========================
DI(N-1) ................========
@
| | | |
INDEX(lo8) =========================
INDEX(hi8) =========================
JA(lo8) ......===================
JA(hi8) ......===================
@
| | | |
DOBL ========================
DOBH ========================
CMD_XX ========================
EA ========================
@
| | | |
__ _____ _____ _____ _____ _____ _____
|_____| |_____| |_____| |_____| |_____| |_____| |___
XX_DOB .....===================
DI .............===========
@
| | | |
PC =========================
@
PM_OUT(N+1) =========================
デバイス関係
- (削除) ADC , AIN → (追加) USART
ADC, AIN を削除して、空いたアドレスや割り込みを使って USART を入れる。 - (変更) PORT 新インターフェイス → 旧インターフェイス
新インターフェイスは機能が増え プルアップ専用の PEUx も追加になっている。
これを普通の インターフェイスに変更。できたら新インターフェイスも作ってみたいが、多分不可能。 - (削除) PCINT0/1/2 → (追加) INT1
割り込み線の PORTへの割り当ては自由にできるので、PCINT0/1/2 を廃止して INT1 を追加。
必要なら INT2/3 も追加することを検討する方針。
GIFR/GIMSK は bit1 に割り当て。ISC1 は、MCUCR の上位から割り当て (bit5,4)
MCUCRには BOD の許可や SM(スリープモード), SE(スリープ許可)がある。これらの機能を全部無視すれば、4 つの ISC を入れられる。ただ、SLEEP 命令はいずれサポートしたい。SM や SE が欲しくなるかも知れないので INT2/3 を入れるかどうかは保留中。 - (削除) OSCCAL , CLKPSR, CLKMSR , PRR , QTCSR , NVMCMD, NVMCSR , PCMSK0/1/2
このあたりは、全く対応するつもりがない。というか対応出来ない。 - (保留) WDTCSR , CCP
WDT はいずれ対応したい。新インターフェイスとして CCP があるが、WDT しか対応しないのなら 旧インターフェイスにするかも。 - (保留) Timer1
単に 16bit 版の タイマーが欲しいだけなので、楽をして Timer0 をベースにしようかと。 - (保留) TWI
これまた、スレーブ専用の新インターフェイスになっている。
ドライバも新規で書く必要があるのなら、既存のものを採用するかも。ただ、構想上、同じ線でマスターとスレーブ両方実装できる必要がある。 - その他
SPI と Timer0 は実装済み。基本はサブセットで細いところまで互換にするつもりはない。
ちょっと上記とは内容がちがうが、ここにメモ。
モデルとした AVR は ATtiny40 。avr-libc のヘッダファイルとかを流用できるようにしようと考えてこれをモデルにした。ただし、互換は目指していない。... というより無理。ADC/AIN などはなから無理だし。
そのうえ USART も付けたい。
現状は、gcc のビルドも出来ていて、rtavr 専用のヘッダも用意する方向で考えている。専用のヘッダを使った場合、rtavr を自由に config して、それに ヘッダを合わせるようなことも検討中。
それでも モデルは ATtiny40 なのだ。何を付けて、何はサポートしないかちょっと書いておく。
関連記事:
- AVR互換コアの仕様(メモ)
- AVR互換コアの仕様(その2)
- AVR互換コアの仕様(その3)
- AVR互換コア(その4)
- AVR互換コア(その5)
- AVR互換コア(その6 デバイス)
- AVR互換コア(設計メモ) この記事
- Tiny20/Tiny40
- AVR Toolchain
- AVR Toolchain (その2)
著作権について
ここで提示しているコードは正しく動作しないとはいえ、既に著作権は発生しています。
著作権は、すzが保持しており放棄はしていません。
教育目的および私用目的では、もともと著作権の範囲外なので自由につかえます。また、ライセンスとして、GPL を適用しています。GPL に従う範囲において 個別の許可なく使用することができます。許諾のための連絡も不要です。
なお、GPL なので、生成したバイナリを作り直せる範囲のソース開示が必要になります。FPGA だと 通常 チップ全体になってしまいます。また、開示したソースを GPL の範囲で再利用されることを妨げることはできなくなります。このコードを利用する場合この点に留意してください。
個別の許可を得れば、GPL 以外の条件での使用は可能です。が、作業中のものには許可は出さない予定です。作業が完了(もしくは中断)したとき、ライセンスは見直します。
なお、すでに公開してしまったものの、ライセンスを取り消すことはできないと考えていますが、ライセンスの追加は可能です。また、新しく公開するものについては、ライセンスしないことすら可能で、どのような制限もありません。変更する可能性がありますので、この点にも留意してください。