読者です 読者をやめる 読者になる 読者になる

Sis puella magica!

いろいろ書く予定

CPU実験について(完動までのまとめ)

CPU実験 Advent Calendar 2015 - Adventarの記事です。

特別面白そうな話題は思いつかなかったのでこれまでの経緯をまとめただけです。

アーキテクチャについて

まずは今回自分が作成したコアについて簡単に説明しておく。

  • tomasuloのアルゴリズムを使ったアウトオブオーダー実行
  • 汎用レジスタ32bit×32個(うち一つはゼロレジスタ)
  • 外部との入出力はRS232C
  • 命令メモリはBlockRAM、データはSRAM
  • ハードウェア実装のプログラムローダー

ソースコードはここ
cpu2015g6/core · GitHub

完動までの流れ

  • 7月末 Sセメスターのハードウェア実験の最後の課題(アキュムレータマシンを作る)で、アキュムレータマシンでは面白くなさそうなのでtomasuloの実装をする。再帰fibくらいまでは動いたが、コードが汚く論理合成にとても時間がかかる(2,3時間位)のでそれ以上続ける気が無くなった。CPU実験ではまた最初から書き直そうと思った。
  • 9/29 班決め
  • 10/4 ISA(暫定版)が決まる(後に何度か修正された)。コアの設計を始める。

これから使う型の定義を書くところから始め、実装を進めていく。

  • 10/14-18 用事があったため進捗なし
  • 10/19 Check Syntaxでinternal errorが発生する。最初のxstクソ案件。
  • 10/20 internal errorの原因はだいたいわかったが、論理合成はまだできない。
  • 10/22,23 論理合成に成功したが、LUTを128%も使っていた。レジスタの数を減らすことで大きく改善されることが分かった。
  • 10/28 xstがよくわからないerrorを吐くが原因がわからない。xstクソ案件。
  • 10/31 ネストしたrecordが原因だったらしい。xstクソ案件解決。
  • 11/3 modelsimと実機の動作が一致しない。
  • 11/5 behavioralとpost-translateの2種類のシミュレーションを行ったところ既に動作が一致しない。xstクソ案件。

度重なるxstクソ案件でつらい時期

  • 11/9 とある列挙型が何故か壊れることが原因だと判明した。xstクソ案件解決。
  • 11/9 コンパイラがほぼ完成し、シミュレーターではmin-rtが動く状態になる。
  • 11/10 最初の命令が実行されないバグを見つけて修正した。
  • 11/11,12 再帰fibが一度動いたが、プログラムローダーを実装したらまた動かなくなった。プログラムローダーでいろいろ試したところ、SRAMの挙動が怪しそうであった。
  • 11/16 SRAMが直った。再帰fibが動いたのでそろそろmin-rtを試してみようと考える。
  • 11/16夜 他の班が完動したのに便乗して寿司を食べに行く。QOLが大いに向上する。
  • 11/19,20 シリアル通信の動作が安定しないことに気が付いた。入力にラッチを挟んでいなかったことが原因だった。
  • 11/20-22 min-rtを実機で動かしたところ、write_ppm_header関数の出力は確認できたが、そこで止まってしまっているようだった。printfデバッグを始める。
  • 11/23 FPUの挙動におかしな点を見つける。FPU係に修正を依頼し、夜には実機が画像を出力した。
  • 11/24 シミュレーター側のFPUの修正の結果diffがなくなり完動。

これまでに踏んできたバグについて

SRAMのタイミングの問題

今使っている基板のSRAMは入出力のタイミングが難しいようで、マニュアルではreadもwriteもアドレスを入力してから2クロック後にデータのやりとりを行うことになっているが、3クロック待たないと出力が出てこないように見えることがある。レイテンシは少し大きくなるがSRAMへの入出力にはラッチを挟んだほうが(たぶん)挙動は安定する。

シリアル通信のラッチ

シリアル通信(受信側)の入力を一旦ラッチに受け取らないとたまに受信したデータが壊れる。ハードウェア実験のときにはラッチを入れなくても何故か動いてしまったのでCPU実験まで気が付かなかった。

procedureの引数の型に(package).(type)の形式のものがあるとCheck Syntaxでinternal error

record型などをpackageとしてまとめていたので、それをprocedureの引数の指定に使おうと思ったら論理合成どころかCheck Syntaxでinternal errorが発生した。型名をaliasで置き換えたらinternal errorは発生しなくなったが、論理合成では別のエラーとなったので、仕方なくprocedureを諦めて直に書くことにした。
xstはクソ。

