资讯动态

FPGA实战:Verilog DES加密算法从仿真到上板全流程

发布时间:2026/10/21 15:15:33 来源:尧图企业网站定制
简介这是一份面向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 轮 Feistel64 位明文经过初始置换 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 28d0; d_reg 28d0; 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) 6d0 : dout 4d14; 6d1 : dout 4d4; 6d2 : dout 4d13; // ... 中间省略完整表共 64 项 6d63 : dout 4d7; default: dout 4d0; 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 64h133457799BBCDFF1; key_load 1; (posedge clk); key_load 0; // 等待密钥调度完成 repeat(20) (posedge clk); // 输入明文 plain_in 64h0123456789ABCDEF; data_valid 1; (posedge clk); data_valid 0; // 等待加密完成 wait(out_valid); (posedge clk); if (cipher_out 64h85E813540F0AB405) $display(PASS: cipher %h, cipher_out); else $display(FAIL: cipher %h, expected 85E813540F0AB405, cipher_out); #100 $finish; end endmoduletestbench 里几个关键点时钟周期设为 10ns 对应 100MHzkey_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 模块都会强制走一遍参数扫描加多向量仿真确认每种配置下的密文都对得上标准向量。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