
简介基于Verilog HDL的AES加密设计完整工程面向FPGA与数字IC设计学习者演示了高级加密标准从算法到硬件电路的整体落地方法。包内共214个文件压缩后仅7.4MB包含Quartus II工程配置qpf/qsf、Verilog源码.v/.vhd、仿真波形.wlf、SOF配置文件、编译报告及mif初始化数据同时保留大量备份与脚本文件便于对照学习环境搭建与调试过程。该工程已有199人学习下载属于中小型教学级工程适合课设、竞赛或自学实践。通过aes_top顶层模块可直观理解字节替代、行位移、列混淆与密钥扩展等核心步骤如何映射为可综合的数字逻辑配合工程自带的仿真与工程设置读者可以直接在Quartus II中打开、编译并下载到FPGA验证。源码层次清晰、寄存器与组合逻辑划分明确既适合初学硬件描述语言者逐段研读也适合中高级开发者快速移植复用从而深入掌握硬件实现AES并行加速思想与VLSI设计流程兼具算法学习和工程实践参考价值。1. AES 加密的 Verilog 实现到底难在哪大多数工程师第一次接触 AES 时第一反应是“算法不是已经定死了吗照抄 C 代码转成 Verilog 不就行了”。真动手才发现问题根本不在加密公式本身而在硬件该怎么组织数据通路一个 128 位状态块在 10 轮运算里如何在字节、列、行之间反复变换密钥扩展是提前算好存在寄存器里还是每轮现场生成S 盒用 ROM 还是用组合逻辑以及遇到非标准数据位宽时该怎么拼接。这些决策直接影响吞吐率、资源占用和时序收敛。本文以 AES-128 加密为主讲一套我在项目里验证过的 Verilog HDL 实现路径模块划分、密钥扩展、轮运算、仿真验证再到如何把裸核包装成能接 FIFO 或 AXI 流的接口。适合正在写 FPGA 加速逻辑、或者打算把 STM32 软件 AES 换成硬核的开发者参考。2. 用 Verilog 搭 AES 加密的模块边界与数据通路2.1 把 AES-128 拆成可综合的几个子模块AES-128 加密的 10 轮结构固定但 Verilog 里不能把每一轮都写成独立的 always 块再复制十遍那样面积和布线压力都会失控。常见做法是把一次加密拆成四个职责清晰的子模块状态变换单元SubBytes、ShiftRows、MixColumns、轮密钥 XOR 单元、密钥扩展单元和控制状态机。顶层只负责拼接数据流和处理握手信号这样每部分都能单独仿真和时序优化。我采用的模块划分如下模块名职责输入关键信号输出关键信号aes_top顶层集成控制流水节拍clk, rst_n, start, key[127:0], data_in[127:0]data_out[127:0], donekey_expand从初始密钥生成 10 个轮密钥key, key_en, key_moderound_key[127:0] 或 key_ramsbox_rom提供 SubBytes 所用 S 盒值addr[7:0]sbox_out[7:0]round_transform完成一轮 SubBytes ShiftRows MixColumns AddRoundKeystate_in[127:0], round_key[127:0]state_out[127:0]control_fsm控制轮数、密钥加载时机和完成标志start, next_roundround_valid, done, key_en这种划分的逻辑很简单加密密钥是 128 位每轮需要 128 位轮密钥第 0 轮直接与初始密钥 XOR第 1 到第 9 轮做完整变换第 10 轮省略 MixColumns。控制状态机只需要一个 4 位计数器从 0 数到 10并在第 10 轮让 MixColumns 输出无效即可。顶层接口不建议写得太“聪明”把明文和密钥都做成同步输入用 start 信号拉高表示数据有效加密完成后 done 拉高一个周期。这样无论后端接到 SPI、UART 还是 AXI 总线适配成本都最低。2.2 替换 S 盒查表还是组合逻辑SubBytes 是 AES 里唯一非线性的操作本质是有限域 GF(2^8) 上的乘法逆元加仿射变换。Verilog 实现方式基本只有两种用 case 语句写 256 个 8 位常量输出或者用分布式 ROM。FPGA 上推荐直接例化 Block RAM 或用 case 生成组合逻辑ASIC 里则常用 PLA 优化。查表方式的 Verilog 代码很直接module sbox_rom ( input logic [7:0] addr, output logic [7:0] data ); always_comb begin case (addr) 8h00: data 8h63; 8h01: data 8h7c; 8h02: data 8h77; // ... 完整 256 项 8hff: data 8h16; default: data 8h00; endcase end endmodule这里用的是组合逻辑查表好处是不占 BRAM坏处是综合后 CLB 资源消耗较大。如果项目里 BRAM 有富余用casez或只读存储器例化反而更容易跑高频率。S 盒实际只需要做一次查表然后通过 128 位状态复制成 16 个查表端口。注意地址是状态字节本身不需要额外转换。另一个细节是逆 S 盒只在解密时用。如果只做加密电路不需要例化逆向 S 盒能省一半 ROM 面积。若是要支持解密最好把两个 S 盒分开放别用一个多端口 RAM 做双向映射否则时序很难看。2.3 轮密钥加法与列混合的位宽设计AddRoundKey 是状态与轮密钥按位异或128 位宽一次完成。MixColumns 则要对每一列做 GF(2^8) 乘法累加。硬件上最常踩的坑是字节顺序AES 状态矩阵是列优先存储但人习惯按行读。Verilog 里如果把state[127:0]直接按位拼接很容易把列混淆。我一般把状态定义成 16 字节数组用genvar做列运算logic [7:0] state [0:15]; // state[0]..state[3] 是第 0 列 always_comb begin for (int c 0; c 4; c) begin for (int r 0; r 4; r) begin tmp_state[c*4r] state[c*4r]; end end // MixColumns 每列第 0 行 for (int c 0; c 4; c) begin mix_out[c*40] gf_mul(2, state[c*40]) ^ gf_mul(3, state[c*41]) ^ state[c*42] ^ state[c*43]; end endgf_mul可以用移位和条件异或实现参数只有 2、3、1 三种不必写通用有限域乘法器。对 2 的乘法是左移一位如果最高位溢出则异或 0x1b。对 3 的乘法就是对 2 的乘法结果再异或原值。这样每个列运算只有 4 个乘法和 6 个异或逻辑级数很低。注意时序面积权衡如果想提高吞吐可以把 4 列乘法并行然后 Reg 打一拍。如果时钟频率不高组合直接输出也可以。但混合运算的路径从状态寄存器到 MixColumns 输出再到下一轮 SubBytes 输入中间还要插入轮密钥异或往往容易成为关键路径。建议在每轮输出端加流水寄存器哪怕多占一点资源对时序收敛帮助极大。3. 密钥扩展的 Verilog 写法一次算完还是边算边用3.1 密钥扩展的本质W0-W43AES-128 密钥扩展从初始 16 字节出发生成 44 个字word每字 32 位每 4 个字组成一个 128 位轮密钥。第 i 个字由前一个字和 i-4 个字决定规则分两类i 是 4 的倍数时先循环左移一字节再逐字节过 S 盒最后第一个字节异或轮常量 Rcon[i/4]其他位置则直接与前一个字异或。在 Verilog 里可以把它看成 11 个 128 位寄存器的链但从第 0 轮开始就能用一块 RAM 或寄存器数组存下来。资源允许时我倾向用 176 字节的寄存器组编译后直接扇出给加密轮使用。3.2 边算边用的流水设计不需要提前算完所有轮密钥。加密轮从第 1 轮开始每轮使用当前轮密钥下一轮密钥可以在上一轮进行状态变换的同时并行计算。这样能省掉存储 10 个轮密钥的寄存器但控制逻辑要小心错拍。下面是一个简化的边算边用代码结构always_ff (posedge clk or negedge rst_n) begin if (!rst_n) begin round_cnt 4d0; current_key 128d0; end else if (key_load_en) begin current_key user_key; round_cnt 4d0; end else if (round_valid) begin current_key next_key; // 下一轮密钥 round_cnt round_cnt 1b1; end end always_comb begin // 生成 next_key 的逻辑 if (round_cnt[0] 1b0) begin // 对每 4 字组的第 0 个子字做非线性处理 temp_word {sbox(word[23:16]), sbox(word[15:8]), sbox(word[7:0]), sbox(word[31:24])} ^ rcon; next_key {current_key[127:32], temp_word ^ current_key[127:96], current_key[95:64] ^ current_key[63:32], current_key[63:32] ^ current_key[31:0]}; end end代码里的round_cnt[0]并不是真正的条件判断实际上应该用是否整除 4 来触发特殊的字变换。上面的结构只是为了展示密钥扩展的每个字生成只依赖相邻字因此完全可以在使用当前轮密钥的同一个周期里生成下一轮密钥。注意寄存器更新要在时钟沿组合逻辑只负责计算不要出现反馈回路。这种方案的面积比 RAM 方案小得多缺点是不能随机跳轮。对加密芯片设计来说AES 本来就是顺序执行所以边算边用是更常见的硬件实现方式。3.3 两套方案的时序对比与参数选择方案资源消耗时序特性适用场景预计算存 RAM176x8 bit RAM 读写端口初始化时串行写入之后每轮读一次路径短多轮并行展开、密钥不频繁更换边算边用128 bit 寄存器 少量组合每轮计算路径长需要流水或降频资源紧张的 SoC、单核加密引擎参数选择上没有绝对标准。我做过的 AES 核里如果系统时钟高于 200 MHz边算边用方案容易让 S 盒和异或链挤在同一条组合路径上建议拆分流水级第一级算 SubBytes ShiftRows第二级算 MixColumns AddRoundKey密钥扩展放第三级。如果时钟只有 100 MHz 左右直接组合完成也没有问题。另一个常见误解是密钥加载与加密不能并行。实际上密钥扩展只需要执行一次通常比加密快得多。若密钥固定不变可以在复位后立刻算出所有轮密钥存好若密钥每次变化就需要在加密开始前留出时钟周期做密钥装载。控制状态机里至少要有IDLE,LOAD_KEY,ENCRYPT,DONE四个状态其中LOAD_KEY长度取决于密钥扩展采用何种方式。4. 加密核跑通仿真激励、加密模式与常见坑4.1 最小测试台读入明文和密钥比对已知向量写 Verilog 加密模块不能只仿真“有没有翻转”必须用 NIST FIPS-197 附录的已知答案做比对。最小测试台需要做三件事初始化密钥、输入明文、比较密文。用文本方式读入数据时注意大小端Verilog 里$readmemh读入的是字节流而加密算法内部状态是按列组织的这里最容易错位。一个简洁的测试台结构如下module aes_tb; logic clk, rst_n, start; logic [127:0] key, data_in, data_out; logic done; // 测试向量密钥 000102030405060708090a0b0c0d0e0f // 明文 00112233445566778899aabbccddeeff // 密文 69c4e0d86a7b0430d8cdb78070b4c55a initial begin clk 0; forever #5 clk ~clk; end initial begin rst_n 0; #10 rst_n 1; key 128h000102030405060708090a0b0c0d0e0f; data_in 128h00112233445566778899aabbccddeeff; start 1; #10 start 0; wait (done); if (data_out 128h69c4e0d86a7b0430d8cdb78070b4c55a) $display(AES-128 test PASSED); else $display(AES-128 test FAILED, got %h, data_out); $finish; end endmodule注意wait (done)后面紧接着比较输出这要求测试台在 done 信号的同一拍或下一拍采样 data_out。如果设计里 done 高电平仅持续一个周期最好在 always_ff 里打一拍再比较避免因采样沿导致错误。另外比更严格能发现 X 态和 Z 态。4.2 模式问题ECB/CBC 在硬件里的代价很多网上流传的 Verilog AES 代码仅仅实现了 ECB 模式。ECB 在工程上意味着每个 128 位块独立加密同一明文块永远产生同一密文块这种方式在安全要求高的场景不能直接用。CBC 模式需要在硬件里加一个 128 位异或单元和反馈路径并且必须保证包括初始向量 IV 在内的数据块按顺序进入流水线。一旦加密核内部是多级流水CBC 的反馈会让后一个块必须等前一个块的密文产生流水线吞吐直接降级为每 N 个时钟一个块。实际工程里流式接口如 AES-CTR、GCM对硬件流水线友好得多因为 CTR 模式可以把计数器预先加密明文与密文之间的依赖只发生在最后的异或。如果你只是做一个数据加解密 IP我建议至少留出模式选择端口并在文档里注明ECB 用于测试向量CTR/GCM 用于实际数据。Verilog 代码中实现 CBC 时关键点是反馈异或必须发生在 AddRoundKey 之前且加密输出的密文经过寄存器打拍后与下一个块异或这个打拍数决定了块间间隔。4.3 仿真通过但上板失败三处最容易错的地方第一处是异步复位与密钥加载时序冲突。仿真里复位释放往往很“干净”上电后 PLL 未锁定或者外部按键抖动可能导致 key_en 被误触发。建议在顶层对 key_en 和 start 做至少两级同步加边沿检测避免亚稳态将密钥扩展状态机打入非法状态。第二处是 S 盒 ROM 初始化的 X 态。FPGA 综合时如果 S 盒的 case 语句缺少 default未被覆盖的地址会返回未知值。上板后这些地址可能因为 RAM 初始化失败而永远输出 0导致密文前几轮正确、后几轮错误。排查时可以打印每轮的中间状态并与软件参考实现逐轮对比能很快定位是 S 盒还是 MixColumns 出了问题。第三处是数据位宽跨时钟域或位序颠倒。比如从 STM32 的 SPI 或 UART 接收加密数据时串行字节序是 LSB first 还是 MSB first而 AES 算法定义的是按字节地址递增排列。很多人仿真时直接把整条 128 位数据从存储器复制看起来对实际字节被反转。一个实用的验证方法是写一段 Python 或 C 脚本把同样的明文密钥在不同字节序下加密对比硬件输出的密文落在哪种序上然后固定转换逻辑。5. 把 AES 核封装成 AXI4-Stream 接口并跑通时序收敛做产品时AES 核一般不会独立工作前端可能是 DMA 搬运后端接 AXI-Stream FIFO。我习惯给 AES 核加两级 AXI-Stream 接口主端口接收明文从端口输出密文。这样可以在不修改加密内核的前提下把数据拼接成 128 位块。具体做法是内部维持一个 128 位移位寄存器每接收 16 个 8 位数据就启动一次加密在加密完成后再按 16 字节流出。时序收敛上有一个很有效的小技巧把状态变换内部的 ShiftRows 操作与 S 盒查表合并到同一个流水级里。因为 ShiftRows 本质上只是字节位置重排不产生任何逻辑综合后只是布线改变。把 S 盒查表结果输出后的重新排布放到下一个寄存器的输入处会让关键路径从“S盒输出 重排 XOR”缩短为“S盒输出 重排”。列混合运算则尽量使用 CSA 加法结构不要直接写四输入异或的连续链综合器虽然会优化但手动分组更容易控制布局。验证时序收敛时除了查看时序报告里的 WNS还要检查跨时钟域路径是否存在。如果 AES 核工作在 250 MHz而输入 FIFO 是 125 MHz要用异步 FIFO 而不是在 RTL 里直接打拍。异步 FIFO 的深度建议至少 8 个 128 位块避免加密核被读侧空档阻塞。I2C 或串口这类低速接口包一层 AXI-Stream 适配器时注意tlast信号要在第 16 字节拉高否则下游会一直等数据导致死锁。如果你只是需要快速评估加密核是否满足带宽需求可以在仿真里统计valid ready同时有效的周期数用总数据位宽除以有效周期数得到实际吞吐率。这个指标比单纯看最高时钟频率更有说服力。最终确保 AES 模块在大端和小端系统中都能拿到相同密文并用至少三条测试向量标准向量、随机密钥、全零明文回归再接入真实数据流抓波形确认边界字节序——做到这一步基于 Verilog HDL 的 AES 加密核才算真正可用。本文还有配套的精品资源点击获取