☰
AES加解密Verilog实现:从RTL源码到仿真实战
2026/10/20 23:48:14 网站建设 项目流程

简介:这份资源面向电子工程师、密码学研究者及FPGA开发者,提供基于Verilog HDL的AES加解密硬件实现方案,可用于通信加密、数据存储与网络安全等对实时性要求较高的场景。压缩包共146个文件,约8.58MB,以cdb、hdb、qmsg、rpt、kpt等综合与仿真过程文件为主,另含v、vo、qsf、sof、pof等Verilog源码、综合网表与FPGA烧录文件,以及readme、txt说明文档,完整保留了从设计到实现的工程痕迹。资源围绕AES算法的字节代换、行移位、列混淆与轮密钥加四个核心步骤展开,展示了如何用Verilog将S盒、密钥扩展与多轮迭代转化为可综合的硬件电路,并支持128、192、256位密钥与128位数据块。已有281人学习下载,适合希望理解AES加解密芯片设计流程、对照工程文件排查综合与实现问题的读者参考。

1. AES 加解密 Verilog 实现:一份能跑通仿真、能对上标准向量的硬件源码包

做 FPGA 或 ASIC 的同行大概都遇到过这种局面:算法同事丢过来一句“AES 加解密,128 位,ECB 模式”,然后你打开编辑器,面对的是 S 盒怎么生成、密钥扩展怎么排、轮函数怎么流水这一连串问题。软件里一行crypto库调用的事,落到 RTL 就是几百行状态机和一堆时序约束。这份 AES.zip 就是冲着这个场景来的——它把 AES 加解密算法用 Verilog 完整实现了一遍,包含加密、解密、密钥扩展和测试平台,属于典型的 AES 加解密芯片前端原型代码。适合正在做安全模块、加密 IP 核或者密码学课程设计的硬件工程师,也适合想从 RTL 层面真正看懂 AES 每一轮在干什么的人。它不是那种只给一个顶层模块、里面全是黑盒的“演示代码”,而是把轮运算、字节代换、列混合这些环节都摊开写了。

2. AES 算法硬件化的三个核心决策:S 盒、密钥扩展与轮结构

在动手看代码之前,有几个设计选择必须先想清楚,因为它们直接决定了这份源码能不能综合、面积多大、跑多快。AES 加解密芯片的 RTL 实现和软件实现最大的区别在于:软件可以随便查表、递归调用,硬件必须把每一步都展开成并行的组合逻辑或时序逻辑。下面这三件事,是读这份代码时最该盯住的地方。

2.1 S 盒用查表还是用组合逻辑算

AES 的字节代换(SubBytes)本质是一个 8 位到 8 位的非线性映射。软件里就是一张 256 字节的表,硬件里则有两条路:一是直接用case语句或 ROM 实现查找表,二是用有限域 GF(2^8) 上的求逆运算加仿射变换组合逻辑算出来。

这份源码走的是查找表路线,常见做法是用case语句把 256 个映射值写死。这样做的好处是延迟确定、综合结果可预测,缺点是面积随工艺不同可能偏大。如果换成组合逻辑实现,面积会小一些,但关键路径会变长,时序收敛更费劲。

// S盒查找表实现(加密方向) function [7:0] sbox; input [7:0] data_in; begin case (data_in) 8'h00: sbox = 8'h63; 8'h01: sbox = 8'h7c; 8'h02: sbox = 8'h77; // ... 中间省略,完整表共256项 8'hff: sbox = 8'h16; default: sbox = 8'h00; endcase end endfunction

这段代码的逻辑很直白:输入一个字节,输出对应的 S 盒值。参数上唯一需要注意的是default分支——虽然 8 位输入理论上覆盖了全部 256 种情况,但写default是好习惯,防止综合工具推断出锁存器。解密用的逆 S 盒同理,只是表值不同。

提示:如果你的综合工具支持rom_style属性,可以显式指定用块 RAM 还是分布式 RAM 实现这张表,面积和速度差别不小。

2.2 密钥扩展是独立模块还是内联

AES-128 需要把 128 位原始密钥扩展成 11 组 128 位的轮密钥,共 44 个 32 位字。硬件实现时,密钥扩展可以做成一个独立模块,在加密开始前先把所有轮密钥算好存起来;也可以内联在轮运算里,边算边用。

这份源码把密钥扩展单独拎出来了,key_expansion模块接收原始密钥和有效信号,输出 11 组轮密钥。这种做法的好处是加密和解密可以共用同一套轮密钥生成逻辑,解密时只需要把轮密钥顺序反过来用。代价是需要额外的寄存器存储 44 个字,大约 1408 个触发器。

