这一篇在干嘛?

前三章讲了速度、面积、功耗的架构原理,这一章用 AES(高级加密标准)加密核把它们全部实战一遍:同一个加密算法,做出紧凑迭代、部分流水、全流水三种架构,用真实器件数据对比”吞吐量 × 面积”的取舍。读完你会明白:为什么流水线架构在最坏情况下可能退化到和迭代架构一样慢——以及全流水密钥扩展如何救场。

背景:AES 是什么?

AES(原名 Rijndael)是 NIST 在 FIPS PUB 197 中发布的电子数据加密标准,用来取代已被攻破的 DES。它是对称密钥分组密码:把 128 位明文块映射为 128 位密文块,密钥长度可选 128/192/256 位(密钥越长 → 密钥空间越大 → 越安全)。AES 刻意设计得适合硬件实现——这里”容易”意味着不易出错(可靠)且更快(简单的组合逻辑)。本章只深入与架构相关的部分,完整的算法细节请查 FIPS 197。

4.1 AES 的三种架构

4.1.1 算法结构:轮变换与密钥扩展

AES 的加密由四个变换模块组成,每跑一遍叫一”轮(round)“:

变换本质
SubBytes(字节替换)位映射(查表)
ShiftRows(行移位)交换(纯布线)
MixColumns(列混合)GF(2⁸) 上的乘法变换
AddRoundKey(轮密钥加)GF(2) 上的逐位异或

轮数由密钥长度决定:128 位 → 10 轮;192 位 → 12 轮;256 位 → 14 轮。每一轮用的轮密钥都不同,由原始密钥经密钥扩展(key expansion)派生——密钥扩展与数据通路并行运行,它是本章架构设计的焦点之一。

密钥扩展规则(设 word = 32 位,N_k = Keysize/32,即 4/6/8):扩展密钥的前 N_k 个字直接填原始密钥;此后每个新的 32 位字 = 前一个字 XOR 再往前第 N_k 个字。当字的位置是 N_k 的整数倍时,要先做变换再异或:先循环移位(RotWord),再对 4 个字节做 8 位映射(SubWord),最后异或一个轮常量 Rcon(FIPS 197 定义为 [x^(i-1), 00, 00, 00],x 即 GF(2⁸) 中的 {02})。

单个密钥扩展单元是独立自治的模块,实现如下:

module KeyExpl1Enc(
    // updated values to be passed to next iteration
    output [3:0] oKeyIter, oKeyIterModNk,
    oKeyIterDivNk,
    output [32*`Nk-1:0] oNkKeys,
    input iClk, iReset,
    // represents total # of iterations and value mod Nk
    input [3:0] iKeyIter, iKeyIterModNk,
    iKeyIterDivNk,
    // The last Nk keys generated in key expansion
    input [32*`Nk-1:0] iNkKeys);
    // updated values to be passed to next iteration
    reg [3:0] oKeyIter, oKeyIterModNk,
    oKeyIterDivNk;
    reg [32*`Nk-1:0] OldKeys;
    reg [31:0] InterKey; // intermediate key value
    wire [32*`Nk-1:0] oNkKeys;
    wire [31:0] PrevKey, RotWord, SubWord,
    NewKeyWord;
    wire [31:0] KeyWordNk;
    wire [31:0] Rcon;
 
assign PrevKey = iNkKeys[31:0]; // last word in key array
assign KeyWordNk = OldKeys[32*`Nk-1:32*`Nk-32];
 
// 1 byte cyclic permutation
assign RotWord = {PrevKey[23:0], PrevKey[31:24]};
 
// new key calculated in this round
assign NewKeyWord = KeyWordNk ^ InterKey;
 
