基于Verilog的FPGA AES加密实现与仿真验证
2026/9/16 13:41:00 网站建设 项目流程

简介:一套基于Verilog HDL实现AES加密算法的完整Quartus II工程,面向FPGA开发者和信息安全学习者,适合希望深入理解对称加密硬件实现及数字电路设计的读者。包内共214个文件,包含硬件描述语言源码(v/vhd)、Quartus工程文件(qpf/qsf)、仿真波形(wlf)以及FPGA配置文件(sof/mif)等,压缩包大小仅7.4MB,工程结构完整,可直接导入Quartus II进行编译、仿真与下载验证。已有199人学习下载。工程以aes_top顶层模块为核心,整合了密钥扩展、字节替代、行位移和列混淆等子模块,便于对照AES算法原理逐层分析硬件实现细节;同时附带完整工程配置与仿真脚本,免去环境搭建与代码编写成本,可帮助读者快速搭建AES加密的FPGA验证平台,是兼顾算法学习与工程实践的实用资料。

1. 为什么要用Verilog在FPGA上实现AES加密

AES作为对称加密的事实标准,在软件层面已经被优化到相当高的吞吐量,但如果你做的是高速网络接口、磁盘加密控制器或者卫星链路,软件实现的瓶颈会立刻暴露出来:CPU占用高、时延抖动大、难以满足线速加密。我在一个数据采集项目中就遇到过这样的问题,千兆以太网的数据进来,软件AES硬生生把吞吐压到了300Mbps,最后不得不把加密逻辑搬进FPGA。用Verilog实现AES不仅能把加密延迟压缩到几十个时钟周期,还能通过流水线让每个时钟周期都吐出一个加密结果,吞吐率直接提升一个数量级。

本文基于的工程是一个完整的Quartus II项目,核心文件是aes_top.v,配套了regist.vcrp_cl.v.bak以及ModelSim的.do仿真脚本。里面包含AES加密需要的全部子模块,从S盒替换到密钥扩展,都可以直接在FPGA上综合运行。这一篇我会从算法结构、RTL设计、工程导入到仿真验证逐步拆解,最后给出CBC模式改造和流水线优化的具体做法,适合刚接触硬件加密的工程师,也适合想把手头AES代码做快的熟手。

2. AES加密算法的四个核心变换与轮密钥编排

2.1 AES的分组结构与轮函数执行流程

AES是分组密码,分组长度固定为128比特,密钥长度支持128、192和256比特,对应10轮、12轮和14轮迭代。每一轮加密在硬件上要依次执行字节替代、行位移、列混淆和轮密钥加四个步骤,最后一轮特殊处理,不做列混淆。这个结构保证了雪崩效应:明文中的一个比特变化,经过若干轮后会扩散到整个分组。

密钥长度轮数每轮操作数密钥扩展输出字
128 bit10444字(176字节)
192 bit12452字(208字节)
256 bit14460字(240字节)

在Verilog实现中,最常见的做法是并行展开所有轮次(combinational loop)或者用状态机做单轮复用。前者吞吐率高但面积大,后者面积小但每轮需要多个时钟周期。aes_top.v里采用的是标准的状态机结构,用计数器控制轮次,并在最后一轮跳过MixColumns,这样便于在FPGA上做时序收敛。

2.2 SubBytes字节替代与S盒的存储方式

字节替代是非线性变换,AES用固定的S盒查表实现。S盒是一个256字节的查找表,本质上是有限域GF(2^8)上的乘法逆元再经过仿射变换。硬件实现S盒有两种思路:一种是用ROM或者分布式RAM存储256个字节;另一种是直接用case语句展开组合逻辑。前者资源开销小,后者延迟低但LUT消耗大。

S盒替换是对状态矩阵的每个字节独立操作。由于每个字节的替换互不依赖,这就成了FPGA并行优势的最佳体现——16个字节可以同时做查表,一次完成整个状态矩阵的替换。在Verilog代码里就是一个function或者case语句块,从x0到x255映射到对应的S盒值。

function [7:0] sbox_lookup; input [7:0] addr; begin case (addr) 8'h00: sbox_lookup = 8'h63; 8'h01: sbox_lookup = 8'h7c; // 此处只展示前两个映射 default: sbox_lookup = 8'h00; endcase end endfunction

这段代码的核心是case分支实现了完整的S盒映射表。实际工程里会把256个条目全部写上,或者直接用一个只读数组初始化。综合工具会自动优化成LUT结构,在Cyclone系列FPGA上,单个S盒大约消耗16个LUT,延迟在2ns以内。

2.3 ShiftRows与MixColumns的状态矩阵变换

ShiftRows是对128位状态的不同行做循环左移,第0行不移,第1行移1字节,第2行移2字节,第3行移3字节。这个操作用Verilog实现非常直观,本质上只是重新编排位宽信号的拼接顺序,不消耗任何逻辑资源。

MixColumns则是在GF(2^8)上做矩阵乘法,每列4个字节与固定矩阵相乘,涉及乘2和乘3的运算。在GF(2^8)中,乘2相当于左移一位,最高位溢出时与0x1b异或;乘3等于乘2再异或原数。这个逻辑在每个时钟周期内都需要组合路径完成,是影响AES主频的关键路径。

// 单列MixColumns组合逻辑,xtime为GF(2^8)乘2运算 wire [7:0] a0 = state[31:24], a1 = state[23:16]; wire [7:0] a2 = state[15:8], a3 = state[7:0]; wire [7:0] t0 = xtime(a0) ^ xtime(a1) ^ a1 ^ a2 ^ a3; wire [7:0] t1 = a0 ^ xtime(a1) ^ xtime(a2) ^ a2 ^ a3; wire [7:0] t2 = a0 ^ a1 ^ xtime(a2) ^ xtime(a3) ^ a3; wire [7:0] t3 = xtime(a0) ^ a0 ^ a1 ^ a2 ^ xtime(a3); assign mix_col_out = {t0, t1, t2, t3};

这段代码里需要注意xtime的实现方式:xtime(x) = (x << 1) ^ (x[7] ? 8'h1b : 8'h00)。四个输出字节t0t3各自是4个输入字节在不同系数下的GF乘法异或和,每个输出只依赖本列的4个输入字节,所以四列之间天然并行,非常适合作成4个独立的组合逻辑块。

2.4 密钥扩展与轮密钥加

密钥扩展是AES里最容易出小错的环节。它的核心逻辑是:每4个字为一组,新字由旧字循环左移、S盒替换、与轮常量异或后得到。128位主密钥生成44个字,每轮使用4个字,共11组轮密钥(含初始轮密钥)。Verilog实现时,一般用一个memory数组存储全部扩展后的轮密钥,或者并行计算出每一轮的轮密钥寄存。

轮密钥加操作最简单,就是把状态和当前轮的轮密钥做异或。在硬件上这意味着128个异或门,信号延迟极小,对关键路径几乎没有影响。真正的复杂度在密钥扩展的状态反馈:每个新字依赖前一个字,串行生成时会产生额外的时钟周期开销。

// 密钥扩展核心:每轮的w[0]需要经过g函数变换 function [31:0] keygen_g; input [31:0] word_in; input [7:0] rcon; reg [31:0] rotated; begin rotated = {word_in[23:0], word_in[31:24]}; // 循环左移一个字节 keygen_g = {sbox_lookup(rotated[31:24]), sbox_lookup(rotated[23:16]), sbox_lookup(rotated[15:8]), sbox_lookup(rotated[7:0])} ^ {24'h000000, rcon}; end endfunction

keygen_g函数是密钥扩展中的核心非线性变换,先把输入的32位字循环左移8位,然后逐字节做S盒替换,最后和轮常量rcon异或。rcon按轮次取01, 02, 04, 08, 10, 20, 40, 80, 1b, 36。有一点容易被忽略:rcon只在每个密钥组的第一个字时使用,后面三个字只是简单的前字异或。还有,第11轮之后就没有密钥扩展了,控制器需要在加密完最后一轮后停止扩展逻辑,避免生成多余密钥导致握手错误。

3. Verilog RTL设计:从S盒到aes_top顶层模块级联

3.1 工程文件结构与模块划分

这个Quartus工程的文件结构很典型,aes_top.v作为顶层模块,负责实例化所有子模块并编排控制状态机。regist.v从名字看应该是密钥寄存模块,负责主密钥的锁存和轮密钥的更新。crp_cl.v.bak文件名的.bak后缀说明它是列混淆模块的备份版本,可能是调试过程中保存的一个历史版本,正式编译时Quartus默认只综合.v文件,所以这个备份文件不会进入综合流程。

文件/模块功能推测在加密链路中的位置
aes_top.v顶层控制与状态机调度所有子模块
regist.v密钥寄存与更新为轮密钥加提供数据
crp_cl.v.bak列混淆组合逻辑(备份)每轮中间状态变换
.ammdb / .vprQuartus工程数据库文件编译中间产物,可删除
.do.bak文件ModelSim仿真脚本备份记录历史仿真命令

