FPGA实现AES-128加密算法:Verilog硬件设计与工程实践详解
2026/9/10 2:06:11 网站建设 项目流程

简介:面向FPGA与硬件安全开发者的AES-128完整Verilog实现,基于Rijndael算法,覆盖密钥扩展、字节替换、行移位、列混淆等核心模块,并附带VHDL对照代码,适合用于学习对称加密算法的硬件加速、安全模块设计与芯片验证流程。压缩包共230个文件,约119.72MB,除6份Verilog源码外,还包含存储器初始化文件、Quartus工程配置、测试平台、仿真波形以及日志备份文件,能够支撑从功能仿真、逻辑综合到工程实现的完整设计流程。资源内附详细说明文档与测试平台,读者可对照密钥扩展逻辑、S盒生成、轮函数实现以及工程目录结构,逐步梳理AES-128在可编程逻辑器件上的数据通路和时序约束方法,同时参考作者在仿真调试中保留的中间文件与备份记录,减少硬件调试弯路。已有约1190人浏览学习,对正在接触加密算法硬件实现、Verilog工程调试或安全芯片设计的开发者具有不错的参考价值。

1. 项目概述:为什么选择用FPGA实现AES-128

AES-128作为对称加密算法的工业标准,在数据安全领域有着不可撼动的地位。从嵌入式设备到高速通信系统,AES-128几乎无处不在。而用FPGA实现AES-128,向来是数字IC设计、FPGA开发者的经典练习项目,也是很多企业在招聘笔试和面试中喜欢考察的题目。

我对这个项目的定位很明确:不追求极致的吞吐率,也不刻意压榨资源,而是要做一套结构清晰、时序合理、可直接落地的Verilog实现。整个工程包含密钥扩展、加密主流程、Testbench仿真环境,以及必要的文档注释,适合三种人阅读:刚入门FPGA想找个完整项目练手的开发者、需要在自己的系统里集成硬件加密加速模块的工程师、以及准备数字IC/FPGA相关岗位面试的求职者。

AES-128的核心参数值得先摆出来:密钥长度128 bit,分组长度128 bit,加密轮数10轮。前9轮执行完整的四个操作——字节代换(SubBytes)、行移位(ShiftRows)、列混合(MixColumns)、轮密钥加(AddRoundKey),最后一轮省略列混合。解密过程则是加密的逆运算,操作顺序和轮密钥的使用顺序都要反转。记住这些基本事实,后面所有的代码和时序设计都围绕它们展开。

2. 整体架构与模块划分思路

2.1 架构选型:迭代式结构为什么是首选

FPGA实现AES有两种主流架构:循环迭代式和全流水线式。

循环迭代式只实现一轮的运算逻辑,通过状态机控制数据在同一个组合逻辑环路里跑10次,每轮结果寄存后反馈到输入端。优点是资源消耗小,一个轮函数模块可以被反复复用,综合后逻辑门数大约只有全流水线的十分之一;缺点是吞吐率较低,每处理一个128 bit数据块需要10个时钟周期。

全流水线式则将10轮运算全部展开,每轮之间用寄存器隔开,数据像流水一样依次流过各级。优点是每个时钟周期都能输出一个加密结果,吞吐率极高,适合高速通信场景;缺点是资源消耗大,S盒、列混合等逻辑需要复制10份,同时时序收敛难度也相应增加。

我选择的是迭代式结构。理由很简单:在绝大多数实际应用场景中,AES加密的数据速率不会高到必须用全流水线的程度,而迭代式结构代码量少、更容易理解和调试,新手也更容易掌握。如果你之后有高速处理的需求,把迭代式改成流水线式也并非难事——本质上就是将反馈寄存器替换成级间流水寄存器。

2.2 顶层模块与子功能划分

整个工程按功能拆分成四个模块,各司其职:

  • aes128_top.v:顶层模块,负责例化子模块、对外接口管理、加解密模式选择
  • aes128_key_expand.v:密钥扩展模块,完成10轮轮密钥的生成与存储
  • aes128_cipher.v:加密轮函数模块,实现轮内的四个代数运算
  • aes128_tb.v:Testbench,用于仿真验证,不参与综合