// calculate new key set
assign oNkKeys = {OldKeys[32*`Nk-33:0], NewKeyWord};
 
// calculate Rcon over GF(2^8)
assign Rcon = iKeyIterDivNk == 8'h1 ? 32'h01000000:
    iKeyIterDivNk == 8'h2 ? 32'h02000000:
    iKeyIterDivNk == 8'h3 ? 32'h04000000:
    iKeyIterDivNk == 8'h4 ? 32'h08000000:
    iKeyIterDivNk == 8'h5 ? 32'h10000000:
    iKeyIterDivNk == 8'h6 ? 32'h20000000:
    iKeyIterDivNk == 8'h7 ? 32'h40000000:
    iKeyIterDivNk == 8'h8 ? 32'h80000000:
    iKeyIterDivNk == 8'h9 ? 32'h1b000000:
    32'h36000000;
SboxEnc SboxEnc0(.iPreMap(RotWord[31:24]),
 .oPostMap(SubWord[31:24]));
SboxEnc SboxEnc1(.iPreMap(RotWord[23:16]),
 .oPostMap(SubWord[23:16]));
SboxEnc SboxEnc2(.iPreMap(RotWord[15:8]),
 .oPostMap(SubWord[15:8]));
SboxEnc SboxEnc3(.iPreMap(RotWord[7:0]),
 .oPostMap(SubWord[7:0]));
`ifdef Nk8
 
wire [31:0] SubWordNk8;
 
// Substitution only when Nk = 8
SboxEnc SboxEncNk8_0(.iPreMap(PrevKey[31:24]),
 .oPostMap(SubWordNk8[31:24]));
SboxEnc SboxEncNk8_1(.iPreMap(PrevKey[23:16]),
 .oPostMap(SubWordNk8[23:16]));
SboxEnc SboxEncNk8_2(.iPreMap(PrevKey[15:8]),
 .oPostMap(SubWordNk8[15:8]));
SboxEnc SboxEncNk8_3(.iPreMap(PrevKey[7:0]),
 .oPostMap(SubWordNk8[7:0]));
`endif
 
always @(posedge iClk)
if(!iReset) begin
    oKeyIter <= 0;
    oKeyIterModNk <= 0;
    InterKey <= 0;
    oKeyIterDivNk <= 0;
    OldKeys <= 0;
end
else begin
    oKeyIter <= iKeyIter + 1;
    OldKeys <= iNkKeys;
 
    // update "Key iteration mod Nk" for next iteration
    if(iKeyIterModNk + 1 == `Nk) begin
        oKeyIterModNk <= 0;
        oKeyIterDivNk <= iKeyIterDivNk+1;
    end
    else begin
        oKeyIterModNk <= iKeyIterModNk + 1;
        oKeyIterDivNk <= iKeyIterDivNk;
    end
 
    if(iKeyIterModNk == 0)
        InterKey <= SubWord ^ Rcon;
    `ifdef Nk8
    // an option only for Nk = 8
    else if(iKeyIterModNk == 4)
        InterKey <= SubWordNk8;
    `endif
    else
        InterKey <= PrevKey;
end
endmodule

点评

  • RotWord 用一条拼接赋值完成字节循环移位——纯布线操作,零逻辑;
  • 4 个 SboxEnc 实例并行完成 SubWord 的 4 字节映射;
  • oKeyIterModNk(当前迭代序号 mod N_k)用于判断”这个字是否落在 N_k 的倍数上”(决定是否做 SubWord+Rcon 变换);Nk=8 时还有额外的每 4 个字一次的替换(`ifdef Nk8 分支)——这是 AES-256 特有的规则;
  • Rcon 用三层条件表达式硬编码,因为它是标准固定的常量表。

数据路径的自治单元则是把一轮的四个变换串起来:

module RoundEnc(
    output [32*`Nb-1:0] oBlockOut,
    output oValid,
    input iClk, iReset,
    input [32*`Nb-1:0] iBlockIn, iRoundKey,
    input iReady,
    input [3:0] iRound);
    wire [32*`Nb-1:0] wSubOut, wShiftOut, wMixOut;
    wire wValidSub, wValidShift, wValidMix;
 
SubBytesEnc sub( .iClk(iClk), .iReset(iReset),
    .iBlockIn(iBlockIn),
    .oBlockOut(wSubOut),
    .iReady(iReady),
    .oValid(wValidSub));
 
ShiftRowsEnc shift( .iBlockIn(wSubOut), .oBlockOut(wShiftOut),
    .iReady(wValidSub), .oValid (wValidShift));
 
MixColumnsEnc mixcolumn( .iClk(iClk), .iReset(iReset),
    .iBlockIn(wShiftOut),
    .oBlockOut(wMixOut),
    .iReady(wValidShift),
    .oValid(wValidMix),
    .iRound(iRound));
 
AddRoundKeyEnc addroundkey(.iClk(iClk), .iReset(iReset),
    .iBlockIn(wMixOut),
    .iRoundKey(iRoundKey),
    .oBlockOut(oBlockOut),
    .iReady(wValidMix),
    .oValid(oValid));
endmodule

点评:四个子模块用 iReady/oValid 握手信号串联成链,每个子模块知道自己的输出何时有效。假设每个 Round 的延迟是 4 拍(后面按逻辑均衡分配流水级时印证这一点),各级流水线划分如下。

4.1.2 每个子模块的流水级分配

SubBytes:1 级流水。 SubBytes 是 8 位到 8 位的映射,由于迭代特性和映射空间很小,最自然的实现是查表(look-up table)——一个同步的 8×256(2⁸)ROM,带一级流水(图 4.1)。

图:SubBytes 模块中的 8 位映射——同步 ROM 查表

ShiftRows:0 级流水。 它只是把数据块的行做交换,不需要任何逻辑(纯布线)。如果在这里再插一级流水寄存器,会造成流水级之间逻辑严重不均衡,反而降低最高频率和总吞吐量(图 4.2)——这正是第 1 章”寄存器平衡”思想的反面应用:没有逻辑的地方不要放寄存器

图:ShiftRows 实现——纯布线交换,零逻辑

MixColumns:2 级流水。 它是四个变换里逻辑最多的,是插额外流水级的最佳位置。其层次结构如图 4.3:MixColumns 由 Map-Column 模块构成(图 4.4),Map-Column 又以 Poly-Mult X2(多项式 ×2 乘法器)为基本构件(图 4.5)。

图:MixColumns 层次结构

图:Map-Column 层次结构

图:多项式乘法 ×2 模块

AddRoundKey:1 级流水。 把来自密钥扩展流水线的轮密钥与数据块做异或(图 4.6)。

图:AddRoundKey 模块——数据块与轮密钥异或

合计 1+0+2+1 = 4 拍,正好对应前面假设的”每个 Round 延迟 4 时钟”——这就是第 1 章讲的逻辑均衡:流水级加在逻辑最重的地方。

4.1.3 紧凑架构(Compact Architecture)

第一种实现:迭代复用逻辑资源。数据与密钥先在 Initial Round 模块相加并寄存,然后进入加密循环,按顺序流过 SubBytes → ShiftRows → MixColumns → AddRoundKey,每轮结束寄存一次,重复至轮数用完(图 4.7)。这就是第 2 章”卷起流水线”思想的标准应用。

图:紧凑(迭代)实现——一套 Round 逻辑循环使用

顶层实现:

module AES_Enc_core(
    output [32*`Nb-1:0] oCiphertext, // output ciphertext
    output oValid, // data at output is valid
    output oKeysValid,
    input    iClk, iReset,
    input [32*`Nb-1:0]    iPlaintext, // input data to be encrypted
    input [32*`Nk-1:0]    iKey, // input cipher key
    input    iReady, // valid data to encrypt
    input    iNewKey); // signals new key is input
// registered inputs
wire    [32*`Nk-1:0]    wKeyReg;
wire    wNewKeyReg, wReadyReg;
wire    [127:0]    wPlaintextReg, wBlockOutInit;
wire    [127:0]    wRoundKeyInit, wRoundKey;
 
// register inputs
InputRegsEnc InputRegs( .iClk(iClk), .iReset(iReset),
    .iKey(iKey),
    .iNewKey(iNewKey), .iPlaintext (iPlaintext),
    .oKeysValid(oKeysValid),
    .iReady(iReady),
    .oKey(wKeyReg), .oPlaintext (wPlaintextReg),
    .oReady(wReadyReg));
 
// initial addition of round key
AddRoundKeyEnc InitialKey( .iClk(iClk), .iReset(iReset),
    .iBlockIn(wPlaintextReg),
    .iRoundKey(wRoundKeyInit),
    .oBlockOut(wBlockOutInit),
    .iReady(wReadyReg),
    .oValid(wValidInit));
 
// Number of rounds is a function of key size (10, 12, or 14)
// Key expansion block
KeyExpansionEnc KeyExpansion( .iClk(iClk), .iReset (iReset),
    .iNkKeys(wKeyReg),
    .iReady(wReadyReg),
    .oRoundKey(wRoundKey));
 
RoundsIterEnc RoundsIter( .iClk(iClk), .iReset(iReset),
    .iBlockIn(wBlockOutInit),
    .oBlockOut(oCiphertext),
    .iReady(wValidInit),
    .oValid(oValid),
    .iRoundKey(wRoundKey));
 
`ifdef Nk4
assign wRoundKeyInit = wKeyReg[128-1:0];
`endif
 
`ifdef Nk6
assign wRoundKeyInit = wKeyReg[192-1:192-128];
`endif
 
`ifdef Nk8
assign wRoundKeyInit = wKeyReg[256-1:256-128];
`endif
 
endmodule

点评:紧凑架构的迭代操作由 KeyExpansionEnc(密钥扩展迭代)和 RoundsIterEnc(数据通路迭代)两个模块承担,每一轮迭代从密钥扩展模块拿一个唯一的轮密钥。初始轮密钥按 N_k 配置取 iKey 的最高 128 位。

密钥扩展的循环复用如下——把单个扩展模块的输出接回自己的输入:

module KeyExpansionEnc(
    output [128-1:0] oRoundKey,
    input iClk, iReset,
    // The last Nk keys generated in initial key expansion
    input [32*`Nk-1:0] iNkKeys,
    input iReady); // signals a new key is input
    wire [3:0] KeyIterIn, KeyIterOut;
    wire [3:0] KeyIterDivNkIn, KeyIterDivNkOut;
    wire [3:0] KeyIterModNkIn, KeyIterModNkOut;
    wire [32*`Nk-1:0] NkKeysOut, NkKeysIn;
    wire wReady;
 
assign wReady = iReady;
assign KeyIterIn = wReady ? `Nk : KeyIterOut;
assign oRoundKey = NkKeysOut[32*`Nk-1:32*`Nk-128];
assign KeyIterModNkIn = wReady ? 4'h0 : KeyIterModNkOut;
assign KeyIterDivNkIn = wReady ? 4'h1 : KeyIterDivNkOut;
assign NkKeysIn = wReady ? iNkKeys : NkKeysOut;
 
KeyExp1Enc KeyExp1(.iClk(iClk), .iReset(iReset),
    .iKeyIter(KeyIterIn),
    .iKeyIterModNk(KeyIterModNkIn),
    .iNkKeys(NkKeysIn), .iKeyIterDivNk
    (KeyIterDivNkIn),
    .oKeyIter(KeyIterOut),
    .oKeyIterModNk(KeyIterModNkOut),
    .oNkKeys(NkKeysOut),
    .oKeyIterDivNk(KeyIterDivNkOut));
endmodule

点评(图 4.8):KeyExp1Enc 的输出 NkKeysOut / KeyIterOut 等被回环接回自己的输入(非 wReady 时取输出值),每轮迭代用同一套扩展逻辑算下一个字——逻辑为每一轮所复用。wReady 有效时注入初值(迭代计数从 Nk 开始、mod 归零、载入新密钥),相当于状态机的复位。

图:迭代式密钥扩展——输出回环接回输入

4.1.4 部分流水架构(Partially Pipelined Architecture)

第二种实现:一轮 AES 用 11~14 个时钟完成(视密钥长度)。如图 4.9,例化多个数据通路核组成流水线,密钥扩展以静态方式完成。以下是 N_k = 4 的代码:

图:部分流水实现——数据路径多级级联,密钥静态扩展

module AES_core(
    output [32*`Nb-1:0] oCiphertext, // output ciphertext
    output oValid, // data at output is valid
    // signals that new key has been completely processed
    output oKeysValid,
    input iClk, iReset,
    input [32*`Nb-1:0] iPlaintext, // input data to be encrypted
    input [32*`Nk-1:0] iKey, // input cipher key
    input iReady, // valid data to encrypt
    input iNewKey); // signals new key is input
wire [32*`Nb-1:0] wRoundKey1, wRoundKey2, wRoundKey3, wRoundKey4, wRoundKey5, wRoundKey6, wRoundKey7, wRoundKey8, wRoundKey9, wRoundKeyFinal, wRoundKeyInit;
wire [32*`Nb-1:0] wBlockOut1, wBlockOut2, wBlockOut3, wBlockOut4, wBlockOut5, wBlockOut6,
wBlockOut7, wBlockOut8,
wBlockOut9, wBlockOutInit;
wire [32*`Nk-1:0]    wNkKeysInit;
wire [3:0]    wKeyIterInit;
wire [3:0]    wKeyIterModNkInit;
wire [3:0]    wKeyIterDivNkInit;
wire    wValid1, wValid2, wValid3,
    wValid4,
    wValid5, wValid6, wValid7,
    wValid8,
    wValid9, wValidFinal,
    wValidInit;
wire    wNewKeyInit;
wire [128*(`Nr+1)-1:0]    wKeys; // complete set of round keys
 