// 密钥扩展中的字循环与S盒替换 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin w[0] <= 32'h0; // 复位清零 end else if (key_valid) begin for (i = 0; i < 4; i = i + 1) begin w[i] <= key_in[127 - 32*i -: 32]; // 原始密钥拆成4个字 end for (i = 4; i < 44; i = i + 1) begin temp = w[i-1]; if (i % 4 == 0) begin temp = sub_word(rot_word(temp)) ^ rcon[i/4]; // 字循环+S盒+轮常量 end w[i] <= w[i-4] ^ temp; end end end

这里的关键参数是rcon轮常量数组,AES 规范里定义了 10 个值,从8'h01开始每次在 GF(2^8) 上乘 2。rot_word是左移一个字节,sub_word是对四个字节分别过 S 盒。注意i % 4 == 0这个条件——只有每四个字的第一字才做特殊处理,其余就是简单异或。

2.3 轮运算的流水线级数怎么定

AES-128 加密是 10 轮,解密也是 10 轮。硬件里可以把每一轮做成一个时钟周期,10 个周期出结果;也可以把多轮串成组合逻辑,一个周期出结果但时钟频率会很低;还可以插入流水线寄存器,提高吞吐率。

这份源码采用的是“一轮一周期”的迭代结构,加密一个分组需要 11 个时钟周期(含初始轮密钥加)。这个选择在面积和速度之间取了平衡,适合对吞吐率要求不是极端高的场景。如果你要做高吞吐率的 AES 加解密芯片,常见做法是展开成 10 级流水线,每个周期都能出一个结果,但面积会翻好几倍。

// 轮运算状态机(简化) localparam IDLE = 2'b00; localparam ROUND = 2'b01; localparam DONE = 2'b10; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; round_cnt <= 4'd0; end else begin case (state) IDLE: begin if (start) begin state <= ROUND; round_cnt <= 4'd0; data_reg <= data_in ^ round_key[0]; // 初始轮密钥加 end end ROUND: begin if (round_cnt == 4'd9) begin // 最后一轮不做列混合 data_reg <= add_round_key(shift_rows(sub_bytes(data_reg)), round_key[10]); state <= DONE; end else begin data_reg <= add_round_key(mix_columns(shift_rows(sub_bytes(data_reg))), round_key[round_cnt+1]); round_cnt <= round_cnt + 1; end end DONE: begin data_out <= data_reg; state <= IDLE; end endcase end end

这段状态机是整个加密路径的骨架。round_cnt从 0 数到 9,对应第 1 到第 10 轮。注意第 10 轮(round_cnt == 9)跳过了mix_columns,这是 AES 规范明确规定的。add_round_key就是异或,shift_rows是行移位,sub_bytes是字节代换。每个函数的实现细节在源码里都有对应模块。

3. 从仿真到上板:AES 加解密 Verilog 的实操流程

拿到一份 RTL 源码,第一步永远不是直接上板,而是跑仿真。仿真的目的是验证功能正确性,用标准测试向量对一遍,确认加密结果和 NIST 规范一致。这一步过了,再谈综合、时序约束和板级验证。

3.1 搭建仿真环境与标准测试向量

这份源码包里带了 testbench,但你需要自己确认仿真工具和波形查看方式。常见做法是用 Icarus Verilog 做编译仿真,用 GTKWave 看波形;如果公司有 VCS 或 Questasim,直接建工程也行。

# 用 Icarus Verilog 编译并仿真 iverilog -o aes_tb aes_top.v aes_encrypt.v aes_decrypt.v key_expansion.v sbox.v tb_aes.v vvp aes_tb # 生成波形文件后用 GTKWave 查看 gtkwave aes_tb.vcd

编译命令里把所有相关源文件都列上,-o指定输出可执行文件名。vvp是 Icarus 的仿真运行器,跑完后会生成 VCD 波形文件。如果你用的是其他仿真器,把文件列表喂进去就行,注意tb_aes.v里可能引用了$dumpfile和$dumpvars,这是生成波形的关键。

标准测试向量用 NIST FIPS-197 里给出的那组:

参数值
明文00112233445566778899aabbccddeeff
密钥000102030405060708090a0b0c0d0e0f
密文69c4e0d86a7b0430d8cdb78070b4c55a

在 testbench 里把明文和密钥赋进去,跑完仿真后看data_out是不是69c4e0d8...。如果对不上,先别急着改代码,按下面的顺序排查:密钥扩展的轮常量对不对、S 盒表值有没有抄错、列混合的系数是不是02 03 01 01。

