AES128 FPGA Verilog实现:从仿真到上板的完整工程指南
发布时间:2026/10/17 14:09:05 锦皓数字建站

简介这份资源提供AES128对称加密算法在FPGA上的完整Verilog实现面向数字电路设计学习者、密码学硬件加速开发者以及需要高速低延迟安全模块的嵌入式工程师。压缩包共230个文件约119.72MB以Verilog源码.v和VHDL源码.vhd为核心涵盖密钥扩展、字节替换、行移位、列混淆、轮密钥加等加密阶段模块并附带测试平台、Makefile、综合报告.rpt、仿真数据库.cdb、.hdb及Quartus工程文件.qsf、.qpf等便于直接编译、综合与上板验证。已有1202人学习下载说明该实现具备一定参考价值。读者可借此深入理解AES128轮函数分解与硬件流水线设计思路掌握S盒替换、列混合等关键模块的Verilog描述方法并参考测试激励与仿真流程完成功能验证为构建高速低功耗加密模块打下工程基础。1. 从一次时序不收敛说起这个 AES128 FPGA 工程到底能干什么前阵子帮一个做数据采集的朋友看板子他要在 Artix-7 上对高速采样流做实时加密一开始图省事用了某开源核结果 200MHz 下时序怎么都收不敛关键路径全卡在 Sbox 的组合逻辑上。后来换成一份结构更清晰的 AES128 Verilog 实现把轮运算拆成流水、Sbox 改用 BRAM 查表时序一下就过了。这件事让我意识到AES 的 FPGA 实现从来不是「能算对」就完事而是「在目标频率和资源约束下能算对」。这份 AES128-FPGA-Verilog 完整代码就是围绕这个目标组织的一套可综合、可仿真、可上板的工程覆盖密钥扩展、轮函数、Sbox 查表、加解密顶层以及 testbench 验证链路。它适合两类人一类是刚接触对称加密硬件实现、想找一个能跑通全流程的入门工程另一类是要把 AES 嵌进自己数据通路、需要一份结构干净、方便裁剪的 RTL 底子。下面我按「先看清结构、再跑通仿真、最后动手改」的顺序拆一遍。2. 工程结构与数据通路AES128 在 RTL 里是怎么摆的2.1 目录组织与模块划分拿到一份 RTL 工程我习惯先看目录再读代码因为目录结构基本暴露了作者的抽象思路。这份工程常见做法是按功能分层大致长这样aes128_fpga/ ├── rtl/ │ ├── aes_top.v // 加解密顶层含状态机与接口 │ ├── aes_encrypt.v // 加密轮运算 │ ├── aes_decrypt.v // 解密轮运算 │ ├── key_expand.v // 密钥扩展 │ ├── sbox.v // Sbox 查表BRAM/LUT 可选 │ ├── inv_sbox.v // 逆 Sbox │ ├── mixcolumns.v // 列混合 │ └── inv_mixcolumns.v // 逆列混合 ├── tb/ │ ├── tb_aes_top.v // 顶层 testbench │ └── aes_vectors.txt // 标准测试向量 ├── sim/ │ └── run_sim.sh // 仿真脚本 └── constr/ └── aes_pin.xdc // 引脚与时序约束这种拆法的好处是每个变换独立成模块综合工具能分别做资源共享和流水优化。aes_top.v只负责调度不掺运算逻辑改加密模式比如从 ECB 换 CBC时不用动底层。2.2 一轮加密的数据通路AES128 一轮包含 SubBytes、ShiftRows、MixColumns、AddRoundKey 四步最后一轮去掉 MixColumns。RTL 里通常把 ShiftRows 做成纯连线零成本SubBytes 走 Sbox 查表MixColumns 用异或和 xtime 组合实现。核心一轮的写法大致如下// 单轮加密state 为 128bitround_key 为当轮密钥 module aes_round ( input wire [127:0] state_in, input wire [127:0] round_key, output wire [127:0] state_out ); wire [127:0] sub_bytes_out; wire [127:0] shift_rows_out; wire [127:0] mix_columns_out; // SubBytes逐字节查 Sbox genvar i; generate for (i 0; i 16; i i 1) begin : sbox_gen sbox u_sbox ( .addr (state_in[8*i : 8]), .dout (sub_bytes_out[8*i : 8]) ); end endgenerate // ShiftRows第 r 行循环左移 r 字节纯连线 assign shift_rows_out { sub_bytes_out[127:120], sub_bytes_out[87:80], sub_bytes_out[47:40], sub_bytes_out[7:0], // ... 其余三行按行移位规则重排 sub_bytes_out[95:88], sub_bytes_out[55:48], sub_bytes_out[15:8], sub_bytes_out[103:96], sub_bytes_out[63:56], sub_bytes_out[23:16], sub_bytes_out[111:104],sub_bytes_out[71:64], sub_bytes_out[31:24], sub_bytes_out[119:112], sub_bytes_out[79:72], sub_bytes_out[39:32] }; // MixColumns每列做 GF(2^8) 矩阵乘 mixcolumns u_mix ( .data_in (shift_rows_out), .data_out (mix_columns_out) ); assign state_out mix_columns_out ^ round_key; endmodule逻辑说明generate循环把 16 个字节并行送进 Sbox这是面积换速度的典型做法ShiftRows 用位拼接实现综合后不消耗逻辑资源MixColumns 内部对每列做{02}·a0 ^ {03}·a1 ^ a2 ^ a3这类运算其中乘 2 就是左移加条件异或0x1b。参数上state_in和round_key都是 128 位字节序要和标准向量对齐否则仿真对不上。2.3 密钥扩展与轮密钥生成密钥扩展把 128 位主密钥扩展成 11 组轮密钥。它和轮运算可以并行常见做法是上电后先算完存进寄存器组或者边算边用。关键点是 Rcon 常量和 RotWord、SubWord 的顺序// 密钥扩展每轮生成 4 个 32bit 字 always (posedge clk) begin if (start) begin // 前 4 个字直接来自主密钥 w[0] key_in[127:96]; w[1] key_in[95:64]; w[2] key_in[63:32]; w[3] key_in[31:0]; end else begin for (i 4; i 44; i i 1) begin temp w[i-1]; if (i % 4 0) // RotWord SubWord Rcon temp subword(rotword(temp)) ^ {rcon[i/4], 24h0}; w[i] w[i-4] ^ temp; end end end逻辑说明i % 4 0时做特殊处理这是 AES 密钥调度的固定规则。rcon是轮常量数组从8h01开始每轮 xtime 递推。参数上w是 44 个 32 位字的数组对应 11 组轮密钥。这里如果写成组合逻辑一次性展开综合工具可能报扇出过大我一般会加一级寄存器打拍。3. 仿真跑通从测试向量到波形确认3.1 testbench 怎么搭仿真阶段最怕的是「代码看着对结果对不上」。这份工程的 testbench 思路是读标准向量、灌进 DUT、比对输出。核心结构module tb_aes_top; reg clk, rst_n, start; reg [127:0] plain_text, key; wire [127:0] cipher_text; wire done; aes_top u_dut ( .clk (clk), .rst_n (rst_n), .start (start), .plain_text (plain_text), .key (key), .cipher_text (cipher_text), .done (done) ); initial begin clk 0; rst_n 0; start 0; #20 rst_n 1; // FIPS-197 标准向量 key 128h000102030405060708090a0b0c0d0e0f; plain_text 128h00112233445566778899aabbccddeeff; #10 start 1; #10 start 0; wait (done); // 期望密文 69c4e0d86a7b0430d8cdb78070b4c55a if (cipher_text 128h69c4e0d86a7b0430d8cdb78070b4c55a) $display(PASS); else $display(FAIL: got %h, cipher_text); $finish; end always #5 clk ~clk; // 100MHz endmodule逻辑说明wait(done)等顶层拉高完成信号再比对避免时序竞争。参数上key和plain_text用的是 FIPS-197 附录里的经典向量密文期望值69c4e0d8...是标准答案对不上就说明轮运算或密钥扩展有问题。3.2 仿真命令与波形观察用 Vivado 自带的 xsim 或 ModelSim 都能跑。以 xsim 为例# 编译 RTL 和 testbench xvlog ../rtl/*.v ../tb/tb_aes_top.v # 精化顶层 xelab -debug typical tb_aes_top -s aes_sim # 运行仿真 xsim aes_sim -runall跑完后重点看三处波形一是done是否在合理周期数后拉高纯组合一轮约 10 个周期流水线会更短二是cipher_text是否等于期望值三是密钥扩展的w数组每轮是否递推正确。如果done一直不拉高多半是状态机卡在某个状态检查start的握手逻辑。提示仿真通过不代表上板通过。综合后的时序报告和仿真结果是两回事务必在综合后再跑一次带 SDF 反标的门级仿真。4. 上板与综合时序、资源与约束怎么调4.1 综合选项与资源估算AES128 在 FPGA 上的资源占用主要看 Sbox 怎么实现。用 LUT 实现一个 Sbox 约几十个 LUT16 个并行就是几百个用 BRAM 实现则消耗 1 到 2 个块 RAM但读延迟增加一拍。常见做法是面积优先用 BRAM、速度优先用 LUT 加流水。综合时打开-flatten_hierarchy有助于跨模块优化但会牺牲可读性调试阶段建议关掉。4.2 时序约束怎么写约束文件里最关键的是时钟周期和输入输出延迟# 100MHz 主时钟 create_clock -period 10.000 -name sys_clk [get_ports clk] # 输入延迟假设上游器件有 2ns 输出延迟 set_input_delay -clock sys_clk 2.000 [get_ports {plain_text[*] key[*] start}] # 输出延迟 set_output_delay -clock sys_clk 2.000 [get_ports {cipher_text[*] done}]逻辑说明create_clock定义时序基准set_input_delay告诉工具外部信号到达时间。如果时序报告里关键路径是 Sbox 到 MixColumns说明组合逻辑太深需要插流水寄存器。参数上周期 10ns 对应 100MHz想跑更高频率就缩短周期但要注意 Sbox 的 BRAM 读延迟。4.3 常见综合警告怎么读综合日志里出现LUT as memory说明 Sbox 被映射成分布式 RAM这是正常的出现multi-driven net则要查是不是同一信号在多个 always 块里赋值出现timing not met就得回去看关键路径。我一般会先把时序违例的路径单独拎出来看是逻辑层级太深还是扇出太大前者插流水后者加寄存器复制。5. 避坑与排查几个让我熬夜的典型问题5.1 仿真对不上标准向量现象testbench 报 FAIL密文和期望值差几位。原因字节序搞反了AES 标准里 state 是按列优先排列的而 Verilog 里习惯按位拼接很容易把行和列弄混。解决拿 FIPS-197 的中间轮结果逐轮比对定位到是哪一步出错通常 ShiftRows 的位选最容易写错。5.2 上板后输出一直是 0现象仿真通过上板后cipher_text恒为 0。原因复位没释放或者start脉冲太窄被时钟域漏采。解决用示波器或 ILA 抓rst_n和start确认复位释放后至少保持几个时钟周期start脉冲宽度要大于一个时钟周期。5.3 时序收敛但功能偶发错误现象时序报告干净但长时间跑偶尔出错。原因跨时钟域信号没做同步或者 BRAM 读使能和地址没对齐。解决检查所有跨时钟域路径加两级同步器BRAM 读数据要延迟一拍再使用别直接用当拍地址对应的数据。5.4 资源占用远超预期现象综合后 LUT 用量比预估高好几倍。原因Sbox 被完全展开成组合逻辑没有复用。解决把 Sbox 改成 BRAM 实现或者用一轮一个 Sbox 的迭代结构换面积代价是吞吐率下降。5.5 解密结果和加密对不上现象加密正确解密出来是乱码。原因解密轮密钥顺序用反了或者 InvMixColumns 的矩阵系数写错。解决解密时轮密钥要逆序使用InvMixColumns 的系数是{0e,0b,0d,09}和加密的{02,03,01,01}不同别抄错。6. 二次开发把这份 AES 核改成你要的样子拿到一份能跑的 AES 核真正的价值在于改。我一般从三个方向动手。第一是换加密模式ECB 最简单但安全性弱实际项目多用 CBC 或 CTR。改 CBC 需要在顶层加一个异或环节把上一组密文和当前明文异或后再进轮运算初始化向量 IV 作为一个额外输入。改 CTR 则要把计数器加密后和明文异或好处是可以并行。第二是调流水线深度换吞吐。当前结构如果是一轮一个周期吞吐率约 128bit/10cycle把轮运算拆成两级流水吞吐率翻倍但面积增加。具体做法是在 MixColumns 前后各插一级寄存器用 valid 信号逐级传递。改的时候注意握手信号要跟着流水走否则done会提前拉高。第三是裁剪接口。如果只要加密不要解密直接把aes_decrypt、inv_sbox、inv_mixcolumns从综合列表里去掉面积能省近一半。如果数据位宽不是 128比如 8 位串行输入就在顶层加一个移位寄存器做串并转换轮运算内部保持 128 位并行。验证改造是否成功我的习惯是每次改完先跑标准向量仿真再跑一组随机向量和软件实现比如 Python 的 pycryptodome比对。下面这段 Python 可以生成随机向量并算出期望密文from Crypto.Cipher import AES import os key os.urandom(16) plain os.urandom(16) cipher AES.new(key, AES.MODE_ECB) expected cipher.encrypt(plain) print(fkey{key.hex()} plain{plain.hex()} expect{expected.hex()})把打印出来的值填进 testbench跑一遍就知道改对没有。这个习惯帮我省了无数次上板才发现问题的返工。注意改流水线时valid 信号的复位值要设成 0否则上电后第一组数据可能被误判为有效。从那以后我每次改完 RTL都强制走一遍「标准向量仿真 → 随机向量比对 → 综合时序检查 → 门级仿真」四步少一步都不敢上板。希望这份拆解帮到你代码拿下去先跑通仿真再按自己的数据通路慢慢裁。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。