いまどの段階か整理しておく。
作ろうとしているのは、Tiny10 などの "the Reduced Core TinyAVR" の互換プロセッサコア。レジスタが 16 個と少ないが、avr-gcc は対応している。これを採用してよりコンパクトなコアを作りたい。
まずは、汎用レジスタ(GPR)のデータ構造をだいたい決めて 、それだけを verilog で 書いてみた。次に RAM と ROM 。今は I/Oレジスタ。全部プロトタイプで簡素なもの。こうやって コアのインターフェイスを決めようとしている段階。
現時点のものをリストして簡単に特徴を記載すると --
- rtavr_gpr_16.v (ver 003)
分散 RAM 16x8 デュアルポート を中心にして構成。2 倍速で動作させて 1 クロックで 4 load 2 store にしている。 X/Y/Z レジスタの PREDEC/POSTINC の機能も含んでいる。 - rtavr_sram_2KB.v (ver 002)
ブロック RAM を 使った 2K x 8bit RAM 。8bit バス接続。もうひとつのポートは未使用。 - rtavr_rom_8KB.v (ver 001)
ブロック RAM 4 つ を 使った 4K x 16bit ROM 。命令用の 16 bit ポート と 8bit バス接続 の 2 つのポートがある。8bit バス接続の方は Write 可能にしている。 - rtavr_ior_port.v (ver 001)
I/O レジスタのインターフェイスを決めるために書いてみた PORT 。 - rtavr_ior.v (ver 001)
(新) PORT だけではイメージがつかめないので、I/O レジスタのトップレイヤ を作った。Timer0(8bit) も作り PORT と合わせて 1つのファイルに格納している。 (上の rtavr_ior_port.v は Obsolute)
最終的には、Tiny40 のサブセット + USART にするつもり。
注) ところで、これらのソースは、tabset 8 を想定しているが、ちょっとインデントがおかしくなっている。Web Pack ISE だと Edit → Preferences → ISE Text Editor での設定が標準では Tab width 3 になっている。 更新するときは、tabset 8 で整形しなおすつもり。
こういうわけで、I/O レジスタ に入った。新たに決めようとしているのは、割り込み。Timer0 からの割り込みラインは作ったが、それをどう CPU につなげるのが良いか検討中。
CPU につなげるものは、他にも 沢山あるが、ステータスレジスタ, スタックポインタ が最低限必要でこれも合わせて検討する。
あと スライスを沢山使っているのが悩み。現在の rtavr_ior_port.v (ver 001) は、
Number of Slice Flip Flops : 121
Number of 4 input LUTs : 152
Number of occupied Slices : 137
Total Number of 4 input LUTs : 161
Number of bonded IOBs : 49
FPGA 内部のモジュールの接続にバスを使おうとしたのだが、やはりダメなようだ。... と言っても エラーになるのではなく勝手に変換されるようだ。
- Spartan-3 は、"内部トライステート・リソースを持たない" そうだ。Xilinx の FPGA には持っているものもあるみたいだが、一般的ではないらしい。
- 良くはわからないが、内部トライステートが可能でも トライステート・ドライバ数が増えるとダメらしい。その時は マルチプレクサ に自動で変換するものらしい。(syn_tristatetomux がキーワード?)
- ただ ROM で最初に使ったときは、logic に変換したというメッセージが出たが、pull-up がナントカと出ていたから ワイヤード OR ? マルチプレクサ に pull-up は関係なさそうだし何だろう?
- あと、 rtavr_ior.v では、下位モジュール同士で接続しているが、エラーにはなっていない。
さて、どうするのが良いのだろう? バス使った方が記述が楽なのだが .. 少なくとも同一モジュール内では、明確に マルチプレクサ を使おうかとは思う。
ここを見ると、CoreConnect, AMBA, WISHBONE といったオンチップバスが標準として定義されているらしい。これらは、一体どうやってバスを実現しているのだろう? ちょっと勉強してみよう。 - WISHBONE (Opencores.org) に行くと、仕様書と 他のバスとの比較の文書がある。
そういうわけで、いろいろやってみた。
- rtavr_ior.v (ver 002)
新しく作った I/O レジスタ(IOR) モジュールで、 - (1) 完全マルチプレクス + 上位で全部アドレスデコード
- (2) 完全マルチプレクス + サブモジュールでもアドレスデコード
- (3) それぞれ自モジュールだけマルチプレクス してモジュール間は (記述だけ)トライステート
の 3 つの規模を比較してみた。
// DO: (1)NORMAL (2)NORMAL (3)TRISTATE(FAKE)
// (SELF_DECODE) (SELF_DECODE)
// Number of Slice Flip Flops: 121 121 121
// Number of 4 input LUTs: 185 170 200
// Number of occupied Slices: 153 145 163
// Total Number of 4 input LUTs: 194 179 209
// Number of bonded IOBs : 49 49 49
// IOB Flip Flops : 48 48 48
// Number of BUFGMUXs : 1 1 1
// Number of RAMB16BWEs : - - -
結果はこうなった。 - サブモジュールでもアドレスデコードすると 二重に記述するから効率が下がると思ったのだが、結果は逆になった。
- 論理的にはまったく同一で、モジュール間のインターフェイスだけが違うのだが、結果が違う。gcc の inline 関数のようなものではないようだ。
- トライステート記述だと効率が落ちた。楽をすると結果も良くない.. ということか。
rtavr_ior.v (ver 002) は、470 行もある。いろんな組み合わせを試せるように、記述も ifdef で切り分けているから ソース規模が増えている。これからは、(2) を採用して シンプルにしていこうと思う。 - rtavr_sram_2KB.v (ver 003)
- rtavr_rom_8KB.v (ver 002)
- rtavr_rom_4KB.v (ver 002)
そう決まれば、RAM/ROM も直しておこう。(2) を可能にするため、in/out に分ける。(2) 専用なら OE が不要になるが、OR でつなげることも出来るように OE を活かす。そうなると (3) も 残せるので、比較のために 残しておく。
ついでに 4KB の ROM も作っておく。-- 50A に実装する場合 4KB までしか無理。そして (当面は) 50A に AVR を入れるのがやっと。だから 4KB が必要十分。
// Clock to Setup 4KB 8KB
//on destination clock CLK (2): 1.996 1.881
(3): 1.900 2.101
ROM の Clock to Setup を 比較してみた。が、2ns 程度ということしか分からなかった。
これで、書き換え終了。(2) をメインにするが、セレクタではなく OR でつなげるのもいずれ試してみたい。 - rtavr-wk01.tar.gz
バージョンが良く分からなくなると困るので、ここまでを tar で固めて置く。(ファイル形式も変更: CR+LF → LF)
次は コアに着手する。モジュールの構造で サイズが変わることが分かったので、SREG や スタックポインタ、割り込みのインターフェイスは今の時点では FIX しない。
なお、これからは、個々のファイルはリンクしない。単独でリンクしても意味はなさそう。
少なくとも同一モジュール内では、明確に マルチプレクサ を使うというルールにした。となると、面倒なのは上位モジュール。
今までは、サブモジュールで勝手にアドレスデコードする方が効率が良かったのだが、サブモジュールもマルチプレクスするなら、上位モジュールでも アドレスデコードしないといけない。
コアの設計(1) s0_fetch
コアの設計をするには、まず命令表が必要だ。opcode がちゃんと分かるもの。
『クレア工房:AVRマイコンの命令』を参考資料にしようと思う。あと 『Tiny20/Tiny40』の記事で、整理しているので、それも参照。
さて、最初に作るのは、命令コードを取ってくるところ。PC をインデックスに ROM から読み INST に格納するだけ ... ではない。
少なくとも、設計したGPR の 4 つのレジスタ のアドレスをこの時点で FIX しないといけない。
input [3:0] ADDRAL, ADDRAH, ADDRBL, ADDRBH;
こういう風に名前を決めたから以降はこれを使う。
それぞれの役割は、
これをもっと具体的な命令で定義する。
s0_fetch でやること
PC[11:0] から rom を読み込み その出力 PM_OUT[15:0] を
INST[15:0] に書きこむ。
つぎの値を確定する。
ADDRAL,ADDRAH の組:
0xa,0xb: (26 - 16, 27 - 16)
LD.Rd.X 1001:000d:dddd:1100
ST.X.Rr 1001:001r:rrrr:1100
LD.Rd.X++ 1001:000d:dddd:1101
ST.X++.Rr 1001:001r:rrrr:1101
LD.Rd.--X 1001:000d:dddd:1110
ST.--X.Rr 1001:001r:rrrr:1110
0xc,0xd:(28 - 16, 29 - 16)
LD.Rd.Y 1000:000d:dddd:1000
ST.Y.Rr 1000:001r:rrrr:1000
LD.Rd.Y++ 1001:000d:dddd:1001
ST.Y++.Rr 1001:001r:rrrr:1001
LD.Rd.--Y 1001:000d:dddd:1010
ST.--Y.Rr 1001:001r:rrrr:1010
0xe,0xf:(30 - 16, 31 - 16)
LD.Rd.Z 1000:000d:dddd:0000
ST.Z.Rr 1000:001r:rrrr:0000
LD.Rd.Z++ 1001:000d:dddd:0001
ST.Z++.Rr 1001:001r:rrrr:0001
LD.Rd.--Z 1001:000d:dddd:0010
ST.--Z.Rr 1001:001r:rrrr:0010
PM_OUT[7:4] , INST[7:4] (一クロック遅れ)
それ以外
(例)ADD 0000:11rd:dddd:rrrr
IN 1011:0AAd:dddd:AAAA
OUT 1011:1AAr:rrrr:AAAA
POP 1001:000d:dddd:1111
PUSH 1001:001d:dddd:1111
SBRC 1111:110r:rrrr:0bbb
SBRS 1111:111r:rrrr:0bbb
ORI 0110:KKKK:dddd:KKKK
ADDRBL,ADDRBH の組:
0xe,0xf:(30 - 16, 31 - 16)
IJMP 1001:0100:0000:1001
ICALL 1001:0101:0000:1001
PM_OUT[3:0] ,PM_OUT[3:0]
それ以外
(例)ADD 0000:11rd:dddd:rrrr
PREDEC の値
LD.Rd.--X 1001:000d:dddd:1110
ST.--X.Rr 1001:001r:rrrr:1110
LD.Rd.--Y 1001:000d:dddd:1010
ST.--Y.Rr 1001:001r:rrrr:1010
LD.Rd.--Z 1001:000d:dddd:0010
ST.--Z.Rr 1001:001r:rrrr:0010
POSTINC の値
LD.Rd.X++ 1001:000d:dddd:1101
ST.X++.Rr 1001:001r:rrrr:1101
LD.Rd.Y++ 1001:000d:dddd:1001
ST.Y++.Rr 1001:001r:rrrr:1001
LD.Rd.Z++ 1001:000d:dddd:0001
ST.Z++.Rr 1001:001r:rrrr:0001
あと I/O レジスタ。GPR と同じタイミングと決めたから、アドレスを FIX する。
s0_fetch でやること(2)
IOR_ADDR の値
{ PM_OUT[10:9], PM_OUT[3:0] }
IN 1011:0AAd:dddd:AAAA
OUT 1011:1AAr:rrrr:AAAA
{ 1'b0 , PM_OUT[7:3] }
CBI 1001:1000:AAAA:Abbb
SBIC 1001:1001:AAAA:Abbb
SBI 1001:1010:AAAA:Abbb
SBIS 1001:1011:AAAA:Abbb
これ以外にメモリ空間からのアクセスがある。GPR/IOR のアクセスメソッドはこれ以外にないわけだから、これもセレクタで切り替えなければならない。だが、ここでは枠だけ考えておく。
s0_fetch を作ってみた。
- rtavr-wk02.tar.gz
- rtavr_s0_fetch.v (ver 001) + rtavr_rom_8KB.v (ver 002)
最初は ROM なしで作ったのだが、性能が分からないので、8KB ROM を 内蔵する版も作った。ちなみに、ファイル単位のリンクを作らないつもりだったが、気が変わった。-- やはり簡単に内容を見れた方が良いような気がしてきた。
// - with 8KB ROM - NORMAL(old) NORMAL(new) NO_EXACC
// Number of Slice Flip Flops: 34 24 24
// Number of 4 input LUTs: 47 44 39
// Number of occupied Slices: 39 36 33
// Total Number of 4 input LUTs: 50 45 39
// Number of bonded IOBs : 92 91 84
// IOB Flip Flops : - - -
// Number of BUFGMUXs : 1 1 1
// Number of RAMB16BWEs : 4 4 4
// Clock to Setup
//on destination clock CLK 4.436 4.101 3.660
new : remove EXACC_GPR
結果を先に書くとこう。今度は思ったよりは小さい。ちなみに NO_EXACC は、メモリ空間からのアクセスなし版で比較用。
性能は、余裕ありまくり。これなら確実に 他がボトルネックになる。
内容は、上記で書いたものを verilog に変換しただけ。
... 実は、IOR でロードするものがなかったら SREG をロードするようにしてみた... のだが そのタイミングで 前の命令が s2_execute を実行しているので 意味がない。... とは思うが役に立つかも知れないので残しておく。
追記: 間違い訂正
普通の Tiny Core だと アドレス空間は、先頭に GPR (32B) 次に IOR (64B) があって、その後 RAM が続く。だが こいつは、先頭に IOR (64B) があって、その後 RAM。要するに GPR は メモリ空間からアクセスできない。-- 結構楽になる。
次は、 命令デコードを後回しにして s2_execute にしようかと思う。... ところで、s3_writeback を置く予定で考えていたが、s2_execute でメモリアクセスした方が良いような気がしてきた。そのあたりも合わせて検討する。
s2_execute を先に作って、それのための信号を 命令デコードで用意するという方針で、先に s2_execute を作るのは間違っていないはず。それで、ちょっとやっているのだがなかなかに手強い。
それはともかく、いつも混乱するのでメモ。
-----------------------------------------------------------------------
PC[11:0]
-----------------------------------------------------------------------
(S0_fetch) rom
-----------------------------------------------------------------------
INST[15:0] PREDEC/POSTINC ADDRAL ADDRAH ADDRBL ADDRBH
-----------------------------------------------------------------------
(S1_decode) gpr
-----------------------------------------------------------------------
DOAL DOAH DOBL DOBH
-----------------------------------------------------------------------
(S2_execute) ---- ADDR --- Rd Rr
-----------------------------------------------------------------------
DI
-----------------------------------------------------------------------
混乱するのは、状態を覚えるラッチ類。これらは、どこかのステートに属するというより、ステートの間にあると考えたほうが混乱しないようだ。
今 注目しているのは、S2_execute なわけだが、DOBL / DOBH の役割を それぞれ Rd / Rr に固定したほうが良さそう。(ICALL を除く) 。DOAL/DOAH の役割は、インデックス・レジスタ用。S2 の頭で メモリ空間への 読み書きの準備が出来ている。ストアする場合は、Rr を 書き出すし、ロードする場合は、S2 の最後までに 読み込んだデータを DI に送り込める。
ちなみに、ストア系の命令は、Rd の位置に Rr が来る。ちょっと具合が悪いので、切り分けて (Rr 用の) DOBH からも 値が出てくるように変更した。
算術命令は、基本 Rd/Rr を入力として DI に出力する。それにマッチしているので都合が良い。
そうなると IOR も同じタイミングでアクセスしたくなる。... というわけで IOR を S0_fetch で扱うのはヤメ。
さて、S2_execute が手強いわけだが、そもそも基本方針でつまる。普通は、ひとつの ALU のロジックを決めて、配線をそこに集める。AVR Core は ALU が別コンポーネントになっているし、KX_AVR も (よく分かっていないが) 同じようなつくりのようだ。
だが、配線の上に ALU を載せていくという設計もある。こういうつくりだと、ALU が 2 つ 3 つ必要になる。ただし、メインの ALU は Rd/Rr を入力して DI に出力するものになるので、それは同じ。
で、こっちの方が C 言語のプログラム風で 記述がしやすい。Navre も こっちの方針のようだ。配線の間に組み合わせ回路を置くのは別に悪い考えではないようにも思う。問題はどちらが 効率が良いのか? -- 結局両方作ってみて比べることになりそう。
あと、命令のデコードの方針。IOR のテストで、アドレスデコーダをローカルに持った方がスペース効率が良いことは分かった。S2_execute でも同じような話がある。(S2 で) if 文が必要なくなるぐらい、(命令デコードで) 条件を細分化してしまう手もあるわけだ。だが、そうすると 却ってスペース効率が悪くなる。インターフェイスをどうするかについても悩ましい問題だ。
ところで、後で気がついたこと。なんと LDS/STS 命令が 1 ワードのものに置き変わっている。
IN 1011:0AAd:dddd:AAAA # load from I/O port to register
OUT 1011:1AAr:rrrr:AAAA # store to I/O port from register
LDS 1010:0kkK:dddd:kkkk # 16bit
STS 1010:1kkK:dddd:kkkk # 16bit
IN/OUT 命令の仲間のような位置づけ。アドレスも 6bit分の kkkkkk は IN/OUT と同じで、その上の 2bit が (K == 0) ? "10" : "01" 。要するに RAM の先頭から128B のみがアクセスできる。
あと LD/ST 関係のメモ:
X++ とか、PREDEC/POSTINC したレジスタを ロード・ストアする場合の動作は未定義。だが、次の命令で参照する場合、正しい値が取れないといけない。今の設計では、上位バイトが変更になる場合、1クロック遅らせるから 上位バイト自身は問題ないはず。下位バイトが変更になる場合、クロックの前半で値が変わるから、Rr (DOBH) での読み出しは問題ないはず。Rd (DOBL) での読み出しは、前の値が読めてしまう。また Rd の書き込みデータは読もうとしても、常に前のデータしか見えない。
上の図で言うと(S1_decode)で1クロックかけて DOAL,DOAH,DOBL,DOBH の値を決めるが、同時にひとつ前の命令が、(S2_execute ) の状態で 更新するデータを 作っていて、それは DI の入力になる。DI のレジスタ番号も GPR は知っている (そうでなければ更新できない)。
よくよく考えれば、更新しようとしているデータが何であれ、DOAL,DOAH,DOBL,DOBH の値を決める前に確定するから、先取りすればよいわけだ。
ただし、BL,BH はそれで良いが、AL,AH はどうなのか? これはインデックスレジスタ値で、更新値が決まる前に参照して、さらに AL は更新までしてしまっている。値が決まっていないわけだから 1クロック遅らせて実行するしかない。なお、1クロック遅らせることを決めるのは、自分の命令の場合は、S0_fetch 。S1_decode では次の命令だけ止めることが出来る。(S1_decode = 次の命令の S0_fetch)
もっと正確に書いとかないとコードに落とせない。混乱するのでもう一度書いておくが、後ろの命令との競合ではなく実行中のひとつ前の命令との競合。
(1) POSTINC/PREDEC しないケースは、BL,BH と同じように置き換えて問題ない。
問題があるのはPOSTINC/PREDECするケース。
(2) AH は 更新されるとは限らないが、AL の更新後 - S1_decode時でないと分からない。つじつまを合わせることは不可能ではないが、面倒。POSTINC/PREDECするケース で AL/AH が競合するなら、1 クロックずらすことで対処したい。
(3) このようなことが起きるのは、X を更新した次の命令で、X をインデックスに ++ してアクセスするような場合。ループの中では起きない。
(4) そもそも、普通のAVR で ST は 2 クロック。Tiny10 は POSTINC が 1 クロックで PREDEC が 2 クロック。こいつは 両方1クロックだが、 前後共に 1 クロックづつ入って 3 クロックになる場合がある。ただし条件があるので避けるコーディングは可能。
ところで、同じような話がある 。ひとつは条件分岐。AVR は、分岐する場合でも、2 クロックしかかからない。S1 になった時点で、PC が決まっていて次の命令は既に読み込もうとしている。だから間に合わない。S1 終了時に 読み込んだ命令をキャンセルして 分岐先の PC にすると やっと 2 クロックでの分岐ができる。条件分岐命令は、S1 での実行になるのだが、ひとつ前の命令が実行中で 条件を設定中なわけだ。上で書いたのと同じようにフラグは先取りする必要がある。
もうひとつは、条件スキップ命令。CPSE , SBRC/SBRS, SBIC/SCIS の 3 種類。CPSE は演算結果でスキップするかどうか決める。SBRC/SBRS はレジスタのビット値で SBIC/SCIS は I/O レジスタのビット値。
これらは、S2 で決まる値。スキップ対象の次の命令の S1 で先取りして、無効にするかどうか決めないといけない。
結構面倒な話だが、気になっていたのは、IOR の アクセスタイミング。このように処理するのなら、決めた通りで良いようだ。ちなみに、今まで"先取り"と書いて来たが"フォワーディング"という用語がある。でも長いので"先取り"で通す。
(AVR互換コアの仕様(その3)に続く)