Everything the slides compress into thirty screenshots, unpacked: a synthesisable FIFO and its pointer arithmetic, a two-channel temperature monitor built from a register, a shift-add converter, an averaging filter and a FIFO controller, and the AMD2901 bit-slice microcontroller as a textbook datapath.投影片用三十張截圖壓縮掉的內容,在這裡全部攤開來:一個可合成的 FIFO 與它的指標運算、一套由暫存器、移位相加轉換器、平均濾波器與 FIFO 控制器組成的雙通道溫度監測系統,以及作為教科書級資料路徑範例的 AMD2901 bit-slice 微控制器。スライドが 30 枚のスクリーンショットに圧縮した内容を、ここですべて展開する。合成可能な FIFO とそのポインタ演算、レジスタ・シフト加算変換器・平均フィルタ・FIFO コントローラから成る 2 チャネル温度モニタ、そして教科書的なデータパスとしての AMD2901 ビットスライス・マイクロコントローラである。
The first seven lectures of this course each taught one thing. Lecture 1 introduced modules, nets and registers. Lecture 2 taught you how to build a testbench and watch a simulation. Lecture 3 covered syntax, user-defined primitives and the delay model. Lecture 4 moved from gates to behaviour. Lectures 5, 6 and 7 then narrowed the language down to the subset a logic synthesiser will accept, and told you which coding templates produce combinational logic, which produce flip-flops, and which produce the latches you did not want.本課程前七講每一講都只教一件事。第 1 講介紹模組、net 與 register。第 2 講教你怎麼寫測試平台、怎麼看一次模擬。第 3 講講語法、使用者自訂原件(UDP)與延遲模型。第 4 講從閘級走向行為級。第 5、6、7 講則把語言收斂到邏輯合成器願意接受的子集,並告訴你哪些寫法會產生組合邏輯、哪些會產生正反器、哪些會產生你根本不想要的閂鎖。本講義の最初の 7 回は、それぞれ一つのことだけを教えてきた。第 1 講はモジュール・ネット・レジスタを導入した。第 2 講はテストベンチの書き方とシミュレーションの見方を教えた。第 3 講は構文・ユーザ定義プリミティブ(UDP)・遅延モデルを扱った。第 4 講はゲートレベルから振る舞いレベルへ移った。第 5・6・7 講では言語を論理合成器が受け付ける部分集合まで絞り込み、どの記述テンプレートが組合せ論理を生み、どれがフリップフロップを生み、どれが望んでもいないラッチを生むのかを示した。
Lecture 8 does not introduce a single new construct. Every line of Verilog in it is something you have already seen. What it introduces is the thing no individual lecture can teach: judgement. A design example is a record of the decisions somebody made while turning a one-sentence specification into a netlist, and the only way to learn those decisions is to watch them being made and to argue with them afterwards. Two systems are put on the table.第 8 講沒有引進任何新的語法結構。裡面的每一行 Verilog 你都已經見過。它要引進的,是任何單獨一講都教不了的東西:判斷力。一個設計範例,其實是某個人把一句話的規格變成 netlist 時所做的一連串決定的紀錄;要學會這些決定,唯一的辦法就是看著它們被做出來,然後回過頭來質疑它們。這裡擺上桌的有兩套系統。第 8 講は新しい構文を一つも導入しない。そこに出てくる Verilog の行は、すべてすでに見たものばかりである。導入されるのは、個々の講義では教えられないもの、すなわち判断力である。設計例とは、1 文の仕様を netlist に変えるときに誰かが下した決定の記録であり、その決定を学ぶ唯一の方法は、決定が下される場に立ち会い、あとでそれに異を唱えてみることである。ここでは二つのシステムを机に載せる。
Both examples are presented in the slides as screenshots of code with almost no commentary. That is the gap this page fills. For every block you will get the specification, the architectural choice behind it, the Verilog, a reading of the lines that are not obvious, the timing behaviour, a sketch of the testbench you would write, and a description of what the synthesiser actually produces. Where the slide code is wrong — and in several places it is — the correct behaviour is explained and the discrepancy is flagged rather than repeated.這兩個範例在投影片裡都只是程式碼截圖,幾乎沒有任何說明。本頁要補的就是這段空白。每一個區塊你都會看到:規格、它背後的架構選擇、Verilog 程式碼、那些不是一眼就看得懂的行的解讀、時序行為、你會寫出什麼樣的測試平台,以及合成器實際上會產生什麼。投影片程式碼有錯的地方 —— 而且好幾處確實有錯 —— 本頁會說明正確的行為,並把差異標示出來,而不是照抄一遍。どちらの例もスライドではコードのスクリーンショットとして示されるだけで、解説はほとんどない。このページが埋めるのはその空白である。各ブロックについて、仕様、その背後にあるアーキテクチャ上の選択、Verilog コード、一読では分からない行の読み方、タイミング動作、書くことになるテストベンチの概略、そして合成器が実際に生成するものを示す。スライドのコードが誤っている箇所については——実際に数か所ある——正しい動作を説明し、相違点を指摘する。そのまま書き写すことはしない。
Get into the habit of asking the same three questions of every module you meet, in this order. They will carry you through both design examples and through the exam.養成習慣:遇到任何一個模組,都依序問自己同樣的三個問題。這三個問題會帶你走完兩個設計範例,也會帶你走完考試。どのモジュールに出会っても、同じ三つの問いをこの順で自分に投げかける習慣をつけたい。この三つが、二つの設計例と試験の両方を通して支えになる。
always block to flip-flops, a level-sensitive one with complete assignment to a cone of gates, an incomplete one to latches, a case to a multiplexer or a decoder, a constant shift to wiring, a variable shift to a barrel shifter, a memory array read by a continuous assignment to a read multiplexer. You should be able to draw the block diagram before the tool draws it for you.這個寫法會隱含什麼硬體?可合成子集裡的每一種寫法都對應一種結構:邊緣觸發的 always 區塊對應正反器,位準觸發且指定完整的對應一團組合閘,指定不完整的對應閂鎖,case 對應多工器或解碼器,常數移位對應接線,變動移位對應桶形移位器,用連續指定讀出的記憶體陣列對應一個讀取多工器。你應該要能在工具畫出方塊圖之前,自己先畫出來。この記述テンプレートはどんなハードウェアを含意するのか。合成可能な部分集合の各構文は、ある構造に対応する。エッジ記述の always ブロックはフリップフロップ、代入が完全なレベル記述は組合せゲートの集まり、不完全なものはラッチ、case はマルチプレクサまたはデコーダ、定数シフトは配線、可変シフトはバレルシフタ、連続代入で読み出すメモリ配列は読み出しマルチプレクサに対応する。ツールが描く前に、自分でブロック図を描けるようになりたい。The design examples are an exercise in applying rules you already have. It is worth laying them out in one table so that when a rule bites later you can see which lecture it came from. Each row is a rule we will invoke by name in the rest of this page.設計範例是把你已經擁有的規則拿來應用的練習。值得把它們列成一張表,這樣後面某條規則咬你一口時,你可以看出它是從哪一講來的。表中每一列都是本頁後面會直接點名引用的規則。設計例は、すでに手にしている規則を適用する演習である。一覧表にしておけば、後で規則に足をすくわれたとき、それがどの講から来たものかを確かめられる。各行は、このページの残りで名指しで参照する規則である。
| Rule規則規則 | From出處出典 | Where it bites in Lecture 8在第 8 講哪裡會咬人第 8 講でどこに効いてくるか |
|---|---|---|
Edge-sensitive always with posedge clk (optionally or posedge rst) infers flip-flops; assign with <=.含 posedge clk(必要時再加 or posedge rst)的邊緣觸發 always 會推論出正反器;要用 <= 指定。posedge clk(必要なら or posedge rst も)を持つエッジ記述の always はフリップフロップを推論する。代入は <= を使う。 | L6 | Data register, FIFO pointers, filter shift register, register file, Q register.資料暫存器、FIFO 指標、濾波器移位暫存器、暫存器檔、Q 暫存器。データレジスタ、FIFO のポインタ、フィルタのシフトレジスタ、レジスタファイル、Q レジスタ。 |
Level-sensitive always that fails to assign an output on some path infers a latch.位準觸發的 always 只要有某條路徑沒有指定到某個輸出,就會推論出閂鎖。レベル記述の always で、ある経路において出力に代入し損ねるとラッチが推論される。 | L5 | The write output of the FIFO controller; the default arms of every case in the 2901.FIFO 控制器的 write 輸出;2901 裡每個 case 的 default 分支。FIFO コントローラの write 出力、および 2901 の各 case の default 分岐。 |
A case that is not full and not parallel becomes a priority encoder, not a multiplexer.不完整(full)也不互斥(parallel)的 case 會變成優先編碼器,而不是多工器。full でも parallel でもない case は、マルチプレクサではなくプライオリティエンコーダになる。 | L5 | Operand selector, ALU function decode, destination decode.運算元選擇器、ALU 功能解碼、目的地解碼。オペランドセレクタ、ALU の機能デコード、デスティネーションのデコード。 |
Assigning z on some path of a procedural block infers a three-state driver.在程序區塊的某條路徑上指定 z,會推論出三態驅動器。手続き型ブロックのある経路で z を代入すると、トライステートドライバが推論される。 | L5 | FIFO controller data_out, register file ram0/ram3, Q register qs0/qs3, output selector, zero flag.FIFO 控制器的 data_out、暫存器檔的 ram0/ram3、Q 暫存器的 qs0/qs3、輸出選擇器、零旗標。FIFO コントローラの data_out、レジスタファイルの ram0/ram3、Q レジスタの qs0/qs3、出力セレクタ、ゼロフラグ。 |
* is expensive, / and % are only synthesisable by a power-of-two constant, and shifts by a constant are free wiring.* 很貴,/ 與 % 只有在除數是 2 的冪次常數時才可合成,而常數位移是免費的接線。* は高価であり、/ と % は 2 のべき乗の定数による場合しか合成できない。定数シフトは配線だけで済む。 | L7 | The whole of the Celsius-to-Fahrenheit converter and the divide-by-four in the filter.整個攝氏轉華氏轉換器,以及濾波器裡的除以四。摂氏華氏変換器の全体と、フィルタ内の 4 分周(4 で割る処理)。 |
A for loop must have statically computable bounds so the tool can unroll it; the index must be wide enough to exceed the limit.for 迴圈的邊界必須在靜態就能算出來,工具才能展開;而且索引的位元寬度必須大到能超過上限。for ループは、ツールが展開できるよう境界が静的に決まらなければならない。さらにループ変数は上限を超えられるだけの幅が必要である。 | L7 | The register-file reset loop, whose index needs five bits to count past 15.暫存器檔的重置迴圈,它的索引需要五個位元才數得過 15。レジスタファイルのリセットループ。ループ変数が 15 を越えて数えるには 5 ビット必要になる。 |
| Expression width is the maximum of the operand widths and the assignment target; context-determined operands are extended first and the operator applied afterwards.運算式的寬度是各運算元寬度與指定目標寬度的最大值;由上下文決定寬度的運算元會先被擴展,然後才套用運算子。式の幅は、各オペランドの幅と代入先の幅の最大値で決まる。文脈で幅が決まるオペランドはまず拡張され、その後に演算子が適用される。 | L3, L7 | The filter's four-term sum (silently truncated to 12 bits) and the ALU's ~s (silently widened to 5 bits).濾波器的四項總和(被悄悄截斷成 12 位元),以及 ALU 的 ~s(被悄悄擴展成 5 位元)。フィルタの 4 項の総和(黙って 12 ビットに切り詰められる)と、ALU の ~s(黙って 5 ビットに拡張される)。 |
Compiler directives such as `define are processed before synthesis, so symbolic opcodes cost nothing.`define 這類編譯器指令在合成之前就處理完了,所以符號化的運算碼不花任何成本。`define のようなコンパイラ指令は合成より前に処理されるため、記号的なオペコードはコストゼロである。 | L7 | All 24 AMD2901 mnemonics.AMD2901 全部 24 個助記符。AMD2901 の 24 個のニーモニックすべて。 |
Parameters are overridden at instantiation with #(...); forgetting the override silently uses the default.參數要在實例化時用 #(...) 覆寫;忘了覆寫就會悄悄沿用預設值。パラメータはインスタンス化時に #(...) で上書きする。上書きを忘れると既定値が黙って使われる。 | L7 | reg_load_R #(4) inside the Q register, whose default size is 8.Q 暫存器裡的 reg_load_R #(4),它的 size 預設值是 8。Q レジスタ内の reg_load_R #(4)。この size の既定値は 8 である。 |
| Positional port connection binds by order; named connection binds by name and survives a change of port order.位置式埠連接依順序綁定;具名連接依名稱綁定,埠的順序改了也不會出錯。位置指定のポート接続は順序で結合する。名前指定の接続は名前で結合するため、ポート順序が変わっても壊れない。 | L1, L7 | The 9-instance top level of the temperature monitor, where one transposed argument would be silent.溫度監測系統那個有九個實例的頂層模組 —— 在那裡把兩個引數對調,不會有任何警告。温度モニタの 9 インスタンスから成るトップレベル。そこで引数を 1 組入れ替えても何の警告も出ない。 |
Before the detail, fix the shape of both designs in your mind. The temperature monitor is a streaming datapath: data flows left to right through a fixed pipeline, every stage does the same thing on every clock, and the only control is at the very end where a host decides which buffer to drain. The AMD2901 is a controlled datapath: data circulates through a loop of register file → operand selector → ALU → back to register file, and a microinstruction word decides, every cycle, what each multiplexer in that loop does. Almost every digital system you will ever build is one of these two shapes or a composition of them.進入細節之前,先把兩個設計的形狀記在腦中。溫度監測系統是一條串流式資料路徑:資料由左往右流過一條固定的管線,每一級每個時脈都做同一件事,唯一的控制在最末端 —— 由主機決定要把哪一個緩衝區抽空。AMD2901 則是一條受控資料路徑:資料在「暫存器檔 → 運算元選擇器 → ALU → 回到暫存器檔」的迴路中循環,而一個微指令字每個週期決定這個迴路裡每一個多工器要做什麼。你這輩子會建的數位系統,幾乎都是這兩種形狀之一,或是兩者的組合。細部に入る前に、二つの設計の形を頭に入れておきたい。温度モニタはストリーミング型データパスである。データは固定されたパイプラインを左から右へ流れ、各段は毎クロック同じ処理を行い、制御は最後尾だけ——どちらのバッファを吸い出すかをホストが決める部分だけにある。AMD2901 は制御されたデータパスである。データはレジスタファイル → オペランドセレクタ → ALU → レジスタファイル、というループを巡り、マイクロ命令語がそのループ内の各マルチプレクサの動作を毎サイクル決める。これから作るディジタルシステムのほとんどは、この二つの形のどちらか、あるいはその組み合わせである。
select and a read strobe at the output.控制:管線內部沒有控制;輸出端有一個 2 位元的 select 與一個 read 脈衝。制御:パイプライン内には無し。出力側に 2 ビットの select と read ストローブがある。One more framing remark. Both examples were written in an era when Verilog-1995 was the language and a gate-array or standard-cell library was the target. That shows: outputs are re-declared as reg on a separate line rather than written output reg, three-state nets are used freely inside the chip, and blocking assignments appear in clocked blocks. None of that is fatal, but all of it would be flagged by a modern linter. Part of reading a design example critically is being able to say "this is historically normal and functionally fine" about one line and "this is a genuine race" about the next. We will do exactly that.最後再講一點背景。這兩個範例都寫在 Verilog-1995 還是主流語言、目標是閘陣列或標準元件庫的年代。這一點看得出來:輸出是另起一行重新宣告成 reg,而不是寫成 output reg;三態 net 在晶片內部被自由使用;時脈區塊裡出現阻塞指定。這些都不致命,但現代的 linter 每一項都會標出來。批判性地讀一個設計範例,有一部分就是要能對某一行說「這在當年很正常,功能上也沒問題」,而對下一行說「這是真正的競爭」。我們接下來就要這樣做。もう一点、背景を補っておく。どちらの例も、Verilog-1995 が標準でありゲートアレイや標準セルライブラリが実装対象だった時代に書かれている。それは記述にも表れている。出力は output reg とは書かず、別行で reg として再宣言される。チップ内部でトライステートのネットが自由に使われる。クロック同期ブロックにブロッキング代入が現れる。どれも致命的ではないが、現代の linter ならすべて指摘するだろう。設計例を批判的に読むとは、ある行については「これは当時として普通で機能上も問題ない」と言い、次の行については「これは本物のレースである」と言えることでもある。これから実際にそれをやる。
A first-in first-out buffer exists to decouple a producer from a consumer. The producer in our system is the filter, which emits one averaged sample every clock, forever, whether anybody is listening or not. The consumer is a host processor that reads whenever it feels like it. Without a buffer between them, every sample produced while the host is busy is simply lost, and the host must poll at the producer's rate or not at all. With a buffer of depth $N$, the host may be absent for up to $N$ sample periods and still miss nothing.先進先出緩衝器的存在,是為了讓生產者與消費者解耦。在我們這套系統裡,生產者是濾波器:它每個時脈都吐出一筆平均後的樣本,不管有沒有人在聽。消費者是一顆主處理器,它高興的時候才來讀。中間若沒有緩衝區,主機忙碌期間產生的每一筆樣本就直接遺失,而主機只能以生產者的速率輪詢,否則就什麼也拿不到。有了深度 $N$ 的緩衝區,主機最多可以離開 $N$ 個樣本週期而仍不漏掉任何東西。先入れ先出しバッファは、生産者と消費者を切り離すために存在する。本システムの生産者はフィルタであり、誰かが聞いていようがいまいが、毎クロック平均済みのサンプルを 1 つ出し続ける。消費者はホストプロセッサで、気が向いたときに読む。両者の間にバッファがなければ、ホストが取り込めない間に作られたサンプルはすべて失われ、ホストは生産者と同じレートでポーリングするしかなくなる。深さ $N$ のバッファがあれば、ホストは最大 $N$ サンプル周期のあいだ離れていても何も取りこぼさない。
That is the whole value proposition, and it comes with the whole set of problems. A buffer of finite depth can be full, at which point the producer must be told to stop or its data will be dropped. It can be empty, at which point the consumer must be told to wait or it will read rubbish. Those two flags are not decoration; they are the flow-control protocol, and getting them right is the entire intellectual content of a FIFO design. Everything else — the memory, the pointers, the output register — is bookkeeping.這就是 FIFO 的全部價值,而它也帶來全部的問題。有限深度的緩衝區會滿,這時必須通知生產者停下來,否則資料就被丟掉。它也會空,這時必須通知消費者等一下,否則它會讀到垃圾。那兩個旗標不是裝飾品,它們就是流量控制協定,而把它們做對,正是整個 FIFO 設計的全部智力內容。其餘的東西 —— 記憶體、指標、輸出暫存器 —— 都只是記帳。これが FIFO の価値のすべてであり、同時に問題のすべても連れてくる。有限の深さを持つバッファは満杯になりうる。そのときは生産者に止まるよう伝えなければ、データは捨てられる。空にもなりうる。そのときは消費者に待つよう伝えなければ、ゴミを読むことになる。この二つのフラグは飾りではなくフロー制御のプロトコルそのものであり、これを正しく作ることが FIFO 設計の知的内容のすべてである。残り——メモリ、ポインタ、出力レジスタ——はすべて帳簿付けにすぎない。
Words come out in the order they went in; a write when stack_full is asserted is refused; a read when stack_empty is asserted returns nothing meaningful. Any FIFO that honours those three statements is correct, whatever its internal organisation. Any FIFO that lets the flags lie — that says "not full" one cycle before it overflows, or "not empty" when there is nothing to give — is broken, no matter how elegant its code.資料字按進入的順序出來;在 stack_full 被拉高時的寫入會被拒絕;在 stack_empty 被拉高時的讀取不會傳回任何有意義的東西。任何 FIFO 只要守住這三句話就是正確的,內部怎麼組織都無所謂。任何讓旗標說謊的 FIFO —— 在溢位前一個週期還說「未滿」,或在沒東西可給時說「非空」—— 就是壞的,程式碼再漂亮也一樣。ワードは入った順に出てくる。stack_full が立っているときの書き込みは拒否される。stack_empty が立っているときの読み出しは意味のある値を返さない。この三つを守る FIFO は、内部構成がどうであれ正しい。フラグに嘘をつかせる FIFO——オーバフローの 1 サイクル前に「満杯でない」と言う、渡すものが無いのに「空でない」と言う——は、どれほどコードが美しくても壊れている。
The slide draws the FIFO twice: once as a black box with eight connections, and once with its internals showing. Read the black box first, because the port list is the specification.投影片把 FIFO 畫了兩次:一次是有八條連線的黑盒子,一次是把內部攤開來。先讀黑盒子,因為埠列表就是規格。スライドは FIFO を 2 回描いている。1 回は 8 本の接続を持つブラックボックスとして、もう 1 回は内部を見せた形で。まずブラックボックスを読むとよい。ポートリストこそが仕様だからである。
| Port埠ポート | Width寬度幅 | Direction方向方向 | Meaning and protocol意義與協定意味とプロトコル |
|---|---|---|---|
Data_in | stack_width = 12 | input輸入入力 | The word to be stored. Sampled at the rising edge on which write_to_stack is high and the FIFO is not full.要被存起來的資料字。在 write_to_stack 為高且 FIFO 未滿的那個上升邊緣被取樣。格納されるワード。write_to_stack が High かつ FIFO が満杯でない立ち上がりエッジでサンプルされる。 |
Data_out | 12 | output, registered輸出,已暫存出力、レジスタ付き | The oldest stored word. Updated at the rising edge on which read_from_stack is high and the FIFO is not empty, so it is valid from the following cycle.最舊的那個已存資料字。在 read_from_stack 為高且 FIFO 非空的那個上升邊緣更新,因此它從下一個週期起才有效。格納されている最も古いワード。read_from_stack が High かつ FIFO が空でない立ち上がりエッジで更新されるため、有効になるのは次のサイクルからである。 |
stack_empty | 1 | output, combinational輸出,組合邏輯出力、組合せ | High when the occupancy is zero. The consumer must inspect it before asserting a read.佔用量為零時為高。消費者在提出讀取之前必須先檢查它。占有数がゼロのとき High になる。消費者は読み出しを立てる前に必ずこれを調べる。 |
stack_full | 1 | output, combinational輸出,組合邏輯出力、組合せ | High when the occupancy equals stack_height. The producer must inspect it before asserting a write.佔用量等於 stack_height 時為高。生產者在提出寫入之前必須先檢查它。占有数が stack_height に等しいとき High になる。生産者は書き込みを立てる前に必ずこれを調べる。 |
clk | 1 | input輸入入力 | Single clock. Both the write side and the read side are in this domain; this is a synchronous FIFO, not a clock-domain crossing.單一時脈。寫入側與讀取側都在這個時脈域裡;這是同步 FIFO,不是跨時脈域的 FIFO。単一クロック。書き込み側も読み出し側もこのドメインに属する。これは同期 FIFO であり、クロックドメイン間をまたぐものではない。 |
rst | 1 | input輸入入力 | Asynchronous, active high. Clears the pointers, the occupancy and the output register. Note it does not clear the memory array.非同步、高電位有效。清除指標、佔用量與輸出暫存器。注意它不會清除記憶體陣列。非同期、アクティブ High。ポインタ、占有数、出力レジスタをクリアする。メモリ配列はクリアしない点に注意する。 |
write_to_stack | 1 | input輸入入力 | Write request for the coming rising edge.對即將到來的上升邊緣提出寫入請求。次に来る立ち上がりエッジに対する書き込み要求。 |
read_from_stack | 1 | input輸入入力 | Read request for the coming rising edge.對即將到來的上升邊緣提出讀取請求。次に来る立ち上がりエッジに対する読み出し要求。 |
Three things in that table are design decisions rather than facts of nature, and you should notice them as decisions. First, the flags are combinational functions of registered state, which means they are stable for a whole cycle and describe the FIFO as it stands before the next edge; this is the easy and safe choice, and it costs one gate delay plus a comparator on the flag path. Second, Data_out is registered, which means a read request and its data are one cycle apart; the alternative, a combinational read, is discussed in §3.7. Third, the memory is not reset, which is exactly right for hardware — resetting 96 flip-flops costs area and a reset tree, and the empty flag already prevents anyone from reading uninitialised content.那張表裡有三件事是設計決定,而不是自然法則,你應該要能看出它們是決定。第一,旗標是暫存狀態的組合函數,因此整個週期都穩定,描述的是下一個邊緣之前的 FIFO 狀態;這是簡單又安全的選擇,代價是旗標路徑上多一級閘延遲加一個比較器。第二,Data_out 是已暫存的,所以讀取請求與它的資料相差一個週期;另一種做法是組合式讀取,見 §3.7。第三,記憶體不重置,對硬體而言這完全正確 —— 重置 96 個正反器要付出面積與一棵重置樹,而空旗標本來就擋住了任何人去讀未初始化的內容。この表の中の三つは自然の法則ではなく設計上の決定であり、決定として意識すべきものである。第一に、フラグはレジスタ値の組合せ関数である。したがって 1 サイクルのあいだ安定で、次のエッジより前の FIFO の状態を表す。これは単純で安全な選択で、代償はフラグ経路上のゲート遅延 1 段と比較器 1 個である。第二に、Data_out はレジスタ出力であり、読み出し要求とそのデータは 1 サイクル離れる。もう一方の選択肢である組合せ読み出しは §3.7 で扱う。第三に、メモリはリセットしない。これはハードウェアとしてまったく正しい。96 個のフリップフロップをリセットすれば面積とリセットツリーを食うし、空フラグがそもそも未初期化の内容を読ませないからである。
The three parameters make the module reusable: stack_width sets the data path, stack_height sets the depth, and stack_ptr_width sets the pointer size. The last one is redundant information — it must equal $\log_2(\textit{stack\_height})$ — and that redundancy is a trap. Override the height to 16 and forget to override the pointer width, and you get a FIFO with 16 words of memory of which only 8 are ever addressed, with flags that insist it is full at 16. A modern design would compute the pointer width with $clog2 instead of asking the user.三個參數讓這個模組可以重複使用:stack_width 決定資料路徑寬度,stack_height 決定深度,stack_ptr_width 決定指標大小。最後一個是冗餘資訊 —— 它必須等於 $\log_2(\textit{stack\_height})$ —— 而這個冗餘就是陷阱。把 height 覆寫成 16 卻忘了覆寫指標寬度,你就得到一個有 16 字記憶體但永遠只定址到其中 8 字的 FIFO,而且旗標堅持它在 16 字時才滿。現代的寫法會用 $clog2 自己算出指標寬度,而不是要使用者提供。三つのパラメータがこのモジュールを再利用可能にしている。stack_width がデータパス幅を、stack_height が深さを、stack_ptr_width がポインタのサイズを決める。最後のものは冗長な情報であり——$\log_2(\textit{stack\_height})$ に等しくなければならない——その冗長さが落とし穴になる。height を 16 に上書きしてポインタ幅の上書きを忘れると、16 ワードのメモリを持ちながら 8 ワードしかアドレスされず、しかも 16 で満杯だと言い張るフラグを持つ FIFO ができあがる。現代的な設計なら、利用者に聞かずに $clog2 でポインタ幅を計算する。
The internal organisation is the classic circular buffer. A block of memory stack[0]…stack[stack_height-1] is addressed by two pointers. write_ptr holds the address of the next free slot; read_ptr holds the address of the oldest word still in the buffer. A write stores Data_in at stack[write_ptr] and advances write_ptr. A read fetches stack[read_ptr] and advances read_ptr. Neither pointer ever moves backwards and neither is ever reset in normal operation; they simply count up and wrap.內部組織就是經典的環狀緩衝區。一塊記憶體 stack[0]…stack[stack_height-1] 由兩個指標定址。write_ptr 存放下一個空位的位址;read_ptr 存放仍在緩衝區裡最舊那個字的位址。一次寫入把 Data_in 存到 stack[write_ptr] 並讓 write_ptr 前進。一次讀取取出 stack[read_ptr] 並讓 read_ptr 前進。兩個指標都不會倒退,正常運作時也都不會被重置;它們只是一直往上數,然後繞回去。内部構成は古典的なリングバッファである。メモリ領域 stack[0]…stack[stack_height-1] を二つのポインタでアドレスする。write_ptr は次の空きスロットのアドレスを、read_ptr はバッファ内に残る最も古いワードのアドレスを保持する。書き込みは Data_in を stack[write_ptr] に格納し、write_ptr を進める。読み出しは stack[read_ptr] を取り出し、read_ptr を進める。どちらのポインタも後戻りせず、通常動作ではリセットもされない。ただ数え上がり、折り返すだけである。
The wrap is free, and this is the first small piece of hardware thinking in the design. read_ptr and write_ptr are three bits wide and the depth is eight. Incrementing 3'b111 gives 3'b000 with the carry discarded, which is precisely modulo-8 arithmetic. There is no comparison against the depth, no conditional reset of the pointer to zero, no adder wider than it needs to be. A power-of-two depth buys you the modulo for nothing. If you ever specify a FIFO of depth 10, you pay for it: the pointer needs four bits, it can reach 15, and every increment now needs ptr == 9 ? 0 : ptr + 1, which is an extra comparator and an extra multiplexer in the critical path. Choose powers of two unless the specification forbids it.繞回是免費的,而這是這個設計裡第一處小小的硬體思維。read_ptr 與 write_ptr 都是三位元寬,深度是八。把 3'b111 加一得到 3'b000,進位被丟掉,這正好就是模 8 運算。不需要跟深度比較,不需要有條件地把指標清零,也不需要比必要寬度更寬的加法器。深度取 2 的冪次,模運算就白送你。如果哪天你規定 FIFO 深度是 10,你就得付代價:指標要四位元、會數到 15,而每次加一都變成 ptr == 9 ? 0 : ptr + 1,關鍵路徑上就多了一個比較器和一個多工器。除非規格不允許,否則請選 2 的冪次。折り返しはタダであり、これがこの設計における最初の小さなハードウェア的思考である。read_ptr と write_ptr は 3 ビット幅で、深さは 8 である。3'b111 に 1 を足すとキャリーが捨てられて 3'b000 になり、これがちょうど 8 を法とする演算になる。深さとの比較も、ポインタを条件付きでゼロに戻す処理も、必要以上に広い加算器も要らない。2 のべき乗の深さは、剰余演算をタダで与えてくれる。深さ 10 の FIFO を仕様に書けば、その代償を払うことになる。ポインタは 4 ビット必要で 15 まで届き、インクリメントは毎回 ptr == 9 ? 0 : ptr + 1 となって、クリティカルパスに比較器 1 個とマルチプレクサ 1 個が増える。仕様が禁じない限り 2 のべき乗を選ぶべきである。
Here is the one genuinely subtle thing about FIFOs, and it is worth slowing down for because it is asked in examinations constantly.接下來是 FIFO 唯一真正微妙的地方,值得放慢速度看,因為考試考個不停。ここからが FIFO で唯一本当に微妙な点である。試験で繰り返し問われるので、速度を落として読む価値がある。
Suppose you have only the two pointers and nothing else. Start from reset with both pointers at zero; the buffer is empty and write_ptr == read_ptr. Now write eight words. Each write advances write_ptr: 0, 1, 2, …, 7, and after the eighth write it wraps back to 0. The buffer is now full, and write_ptr == read_ptr again. The two pointers hold exactly the same pair of values in both situations, and the state of the memory is the only thing that differs — which the pointers cannot see.假設你只有那兩個指標,別的什麼都沒有。從重置開始,兩個指標都是零;緩衝區是空的,而且 write_ptr == read_ptr。現在寫入八個字。每次寫入讓 write_ptr 前進:0、1、2、…、7,第八次寫完之後它繞回 0。緩衝區現在是滿的,而 write_ptr == read_ptr 又成立了。兩種情況下兩個指標的值完全相同,唯一不同的是記憶體的狀態 —— 而指標看不到記憶體。ポインタ 2 本のほかに何も無いとしよう。リセット直後、両方のポインタはゼロである。バッファは空で、write_ptr == read_ptr が成り立つ。ここで 8 ワード書き込む。書き込みごとに write_ptr は 0、1、2、…、7 と進み、8 回目のあとで 0 へ折り返す。バッファは今や満杯だが、再び write_ptr == read_ptr である。二つの状況でポインタの値の組はまったく同じで、違うのはメモリの状態だけ——そしてポインタはメモリを見ることができない。
With $\log_2 N$-bit pointers, the condition write_ptr == read_ptr is necessary for both "empty" and "full" and sufficient for neither. The pointer pair takes $N^2$ distinct values but the FIFO has $N+1$ distinct occupancies (0 through $N$); the map from pointers to occupancy is $(\textit{wp} - \textit{rp}) \bmod N$, which is onto $\{0,\dots,N-1\}$ and therefore cannot distinguish occupancy 0 from occupancy $N$. You need at least one extra bit of state, somewhere, or you must reduce the number of reachable occupancies.用 $\log_2 N$ 位元的指標時,條件 write_ptr == read_ptr 對「空」與「滿」都是必要條件,對兩者都不是充分條件。指標對一共有 $N^2$ 種取值,但 FIFO 有 $N+1$ 種相異的佔用量(0 到 $N$);從指標映到佔用量的函數是 $(\textit{wp} - \textit{rp}) \bmod N$,它映滿 $\{0,\dots,N-1\}$,因此根本無法區分佔用量 0 與佔用量 $N$。你至少得在某處多放一個位元的狀態,否則就得減少可達的佔用量個數。$\log_2 N$ ビットのポインタでは、条件 write_ptr == read_ptr は「空」と「満杯」の双方にとって必要条件であり、どちらに対しても十分条件ではない。ポインタの組は $N^2$ 通りの値を取るが、FIFO の占有数は $N+1$ 通り(0 から $N$)ある。ポインタから占有数への写像は $(\textit{wp} - \textit{rp}) \bmod N$ であり、$\{0,\dots,N-1\}$ の上への写像になるため、占有数 0 と占有数 $N$ を区別できない。どこかに最低 1 ビットの状態を追加するか、到達可能な占有数を減らすしかない。
That counting argument tells you immediately that there are exactly three families of solution: add a bit of state that counts, add a bit of state to each pointer, or throw away one of the occupancies. All three are used in practice and you should be able to write and cost each of them.這個計數論證立刻告訴你:解法正好有三族 —— 加一個會計數的狀態位元、給每個指標各加一個狀態位元,或是丟掉其中一種佔用量。這三種在實務上都有人用,而你應該都能寫出來並估出成本。この数え上げの議論から、解法はちょうど三つの系統しかないことがただちに分かる。数えるための状態を 1 ビット足すか、各ポインタに状態を 1 ビットずつ足すか、占有数を一つ捨てるかである。三つとも実務で使われており、どれも書けて、コストを見積もれるようになりたい。
This is the cure the lecture's FIFO uses. Carry a separate register ptr_diff that holds the number of words currently stored. It is incremented on a successful write, decremented on a successful read, and left alone otherwise. Because the occupancy ranges from 0 to $N$ inclusive, it needs $\log_2 N + 1$ bits: in our case four bits for a depth of eight. The declaration reg [stack_ptr_width:0] ptr_diff; says exactly that — note the missing -1, which is the whole point of the line and which students routinely copy as [stack_ptr_width-1:0] and then spend an afternoon debugging.這就是本講的 FIFO 所採用的解法。另外帶一個暫存器 ptr_diff,存放目前已存的字數。寫入成功時加一,讀取成功時減一,其餘情況不動。因為佔用量的範圍是 0 到 $N$(含兩端),它需要 $\log_2 N + 1$ 個位元:在我們這裡,深度八就要四位元。宣告 reg [stack_ptr_width:0] ptr_diff; 說的正是這件事 —— 注意那個不見了的 -1,這才是這一行的重點,而學生常常照抄成 [stack_ptr_width-1:0],然後花一個下午除錯。これが本講の FIFO が採る解法である。現在格納されているワード数を保持する別レジスタ ptr_diff を持つ。書き込みが成立すれば増やし、読み出しが成立すれば減らし、それ以外では変えない。占有数は 0 から $N$ まで(両端を含む)なので $\log_2 N + 1$ ビット必要であり、深さ 8 の本件では 4 ビットになる。宣言 reg [stack_ptr_width:0] ptr_diff; はまさにそれを言っている。消えている -1 に注意したい。そこがこの行の肝であり、学生はしばしば [stack_ptr_width-1:0] と書き写して半日デバッグに費やす。
With $N = 8$, the full test is a comparison against 4'b1000, which is just ptr_diff[3] — a single wire, since the counter never exceeds 8. The empty test is a four-input NOR. Both flags are therefore essentially free. The cost of the cure is the counter itself: four flip-flops and an incrementer/decrementer, or about 30 to 40 gate equivalents in a standard-cell library.當 $N = 8$ 時,滿的判斷是跟 4'b1000 比較,也就是只看 ptr_diff[3] —— 一條線而已,因為計數值永遠不會超過 8。空的判斷是一個四輸入 NOR。所以兩個旗標基本上都是免費的。這個解法的代價是計數器本身:四個正反器加一個加一/減一電路,在標準元件庫裡大約是 30 到 40 個等效閘。$N = 8$ のとき、満杯の判定は 4'b1000 との比較、すなわち ptr_diff[3] を見るだけで済む。カウンタが 8 を超えないので配線 1 本である。空の判定は 4 入力 NOR である。したがってフラグは実質タダである。この解法のコストはカウンタ自体であり、フリップフロップ 4 個とインクリメンタ/デクリメンタ、標準セルライブラリでおよそ 30〜40 ゲート相当になる。
An occupancy counter is a single register that must be updated by both the write side and the read side. That is fine in a synchronous FIFO, where both sides share one clock. It is impossible in an asynchronous FIFO, where the write side runs on wclk and the read side on rclk: a single counter would have to be clocked by two unrelated clocks. This is the reason you will not find this style in any clock-domain-crossing FIFO, and the reason Cure 2 is the industry standard for those.佔用量計數器是單一一個暫存器,卻必須由寫入側與讀取側共同更新。在同步 FIFO 裡這沒問題,因為兩側共用一個時脈。但在非同步 FIFO 裡這是不可能的:寫入側跑 wclk,讀取側跑 rclk,單一個計數器就得被兩個毫無關係的時脈驅動。這就是為什麼你在任何跨時脈域的 FIFO 裡都看不到這種寫法,也是為什麼解法二才是那種場合的業界標準。占有数カウンタは一つのレジスタでありながら、書き込み側と読み出し側の双方が更新しなければならない。両側が同じクロックを共有する同期 FIFO なら問題ない。しかし書き込み側が wclk、読み出し側が rclk で動く非同期 FIFO では不可能である。単一のカウンタを無関係な二つのクロックで叩くことになるからである。クロックドメインをまたぐ FIFO でこの流儀を見かけない理由はここにあり、その場面では解法 2 が業界標準になる理由でもある。
Make each pointer one bit wider than the address it supplies. For a depth of eight, use four-bit pointers whose low three bits address the memory and whose top bit is a "lap counter" that toggles every time the pointer wraps. Now the pointer pair carries enough information on its own.讓每個指標都比它要提供的位址多一個位元。深度為八時,就用四位元的指標:低三位元去定址記憶體,最高位元是一個「圈數計數器」,指標每繞回一次就翻轉一次。這樣指標對本身就帶有足夠的資訊了。各ポインタを、供給するアドレスより 1 ビット広く取る。深さ 8 なら 4 ビットのポインタとし、下位 3 ビットでメモリをアドレスし、最上位ビットはポインタが折り返すたびに反転する「周回カウンタ」とする。これでポインタの組だけで十分な情報を持つようになる。
In words: the FIFO is empty when the pointers agree completely, including the lap bit — the writer has written exactly as many words as the reader has read. It is full when the addresses agree but the lap bits differ — the writer is exactly one whole lap ahead of the reader. Read it once more and convince yourself that these two conditions are mutually exclusive and cover the two coincident cases.用白話說:當兩個指標連圈數位元都完全相同時,FIFO 是空的 —— 寫入者寫進去的字數正好等於讀取者讀走的字數。當位址相同但圈數位元不同時,FIFO 是滿的 —— 寫入者正好領先讀取者整整一圈。請再讀一次,說服自己這兩個條件互斥,而且涵蓋了兩種重合的情況。言葉にすれば、周回ビットまで含めてポインタが完全に一致するとき FIFO は空である——書き手が書いたワード数と読み手が読んだワード数がちょうど等しい。アドレスは一致するが周回ビットが異なるとき FIFO は満杯である——書き手が読み手のちょうど 1 周先にいる。もう一度読み返して、この二つの条件が互いに排他的で、かつ一致する二つの場合を覆い尽くしていることを自分で納得してほしい。
// Cure 2: four-bit pointers for an eight-deep FIFO reg [3:0] wp, rp; wire do_wr = write_to_stack && !stack_full; wire do_rd = read_from_stack && !stack_empty; always @(posedge clk or posedge rst) if (rst) begin wp <= 4'd0; rp <= 4'd0; end else begin if (do_wr) wp <= wp + 4'd1; if (do_rd) rp <= rp + 4'd1; end assign stack_empty = (wp == rp); assign stack_full = (wp[3] != rp[3]) && (wp[2:0] == rp[2:0]); // memory is addressed with the LOW three bits only // stack[wp[2:0]] <= Data_in; Data_out <= stack[rp[2:0]];
Two bonuses come with this style. First, the occupancy is available for free as wp - rp (a four-bit subtraction interpreted modulo 16, which gives the correct 0…8 answer), so you can expose an "almost full" threshold without a separate counter. Second, and much more important, each pointer is now written by exactly one clock domain, which is what makes it safe to Gray-code the pointers and synchronise them across a clock boundary with two flip-flops. That is how every asynchronous FIFO in every modern SoC works, and recognising the wrap-bit trick here is the bridge from this lecture to that topic.這種寫法還附帶兩個好處。第一,佔用量免費可得,就是 wp - rp(四位元減法,以模 16 解讀,會給出正確的 0…8 答案),所以你不必另設計數器就能提供一個「快滿了」的門檻。第二,而且重要得多:現在每個指標都只被一個時脈域寫入,這正是「把指標改成格雷碼、再用兩級正反器同步過時脈邊界」之所以安全的原因。現代 SoC 裡每一個非同步 FIFO 都是這樣做的,而在這裡認出繞回位元這個技巧,就是從本講通往那個主題的橋。この流儀には二つのおまけがつく。第一に、占有数が wp - rp としてタダで得られる(4 ビットの減算を 16 を法として解釈すれば、正しく 0…8 が出る)。したがって別途カウンタを置かずに「もうすぐ満杯」のしきい値を外に出せる。第二に、そしてこちらがはるかに重要だが、各ポインタがちょうど一つのクロックドメインからのみ書かれるようになる。これこそ、ポインタをグレイコード化してフリップフロップ 2 段でクロック境界を越えて同期させても安全な理由である。現代の SoC の非同期 FIFO はすべてこの方式であり、ここでラップビットの技法を見抜けることが、本講からその話題への橋渡しになる。
The cheapest cure in logic: declare that the FIFO is full when there is exactly one free slot left, so the pointers never actually coincide in the full state.邏輯上最便宜的解法:規定「還剩剛好一個空位時就算滿」,這樣在滿的狀態下兩個指標永遠不會真正重合。論理としては最も安価な解法である。空きスロットがちょうど 1 つ残った時点で満杯と宣言してしまえば、満杯状態でポインタが実際に一致することはなくなる。
No extra register at all; just an incrementer and a comparator on the write pointer, and the incremented value is usually available anyway because you need it for the next write. The price is one word of storage out of $N$. For an eight-deep, twelve-bit FIFO that is 12 flip-flops thrown away to save a 4-bit counter — a bad trade. For a 4096-deep FIFO built from a RAM macro, one word is negligible and the cure is excellent. The arithmetic is simple: the cure is good when a word of storage is cheaper than $\log_2 N + 1$ flip-flops plus an up/down counter, which is true as soon as the memory is a compiled RAM rather than a pile of flops.完全不需要額外的暫存器;只要在寫指標上加一個加一電路與一個比較器,而且加一後的值通常本來就有,因為下一次寫入要用它。代價是 $N$ 個字裡損失一個。對一個八字深、十二位元寬的 FIFO 來說,這等於丟掉 12 個正反器來省下一個 4 位元計數器 —— 很划不來。但對一個用 RAM 巨集做的 4096 字深 FIFO 來說,一個字微不足道,這個解法就非常好。算式很簡單:當一個字的儲存比 $\log_2 N + 1$ 個正反器加一個上/下數計數器還便宜時,這個解法就划算;而只要記憶體是編譯出來的 RAM 而不是一堆正反器,這就成立了。追加のレジスタは一切要らない。書き込みポインタにインクリメンタと比較器を置くだけで、しかもインクリメント結果は次の書き込みに必要なので大抵すでに手元にある。代償は $N$ ワードのうち 1 ワードを失うことである。深さ 8・幅 12 ビットの FIFO なら、4 ビットのカウンタ 1 個を節約するためにフリップフロップ 12 個を捨てることになり、割に合わない。RAM マクロで作る深さ 4096 の FIFO なら 1 ワードは無視でき、この解法は非常に優れている。算数は単純で、1 ワードの記憶が $\log_2 N + 1$ 個のフリップフロップと上下カウンタより安いときにこの解法はよい。メモリがフリップフロップの山ではなくコンパイルされた RAM になった時点で、それは成り立つ。
| Cure解法解法 | Extra state額外狀態追加の状態 | Usable capacity可用容量使える容量 | Flag logic旗標邏輯フラグ論理 | Works across clock domains?能跨時脈域嗎?クロックドメインをまたげるか |
|---|---|---|---|---|
| Occupancy counter佔用量計數器占有数カウンタ | $\log_2 N + 1$ flops plus up/down logic$\log_2 N + 1$ 個正反器加上/下數邏輯$\log_2 N + 1$ 個のフリップフロップと上下カウント論理 | $N$ | Two equality comparisons against constants兩次與常數的相等比較定数との等値比較 2 回 | No — one register, two clocks不行 —— 一個暫存器,兩個時脈不可——レジスタ 1 個に対しクロック 2 本 |
| Extra wrap bit多一個繞回位元ラップビットを追加 | One flop per pointer (two in total)每個指標各多一個正反器(共兩個)ポインタごとにフリップフロップ 1 個(合計 2 個) | $N$ | One $(n{+}1)$-bit and one $n$-bit comparison一次 $(n{+}1)$ 位元比較加一次 $n$ 位元比較$(n{+}1)$ ビット比較 1 回と $n$ ビット比較 1 回 | Yes, and it is the standard method可以,而且這是標準做法可能であり、しかもこれが標準的な方法である |
| Sacrifice one slot犧牲一個位置1 スロットを犠牲にする | None無なし | $N - 1$ | One comparison against an incremented pointer一次與加一後指標的比較インクリメント後のポインタとの比較 1 回 | Yes, but the incrementer sits in the compare path可以,但加一電路落在比較路徑上可能だが、インクリメンタが比較経路に入る |
The lecture's choice of the occupancy counter is the right one for its context: a single clock, a small depth, a flop-based memory, and a desire for the clearest possible code. You should still be able to rewrite it in either of the other two styles on demand, because the question "your FIFO must now cross from a 100 MHz domain to a 156.25 MHz domain, what changes?" is a standard interview and examination question, and the answer begins "the occupancy counter has to go".本講選擇佔用量計數器,以它的情境而言是對的:單一時脈、深度很小、用正反器做記憶體,而且希望程式碼盡可能清楚。但你仍然應該能隨時把它改寫成另外兩種寫法,因為「你的 FIFO 現在要從 100 MHz 域跨到 156.25 MHz 域,哪裡要改?」是標準的面試與考試題目,而答案的第一句就是「佔用量計數器必須拿掉」。本講が占有数カウンタを選んだのは、その文脈では正しい。単一クロック、浅い深さ、フリップフロップによるメモリ、そしてできる限り分かりやすいコードという条件に合っている。それでも、求めに応じて他の二つの流儀に書き換えられるようにしておきたい。「この FIFO を 100 MHz ドメインから 156.25 MHz ドメインへ渡すことになった。何が変わるか」は面接でも試験でも定番の問いであり、答えは「占有数カウンタは捨てることになる」から始まるからである。
Problem: A FIFO is 32 words deep and 16 bits wide, built from standard-cell flip-flops. For each of the three cures, state the pointer width, the extra state in flip-flops, the usable capacity in words, and the total flip-flop count for the whole FIFO including the memory and the registered output. Which cure is cheapest here, and would your answer change if the memory were a compiled single-port SRAM instead of flops?問題:一個 FIFO 深度 32 字、寬 16 位元,用標準元件的正反器實作。請對三種解法各寫出指標寬度、以正反器計的額外狀態、以字計的可用容量,以及包含記憶體與已暫存輸出在內整個 FIFO 的正反器總數。在這裡哪一種解法最便宜?如果記憶體改成編譯出來的單埠 SRAM 而不是正反器,你的答案會改變嗎?問題:深さ 32 ワード、幅 16 ビットの FIFO を標準セルのフリップフロップで作る。三つの解法それぞれについて、ポインタ幅、フリップフロップ数で表した追加の状態、ワード数で表した使える容量、そしてメモリとレジスタ出力を含む FIFO 全体のフリップフロップ総数を示せ。ここではどの解法が最も安いか。メモリがフリップフロップではなくコンパイルされたシングルポート SRAM だったら答えは変わるか。
Memory: $32 \times 16 = 512$ flops. Registered output: 16 flops. Those are common to all three.記憶體:$32 \times 16 = 512$ 個正反器。已暫存輸出:16 個正反器。這兩項三種解法都一樣。メモリ:$32 \times 16 = 512$ 個のフリップフロップ。レジスタ出力:16 個。この二つは三つの解法に共通である。
| Cure解法解法 | Pointer width指標寬度ポインタ幅 | Extra state額外狀態追加の状態 | Capacity容量容量 | Total flops正反器總數フリップフロップ総数 |
|---|---|---|---|---|
| Counter計數器カウンタ | 5 + 5 = 10 | 6 (a 6-bit counter for 0…32)6(0…32 要用 6 位元計數器)6(0…32 を数えるには 6 ビットのカウンタ) | 32 | 512 + 16 + 10 + 6 = 544 |
| Wrap bit繞回位元ラップビット | 6 + 6 = 12 | 0 beyond the pointers指標之外為 0ポインタ以外は 0 | 32 | 512 + 16 + 12 = 540 |
| One slot犧牲一個位置1 スロット犠牲 | 5 + 5 = 10 | 0 | 31 | 512 + 16 + 10 = 538 |
In flip-flops the three are within 1 % of each other, so the decision is not made on area: the memory dominates completely. The counter cure costs an up/down counter in combinational logic that the others do not need, and it is the only one that cannot be split across clock domains, so the wrap-bit cure is the sensible default.以正反器數來看,三者的差距在 1 % 以內,所以決策不是靠面積做的:記憶體完全主導一切。計數器解法多出一個另外兩種不需要的上/下數組合邏輯,而且它是唯一不能拆到兩個時脈域的,所以繞回位元解法是合理的預設選擇。フリップフロップ数で見ると三者の差は 1 % 以内であり、面積で決まる話ではない。メモリが完全に支配しているからである。カウンタ解法は他の二つが要らない上下カウントの組合せ論理を必要とし、しかもクロックドメインをまたいで分割できない唯一の方式でもある。したがってラップビット解法が妥当な既定値になる。
If the memory becomes a compiled SRAM, the 512 flops disappear from the flop count and are replaced by a macro of roughly one tenth the area. Now the remaining flops matter relatively more, the one-slot cure's wasted word costs essentially nothing (one row of an SRAM), and the one-slot cure becomes genuinely the cheapest. But the SRAM also forces the read to be synchronous with a one-cycle latency, which changes the Data_out timing discussed in §3.7, so the choice of memory and the choice of flag scheme are not independent decisions.如果記憶體換成編譯出來的 SRAM,那 512 個正反器就從正反器數裡消失,換成一個面積約十分之一的巨集。這時剩下的正反器相對就重要得多,而犧牲一個位置所浪費的那個字幾乎不花什麼(SRAM 的一列而已),於是犧牲一個位置真的變成最便宜的解法。但 SRAM 同時也強迫讀取變成同步、帶一個週期的延遲,這會改變 §3.7 討論的 Data_out 時序 —— 所以記憶體的選擇與旗標方案的選擇並不是兩個獨立的決定。メモリがコンパイル済み SRAM になれば、512 個のフリップフロップはカウントから消え、面積にしておよそ 10 分の 1 のマクロに置き換わる。すると残りのフリップフロップの比重が相対的に大きくなり、1 スロット犠牲の解法が捨てる 1 ワードは実質タダ(SRAM の 1 行)になるため、1 スロット犠牲が本当に最も安くなる。ただし SRAM は読み出しを 1 サイクル遅延の同期読み出しに強制するため、§3.7 で論じる Data_out のタイミングが変わる。メモリの選択とフラグ方式の選択は独立した決定ではないのである。
Problem: An eight-deep FIFO uses Cure 2 with four-bit pointers. Starting from wp = rp = 4'b0000, eleven words are written and four are read, in the order W W W W W W W W R R R R, with the FIFO refusing illegal operations. Give wp, rp, stack_empty, stack_full and the occupancy after the sequence. Then continue with four more writes and give the same quantities again.問題:一個八字深的 FIFO 用解法二,指標四位元。從 wp = rp = 4'b0000 開始,依序進行 W W W W W W W W R R R R,共寫入十一次、讀取四次,FIFO 會拒絕不合法的操作。請給出這串操作之後的 wp、rp、stack_empty、stack_full 與佔用量。接著再寫入四次,再給出同樣這幾個量。問題:深さ 8 の FIFO で解法 2 を使い、ポインタは 4 ビットとする。wp = rp = 4'b0000 から始めて、W W W W W W W W R R R R の順に書き込み 11 回・読み出し 4 回を試みる。FIFO は不正な操作を拒否する。この系列のあとの wp、rp、stack_empty、stack_full、占有数を示せ。続けてさらに 4 回書き込み、同じ量をもう一度示せ。
Eight writes take wp from 0000 through 1000. At that point wp[3] = 1, rp[3] = 0, and wp[2:0] = rp[2:0] = 000, so stack_full = 1. The problem says eleven writes were attempted; the ninth, tenth and eleventh are refused because the FIFO is full, so wp stays at 1000. This is why the flags must be checked by the FIFO itself and not merely advertised to the producer.八次寫入把 wp 從 0000 推到 1000。此時 wp[3] = 1、rp[3] = 0,而 wp[2:0] = rp[2:0] = 000,所以 stack_full = 1。題目說嘗試寫入十一次;第九、十、十一次因為 FIFO 已滿而被拒絕,所以 wp 停在 1000。這正是為什麼旗標必須由 FIFO 自己檢查,而不是只公告給生產者看。8 回の書き込みで wp は 0000 から 1000 まで進む。この時点で wp[3] = 1、rp[3] = 0、かつ wp[2:0] = rp[2:0] = 000 なので stack_full = 1 である。問題では 11 回の書き込みを試みるが、9・10・11 回目は満杯のため拒否され、wp は 1000 のまま動かない。フラグを生産者に知らせるだけでなく、FIFO 自身が必ず検査しなければならない理由がここにある。
Four reads then take rp from 0000 to 0100. Now wp = 1000, rp = 0100: not equal, so not empty; wp[2:0] is 000 while rp[2:0] is 100, so not full. Occupancy $= 1000_2 - 0100_2 = 4$. Four words held, four free.接著四次讀取把 rp 從 0000 推到 0100。現在 wp = 1000、rp = 0100:不相等,所以不空;wp[2:0] 是 000 而 rp[2:0] 是 100,所以不滿。佔用量 $= 1000_2 - 0100_2 = 4$。存了四個字,還空四個。続く 4 回の読み出しで rp は 0000 から 0100 へ進む。いま wp = 1000、rp = 0100 で、一致しないので空ではない。wp[2:0] は 000、rp[2:0] は 100 なので満杯でもない。占有数は $1000_2 - 0100_2 = 4$ である。4 ワード保持、4 ワード空き。
Four more writes take wp from 1000 to 1100. Now wp[3] = 1, rp[3] = 0 differ and wp[2:0] = rp[2:0] = 100, so stack_full = 1 again, occupancy $1100_2 - 0100_2 = 8$. Note that the full condition has now been detected at a completely different memory address from the first time — the lap bit, not the address, is what carries the information.再四次寫入把 wp 從 1000 推到 1100。現在 wp[3] = 1 與 rp[3] = 0 不同,而 wp[2:0] = rp[2:0] = 100,所以 stack_full = 1 又成立,佔用量 $1100_2 - 0100_2 = 8$。注意這次偵測到滿的記憶體位址,跟第一次完全不同 —— 帶著資訊的是圈數位元,不是位址。さらに 4 回書き込むと wp は 1000 から 1100 へ進む。wp[3] = 1 と rp[3] = 0 は異なり、wp[2:0] = rp[2:0] = 100 なので再び stack_full = 1、占有数は $1100_2 - 0100_2 = 8$ である。今回満杯が検出されたメモリアドレスは 1 回目とまったく違う点に注意したい。情報を運んでいるのはアドレスではなく周回ビットである。
Here is the complete module as the slides give it, cleaned up only in layout — the content is unchanged, including the defects, which are dissected in §3.6. Read it once for shape, then we will take it apart.以下是投影片給的完整模組,只在排版上整理過 —— 內容完全沒動,連缺陷也保留,那些缺陷會在 §3.6 解剖。先讀一遍抓形狀,然後我們再把它拆開。以下はスライドにあるモジュールの全体で、整えたのはレイアウトだけである。内容は欠陥も含めてそのままで、欠陥は §3.6 で解剖する。まず全体の形をつかむために一度通して読み、そのあとで分解していく。
module FIFO (Data_out, Data_in, stack_empty, stack_full,
clk, rst, write_to_stack, read_from_stack);
parameter stack_width = 12; // width of stack and data paths
parameter stack_height = 8; // height of stack, in words
parameter stack_ptr_width = 3; // width of pointer that addresses the stack
output [stack_width-1:0] Data_out;
output stack_empty, stack_full;
input [stack_width-1:0] Data_in;
input clk, rst;
input write_to_stack, read_from_stack;
reg [stack_ptr_width-1:0] read_ptr, write_ptr; // addresses for reading and writing
reg [stack_ptr_width:0] ptr_diff; // gap between the pointers: 0..8 needs 4 bits
reg [stack_width-1:0] Data_out;
reg [stack_width-1:0] stack [stack_height-1:0]; // the memory array
assign stack_empty = (ptr_diff == 0) ? 1'b1 : 1'b0;
assign stack_full = (ptr_diff == stack_height) ? 1'b1 : 1'b0;
always @ (posedge clk or posedge rst) begin: data_transfer
if (rst) Data_out = 0;
else if ((read_from_stack) && (!write_to_stack) && (!stack_empty))
Data_out <= stack[read_ptr];
else if ((write_to_stack) && (!read_from_stack) && (!stack_full))
stack[write_ptr] <= Data_in;
end // data_transfer
always @ (posedge clk or posedge rst) begin: update_stack_ptrs
if (rst) begin
read_ptr <= 0;
write_ptr <= 0;
ptr_diff <= 0;
end
else
if ((write_to_stack) && (!stack_full) && (!read_from_stack)) begin
write_ptr <= write_ptr + 1; // address for the next clock edge
ptr_diff <= ptr_diff + 1;
end
else if ((!write_to_stack) && (!stack_empty) && (read_from_stack)) begin
read_ptr <= read_ptr + 1;
ptr_diff <= ptr_diff - 1;
end
end // update_stack_ptrs
endmodule
The port declarations follow the Verilog-1995 convention the whole course uses: the module header lists bare names, and the directions and widths come afterwards in the body. Data_out appears twice, once as output [stack_width-1:0] and once as reg [stack_width-1:0]. That is not a redefinition; it is the only legal way in 1995 syntax to say that a port is driven from a procedural block. Verilog-2001 lets you collapse it to output reg [11:0] Data_out, and Lecture 1 showed both. The other outputs, stack_empty and stack_full, are not re-declared because they are driven by continuous assignments, which must drive nets.埠宣告遵循整門課一路使用的 Verilog-1995 慣例:模組標頭只列出名字,方向與寬度在本體後面才給。Data_out 出現兩次,一次是 output [stack_width-1:0],一次是 reg [stack_width-1:0]。那不是重複定義;在 1995 語法裡,這是唯一合法的方式來表達「這個埠由程序區塊驅動」。Verilog-2001 允許你把它縮寫成 output reg [11:0] Data_out,第 1 講兩種都示範過。另外兩個輸出 stack_empty 與 stack_full 沒有重新宣告,因為它們由連續指定驅動,而連續指定必須驅動 net。ポート宣言は本講義が一貫して使う Verilog-1995 の流儀に従う。モジュールヘッダには名前だけを並べ、方向と幅は本体側で後から与える。Data_out は 2 回現れ、1 回は output [stack_width-1:0]、もう 1 回は reg [stack_width-1:0] である。これは再定義ではなく、「このポートは手続き型ブロックから駆動される」と言うための 1995 構文における唯一の合法な書き方である。Verilog-2001 なら output reg [11:0] Data_out とまとめられ、第 1 講では両方を示した。他の出力 stack_empty と stack_full が再宣言されていないのは、それらが連続代入で駆動され、連続代入はネットを駆動しなければならないからである。
The memory is declared reg [stack_width-1:0] stack [stack_height-1:0]. Read the two ranges in the right order: the first range is the width of each element, the second is the number of elements. stack[3] is a 12-bit word; stack[3][7] is one bit of it in Verilog-2001 but is illegal in 1995, where you must copy the word to a temporary first. A memory may not appear as a whole in an expression, may not be a port, and may not be assigned by a continuous assignment; it is read and written one word at a time inside procedural code, which is exactly what the two always blocks do.記憶體宣告成 reg [stack_width-1:0] stack [stack_height-1:0]。兩個範圍要照正確順序讀:第一個範圍是每個元素的寬度,第二個是元素的個數。stack[3] 是一個 12 位元的字;stack[3][7] 在 Verilog-2001 裡是它的其中一個位元,但在 1995 裡不合法,那時你得先把整個字複製到暫時變數。記憶體不可以整個出現在運算式裡、不可以當埠、也不可以被連續指定賦值;它只能在程序性程式碼裡一次讀寫一個字,而這正是那兩個 always 區塊在做的事。メモリは reg [stack_width-1:0] stack [stack_height-1:0] と宣言される。二つの範囲は正しい順で読む必要がある。前の範囲が各要素の幅、後ろが要素数である。stack[3] は 12 ビットのワードであり、stack[3][7] は Verilog-2001 ではそのうちの 1 ビットを指すが、1995 では不正で、まずワードを一時変数へ写す必要がある。メモリは式の中に全体として現れることも、ポートになることも、連続代入で駆動されることもできない。手続き型コードの中で 1 ワードずつ読み書きするものであり、二つの always ブロックがまさにそれを行っている。
The highlighted line is the one to remember. reg [stack_ptr_width:0] ptr_diff gives four bits, not three, because the occupancy of an eight-word FIFO has nine possible values. Change it to [stack_ptr_width-1:0] and ptr_diff becomes a mod-8 counter: after eight writes it reads zero, stack_empty goes high while the FIFO is physically full, stack_full can never assert because ptr_diff == 8 is unreachable, and the FIFO will happily overwrite itself for ever. The symptom in simulation is maddening — data comes out in the right order for the first few words and then silently scrambles — which is why this one-character difference is worth memorising.標示出來的那一行才是要記住的。reg [stack_ptr_width:0] ptr_diff 給的是四位元而不是三位元,因為八字 FIFO 的佔用量有九種可能值。把它改成 [stack_ptr_width-1:0],ptr_diff 就變成一個模 8 計數器:寫滿八次之後它讀回零,stack_empty 在 FIFO 實體上已滿時被拉高,stack_full 永遠不可能成立(因為 ptr_diff == 8 到不了),於是 FIFO 會開開心心地永遠覆寫自己。模擬時的症狀讓人抓狂 —— 前幾個字順序還對,之後就悄悄亂掉 —— 所以這一個字元的差別值得背起來。強調された行こそ覚えておくべきものである。reg [stack_ptr_width:0] ptr_diff は 3 ビットではなく 4 ビットを与える。8 ワードの FIFO の占有数は 9 通りあるからである。これを [stack_ptr_width-1:0] に変えると ptr_diff は 8 を法とするカウンタになる。8 回書き込んだ時点で値はゼロに戻り、物理的には満杯なのに stack_empty が立ち、ptr_diff == 8 には到達できないので stack_full は決して立たず、FIFO は平然と自分自身を上書きし続ける。シミュレーション上の症状は実に厄介で、最初の数ワードは正しい順で出てきて、その後に黙って乱れる。この 1 文字の違いを覚えておく価値はそこにある。
Both flags are continuous assignments with a conditional operator. The conditional is redundant: (ptr_diff == 0) is already a one-bit value, so assign stack_empty = (ptr_diff == 0); is identical and shorter. The explicit ? 1'b1 : 1'b0 is a stylistic habit from a time when people worried about the width of comparison results; it costs nothing and synthesises to the same comparator.兩個旗標都是帶條件運算子的連續指定。那個條件運算子是多餘的:(ptr_diff == 0) 本來就是一個一位元的值,所以 assign stack_empty = (ptr_diff == 0); 完全等價而且更短。明寫 ? 1'b1 : 1'b0 是舊時代的風格習慣,當年大家會擔心比較結果的位元寬度;它不花成本,合成出來也是同一個比較器。どちらのフラグも条件演算子つきの連続代入である。この条件演算子は冗長で、(ptr_diff == 0) はもともと 1 ビットの値なので、assign stack_empty = (ptr_diff == 0); と書けば同じ意味で短い。? 1'b1 : 1'b0 を明示するのは、比較結果の幅を気にしていた時代のスタイル上の習慣である。コストはかからず、合成結果も同じ比較器になる。
What matters is when these flags are valid. They are pure combinational functions of ptr_diff, which is a register. Therefore, during any given clock cycle, the flags describe the FIFO's occupancy as it was established by the previous rising edge. They are stable for the whole cycle, they change shortly after the edge, and the producer and consumer can sample them with ordinary setup and hold margins. This is the behaviour you want and it is why both flags can be fed straight back into the two always blocks without creating a loop: the blocks only look at the flags at an edge, and at that instant the flags still describe the pre-edge state.真正重要的是這些旗標什麼時候有效。它們是 ptr_diff 的純組合函數,而 ptr_diff 是暫存器。因此在任何一個時脈週期內,旗標描述的都是上一個上升邊緣所確立的佔用量。它們整個週期都穩定,在邊緣後不久才改變,生產者與消費者可以用一般的 setup/hold 餘裕去取樣它們。這正是你要的行為,也是為什麼兩個旗標可以直接餵回那兩個 always 區塊而不會造成迴路:區塊只在邊緣看旗標,而在那一瞬間旗標描述的仍然是邊緣之前的狀態。本当に重要なのは、このフラグがいつ有効かである。フラグは ptr_diff の純粋な組合せ関数であり、ptr_diff はレジスタである。したがって任意のクロックサイクル中、フラグは直前の立ち上がりエッジで確定した占有数を表す。1 サイクルのあいだ安定しており、エッジの直後に変化するので、生産者も消費者も通常のセットアップ/ホールドの余裕でサンプルできる。これが望ましい動作であり、両方のフラグをそのまま二つの always ブロックへ戻してもループにならない理由でもある。ブロックはエッジでしかフラグを見ず、その瞬間のフラグはまだエッジ前の状態を表しているからである。
A consumer that sees stack_empty = 0 at edge $n$ may assert read_from_stack for edge $n{+}1$ and is guaranteed to get a word, because nothing can remove a word except that consumer itself. A producer that sees stack_full = 0 is similarly safe. The flags are not stale in any way that matters, because in a single-clock FIFO only the two agents reading the flags can change the occupancy. In an asynchronous FIFO this stops being true: the empty flag must be computed pessimistically on the read side, because the write pointer it compares against is a synchronised, therefore delayed, copy. "Pessimistic" means it may say empty when a word has just arrived — never the reverse.在邊緣 $n$ 看到 stack_empty = 0 的消費者,可以為邊緣 $n{+}1$ 拉起 read_from_stack,而且保證拿得到一個字,因為除了它自己以外沒有人能把字拿走。看到 stack_full = 0 的生產者同理也是安全的。旗標在任何有意義的層面上都不算過時,因為在單一時脈的 FIFO 裡,只有讀旗標的那兩個角色能改變佔用量。在非同步 FIFO 裡這就不成立了:空旗標必須在讀取側以悲觀的方式計算,因為它拿來比較的寫指標是同步過來的、因此是延遲的副本。「悲觀」的意思是它可能在一個字剛到時仍說空 —— 絕不會反過來。エッジ $n$ で stack_empty = 0 を見た消費者は、エッジ $n{+}1$ に向けて read_from_stack を立ててよく、必ず 1 ワードを得られる。ワードを取り去れるのはその消費者自身だけだからである。stack_full = 0 を見た生産者も同様に安全である。単一クロックの FIFO では、フラグを読む二者だけが占有数を変えられるので、フラグが古いことは実質的に問題にならない。非同期 FIFO ではこれが成り立たなくなる。空フラグは読み出し側で悲観的に計算しなければならない。比較対象の書き込みポインタが同期済み、すなわち遅れたコピーだからである。「悲観的」とは、ワードが到着した直後でも空と言うことがある、という意味であり、その逆は決して起きない。
The first always block, named data_transfer by the begin: data_transfer label, moves data in and out of the memory. The named block is not decoration: it gives the block an identifier you can use in hierarchical references from a testbench, and it is required if you want to declare local variables inside it.第一個 always 區塊靠 begin: data_transfer 這個標籤被命名為 data_transfer,它負責把資料搬進與搬出記憶體。命名區塊不是裝飾:它給了這個區塊一個識別字,讓測試平台可以用階層式參照指到它,而且如果你想在區塊內宣告區域變數,命名是必要的。最初の always ブロックは begin: data_transfer というラベルで data_transfer と命名されており、メモリへのデータの出し入れを担当する。ブロックへの命名は飾りではない。テストベンチから階層参照で指せる識別子を与えるものであり、ブロック内でローカル変数を宣言したい場合には必須でもある。
Three mutually exclusive branches follow the reset. The read branch fires when a read is requested, a write is not, and the FIFO is not empty; it copies stack[read_ptr] into the output register. The write branch fires when a write is requested, a read is not, and the FIFO is not full; it copies Data_in into stack[write_ptr]. When neither branch fires nothing happens and both the memory and the output register hold — which is the correct behaviour for an edge-triggered block and costs nothing, because flip-flops hold by default.重置之後跟著三個互斥的分支。讀取分支在「有讀取請求、沒有寫入請求、且 FIFO 不空」時啟動,它把 stack[read_ptr] 複製到輸出暫存器。寫入分支在「有寫入請求、沒有讀取請求、且 FIFO 未滿」時啟動,它把 Data_in 複製到 stack[write_ptr]。兩個分支都沒啟動時什麼事都不發生,記憶體與輸出暫存器都保持原值 —— 這正是邊緣觸發區塊該有的行為,而且不花任何成本,因為正反器本來就預設保持。リセットの後に、互いに排他的な三つの分岐が続く。読み出し分岐は、読み出し要求があり、書き込み要求がなく、FIFO が空でないときに発火し、stack[read_ptr] を出力レジスタへ写す。書き込み分岐は、書き込み要求があり、読み出し要求がなく、FIFO が満杯でないときに発火し、Data_in を stack[write_ptr] へ写す。どちらも発火しなければ何も起こらず、メモリも出力レジスタも値を保持する。これはエッジ記述ブロックとして正しい動作であり、フリップフロップは既定で保持するのでコストもかからない。
Notice the asymmetry the branches create. Because the conditions include !write_to_stack and !read_from_stack respectively, a cycle in which both requests are high does nothing at all: no word is stored and no word is fetched. The write is lost and the read returns stale data. For a FIFO that is sitting between an unstoppable producer and an intermittent consumer, this is a serious functional limitation, and §3.6 shows the fix.注意這些分支造成的不對稱。因為條件裡分別含有 !write_to_stack 與 !read_from_stack,所以兩個請求同時為高的那一個週期什麼都不做:沒有字被存進去,也沒有字被取出來。寫入遺失,讀取傳回舊資料。對一個夾在停不下來的生產者與斷斷續續的消費者之間的 FIFO 來說,這是嚴重的功能限制,而 §3.6 會給出修法。この分岐が生む非対称性に注意したい。条件にそれぞれ !write_to_stack と !read_from_stack が入っているため、両方の要求が High になるサイクルでは何も起こらない。ワードは格納されず、取り出されもしない。書き込みは失われ、読み出しは古いデータを返す。止まらない生産者と断続的な消費者のあいだに挟まる FIFO としては深刻な機能上の制約であり、§3.6 で修正を示す。
Notice also what the read branch does not do: it does not clear the memory word it has just read. The word stays in stack[read_ptr] until it is overwritten several laps later. That is right — clearing it would cost a second write port — and it means that if you dump the memory contents in a waveform viewer you will see old data lying around behind the read pointer. That is normal and is not a bug.也注意讀取分支沒做什麼:它不會清掉剛讀出的那個記憶體字。那個字會一直留在 stack[read_ptr],直到好幾圈之後被覆寫。這是對的 —— 清掉它要多一個寫入埠 —— 而這也表示如果你在波形檢視器裡把記憶體內容倒出來,會看到讀指標後面還躺著舊資料。那是正常的,不是 bug。読み出し分岐がしないことにも注意したい。読み出したメモリのワードをクリアしないのである。そのワードは数周あとに上書きされるまで stack[read_ptr] に残り続ける。これは正しい。クリアするには書き込みポートがもう一つ要るからである。そのため波形ビューアでメモリの中身を覗くと、読み出しポインタの後ろに古いデータが残って見える。これは正常であり、バグではない。
The second block maintains read_ptr, write_ptr and ptr_diff. Its branch conditions are written to mirror the data block exactly, which is the essential discipline here: the pointer must move if and only if the data moved. If the two blocks ever disagree — if a word is written but the pointer is not advanced, or the pointer advances with no word written — the FIFO is corrupt and the corruption is invisible until data comes out in the wrong order.第二個區塊維護 read_ptr、write_ptr 與 ptr_diff。它的分支條件寫得與資料區塊完全對應,而這正是這裡最關鍵的紀律:資料動了,指標才可以動;指標動了,資料也一定動過。只要這兩個區塊有一次不一致 —— 存了字卻沒推進指標,或指標推進了卻沒存字 —— FIFO 就壞了,而且這種壞法要等到資料以錯誤順序出來才看得見。二つ目のブロックは read_ptr、write_ptr、ptr_diff を保守する。その分岐条件はデータ側のブロックと正確に対応するよう書かれており、これがここでの要となる規律である。データが動いたとき、かつそのときに限り、ポインタも動かなければならない。両ブロックが一度でも食い違えば——ワードを書いたのにポインタを進めない、あるいはワードを書かずにポインタだけ進める——FIFO は壊れ、その壊れ方はデータが誤った順で出てくるまで見えない。
That duplication of the conditions is also the weakness of this coding style. The condition appears twice, in two different always blocks, and a maintainer who changes one and not the other introduces exactly the bug just described. A more robust style computes the conditions once:條件被重複寫兩次,也正是這種寫法的弱點。同一個條件出現在兩個不同的 always 區塊裡,而一個維護者只改了其中一個,就會製造出剛才講的那個 bug。比較穩固的寫法會把條件只算一次:条件が二度書かれることは、この流儀の弱点でもある。同じ条件が二つの別々の always ブロックに現れるため、片方だけを直した保守者は、いま述べたバグをそのまま作り込むことになる。より堅牢な流儀では条件を一度だけ計算する。
wire do_write = write_to_stack && !stack_full;
wire do_read = read_from_stack && !stack_empty;
always @(posedge clk or posedge rst)
if (rst) Data_out <= 0;
else if (do_read) Data_out <= stack[read_ptr];
always @(posedge clk)
if (do_write) stack[write_ptr] <= Data_in; // memory: no reset
always @(posedge clk or posedge rst)
if (rst) begin write_ptr <= 0; read_ptr <= 0; ptr_diff <= 0; end
else begin
if (do_write) write_ptr <= write_ptr + 1'b1;
if (do_read) read_ptr <= read_ptr + 1'b1;
case ({do_write, do_read})
2'b10: ptr_diff <= ptr_diff + 1'b1;
2'b01: ptr_diff <= ptr_diff - 1'b1;
default: ptr_diff <= ptr_diff; // 00 and 11 both leave the count alone
endcase
end
This version is shorter, states the intent once, and — because do_write and do_read no longer exclude each other — supports simultaneous read and write, with the occupancy correctly unchanged when both happen. It also separates the memory into its own always @(posedge clk) block with no reset, which is what you want if the memory is ever to be mapped onto a RAM macro: a RAM has no reset pin, and a reset in the inference template will prevent the tool from recognising it.這個版本更短,意圖只敘述一次,而且 —— 因為 do_write 與 do_read 不再互相排斥 —— 支援同時讀寫,並在兩者同時發生時正確地讓佔用量維持不變。它也把記憶體分到自己的 always @(posedge clk) 區塊、不帶重置,這正是你希望記憶體有朝一日能映射到 RAM 巨集時該做的事:RAM 沒有重置腳位,推論樣板裡有重置會讓工具認不出它。この版はより短く、意図を一度だけ述べており、しかも do_write と do_read が互いを排除しなくなったため、同時の読み書きに対応し、両方が起きたときは占有数が正しく変わらないままになる。さらにメモリをリセットなしの always @(posedge clk) ブロックへ分けている。これはメモリをいずれ RAM マクロへ割り当てたい場合に望ましい形である。RAM にはリセットピンがなく、推論テンプレートにリセットがあるとツールが RAM と認識できなくなるからである。
This design is an unusually clean illustration of the nonblocking rule from Lecture 6, because the two always blocks genuinely read each other's outputs.這個設計是第 6 講非阻塞規則異常乾淨的範例,因為那兩個 always 區塊真的在讀彼此的輸出。この設計は、第 6 講のノンブロッキング規則を際立ってきれいに示す例である。二つの always ブロックが本当に互いの出力を読んでいるからである。
At a rising clock edge, both blocks are triggered in an order the language does not define. The pointer block evaluates !stack_full, which is a function of ptr_diff, and the data block evaluates stack[read_ptr], which is a function of read_ptr. Both ptr_diff and read_ptr are updated by the pointer block at this very edge. If those updates were made with blocking assignments, the new values would be visible immediately, and whether the data block saw the old or the new read_ptr would depend on which block the simulator happened to execute first — a race, and the two orders produce different, both plausible-looking, waveforms.在時脈上升邊緣,兩個區塊都被觸發,而語言並不規定它們的執行順序。指標區塊會求值 !stack_full,那是 ptr_diff 的函數;資料區塊會求值 stack[read_ptr],那是 read_ptr 的函數。而 ptr_diff 與 read_ptr 都正好在這個邊緣被指標區塊更新。如果那些更新用的是阻塞指定,新值會立刻可見,於是資料區塊看到的是舊的還是新的 read_ptr,就取決於模擬器剛好先執行哪個區塊 —— 這就是競爭,而且兩種順序會產生不同、卻都看起來合理的波形。クロックの立ち上がりエッジで両方のブロックが起動するが、その順序を言語は定めていない。ポインタブロックは ptr_diff の関数である !stack_full を評価し、データブロックは read_ptr の関数である stack[read_ptr] を評価する。そして ptr_diff も read_ptr も、まさにこのエッジでポインタブロックが更新する。もしその更新がブロッキング代入なら新しい値がただちに見えてしまい、データブロックが古い read_ptr を見るか新しい方を見るかは、シミュレータがどちらのブロックを先に実行したかで決まる。これがレースであり、二つの順序はそれぞれ異なる、しかしどちらももっともらしい波形を生む。
With nonblocking assignments the race disappears. Every right-hand side in the time step is evaluated using the pre-edge values; all the left-hand sides are updated afterwards, in the nonblocking update region. The data block is therefore guaranteed to index the memory with the old read_ptr — the address of the word that is actually the oldest — and to test the old stack_full — the occupancy before this edge's write. Both are what the hardware does, because in the hardware the flip-flop outputs feeding the address multiplexer and the comparator do not change until after the edge has propagated.用非阻塞指定,競爭就消失了。該時間步內的每一個右手邊都用邊緣前的值求值;所有左手邊則在之後的非阻塞更新區間才更新。因此資料區塊保證會用舊的 read_ptr 去索引記憶體 —— 也就是真正最舊那個字的位址 —— 並且測試舊的 stack_full,也就是這個邊緣的寫入之前的佔用量。這兩件事都正是硬體的行為,因為在硬體裡,餵給位址多工器與比較器的那些正反器輸出,要等到邊緣傳播完才會改變。ノンブロッキング代入を使えばレースは消える。その時間ステップのすべての右辺はエッジ前の値で評価され、左辺はその後のノンブロッキング更新領域でまとめて更新される。したがってデータブロックは必ず古い read_ptr でメモリを引き——それが実際に最も古いワードのアドレスである——古い stack_full、すなわちこのエッジの書き込み以前の占有数を判定する。どちらもハードウェアの振る舞いそのものである。ハードウェアでは、アドレスのマルチプレクサと比較器へ入るフリップフロップ出力は、エッジが伝わり終わるまで変化しないからである。
Use <= for every signal assigned inside an edge-triggered always block. Use = for every signal assigned inside a level-sensitive (combinational) always block. Never assign the same variable from two different always blocks. Never mix = and <= on the same variable. These four sentences eliminate essentially every simulation-synthesis mismatch in RTL code, and the FIFO breaks the fourth one.在邊緣觸發的 always 區塊裡指定的每一個訊號,都用 <=。在位準觸發(組合)的 always 區塊裡指定的每一個訊號,都用 =。絕不要在兩個不同的 always 區塊裡指定同一個變數。絕不要在同一個變數上混用 = 與 <=。這四句話基本上消除了 RTL 程式碼裡所有的模擬/合成不一致,而這個 FIFO 違反了第四句。エッジ記述の always ブロック内で代入するすべての信号には <= を使う。レベル記述(組合せ)の always ブロック内で代入するすべての信号には = を使う。同じ変数を二つの異なる always ブロックから代入しない。同じ変数に = と <= を混ぜない。この四つで RTL のシミュレーション・合成不一致はほぼ一掃でき、この FIFO は四つ目に違反している。
The listing on slide 5 contains three problems. One is a typographical accident, one is a style violation that happens to be harmless here, and one is a genuine design limitation. Being able to tell them apart is the skill this section is really teaching.投影片第 5 頁的程式碼有三個問題。一個是排版上的意外,一個是風格違規但在這裡剛好無害,還有一個是真正的設計限制。能把它們分辨開來,才是這一節真正要教的本事。スライド 5 ページ目のコードには三つの問題がある。一つは組版上の事故、一つはスタイル違反だがここではたまたま無害なもの、そしてもう一つは本物の設計上の制約である。この三つを見分けられることこそ、この節が本当に教えている技能である。
As printed, the second branch of the data block reads印出來的樣子是,資料區塊的第二個分支寫成印刷されたままでは、データブロックの二つ目の分岐はこう書かれている。
else if ((write_to_stack) && (!read_from_stack) && (!stack_full));
stack [write_ptr] <= Data_in;
with a semicolon immediately after the closing parenthesis (and an unbalanced extra parenthesis besides). In Verilog, as in C, a lone semicolon is a null statement and is a perfectly legal body for an if. The condition is therefore evaluated, nothing is done either way, and the following line — stack[write_ptr] <= Data_in; — is not part of the if at all. It becomes the next statement in the block, executed unconditionally on every clock edge that is not a reset.在右括號後面緊接一個分號(而且還多了一個不成對的括號)。在 Verilog 裡跟在 C 裡一樣,孤零零的分號是一個空敘述,而且完全合法地可以當作 if 的本體。於是條件照樣被求值,但不論真假都不做任何事,而下一行 —— stack[write_ptr] <= Data_in; —— 根本就不屬於那個 if。它變成區塊裡的下一個敘述,在每一個不是重置的時脈邊緣都無條件執行。閉じ括弧の直後にセミコロンが付いている(しかも対応の取れない括弧も 1 個余分にある)。Verilog でも C と同じく、単独のセミコロンは空文であり、if の本体としてまったく合法である。したがって条件は評価されるが、真でも偽でも何も行われず、次の行 stack[write_ptr] <= Data_in; は if の一部ではなくなる。それはブロック内の次の文となり、リセットでないすべてのクロックエッジで無条件に実行される。
The consequence is that every rising edge writes Data_in into stack[write_ptr], whether a write was requested or not, whether the FIFO is full or not, and even on cycles when a read is in progress. The pointers, however, still advance only on a legal write, so the slot under write_ptr is simply rewritten with whatever happens to be on Data_in until a real write moves the pointer on. A FIFO built this way appears to work for simple test patterns — the last value written before the pointer advances is the one that survives — and corrupts data as soon as Data_in changes between the request and the edge. Treat it as a transcription error in the slide, not as intended behaviour, and let it teach you how much a semicolon can cost.結果是每一個上升邊緣都會把 Data_in 寫進 stack[write_ptr]:不管有沒有提出寫入請求、不管 FIFO 滿不滿,甚至在讀取進行中的週期也照寫。但指標仍然只在合法寫入時才前進,所以 write_ptr 指到的那個位置就被 Data_in 當下的值一再重寫,直到一次真正的寫入把指標推走為止。這樣建出來的 FIFO 在簡單的測試樣式下看起來會正常 —— 指標前進之前最後寫進去的那個值會活下來 —— 但只要 Data_in 在請求與邊緣之間變動,資料就毀了。把它當成投影片的謄寫錯誤,而不是有意的行為,並讓它教你一個分號可以多貴。結果として、すべての立ち上がりエッジで Data_in が stack[write_ptr] に書き込まれる。書き込み要求の有無にも、FIFO が満杯かどうかにも関係なく、読み出し中のサイクルでさえ書き込む。それでもポインタは正当な書き込みのときしか進まないので、write_ptr の指すスロットは、本物の書き込みがポインタを進めるまで、そのときどきの Data_in で上書きされ続ける。こうして作られた FIFO は単純なテストパターンでは動いて見える——ポインタが進む直前に書かれた値が生き残るからである——が、要求とエッジのあいだに Data_in が変わった瞬間にデータは壊れる。これは意図した動作ではなくスライドの転記ミスと見なし、セミコロン 1 個がどれほど高くつくかの教訓として受け取るとよい。
The reset branch uses Data_out = 0; while the read branch uses Data_out <= stack[read_ptr];. The same variable is assigned with both operators in the same block. The language permits it, but it is forbidden by every coding standard and flagged by every linter, for a good reason: a blocking assignment schedules its update in the active region and a nonblocking one in the NBA region, so any other process in the same time step sees a different value depending on which branch fired. Here nothing else reads Data_out in the same time step, so no harm is done and synthesis produces the same twelve flip-flops with asynchronous reset either way. Fix it anyway — write Data_out <= 0; — because the cost of fixing it is zero and the cost of a reviewer wondering whether it was deliberate is not.重置分支用的是 Data_out = 0;,而讀取分支用的是 Data_out <= stack[read_ptr];。同一個變數在同一個區塊裡被兩種運算子指定。語言允許,但每一份程式碼規範都禁止,每一個 linter 都會標出來,而且理由充分:阻塞指定把更新排在 active 區間,非阻塞排在 NBA 區間,所以同一時間步裡任何其他行程看到的值,會隨著哪個分支啟動而不同。在這裡沒有別的東西會在同一時間步讀 Data_out,所以沒有造成傷害,合成出來兩種寫法都是同樣十二個帶非同步重置的正反器。還是改掉吧 —— 寫成 Data_out <= 0; —— 因為改它的成本是零,而讓審查者去猜「這是故意的嗎」的成本不是零。リセット分岐は Data_out = 0; を使い、読み出し分岐は Data_out <= stack[read_ptr]; を使っている。同じ変数が同じブロック内で両方の演算子によって代入されている。言語としては許されるが、どのコーディング規約も禁じ、どの linter も指摘する。理由は明快で、ブロッキング代入は更新をアクティブ領域に、ノンブロッキング代入は NBA 領域にスケジュールするため、同じ時間ステップの他のプロセスが見る値がどちらの分岐が発火したかで変わってしまう。ここでは同じ時間ステップで Data_out を読むものが他に無いので実害はなく、合成結果もどちらでも非同期リセット付きフリップフロップ 12 個で同じである。それでも Data_out <= 0; と直しておきたい。直すコストはゼロで、レビュアーに「これは意図的か」と考えさせるコストはゼロではないからである。
This is the real design limitation, and unlike the first two it is clearly intentional in the code. Every branch in both blocks requires the opposite request to be low. When a producer and a consumer both act in the same cycle — the normal steady state of a FIFO that is being drained as fast as it is being filled — the FIFO stalls completely: the incoming word is dropped and the outgoing word is not fetched. Throughput collapses to zero in exactly the operating point you care about most.這才是真正的設計限制,而且不像前兩個,它在程式碼裡明顯是刻意的。兩個區塊裡的每一個分支都要求對方的請求為低。當生產者與消費者在同一個週期都動作 —— 也就是一個被填滿多快就被抽乾多快的 FIFO 的正常穩態 —— FIFO 會完全停擺:進來的字被丟掉,出去的字也沒被取出。恰恰在你最在意的那個工作點上,吞吐量塌成零。これこそ本物の設計上の制約であり、前の二つと違ってコード上明らかに意図的である。両ブロックのすべての分岐が、相手側の要求が Low であることを要求している。生産者と消費者が同じサイクルで動くとき——満たされるのと同じ速さで吸い出される FIFO の通常の定常状態である——FIFO は完全に停止する。入ってくるワードは捨てられ、出ていくワードも取り出されない。最も重要な動作点でスループットがゼロに落ちるのである。
else if (read_from_stack
&& !write_to_stack
&& !stack_empty)
Data_out <= stack[read_ptr];
else if (write_to_stack
&& !read_from_stack
&& !stack_full)
stack[write_ptr] <= Data_in;if (do_read) Data_out <= stack[read_ptr]; // a separate statement, not an else-if if (do_write) stack[write_ptr] <= Data_in; // with // do_read = read_from_stack && !stack_empty // do_write = write_to_stack && !stack_full
The corrected version raises a question the slide version never has to answer: when the FIFO is empty and a read and a write arrive together, should the incoming word pass straight through to Data_out? With the code above it does not — the write lands in memory and the read is suppressed because stack_empty is still high — so the word is available on the next read. That is the conventional, simple answer. A "first-word fall-through" FIFO would forward it combinationally, which costs a bypass multiplexer and lands Data_out in a longer timing path. Decide which you want and document it, because it is exactly the kind of corner case that two blocks of a chip will disagree about at integration.修正後的版本帶出一個投影片版永遠不必回答的問題:當 FIFO 是空的、而讀取與寫入同時到來時,進來的那個字應該直接穿到 Data_out 嗎?以上面的程式碼來說是不會的 —— 寫入落在記憶體裡,而讀取因為 stack_empty 仍為高而被抑制 —— 所以那個字要等下一次讀取才拿得到。這是傳統、簡單的答案。所謂「first-word fall-through」的 FIFO 會用組合邏輯把它直接轉送出去,代價是一個旁路多工器,而且把 Data_out 放進一條更長的時序路徑。決定你要哪一種並寫進文件,因為這正是晶片裡兩個區塊在整合時最容易各說各話的那種邊角案例。修正版は、スライド版なら答えずに済んだ問いを突きつける。FIFO が空のときに読み出しと書き込みが同時に来たら、入ってきたワードはそのまま Data_out へ抜けるべきか。上のコードでは抜けない。書き込みはメモリに入り、stack_empty がまだ High なので読み出しは抑止される。したがってそのワードは次の読み出しで得られる。これが従来どおりの単純な答えである。「first-word fall-through」型の FIFO なら組合せ的に転送するが、バイパス用のマルチプレクサを要し、Data_out をより長いタイミングパスに置くことになる。どちらを採るか決めて文書化しておきたい。統合時にチップ内の二つのブロックが食い違うのは、まさにこの種の境界ケースだからである。
Because Data_out is a register written at the clock edge, the word requested at edge $n$ appears at the output shortly after edge $n$ and is stable throughout cycle $n{+}1$. The consumer therefore sees its data one cycle after it asked. This is the standard or non-look-ahead FIFO interface, and it has a pleasant property: the output is a flip-flop, so the timing path leaving the FIFO is short and the design will not limit the clock frequency of whatever it drives.因為 Data_out 是在時脈邊緣被寫入的暫存器,所以在邊緣 $n$ 請求的字會在邊緣 $n$ 之後不久出現在輸出,並在整個週期 $n{+}1$ 保持穩定。消費者因此在提出要求的一個週期後才看到資料。這就是標準型或非預看型的 FIFO 介面,它有個很好的性質:輸出是一個正反器,所以離開 FIFO 的時序路徑很短,這個設計不會限制它所驅動對象的時脈頻率。Data_out はクロックエッジで書かれるレジスタなので、エッジ $n$ で要求されたワードはエッジ $n$ の直後に出力へ現れ、サイクル $n{+}1$ のあいだ安定している。したがって消費者は要求の 1 サイクル後にデータを見る。これが標準型あるいは先読みなしの FIFO インタフェースであり、好ましい性質を持つ。出力がフリップフロップなので FIFO から出るタイミングパスが短く、この設計が駆動先のクロック周波数を制限することがない。
The alternative is a first-word fall-through or show-ahead FIFO, in which the oldest word is presented on the output continuously, without a request, and read_from_stack behaves as an acknowledgement that discards it and presents the next. That interface is friendlier to the consumer — no latency, no need to pipeline the request — but it puts the memory's read multiplexer directly on the output, which for a 16-bit, 32-deep FIFO is a 32-to-1 multiplexer in the output path. In a cell-based ASIC flow you would almost always keep the registered output and let the consumer deal with the cycle of latency.另一種做法是 first-word fall-through(又稱 show-ahead)FIFO:最舊的那個字不必請求就一直呈現在輸出上,而 read_from_stack 的角色變成「確認收到」,把它丟掉並呈現下一個。那種介面對消費者比較友善 —— 沒有延遲,也不必把請求管線化 —— 但它把記憶體的讀取多工器直接放在輸出上,對一個 16 位元、32 字深的 FIFO 來說,那就是輸出路徑上一個 32 對 1 的多工器。在 cell-based 的 ASIC 流程裡,你幾乎總是會保留暫存輸出,讓消費者自己去處理那一個週期的延遲。もう一つの選択肢は first-word fall-through(show-ahead とも呼ぶ)型の FIFO である。最も古いワードが要求なしに常時出力へ提示され、read_from_stack はそれを破棄して次を提示する確認信号として働く。このインタフェースは消費者にやさしい——レイテンシがなく、要求をパイプライン化する必要もない——が、メモリの読み出しマルチプレクサを出力に直結させることになり、16 ビット・深さ 32 の FIFO なら出力経路に 32 対 1 のマルチプレクサが入る。セルベースの ASIC フローでは、ほぼ常にレジスタ出力を保ち、1 サイクルのレイテンシは消費者側に処理させる。
empty is high only until the first word lands. Eight writes bring ptr_diff to 8 and assert full. In cycle 9 a read and a write are asserted together: the model refuses both, so the occupancy freezes and the ninth word is silently lost. The read asserted alone in cycle 10 produces word A on Data_out in cycle 11 — the one-cycle latency of a registered output — and clears full.投影片版 FIFO 在十一個週期中的時序。empty 只在第一個字落地之前為高。八次寫入把 ptr_diff 推到 8 並拉起 full。在週期 9,讀取與寫入同時被拉起:這個模型兩者都拒絕,所以佔用量凍住,第九個字悄悄遺失。週期 10 單獨拉起的讀取,讓字 A 在週期 11 出現在 Data_out 上 —— 這就是暫存輸出的一個週期延遲 —— 同時清掉 full。スライド版 FIFO の 11 サイクル分のタイミング。empty は最初のワードが入るまでだけ High である。8 回の書き込みで ptr_diff は 8 に達し full が立つ。サイクル 9 では読み出しと書き込みが同時に立つが、このモデルは両方を拒否するので占有数は凍りつき、9 番目のワードは黙って失われる。サイクル 10 で単独に立てられた読み出しにより、ワード A はサイクル 11 に Data_out へ現れ——これがレジスタ出力の 1 サイクル遅延である——同時に full が下りる。Lecture 2 gave you the machinery; this is where you point it at something. A FIFO testbench has a specific job: it must prove the ordering property, not merely that data comes out. Directed vectors cannot do that convincingly, so the standard approach is a reference model — a second, obviously correct implementation written with no regard for synthesis — and an automatic comparison.第 2 講給了你工具,這裡才是你把它對準目標的地方。FIFO 的測試平台有個特定任務:它必須證明順序性質,而不只是證明資料有出來。指向性向量無法有說服力地做到這件事,所以標準做法是一個參考模型 —— 第二個明顯正確、完全不顧合成的實作 —— 再加上自動比對。第 2 講が道具立てを与えた。ここはそれを実物に向ける場面である。FIFO のテストベンチには固有の仕事がある。データが出てくることではなく、順序の性質を証明しなければならない。指定ベクタではそれを説得力をもって示せないので、標準的な手口は参照モデル——合成をまったく気にせずに書いた、明らかに正しい第 2 の実装——と自動比較である。
`timescale 1ns/100ps module FIFO_tb; parameter W = 12, D = 8; reg [W-1:0] Data_in; reg clk, rst, wr, rd; wire [W-1:0] Data_out; wire empty, full; FIFO #(W, D, 3) DUT (Data_out, Data_in, empty, full, clk, rst, wr, rd); // ---- reference model: an untimed, unsynthesisable queue ---- reg [W-1:0] golden [0:1023]; integer head, tail, errors; // ---- clock ---- initial begin clk = 0; forever #5 clk = ~clk; end // ---- stimulus helpers ---- task do_write (input [W-1:0] v); begin @(negedge clk); // drive away from the active edge if (!full) begin Data_in = v; wr = 1; golden[tail] = v; tail = tail + 1; end @(negedge clk); wr = 0; end endtask task do_read; begin @(negedge clk); if (!empty) rd = 1; @(negedge clk); rd = 0; @(negedge clk); // one cycle of read latency if (Data_out !== golden[head]) begin $display("%0t ERROR: expected %h, got %h", $time, golden[head], Data_out); errors = errors + 1; end head = head + 1; end endtask // ---- scenario ---- integer i; initial begin $dumpfile("fifo.vcd"); $dumpvars(0, FIFO_tb); rst = 1; wr = 0; rd = 0; Data_in = 0; head = 0; tail = 0; errors = 0; @(negedge clk); rst = 0; for (i = 0; i < 12; i = i + 1) do_write(12'h100 + i); // overfill: 8 land, 4 refused if (!full) $display("ERROR: FIFO should be full after 8 writes"); for (i = 0; i < 8; i = i + 1) do_read; // drain if (!empty) $display("ERROR: FIFO should be empty after 8 reads"); $display("done, %0d errors", errors); $finish; end endmodule
Several points of technique are worth lifting out of that listing.那段程式碼裡有幾個技巧值得單獨拉出來講。あのリストから取り出して述べておきたい技法がいくつかある。
negedge clk before changing an input. The design samples on posedge, so this gives a full half-cycle of setup and removes any question of whether the testbench and the design raced. This is the single most common cause of "it worked in simulation and the waveform looks insane".在非作用邊緣驅動。每一個刺激 task 在改變輸入之前都先等 negedge clk。設計是在 posedge 取樣的,所以這給了整整半個週期的 setup,也完全排除了測試平台與設計之間是否競爭的疑問。「模擬時明明會動,波形卻看起來瘋了」最常見的單一成因就是這個。非アクティブエッジで駆動する。各刺激タスクは入力を変える前に必ず negedge clk を待つ。設計は posedge でサンプルするので、これで半サイクル分のセットアップが丸ごと確保され、テストベンチと設計がレースしていないかという疑問も消える。「シミュレーションでは動いたのに波形が正気でない」の最大の原因がこれである。.nosyn badge belongs on the whole block: non-synthesisable黃金佇列是刻意不可合成的。一個用 integer 當頭尾指標的 1024 項陣列,放在設計裡毫無道理,但它是「先進先出」這句話最清楚的表述方式,而正是這份清楚讓它成為可信的參考。.nosyn 徽章該掛在整個區塊上:non-synthesisableゴールデンキューは意図的に合成不可である。integer のヘッド/テールポインタを持つ 1024 要素の配列など設計に置く理由はないが、「先入れ先出し」という言明としてはこれ以上なく明快であり、その明快さこそが参照として信頼できる理由である。.nosyn のバッジはブロック全体に付く。non-synthesisable!==, not !=. The case-inequality operator treats x and z as ordinary values, so an output that has gone unknown is reported as an error. With != a comparison against an x yields x, which is false-ish, and the error is silently swallowed. Lecture 3 covered the four-value logic; this is where it pays.要用 !== 比較,不是 !=。大小寫相等(case-inequality)運算子把 x 與 z 當成一般的值,所以輸出跑成未知時會被回報為錯誤。用 != 時,與 x 的比較結果是 x,那在判斷上接近假,於是錯誤被悄悄吞掉。第 3 講講過四值邏輯;這裡就是它派上用場的地方。比較には != ではなく !== を使う。ケース不等価演算子は x と z を普通の値として扱うので、出力が不定になればエラーとして報告される。!= では x との比較結果が x になり、判定上は偽寄りに扱われてエラーが黙って飲み込まれる。第 3 講で扱った 4 値論理が効いてくるのがここである。Beyond this directed test, the serious verification of a FIFO is random: drive wr and rd from independent random streams with controllable densities, run for a hundred thousand cycles, and check the reference queue continuously. That exercises the full/empty boundaries, the wrap of both pointers and the concurrent-access corner far better than any hand-written sequence, and it is how the concurrent-access defect of §3.6 would be caught within a few hundred cycles.在這個指向性測試之外,FIFO 認真的驗證是隨機的:用兩條獨立、密度可調的隨機流去驅動 wr 與 rd,跑個十萬個週期,並持續檢查參考佇列。這樣對滿/空邊界、兩個指標的繞回,以及同時存取那個角落的操練,遠比任何手寫序列徹底;§3.6 那個同時存取的缺陷,用這種方式幾百個週期內就會被抓到。この指定テストの先にある、FIFO の本格的な検証はランダム検証である。wr と rd を密度を調整できる独立なランダム列で駆動し、10 万サイクル回しながら参照キューを常時照合する。満杯/空の境界、両ポインタの折り返し、同時アクセスの隅のケースを、手書きの系列よりはるかによく叩ける。§3.6 の同時アクセスの欠陥なら数百サイクルで捕まるだろう。
Running this module through a standard-cell flow with the default effort gives a netlist with four recognisable pieces.把這個模組丟進標準元件流程、用預設的 effort 跑一次,會得到一個由四個可辨認部分組成的 netlist。このモジュールを標準セルのフローへ通し、既定の effort で合成すると、見分けのつく 4 つの部分からなる netlist が得られる。
| Source來源元の記述 | Inferred hardware推論出的硬體推論されるハードウェア | Approximate size大致規模おおよその規模 |
|---|---|---|
reg [11:0] stack [7:0] written at a clock edge under an enable在時脈邊緣、帶致能條件寫入的 reg [11:0] stack [7:0]クロックエッジでイネーブル付きに書かれる reg [11:0] stack [7:0] | 96 D-type flip-flops with clock enables, plus a 3-to-8 address decoder generating eight enable signals96 個帶時脈致能的 D 型正反器,加上一個產生八條致能訊號的 3 對 8 位址解碼器クロックイネーブル付き D フリップフロップ 96 個と、8 本のイネーブル信号を作る 3-to-8 アドレスデコーダ | 96 FF + ~20 gates96 個 FF + 約 20 個閘FF 96 個 + ゲート約 20 個 |
stack[read_ptr] read in an edge-triggered block在邊緣觸發區塊裡讀出的 stack[read_ptr]エッジ記述ブロック内で読まれる stack[read_ptr] | A 12-bit wide 8-to-1 multiplexer feeding the output register's D input一個 12 位元寬的 8 對 1 多工器,接到輸出暫存器的 D 輸入出力レジスタの D 入力へ入る 12 ビット幅 8 対 1 マルチプレクサ | ~96 AOI/MUX cells約 96 個 AOI/MUX 元件AOI/MUX セル約 96 個 |
Data_out | 12 flip-flops with asynchronous reset; the library cell has an active-low reset, so an inverter appears on rst12 個帶非同步重置的正反器;元件庫的那顆元件是低電位有效重置,所以 rst 上會冒出一個反相器非同期リセット付きフリップフロップ 12 個。ライブラリのセルはアクティブ Low リセットなので rst にインバータが 1 個現れる | 12 FF + 1 INV12 個 FF + 1 個 INVFF 12 個 + INV 1 個 |
read_ptr, write_ptr, ptr_diff | 3 + 3 + 4 = 10 flip-flops, two 3-bit incrementers, one 4-bit up/down incrementer, and the branch-condition logic3 + 3 + 4 = 10 個正反器、兩個 3 位元加一電路、一個 4 位元上/下數加一電路,以及分支條件邏輯3 + 3 + 4 = 10 個のフリップフロップ、3 ビットのインクリメンタ 2 個、4 ビットの上下インクリメンタ 1 個、および分岐条件の論理 | 10 FF + ~60 gates10 個 FF + 約 60 個閘FF 10 個 + ゲート約 60 個 |
stack_empty, stack_full | A 4-input NOR and a single wire (ptr_diff[3])一個四輸入 NOR,以及一條線(ptr_diff[3])4 入力 NOR 1 個と配線 1 本(ptr_diff[3]) | ~2 gates約 2 個閘ゲート約 2 個 |
Total: 118 flip-flops and a few hundred gates. The dominant cost by far is the memory — 96 of the 118 flops, and the 8-to-1 read multiplexer. That ratio is what motivates replacing a flop array with a compiled RAM as soon as the depth grows: at eight words a flop array is smaller and faster than a RAM macro plus its overhead, at 64 words it is a wash, and at 1024 words the flop array is absurd.合計:118 個正反器與幾百個閘。成本上遙遙領先的是記憶體 —— 118 個正反器裡有 96 個,再加上那個 8 對 1 讀取多工器。正是這個比例讓人一旦深度變大就想把正反器陣列換成編譯出來的 RAM:八個字時,正反器陣列比 RAM 巨集加上它的周邊更小也更快;64 個字時兩者打平;1024 個字時正反器陣列就荒謬了。合計:フリップフロップ 118 個と数百ゲート。圧倒的に支配的なコストはメモリであり、118 個のうち 96 個と、8 対 1 の読み出しマルチプレクサがそれにあたる。この比率こそ、深さが増えたらフリップフロップ配列をコンパイル済み RAM に置き換えたくなる理由である。8 ワードならフリップフロップ配列のほうが RAM マクロとその周辺より小さく速い。64 ワードで互角になり、1024 ワードではフリップフロップ配列は馬鹿げている。
When the tool shows you the elaborated schematic, look for three shapes and you will recognise any FIFO: a bank of identical flip-flops in a column (the memory), a tree of multiplexers converging to a single bus (the read port), and two small counters with an adder each (the pointers). The slide's screenshots of the data register and the filter show exactly this visual signature — a column of dffrpqb_a cells with the clock bussed across them and an inverter on the reset — and once you can read it you can audit a netlist at a glance.當工具把 elaborate 後的電路圖秀給你看時,找三個形狀,你就能認出任何一個 FIFO:一整欄完全相同的正反器(記憶體)、一棵收斂到單一匯流排的多工器樹(讀取埠),以及兩個各帶一個加法器的小計數器(指標)。投影片裡資料暫存器與濾波器的截圖,顯示的正是這個視覺特徵 —— 一欄 dffrpqb_a 元件,時脈橫向串過它們,重置上掛著一個反相器 —— 一旦你讀得懂,就能一眼稽核一份 netlist。ツールが elaborate 後の回路図を見せてきたら、三つの形を探せばどんな FIFO でも見分けられる。同一のフリップフロップが縦に並んだ列(メモリ)、一本のバスへ収束するマルチプレクサの木(読み出しポート)、そしてそれぞれ加算器を持つ小さなカウンタ 2 個(ポインタ)である。スライドのデータレジスタとフィルタのスクリーンショットは、まさにこの視覚的特徴を示している。dffrpqb_a セルの列にクロックが横断して配られ、リセットにインバータが 1 個付いている。これが読めるようになれば、netlist を一目で監査できる。
Problem: Take the module exactly as the slides give it (concurrent access refused, defects 1 and 2 corrected). After reset, the following requests are sampled on successive rising edges. Give write_ptr, read_ptr, ptr_diff, stack_empty, stack_full and Data_out after each edge. Depth is 8.問題:完全照投影片給的模組(同時存取被拒絕,缺陷 1 與 2 已修正)。重置之後,下列請求在連續的上升邊緣被取樣。請給出每個邊緣之後的 write_ptr、read_ptr、ptr_diff、stack_empty、stack_full 與 Data_out。深度為 8。問題:スライドのモジュールをそのまま使う(同時アクセスは拒否、欠陥 1 と 2 は修正済みとする)。リセット後、次の要求が連続する立ち上がりエッジでサンプルされる。各エッジ後の write_ptr、read_ptr、ptr_diff、stack_empty、stack_full、Data_out を示せ。深さは 8 とする。
Edges 1–10: W(A), W(B), W(C), R, W(D), R+W(E), R, R, R, R.邊緣 1–10:W(A)、W(B)、W(C)、R、W(D)、R+W(E)、R、R、R、R。エッジ 1–10:W(A)、W(B)、W(C)、R、W(D)、R+W(E)、R、R、R、R。
Remember that the flag values used to decide each edge's action are the ones established by the previous edge, and that Data_out is updated at the same edge that advances read_ptr.記住:決定每個邊緣行為的旗標值,是前一個邊緣所確立的,而 Data_out 是在推進 read_ptr 的那同一個邊緣被更新的。各エッジの動作を決めるフラグの値は直前のエッジで確定したものであり、Data_out は read_ptr を進めるのと同じエッジで更新される点を思い出してほしい。
| Edge邊緣エッジ | Request請求要求 | Action taken實際動作実際の動作 | write_ptr | read_ptr | ptr_diff | empty | full | Data_out |
|---|---|---|---|---|---|---|---|---|
| — | reset重置リセット | clear清除クリア | 0 | 0 | 0 | 1 | 0 | 000 |
| 1 | W(A) | store A at 0把 A 存到位置 0A を 0 番に格納 | 1 | 0 | 1 | 0 | 0 | 000 |
| 2 | W(B) | store B at 1把 B 存到位置 1B を 1 番に格納 | 2 | 0 | 2 | 0 | 0 | 000 |
| 3 | W(C) | store C at 2把 C 存到位置 2C を 2 番に格納 | 3 | 0 | 3 | 0 | 0 | 000 |
| 4 | R | fetch slot 0取出位置 00 番を取り出す | 3 | 1 | 2 | 0 | 0 | A |
| 5 | W(D) | store D at 3把 D 存到位置 3D を 3 番に格納 | 4 | 1 | 3 | 0 | 0 | A |
| 6 | R + W(E) | nothing什麼都沒做何も起きない | 4 | 1 | 3 | 0 | 0 | A |
| 7 | R | fetch slot 1取出位置 11 番を取り出す | 4 | 2 | 2 | 0 | 0 | B |
| 8 | R | fetch slot 2取出位置 22 番を取り出す | 4 | 3 | 1 | 0 | 0 | C |
| 9 | R | fetch slot 3取出位置 33 番を取り出す | 4 | 4 | 0 | 1 | 0 | D |
| 10 | R | refused (empty)被拒絕(已空)拒否(空のため) | 4 | 4 | 0 | 1 | 0 | D |
Two observations. Word E is lost at edge 6 and nothing in the interface tells the producer — the FIFO was neither full nor empty, so the producer had every reason to believe its word was taken. Silent data loss with no error flag is the worst kind of bug. And at edge 9 the pointers coincide at 4, which is the empty state; had the buffer been filled all the way round they would also have coincided in the full state, which is the ambiguity of §2.4 showing up in a concrete trace.兩點觀察。字 E 在邊緣 6 遺失,而介面上沒有任何東西告訴生產者 —— FIFO 既不滿也不空,所以生產者完全有理由相信它的字被收下了。沒有錯誤旗標的無聲資料遺失,是最糟的一種 bug。另外在邊緣 9,兩個指標在 4 重合,那是空的狀態;如果緩衝區曾經被填滿整整一圈,它們在滿的狀態下也會重合 —— 這就是 §2.4 那個歧義在具體追蹤裡現身。観察が二つある。ワード E はエッジ 6 で失われるが、インタフェース上それを生産者に伝えるものは何もない。FIFO は満杯でも空でもなかったので、生産者には自分のワードが受け取られたと信じる理由しかなかった。エラーフラグを伴わない無言のデータ損失は、最悪の種類のバグである。もう一つ、エッジ 9 で両ポインタは 4 で一致するが、これは空の状態である。バッファがぐるりと一周満たされていれば満杯の状態でも一致したはずで、これが §2.4 のあいまいさが具体的なトレース上に現れた姿である。
Problem: Assume the slide's semicolon after the second else if is real. The FIFO is reset and then Data_in carries the values 5, 6, 7, 8 on four successive cycles, but write_to_stack is high only on the first and the fourth. No reads occur. What is in stack[0] and stack[1] afterwards, and what is ptr_diff? What single-character change fixes it, and what lint rule would have caught it?問題:假設投影片裡第二個 else if 後面那個分號是真的。FIFO 重置之後,Data_in 在連續四個週期帶著 5、6、7、8,但 write_to_stack 只在第一與第四個週期為高。期間沒有任何讀取。之後 stack[0] 與 stack[1] 裡是什麼?ptr_diff 又是多少?改哪一個字元可以修好它?什麼 lint 規則本來可以抓到它?問題:スライドにある二つ目の else if の後のセミコロンが本物だと仮定する。FIFO をリセットしたあと、Data_in が連続する 4 サイクルで 5、6、7、8 を運ぶが、write_to_stack が High になるのは 1 回目と 4 回目だけである。読み出しは起きない。終了時点で stack[0] と stack[1] には何が入っており、ptr_diff はいくつか。1 文字のどんな修正で直るか。どの lint 規則なら検出できたか。
With the semicolon, stack[write_ptr] <= Data_in; executes on every non-reset edge.有了那個分號,stack[write_ptr] <= Data_in; 會在每一個非重置的邊緣執行。そのセミコロンがあると、stack[write_ptr] <= Data_in; はリセット以外のすべてのエッジで実行される。
write_ptr = 0, Data_in = 5. stack[0] ← 5. The write request is genuine, so the pointer block advances write_ptr to 1 and ptr_diff to 1.邊緣 1:write_ptr = 0、Data_in = 5。stack[0] ← 5。寫入請求是真的,所以指標區塊把 write_ptr 推到 1、ptr_diff 推到 1。エッジ 1:write_ptr = 0、Data_in = 5。stack[0] ← 5。書き込み要求は本物なので、ポインタブロックは write_ptr を 1 へ、ptr_diff を 1 へ進める。write_ptr = 1, Data_in = 6. stack[1] ← 6 even though no write was requested. Pointers unchanged.邊緣 2:write_ptr = 1、Data_in = 6。即使沒有寫入請求,stack[1] ← 6 還是發生了。指標不變。エッジ 2:write_ptr = 1、Data_in = 6。書き込み要求が無いのに stack[1] ← 6 が起きる。ポインタは変わらない。write_ptr = 1, Data_in = 7. stack[1] ← 7, overwriting the 6. Pointers unchanged.邊緣 3:write_ptr = 1、Data_in = 7。stack[1] ← 7,把剛才的 6 覆寫掉。指標不變。エッジ 3:write_ptr = 1、Data_in = 7。stack[1] ← 7 となり、先ほどの 6 を上書きする。ポインタは変わらない。write_ptr = 1, Data_in = 8. stack[1] ← 8. The request is genuine, so write_ptr becomes 2 and ptr_diff becomes 2.邊緣 4:write_ptr = 1、Data_in = 8。stack[1] ← 8。請求是真的,所以 write_ptr 變成 2、ptr_diff 變成 2。エッジ 4:write_ptr = 1、Data_in = 8。stack[1] ← 8。要求は本物なので write_ptr は 2 に、ptr_diff は 2 になる。Result: stack[0] = 5, stack[1] = 8, ptr_diff = 2. Notice that the data read out later would be 5 then 8 — which is correct. The bug is invisible on this stimulus because the last unconditional write before the pointer advanced happened to carry the right value. It becomes visible the moment Data_in changes after the requested edge, or the moment a read and an idle cycle interleave so that the slot under write_ptr gets clobbered after a word that was meant to stay.結果:stack[0] = 5、stack[1] = 8、ptr_diff = 2。注意稍後讀出來的資料會是 5 再來 8 —— 這是正確的。這個 bug 在這組刺激下看不見,因為指標前進之前最後那次無條件寫入剛好帶著正確的值。只要 Data_in 在被請求的那個邊緣之後改變,或只要讀取與閒置週期交錯、使得 write_ptr 底下那個本該保留的字被蓋掉,它就會現形。結果は stack[0] = 5、stack[1] = 8、ptr_diff = 2 である。あとで読み出されるデータが 5、8 の順になる点に注意したい。これは正しい。この刺激ではバグが見えないのは、ポインタが進む直前の無条件書き込みがたまたま正しい値を運んでいたからである。要求されたエッジの後に Data_in が変われば、あるいは読み出しとアイドルサイクルが交互に来て write_ptr の下にある残すべきワードが潰されれば、たちまち表に出る。
The fix is to delete the semicolon. A lint rule that catches it: "an if or else whose body is a null statement" (most tools call it empty-statement or suspicious-semicolon). A coding standard that mandates begin/end around every branch body, however short, also makes the construct impossible to write by accident.修法是把那個分號刪掉。能抓到它的 lint 規則是:「if 或 else 的本體是一個空敘述」(多數工具叫它 empty-statement 或 suspicious-semicolon)。另外,規定每個分支本體不論多短都要加 begin/end 的程式碼規範,也會讓這種寫法根本不可能不小心寫出來。直し方はそのセミコロンを消すことである。検出できる lint 規則は「if または else の本体が空文である」(多くのツールで empty-statement あるいは suspicious-semicolon と呼ばれる)。また、どんなに短くても分岐の本体に begin/end を付けることを義務づけるコーディング規約でも、この構文をうっかり書くことは不可能になる。
Problem: Rewrite the pointer block so that a simultaneous read and write is handled correctly. State what ptr_diff must do in that case and why, and explain what should happen if a simultaneous read and write arrives when the FIFO is (a) empty, (b) full.問題:改寫指標區塊,讓同時讀寫能被正確處理。說明在那種情況下 ptr_diff 必須怎麼變、為什麼;並說明當 FIFO 處於 (a) 空、(b) 滿 時,同時讀寫到來應該發生什麼事。問題:同時の読み書きを正しく扱えるようポインタブロックを書き直せ。その場合 ptr_diff がどうあるべきか、またその理由を述べよ。さらに FIFO が (a) 空、(b) 満杯のときに読み書きが同時に来たら何が起こるべきかを説明せよ。
wire do_write = write_to_stack && !stack_full;
wire do_read = read_from_stack && !stack_empty;
always @(posedge clk or posedge rst)
if (rst) begin write_ptr <= 0; read_ptr <= 0; ptr_diff <= 0; end
else begin
if (do_write) write_ptr <= write_ptr + 1'b1;
if (do_read) read_ptr <= read_ptr + 1'b1;
if (do_write && !do_read) ptr_diff <= ptr_diff + 1'b1;
else if (do_read && !do_write) ptr_diff <= ptr_diff - 1'b1;
end
ptr_diff must stay unchanged when both happen, because one word enters and one word leaves, so the occupancy is the same. The two pointers both advance; the gap between them is what is constant, which is a good way to remember that ptr_diff is a difference and not a position.兩者同時發生時 ptr_diff 必須保持不變,因為一個字進來、一個字出去,佔用量相同。兩個指標都會前進;保持不變的是它們之間的間距 —— 這是記住「ptr_diff 是差,不是位置」的好方法。両方が同時に起きるとき ptr_diff は変わってはならない。1 ワードが入り 1 ワードが出るので、占有数は同じだからである。ポインタは両方とも進むが、一定に保たれるのは両者の差である。ptr_diff が位置ではなく差であることを覚えておく良い手掛かりになる。
(a) Empty: do_read is suppressed by !stack_empty, so only the write happens and ptr_diff goes to 1. The consumer gets nothing this cycle, which is correct — there was nothing to give at the moment it asked, and it should have checked stack_empty first. (If you want the arriving word forwarded immediately you must build a first-word fall-through bypass, which is a different and more expensive FIFO.)(a) 空:do_read 被 !stack_empty 擋掉,所以只有寫入發生,ptr_diff 變成 1。消費者這個週期什麼也沒拿到,這是正確的 —— 它開口的那一刻本來就沒東西可給,它本來就該先檢查 stack_empty。(如果你希望剛到的字立刻被轉送出去,你就得做一個 first-word fall-through 的旁路,那是另一種、而且更貴的 FIFO。)(a) 空:do_read は !stack_empty によって抑止されるので、書き込みだけが起こり ptr_diff は 1 になる。消費者はこのサイクルで何も得られないが、それで正しい。要求した時点で渡せるものが無かったのであり、先に stack_empty を調べるべきだった。(到着したワードを即座に転送したいなら first-word fall-through のバイパスを作ることになるが、それは別種の、より高価な FIFO である。)
(b) Full: do_write is suppressed by !stack_full, so only the read happens and ptr_diff falls to 7. The producer's word is refused. An alternative design would allow the write, because the read is simultaneously freeing a slot — but then stack_full would have to be computed as "full and not being read this cycle", which puts the read request into the full-flag path and into the producer's timing budget. Most designs do not do this; they let the producer retry.(b) 滿:do_write 被 !stack_full 擋掉,所以只有讀取發生,ptr_diff 掉到 7。生產者的字被拒絕。另一種設計會允許這次寫入,因為讀取同時正在釋放一個位置 —— 但那樣 stack_full 就得算成「已滿且這個週期沒有在讀」,這會把讀取請求拉進滿旗標的路徑,也拉進生產者的時序預算裡。多數設計不這麼做,而是讓生產者重試。(b) 満杯:do_write は !stack_full によって抑止されるので、読み出しだけが起こり ptr_diff は 7 に下がる。生産者のワードは拒否される。別の設計なら、読み出しが同時にスロットを空けているのだから書き込みを許すこともできる。しかしその場合 stack_full は「満杯かつこのサイクルで読み出されていない」として計算せねばならず、読み出し要求が満杯フラグの経路に、ひいては生産者のタイミング予算に入り込む。ほとんどの設計はそうせず、生産者に再試行させる。
The specification fits in four sentences. Two sensors deliver 12-bit Celsius readings on channel_1 and channel_2, one reading per clock. Each channel's reading is registered, converted to Fahrenheit, averaged over the last four samples and pushed into an eight-deep FIFO. A host asserts read with a 2-bit select to drain either FIFO, or to read a status word reporting the empty and full flags of both. When the host is not reading, both FIFOs are being written.規格四句話就寫得完。兩個感測器分別在 channel_1 與 channel_2 上送出 12 位元的攝氏讀數,每個時脈一筆。每條通道的讀數先被暫存,轉換成華氏,對最近四筆取平均,然後推進一個八字深的 FIFO。主機拉起 read 並給一個 2 位元的 select,就能抽取其中一個 FIFO,或讀一個回報兩邊空/滿旗標的狀態字。主機沒在讀的時候,兩個 FIFO 都在被寫入。仕様は 4 文で収まる。センサ 2 個が channel_1 と channel_2 に 12 ビットの摂氏値を 1 クロックあたり 1 サンプル送る。各チャネルの値はレジスタに取り込まれ、華氏へ変換され、直近 4 サンプルで平均され、深さ 8 の FIFO へ押し込まれる。ホストは 2 ビットの select とともに read を立てて、どちらかの FIFO を吸い出すか、両方の空・満杯フラグを報告するステータスワードを読む。ホストが読んでいないあいだ、両方の FIFO は書き込まれ続ける。
The architecture that follows from that specification is almost forced. Two identical channel pipelines, built from four modules each, and one shared controller at the output. Every stage is a module; the top level is nothing but nine instantiations and a handful of wires. This is the house style of structured hardware design and it is worth saying explicitly why it is used: a module with a clean port list can be simulated, verified and timed on its own, swapped for a different implementation without disturbing its neighbours, and instantiated twice — which is exactly what happens here, since both channels share the same four module definitions.從那份規格推出來的架構幾乎是被逼出來的。兩條完全相同的通道管線,每條由四個模組組成,輸出端再加一個共用的控制器。每一級都是一個模組;頂層除了九次實例化與幾條線之外什麼也沒有。這是結構化硬體設計的慣用風格,而它為什麼被採用值得明講:一個埠列表乾淨的模組可以單獨模擬、驗證與做時序分析,可以在不驚動鄰居的情況下換成另一種實作,也可以被實例化兩次 —— 這裡發生的正是這件事,因為兩條通道共用同樣那四個模組定義。この仕様から導かれるアーキテクチャはほとんど一意に決まる。同一のチャネルパイプラインを 2 本、それぞれ 4 モジュールで構成し、出力側に共有のコントローラを 1 個置く。各段がモジュールであり、トップレベルは 9 個のインスタンスと数本のワイヤしかない。これは構造化ハードウェア設計の定石であり、なぜそうするのかを明示しておく価値がある。ポートリストの整ったモジュールは単体でシミュレーション・検証・タイミング解析ができ、隣に影響を与えずに別実装へ差し替えられ、2 回インスタンス化もできる。ここで起きているのはまさにそれで、2 本のチャネルが同じ 4 つのモジュール定義を共有している。
Before the modules, trace a single sample through the pipeline and count the clocks, because this is the kind of question that gets asked and the kind of detail that gets lost. A reading presented on channel_1 is captured by the register at edge $n$. The converter is combinational, so the Fahrenheit value is available during cycle $n{+}1$ with no further delay. The filter samples on the falling edge, so the converted value enters temp[3] at the falling edge within cycle $n{+}1$ and needs three more falling edges to reach temp[0]; the average including this sample is available from that first falling edge onward, because the output is a combinational sum of all four stages. The FIFO stores the average at the next rising edge on which write is high. End to end, a reading influences the output of the system about two clock periods after it arrives and remains visible in the average for four sample periods.在看模組之前,先讓一筆樣本走完管線並數一數時脈數,因為這類問題會被考,而這類細節最容易被忽略。出現在 channel_1 上的一筆讀數,在邊緣 $n$ 被暫存器捕捉。轉換器是組合邏輯,所以華氏值在週期 $n{+}1$ 內就可得,沒有額外延遲。濾波器在下降緣取樣,所以轉換後的值在週期 $n{+}1$ 內的下降緣進入 temp[3],還要再三個下降緣才會到 temp[0];而包含這筆樣本的平均值,從那第一個下降緣起就可得,因為輸出是四級的組合總和。FIFO 則在下一個 write 為高的上升邊緣把平均值存起來。從頭到尾,一筆讀數大約在它抵達後兩個時脈週期開始影響系統輸出,並在平均值裡持續可見四個樣本週期。モジュールを見る前に、サンプルを 1 つパイプラインに通してクロック数を数えておきたい。こうした問いは実際に出るし、こうした細部こそ落としやすいからである。channel_1 に現れた値はエッジ $n$ でレジスタに捕捉される。変換器は組合せなので、華氏の値は追加の遅延なくサイクル $n{+}1$ のあいだに得られる。フィルタは立ち下がりエッジでサンプルするので、変換後の値はサイクル $n{+}1$ 内の立ち下がりエッジで temp[3] に入り、temp[0] へ届くにはさらに 3 回の立ち下がりエッジが要る。このサンプルを含む平均値は、その最初の立ち下がりエッジ以降ずっと得られる。出力が 4 段すべての組合せ和だからである。FIFO は write が High である次の立ち上がりエッジで平均値を格納する。端から端まで見ると、ある値は到着のおよそ 2 クロック後にシステム出力へ影響しはじめ、平均の中に 4 サンプル周期のあいだ残る。
module temp_register (temp_out, temp_in, clk, rst);
output [11:0] temp_out;
input [11:0] temp_in;
input clk, rst;
reg [11:0] temp_out;
always @ (posedge clk or posedge rst)
begin
if (rst == 1) temp_out = 0;
else temp_out = temp_in;
end
endmodule
Twelve D-type flip-flops with an asynchronous active-high reset, and nothing else. There is no enable, so the register samples the input on every clock without exception: the sample rate of the system is the clock rate. If you wanted a slower sample rate you would add an enable and a prescaler, and the register would become if (rst) ... else if (en) temp_out <= temp_in; — the standard enable-register template from Lecture 6, which infers a flop with a clock-enable pin rather than a latch, provided the else is genuinely absent.十二個帶非同步高電位有效重置的 D 型正反器,別無其他。沒有致能,所以暫存器每個時脈都無例外地取樣輸入:系統的取樣率就是時脈率。如果你想要較低的取樣率,就加一個致能與一個除頻器,暫存器會變成 if (rst) ... else if (en) temp_out <= temp_in; —— 那是第 6 講的標準致能暫存器樣板,只要那個 else 真的不存在,它推論出來的就是帶時脈致能腳位的正反器,而不是閂鎖。非同期アクティブ High リセット付きの D フリップフロップ 12 個、それだけである。イネーブルが無いので、レジスタは例外なく毎クロック入力をサンプルする。すなわちシステムのサンプルレートはクロックレートそのものである。もっと遅いサンプルレートが欲しければイネーブルとプリスケーラを足し、レジスタは if (rst) ... else if (en) temp_out <= temp_in; になる。これは第 6 講の標準的なイネーブル付きレジスタのテンプレートであり、else が本当に存在しない限り、ラッチではなくクロックイネーブルピン付きのフリップフロップが推論される。
Two remarks on style. First, the assignments use = rather than <= in a clocked block. As in the FIFO, this is harmless here because nothing else in the same time step reads temp_out — the converter that consumes it is a continuous assignment, which reacts after the fact either way — but it is not defensible as a habit, and if a second clocked block were ever added that read temp_out, it would become a race overnight. Second, if (rst == 1) is more verbose than if (rst) and subtly different in the presence of x: rst == 1 evaluates to x when rst is x, and so does if (rst), so in this case they behave identically. The habit of writing the explicit comparison comes from VHDL and is harmless.兩點風格上的評論。第一,時脈區塊裡的指定用的是 = 而不是 <=。跟 FIFO 一樣,這裡無害,因為同一時間步內沒有別的東西讀 temp_out —— 消費它的轉換器是連續指定,不管怎樣都是事後反應 —— 但把它當習慣是說不過去的,而且只要哪天多加一個會讀 temp_out 的時脈區塊,它一夜之間就變成競爭。第二,if (rst == 1) 比 if (rst) 囉嗦,在有 x 時還有微妙差異:rst 為 x 時 rst == 1 求值為 x,而 if (rst) 也是,所以在這裡兩者行為完全相同。明寫比較的習慣來自 VHDL,無害。スタイルについて二点。第一に、クロック同期ブロックの代入が <= ではなく = である。FIFO と同様ここでは無害で、同じ時間ステップに temp_out を読むものが他に無いからである(消費側の変換器は連続代入であり、いずれにせよ事後に反応する)。しかし習慣として擁護できるものではなく、temp_out を読むクロック同期ブロックがもう一つ加われば一夜にしてレースになる。第二に、if (rst == 1) は if (rst) より冗長で、x が絡むとわずかに違う。rst が x のとき rst == 1 は x に評価されるが、if (rst) も同じなので、この場合の振る舞いは一致する。明示的な比較を書く習慣は VHDL 由来で、害はない。
The slide's synthesis screenshot is worth decoding, because it is the simplest netlist in the lecture and establishes a reading you will reuse. Twelve identical cells named dffrpqb_a stand in a column. The dff prefix is a D flip-flop; r indicates a reset pin; p and q/qb name the output pins; the _a suffix is a drive-strength variant. The twelve bits of temp_in fan out one per cell, clk is bussed down the column, and rst arrives through a single inverter — because the library's flip-flop has an active-low reset pin, and the RTL asked for an active-high reset. That one inverter is the entire cost of the polarity mismatch, and it is a good illustration of the fact that synthesis maps your intent onto what the library actually offers rather than building what you literally wrote.投影片的合成截圖值得解讀一下,因為它是本講裡最簡單的 netlist,而且建立了一套你之後會重複使用的讀法。十二個同名的 dffrpqb_a 元件排成一欄。dff 前綴代表 D 型正反器;r 代表有重置腳;p 與 q/qb 是輸出腳的命名;_a 後綴是驅動強度的版本。temp_in 的十二個位元一個元件分一位,clk 沿著這一欄橫向配線,而 rst 是經過單一個反相器進來的 —— 因為元件庫的正反器有一個低電位有效的重置腳,而 RTL 要的是高電位有效的重置。那一個反相器就是極性不合的全部代價,也很好地說明了一件事:合成是把你的意圖對映到元件庫實際提供的東西上,而不是照字面把你寫的東西蓋出來。スライドの合成結果のスクリーンショットは読み解く価値がある。本講で最も単純な netlist であり、以後繰り返し使う読み方を定めてくれるからである。同じ名前の dffrpqb_a セルが 12 個、縦に並んでいる。dff という接頭辞は D フリップフロップ、r はリセットピンの存在、p と q/qb は出力ピンの名前、末尾の _a は駆動能力のバリアントを表す。temp_in の 12 ビットが 1 セルに 1 ビットずつ入り、clk は列を横切って配られ、rst はインバータ 1 個を通って届く。ライブラリのフリップフロップのリセットピンがアクティブ Low である一方、RTL はアクティブ High のリセットを要求したからである。そのインバータ 1 個が極性不一致のコストのすべてであり、合成が記述をそのまま形にするのではなく、意図をライブラリが実際に提供するものへ写像するのだ、という事実をよく示している。
Now the interesting block. The specification is a single formula.現在進入有趣的區塊。規格就是一條公式。ここから面白いブロックに入る。仕様は式 1 本である。
The obvious Verilog is the direct transcription, and the slides give two versions of it:最直覺的 Verilog 就是照字面抄過來,投影片給了兩個版本:最も素直な Verilog は式をそのまま書き写したもので、スライドはその 2 つの版を示している。
assign temp_out = temp_in * 9 / 5 + 32; non-synthesisable assign temp_out = (9 * temp_in) / 5 + 32; non-synthesisable
Both are rejected, and it is important to be precise about why, because the two operators fail for different reasons.兩個都被否決,而為什麼被否決必須講精確,因為那兩個運算子失敗的理由並不相同。どちらも却下されるが、なぜ却下されるのかを正確に述べておく必要がある。二つの演算子は別々の理由で失敗するからである。
The multiplication by the constant 9 is not the problem. A synthesiser handles x * 9 easily: 9 is $8 + 1$, so x * 9 becomes (x << 3) + x, one shift (free) and one adder. Even a non-constant multiply is synthesisable in any modern tool — expensively, as an array of partial products — so * is a matter of cost, not legality.乘以常數 9 不是問題。合成器處理 x * 9 毫不費力:9 就是 $8 + 1$,所以 x * 9 變成 (x << 3) + x,一次移位(免費)加一個加法器。就算是非常數的乘法,任何現代工具也都能合成 —— 代價高昂,做成部分積陣列 —— 所以 * 是成本問題,不是合法性問題。定数 9 を掛けることは問題ではない。合成器は x * 9 を難なく処理する。9 は $8 + 1$ なので x * 9 は (x << 3) + x になり、シフト 1 回(タダ)と加算器 1 個で済む。定数でない乗算でさえ現代のツールなら合成できる——部分積の配列として高価にではあるが——ので、* はコストの問題であって合法性の問題ではない。
The division by 5 is the problem. Division by a power of two is a right shift and costs nothing; this is the one case every tool accepts. Division by any other constant, and division by a variable, requires a real divider: a sequential restoring or non-restoring algorithm taking one cycle per quotient bit, or a huge combinational SRT array. Verilog does not say how long an expression takes, so a combinational assign containing a general divide has no sensible hardware meaning, and the overwhelming majority of synthesis tools simply refuse it. That is the rule from Lecture 7: / and % are synthesisable only when the right operand is a constant power of two.除以 5 才是問題。除以 2 的冪次就是右移,不花成本;這是每個工具都接受的唯一情況。除以其他任何常數、以及除以變數,都需要一個真正的除法器:一個每求一個商位元花一個週期的循序回復式或不回復式演算法,或是一大片組合式的 SRT 陣列。Verilog 沒有規定一個運算式要花多久,所以含有一般性除法的組合 assign 沒有合理的硬體意義,絕大多數合成工具乾脆拒絕它。這就是第 7 講的規則:/ 與 % 只有在右運算元是 2 的冪次常數時才可合成。問題は 5 で割ることである。2 のべき乗による除算は右シフトであり、コストはかからない。すべてのツールが受け付ける唯一の場合である。それ以外の定数による除算、および変数による除算は本物の除算器を要する。商 1 ビットにつき 1 サイクルかかる逐次の復元法・非復元法か、巨大な組合せ SRT 配列である。Verilog は式にどれだけ時間がかかるかを規定しないので、一般の除算を含む組合せの assign には意味のあるハードウェア的解釈が存在せず、圧倒的多数の合成ツールはこれを単に拒否する。これが第 7 講の規則である。/ と % は右オペランドが 2 のべき乗の定数であるときにのみ合成できる。
The two versions are not equivalent even in exact arithmetic, because Verilog's / truncates towards zero on integers. temp_in * 9 / 5 and (9 * temp_in) / 5 happen to be the same expression — the operators are left-associative and both multiply first — but temp_in / 5 * 9 would not be: for temp_in = 37 the first gives $333/5 = 66$ and the second gives $7 \times 9 = 63$. Whenever you reassociate integer arithmetic you change the answer. This is the single most common source of "my optimised version gives different numbers" in fixed-point design.即使在精確算術下,那兩個版本也不等價,因為 Verilog 的 / 對整數是朝零截斷。temp_in * 9 / 5 與 (9 * temp_in) / 5 恰好是同一個運算式 —— 運算子左結合,兩者都先乘 —— 但 temp_in / 5 * 9 就不是了:當 temp_in = 37 時,前者給 $333/5 = 66$,後者給 $7 \times 9 = 63$。只要你把整數算術重新結合,答案就變了。這是定點設計中「我最佳化過的版本算出不同數字」最常見的單一成因。厳密な算術で考えても二つの版は等価でない。Verilog の / は整数に対してゼロ方向へ切り捨てるからである。temp_in * 9 / 5 と (9 * temp_in) / 5 はたまたま同じ式である——演算子は左結合で、どちらも先に掛ける——が、temp_in / 5 * 9 は違う。temp_in = 37 なら前者は $333/5 = 66$、後者は $7 \times 9 = 63$ になる。整数算術を結合し直せば必ず答えが変わる。固定小数点設計で「最適化した版が違う数を出す」の最大の原因がこれである。
So the divider must go, and with it the exactness. The engineering question becomes: what is the cheapest circuit whose answer is close enough? That question — replacing an exact operation with a cheap approximation whose error you can bound — is the heart of fixed-point datapath design, and the rest of this subsection is a worked instance of it.所以除法器必須拿掉,精確性也跟著拿掉。工程問題於是變成:答案夠接近的最便宜電路是哪一個?這個問題 —— 用一個誤差可以界定的便宜近似去取代精確運算 —— 正是定點資料路徑設計的核心,而本小節其餘部分就是它的一個實作範例。したがって除算器は捨てねばならず、厳密さも一緒に捨てることになる。工学的な問いはこう変わる。十分に近い答えを出す最も安い回路は何か。厳密な演算を、誤差を評価できる安価な近似で置き換えること——この問いこそ固定小数点データパス設計の核心であり、この節の残りはその実例である。
Write 1.8 in binary. The integer part is 1; the fraction is obtained by repeated doubling: $0.8 \to 1.6 \to 1.2 \to 0.4 \to 0.8$, emitting 1, 1, 0, 0 and then repeating for ever.把 1.8 寫成二進位。整數部分是 1;小數部分用反覆加倍取得:$0.8 \to 1.6 \to 1.2 \to 0.4 \to 0.8$,依序吐出 1、1、0、0,然後永遠重複。1.8 を 2 進で書いてみる。整数部は 1、小数部は繰り返し 2 倍して得る。$0.8 \to 1.6 \to 1.2 \to 0.4 \to 0.8$ となり、1、1、0、0 を出したあと永久に繰り返す。
Because the expansion never terminates, no finite shift-add network computes $1.8x$ exactly; you must truncate it somewhere, and where you truncate is the accuracy/area trade-off. Cut after two fractional bits and you keep $1 + \frac12 + \frac14 = 1.75$, which is the choice the lecture makes and which it describes, accurately, as "synthesisable but incorrect" — then immediately notes that it is nevertheless the common approach in IC design. Both halves of that sentence are true and the tension between them is the lesson.因為展開永不終止,沒有任何有限的移位相加網路能精確算出 $1.8x$;你一定得在某處截斷,而截在哪裡就是精度/面積的取捨。切在兩個小數位之後,你保留 $1 + \frac12 + \frac14 = 1.75$,這就是本講的選擇,而它也準確地把這個選擇描述為「可合成但不正確」—— 然後立刻補上一句:這在 IC 設計裡仍然是常見做法。那句話的兩半都是真的,而兩者之間的張力正是這一課。展開が終わらないため、有限のシフト加算回路で $1.8x$ を厳密に計算することはできない。どこかで打ち切るしかなく、どこで打ち切るかが精度と面積のトレードオフになる。小数 2 桁で切れば $1 + \frac12 + \frac14 = 1.75$ が残る。これが本講の選択であり、本講はそれを「合成可能だが正確ではない」と正しく述べたうえで、それでも IC 設計では一般的な手法だと直ちに付け加える。この文の両方が真であり、その緊張関係こそが学ぶべき点である。
The slides give three codings of $1.75x + 32$. The first uses division by power-of-two constants held in parameters:投影片給了 $1.75x + 32$ 的三種寫法。第一種用放在參數裡的 2 冪次常數做除法:スライドは $1.75x + 32$ を 3 通りに記述している。1 つ目はパラメータに置いた 2 のべき乗の定数で割る形である。
parameter k1 = 2'b10; // 2 parameter k2 = 3'b100; // 4 parameter k32 = 6'b100000; // 32 assign temp_out = (temp_in + temp_in / k1 + temp_in / k2) + k32;
This is legal and synthesises, because the divisors are constant powers of two and the tool turns each division into a right shift. It is, however, a roundabout way of saying what you mean, and it depends on the tool recognising that a parameter whose value is 4 is a power of two — which every tool does, but which is a dependency you did not need to create. The other two forms say it directly:這是合法的、也合成得出來,因為除數是常數的 2 冪次,工具會把每個除法變成右移。但這是個繞圈子的說法,而且它依賴工具認出「一個值為 4 的 parameter 是 2 的冪次」—— 每個工具都做得到,但這是你本來不必製造出來的依賴。另外兩種寫法則直截了當:これは合法で合成もできる。除数が定数の 2 のべき乗であり、ツールが各除算を右シフトに変えるからである。ただし言いたいことを遠回りに言う書き方であり、「値が 4 の parameter は 2 のべき乗である」とツールが認識することに依存する。どのツールもそうするが、作らずに済む依存である。残り 2 つの書き方はもっと直接的である。
module temp_converter (temp_out, temp_in); input [11:0] temp_in; output [11:0] temp_out; parameter k32 = 6'b100000; // decimal 32 // F = 9/5 C + 32 // 9/5 = 1.8 ~ 1 + .5 + .25 assign temp_out = temp_in + (temp_in >> 1) + (temp_in >> 2) + k32; endmodule
module temp_converter (temp_out, temp_in);
input [11:0] temp_in;
output [11:0] temp_out;
parameter k32 = 6'b100000;
assign temp_out = ((temp_in << 1)
- (temp_in >> 2))
+ k32;
endmoduleBoth are pure combinational logic with no sequential element, no enable and no clock. A shift by a constant is not a circuit at all — it is a renaming of wires. temp_in >> 2 means "take bits 11 down to 2 and call them bits 9 down to 0, and supply two zeros at the top", which the place-and-route tool implements with routing and nothing else. So the entire cost of each converter is the adders.兩者都是純組合邏輯,沒有循序元件、沒有致能、沒有時脈。常數移位根本不是電路 —— 它只是把線重新命名。temp_in >> 2 的意思是「取位元 11 到 2,把它們叫做位元 9 到 0,最上面補兩個零」,而繞線工具只用繞線就實現它,別無其他。所以每個轉換器的全部成本就是那些加法器。どちらも純粋な組合せ論理で、順序素子もイネーブルもクロックもない。定数シフトはそもそも回路ではなく、配線の呼び替えにすぎない。temp_in >> 2 は「ビット 11 から 2 を取り、それをビット 9 から 0 と呼び、上位に 0 を 2 個供給する」という意味で、配置配線ツールは配線だけでこれを実現する。したがって各変換器のコストは加算器がすべてである。
Algebraically the two alternatives are the same number, $1.75x$. In integer hardware they are not the same circuit, and the difference is instructive.代數上兩種寫法是同一個數 $1.75x$。在整數硬體裡它們不是同一個電路,而這個差別很有啟發性。代数的には二つの方式は同じ数 $1.75x$ である。しかし整数ハードウェアでは同じ回路ではなく、その違いは示唆に富む。
Alternative 1 truncates twice, once in each shift, so it computes $x + \lfloor x/2 \rfloor + \lfloor x/4 \rfloor$. Each floor removes at most just under one, so the result is at most $1.75x$ and at least $1.75x - 1.5$. Alternative 2 truncates once, in a term that is subtracted, so it computes $2x - \lfloor x/4 \rfloor$; removing something from the subtrahend makes the result larger, so it is at least $1.75x$ and at most $1.75x + 0.75$.寫法一截斷兩次,每次移位各一次,所以它算的是 $x + \lfloor x/2 \rfloor + \lfloor x/4 \rfloor$。每個下取整最多拿掉略小於一,所以結果最多是 $1.75x$,最少是 $1.75x - 1.5$。寫法二只截斷一次,而且那一項是被減的,所以它算的是 $2x - \lfloor x/4 \rfloor$;從減數裡拿掉東西會讓結果變大,所以它最少是 $1.75x$,最多是 $1.75x + 0.75$。方式 1 はシフトごとに 1 回ずつ、計 2 回切り捨てるので、計算するのは $x + \lfloor x/2 \rfloor + \lfloor x/4 \rfloor$ である。各床関数が落とす量はいずれも 1 弱までなので、結果は高々 $1.75x$、最小で $1.75x - 1.5$ になる。方式 2 の切り捨ては 1 回だけで、しかもその項は引かれる側にある。計算するのは $2x - \lfloor x/4 \rfloor$ であり、引く側から取り除けば結果は大きくなるので、最小で $1.75x$、高々 $1.75x + 0.75$ である。
So Alternative 2 is never below the ideal $1.75x$ and Alternative 1 is never above it. Since the systematic error of the $1.75$ approximation itself is negative — $1.75x$ falls short of $1.8x$ by $0.05x$ — Alternative 2's upward truncation partially cancels the systematic error while Alternative 1's downward truncation adds to it. Alternative 2 is therefore not merely cheaper; it is also more accurate. That is a satisfying result and a rare one: usually you pay for accuracy.所以寫法二永遠不會低於理想的 $1.75x$,而寫法一永遠不會高於它。由於 $1.75$ 這個近似本身的系統誤差是負的 —— $1.75x$ 比 $1.8x$ 少了 $0.05x$ —— 寫法二向上的截斷會部分抵消系統誤差,而寫法一向下的截斷則是雪上加霜。所以寫法二不只比較便宜,也比較準。這是個令人滿意而且少見的結果:通常精度是要花錢買的。したがって方式 2 は理想値 $1.75x$ を決して下回らず、方式 1 は決して上回らない。$1.75$ という近似そのものの系統誤差は負であり——$1.75x$ は $1.8x$ より $0.05x$ 小さい——方式 2 の上向きの切り捨ては系統誤差を部分的に打ち消すのに対し、方式 1 の下向きの切り捨てはそれを増やす。つまり方式 2 は安いだけでなく、より正確でもある。これは気持ちのよい、そしてまれな結果である。普通、精度は代価を払って買うものだからである。
The systematic term dominates as soon as $x$ exceeds about 30, and it grows without bound: at 500 °C the error is 25 °F. A 2.8 % relative error is acceptable for a fan controller and unacceptable for anything that calls itself an instrument. The right engineering response is not to shrug but to ask what accuracy the specification actually demands and then buy exactly that much with the next subsection's techniques.只要 $x$ 超過大約 30,系統項就開始主導,而且它無上限地成長:500 °C 時誤差是 25 °F。2.8 % 的相對誤差對風扇控制器可以接受,對任何自稱儀器的東西都不可接受。正確的工程回應不是聳聳肩,而是去問規格到底要求多少精度,然後用下一小節的技巧精準地買到那麼多。$x$ が 30 程度を超えると系統項が支配しはじめ、しかも際限なく増える。500 °C では誤差が 25 °F になる。相対誤差 2.8 % はファンの制御器には許容できても、計測器を名乗るものには許容できない。工学的に正しい反応は肩をすくめることではなく、仕様が実際にどれだけの精度を要求しているかを問い、次の節の手法でちょうどその分だけを買うことである。
temp_out is 12 bits and every operand is 12 bits, so the whole expression is evaluated in 12 bits and wraps modulo 4096. In Alternative 2, temp_in << 1 already overflows for any temp_in above 2047. If the input is a raw 12-bit ADC code, the converter produces nonsense over half its input range. The fix is to widen the output to 13 or 14 bits, or to declare the physical range in the specification and check it. This is the context-determined width rule of Lecture 3 doing exactly what it is documented to do, silently.位元寬度。temp_out 是 12 位元,每個運算元也都是 12 位元,所以整個運算式在 12 位元下求值並以 4096 為模繞回。在寫法二裡,只要 temp_in 超過 2047,temp_in << 1 就已經溢位。如果輸入是未經處理的 12 位元 ADC 碼,這個轉換器在超過一半的輸入範圍上都產生垃圾。修法是把輸出加寬到 13 或 14 位元,或是在規格裡宣告物理範圍並加以檢查。這正是第 3 講那條「由上下文決定寬度」的規則在悄無聲息地、完全照文件所寫地發揮作用。ビット幅。temp_out は 12 ビット、オペランドもすべて 12 ビットなので、式全体が 12 ビットで評価され 4096 を法として折り返す。方式 2 では temp_in が 2047 を超えた時点で temp_in << 1 がすでにオーバフローする。入力が生の 12 ビット ADC コードなら、この変換器は入力範囲の半分以上でナンセンスを出す。修正は出力を 13 ないし 14 ビットへ広げるか、物理的な範囲を仕様に明記して検査することである。第 3 講の文脈依存幅の規則が、文書どおりに、しかし黙って働いている例である。>> is a logical right shift: it fills with zeros. For a negative two's-complement temperature it produces a large positive number, and the converter breaks completely below 0 °C. Verilog-1995 has no arithmetic right shift at all; Verilog-2001 added >>>, which fills with the sign bit, and the signed keyword to make operands signed. If the sensor can report sub-zero temperatures you must use reg signed [11:0] and >>> throughout. A neat check on the result: $-40$ °C is exactly $-40$ °F, so a correct signed converter must map $-40$ to $-40$ and the 1.75 approximation must map it to $1.75 \times (-40) + 32 = -38$.正負號。>> 是邏輯右移:它補零。對一個二補數的負溫度,它會產生一個很大的正數,所以這個轉換器在 0 °C 以下完全失效。Verilog-1995 根本沒有算術右移;Verilog-2001 加了會補符號位元的 >>>,以及讓運算元帶號的 signed 關鍵字。如果感測器會回報零下溫度,你全程都必須用 reg signed [11:0] 與 >>>。一個漂亮的檢查方法:$-40$ °C 剛好就是 $-40$ °F,所以正確的帶號轉換器必須把 $-40$ 映到 $-40$,而 1.75 的近似則必須把它映到 $1.75 \times (-40) + 32 = -38$。符号。>> は論理右シフトであり、ゼロを詰める。2 の補数の負の温度に対しては大きな正の数を返すので、この変換器は 0 °C 未満で完全に破綻する。Verilog-1995 には算術右シフトが存在しない。Verilog-2001 で符号ビットを詰める >>> と、オペランドを符号付きにする signed キーワードが加わった。センサが氷点下を報告しうるなら、全体を通して reg signed [11:0] と >>> を使わねばならない。結果の手頃な検算として、$-40$ °C はちょうど $-40$ °F なので、正しい符号付き変換器は $-40$ を $-40$ へ写し、1.75 の近似は $1.75 \times (-40) + 32 = -38$ へ写さねばならない。Suppose the specification demands better than 0.2 % relative error. You have three options, and knowing all three is what distinguishes a datapath designer from someone who copies a slide.假設規格要求相對誤差優於 0.2 %。你有三個選項,而三個都知道,正是資料路徑設計者與照抄投影片的人之間的差別。仕様が相対誤差 0.2 % 未満を要求しているとしよう。選択肢は三つあり、その三つを知っていることが、データパス設計者とスライドを書き写す人との差である。
Option A — keep more bits of the expansion. Continue the binary expansion of 1.8 past the second fractional bit. The next two set bits are at $2^{-5}$ and $2^{-6}$:選項 A —— 保留展開式的更多項。把 1.8 的二進位展開繼續寫過第二個小數位。接下來兩個為 1 的位元在 $2^{-5}$ 與 $2^{-6}$:選択肢 A — 展開の項をもっと残す。1.8 の 2 進展開を小数第 2 位より先へ続ける。次に 1 が立つのは $2^{-5}$ と $2^{-6}$ である。
assign temp_out = temp_in + (temp_in >> 1) + (temp_in >> 2)
+ (temp_in >> 5) + (temp_in >> 6) + k32;
Two extra adders buy a sixteen-fold reduction in systematic error. At 100 °C the result is $100 + 50 + 25 + 3 + 1 + 32 = 211$ against the exact 212 — and the remaining shortfall is now mostly truncation, not approximation. An equivalent and cheaper formulation computes it as one multiply-by-115 followed by a right shift of 6, which a good synthesiser will restructure into a carry-save tree.多兩個加法器,換來系統誤差縮小十六倍。在 100 °C 時結果是 $100 + 50 + 25 + 3 + 1 + 32 = 211$,對上精確的 212 —— 而剩下的差額現在主要來自截斷,不是來自近似。另一種等價而且更便宜的寫法,是算成「乘以 115 再右移 6」,好的合成器會把它重整成一棵進位保留樹。加算器 2 個の追加で、系統誤差は 16 分の 1 になる。100 °C では $100 + 50 + 25 + 3 + 1 + 32 = 211$ となり、厳密値 212 に対する残りの不足はもはや近似ではなく主に切り捨てによる。等価でより安い定式化は、115 倍して 6 ビット右シフトする形であり、よい合成器ならこれをキャリーセーブ木に組み替える。
Option B — scale and round instead of truncating the expansion. Choose a power of two $2^k$, take $m = \mathrm{round}(1.8 \times 2^k)$, compute $m \cdot x$ with shifts and adds, and shift right by $k$ at the end. With $k = 8$, $m = \mathrm{round}(460.8) = 461$:選項 B —— 先放大再捨入,而不是截斷展開式。挑一個 2 的冪次 $2^k$,取 $m = \mathrm{round}(1.8 \times 2^k)$,用移位與加法算出 $m \cdot x$,最後再右移 $k$ 位。取 $k = 8$ 時,$m = \mathrm{round}(460.8) = 461$:選択肢 B — 展開を打ち切るのではなく、スケールして丸める。2 のべき乗 $2^k$ を選び、$m = \mathrm{round}(1.8 \times 2^k)$ とし、$m \cdot x$ をシフトと加算で計算して、最後に $k$ ビット右シフトする。$k = 8$ なら $m = \mathrm{round}(460.8) = 461$ である。
and $461 = 512 - 32 - 16 - 2 - 1$, a canonical-signed-digit form needing four subtractions, or $461 = 256 + 128 + 64 + 8 + 4 + 1$, needing five additions. Crucially, the single final shift means there is only one truncation in the whole datapath instead of one per term, which removes most of the $\varepsilon$ noise as well. Add a rounding constant of $2^{k-1} = 128$ before the shift and the remaining error becomes symmetric and bounded by half a degree.而 $461 = 512 - 32 - 16 - 2 - 1$ 是一個需要四次減法的 CSD(正負數位)形式,或 $461 = 256 + 128 + 64 + 8 + 4 + 1$,需要五次加法。關鍵在於:最後只移位一次,代表整條資料路徑裡只有一次截斷,而不是每一項各一次,這也把大部分的 $\varepsilon$ 雜訊一併去掉。在移位前再加上一個 $2^{k-1} = 128$ 的捨入常數,剩下的誤差就變成對稱、且以半度為界。$461 = 512 - 32 - 16 - 2 - 1$ は減算 4 回で済む CSD(正負数字表現)形であり、$461 = 256 + 128 + 64 + 8 + 4 + 1$ なら加算 5 回になる。重要なのは最後のシフトが 1 回だけであることで、データパス全体での切り捨てが項ごとではなく1 回に減り、$\varepsilon$ 由来の雑音の大半も消える。シフトの前に丸め定数 $2^{k-1} = 128$ を足せば、残る誤差は対称になり、半度以内に収まる。
Option C — a lookup table. If the input range is genuinely bounded — a thermometer covering 0 to 127 °C, say — then a 128-entry ROM holding the exactly rounded Fahrenheit value is 128 × 9 bits = 1152 bits of ROM, which in a standard-cell flow synthesises to a few hundred gates of decoded logic and is exact by construction. For small input ranges a table beats arithmetic every time, and the mistake students make is to assume a table is always too big without working out how big it actually is.選項 C —— 查表。如果輸入範圍真的有界 —— 比如一支只涵蓋 0 到 127 °C 的溫度計 —— 那麼一張存放精確捨入後華氏值的 128 項 ROM 就是 128 × 9 位元 = 1152 位元,在標準元件流程裡會合成成幾百個閘的解碼邏輯,而且本質上就是精確的。對小的輸入範圍,查表每次都贏算術,而學生常犯的錯是沒算過到底多大,就先假設表一定太大。選択肢 C — ルックアップテーブル。入力範囲が本当に有界なら——たとえば 0〜127 °C をカバーする温度計——正しく丸めた華氏値を持つ 128 エントリの ROM は 128 × 9 ビット = 1152 ビットであり、標準セルのフローでは数百ゲートのデコード論理に合成され、構成上そもそも厳密である。入力範囲が小さい場合、テーブルは常に算術に勝つ。学生がしがちな誤りは、実際にどれほどの大きさか計算せずに、テーブルは必ず大きすぎると決めてかかることである。
| Method方法方法 | Relative error相對誤差相対誤差 | Adders / subtractors加法器/減法器加算器/減算器 | Carry chains in series串聯的進位鏈直列になるキャリーチェーン | Notes備註備考 |
|---|---|---|---|---|
| 1.75, Alternative 11.75,寫法一1.75、方式 1 | −2.78 % plus up to 1.5 LSB−2.78 % 再加最多 1.5 LSB−2.78 % に加えて最大 1.5 LSB | 2 + constant add2 個加上一次常數加法2 個 + 定数加算 | 3 | The slide's choice投影片的選擇スライドの選択 |
| 1.75, Alternative 21.75,寫法二1.75、方式 2 | −2.78 % plus up to 0.75 LSB upward−2.78 % 再加最多向上 0.75 LSB−2.78 % に加えて上向きに最大 0.75 LSB | 1 + 1 + constant add1 + 1 個加上一次常數加法1 + 1 個 + 定数加算 | 2 | Cheaper and better更便宜也更準安くて精度もよい |
| 115/64 | −0.174 % | 4 + constant add4 個加上一次常數加法4 個 + 定数加算 | 5, or 3 with a tree5,用樹則為 35、木を使えば 3 | Good default不錯的預設選擇妥当な既定値 |
| 461/256 with rounding461/256 加捨入461/256 と丸め | +0.043 % | 4 (CSD) + constant add4 個(CSD)加上一次常數加法4 個(CSD)+ 定数加算 | 3 with a tree用樹則為 3木を使えば 3 | One truncation only只截斷一次切り捨ては 1 回だけ |
| 128-entry ROM128 項 ROM128 エントリの ROM | Exact (rounded)精確(已捨入)厳密(丸め済み) | 0 | 1 decode1 次解碼デコード 1 回 | Only for a bounded range只適用於有界的範圍範囲が有界な場合に限る |
module temp_filter (av_temp, clk, rst, temp_data);
output [11:0] av_temp;
input clk;
input [11:0] temp_data;
reg [11:0] temp [3:0];
always @ (negedge clk or posedge rst)
if (rst) begin
temp[0] <= 0; temp[1] <= 0; temp[2] <= 0; temp[3] <= 0;
end
else begin: data_pipeline
temp[3] <= temp_data;
temp[2] <= temp[3];
temp[1] <= temp[2];
temp[0] <= temp[1];
end
assign av_temp = (temp[3] + temp[2] + temp[1] + temp[0]) >> 2;
endmodule
The module as printed has a defect you must fix before it will even compile: rst appears in the sensitivity list and in the body but is never declared as an input. Add input rst;. (Verilog would otherwise create an implicit one-bit wire with no driver, which is permanently z, so the reset branch would be unreachable and the simulation would start with x in every stage.)印出來的模組有一個缺陷,不修它連編譯都過不了:rst 出現在敏感列表與本體裡,卻從未被宣告成輸入。請加上 input rst;。(否則 Verilog 會造出一條沒有驅動者的隱含一位元 wire,它永遠是 z,於是重置分支永遠進不去,模擬一開始每一級都是 x。)印刷されたモジュールには、コンパイルすら通らない欠陥がある。rst が感度リストと本体に現れるのに、入力として宣言されていない。input rst; を追加する必要がある。(さもなければ Verilog は駆動元のない暗黙の 1 ビット wire を作る。それは永久に z なのでリセット分岐へ到達できず、シミュレーションは全段 x で始まる。)
With that fixed, the block is a four-deep, twelve-bit shift register: 48 flip-flops. The newest sample lives in temp[3] and the oldest in temp[0], and every clock each stage copies from its neighbour. The ordering of the four statements is the thing to look at, because it is the clearest illustration of the nonblocking rule you will find.修好之後,這個區塊就是一個四級、十二位元的移位暫存器:48 個正反器。最新的樣本住在 temp[3],最舊的在 temp[0],每個時脈每一級都從鄰居複製過來。要看的是那四個敘述的順序,因為這是你能找到最清楚的非阻塞規則例示。修正すれば、このブロックは 4 段・12 ビットのシフトレジスタ、すなわちフリップフロップ 48 個になる。最新のサンプルは temp[3] に、最も古いものは temp[0] にあり、毎クロック各段が隣から値を写す。注目すべきは 4 つの文の順序である。ノンブロッキング規則のこれ以上なく明快な例になっているからである。
temp[3] <= temp_data; temp[2] <= temp[3]; temp[1] <= temp[2]; temp[0] <= temp[1];
Every right-hand side is read before any left-hand side is updated, so temp[2] receives the value temp[3] held before this edge. Four independent flip-flop banks in a chain. The order of the four statements is irrelevant — you could shuffle them and get the same hardware.每一個右手邊都在任何左手邊被更新之前讀取,所以 temp[2] 收到的是這個邊緣之前 temp[3] 的值。四組獨立的正反器串成一條鏈。那四個敘述的順序無關緊要 —— 你把它們打亂,得到的硬體一模一樣。すべての右辺は、いずれの左辺が更新される前に読まれるので、temp[2] はこのエッジ以前の temp[3] の値を受け取る。独立した 4 組のフリップフロップが鎖になる。4 つの文の順序は無関係で、並べ替えても同じハードウェアになる。
temp[3] = temp_data; temp[2] = temp[3]; temp[1] = temp[2]; temp[0] = temp[1];
Each assignment takes effect immediately, so temp[2] receives the value just written to temp[3], and so on down. All four registers end up holding temp_data. The average of four identical numbers is that number: the filter does nothing at all, and the synthesiser cheerfully optimises three of the four register banks away.每個指定立刻生效,所以 temp[2] 收到的是剛剛寫進 temp[3] 的值,往下依此類推。四個暫存器最後都存著 temp_data。四個相同數字的平均就是那個數字:濾波器什麼事都沒做,而合成器會開開心心地把四組暫存器裡的三組最佳化掉。各代入はただちに効くので、temp[2] はいま temp[3] に書かれた値を受け取り、以下同様に伝わる。4 つのレジスタはすべて temp_data を保持することになる。同じ数 4 つの平均はその数そのものである。フィルタは何もしておらず、合成器は 4 組のうち 3 組を喜んで消し去る。
Note the asymmetry of the failure: with blocking assignments written in the reverse order — temp[0] = temp[1]; first and temp[3] = temp_data; last — the block behaves correctly again, because each stage is read before it is overwritten. That is exactly why the rule is "always use nonblocking in a clocked block" rather than "be careful about ordering". A rule that depends on statement order is a rule that a later edit will break.注意這個失效的不對稱性:如果阻塞指定以相反順序寫 —— 先 temp[0] = temp[1];,最後 temp[3] = temp_data; —— 這個區塊又正確了,因為每一級都在被覆寫之前先被讀取。這正是為什麼規則是「時脈區塊裡一律用非阻塞」,而不是「小心順序」。一條依賴敘述順序的規則,就是一條日後某次修改一定會打破的規則。この破綻の非対称性に注意したい。ブロッキング代入を逆順に書けば——先に temp[0] = temp[1];、最後に temp[3] = temp_data;——各段は上書きされる前に読まれるので、ブロックは再び正しく動く。だからこそ規則は「順序に気をつける」ではなく「クロック同期ブロックでは常にノンブロッキングを使う」なのである。文の順序に依存する規則は、いつか誰かの編集で壊れる規則である。
The averaging itself is a single continuous assignment: add the four stages and shift right by two. Division by four is a right shift because four is a power of two; this is the one case Lecture 7 blesses, and it costs nothing.平均本身就是一個連續指定:把四級加起來再右移兩位。除以四就是右移,因為四是 2 的冪次;這是第 7 講唯一放行的情況,而且不花成本。平均そのものは連続代入 1 本である。4 段を足して 2 ビット右シフトするだけである。4 は 2 のべき乗なので 4 で割ることは右シフトであり、これは第 7 講が唯一認める場合で、コストもかからない。
The floor is the price. Shifting right discards the two least significant bits of the sum, which is truncation towards zero, so the computed average is never larger than the true average and is up to $3/4$ smaller. Averaged over uniformly distributed remainders $r \in \{0,1,2,3\}$ the expected error is $-\mathbb{E}[r]/4 = -1.5/4 = -0.375$ — a systematic downward bias of three eighths of a degree, on top of whatever the converter already cost you.代價是那個下取整。右移會丟掉總和最低的兩個位元,那是朝零截斷,所以算出來的平均永遠不會大於真平均,而且最多小 $3/4$。在餘數 $r \in \{0,1,2,3\}$ 均勻分布的假設下,期望誤差是 $-\mathbb{E}[r]/4 = -1.5/4 = -0.375$ —— 一個系統性的、向下八分之三度的偏差,而且是疊在轉換器已經讓你付出的代價之上。代償は床関数である。右シフトは総和の下位 2 ビットを捨てる。これはゼロ方向への切り捨てなので、計算される平均は真の平均を決して上回らず、最大で $3/4$ 小さくなる。余り $r \in \{0,1,2,3\}$ が一様分布だとすると期待誤差は $-\mathbb{E}[r]/4 = -1.5/4 = -0.375$、すなわち8 分の 3 度ぶんの系統的な下向きバイアスであり、しかも変換器ですでに払った分の上に乗る。
A bias is worse than noise, because noise averages out over repeated measurements and a bias does not. Removing it costs one constant addition:偏差比雜訊糟,因為雜訊在反覆量測下會平均掉,偏差不會。去掉它只要一次常數加法:バイアスは雑音より厄介である。雑音は測定を繰り返せば平均で消えるが、バイアスは消えない。除くには定数加算 1 回で足りる。
wire [13:0] sum = temp[3] + temp[2] + temp[1] + temp[0];
assign av_temp = (sum + 14'd2) >> 2; // round to nearest instead of truncating
Adding half the divisor before dividing is round-half-up. The error now lies in $[-0.5, +0.5)$ and its mean is close to zero, for the cost of forcing a 1 into bit 1 of a carry chain that was being computed anyway. There is essentially never a reason not to do this in a datapath that will be read by a human.在除之前先加上除數的一半,就是四捨五入。現在誤差落在 $[-0.5, +0.5)$,平均值接近零,代價只是在一條本來就要算的進位鏈上把位元 1 強制設為 1。在一條會被人類閱讀的資料路徑裡,基本上找不到不這麼做的理由。割る前に除数の半分を足す、すなわち四捨五入である。誤差は $[-0.5, +0.5)$ に収まり、その平均はほぼゼロになる。代償は、どのみち計算されるキャリーチェーンのビット 1 に 1 を押し込むことだけである。人が読むデータパスで、これをやらない理由は事実上存在しない。
In assign av_temp = (temp[3] + temp[2] + temp[1] + temp[0]) >> 2; the operands are 12 bits and the assignment target is 12 bits. For a shift expression the left operand is context-determined and the right operand is self-determined, so the four-term sum is evaluated at $\max(12, 12) = 12$ bits and the carries out of bit 11 are thrown away before the shift ever happens. With all four stages holding 2000, the true sum is 8000; in 12 bits that becomes $8000 - 4096 = 3904$, and $3904 \gg 2 = 976$ — an "average" of 976 from four samples of 2000. The fix is the wire [13:0] sum above: compute the sum in a wide enough net and take the correct bit field, sum[13:2], or round first as shown. This is the single most commonly examined width trap in Verilog and it is worth being able to state the rule from memory.在 assign av_temp = (temp[3] + temp[2] + temp[1] + temp[0]) >> 2; 裡,運算元是 12 位元,指定目標也是 12 位元。對移位運算式而言,左運算元由上下文決定寬度,右運算元自定寬度,所以四項總和是在 $\max(12, 12) = 12$ 位元下求值,位元 11 的進位在移位還沒發生之前就被丟掉了。四級全都存著 2000 時,真正的總和是 8000;在 12 位元下變成 $8000 - 4096 = 3904$,而 $3904 \gg 2 = 976$ —— 四筆 2000 的樣本平均出 976。修法就是上面那個 wire [13:0] sum:在夠寬的 net 裡算總和,再取正確的位元欄位 sum[13:2],或是照前面所示先做捨入。這是 Verilog 裡最常被考的單一寬度陷阱,值得把規則背到能隨口說出來。assign av_temp = (temp[3] + temp[2] + temp[1] + temp[0]) >> 2; ではオペランドが 12 ビット、代入先も 12 ビットである。シフト式では左オペランドの幅が文脈で決まり、右オペランドは自己決定である。したがって 4 項の総和は $\max(12, 12) = 12$ ビットで評価され、ビット 11 からの桁上げはシフトが起きる前に捨てられる。4 段すべてが 2000 を保持していれば真の総和は 8000 だが、12 ビットでは $8000 - 4096 = 3904$ となり、$3904 \gg 2 = 976$ である。2000 のサンプル 4 つから「平均」976 が出てくる。修正は前掲の wire [13:0] sum、すなわち十分に広いネットで総和を計算して正しいビット範囲 sum[13:2] を取るか、示したように先に丸めることである。Verilog で最も頻繁に出題される幅の落とし穴であり、規則をそらで言えるようにしておく価値がある。
The filter clocks on negedge clk while every other register in the system clocks on posedge clk. Why? Because the converter sits between the register and the filter as a block of combinational logic, and using the opposite edge gives that logic half a clock period instead of a whole one — or, read the other way, it makes the filter sample the converter's output half a period after the register launched it, which in a slow design is simply "a bit of extra margin by accident".這個濾波器用 negedge clk 觸發,而系統裡其他每一個暫存器都用 posedge clk。為什麼?因為轉換器以一團組合邏輯的身分夾在暫存器與濾波器之間,用相反的邊緣,等於給那團邏輯半個時脈週期而不是一整個 —— 或者反過來讀:它讓濾波器在暫存器送出資料後半個週期才取樣轉換器的輸出,在一個慢速設計裡,這不過就是「不小心多了一點餘裕」。このフィルタは negedge clk で動作するが、システムの他のレジスタはすべて posedge clk である。なぜか。変換器が組合せ論理の塊としてレジスタとフィルタのあいだに挟まっており、反対のエッジを使えばその論理に 1 クロック周期ではなく半周期が与えられる——逆から読めば、レジスタが送り出してから半周期後にフィルタが変換器の出力をサンプルする——ということであり、低速な設計なら要するに「たまたま余裕が少し増えた」だけである。
In a cell-based ASIC flow this is a bad habit, and you should be able to say why in an interview.在 cell-based 的 ASIC 流程裡,這是個壞習慣,而你應該能在面試時說出原因。セルベースの ASIC フローではこれは悪い習慣であり、その理由を面接で言えるようにしておきたい。
The correct structure is to clock everything on the rising edge and, if the converter's delay is a problem, to register the converter output — adding one pipeline stage and one cycle of latency rather than borrowing half a clock.正確的結構是全部用上升邊緣,而如果轉換器的延遲真的是問題,就把轉換器的輸出暫存起來 —— 多一級管線、多一個週期的延遲,而不是去借半個時脈。正しい構成はすべてを立ち上がりエッジにすることであり、変換器の遅延が本当に問題なら、変換器の出力をレジスタに取る——パイプラインを 1 段、レイテンシを 1 サイクル増やすのであって、半クロックを借りるのではない。
What the filter does to the data is worth one paragraph of signal theory, since this is a communications course. A four-tap moving average is an FIR filter with impulse response $h = \frac14[1,1,1,1]$.濾波器對資料做了什麼,值得用一段訊號理論說明,畢竟這是一門通訊課。四抽頭的移動平均是一個脈衝響應為 $h = \frac14[1,1,1,1]$ 的 FIR 濾波器。フィルタがデータに何をするかは、通信の講義である以上、信号理論で一段落ぶん述べておく価値がある。4 タップの移動平均はインパルス応答 $h = \frac14[1,1,1,1]$ を持つ FIR フィルタである。
Its gain at DC is exactly 1, so steady temperatures pass through unchanged — which is what you want from a smoothing filter. It has exact nulls at $\omega = \pi/2$, $\pi$ and $3\pi/2$, that is at one quarter, one half and three quarters of the sampling rate, so periodic interference at those frequencies is annihilated. Its group delay is $(N-1)/2 = 1.5$ samples, constant with frequency because the response is linear phase, so all frequency components are delayed equally and the shape of a transient is preserved rather than smeared asymmetrically. And for white measurement noise of variance $\sigma^2$ the output variance is $\sigma^2/4$, a factor of two in standard deviation or 6 dB — the classic "averaging $N$ samples buys $\sqrt{N}$" result. All of that for 48 flip-flops and three adders.它在直流的增益正好是 1,所以穩定的溫度原封不動通過 —— 這正是你要平滑濾波器做的事。它在 $\omega = \pi/2$、$\pi$ 與 $3\pi/2$,也就是取樣率的四分之一、二分之一與四分之三處有精確的零點,所以落在這些頻率的週期性干擾會被完全消滅。它的群延遲是 $(N-1)/2 = 1.5$ 個樣本,與頻率無關,因為它是線性相位響應,所以所有頻率成分被延遲同樣多,暫態的形狀被保留而不是被不對稱地抹開。而對變異數 $\sigma^2$ 的白色量測雜訊,輸出變異數是 $\sigma^2/4$,標準差差兩倍,也就是 6 dB —— 這就是經典的「平均 $N$ 筆換到 $\sqrt{N}$」結果。這一切只花 48 個正反器與三個加法器。直流での利得はちょうど 1 なので、一定の温度はそのまま通る。平滑フィルタに求めるのはまさにそれである。$\omega = \pi/2$、$\pi$、$3\pi/2$、すなわちサンプリング周波数の 1/4、1/2、3/4 に厳密な零点を持つので、その周波数の周期的な干渉は完全に消える。群遅延は $(N-1)/2 = 1.5$ サンプルで周波数によらない。応答が直線位相だからであり、すべての周波数成分が等しく遅れるので、過渡の形は非対称に滲むことなく保たれる。また分散 $\sigma^2$ の白色測定雑音に対して出力分散は $\sigma^2/4$、標準偏差で 2 分の 1、すなわち 6 dB となる。「$N$ サンプル平均すれば $\sqrt{N}$ 得をする」という古典的な結果である。これらすべてがフリップフロップ 48 個と加算器 3 個で手に入る。
One structural note: because the sum is recomputed from all four stages every cycle, the filter is a sliding window that produces a new output on every sample, not a block average that produces one output per four samples. The distinction matters when you count how fast the FIFO fills: the filter emits one word per clock, exactly as fast as the sensor supplies one.一點結構上的補充:因為總和每個週期都從四級重新算一次,這個濾波器是一個滑動視窗,每筆樣本都產生一個新輸出,而不是每四筆樣本才產生一個輸出的區塊平均。要算 FIFO 多快填滿時,這個區別就有關係:濾波器每個時脈吐一個字,跟感測器供應的速度完全一樣快。構造についての補足を一つ。総和は毎サイクル 4 段すべてから計算し直されるので、このフィルタはスライディング窓であり、サンプルごとに新しい出力を出す。4 サンプルごとに 1 出力を出すブロック平均ではない。FIFO がどれだけ速く埋まるかを数えるときにこの違いが効く。フィルタはセンサが供給するのとまったく同じ速さで、毎クロック 1 ワードを出す。
module fifo_controller (data_out, data_1, data_2, select, read, write,
empty_1, full_1, read_1, empty_2, full_2, read_2);
input [11:0] data_1, data_2;
input [1:0] select;
input read, full_1, empty_1, full_2, empty_2;
output [11:0] data_out;
output read_1, read_2, write;
reg [11:0] data_out;
reg read_1, read_2, write;
always @ (select or data_1 or data_2 or read or full_1
or empty_1 or full_2 or empty_2) begin
read_1 = 0;
read_2 = 0;
if (read == 1)
begin
write = 0;
case (select)
2'b01: begin data_out = data_1; read_1 <= 1; end
2'b10: begin data_out = data_2; read_2 <= 1; end
2'b00: begin data_out = {empty_1, empty_2, full_1, full_2}; end
default: data_out = 12'bz;
endcase
end
else if (read == 0)
begin
data_out = 12'bz;
write = 1;
end
else data_out = 12'bz;
end
endmodule
This is a level-sensitive always block, so by the Lecture 5 rules it must assign every output on every path or it will infer latches. Walk the paths and the behaviour falls out:這是一個位準觸發的 always 區塊,所以照第 5 講的規則,它必須在每一條路徑上都指定每一個輸出,否則就會推論出閂鎖。把路徑走一遍,行為自然就出來了:これはレベル記述の always ブロックなので、第 5 講の規則により、すべての経路ですべての出力に代入しなければラッチが推論される。経路をたどれば動作はおのずと見えてくる。
read | select | data_out | read_1 / read_2 | write |
|---|---|---|---|---|
| 1 | 01 | the word presented by FIFO 1FIFO 1 送出的資料字FIFO 1 が提示するワード | 1 / 0 | 0 |
| 1 | 10 | the word presented by FIFO 2FIFO 2 送出的資料字FIFO 2 が提示するワード | 0 / 1 | 0 |
| 1 | 00 | {8'b0, empty_1, empty_2, full_1, full_2} | 0 / 0 | 0 |
| 1 | 11 | high impedance高阻抗ハイインピーダンス | 0 / 0 | 0 |
| 0 | any任意任意 | high impedance高阻抗ハイインピーダンス | 0 / 0 | 1 — both FIFOs are written1 —— 兩個 FIFO 都被寫入1 — 両方の FIFO が書き込まれる |
The status word deserves a note. {empty_1, empty_2, full_1, full_2} is a four-bit concatenation assigned to a twelve-bit target, so it is zero-extended on the left: the flags land in bits 3 down to 0 and bits 11 down to 4 are zero. Concatenations are always self-determined — their width is the sum of their parts, regardless of the target — which makes them the one place in Verilog where you can rely on the width being exactly what you wrote.那個狀態字值得補一句。{empty_1, empty_2, full_1, full_2} 是一個四位元的串接,被指定給十二位元的目標,所以左邊補零:旗標落在位元 3 到 0,位元 11 到 4 都是零。串接永遠是自定寬度的 —— 它的寬度就是各部分之和,與目標無關 —— 這讓它成為 Verilog 裡唯一一個你可以放心相信「寬度就是你寫的那樣」的地方。ステータスワードについて一言。{empty_1, empty_2, full_1, full_2} は 4 ビットの連結で、12 ビットの代入先に入るので左側がゼロ拡張される。フラグはビット 3 から 0 に入り、ビット 11 から 4 はゼロになる。連結は常に自己決定幅であり——幅は代入先に関係なく各部分の和である——Verilog の中で幅が書いたとおりであると当てにできる唯一の場所になっている。
read_1 <= 1 inside a level-sensitive block mixes assignment styles on a variable that was set with read_1 = 0 three lines earlier. Here it happens to work: the blocking zero is applied immediately, the nonblocking one is applied in the NBA region of the same time step, and the final value is 1. But a nonblocking assignment to a variable that is also in the block's own sensitivity list can cause the block to re-trigger and, in pathological cases, to oscillate. Write read_1 = 1;.組合區塊裡的非阻塞指定。位準觸發區塊裡的 read_1 <= 1,與三行之前用 read_1 = 0 設定的同一個變數混用了指定風格。這裡剛好會動:阻塞的那個零立刻生效,非阻塞的那個在同一時間步的 NBA 區間生效,最終值是 1。但對一個同時也在該區塊自己敏感列表裡的變數做非阻塞指定,可能讓區塊重新觸發,在病態情況下甚至振盪。請寫 read_1 = 1;。組合せブロック内のノンブロッキング代入。レベル記述のブロック内の read_1 <= 1 は、3 行前に read_1 = 0 で設定された同じ変数に対して代入方式を混ぜている。ここではたまたま動く。ブロッキングのゼロが即座に効き、ノンブロッキングのほうが同じ時間ステップの NBA 領域で効いて、最終値は 1 になる。しかし、そのブロック自身の感度リストにも含まれる変数へノンブロッキング代入すると、ブロックが再起動し、病的な場合には発振しうる。read_1 = 1; と書くべきである。write is not pre-assigned. read_1 and read_2 are defaulted to 0 at the top of the block, but write is not. On the read == x path — the final bare else — write keeps its previous value, which is the textbook definition of an inferred latch. Most tools notice that a one-bit signal compared against 1 and then against 0 covers everything and will not actually build the latch, but you are relying on the tool's cleverness instead of on your own code. Add write = 0; beside the other two defaults.write 沒有被預先指定。read_1 與 read_2 在區塊開頭被預設為 0,但 write 沒有。在 read == x 那條路徑上 —— 也就是最後那個光禿禿的 else —— write 會保持前一個值,這正是教科書上推論閂鎖的定義。多數工具會注意到一個一位元訊號先跟 1 比、再跟 0 比已經窮盡所有情況,因而不會真的造出閂鎖,但你這是在依賴工具的聰明,而不是依賴自己的程式碼。請在另外兩個預設值旁邊加上 write = 0;。write に既定値が与えられていない。read_1 と read_2 はブロックの冒頭で 0 に初期化されるが、write はされていない。read == x の経路——最後の裸の else——では write が前の値を保持する。これは教科書どおりのラッチ推論の定義である。多くのツールは、1 ビット信号を 1 と比べ次に 0 と比べれば全域を覆うと気づき、実際にはラッチを作らない。しかしそれは自分のコードではなくツールの賢さに頼っていることになる。他の二つの既定値の隣に write = 0; を足すべきである。write is simply !read, and it is wired to both FIFOs. So every cycle in which the host is not reading, both FIFOs store whatever their filter currently outputs — including cycles in which the filter output has not changed, so the FIFOs fill with duplicates within eight clocks and then sit full. And every cycle in which the host is reading, neither FIFO stores anything, so genuinely new samples are dropped. A correct design gates the write with a "new sample valid" strobe from the sensor and keeps the read and write sides independent, which is exactly what a FIFO is for. As written, the controller defeats the buffer it is controlling.系統層次上這個寫入協定是錯的。write 就只是 !read,而且接到兩個 FIFO 上。於是主機沒在讀的每一個週期,兩個 FIFO 都把各自濾波器當下的輸出存起來 —— 包括濾波器輸出根本沒變的那些週期,所以 FIFO 在八個時脈內就塞滿重複值,然後一直滿著。而主機正在讀的每一個週期,兩個 FIFO 都不存任何東西,於是真正的新樣本被丟掉。正確的設計會用感測器送來的「新樣本有效」脈衝去把寫入打開,並讓讀取側與寫入側彼此獨立 —— 而那正是 FIFO 存在的目的。照現在的寫法,控制器打敗了它所控制的那個緩衝區。システムのレベルで書き込みプロトコルが誤っている。write は単に !read であり、しかも両方の FIFO に配線されている。したがってホストが読んでいないすべてのサイクルで、両方の FIFO がそれぞれのフィルタの現在の出力を格納する。フィルタ出力が変化していないサイクルも含むので、FIFO は 8 クロックで重複値に埋まり、そのまま満杯になる。逆にホストが読んでいるサイクルではどちらの FIFO も何も格納しないので、本当に新しいサンプルが捨てられる。正しい設計では、センサからの「新サンプル有効」ストローブで書き込みをゲートし、読み出し側と書き込み側を独立に保つ。それこそ FIFO の存在理由である。現状のコードでは、コントローラが自ら制御するバッファを無力化している。The 12'bz assignments infer three-state drivers: twelve buffers whose enable is the OR of the conditions under which a real value is driven. Inside a cell-based ASIC, internal three-state nets are strongly discouraged — they cannot be analysed for contention by static timing, they float when no driver is enabled, and they need bus holders to avoid crowbar current in the receivers. The modern equivalent is a multiplexer with an explicit default. Three-state is still normal at the chip boundary, where data_out would go to a bidirectional pad; if this controller's output is genuinely a chip pin shared with other devices, the code is right, and if it is an internal net, it is not.那些 12'bz 的指定會推論出三態驅動器:十二個緩衝器,致能訊號是「會驅動真值的那些條件」的 OR。在 cell-based ASIC 內部,內部三態 net 是強烈不建議的 —— 靜態時序分析無法分析它們的爭用、沒有驅動者致能時它們會浮接、而且還需要 bus holder 以免接收端出現貫穿電流。現代的等價做法是一個帶明確預設值的多工器。三態在晶片邊界仍然正常,那裡 data_out 會接到一個雙向的 pad;如果這個控制器的輸出真的是一支與其他裝置共用的晶片腳位,那程式碼就是對的;如果它是一條內部 net,那就不對。12'bz の代入はトライステートドライバを推論する。実値を駆動する条件の OR をイネーブルとする 12 個のバッファである。セルベース ASIC の内部ではトライステートのネットは強く非推奨である。静的タイミング解析で競合を解析できず、どのドライバもイネーブルでないとき浮き、受け側の貫通電流を避けるためにバスホルダが必要になる。現代的な等価物は既定値を明示したマルチプレクサである。チップ境界ではトライステートは今も普通で、そこでは data_out は双方向パッドへ向かう。このコントローラの出力が本当に他のデバイスと共有するチップのピンならコードは正しく、内部ネットなら正しくない。
One timing subtlety to carry forward. read_1 is produced combinationally from read and select, and the FIFO samples it at the next rising edge. The FIFO's Data_out is registered. So in the cycle during which the host asserts read, data_1 still carries the previous word, and the controller is multiplexing stale data onto data_out. The host sees the requested word one cycle later. If you draw the system waveform without noticing this you will conclude the FIFO is off by one; it is not, it is pipelined, and the protocol the host must follow is "assert read, wait one cycle, sample".一個要帶著走的時序細節。read_1 是從 read 與 select 組合產生的,而 FIFO 在下一個上升邊緣才取樣它。FIFO 的 Data_out 是暫存的。所以在主機拉起 read 的那個週期裡,data_1 上帶的還是前一個字,控制器把舊資料多工到 data_out 上。主機要再過一個週期才看到它要的字。如果你畫系統波形時沒注意到這一點,你會下結論說 FIFO 差了一拍;它沒有,它是管線化的,而主機必須遵守的協定是「拉起 read,等一個週期,再取樣」。先へ持っていくべきタイミング上の細部がある。read_1 は read と select から組合せ的に作られ、FIFO はそれを次の立ち上がりエッジでサンプルする。FIFO の Data_out はレジスタ出力である。したがってホストが read を立てたサイクルでは data_1 はまだ前のワードを運んでおり、コントローラは古いデータを data_out へ多重化している。ホストが要求したワードを見るのは 1 サイクル後である。これに気づかずにシステムの波形を描けば、FIFO が 1 拍ずれていると結論してしまう。ずれてはおらず、パイプライン化されているだけで、ホストが守るべきプロトコルは「read を立て、1 サイクル待ち、サンプルする」である。
module temp_monitor_unit (channel_1, channel_2, select, clk, rst, read, data_out); output [11:0] data_out; input [11:0] channel_1, channel_2; input [1:0] select; input clk, rst, read; wire [11:0] temp1_out, temp2_out, temp_conv1, temp_conv2, av_temp1, av_temp2; wire [11:0] Data_out1, Data_out2; temp_register M1 (temp1_out, channel_1, clk, rst); // (1) temp_register M2 (temp2_out, channel_2, clk, rst); temp_converter M3 (temp_conv1, temp1_out); // (2) temp_converter M4 (temp_conv2, temp2_out); temp_filter M5 (av_temp1, clk, rst, temp_conv1); // (3) temp_filter M6 (av_temp2, clk, rst, temp_conv2); FIFO M7 (Data_out1, av_temp1, stack_empty1, stack_full1, clk, rst, write_to_stack, read_from_stack1); // (5) FIFO M8 (Data_out2, av_temp2, stack_empty2, stack_full2, clk, rst, write_to_stack, read_from_stack2); fifo_controller M9 (data_out, Data_out1, Data_out2, // (4) select, read, write_to_stack, stack_empty1, stack_full1, read_from_stack1, stack_empty2, stack_full2, read_from_stack2); endmodule
Twelve lines of instantiation and no logic at all — this is what a good top level looks like. Three integration hazards are visible in it, and all three are the kind of thing that survives a code review and dies in silicon.十二行實例化,完全沒有邏輯 —— 好的頂層就長這樣。裡面看得到三個整合上的危險,而且三個都是那種「通過程式碼審查、死在矽片上」的東西。インスタンス化 12 行、論理はまったく無し——よいトップレベルとはこういうものである。その中に統合上の危険が三つ見えており、三つとも「コードレビューを通り抜けてシリコンで死ぬ」種類のものである。
Implicit nets. stack_empty1, stack_full1, read_from_stack1, write_to_stack and their channel-2 partners are never declared. Verilog creates an implicit one-bit wire for any undeclared identifier used in a port connection, so the module compiles and works. The danger is that a typo also creates a wire: write stack_emtpy1 in one of the two places and you get two separate, silent, one-bit nets, one driven and never read, the other read and never driven, and the design simulates with an x that you will chase for an afternoon. Put `default_nettype none at the top of the file and every undeclared identifier becomes a compile error. There is no downside and it is the single highest-value one-line change you can make to a Verilog project.隱含 net。stack_empty1、stack_full1、read_from_stack1、write_to_stack 以及它們在通道 2 的夥伴,都從未被宣告。Verilog 會為任何用在埠連接上的未宣告識別字造出一條隱含的一位元 wire,所以模組編譯得過也能動。危險在於打錯字也會造出一條 wire:在兩處之一寫成 stack_emtpy1,你就得到兩條各自獨立、沉默的一位元 net,一條有驅動卻沒人讀,另一條有人讀卻沒驅動,於是設計模擬出一個 x,讓你追一個下午。在檔案開頭放上 `default_nettype none,每一個未宣告的識別字就會變成編譯錯誤。這沒有任何壞處,而且是你能對一個 Verilog 專案做的、單行改動裡價值最高的一個。暗黙のネット。stack_empty1、stack_full1、read_from_stack1、write_to_stack とそのチャネル 2 の相方は、どれも宣言されていない。Verilog はポート接続に使われた未宣言の識別子に対して暗黙の 1 ビット wire を作るので、モジュールはコンパイルも動作もする。危険なのは綴り誤りでも wire ができてしまうことである。2 か所のうち一方を stack_emtpy1 と書けば、独立した無言の 1 ビットネットが 2 本でき、一方は駆動されるが読まれず、他方は読まれるが駆動されない。設計は x を出し、半日それを追うことになる。ファイルの先頭に `default_nettype none を置けば、未宣言の識別子はすべてコンパイルエラーになる。欠点は無く、Verilog プロジェクトに加えられる 1 行の変更としては最も価値が高い。
Implicit nets are one bit wide. If an undeclared identifier is connected to a multi-bit port, you get a one-bit wire and eleven dangling bits, usually with a warning you will not read. All the implicit nets here happen to be one bit, so no harm is done, but the mechanism is waiting.隱含 net 只有一位元寬。如果一個未宣告的識別字被接到多位元的埠上,你會得到一條一位元 wire 與十一個懸空的位元,通常還附帶一個你不會去讀的警告。這裡所有的隱含 net 剛好都是一位元,所以沒出事,但這個機制一直在等。暗黙のネットは 1 ビット幅である。未宣言の識別子を多ビットのポートに接続すると、1 ビットの wire と宙に浮いた 11 ビットが得られる。たいてい読まれない警告が付いてくるだけである。ここでは暗黙のネットがたまたますべて 1 ビットなので実害はないが、この仕組みは待ち構えている。
Positional port binding. Every instance binds by position. M9's argument list has twelve entries that must line up exactly with fifo_controller's port order (data_out, data_1, data_2, select, read, write, empty_1, full_1, read_1, empty_2, full_2, read_2). Check them: stack_empty1 against empty_1, stack_full1 against full_1, read_from_stack1 against read_1 — correct. Now imagine somebody adds a port to fifo_controller in the middle of its list. Every instance in the project silently misconnects and nothing flags it, because all the ports are one bit wide and all the types match. Named binding makes the whole class of bug impossible:位置式埠綁定。每個實例都用位置綁定。M9 的引數列有十二項,必須與 fifo_controller 的埠順序 (data_out, data_1, data_2, select, read, write, empty_1, full_1, read_1, empty_2, full_2, read_2) 完全對齊。檢查一下:stack_empty1 對 empty_1、stack_full1 對 full_1、read_from_stack1 對 read_1 —— 正確。現在想像有人在 fifo_controller 的埠列中間插進一個新埠。專案裡的每一個實例都會悄悄接錯,而且沒有任何東西會示警,因為所有埠都是一位元寬、型別也都相符。具名綁定讓這整類 bug 不可能發生:位置指定のポート結合。どのインスタンスも位置で結合している。M9 の引数リストは 12 項目あり、fifo_controller のポート順 (data_out, data_1, data_2, select, read, write, empty_1, full_1, read_1, empty_2, full_2, read_2) と正確に並ばなければならない。確かめてみると、stack_empty1 は empty_1 に、stack_full1 は full_1 に、read_from_stack1 は read_1 に対応しており正しい。では誰かが fifo_controller のポートリストの途中にポートを 1 本足したらどうなるか。プロジェクト中のすべてのインスタンスが黙って誤接続になり、何も警告しない。すべてのポートが 1 ビット幅で型も一致するからである。名前指定の結合なら、この種のバグはそもそも起こりえない。
fifo_controller M9 (
.data_out (data_out), .data_1 (Data_out1), .data_2 (Data_out2),
.select (select), .read (read), .write (write_to_stack),
.empty_1 (stack_empty1), .full_1 (stack_full1), .read_1 (read_from_stack1),
.empty_2 (stack_empty2), .full_2 (stack_full2), .read_2 (read_from_stack2));
It is more typing and it is worth every character. Use positional binding only for trivial, stable, two-or-three-port primitives, and named binding for everything else.打字量是多了,而且每一個字元都值得。位置式綁定只留給瑣碎、穩定、兩三個埠的基本元件,其餘一律用具名綁定。打つ量は増えるが、その 1 文字ずつに価値がある。位置指定の結合は、ポートが 2〜3 本の些細で安定したプリミティブだけに留め、それ以外はすべて名前指定にする。
A system testbench for this unit has to do three things the module-level benches could not: drive both channels with physically plausible data, check the arithmetic end to end against a behavioural model of the specification, and exercise the controller's select codes including the status read.這個單元的系統級測試平台必須做三件模組級測試平台做不到的事:用物理上合理的資料驅動兩條通道、拿整條路徑的算術去對規格的行為模型做端對端比對,以及把控制器的 select 碼(含狀態讀取)都操練過。このユニットのシステムレベルのテストベンチは、モジュール単体のベンチにはできない三つのことをしなければならない。物理的にありうるデータで両チャネルを駆動すること、仕様の振る舞いモデルに対して算術を端から端まで照合すること、そしてステータス読み出しを含むコントローラの select コードをすべて叩くことである。
// stimulus: a slow ramp on channel 1, a step with noise on channel 2 integer k; initial begin rst = 1; read = 0; select = 2'b00; channel_1 = 0; channel_2 = 0; @(negedge clk); rst = 0; for (k = 0; k < 64; k = k + 1) begin @(negedge clk); channel_1 = k; // 0..63 degrees C channel_2 = (k < 32) ? 20 : 20 + ($random % 3); // step plus +/-2 noise end // now drain FIFO 1, then read the status word select = 2'b01; read = 1; repeat (8) @(negedge clk); read = 0; @(negedge clk); select = 2'b00; read = 1; @(negedge clk); $display("status = %b (empty1 empty2 full1 full2)", data_out[3:0]); $finish; end // reference model of the whole channel, written for clarity not synthesis function [11:0] golden_channel (input [11:0] c0, c1, c2, c3); reg [15:0] f0, f1, f2, f3; begin f0 = c0 + (c0 >> 1) + (c0 >> 2) + 32; // repeat for c1, c2, c3 f1 = c1 + (c1 >> 1) + (c1 >> 2) + 32; f2 = c2 + (c2 >> 1) + (c2 >> 2) + 32; f3 = c3 + (c3 >> 1) + (c3 >> 2) + 32; golden_channel = (f0 + f1 + f2 + f3) >> 2; end endfunction
The reference deliberately models the implementation's arithmetic, including the 1.75 approximation, not the ideal $1.8T + 32$. That is the right choice for a regression test — it checks that the RTL does what was designed — but it means the reference cannot catch a wrong approximation. The accuracy question must be answered separately, by comparing the reference against floating-point over the whole input range and recording the worst-case error as a specification item. Keeping those two checks separate (does the RTL match the model? does the model meet the spec?) is a discipline worth acquiring early.這個參考模型刻意模擬的是實作的算術,包含 1.75 的近似,而不是理想的 $1.8T + 32$。對回歸測試而言這是正確的選擇 —— 它檢查 RTL 有沒有做到當初設計的事 —— 但這也表示參考模型抓不到「近似選錯了」。精度問題必須另外回答:把參考模型拿去跟整個輸入範圍上的浮點值比對,並把最壞情況誤差記錄成一條規格項目。把這兩個檢查分開(RTL 有沒有符合模型?模型有沒有達到規格?),是一項值得早點養成的紀律。この参照モデルは意図的に実装の算術を、すなわち 1.75 の近似を含む形でモデル化しており、理想的な $1.8T + 32$ ではない。回帰テストとしてはこれが正しい選択である——RTL が設計どおりかを調べるものだからである——が、裏を返せば近似そのものの誤りは参照モデルでは捕まえられない。精度の問いは別に答えねばならない。参照モデルを入力範囲全体にわたって浮動小数点と比較し、最悪誤差を仕様項目として記録するのである。この二つの検査(RTL はモデルと一致するか、モデルは仕様を満たすか)を分けておくのは、早く身につけておきたい規律である。
For the synthesis result, count what the slides show. Per channel: 12 flops in the register, 0 in the converter, 48 in the filter, and 118 in the FIFO — 178 flops. Two channels is 356 flops, plus a purely combinational controller. The tool's schematic for the whole unit, which the final slide shows, is recognisably the block diagram of §4.1 with the two FIFOs at the top, the controller to their right, and the register/converter/filter chains below — which is the point of writing the design hierarchically in the first place. If your elaborated schematic does not look like the block diagram you drew, either you wrote something other than what you meant or the tool flattened a hierarchy you wanted preserved.合成結果就數一數投影片上顯示的東西。每條通道:暫存器 12 個正反器、轉換器 0 個、濾波器 48 個、FIFO 118 個 —— 共 178 個。兩條通道就是 356 個正反器,再加上一個純組合的控制器。工具為整個單元畫出的電路圖(最後一張投影片)認得出來就是 §4.1 的方塊圖:兩個 FIFO 在上、控制器在它們右邊、暫存器/轉換器/濾波器鏈在下方 —— 而這正是一開始就把設計寫成階層式的意義所在。如果你 elaborate 出來的電路圖跟你畫的方塊圖長得不一樣,不是你寫的東西跟你想的不同,就是工具把你想保留的階層攤平了。合成結果はスライドが示すものを数えればよい。1 チャネルあたり、レジスタ 12 個、変換器 0 個、フィルタ 48 個、FIFO 118 個で計 178 個のフリップフロップ。2 チャネルで 356 個、これに純粋な組合せのコントローラが加わる。最後のスライドが示すユニット全体の回路図は、見ればそれと分かる §4.1 のブロック図そのものである。上に FIFO が 2 つ、その右にコントローラ、下にレジスタ/変換器/フィルタの連なりが並ぶ。設計を最初から階層的に書く意義はここにある。elaborate した回路図が自分の描いたブロック図と似ていないなら、意図と違うものを書いたか、保ちたかった階層をツールが平坦化したかのどちらかである。
Problem: Four consecutive Celsius samples arrive on one channel: 20, 21, 23, 24. Using Alternative 1 of the converter and the slide's truncating filter, compute the four converted values and the averaged output once all four are in the pipeline. Compare with the true Fahrenheit average of the four readings and attribute the total error to its two causes. Then redo the filter stage with round-to-nearest.問題:一條通道上連續到來四筆攝氏樣本:20、21、23、24。用轉換器的寫法一與投影片那個會截斷的濾波器,算出四個轉換值,以及四筆都進入管線之後的平均輸出。與這四筆讀數真正的華氏平均比較,並把總誤差歸因到它的兩個成因。然後把濾波器那一級改成四捨五入再算一次。問題:あるチャネルに摂氏のサンプルが 4 つ連続して届く:20、21、23、24。変換器の方式 1 とスライドの切り捨て型フィルタを使って、4 つの変換値と、4 つすべてがパイプラインに入ったあとの平均出力を求めよ。4 つの読み値の真の華氏平均と比べ、全誤差を二つの原因に帰属させよ。そのうえでフィルタ段を四捨五入にして計算し直せ。
Conversion, $f = c + \lfloor c/2 \rfloor + \lfloor c/4 \rfloor + 32$:轉換,$f = c + \lfloor c/2 \rfloor + \lfloor c/4 \rfloor + 32$:変換、$f = c + \lfloor c/2 \rfloor + \lfloor c/4 \rfloor + 32$:
| $T_C$ | $\lfloor c/2\rfloor$ | $\lfloor c/4\rfloor$ | Converted轉換後変換値 | Exact $1.8c+32$精確值 $1.8c+32$厳密値 $1.8c+32$ | Error誤差誤差 |
|---|---|---|---|---|---|
| 20 | 10 | 5 | 67 | 68.0 | −1.0 |
| 21 | 10 | 5 | 68 | 69.8 | −1.8 |
| 23 | 11 | 5 | 71 | 73.4 | −2.4 |
| 24 | 12 | 6 | 74 | 75.2 | −1.2 |
Filter, truncating: sum $= 67 + 68 + 71 + 74 = 280$; $280 \gg 2 = 70$ exactly (280 is divisible by 4, so no truncation loss this time).濾波器(截斷版):總和 $= 67 + 68 + 71 + 74 = 280$;$280 \gg 2 = 70$,剛好整除(280 可被 4 整除,所以這次沒有截斷損失)。フィルタ(切り捨て版):総和 $= 67 + 68 + 71 + 74 = 280$、$280 \gg 2 = 70$ でちょうど割り切れる(280 は 4 の倍数なので今回は切り捨て損がない)。
True answer: the exact Fahrenheit average is $(68.0 + 69.8 + 73.4 + 75.2)/4 = 286.4/4 = 71.6$ °F.真正的答案:精確的華氏平均是 $(68.0 + 69.8 + 73.4 + 75.2)/4 = 286.4/4 = 71.6$ °F。真の答え:厳密な華氏の平均は $(68.0 + 69.8 + 73.4 + 75.2)/4 = 286.4/4 = 71.6$ °F である。
Error attribution: total error $70 - 71.6 = -1.6$ °F. Of that, the approximation $1.75$ instead of $1.8$ accounts for $-0.05 \times \overline{T_C} = -0.05 \times 22 = -1.1$ °F, and the four per-sample truncations in the shifts account for the remaining $-0.5$ °F on average. The filter's own truncation contributed nothing here because the sum happened to be a multiple of four — which is luck, not design.誤差歸因:總誤差 $70 - 71.6 = -1.6$ °F。其中,用 $1.75$ 取代 $1.8$ 的近似佔了 $-0.05 \times \overline{T_C} = -0.05 \times 22 = -1.1$ °F,而四次移位裡的逐樣本截斷平均佔掉剩下的 $-0.5$ °F。濾波器本身的截斷這次沒有貢獻,因為總和剛好是 4 的倍數 —— 那是運氣,不是設計。誤差の帰属:全誤差は $70 - 71.6 = -1.6$ °F である。うち $1.8$ を $1.75$ で近似した分が $-0.05 \times \overline{T_C} = -0.05 \times 22 = -1.1$ °F、シフトにおけるサンプルごとの切り捨てが平均して残りの $-0.5$ °F を占める。フィルタ自身の切り捨ては今回寄与していない。総和がたまたま 4 の倍数だったからであり、設計ではなく運である。
With round-to-nearest in the filter: $(280 + 2) \gg 2 = 282 \gg 2 = 70$, unchanged. Try instead the sample set 20, 21, 23, 25, giving converted values 67, 68, 71, 75 and a sum of 281: truncating gives $281 \gg 2 = 70$, rounding gives $(281{+}2) \gg 2 = 70$. For a sum of 283 the two differ: $70$ versus $71$. The rounding fix shows up statistically, as the removal of a $-0.375$ bias, rather than dramatically on any one sample — which is precisely why it is easy to skip and worth not skipping.濾波器改成四捨五入:$(280 + 2) \gg 2 = 282 \gg 2 = 70$,沒變。改試樣本 20、21、23、25,轉換值為 67、68、71、75,總和 281:截斷得 $281 \gg 2 = 70$,捨入得 $(281{+}2) \gg 2 = 70$。總和為 283 時兩者才不同:$70$ 對 $71$。捨入這個修正是在統計上顯現的 —— 它移除了 $-0.375$ 的偏差 —— 而不是在某一筆樣本上戲劇性地現身;這正是它容易被略過、卻不該略過的原因。フィルタを四捨五入にすると:$(280 + 2) \gg 2 = 282 \gg 2 = 70$ で変わらない。代わりにサンプル 20、21、23、25 を試すと変換値は 67、68、71、75、総和は 281 となり、切り捨てでは $281 \gg 2 = 70$、丸めでは $(281{+}2) \gg 2 = 70$ である。総和が 283 なら $70$ と $71$ で差が出る。丸めの効果は $-0.375$ のバイアスが消えるという統計的な形で現れ、個々のサンプルで劇的に現れるわけではない。だからこそ省きやすく、しかし省くべきでない。
Problem: The specification is now: relative error of the multiplication by 1.8 must not exceed 0.2 %, inputs are 0 to 200 °C in 12-bit unsigned form, and the output must not overflow. Write the module. State the number of adders, the output width, and the worst-case absolute error in °F over the input range.問題:規格現在是:乘以 1.8 的相對誤差不得超過 0.2 %,輸入為 0 到 200 °C 的 12 位元無號數,而且輸出不可溢位。請寫出這個模組。說明加法器的個數、輸出寬度,以及在整個輸入範圍上以 °F 計的最壞絕對誤差。問題:仕様は次のとおりである。1.8 倍の相対誤差は 0.2 % を超えてはならない。入力は 0〜200 °C を 12 ビット符号なしで表す。出力はオーバフローしてはならない。このモジュールを書け。加算器の個数、出力幅、入力範囲全体での最悪の絶対誤差(°F)を示せ。
Take the expansion to five terms: $1 + \frac12 + \frac14 + \frac1{32} + \frac1{64} = \frac{115}{64} = 1.796875$. Relative error $= (1.8 - 1.796875)/1.8 = 0.174\,\%$, which meets the 0.2 % requirement with a little room.把展開取到五項:$1 + \frac12 + \frac14 + \frac1{32} + \frac1{64} = \frac{115}{64} = 1.796875$。相對誤差 $= (1.8 - 1.796875)/1.8 = 0.174\,\%$,符合 0.2 % 的要求,還留了一點餘裕。展開を 5 項まで取る:$1 + \frac12 + \frac14 + \frac1{32} + \frac1{64} = \frac{115}{64} = 1.796875$。相対誤差は $(1.8 - 1.796875)/1.8 = 0.174\,\%$ で、0.2 % の要求を少し余裕をもって満たす。
Output width: the largest output is $1.8 \times 200 + 32 = 392$, which needs 9 bits; keeping 12 bits for interface compatibility is comfortable, and because we never shift left there is no intermediate larger than the result. (Had we used the 2 − ¼ style, the intermediate temp_in << 1 would reach 400, still inside 12 bits for this range, but it is the intermediate you must check, not the result.)輸出寬度:最大的輸出是 $1.8 \times 200 + 32 = 392$,需要 9 位元;為了介面相容而保留 12 位元很寬裕,而且因為我們從不左移,不會有比結果更大的中間值。(如果改用 2 − ¼ 的寫法,中間值 temp_in << 1 會到 400,在這個範圍內仍落在 12 位元內,但你要檢查的是中間值,不是結果。)出力幅:最大の出力は $1.8 \times 200 + 32 = 392$ で 9 ビット必要である。インタフェース互換のため 12 ビットのままにしておけば十分余裕がある。左シフトを一切しないので、結果より大きい中間値も生じない。(2 − ¼ の形を使えば中間値 temp_in << 1 が 400 に達するが、この範囲ならまだ 12 ビットに収まる。とはいえ確認すべきは結果ではなく中間値である。)
module temp_converter_fine (temp_out, temp_in);
input [11:0] temp_in;
output [11:0] temp_out;
// 9/5 = 1.8 ~ 115/64 = 1 + 1/2 + 1/4 + 1/32 + 1/64 (error 0.174%)
wire [12:0] scaled = {1'b0, temp_in}
+ {2'b0, temp_in[11:1]}
+ {3'b0, temp_in[11:2]}
+ {6'b0, temp_in[11:5]}
+ {7'b0, temp_in[11:6]};
assign temp_out = scaled[11:0] + 12'd32;
endmodule
Four additions for the scaling plus one constant addition: five adders, of which the last is a cheap incrementer on bits 5 and up. Every addend is explicitly zero-extended to 13 bits so that no term is truncated by the expression width — do not rely on the context rule here, state it.縮放用四次加法,再加一次常數加法:五個加法器,其中最後一個是位元 5 以上的廉價加一電路。每個加數都明確地零擴展到 13 位元,這樣就不會有任何一項被運算式寬度截斷 —— 這裡不要依賴上下文規則,把它寫明。スケーリングに加算 4 回、さらに定数加算 1 回で、加算器は 5 個。うち最後の 1 個はビット 5 以上に対する安価なインクリメンタである。各加数は明示的に 13 ビットへゼロ拡張し、式の幅によってどの項も切り詰められないようにする。ここは文脈の規則に頼らず、明記すること。
Worst-case error. Systematic: $-0.003125 \times 200 = -0.625$ °F at the top of the range. Truncation: each of the four shifted terms loses up to just under 1, so up to $-4$ in the worst case… which is larger than the systematic error and would blow the budget if the specification were on absolute error. It is not — the specification is on the relative error of the multiplication — but this is exactly the moment to notice that per-term truncation now dominates, and to switch to Option B of §4.6: compute $115 \times \textit{temp\_in}$ at full width with no intermediate truncation and shift right by 6 once at the end, optionally adding 32 (half of 64) first to round. That version has a single truncation, worst-case error 0.5 LSB, and the same number of adders.最壞誤差。系統項:範圍上限處為 $-0.003125 \times 200 = -0.625$ °F。截斷項:四個移位項每個最多損失略小於 1,所以最壞情況可達 $-4$ …… 這比系統誤差還大,如果規格訂的是絕對誤差,預算就爆了。規格訂的不是絕對誤差 —— 它訂的是乘法的相對誤差 —— 但這正是該注意到「逐項截斷現在已經主導」的時刻,也該改用 §4.6 的選項 B:以全寬度算出 $115 \times \textit{temp\_in}$、中途完全不截斷,最後一次右移 6 位,想捨入的話先加 32(64 的一半)。那個版本只截斷一次,最壞誤差 0.5 LSB,而且加法器個數一樣。最悪誤差。系統項:範囲の上端で $-0.003125 \times 200 = -0.625$ °F。切り捨て項:シフトされた 4 項がそれぞれ最大で 1 弱を失うので、最悪 $-4$ に達しうる……これは系統誤差より大きく、仕様が絶対誤差で切られていれば予算を超える。ここでの仕様は絶対誤差ではなく乗算の相対誤差なので超えないが、項ごとの切り捨てがいまや支配的になったと気づくべき場面であり、§4.6 の選択肢 B へ移るべきところでもある。すなわち $115 \times \textit{temp\_in}$ を全幅で、途中の切り捨てなしに計算し、最後に 1 回だけ 6 ビット右シフトする。丸めたければ先に 32(64 の半分)を足す。この版は切り捨てが 1 回だけ、最悪誤差 0.5 LSB で、加算器の数は同じである。
Problem: A student changes all four assignments in temp_filter's pipeline from <= to =, leaving the order unchanged. Describe (a) the simulated behaviour of av_temp, (b) the netlist the synthesiser produces, (c) whether simulation and synthesis agree, and (d) a one-line change to the order that restores correct behaviour without restoring <=. Why is (d) still the wrong fix?問題:一位學生把 temp_filter 管線裡的四個指定全部從 <= 改成 =,順序不動。請描述 (a) av_temp 的模擬行為、(b) 合成器產生的 netlist、(c) 模擬與合成是否一致,以及 (d) 只改順序的一行修改,讓行為恢復正確而不必改回 <=。為什麼 (d) 仍然是錯的修法?問題:ある学生が temp_filter のパイプラインの 4 つの代入をすべて <= から = に変え、順序はそのままにした。(a) av_temp のシミュレーション上の振る舞い、(b) 合成器が生成する netlist、(c) シミュレーションと合成が一致するか、(d) <= に戻さずに順序だけを変えて正しい動作を取り戻す 1 行の修正、をそれぞれ述べよ。なぜ (d) はそれでも誤った直し方なのか。
(a) With blocking assignments executed top to bottom, temp[3] takes temp_data, then temp[2] takes the value just written to temp[3], and so on. After the edge all four registers hold temp_data. Therefore the sum is $4\,x$ and the shift undoes it exactly, so av_temp equals temp_data: the filter is a wire with one clock of delay and no smoothing whatsoever.(a) 阻塞指定由上往下執行,temp[3] 取得 temp_data,接著 temp[2] 取得剛剛寫進 temp[3] 的值,依此類推。邊緣過後四個暫存器都存著 temp_data。因此總和是 $4\,x$,而移位剛好把它抵銷,所以 av_temp 等於 temp_data:這個濾波器變成一條延遲一個時脈的線,完全沒有平滑作用。(a) ブロッキング代入は上から下へ実行されるので、temp[3] が temp_data を取り、次に temp[2] がいま temp[3] に書かれた値を取り、以下同様になる。エッジの後、4 つのレジスタはすべて temp_data を保持する。したがって総和は $4\,x$ となり、シフトがそれをちょうど打ち消すので av_temp は temp_data に等しい。フィルタは 1 クロック遅延の配線にすぎず、平滑化はまったく起こらない。
(b) Synthesis follows the same semantics. It sees that temp[2], temp[1] and temp[0] are all assigned the same expression as temp[3], merges them, and then notices that the adder is computing $4x$ which the shift immediately undoes. The netlist collapses to twelve flip-flops and nothing else — 36 of the 48 flops and all three adders disappear. If you ever see a block come back from synthesis dramatically smaller than you expected, suspect exactly this.(b) 合成遵循同樣的語意。它看到 temp[2]、temp[1] 與 temp[0] 都被指定成與 temp[3] 相同的運算式,於是把它們合併,接著注意到加法器算的是 $4x$ 而移位馬上把它抵銷。netlist 塌成十二個正反器,別無其他 —— 48 個正反器裡的 36 個與三個加法器全部消失。哪天你看到某個區塊合成回來比預期小得離譜,就該懷疑正是這件事。(b) 合成も同じ意味論に従う。temp[2]、temp[1]、temp[0] が temp[3] と同じ式を代入されていることを見てこれらを統合し、さらに加算器が $4x$ を計算してシフトがそれを即座に打ち消していることに気づく。netlist はフリップフロップ 12 個だけに縮み、48 個のうち 36 個と加算器 3 個すべてが消える。合成から戻ってきたブロックが予想よりはるかに小さいときは、まさにこれを疑うべきである。
(c) Yes, they agree — and that is the trap. This is not a simulation-synthesis mismatch; it is a design that is wrong in both. Mismatches get caught by gate-level simulation; a design that is consistently wrong does not.(c) 一致 —— 而這正是陷阱。這不是模擬/合成不一致,而是一個在兩邊都錯的設計。不一致可以被閘級模擬抓到;一個前後一致地錯著的設計則抓不到。(c) 一致する。そしてそこが落とし穴である。これはシミュレーションと合成の不一致ではなく、両方で同じように誤っている設計である。不一致ならゲートレベルシミュレーションで捕まるが、一貫して誤っている設計は捕まらない。
(d) Reverse the order: temp[0] = temp[1]; temp[1] = temp[2]; temp[2] = temp[3]; temp[3] = temp_data;. Each stage is now read before it is overwritten, so the shift works.(d) 把順序顛倒:temp[0] = temp[1]; temp[1] = temp[2]; temp[2] = temp[3]; temp[3] = temp_data;。現在每一級都在被覆寫之前先被讀取,移位就正常了。(d) 順序を逆にする:temp[0] = temp[1]; temp[1] = temp[2]; temp[2] = temp[3]; temp[3] = temp_data;。これで各段は上書きされる前に読まれるので、シフトは正しく動く。
It is still the wrong fix because the correctness now depends on the textual order of four statements inside a block. Any future edit that reorders them, or inserts a fifth stage in the middle, silently breaks the design, and nothing in the code announces the dependency. The nonblocking version is order-independent and therefore edit-proof. "Correct but fragile" is a category of code to be avoided, not celebrated.它仍然是錯的修法,因為正確性現在取決於區塊裡四個敘述的文字順序。日後任何一次把它們重排、或在中間插入第五級的修改,都會悄悄打破這個設計,而程式碼裡沒有任何東西宣告這個依賴。非阻塞的版本與順序無關,因此禁得起編輯。「正確但脆弱」是一類應該避免、而不是拿來誇耀的程式碼。それでも誤った直し方である。正しさがブロック内の 4 文の字面上の順序に依存するようになるからである。将来それらを並べ替えたり、途中に 5 段目を挿入したりする編集は、黙ってこの設計を壊す。しかもコードにはその依存を知らせるものが何もない。ノンブロッキング版は順序に依存せず、したがって編集に強い。「正しいが脆い」は称えるものではなく避けるべきコードの一類である。
Problem: FIFO 1 is empty and FIFO 2 is full. (a) Give data_out in hexadecimal for read = 1, select = 2'b00. (b) What are read_1, read_2 and write in that state? (c) Identify every signal in the controller at risk of inferring a latch and give the minimal code change that removes the risk. (d) Explain what happens to incoming filter samples during a long burst of reads, and why that defeats the purpose of the FIFO.問題:FIFO 1 為空、FIFO 2 已滿。(a) 請以十六進位給出 read = 1, select = 2'b00 時的 data_out。(b) 那個狀態下 read_1、read_2 與 write 各是多少?(c) 指出控制器裡每一個有推論出閂鎖之虞的訊號,並給出消除風險的最小修改。(d) 說明在一長串連續讀取期間,進來的濾波樣本會怎麼樣,以及為什麼那會抵銷 FIFO 的意義。問題:FIFO 1 は空、FIFO 2 は満杯である。(a) read = 1, select = 2'b00 のときの data_out を 16 進で示せ。(b) そのときの read_1、read_2、write はいくつか。(c) コントローラ内でラッチが推論される恐れのある信号をすべて挙げ、その危険を除く最小の修正を示せ。(d) 長い読み出しバーストのあいだ、到着するフィルタ出力のサンプルがどうなるか、そしてなぜそれが FIFO の意味を失わせるかを説明せよ。
(a) {empty_1, empty_2, full_1, full_2} = {1, 0, 0, 1} = 4'b1001, zero-extended to twelve bits gives 12'b0000_0000_1001 = 12'h009.(a) {empty_1, empty_2, full_1, full_2} = {1, 0, 0, 1} = 4'b1001,零擴展成十二位元就是 12'b0000_0000_1001 = 12'h009。(a) {empty_1, empty_2, full_1, full_2} = {1, 0, 0, 1} = 4'b1001 であり、12 ビットへゼロ拡張すると 12'b0000_0000_1001 = 12'h009 となる。
(b) read_1 = 0 and read_2 = 0 — the 2'b00 case arm assigns neither, so both keep the zeros assigned at the top of the block, which is the correct behaviour for a status read: you are inspecting the flags, not consuming data. write = 0, because read == 1.(b) read_1 = 0、read_2 = 0 —— 2'b00 那個 case 分支兩者都沒指定,所以都保持區塊開頭指定的零,而這對狀態讀取來說正是正確行為:你在檢查旗標,不是在消耗資料。write = 0,因為 read == 1。(b) read_1 = 0、read_2 = 0 である。2'b00 の case 分岐はどちらにも代入しないので、ブロック冒頭で与えられたゼロのままになる。ステータス読み出しとしてはこれが正しい。フラグを調べているのであって、データを消費しているのではないからである。read == 1 なので write = 0 である。
(c) Only write is genuinely at risk. read_1 and read_2 are defaulted at the top of the block so every path assigns them. data_out is assigned on every path including the case default and the final else. write is assigned on the read==1 path and on the read==0 path but not on the final else. Minimal fix: add write = 0; next to read_1 = 0; read_2 = 0; at the top, and delete the now-redundant assignment inside the read==1 branch if you like. As a bonus this also removes the need for the else if (read == 0) to be an else if at all — it can become a plain else, and the unreachable third branch disappears.(c) 真正有風險的只有 write。read_1 與 read_2 在區塊開頭有預設值,所以每條路徑都指定到它們。data_out 在每一條路徑上(含 case 的 default 與最後那個 else)都有指定。write 在 read==1 與 read==0 兩條路徑上有指定,但在最後那個 else 上沒有。最小修法:在最上面的 read_1 = 0; read_2 = 0; 旁邊加上 write = 0;,並且若你願意,把 read==1 分支裡現在多餘的那次指定刪掉。順帶的好處是,這也讓 else if (read == 0) 不必再是 else if —— 它可以變成單純的 else,那個到不了的第三分支就消失了。(c) 本当に危険なのは write だけである。read_1 と read_2 はブロック冒頭で既定値が与えられるので、すべての経路で代入される。data_out は case の default と最後の else を含め、すべての経路で代入される。write は read==1 と read==0 の経路では代入されるが、最後の else では代入されない。最小の修正は、冒頭の read_1 = 0; read_2 = 0; の隣に write = 0; を足すことであり、望むなら read==1 の分岐内で冗長になった代入を消してもよい。おまけとして、else if (read == 0) を else if にしておく必要もなくなる。素の else にでき、到達不能な第 3 分岐は消える。
(d) During a read burst write is held at 0, so neither FIFO stores anything. The filters keep producing a new average every clock and every one of them is discarded. A FIFO exists precisely so that the producer can keep producing while the consumer is busy; wiring write = !read inverts that guarantee and makes the system lose data exactly when it is under load. The correct connection is write_i = sample_valid_i && !stack_full_i, derived from the sensor's data-rate strobe and completely independent of read. The FIFO's own full flag then provides the only legitimate reason to drop a sample, and that reason can be reported.(d) 讀取期間 write 一直是 0,所以兩個 FIFO 都不存任何東西。濾波器每個時脈仍持續產出一筆新的平均值,而每一筆都被丟掉。FIFO 存在的目的,正是讓生產者在消費者忙碌時還能繼續生產;把 write = !read 接上去,等於把這個保證反轉,讓系統恰好在負載最重時遺失資料。正確的接法是 write_i = sample_valid_i && !stack_full_i,由感測器的資料率脈衝推導而來,與 read 完全無關。這樣一來,FIFO 自己的滿旗標就成為丟棄樣本的唯一正當理由,而那個理由還可以被回報出來。(d) 読み出しのあいだ write は 0 に保たれるので、どちらの FIFO も何も格納しない。フィルタは毎クロック新しい平均を出し続け、そのすべてが捨てられる。FIFO が存在するのは、消費者が忙しいあいだも生産者が生産を続けられるようにするためである。write = !read と配線することはその保証を反転させ、負荷が高いときにこそデータを失うシステムにしてしまう。正しい接続は write_i = sample_valid_i && !stack_full_i であり、センサのデータレートのストローブから導かれ、read とはまったく無関係である。こうすれば、サンプルを捨てる唯一の正当な理由は FIFO 自身の満杯フラグになり、その理由は報告することもできる。
The Am2901 was introduced by Advanced Micro Devices in 1975 and is the most successful bit-slice component ever made. The idea behind a bit slice is simple and, in an era before you could put a whole processor on one die, decisive: build a 4-bit-wide vertical cross-section of a processor's datapath — register file, ALU, shifters, flag logic — and bring out, as pins, every signal that would have crossed the cut. Four of those chips side by side, with the cut signals wired together, make a 16-bit datapath. Eight make a 32-bit one. The architecture is set by the system designer, not by the chip vendor.Am2901 由 Advanced Micro Devices 在 1975 年推出,是有史以來最成功的bit-slice元件。bit slice 背後的想法很簡單,而且在一顆晶粒放不下整顆處理器的年代具有決定性:做出處理器資料路徑的一個 4 位元寬垂直剖面 —— 暫存器檔、ALU、移位器、旗標邏輯 —— 並把每一條會穿過這個切面的訊號都拉成腳位。四顆這樣的晶片並排,把切面訊號接起來,就是一條 16 位元資料路徑。八顆就是 32 位元。架構由系統設計者決定,而不是晶片廠商。Am2901 は 1975 年に Advanced Micro Devices が発表した、史上最も成功したビットスライス部品である。ビットスライスの発想は単純で、プロセッサ全体を 1 つのダイに載せられなかった時代には決定的だった。プロセッサのデータパスの 4 ビット幅の縦断面——レジスタファイル、ALU、シフタ、フラグ論理——を作り、その切断面を横切る信号をすべてピンとして出すのである。そのチップを 4 個並べ、切断面の信号を互いに配線すれば 16 ビットのデータパスになる。8 個なら 32 ビットである。アーキテクチャはチップベンダではなくシステム設計者が決める。
The signals that cross the cut are exactly the ones you would expect: the carry (c_in into the bottom slice, c_out out of the top), the shift chain of the register file (ram0 at the bottom, ram3 at the top), the shift chain of the Q register (qs0, qs3), and the group carry-generate and carry-propagate signals g_bar and p_bar, which feed a companion look-ahead carry generator so that the carry does not have to ripple through every slice in sequence. Understanding this is understanding why the module's port list looks the way it does: almost every "odd" port exists because the design is a slice of something bigger.穿過切面的訊號正是你會預期的那些:進位(進入最低位 slice 的 c_in、從最高位出來的 c_out)、暫存器檔的移位鏈(底部的 ram0、頂部的 ram3)、Q 暫存器的移位鏈(qs0、qs3),以及群組進位產生與進位傳遞訊號 g_bar 與 p_bar,它們餵給一顆配套的進位預看產生器,讓進位不必依序漣波穿過每一片 slice。看懂這點,就看懂了這個模組的埠列為什麼長成那樣:幾乎每一個「奇怪」的埠,都是因為這個設計是更大東西的一片切片。切断面を横切る信号は、まさに予想どおりのものである。キャリー(最下位スライスへ入る c_in、最上位から出る c_out)、レジスタファイルのシフト連鎖(下端の ram0、上端の ram3)、Q レジスタのシフト連鎖(qs0、qs3)、そしてグループのキャリー生成・伝搬信号 g_bar と p_bar である。後者は付属のキャリー先見生成器へ入り、キャリーが各スライスを順に伝わらずに済むようにする。これが分かれば、このモジュールのポートリストがなぜこの形なのかも分かる。「奇妙」に見えるポートのほとんどは、この設計がより大きなものの一切片だからこそ存在している。
The second idea is microprogramming. The 2901 has no instruction decoder and no program counter. It is a datapath with a 9-bit control word, and something else must supply a fresh control word every cycle. That something is a microcode ROM addressed by a microprogram sequencer — in the AMD family, an Am2910 — and the "instruction set" of the resulting machine is whatever the microcode author writes. This is why the 2901 is such a good teaching example: it separates the datapath, which you are about to read in Verilog, from the control, which is a table of bits in a ROM, as cleanly as any real design ever has.第二個想法是微程式化。2901 沒有指令解碼器,也沒有程式計數器。它是一條帶 9 位元控制字的資料路徑,而每個週期都得由別的東西供應一個新的控制字。那個別的東西,就是由微程式定序器定址的微碼 ROM —— 在 AMD 家族裡就是 Am2910 —— 而由此得到的機器的「指令集」,就是微碼作者寫出來的任何東西。這正是為什麼 2901 是這麼好的教學範例:它把資料路徑(你馬上要用 Verilog 讀的東西)與控制(ROM 裡一張位元表)分得乾乾淨淨,乾淨程度不輸任何真實設計。第二の発想はマイクロプログラミングである。2901 には命令デコーダもプログラムカウンタもない。9 ビットの制御語を持つデータパスであり、毎サイクル新しい制御語を供給するものが別に必要である。それがマイクロプログラムシーケンサ——AMD ファミリなら Am2910——でアドレスされるマイクロコード ROM であり、そうして得られる機械の「命令セット」とは、マイクロコードの書き手が書いたものに他ならない。2901 が優れた教材である理由はここにある。これから Verilog で読むデータパスと、ROM の中のビット表である制御とを、実在のどの設計にも劣らないほどきれいに分離しているからである。
The temperature monitor is a pipeline: data flows one way and the control is trivial. The 2901 is the other canonical shape — a loop. Values leave the register file, pass through a selector, an ALU and a shifter, and come back to the register file, all within one clock period, with multiplexers at every junction steered by a control word. Every processor datapath, every DSP MAC unit and every configurable accelerator you will ever design is a version of this picture. Learn to read this one and you can read all of them.溫度監測系統是一條管線:資料單向流動,控制微不足道。2901 則是另一種典型形狀 —— 一個迴路。值離開暫存器檔,經過選擇器、ALU 與移位器,再回到暫存器檔,而這一切都在一個時脈週期內完成,每個交會點都有多工器,由一個控制字操控。你這輩子會設計的每一條處理器資料路徑、每一個 DSP 的 MAC 單元、每一個可組態加速器,都是這張圖的某個版本。學會讀這一個,你就能讀懂全部。温度モニタはパイプラインであり、データは一方向に流れ、制御は些細である。2901 はもう一つの典型的な形、すなわちループである。値はレジスタファイルを出て、セレクタ、ALU、シフタを通り、レジスタファイルへ戻る。それがすべて 1 クロック周期内で起こり、各合流点のマルチプレクサは制御語で操られる。これから設計するであろうすべてのプロセッサデータパス、すべての DSP の MAC ユニット、すべての再構成可能アクセラレータは、この絵の変種である。この一つを読めるようになれば、すべてが読める。
Follow one cycle around that loop and the whole architecture becomes concrete.跟著那個迴路走一圈,整個架構就具體了。そのループを 1 周たどれば、アーキテクチャ全体が具体的になる。
a and b present two words from the register file on the combinational read ports ad and bd. These reads are asynchronous — the moment a changes, ad changes — so the register file behaves like two 16-to-1 multiplexers, not like a memory with a read cycle.位址欄位 a 與 b 把暫存器檔裡的兩個字送上組合式讀取埠 ad 與 bd。這些讀取是非同步的 —— a 一變,ad 就變 —— 所以暫存器檔的行為像兩個 16 對 1 的多工器,而不像有讀取週期的記憶體。アドレスフィールド a と b が、レジスタファイルの 2 ワードを組合せ読み出しポート ad、bd に提示する。この読み出しは非同期であり——a が変わった瞬間に ad が変わる——レジスタファイルは読み出しサイクルを持つメモリではなく、16 対 1 のマルチプレクサ 2 個のように振る舞う。i[2:0], chooses the ALU's two inputs r and s from a menu of five sources: ad, bd, the Q register, the external data input d, and the constant zero.運算元選擇器由 i[2:0] 操控,從五個來源的選單裡挑出 ALU 的兩個輸入 r 與 s:ad、bd、Q 暫存器、外部資料輸入 d,以及常數零。オペランドセレクタは i[2:0] で制御され、5 つの供給源——ad、bd、Q レジスタ、外部データ入力 d、定数ゼロ——から ALU の 2 入力 r と s を選ぶ。i[5:3], computes one of eight functions of r, s and c_in, and emits alu_out together with c_out, ovr, g_bar and p_bar.ALU 由 i[5:3] 操控,計算 r、s 與 c_in 的八種函數之一,並輸出 alu_out 以及 c_out、ovr、g_bar 與 p_bar。ALU は i[5:3] で制御され、r、s、c_in の 8 種の関数のいずれかを計算して、alu_out とともに c_out、ovr、g_bar、p_bar を出す。i[8:6] decides three independent things at once: whether the register file is written (and whether what is written is alu_out, alu_out shifted up, or alu_out shifted down), whether the Q register is loaded (and whether shifted), and whether the output y shows alu_out or the register-file word ad.目的地欄位 i[8:6] 一次決定三件互相獨立的事:暫存器檔要不要寫入(以及寫進去的是 alu_out、上移後的 alu_out、還是下移後的 alu_out)、Q 暫存器要不要載入(以及要不要先移位),還有輸出 y 顯示的是 alu_out 還是暫存器檔的字 ad。デスティネーションフィールド i[8:6] は、互いに独立な三つのことを同時に決める。レジスタファイルに書き込むか(書くのは alu_out か、上方向にシフトした alu_out か、下方向にシフトした alu_out か)、Q レジスタにロードするか(シフトするか)、そして出力 y が alu_out を見せるかレジスタファイルのワード ad を見せるか、である。Everything in steps 1 to 4 is combinational. The only state in the whole slice is the 16-word register file and the 4-bit Q register — 68 flip-flops. The entire cycle time of a machine built from these slices is the delay of that combinational loop, and §7.4 walks it.步驟 1 到 4 裡的一切都是組合邏輯。整片 slice 裡唯一的狀態就是那個 16 字的暫存器檔與 4 位元的 Q 暫存器 —— 68 個正反器。用這些 slice 組成的機器,整個週期時間就是那個組合迴路的延遲,而 §7.4 會把它走一遍。手順 1 から 4 まではすべて組合せである。このスライス全体で状態を持つのは 16 ワードのレジスタファイルと 4 ビットの Q レジスタだけ、合計 68 個のフリップフロップである。これらのスライスで組む機械のサイクルタイムは、その組合せループの遅延そのものであり、§7.4 でそれをたどる。
Three fields of three bits, eight codes each, giving $8 \times 8 \times 8 = 512$ microinstructions — every combination legal, which is a deliberate property of an orthogonal encoding. Together with the two 4-bit addresses and the carry-in, a complete 2901 control word is 9 + 4 + 4 + 1 = 18 bits wide, and that is the width of one slice of microcode ROM.三個欄位、各三位元、各八種碼,給出 $8 \times 8 \times 8 = 512$ 個微指令 —— 每一種組合都合法,這是正交編碼刻意具備的性質。再加上兩個 4 位元位址與進位輸入,一個完整的 2901 控制字寬 9 + 4 + 4 + 1 = 18 位元,那也就是一片微碼 ROM 的寬度。3 ビットのフィールドが三つ、各 8 コードで $8 \times 8 \times 8 = 512$ 通りのマイクロ命令になる。どの組み合わせも合法であり、これは直交符号化が意図して備える性質である。これに 4 ビットのアドレス 2 本とキャリー入力を加えると、完全な 2901 の制御語は 9 + 4 + 4 + 1 = 18 ビット幅となり、それがマイクロコード ROM 1 面の幅になる。
It is traditional to write 2901 instructions as three octal digits, most significant field first, because each 3-bit field is exactly one octal digit. "3 0 1" means destination 3 (ramf), function 0 (add), source 1 (ab). Get comfortable with that notation; examination questions use it.2901 的指令傳統上寫成三個八進位數字,最高位欄位在前,因為每個 3 位元欄位剛好是一個八進位數字。「3 0 1」的意思是目的地 3(ramf)、功能 0(add)、來源 1(ab)。請習慣這個記法;考題會用。2901 の命令は伝統的に 8 進 3 桁で、上位フィールドから順に書く。各 3 ビットフィールドがちょうど 8 進 1 桁だからである。「3 0 1」はデスティネーション 3(ramf)、機能 0(add)、ソース 1(ab)を意味する。この記法に慣れておきたい。試験問題はこれを使う。
Notice what the orthogonality buys and what it costs. It buys a decoder of trivial size — each field is decoded independently, with no cross-product — and complete freedom for the microcode author. It costs encoding efficiency: many of the 512 combinations are useless (adding two registers and then throwing the result away is legal and pointless), and the field widths are fixed even where fewer codes would do. For a microcode ROM whose width you control, the trade is worth it. For a user-visible instruction set with a fixed 32-bit word, it usually is not, which is why real ISAs are full of irregular encodings.注意正交性買到什麼、又付出什麼。它買到一個小到微不足道的解碼器 —— 每個欄位各自獨立解碼,沒有交叉乘積 —— 以及微碼作者的完全自由。它付出的是編碼效率:512 種組合裡很多是沒用的(把兩個暫存器相加再把結果丟掉是合法而無意義的),而且就算某欄位用不了那麼多碼,寬度仍然固定。對一個寬度由你決定的微碼 ROM 來說,這筆交易划算。對一個使用者可見、字長固定為 32 位元的指令集來說,通常就不划算了,這也是為什麼真實的 ISA 充滿不規則編碼。直交性が何を買い、何を支払うかに注意したい。買えるのは、ほとんど無視できる大きさのデコーダ——各フィールドを独立にデコードでき、交差項がない——と、マイクロコードの書き手にとっての完全な自由である。支払うのは符号化効率で、512 通りの多くは役に立たない(レジスタ 2 本を足して結果を捨てる命令は合法だが無意味である)し、より少ないコードで足りる場所でもフィールド幅は固定である。幅を自分で決められるマイクロコード ROM なら、この取引は割に合う。語長 32 ビット固定の、ユーザから見える命令セットなら普通は割に合わない。実在の ISA が不規則な符号化だらけなのはそのためである。
| Mnemonic助記符ニーモニック | Octal八進位8 進 | i[2:0] | r (ALU left)r(ALU 左)r(ALU 左) | s (ALU right)s(ALU 右)s(ALU 右) | Typical use典型用途典型的な用途 |
|---|---|---|---|---|---|
aq | 0 | 000 | ad | q | Combine a register with the accumulator-like Q把一個暫存器與類似累加器的 Q 結合レジスタと、アキュムレータ的な Q を組み合わせる |
ab | 1 | 001 | ad | bd | Register-to-register arithmetic, result to b暫存器對暫存器的算術,結果寫回 bレジスタ間の算術、結果はアドレス b へ |
zq | 2 | 010 | 0 | q | Pass or negate Q; add Q to the carry傳遞或取負 Q;或把 Q 加上進位Q をそのまま通す/符号反転する、あるいは Q にキャリーを足す |
zb | 3 | 011 | 0 | bd | Pass, increment or shift register b傳遞、加一或移位暫存器 bレジスタ b をそのまま通す、+1 する、シフトする |
za | 4 | 100 | 0 | ad | Pass or increment register a, write to b (a move)傳遞或對暫存器 a 加一,寫入 b(等於搬移)レジスタ a をそのまま通す/+1 して b へ書く(すなわち移動) |
da | 5 | 101 | d | ad | Combine external data with a register把外部資料與一個暫存器結合外部データとレジスタを組み合わせる |
dq | 6 | 110 | d | q | Combine external data with Q把外部資料與 Q 結合外部データと Q を組み合わせる |
dz | 7 | 111 | d | 0 | Load an immediate: ALU output is just d載入立即值:ALU 的輸出就是 d即値のロード:ALU の出力はそのまま d である |
Two readings help here. First, r only ever takes three values — ad, zero, or d — so the r multiplexer is 3-to-1, while s takes four — q, bd, ad, zero — so its multiplexer is 4-to-1. The eight source codes are not eight independent selections; they are eight of the twelve possible pairs, chosen because they are the useful ones. Second, the mnemonics are mnemonic: the first letter names the r source and the second names the s source, with z meaning zero. Once you see that, the table does not need memorising. (The slide's table writes the zero as the letter "o" in the operand columns; it is a zero.)這裡有兩種讀法有幫助。第一,r 只會取三種值 —— ad、零或 d —— 所以 r 的多工器是 3 對 1;而 s 取四種 —— q、bd、ad、零 —— 所以它的多工器是 4 對 1。那八個來源碼不是八次獨立的選擇;它們是十二種可能配對裡的八種,挑出來是因為它們有用。第二,助記符真的是「助記」的:第一個字母是 r 的來源,第二個字母是 s 的來源,z 代表零。看懂這點之後,這張表就不必背了。(投影片的表在運算元欄裡把零寫成字母「o」;那是零。)ここでは二通りの読み方が助けになる。第一に、r が取る値は ad、ゼロ、d の 3 つだけなので r 側のマルチプレクサは 3 対 1 であり、s は q、bd、ad、ゼロの 4 つを取るので 4 対 1 である。8 つのソースコードは 8 回の独立な選択ではなく、12 通りの組のうち有用な 8 通りを選んだものである。第二に、ニーモニックは文字どおり記憶の助けになっている。1 文字目が r の供給源、2 文字目が s の供給源を表し、z はゼロを意味する。そこに気づけば、この表を暗記する必要はない。(スライドの表はオペランド欄でゼロを文字「o」と書いているが、あれはゼロである。)
| Mnemonic (table)助記符(表)ニーモニック(表) | `define name`define 名稱`define の名前 | Octal八進位8 進 | i[5:3] | Operation運算演算 | Notes備註備考 |
|---|---|---|---|---|---|
add | `add | 0 | 000 | $r + s + c_{in}$ | The only function that uses the carry in唯一會用到進位輸入的功能キャリー入力を使う唯一の機能 |
subr | `subr | 1 | 001 | $s - r$ | Table says s−r; the code computes r−s. See §6.3表上寫 s−r;程式碼算的是 r−s。見 §6.3表は s−r と書くが、コードは r−s を計算する。§6.3 を参照 |
subs | `subs | 2 | 010 | $r - s$ | Table says r−s; the code computes s−r. See §6.3表上寫 r−s;程式碼算的是 s−r。見 §6.3表は r−s と書くが、コードは s−r を計算する。§6.3 を参照 |
or | `orrs | 3 | 011 | $r \mathbin{|} s$ | Bitwise. Renamed because or is a Verilog keyword逐位元。改名是因為 or 是 Verilog 的關鍵字ビット単位。or が Verilog の予約語なので改名されている |
and | `andrs | 4 | 100 | $r \mathbin{\&} s$ | Bitwise. Renamed because and is a Verilog keyword逐位元。改名是因為 and 是 Verilog 的關鍵字ビット単位。and が Verilog の予約語なので改名されている |
notrs | `notrs | 5 | 101 | $\bar r \mathbin{\&} s$ | "Bit clear": clears the bits of s selected by r「位元清除」:把 s 中被 r 選到的位元清掉「ビットクリア」:r が選んだ s のビットを落とす |
exor | `exor | 6 | 110 | $r \oplus s$ | Bitwise逐位元ビット単位 |
exnor | `exnor | 7 | 111 | $\overline{r \oplus s}$ | Bitwise; equality test when the result is all ones逐位元;結果全為 1 時就是相等測試ビット単位。結果が全 1 なら等値判定になる |
The right-hand column of slide 20 writes the or operation as r || s and the and operation as r && s. Those are the logical operators: they reduce each operand to a single truth value and return a one-bit result, so 4'b0011 && 4'b0100 is 1'b1, not 4'b0000. The ALU needs the bitwise operators | and &, and the actual module on slide 25 uses them correctly. The distinction between logical and bitwise operators is Lecture 3 material and is one of the most reliable sources of marks lost in an examination; the mnemonic that works is "logical operators always return one bit".投影片 20 的右欄把 or 運算寫成 r || s,把 and 運算寫成 r && s。那些是邏輯運算子:它們把每個運算元化約成單一真假值,傳回一位元的結果,所以 4'b0011 && 4'b0100 是 1'b1,不是 4'b0000。ALU 要的是逐位元運算子 | 與 &,而投影片 25 上真正的模組用對了。邏輯運算子與逐位元運算子的區別是第 3 講的內容,也是考試失分最穩定的來源之一;好用的口訣是「邏輯運算子永遠只傳回一位元」。スライド 20 の右の欄は、or の演算を r || s、and の演算を r && s と書いている。これらは論理演算子であり、各オペランドを単一の真理値に縮約して 1 ビットの結果を返す。したがって 4'b0011 && 4'b0100 は 4'b0000 ではなく 1'b1 である。ALU が必要とするのはビット単位の演算子 | と & であり、スライド 25 の実際のモジュールは正しくそれを使っている。論理演算子とビット演算子の区別は第 3 講の内容で、試験で点を落とす最も安定した原因の一つでもある。覚え方は「論理演算子は常に 1 ビットを返す」である。
This is the densest table in the lecture and the one most worth slowing down over, because it controls three separate pieces of hardware simultaneously.這是本講最密的一張表,也是最值得放慢速度看的一張,因為它同時控制三塊各自獨立的硬體。本講で最も情報密度の高い表であり、最も速度を落として読む価値のある表でもある。三つの独立したハードウェアを同時に制御しているからである。
| Mnemonic助記符ニーモニック | Oct八進8 進 | Output y輸出 y出力 y | Register file: shift暫存器檔:移位レジスタファイル:シフト | Register file: value written to address b暫存器檔:寫入位址 b 的值レジスタファイル:アドレス b へ書く値 | Q: shiftQ:移位Q:シフト | Q: value loadedQ:載入的值Q:ロードする値 |
|---|---|---|---|---|---|---|
qreg | 0 | alu_out | — | nothing written不寫入書き込まない | none無なし | alu_out |
nop | 1 | alu_out | — | nothing written不寫入書き込まない | — | nothing loaded不載入ロードしない |
rama | 2 | reg_f[a] | none無なし | alu_out | — | nothing loaded不載入ロードしない |
ramf | 3 | alu_out | none無なし | alu_out | — | nothing loaded不載入ロードしない |
ramqd | 4 | alu_out | down下移下へ | alu_out >> 1 | down下移下へ | Q >> 1 |
ramd | 5 | alu_out | down下移下へ | alu_out >> 1 | — | nothing loaded不載入ロードしない |
ramqu | 6 | alu_out | up上移上へ | alu_out << 1 | up上移上へ | Q << 1 |
ramu | 7 | alu_out | up上移上へ | alu_out << 1 | — | nothing loaded不載入ロードしない |
Four facts to extract from it, each of which is a potential examination question on its own.從這張表要抽出四個事實,每一個本身都可能是考題。この表から取り出すべき事実が四つある。どれも単独で試験問題になりうる。
b, never at a. Address a is read-only. A register-to-register operation therefore always overwrites its second operand, and a move from a to b is za + add + rama-or-ramf.暫存器檔永遠寫在位址 b,絕不寫在 a。位址 a 是唯讀的。因此暫存器對暫存器的運算一定會覆寫它的第二個運算元,而「把 a 搬到 b」就是 za + add + rama 或 ramf。レジスタファイルへの書き込みは常にアドレス b であり、a へは決して書かない。アドレス a は読み出し専用である。したがってレジスタ間演算は必ず第 2 オペランドを上書きし、a から b への移動は za + add + rama または ramf になる。rama sends something other than alu_out to y. On rama the output shows reg_f[a], the register-file word at address a, while the ALU result goes quietly into address b. That is the "write and simultaneously read something else" instruction, and it exists so that you can move data out of the machine without spending a cycle.只有 rama 會把 alu_out 以外的東西送到 y。在 rama 時輸出顯示的是 reg_f[a],也就是位址 a 的暫存器檔字,而 ALU 的結果則靜靜地寫進位址 b。那是「一邊寫入、一邊讀出另一個東西」的指令,它存在的目的是讓你不必多花一個週期就能把資料搬出機器。alu_out 以外のものを y へ送るのは rama だけである。rama では出力にアドレス a のレジスタファイルのワード reg_f[a] が現れ、ALU の結果は静かにアドレス b へ入る。これは「書き込みつつ別のものを読み出す」命令であり、1 サイクルを費やさずにデータを機械の外へ出せるようにするために存在する。ram3 pin and its bit 0 leaves on the ram0 pin; on an up-shift the directions reverse. The same for Q with qs3 and qs0. That is what makes a multi-slice shift possible — and, as §7.1 shows, it is also what makes the double-length shift of a multiply possible within a single slice.端點位元來自腳位,也送往腳位。下移時,暫存器檔的位元 3 由 ram3 腳位填入,位元 0 則由 ram0 腳位離開;上移時方向反轉。Q 與 qs3、qs0 也一樣。這就是多片 slice 聯合移位之所以可能的原因 —— 而且如 §7.1 所示,這也是單片 slice 內能做乘法所需雙倍長度移位的原因。端のビットはピンから来て、ピンへ出ていく。下方向シフトではレジスタファイルのビット 3 が ram3 ピンから埋められ、ビット 0 は ram0 ピンから出ていく。上方向シフトでは向きが逆になる。Q も qs3、qs0 で同様である。これが複数スライスにまたがるシフトを可能にしており、§7.1 で見るように、1 スライス内で乗算に必要な倍長シフトを可能にしているのも同じ仕組みである。Slide 21 has four extra columns headed "reg_file bit shifts" and "Q register bit shifts", each split into "bit 3" and "bit 0". They name the source of each end bit after the shift. For ramqd on the register file: bit 3 ← ram3 (from the slice above), bit 0 ← d[1], meaning bit 1 of the word being shifted. For ramqu: bit 3 ← d[2], bit 0 ← ram0 (from the slice below). Read them that way and they agree exactly with the concatenations in the Verilog of §6.1. Where the table and the code genuinely disagree is over the Q register's source word, discussed in §6.6.投影片 21 多了四欄,標題是「reg_file bit shifts」與「Q register bit shifts」,各自再分成「bit 3」與「bit 0」。它們指的是移位之後每個端點位元的來源。暫存器檔在 ramqd 時:位元 3 ← ram3(來自上一片 slice),位元 0 ← d[1],也就是被移位那個字的位元 1。ramqu 時:位元 3 ← d[2],位元 0 ← ram0(來自下一片 slice)。這樣讀,它們就與 §6.1 的 Verilog 裡那些串接完全吻合。表與程式碼真正不一致的地方,是 Q 暫存器的來源字,那在 §6.6 討論。スライド 21 には「reg_file bit shifts」「Q register bit shifts」と題する欄が 4 つ増えており、それぞれ「bit 3」と「bit 0」に分かれている。これらはシフト後の各端ビットの供給元を示す。レジスタファイルの ramqd では、ビット 3 ← ram3(上位スライスから)、ビット 0 ← d[1]、すなわちシフトされるワードのビット 1 である。ramqu では、ビット 3 ← d[2]、ビット 0 ← ram0(下位スライスから)となる。こう読めば §6.1 の Verilog の連結と正確に一致する。表とコードが本当に食い違うのは Q レジスタの供給元のワードであり、それは §6.6 で扱う。
// amd2901 bit slice microprocessor // operands: instruction[2:0] `define aq 3'b000 `define ab 3'b001 `define zq 3'b010 `define zb 3'b011 `define za 3'b100 `define da 3'b101 `define dq 3'b110 `define dz 3'b111 // opcodes: instruction[5:3] `define add 3'b000 `define subr 3'b001 `define subs 3'b010 `define orrs 3'b011 `define andrs 3'b100 `define notrs 3'b101 `define exor 3'b110 `define exnor 3'b111 // destination mnemonics: instruction[8:6] `define qreg 3'b000 `define nop 3'b001 `define rama 3'b010 `define ramf 3'b011 `define ramqd 3'b100 `define ramd 3'b101 `define ramqu 3'b110 `define ramu 3'b111
`define is a compiler directive, handled by the pre-processor before the parser ever sees the file, exactly as #define is in C. The expansion is textual, it happens once, and synthesis therefore sees only 3'b100 — the symbolic names cost nothing in area, delay or power, which is the whole point. Lecture 7 covered directives; this is the canonical use of them.`define 是編譯器指令,由前置處理器在剖析器看到檔案之前就處理掉,跟 C 的 #define 完全一樣。展開是文字層級的、只發生一次,因此合成只看到 3'b100 —— 符號名稱在面積、延遲與功耗上都不花成本,這正是重點。第 7 講講過指令;這就是它們的典型用途。`define はコンパイラ指令であり、C の #define とまったく同じく、パーサがファイルを見る前にプリプロセッサが処理する。展開はテキストレベルで一度だけ起こるので、合成が見るのは 3'b100 だけである。記号名は面積にも遅延にも電力にもコストを足さない。そこがすべてである。第 7 講でコンパイラ指令を扱ったが、これはその典型的な用法である。
Several details are worth lifting out.有幾個細節值得拉出來講。いくつかの細部を取り出しておきたい。
`define ramqd 3'b100 defines the macro; dest_ctl == `ramqd uses it. Forgetting the back-tick at the use site gives you an undeclared identifier, which — if `default_nettype has not been set to none — becomes an implicit one-bit wire of value z, and the comparison is never true. The symptom is a decoder arm that never fires.使用處也必須加反引號。`define ramqd 3'b100 定義這個巨集;dest_ctl == `ramqd 使用它。在使用處忘了反引號,你得到的是一個未宣告的識別字 —— 如果 `default_nettype 沒設成 none,它會變成一條值為 z 的隱含一位元 wire,於是那個比較永遠不成立。症狀就是某個解碼分支永遠不會啟動。使用側にもバッククォートが要る。`define ramqd 3'b100 がマクロを定義し、dest_ctl == `ramqd がそれを使う。使用側でバッククォートを忘れると未宣言の識別子になり——`default_nettype を none にしていなければ——値 z の暗黙の 1 ビット wire になって、比較は決して真にならない。症状は、あるデコード分岐が決して発火しないことである。or, and and notrs, but or and and are Verilog gate primitives and cannot be macro names without confusing every reader and some tools. They become `orrs and `andrs. This is a general hazard of macros: the name space is global and flat.有三個名字不得不改。功能表把三個運算叫做 or、and 與 notrs,但 or 與 and 是 Verilog 的閘級基本元件,拿來當巨集名會讓每個讀者、還有某些工具都混淆。於是它們變成 `orrs 與 `andrs。這是巨集普遍的危險:名稱空間是全域而且扁平的。三つの名前は変更を強いられた。機能表は三つの演算を or、and、notrs と呼ぶが、or と and は Verilog のゲートプリミティブであり、マクロ名にすればすべての読者と一部のツールを混乱させる。そこで `orrs、`andrs になった。これはマクロ一般の危険で、名前空間がグローバルかつ平坦だからである。`define stays in effect for every file compiled after it in the same invocation, across module boundaries. In a large project this is a real hazard — two blocks each defining `add differently will silently take whichever was compiled last. The disciplined answers are to prefix every macro (`AMD_ADD), to `undef at the end of the file, or to use localparam instead, which is scoped to the module and visible in the waveform viewer as a named value.巨集是全域的,而且會殘留。一個 `define 在同一次呼叫中,對它之後編譯的每一個檔案都持續有效,跨越模組邊界。在大型專案裡這是真實的危險 —— 兩個區塊各自把 `add 定義成不同的東西,最後會悄悄採用最後編譯的那個。有紀律的做法是:給每個巨集加前綴(`AMD_ADD)、在檔案結尾 `undef,或改用 localparam(它的範圍限於模組,而且在波形檢視器裡會以具名值顯示)。マクロはグローバルで、しかも残り続ける。`define は同じ呼び出しの中でそれ以降にコンパイルされるすべてのファイルに、モジュール境界を越えて効き続ける。大規模プロジェクトではこれは現実の危険であり、二つのブロックが `add を別々に定義していれば、最後にコンパイルされた方が黙って採用される。規律ある答えは、マクロに接頭辞を付ける(`AMD_ADD)、ファイル末尾で `undef する、あるいは localparam を使うことである。後者はモジュールにスコープが限られ、波形ビューアでも名前付きの値として見える。parameter? Because these constants are used in three different modules and would have to be declared in each. A `define in a shared header is the Verilog-1995 way to share a constant across modules. SystemVerilog's packages and enumerated types are the modern answer and give you type checking and waveform decoding as well.為什麼不用 parameter?因為這些常數用在三個不同的模組裡,用 parameter 就得在每個模組各宣告一次。把 `define 放在共用標頭檔,是 Verilog-1995 跨模組共用常數的做法。SystemVerilog 的 package 與列舉型別才是現代答案,而且還附帶型別檢查與波形解碼。なぜ parameter ではないのか。これらの定数は三つの異なるモジュールで使われ、parameter ならそれぞれで宣言し直す必要があるからである。共有ヘッダに `define を置くのが、Verilog-1995 でモジュール間に定数を共有する方法である。SystemVerilog の package と列挙型が現代的な答えであり、型検査と波形上のデコードまで付いてくる。Problem: For each three-octal-digit instruction (destination, function, source) state in one line what the slice does, naming the registers involved. Assume a and b are supplied and c_in = 0 unless stated.問題:對每一個三位八進位數字的指令(目的地、功能、來源),用一行說明這片 slice 做了什麼,並指名涉及的暫存器。除非另有說明,假設 a 與 b 已給定、c_in = 0。問題:8 進 3 桁で書かれた各命令(デスティネーション、機能、ソース)について、このスライスが何をするかを 1 行で述べ、関係するレジスタを明示せよ。特に断らない限り a と b は与えられ、c_in = 0 とする。
(a) 1 0 1 (b) 3 0 1 (c) 0 6 7 (d) 2 0 4 (e) 3 4 5 (f) 6 0 3 with $c_{in}=0$ (g) 3 1 1 with $c_{in}=1$(a) 1 0 1 (b) 3 0 1 (c) 0 6 7 (d) 2 0 4 (e) 3 4 5 (f) 6 0 3,$c_{in}=0$ (g) 3 1 1,$c_{in}=1$(a) 1 0 1 (b) 3 0 1 (c) 0 6 7 (d) 2 0 4 (e) 3 4 5 (f) 6 0 3、$c_{in}=0$ (g) 3 1 1、$c_{in}=1$
(a) 1 0 1 = nop, add, ab. ALU computes $\textit{reg}[a] + \textit{reg}[b] + c_{in}$ and the result appears on y, but nothing is stored anywhere. A pure compare-and-inspect: the flags c_out, ovr, f_0 and f_3 are set by the operation and the microprogram can branch on them. This is how a 2901-based machine implements "compare".(a) 1 0 1 = nop, add, ab。ALU 算出 $\textit{reg}[a] + \textit{reg}[b] + c_{in}$,結果出現在 y 上,但哪裡都沒存。這是純粹的比較與檢視:旗標 c_out、ovr、f_0 與 f_3 由這次運算設定,微程式可以依它們分支。這就是 2901 機器實現「比較」的方式。(a) 1 0 1 = nop, add, ab。ALU が $\textit{reg}[a] + \textit{reg}[b] + c_{in}$ を計算し、結果は y に現れるが、どこにも格納されない。純粋な比較と検査である。フラグ c_out、ovr、f_0、f_3 がこの演算で設定され、マイクロプログラムはそれで分岐できる。2901 ベースの機械が「比較」を実装する方法である。
(b) 3 0 1 = ramf, add, ab. $\textit{reg}[b] \leftarrow \textit{reg}[a] + \textit{reg}[b] + c_{in}$, and y shows the same value. The standard register-to-register add, with the result overwriting operand b.(b) 3 0 1 = ramf, add, ab。$\textit{reg}[b] \leftarrow \textit{reg}[a] + \textit{reg}[b] + c_{in}$,而 y 顯示同一個值。標準的暫存器對暫存器加法,結果覆寫運算元 b。(b) 3 0 1 = ramf, add, ab。$\textit{reg}[b] \leftarrow \textit{reg}[a] + \textit{reg}[b] + c_{in}$ であり、y にも同じ値が出る。結果がオペランド b を上書きする、標準的なレジスタ間加算である。
(c) 0 6 7 = qreg, exor, dz. Source dz gives $r = d$, $s = 0$; the ALU computes $d \oplus 0 = d$; the destination qreg loads the result into Q and writes nothing to the register file. This is "load Q from the external data bus" — note that XOR with zero is being used as a pass-through because there is no explicit MOVE function. add with $c_{in}=0$ would do the same job.(c) 0 6 7 = qreg, exor, dz。來源 dz 給 $r = d$、$s = 0$;ALU 算 $d \oplus 0 = d$;目的地 qreg 把結果載入 Q,暫存器檔什麼也不寫。這就是「從外部資料匯流排載入 Q」—— 注意這裡是拿「與零做 XOR」當作直通,因為沒有明確的 MOVE 功能。$c_{in}=0$ 的 add 也能做同樣的事。(c) 0 6 7 = qreg, exor, dz。ソース dz により $r = d$、$s = 0$ となり、ALU は $d \oplus 0 = d$ を計算する。デスティネーション qreg は結果を Q にロードし、レジスタファイルには何も書かない。これは「外部データバスから Q へロードする」命令である。明示的な MOVE 機能が無いため、ゼロとの XOR を素通しとして使っている点に注意したい。$c_{in}=0$ の add でも同じことができる。
(d) 2 0 4 = rama, add, za. Source za gives $r = 0$, $s = \textit{reg}[a]$; the ALU computes $\textit{reg}[a] + c_{in}$; rama writes that into $\textit{reg}[b]$ while y shows $\textit{reg}[a]$ — the old value, because the read is combinational from the flip-flop outputs and the write happens at the edge. With $c_{in}=0$ this is "move a to b and simultaneously put a on the output bus"; with $c_{in}=1$ it is "copy a to b, incremented".(d) 2 0 4 = rama, add, za。來源 za 給 $r = 0$、$s = \textit{reg}[a]$;ALU 算 $\textit{reg}[a] + c_{in}$;rama 把它寫進 $\textit{reg}[b]$,而 y 顯示的是 $\textit{reg}[a]$ —— 舊值,因為讀取是從正反器輸出組合而來,寫入則在邊緣才發生。$c_{in}=0$ 時這是「把 a 搬到 b,同時把 a 放上輸出匯流排」;$c_{in}=1$ 時則是「把 a 加一後複製到 b」。(d) 2 0 4 = rama, add, za。ソース za により $r = 0$、$s = \textit{reg}[a]$ となり、ALU は $\textit{reg}[a] + c_{in}$ を計算する。rama はそれを $\textit{reg}[b]$ へ書き、y には $\textit{reg}[a]$——すなわち古い値——が出る。読み出しはフリップフロップ出力からの組合せであり、書き込みはエッジで起こるからである。$c_{in}=0$ なら「a を b へ移しつつ a を出力バスに出す」、$c_{in}=1$ なら「a を +1 して b へ複写する」である。
(e) 3 4 5 = ramf, andrs, da. Source da gives $r = d$, $s = \textit{reg}[a]$; the ALU computes $d \mathbin{\&} \textit{reg}[a]$; ramf stores it at $\textit{reg}[b]$ and shows it on y. A masking operation with the mask supplied from outside.(e) 3 4 5 = ramf, andrs, da。來源 da 給 $r = d$、$s = \textit{reg}[a]$;ALU 算 $d \mathbin{\&} \textit{reg}[a]$;ramf 把它存到 $\textit{reg}[b]$ 並顯示在 y 上。這是一次遮罩運算,而遮罩由外部供給。(e) 3 4 5 = ramf, andrs, da。ソース da により $r = d$、$s = \textit{reg}[a]$ となり、ALU は $d \mathbin{\&} \textit{reg}[a]$ を計算する。ramf がそれを $\textit{reg}[b]$ へ格納し、y にも出す。マスクを外部から与えるマスク演算である。
(f) 6 0 3 = ramqu, add, zb. Source zb gives $r = 0$, $s = \textit{reg}[b]$; the ALU computes $\textit{reg}[b] + 0 = \textit{reg}[b]$; ramqu stores that value shifted left by one into $\textit{reg}[b]$, with ram0 entering the bottom, and simultaneously shifts Q left with qs0 entering its bottom. So $\textit{reg}[b] \leftarrow 2\,\textit{reg}[b]$ and the register file's old bit 3 leaves on ram3. This is the arithmetic left shift, the core step of a shift-and-add multiply or of a left-shifting divide.(f) 6 0 3 = ramqu, add, zb。來源 zb 給 $r = 0$、$s = \textit{reg}[b]$;ALU 算 $\textit{reg}[b] + 0 = \textit{reg}[b]$;ramqu 把那個值左移一位後存回 $\textit{reg}[b]$,底部由 ram0 進入,同時把 Q 也左移、底部由 qs0 進入。於是 $\textit{reg}[b] \leftarrow 2\,\textit{reg}[b]$,而暫存器檔原本的位元 3 從 ram3 離開。這就是算術左移,也是移位相加乘法或左移式除法的核心步驟。(f) 6 0 3 = ramqu, add, zb。ソース zb により $r = 0$、$s = \textit{reg}[b]$ となり、ALU は $\textit{reg}[b] + 0 = \textit{reg}[b]$ を計算する。ramqu はその値を1 ビット左シフトして $\textit{reg}[b]$ へ格納し、下端には ram0 が入る。同時に Q も左シフトされ、下端には qs0 が入る。すなわち $\textit{reg}[b] \leftarrow 2\,\textit{reg}[b]$ であり、レジスタファイルの元のビット 3 は ram3 から出ていく。これが算術左シフトであり、シフト加算乗算や左シフト型除算の中核となる手順である。
(g) 3 1 1 = ramf, subr, ab. Source ab gives $r = \textit{reg}[a]$, $s = \textit{reg}[b]$. By the table, subr is $s - r$, so $\textit{reg}[b] \leftarrow \textit{reg}[b] - \textit{reg}[a]$. By the code on slide 25, which computes $r + \bar s + c_{in}$, it is $\textit{reg}[a] - \textit{reg}[b]$ when $c_{in} = 1$. The two differ by a sign. Whenever this instruction appears in an exercise, say which source you are following; §6.3 explains the discrepancy in full.(g) 3 1 1 = ramf, subr, ab。來源 ab 給 $r = \textit{reg}[a]$、$s = \textit{reg}[b]$。照表,subr 是 $s - r$,所以 $\textit{reg}[b] \leftarrow \textit{reg}[b] - \textit{reg}[a]$。照投影片 25 的程式碼(它算的是 $r + \bar s + c_{in}$),在 $c_{in} = 1$ 時則是 $\textit{reg}[a] - \textit{reg}[b]$。兩者差一個正負號。只要這個指令出現在習題裡,就請說清楚你依的是哪一個來源;§6.3 會完整解釋這個不一致。(g) 3 1 1 = ramf, subr, ab。ソース ab により $r = \textit{reg}[a]$、$s = \textit{reg}[b]$ となる。表に従えば subr は $s - r$ なので $\textit{reg}[b] \leftarrow \textit{reg}[b] - \textit{reg}[a]$ である。スライド 25 のコードは $r + \bar s + c_{in}$ を計算するので、$c_{in} = 1$ のとき $\textit{reg}[a] - \textit{reg}[b]$ になる。両者は符号が逆である。この命令が演習に出てきたら、どちらの典拠に従うかを明示すること。§6.3 でこの食い違いを詳しく説明する。
module Register_file (clk, rst, a, b, f, dest_ctl, ram0, ram3, ad, bd); input clk, rst; input [3:0] a, b, f; input [2:0] dest_ctl; inout ram0, ram3; output [3:0] ad, bd; reg [3:0] ab_data [15:0]; // the memory: 16 elements of 4 bits wire [3:0] data; wire ram_en; reg [4:0] row_index; // note: the size of row_index is critical assign ram_en = ((dest_ctl == `qreg) || (dest_ctl == `nop)) ? 0 : 1; always @ (posedge clk or posedge rst) if (rst == 1) for (row_index = 0; row_index < 16; row_index = row_index + 1) ab_data[row_index] = 0; else if (ram_en) ab_data[b] = data; assign data = (dest_ctl == `ramqu || dest_ctl == `ramu) ? {f[2], f[1], f[0], ram0} : (dest_ctl == `ramqd || dest_ctl == `ramd) ? {ram3, f[3], f[2], f[1]} : (dest_ctl == `rama || dest_ctl == `ramf) ? f : 4'bxxxx; assign ad = ab_data[a]; assign bd = ab_data[b]; assign ram3 = (dest_ctl == `ramu || dest_ctl == `ramqu) ? f[3] : 1'bz; assign ram0 = (dest_ctl == `ramd || dest_ctl == `ramqd) ? f[0] : 1'bz; endmodule
The port f is the ALU output; the top level connects alu_out to it. dest_ctl is instruction[8:6].埠 f 是 ALU 的輸出;頂層把 alu_out 接到它。dest_ctl 就是 instruction[8:6]。ポート f は ALU の出力であり、トップレベルが alu_out をそこへ接続する。dest_ctl は instruction[8:6] である。
The write enable. ram_en is low for exactly two destination codes, qreg and nop, which are the two that the table says write nothing to the register file. Every other code writes. This single line is the complete write-enable decode, and it is a nice illustration of coding against a table rather than enumerating cases: six positive cases collapse into two negative ones.寫入致能。ram_en 只在兩個目的地碼 qreg 與 nop 時為低,而那兩個正是表上說「不寫暫存器檔」的碼。其餘每一個碼都會寫。這一行就是完整的寫入致能解碼,也漂亮地示範了「照著表寫程式」而不是逐一列舉:六個正面情況塌成兩個反面情況。書き込みイネーブル。ram_en が Low になるのはデスティネーションコード qreg と nop のちょうど二つだけで、これは表が「レジスタファイルに何も書かない」とする二つである。残りはすべて書き込む。この 1 行が書き込みイネーブルのデコードのすべてであり、場合を列挙するのではなく表に沿って書く好例でもある。肯定形の 6 ケースが否定形の 2 ケースに畳み込まれている。
The reset loop. The highlighted declaration is the one the comment flags as critical. The loop runs row_index from 0 while it is less than 16, so the index must be able to hold 16 in order for the test to fail and the loop to end. Four bits cannot: 4'b1111 + 1 is 4'b0000, the test 0 < 16 is true again, and the loop runs for ever. In simulation that is an infinite loop inside a zero-delay block — the simulator hangs at time zero with no output, which is a uniquely unhelpful failure. In synthesis the tool must unroll the loop statically and will report that the bounds cannot be determined. Five bits fix it. The general rule from Lecture 7: a loop index must be able to represent a value that fails the termination test.重置迴圈。被標示出來的那個宣告,就是註解標為關鍵的那一行。迴圈讓 row_index 從 0 開始、在小於 16 時持續執行,所以索引必須裝得下 16,測試才會失敗、迴圈才會結束。四位元裝不下:4'b1111 + 1 是 4'b0000,測試 0 < 16 又為真,迴圈就永遠跑下去。在模擬裡那是一個零延遲區塊內的無窮迴圈 —— 模擬器在時間零卡住、什麼輸出都沒有,這是一種格外沒有幫助的失效方式。在合成時,工具必須靜態展開迴圈,它會回報邊界無法決定。五位元就解決了。第 7 講的通則是:迴圈索引必須能表示一個會使終止測試失敗的值。リセットループ。強調された宣言が、コメントで critical と記された行である。ループは row_index を 0 から始め、16 未満のあいだ回る。したがって判定が偽になってループが終わるには、ループ変数が 16 を保持できる必要がある。4 ビットでは保持できない。4'b1111 + 1 は 4'b0000 となり、判定 0 < 16 は再び真になってループは終わらない。シミュレーションでは遅延ゼロのブロック内の無限ループとなり、シミュレータは時刻 0 で何も出力せずに固まる。これは格別に手掛かりの乏しい失敗の仕方である。合成ではツールがループを静的に展開しなければならず、境界が決定できないと報告してくる。5 ビットなら解決する。第 7 講の一般則はこうである。ループ変数は、終了判定を偽にする値を表現できなければならない。
Reading is combinational, writing is clocked. ad and bd are continuous assignments indexing the memory, so they follow a and b with only gate delay: two asynchronous read ports. The write happens in the clocked block at address b: one synchronous write port. A 16×4 file with two asynchronous reads and one synchronous write synthesises to 64 flip-flops, a 4-to-16 decoder producing 16 enables, and two 16-to-1 four-bit multiplexers — about 64 flops and 150 to 200 gates. There is no commodity RAM macro with this shape, so a flop-based file is the right implementation; for a larger file you would use a two-port register-file compiler.讀取是組合的,寫入是時脈同步的。ad 與 bd 是索引記憶體的連續指定,所以它們只隔一點閘延遲就跟著 a 與 b 走:兩個非同步讀取埠。寫入發生在時脈區塊裡、寫到位址 b:一個同步寫入埠。一個帶兩個非同步讀取與一個同步寫入的 16×4 暫存器檔,會合成成 64 個正反器、一個產生 16 條致能的 4 對 16 解碼器,以及兩個 16 對 1 的四位元多工器 —— 大約 64 個正反器加 150 到 200 個閘。市面上沒有這種形狀的 RAM 巨集,所以用正反器做是正確的實作;檔案更大時,你會改用雙埠暫存器檔編譯器。読み出しは組合せ、書き込みはクロック同期である。ad と bd はメモリを添字参照する連続代入なので、ゲート遅延だけで a、b に追従する。すなわち非同期読み出しポート 2 本である。書き込みはクロック同期ブロック内でアドレス b に対して行われる。同期書き込みポート 1 本である。非同期読み出し 2 本と同期書き込み 1 本を持つ 16×4 のファイルは、フリップフロップ 64 個、16 本のイネーブルを作る 4-to-16 デコーダ、4 ビット 16 対 1 のマルチプレクサ 2 個に合成される。フリップフロップ 64 個とゲート 150〜200 個ほどである。この形の汎用 RAM マクロは存在しないので、フリップフロップによる実装が正解である。もっと大きなファイルなら 2 ポートのレジスタファイルコンパイラを使うことになる。
Read-during-write. Because the reads come from flip-flop outputs and the write lands at the clock edge, ad and bd show the old contents throughout the cycle in which a write to the same address is in flight. There is no write-through. That is exactly what makes instruction (d) of Practice 10 work: rama writes the ALU result into address b while y shows the pre-write value of address a.寫入當下的讀取。因為讀取取自正反器輸出,而寫入落在時脈邊緣,所以在一次對同位址的寫入進行中的整個週期裡,ad 與 bd 顯示的都是舊內容。沒有寫入直通。這正是練習 10 的指令 (d) 能成立的原因:rama 把 ALU 結果寫進位址 b,而 y 顯示的是位址 a 寫入前的值。書き込み中の読み出し。読み出しはフリップフロップ出力から来て、書き込みはクロックエッジで着地するので、同じアドレスへの書き込みが進行中のサイクル全体にわたって ad と bd は古い内容を示す。ライトスルーは無い。練習 10 の命令 (d) が成立するのはまさにこれによる。rama が ALU の結果をアドレス b に書くあいだ、y はアドレス a の書き込み前の値を示す。
The shift multiplexer. data is a three-way conditional selecting what actually gets written:移位多工器。data 是一個三向條件式,選出真正要被寫入的東西:シフト用マルチプレクサ。data は三方向の条件式で、実際に書き込まれるものを選ぶ。
| Destination目的地デスティネーション | data | Meaning意義意味 |
|---|---|---|
ramqu, ramu | {f[2], f[1], f[0], ram0} | $f$ shifted left one place; ram0 enters at the bottom; $f[3]$ is lost from this slice and leaves on the ram3 pin$f$ 左移一位;ram0 從底部進入;$f[3]$ 離開這片 slice,由 ram3 腳位送出$f$ を 1 ビット左シフト。ram0 が下端から入り、$f[3]$ はこのスライスを離れて ram3 ピンから出る |
ramqd, ramd | {ram3, f[3], f[2], f[1]} | $f$ shifted right one place; ram3 enters at the top; $f[0]$ leaves on the ram0 pin$f$ 右移一位;ram3 從頂部進入;$f[0]$ 由 ram0 腳位離開$f$ を 1 ビット右シフト。ram3 が上端から入り、$f[0]$ は ram0 ピンから出る |
rama, ramf | f | Unshifted不移位シフトなし |
qreg, nop | 4'bxxxx | Unreachable: ram_en is low, so this value is never captured. A true don't-care, and declaring it as x lets the synthesiser use it for optimisation到不了:ram_en 為低,所以這個值永遠不會被捕捉。這是真正的 don't care,把它宣告成 x 讓合成器可以拿去最佳化到達不能である。ram_en が Low なのでこの値が取り込まれることはない。真のドントケアであり、x と書いておけば合成器が最適化に使える |
Writing the shifts as concatenations rather than as f >> 1 is deliberate and necessary: a shift operator would fill the vacated bit with a zero, and here it must be filled from a pin. Concatenation is the only way to express "shift and insert this particular bit", and it is the standard idiom for any shifter with a serial input.把移位寫成串接而不是 f >> 1,是刻意而且必要的:移位運算子會把空出來的位元補零,但這裡它必須由腳位填入。串接是唯一能表達「移位並插入這個特定位元」的寫法,也是任何帶串列輸入的移位器的標準慣用法。シフトを f >> 1 ではなく連結で書くのは意図的であり、かつ必要である。シフト演算子は空いたビットをゼロで埋めるが、ここではピンから埋めなければならない。「シフトしつつこの特定のビットを挿入する」を表現できるのは連結だけであり、シリアル入力を持つシフタの標準的な書き方でもある。
The bidirectional pins. ram0 and ram3 are declared inout and driven through conditional continuous assignments that produce 1'bz when not driving. On an up-shift the slice drives its top bit out on ram3 and receives the bit below on ram0; on a down-shift the directions swap. Because every slice follows the same rule, wiring slice $k$'s ram3 to slice $k{+}1$'s ram0 gives a shift chain of any length with no extra logic, and the fact that only one end of each wire ever drives means there is no contention. The lowest slice's ram0 and the highest slice's ram3 are left for the system designer to tie — to zero for a logical shift, to the sign bit for an arithmetic right shift, to the carry for a rotate through carry, or, as §7.1 shows, to the other shift chain for a multiply.雙向腳位。ram0 與 ram3 宣告成 inout,由條件式連續指定驅動,不驅動時產生 1'bz。上移時,這片 slice 把自己的最高位元由 ram3 送出,同時由 ram0 接收下面那一位元;下移時方向對調。因為每片 slice 都遵守同一條規則,把第 $k$ 片的 ram3 接到第 $k{+}1$ 片的 ram0,就得到任意長度的移位鏈,不需要額外邏輯;而每條線上永遠只有一端在驅動,所以不會有爭用。最低那片的 ram0 與最高那片的 ram3 留給系統設計者去接 —— 邏輯移位就接零、算術右移就接符號位元、帶進位旋轉就接進位,或者如 §7.1 所示,接到另一條移位鏈去做乘法。双方向ピン。ram0 と ram3 は inout として宣言され、条件付きの連続代入で駆動され、駆動しないときは 1'bz を出す。上方向シフトではスライスは自分の最上位ビットを ram3 から出し、同時に下のビットを ram0 から受け取る。下方向シフトでは向きが入れ替わる。すべてのスライスが同じ規則に従うので、$k$ 番目のスライスの ram3 を $k{+}1$ 番目の ram0 へ配線すれば、追加の論理なしに任意長のシフト連鎖ができる。各配線の駆動側は常に片方だけなので競合も起きない。最下位スライスの ram0 と最上位スライスの ram3 は、システム設計者が接続すべく残されている。論理シフトならゼロへ、算術右シフトなら符号ビットへ、キャリー付きローテートならキャリーへ、あるいは §7.1 で見るように乗算のためにもう一方のシフト連鎖へつなぐ。
ab_data[row_index] = 0 and ab_data[b] = data use =. The reset loop is defensible — it is a sequence of 16 writes that genuinely must happen in order within one edge — but ab_data[b] = data is a race against every other block that reads ad or bd in the same time step, because the continuous assignments propagate the new value immediately in the active region. Use <=.時脈區塊裡的阻塞指定。ab_data[row_index] = 0 與 ab_data[b] = data 都用 =。重置迴圈還說得過去 —— 它是一連串 16 次寫入,在同一個邊緣內本來就必須按順序發生 —— 但 ab_data[b] = data 是對同一時間步內每一個會讀 ad 或 bd 的區塊的競爭,因為那些連續指定會在 active 區間立刻把新值傳出去。請用 <=。クロック同期ブロック内のブロッキング代入。ab_data[row_index] = 0 も ab_data[b] = data も = を使っている。リセットループは擁護できる——1 つのエッジ内で順に起こらねばならない 16 回の書き込みだからである——が、ab_data[b] = data は同じ時間ステップで ad や bd を読むすべてのブロックに対するレースである。連続代入が新しい値をアクティブ領域で即座に伝えてしまうからである。<= を使うべきである。module operand_selector (d, ad, bd, q, op_sel, r, s);
input [3:0] d, ad, bd, q;
input [2:0] op_sel;
output [3:0] r, s;
reg [3:0] r, s;
// operand codes
always @ (op_sel) begin
case (op_sel)
`aq, `ab: assign r = ad;
`zq, `zb, `za: assign r = 4'b0;
default: assign r = d;
endcase
case (op_sel)
`aq, `zq, `dq: assign s = q;
`ab, `zb: assign s = bd;
`za, `da: assign s = ad;
default: assign s = 4'b0;
endcase
end
endmodule
Check it against the table of §5.4 arm by arm. For r: aq and ab give ad; zq, zb and za give zero; the default catches da, dq and dz, all of which give d. For s: aq, zq, dq give q; ab, zb give bd; za, da give ad; the default catches dz alone and gives zero. Every one of the sixteen entries matches. This module is correct.拿它逐個分支對照 §5.4 的表。r 方面:aq 與 ab 給 ad;zq、zb、za 給零;default 接住 da、dq、dz,它們都給 d。s 方面:aq、zq、dq 給 q;ab、zb 給 bd;za、da 給 ad;default 只接住 dz,給零。十六項全部吻合。這個模組是對的。§5.4 の表と分岐ごとに突き合わせてみる。r 側では、aq と ab が ad、zq・zb・za がゼロ、default が da・dq・dz を受けてすべて d を与える。s 側では、aq・zq・dq が q、ab・zb が bd、za・da が ad、default は dz だけを受けてゼロを与える。16 項目すべてが一致する。このモジュールは正しい。
What is unusual is the construct. The sensitivity list contains only op_sel, yet r and s depend on ad, bd, q and d as well. In an ordinary combinational block that would be an incomplete sensitivity list and a classic Lecture 5 bug: the outputs would freeze whenever a data input changed without op_sel changing, simulation would disagree with the synthesised hardware, and the mismatch would surface only at gate level. Here it is not a bug, because the assignments are not ordinary procedural assignments. They are procedural continuous assignments.不尋常的是它的寫法。敏感列表裡只有 op_sel,但 r 與 s 還依賴 ad、bd、q 與 d。在一般的組合區塊裡,那會是一個不完整的敏感列表,也是第 5 講的經典 bug:只要資料輸入改變而 op_sel 沒變,輸出就凍住,模擬會與合成出的硬體不一致,而這個不一致只會在閘級浮現。但這裡不是 bug,因為那些指定不是一般的程序性指定,而是程序式連續指定(PCA)。変わっているのはその書き方である。感度リストには op_sel しかないのに、r と s は ad、bd、q、d にも依存している。通常の組合せブロックならこれは不完全な感度リストであり、第 5 講の典型的なバグである。op_sel が変わらずにデータ入力だけが変われば出力は凍りつき、シミュレーションと合成後のハードウェアが食い違い、その不一致はゲートレベルでしか表面化しない。しかしここではバグではない。これらの代入が通常の手続き型代入ではなく、手続き型連続代入(PCA)だからである。
assign inside an always block means在 always 區塊裡寫 assign 是什麼意思always ブロック内の assign の意味A procedural continuous assignment establishes a persistent binding between a reg and an expression, and that binding stays in force until it is overridden by another assign to the same variable or removed by deassign. So when op_sel changes, the block runs once and re-establishes the binding; between those events, r continues to track ad or d continuously, as if it were driven by a module-level continuous assignment. The sensitivity list only has to wake the block when the choice changes, not when the data changes, which is why the lecture notes call this the "simulation efficient" style — a block of four muxes re-evaluated eight times per instruction instead of every time any data bit toggles.程序式連續指定在一個 reg 與一個運算式之間建立持續的綁定,而這個綁定會一直有效,直到被另一個對同一變數的 assign 覆寫,或被 deassign 移除為止。所以 op_sel 一變,這個區塊就跑一次、重新建立綁定;在這些事件之間,r 會持續跟著 ad 或 d 走,彷彿它是被模組層級的連續指定驅動的。敏感列表只需要在選擇改變時喚醒區塊,而不必在資料改變時喚醒 —— 這就是講義把這種寫法稱為「模擬效率高」的原因:一組四個多工器,每道指令只重新求值八次,而不是任何一個資料位元翻轉就重算。手続き型連続代入は、reg と式のあいだに持続的な結合を確立し、その結合は同じ変数への別の assign で上書きされるか deassign で解除されるまで効き続ける。したがって op_sel が変わるとブロックが 1 回走って結合を張り直し、その合間 r はあたかもモジュールレベルの連続代入で駆動されているかのように ad や d を追い続ける。感度リストは選択が変わったときにブロックを起こせばよく、データが変わったときに起こす必要はない。講義資料がこの書き方を「シミュレーション効率がよい」と呼ぶのはそのためである。4 つのマルチプレクサの組が、データビットが 1 本動くたびではなく、命令あたり 8 回だけ再評価される。
Should you write code this way? No. Three reasons: procedural continuous assignments are poorly supported by synthesis tools and some will reject the module outright; they were deprecated in IEEE 1364-2005 and removed from the SystemVerilog synthesisable subset; and the efficiency they buy is irrelevant on any simulator built in the last twenty years, all of which use event-driven scheduling that already skips unchanged inputs. The portable equivalent is three lines shorter and will synthesise anywhere:你應該這樣寫程式嗎?不應該。三個理由:程序式連續指定在合成工具裡支援很差,有些工具會直接拒絕這個模組;它在 IEEE 1364-2005 被標為棄用,並從 SystemVerilog 的可合成子集中移除;而且它買到的效率,在過去二十年內做出來的任何模擬器上都無關緊要,那些模擬器用的事件驅動排程本來就會跳過沒變的輸入。可攜的等價寫法短三行,而且到哪裡都合成得出來:こう書くべきか。書くべきではない。理由は三つある。手続き型連続代入は合成ツールのサポートが乏しく、モジュールごと拒否するツールもある。IEEE 1364-2005 で非推奨となり、SystemVerilog の合成可能部分集合からは削除された。そして、それが買う効率はここ 20 年に作られたどのシミュレータでも無意味である。いずれもイベント駆動のスケジューリングで、変化していない入力はもともと飛ばすからである。移植性のある等価な書き方は 3 行短く、どこでも合成できる。
always @ (op_sel) begin
case (op_sel)
`aq, `ab: assign r = ad;
`zq, `zb, `za: assign r = 4'b0;
default: assign r = d;
endcase
...
endalways @* with blocking可攜的寫法 —— always @* 配阻塞指定移植性のある書き方 — always @* とブロッキング代入always @* begin
case (op_sel)
`aq, `ab: r = ad;
`zq, `zb, `za: r = 4'b0;
default: r = d;
endcase
...
end
// or, even better, two continuous assignments
// with nested conditionals and no always blockBoth case statements have a default, so every path assigns both outputs and no latch can be inferred. Both are also full and parallel — all eight codes appear exactly once across the arms — so the synthesiser builds a plain multiplexer rather than a priority chain. The result is a 3-to-1 four-bit mux for r and a 4-to-1 four-bit mux for s: roughly 30 gates in total, and about two gate delays.兩個 case 都有 default,所以每一條路徑都指定了兩個輸出,推論不出閂鎖。兩者也都是完整且互斥的 —— 八個碼在各分支中各出現恰好一次 —— 所以合成器建的是單純的多工器,而不是優先鏈。結果是 r 一個 3 對 1 的四位元多工器、s 一個 4 對 1 的四位元多工器:總共大約 30 個閘,約兩級閘延遲。どちらの case にも default があるので、すべての経路で両方の出力に代入され、ラッチは推論されない。どちらも full かつ parallel であり——8 つのコードが各分岐にちょうど 1 回ずつ現れる——合成器はプライオリティ連鎖ではなく素直なマルチプレクサを作る。結果は r 用の 3 対 1 の 4 ビットマルチプレクサと s 用の 4 対 1 のそれで、合計おおよそ 30 ゲート、遅延にして 2 段ほどである。
module ALU_amd2901 (r, s, c_in, ctl, alu_out, g_bar, p_bar, ovr, c_out);
input [3:0] r, s;
input c_in;
input [2:0] ctl;
output [3:0] alu_out;
output g_bar, p_bar, ovr, c_out;
reg [3:0] alu_out;
reg c_out;
always @ (r or s or c_in or ctl)
case (ctl)
`add: {c_out, alu_out} = r + s + c_in;
`subr: {c_out, alu_out} = r + ~s + c_in;
`subs: {c_out, alu_out} = s + ~r + ~c_in;
`orrs: {c_out, alu_out} = {1'b0, r | s};
`andrs: {c_out, alu_out} = {1'b0, r & s};
`notrs: {c_out, alu_out} = {1'b0, (~r) & s};
`exor: {c_out, alu_out} = {1'b0, r ^ s};
`exnor: {c_out, alu_out} = {1'b0, r ~^ s};
default: {c_out, alu_out} = 5'bx;
endcase
assign ovr = c_out ^ alu_out[3];
assign g_bar = ~( (r[3] & s[3]) |
((r[3] | s[3]) & (r[2] & s[2])) |
((r[3] | s[3]) & (r[2] | s[2]) & (r[1] & s[1])) |
((r[3] | s[3]) & (r[2] | s[2]) & (r[1] | s[1]) & (r[0] & s[0])) );
assign p_bar = ~((r[3] ^ s[3]) & (r[2] ^ s[2]) & (r[1] ^ s[1]) & (r[0] ^ s[0]));
endmodule
Start with what is right. The case is complete in all eight codes and has a default, so there is no latch. The sensitivity list names every input, so the block is a genuine combinational description. And the use of a concatenation on the left-hand side — {c_out, alu_out} — is the idiom you should take away from this module: it makes the assignment target five bits wide, so the five-bit sum of a four-bit addition is captured in full, with the carry landing in c_out instead of being discarded. Writing alu_out = r + s + c_in; with a four-bit target would throw the carry away, and you would need a second, separately computed expression to recover it.先說對的地方。case 八個碼都齊全,而且有 default,所以沒有閂鎖。敏感列表列出每一個輸入,所以這是一個貨真價實的組合描述。而在左手邊用串接 —— {c_out, alu_out} —— 是你該從這個模組帶走的慣用法:它讓指定目標寬五位元,於是四位元加法的五位元和被完整收下,進位落在 c_out 裡而不是被丟掉。寫成四位元目標的 alu_out = r + s + c_in; 會把進位扔掉,你得另外再寫一個獨立運算式把它救回來。まず正しい点から。case は 8 コードすべてを網羅し、default も持つのでラッチは生じない。感度リストはすべての入力を挙げており、本物の組合せ記述になっている。そして左辺に連結を使う {c_out, alu_out} こそ、このモジュールから持ち帰るべき書き方である。代入先が 5 ビット幅になるので、4 ビット加算の 5 ビットの和が丸ごと受け取られ、キャリーは捨てられずに c_out へ入る。4 ビットの代入先で alu_out = r + s + c_in; と書けばキャリーは失われ、それを取り戻すために別の式をもう一本書く羽目になる。
The logical functions are handled differently and correctly: {1'b0, r | s} explicitly supplies the carry bit as zero. Because that concatenation is itself five bits, the inner r | s is self-determined at four bits and is not affected by the surrounding width. That distinction is about to matter enormously.邏輯功能則用了不同而且正確的處理:{1'b0, r | s} 明確地把進位位元供成零。因為那個串接本身是五位元,裡面的 r | s 是在四位元下自定寬度的,不受外圍寬度影響。這個區別馬上就要變得極其重要。論理演算は別のやり方で、しかも正しく扱われている。{1'b0, r | s} はキャリービットを明示的にゼロとして与える。この連結自体が 5 ビットなので、内側の r | s は 4 ビットで自己決定され、外側の幅に影響されない。この区別がこの直後に決定的な意味を持つ。
The table of §5.5 says subr is $s - r$ and subs is $r - s$. The code computes $r + \bar s + c_{in}$ for subr, which is $r - s$ when $c_{in} = 1$, and $s + \bar r + \overline{c_{in}}$ for subs, which is $s - r$ when $c_{in} = 0$. So the two mnemonics are swapped relative to the table, and the two use opposite carry-in conventions, which is worse: a microprogram that sets $c_{in} = 1$ for one subtraction and expects the same convention for the other gets an answer that is off by one. The Am2901 datasheet defines both subtractions with the same convention, $A - B = A + \bar B + C_{in}$, so the correct code is `subr: {c_out, alu_out} = s + ~r + c_in; and `subs: {c_out, alu_out} = r + ~s + c_in;. When answering an examination question, state which convention you are using before you compute.§5.5 的表說 subr 是 $s - r$、subs 是 $r - s$。程式碼對 subr 算的是 $r + \bar s + c_{in}$,在 $c_{in} = 1$ 時就是 $r - s$;對 subs 算的是 $s + \bar r + \overline{c_{in}}$,在 $c_{in} = 0$ 時是 $s - r$。所以兩個助記符相對於表是對調的,而且兩者用的進位輸入慣例相反,這更糟:一段微程式若對其中一個減法設 $c_{in} = 1$,又以為另一個也是同樣慣例,答案就會差一。Am2901 的資料手冊把兩個減法都定義成同一個慣例 $A - B = A + \bar B + C_{in}$,所以正確的程式碼是 `subr: {c_out, alu_out} = s + ~r + c_in; 與 `subs: {c_out, alu_out} = r + ~s + c_in;。回答考題時,請先說明你採用哪個慣例,再開始計算。§5.5 の表は subr を $s - r$、subs を $r - s$ としている。コードは subr に対して $r + \bar s + c_{in}$ を計算し、$c_{in} = 1$ ならこれは $r - s$ である。subs に対しては $s + \bar r + \overline{c_{in}}$ を計算し、$c_{in} = 0$ なら $s - r$ である。つまり二つのニーモニックは表に対して入れ替わっており、しかもキャリー入力の規約が互いに逆である。こちらのほうが厄介で、片方の減算で $c_{in} = 1$ を設定し、もう一方も同じ規約だと思い込んだマイクロプログラムは 1 だけずれた答えを得る。Am2901 のデータシートはどちらの減算も同じ規約 $A - B = A + \bar B + C_{in}$ で定義しているので、正しいコードは `subr: {c_out, alu_out} = s + ~r + c_in; と `subs: {c_out, alu_out} = r + ~s + c_in; である。試験で答えるときは、計算を始める前にどちらの規約に従うかを述べること。
Now the subtle one, and it is genuinely subtle — subtle enough that it survives in textbooks. Consider現在講微妙的那個,而且它是真的微妙 —— 微妙到連教科書裡都還留著。考慮ここからが微妙な話であり、本当に微妙である。教科書にすら生き残っているほどに。次を考えてみる。
{c_out, alu_out} = r + ~s + c_in;
and ask what ~s evaluates to. The intuitive answer is "the four-bit complement of s", so for s = 4'b0101 it would be 4'b1010 = 10. That is not what the language says.然後問 ~s 求值是多少。直覺的答案是「s 的四位元補數」,所以 s = 4'b0101 時應該是 4'b1010 = 10。語言規定的不是那樣。そして ~s が何に評価されるかを問う。直感的な答えは「s の 4 ビットの補数」であり、s = 4'b0101 なら 4'b1010 = 10 になるはずである。言語が定めているのはそうではない。
IEEE 1364 sizes an expression in two steps. First, the expression's width is the maximum of the widths of all its context-determined operands and of the assignment target. Here the target is the five-bit concatenation {c_out, alu_out}, so the expression width is five. Second, every context-determined operand is extended to that width before the operator is applied. The operand of a bitwise negation is context-determined. So s is first zero-extended to 5'b00101 and then complemented, giving 5'b11010 = 26, not 10.IEEE 1364 以兩個步驟決定運算式的寬度。第一,運算式的寬度是「所有由上下文決定寬度的運算元以及指定目標」的寬度最大值。這裡目標是五位元的串接 {c_out, alu_out},所以運算式寬度是五。第二,每一個由上下文決定的運算元都會先被擴展到那個寬度,然後才套用運算子。逐位元取反的運算元是由上下文決定的。所以 s 先被零擴展成 5'b00101,然後才取反,得到 5'b11010 = 26,而不是 10。IEEE 1364 は式の幅を 2 段階で決める。第一に、式の幅は文脈で幅が決まるすべてのオペランドと代入先の幅の最大値である。ここでの代入先は 5 ビットの連結 {c_out, alu_out} なので、式の幅は 5 になる。第二に、文脈で決まるオペランドはその幅へ拡張されてから演算子が適用される。ビット単位否定のオペランドは文脈で決まる。したがって s はまず 5'b00101 へゼロ拡張され、その後で反転されて 5'b11010 = 26 となり、10 にはならない。
Let r = 4'b0011 (3), s = 4'b0101 (5), c_in = 1, function `subr. The intended operation is $3 - 5 = -2$.取 r = 4'b0011(3)、s = 4'b0101(5)、c_in = 1,功能為 `subr。意圖中的運算是 $3 - 5 = -2$。r = 4'b0011(3)、s = 4'b0101(5)、c_in = 1、機能は `subr とする。意図された演算は $3 - 5 = -2$ である。
alu_out = 4'b1110 ($-2$ in two's complement, correct) and c_out = 0. A zero carry out of a subtraction means a borrow occurred, which is right: $3 < 5$.四位元的本意。$r + \bar s + c_{in} = 3 + 10 + 1 = 14 = 5'b01110$。所以 alu_out = 4'b1110(二補數的 $-2$,正確),c_out = 0。減法的進位輸出為零表示發生了借位,這是對的:$3 < 5$。4 ビットでの意図。$r + \bar s + c_{in} = 3 + 10 + 1 = 14 = 5'b01110$。したがって alu_out = 4'b1110(2 の補数で $-2$、正しい)、c_out = 0 である。減算でキャリー出力がゼロということは借りが発生したという意味で、$3 < 5$ だから正しい。5'b00011, $\sim s$ becomes 5'b11010 = 26, $c_{in}$ extends to 5'b00001. The sum is $3 + 26 + 1 = 30 = 5'b11110$. So alu_out = 4'b1110 — the same, because the extra 16 landed above bit 3 — but c_out = 1.語言實際算出來的。$r$ 擴展成 5'b00011,$\sim s$ 變成 5'b11010 = 26,$c_{in}$ 擴展成 5'b00001。總和是 $3 + 26 + 1 = 30 = 5'b11110$。所以 alu_out = 4'b1110 —— 一樣,因為多出來的 16 落在位元 3 以上 —— 但 c_out = 1。言語が実際に計算するもの。$r$ は 5'b00011 へ拡張され、$\sim s$ は 5'b11010 = 26 になり、$c_{in}$ は 5'b00001 へ拡張される。和は $3 + 26 + 1 = 30 = 5'b11110$ である。したがって alu_out = 4'b1110 は同じ——余分な 16 はビット 3 より上に落ちるからである——が、c_out = 1 になる。c_out of one slice to c_in of the next, therefore produces a completely wrong 16-bit answer, not merely a wrong flag. Multi-precision arithmetic, comparison branches and the condition codes of the whole machine all break.造成的損害。結果位元是對的,而進位旗標是反的。每一次借位都被回報成「沒有借位」,反之亦然。於是一個把前一片 slice 的 c_out 接到下一片 c_in 的多片減法,產生的是完全錯誤的 16 位元答案,而不只是一個錯的旗標。多精度算術、比較分支,以及整台機器的條件碼,全部壞掉。被害。結果のビットは正しいのに、キャリーフラグが反転する。借りが生じたのに「借りなし」と報告され、その逆も起こる。あるスライスの c_out を次のスライスの c_in へつなぐ多スライス減算では、フラグが一つ誤るどころか 16 ビットの答えそのものが完全に誤る。多倍長演算も、比較による分岐も、機械全体の条件コードも壊れる。`subr: {c_out, alu_out} = r + {1'b0, ~s} + c_in;
// or, clearer still:
wire [3:0] s_bar = ~s;
wire [3:0] r_bar = ~r;
...
`subr: {c_out, alu_out} = r + s_bar + c_in;
Both force the inversion to happen on four bits and then zero-extend the four-bit result, which is what was meant.兩種寫法都強迫取反在四位元上發生,然後再把四位元的結果零擴展 —— 那才是原意。どちらも反転を 4 ビットで行わせ、その 4 ビットの結果をゼロ拡張する。それが本来の意図である。Operands of the arithmetic, bitwise, unary-minus, bitwise-negation and conditional operators, and the left operand of a shift, are context-determined: they are widened to the expression width (which includes the assignment target) before the operator runs. Operands of the comparison, logical, reduction and shift-count positions, and the contents of a concatenation, are self-determined: they are evaluated at their own width. Therefore ~s in a wide context is not the narrow complement of s, and this bites every time somebody writes a subtractor with an explicit carry out.算術運算子、逐位元運算子、一元負號、逐位元取反與條件運算子的運算元,以及移位運算子的左運算元,都是由上下文決定寬度的:它們會在運算子執行之前被加寬到運算式寬度(其中包含指定目標的寬度)。比較運算子、邏輯運算子、縮減運算子的運算元、移位量的位置,以及串接內部的內容,則是自定寬度的:它們以自己的寬度求值。所以在一個較寬的上下文裡,~s 並不是 s 的窄位元補數,而只要有人寫出帶明確進位輸出的減法器,這件事就會咬人。算術演算子、ビット演算子、単項マイナス、ビット否定、条件演算子のオペランド、およびシフト演算子の左オペランドは文脈で幅が決まる。演算子が働く前に、式の幅(代入先の幅を含む)まで拡張される。比較演算子・論理演算子・縮約演算子のオペランド、シフト量の位置、連結の中身は自己決定であり、自分自身の幅で評価される。したがって幅の広い文脈における ~s は s の狭い補数ではなく、明示的なキャリー出力を持つ減算器を書けば必ずこの件に噛まれる。
The overflow flag. The code computes ovr = c_out ^ alu_out[3]. Signed two's-complement overflow is not that. It is the exclusive-OR of the carry into the most significant bit position and the carry out of it:溢位旗標。程式碼算的是 ovr = c_out ^ alu_out[3]。帶號二補數的溢位不是那個。它是「進入最高位元位置的進位」與「從最高位元位置出來的進位」的互斥或:オーバフローフラグ。コードは ovr = c_out ^ alu_out[3] を計算している。符号付き 2 の補数のオーバフローはそれではない。最上位ビット位置への桁上げと、そこからの桁上げの排他的論理和である。
The second form is the one to remember in words: overflow can only happen when the two addends have the same sign, and it has happened when the result's sign differs from theirs. Adding a positive to a negative can never overflow, because the magnitude of the result is bounded by the larger operand.第二種形式是值得用白話記住的:只有兩個加數同號時才可能溢位,而當結果的符號與它們不同時,溢位就發生了。正數加負數永遠不可能溢位,因為結果的大小不會超過較大的那個運算元。第二の形は言葉で覚えておく価値がある。オーバフローは二つの加数の符号が同じときにしか起こりえず、結果の符号がそれらと異なればオーバフローが起きている。正と負を足してオーバフローすることは決してない。結果の大きさが大きい方のオペランドで抑えられるからである。
Check the slide's formula on the example above. There, $c_4 = 0$ (taking the four-bit interpretation) and $f_3 = 1$, so ovr = 0 ^ 1 = 1 — the slide's flag claims an overflow. But $3 - 5 = -2$ fits comfortably in four bits, so there is none. Using the sign rule: the adder's operands are $r = 0011$ (positive) and $\bar s = 1010$ (negative), different signs, so overflow is impossible. The slide's expression is really "carry out exclusive-OR sign", which is a different and far less useful quantity. The correct implementation needs the internal carry, which the expression r + s + c_in does not expose; you must either compute the carry chain explicitly or use the sign-comparison form:拿投影片的公式去驗上面那個例子。那裡 $c_4 = 0$(採四位元解讀)而 $f_3 = 1$,所以 ovr = 0 ^ 1 = 1 —— 投影片的旗標宣稱溢位了。但 $3 - 5 = -2$ 在四位元裡綽綽有餘,根本沒有溢位。用符號規則看:加法器的運算元是 $r = 0011$(正)與 $\bar s = 1010$(負),符號不同,所以不可能溢位。投影片的運算式其實是「進位輸出互斥或符號位元」,那是另一個、而且有用得多的量。正確的實作需要內部進位,而運算式 r + s + c_in 並不暴露它;你得自己明確算出進位鏈,或改用符號比較的形式:上の例にスライドの式を当ててみる。そこでは(4 ビットで解釈すれば)$c_4 = 0$、$f_3 = 1$ なので ovr = 0 ^ 1 = 1 となり、スライドのフラグはオーバフローを主張する。しかし $3 - 5 = -2$ は 4 ビットに余裕で収まり、オーバフローは起きていない。符号の規則で見れば、加算器のオペランドは $r = 0011$(正)と $\bar s = 1010$(負)で符号が異なるため、オーバフローはありえない。スライドの式は実のところ「キャリー出力と符号ビットの排他的論理和」であり、別物でしかも有用性ははるかに劣る。正しい実装には内部キャリーが必要だが、式 r + s + c_in はそれを外に出さない。キャリー連鎖を明示的に計算するか、符号比較の形を使うことになる。
// correct for the add function; the operands of the adder are r and sx, // where sx is s for `add and ~s for a subtract assign ovr = (r[3] == sx[3]) & (alu_out[3] != r[3]);
Generate and propagate. The last two assignments compute the inverted group-generate and group-propagate signals that feed an external carry-look-ahead generator (in the AMD family, an Am2902). For a four-bit group with bit generate $g_i = r_i s_i$ and bit propagate $p_i = r_i + s_i$:generate 與 propagate。最後那兩個指定算的是反相後的群組 generate 與群組 propagate 訊號,它們要餵給外部的進位預看產生器(在 AMD 家族裡是 Am2902)。對一個四位元群組,位元 generate 為 $g_i = r_i s_i$、位元 propagate 為 $p_i = r_i + s_i$:generate と propagate。最後の二つの代入は、外部のキャリー先見生成器(AMD ファミリでは Am2902)へ入る、反転されたグループ generate とグループ propagate を計算している。4 ビットのグループについて、ビット generate を $g_i = r_i s_i$、ビット propagate を $p_i = r_i + s_i$ とすると:
and the carry out of the group is $C_4 = G + P\,C_0$, computed in two gate delays no matter how many groups you chain. Compare the g_bar expression line by line with the formula and you will find it is exactly $\overline{G}$ with $p_i$ written as (r[i] | s[i]). The p_bar expression uses the exclusive-or form of the bit propagate, $p_i = r_i \oplus s_i$, rather than the OR form. Both are valid in the look-ahead equation — the two differ only when $r_i = s_i = 1$, in which case $g_i = 1$ and the carry is generated anyway, so the value of $P$ is irrelevant — and the XOR form is preferred in practice because the same XOR gate also produces the half-sum needed for the sum bit, saving a gate per bit.而群組的進位輸出是 $C_4 = G + P\,C_0$,不論你串接多少個群組,都只花兩級閘延遲。把 g_bar 的運算式逐行對照這條公式,你會發現它正好是 $\overline{G}$,其中 $p_i$ 寫成 (r[i] | s[i])。p_bar 的運算式用的是位元 propagate 的互斥或形式 $p_i = r_i \oplus s_i$,而不是 OR 形式。兩者在預看方程式裡都有效 —— 它們只在 $r_i = s_i = 1$ 時不同,而那時 $g_i = 1$、進位本來就會產生,所以 $P$ 的值無關緊要 —— 實務上偏好 XOR 形式,因為同一個 XOR 閘也產生和位元所需的半和,每一位元省下一個閘。グループのキャリー出力は $C_4 = G + P\,C_0$ であり、グループをいくつ縦続しても 2 段のゲート遅延で求まる。g_bar の式をこの公式と 1 行ずつ照合すれば、それがちょうど $\overline{G}$ であり、$p_i$ が (r[i] | s[i]) と書かれていることが分かる。p_bar の式はビット propagate の OR 形ではなく排他的論理和形 $p_i = r_i \oplus s_i$ を使っている。どちらも先見の式として妥当である——両者が異なるのは $r_i = s_i = 1$ のときだけで、そのときは $g_i = 1$ でキャリーはどのみち生成されるため $P$ の値は関係ない——が、実務では XOR 形が好まれる。同じ XOR ゲートが和ビットに必要な半加算も作るので、ビットごとにゲートが 1 個節約できるからである。
Both expressions are functions of r and s directly. That is correct for add, where the adder's operands really are $r$ and $s$. It is wrong for the two subtractions, where the adder's second operand is $\bar s$ or $\bar r$, and it is meaningless for the five logic functions, which involve no carry at all. In a correct implementation, $G$ and $P$ must be computed from the actual adder inputs after the function multiplexer, and should be forced to the "no carry" state ($G=0$, $P=0$) for the logic functions so that a look-ahead chain across slices does not fabricate a carry. The slide's version will give wrong carries in a multi-slice subtraction, on top of the carry-polarity problem of §6.4.兩個運算式都直接是 r 與 s 的函數。對 add 來說這是對的,因為那時加法器的運算元真的就是 $r$ 與 $s$。但對兩個減法來說是錯的,因為那時加法器的第二個運算元是 $\bar s$ 或 $\bar r$;而對五個邏輯功能來說則毫無意義,因為它們根本不涉及進位。在正確的實作裡,$G$ 與 $P$ 必須從功能多工器之後的實際加法器輸入去算,而且對邏輯功能應該被強制成「無進位」狀態($G=0$、$P=0$),免得跨 slice 的預看鏈憑空造出一個進位。投影片的版本在多片 slice 的減法裡會給出錯誤的進位,而且是疊在 §6.4 那個進位極性問題之上。どちらの式も r と s の直接の関数である。add ではこれで正しい。加算器のオペランドが本当に $r$ と $s$ だからである。しかし二つの減算では誤りで、そのとき加算器の第 2 オペランドは $\bar s$ あるいは $\bar r$ である。さらに五つの論理機能では無意味である。そこにはキャリーが一切関わらないからである。正しい実装では $G$ と $P$ を機能マルチプレクサの後の実際の加算器入力から計算し、論理機能では「キャリーなし」の状態($G=0$、$P=0$)に強制して、スライドにまたがる先見連鎖が存在しないキャリーを作り出さないようにする。スライドの版は多スライス減算で誤ったキャリーを与え、しかもそれは §6.4 のキャリー極性の問題の上に重なる。
module Q_register (clk, rst, data_in, dest_ctl, qs0, qs3, q); // parameterized register with shift left (up), shift right (down) // and transfer of interface bits to/from companion slices input clk, rst; input [3:0] data_in; input [2:0] dest_ctl; inout qs0, qs3; output [3:0] q; reg [3:0] data; reg q_en; reg_load_R #(4) M1 (data, clk, rst, q_en, q); bufif1 (qs3, data_in[3], (dest_ctl == `ramu || dest_ctl == `ramqu)); bufif1 (qs0, data_in[0], (dest_ctl == `ramd || dest_ctl == `ramqd)); always @ (dest_ctl) case (dest_ctl) `qreg, `ramqd, `ramqu: assign q_en = 1; default: assign q_en = 0; endcase always @ (dest_ctl or qs0 or qs3) case (dest_ctl) // shift left (up) `ramqu: assign data = {data_in[2], data_in[1], data_in[0], qs0}; // shift right (down) `ramqd: assign data = {qs3, data_in[3], data_in[2], data_in[1]}; // pass through `qreg: assign data = data_in; default: assign data = 4'bx; endcase endmodule module reg_load_R (data, clk, reset, load, q); parameter size = 8; input [size-1:0] data; input clk, reset, load; output [size-1:0] q; reg [size-1:0] q; always @ (posedge clk or posedge reset) if (reset == 1) q = 0; else if (load == 1) q = data; endmodule
Three pieces: a generic loadable register instantiated with a parameter override, a pair of three-state drivers for the serial pins, and two decoders built from the destination field.三個部分:一個帶參數覆寫實例化的通用可載入暫存器、一對給串列腳位用的三態驅動器,以及兩個由目的地欄位建出來的解碼器。三つの部分からなる。パラメータを上書きしてインスタンス化された汎用のロード可能レジスタ、シリアルピン用のトライステートドライバ 2 個、そしてデスティネーションフィールドから作られる二つのデコーダである。
The parameter override. reg_load_R #(4) M1 (...) sets size to 4 for this instance. The module's default is 8, which is the trap: omit the #(4) and you instantiate an eight-bit register whose top four bits are unconnected and whose four-bit output is silently truncated from an eight-bit q. There is no error, and the symptom — a Q register that works perfectly — never appears, because the extra bits simply cost area. This is the single strongest argument for named parameter overrides, #(.size(4)), which Verilog-2001 allows and which at least makes the intent legible.參數覆寫。reg_load_R #(4) M1 (...) 把這個實例的 size 設成 4。模組的預設值是 8,而那就是陷阱:漏掉 #(4),你實例化出來的是一個八位元暫存器,它最上面四位元沒接、而四位元的輸出是從八位元的 q 被悄悄截斷來的。不會報錯,而症狀 —— 一個運作完美的 Q 暫存器 —— 永遠不會出現,因為多出來的位元只是多佔面積。這是支持具名參數覆寫 #(.size(4)) 最有力的單一論據;Verilog-2001 允許這種寫法,而且至少讓意圖可讀。パラメータの上書き。reg_load_R #(4) M1 (...) はこのインスタンスの size を 4 に設定する。モジュールの既定値は 8 であり、そこが落とし穴である。#(4) を落とせば 8 ビットのレジスタがインスタンス化され、上位 4 ビットは未接続、4 ビットの出力は 8 ビットの q から黙って切り詰められたものになる。エラーは出ず、症状——完璧に動く Q レジスタ——は決して現れない。余分なビットは面積を食うだけだからである。これは名前付きのパラメータ上書き #(.size(4)) を支持する最も強い論拠である。Verilog-2001 はこの書き方を許し、少なくとも意図が読み取れるようになる。
The loadable register. reg_load_R is the enable-register template from Lecture 6 §4.4: an edge-triggered block with an asynchronous reset and, under the else, a single conditional load with no else branch. The absence of that final else is deliberate and is what makes it a flip-flop with a clock-enable pin rather than a latch. The reason it is not a latch is that the block is edge-sensitive: holding is the default behaviour of a flip-flop, so "assign nothing" means "hold", and nothing is inferred. In a level-sensitive block the same code would infer a latch, which is why Lecture 5 and Lecture 6 state the apparently contradictory rules that they do. (The module again uses blocking assignments in a clocked block; use <=.)可載入暫存器。reg_load_R 就是第 6 講 §4.4 的致能暫存器樣板:一個帶非同步重置的邊緣觸發區塊,在 else 之下有一個單一的條件載入,而且沒有 else 分支。最後那個 else 的缺席是刻意的,也正是它讓這成為一個帶時脈致能腳位的正反器而不是閂鎖。它之所以不是閂鎖,是因為這個區塊是邊緣觸發的:保持本來就是正反器的預設行為,所以「什麼都不指定」就是「保持」,不會推論出任何東西。在位準觸發區塊裡,同樣的程式碼會推論出閂鎖,這也是第 5 講與第 6 講那兩條看似矛盾的規則的由來。(這個模組同樣在時脈區塊裡用阻塞指定;請用 <=。)ロード可能レジスタ。reg_load_R は第 6 講 §4.4 のイネーブル付きレジスタのテンプレートそのものである。非同期リセット付きのエッジ記述ブロックがあり、else の下に条件付きロードが一つだけあって、その else 分岐は無い。最後の else が無いのは意図的であり、それがこれをラッチではなくクロックイネーブルピン付きフリップフロップにしている。ラッチにならないのはブロックがエッジ記述だからである。保持はフリップフロップの既定の振る舞いなので「何も代入しない」は「保持する」を意味し、何も推論されない。レベル記述ブロックなら同じコードはラッチを推論する。第 5 講と第 6 講が一見矛盾する規則を述べているのはそのためである。(このモジュールもクロック同期ブロックでブロッキング代入を使っている。<= にすべきである。)
The three-state serial pins. bufif1 is a Verilog gate primitive: a buffer whose output is driven when its control input is 1 and is high-impedance otherwise. The two instances here are unnamed, which is legal for primitives, and they drive qs3 with data_in[3] while shifting up and qs0 with data_in[0] while shifting down. The purpose is the same as for the register file's ram0/ram3: a slice sends the bit that falls off its end to its neighbour, and because only one slice drives each wire at a time there is no contention.三態的串列腳位。bufif1 是 Verilog 的閘級基本元件:一個緩衝器,控制輸入為 1 時驅動輸出,否則為高阻抗。這裡的兩個實例是未命名的,對基本元件而言這合法;它們在上移時用 data_in[3] 驅動 qs3,在下移時用 data_in[0] 驅動 qs0。目的與暫存器檔的 ram0/ram3 相同:一片 slice 把從自己端點掉出去的位元送給鄰居,而因為每條線同一時間只有一片在驅動,所以不會爭用。トライステートのシリアルピン。bufif1 は Verilog のゲートプリミティブで、制御入力が 1 のとき出力を駆動し、それ以外ではハイインピーダンスになるバッファである。ここの 2 インスタンスは無名だが、プリミティブでは合法である。上方向シフトでは data_in[3] で qs3 を、下方向シフトでは data_in[0] で qs0 を駆動する。目的はレジスタファイルの ram0/ram3 と同じで、スライスは自分の端から落ちるビットを隣へ送る。各配線を同時に駆動するのは 1 スライスだけなので競合は起きない。
The two decoders. The first produces the load enable q_en, which is high for exactly the three destinations that the table says load Q — qreg, ramqd, ramqu. Correct. The second produces the value to load, choosing between an up-shift, a down-shift, a pass-through, and 4'bx for the four destinations that do not load at all, which is a true don't-care because q_en is low for all of them.兩個解碼器。第一個產生載入致能 q_en,它在表上說會載入 Q 的那三個目的地 —— qreg、ramqd、ramqu —— 時為高。正確。第二個產生要載入的值,在上移、下移、直通之間選擇,並對那四個根本不載入的目的地給 4'bx,那是真正的 don't care,因為對它們而言 q_en 都是低的。二つのデコーダ。一つ目はロードイネーブル q_en を作り、表が Q をロードするとする三つのデスティネーション——qreg、ramqd、ramqu——でちょうど High になる。正しい。二つ目はロードする値を作り、上方向シフト・下方向シフト・素通しの中から選び、まったくロードしない四つのデスティネーションには 4'bx を与える。それらでは q_en が Low なので、これは真のドントケアである。
The destination table of §5.6 says that on ramqd the Q register loads Q shifted right and on ramqu it loads Q shifted left: the Q register shifts itself, in parallel with the register file shifting the ALU result. The code shifts data_in, and the top level connects data_in to alu_out. So this model makes the Q register load a shifted copy of the ALU output instead of a shifted copy of its own contents — which is a completely different machine, and one in which the double-length shift that makes multiplication and division work does not exist. The datasheet-faithful version feeds the module's own output back into the shift paths:§5.6 的目的地表說:ramqd 時 Q 暫存器載入的是 Q 右移後的值,ramqu 時載入的是 Q 左移後的值:Q 暫存器移位的是它自己,與暫存器檔移位 ALU 結果並行。但程式碼移位的是 data_in,而頂層把 data_in 接到 alu_out。所以這個模型讓 Q 暫存器載入的是 ALU 輸出的移位副本,而不是它自己內容的移位副本 —— 那是一台完全不同的機器,而且在那台機器裡,讓乘法與除法得以運作的雙倍長度移位根本不存在。忠於資料手冊的版本會把模組自己的輸出接回移位路徑:§5.6 のデスティネーション表は、ramqd では Q レジスタが Q を右シフトした値を、ramqu では Q を左シフトした値をロードするとしている。すなわち Q レジスタは、レジスタファイルが ALU の結果をシフトするのと並行して、自分自身をシフトする。ところがコードがシフトしているのは data_in であり、トップレベルは data_in を alu_out につないでいる。つまりこのモデルでは、Q レジスタは自分の内容のシフト版ではなく ALU 出力のシフト版をロードする。これはまったく別の機械であり、そこには乗算と除算を成り立たせる倍長シフトが存在しない。データシートに忠実な版は、モジュール自身の出力をシフト経路へ戻す。
`ramqu: data = {q[2], q[1], q[0], qs0}; // Q << 1, qs0 in at the bottom
`ramqd: data = {qs3, q[3], q[2], q[1]}; // Q >> 1, qs3 in at the top
`qreg: data = data_in; // load alu_out unshifted
Equally, the bufif1 enables should be the Q-shift destinations only — ramqu for qs3 and ramqd for qs0 — and the data driven should be q[3] and q[0], not data_in[3] and data_in[0]. Including ramu and ramd in the enables, as the slide does, makes the Q register drive the shared Q shift wires during instructions in which the table says Q is not applicable. On a single slice nothing visibly breaks; across four slices it is bus contention.同樣地,bufif1 的致能應該只有 Q 移位那些目的地 —— qs3 對 ramqu、qs0 對 ramqd —— 而且驅動出去的資料應該是 q[3] 與 q[0],不是 data_in[3] 與 data_in[0]。像投影片那樣把 ramu 與 ramd 也放進致能裡,會讓 Q 暫存器在表上說「Q 不適用」的指令期間去驅動共用的 Q 移位線。在單片 slice 上看不出壞處;跨四片 slice 就是匯流排爭用。同様に、bufif1 のイネーブルは Q のシフトを伴うデスティネーションだけ——qs3 には ramqu、qs0 には ramqd——であるべきで、駆動するデータも data_in[3]、data_in[0] ではなく q[3]、q[0] であるべきである。スライドのようにイネーブルへ ramu と ramd を含めると、表が「Q は該当なし」とする命令のあいだに Q レジスタが共有の Q シフト線を駆動してしまう。1 スライスでは目に見える破綻はないが、4 スライスにまたがればバス競合である。
Finally, the two always blocks here use the same procedural-continuous-assignment style as the operand selector, with the same caveats — and the same portable rewrite is to delete the word assign, change the sensitivity list to @*, and be done.最後,這裡的兩個 always 區塊跟運算元選擇器一樣用了程序式連續指定的寫法,注意事項也一樣 —— 而同樣可攜的改寫就是:刪掉 assign 這個字、把敏感列表改成 @*,完事。最後に、ここの二つの always ブロックもオペランドセレクタと同じ手続き型連続代入の流儀で書かれており、注意点も同じである。移植性のある書き換えも同じで、assign という語を削り、感度リストを @* にすれば済む。
module output_selector (mux_out, channel_1, channel_2, sel, enable);
input [3:0] channel_1, channel_2;
input [2:0] sel;
input enable; // active low
output [3:0] mux_out;
assign mux_out = (enable == 0 && sel == `rama) ? channel_1 :
(enable == 0) ? channel_2 : 4'bz;
endmodule
The smallest module in the design and the easiest to misread, because its port names are generic while its connection is specific. The top level instantiates it as output_selector M3 (y, ad, alu_out, instruction[8:6], enable);, so channel_1 is ad, channel_2 is alu_out, and sel is the destination field. Substituting:這是整個設計裡最小的模組,也是最容易讀錯的,因為它的埠名很通用而它的接線很具體。頂層把它實例化成 output_selector M3 (y, ad, alu_out, instruction[8:6], enable);,所以 channel_1 是 ad、channel_2 是 alu_out、sel 是目的地欄位。代入之後:設計中で最も小さいモジュールであり、最も読み違えやすいモジュールでもある。ポート名が一般的である一方、接続は具体的だからである。トップレベルは output_selector M3 (y, ad, alu_out, instruction[8:6], enable); としてインスタンス化するので、channel_1 は ad、channel_2 は alu_out、sel はデスティネーションフィールドである。代入すると次のようになる。
which is exactly the "Output y" column of the destination table: reg_f[a] for rama and alu_out for everything else. The nested conditional operator is being used as a two-level priority multiplexer, which is the natural continuous-assignment idiom for this shape; it synthesises to a 2-to-1 multiplexer followed by a three-state buffer, four bits wide.這正好就是目的地表的「輸出 y」那一欄:rama 時是 reg_f[a],其餘一律是 alu_out。巢狀的條件運算子在這裡被當成兩層的優先多工器,對這種形狀而言那是連續指定最自然的慣用法;它會合成成一個 2 對 1 多工器後面接一個三態緩衝器,寬四位元。これはデスティネーション表の「出力 y」の欄そのものである。rama では reg_f[a]、それ以外はすべて alu_out である。入れ子になった条件演算子は 2 段のプライオリティマルチプレクサとして使われており、この形に対しては連続代入としてごく自然な書き方である。合成すると 2 対 1 のマルチプレクサの後ろにトライステートバッファが続く、4 ビット幅の回路になる。
enable is active low, and the code says so only in a comment. That is how active-low signals become bugs. Verilog has no type system to help, so the convention is naming: call it enable_n, n_enable or enable_bar, pick one convention for the whole project, and the comment becomes unnecessary. The three-state output is appropriate here, because y on a real Am2901 is a chip pin that shares a bus with other devices; this is one of the few places in the design where internal three-state logic is genuinely the right answer, because it is not internal.enable 是低電位有效,而程式碼只在註解裡說了這件事。低電位有效訊號就是這樣變成 bug 的。Verilog 沒有型別系統可以幫忙,所以慣例只能靠命名:叫它 enable_n、n_enable 或 enable_bar,整個專案挑一種用,註解就不必要了。這裡的三態輸出是恰當的,因為真實 Am2901 上的 y 是一支與其他裝置共用匯流排的晶片腳位;這是整個設計裡少數幾個內部三態邏輯真的正確的地方,因為它其實不是內部的。enable はアクティブ Low であり、コードはそれをコメントでしか述べていない。アクティブ Low の信号はこうしてバグになる。Verilog には助けになる型システムが無いので、規約は命名で担うしかない。enable_n、n_enable、enable_bar のどれかに決め、プロジェクト全体で一つの流儀に揃えれば、コメントは不要になる。ここでのトライステート出力は適切である。実物の Am2901 の y は他のデバイスとバスを共有するチップのピンだからである。この設計の中で内部トライステート論理が本当に正しい数少ない箇所であり、それは実のところ内部ではないからである。
module amd2901 (clk, rst, enable, c_in, a, b, d, instruction, ram0, ram3,
qs0, qs3, y, g_bar, p_bar, ovr, c_out, f_0, f_3);
input clk, rst, c_in, enable;
input [3:0] a, b, d;
input [8:0] instruction;
inout ram0, ram3;
inout qs0, qs3;
output [3:0] y;
output g_bar, p_bar, ovr, c_out, f_0, f_3;
wire [3:0] ad, bd, q, r, s, alu_out;
Register_file M1 (
clk, // system clock
rst, // global reset
a, b, // addresses into the register file
alu_out, // data path from the ALU
instruction[8:6], // microinstruction bits for the destination
ram0, ram3, // LSB and MSB of the memory word
ad, bd // the two 4-bit read ports
);
ALU_amd2901 M2 (r, s, c_in, instruction[5:3], alu_out, g_bar, p_bar, ovr, c_out);
output_selector M3 (y, ad, alu_out, instruction[8:6], enable);
operand_selector M4 (d, ad, bd, q, instruction[2:0], r, s);
Q_register M5 (clk, rst, alu_out, instruction[8:6], qs0, qs3, q);
assign f_0 = (alu_out == 4'b0) ? 0 : 1'bz;
assign f_3 = alu_out[3];
endmodule
Five instances and two assignments. The wires ad, bd, q, r, s and alu_out are the internal nets of the loop in §5.2, and tracing them through the five port lists is the most useful ten minutes you can spend on this design.五個實例與兩個指定。wire ad、bd、q、r、s 與 alu_out 就是 §5.2 那個迴路的內部 net,而把它們穿過五份埠列表追一遍,是你能花在這個設計上最有用的十分鐘。インスタンス 5 個と代入 2 本。ワイヤ ad、bd、q、r、s、alu_out は §5.2 のループの内部ネットであり、それらを 5 つのポートリストを通して追いかける 10 分が、この設計に費やせる最も有益な時間である。
One structural question deserves an answer, because the diagram looks alarming: is there a combinational loop? The loop on the diagram is register file → operand selector → ALU → register file. Follow it in the netlist: ad and bd come from flip-flop outputs through a read multiplexer; they feed the operand selector combinationally; r and s feed the ALU combinationally; alu_out feeds the register file's data multiplexer combinationally and then stops at the write port, which is a clocked input. It also feeds the Q register's shift multiplexer and stops at that register's D input. And it feeds the output selector, which leaves the chip. So every path from a flip-flop output back to a flip-flop input passes through exactly one combinational cone and terminates at a clocked element. No loop. The apparent cycle in the block diagram is closed by registers, which is exactly how every datapath loop is closed.有一個結構性的問題值得回答,因為那張圖看起來很嚇人:這裡有組合迴路嗎?圖上的迴路是暫存器檔 → 運算元選擇器 → ALU → 暫存器檔。在 netlist 裡跟著走:ad 與 bd 經由讀取多工器來自正反器輸出;它們以組合方式餵進運算元選擇器;r 與 s 以組合方式餵進 ALU;alu_out 以組合方式餵進暫存器檔的 data 多工器,然後停在寫入埠,那是一個時脈同步的輸入。它也餵進 Q 暫存器的移位多工器,停在那個暫存器的 D 輸入。它還餵進輸出選擇器,然後離開晶片。所以每一條從正反器輸出回到正反器輸入的路徑,都恰好穿過一團組合邏輯錐並終止於一個時脈元件。沒有迴路。方塊圖上那個看似的循環,是由暫存器閉合的,而每一條資料路徑迴路都正是這樣閉合的。一つ構造上の問いに答えておく価値がある。あの図は不穏に見えるからである。組合せループはあるのか。図のループはレジスタファイル → オペランドセレクタ → ALU → レジスタファイルである。netlist でたどってみる。ad と bd は読み出しマルチプレクサを介してフリップフロップ出力から来て、組合せ的にオペランドセレクタへ入る。r と s は組合せ的に ALU へ入る。alu_out は組合せ的にレジスタファイルの data マルチプレクサへ入り、クロック同期の入力である書き込みポートで止まる。また Q レジスタのシフト用マルチプレクサへも入り、そのレジスタの D 入力で止まる。さらに出力セレクタへ入ってチップの外へ出る。つまりフリップフロップ出力からフリップフロップ入力へ戻るどの経路も、ちょうど一つの組合せコーンを通ってクロック素子で終わる。ループは無い。ブロック図上の見かけの循環はレジスタによって閉じられており、あらゆるデータパスのループはまさにそのように閉じられる。
assign f_0 = (alu_out == 4'b0) ? 0 : 1'bz; drives a logic zero when the slice's nibble is zero, and floats otherwise. Now wire four slices' f_0 pins together with a pull-up resistor, which is the whole point of an open-drain flag: the shared line reads low if any slice drives it low, that is if any nibble is zero. What you want is "the whole word is zero", which means every nibble is zero — a wired-AND. So the condition must be inverted: a slice should pull the line low when its nibble is non-zero and float when it is zero, and then the pull-up makes the line high only when every slice is floating.assign f_0 = (alu_out == 4'b0) ? 0 : 1'bz; 在這片 slice 的 nibble 是零時驅動邏輯零,其餘時候浮接。現在把四片 slice 的 f_0 腳位接在一起並加一個上拉電阻,那正是開汲極旗標的用意:只要任何一片 slice 把這條共用線拉低,線上就讀到低,也就是任何一個 nibble 為零就低。但你要的是「整個字為零」,那表示每一個 nibble 都為零 —— 是一個 wired-AND。所以條件必須反過來:一片 slice 應該在它的 nibble 非零時把線拉低、為零時浮接,這樣上拉才會在每片都浮接時把線拉高。assign f_0 = (alu_out == 4'b0) ? 0 : 1'bz; は、このスライスのニブルがゼロのとき論理ゼロを駆動し、それ以外では浮かせる。ここで 4 スライスの f_0 ピンを互いに接続してプルアップ抵抗を付ける。それがオープンドレインのフラグの眼目である。共有線はいずれかのスライスが Low に引けば Low を示す。すなわちどれか一つのニブルがゼロなら Low になる。欲しいのは「ワード全体がゼロ」であり、それはすべてのニブルがゼロ、つまりワイヤード AND である。したがって条件は反転しなければならない。スライドは自分のニブルが非ゼロのときに線を Low に引き、ゼロのときは浮かせるべきで、そうすればすべてが浮いているときだけプルアップが線を High にする。
assign f_0 = (alu_out == 4'b0) ? 1'bz : 1'b0; // open-drain, wired-AND across slices
The same reasoning explains the netlist on the final slide, which shows a four-input OR gate — computing "this nibble is non-zero" — feeding an inverting three-state cell whose output is labelled zero. The hardware in the picture implements the correct function; the RTL as printed does not. f_3 = alu_out[3] is simply the sign bit and is right; on a multi-slice machine only the top slice's f_3 is meaningful.同樣的推理也解釋了最後一張投影片上的 netlist:圖上是一個四輸入 OR 閘 —— 算的是「這個 nibble 非零」—— 餵給一個輸出標示為 zero 的反相三態元件。圖裡的硬體實現的是正確的函數;印出來的 RTL 則不是。f_3 = alu_out[3] 就單純是符號位元,這是對的;在多片 slice 的機器上,只有最高那片的 f_3 有意義。同じ理屈が最後のスライドの netlist も説明する。そこには「このニブルは非ゼロである」を計算する 4 入力 OR ゲートがあり、出力が zero と記された反転トライステートセルへ入っている。図のハードウェアは正しい関数を実装しており、印刷された RTL はそうではない。f_3 = alu_out[3] は単に符号ビットであり、これは正しい。多スライスの機械では最上位スライスの f_3 だけが意味を持つ。
Problem: The register file's reset loop is for (row_index = 0; row_index < 16; row_index = row_index + 1) with reg [4:0] row_index. (a) Explain precisely what happens in simulation if row_index is declared [3:0]. (b) What does a synthesis tool do? (c) Rewrite the loop so that a 4-bit index is safe. (d) The register file has 16 entries; how wide must the index be if the file is parameterised to N entries, and what is the one value of N for which the naive answer is wrong?問題:暫存器檔的重置迴圈是 for (row_index = 0; row_index < 16; row_index = row_index + 1),搭配 reg [4:0] row_index。(a) 若 row_index 宣告成 [3:0],請精確說明模擬時會發生什麼事。(b) 合成工具會怎麼做?(c) 改寫這個迴圈,讓 4 位元索引也安全。(d) 這個暫存器檔有 16 項;若把檔案參數化成 N 項,索引必須多寬?又有哪一個 N 值會讓直覺的答案是錯的?問題:レジスタファイルのリセットループは for (row_index = 0; row_index < 16; row_index = row_index + 1) で、reg [4:0] row_index を伴う。(a) row_index を [3:0] と宣言した場合、シミュレーションで正確に何が起こるかを説明せよ。(b) 合成ツールはどうするか。(c) 4 ビットの変数でも安全になるようループを書き直せ。(d) このファイルは 16 エントリだが、N エントリにパラメータ化した場合、変数は何ビット必要か。また素朴な答えが誤りになる唯一の N はどれか。
(a) A 4-bit register can hold 0 to 15. After the iteration with row_index = 15, the update row_index + 1 produces 16, which is truncated to 4'b0000. The test 0 < 16 is true, so the loop body runs again, for ever. Because the loop is inside a zero-delay procedural block, simulation time never advances: the simulator appears to hang immediately after the first reset edge, producing no waveform and no output. Some simulators detect the condition and abort with an "iteration limit exceeded" message; most do not.(a) 4 位元暫存器能存 0 到 15。在 row_index = 15 那次迭代之後,更新 row_index + 1 產生 16,被截斷成 4'b0000。測試 0 < 16 為真,於是迴圈本體再跑一次,永遠跑下去。因為迴圈在一個零延遲的程序區塊裡,模擬時間永遠不會前進:模擬器在第一個重置邊緣之後立刻看起來卡住,不產生波形也沒有輸出。有些模擬器會偵測到這個狀況並以「超過迭代上限」中止;多數不會。(a) 4 ビットのレジスタは 0 から 15 までしか保持できない。row_index = 15 の反復のあと、更新 row_index + 1 は 16 を生むが 4'b0000 に切り詰められる。判定 0 < 16 は真なのでループ本体がもう一度走り、以後永遠に続く。ループは遅延ゼロの手続き型ブロック内にあるため、シミュレーション時刻は前へ進まない。最初のリセットエッジの直後からシミュレータは固まったように見え、波形も出力も出ない。この状態を検出して「反復回数の上限超過」で中断するシミュレータもあるが、多くはそうしない。
(b) Synthesis must unroll every loop statically, so it evaluates the bounds at elaboration time. With a 4-bit index the termination condition is never false and the loop count cannot be determined, so the tool reports an error such as "loop is not statically determinable" or "unroll limit exceeded" and refuses to elaborate the module. This is one of the rare cases where synthesis catches a bug that simulation only hangs on.(b) 合成必須靜態展開每一個迴圈,所以它在 elaboration 時求值邊界。用 4 位元索引時,終止條件永遠不為假、迴圈次數無法決定,於是工具報出類似「loop is not statically determinable」或「unroll limit exceeded」的錯誤,拒絕 elaborate 這個模組。這是少數幾個「合成抓得到、而模擬只會卡住」的 bug。(b) 合成はすべてのループを静的に展開しなければならないので、境界を elaboration 時に評価する。4 ビットの変数では終了条件が偽にならず、反復回数が決定できないため、ツールは「loop is not statically determinable」や「unroll limit exceeded」のようなエラーを出してモジュールの elaborate を拒否する。合成が捕まえられ、シミュレーションは固まるだけ、という稀な種類のバグである。
(c) Compare against the last valid index instead of the count, and break out of the comparison before the wrap:(c) 改成與最後一個合法索引比較,並在繞回之前就跳出比較:(c) 件数ではなく最後の有効な添字と比較し、折り返す前に比較を抜けるようにする。
integer i; // an integer is 32 bits; always safe
...
if (rst) for (i = 0; i < 16; i = i + 1) ab_data[i] <= 0;
Using an integer for a loop index is the idiomatic answer and removes the problem entirely, because an integer is at least 32 bits wide and is never the limiting factor. (An integer used only as a loop index is not synthesised into hardware; the loop is unrolled and the index disappears.)用 integer 當迴圈索引是慣用的答案,而且徹底消除這個問題,因為 integer 至少 32 位元寬,永遠不會是限制因素。(只拿來當迴圈索引的 integer 不會被合成成硬體;迴圈被展開,索引就消失了。)ループ変数に integer を使うのが定石の答えであり、問題を完全に消し去る。integer は少なくとも 32 ビット幅で、制約になることがないからである。(ループ変数としてのみ使われる integer はハードウェアに合成されない。ループは展開され、変数は消える。)
(d) For N entries, the index must be able to represent N itself, which needs $\lceil \log_2(N+1) \rceil$ bits — that is, $clog2(N+1), or equivalently $clog2(N)+1 whenever N is a power of two. The naive answer $clog2(N) is wrong for exactly the powers of two, which is the only case anybody ever uses, and that is why this bug is so common. For N = 16, $clog2(16) = 4 and you need 5.(d) 對 N 項而言,索引必須能表示 N 本身,所以需要 $\lceil \log_2(N+1) \rceil$ 個位元 —— 也就是 $clog2(N+1),或在 N 是 2 的冪次時等價於 $clog2(N)+1。直覺的答案 $clog2(N) 恰恰在 2 的冪次時是錯的,而那又是大家唯一會用的情況,這正是這個 bug 這麼常見的原因。對 N = 16,$clog2(16) = 4,而你需要 5。(d) N エントリなら、変数は N そのものを表現できなければならないので $\lceil \log_2(N+1) \rceil$ ビット、すなわち $clog2(N+1) が必要である。N が 2 のべき乗なら $clog2(N)+1 と等価である。素朴な答え $clog2(N) がまさに誤りになるのは 2 のべき乗のときであり、しかも実際に使われるのはその場合だけである。このバグがこれほど多い理由はそこにある。N = 16 では $clog2(16) = 4 だが、必要なのは 5 である。
Problem: With r = 4'b1001, s = 4'b0011, c_in = 1, evaluate the slide's `subr line {c_out, alu_out} = r + ~s + c_in;. Give (a) the value a reader expects from four-bit intuition, (b) the value IEEE 1364 actually produces, (c) the correct borrow flag for $9 - 3$, and (d) the value of the slide's ovr expression together with whether a real signed overflow occurred.問題:取 r = 4'b1001、s = 4'b0011、c_in = 1,求值投影片那行 `subr:{c_out, alu_out} = r + ~s + c_in;。請給出 (a) 讀者以四位元直覺預期的值、(b) IEEE 1364 實際產生的值、(c) $9 - 3$ 正確的借位旗標,以及 (d) 投影片 ovr 運算式的值,並說明是否真的發生了帶號溢位。問題:r = 4'b1001、s = 4'b0011、c_in = 1 として、スライドの `subr の行 {c_out, alu_out} = r + ~s + c_in; を評価せよ。(a) 4 ビットの直感から期待される値、(b) IEEE 1364 が実際に生む値、(c) $9 - 3$ に対する正しい借りフラグ、(d) スライドの ovr 式の値と、実際に符号付きオーバフローが起きたかどうか、をそれぞれ示せ。
(a) Four-bit intuition. $\sim s = 4'b1100 = 12$. Then $9 + 12 + 1 = 22 = 5'b10110$, so alu_out = 4'b0110 = 6 and c_out = 1. And indeed $9 - 3 = 6$, with c_out = 1 meaning "no borrow", which is correct because $9 \ge 3$.(a) 四位元直覺。$\sim s = 4'b1100 = 12$。於是 $9 + 12 + 1 = 22 = 5'b10110$,所以 alu_out = 4'b0110 = 6、c_out = 1。而 $9 - 3 = 6$ 確實如此,c_out = 1 表示「沒有借位」,這是正確的,因為 $9 \ge 3$。(a) 4 ビットの直感。$\sim s = 4'b1100 = 12$ である。$9 + 12 + 1 = 22 = 5'b10110$ なので alu_out = 4'b0110 = 6、c_out = 1 となる。実際 $9 - 3 = 6$ であり、c_out = 1 は「借りなし」を意味して $9 \ge 3$ と整合する。
(b) What the language does. The assignment target is the five-bit concatenation, so the expression width is five and every context-determined operand is widened first. s becomes 5'b00011, and ~s becomes 5'b11100 = 28. The sum is $9 + 28 + 1 = 38 = 6'b100110$, truncated to five bits: 5'b00110. So alu_out = 4'b0110 = 6 — still correct — and c_out = 0.(b) 語言實際做的事。指定目標是五位元串接,所以運算式寬度是五,每個由上下文決定的運算元都會先被加寬。s 變成 5'b00011,~s 變成 5'b11100 = 28。總和是 $9 + 28 + 1 = 38 = 6'b100110$,截斷成五位元就是 5'b00110。所以 alu_out = 4'b0110 = 6 —— 仍然正確 —— 而 c_out = 0。(b) 言語が実際に行うこと。代入先が 5 ビットの連結なので式の幅は 5 となり、文脈で決まるオペランドはまず拡張される。s は 5'b00011 に、~s は 5'b11100 = 28 になる。和は $9 + 28 + 1 = 38 = 6'b100110$ で、5 ビットに切り詰めると 5'b00110 である。したがって alu_out = 4'b0110 = 6——これは依然正しい——で、c_out = 0 となる。
(c) The correct flag is 1 (no borrow). The code produces 0. Compare with the example in §6.4, where the true flag was 0 and the code produced 1: the carry flag is systematically inverted for both subtractions, in every case. The result bits are always right, because the spurious 16 contributed by the widened complement lands entirely above bit 3; only the carry is corrupted. That is precisely why the bug survives a casual test — you have to look at the flag, and most testbenches check the data.(c) 正確的旗標是 1(沒有借位)。程式碼產生 0。和 §6.4 的例子對照:那裡真正的旗標是 0 而程式碼產生 1 —— 兩個減法在所有情況下,進位旗標都被系統性地反轉。結果位元永遠是對的,因為被加寬的補數多貢獻的那個 16 完全落在位元 3 以上;只有進位被破壞。這正是這個 bug 能躲過隨便一測的原因 —— 你得去看旗標,而多數測試平台只檢查資料。(c) 正しいフラグは 1(借りなし)である。コードは 0 を出す。§6.4 の例と比べると、あちらでは真のフラグが 0 でコードは 1 を出していた。二つの減算のどちらでも、キャリーフラグはあらゆる場合に系統的に反転する。結果のビットは常に正しい。拡張された補数が余分に寄与する 16 がすべてビット 3 より上に落ちるからである。壊れるのはキャリーだけである。このバグが軽い試験をすり抜ける理由はそこにある。フラグを見なければならないが、多くのテストベンチはデータしか調べない。
(d) Overflow. The slide computes ovr = c_out ^ alu_out[3]. Using its own (wrong) c_out = 0 and alu_out[3] = 0, it reports ovr = 0. Using the four-bit c_out = 1 it would report ovr = 1. Neither is a derivation of the real condition. The real answer: the adder's operands are $r = 1001$ and $\sim s = 1100$, both negative when read as signed, and the result $0110$ is positive — same-sign operands, different-sign result, so a signed overflow did occur in the underlying addition. But the operation being performed is an unsigned subtraction $9 - 3$, for which the meaningful flag is the borrow, not the overflow; reading the signed-overflow flag after an unsigned operation is a category error, and a correct ALU exposes both flags and lets the microcode choose. This is why the flag definitions matter so much more than the arithmetic.(d) 溢位。投影片算的是 ovr = c_out ^ alu_out[3]。用它自己那個(錯的)c_out = 0 與 alu_out[3] = 0,它回報 ovr = 0。若用四位元的 c_out = 1,它會回報 ovr = 1。兩者都不是真正條件的推導。真正的答案是:加法器的運算元是 $r = 1001$ 與 $\sim s = 1100$,以帶號讀都是負的,而結果 $0110$ 是正的 —— 同號運算元、異號結果,所以底層那次加法確實發生了帶號溢位。但正在執行的運算是無號減法 $9 - 3$,它有意義的旗標是借位,不是溢位;在無號運算之後去讀帶號溢位旗標是範疇錯誤,而正確的 ALU 會把兩個旗標都暴露出來,讓微碼自己選。這也正是為什麼旗標的定義比算術本身重要得多。(d) オーバフロー。スライドは ovr = c_out ^ alu_out[3] を計算する。自身の(誤った)c_out = 0 と alu_out[3] = 0 を使えば ovr = 0 と報告する。4 ビットの c_out = 1 を使えば ovr = 1 となる。どちらも本来の条件の導出ではない。真の答えはこうである。加算器のオペランドは $r = 1001$ と $\sim s = 1100$ で、符号付きとして読めばどちらも負であり、結果 $0110$ は正である。同符号のオペランドに対して異符号の結果なので、下層の加算では符号付きオーバフローが確かに起きている。しかし実行されている演算は符号なし減算 $9 - 3$ であり、意味のあるフラグはオーバフローではなく借りである。符号なし演算のあとに符号付きオーバフローフラグを読むのは範疇の取り違えであり、正しい ALU は両方のフラグを出してマイクロコードに選ばせる。フラグの定義が算術そのものよりはるかに重要である理由がここにある。
Problem: alu_out = 4'b1011 and the Q register currently holds 4'b0110. For each destination give the word written to the register file at address b, the value loaded into Q, and the state of all four serial pins (driven with what value, or high-impedance, and whether the slice is driving or receiving). Use the slide's code, then say where the datasheet would differ.問題:alu_out = 4'b1011,Q 暫存器目前存著 4'b0110。請對每個目的地給出寫進暫存器檔位址 b 的字、載入 Q 的值,以及四支串列腳位的狀態(被驅動成什麼值,或高阻抗;以及這片 slice 是在驅動還是在接收)。先用投影片的程式碼作答,再說明資料手冊會在哪裡不同。問題:alu_out = 4'b1011、Q レジスタは現在 4'b0110 を保持している。各デスティネーションについて、レジスタファイルのアドレス b に書かれるワード、Q にロードされる値、4 本のシリアルピンの状態(どの値で駆動されるか、あるいはハイインピーダンスか、またこのスライスが駆動側か受信側か)を示せ。まずスライドのコードで答え、次にデータシートならどこが違うかを述べよ。
(a) ramf (b) ramu with ram0 = 1 (c) ramqd with ram3 = 0 and qs3 = 1 (d) nop(a) ramf (b) ramu,ram0 = 1 (c) ramqd,ram3 = 0 且 qs3 = 1 (d) nop(a) ramf (b) ramu、ram0 = 1 (c) ramqd、ram3 = 0 かつ qs3 = 1 (d) nop
(a) ramf: ram_en = 1, data = f = 4'b1011, so the register file at address b receives 1011 unshifted. q_en = 0, so Q holds 0110. All four pins high-impedance: ram3 and ram0 are driven only for shift destinations, and the bufif1 enables are false.(a) ramf:ram_en = 1,data = f = 4'b1011,所以位址 b 的暫存器檔收到未移位的 1011。q_en = 0,所以 Q 保持 0110。四支腳位全是高阻抗:ram3 與 ram0 只在移位目的地時才驅動,而 bufif1 的致能為假。(a) ramf:ram_en = 1、data = f = 4'b1011 なので、アドレス b のレジスタファイルはシフトされていない 1011 を受け取る。q_en = 0 なので Q は 0110 を保持する。4 本のピンはすべてハイインピーダンスである。ram3 と ram0 はシフトのデスティネーションでしか駆動されず、bufif1 のイネーブルも偽だからである。
(b) ramu with ram0 = 1: data = {f[2], f[1], f[0], ram0} = {0, 1, 1, 1} = 4'b0111 — the ALU result shifted left with a 1 entering from the slice below. The register file at b gets 0111. ram3 is driven by this slice with f[3] = 1, sending the displaced top bit upward; ram0 is an input here, supplied by the slice below. q_en = 0, so Q holds 0110. On the slide's code, qs3 is also driven, with data_in[3] = 1, because the enable includes ramu — which the table marks "not applicable" for Q. qs0 is high-impedance. The datasheet would leave both Q pins floating on ramu.(b) ramu,ram0 = 1:data = {f[2], f[1], f[0], ram0} = {0, 1, 1, 1} = 4'b0111 —— ALU 結果左移一位,下面那片 slice 送來的 1 從底部進入。位址 b 的暫存器檔拿到 0111。ram3 由這片 slice 驅動成 f[3] = 1,把被擠出去的最高位元往上送;ram0 在這裡是輸入,由下面那片提供。q_en = 0,所以 Q 保持 0110。照投影片的程式碼,qs3 也被驅動,值是 data_in[3] = 1,因為致能條件裡包含了 ramu —— 而表上標示 Q 在 ramu 時「不適用」。qs0 是高阻抗。資料手冊的版本會在 ramu 時讓兩支 Q 腳位都浮接。(b) ramu、ram0 = 1:data = {f[2], f[1], f[0], ram0} = {0, 1, 1, 1} = 4'b0111、すなわち ALU の結果を 1 ビット左シフトし、下位スライスからの 1 が下端に入る。アドレス b のレジスタファイルは 0111 を受け取る。ram3 はこのスライスが f[3] = 1 で駆動し、押し出された最上位ビットを上へ送る。ram0 はここでは入力で、下位スライスが供給する。q_en = 0 なので Q は 0110 を保持する。スライドのコードでは qs3 も data_in[3] = 1 で駆動される。イネーブルに ramu が含まれているからであり、表は ramu のとき Q は「該当なし」としている。qs0 はハイインピーダンスである。データシートどおりなら ramu では Q の両ピンとも浮いたままになる。
(c) ramqd with ram3 = 0, qs3 = 1: Register file: data = {ram3, f[3], f[2], f[1]} = {0, 1, 0, 1} = 4'b0101 — the ALU result shifted right with a 0 entering from above. ram0 is driven by this slice with f[0] = 1, sending the displaced bottom bit downward; ram3 is an input. Q register: q_en = 1 and the slide's code loads {qs3, data_in[3], data_in[2], data_in[1]} = {1, 1, 0, 1} = 4'b1101. qs0 is driven with data_in[0] = 1; qs3 is an input.(c) ramqd,ram3 = 0、qs3 = 1:暫存器檔:data = {ram3, f[3], f[2], f[1]} = {0, 1, 0, 1} = 4'b0101 —— ALU 結果右移一位,上方送來的 0 從頂部進入。ram0 由這片 slice 驅動成 f[0] = 1,把被擠出去的最低位元往下送;ram3 是輸入。Q 暫存器:q_en = 1,而投影片的程式碼載入 {qs3, data_in[3], data_in[2], data_in[1]} = {1, 1, 0, 1} = 4'b1101。qs0 被驅動成 data_in[0] = 1;qs3 是輸入。(c) ramqd、ram3 = 0、qs3 = 1:レジスタファイル:data = {ram3, f[3], f[2], f[1]} = {0, 1, 0, 1} = 4'b0101、すなわち ALU の結果を 1 ビット右シフトし、上から来た 0 が上端に入る。ram0 はこのスライスが f[0] = 1 で駆動し、押し出された最下位ビットを下へ送る。ram3 は入力である。Q レジスタ:q_en = 1 で、スライドのコードは {qs3, data_in[3], data_in[2], data_in[1]} = {1, 1, 0, 1} = 4'b1101 をロードする。qs0 は data_in[0] = 1 で駆動され、qs3 は入力である。
Where the datasheet differs: Q should load its own contents shifted right with qs3 entering the top, that is {qs3, q[3], q[2], q[1]} = {1, 0, 1, 1} = 4'b1011, and the bit it sends out on qs0 should be q[0] = 0, not data_in[0] = 1. The slide's model loads 1101 and sends out 1; the real part loads 1011 and sends out 0. This is the discrepancy of §6.6 shown in numbers, and it is the reason the slide's Q register cannot perform the double-length shift that §7.1 needs.資料手冊不同之處:Q 應該載入它自己的內容右移、由 qs3 從頂部進入,也就是 {qs3, q[3], q[2], q[1]} = {1, 0, 1, 1} = 4'b1011,而它由 qs0 送出的位元應該是 q[0] = 0,不是 data_in[0] = 1。投影片的模型載入 1101 並送出 1;真實的元件載入 1011 並送出 0。這就是 §6.6 那個不一致用數字呈現出來的樣子,也是投影片的 Q 暫存器無法執行 §7.1 所需雙倍長度移位的原因。データシートとの相違:Q は自分自身の内容を右シフトし、上端には qs3 が入るべきで、すなわち {qs3, q[3], q[2], q[1]} = {1, 0, 1, 1} = 4'b1011 である。また qs0 から送り出すビットは data_in[0] = 1 ではなく q[0] = 0 であるべきである。スライドのモデルは 1101 をロードして 1 を送り出すが、実物は 1011 をロードして 0 を送り出す。これが §6.6 の食い違いを数値で示したものであり、スライドの Q レジスタが §7.1 に必要な倍長シフトを実行できない理由でもある。
(d) nop: ram_en = 0, so nothing is written to the register file and the value of data — 4'bxxxx — never reaches a flip-flop. q_en = 0, so Q holds. All four pins high-impedance. The ALU still computes and y still shows alu_out, so the flags are set and a microprogram can branch on them: nop is "compute and test without storing", not "do nothing".(d) nop:ram_en = 0,所以暫存器檔什麼都不寫,而 data 的值 4'bxxxx 永遠到不了任何正反器。q_en = 0,所以 Q 保持。四支腳位全是高阻抗。ALU 仍然在算,y 仍然顯示 alu_out,所以旗標有被設定,微程式可以依它們分支:nop 的意思是「算出來並測試但不儲存」,不是「什麼都不做」。(d) nop:ram_en = 0 なのでレジスタファイルには何も書かれず、data の値 4'bxxxx がフリップフロップに届くことはない。q_en = 0 なので Q は保持される。4 本のピンはすべてハイインピーダンスである。ALU は計算を続け y も alu_out を示すので、フラグは設定され、マイクロプログラムはそれで分岐できる。nop は「何もしない」ではなく「計算して判定するが格納しない」である。
Everything in the 2901 exists for a reason, and the reason for the double-length shift — the ramqd destination that shifts the register file and the Q register together — is multiplication. There is no multiplier in the datapath; there is a shift-and-add loop in the microcode, and following one all the way through is the best possible test of whether you have understood the machine.2901 裡的每樣東西都有存在的理由,而雙倍長度移位 —— 也就是讓暫存器檔與 Q 暫存器一起移位的 ramqd 目的地 —— 的理由就是乘法。這條資料路徑裡沒有乘法器;乘法是微碼裡的一個移位相加迴圈,而把它從頭到尾走一遍,是檢驗你是否真的懂這台機器的最好測試。2901 のすべての要素には存在理由があり、倍長シフト——レジスタファイルと Q レジスタを一緒にシフトする ramqd デスティネーション——の理由は乗算である。このデータパスに乗算器は無い。あるのはマイクロコード内のシフト加算ループであり、それを最後までたどることが、この機械を本当に理解したかを測る最良の試験になる。
The algorithm is the one you do by hand, read from the bottom. To compute $M \times Q$, keep a double-length accumulator in two halves: the high half in a register-file word, the low half in the Q register. Repeat once per bit of the multiplier: if the bit of $Q$ that is about to fall off the bottom is 1, add the multiplicand to the high half; then shift the whole double-length pair right by one. After four iterations the product occupies the pair, high half first, and the multiplier has been consumed bit by bit.這個演算法就是你用手算的那一個,由下往上讀。要算 $M \times Q$,就用兩個半邊保存一個雙倍長度的累加器:高半邊放在暫存器檔的一個字裡,低半邊放在 Q 暫存器裡。乘數的每一個位元重複一次:如果 $Q$ 底部即將掉出去的那個位元是 1,就把被乘數加到高半邊;然後把整個雙倍長度的一對右移一位。四次迭代之後,乘積就佔據這一對、高半邊在前,而乘數則被一位一位地消耗掉了。アルゴリズムは手計算でやるのと同じもので、下から読む。$M \times Q$ を計算するには、倍長のアキュムレータを二つの半分に分けて持つ。上半分はレジスタファイルのワード、下半分は Q レジスタである。乗数のビットごとに次を繰り返す。$Q$ の下端からこぼれようとしているビットが 1 なら被乗数を上半分に加える。そのうえで倍長の対を丸ごと 1 ビット右シフトする。4 回の反復のあと、積はこの対に上半分を先頭として収まり、乗数は 1 ビットずつ消費され尽くしている。
The bit that leaves the bottom of the register-file half must enter the top of the Q half. In 2901 terms, ram0 must be connected to qs3. And the bit that enters the top of the register-file half must be the carry out of the add, so that an unsigned sum of five bits survives the shift: ram3 is tied to c_out. Both connections are made outside the slice, by the system designer, which is exactly why those pins exist. This is the moment where the bit-slice philosophy pays off — the chip does not know what a multiply is, and the wiring supplies the missing idea.從暫存器檔那半邊底部離開的位元,必須進入 Q 那半邊的頂部。以 2901 的術語說,ram0 必須接到 qs3。而進入暫存器檔半邊頂部的位元,必須是那次加法的進位輸出,這樣五位元的無號和才能在移位中存活:ram3 接到 c_out。這兩條連線都在 slice 外面、由系統設計者接上,而這正是那些腳位存在的理由。這就是 bit-slice 哲學回本的時刻 —— 晶片不知道什麼叫乘法,而接線補上了缺的那個想法。レジスタファイル側の半分の下端から出るビットは、Q 側の半分の上端へ入らなければならない。2901 の用語で言えば ram0 を qs3 につなぐ。そしてレジスタファイル側の上端へ入るビットは、その加算のキャリー出力でなければならない。そうしてはじめて 5 ビットの符号なしの和がシフトを生き延びる。すなわち ram3 を c_out につなぐ。どちらの接続もスライスの外でシステム設計者が行うものであり、それらのピンが存在する理由がまさにこれである。ビットスライスの思想が報われる瞬間である。チップは乗算が何かを知らず、足りない発想を配線が補っている。
Set up: multiplicand $M = 5 = 0101$ in register-file word 1; accumulator $ACC = 0 = 0000$ in word 0; multiplier $3 = 0011$ in Q. Addresses a = 1 (the multiplicand), b = 0 (the accumulator). Wiring: ram0 → qs3, ram3 ← c_out, qs0 tied low. Each step is one microinstruction, chosen by testing q[0].設定:被乘數 $M = 5 = 0101$ 放在暫存器檔的字 1;累加器 $ACC = 0 = 0000$ 放在字 0;乘數 $3 = 0011$ 放在 Q。位址 a = 1(被乘數)、b = 0(累加器)。接線:ram0 → qs3、ram3 ← c_out、qs0 接低。每一步就是一道微指令,依 q[0] 的測試結果選擇。設定:被乗数 $M = 5 = 0101$ をレジスタファイルのワード 1 に、アキュムレータ $ACC = 0 = 0000$ をワード 0 に、乗数 $3 = 0011$ を Q に置く。アドレスは a = 1(被乗数)、b = 0(アキュムレータ)。配線は ram0 → qs3、ram3 ← c_out、qs0 は Low 固定。各手順は q[0] の判定で選ばれる 1 つのマイクロ命令である。
ramqd / add / ab = octal 4 0 1, $c_{in} = 0$.步驟 1。$q_0 = 1$,所以要加。指令 ramqd / add / ab = 八進位 4 0 1,$c_{in} = 0$。手順 1。$q_0 = 1$ なので加算する。命令は ramqd / add / ab = 8 進 4 0 1、$c_{in} = 0$ である。ramqd / add / zb = octal 4 0 3, $c_{in} = 0$: $r = 0$, $s = bd = ACC$.步驟 3。$q_0 = 0$,所以不加 —— 讓累加器原封不動穿過 ALU。指令 ramqd / add / zb = 八進位 4 0 3,$c_{in} = 0$:$r = 0$、$s = bd = ACC$。手順 3。$q_0 = 0$ なので加算しない。アキュムレータをそのまま ALU に通す。命令は ramqd / add / zb = 8 進 4 0 3、$c_{in} = 0$ で、$r = 0$、$s = bd = ACC$ である。Two things in that trace are worth dwelling on. First, the multiplier bits are consumed from the bottom of Q and the product bits arrive from the top of Q, filling the space the multiplier vacates — the two halves share one register and never collide, which is the elegance of the algorithm. Second, the choice between "add" and "pass through" is made by the microcode branching on $q_0$, which the slice exposes as a pin precisely so that the sequencer can test it. The datapath is dumb; the loop lives in the ROM.那段追蹤裡有兩件事值得多停留一下。第一,乘數的位元從 Q 的底部被消耗掉,而乘積的位元從 Q 的頂部進來,填滿乘數空出來的位置 —— 兩個半邊共用一個暫存器卻從不相撞,這正是這個演算法優雅的地方。第二,「加」與「直接穿過」之間的選擇,是由微碼依 $q_0$ 分支做出的,而 slice 把 $q_0$ 拉成腳位,正是為了讓定序器可以測試它。資料路徑是笨的;迴圈住在 ROM 裡。このトレースには、立ち止まる価値のある点が二つある。第一に、乗数のビットは Q の下端から消費され、積のビットは Q の上端から入ってきて、乗数が空けた場所を埋めていく。二つの半分が一つのレジスタを共有しながら決して衝突しない。そこがこのアルゴリズムの優美さである。第二に、「加える」か「そのまま通す」かの選択は、マイクロコードが $q_0$ を見て分岐して決めている。スライスが $q_0$ をピンとして出しているのは、まさにシーケンサがそれを判定できるようにするためである。データパスは愚かで、ループは ROM の中に住んでいる。
And one caveat: the trace above assumes the Q register shifts its own contents, as the datasheet specifies. With the slide's Q register, which shifts alu_out instead, every step would load a shifted copy of the accumulator into Q, the multiplier would be destroyed on the first step, and the algorithm would produce nothing. That is the practical consequence of the discrepancy flagged in §6.6, and it is the strongest evidence that the discrepancy is a transcription error rather than a design choice.還有一個但書:上面那段追蹤假設 Q 暫存器移位的是它自己的內容,也就是資料手冊所規定的行為。若換成投影片那個改為移位 alu_out 的 Q 暫存器,每一步都會把累加器的移位副本載入 Q,乘數在第一步就被毀掉,演算法什麼也算不出來。這就是 §6.6 標出的那個不一致在實務上的後果,也是那個不一致是謄寫錯誤而非設計選擇的最強證據。もう一つ但し書きがある。上のトレースは、データシートの規定どおり Q レジスタが自分自身の内容をシフトすることを前提にしている。alu_out をシフトするスライドの Q レジスタでは、各手順でアキュムレータのシフト版が Q にロードされ、乗数は最初の手順で破壊され、アルゴリズムは何も生まない。これが §6.6 で指摘した食い違いの実務上の帰結であり、その食い違いが設計上の選択ではなく転記ミスであることの最も強い証拠でもある。
To build a 16-bit machine, instantiate four slices and wire the cut signals. The rules follow directly from what each pin does.要建一台 16 位元的機器,就實例化四片 slice 並把切面訊號接起來。規則直接從每支腳位的作用推出來。16 ビットの機械を作るには、スライスを 4 枚インスタンス化して切断面の信号を配線する。規則は各ピンの役割からそのまま導かれる。
| Signal訊號信号 | Connection between slice $k$ (lower) and slice $k{+}1$ (upper)第 $k$ 片(較低)與第 $k{+}1$ 片(較高)之間的連接$k$ 番目(下位)と $k{+}1$ 番目(上位)のスライス間の接続 | At the ends兩端両端では |
|---|---|---|
| Carry進位キャリー | c_out of $k$ → c_in of $k{+}1$ for a ripple chain; or g_bar/p_bar of every slice → a look-ahead generator that supplies all four c_ins漣波鏈:第 $k$ 片的 c_out → 第 $k{+}1$ 片的 c_in;或:每片的 g_bar/p_bar → 一個預看產生器,由它供應全部四個 c_inリップル連鎖なら $k$ の c_out → $k{+}1$ の c_in。あるいは各スライスの g_bar/p_bar → 先見生成器が 4 本の c_in をすべて供給する | c_in of slice 0 from the microcode; c_out of slice 3 is the machine's carry flag第 0 片的 c_in 來自微碼;第 3 片的 c_out 就是這台機器的進位旗標スライス 0 の c_in はマイクロコードから。スライス 3 の c_out が機械のキャリーフラグになる |
| Register-file shift暫存器檔移位レジスタファイルのシフト | ram3 of $k$ ↔ ram0 of $k{+}1$第 $k$ 片的 ram3 ↔ 第 $k{+}1$ 片的 ram0$k$ の ram3 ↔ $k{+}1$ の ram0 | ram0 of slice 0 and ram3 of slice 3 are tied by the system: 0 for a logical shift, the sign bit for an arithmetic right shift, c_out for a multiply第 0 片的 ram0 與第 3 片的 ram3 由系統接定:邏輯移位接 0、算術右移接符號位元、乘法接 c_outスライス 0 の ram0 とスライス 3 の ram3 はシステム側で固定する。論理シフトなら 0、算術右シフトなら符号ビット、乗算なら c_out である |
| Q shiftQ 移位Q のシフト | qs3 of $k$ ↔ qs0 of $k{+}1$第 $k$ 片的 qs3 ↔ 第 $k{+}1$ 片的 qs0$k$ の qs3 ↔ $k{+}1$ の qs0 | qs3 of slice 3 ← ram0 of slice 0 for a multiply; qs0 of slice 0 tied low乘法時第 3 片的 qs3 ← 第 0 片的 ram0;第 0 片的 qs0 接低乗算ではスライス 3 の qs3 ← スライス 0 の ram0。スライス 0 の qs0 は Low 固定 |
| Zero flag零旗標ゼロフラグ | f_0 of all four wired together with a pull-up (wired-AND)四片的 f_0 接在一起並加上拉(wired-AND)4 枚の f_0 を互いに接続しプルアップを付ける(ワイヤード AND) | One line to the sequencer's condition input一條線接到定序器的條件輸入1 本の線がシーケンサの条件入力へ |
| Sign, overflow符號、溢位符号、オーバフロー | Not chained不串接連鎖しない | Only slice 3's f_3 and ovr are meaningful只有第 3 片的 f_3 與 ovr 有意義意味があるのはスライス 3 の f_3 と ovr だけ |
a, b, d, instruction, clk, rst, enable | Broadcast identically to all four slices完全相同地廣播到四片 slice4 枚のスライスへ同一に配る | From the microcode ROM and the data bus來自微碼 ROM 與資料匯流排マイクロコード ROM とデータバスから |
The arithmetic of the carry is the interesting part. A ripple chain across four slices costs four slice-delays of carry propagation in series, which for a 16-bit machine is the dominant term in the cycle time. The look-ahead alternative computes all four carries in parallel from the g_bar/p_bar pairs in two gate delays, at the cost of one extra chip and eight extra pins per slice. That trade — spend pins and a part to buy a shorter critical path — is the whole reason g_bar and p_bar exist, and it is a decision you will make again every time you size an adder.進位的算術是有意思的部分。四片 slice 的漣波鏈,進位傳播要串四個 slice 延遲,對一台 16 位元機器而言那是週期時間裡的主導項。預看的替代方案從各片的 g_bar/p_bar 對以兩級閘延遲平行算出全部四個進位,代價是多一顆晶片、每片多八支腳位。那個取捨 —— 花腳位與一顆零件去買一條更短的關鍵路徑 —— 正是 g_bar 與 p_bar 存在的全部理由,而每次你決定加法器規模時,都會再做一次這個決定。面白いのはキャリーの算術である。4 枚にまたがるリップル連鎖は、キャリー伝搬がスライス遅延 4 段分直列になり、16 ビットの機械ではそれがサイクルタイムの支配項になる。先見方式は各スライスの g_bar/p_bar の対から 4 本のキャリーを 2 段のゲート遅延で並列に求める。代償はチップ 1 個とスライスあたり 8 本の追加ピンである。このトレードオフ——ピンと部品を払ってクリティカルパスの短さを買う——こそ g_bar と p_bar が存在する理由のすべてであり、加算器の規模を決めるたびに同じ判断を繰り返すことになる。
Because the 2901 is a controlled datapath, its testbench is a microprogram: an array of control words played into the instruction port one per cycle, with the expected state checked at the end of each.因為 2901 是一條受控資料路徑,它的測試平台本身就是一段微程式:一個控制字的陣列,每週期送一個進指令埠,並在每個週期結束時檢查預期的狀態。2901 は制御されたデータパスなので、そのテストベンチ自体がマイクロプログラムである。制御語の配列を 1 サイクルに 1 語ずつ命令ポートへ流し込み、各サイクルの終わりに期待される状態を照合する。
module amd2901_tb; reg clk, rst, c_in, enable; reg [3:0] a, b, d; reg [8:0] instruction; wire [3:0] y; wire g_bar, p_bar, ovr, c_out, f_0, f_3; wire ram0, ram3, qs0, qs3; // pull-ups model the open-drain flag and the floating shift pins pullup (f_0); pullup (ram0); pullup (ram3); pullup (qs0); pullup (qs3); amd2901 DUT (clk, rst, enable, c_in, a, b, d, instruction, ram0, ram3, qs0, qs3, y, g_bar, p_bar, ovr, c_out, f_0, f_3); initial begin clk = 0; forever #5 clk = ~clk; end // one microinstruction per call task micro (input [2:0] dest, input [2:0] func, input [2:0] src, input [3:0] aa, bb, dd, input cc); begin @(negedge clk); instruction = {dest, func, src}; a = aa; b = bb; d = dd; c_in = cc; @(posedge clk); // the edge that stores the result #1; // settle before sampling $display("%0t i=%o%o%o a=%0d b=%0d d=%0d | y=%h c_out=%b ovr=%b f_0=%b", $time, dest, func, src, aa, bb, dd, y, c_out, ovr, f_0); end endtask initial begin rst = 1; enable = 0; instruction = 9'o111; a = 0; b = 0; d = 0; c_in = 0; @(negedge clk); rst = 0; // load 5 into register 1: dest=ramf, func=add, src=dz, d=5, b=1 micro(3'o3, 3'o0, 3'o7, 4'd0, 4'd1, 4'd5, 1'b0); // load 0 into register 0 micro(3'o3, 3'o0, 3'o7, 4'd0, 4'd0, 4'd0, 1'b0); // load 3 into Q: dest=qreg, func=add, src=dz, d=3 micro(3'o0, 3'o0, 3'o7, 4'd0, 4'd0, 4'd3, 1'b0); // four multiply steps, choosing the source field from q[0] // q[0]==1 -> src=ab (add), q[0]==0 -> src=zb (pass) // (a real sequencer reads q0; here the testbench knows the answer) micro(3'o4, 3'o0, 3'o1, 4'd1, 4'd0, 4'd0, 1'b0); micro(3'o4, 3'o0, 3'o1, 4'd1, 4'd0, 4'd0, 1'b0); micro(3'o4, 3'o0, 3'o3, 4'd1, 4'd0, 4'd0, 1'b0); micro(3'o4, 3'o0, 3'o3, 4'd1, 4'd0, 4'd0, 1'b0); // read back register 0 (the high half) with dest=rama micro(3'o2, 3'o0, 3'o4, 4'd0, 4'd0, 4'd0, 1'b0); $finish; end endmodule
Technique notes. The micro task drives on the falling edge and samples after the rising edge, which is the same discipline as the FIFO bench and for the same reason. The pullup primitives are essential: without them every three-state net in the design floats at z, every comparison against it yields x, and the whole simulation turns red. Modelling the external pull-ups in the testbench is how you verify an open-drain flag, and doing so would have exposed the polarity error of §6.8 immediately — with four slices and the slide's code, the wired-AND line reads low whenever any nibble is zero, which a two-slice test with values 0 and 5 reveals in one cycle.技巧上的幾點。micro task 在下降緣驅動、在上升緣之後取樣,這跟 FIFO 測試平台是同樣的紀律、同樣的理由。那些 pullup 基本元件是必要的:沒有它們,設計裡每一條三態 net 都浮在 z,每一次對它的比較都得到 x,整個模擬一片紅。在測試平台裡把外部上拉模型出來,正是驗證開汲極旗標的方法,而這麼做本來可以立刻揭露 §6.8 的極性錯誤 —— 用四片 slice 加上投影片的程式碼,只要任何一個 nibble 是零,那條 wired-AND 線就讀到低,而一個用 0 與 5 兩個值的雙 slice 測試,一個週期就看得出來。技法上の注意点。micro タスクは立ち下がりで駆動し立ち上がりの後にサンプルする。FIFO のベンチと同じ規律で、理由も同じである。pullup プリミティブは必須である。これが無ければ設計中のトライステートのネットはすべて z に浮き、それとの比較はすべて x になり、シミュレーション全体が真っ赤になる。外部プルアップをテストベンチでモデル化することが、オープンドレインのフラグを検証する方法であり、そうしていれば §6.8 の極性の誤りはただちに露呈したはずである。4 枚のスライスとスライドのコードでは、どれか一つのニブルがゼロであればワイヤード AND の線が Low を示す。値 0 と 5 を使う 2 スライスの試験なら 1 サイクルで分かる。
A serious verification plan for this slice would add: a reference model of the ALU written as a function and compared on all $8 \times 16 \times 16 \times 2 = 4096$ combinations of function, r, s and c_in — small enough to be exhaustive, which is a luxury you rarely get and should always take; a directed test for every destination code checking the register file, Q and the four serial pins; and a multi-slice instance to check the shift chains and the carry.這片 slice 認真的驗證計畫還會加上:一個寫成 function 的 ALU 參考模型,對功能、r、s 與 c_in 的全部 $8 \times 16 \times 16 \times 2 = 4096$ 種組合逐一比對 —— 小到可以窮舉,這是你很少有、而且一有就該拿的奢侈;對每一個目的地碼做指向性測試,檢查暫存器檔、Q 與四支串列腳位;以及一個多片 slice 的實例,用來檢查移位鏈與進位。このスライスの本格的な検証計画には、さらに次を加えたい。function として書いた ALU の参照モデルを用意し、機能・r・s・c_in の全 $8 \times 16 \times 16 \times 2 = 4096$ 通りで照合する——網羅できるほど小さく、これは滅多に得られない贅沢であり、得られるなら必ず取るべきである。各デスティネーションコードに対する指定テストでレジスタファイル・Q・4 本のシリアルピンを確認する。そして複数スライスのインスタンスでシフト連鎖とキャリーを確認する。
The cycle time of a 2901-based machine is the delay of one trip around the loop. Walk it with the netlist in front of you:一台 2901 機器的週期時間,就是繞那個迴路跑一圈的延遲。對著 netlist 走一遍:2901 ベースの機械のサイクルタイムは、ループを 1 周する遅延そのものである。netlist を見ながらたどってみる。
ab_data flip-flop output: one clock-to-Q.時脈邊緣到 ab_data 正反器輸出:一個 clock-to-Q。クロックエッジから ab_data のフリップフロップ出力まで:clock-to-Q 1 回分。ad or bd: typically four levels of 2-to-1 muxes, or two levels if the library has 4-to-1 cells.穿過 16 對 1 的讀取多工器到 ad 或 bd:典型是四層 2 對 1 多工器,若元件庫有 4 對 1 元件則是兩層。16 対 1 の読み出しマルチプレクサを通って ad または bd まで:通常は 2 対 1 マルチプレクサ 4 段、ライブラリに 4 対 1 セルがあれば 2 段。r/s: two levels.穿過運算元選擇器的 4 對 1 多工器到 r/s:兩層。オペランドセレクタの 4 対 1 マルチプレクサを通って r/s まで:2 段。ab_data flip-flop input.在 ab_data 正反器輸入處的 setup。ab_data のフリップフロップ入力でのセットアップ。Roughly twelve to fourteen levels of logic, which in a modern standard-cell process is a few hundred picoseconds and in 1975 bipolar LSI was about 100 nanoseconds — the Am2901's quoted cycle time was indeed in that range. The two longest contributors are the register-file read multiplexer and the ALU carry chain, which is exactly why the look-ahead pins exist and why larger register files are pipelined with a registered read port.大約十二到十四層邏輯,在現代標準元件製程裡是幾百皮秒,而在 1975 年的雙極 LSI 裡大約是 100 奈秒 —— Am2901 標稱的週期時間確實就在那個範圍。兩個最長的貢獻者是暫存器檔的讀取多工器與 ALU 的進位鏈,這正是預看腳位存在的原因,也是更大的暫存器檔要用暫存式讀取埠來管線化的原因。およそ 12〜14 段の論理であり、現代の標準セルプロセスなら数百ピコ秒、1975 年のバイポーラ LSI なら 100 ナノ秒ほどになる。実際 Am2901 の公称サイクルタイムはその範囲にあった。最も長い二つの寄与はレジスタファイルの読み出しマルチプレクサと ALU のキャリー連鎖であり、先見用のピンが存在する理由も、より大きなレジスタファイルを読み出しポートのレジスタ化でパイプライン化する理由も、まさにそこにある。
In parallel with that path, the dest_ctl field must be decoded in time to steer the shifter and to produce ram_en. That is a short path and will not limit anything, but it is a reminder that in a controlled datapath there are always two kinds of path — data and control — and the tool reports them together.與那條路徑平行地,dest_ctl 欄位必須及時解碼,才能操控移位器並產生 ram_en。那是一條短路徑,不會限制任何東西,但它提醒你:在一條受控資料路徑裡,永遠有兩種路徑 —— 資料與控制 —— 而工具會把它們一起回報。その経路と並行して、dest_ctl フィールドはシフタを操り ram_en を作れるよう間に合わなければならない。これは短い経路で何かを制限することはないが、制御されたデータパスには常にデータと制御という二種類の経路があり、ツールはそれらをまとめて報告する、ということを思い出させてくれる。
The elaborated schematic on the final slide shows all five sub-blocks preserved as separate boxes with the loop clearly visible, plus two pieces of glue at the top level: the four-input OR and the inverting three-state cell for f_0. If your own synthesis run flattens the hierarchy and gives you an undifferentiated mat of gates, you have lost the ability to read the result; set the tool to preserve the hierarchy for a first look and flatten only when you are optimising.最後一張投影片上 elaborate 後的電路圖,把五個子區塊都保留成各自的方框,迴路清楚可見,再加上頂層的兩塊膠合邏輯:給 f_0 用的四輸入 OR 與反相三態元件。如果你自己跑合成時工具把階層攤平、給你一團難以分辨的閘海,你就失去了讀懂結果的能力;第一次看的時候請設定工具保留階層,等到要最佳化時再攤平。最後のスライドの elaborate 後の回路図は、5 つのサブブロックを別々の箱として保ち、ループがはっきり見える形になっている。これにトップレベルの糊付け論理が二つ加わる。f_0 用の 4 入力 OR と反転トライステートセルである。自分で合成を回したときにツールが階層を平坦化して区別のつかないゲートの海を返してきたら、結果を読む力を失ったということである。最初に眺めるときは階層を保つよう設定し、最適化の段階になってから平坦化すべきである。
Problem: A 4-bit slice is mid-multiply with $ACC = 4'b0110$ in register-file word 0, multiplicand $M = 4'b0111$ in word 1, and $Q = 4'b1010$. The wiring is ram0 → qs3 and ram3 ← c_out. (a) Which instruction should the sequencer issue? (b) Give the ALU result and c_out. (c) Give the new $ACC$ and the new $Q$. (d) What would go wrong if ram3 were tied to 0 instead of to c_out?問題:一片 4 位元 slice 正在乘法途中:暫存器檔的字 0 裡 $ACC = 4'b0110$,字 1 裡被乘數 $M = 4'b0111$,而 $Q = 4'b1010$。接線是 ram0 → qs3、ram3 ← c_out。(a) 定序器應該發出哪一道指令?(b) 給出 ALU 的結果與 c_out。(c) 給出新的 $ACC$ 與新的 $Q$。(d) 如果 ram3 改接到 0 而不是 c_out,會出什麼問題?問題:4 ビットのスライスが乗算の途中にあり、レジスタファイルのワード 0 に $ACC = 4'b0110$、ワード 1 に被乗数 $M = 4'b0111$、$Q = 4'b1010$ が入っている。配線は ram0 → qs3、ram3 ← c_out である。(a) シーケンサはどの命令を出すべきか。(b) ALU の結果と c_out を示せ。(c) 新しい $ACC$ と $Q$ を示せ。(d) ram3 を c_out ではなく 0 に固定したら何が起こるか。
(a) The multiplier bit about to be consumed is $q_0 = 0$, so do not add: pass the accumulator through and shift. Instruction ramqd / add / zb = octal 4 0 3, with $c_{in} = 0$, $b = 0$. (Had $q_0$ been 1 the instruction would be 4 0 1 with $a = 1$, $b = 0$.)(a) 即將被消耗的乘數位元是 $q_0 = 0$,所以不要加:把累加器穿過去並移位。指令 ramqd / add / zb = 八進位 4 0 3,$c_{in} = 0$、$b = 0$。(若 $q_0$ 是 1,指令就會是 4 0 1,$a = 1$、$b = 0$。)(a) これから消費される乗数のビットは $q_0 = 0$ なので加算しない。アキュムレータをそのまま通してシフトする。命令は ramqd / add / zb = 8 進 4 0 3、$c_{in} = 0$、$b = 0$ である。($q_0$ が 1 なら命令は 4 0 1、$a = 1$、$b = 0$ になる。)
(b) Source zb gives $r = 0$, $s = bd = ACC = 0110$. The ALU computes $0 + 0110 + 0 = 0110$ with $c_{out} = 0$.(b) 來源 zb 給 $r = 0$、$s = bd = ACC = 0110$。ALU 算出 $0 + 0110 + 0 = 0110$,$c_{out} = 0$。(b) ソース zb により $r = 0$、$s = bd = ACC = 0110$ となる。ALU は $0 + 0110 + 0 = 0110$ を計算し、$c_{out} = 0$ である。
(c) ramqd shifts both halves right.(c) ramqd 把兩個半邊都右移。(c) ramqd は両方の半分を右シフトする。
Register file: $ACC \leftarrow \{\textit{ram3}, f[3], f[2], f[1]\} = \{c_{out}, 0, 1, 1\} = \{0,011\} = \mathbf{0011}$.暫存器檔:$ACC \leftarrow \{\textit{ram3}, f[3], f[2], f[1]\} = \{c_{out}, 0, 1, 1\} = \{0,011\} = \mathbf{0011}$。レジスタファイル:$ACC \leftarrow \{\textit{ram3}, f[3], f[2], f[1]\} = \{c_{out}, 0, 1, 1\} = \{0,011\} = \mathbf{0011}$。
The bit leaving the bottom is $f[0] = 0$, driven out on ram0 and therefore arriving at qs3.從底部離開的位元是 $f[0] = 0$,由 ram0 驅動出去,因此抵達 qs3。下端から出ていくビットは $f[0] = 0$ であり、ram0 から駆動されて qs3 に届く。
Q: $\leftarrow \{\textit{qs3}, q[3], q[2], q[1]\} = \{0, 1, 0, 1\} = \mathbf{0101}$.Q:$\leftarrow \{\textit{qs3}, q[3], q[2], q[1]\} = \{0, 1, 0, 1\} = \mathbf{0101}$。Q:$\leftarrow \{\textit{qs3}, q[3], q[2], q[1]\} = \{0, 1, 0, 1\} = \mathbf{0101}$。
Pair after the step: $0011\;0101$. Sanity check: before the step the pair was $0110\;1010 = 106$; after a right shift of the whole pair it should be $53 = 0011\,0101$. Correct.這一步之後的一對:$0011\;0101$。檢查一下:這一步之前這一對是 $0110\;1010 = 106$;把整個一對右移一位應該得到 $53 = 0011\,0101$。正確。この手順のあとの対:$0011\;0101$。検算すると、手順前の対は $0110\;1010 = 106$ であり、対を丸ごと 1 ビット右シフトすれば $53 = 0011\,0101$ になるはずである。正しい。
(d) Tying ram3 to 0 throws away the carry out of the addition steps. In this particular step $c_{out}$ happens to be 0, so nothing is lost — but in a step where the multiplicand is added to a large accumulator, the sum is a five-bit quantity and its top bit lives in $c_{out}$. Discarding it makes the accumulator wrap modulo 16 and the product is wrong by a multiple of 16 in the high half. The symptom is that small products are correct and large ones are not, which is the signature of a lost carry everywhere in arithmetic hardware. For a signed multiply you would instead tie ram3 to the sign of the result, so that the shift is arithmetic rather than logical — the two cases need different wiring, and choosing between them is exactly the kind of decision the bit-slice architecture leaves to the system designer.(d) 把 ram3 接到 0,等於把加法步驟的進位輸出丟掉。在這個特定步驟裡 $c_{out}$ 剛好是 0,所以沒有損失 —— 但在某個把被乘數加到一個很大的累加器上的步驟裡,和是一個五位元的量,它的最高位元住在 $c_{out}$ 裡。把它丟掉會讓累加器以 16 為模繞回,乘積的高半邊就差了 16 的倍數。症狀是小的乘積正確、大的不正確,而這在算術硬體裡到處都是「進位遺失」的標誌。做帶號乘法時,你會改把 ram3 接到結果的符號,讓移位變成算術移位而不是邏輯移位 —— 兩種情況需要不同的接線,而在兩者之間做選擇,正是 bit-slice 架構留給系統設計者的那種決定。(d) ram3 を 0 に固定すると、加算手順のキャリー出力を捨てることになる。この特定の手順では $c_{out}$ がたまたま 0 なので失うものはない。しかし被乗数を大きなアキュムレータに加える手順では、和は 5 ビットの量であり、その最上位ビットは $c_{out}$ にある。それを捨てればアキュムレータは 16 を法として折り返し、積の上半分は 16 の倍数だけ狂う。症状は、小さな積は正しく大きな積は誤るというもので、算術ハードウェア全般におけるキャリー消失の典型的な兆候である。符号付き乗算なら ram3 を結果の符号につなぎ、シフトを論理ではなく算術にする。二つの場合で配線が異なり、そのどちらを選ぶかこそ、ビットスライス方式が設計者に委ねる類の判断である。
| # | Trap陷阱落とし穴 | Where出現位置登場箇所 | What to say該怎麼答何と答えるか |
|---|---|---|---|
| 1 | Occupancy counter one bit too narrow佔用量計數器少了一位元占有数カウンタが 1 ビット足りない | §3.1 | A FIFO of depth $N$ has $N+1$ occupancies, so the counter needs $\log_2 N + 1$ bits. [stack_ptr_width:0], not [stack_ptr_width-1:0].深度 $N$ 的 FIFO 有 $N+1$ 種佔用量,所以計數器需要 $\log_2 N + 1$ 個位元。要寫 [stack_ptr_width:0],不是 [stack_ptr_width-1:0]。深さ $N$ の FIFO は占有数が $N+1$ 通りあるので、カウンタには $\log_2 N + 1$ ビットが要る。[stack_ptr_width-1:0] ではなく [stack_ptr_width:0] と書く。 |
| 2 | Pointer coincidence means full or empty指標重合代表滿或空ポインタの一致は満杯または空を意味する | §2.4 | Name all three cures and say which one survives a clock-domain crossing.說出三種解法的名字,並指出哪一種能撐過跨時脈域。三つの解法を名指しし、クロックドメインをまたいでも生き残るのはどれかを述べる。 |
| 3 | A semicolon after if (...)if (...) 後面的一個分號if (...) の直後のセミコロン | §3.6 | It is a null statement; the next line becomes unconditional.那是一個空敘述;下一行變成無條件執行。それは空文であり、次の行が無条件に実行される。 |
| 4 | Blocking assignment in a clocked block時脈區塊裡的阻塞指定クロック同期ブロック内のブロッキング代入 | §3.6, §4.2, §6.1 | Harmless where nothing else reads the variable in the same time step; a race the moment anything does. Always write <=.同一時間步內沒有別的東西讀那個變數時無害;一旦有,立刻變成競爭。一律寫 <=。同じ時間ステップでその変数を読むものが他に無ければ無害だが、一つでもあれば即座にレースになる。常に <= と書く。 |
| 5 | / by a non-power-of-two除以非 2 冪次的 /2 のべき乗でない除数による / | §4.3 | Not synthesisable. * by a constant is fine; / by 2, 4, 8 is a shift.不可合成。乘以常數的 * 沒問題;除以 2、4、8 則是移位。合成できない。定数による * は問題なく、2・4・8 による / はシフトになる。 |
| 6 | Algebraically equal, arithmetically different代數上相等,算術上不同代数的には等しいが算術的には違う | §4.5 | $1 + \frac12 + \frac14$ truncates downward, $2 - \frac14$ truncates upward. Reassociating integer arithmetic changes the answer.$1 + \frac12 + \frac14$ 向下截斷,$2 - \frac14$ 向上截斷。重新結合整數算術就會改變答案。$1 + \frac12 + \frac14$ は下向きに、$2 - \frac14$ は上向きに切り捨てる。整数算術を結合し直せば答えが変わる。 |
| 7 | Sum of four 12-bit numbers evaluated at 12 bits四個 12 位元數的總和在 12 位元下求值12 ビットの数 4 つの総和が 12 ビットで評価される | §4.8 | Expression width is the max of operand and target widths; widen the sum to 14 bits and take sum[13:2].運算式寬度是運算元與指定目標寬度的最大值;把總和加寬到 14 位元,再取 sum[13:2]。式の幅はオペランドと代入先の幅の最大値である。総和を 14 ビットへ広げて sum[13:2] を取る。 |
| 8 | Shift register written with blocking assignments用阻塞指定寫的移位暫存器ブロッキング代入で書かれたシフトレジスタ | §4.7 | All stages collapse to the newest sample. Synthesis agrees with simulation — both are wrong.所有級都塌成最新的那筆樣本。合成與模擬一致 —— 兩邊都錯。全段が最新のサンプルに潰れる。合成とシミュレーションは一致するが、両方とも誤っている。 |
| 9 | An output not assigned on every path of a combinational block組合區塊裡有輸出沒在每條路徑上被指定組合せブロックで、すべての経路では代入されない出力がある | §4.10 | write in the FIFO controller. Pre-assign every output at the top of the block.FIFO 控制器的 write。在區塊最上面把每個輸出都先指定一次。FIFO コントローラの write。ブロックの冒頭ですべての出力に既定値を与える。 |
| 10 | Loop index too narrow to fail its own test迴圈索引窄到無法讓自己的測試失敗ループ変数が自分の終了判定を偽にできないほど狭い | §6.1 | Counting to 16 needs 5 bits. Use an integer.數到 16 需要 5 位元。用 integer。16 まで数えるには 5 ビット必要。integer を使う。 |
| 11 | ~s widened before inversion~s 在取反之前先被加寬~s が反転の前に拡張される | §6.4 | In a 5-bit context, ~4'b0101 is 5'b11010, not 4'b1010. The carry flag comes out inverted.在五位元的上下文裡,~4'b0101 是 5'b11010,不是 4'b1010。進位旗標會反過來。5 ビットの文脈では ~4'b0101 は 4'b1010 ではなく 5'b11010 である。キャリーフラグは反転して出てくる。 |
| 12 | ovr = c_out ^ sign is not overflowovr = c_out ^ sign 不是溢位ovr = c_out ^ sign はオーバフローではない | §6.5 | Overflow is carry-in to the MSB XOR carry-out, or equivalently "same-sign operands, different-sign result".溢位是「進入 MSB 的進位」互斥或「從 MSB 出來的進位」,或等價地說「同號運算元、異號結果」。オーバフローは MSB への桁上げと MSB からの桁上げの排他的論理和、あるいは同じことだが「同符号のオペランドに異符号の結果」である。 |
Data_out means one cycle of read latency.Data_out 是暫存的,代表讀取有一個週期的延遲。Data_out がレジスタ出力であることは、読み出しに 1 サイクルのレイテンシがあることを意味する。<= for state; always @* + = for logic.狀態用「邊緣區塊 + <=」;邏輯用「always @* + =」。状態にはエッジ記述ブロックと <=、論理には always @* と =。case with a default gives a mux, not a priority chain.完整、互斥且帶 default 的 case 給你多工器,不是優先鏈。full かつ parallel で default を持つ case は、プライオリティ連鎖ではなくマルチプレクサになる。`default_nettype none and named port binding.`default_nettype none 與具名埠綁定。`default_nettype none と名前指定のポート結合。r then s; z is zero.來源助記符先寫 r 再寫 s;z 是零。ソースのニーモニックは r、s の順に表す。z はゼロである。b; address a is read-only.暫存器檔只寫在位址 b;位址 a 是唯讀的。レジスタファイルへの書き込みはアドレス b のみ。アドレス a は読み出し専用である。rama puts something other than alu_out on y.只有 rama 會把 alu_out 以外的東西放上 y。alu_out 以外のものを y に出すのは rama だけである。ram3↔ram0 and qs3↔qs0 chain the shifters.ram3↔ram0 與 qs3↔qs0 把移位器串接起來。ram3↔ram0 と qs3↔qs0 がシフタを連鎖させる。g_bar/p_bar feed a look-ahead generator instead of rippling the carry.g_bar/p_bar 餵給預看產生器,取代讓進位漣波傳遞。g_bar/p_bar は先見生成器へ入り、キャリーのリップル伝搬を置き換える。f_0 is open-drain and wired-AND: pull low when non-zero.f_0 是開汲極、wired-AND:非零時拉低。f_0 はオープンドレインのワイヤード AND。非ゼロのとき Low に引く。!== so that x and z are caught.用 !== 比較,這樣 x 與 z 才抓得到。!== で比較し、x と z を捕まえる。x.把外部上拉模型出來,否則每一條三態 net 都讀成 x。外部プルアップをモデル化する。さもないとトライステートのネットはすべて x と読まれる。A last word about what this lecture is really assessing. Neither design example is difficult in the sense that the Verilog is hard to write; everything here is first-year syntax. What is difficult is noticing, as you read, which lines are load-bearing — the four-bit occupancy counter, the two truncating shifts that are not equivalent, the five-bit loop index, the widened complement — and being able to say what each one costs and what would happen if it were wrong. That skill does not come from memorising the modules. It comes from taking each block, asking the three questions of §1.2, and refusing to move on until all three have answers.最後說一句這一講真正在評量什麼。兩個設計範例都不難,難不在 Verilog 難寫;這裡的一切都是第一年的語法。難的是在閱讀的過程中注意到哪些行是承重的 —— 那個四位元的佔用量計數器、那兩個並不等價的截斷移位、那個五位元的迴圈索引、那個被加寬的補數 —— 並且能說出每一個各花多少成本、若寫錯了會發生什麼事。這項能力不是靠背模組來的。它來自把每一個區塊拿起來,問 §1.2 的那三個問題,並且在三個都還沒有答案之前拒絕往下走。最後に、この講義が本当に測っているものについて一言。二つの設計例はどちらも難しくない。Verilog が書きにくいという意味では難しくなく、ここにあるのはすべて 1 年目の構文である。難しいのは、読み進めながらどの行が荷重を支えているかに気づくこと——4 ビットの占有数カウンタ、等価でない二つの切り捨てシフト、5 ビットのループ変数、拡張されてしまう補数——そして、それぞれが何を費やし、もし誤っていたら何が起こるかを言えることである。この能力はモジュールを暗記して得られるものではない。各ブロックを手に取り、§1.2 の三つの問いを投げ、三つすべてに答えが出るまで先へ進まないことから得られる。