// registered inputs
wire [32*`Nk-1:0]    wKeyReg;
wire    wNewKeyReg, wReadyReg;
wire [127:0]    wPlaintextReg;
 
// register inputs
InputRegs InputRegs(    .iClk(iClk), .iReset(iReset),
    .iKey(iKey),
    .iNewKey(iNewKey),
    .iPlaintext(iPlaintext),
    .iReady(iReady), .oKey(wKeyReg)
    .oNewKey(wNewKeyReg),
    .oPlaintext(wPlaintextReg),
    .oReady(wReadyReg));
 
// initial key expansion
KeyExpInit KeyExpInit(    .iClk(iClk), .iReset(iReset),
    .iNkKeys(wKeyReg), .iNewKey
    (wNewKeyReg),
    .oKeyIter(wKeyIterInit),
    .oNewKey(wNewKeyInit),
    .oKeyIterModNk
    (wKeyIterModNkInit),
    .oNkKeys(wNkKeysInit),
    .oKeyIterDivNk
    (wKeyIterDivNkInit));
 
// initial addition of round key
AddRoundKey InitialKey(  .iClk(iClk), .iReset(iReset),
    .iBlockIn(wPlaintextReg),
    .iRoundKey(wRoundKeyInit),
    .oBlockOut(wBlockOutInit),
    .iReady(wReadyReg),
    .oValid(wValidInit));
 
// Key expansion block
KeyExpansion KeyExpansion( .iClk(iClk),
    .iReset(iReset),
    .iKeyIter(wKeyIterInit),
    .iKeyIterModNk(wKeyIterModNkInit),
    .iNkKeys(wNkKeysInit),
    .iKeyIterDivNk(wKeyIterDivNkInit),
    .iNewKey(wNewKeyInit),
    .oKeys(wKeys), .oKeysValid (oKeysValid));
 
// round transformation blocks
Round R1( .iClk(iClk), .iReset (iReset),
    .iBlockIn(wBlockOutInit),
    .iRoundKey(wRoundKey1),
    .oBlockOut(wBlockOut1),
    .iReady(wValidInit),
    .oValid(wValid1));
 
Round R9( .iClk(iClk), .iReset (iReset),
    .iBlockIn(wBlockOut8),
    .iRoundKey(wRoundKey9),
    .oBlockOut(wBlockOut9),
    .iReady(wValid8),
    .oValid(wValid9));
 
// 10 rounds total
// Initial key addition
assign wRoundKeyFinal = wKeys[128*(`Nr-7)-1: 128*(`Nr-8)];
 
// round key assignments
assign wRoundKey9 = wKeys[128*(`Nr-6)-1: 128*(`Nr-7)];
assign wRoundKey8 = wKeys[128*(`Nr-5)-1: 128*(`Nr-6)];
assign wRoundKey7 = wKeys[128*(`Nr-4)-1: 128*(`Nr-5)];
assign wRoundKey6 = wKeys[128*(`Nr-3)-1: 128*(`Nr-4)];
assign wRoundKey5 = wKeys[128*(`Nr-2)-1: 128*(`Nr-3)];
assign wRoundKey4 = wKeys[128*(`Nr-1)-1: 128*(`Nr-2)];
assign wRoundKey3 = wKeys[128*`Nr-1: 128*(`Nr-1)];
assign wRoundKey2 = wKeys[128*(`Nr+1)-1: 128*`Nr];
 
assign wRoundKey1 = wNkKeysInit[128-1:0];
assign wRoundKeyInit = iKey[128-1:0];
 
FinalRound FinalRound( .iClk(iClk), .iReset(iReset),
    .iBlockIn(wBlockOut9),
    .iRoundKey(wRoundKeyFinal),
    .oBlockOut(oCiphertext),
    .iReady(wValid9), .oValid
    (oValid));
endmodule

点评

  • 中间的 R2~R8 与 R1/R9 结构完全相同(代码里省略),共 10 级 Round 串成流水线;最后一轮 FinalRound 与普通 Round 的区别是没有 MixColumns(标准规定);
  • 密钥扩展先”静态”算完整套轮密钥存进 wKeys(宽度 128×(Nr+1)),各级 Round 按下标切取自己那把轮密钥;
  • 省略号式的写法说明:这种架构的模块数与轮数成正比,代码量大但结构机械。

这个架构有个致命软肋:如果新密钥的到来速度快于加密速度,外围系统必须”聪明地”等流水线排空(pipe to empty)才能送入新数据块和新密钥——这个状态必须反馈给上游,让数据和密钥被正确缓冲。最坏情况下(每加密一个数据块就换一次密钥),流水线永远填不满,吞吐量退化到与迭代架构相同——那这片巨大的面积就白花了(更别提没达到宣传吞吐量的失落感)。下一节的架构就是为消灭这个问题而生的。

4.1.5 全流水架构(Fully Pipelined Architecture)

“全流水”指的是:密钥扩展与轮变换流水线并行运行,流水线中对应级在恰当的时刻互相供给精确的信息——任何一级、任何数据块的轮密钥只在恰好需要的那一拍有效、被对应的 Round 使用。于是每个数据块都可以用不同的密钥,且延迟和等待状态零惩罚;无论密钥怎么换,轮变换流水线的最大吞吐量始终保持(图 4.10)。

图:全流水密钥扩展——与数据流水线逐级并行对齐

要做到这一点,时序必须非常精确:

  1. 密钥扩展单次迭代(扩展 4 个 32 位字)与”当前正在生成、下轮就要用的密钥”的前一轮完全同步;
  2. Key Expansion 块的时钟延迟必须与 Round 块相当(典型 1~4 拍);
  3. 每个密钥扩展块必须在恰好相同数量的时钟周期内生成轮密钥,且密钥呈现给 AddRoundKey 子块时必须有效。

实现手段:把每个密钥扩展块拆成 4 个增量扩展块,每个块按 NIST 规范生成一个字(128/4 = 32 位)的密钥,各给一级流水(图 4.11)。S-box 实现为同步 8×256 ROM;为了保持延迟时序精确,Rcon 计算也要加一级流水(图 4.12)。

图:32 位密钥扩展级的传播

图:Key-Exp1 内部单字扩展——S-box 用同步 ROM,Rcon 加流水级对齐

还有个细节:密钥流水线必须比数据流水线早一拍完成——因为 AddRoundKey 块要把 XOR 操作的最终结果打迸最后的寄存器,轮密钥必须提前就位。换句话说,密钥扩展块的第 4 拍必须与对应 Round 块的第 3 拍同步。这靠密钥扩展过程开头的初始密钥加法来错相(图 4.13):前 128 位之外的密钥数据在第一个时钟就开始扩展,而数据流水线在第二个时钟才启动(初始 AddRoundKey 操作引入一拍延迟)。

图:密钥流水线的相位偏移——比数据流水线早一拍

N_k = 4 的顶层实现:

module AES_core(
    output [32*`Nb-1:0] oCiphertext, // output ciphertext
    output oValid, // data at output is valid
    input iClk, iReset,
    input [32*`Nb-1:0] iPlaintext, // input data to be encrypted
    input [32*`Nk-1:0] iKey, // input cipher key
    input iReady); // valid data to encrypt
