まず目的は、『(1)自作したい(作ってみたい)』 というのがまずあるが、『(2)それを応用したい』というのもある。何に応用するかも多少はっきりしてきている。
だが今のところフルスクラッチで作れるだけの実力はない。現時点では改造できるかすら怪しい。
さて、どうやっていくか -- まぁこれは公開されているソースを読んでいくのが近道だろう。実際に動かすかどうか別にして、Implement して情報をみたり、シミュレータで動作を確かめたりすることもできる。
- KX_AVR (Verilog)
コンパクトな AVR コアで、Xilinx の プリミティブを多用している。ただ、CPU コアだけで メモリや PORT はない。ソースコードを眺めてみたが結構読みにくい。-- 最適化されたものの常ではあるが読みこなすのは結構厳しい。
読むのが難しい理由が分かった。LUT (Look-Up-Table -- ROMみたいなもの) を多用していて テーブルから 論理に変換しないと 何をやっているかすら分からない ... ということだ。そして、LUT など構成をガチガチにしているので、論理が簡単になっても 自動最適化できず、決められたものより小さくならない。 - AVR Core (VHDL)
CPU コアだけではなく、周辺装置も含まれている。JTAG や 外部バス まで付いていて、ATmega103 互換になっているらしい。だが、規模が大きいのだ。そして、一部の機能を disable して小さくできるようにはなっていない。
ところで、こちらは VHDL 。実をいうと Verilog のコードを読んでみて、書いたり変更するのが楽そうに見えたので、メインを Verilog にしようと思っている。規模を小さくするつもりで、そのためには作り直しになると予想している。なので、参考にするが、書くのは Verilog のつもり。 - Navre AVR clone (Verilog)
これも ここに含まれるのは CPU コアのみ。ちょっと見てみたが、かなり読みやすい。ソースのサイズも小さい。だが 一部の命令がインプリメントされていない。gcc が使えるものでないと (2)の目的に合わない。あと パイプラインが 2段。作りたいのは、4 段パイプラインなので 2 段というのは微妙。
これは、書き方の流儀というか作法の参考にしようかと思う。 - 初めてでも使えるVerilog HDL文法ガイド ―― 文法ガイド編 ([1] [2] [3] [4])
- 初めてでも使えるVerilog HDL文法ガイド ―― 記述スタイル編 ([1] [2] [3] [4] [5] [6])
とりあえずは、これだけ知っていれば読めるし、書けるような気すらする。 - ブロックRAM(デュアルポート)
デュアルポートブロックRAM の定義の仕方が載っている。デュアルポートというものを どう定義するのかこれを読んで初めて分かった。
参考にするものは、決めた。
これらを理解するだけでは、実は目的を達成できない。なにしろ Verilog の 文法すら知らないのだ。それに、ブロックRAM や 分散RAM 他の Xilinx の プリミティブの知識がないとコンパクトなものは作れないようだ。参考になりそうなものをここにメモしていこう。
ここから本題。上に書いたことをもとに、作りたい Tiny10系の AVR互換コアはどういうものなのか決めていこう。
まずは核となる CPU コアについて
- (avr-gcc上の) アーキテクチャ: attiny10
- レジスタ: r16-r31 の 16 個。
- 命令: Tiny系 から LPM/SPM , MOVEW/LDD/STD を除いたもの。ROM は 8KB まで。
- アドレス空間: X/Y/Z で 16bit 空間にアクセス可能にする。
メモリ空間(X,Y,Z)
0x0000 - 0x001f I/O レジスタ
0x0020 - 0x003f I/O レジスタ(予約)
0x0040 - 0x043f RAM (1KB の場合)
0x3FC0 - 0x3FC3 DEVICE ID (必要なら)
0x4000 - ROM
I/O 空間 (IN,OUT 等)
0x00 - 0x3f I/O レジスタ
RAM 空間 (LDS STS direct)
0x00 - 0x7f RAM の下位 128B
RAMAR/RAMDR (サポートしない予定) - 実行命令サイクル: Tiny10系 は参考にしない。むしろ Tiny系準拠。
- 外部仕様 -- 調査中
(決めたら載せる) - モジュール分割
KX_AVR も Navre も CPU をモジュール分割していない。一方 AVR Core は、ALU/汎用レジスタ/IOレジスタ/IOレジスタマッピング/命令フェッチ-デコード という風にいくつかのサブモジュールを作っている。
いまのところ 分散メモリをどう使うかの関係で、汎用レジスタ は サブモジュール化するつもり。他は決めていない。
MCU 全体について
- フラッシュメモリ
ブロックRAMを使う。命令系の都合もあり 4Kx16bit(8KB) まで。
デュアルポートを使って、ポートを 命令フェッチ(16bit) と バス(8bit) に振り分けたい。あと、パリティ用のビットがあるので、なにか有効に使えないか検討しようと思う。 - RAM
ブロックRAMを使う。2Kx8bit (2KB)。 バス(8bit)に接続するだけで十分のはず。 - 汎用レジスタ (GPR)
1 クロックで Load + Load + Store するのだから 2 出力 1 入力は最低必要。さらに X/Y/Z でのアクセスは、バスに 16bit 出力して プレ・デクリメント, ポスト・インクリメントできるようにしなくてはならない。SBRC/SBRS 命令というのもあって、さらに別ポートが必要(かも知れない)。
それでも MOVW 系や乗算命令がないので、普通のAVR より単純。 ここが規模縮小のポイントなので、どう構成するか非常に重要。よくよく考えたい。 - I/O レジスタ (デバイス)
ベースは Tiny40 。デバイスのマッピングは、これから ADC など 0x0D - 0x14 を除いて USART をそこに置いたものが フルセットと想定。で、それの サブセットを作っていく。
少なくとも PORT と TIMER は載せる。それに関連した割り込みも。そうしないとなにも出来ない。ただフルセットは考えていない。特にプルアップは外部に接続するときだけ必要だし、IOB の機能で自由にならないし。PORT では tristate まで考える。
PORT の機能は、Tiny10系と従来のAVRでは相当違う。まずは従来のAVR互換の方針で検討する。
TIMER は、8bit の Timer0 と 16bit の Timer1 が標準的でそれに準じようと思う。
あと PWM の PORT との兼用とか ピン変化割り込みとか 様々な機能があるが、まずはサブセットを目指す。拡張したとしても 必要なければ削れるように考慮する。
あとの機能もオプショナルにして 順次拡張していきたい。入れたいのは、USART / SPI / TWI 。
構成方法については、AVR Core を参考にする。IOレジスタ/IOレジスタマッピング のサブモジュールを入れる(かも)。
以上が本題。あと、新たに分かったことや 経緯など この下に追記していくつもり。
追記: AVR Core の 汎用レジスタモジュールの定義
input ireset : (内部)リセット
input cp2en : クロック
// 2 load + 1 store
input reg_rd_wr
input reg_rd_adr[4:0]
output reg_rd_out[7:0]
input reg_rd_in[7:0]
input reg_rr_adr[4:0]
output reg_rd_out[7:0]
// 有効アドレス(EA)
// reg_h_addr の意味
// 001 : X
// 010 : Y
// 100 : Z
// reg_h_wr は EA ライトバック指示
input post_inc
input pre_dec
input reg_h_wr
input reg_h_adr[2:0]
output reg_h_out[15:0]
// Z 値 LPM/ELPM 用(不要)
output reg_z_out[15:0]
定義は 予想と違う。MOVW 系がないようだ。
reg_z_out を抜いて これだけを Implement すると...
Number of Slice Flip Flops 256
Number of 4 input LUTs 578
Number of occupied Slices 412
.. 分散RAM が使えていない。全部FF (32x8 = 512)だと こうなるわけか。
それはともかく、この定義ベースで 作ってみよう。
さて、そもそも分散RAM とはどういうものなのか? Spartan-3 の ug331 に説明があるが、どうも 16x1bit の デュアルポートが構成できるようだ。出来ることは 2 load + 1 store。X/Y/Z レジスタがなければ具合良く 16x8bit が入るわけだ。LD 命令を 考えると X/Y/Z レジスタ は、1 store とは別にインクリメント/デクリメントした値を書き戻さないといけないので、この中には入れられない(かも知れない)。
実際のAVRは、どれぐらいのクロックが必要かちょっと見てみたところ
normal Tiny10
LD Rd,X 2 1/2 S L L
LD Rd,X++ 2 2 S L L WB
LD Rd,--X 2 2/3 S L L WB
ST X,Rr 2 1 L L L
ST X++,Rr 2 1 L L L WB
ST --X,Rr 2 2 L L L WB
と、むしろ速い場合がある。 あと、プレ・デクリメントのために 1 クロック消費している。
この表をじっくり見ていると 16bit デュアルポートならなんとかなるような... あるいは 汎用レジスタだけ 2 倍速で動かすとか.. 倍速なら インクリメント/デクリメントも キャリー付き 8bit で済むし良いかも。
2 倍速のクロックは入力するが、インターフェイス自体は標準速という 考えで作れば良いのかな?
こういう高望みをして 果たして作れるのか? 不安になるがまぁトライしてみよう。
よく分からなくなった。どういう機能が必要かというのは、パイプラインの処理がイメージ出来ていないと 分からない。なので、整理してみる。

