先把 RTL 想成硬體版施工圖
這篇不是要你一開始就會寫完整 AES RTL。先把 RTL 想成硬體工程師的施工圖:資料要從哪裡進來、每一拍做什麼、哪個暫存器保存中間結果、控制器什麼時候說可以輸出,都要畫清楚。
AES-128 的演算法本身像一套工序;RTL 則是把這套工序變成真的硬體節奏。圖片會帶你看資料路徑、Round 控制、FSM、S-box 與 MixColumns 各自站在哪裡。
如果你是初學者,先不要被訊號名稱嚇到。先問:資料現在在哪裡?下一拍要去哪裡?誰負責決定流程往下走?這三個問題抓住,RTL 就開始變得可讀。
AES-128 RTL 初學者圖解
感測器準備送出 16 bytes 的資料。金鑰已經載入加密核心,但接收密文的下游正忙著。這時,核心可以先算完嗎?可以;前提是它有地方保管密文,而且不會被下一筆資料覆蓋。
設計這個模組,需要同時回答兩類問題。AES 決定密文的數值;介面則決定哪個上升緣接收資料,以及何時交出結果。少了後者,就算密文算對,也可能漏送或重複送出。
本文建立一個 AES-128 iterative core:只做加密,一次處理一個區塊,每拍完成一輪。先沿著資料走過電路,再安排時序,最後用已知答案除錯。程式片段說明接線與控制契約,並非可直接編譯的完整 AES IP。
先備知識是 XOR、組合邏輯、暫存器,以及 always_ff 的 nonblocking assignment。AES 決定每一步的數值轉換;下文的 FSM、握手與拍數則是我們選定的硬體設計,不是 AES 標準規定。
想先用漫畫建立 AES 概念,可閱讀 AES 漫畫小教室。
1. 先認識 AES-128
AES-128 的「128」指金鑰長度。AES 各種版本都處理 128-bit 區塊;改用較長的 key,不代表一次能加密更多資料。這裡先固定 AES-128,避免把不同輪數與 key expansion 混入第一版。
AES-128 每次處理:
- 一組 128-bit 明文(Plaintext)
- 一把 128-bit 金鑰(Cipher Key)
- 產生一組 128-bit 密文(Ciphertext)
- 先做 initial AddRoundKey,再執行 10 個 rounds
AES-128 不是把 128 bits 當成一個大數字計算,而是先切成 16 個 bytes。
128 bits = 16 bytes
Plaintext = 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF
| | | | | | | | | | | | | | | |
B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 B10...........B15
先看全貌:一筆資料如何走過 AES IP?
先把自己放在系統設計者的位置:處理器或感測器會送來資料,你的 AES IP 是其中一個負責加密的硬體模組。系統希望交給它一筆明文,稍後取回正確密文。它不需要知道 S-box 裡面如何實現,卻必須知道何時能交資料、何時能取結果。
我們先把核心分成三個區域。這張圖只表達分工,不是逐條訊號的接線圖;此時先不背模組名稱。
資料處理區(Datapath)負責「算什麼」。 它把明文存住,依照 AES 的步驟修改這 16 bytes,並保存每一輪的中間結果。你後面看到的 SubBytes、ShiftRows、MixColumns 和 AddRoundKey,都屬於這條資料處理路徑。
金鑰區負責「這一輪用哪把 key」。 外面只交給核心一把原始金鑰,但 AES 每一輪使用的是由它推導出的 round key。這些 round keys 不是重新隨機產生的密碼,也不用請上游每拍傳一把;它們由核心的 key expansion 電路依規則算出。
控制區(Controller)負責「何時做」。 它記住核心正在等資料、算第幾輪,還是等下游取結果。它不替資料做 S-box 或 XOR,而是決定暫存器何時更新、目前選哪條資料路徑,以及何時宣告輸出有效。
跟著一筆明文走一次
假設 key 已載入。上游提出一筆明文,核心也表示能接收;到了雙方約定的時脈邊緣,交付才成立。核心把這 16 bytes 存進自己的暫存器,上游此後可以改變輸入。如果沒有先存住,後面的運算就可能讀到上游下一筆資料。
核心先完成 initial AddRoundKey,再進行十輪處理。每一輪,資料區讀取目前的中間值,金鑰區提供這輪所需的 key,控制區確保結果在正確時間寫回。中間值還不是可以交出去的密文,即使它已經看起來沒有規律,也不能提前把 out_valid 拉高。
最後一輪完成後,核心宣告結果有效。如果下游正在忙,密文就繼續留在暫存器裡;等到下游確實接收,這筆工作才結束。因此「計算完成」與「交易完成」可能發生在不同時間。第 14 節的 valid/ready 就是把這個交付約定變成訊號。
十輪演算法,需要十套電路嗎?
不一定。想像同一張工作桌:第一輪處理完,把結果放回桌上,再用同一套工具做第二輪。本文選擇的 iterative architecture(反覆使用同一套硬體) 就是這個意思:保留一套 round 電路,讓資料多次通過它。
另一種做法是展開多套 round 電路;若再適當切割 pipeline,可以讓不同區塊同時在不同階段運作。代價是更多硬體、暫存器與控制設計。第一次實作先用一套電路,較容易觀察「這筆資料現在在哪裡」,也方便逐輪比對。
要分清楚兩個詞:round 是演算法的一段工作,cycle 是兩個時脈邊緣之間的時間。 一輪可以分好幾拍做,也可以用較大的組合電路在一拍內完成。本文採一拍一輪,前提是該輪運算與 key expansion 能在這一拍內穩定下來。
從演算法走到硬體:誰保存資料,誰負責計算?
在軟體中,你可以寫 state = round(state, key),執行完就得到新值。硬體中,要把它拆成兩件事:暫存器保存目前的 state,組合電路根據目前輸入算出下一個 state。 兩者之間由時脈邊緣接起來。
時脈邊緣過後,暫存器輸出目前的值,round 電路就開始對這組輸入產生反應。因為每個邏輯閘都有延遲,結果不會瞬間正確;它需要一段時間穩定。下一個時脈邊緣到來時,暫存器才把穩定後的結果存下來,作為下一輪的起點。
因此,組合電路不是等到 clk 才開始算,時脈主要決定暫存器何時取樣。RTL 裡常把前者寫成 assign、always_comb 或純組合子模組;把後者寫在 always_ff @(posedge clk)。若組合路徑太長,下一拍來了還沒算穩,就算公式完全正確,硬體仍可能無法在目標頻率工作。
回授也不等於無止境加密。控制器會決定是否寫回:正在算 round 時更新;密文等待下游時保持。它同時記住 round counter,讓第一次走 initial XOR、一般輪走完整轉換、最後一輪略過 MixColumns。第 12、13 節會把這些動作展開成精確拍數。
讀到這裡先確認:為什麼資料區之外還需要控制器? 因為同一份中間資料、同一套電路,在不同階段要做的動作不同;電路不會自行知道目前是第一輪、最後一輪,或應該停著等下游。
2. 打開三個區域:Top-Level 架構與介面
現在再看圖 2。把 State Register 與 AES Round Datapath 合起來,就是導讀圖 A 的資料區;Original Key、Working Round Key 與 Key Expansion 是金鑰區;Controller FSM 是控制區。這次才把區域打開,看到需要保存哪些值,以及控制器要管理哪些動作。
第一遍沿著 in_data → State Register → Round Datapath → State Register → out_data 看資料;第二遍看 key;第三遍才讀虛線控制。圖中的線路都存在,但不代表每一拍都會寫入每個暫存器。
圖 2 的 key 路徑有一個重要約定:初始 XOR 使用 K0,而且這一拍不更新 Working Round Key。 到第 r 輪時,working register 裡仍是 K[r−1];Key Expansion 組合電路產生 K[r],這把 key 直接供本輪計算,並在輪末與資料結果一起存下來。因此圖上的 Next Round Key 是「相對於暫存器舊值的下一把」,也是「本輪要用的 key」,不是再等一輪才用。第 12 節有逐拍對照表。
Top-Level IO
| 訊號 | 方向 | 寬度 | 功能 |
|---|---|---|---|
clk | Input | 1 | 系統時脈 |
rst_n | Input | 1 | Active-low synchronous reset |
key_valid | Input | 1 | 表示 key_data 有效 |
key_ready | Output | 1 | 核心可以接收新金鑰 |
key_data | Input | 128 | AES-128 cipher key |
in_valid | Input | 1 | 表示 in_data 有效 |
in_ready | Output | 1 | 核心可以接收明文 |
in_data | Input | 128 | 128-bit plaintext |
out_valid | Output | 1 | 表示 out_data 有效 |
out_ready | Input | 1 | 下游可以接收密文 |
out_data | Output | 128 | 128-bit ciphertext |
busy | Output | 1 | 交易未完成,包含等待下游接收 |
資料只有在 valid=1 且 ready=1 的 clock edge 才真正被接收。
讀 IO 表時,可以把 valid 理解成來源說「我手上這筆資料有效」,ready 理解成接收方說「這一拍我接得下」。只有一方說好還不能算交付。例如 in_valid=1 但核心忙碌、in_ready=0,上游就必須繼續拿著同一筆明文,不能當成核心已經收走。
本文全部以 clock 上升緣取樣。上游在等待期間要維持 valid 與資料,接收端不能只看到 valid 就算接收成功。金鑰與明文是兩個獨立通道,優先順序在第 13 節明確定義。
3. 完整 AES-128 加密流程
先把初始 XOR、一般輪與最後一輪分開。它們是同一次 AES-128 計算的不同階段。控制器必須知道現在處於哪個階段,才能選對資料路徑與金鑰。
初學者只要先記住:
Round 0 = AddRoundKey
Round 1~9 = SubBytes + ShiftRows + MixColumns + AddRoundKey
Round 10 = SubBytes + ShiftRows + AddRoundKey
Final Round 不做 MixColumns。
4. AES State 的 4 x 4 Byte 排列
AES 把十六個 bytes 排成四個 row(橫列)與四個 column(直欄)。這份排列稱為 state,後面的 ShiftRows 與 MixColumns 都以它為基準。它不是為了畫圖方便而任意選出的表格;排列錯誤會改變運算結果。
AES 把 16 bytes 排成 4 rows x 4 columns,而且採用 column-major 排列。
128-bit input bytes:
B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 B10 B11 B12 B13 B14 B15
放入 AES State 後:
Column 0 Column 1 Column 2 Column 3
Row 0 B0 B4 B8 B12
Row 1 B1 B5 B9 B13
Row 2 B2 B6 B10 B14
Row 3 B3 B7 B11 B15
對應 RTL bit mapping:
B0 = state[127:120] B8 = state[63:56]
B1 = state[119:112] B9 = state[55:48]
B2 = state[111:104] B10 = state[47:40]
B3 = state[103:96] B11 = state[39:32]
B4 = state[95:88] B12 = state[31:24]
B5 = state[87:80] B13 = state[23:16]
B6 = state[79:72] B14 = state[15:8]
B7 = state[71:64] B15 = state[7:0]
這個排列方式必須先固定,否則 ShiftRows 和 MixColumns 很容易接錯 byte。
用公式表示就是 s[row][col] = B[4*col + row];在本文 packed vector 中,B[i] = state[127 - 8*i -: 8]。因此 Column 0 是 state[127:96]。這是介面位元約定,不應由 CPU 的大小端習慣猜測。
5. 一個 Normal Round 的模組連接
一般輪依序做四個轉換,但「依序」不代表需要四拍。組合電路可以把前一個轉換的輸出,直接接到下一個輸入。本篇在整輪末端才保存結果,因此一輪使用一個時脈週期。
這個安排把四個轉換的延遲放在同一條路徑。若路徑太慢,可以增加暫存階段;代價是 round、key 與 FSM 的時序都要跟著調整。模組分成幾個檔案,並不能決定硬體需要幾拍。
前面先把 round 當成一個盒子,現在把它拆開。SubBytes 改變每個 byte 的值,ShiftRows 改變 byte 的位置,MixColumns 混合 column 裡的值,最後 AddRoundKey 注入這一輪的金鑰資訊。這四個動作串成一條組合路徑;「畫成四個方塊」不代表必須花四個 cycles。
aes_encrypt_round IO
module aes_encrypt_round (
input logic [127:0] state_i,
input logic [127:0] round_key_i,
output logic [127:0] state_o
);
此模組是 combinational logic。輸入一個 state 和 round key,輸出完成一個 normal round 的新 state。
6. SubBytes:每個 Byte 查 S-box
功能
- 將 128-bit state 分成 16 個 8-bit bytes。
- 每個 byte 獨立通過同一張 AES S-box 查表。
- 16 個 bytes 的位置不改變。
- 16 S-boxes 可以平行運算。
這裡的「查表」是在描述輸入與輸出的對應,不是要求硬體執行十六次軟體查詢。若有十六個組合 S-box,十六個 bytes 可以同時轉換。若你為了省面積只放一個 S-box,就要分時處理不同 bytes,還需要暫存與 byte counter,原來一拍一輪的控制方式也必須改寫。
SubBytes 提供非線性轉換:不能只用固定的 XOR 與位元搬移取代它。實作時,先不要自行簡化標準表;先讓全部 256 種輸入都得到正確輸出,再討論不同 S-box 電路的面積與延遲。
IO
module aes_sub_bytes (
input logic [127:0] state_i,
output logic [127:0] state_o
);
module aes_sbox (
input logic [7:0] data_i,
output logic [7:0] data_o
);
小例子
S-box(8'h53) = 8'hED
輸入 byte 53
|
AES S-box
|
輸出 byte ED
把標準明文的第一個 byte 接到 S-box
沿用第 1 節明文與第 17 節金鑰。Initial AddRoundKey 的 B0 是 00 XOR 00 = 00,B1 是 11 XOR 01 = 10。這裡 10 是十六進位的 16,不是十進位十。十六個 XOR 得到 00102030405060708090a0b0c0d0e0f0。
S-box 的高四位選標準表的 row、低四位選 column。00 查得 63,10 查得 ca,20 查得 b7,30 查得 04。所以第一個 column 經 SubBytes 成為 63 ca b7 04。查表只換值;這四個 bytes 仍在原來的位置。00 變 63 不是 XOR key 的第二次操作。
這裡把標準 S-box 當成已定義的 256 項函式;設計查表電路不必先推導其有限體反元素與 affine transformation。進階代數實作仍須逐項比對同一張表。先在互動台核對 initial XOR、Round 1 SubBytes,才進行位置重排。
7. ShiftRows:移動位置,不改變 Byte 數值
如果每一輪都只讓同一 column 的四個 bytes 互相作用,不同 columns 就缺少交流。ShiftRows 先把不同 rows 錯開,使下一步 MixColumns 能把原本來自不同 columns 的資料混在一起。先理解這個目的,再看以下排列,就不只是背「左移幾格」。
ShiftRows 前 ShiftRows 後
B0 B4 B8 B12 B0 B4 B8 B12
B1 B5 B9 B13 ---> B5 B9 B13 B1
B2 B6 B10 B14 B10 B14 B2 B6
B3 B7 B11 B15 B15 B3 B7 B11
Row 0:左移 0 格
Row 1:循環左移 1 格
Row 2:循環左移 2 格
Row 3:循環左移 3 格
RTL 接線觀念
IO
module aes_shift_rows (
input logic [127:0] state_i,
output logic [127:0] state_o
);
ShiftRows 只需要重新接線,不需要 S-box、加法器、乘法器或 register。
「循環左移」是矩陣上的描述;固定的 AES ShiftRows 在 RTL 裡通常就是把某一個輸入 byte 接到指定的輸出位置,不必用一個可變位移器,也不是每拍搬動一格。檢查時給 B0~B15 十六個不同的值,看它們各自到了哪裡,比只看最後密文更容易找到接反的線。
同一輪,重新讀出第一個 column
Round 1 SubBytes 的四列是:
row0: 63 09 cd ba;row1: ca 53 60 70;row2: b7 d0 e0 e1;row3: 04 51 e7 8c。
Row1 左移一格後開頭變 53;row2 左移兩格後開頭變 e0;row3 左移三格後開頭變 8c。Row0 不動。因此新的 column0 是 63 53 e0 8c,不是原來的 63 ca b7 04。依欄優先順序讀回十六個 bytes,得到 6353e08c0960e104cd70b751bacad0e7。這也是互動台 Round 1 ShiftRows 右側矩陣的第一欄。
8. MixColumns:每次混合一個 Column
MixColumns 將每個 column 內的 4 個 bytes 混合。四個 columns 彼此獨立,可以平行運算。
「混合」的意思是每個輸出 byte 都由這個 column 的四個輸入 bytes 共同決定。只改一個輸入 byte,便可能改變該 column 的多個輸出 bytes;配合前面的 ShiftRows,影響會在後續 rounds 擴散。這裡仍是可逆的規則,不是把資訊丟掉的平均值或雜湊。
單一 Column 內部連接
輸入 Column 輸出 Column
a0 ---- x2 --+ y0 = 2*a0 ^ 3*a1 ^ a2 ^ a3
a1 ---- x3 --+--> XOR --> y0 y1 = a0 ^ 2*a1 ^ 3*a2 ^ a3
a2 --------- + y2 = a0 ^ a1 ^ 2*a2 ^ 3*a3
a3 --------- + y3 = 3*a0 ^ a1 ^ a2 ^ 2*a3
這裡的乘 2、乘 3是在有限體 GF(2^8) 中運算,不是一般整數乘法。
mul2(x) = xtime(x)
mul3(x) = xtime(x) XOR x
xtime 的 reduction polynomial 是 x^8 + x^4 + x^3 + x + 1。先丟掉移出的最高位;若原本 bit 7 為 1,再 XOR 8'h1b:
function automatic logic [7:0] xtime(input logic [7:0] x);
return {x[6:0], 1'b0} ^ (8'h1b & {8{x[7]}});
endfunction
例如 xtime(8'h57)=8'hae、xtime(8'hae)=8'h47。單一 column db 13 53 45 經 MixColumns 後應為 8e 4d a1 bc,可先用這個小測試檢查 GF 運算與 byte 順序。
IO
module aes_mix_columns (
input logic [127:0] state_i,
output logic [127:0] state_o
);
module aes_mix_one_column (
input logic [31:0] column_i,
output logic [31:0] column_o
);
初學者記憶方式
ShiftRows:bytes 在不同 columns 之間移動。MixColumns:把同一 column 內的 4 個 bytes 混合。- AES 標準沒有
MixRows這個步驟。
從 63 53 e0 8c 算到 5f 72 64 15
GF(2⁸) 的元素可寫成一個 byte;加法是 XOR,乘法依本節 polynomial 縮減。這不是帶 carry 的整數加法。先用 xtime 算乘 2:63→c6、53→a6、e0→db、8c→03。後兩者原 bit7=1,例如 xtime(e0)=c0 XOR 1b=db。
乘 3 等於乘 2 再 XOR 原值,所以得到 a5、f5、3b、8f。代入四條 column 公式:
y0=c6 XOR f5 XOR e0 XOR 8c=5f
y1=63 XOR a6 XOR 3b XOR 8c=72
y2=63 XOR 53 XOR db XOR 8f=64
y3=a5 XOR 53 XOR e0 XOR 03=15
每個輸出都用了同一個 column 的四個輸入。其餘三欄各自算,合起來才是第 17 節完整的 MixColumns 中間值。在互動台前進一步,核對 column0=5f 72 64 15;不必靠「看起來更亂」判斷正確。
9. AddRoundKey:State 與 Round Key 做 XOR
前面三個轉換使用固定、公開的規則。AddRoundKey 把本輪金鑰加入 state,讓結果取決於這把 key。不要因為其他轉換看起來複雜,就把金鑰路徑當成次要接線。
State 與 key 都是 128 bits,每個位元各做一次 XOR。這裡沒有 carry,也不需要逐 bit 計算 128 拍。真正要確認的是:兩邊是否使用相同的 byte 排列,送來的又是否為本輪所需的 key。
功能
state_o[127:0] = state_i[127:0] XOR round_key_i[127:0]
Byte 例子
State byte = 8'h53 = 0101_0011
Key byte = 8'hCA = 1100_1010
---------
Result = 8'h99 = 1001_1001
IO
module aes_add_round_key (
input logic [127:0] state_i,
input logic [127:0] round_key_i,
output logic [127:0] state_o
);
10. Final Round 的連接
第十輪省略 MixColumns,仍要做 SubBytes、ShiftRows 與 AddRoundKey。這是 AES-128 的規定,不能因為一般輪已經寫好,就再原封不動地呼叫一次。
Final Round 與 Normal Round 的唯一主要差別:
Normal Round:有 MixColumns
Final Round :No MixColumns
硬體不一定要另放一套最後輪電路。可以共用 SubBytes、ShiftRows 與 XOR,讓控制器在第十輪選擇繞過 MixColumns。圖在說明功能順序,並未要求兩份完整硬體。
金鑰仍需前進到 K10。若只做 bypass,卻繼續使用 K9,最後密文仍會錯。驗證這一輪時,分別看「選了哪條路」與「用了哪把 key」,比只看 counter 到了 10 更可靠。
11. AES-128 Key Expansion
先把兩種儲存需求分清楚:原始 key 要留下來,working key 則會跟著運算向前走。 如果只用一個暫存器,算完後它已經變成 K10;下一筆資料若直接接著算,就會從錯誤的 key 開始。保存 K0,讓每筆新交易重新展開,是本文採用的簡單安排。
原始 128-bit key 會被展開成 11 把 128-bit round keys。
一次 Key Expansion Step
將一把 128-bit key 分成四個 32-bit words:
key_i[127:0] = { w0, w1, w2, w3 }
計算式:
temp = SubWord(RotWord(w3)) XOR Rcon(round)
w4 = w0 XOR temp
w5 = w1 XOR w4
w6 = w2 XOR w5
w7 = w3 XOR w6
next_round_key = {w4, w5, w6, w7}
Key Expansion IO
module aes128_key_expand_step (
input logic [127:0] key_i,
input logic [3:0] round_i,
output logic [127:0] key_o
);
round_i 的有效範圍是 1 到 10。
RotWord({a,b,c,d})={b,c,d,a};SubWord 對四個 bytes 各做一次 S-box。Rcon(round) 是 32-bit word,round 1 到 10 依序為 01000000, 02000000, 04000000, 08000000, 10000000, 20000000, 40000000, 80000000, 1b000000, 36000000,常數在最高 byte,不能 XOR 到最低 byte。
Round Key 0 就是原始 key,另產生 K1~K10;所以是 10 次展開、11 把 key,並非 11 個完整 rounds。本文採 on-the-fly 展開:保存一把原始 key,另用 working round-key register 隨 round 更新,每個新區塊都從 K0 重來。預先保存全部 keys 是另一種取捨,僅 key 資料就需要 11 × 128 = 1408 bits。
算出 K1,再把它接回第一輪
標準 key 的 w0=00010203、w1=04050607、w2=08090a0b、w3=0c0d0e0f。RotWord(w3)=0d0e0f0c;四個 S-box 查表得到 d7ab76fe。Round 1 的 Rcon=01000000,故 temp=d6ab76fe。
接著 w4=00010203 XOR d6ab76fe=d6aa74fd;w5=04050607 XOR d6aa74fd=d2af72fa;w6=08090a0b XOR d2af72fa=daa678f1;w7=0c0d0e0f XOR daa678f1=d6ab76fe。這四個 words 合起來是 K1,不是四把不同輪的 key。
第 8 節已算出 Round 1 MixColumns 的第一欄 5f726415;它 XOR K1 第一個 word d6aa74fd,得到 89d810e8。其餘三欄使用 K1 對應欄,才得到完整第一輪結果。Initial XOR 使用 K0;這次 XOR 使用 K1。第 12 節在 E2 保存第一輪結果與 K1,互動台則拆成可觀察的微步,不能把每一次按「下一步」當成一拍 RTL。
12. Iterative RTL Datapath
Iterative 的意思是重複使用同一套 round 電路。State 暫存器保存上一輪結果,下一輪再把它送進同一條資料路徑。這樣不必為十輪各放一套電路,但同一時間只能推進本篇正在處理的區塊。
每個 Clock 做什麼
| Clock 階段 | State Register 更新內容 | 使用的 Key |
|---|---|---|
| 接收輸入 | plaintext | 無 |
| Round 0 | plaintext XOR original_key | Round Key 0 |
| Round 1 | Normal Round 結果 | Round Key 1 |
| Round 2 | Normal Round 結果 | Round Key 2 |
| … | … | … |
| Round 9 | Normal Round 結果 | Round Key 9 |
| Round 10 | Final Round 結果 | Round Key 10 |
| Output | 保持結果直到被接收 | 無 |
將拍數定義到時脈邊緣
假設 key 已載入,E0 是明文握手的上升緣。下表寫的是該邊緣完成更新後的值;Round 邏輯和 key expansion 都是組合邏輯。
| 邊緣 | 動作 | Working key | 邊緣後的 FSM |
|---|---|---|---|
| E0 | 鎖住明文,working key 載入 K0 | K0 | ROUND_0 |
| E1 | State XOR K0,counter 設為 1 | K0 | NORMAL_ROUND |
| E2~E10 | 各完成 round 1~9;使用組合產生的 next key | K1~K9 | 最後進入 FINAL_ROUND |
| E11 | 完成 round 10;State 直接保存密文 | K10 | OUTPUT_HOLD,out_valid=1 |
| E12 或更晚 | out_valid && out_ready,密文傳出 | 保持 | READY |
| E13 或更晚 | 最早接收下一個明文 | 重載 K0 | ROUND_0 |
因此,輸入握手到輸出有效是 11 個週期;最早輸出握手在 E12;無 stall 的輸入間隔是 13 個週期。這個簡單版本不在輸出握手同拍接新資料。若 clock frequency 為 f_clk,無 stall 且不換 key 時,吞吐量是 128 × f_clk / 13 bit/s;頻率必須由綜合與時序分析取得。
圖中由 State Register 直接供應 out_data,不再多接一拍 output register。NORMAL_ROUND 的核心更新概念如下,next_round_key 必須由目前的 working key 與 counter 組合產生:
// Conceptual fragment inside NORMAL_ROUND; helper functions are omitted.
state_reg <= normal_round(state_reg, next_round_key);
round_key_reg <= next_round_key;
若寫成 normal_round(state_reg, round_key_reg),右側讀到的是更新前的 key,會錯用 K[r-1]。FINAL_ROUND 同樣使用由 K9 展開的 K10,但 bypass MixColumns。
一拍一個 round 需要整條組合路徑在一個週期內穩定;key expansion 的延遲也要納入。此例的資料路徑用 16 S-boxes,獨立 key expansion 另用 4 個。若改用同步讀取記憶體或共用少量 S-box,就要重新排時序,不能沿用這張拍數表。
13. Controller FSM
FSM 可以看成核心的工作進度表。只記 round counter 還不夠,因為 counter 無法單獨表達「還沒載 key」「等明文」「密文算好但下游還沒收」這些情況。狀態負責區分階段,counter 負責在一般輪裡計數;兩者一起決定資料路徑的選擇與暫存器的更新。
狀態功能
| FSM State | 功能 |
|---|---|
NO_KEY | 等待載入 128-bit key |
READY | 已有 key,等待 plaintext |
ROUND_0 | 執行 initial AddRoundKey |
NORMAL_ROUND | 依序執行 rounds 1 到 9 |
FINAL_ROUND | 執行 round 10,不做 MixColumns |
OUTPUT_HOLD | out_valid=1,保持密文直到 out_ready=1 |
握手與重設契約
key_ready = rst_n && (fsm == NO_KEY || fsm == READY)。in_ready = rst_n && (fsm == READY) && !key_valid。若兩個 valid 同時出現,先收 key,明文來源維持資料到下一次握手;不會在同拍誤用舊 key。out_valid = rst_n && (fsm == OUTPUT_HOLD)。計算與等待輸出期間都不接新 key 或新明文。busy定義為交易尚未結束:從 ROUND_0 到 OUTPUT_HOLD 都是 1,包含下游背壓等待時間。rst_n=0的上升緣回到 NO_KEY,清除 key、working key、state 與 counter,取消未完成交易。重設期間 ready/valid 為 0,重設解除後必須重新載入 key。
同步 reset 只有遇到 clock edge 才清除暫存器;這個教學清除流程不等同於通過產品級 zeroization 或抗側通道驗證。
14. Valid/Ready 握手圖
握手要在約定的上升緣判斷,不能只看波形上兩條線曾經碰到高電位。核心提出 valid,表示結果已準備好;下游提出 ready,表示可以接收。兩者在取樣緣同時為 1,這筆密文才完成交付。
Rising edge E0 E1 ... E10 E11 E12 E13 E14
in_valid 1 0 ... 0 0 0 0 0
in_ready 1 0 ... 0 0 0 0 0
out_valid 0 0 ... 0 0 1 1 1
out_ready 0 0 ... 0 0 0 0 1
Transfer IN OUT
這張表列的是上升緣取樣前的訊號;E11 更新完成後 out_valid 才變成 1,所以 E12 才能最早握手。此例下游到 E14 才接收,E14 更新後回到 READY,若沒有 key_valid,in_ready 恢復為 1;E15 才能接下一筆。READY 空閒時 in_ready 可以持續為 1,不必等待 in_valid 才升高。
當 out_valid=1 但 out_ready=0:
out_valid必須保持為 1。out_data必須保持不變。- 核心停在
OUTPUT_HOLD。
回到感測器的例子,下游忙碌不需要重新加密。核心只要停止更新 state,保留已完成的密文。此時 busy 仍為 1,因為交易還沒結束。若把 busy 當成「round 電路正在算」,就可能過早接收下一筆並覆蓋結果。
15. 建議的 RTL Module Hierarchy
在 RTL 中 aes_sub_bytes 可以由 normal round 和 final round 共用同一套 combinational logic;上圖主要用來表達功能階層。
模組階層先幫你分配責任:controller 管時機,round 管 state 的轉換,key expansion 管下一把 key。每個積木都應有可獨立比對的輸入與輸出,才容易定位第一個錯誤。
實際例化時再決定哪些邏輯共用。若照功能樹各放一份 normal 與 final round,可能增加不必要的面積。反過來,共用較少的 S-box 雖然可能省面積,卻會增加排程工作;不能只改階層而保留原拍數。
16. 建議實作順序
第一次實作先求能定位錯誤,不必一開始就追最高吞吐量。先建立 byte 排列與四個積木的測試,再逐步整合。每加入一層,都應保留前一層的測試,避免新接線掩蓋既有錯誤。
- 先用一張圖說明 plaintext + key 產生 ciphertext。
- 解釋 128 bits 如何拆成 16 bytes 和 4 x 4 state。
- 只介紹四個基本積木:SubBytes、ShiftRows、MixColumns、AddRoundKey。
- 組合出 normal round。
- 說明 final round 少了 MixColumns。
- 再加入 key expansion。
- 最後才解釋 register、round counter、FSM 和 valid/ready。
讀懂這七步後,動手順序可更細:先測完 256 個 S-box 輸入,再用互不相同的 bytes 檢查 ShiftRows。接著測 xtime、MixColumns、一般輪與最後輪 bypass。Key expansion 獨立通過後,才接上暫存器與控制器。
整合最後要測兩筆連續交易。第一筆只證明從 K0 開始能算出密文;第二筆才能檢查 working key 是否重新載入 K0。再加入 key 同拍競爭、背壓與 reset,確認控制契約和數值一樣正確。
17. 最小實作檢查表
- State byte mapping 已明確定義。
- SubBytes 有 16 個 byte substitutions。
- ShiftRows 只做 byte permutation。
- MixColumns 對 4 個 columns 分別運算。
- AddRoundKey 是 128-bit XOR。
- Key Expansion 能產生 Round Keys 1 到 10。
- Rounds 1 到 9 有 MixColumns。
- Round 10 No MixColumns。
-
out_valid && !out_ready時輸出保持不變。 - 通過 FIPS-197 標準測試向量。
標準測試向量:
Key = 000102030405060708090A0B0C0D0E0F
Plaintext = 00112233445566778899AABBCCDDEEFF
Ciphertext = 69C4E0D86A7B0430D8CDB78070B4C55A
密文不對時,找第一個分歧點
以下值沿用上述 Key/Plaintext,全部依第 4 節的 packed byte 順序列出。對照 NIST 2001 版 FIPS 197 附錄 C.1,可先把錯誤縮小到一個子模組:
After initial XOR : 00102030405060708090a0b0c0d0e0f0
Round 1 SubBytes : 63cab7040953d051cd60e0e7ba70e18c
Round 1 ShiftRows : 6353e08c0960e104cd70b751bacad0e7
Round 1 MixColumns: 5f72641557f5bc92f7be3b291db9f91a
Round Key 1 : d6aa74fdd2af72fadaa678f1d6ab76fe
Round 1 result : 89d810e8855ace682d1843d8cb128fe4
| 測試 | 要抓的錯誤 |
|---|---|
| 256 種 S-box 輸入逐一比對標準表 | 查表缺項、抄錯值、latch |
| 16 個不同 byte 做 ShiftRows | row/column 與 packed vector 接反 |
| 每一輪 state 與 K1~K10 比對 | key 晚一拍、Rcon 位置錯、round counter 錯 |
| 同一 key 連續加密兩個區塊 | 第二塊誤從 K10 開始 |
| key_valid 與 in_valid 同時為 1 | 換 key 優先順序未落實 |
| out_ready 拉低數拍後恢復 | out_valid 消失、密文被覆蓋或重複傳出 |
| 計算中與 OUTPUT_HOLD 時 reset | 舊交易在 reset 後冒出、未重新載 key |
| 多組隨機 key/明文與獨立參考模型比較 | 單一已知答案未涵蓋的資料錯誤 |
Testbench 只在 valid && ready 的上升緣記一筆交易。Scoreboard 比對接收/送出數量與順序,reset 時清掉取消的期待結果,並設 timeout,避免把卡死當成測試仍在跑。
這些檢查分成數值與交易兩條線。已知答案能抓到密文錯誤,卻不會自動抓到同一筆密文被送了兩次。只檢查交易數量,也可能讓錯誤密文通過。兩條線都成立,才算完成本篇的功能驗證目標。
做對演算法後,還缺什麼?
這裡是一個單區塊加密 primitive。要處理訊息,還要選擇合適的運作模式,定義 nonce/IV、金鑰管理與完整性保護;通過向量也不代表已處理功耗/電磁側通道或故障注入。先把本篇的功能與握手驗證完成,再進到系統安全設計。
18. 參考資料與閱讀範圍
- NIST FIPS 197,2023 更新版:§3.4 State、§4.2 有限體乘法、§5.1 加密與 §5.2 Key Expansion。2023 更新未改變 AES 演算法。
- NIST FIPS 197,2001 原版:附錄 C.1 為本文
001122…向量及中間值的來源;原版用於歷史範例,演算法規範以更新版為準。 - NIST Examples with Intermediate Values:延伸比對資料。
本文的 E0~E13 時序、key 優先策略與 reset 行為是教學架構的設計契約,並非 NIST 規定的介面。
本篇先省略解密、訊息運作模式、AXI/DMA 與高效能 pipeline。完成這個核心後,再依系統需求加入這些功能。側通道與故障防護屬於另外的安全驗證,不能由單一測試向量推得。
MY ACADEMY · LESSON FILM
教學影片
影片依序說明本課的資料路徑。看完一段,可以回到下面的互動練習,改變輸入或故障條件。動畫呈現教學模型;它沒有替代 RTL 模擬。
旁白使用合成聲音。互動教學與動畫均有模型邊界;請以本課的來源與驗證範圍解讀結果。
MY ACADEMY · RTL LAB
操作 AES:看每一步如何改變 state
輸入一個 128-bit 區塊與金鑰。先停在 Round 1 的 ShiftRows,對照兩側矩陣;再走到 Round 10,確認這輪沒有 MixColumns。
矩陣使用 FIPS 197 的欄優先排列。位置 [row, column] 對應輸入 byte 4×column+row。每一步顯示運算前後的值。這是演算法微步,不宣稱每一步對應一拍 RTL。
證據範圍:瀏覽器功能教學模型。獨立比對使用瀏覽器 Web Crypto。沒有執行 RTL、綜合、形式證明或側通道測試。
獨立比對使用 Web Crypto 的 CBC 第一個區塊,IV=0,所以 C0=AES_K(P0 XOR 0)=AES_K(P0)。只取輸出的前 16 bytes;額外 padding 區塊不影響第一塊。這是在比對單區塊 AES,不是在此教學台實作訊息模式。
運算前
運算後
查看區塊、排程與輪金鑰
輸出握手:valid/ready
走到 OUTPUT 後,輸出保持不變。切換 ready,再按「取樣一拍」才能記錄交付。設定 ready 本身不會交付。這個握手是另外定義的教學介面。
遷移練習:如果 downstream 尚未 ready,RTL 能否清除 output_valid 或覆寫結果?先用一句話寫出保持規則:除非 reset 取消交易,valid 與資料必須保持到交握成立。SVA 語法可作後續練習。
學習指南
密碼演算法 RTL 設計
查看課程大綱 → · 進度只計入已發布課程
先備知識
- AES Round 運算與同步邏輯
我學會了什麼
- 把 AES 分成資料路徑與控制
- 定義 valid/ready 行為
- 保持位元組順序一致