wire [32*`Nb-1:0] wRoundKey1, wRoundKey2, wRoundKey3, wRoundKey4, wRoundKey5, wRoundKey6, wRoundKey7, wRoundKey8, wRoundKey9, wRoundKeyFinal, wRoundKeyInit;
wire [32*`Nb-1:0] wBlockOut1, wBlockOut2, wBlockOut3, wBlockOut4, wBlockOut5, wBlockOut6, wBlockOut7, wBlockOut8, wBlockOut9, wBlockOutInit;
wire [32*`Nk-1:0] wNkKeys1, wNkKeys2, wNkKeys3, wNkKeys4, wNkKeys5, wNkKeys6, wNkKeys7, wNkKeys8, wNkKeys9, wNkKeysFinal, wNkKeysInit;
wire [3:0] wKeyIter1, wKeyIter2, wKeyIter3, wKeyIter4, wKeyIter5, wKeyIter6, wKeyIter7, wKeyIter8, wKeyIter9, wKeyIterFinal, wKeyIterInit;
wire [3:0] wKeyIterModNk1, wKeyIterModNk2, wKeyIterModNk3, wKeyIterModNk4, wKeyIterModNk5, wKeyIterModNk6, wKeyIterModNk7, wKeyIterModNk8, wKeyIterModNk9, wKeyIterModNkFinal, wKeyIterModNkInit;
wire [3:0] wKeyIterDivNk1, wKeyIterDivNk2, wKeyIterDivNk3, wKeyIterDivNk4, wKeyIterDivNk5, wKeyIterDivNk6, wKeyIterDivNk7, wKeyIterDivNk8, wKeyIterDivNk9, wKeyIterDivNkFinal, wKeyIterDivNkInit;
wire wValid1, wValid2, wValid3, wValid4, wValid5, wValid6, wValid7, wValid8, wValid9, wValidFinal, wValidInit;
 
// Initial key addition
assign wRoundKeyInit = wKeyReg[32*`Nk-1:32*`Nk-128];
 
// round key assignments
assign wRoundKey1    = wNkKeysInit[32*`Nb-1:0];
assign wRoundKey2    = wNkKeys1[32*`Nb-1:0];
assign wRoundKey3    = wNkKeys2[32*`Nb-1:0];
assign wRoundKey4    = wNkKeys3[32*`Nb-1:0];
assign wRoundKey5    = wNkKeys4[32*`Nb-1:0];
assign wRoundKey6    = wNkKeys5[32*`Nb-1:0];
assign wRoundKey7    = wNkKeys6[32*`Nb-1:0];
assign wRoundKey8    = wNkKeys7[32*`Nb-1:0];
assign wRoundKey9    = wNkKeys8[32*`Nb-1:0];
 