xstのよくわからないエラーで論理合成できない

発生したエラーはERROR:Xst:1706とERROR:Xst:1847。原因はネストしたrecordがxstの御機嫌を損ねたことらしかった。ネストの深いところにあったとあるメンバを浅いところに移動させたところ何故かエラーが出なくなった。
このエラーについてgoogle検索しても使えそうな文書が見当たらず、最初は原因がさっぱり分からなかった。論理合成中にWARNING:Xst:737(このwarningはvariableの初期化を適切に行っていない時に発生することがある)が出ていることも少し経ってから見つけたが、variableの初期化漏れは全く見つからなかったのでxstのrecordの扱いがおかしいのではないかと疑い、試行錯誤の結果エラーは発生しなくなった(WARNING:Xst:737も消えた)。
また、このバグを突き止めようと試行錯誤していた時に、recordのメンバの順序を変えただけでinternal errorが発生したこともあった。
xstはクソ。

とある列挙型が(xstのバグにより)壊れる

再帰fibがmodelsimの上では動くようになったので実機で動かそうとしたところ、おかしな挙動をした。
バグが出たら最初にxstを疑うようになった私は、iseのシミュレーションの画面でbehavioral,post-translate,post-map,post-routeの4つが選べることに気が付いて、post-translateで再帰fibを動かしてみた。
post-*は論理合成後のシミュレーションを行うのでbehavioralに比べて実機に忠実なシミュレーションであり、xstのバグのせいでおかしな動作をする場合にはpost-*でも捉えられるはずである。ただbehavioralに比べて遅いし、当然論理合成してからでないとシミュレーションはできない。
post-translateでおかしな挙動を捉えることには成功したが、原因の箇所を特定する作業はとても面倒だった。post-translateではtopからの出力以外はsignalやvariableの値を確認することは難しいので、値を確認するには何らかの形でtopから出力させる必要があった。しかし、そのための書き換えを行うたびに論理合成を行う必要があり、やたらと時間がかかった。
結局、とある列挙型が何故か壊れるようだったので列挙型をやめてstd_logic_vectorとconstantの組み合わせに書きなおしたら直った。
xstはクソ。

その他

その他にもここで詳しく説明するほどではないが些細なバグがいくつかあった。

  • 最初の命令が実行されない
  • プログラムローダーが終了しない
  • signalの初期化忘れ

など

VHDLの書き方関係

基本的な方針としては(他の言語でも同じだけど)

  • 適当な大きさに分割して読みやすくする
  • 同じ処理、同じ値は一箇所にまとめて容易に書き換えられるようにする

ということに気をつけてコードを書いていた。

library

std_logic_arithは使わずにnumeric_stdを使った。

twoproc

wasabizさんの記事 twoprocの書き方 - wasabizの日記 参照
完全に従ってるわけではないがクロック同期な部分と組み合わせ回路を分けて書くと動作が分かりやすい。あとはprocessの中の方がforとか使えるので組み合わせ回路も書きやすい。

function, procedure

ひとまとまりの処理に名前をつける。

record

データをまとめる。
初期化に使う値はよく使うのでrecordの定義の直後にconstantとして定義しておくことが多かった。

type pohe is record
  p, o, h, e : std_logic;
end record;
constant pohe_zero : pohe := ('0', '0', '0', '0');
signal s : pohe := pohe_zero;

また、recordへの代入は

s <= (
  p => '1',
  o => '0',
  h => '1',
  e => '0'
);

のような書き方もできるので、これもよく使った。

列挙型
type pohe is (P, O, H, E);

std_logic_vectorとconstantの組み合わせでもできなくはないがこちらの方が使いやすい。
また、場合分けの時に列挙型とcaseの組み合わせで使うと(othersを使わなければ)漏れがあったときにエラーになるのでバグが減らせるかも。

constant (integer)

std_logic_vectorの幅の指定に使って、後から変更したくなった時に一箇所書き換えれば済むようにした。

知ってると便利かも

subtype

std_logic_vectorに何のデータが入っているのかを表す名前をつける目的でよく使った。

alias
signal s : std_logic_vector(31 downto 0);
alias pohe : std_logic_vector(7 downto 0) is s(31 downto 24);

とすれば

pohe <= x"41";

s(31 downto 24) <= x"41";

と同じである。