ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

FPGA实现AES-128加密算法:Verilog硬件设计与工程实践详解

2026/9/10 2:06:23 拓冰建站 浏览量
FPGA实现AES-128加密算法:Verilog硬件设计与工程实践详解 简介面向FPGA与硬件安全开发者的AES-128完整Verilog实现基于Rijndael算法覆盖密钥扩展、字节替换、行移位、列混淆等核心模块并附带VHDL对照代码适合用于学习对称加密算法的硬件加速、安全模块设计与芯片验证流程。压缩包共230个文件约119.72MB除6份Verilog源码外还包含存储器初始化文件、Quartus工程配置、测试平台、仿真波形以及日志备份文件能够支撑从功能仿真、逻辑综合到工程实现的完整设计流程。资源内附详细说明文档与测试平台读者可对照密钥扩展逻辑、S盒生成、轮函数实现以及工程目录结构逐步梳理AES-128在可编程逻辑器件上的数据通路和时序约束方法同时参考作者在仿真调试中保留的中间文件与备份记录减少硬件调试弯路。已有约1190人浏览学习对正在接触加密算法硬件实现、Verilog工程调试或安全芯片设计的开发者具有不错的参考价值。1. 项目概述为什么选择用FPGA实现AES-128AES-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.vTestbench用于仿真验证不参与综合这种划分方式遵循了高内聚、低耦合的设计原则每个模块只关注自己的职责接口清晰明确。后续如果要增加解密功能只需要新增一个对应的轮函数模块或者给现有模块添加可配置的模式选择信号即可。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], 1b0} ^ (8h1B {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。注意初始阶段round0先做一次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 128h000102030405060708090a0b0c0d0e0f; data_in 128h00112233445566778899aabbccddeeff; decrypt 1b0; (posedge clk); start 1b1; (posedge clk); start 1b0; wait (done); if (data_out 128h69c4e0d86a7b0430d8cdb78070b4c55a) $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 1b0; 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里的有限域乘法是怎么推导出来的。把这三个点讲清楚这个项目写在简历上的含金量会高很多。本文还有配套的精品资源点击获取