stage 0 fetch 命令フェッチ
stage 1 decode 命令解析
stage 2 execute 実行
stage 3 writeback 結果の格納
ひとつの命令に注目すると、上記の 4 つのステージがあって、1クロックづつ後に 次のステージが実行される。ひとつのステージに注目すると 続く命令が どんどん来る。(図では左が過去)
あるステージは、続くステージに FF を使って 状態を引き渡す。(INST) などがそれに当たる。
いま注目しているのは、汎用レジスタ GPR。その周りの情報の流れを書いてみた。

この図は GPR を 普通に 分散メモリで構成したときの流れ。明らかに無理がある。
ひとつの問題は、stage 2 でやることが多いということ。なぜそうなってしまうかというと、(Rd_adr) が 書き戻すアドレスも兼ねているため。ステージをずらすと (Rd_adr) が 2種類必要になる。また、X/Y/Z レジスタを 読みだして (BUS_ADDR) に入れたいがそのためのパスがない。
ST 命令を考えると BUSに書きだすレジスタのために で 1つのポートを使うので、使える ポートは 8bit が 1つしかない。X/Y/Z レジスタの書き戻しはおろか 読み出しすら満足にできないわけだ。

そこで 強引に 分散メモリを使うために、 倍速という 方法を考えたわけだ。
(Rd_adr) を 前半は Read 、後半は Write という風に使い分ける。ステージを分けるように書いてみたが .. たぶん間違い。ステージを分けると 違う命令のために使うわけで、次に書くようなことをすると 競合が起きて具合が悪い。ステージを分けは最初の図のようにするしかないかも。
X/Y/Z レジスタについてだが、まずは (BUS_ADDR) に 値をロードしたい。
ST X,Rr を考えてみると ... Rr 読み出しにひとつのポートを使う。もうひとつのポートを使って 2 回に分けて 読み出す。図では反対になってしまったが、(Rd_addr) の方を使えば 書き戻しも出来るはず。
一方 LD の方はまだ考えていない。BUS からの 読み出しにまるまる 1クロック使う前提 にすると、値が得られるのは stage 3 が終わった後。1 クロック挿入しないと どうにもならないような気がする。逆に 挿入するならば、GPR を自由に使えるので なんとでもなる。
あと、Zレジスタは、IJMP , ICALLでも使う。これらは 2 命令での実行。... ということは stage 1 のときに Z レジスタを得て その次の PC にしないと いけない。... やはり GPR の読み出しは stage 1 に持っていった方が良いということか。
だいぶ穴が多いが、なんとかなりそうな気もする。これを元に考えなおしてみることにしよう。
Tiny10 で クロック数が変わったもの(メモ)
話題が変わるが、LD/ST 以外でもあった。
normal Tiny10
RCALL 2 2/3
ICALL 2 2/3
RET 4 4/5
RETI 4 4/5
SBI 2 1
CBI 2 1
メモリアクセスが 2 クロックに 1回しかできないので、1 クロック遅れる場合が出る。一方 CBI/SBI が 1クロックになっている。
話題を元に戻して上記で書いたことの整理。図にしてみた。
- (1) ADD や MOV 命令 など