3.2 解密路径的轮密钥顺序与逆变换

解密比加密多一个坑:轮密钥的使用顺序是反的。加密时用round_key[0]到round_key[10],解密时要用round_key[10]到round_key[0]。而且解密轮里的变换是逆字节代换、逆行移位、逆列混合,顺序也和加密相反。

// 解密轮运算(简化) always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; end else begin case (state) IDLE: begin if (start) begin data_reg <= data_in ^ round_key[10]; // 用最后一组轮密钥 round_cnt <= 4'd0; state <= ROUND; end end ROUND: begin if (round_cnt == 4'd9) begin data_reg <= add_round_key(inv_shift_rows(inv_sub_bytes(data_reg)), round_key[0]); state <= DONE; end else begin data_reg <= add_round_key(inv_mix_columns(inv_shift_rows(inv_sub_bytes(data_reg))), round_key[9-round_cnt]); round_cnt <= round_cnt + 1; end end endcase end end

注意round_key[9-round_cnt]这个索引——随着round_cnt增加,轮密钥索引递减。最后一轮用round_key[0],和加密的初始轮密钥加对称。逆 S 盒、逆行移位、逆列混合的实现都在对应模块里,逆列混合的系数是0e 0b 0d 09,和加密的02 03 01 01对应。

3.3 综合约束与面积优化

仿真过了之后,下一步是综合。这份代码是纯 RTL,没有厂商相关的原语,所以 Xilinx Vivado、Intel Quartus、Synopsys DC 都能吃。但综合之前有几件事要做:确认时钟频率目标、检查有没有意外推断出的锁存器、看看 S 盒被综合成了什么资源。

# Vivado 综合脚本示例 read_verilog {aes_top.v aes_encrypt.v aes_decrypt.v key_expansion.v sbox.v} synth_design -top aes_top -part xc7a100tcsg324-1 report_utilization -file utilization.rpt report_timing_summary -file timing.rpt

report_utilization会告诉你 LUT、FF、BRAM 各用了多少。如果 S 盒被推断成分布式 RAM,LUT 用量会比较大;如果手动例化了块 RAM,LUT 会省下来但 BRAM 占用增加。report_timing_summary看关键路径,如果时序不满足,优先检查列混合和 S 盒的组合逻辑深度。

注意:综合报告里如果看到 latch 推断警告,八成是某个case或if分支没写全,回去补default或else。

4. 避坑与排查:AES Verilog 实现里最容易翻车的五个地方

这份代码我前后仿真加综合折腾了几轮,踩过的坑不算少。下面这五条是血泪经验,每一条都对应一个具体的现象、原因和解决方式。

4.1 仿真结果全零或全 X

现象:跑完仿真,data_out一直是xxxxxxxx或者全零,波形里状态机不动。

原因:最常见的是复位信号没接对。testbench 里rst_n初始值没设,或者复位释放时机和start信号冲突。另一个可能是key_valid没拉高,密钥扩展模块根本没启动。

解决:在 testbench 开头强制rst_n = 0,等几个时钟周期后再拉高。确认start脉冲宽度至少一个时钟周期,且不在复位期间。用波形确认key_expansion模块的key_valid确实被触发过。

4.2 加密结果和标准向量差一个字节

现象:大部分字节对得上,但某几个字节不对,而且每次错的位置还不一样。

原因:列混合的 GF(2^8) 乘法实现有问题。AES 的列混合涉及乘 2 和乘 3,乘 2 是左移后条件异或0x1b,乘 3 是乘 2 再异或原值。如果左移后忘了条件异或,高位就会丢。

解决:检查xtime函数的实现,确认左移后判断最高位是否为 1,是则异或8'h1b。乘 3 的实现应该是xtime(a) ^ a,不要写成别的。

// GF(2^8)乘2运算 function [7:0] xtime; input [7:0] a; begin xtime = {a[6:0], 1'b0} ^ (a[7] ? 8'h1b : 8'h00); end endfunction

4.3 解密结果不对但加密正常

现象:加密路径用标准向量验证通过,解密路径输出乱码。

原因:轮密钥索引用反了,或者逆列混合的系数写错了。解密第一轮用的是round_key[10],不是round_key[0]。逆列混合的系数是0e 0b 0d 09,不是02 03 01 01。

解决:对着 FIPS-197 文档把解密轮密钥顺序和逆变换系数逐一核对。特别容易错的是inv_mix_columns里四个系数的顺序,以及inv_shift_rows的移位方向——加密是左移,解密是右移。