这种划分方式遵循了"高内聚、低耦合"的设计原则,每个模块只关注自己的职责,接口清晰明确。后续如果要增加解密功能,只需要新增一个对应的轮函数模块,或者给现有模块添加可配置的模式选择信号即可。

3. 核心细节解析:S盒、密钥扩展与轮函数

3.1 SubBytes的实现:查找表还是组合逻辑

字节代换是AES中唯一一个非线性操作,也是整个算法安全性的基石。它本质上是有限域GF(2^8)上的乘法逆元运算加仿射变换。理论上可以直接用组合逻辑去计算乘法逆元,但这样做逻辑深度大、时序难以收敛,代码也会非常晦涩。

实际工程中最常用的做法是查表。把S盒的值预计算好存成一个256×8 bit的查找表,输入字节映射到输出字节只需要一次简单索引。这种方法实现代价极低,一个LUT就能覆盖一个字节的全部替换逻辑,且几乎没有时序压力。S盒表的生成方式有两种:一是直接用十六进制文本定义,二是用Verilog函数在仿真时初始化。我倾向于直接定义真实值的数组,这样综合工具和仿真器的行为完全一致,不会出现模型不一致的问题。

S盒的索引规则有一点需要特别注意:输入字节的高4位决定行索引,低4位决定列索引。很多新手在这里踩坑,把行列顺序搞反,导致加解密的结果完全对不上。

3.2 密钥扩展:为什么每轮都要重新算

AES-128有10轮加密,而每轮需要一个独立的轮密钥,加上初始的AddRoundKey阶段,一共需要11个128 bit的轮密钥。这些密钥不是凭空变出来的,而是通过密钥扩展算法从原始密钥一步步推导出来的。

密钥扩展的核心逻辑是:将128 bit原始密钥视为4个32 bit字(W0到W3),后续每个新的字由前一个字与更早的一个字异或得到。每产生4个新字(即一组轮密钥),就要执行一次G函数。G函数包含三个步骤:字循环左移一个字节、逐字节S盒替换、与轮常量Rcon异或。

轮常量Rcon是扩展算法中的"调味料",它是一个由GF(2^8)上的生成元按幂次递增得到的数组,第一轮到第十轮分别对应不同的值。没有Rcon的参与,不同轮次之间的密钥扩展方式会完全相同,算法将出现严重的安全隐患。

在代码实现上,密钥扩展既可以每轮实时计算,也可以一次性全部预计算后存储。我采用的方式是:复位后将所有11个轮密钥一次性计算完,存储为寄存器数组,后续每轮直接从数组中取用。这样轮函数的时序更干净,计算路径上没有额外的密钥扩展延迟,控制逻辑也更简单。代价是额外的寄存器资源:11×128 bit总共352个D触发器,对主流FPGA芯片来说完全不值一提。

3.3 MixColumns的有限域运算

列混合操作是把状态矩阵的每一列视为GF(2^8)上的一个4维向量,与一个固定的可逆矩阵相乘。这个矩阵乘法和普通的矩阵乘法区别在于:加法是逐位异或(XOR),乘法是有限域乘法。

有限域乘法是理解的难点。以乘2为例——在GF(2^8)中乘以0x02可以拆解为:先左移一位,如果最高位为1,则再异或0x1B(即不可约多项式x^8 + x^4 + x^3 + x + 1对应的截断值)。这在硬件上的实现极其简单:{a[6:0], 1'b0} ^ (8'h1B & {8{a[7]}})。乘3则是乘2的结果再异或原值。

熟练掌握xtime(乘2操作)的写法后,整个MixColumns模块的Verilog实现其实只需要几条连续赋值语句。我在代码里没有采用线性代数的抽象表示方式,而是直接对每个输出字节的4个系数用异或组合展开,代码直观、逻辑清晰,综合优化也更容易。

4. 实操过程:完整代码实现与仿真验证

4.1 顶层模块接口定义与状态机控制

顶层模块的接口设计需要兼顾测试的便利性和实际使用的灵活性。我定义的接口如下:

module aes128_top ( input wire clk, input wire rst_n, input wire start, // 启动信号,高电平有效 input wire decrypt, // 加解密选择:0加密,1解密 input wire [127:0] key, // 128位密钥 input wire [127:0] data_in, // 输入明文/密文 output reg [127:0] data_out, // 输出密文/明文 output reg busy, // 忙信号,运算期间为高 output reg done // 完成信号,单周期脉冲 );

加密流程的状态机分为IDLE和WORK两个状态。IDLE状态下检测到start拉高,锁存输入数据并切换到WORK状态;WORK状态下启动轮计数器,完成10轮迭代后拉高done单周期脉冲,回到IDLE状态。整个流程用时序逻辑驱动,避免出现组合逻辑反馈环。

内部状态寄存器存储当前迭代过程中的数据块(state)。每轮开始时根据轮计数从密钥扩展模块的存储数组中取出对应的轮密钥进行AddRoundKey。注意初始阶段(round=0)先做一次AddRoundKey,之后每轮先SubBytes、ShiftRows、MixColumns(最后一轮跳过),再做AddRoundKey。

4.2 轮函数模块的Verilog实现

以下是加密轮函数核心逻辑的精简代码框架:

// SubBytes: 查表替换 wire [7:0] sb_out [0:15]; genvar i; generate for (i = 0; i < 16; i = i + 1) begin : gen_subbytes aes_sbox u_sbox ( .addr(state_in[i*8 +: 8]), .dout(sb_out[i]) ); end endgenerate // ShiftRows: 按行循环移位 wire [127:0] shiftrows_out; assign shiftrows_out = { sb_out[0], sb_out[5], sb_out[10], sb_out[15], sb_out[4], sb_out[9], sb_out[14], sb_out[3], sb_out[8], sb_out[13], sb_out[2], sb_out[7], sb_out[12], sb_out[1], sb_out[6], sb_out[11] };

ShiftRows的逻辑要特别强调一下。AES状态矩阵的元素是按列优先排列的,128 bit数据进到轮函数后,最高字节对应第0行第0列,最低字节对应第3行第3列。做行移位时,第0行不动,第1行循环左移1字节,第2行左移2字节,第3行左移3字节。如果对状态矩阵的排列理解不到位,这里最容易出错。

MixColumns部分的实现则是根据列混合矩阵的系数(0x02、0x03、0x01、0x01)对每列的4个字节做有限域加权异或。以状态矩阵的第一列为例,输出字节0的计算方式为:

wire [7:0] c0 = xtime(state_mix[0*8 +: 8]) ^ (xtime(state_mix[1*8 +: 8]) ^ state_mix[1*8 +: 8]) ^ state_mix[2*8 +: 8] ^ state_mix[3*8 +: 8];

4.3 Testbench设计思路与验证结果

Testbench是验证环节的核心。我习惯在Testbench里做两件事:一是用标准测试向量检验功能正确性,二是用随机数据做加解密往返一致性检查。

标准测试向量是FIPS-197文档附录中给出的官方例子,所有AES实现都必须通过这个验证。密钥取000102030405060708090a0b0c0d0e0f,明文取00112233445566778899aabbccddeeff,加密输出应为69c4e0d86a7b0430d8cdb78070b4c55a

Testbench的结构大致如下:

initial begin clk = 0; rst_n = 0; #100 rst_n = 1; key = 128'h000102030405060708090a0b0c0d0e0f; data_in = 128'h00112233445566778899aabbccddeeff; decrypt = 1'b0; @(posedge clk); start = 1'b1; @(posedge clk); start = 1'b0; wait (done); if (data_out == 128'h69c4e0d86a7b0430d8cdb78070b4c55a) $display("TEST PASSED"); else $display("TEST FAILED"); $finish; end

我在Vivado的XSim以及ModelSim下都跑过这个测试,加密10轮、解密需要做逆向密钥扩展,同样能正确还原。实际仿真中一个重要技巧是在Testbench里周期性检查busy信号和done信号的时序关系,确保数据锁存的时钟沿是正确的。如果start信号只维持一个周期,需要确认顶层模块能可靠地锁存输入,不会出现漏采或重复采的问题。

4.4 工程文件组织与综合实现要点

完整工程在Vivado 2020.2以及Quartus Prime中都验证过。文件组织建议如下:

prj/ ├── rtl/ │ ├── aes128_top.v │ ├── aes128_key_expand.v │ ├── aes128_cipher.v │ └── aes_sbox.v └── sim/ └── aes128_tb.v

综合时选择Artix-7 xc7a35t芯片,默认策略下资源占用约为468个LUT、312个寄存器,时序跑到100 MHz以上很轻松。这足以说明迭代式架构的资源优势相当明显。如果你的设计有更严格的时序要求,可以通过流水化S盒输出寄存器来进一步优化关键路径。

5. 常见问题与排查技巧实录

5.1 加密结果和标准答案对不上

这个问题出现的频率最高。我在调试中总结出三个最主要的排查点:

第一,检查SubBytes的S盒表是否填入正确。S盒表有256个值,任何一个值抄错都会导致结果错误。建议直接从FIPS-197附录A中复制常量定义,不要手工输入。

第二,检查行移位的数据排列顺序。如前所述,AES状态矩阵的行列方向和Verilog里bit的排列方向有一个映射关系,搞反了ShiftRows的移位方向,前几轮的结果可能看起来有规律但实际完全错误。

第三,检查轮数控制。AES-128一共10轮,但很多人在代码里容易写多一轮或者少一轮。我在状态机里用的计数器从0到9共10个周期,第9轮(即最后一轮)要跳过MixColumns,这个条件分支很容易被遗漏。

5.2 仿真出现X态或高阻态

出现X态基本可以断定是复位信号或数据路径上的寄存器没有被正确初始化。我的做法是给所有内部寄存器明确复位值,并且在Testbench里给足够的复位时间后再拉高rst_n。同时检查代码中是否存在位宽不匹配的情况——比如把8 bit的信号赋值给128 bit的变量,高位会自动补零而不是保持X态(这取决于综合工具的设置),但应该养成位宽完全一致的习惯。

5.3 仿真卡死或长时间无结果

如果wait(done)永远等不到,大概率是状态机跳转条件没满足。我遇到过的情况是start信号只拉高了一个周期,但状态机在IDLE状态用电平检测,导致start为高时没有同步到时钟沿上。解决办法是在状态机入口加一个start寄存一拍的前沿检测逻辑:

reg start_d; always @(posedge clk or negedge rst_n) begin if (!rst_n) start_d <= 1'b0; else start_d <= start; end wire start_pos = start & ~start_d;

5.4 综合后功能异常但仿真正常

这种情况在FPGA开发中也不少见。仿真是理想时序,而综合后存在门延迟和布线延迟。我遇到过的主要原因是组合逻辑环——例如某些内部信号直接由状态值组合产生,但没有经过时钟寄存,导致时序分析时出现latch推断或comb loop警告。排查时重点看综合报告中的Warning信息,特别是Latch inferred和Combinational loop这两类警告。另一个原因是S盒使用always块时漏写了敏感列表,这种情况在Vivado中通常会综合成RAM而不是组合逻辑,功能上会有隐蔽的差异。

6. 进一步扩展:解密实现与工程建议

当前工程只实现了加密方向。如果需要解密功能,有两条路线可以选择。第一条是实现完整的AES解密轮函数,包括逆S盒、逆行移位、逆列混合,密钥扩展也要改为逆向读取轮密钥。第二条是利用AES的结构对称性——解密相当于用逆序的轮密钥重新执行加密轮函数的逆过程。第一条路线的代码量大约是加密的两倍,但逻辑清晰,更推荐参考实现。

另一个值得做的扩展是通过AXI-Lite总线把AES模块封装成可被处理器访问的外设,配合Zynq或软核处理器使用。这样在主系统中,CPU只要向特定地址写入密钥和明文,再读取结果寄存器就能完成硬件加密,非常符合实际工程项目中软硬件协同工作的典型场景。

如果你准备拿这个项目去面试,我建议重点关注三个面试官常问的点:状态机为什么这么划分、S盒为什么用查找表实现而不是组合逻辑、以及MixColumns里的有限域乘法是怎么推导出来的。把这三个点讲清楚,这个项目写在简历上的含金量会高很多。

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

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

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

立即咨询