// register inputs
InputRegs InputRegs(    .iClk(iClk), .iReset(iReset),
    .iKey(iKey),
    .iPlaintext(iPlaintext),
    .iReady(iReady), .oKey(wKeyReg),
    .oPlaintext(wPlaintextReg),
    .oReady(wReadyReg));
 
// initial key expansion
KeyExpInit KeyExpInit(    .iClk(iClk), .iReset(iReset),
    .iNkKeys(wKeyReg),
    .oKeyIter(wKeyIterInit),
    .oKeyIterModNk(wKeyIterModNkInit),
    .oNkKeys(wNkKeysInit),
    .oKeyIterDivNk
    (wKeyIterDivNkInit));
 
// initial addition of round key
AddRoundKey InitialKey(    .iClk(iClk), .iReset(iReset),
    .iBlockIn(wPlaintextReg),
    .iRoundKey(wRoundKeyInit),
    .oBlockOut(wBlockOutInit),
    .iReady(wReadyReg),
    .oValid(wValidInit));
 
// Number of rounds is a function of key size (10, 12, or 14)
 
// Key expansion blocks
KeyExpBlock KeyExpBlock1(    .iClk(iClk), .iReset(iReset),
    .iKeyIter(wKeyIterInit),
    .iKeyIterModNk(wKeyIterModNkInit),
    .iNkKeys(wNkKeysInit),
    .iKeyIterDivNk(wKeyIterDivNkInit),
    .oKeyIter(wKeyIter1),
    .oKeyIterModNk(wKeyIterModNk1),
    .oNkKeys(wNkKeys1),
    .oKeyIterDivNk(wKeyIterDivNk1));
 
