ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AES加密算法的Verilog RTL实现:从算法到仿真验证

AES加密算法的Verilog RTL实现:从算法到仿真验证 简介AES加密完整Verilog工程包面向可编程逻辑器件与专用芯片设计人员提供一套可综合的寄存器传输级代码。工程完整覆盖字节代换、行位移、列混淆和轮密钥加法并包含密钥扩展、S盒替换、顶层状态控制等关键子模块可帮助读者从代码层面看清AES算法从明文到密文的数据通路与多轮控制逻辑适合对照学习对称加密引擎的硬件构造。压缩包共111个文件约6.5MB以Verilog源码、仿真波形、工程配置文件为核心同时提供存储器初始化文件、比特流下载文件、readme说明以及综合与时序报告基本覆盖从功能仿真、综合验证到板卡部署的完整流程。目前已有2022人学习适合希望基于完整工程快速理解AES硬件实现、或需要移植到自己器件平台进行二次开发的工程师借助清晰目录结构和可独立调用的算法子模块能有效缩短硬件加密功能的开发与排错周期也可作为设计高性能加密IP核的起点。 做FPGA和数字IC的朋友应该都有体会加密算法这种东西平时用软件一把梭感觉很顺畅一旦要求用Verilog在RTL层面把它实现出来难度完全不是一个量级的。AES作为最主流的对称加密标准在嵌入式、通信、物联网终端里几乎是绕不开的模块很多招聘JD上也会明确写“熟悉AES/DES等加解密算法的硬件实现”。这篇博文我就基于一份完整可综合的AES加密Verilog源码把从算法到RTL的关键设计思路、模块划分、仿真验证方法和实际踩过的坑都捋一遍给正在做或者准备做这块的朋友一个参考。项目本身是基于AES-128ECB模式重点在加密通路整体代码风格是标准的同步时序设计适合集成到自己的SoC或者MCU总线系统里。不管是毕业设计、竞赛还是正经的流片项目这套思路都能直接套用。1. 整体设计与核心决策1.1 为什么用Verilog硬撸AES而不是直接上软件软件实现AES说白了就是查表、异或、字节替换CPU跑起来逻辑非常清晰调试也方便。但放到硬件场景里需求就变了要么是追求极致的吞吐率比如万兆网络的IPSec引擎要么是追求低延迟像存储控制器里的在线加密要么是追求低功耗用于电池供电的传感器节点。在这些场景下纯软件实现要么速度跟不上要么功耗压不住这时候就需要一个专门的硬件加速模块。Verilog写AES本质上做的事情和软件是一样的都是那几轮SubBytes、ShiftRows、MixColumns、AddRoundKey。但硬件的核心价值在于并行化和流水线化。我们可以把一个字节的替换操作变成一个纯组合逻辑的S盒查找可以把一轮迭代的状态机跟密钥扩展模块并行跑可以通过流水线寄存器把10轮运算在时间上重叠起来。这些东西在C语言里写不出来但在RTL里是基本功。写这份源码的时候我定的目标很明确首先保证功能完全正确必须通过标准测试向量验证其次代码风格要干净每一个模块的职责单一方便集成再有不追求极端性能目标是能在常见的FPGA开发板上跑出不错的频率100MHz以上。1.2 顶层架构一个状态机拉通全流程AES-128的加密流程固定是10轮前9轮结构一样最后一轮没有MixColumns。整个数据通路是128位也就是4x4的字节矩阵算法里叫State。源码的顶层模块核心是划分成了三个大块轮运算的数据通路、密钥扩展的生成逻辑以及一个主控状态机。主控状态机是整个模块的中枢它决定了什么时刻做初始AddRoundKey什么时候开始第1轮到第10轮每一轮里是先做SubBytes还是先做ShiftRows以及最终数据什么时候输出。很多初学者写AES容易出问题就是状态机跟数据通路的时序对不上。比如密钥扩展模块算出了第3轮的轮密钥结果主控状态机还在第2轮数据自然就全乱了。所以我把状态机的状态定义和密钥扩展模块的轮数计数器做成了强耦合关系用同一个轮数寄存器来驱动两边从根本上杜绝了时序错位。顶层端口设计上除了常规的时钟、复位、128位输入数据和密钥、输出数据和输出有效标志之外我还加了一个busy信号和en信号。en是输入使能相当于告诉模块“这组数据和密钥可以开始处理了”busy是高电平有效告诉外部“我正在处理上一组数据你先别喂新的”。这种握手时序非常简单但对于集成到总线系统里已经够用了不需要搞复杂的AXI握手。128位在硬件里其实不算大但要注意在SOC系统里一个地址可能只有32位宽度所以集成的时候往往需要在外面套一层FIFO或者寄存器组来做数据拼装源码里没有把这部分包含进去保持模块的纯粹性。1.3 S盒实现方式空间换时间的经典取舍AES算法里的SubBytes是整个加密过程中最核心的非线性变换本质上是基于GF(2^8)有限域上的乘法逆元然后做仿射变换。如果看图说话这个操作就是一张256字节的查找表输入一个字节输出一个字节一一对应。在RTL里实现S盒有几种主流做法。第一种是直接用case语句把256个映射全列出来这相当于告诉综合工具用纯组合逻辑生成一个查找表在ASIC流程里更常见FPGA里也能用但会消耗大量的LUT第二种是例化成一个只读存储器ROM在FPGA里映射成BRAM资源占用小但会有一定的读延迟时序上多一级第三种是把ROM原语/LUTRAM手动例化追求极致的时序优化。我在源码里选了第一种纯组合逻辑case实现。原因很简单这套代码的主目标是逻辑正确、可读性强、方便移植。case语句写出来的S盒综合工具能自动优化在主流FPGA赛灵思、Altera上综合后频率并不差最主要的是仿真里调试非常直观你可以直接看到输入字节对应输出字节的波形。如果你追求BRAM方案把这256个case转成mem文件或者initial语句里的数组就能轻松改成ROM实现源码结构不用动。这里有个细节AES的S盒和逆S盒是两张不同的表如果只做加密只需要实现S盒如果要解密还得加一张逆S盒。这份源码专注加密通路所以只实现了S盒资源上能省不少。提示S盒的Verilog实现里注意一定别写漏了映射项。有一个比较笨但有效的办法把S盒的case语句写成generate循环或者从外部文件读入避免256行case漏写。实测下来手写case漏一两个映射是非常容易犯的错因为这个表中的数据毫无规律人工核对极容易看花眼。2. 核心模块实现细节2.1 字节替换、行移位和列混合的实现思路AES的一轮操作里SubBytes是字节级的ShiftRows和MixColumns是矩阵级的。在Verilog里把128位数据展开看就是16个字节。ShiftRows的操作是把矩阵的第0行不动第1行循环左移1个字节第2行循环左移2个字节第3行循环左移3个字节。RTL实现ShiftRows非常粗暴就是连线。因为你只需要把输入128位里的某些字节交换位置输出128位。这种操作在时序上零延迟纯走线。写代码的时候把输入数据按字节切片然后按ShiftRows规则重排拼成输出。很多初学者容易在这里被绕晕核心原因是对“行”和“列”的字节顺序理解不一致。我自己写的时候会先在纸上画出4x4矩阵标好每个字节编号然后再按规则连线能减少大量低级错误。MixColumns操作稍微复杂一点它的数学本质是对状态矩阵的每一列做一次矩阵乘法跟一个固定的矩阵在GF(2^8)上相乘。在RTL里这个操作最终可以被简化成每个输出字节是4个输入字节各自乘以2或3之后的异或和。乘以2的运算在GF(2^8)里有专门的实现办法左移一位如果最高位是1再异或上0x1B。乘以3等于先乘以2再加上原数本身。这个操作用函数表达最方便。我在源码里把xtime操作封装成functionMixColumns里每个字节的运算都调用它代码非常简洁。组合逻辑延时的大头就在MixColumns这一段。因为一个列的输出依赖4个输入字节的xtime运算结果如果纯用组合逻辑16个字节的输出交织在一起逻辑级数会偏高频率容易受限。流水线设计里通常会在轮与轮之间插寄存器来切断组合路径。我没做深度流水每轮组合逻辑一轮搞定然后输出寄存器打一拍状态机控制轮次循环。这个设计在100MHz左右没问题想上更高频率就得在轮内部插入流水级了。2.2 密钥扩展的实现路径选择AES-128的密钥扩展要从初始16字节密钥生成11组128位的轮密钥第0轮用于初始加扰后面10轮各用一组。密钥扩展算法本身不复杂以字为单位32位每个新字由前一个字和前面第4个字异或得到如果字的位置是4的倍数要先做循环左移、S盒替换再异或上轮常数Rcon。硬件实现密钥扩展有两条路线。一条是预计算型把11组轮密钥全部算出来存到寄存器堆里加密的时候一个周期取一组出来用。另一条是在线计算型每执行一轮加密密钥扩展模块同步算出一组新的轮密钥供下一轮使用。我在源码里用的是在线计算方案。为什么因为AES-128只有10轮在线计算每组密钥的硬件开销就几个异或门加一个S盒完全可以复用加密数据通路的S盒逻辑没必要为它单独存11组完整密钥省下来的寄存器资源非常可观。在线计算的另一个好处是启动快。预计算方案需要先执行一轮完整的密钥扩展才能在加密指令到来之前准备好所有密钥这是额外的延迟。在线计算方案在加密启动的第一个周期就开始同步计算等到第二轮需要轮密钥时早就算好了。代价是流程控制上要更仔细一点初始AddRoundKey用的是原始密钥第一轮要用的轮密钥是密钥扩展算出来的第一个新字组这中间的时序关系必须对齐。我是用一个简单的轮数计数器控制密钥扩展模块当状态机进入Round_N的时候密钥扩展模块输出刚好是Round_N对应的轮密钥时序天然对齐不需要额外的缓存。2.3 状态机与轮数控制主状态机的设计我特意保持简单没有用复杂的序列化技巧。整个状态机就四个状态IDLE、ROUND、LAST_ROUND、DONE。IDLE等待使能信号en有效就锁存输入数据同时把初始密钥存到密钥扩展模块里。接着进入ROUND状态执行第1轮到第9轮每轮占用一个时钟周期。然后状态跳到LAST_ROUND执行最后一轮少了MixColumns最后一个时钟周期输出密文拉高valid信号同时回到IDLE状态。这里有一个大家常讨论的问题——是否要做流水线如果做流水线比如把10轮拆成深度10的流水线那么理论上可以同时处理10组数据吞吐率大幅提升但代价是延迟增加、资源成倍上涨。从数据的昨天说第一组数据算完依旧需要十几个周期但后续数据每个周期都能出一个结果。我用的是最简单的迭代型架构数据逐个进、逐个出面积小、功耗低、逻辑简单对大部分嵌入式场景已经足够。真要追求吞吐率这份源码里的S盒和轮函数模块是现成的你可以很轻松地把它们改造成流水线形式。我自己的看法是迭代型方案做出来调试的时间最少适合快速出活性能不够再上流水线这样递进的开发节奏最稳。3. 实操过程与验证记录3.1 从算法到RTL的翻译过程拿到这个项目的时候我的做法不是直接撸代码而是先花了几个小时把FIPS-197标准文档里面的算法流程完整梳理了一遍。AES加密在软件里实现过一次的人应该都有感觉正着写还行但是一旦要在RTL里做你对每一个中间状态的位宽、顺序、字节排列都必须极其精确差一个bit都不行。所以第一步就是画框图把数据通路的流向画清楚明文进来先和K0异或然后进10轮迭代。代码组织结构上我按功能分模块写。顶层叫aes_cipher_top里面例化了三个核心子模块sbox用于执行字节替换key_expand用于在线生成轮密钥round_function处理一轮的完整变换SubBytes、ShiftRows、MixColumns、AddRoundKey。这样拆的好处是每个模块单独可测debug的时候能快速定位是哪个环节出错。S盒的case语句有256项在代码里占了很大篇幅但这部分是不需要动脑子的复制工作反而最容易出错所以我用脚本从标准文档里提取S盒表自动生成Verilog片段再手动粘贴到文件里。这样既保证了准确性又节省了大量时间。写代码的时候还有一个细节所有的数据线、控制线我统一用大端还是小端AES标准里对字节排列顺序有明确规定我全程用大端方式来理解State矩阵的字节顺序确保跟标准测试向量一致。如果不小心在某个模块里搞反了字节顺序功能仿真时表现为密文完全不对而且极其难排查。3.2 testbench设计与标准向量比对RTL写完之后验证环节的重要性怎么强调都不过分。这里说的验证不是随便给一组数据跑一下波形拉出来看看而是必须用标准测试向量做正式对比。FIPS-197文档附录里给了好几组明文、密钥、密文的对应关系这就是权威答案。任何自研的AES硬件过不了这些向量就等于白做。我在testbench里写了三个主要部分时钟和复位信号的生成、输入激励的施加、输出结果的自动对比。时钟用了10ns周期100MHz复位先拉低再拉高模拟上电复位过程。激励部分定义了任务task可以方便地填入不同的明文和密钥组合。输出对比不靠人眼而是用$display配合条件判断如果密文和期望值不一致就直接报错一致就打印当前测试用例通过。测试用例我准备了三组。第一组是FIPS-197文档里的标准样例明文00112233445566778899aabbccddeeff密钥000102030405060708090a0b0c0d0e0f期望密文69c4e0d86a7b0430d8cdb78070b4c55a。第二组跑的是全零密钥加密全零明文的情况这在很多开源核里也经常被当作基础测试。还有一组我故意改了明文中一个bit验证雪崩效应是否明显——加密算法对输入极其敏感如果改动一个bit后输出变化不大那大概率是逻辑有严重缺陷。这些测试跑下来全部通过才敢说这个模块功能基本可靠。3.3 上板验证与资源占用功能仿真通过之后真正的考验在上板。我找了一块常规的FPGA开发板做验证用板上按键模拟en信号开关组输入明文和密钥LED灯显示密文的部分字节。一开始遇到一个小麻烦按键按下瞬间的机械抖动导致en信号多次触发模块被启动了两次密文被刷掉了。解决方式是加了个简单的消抖模块或者用开发板自带的按键消抖IP另外把en改成边沿检测逻辑只在按键按下的上升沿生效。Synplify或者Vivado综合之后资源占用数字还是比较漂亮的。在赛灵思7系列上这套代码用了不到2000个LUT触发器大概几百个BRAM为零因为S盒用的组合逻辑。时钟频率综合结果能到150MHz左右实际时序约束跑100MHz非常稳。如果你改用BRAM存S盒LUT资源还能再降一半多但组合逻辑延时幅度会受影响需要根据自己项目的资源瓶颈来选择。这个资源水平意味着它非常轻量可以轻松地挂到任意系统的APB或者AXI-Lite总线上做一个内存映射的加密外设向CPU提供加解密服务。4. 常见问题与排查技巧实录4.1 S盒复位态和锁存器的坑综合后检查报告发现S盒模块里生成了16个锁存器Latch这是一个非常隐蔽的隐患。原因是case语句的default分支没有写全有的输出分支在某种条件下没有被赋值综合工具就会推断成锁存器来保持之前的值。锁存器在FPGA里一般是不推荐使用的容易带来时序问题和毛刺。解决方法是检查所有case语句把default分支补上或者给所有输出寄存器赋默认值。这个问题之所以隐蔽是因为功能仿真的时候输入组合覆盖不到那个没有赋值的分支波形看起来完全正常只有到了上板测试或者时序分析时才暴露出来。所以写组合逻辑的时候我习惯在所有case语句的default分支里写明“输出等于全零”或者“保持当前值但其实不该出现”并且跑一遍代码规范检查把锁存器报告清零。4.2 时序收敛不了怎么办迭代型AES在频率做高之后最常遇到的综合时序问题是关键路径集中在MixColumns和下一轮AddRoundKey的组合逻辑上。因为这两段逻辑是串连的轮内组合延迟没法继续压。解决办法有三个方向一是把MixColumns内部的组合逻辑打散通过改变运算顺序缩短关键路径二是把SubBytes和MixColumns之间插入寄存器牺牲一个周期的吞吐量但频率能明显拉高三是对S盒动手用BRAM实现S盒天然自带一级寄存器把组合逻辑路径断开。从我个人经验看如果目标频率在100~150MHz之间组合逻辑S盒方案完全够用。要跑到200MHz以上基本就得考虑流水线或者BRAM方案了。做时序优化的时候一定要先看综合工具报出来的关键路径报告不要瞎猜是哪一段逻辑慢了用路径报告指导优化事半功倍。4.3 输入数据和密钥同步的对齐问题这个坑我踩过最多次。如果你在en有效的同时把数据送到输入端口但密钥晚了一个周期才稳定那么初始AddRoundKey打进去的密钥就是错的后面全线崩溃。关键是明文、密钥、en这三个信号必须在同一个时钟沿被采样不能在顶层逻辑里错开。我的做法是用一个寄存器组把明文和密钥在en有效的那一拍同时缓存下来之后所有运算都用缓存的副本不直接引用输入端口这样即使外部总线时序有毛刺也不会污染内部状态。还有一点在多周期处理同一组数据的场景下如果外部又把新的数据喂进来了busy信号必须能有效地阻止新数据覆盖旧数据。我听有的朋友说他们直接把en接在总线的写选通上结果遇到了连续两次写操作就会把第一组密文冲掉的情况这就是顶层握手设计没考虑好的典型。源码里我专门做了一个标志位寄存en有效且busy为低时才接受新数据否则忽略这个处理虽然简单但非常管用。4.4 仿真常见错误与调试手段仿真时最常见的错误是波形和期望完全对不上见到的原因千奇百怪但大多逃不出这三类第一字节序搞反State矩阵的行列和标准文档不一致第二轮密钥生成和轮运算错位密钥扩展算快了或者算慢了第三最后一轮漏掉了“不执行MixColumns”这个条件导致最后一轮结果也不对。遇到波形不对的时候我建议一定不要直接去猜代码哪里错了而是逐轮打印中间值。我在testbench里加了辅助任务每轮结束后把当前State矩阵的16个字节以十六进制打印出来跟标准文档提供的过程中间值做对比。AES标准文档里其实有详细的中间转换值虽然不像最终密文那么全但足够定位到底是在第几轮跑偏了。查到是哪一轮开始不对之后再去查那一轮对应的模块逻辑问题立刻缩小到很小的范围。这个方法帮我节省了大量的排查时间强烈推荐所有做AES硬件实现的人都把中间值打印加进自己的验证环境里。写在最后这份AES加密Verilog源码从算法梳理到最终的仿真、上板验证整个流程走下来最大的体会是AES的RTL实现技术难度不高但对细心程度的要求极高。字节序、时序控制、状态机对齐每一处都是细节错一个bit就是满盘皆输。它的工程意义在于你不仅掌握了一个具体的加密模块更熟悉了一套从标准文档到RTL再到验证的完整转换方法这个方法论适用于任何密码算法或通信协议的硬件实现。最后分享一个个人习惯在代码顶层文件里写清楚端口时序图和设计文档的版本号哪怕只是一个极简的时序图对于几个月后回过头来维护代码的自己都是巨大的帮助。硬件工程里工程素养往往比天马行空的技巧更能决定一个项目能走多远。希望在啃AES Verilog实现的朋友都能顺利跑通自己的第一版然后把这份从容带到更复杂的项目里去。本文还有配套的精品资源点击获取
返回列表