Verilog硬件实现DES加解密:从算法原理到FPGA工程实践

发布时间:2026/7/17 23:07:45
Verilog硬件实现DES加解密:从算法原理到FPGA工程实践 1. 项目概述与核心价值最近在做一个需要硬件加速加密的项目选型时又绕不开经典的DES算法。虽然现在AES是主流但在一些对兼容性有要求、或者资源受限的FPGA/ASIC场景里DES及其变体比如3DES依然有它的用武之地。网上关于DES软件实现的资料一抓一大把但用Verilog在硬件层面实现一个完整DES加解密模块的、能直接照着写的教程却不多很多要么只讲原理要么代码不完整调试起来一头雾水。这个教程的目的就是把手把手带你用Verilog实现一个从明文输入到密文输出反之亦然的完整DES加解密引擎。我们不止是把代码写出来更要搞清楚每一行代码背后的硬件电路是怎么工作的比如初始置换IP怎么用组合逻辑实现16轮Feistel结构如何用状态机优雅地控制以及最后的逆置换IP-1又该如何收尾。我会把整个设计拆解成清晰的数据通路和控制单元并提供可综合的代码。无论你是正在学习数字电路设计的学生还是需要快速在FPGA上验证加密功能的工程师这篇内容都能给你一个扎实的起点和可复用的框架。2. DES算法原理与硬件映射考量在动手写代码之前我们必须吃透DES算法的核心并思考如何将它“翻译”成高效的硬件电路。DES是一种对称分组加密算法密钥和加解密流程对称分组长度为64位密钥长度56位外加8位奇偶校验位实际输入64位。它的核心是Feistel网络结构这个结构有一个绝佳的特性加解密过程可以使用几乎相同的硬件只是子密钥的使用顺序相反这在硬件实现上能节省大量资源。2.1 Feistel网络结构与硬件流水线思想DES的16轮操作构成了一个Feistel网络。每一轮的操作可以概括为L_i R_{i-1}R_i L_{i-1} XOR F(R_{i-1}, K_i)其中L和R分别是64位数据的左32位和右32位K_i是第i轮的子密钥F是轮函数。对于硬件实现最直观的想法是能不能用16级流水线来实现每一级寄存器存储当前轮的L和R同时组合逻辑计算F函数和异或下一时钟周期自动进入下一轮。这样理论上每个时钟周期都能吞入一个新的数据块吞吐量极高。但代价是面积你需要16套F函数的逻辑和相应的寄存器。在资源紧张的FPGA上这可能不现实。更常见的折中方案是单轮迭代设计只实现一套F函数的组合逻辑和一个轮状态寄存器。通过一个状态机控制让同一个硬件逻辑重复运行16次完成一个数据块的加解密。这样面积最小但吞吐量也最低完成一个块需要至少16个时钟周期加上初始和最终处理。我们这个教程将采用这种面积最优的设计因为它最通用也最容易理解。理解了单轮迭代你自然就能扩展到多轮并行或流水线设计。2.2 核心运算单元的硬件实现选择DES算法中包含几种关键运算置换Permutation、扩展置换Expansion、替代Substitution通过S盒和异或XOR。在Verilog中我们需要为每一种运算选择最合适的实现方式。置换操作如IP, IP-1, PC-1, PC-2, P盒这本质上是一个固定的位序重排。在硬件里这不消耗任何逻辑资源只是连线的艺术。在Verilog中我们直接用assign语句进行位拼接即可。例如初始置换IP表有64个条目每个条目指明输出位对应输入位的索引。我们可以这样实现// 假设 input_data 是64位输入明文 wire [63:0] ip_out; assign ip_out {input_data[6], input_data[14], input_data[22], ... , input_data[25]}; // 严格按照IP表拼接注意虽然代码看起来是一行赋值但综合后就是一组固定的连线。扩展置换E将32位的右半部分R扩展为48位。同样属于固定位映射用assign和位拼接实现。注意扩展操作有重复位这是为了与48位子密钥进行异或。S盒替代这是DES安全性的核心也是硬件设计的重点。每个S盒将6位输入映射为4位输出。千万不要用查找表LUT的思维去写case语句一个S盒有64种输入情况写出来冗长且不易维护。更专业的做法是将S盒的真值表转化为组合逻辑表达式。我们可以利用综合工具的优化能力或者手动提取布尔表达式。一种清晰且可维护的方法是使用Verilog的function或always *块并在其中用case语句完整列出64种映射。对于现代综合工具这通常能被很好地优化成最简组合逻辑。// 以S盒1为例 function 实现 function [3:0] sbox1; input [5:0] addr; begin case(addr) 6‘h00: sbox1 4‘he; // 行addr[5]addr[0] 00, 列addr[4:1] 0000 6‘h01: sbox1 4‘h4; // ... 完整列出64项 6‘h3f: sbox1 4‘hd; default: sbox1 4‘h0; // 避免锁存器 endcase end endfunction在综合报告中你需要关注这部分逻辑是否被优化得足够小。异或运算Verilog中的^运算符直接对应硬件中的异或门这是最基础的逻辑门实现起来没有悬念。循环左移在密钥调度中每轮需要对56位密钥的两个28位半部分进行循环左移1位或2位。这可以用位拼接操作轻松实现{C[26:0], C[27]}就是将28位寄存器C循环左移1位。2.3 密钥调度模块的预先计算策略密钥调度是从初始56位密钥生成16个48位子密钥的过程。它包含置换选择PC-1、循环左移和置换选择PC-2。在单轮迭代设计中我们有两种策略实时计算每轮开始时根据当前轮数对密钥寄存器进行相应次数的循环左移然后经过PC-2产生当前子密钥。这需要额外的控制逻辑。预先计算在加解密开始前用一个初始化过程或单独的时钟周期提前计算出16个子密钥并存入一个长度为16的寄存器数组中。在后续的16轮中直接按顺序加密或逆序解密读取即可。我强烈推荐预先计算策略。原因有三首先它简化了轮运算过程的状态机控制每轮只需读取一个固定的子密钥其次它避免了在关键路径上加入密钥移位的逻辑有利于提高电路时序性能最后代码结构更清晰子密钥数组reg [47:0] subkey [0:15]一目了然。我们将在设计中使用这种方法。3. DES加解密模块的详细设计与实现现在我们开始搭建整个DES模块。我们将它划分为三个主要部分密钥调度单元、数据通路轮函数F和主控制状态机。模块的顶层接口将包括时钟、复位、加解密模式选择、数据输入/输出以及有效信号。3.1 顶层接口与模块划分首先定义顶层模块des_core的接口module des_core ( input wire clk, // 系统时钟 input wire rst_n, // 异步低电平复位 input wire start, // 启动信号高电平有效 input wire mode, // 0-加密1-解密 input wire [63:0] data_in, // 输入数据明文或密文 input wire [63:0] key_in, // 输入密钥64位含校验位 output reg [63:0] data_out, // 输出数据密文或明文 output reg ready, // 模块空闲可接收新任务 output reg valid // data_out输出有效 );ready信号指示模块是否可以接收新的start信号。当start和ready同时为高时模块锁存当前的data_in和key_in并开始计算。计算完成后valid信号拉高一个周期同时data_out上出现有效结果。内部我们将实例化几个子模块或逻辑块key_schedule密钥调度模块在收到启动信号后计算16个子密钥。round_function组合逻辑实现的轮函数F它接收32位的R和48位的子密钥输出32位结果。control_fsm控制状态机管理整个16轮迭代的流程包括初始置换、16轮循环、左右交换和逆置换。3.2 密钥调度模块的实现细节密钥调度模块key_schedule在启动信号有效时工作。它接收64位的key_in首先经过PC-1置换丢弃奇偶校验位得到56位有效密钥。然后根据DES标准这56位被分成两个28位的C0和D0。接下来需要根据轮数进行循环左移。DES标准规定了每轮的左移位数第1、2、9、16轮左移1位其余轮左移2位。我们可以用一个for循环在Verilog的always块中实现但注意这是用来生成组合逻辑或顺序逻辑的描述综合工具会展开循环。// 在key_schedule模块内部 reg [55:0] pc1_out; reg [27:0] C [0:16]; // 额外多一位C[0]为初始值 reg [27:0] D [0:16]; integer i; // PC-1置换 (示例需补全完整56位映射) always * begin pc1_out {key_in[6], key_in[14], ... , key_in[25]}; // 按PC-1表 {C[0], D[0]} pc1_out; // 拆分 end // 循环左移并生成CD组合 always (posedge clk or negedge rst_n) begin if (!rst_n) begin for (i0; i16; ii1) begin C[i] 28‘b0; D[i] 28‘b0; end end else if (key_schedule_start) begin // 内部启动信号 for (i1; i16; ii1) begin case(i) 1,2,9,16: begin // 左移1位 C[i] {C[i-1][26:0], C[i-1][27]}; D[i] {D[i-1][26:0], D[i-1][27]}; end default: begin // 左移2位 C[i] {C[i-1][25:0], C[i-1][27:26]}; D[i] {D[i-1][25:0], D[i-1][27:26]}; end endcase end end end // 为每一轮i通过PC-2置换从{C[i], D[i]}生成48位子密钥subkey[i-1] // PC-2也是一个固定的位选择置换 always * begin for (i1; i16; ii1) begin subkey[i-1] {CD_combined[某位], ...}; // 从{C[i], D[i]}构成的56位中按PC-2表选择48位 end end注意上面的for循环在always *中用于生成组合逻辑综合工具会将其完全展开。C和D数组被定义为reg并在时钟沿更新这意味着子密钥的预计算可以在几个周期内完成取决于你的设计可以是一个周期完成所有计算也可以是流水式。为了简化控制我们可以让密钥调度在一个周期内完成所有计算组合逻辑但要注意路径延迟。更稳妥的做法是用2-3个时钟周期完成密钥预计算然后再开始数据路径的计算。3.3 轮函数F的组合逻辑实现轮函数F是DES的核心运算单元它完全由组合逻辑构成不包含任何时序元件。其输入是32位的R和48位的subkey输出是32位的F_out。它的步骤如下扩展置换E将32位R扩展为48位。与子密钥异或expanded_R ^ subkey。S盒替代将48位结果分成8组6位每组输入一个S盒得到8组4位输出共32位。P盒置换对32位S盒输出进行固定置换。在Verilog中我们用一个always *块或纯assign语句来实现。为了清晰我们可以将其封装成一个function或一个独立的组合逻辑always块。function [31:0] round_function_f; input [31:0] R; input [47:0] K; reg [47:0] expanded_R; reg [47:0] sbox_in; reg [31:0] sbox_out; reg [31:0] p_out; begin // 1. 扩展置换E expanded_R {R[0], R[31], R[30], ... , R[0]}; // 按E位表扩展 // 2. 异或 sbox_in expanded_R ^ K; // 3. S盒替代 (调用8个S盒函数) sbox_out[31:28] sbox1(sbox_in[47:42]); sbox_out[27:24] sbox2(sbox_in[41:36]); // ... 省略其他6个S盒 sbox_out[3:0] sbox8(sbox_in[5:0]); // 4. P盒置换 p_out {sbox_out[16], sbox_out[25], ... , sbox_out[11]}; // 按P盒表置换 round_function_f p_out; end endfunction这个function可以在主状态机中直接调用计算当前轮的F函数值。3.4 主控制状态机设计这是整个模块的大脑。状态机需要管理以下流程空闲 - 初始置换 - 进行16轮迭代 - 32位交换 - 逆置换 - 输出。我们定义一个状态寄存器state以及轮计数器round_cnt。状态定义示例localparam S_IDLE 4‘d0; localparam S_INIT_PERM 4‘d1; localparam S_ROUND 4‘d2; localparam S_SWAP 4‘d3; localparam S_FINAL_PERM 4‘d4; localparam S_OUTPUT 4‘d5; reg [3:0] state, next_state; reg [3:0] round_cnt; // 0-15计数 reg [63:0] data_reg; // 存储中间数据 reg [31:0] L_reg, R_reg;状态转移逻辑 (always *) 和状态寄存器更新逻辑 (always (posedge clk)) 是标准写法。关键在于S_ROUND状态进入S_ROUND时round_cnt清零L_reg和R_reg加载经过IP置换后的左32位和右32位。在S_ROUND的每个周期完成一轮Feistel运算L_next R_reg;R_next L_reg ^ round_function_f(R_reg, subkey[round_cnt]);然后更新L_reg和R_reground_cnt加1。当round_cnt 15时表示16轮已完成因为我们从0开始计数下一状态转入S_SWAP。在S_SWAP状态执行最后一轮之后必要的左右交换根据Feistel结构最后一轮后不需要交换但标准DES在16轮后有一个额外的交换或者我们在初始分拆时就已经是标准顺序需要仔细对照算法。实际上常见的实现是在16轮迭代完成后将R_reg和L_reg直接拼接成{R_reg, L_reg}然后送入逆置换IP-1。这里是一个常见的易错点务必根据你代码中L_reg和R_reg的初始定义来确认拼接顺序。之后进入S_FINAL_PERM进行逆置换然后S_OUTPUT输出结果并拉高valid信号。关于加解密模式加解密的区别仅在于子密钥的使用顺序。加密时使用subkey[0]到subkey[15]解密时使用subkey[15]到subkey[0]。我们可以在状态机中根据mode信号决定在S_ROUND状态下是从subkey[round_cnt]还是subkey[15-round_cnt]读取密钥。或者更简单的方法是在密钥预计算完成后如果mode是解密就将subkey数组整体反转。后者控制逻辑更简单。4. 功能仿真、综合与板级调试实录代码写完了但这只是万里长征第一步。让代码在仿真中正确运行在FPGA上稳定工作才是真正的挑战。4.1 测试平台构建与标准测试向量验证搭建一个全面的测试平台Testbench至关重要。你需要测试基本功能使用标准测试向量NIST或教科书上常见的明文、密钥、密文对验证加密和解密功能。边界情况输入全0、全1的数据和密钥。模式切换连续进行加密、解密操作检查模式切换是否正常。背靠背Back-to-Back操作在一个数据块处理完成valid拉高后立即输入下一个数据块检查ready信号和流水线如果有是否正常。在Testbench中最重要的就是应用标准测试向量。例如一个著名的测试向量是明文: 0x0123456789ABCDEF 密钥: 0x133457799BBCDFF1 密文: 0x85E813540F0AB405你的Testbench应该能在仿真中自动比对输出结果并报告成功或失败。timescale 1ns/1ps module tb_des_core(); reg clk, rst_n, start, mode; reg [63:0] data_in, key_in; wire [63:0] data_out; wire ready, valid; des_core uut (.*); // 实例化被测单元 initial begin clk 0; forever #5 clk ~clk; end initial begin rst_n 0; #100 rst_n 1; // 测试加密 mode 0; data_in 64‘h0123456789ABCDEF; key_in 64‘h133457799BBCDFF1; wait(ready); (posedge clk) start 1; (posedge clk) start 0; wait(valid); if (data_out 64‘h85E813540F0AB405) $display(加密测试 PASSED); else $display(加密测试 FAILED, 得到: %h, data_out); // 接着可以用此密文和相同密钥测试解密... #1000 $finish; end endmodule4.2 综合与实现中的关键问题将代码放入Vivado、Quartus等工具进行综合和实现时你可能会遇到以下问题时序违例关键路径可能出现在轮函数F中尤其是S盒的组合逻辑链可能较长。如果时钟频率设得较高比如超过100MHz很容易出现建立时间违例。解决办法可以尝试流水线化轮函数F本身将其拆分为E扩展异或、S盒、P盒两级或三级流水但这会改变模块接口和状态机设计。更简单的方法是降低时钟频率或者启用综合工具的时序优化策略如retiming pipeline优化。对于初学者先保证功能正确再优化时序。面积占用虽然我们用的是单轮迭代但8个S盒的组合逻辑加起来面积也不小。在综合报告中查看LUT的使用情况。优化思路如果资源紧张可以考虑时间复用S盒只实例化1个或4个S盒在多个周期内分时计算8个S盒的输出。但这会显著增加计算周期数状态机变得更复杂属于面积换时间的权衡。子密钥数组的综合reg [47:0] subkey [0:15]这样的二维数组综合工具会推断出16个48位寄存器。确保你的初始化逻辑和读取逻辑是清晰的避免综合出不需要的锁存器。4.3 板级调试与信号抓取技巧当把设计下载到FPGA后如果结果不对仿真又没问题板级调试就派上用场了。内嵌逻辑分析仪ILA是你的最好朋友在Vivado中通过Mark Debug将关键信号如state、round_cnt、L_reg、R_reg、当前使用的subkey、valid、ready勾选为调试信号。重新综合实现后上板触发抓取波形。对比仿真波形和实际硬件波形差异点往往就是问题所在。常见问题复位信号不稳定、时钟域交叉问题如果用了多个时钟、异步信号同步没做好。检查复位和时钟确保复位信号在板卡启动后确实释放了。用示波器或ILA看时钟是否稳定频率是否正确。输入数据同步如果data_in和key_in来自其他异步时钟域务必在des_core的时钟域下进行至少两级寄存器同步防止亚稳态。输出负载data_out后面如果直接连接到引脚或其它模块确保驱动能力足够或者添加输出寄存器缓冲。5. 性能优化与扩展思路一个基础的单轮迭代DES核心已经完成。如果你需要更高的性能可以考虑以下优化方向流水线化如前所述将16轮展开成16级流水线。输入数据可以每个时钟周期进入一级吞吐量接近每个周期一个数据块但延迟仍是16个周期。面积会增大近16倍。部分展开展开成4级流水线每级处理4轮。这是一个面积和吞吐量的折中方案。你需要复制4份轮函数逻辑并设计一个更复杂的、4阶段的状态机或流水线控制逻辑。轮内流水在单轮函数F内部插入流水线寄存器将E扩展、S盒、P盒分成两到三级可以提高主频从而在迭代架构下提高吞吐量。支持3DES3DES是DES的增强版使用两个或三个密钥进行三次DES运算加密-解密-加密。你可以在顶层实例化三个des_core模块或者让同一个核心重复运行三次通过状态机控制密钥和模式的切换。这能显著提升安全性但性能会下降。添加标准接口如AXI4-Stream或Avalon-ST让你的DES核心更容易集成到SoC系统中与DMA、处理器等协同工作。实现一个完整的DES硬件模块就像搭建一个精密的机械钟表每一个齿轮逻辑模块都必须严丝合缝。从理解算法到硬件映射从编写RTL到调试波形每一步都需要耐心和严谨。希望这个教程能为你提供一个坚实的起点和清晰的路线图。当你第一次在示波器上看到自己设计的模块正确输出密文时那种成就感绝对是软件仿真无法比拟的。硬件设计的乐趣就在于这种从抽象算法到物理实现的完整掌控感。如果在实现过程中遇到具体问题比如某个S盒的输出总是不对或者状态机卡在某个状态回头仔细对照标准算法步骤和你的代码用仿真波形一步步跟踪数据流问题总会迎刃而解。