
简介基于Verilog的DES加密算法实现资源面向FPGA开发者与信息安全方向学习者展示如何用硬件描述语言在Quartus环境中搭建DES加密完整工程。资源共134个文件压缩包409KB以8个Verilog源文件为核心覆盖DES轮函数、S盒、P盒、密钥生成等模块辅以mif初始化文件、qsf/qpf工程配置、rdb/rpt仿真报告与readme说明可帮助理解FPGA综合流程与模块化设计方法。已有1063人学习下载。借助该资源可掌握对称加密算法到硬件逻辑的转化思路了解初始置换、逆置换、16轮迭代及子密钥调度在FPGA上的实现细节同时可作为调试与二次开发的参考模板适合想将DES算法落地到可编程逻辑器件的入门至进阶人员。 打开EDA工具新建一个Verilog文件把DES写进去——这件事我在三个项目里干过。第一次是毕业设计凑数第二次是帮同事做遗留通信接口的硬件加速第三次是为了给团队写内部培训材料。如果你也想在FPGA上跑一个不是流水账般“点灯”的算法项目基于Verilog的DES加密算法是个被低估的选择它有明确的标准文档、有现成的测试向量、有经典的S盒和置换表写完之后还能直接对比软件参考代码验证几乎每一步都有章可循。很多人一听DES就觉得过时了毕竟AES早就是默认选项。但DES在硬件实现层面有它独特的教学价值每一轮运算量不大但层次分明既有大量的位重排又有适合LUT结构的S盒查表还有密钥调度这种带状态的逻辑。把DES用Verilog写完一遍你对组合逻辑、时序控制、模块划分、仿真验证的理解都会巩固不少。这篇内容主要给正在学Verilog、或者准备把加密算法搬到FPGA上的读者我会把我实际写代码时的思路、模块划分、仿真验证方法以及踩过的几个比较典型的坑一次性讲清楚。1. 为什么现在还要把DES搬到FPGA上1.1 DES还活着主要活在兼容性场景里先别急着说DES老。在很多工业设备、金融终端、旧通信协议里3DES仍然被用于数据完整性保护和链路加密尤其是一些强制要求兼容旧系统的场景。虽然它的安全性在今天看来已经不够强但把它作为“遗留算法”去加速或替换依然是不少FPGA项目的实际需求。你写的不一定是一个“面向未来的加密核”可能只是一个必须和其他旧设备对齐的接口模块。另外DES的算法结构是学习对称加密硬件实现的极好样本。它没有AES那种复杂的列混合和有限域运算也没有分组链接模式下太多需要额外处理的数学概念。DES就是“初始置换、16轮轮函数、逆初始置换”三条主线轮函数里再做“扩展、异或、S盒、P置换”。这让它很适合用Verilog从头到尾手写一遍因为每一个环节都能在纸上画出来跟代码能一一对应。1.2 学DES是理解硬件数据通路的捷径我经常跟新人说如果你能独立写完一个能通过标准测试向量的DES模块你对FPGA开发的核心感觉基本就建立起来了。原因是它会逼你去处理好几个硬件问题数据位宽和位序DES里到处都是64位到48位、48位到32位的变化每个位置都要精确对应一旦搞错一位密文就差十万八千里。组合逻辑与寄存器的边界轮函数里的置换和S盒都是纯组合逻辑但轮与轮之间必须用寄存器锁存。状态机设计16轮循环控制、密钥加载、数据加载、输出有效信号这本身就是一个典型的状态机。仿真验证DES有非常明确的标准测试向量这比很多“看起来能跑但不知道对不对”的项目要靠谱得多。所以即便你不做密码学方向我仍然建议把DES当作一个练手项目。它比FIFO、UART这类基础模块更能训练你对“数据流”的整体把控。2. 从算法到硬件DES在FPGA眼里就是“换线、查表、异或”2.1 初始置换和逆置换本质是连线DES对64位明文做的第一件事是初始置换IP把输入的各个比特按照固定表格重新排列加密完成后再做逆初始置换IP⁻¹。这两个置换在FPGA里几乎不消耗逻辑资源因为它们不是运算而是“布线”。你只要把输入的第某一位接到输出的第某一位上综合工具就会把它变成导线或者查找表映射。这对Verilog实现特别友好。你可以用一个函数把这64位的映射写清楚也可以直接用连续赋值逐位搬移。很多初学者会以为置换需要使用循环或者复杂的数据结构其实不用。在硬件里置换表的本质就是告诉你“输出位i来自输入位table[i]-1”仅此而已。这里要注意一个细节标准文档里的表通常从1开始计数而Verilog的数组索引从0开始所以写代码时要统一转换。我习惯在内部函数里先把索引减1避免每写一行都反复纠结“要不要减一”。2.2 轮函数扩展、S盒、P置换DES的16轮运算每轮都做同样的事。右侧32位数据R先经过扩展置换E变成48位然后与本轮子密钥K做异或得到48位结果。这个48位被分成8个6位块分别送入S_1到S_8这8个S盒每个S盒输出4位合起来又是32位。这32位再经过P置换最后与左侧32位L做异或得到新的R旧的R则直接成为新的L。从硬件角度拆开看这里只有三种基本操作扩展置换把32位按表扩展成48位本质是布线。S盒6位输入查表得到4位输出最适合在FPGA里用LUT或者小块ROM实现。8个S盒相互独立完全可以并行。P置换又是固定查表式的布线。所以轮函数从结构上说是一个组合逻辑深度不算太深、但并行度很高的数据通路。相比软件写法一比特一比特地移位、查表硬件写法天然适合把8个S盒并排摆开一个时钟周期就算完一整轮。2.3 子密钥调度PC-1/PC-2和循环移位DES的另一个核心是子密钥生成。64位密钥先经过PC-1置换去掉8个校验位并重排得到56位。这56位分成C、D两半各28位。每一轮根据轮数对C、D分别循环左移1位或2位然后把移位结果拼成56位再经过PC-2置换选出48位作为本轮子密钥。硬件实现时建议把C、D各放在一个28位寄存器里轮循环时按移位表对这两个寄存器做循环左移每轮移完立即取出去做PC-2。这里有个常见的优化点如果子密钥不在加密过程中实时算而是提前全部算好存入寄存器数组那么轮函数里的关键路径会短很多。但代价是多了一堆寄存器。我在迭代架构里选择每轮实时计算因为一个时钟周期算一个移位并不占用多少路径关键是状态机逻辑要正确。2.4 为什么这种结构对硬件友好AES的S盒是8输入8输出DES的S盒是6输入4输出规模小一轮查表逻辑也更轻。DES的轮函数没有乘法没有有限域求逆几乎所有运算都是“选择某个位放到某个位置”和“查表”。这在现代FPGA里几乎可以全部映射到6输入LUT上不需要额外的DSP或Block RAM。比如一个S盒6位输入4位输出用LUT实现非常自然综合工具会把查表逻辑折叠成几级LUT根本不需要例化ROM。如果你要在FPGA上实现一个加密模块但没有太多资源预算DES这种“小查表布线”的结构其实是比较容易写好时序的。3. 顶层设计怎么搭我选择迭代架构而不是全展开3.1 两种架构的取舍DES的16轮可以做成两种顶层结构一种是“迭代架构”16轮共用同一个轮函数硬件用一个计数器循环执行16次另一种是“全展开/流水线架构”把16个轮函数模块全部例化出来数据像流水线一样依次通过。我自己的项目里选了迭代架构。原因很简单面积小、状态清晰、debug方便。一轮轮函数大概只需要几百个LUT16轮如果全展开虽然吞吐率高但资源会线性增长到几千个LUT还要处理每一级之间的寄存器同步。对于我这种把DES当通信辅助模块用的场景单包加密延迟多几个周期根本不是问题但省下来的资源可以用在其他地方。如果以后真要做高速密文流可以在这个迭代版本基础上改为“多级复制”或者“流水线展开”但不会一上来就写全展开版本因为调试起来非常痛苦。3.2 端口和状态机顶层模块的端口我大致是这样定义的module des_top ( input wire clk, input wire rst_n, input wire start, input wire [63:0] key, input wire [63:0] data_in, output reg [63:0] data_out, output reg valid );state和round是两个核心寄存器。状态机至少需要这些状态IDLE等待、LOAD_KEY加载密钥、LOAD_DATA加载明文、ENCRYPT执行16轮、DONE输出。我在ENCRYPT里用一个3位或4位计数器轮数round_cnt从0数到15每轮上升沿锁存一次L/R。16轮完成之后进入DONE拉高valid数据在下一个周期输出。这里给新手提个醒DES标准文档里写的轮数是“十六轮”但很多实现细节不会告诉你第16轮之后要做一个L/R交换。也就是说最后输出的“预逆置换输入”其实是{R16, L16}而不是{L16, R16}。顶层状态机在输出时必须把这两个32位交换一次否则结果跟你用软件算出来的会对不上。3.3 L/R数据通路与轮计数逻辑我内部的轮函数是这么组织的reg [31:0] L; reg [31:0] R; wire [31:0] f_out; wire [47:0] subkey; always (posedge clk or negedge rst_n) begin if (!rst_n) begin L 32h0; R 32h0; end else if (load_data) begin L after_ip[63:32]; R after_ip[31:0]; end else if (round_en) begin L R; R L ^ f_out; end endf_out是由R和subkey组合逻辑计算出来的32位结果。注意这里L和R的更新会在同一个周期完成所以组合逻辑f_out输入的是这一个周期R寄存器里的值更新后的R下一个周期才锁存。组合逻辑可能比较长扩展置换→异或子密钥→S盒→P置换。只要关键路径能满足时序要求就不用拆寄存器。如果频率上不去可以考虑把S盒输出后再打一拍代价是轮计数逻辑要跟着调整。4. 核心RTL实现置换表、S盒和轮函数4.1 置换的Verilog写法DES里各种置换表的本质都是“输出位i 输入位table[i]-1”。Verilog里最简单的写法是用for循环配合常量数组。以初始置换IP为例function [63:0] ip_perm; input [63:0] data; integer i; begin for (i 0; i 64; i i 1) ip_perm[i] data[IP_TABLE[i]-1]; end endfunctionIP_TABLE是模块内定义的integer数组或者localparam拼接形式。仿真时这种写法可读性很高综合工具也能把它映射成纯连线。如果你害怕函数里的循环综合不了也可以写成连续赋值的组合always块reg [63:0] after_ip; always (*) begin for (i 0; i 64; i i 1) after_ip[i] data_in[IP_TABLE[i]-1]; end两种写法都行本质没有区别。扩展置换E、P置换、PC-1、PC-2、IP⁻¹都照这个套路写。4.2 S盒的两种实现与综合选型S盒是DES里唯一有“查表”语义的地方。实现方式无非两种一种是把每个S盒写成单独的case语句一种是用二维数组初始化。我在第一次写的时候用的是case语句module sbox1 ( input wire [5:0] din, output reg [3:0] dout ); always (*) begin case (din) 6d0 : dout 4d14; 6d1 : dout 4d4; // ... 其余64项 default: dout 4d0; endcase end endmodulecase语句的好处是逻辑清晰综合工具会自动识别成LUT不会意外生成大ROM。缺点是每个S盒要写一个模块8个S盒就是8段差不多的case代码会显得冗长。后面我改用二维数组了reg [3:0] sbox_mem [0:7][0:63]; always (*) begin dout sbox_mem[sbox_idx][din]; end这种写法本质上也是ROM但代码量小很多适合把S盒表放在顶层用一个initial块批量初始化。实际综合时工具会按访问逻辑把它映射成LUT或分布式RAM效果不会比case差。我更推荐这种写法尤其是当你需要把8个S盒统一管理时。这里有一个我自己踩过的坑DES的S盒行和列是“行取6位输入的首尾两位列取中间四位”。如果你把S盒表按标准表格中的行、列组织成二维表很多人容易把索引写成row*16col但如果直接按64项展开要注意标准表里排列的顺序。我后来干脆用64项的一维索引避免自己绕晕。4.3 轮函数和子密钥生成的组合时序轮函数的组合逻辑我打包成一个函数或者一个组合always块来写。关键是子密钥要和轮数对齐。子密钥生成我做了独立的寄存器reg [27:0] c_reg; reg [27:0] d_reg; wire [47:0] subkey; always (posedge clk or negedge rst_n) begin if (!rst_n) begin c_reg 28h0; d_reg 28h0; end else if (load_key) begin c_reg after_pc1[55:28]; d_reg after_pc1[27:0]; end else if (round_en) begin case (round_cnt) 4d0, 4d1, 4d8, 4d15: begin c_reg {c_reg[26:0], c_reg[27]}; d_reg {d_reg[26:0], d_reg[27]}; end default: begin c_reg {c_reg[25:0], c_reg[27:26]}; d_reg {d_reg[25:0], d_reg[27:26]}; end endcase end end这个左移表是DES标准规定的第1、2、9、16轮左移1位其余左移2位。注意c_reg和d_reg在load_key和round_en同一个周期不能同时触发否则密钥还没加载就开始移位了。设计状态机时LOAD_KEY和ENCRYPT必须严格分开。subkey则是从c_reg、d_reg实时组合出来的assign subkey pc2_perm({c_reg, d_reg});这样每轮移位完成后下一轮使用新的子密钥。时序上没问题。5. 仿真验证和工程落地的坑5.1 用经典测试向量把加密过程锁死DES有个特别著名的标准测试例子密钥是0x133457799BBCDFF1明文是0x0123456789ABCDEF加密结果是0x85E813540F0AB405。我第一次调通模块时就靠这一组向量反复比对。但只测一组不够。建议你在testbench里至少准备三组全零密钥加密全零明文看内部寄存器状态是否符合预期。标准向量用来确认整体功能。自己用Python或openssl算几组随机密钥和明文比对密文是否一致。我个人的习惯是先用Python写一个几十行的小脚本输出一组随机向量到文本文件然后testbench从文件读入逐组比较。这样比在波形图里肉眼找错误高效得多。5.2 testbench的结构和自动比对testbench里最关键的是“对比”。我写了一个简单的自检逻辑always (posedge clk) begin if (valid) begin if (data_out ! expected_cipher) $display(Mismatch! round%0d exp%h got%h, round_cnt, expected_cipher, data_out); else $display(Pass: %h - %h, data_in, data_out); end end如果是在Modelsim或者Vivado Simulator里跑这样能在终端直接看到哪组向量失败。改代码时不用反复打开波形窗口去查颜色确实省了很多时间。仿真时要注意初始置换和逆初始置换的位序还有函数和数组索引的一致性。我经常发现仿真通过但上板失败原因往往是没有初始化寄存器或者复位时序不对。像这类模块建议在testbench里一上来就做一次复位再等几个周期再拉start。5.3 容易写错的两个地方第一个是最后一级的L/R交换。很多资料在伪代码里写“经过16轮后最后的输出需要交换左右两半再做逆初始置换”。你如果只盯着标准伪代码很容易漏掉这一步。我的做法是在DONE状态时先组一次{R_reg, L_reg}再对这个结果做IP⁻¹。很多实现教程也会把这一步称之为“最后一轮的特殊处理”。第二个是密钥的校验位。DES的64位密钥有8个校验位分别位于每字节的最高位PC-1会把这些位剔掉。如果你的代码是从某个开源C语言版参考代码抄过来的而它内部已经做了PC-1那么顶层输入的key就应该是包括校验位的64位原样数据。如果错把56位有效密钥当64位传进去后面结果永远不对。5.4 面积与优化方向的个人体会迭代架构实现下来在常见的Artix-7系列FPGA上8个S盒如果用LUT实现整个模块大概占用几百个到一千个LUT左右具体看综合策略和是否做了流水线寄存器。这个量级对于绝大多数FPGA项目都不算负担。如果你想优化性能方向有三个把16轮展开成流水线吞吐率接近每周期一组数据。把S盒换成Block RAM或LUTRAM能省一点LUT但会增加读取延迟。对密钥调度做预计算在加载密钥时把16轮子密钥全部算出并锁存这样加密路径上少一级移位组合逻辑。个人经验是如果是普通的指令级控制或通信协议数据加密迭代架构加一个状态机就够了别一上来就追求高吞吐。先保证功能正确再根据时序报告决定要不要流水线化。6. 一点实际操作层面的建议如果你现在正准备在Verilog里实现DES加密算法我最后给你几个可以少走弯路的建议。第一先建好参考模型再写RTL别边写边对表。我建议你先用Python把DES标准算法完整跑通这样RTL里的每一张表、每一个中间变量都能和软件参考逐位对照。第二S盒和置换表不要手打很容易抄错。能找现成标准表就复制别手工输入。第三仿真通过不等于上板正确如果你的模块最终要接其他逻辑记得把valid信号同步到下游别让下游在数据还没稳定时就采数。DES不是一个“新”算法但把它用Verilog重新实现一遍你会经历从算法理解到硬件映射的完整链路。这个项目做完之后再回头看AES、SM4甚至其他密码算法你会有一种“这些结构我之前见过”的感觉。希望这篇分享能帮你少踩几个坑。本文还有配套的精品资源点击获取