aes_top.v的状态机一般包含IDLE、LOAD_KEY、INIT_ROUND、MAIN_ROUND、FINAL_ROUND、OUTPUT这几个状态。IDLE等待加密启动信号,LOAD_KEY完成主密钥装载,INIT_ROUND做初始轮密钥加,MAIN_ROUND循环执行10到14轮完整变换,FINAL_ROUND跳过多列混淆,OUTPUT输出密文并拉高完成标志。

3.2 S盒替代模块的两种实现方案

S盒的设计直接决定了AES加密模块的面积和速度。方案一是用一个256x8的memory,初始化为S盒的全部值,每次替换需要先寻址再读出,延迟为一个mem读取周期。方案二是直接把S盒写成组合逻辑,查表信号进去到数据出现只经过LUT级联的传播延迟,在时序要求不高的场合可以直接嵌到组合路径里。

推荐用方案二的组合逻辑实现,因为AES轮内的SubBytes和后续ShiftRows在硬件上可以合并优化:ShiftRows只是重排字节位置,不改变字节值,所以可以在S盒输出端直接做线网重连,省掉一个寄存级。这个思路和很多开源AES核的做法一致,具体到代码上就是把16个字节的S盒查表输出直接连到ShiftRows的对应位置上。

3.3 行位移与列混淆的Verilog实现

ShiftRows在代码里不占用时钟周期,用assign语句重排即可。以128位状态为例,假设状态按字节序从高位到低位排布:第0行字节是位[127:120]、[95:88]、[63:56]、[31:24],第1行要左移1字节,第2行左移2字节,第3行左移3字节。写出来就是一串位段拼接,综合工具会把它优化成纯走线。

MixColumns的细节在2.3节已经给出单列的算法,工程中四列是并行的wire,相同逻辑复制4份。这里有一个工程上的取舍:如果做10轮迭代复用,那么MixColumns的结果需要寄存到状态寄存器里供下一轮使用,此时组合逻辑的延迟会直接决定最高工作频率。为了提速,有些设计会把MixColumns的xtime表达式展开成异或树,减少逻辑级数,代价是代码可读性降低。

3.4 密钥扩展寄存器链与轮密钥复用

密钥扩展模块在反复迭代时容易出bug的地方在于寄存器更新时序。轮密钥生成依赖上一轮的密钥字,如果你在同一时钟上升沿既读旧轮密钥做轮密钥加又写新轮密钥,就会产生竞争。稳妥的做法是用两套寄存器:current_key保存当前轮使用的密钥,next_key计算下一轮密钥,时钟沿到来时统一更新。

always @(posedge clk or negedge rst_n) begin if (!rst_n) begin current_key <= 128'h0; round_key <= 128'h0; end else if (load_key) begin current_key <= master_key; round_key <= master_key; end else if (round_valid) begin current_key <= next_key; // 预计算的下一轮密钥 round_key <= current_key; // 当前轮使用的轮密钥 end end

这段代码体现了双寄存器打拍的思路,current_keyround_valid有效时切换为next_key,而round_key则稳定输出当前轮的密钥供轮密钥加模块使用。这么做的好处是密钥更新和状态加密在同一个时钟周期互不干扰。如果后续做流水线优化,这个寄存器结构可以直接作为流水线寄存段使用。

3.5 aes_top顶层控制状态机的编排

顶层状态机是整个模块的调度核心。状态机最少需要四个状态:空闲态等待外部start脉冲,装载态完成密钥加载和初始轮密钥加,轮循环态根据密钥长度反复执行10/12/14轮,输出态把密文锁存到输出寄存器并拉高done信号。状态跳转的逻辑在综合后会得到非常紧凑的有限状态机。

控制信号里最需要注意的是round_counter的计数清零条件。如果在输出态没有把计数器清零,下一次加密启动时会从残留值开始计数,导致轮数错误。这个bug在仿真中不容易发现,因为单次加密结果正确,连续加密两次就会暴露。一个稳妥的做法是在状态机进入LOAD_KEY状态时同步清零轮计数器和数据通路寄存器。

