(その2)で書いたことを実装中だが、まだ 見せられるレベルで 作れていない。だが、一部のインターフェイスは FIX しつつある。課題とかも含めてここで整理しておこうと思う。
(注意:この記事はインターフェイスの変更があったら随時更新するつもり)
RAM -- まずは簡単な方から、
module rtavr_sram_2KB(
input CLK,
input RESET,
input WEA,
input [10:0] ADDRA,
input [7:0] DIA,
output [7:0] DOA,
input WEB, OE,
input [10:0] ADDRB,
input [7:0] DIB,
output [7:0] DOB
);
ROM
module rtavr_rom # (
parameter SIZE = 1024
// parameter SIZE = 2048
// parameter SIZE = 4096
) (
input CLK,
input RESET,
input [11:0] ADDRA,
output [15:0] DOA,
input WEB,OE,
input [12:0] ADDRB,
input [7:0] DIB,
output[7:0] DOB
);
でも、もし汎用の書き方で ブロックRAM にうまく嵌ることが出来れば、それを採用するつもり。そのときは、RAM も統合可能なようにする。
IOR -- 結構悩ましい。
module rtavr_ior (
input CLK,
input RESET,
input WR,RD,
input [7:0] DI,
input [5:0] ADDR,
output [7:0] DO
`ifdef IOR_HAVE_SREG_SP
, input [7:0] SREG_IN
, input [15:0] SP_IN
, output [7:0] SREG_OUT
, output [15:0] SP_OUT
`endif
// external I/O
`ifdef IOR_HAVE_PORTA
, inout [7:0] PORTA
`endif
`ifdef IOR_HAVE_PORTB
, inout [7:0] PORTB
`endif
`ifdef IOR_HAVE_PORTC
, inout [7:0] PORTC
`endif
// interrupt handling
, input RST_INT0
, output INT0
, input RST_TIM0_OVF
, output TIM0_OVF
, input RST_TIM0_COMPA
, output TIM0_COMPA
, input RST_TIM0_COMPB
, output TIM0_COMPB
);
parameter INT0_BIT = 18; // PC2
一方 TIMER0 は外すことは、あまり考えていない。たぶん使う。
GPR -- ここから CPU 内部モジュール。
module rtavr_gpr_16(
input RESET,
`ifdef USE_DMY_CLOCK
input CLK2X_IN,
output CLK_OUT,
`else
input CLK2X,
input CLK,
`endif
input WE,
input PREDEC, POSTINC,
input [7:0] DI,
input [3:0] ADDRAL,
input [3:0] ADDRAH,
input [3:0] ADDRBL,
input [3:0] ADDRBH,
output [7:0] DOAL,
output [7:0] DOAH,
output [7:0] DOBL,
output [7:0] DOBH
`ifdef CHECK_CONFLICT
, output DI_CONFLICT
`endif
, output WB_VALID
, output [11:0] JA < << (追加4)
, output [15:0] INDEX < << (追加4)
);
o ADDRAL/DOAL (PREDEC/POSTINC)
インデックスレジスタ 下位バイト(XL/YL/ZL)
o ADDRAH/DOAH/DI (WE/PREDEC/POSTINC/WB_VALID)
インデックスレジスタ 上位バイト(XH/YH/ZH)
+ Rd WRITE
o ADDRBL/DOBL
Rd READ + ICALL/IJMP 下位バイト
o ADDRBH/DOBH
Rr READ (STORE/OUT の Rd を含む)
+ ICALL/IJMP 上位バイト
先取り用ポートの追加。実効アドレスは、S2 で確定していけないので、S1 に処理を移動。それに伴い 早い時期から読めるように変更。逆に DOAL/DOAL は使わなくなる見込み。(ただし、まだ残しておく)
- DOAL/DOAL は使わないので削除
S0_fetch/S1_decode は入力・出力をはっきりさせないと混乱するので独立したモジュールにする。S2_execute については、関連モジュールが多いので、(今のところ)TOP レベルにする。S3 は存在せず、3段パイプラインの予定。
S0_fetch -- まずは、これから。
module rtavr_s0_fetch (
input RESET,
input CLK,
`ifdef INCLUDE_ROM
input [11:0] PC,
input WEB, OE,
input [12:0] ADDRB,
input [7:0] DIB,
output[7:0] DOB,
`else
input [15:0] PM_OUT,
`endif
output [15:0] INST,
output[3:0] GPR_ADDRAL,
output[3:0] GPR_ADDRAH,
output[3:0] GPR_ADDRBL,
output[3:0] GPR_ADDRBH,
output GPR_PREDEC, GPR_POSTINC
, output WB_INST <<< (追加1)
, input WB_EN <<< (追加1)
,input S0_VALID <<< (追加5)
,output IDX_INST <<< (追加5)
);
GPR に対してレジスタの更新がある指令 GPR_PREDEC, GPR_POSTINC を 出しているが、そうして良いかお伺いを立てることにした。 WB_INSTは、GPR_PREDEC, GPR_POSTINC を出力したいという意味で、 WB_EN が 1 になれば実際に出力する。
許可を出すのは、S1 で、競合があるなら 0 にした上で、PC を更新しないことで 1 クロックずらす。0 にするケースは、競合以外にもあるが S1 の仕事で S0 は関知しない。
WB_EN は、 S0_VALID に名前を変更。IDX_INST は、(PREDEC/POSTINC 以外も含めた)インデックスレジスタ参照の場合 1 。
S1_fetch -- 現状では一部だけ決まっている。説明した内容ですら入っていない。
module rtavr_s1_decode (
input RESET,
input CLK,
output CMD_LOAD, CMD_LOAD_WR, CMD_STORE,
output CMD_OP3, CMD_OP2, CMD_OP1,
output [2:0] OP3_SEL,
output OP3_WC, CMD_OP3_WR,
output [2:0] OP2_SEL,
output [2:0] OP1_SEL,
output [7:0] IMM_DATA,
input [15:0] INST,
input [11:0] JA, <<< 追加4
output [11:0] PC,
output EA_ABS,
output [7:0] ABS_ADDR
, input S0_WB_INST <<< 追加1
, output S0_WB_EN <<< 追加1
, output EA_PUSH, EA_POP<<< 追加2
, output[1:0]PUSH_SEL <<< 追加2
, output CMD_SBIX <<< 追加3
, output CMD_SBRX <<< 追加3
, output [2:0] SBIX_BIT <<< 追加3
, input SBIX_BIT_IN <<< 追加3
, input [15:0] EA_INDEX, <<< 追加4
, input [15:0] SP_IN, <<< 追加4
, output [15:0] SP_OUT, <<< 追加4
, output [15:0] EA, <<< 追加4
, input IDX_INST <<< (追加5)
, output S0_VALID <<< (追加5)
input [7:0] LOAD_DATA, <<< (追加5)
output CMD_XBI, <<< (追加5)
output CMD_BST, <<< (追加5)
output CMD_BLD, <<< (追加5)
input FLAGS_BIT_IN, <<< (追加5)
output XBI_BIT_OUT, <<< (追加5)
output CMD_SETI, <<< (追加5)
output CMD_CLRI, <<< (追加5)
input [4:0] INT_VEC, <<< (追加5)
input INT_REQ, <<< (追加5)
output INT_ACK, <<< (追加5)
);
CMD_OP3L は実際に書き戻す指示。OP3_SEL は 演算の種類で、OP3_WC はキャリーフラグの使い方を指示。
-- PUSH_SEL が 00 の場合は Rr を使う。10 は PC の下位バイトで、11 が PCの上位バイト。
EA_POP は、有効アドレス(EA) に ++SP を使う指示。書き込み先は PUSH_SEL を兼用する。
ICALL/IJMP の 分岐先アドレスは、JA に出てくる。レジスタ番号は、S0 が (指示なしで)決める。
JA は、GPR_DOBL/GPR_DOBH のラッチ前の値を先取り。GPR からもらう。
.. ちょっと POP を 検討。書き込み先 が PC の場合、S1 内で処理をする。書きこむデータは、GPR_DI に入って来ているが、S1 には来ていない。だが、良く考えてみると、JA には、セレクタ経由で GPI_DI のデータが来ているのだ、しかも接続先は PC 。セレクタ をいじってやるだけで、下位 8 バイトは PC に送り込める。
条件スキップ 命令には SBIC/SBIS , SBRC/SBRS , CPSE がある。それらの命令のときの条件を S2 から受け取りたい。
SBIC/SBIS では、CMD_LOAD の指定で IOR から読み込んで来る。合わせて CMD_SBIX を指示して、SBIX_BIT で指定したのビットを SBIX_BIT_IN に入れてもらう。
SBRC/SBRS では、なにもしなくても Rd_in にデータが来ているので、CMD_SBRX を指示して、SBIX_BIT で指定したのビットを SBIX_BIT_IN に入れてもらう。
CPSE は、CMD_OP3 で 演算結果を出すところまでする。欲しいのは、演算結果が 0 がどうか。これは、Z フラグのデータを生成するための中間データでもある。上記の条件以外なら そのデータを SBIX_BIT_IN に入れてもらう。
S1 では、SBIX_BIT_IN と それが 1 でスキップなのか 0 でスキップなのか を判断する内部データ s2_sbix_sel と合わせて スキップ条件を生成する。
ところで、(GPR_DI == 0) というのをベタで書くのと、一旦 wire di_is_zero = (GPR_DI == 0); としてまとめるのと 結果が同じになるかどうか気になる。
中途半端なものではあるが、今のもので比べてみた -- OK 規模は同じ。
次、(GPR_DI == 0) というのは、セレクタを幾つか経由したものだから、結果が出るのが遅い。(Rd_in == Rr_in) とダイレクトに書いたらどうなるのだろう?
(GPR_DI == 0) (Rd_in == Rr_in)
Number of Slice Flip Flops: 284 284
Number of 4 input LUTs: 793 805
Number of occupied Slices: 521 529
Total Number of 4 input LUTs: 830 843
Number used as logic: 777 789
Number used as a route-thru: 37 38
スライスで +8 か。これでボトルネックを解消できるなら価値があるが.. ボトルネックじゃなかったら無駄。今は覚えておくだけにしよう。
それに伴いいろいろなものが、S1 に移ってきた。
RAM の仕様から、実効アドレスの決定は、S1 でやっておかないといけない。そうして初めて S2 で使える。
随分追加した。個々の命令のための ものと、割り込み関係が主。
EA_POP は削除。PUSH_SEL も 1bit 削除。その代わりに入ったのが、LOAD_DATA で S1 内で POP 操作を行う。PUSH も S1 で行うが、書きこむデータはIMM_DATA を使って送り込む。
S2_execute -- TOP レベルとして実装。完成形では、この中に 上記すべてのモジュールを含める。モジュール名は rtavr。
module rtavr (
input RESET,
`ifdef INCLUDE_GPR
input CLK2X
`else
input CLK
`endif
// external I/O
, inout [7:0] PORTA
, inout [7:0] PORTB
, inout [7:0] PORTC
// #mnemonic opcode desc
// NOP 0000:0000:0000:0000 # OP3 に含めた。
// BLD 1111:100d:dddd:0bbb # bit load from T flag
// BST 1111:101d:dddd:0bbb # bit store to flag T
// BRBC 1111:01kk:kkkk:ksss # S1 で実行
// BRBS 1111:00kk:kkkk:ksss # S1 で実行
// BSET 1001:0100:0sss:1000 # bit set in FLAGS
// BCLR 1001:0100:1sss:1000 # bit clear from FLAGS
// IJMP 1001:0100:0000:1001 # S1 で実行
// ICALL 1001:0101:0000:1001 # S1 で実行
// RET 1001:0101:0000:1000 # CMD_LOAD + EA_POP x 2 + XX で指示
// RETI 1001:0101:0001:1000 # 〃
// SLEEP 1001:0101:1000:1000 # sleep mode
// WDR 1001:0101:1010:1000 # watch dog reset
// RJMP 1100:kkkk:kkkk:kkkk # S1 で実行
// RCALL 1101:kkkk:kkkk:kkkk # S1 で実行
// CBI 1001:1000:AAAA:Abbb # CMD_LOAD + CMD_STORE + XX で指示 ?
// SBI 1001:1010:AAAA:Abbb # 〃
// SBIC 1001:1001:AAAA:Abbb # CMD_LOAD + CMD_SBIX
// SBIS 1001:1011:AAAA:Abbb # 〃
// SBRC 1111:110r:rrrr:0bbb # CMD_LOAD + CMD_SBRX
// SBRS 1111:111r:rrrr:0bbb # 〃
スナップショット:
- rtavr-wk05g.tar.gz
- rtavr-wk04.tar.gz
- rtavr-wk03.tar.gz
上記のインターフェイスで作っている現状のもの。05g で全命令実装。インターフェイスはほぼ FIX 。
動くようなものでは全然ないが、とりあえず 規模とかが出た。
// (1) (2) (3)
// Number of Slice Flip Flops: 247 270 284
// Number of 4 input LUTs: 722 622 793
// Number of occupied Slices: 472 430 521
// Total Number of 4 input LUTs: 762 654 830
// Number of bonded IOBs : 26 26 26
// IOB Flip Flops : - - -
// Number of BUFGMUXs : 2 2 2
// Number used for Dual Port RAMs 16 16 16
// Number of RAMB16BWEs : 2 2 2
// Clock to Setup
//on destination clock CLK2X 13.030 12.816 12.091
// (3) rtavr-wk04
既に 472 スライスまでになってしまった。このうち IOR が結構占めている。
IOR の規模:
// Number of Slice Flip Flops: 136
// Number of 4 input LUTs: 223
// Number of occupied Slices: 175
// Total Number of 4 input LUTs: 232
// Number of bonded IOBs : 106
// IOB Flip Flops : 71
// Number of BUFGMUXs : 1
// Number of RAMB16BWEs : -
// CLK to Setup 6.264
規模についての目標を後退させる。初版は、全部を入れて 50A で動けば良い。( 動かすアプリケーションは、SPI FLASH ライタ。)
-- それでも厳しい。SPI は ソフト SPI で良いとしても USART 入れたいし、Core は、今 300 スライスぐらいなのが、1.5 倍にはなりそうだし。
(2) -- 2/17 時点
NOP を CPSE と一緒にして、GPR の クロックの扱いを少し変更。なぜか大分減った。
(3) -- 2/20 時点 (rtavr-wk04)
まだまだだが、スナップショットを取っておく。(追加3) まで。
メモとか追記とか
- Spartan-3A アプリケーションノート
ここに、『APP228 - Virtex デバイスのクォッド ポート メモリ (英語版)』というのがあった、デザインファイルもダウンロードできる。GPRと似たようなものなので、なにか重要なヒントがあるかも知れない。チェックしておきたい。
ちょっと見た分には、普通の verilog で記述したファイルもあった。見てみると CLK2X と CLK を入力にしている。GPR も最終的にはそうするのだろう。
ちょっと思い立って、CLK の nagaedge / posedge と CLK2X の posedge の 3 つに分けてみたが、問題なく Implement できた。GPR の 次の版ではこれを採用しよう。
あと、倍速ベースで『XAPP229 - 多ビット入出力ブロック メモリ』という使い方もある。
(追記)見てみたら DCM のモジュール CLKDLL を使っていた。CLKDLL には、RST, LOCKED というのがある。-- なるほど、 PLL みたいにロックという概念があるわけか。あと BUFG/IBUFG とかも使っている。単なる 2 倍速なら これをマネすれば問題ないのだろう。あと 3 倍速とかはどうやるのだろう?
あ、『XAPP462 - Spartan-3 FPGA におけるデジタル クロック マネージャ (DCM) の使用 (日本語版)』-- これか。 - top level は、rtavr という名前にした。その中に 直接 S2 が入っていて S0/S1 はサブモジュール。(上記で説明したとおり)。
これを元にして、kx_avr や narve のための top level を別ファイルで作ろうかと思う。インターフェイスがこれで良いかのチェックになるし、ROM/RAM/IOR を CORE と別にテストできる。 - S1 を検討中だが、パイプライン制御が混乱してきた。ちょっと整理。まず、こいつは 2 word 命令がないので簡単になる。それを念頭において...
- 分岐: 次の命令はロード済だから、分岐する場合は次のサイクルを無効にする。
- 条件スキップ: S2 で決まるから 今のサイクルを 無効にする。
- XH/YH/ZH WB 条件による 1 クロック挿入 (パイプラインストール)
次の命令を遅らせる。=次のサイクルを無効にするとともに PC を更新しない。 - Rd の更新 と X/Y/Z の競合による 1 クロック挿入 (パイプラインストール)
S0 で検出できるので、先取りして 次のサイクルを無効にするとともに PC を更新しない。
S0 では、PREDEC/POSTINC を 無効にする。
まだまだある。がとりあえず整理。- wire s_invalid 今のサイクルは無効
- wire s_stall PC , INST を更新しない
- reg n_invalid < 1 次を無効にする
- S0_CONFLICT : S0 での競合検出
- WB_VALID : XH/YH/ZH WriteBack 保留中 (GPR からの指示)
次に面倒そうなもの。- ICALL/RCALL -- 2 クロック
(1) PUSH PCH を S2 に指示 , 次を無効にする , 分岐先を PC に。
(2) PUSH PCL を S2 に指示 (INST は無効だが、別状態, PCL は覚えておく) - RET/RETI -- 3 クロック
(1) POP PCL を S2 に指示, PCL はテンポラリ
(2) POP PCH を S2 に指示
次を無効にする , 分岐先(PCL/PCH) を PC に。
(3) 無効 - 割り込み -- 3 クロック
(1) 準備 S0 を止める。
(2) PUSH PCH を S2 に指示 , 分岐先(PCL/PCH)を INT_VEC に。
(3) PUSH PCL を S2 に指示 (INST は無効だが、別状態, PCL は覚えておく)
実際の処理の説明が適当になってしまった。が、ここでは状態の把握ができれば良い。 - 分岐: 次の命令はロード済だから、分岐する場合は次のサイクルを無効にする。
なにか大変な設計ミスをしたんじゃないかという気がしてきた。
条件スキップで、スキップする場合 当然ながら次の命令は実行してはいけない。一方 PREDEC/POSTINC は、S0_fetch で更新してしまうことにしている。一体どういうことが問題になるのか CPSE + LD Rd, --X について整理してみよう。
S0 S1 S2
CPSE
LD Rd,--X CPSE << LD の PREDEC/POSTINC を確定
LD Rd,--X CPSE << LD の PREDEC/POSTINC を実行
<< CPSE の 条件が確定
LD Rd,--X
下位バイトの PREDEC/POSTINC の実行は、サイクルの前半でやるので、後半でスキップすることが分かっても間に合わない。
S0 S1 S2
CPSE
LD Rd,--X (x) CPSE << 次の命令をストール
LD Rd,--X x CPSE << S0 再実行
LD Rd,--X x
LD Rd,--X
どうやって制御するのが良いか分からないが、こういう風に動かないといけない。
ストール が間に入ったら それをまたいで制御しないといけないわけだ。
ただ、分岐一般は、S1 で決めてしまうので、ここまで面倒なことにはならない。それが救いだが、条件が前半で確定しないといけないので、性能のボトルネックになるかも知れない。
-- なかなかに嫌な予感がする。本当に作れるのだろうか?
-- と弱気になったが、考えてみれば 複数クロック命令と同じようなもの。もともと避けては通れない。
とりあえず、スナップショットを取った。
今はパイプライン制御を入れるためのお膳立て段階。skip の判断はできるようになったし、フラグの値も生成するようにした。分岐系も周りを固めつつある。割り込みは未検討だが、PUSH/POP の基本操作もいれつつある。
こうやって見ると、まだまだと言いつつ、結構作りこみが進んでいる。 コメントというかメモが大分入っているし、インターフェイスの占める割合が多いが、総規模 2074 行。結構なところまで来たものだ。
(IOR の拡張をしないなら) 多分 3000行は超えない。気が済むまで実装したら、シミュレータにかけて命令を動かしてみたい。
メモリ (GPR/ROM/RAM) はすべて同期型で WRITE_FIRST となるよう記述を変更。WRITE_FIRST では、アドレスをラッチするように明示する。目的に沿うのでこれを採用。
メモリが同期型になると、いままでのインターフェイスでは不具合が出る。実行アドレスの決定は S1 でしてしまうことにして、必要な信号も S1 に引き込む。
インターフェイスが変わったし、ラッチが入り過ぎていたり、入っていない不安もあるので、図にしてみた。
だいたいは大丈夫のようだ。が、もともとの想定と ステートがずれているような ... 。ROM/RAM のアクセスが非同期だと想定していたが、同期になったため、アドレスの確定が前のステートに移動した。
あと、モジュール間インターフェイスは全部大文字で統一しているが、内部は プレフィックスも適当な状態。
s2 に出力するラッチ類は、s2_ を付け、s1 向けに出力するラッチは s1_ を付けようかと思う。あまり ステートを 意識しない内部のラッチは r_ 。
ラッチ類への入力は、r_ 向けには v_ を使う。s1/s2 向けにも v_ を使ったりするが、意識する場合は、v1_ / v2_ にする。
i_ は、最初に書いたもので、wire と reg の区別もない。 上記のルールで変換していこうと思う。
... ところで、なにか基本的な間違いをしているような気がしてきた。
ROM/RAM の定義では、あらかじめ アドレスを 設定して置いて posedge で READ or WRITE する仕様になっている。こういう定義だと READ は posedge 以降しか行われないし、WRITE は posedge までにデータを確定しておく必要がある。READ して同じアドレスに書き戻す(read-modefy-write) ということはできない。
ROM/RAM 自体はそれで良いのだが、他の論理回路は、negedge で動かすべきなのに、posedge で動かしていた。全部チェックして書き換えないと。
それに加えて、GPR の仕様が合わない。GPR は read-modefy-write が出来ることが前提。どうしたら良いか分からないものの書きなおさないと。
- S0/S1 では、negedge にする。IOR はそのまま。ちなみに、IOR は、非同期読み出しできるから、CBI/SBI で期待する read-modefy-write は可能。
- 分散RAM は非同期読み出しが出来るから、アドレスを negedge でラッチする
なんか、これで良いみたいだ。あと、ROM の読み出しが 想定より遅れる。パイプライン制御で、なにか不具合が起きるかも知れない。ROM を二倍速にして、読み出しを早める手が使えるから 対策はある。必要かどうかパイプライン制御のとき気をつけよう。
いんちき クロックとして、2倍速の CLK2X から CLK を生成しているのだが、これで良いのじゃないかと思えてきた。
- negedge CLK2X から CLK を生成。
- GPR は、posedge CLK2X で駆動。negedge CLK2X でアドレスをラッチしているが、一番早いタイミングなので問題ない。
- その他では、CLK2X は使わず、negedge CLK か posedge CLK のみを使う。
こうなっているから、安全じゃないかと。DCM の2段使用は、推奨されていないようだから、DCM は、33 MHz の水晶から、望む周波数を作るためだけに使う。DCM の機能をみると N/1.5 , N/3, N/5.5 , N/11 (N は 2 - 32) が出来る。 22MHz, 11MHz, 6MHz , 3MHz の N 倍だけが整数 MHz になりそうだ。
そろそろ、シミュレータ も考えようと思う。とりあえず、論理シミュレーションだけで十分。
ここを見て、Windows 版 がある Icarus Verilog を使ってみようかと思う。以前 GTKWaveも使っているし、多分なんとかなるだろう。
- 『Icarus Verilog for Windows』-- Windows 版 setup (GTKWave 同梱)
ISE は重いからあまり使いたくない。シミュレータに移行したら、当面シミュレータだけを使いそう。
top level で、ベクタに変換するようにした。割り込みラインを ベクタに変換し、全部の OR を INT_REQ と定義。あと、INT_ACK が 1 だと 割り込み原因を ベクタから逆変換し 割り込みラインをRST する。
割り込みラインをRST すると割り込みラインが減り 新たな ベクタに変わる。
top level で、こういう処理を入れるのは、割り込みラインの定義をしている所だから。定義が変わっても 変更は、実体がある IOR と top level だけにしたい。
これらの信号は、モジュールの外に出しているが、S1 で対応したときに S1 に渡す。
(続く)
関連記事: