简介:这是一份面向FPGA开发者与数字逻辑学习者的Verilog版DES加密算法实现工程,适合希望深入理解对称加密硬件加速、提升嵌入式安全设计能力的读者。压缩包共134个文件,约409KB,以v源文件、mif存储器初始化文件、qsf与qpf工程配置、cdb与hdb综合数据库、rpt报告及txt说明为主,涵盖DES核心模块、轮函数与S盒、仿真测试平台、顶层接口及综合实现产物,目录结构完整,便于直接复现与二次开发。资源已有1072人学习下载,热度较高。通过研读源码与仿真文件,读者可掌握初始置换、密钥调度、逆初始置换等关键步骤的Verilog描述方法,理解FPGA并行处理对加密吞吐的提升,并借助综合报告与配置文件快速完成工程搭建、功能验证与性能优化,为网络通信、安全存储等场景的硬件加密设计提供可参考的完整范例。
1. 从一次时序违例说起:为什么我要把 DES 塞进 FPGA
去年帮一个做高速接口的朋友看板子,他拿一个现成的 DES IP 核跑 50MHz,功能仿真全绿,上板后串口吐出来的密文每隔几帧就错一个字节。抓了两天波形才发现,问题不在算法,在密钥调度里那组移位寄存器被综合工具优化成了分布式 RAM,读改写撞上了建立时间违例。这件事让我重新翻出自己早年写的那套纯 Verilog DES 实现,从头到尾捋了一遍流水线和密钥路径。DES 本身不复杂,16 轮 Feistel 结构,教科书半页纸就能讲完,但真要在 FPGA 上跑稳、跑快、跑得省资源,选型、流水级数、S 盒映射方式、密钥调度策略,每一个都是分叉口。这份资源就是一套完整的 Verilog DES 加密算法实现,覆盖从轮函数到顶层封装的完整链路,适合正在做 FPGA 加解密模块、需要把 DES 落地到实际工程里的从业者。不管你是要拿它当 IP 核集成,还是想拆开看每一轮怎么打拍,都能直接跑仿真、上板验证。
2. DES 的硬件拆解:轮函数、密钥调度与流水线怎么摆
2.1 Feistel 结构在 Verilog 里的映射方式
DES 的加密流程是标准的 16 轮 Feistel:64 位明文经过初始置换 IP,拆成左右各 32 位,每轮用轮函数 F 处理右半部分,再与左半部分异或,然后左右交换。最后一轮不交换,再过逆初始置换 IP⁻¹ 得到密文。软件实现里这就是一个循环,硬件里则要决定:是做成单轮迭代复用,还是展开成 16 级流水线。
单轮复用的写法资源占用小,一个轮函数模块反复调用 16 次,配合一个状态机控制轮计数。代价是吞吐率低,跑一次加密至少 16 个时钟周期,加上密钥调度和前后置换,实际得 20 个周期以上。如果系统时钟 100MHz,吞吐也就 5MB/s 左右,适合低速场景。
流水线展开则是把 16 轮全部实例化,每轮之间插寄存器打拍。数据像流水一样每周期推进一级,理想情况下每个时钟周期都能吐出一个结果。资源换速度,逻辑面积大概翻十几倍,但在高速接口场景里这是唯一选择。我一般会先确认目标吞吐,再决定展开级数。折中方案是部分展开,比如 4 级流水,每级内部迭代 4 轮,面积和速度取中间值。
// 单轮 Feistel 核心:F 函数 + 异或 + 左右交换 module des_round ( input wire [31:0] L_in, input wire [31:0] R_in, input wire [47:0] subkey, output wire [31:0] L_out, output wire [31:0] R_out ); wire [31:0] f_result; // F 函数:扩展置换 E -> 与子密钥异或 -> S 盒替换 -> P 置换 des_f_function u_f ( .R (R_in), .subkey (subkey), .f_out (f_result) ); assign L_out = R_in; // 左输出直接取右输入 assign R_out = L_in ^ f_result; // 右输出 = 左输入异或 F 结果 endmodule这段代码是轮函数的骨架。L_in和R_in是当前轮的左右半区,subkey是这一轮对应的 48 位子密钥。注意L_out直接等于R_in,这是 Feistel 结构的特征——左半区不需要做任何运算,只是把右半区搬过来。R_out才是真正参与运算的部分。参数上,子密钥宽度固定 48 位,因为扩展置换 E 把 32 位扩成 48 位后要与子密钥异或。如果你把子密钥位宽写错,综合不会报错,但仿真结果会全错,这种位宽不匹配是新手最常见的翻车点之一。
2.2 密钥调度:16 组子密钥怎么生成、怎么存
DES 的密钥调度从 64 位原始密钥出发,先过 PC-1 置换去掉 8 个校验位,得到 56 位有效密钥,拆成左右各 28 位。然后根据轮数做循环左移,每轮移位数不同——第 1、2、9、16 轮移 1 位,其余轮移 2 位。移位后的 56 位再过 PC-2 置换压缩成 48 位子密钥。
硬件实现有两种常见做法。第一种是实时计算:每轮开始前用移位寄存器算出当前子密钥,用完即弃。省存储,但移位逻辑要跟着轮计数走,控制稍复杂。第二种是预计算存储:上电或密钥更新时一次性算出 16 组子密钥,存进一个 16×48 的寄存器阵列或分布式 RAM,加密时直接按轮号索引。这种方式时序更干净,代价是多占一些寄存器资源。
我一般推荐预计算方案,尤其是流水线结构下。因为流水线每拍都在推进,实时计算子密钥的移位逻辑很难跟流水节奏对齐,容易引入额外的组合逻辑延迟。预计算之后,子密钥读取就是一次寄存器索引,路径短、时序好收敛。
// 密钥调度:预计算 16 组 48 位子密钥 module des_key_schedule ( input wire clk, input wire rst_n, input wire key_load, input wire [63:0] key_in, output wire [47:0] subkey [0:15] // 16 组子密钥 ); reg [27:0] c_reg, d_reg; reg [47:0] subkeys [0:15]; integer i; // PC-1 置换后的左右半区 wire [27:0] c_init, d_init; des_pc1 u_pc1 (.key_in(key_in), .c_out(c_init), .d_out(d_init)); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin c_reg <= 28'd0; d_reg <= 28'd0; end else if (key_load) begin c_reg <= c_init; d_reg <= d_init; // 逐轮移位并生成子密钥 for (i = 0; i < 16; i = i + 1) begin // 根据轮号决定移位位数 if (i == 0 || i == 1 || i == 8 || i == 15) begin c_reg <= {c_reg[26:0], c_reg[27]}; d_reg <= {d_reg[26:0], d_reg[27]}; end else begin c_reg <= {c_reg[25:0], c_reg[27:26]}; d_reg <= {d_reg[25:0], d_reg[27:26]}; end subkeys[i] <= des_pc2(c_reg, d_reg); end end end assign subkey = subkeys; endmodule这段密钥调度代码用的是预计算思路。key_load拉高时,PC-1 置换结果载入c_reg和d_reg,然后在一个循环里逐轮移位、逐轮生成子密钥存入subkeys数组。注意移位分支的判断条件:第 1、2、9、16 轮(对应索引 0、1、8、15)移 1 位,其余移 2 位。这个轮次表是 DES 标准固定的,写错任何一轮,密文就完全对不上。des_pc2是一个组合逻辑模块,把 56 位压缩成 48 位。实际综合时,这个 for 循环会被展开成 16 级级联逻辑,如果时序紧张,可以在每级之间插入寄存器做流水。
2.3 S 盒的 FPGA 映射:查找表还是逻辑门
S 盒是 DES 里最“非线性”的部分,8 个 S 盒,每个把 6 位输入映射成 4 位输出。软件里就是查表,硬件里则有几种做法。
最直接的是用 Verilog 的 case 语句描述真值表,综合工具会自动推断成 LUT。FPGA 的 LUT4 或 LUT6 天然适合这种小规模查找表,一个 6 输入 4 输出的 S 盒大概占几个 LUT。8 个 S 盒并行就是几十个 LUT,资源可控。
另一种做法是用 Block RAM 存 S 盒表,地址线接 6 位输入,数据线出 4 位输出。这种方式在 S 盒数量多、或者需要动态切换算法时有用,但 DES 只有 8 个固定 S 盒,用 BRAM 反而浪费——BRAM 的读延迟至少一拍,会打乱流水线节奏。
还有一种偏门做法是用逻辑门直接实现 S 盒的布尔表达式,理论上面积更小,但表达式极其冗长,可读性差,维护成本高。我一般不建议,除非你在做极致的面积优化且愿意花时间验证。
// 单个 S 盒的 LUT 实现(以 S1 为例) module sbox1 ( input wire [5:0] addr, output reg [3:0] dout ); always @(*) begin case (addr) 6'd0 : dout = 4'd14; 6'd1 : dout = 4'd4; 6'd2 : dout = 4'd13; // ... 中间省略,完整表共 64 项 6'd63 : dout = 4'd7; default: dout = 4'd0; endcase end endmoduleS 盒用 case 语句写,综合工具会把它映射成 LUT 网络。addr是 6 位输入,dout是 4 位输出。每个 S 盒 64 项,8 个 S 盒就是 512 项。写的时候建议用脚本从标准表生成 Verilog 代码,手敲容易出错。注意default分支要写上,虽然 6 位地址天然覆盖 0~63,但加上 default 能避免综合工具推断出锁存器。另外,如果综合报告显示 S 盒占了大量 LUT,可以考虑把多个 S 盒的输出编码合并,减少选择逻辑。
3. 从仿真到上板:一套可复现的验证流程
3.1 搭建 testbench:明文、密钥、期望密文怎么对齐
写 testbench 的核心是找到一组标准测试向量。DES 有一组经典的已知答案测试:明文0x0123456789ABCDEF,密钥0x133457799BBCDFF1,加密结果应该是0x85E813540F0AB405。这组向量在无数教材和标准文档里出现过,拿来验证最方便。
testbench 的结构一般是:例化顶层模块,给时钟和复位,在初始块里依次施加密钥加载、明文输入、启动加密,然后等待若干周期后检查输出。关键是要给够等待时间——如果是单轮迭代结构,至少等 20 个周期;如果是 16 级流水线,第一个结果在第 16 拍出来,之后每拍一个。
`timescale 1ns/1ps module tb_des; reg clk, rst_n; reg key_load, data_valid; reg [63:0] key_in, plain_in; wire [63:0] cipher_out; wire out_valid; des_top u_dut ( .clk (clk), .rst_n (rst_n), .key_load (key_load), .key_in (key_in), .data_valid (data_valid), .plain_in (plain_in), .cipher_out (cipher_out), .out_valid (out_valid) ); // 时钟生成 initial clk = 0; always #5 clk = ~clk; // 100MHz initial begin rst_n = 0; key_load = 0; data_valid = 0; #100 rst_n = 1; // 加载密钥 key_in = 64'h133457799BBCDFF1; key_load = 1; @(posedge clk); key_load = 0; // 等待密钥调度完成 repeat(20) @(posedge clk); // 输入明文 plain_in = 64'h0123456789ABCDEF; data_valid = 1; @(posedge clk); data_valid = 0; // 等待加密完成 wait(out_valid); @(posedge clk); if (cipher_out === 64'h85E813540F0AB405) $display("PASS: cipher = %h", cipher_out); else $display("FAIL: cipher = %h, expected = 85E813540F0AB405", cipher_out); #100 $finish; end endmoduletestbench 里几个关键点:时钟周期设为 10ns 对应 100MHz,key_load只拉高一拍就够,因为密钥调度是并行加载的。等待 20 个周期是为了让预计算的子密钥全部写入寄存器阵列。wait(out_valid)是等输出有效信号,比固定延时更可靠。最后用===做全等比较,避免 x 态被误判为通过。如果仿真报 FAIL,先检查密钥和明文有没有接反,再检查子密钥轮次表有没有写错。
3.2 综合与实现:时序约束、资源报告怎么看
仿真过了不代表上板能跑。综合阶段要关注几个东西:一是时序报告里的 WNS(最差负裕量),如果是负数说明有时序违例,得降频或者插流水;二是资源利用率,看 LUT、寄存器、BRAM 各占多少;三是综合警告,尤其是位宽不匹配和锁存器推断的警告,不能忽略。
时序约束文件里至少要定义时钟周期。如果系统时钟 100MHz,周期就是 10ns。输入输出延迟根据外围器件手册填。对于 DES 这种纯组合逻辑加寄存器的设计,关键路径通常在 S 盒输出到下一级寄存器之间,或者密钥调度的移位链上。如果 WNS 不达标,优先在关键路径中间插寄存器,而不是盲目降频。
资源方面,一个单轮迭代的 DES 大概占几百个 LUT 和几百个寄存器,16 级流水线则可能到几千 LUT。具体数字取决于综合工具和器件系列。如果资源超了,先看 S 盒是不是被推断成了分布式 RAM,再检查有没有冗余的移位寄存器被综合成 BRAM。
3.3 上板调试:怎么确认密文是对的
上板后最直接的办法是拿一组已知向量跑一遍,用逻辑分析仪或者嵌入式逻辑分析器抓输出。如果密文不对,按这个顺序排查:先确认时钟频率没超,再看复位有没有正确释放,然后检查密钥加载时序——很多翻车是因为密钥还没稳定就启动了加密。如果用了流水线结构,还要确认数据有效信号和流水延迟对齐,别在第一个结果还没出来的时候就采样。
另一个常见问题是输入数据没有同步到时钟域。如果明文来自异步接口,必须先过两级寄存器做同步,否则亚稳态会导致偶发错误。这种错误在仿真里很难复现,但上板后每隔几帧错一次,非常折磨人。
4. 避坑指南:DES 硬件实现里最容易翻车的五个点
4.1 现象:仿真通过但上板密文随机出错
原因:最常见的是跨时钟域没做同步。明文或密钥来自异步接口,直接打进 DES 模块,亚稳态导致某些位随机翻转。另一个可能是时序违例,综合报告里 WNS 为负但被忽略了。
解决:所有异步输入先过两级触发器同步,再送入加密模块。综合后必看时序报告,WNS 为负就插流水或降频。上板前跑一遍长时间连续加密测试,看错误是否随时间累积。
4.2 现象:密文完全不对,但仿真波形看起来正常
原因:子密钥轮次表写错。DES 的移位轮次是固定的,第 1、2、9、16 轮移 1 位,其余移 2 位。如果写成统一移 1 位或统一移 2 位,仿真时可能因为测试向量碰巧不对而没发现,但换一组数据就暴露。
解决:对照标准轮次表逐轮核对移位分支条件。建议在 testbench 里加多组测试向量,不要只跑一组。
4.3 现象:综合报告显示大量锁存器被推断
原因:组合逻辑的 case 或 if 分支没有覆盖所有条件,综合工具推断出锁存器来保持原值。DES 的 S 盒 case 语句如果漏了 default,或者密钥调度的 if-else 不完整,都会触发。
解决:所有组合逻辑的 case 加 default 分支,if-else 补全 else。综合后检查警告列表,看到 latch inferred 就回去改代码。
4.4 现象:流水线结构下第一个输出延迟不对
原因:流水级数和输出有效信号的延迟没对齐。16 级流水线意味着数据要经过 16 个时钟周期才出来,如果 out_valid 只延迟了 15 拍或者 17 拍,采样时机就错了。
解决:用一个移位寄存器链跟踪 data_valid,每一级流水对应一拍延迟。仿真时打印每拍的有效信号,确认和数据对齐。
4.5 现象:资源利用率远超预期
原因:S 盒被综合成了 Block RAM 而不是 LUT,或者密钥调度的移位寄存器被推断成分布式 RAM。另外,如果代码里用了大量乘法器或大位宽加法器,也会吃掉 DSP 资源。
解决:检查综合报告里的资源明细,确认 S 盒映射方式。如果是 BRAM,改回 case 语句实现。密钥调度尽量用寄存器而非数组,避免被推断成 RAM。
5. 进阶技巧:用参数化设计让 DES 模块适配不同场景
这套代码如果只写死一种结构,换个项目就得重写。我后来把它改成了参数化设计,用parameter控制流水级数和是否启用预计算密钥调度。这样同一个模块,低速场景用单轮迭代,高速场景展开成 16 级流水,综合前改一个参数就行。
module des_top #( parameter PIPELINE_STAGES = 1, // 流水级数:1=单轮迭代,16=全流水 parameter PRECOMPUTE_KEY = 1 // 1=预计算子密钥,0=实时计算 ) ( input wire clk, input wire rst_n, input wire key_load, input wire [63:0] key_in, input wire data_valid, input wire [63:0] plain_in, output wire [63:0] cipher_out, output wire out_valid ); // 根据 PIPELINE_STAGES 生成对应级数的流水线 generate if (PIPELINE_STAGES == 1) begin : gen_iterative // 单轮迭代实现 end else begin : gen_pipeline // 多级流水实现 end endgenerate endmodulePIPELINE_STAGES控制流水级数,PRECOMPUTE_KEY控制密钥调度方式。generate块根据参数选择不同的实现路径。这种写法在综合时只会展开选中的分支,不会浪费资源。参数说明:PIPELINE_STAGES设为 1 时是面积优先,设为 16 时是速度优先,中间值可以做折中。PRECOMPUTE_KEY在流水线模式下建议设为 1,迭代模式下可以设为 0 省寄存器。
验证参数化设计时,我一般会跑一个参数扫描:固定测试向量,分别用 PIPELINE_STAGES = 1、4、16 综合一遍,对比资源报告和时序报告,确认每种配置都能通过仿真。这样交付给不同项目时,直接改参数重新综合就行,不用动逻辑代码。
从那以后我每次交付 DES 模块,都会强制走一遍参数扫描加多向量仿真,确认每种配置下的密文都对得上标准向量。希望帮到你。
本文还有配套的精品资源,点击获取