always @(posedge clk or negedge rst_n) begin if (!rst_n) aes_state <= IDLE; else begin case (aes_state) IDLE: if (start) aes_state <= LOAD_KEY; LOAD_KEY: aes_state <= INIT_ROUND; INIT_ROUND: begin round_cnt <= 4'd0; aes_state <= MAIN_ROUND; end MAIN_ROUND: begin if (round_cnt == NUM_ROUNDS - 2) aes_state <= FINAL_ROUND; else round_cnt <= round_cnt + 1'b1; end FINAL_ROUND: aes_state <= OUTPUT; OUTPUT: aes_state <= IDLE; default: aes_state <= IDLE; endcase end end

状态机代码里的NUM_ROUNDS是参数化的值,128位密钥为10,192位为12,256位为14。约束条件不复杂,但要注意FINAL_ROUND里不能执行MixColumns,这个分支需要在数据通路里用多路选择器单独处理。数据通路的控制信号如sbox_enmix_enkey_add_en由状态机组合输出,需要留意这些信号在状态切换瞬间是否有毛刺,一般通过寄存一拍来消除。

4. Quartus II工程导入与ModelSim仿真脚本验证

4.1 工程文件识别与导入

拿到压缩包后,先不要急着打开Quartus,先检查目录结构。.vpr.ammdb.map.ammdb这类文件是Quartus编译生成的工程数据库,如果工程是用Quartus II打开过的,这些文件会自动重建。aes_top.v是设计的入口文件,regist.v是子模块文件。aes_top.vt.bakregist.v.bak是文件备份,实际编译时可以忽略。

对于.bak后缀的文件,我的建议是不需要手动改后缀名参与编译,就当作历史存档。Quartus只会把工程中添加的.v文件纳入综合,备份文件留在磁盘上不影响工程。如果你后续要自行修改代码,建议先复制一份正式.v文件再改,避免误操作破坏原工程。

在Quartus II中新建工程时,把Family选成你手头FPGA对应的系列,比如Cyclone IV,Device选具体的型号。然后直接把aes_top.vregist.v加入工程,Top-Level Entity设为aes_top。编译前最好先在Assignments菜单里检查Pin Planner有没有未分配引脚,FPGA上板需要引脚约束,单纯做仿真不需要。

4.2 ModelSim脚本仿真的启动与波形查看

工程中的aes_top_run_msim_rtl_verilog.do.bak是ModelSim的批处理脚本备份,内容一般是编译、仿真和添加波形的命令序列。把这个文件复制一份去掉.bak后缀,在ModelSim命令行里执行do aes_top_run_msim_rtl_verilog.do即可自动跑完整个仿真流程。

# ModelSim 仿真脚本示例 vlib work vlog ./aes_top.v ./regist.v ./tb_aes_top.v vsim -L altera_ver -L altera_mf_ver work.tb_aes_top add wave -hex /tb_aes_top/uut/* run -all

vlib work是建立库目录,vlog命令逐个编译源文件,vsim启动仿真并加载testbench。add wave -hex把被测模块内部所有信号以十六进制添加到波形窗口,run -all跑完整个仿真过程直到testbench中执行$finish。如果你想看波形文件,需要在testbench里加$dumpfile$dumpvar系统函数,或者用ModelSim菜单里的Wave窗口手动拖观测点。

仿真结束后主要检查三处:加密完成信号done是否拉高、输出密文cipher_out与标准测试向量是否一致、轮计数器的值是否符合预期。标准测试向量可以用NIST公布的FIPS 197附录C数据来对,密钥为000102...0f,明文为001122...ff,预期密文为69c4e0d86a7b0430d8cdb78070b4c55a

4.3 常见仿真错误与定位方法

仿真中的第一大错误是信号未定义导致vlog编译报错,通常是顶层模块里实例化子模块时端口列表与子模块声明不一致。比如regist.v里定义的端口是key_in,顶层例化时写成了key,编译会直接报错,检查文件头部模块声明即可。

第二大错误是仿真卡住不结束,原因往往是轮循环状态机没有正确退出。如果round_cnt阈值设错或者比较条件写反,状态机会死在MAIN_ROUND里。定位方法很简单:在ModelSim里直接查看round_cnt信号的波形,看它是否从0递增到预设值,如果到某轮后不再变化,说明状态跳转条件有问题。

第三大错误是密文与预期不符但波形看起来每个信号都有值。这类问题几乎都出在密钥扩展顺序上:轮密钥的生成应当先于轮密钥加操作一个时钟周期,如果同步逻辑没有对齐,第一轮就会用错密钥。这时要在波形里同时对比current_keyround_key两个信号的相位关系,确认round_keyround_valid之前已经稳定。