Rd , Rr から読み出し Rd に書き戻すわけだが、リソースをどういう風に使うかの図。リソースとして、PORTA と PORTB がある。PORTA には、IN と OUT(図では使う側からみた OUT/IN になっている) があるが、アドレスは 1 つ。これを 1 クロックの間に 2 回アクセスする計画なので 上限に分けている。
赤で S1_decode/S2_execute の別を書いた。読み込むのは S1_decode だが、書きだすのは S2_execute の最後。これについては後ろで説明。
PORTB からは、Rd を読み出す。今の段階では 分けて使う理由がないので 2 つ専有している。 - ST --X,Rd , ICALL

ST --X,Rd , ICALL ではどう使うか を図示するとこんな風になる。 - ST --X,Rd タイムチャート

時間軸で上記の枠を配置するとこうなる。S2_execute にある枠を使うのは 1 クロック後になるわけだ。
時間的に厳しいのは、XL を読みだして -1 (または +1) して書き戻すところ。命令をデコードした後に初めてアドレスが確定し、そこから読みだして、さらに演算して書き戻すことになる。でも、この位置にしか入らないので、なんとかしなくてはならない。( ちなみに XH-1 の書き戻しが 後ろになるのは、他の命令が S2_execute で使うのでそれに合わせないといけないため。)
幸いなことに、S0_fetch は余裕ありまくりのはずなのだ。ブロックRAM は 200MHz 以上で動作することになっている。なので、S0_fetch の区間に S1_decode でやることを 一部持っていく。せめて アドレスを確定しておくと、S1_decode の最初から 読み出せる(はず) - LD Rr,--X

では、LD 命令はどうなるのか? ... BUS にアドレス を出して 読み込むわけだから、S3_writeback が 終わったときに値が分かる。 だが、書き戻すための枠がない。 - LD Rr,--X タイムチャート

時間軸で書きなおすとこうなる。入れるとしたら Tn+3 になるが、次の命令の S2_execute で使うかも知れない。簡単に対処するなら 無条件に(次の命令を) 1clock 伸ばすことになる。
次の命令が、PORTA を全く使わないならば、そこを借りて書き戻すことは可能。そうやって初めて 1 クロックになる。Tiny10 はそうやっているのかも知れない。普通の Tiny は 無条件に 2 クロック。こいつで 高速化すべきかどうかは、別途検討するが、高速化が不可能なような設計にはしないつもり。
ざっと整理してみると、こんな感じか。汎用レジスタ (GPR) の構造が決まらないとなにも決まらないようだ。これだけをモデル化してみて、確認するのが第一ステップのようだ。
ところで、更新中のデータにアクセスされたらどうするのか?
更新ポイントは 2 ヶ所しかない。それぞれについて考えてみよう。
まず --XL と XL++ 。これらは ロードと同じ枠で更新が入るから 他の命令には影響を与えない。問題は自分自身のみ。ST X++,Rd で Rd が XL だったら 更新前の値を Store しないといけない。... ということは、Rd の枠の 上を使えば問題ないということだ。
ST --X,Rd だったら 更新後? ならば、 Rd の枠の 下を使わなくてはならない。
ちょっと面倒だが、まだ簡単に処置できる方だ。
次の更新ポイント。S2_execute での更新。一番複雑なのは、LD 命令で 2 つ更新中のものがある。それを次の命令で使われる可能性がある。自分自身でも競合するわけだが、それは LD で読んだ値で上書きすることになっているから気にしなくてよい。
追記: 命令の方のデータシートを見たら、例えば Y レジスタ間接で YL や YH をロードする命令は書けるが、どのような結果になるかは未定義だそうだ。
一番簡単なのは、次の命令で競合しているかどうかをチェックして競合すれば 次の命令を 1 clock 遅らせることだ。だが、チェックできているなら、(なにしろ更新中なわけで)内容は 覚えているので、それを使えば良い。だがよく考えてみよう。
AND 命令での Rd のケースでは、値が確定するのは s2_execute の後半。それは 次の命令の s1_decode の後半ということでもある。s1_decode の先頭で 既に値を使っているわけで 明らかに無理。
s1_decode の後半でしか使わないものはどうだろう? ひょっとしたら 間に合うのかも知れない。ただ、こうすることで、最高クロックが上がらないなら 潔く 遅らせた方が得だ。これはどこがクリティカルになるかで決まるから 現時点では決められない。両方作って選ぶという方針にしないとダメかも知れない。
あとは、2 clock 遅れの LD 命令でロードした値がある。そもそも 競合するようなら 1 clock ずらすのだ。問題は 2 clock ずらすかどうか 。
これは単に ストア待ちで 値自身は 決まっている。さすがに 2 clock ずらすのはどうかと思うので、更新中のデータを使う方向で検討しよう。
以上で書いたことを 全部盛りこんで 汎用レジスタモジュールを定義しよう。
汎用レジスタモジュールの定義(1)
- 基本ルール: ステージをまたがってデータを受け渡すには、かならずラッチが必要になる。そのラッチは出力側に置くことにする。
- 分散RAM は、1 クロックで 2 回のアクセスをするわけだが、使う側はクロック単位で動作する。そうすると、PORTB のアドレス 2 つ分をあらかじめ決めておかないといけない。
- 出力側も同じで、2つの出力は 別々ラッチとして分けなければならない。図ではラッチは入れてあるが、XL側は更新タイミングが違う。同期化するために、もう一段ラッチを入れないといけないかも。(これはとりあえず保留)
- Rr_SEL としてセレクタをいれているが、使う側から見ると大きなお世話かも知れない。単に 2 出力にした方が良いのでは? ... そうすると、単純に ポートを 1 つから 2 つにしているだけの回路になる。