4.4 综合后时序不收敛

现象:综合报告显示建立时间违例,关键路径在 S 盒或列混合上。

原因:一轮的组合逻辑太深,S 盒查找表加列混合加轮密钥异或串在一起,路径延迟超过了时钟周期。

解决:把一轮拆成两个时钟周期,中间插流水线寄存器。或者在 S 盒输出后插一级寄存器,把列混合放到下一个周期。代价是加密一个分组从 11 个周期变成 21 个周期,但时钟频率能上去。

4.5 上板后输出不稳定

现象:仿真完全正确,综合也过了,但下载到 FPGA 后输出偶尔出错。

原因:跨时钟域没处理,或者输入信号没有同步。如果data_in来自另一个时钟域,直接打拍可能采到亚稳态。

解决:在输入端口加两级同步寄存器,或者用异步 FIFO 做跨时钟域缓冲。另外确认start信号是单周期脉冲,如果它持续多个周期,状态机会重复触发。

5. 进阶用法:把 AES 核封装成 AXI-Stream 接口并做吞吐率验证

源码给的是裸核,输入输出都是简单的 valid/ready 握手或者直接赋值。实际项目里,AES 加解密芯片通常要挂到总线上,AXI-Stream 是最常见的封装方式。下面说一个我常用的封装思路和验证方法。

5.1 封装 AXI-Stream 从端接口

AXI-Stream 的核心信号是tvalid、tready、tdata、tlast。AES 一个分组是 128 位,所以tdata宽度设 128 位,tlast在最后一个分组时拉高。把 AES 核的start信号接到tvalid & tready的与结果上,data_in接tdata,data_out接tdata输出方向。

// AXI-Stream 从端封装(简化) assign s_axis_tready = (state == IDLE); // 空闲时才能接收 assign start = s_axis_tvalid & s_axis_tready; assign data_in = s_axis_tdata; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin m_axis_tvalid <= 1'b0; end else if (state == DONE) begin m_axis_tvalid <= 1'b1; m_axis_tdata <= data_out; m_axis_tlast <= 1'b1; // 单分组场景 end else if (m_axis_tvalid & m_axis_tready) begin m_axis_tvalid <= 1'b0; end end

这段封装的要点是tready的产生条件——只有状态机在 IDLE 时才允许接收新数据,否则会覆盖正在处理的分组。tlast在单分组场景下直接跟tvalid同步拉高,如果是多分组连续加密,需要额外维护分组计数。

5.2 用 Python 脚本做批量验证

仿真跑单组向量只能证明功能大致正确,要验证吞吐率和边界情况,最好用脚本批量生成测试向量,跑完仿真后自动比对。我一般用 Python 生成随机明文和密钥,调pycryptodome算出期望密文,再写进 testbench 的输入文件里。

# 生成批量测试向量 from Crypto.Cipher import AES import random def gen_vectors(n): vectors = [] for _ in range(n): key = bytes(random.getrandbits(8) for _ in range(16)) pt = bytes(random.getrandbits(8) for _ in range(16)) ct = AES.new(key, AES.MODE_ECB).encrypt(pt) vectors.append((pt.hex(), key.hex(), ct.hex())) return vectors with open("vectors.txt", "w") as f: for pt, key, ct in gen_vectors(100): f.write(f"{pt} {key} {ct}\n")

这个脚本生成 100 组随机向量,每组包含明文、密钥和期望密文,写入文本文件。testbench 用$readmemh或$fscanf读进去,逐组灌入 AES 核,比对输出。跑完 100 组如果全对,基本可以确认功能没问题。

5.3 吞吐率与延迟的实测方法

吞吐率验证需要在仿真里统计时钟周期数。在 testbench 里加一个计数器,从start拉高到done拉高之间的周期数就是单分组延迟。这份源码的迭代结构是 11 个周期出一个分组,128 位除以 11 个周期,再乘以时钟频率,就是理论吞吐率。

指标迭代结构10级流水线
单分组延迟11 周期10 周期(首分组)
吞吐率128bit/11周期128bit/周期
面积小大约 3-5 倍

如果时钟跑 100MHz,迭代结构的吞吐率大约是 1.16 Gbps,流水线结构能到 12.8 Gbps。选哪种取决于你的场景——如果是低功耗嵌入式安全模块,迭代结构够用;如果是高速加密网关,得上流水线。

从那以后我每次拿到加密 IP 核,都强制先跑标准向量再跑随机向量,最后才看综合报告。顺序反了,后面全是后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询