KeyExpBlock KeyExpBlock8( .iClk(iClk), .iReset(iReset),
    .iKeyIter(wKeyIter7),
    .iKeyIterModNk(wKeyIterModNk7),
    .iNkKeys(wNkKeys7),
    .iKeyIterDivNk(wKeyIterDivNk7),
    .oKeyIter(wKeyIter8),
    .oKeyIterModNk(wKeyIterModNk8),
    .oNkKeys(wNkKeys8),
    .oKeyIterDivNk(wKeyIterDivNk8));
 
// round transformation blocks
Round R1( .iClk(iClk), .iReset (iReset),
    .iBlockIn(wBlockOutInit),
    .iRoundKey(wRoundKey1),
    .oBlockOut(wBlockOut1),
    .iReady(wValidInit),
    .oValid(wValid1));
...
 
Round R9( .iClk(iClk), .iReset (iReset),
    .iBlockIn(wBlockOut8),
    .iRoundKey(wRoundKey9),
    .oBlockOut(wBlockOut9),
    .iReady(wValid8),
    .oValid(wValid9));
 
// 10 rounds total
assign wRoundKeyFinal = wNkKeys9[32*`Nb-1:0];
 
KeyExpBlock KeyExpBlock9( .iClk(iClk), .iReset(iReset),
    .iKeyIter(wKeyIter8),
    .iKeyIterModNk (wKeyIterModNk8),
    .iNkKeys(wNkKeys8),
    .iKeyIterDivNk (wKeyIterDivNk8),
    .oKeyIter(wKeyIter9),
    .oKeyIterModNk (wKeyIterModNk9),
    .oNkKeys(wNkKeys9),
    .oKeyIterDivNk(wKeyIterDivNk9));
FinalRound FinalRound( .iClk(iClk), .iReset(iReset), .iBlockIn(wBlockOut9), .iRoundKey(wRoundKeyFinal), .oBlockOut(oCiphertext), .iReady(wValid9), .oValid (oValid));
endmodule

点评

  • 与部分流水架构最大的不同:不再是”一个密钥扩展器算完全部”,而是 KeyExpBlock1KeyExpBlock9 每级 Round 旁边都配一个密钥扩展块,密钥像数据一样逐级流水地生成;
  • 对比前面部分流水的代码——那里所有轮密钥从静态大数组 wKeys 切片取得,而这里 wRoundKeyN = wNkKeys(N-1),来自上一级扩展块刚流出来的值;
  • 代价显而易见:S-box、Rcon 等资源每一级都要复制一份,面积暴涨——这正是 4.2 节数据表里的 5894 个 LUT。

4.2 性能与面积的对比

三种架构用同一套 Verilog 和相同的编码规范实现,分别目标到两种工艺。先看 Xilinx Virtex II FPGA 的数据(表 4.1):

架构面积(Xilinx LUTs)最佳吞吐量(Mbps)最坏情况吞吐量
迭代(Iterative)886340340
部分流水(Partially Pipelined)443215,400314
全流水(Fully Pipelined)589415,40015,400

(最坏情况吞吐量假设每个数据块都引入新密钥。)

再对比 0.35 μm AMI ASIC 工艺(表 4.2):

架构面积(ASIC 门)最佳吞吐量(Mbps)最坏情况吞吐量
迭代3321788788
部分流水15,19140,064817
全流水25,75840,06440,064

(最坏情况吞吐量假设每个数据块都引入新密钥。)

指标定义:

  1. LUTs:AES 核在 FPGA 内消耗的逻辑利用率;
  2. ASIC 门:AES 核在 ASIC 中消耗的逻辑门数;
  3. 最佳吞吐量(Best possible throughput):最好情况(新密钥扩展带来的惩罚延迟最小)下每秒能处理的最大数据位数;
  4. 最坏情况吞吐量(Worst-case throughput):最坏情况(每个数据块都有唯一密钥、密钥扩展惩罚最大)下的吞吐量。

读表的三个关键观察

  • 全流水架构在最坏情况(每个数据块换一把密钥)下比其他架构快两个数量级(15,400/340 ≈ 45 倍;ASIC 上 40,064/788 ≈ 51 倍);
  • 部分流水架构是”陷阱”:最佳吞吐与全流水相同(15,400),但最坏情况暴跌到 314——比迭代架构还慢!因为它在频繁换钥时必须等流水线排空,完全填不满。架构选型必须看最坏情况,而不是厂商宣传的最佳情况;
  • 面积上全流水是迭代架构的约 6.7 倍(FPGA)/ 7.7 倍(ASIC)。吞吐量是用面积换来的——完美呼应第 1、2 章的展开/折叠权衡。

4.3 其他优化

从对比数据看,全流水设计的首要问题是面积利用率,其中大头是存放 S-box 查找表的块 RAM(BRAM)数量。现代实现普遍用 BRAM 查表,因为它实现简单、一拍出结果。但算一笔账:GF(2⁸) 上的任意映射需要一块 8×256 RAM;单个映射不是问题,可每个时钟大约要做 320 次查表——这对 FPGA 是重大压力,内存需求几乎触及大型 FPGA 的上限。

另一条路是用扩展欧几里得算法(Extended Euclidean Algorithm)直接计算乘法逆元来代替查表。但在 GF(2^m) 中求任意多项式的逆,复杂度是 O(m),需要 2m 步计算;而且每步依赖上一步的结果,无法并行——这种迭代算法软件里无所谓,硬件实现的延迟(2×m×轮数 = 160~224 个时钟)对频繁加密小块数据的应用来说不可接受。

第三种方案由 Rijmen(Rijndael 的发明人之一)提出:把 GF(256) 的每个元素表示为系数取自 GF(16) 的一次多项式。设不可约多项式为 x² + Ax + B,任意多项式 bx + c 的乘法逆元为:

点评:这样一来,“在 GF(256) 中求逆”被降维成”在 GF(16) 中求逆 + GF(16) 上的一些算术运算”。GF(16) 的逆元表比 GF(256) 的映射表小得多——可以做出更紧凑的 S-box,直接缓解全流水架构的 BRAM 压力。这是”算法级重构换取硬件友好性”的经典案例。

4.4 本章要点速查

  • AES 一轮 = SubBytes(查表,1 级流水)+ ShiftRows(纯布线,0 级)+ MixColumns(逻辑最重,2 级)+ AddRoundKey(异或,1 级),合计 4 拍——流水级按逻辑量均衡分配。
  • 紧凑架构:Round 和密钥扩展都迭代复用一套逻辑(输出回环接输入),面积最小、吞吐最低。
  • 部分流水架构:数据通路级联成流水线、密钥静态算完;最佳吞吐高,但频繁换钥时退化到迭代架构的速度。
  • 全流水架构:密钥扩展逐级与数据流水线并行对齐(密钥比数据早一拍就位),任何密钥模式下都保持最大吞吐,代价是面积数倍。
  • 选架构要看最坏情况吞吐量:真实系统里密钥更换频率由应用决定,不由你决定。
  • S-box 的面积优化可从算法下手:GF(256) → GF(16) 的复合域分解,查表规模指数级缩小。

常见坑

最容易踩的坑是按最佳吞吐量选架构:看到部分流水架构”15,400 Mbps”的指标就拍板上马,结果应用场景是”每个数据块一把密钥”(如网络加密中密钥频繁协商的场景),实测吞吐只有 314 Mbps——比 886 LUT 的迭代架构还慢一半,却花了 4432 个 LUT。教训:密钥更换频率是架构选型的第一输入,评估时永远用 worst-case 吞吐量,并要求实现后分析确认流水线填充策略。

通关标准:

学完本篇你应该能做到:

  1. 画出 AES 一轮的四个变换及其流水级分配,并解释”为什么 ShiftRows 是 0 级、MixColumns 是 2 级”;
  2. 用”输出回环接输入”的模式把一个自治单元改造成迭代复用结构(紧凑架构的核心手法);
  3. 解释部分流水架构最坏情况退化的机理(流水线填充被密钥扩展阻塞)和全流水架构的对齐要求(密钥比数据早一拍);
  4. 拿着表 4.1/4.2,根据”密钥更换频率 + 面积预算”为具体应用选出正确架构。