- PORTB をベースにして考えると、こちらも基本は、ポートを 1 つ から 2 つに増やす回路。
- ただし入力のひとつは、WriteBack (ちょっと違うけど)。
- また出力とラッチの間に演算が必要。演算の種類は +1 / -1 と +0 (パススルー)。ただし 16bit の演算がしたいわけだから、キャリー(ボロー)あり・なしの区別も必要。
- PORTB と同じように出力を 2 つに分けるのは大きなお世話で、同期化のためのラッチが必要(かも)。
- 信号名を考えるのが面倒になったので適当になってきた。今はラフスケッチだから、これで良いだろう。また、信号をどのように使うかは、命令デコーダの仕事だから現時点で詳細を考える必要もない。
これもまた、図を描いてみた。

まず、PORTB 。基本 PORTA とは独立に考えられるから、簡単なこっちから。
次は難しそうな PORTA 側。
GPR モジュールのコードを書いてみた。(仕切りなおし)
- rtavr_gpr_16.v (ver 003)
まずは、名前を決める。ファイル名にも使うし、module 名にも使う。AVR 命令のデータシートを見たら、Tiny10 のコアは、the Reduced Core TinyAVR というらしい。なので rtavr_ というプレフィックスにした。モジュール名は、rtavr_gpr_16 。16 個しかレジスタがないことを強調。
Tiny10 系には 別の名前もあるらしい。ここにも命令一覧があるが、AVR8L と書いてある。
モジュールインターフェイスは次のようにした。
module rtavr_gpr_16(
input CLK2X,
`ifdef USE_DMY_CLOCK
`else
input CLK,
`endif
input WE, PREDEC, POSTINC,
input [7:0] DI,
input [3:0] ADDRAL, ADDRAH, ADDRBL, ADDRBH,
output [7:0] DOAL, DOAH, DOBL, DOBH
, output WB_VALID
);
基本上で書いたこと。これも名前を付けるのが重要な作業。C とは違って、この名前を上位レイヤにも見せないといけない。
CLK2X が所謂クロック。CLK は、本来 input の予定だが、Timing とかの指示をどうするのか分からないので、とりあえず自分で生成。
WE, PREDEC, POSTINC は更新指示。WE は DI を書きこむ。PREDEC, POSTINC は X/Y/Z レジスタ用。-- だが ADDRAL, ADDRAH で決まるので 実は制限はない。(いまのところ)
ADDRAL, ADDRAH , DOAL, DOAH は、PORTA 側。ADDRBL, ADDRBH , DOBL, DOBH は、PORTB 側 。L/H の区別をしているが、実は ADDRAH == ADDRAL + 1 である必要はない。(いまのところ)
DOAL, DOBL の更新タイミングだけ 90 度早い。これでは困りそうなので ラッチを入れた バージョンも作成。(後述)
WB_VALIDは、PREDEC, POSTINC での更新を保留しているという意味。次の命令が レジスタを更新する (Rd がある) 命令ならば、1クロックずらさないといけない。参照も同様だが、より精度が高い判断ができるように 保留されている レジスタ(= 正しい値が読めない) を WB_ADDR で示すことは可能(デフォルト Off)。
なお、WB_VALID は、インクリメント/デクリメントした結果 H(上位バイト) の値が変わったときのみ 真になる。 確立が低く決しておきないようにコントロールも可能だから これ以上リソースを割かない方が良いだろう。
このインターフェイスに対して、単純な順に TTEST2/TEST3/TEST5/TEST6/TEST7 と 5種類入れてある。TEST2 は、演算もライトバックもない単純化したもの、TEST3 では、PREDEC を入れたが、L(下位バイト)しかライトバックしない。 TEST5 は、H(上位バイト) のライトバックも入れたもの。TEST6 で POSTINC に対応。TEST7 では、DOAL, DOBL の更新タイミングの正規化のため ラッチを 2 段にした。
図で描いた構成と TEST5 が近い。だが、図の構成では POSTINC ではなく PREINC のアドレスが出てしまう。
// TEST2 TEST3 TEST5 TEST6 TEST7
// Number of Slice Flip Flops: 36 36 35 45 61
// Number of 4 input LUTs: 27 37 51 93 93
// Number of occupied Slices: 24 33 40 62 70
// Total Number of 4 input LUTs: 27 44(+17) 60(+16) 100(+40) 100
// Number used for Dual Port RAMs: 16 16 16 16 16
// Number of bonded IOBs: 58 59 60 61 61
// Post-PAR Static Timing Report
// Clock to Setup 5.084 7.782 8.688 9.022 10.302
これらが、どんな風に Implement されるのか見てみた。-- 思ったより LUT の増加が多い。TEST7 が最終仕様(のつもり) なのだが、70 スライスも使っている。縮小できなければ作る意味はないので少々不安。
Dual Port RAM の数は 8 だが、そのために使用する LUT の数は 16 。Spartan-3 ug331 には、Dual Port (16x1 bit) が 1CLBに 1 個入るという説明がある。(図5-3)
性能に関する指標は、Post-PAR Static Timing Report の Clock to Setup の値にした。複雑になるにつれ順当に増えているから、これで良いはず。
コア全体の 性能目標は 40Mhz - 50MHz ぐらい。X2 なので 100MHz で動いて欲しいところ。取ってきた数字が 最大クロックを示すのであれば ちょっとオーバー。
一応本体もソースから転記しておく。TEST7 相当。
(モジュール宣言は省略: 上記とくっつける)
reg CLK;
reg [7:0] i_DOAL,i_DOAH;
reg [7:0] i_DOBL,i_DOBH;
reg [7:0] gpr [0:15];
assign DOAL = i_DOAL;
assign DOAH = i_DOAH;
assign DOBL = i_DOBL;
assign DOBH = i_DOBH;
wire [3:0] ADDRA = (CLK == 1) ? ADDRAL : ADDRAH;
wire [3:0] ADDRB = (CLK == 1) ? ADDRBL : ADDRBH;
wire [8:0] DOA = { 0, gpr[ADDRA] };
reg c;
reg i_wb_valid;
wire WB = (PREDEC | POSTINC);
reg [7:0] i0_DOAL,i0_DOBL;
reg [7:0] i_wb_data;
wire [8:0] DOA2 = PREDEC ? DOA-(~CLK | c) : DOA;
wire [8:0] WB_DATA = POSTINC ? DOA+(~CLK | c): DOA2;
wire WE2 = (CLK == 0) ? (WE | i_wb_valid) : WB ;
wire [7:0] DI2 = (WE == 1) ? DI : (i_wb_valid == 1) ? i_wb_data: WB_DATA ;
assign WB_VALID = i_wb_valid;
always @(posedge CLK2X)
begin
CLK <= ~CLK;
end
always @(posedge CLK2X)
begin
if (CLK == 1) i0_DOAL <= DOA2[7:0];
if (CLK == 1) i0_DOBL <= gpr[ADDRB];
if (CLK == 0) i_DOAL <= i0_DOAL;
if (CLK == 0) i_DOBL <= i0_DOBL;
if (CLK == 0) i_DOAH <= DOA2[7:0];
if (CLK == 0) i_DOBH <= gpr[ADDRB];
if (CLK == 0) i_wb_valid <= WB & c;
if (CLK == 0) i_wb_data <= WB_DATA[7:0];
if (WE2) gpr[ADDRA] <= DI2;
c <= WB_DATA[8];
end
endmodule
Verilog を初めて使ってみたが、書きたいことをストレートに書けて良い感じ。予定どおり Verilog でいこう。
... それにしても 散々検討した内容がこの程度だったとは。全体で 1000 行程にはなるはずなので、先は長いかも。
最終仕様とは言いつつも、SBRC/SBRS 命令(該当ビット 0/1 でスキップ) の検討は未だ。-- s1_decode でレジスタ値が読めるから多分大丈夫。あと メモリマップされたアクセス。全然検討していないが、これのために仕様を変えることはない。
仕様を変えるとすれば、スペース最適化のため。プリミティブを有効に使えるなら仕様も変える。だが、それは基本のコードが動いた後。そして基本のコードでプリミティブを多用するつもりはない。
あとクロック。正しい記述方法が分れば仕様を変更する。90 度ずれた クロックを使うべきならそうするし、DualEdge で良いのならそれを使う。また、リセットを入れるべきなら入れる。
さて、コアは一旦検討をやめて、周辺のインプリメントを見ておきたい。
まずは、RAM 。上記のリンクを見て ちゃんと Implement できるかチェック。
- rtavr_sram_2KB.v (ver 001)
教えのとおりにやれば、問題なかった。短いし全部貼るとこう。
module rtavr_sram_2KB(
input CLK,WEA,WEB,
input [10:0] ADDRA,ADDRB,
input [7:0] DIA,DIB,
output [7:0] DOA,DOB
);
reg [7:0] i_doa;
reg [7:0] i_dob;
reg [7:0] mem [0:2048-1];
assign DOA = i_doa;
assign DOB = i_dob;
always @(posedge CLK)
begin
if(WEA) mem[ADDRA] <= DIA;
i_doa <= mem[ADDRA];
end
always @(posedge CLK)
begin
if(WEB) mem[ADDRB] <= DIB;
i_dob <= mem[ADDRB];
end
endmodule
次のようになったから、これで良いのだろう。
Number of BUFGMUXs: 1
Number of RAMB16BWEs: 1
ところでこれをバスにつなぎたい。... ググって見つかったのをマネした。 - rtavr_sram_2KB.v (ver 002)
- rtavr_sram_2KB.v (ver 005) (最新版 参考まで)
module rtavr_sram_2KB(
input CLK,WEA,
input [10:0] ADDRA,
input [7:0] DIA,
output [7:0] DOA,
input WEB,OE,
input [10:0] ADDRB,
inout wire[7:0] DBUS
);
reg [7:0] mem [0:2048-1];
reg [7:0] i_doa;
assign DOA = i_doa;
always @(posedge CLK)
begin
if(WEA) mem[ADDRA] <= DIA;
i_doa <= mem[ADDRA];
end
reg [7:0] i_dob;
bufif1(DBUS,i_dob,OE);
always @(posedge CLK)
begin
if(WEB) mem[ADDRB] <= DBUS;
i_dob <= mem[ADDRB];
end
endmodule
PORTB の方をバスにしてみた。top-level でしかうまく行かないとかだと嫌なのだが、うまく行っているようだし先に進む。
次は ROM 。バス版を元に 16bit にする。WEA と DIA を外す。サイズを 4K word にする。-- ここまでは問題ない。
だが、うまく ブロックRAM に割り当てないと、分散メモリに割り当てようとして ... 終わらないばかりか メモリを使い尽くして PC が変になる。
とりあえずは、8bit のバスにつなげたい。... 試行錯誤のすえ、ひとつ通るパターンを見つけた。
(追記)最新版は、READ_FIRST から WRITE_FIRST に変更している。 - rtavr_rom_8KB.v (ver 001)
- rtavr_rom.v (ver 005) (最新版 : 参考まで)
module rtavr_rom_8KB(
input CLK,
input [11:0] ADDRA,
output [15:0] DOA,
input WEB,OE,
input [12:0] ADDRB,
inout wire [7:0] DBUS
);
reg [15:0] mem [0:4096-1];
reg [15:0] i_doa;
assign DOA = i_doa;
always @(posedge CLK)
begin
i_doa <= mem[ADDRA];
end
reg [7:0] i_tmp;
reg [15:0] i_dob;
wire [11:0] i_addrb = ADDRB[12:1];
wire WEBH = ADDRB[1:0] & WEB;
wire WEBL = ~ADDRB[1:0] & WEB;
wire OEH = ADDRB[1:0] & OE;
wire OEL = ~ADDRB[1:0] & OE;
bufif1(DBUS,i_dob[7:0],OEL);
bufif1(DBUS,i_dob[15:8],OEH);
always @(posedge CLK)
begin
if(WEBL) i_tmp <= DBUS;
if(WEBH) mem[i_addrb] <= { DBUS , i_tmp };
i_dob <= mem[i_addrb];
end
endmodule
L → H の順で 書かないといけないが、こういう風に Write が出来るようになった。こだわったのは、RAM としても使えると便利だと思ったから。とりあえず Write 出来るようにしておけば、NVRAM の シミュレーションも出来るだろうし。あと、実際のブロックRAM は 18bit あるから 上位ビットを使ってプロテクトもできそうだ。
それに、Write を一切できなくすると 初期化パターンを用意しないと最適化されてしまう。-- これが面倒だったり。
Number of Slice Flip Flops : 8
Number of 4 input LUTs : 10
Number of occupied Slices : 9
Total Number of 4 input LUTs : 10
Number of BUFGMUXs : 1
Number of RAMB16BWEs : 4
Warning : 8 multi-source signals are replaced by logic (pull-up yes):
結果はこんな風になった。ちょっと Warning が嫌な感じだが、まぁいいや後回し。
とりあえず ROM/RAM は バスにつながったようだ。後 I/O レジスタをどうするのか? この検討が終われば コアの設計を再開できる。
I/O レジスタには次の命令がある。
命令 クロック数
IN Rd,P 1
OUT P,Rr 1
SBI P,b 1
CBI P,b 1
SBIC P,b 1/2,3
SBIS P,b 1/2,3
SBIC/SBIS があるので s1_decode の段階で 読めていないといけないが、 今の設計では、IN 命令も条件は同じ。OUT については s2_execute で 書き終わらないと 次の命令の s1_decode で読めない。SBI/CBI は、一旦読んで書き戻すわけだが、上記の条件を満たせば良さそう。
... というわけで バスには接続できない。バスの口を持っていても良いが、GPR で既にバス用の口は持たないことにしたから (どういう風にするかは決めていないが) 同じようにする。
(追記)最新版は、READ_FIRST から WRITE_FIRST に変更している。
module rtavr_ior_32(
input CLK,
input WE,
input [7:0] DI,
input [4:0] ADDR,
output [7:0] DO,
);
同じようにするからには、こんなのが CPU に対するインターフェイスになるはずだ。だが、ステータス・フラグ、割り込みライン、その他多数の外部インターフェイスがある。... やはりバスに接続したい。メモリ用のバスとはタイミングが違うので I/O レジスタ専用のバスにしようと思う。バスにするには、output を tristate にすれば良い。
でどこのアドレスにマップするかは、パラメータで指定しよう。
module rtavr_ior_port(
input CLK,
input WR,RD,
input [7:0] DI,
input [4:0] ADDR,
inout [7:0] DO
// external I/O
, inout [7:0] PORT
);
parameter BASE_ADDR = 0; // PORTA 0x00
// parameter BASE_ADDR = 4; // PORTB 0x04
// parameter BASE_ADDR = 27; // PORTC 0x1b
reg [7:0] i_pin;
reg [7:0] i_ddr;
reg [7:0] i_port;
wire OE_pin = RD & (ADDR == BASE_ADDR);
wire OE_ddr = RD & (ADDR == (BASE_ADDR + 1));
wire OE_port = RD & (ADDR == (BASE_ADDR + 2));
//wire WE_pin = WR & (ADDR == BASE_ADDR);
wire WE_ddr = WR & (ADDR == (BASE_ADDR + 1));
wire WE_port = WR & (ADDR == (BASE_ADDR + 2));
bufif1(DO,i_pin,OE_pin);
bufif1(DO,i_ddr,OE_ddr);
bufif1(DO,i_port,OE_port);
always @(posedge CLK)
begin
//if(WE_pin) i_pin <= DI;
if(WE_ddr) i_ddr <= DI;
if(WE_port) i_port <= DI;
i_pin <= PORT;
end
generate
genvar i;
for (i=0; i<8; i=i+1)
begin : port_out
bufif1(PORT[i],i_port[i],i_ddr[i]);
end
endgenerate
endmodule
同じような調子で作ってみた。今度もひとつ呪文を覚えた。
結果:
Number of Slice Flip Flops : 16
Number of 4 input LUTs : 21
Number of occupied Slices : 19
Total Number of 4 input LUTs : 21
Number of bonded IOBs : 32
IOB Flip Flops : 16
Warning : 8 multi-source signals are replaced by logic (pull-up yes): DO....
... PORT の方は Warning が出ていないから、bufif1() を 複数使うと Warning が出るわけか。
それはともかく、これから沢山作るのに 19 スライスも使っている! 。アドレスデコーダが悪い? 一ヶ所にまとめるべきなのか?
ちょっと記事が長くなりすぎた。だが、これは前提となるもの単なる洗い出し。ここまでで、だいたい概要は掴めた感じがするから、次の段階は別記事をしよう。
(AVR互換コアの仕様(その2) に続く)
ただし、ここに書いたことの訂正などは ここでやるしかない。まだ追記は続くかも。