4.4 工程综合后的资源与性能分析

Quartus编译完成后,在Flow Summary页面可以查看资源占用情况。常规的非流水线AES实现,在Cyclone IV器件上大约消耗2000到3000个逻辑单元,内部存储器消耗为0(如果S盒用组合逻辑实现),最大工作频率在80到120MHz之间,加密吞吐率在10到15Mbps每MHz。也就是说,100MHz时钟下能跑到1Gbps以上,比软件实现快不少。

注意到Flow Summary里的时钟频率如果低于预期,需要打开TimeQuest Timing Analyzer看关键路径报告。一般瓶颈在MixColumns到下一轮状态寄存器的路径上,因为这条路径经过S盒、行位移、列混淆、轮密钥加四级逻辑。可以尝试在S盒输出后加一级流水线寄存器,代价是增加的延迟一个时钟周期,但主频往往能提升30%以上。

5. AES加密工作模式选择与硬件流水线优化技巧

5.1 ECB模式与CBC模式的硬件差异

工程默认实现的是ECB模式,这是最简单的电子密码本模式,每个明文分组独立加密,相同的明文产生相同的密文。ECB在硬件上最容易实现,每组加密过程互不依赖,可以直接多路并行。但ECB在安全性上有明显缺陷:明文中的模式会保留到密文中,这在很多安全标准里是被禁止使用的。

CBC模式解决了这个问题,它的核心在于每个明文分组先与前一个密文分组异或,再做AES加密。第一个分组使用初始化向量IV。硬件实现CBC时,反馈路径引入了一个串行依赖:下一分组的加密必须等当前分组密文输出后才能开始。这意味着ECB可以多组并行,但CBC只能逐组串行,吞吐率直接除以流水线深度。

// CBC模式下,明文与上一轮密文异或再进入加密核 assign aes_din = cbc_mode ? (plaintext ^ prev_cipher) : plaintext; assign prev_cipher = last_cipher_valid ? cipher_reg : init_vector;

代码里通过cbc_mode信号选择ECB还是CBC,prev_cipher在第一个分组时取init_vector,之后取上一组加密结果的密文寄存器。从硬件面积来看,CBC只需要增加一个128位异或器和一个寄存器,开销极小,代价主要是吞吐率下降。

5.2 流水线化AES核心的子级划分方法

要把AES加密核心改成流水线结构,关键是把单轮的组合逻辑在轮间切断,插入寄存器。最常见的划分是每轮一级流水线,这样10轮加密就有10级流水,虽然单个分组加密延迟增加到10个时钟周期,但每个时钟周期都能接收一个新分组,吞吐率达到每时钟周期一个分组。

// 流水线寄存器:在轮间缓存中间状态 always @(posedge clk) begin if (pipe_en) begin stage1_state <= round0_out; stage2_state <= stage1_state_comb; // ... 后续每一级类似 end end

需要注意流水线寄存器插入后,轮密钥必须同步打拍,保证每个分组在流水线各阶段使用的是自己对应的轮密钥。如果不做密钥打拍,轮密钥错位会导致加密结果全部错误。密钥打拍的做法是每组轮密钥也走一组寄存器链,与数据同步移动。

5.3 密钥扩展的预计算与即时生成取舍

密钥扩展如果提前算好全部44个字并存入寄存器,在流水线设计中会增加存储开销;如果每轮即时计算,又会增加关键路径长度。折中方案是分两阶段:在加密开始前用若干时钟周期预计算所有轮密钥,存入memory,流水线运行期间只是依次读出,不再做密钥扩展运算。

这组工程文件里regist.v的寄存器结构偏向于预计算方案,因为从模块命名看它专门负责密钥存储。实际操作时注意预计算的启动信号和加密启动信号需要联动:必须在第一组明文进入加密核之前完成全部密钥扩展,否则流水线会空转等待。一个稳妥的时序控制是,密钥扩展完成后拉高key_ready信号,加密控制器检测到该信号后再允许明文输入。

5.4 上板调试时的引脚约束与时钟管理

如果要把AES核跑到真实FPGA上,除了功能代码还需要做时序约束。在Quartus II里通过Assignment Editor添加时钟约束时,推荐的做法是先跑一次编译,再根据TimeQuest报告的实际Fmax反推约束值,不要把时钟频率设到芯片极限之外。上板调试时最常遇到的问题不是逻辑错误而是复位信号毛刺——建议使用专用的全局复位网络,或者用同步复位替代异步复位来规避。

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

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

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

立即咨询