简介:本资源面向FPGA开发初学者与数字逻辑设计进阶者,提供基于Verilog硬件描述语言的DES对称加密算法完整实现工程,帮助读者理解经典加密算法在可编程逻辑器件上的落地方式。压缩包共134个文件,约409KB,以v源代码、mif存储器初始化文件、cdb与hdb综合数据库文件、rpt报告及txt说明为主,涵盖DES核心模块、轮函数与S盒、密钥生成、仿真测试平台及顶层接口等设计环节,并保留综合与布局布线过程记录。已有1072人学习下载。读者可从中获得可综合的Verilog源码、功能验证思路与工程目录组织参考,适合用于嵌入式安全、硬件加速方向的课程设计或项目原型开发,也可作为学习FPGA并行处理与加密算法硬件化的实践素材。
1. 从一块“跑不动”的板子说起:DES 加密到底怎么用 Verilog 落地
很多人第一次在 FPGA 上做加密算法,都会遇到一个很反直觉的现象:明明在电脑上跑 DES 加密几毫秒就完事,烧进板子后却要么时序不收敛,要么资源占用高得吓人,要么加密结果和软件对不上。问题往往不在算法本身,而在于 Verilog 是硬件描述语言,你写的每一行都在决定电路长什么样。这个项目就是围绕“基于 Verilog 的 DES 加密算法”展开的完整工程实现,目标很明确:把 DES 的 16 轮 Feistel 结构、S 盒替换、密钥调度这些步骤,用可综合的 RTL 代码写出来,最终能在 FPGA 上跑通加解密。它适合正在学数字 IC、做 FPGA 课程设计,或者想把加密模块集成进自己硬件系统的开发者。如果你只会写软件层面的 DES 调用,这个项目能帮你把“算法”真正变成“电路”。
2. 项目架构与目录拆解:DES 在 RTL 里到底分几块
2.1 整体模块划分与数据流
DES 算法在软件里通常是一个函数,输入 64 位明文和 64 位密钥,输出 64 位密文。但在硬件里,它必须被拆成多个时钟周期协作的模块。这个项目的常见做法是采用“轮展开”或“轮复用”两种架构。轮展开会把 16 轮全部铺成组合逻辑,一个周期出结果,但面积大、时序压力大;轮复用只实例化一轮运算单元,用状态机控制迭代 16 次,面积小但需要 16 个周期。对于大多数 FPGA 入门和课程设计场景,轮复用是更稳妥的选择,因为时序容易收敛,资源也友好。
整个数据流大致是:输入寄存器锁存明文和密钥 → 密钥调度模块生成 16 组 48 位子密钥 → 初始置换 IP → 16 轮 Feistel 迭代(每轮包含扩展置换 E、与子密钥异或、S 盒替换、P 盒置换)→ 逆初始置换 IP⁻¹ → 输出寄存器给出密文。解密时子密钥顺序反过来用即可。这个链路里,S 盒是组合逻辑最深的部分,也是时序优化的重点。
2.2 目录结构与文件职责
一个典型的工程目录会包含 RTL 源码、仿真测试、约束文件和工程脚本。下面是一个常见的组织方式:
des_verilog/ ├── rtl/ │ ├── des_top.v // 顶层模块,例化各子模块 │ ├── des_round.v // 单轮 Feistel 运算 │ ├── key_schedule.v // 密钥调度,生成子密钥 │ ├── sbox.v // 8 个 S 盒的查找逻辑 │ ├── permute.v // IP、IP⁻¹、E、P 等置换 │ └── des_fsm.v // 状态机控制轮次与握手 ├── sim/ │ ├── tb_des_top.v // 测试平台,给激励比对结果 │ └── test_vectors.txt // 标准测试向量 ├── constraints/ │ └── des_top.sdc // 时序约束 └── project/ └── build.tcl // 工程创建脚本这个结构的好处是职责清晰:des_top.v只做例化和连线,des_round.v专注一轮运算,key_schedule.v独立处理密钥。调试时你可以单独仿真某一轮,不用每次都跑完整链路。我一般会建议把 S 盒单独放一个文件,因为它是纯组合查找表,改起来最频繁,单独隔离方便替换成分布式 RAM 或 Block RAM 实现。
2.3 关键接口信号定义
顶层模块的端口通常包括时钟、复位、明文输入、密钥输入、密文输出、加解密选择以及 valid/ready 握手信号。下面是一个常见的端口定义:
module des_top ( input wire clk, // 系统时钟 input wire rst_n, // 低电平复位 input wire start, // 启动信号,单周期脉冲 input wire decrypt, // 1=解密,0=加密 input wire [63:0] plain_text, // 明文/密文输入 input wire [63:0] key, // 64 位密钥(含校验位) output reg [63:0] cipher_text,// 加密/解密结果 output reg done // 运算完成标志 );start是单周期脉冲,用来触发状态机从 IDLE 进入运算。decrypt决定子密钥的使用顺序。done在结果锁存后拉高一个周期,方便下游模块采样。这里要注意密钥虽然是 64 位,但 DES 实际有效密钥是 56 位,每字节第 8 位是校验位,密钥调度模块会通过 PC-1 置换丢掉这 8 位。
3. 核心模块实现:从密钥调度到 S 盒替换的逐段拆解
3.1 密钥调度模块:16 组子密钥怎么生成
密钥调度是 DES 里容易被低估的部分。它先把 64 位密钥通过 PC-1 置换压缩成 56 位,然后分成左右各 28 位,每轮根据轮次左移 1 或 2 位,再通过 PC-2 置换选出 48 位子密钥。下面是一个轮复用风格的实现片段:
// key_schedule.v 片段:生成第 i 轮子密钥 reg [55:0] key_56; reg [27:0] c_reg, d_reg; reg [47:0] subkey; // PC-1 置换,去掉校验位并重排 always @(*) begin key_56 = { key[7], key[15], key[23], key[31], key[39], key[47], key[55], key[63], key[6], key[14], key[22], key[30], key[38], key[46], // ... 其余 42 位按 PC-1 表排列 key[4], key[12], key[20], key[28], key[36], key[44], key[52] }; end // 每轮左移,shift_table 定义每轮移位数 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin c_reg <= 28'd0; d_reg <= 28'd0; end else if (load_key) begin c_reg <= key_56[55:28]; d_reg <= key_56[27:0]; end else if (round_en) begin c_reg <= { c_reg[26:0], c_reg[27] }; // 左移 1 位示例 d_reg <= { d_reg[26:0], d_reg[27] }; end end // PC-2 置换,从 56 位中选出 48 位 always @(*) begin subkey = { c_reg[13], d_reg[16], c_reg[4], d_reg[7], // ... 按 PC-2 表 // 共 48 位 c_reg[5], d_reg[9], c_reg[22], d_reg[24] }; end这里的关键是shift_table,16 轮里第 1、2、9、16 轮左移 1 位,其余轮左移 2 位。如果你把移位搞错,加密结果会完全对不上,而且这种错误在波形上很难一眼看出,属于典型的“玄学 bug”。我一般会在测试平台里单独打印每轮子密钥,和软件生成的参考值逐轮比对,这样能快速定位是移位错了还是 PC-2 表写错了。
3.2 单轮 Feistel 运算:扩展、异或、S 盒、P 盒
一轮 Feistel 的输入是 32 位右半部分和 48 位子密钥,输出是 32 位。流程是:32 位先经扩展置换 E 变成 48 位,与子密钥异或,然后分成 8 组 6 位,每组进一个 S 盒变成 4 位,拼成 32 位后再过 P 盒置换。下面是一轮的核心代码:
// des_round.v 片段 wire [47:0] expanded, xored; wire [31:0] sbox_out, pbox_out; // 扩展置换 E:32 位 -> 48 位 assign expanded = { r_in[31], r_in[0], r_in[1], r_in[2], r_in[3], r_in[4], r_in[3], r_in[4], r_in[5], r_in[6], r_in[7], r_in[8], // ... 共 48 位 r_in[27], r_in[28], r_in[29], r_in[30], r_in[31], r_in[0] }; assign xored = expanded ^ subkey; // 8 个 S 盒并行查找 genvar i; generate for (i = 0; i < 8; i = i + 1) begin : sbox_gen // 每组 6 位:最高位和最低位组成行号,中间 4 位组成列号 wire [1:0] row = { xored[i*6+5], xored[i*6] }; wire [3:0] col = xored[i*6+4 : i*6+1]; assign sbox_out[i*4+3 : i*4] = sbox_table[i][row][col]; end endgenerate // P 盒置换 assign pbox_out = { sbox_out[15], sbox_out[6], sbox_out[19], sbox_out[20], // ... 共 32 位 sbox_out[25], sbox_out[4], sbox_out[31], sbox_out[9] };S 盒的row和col计算是新手最容易翻车的地方。6 位输入里,第 1 位和第 6 位拼成 2 位行号,中间 4 位是列号,这个顺序不能反。我见过有人把行号列号搞反,仿真结果全是乱码,查了半天才发现是位选写错了。另外sbox_table通常用case语句或rom实现,综合器会自动推断成查找表。如果资源紧张,可以把 8 个 S 盒合并成一个大 ROM,用地址拼接的方式访问。
3.3 状态机与轮次控制:16 轮怎么串起来
轮复用架构下,状态机负责控制密钥加载、16 轮迭代和结果输出。一个简洁的写法是用计数器配合使能信号:
// des_fsm.v 片段 localparam IDLE = 2'd0, LOAD = 2'd1, ROUND = 2'd2, DONE = 2'd3; reg [1:0] state, next_state; reg [4:0] round_cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; round_cnt <= 5'd0; end else begin state <= next_state; if (state == LOAD) round_cnt <= 5'd0; else if (state == ROUND && round_cnt < 5'd16) round_cnt <= round_cnt + 1'b1; end end always @(*) begin next_state = state; case (state) IDLE: if (start) next_state = LOAD; LOAD: next_state = ROUND; ROUND: if (round_cnt == 5'd15) next_state = DONE; DONE: next_state = IDLE; endcase endround_cnt从 0 到 15 对应 16 轮。注意在 ROUND 状态里,每轮结束时左右寄存器要交换:l_reg <= r_reg; r_reg <= l_reg ^ f(r_reg, subkey);。这个交换是 Feistel 结构的核心,写反了结果就不对。解密时只需要把子密钥顺序倒过来,其他逻辑完全复用,所以decrypt信号通常用来控制子密钥的索引方向。
4. 仿真验证与上板调试:怎么确认你的 DES 是对的
4.1 测试平台搭建与标准向量比对
仿真验证是硬件开发里最不能省的一步。DES 有公开的标准测试向量,比如密钥全 0、明文全 0 时,密文应该是0x8CA64DE9C1B123A7。测试平台的做法是:给start一个脉冲,等done拉高后把cipher_text和期望值比对,不匹配就报错。下面是一个简化的测试平台片段:
// tb_des_top.v 片段 initial begin clk = 0; forever #5 clk = ~clk; // 100MHz 时钟 end initial begin rst_n = 0; start = 0; decrypt = 0; plain_text = 64'h0000000000000000; key = 64'h0000000000000000; #20 rst_n = 1; #10 start = 1; #10 start = 0; wait (done == 1); if (cipher_text !== 64'h8CA64DE9C1B123A7) $display("FAIL: got %h", cipher_text); else $display("PASS"); #100 $finish; end这个测试平台只覆盖了一个向量,实际项目中我建议至少准备 5 组:全 0、全 1、递增模式、随机模式,以及一组解密回环(加密后再解密看是否还原)。wait (done == 1)是阻塞等待,仿真里没问题,但要注意done只拉高一个周期,如果时钟太快可能错过,稳妥做法是用@(posedge done)。
4.2 时序约束与资源优化
上板之前必须加时序约束,否则综合器不知道你的时钟频率目标。SDC 文件里至少要有:
create_clock -name clk -period 10 [get_ports clk] set_input_delay -clock clk 2 [all_inputs] set_output_delay -clock clk 2 [all_outputs]10ns 周期对应 100MHz。如果时序不收敛,优先看 S 盒那条组合路径,因为 6 位查表加异或加 P 盒置换串起来延迟不小。优化手段包括:把 S 盒输出打一拍做流水线、把 16 轮拆成更多级流水、或者降低时钟频率。资源方面,轮复用架构通常只占几百个 LUT 和少量寄存器,对入门级 FPGA 完全够用。如果你发现资源异常高,检查是不是把 S 盒写成了分布式 RAM 而综合器没推断出来。
4.3 常见避坑记录
现象一:仿真结果和软件对不上,但波形看起来正常。原因:PC-1 或 PC-2 置换表写错,或者移位次数搞错。DES 的置换表是固定的,抄错一位就全盘皆输。 解决:把每轮子密钥打印出来,和 Python 的pyDes或在线工具逐轮比对,定位到具体哪一轮开始偏离。
现象二:上板后 done 一直不拉高。原因:状态机卡在某个状态,通常是round_cnt比较条件写错,比如用了==但计数器位宽不够导致永远不相等。 解决:在仿真里看状态机波形,确认round_cnt是否正常递增到 15。位宽建议留够,5 位计数器最大 31,不要用 4 位。
现象三:加密能过,解密结果不对。原因:解密时子密钥顺序没倒过来,或者 IP 和 IP⁻¹ 用反了。 解决:解密流程是密文先过 IP,然后 16 轮用逆序子密钥,最后过 IP⁻¹。检查decrypt信号是否同时控制了子密钥索引和置换选择。
现象四:时序报告显示建立时间违例。原因:S 盒组合逻辑太长,或者顶层连线太多导致布线延迟大。 解决:在 S 盒输出后插入一级寄存器,把一轮拆成两个周期;或者用(* keep = "true" *)约束关键信号,避免综合器过度优化。
现象五:不同综合工具结果不一致。原因:代码里用了不可综合的写法,比如initial块里赋初值、#delay延时、或者对同一信号多驱动。 解决:RTL 代码只使用可综合子集,初值用复位逻辑实现,延时只在测试平台里用。
5. 二次开发与改造思路:把 DES 模块塞进你的系统
5.1 接口改造:从裸端口到 AXI-Stream
如果你要把这个 DES 模块集成到 SoC 或带总线的系统里,第一步是改接口。裸端口用start/done握手,在总线系统里通常要换成 AXI-Stream 或 APB。常见做法是加一个轻量封装层:用tvalid/tready替代start,用tlast标记最后一个数据,done映射成tvalid输出。这样上游可以直接用 DMA 喂数据,不用 CPU 反复轮询。改造时注意握手协议的反压逻辑,tready为低时状态机要暂停,不能丢数据。
5.2 性能提升:流水线与多路并行
轮复用架构 16 个周期出一个结果,吞吐率有限。如果要做高速加密,可以改成两级流水:第一级做扩展和异或,第二级做 S 盒和 P 盒,这样时钟频率能上去,吞吐率翻倍。更激进的做法是轮展开加流水,16 轮全部铺开,每轮之间打拍,一个周期出一个结果,但面积会大很多。选择哪种取决于你的场景:低功耗嵌入式用轮复用,高速通信加解密用全流水。我一般会先跑轮复用版本验证功能,功能对了再逐步加流水线,这样调试压力小。
5.3 算法替换:从 DES 到 3DES 或 AES
DES 现在主要用作教学和遗留系统兼容,实际项目里更常见的是 3DES 或 AES。好消息是这个项目的模块划分方式可以直接复用:密钥调度、轮函数、状态机这三块的结构是通用的。换成 3DES 只需要例化三个 DES 核,用密钥 1 加密、密钥 2 解密、密钥 3 加密,中间加两个数据选择器。换成 AES 则要重写轮函数和密钥扩展,但状态机和接口层几乎不用动。这种“算法核可替换”的设计,是我在做过几个加密项目后养成的习惯:把算法相关逻辑关在一个模块里,外面套一层通用控制壳,换算法时只动内核。
5.4 验证方法:形式验证与覆盖率
功能仿真只能证明你测到的场景是对的,不能证明没测到的场景也是对的。对于加密模块,我建议至少加两个手段:一是用形式验证工具证明加解密互逆,即decrypt(encrypt(x)) == x对所有输入成立;二是收集代码覆盖率,确保 S 盒的每个条目、状态机的每个跳转都被覆盖到。覆盖率报告里如果有未覆盖的 S 盒行,说明你的测试向量不够随机,需要补激励。这一步很多课程设计会跳过,但如果你要把模块交给别人用,覆盖率是基本的质量凭证。
5.5 一个具体技巧:用参数化生成置换表
DES 的置换表是固定的,但手写容易错。我后来养成的习惯是用 Python 脚本生成 Verilog 的case语句或assign表达式,把置换表作为数据输入,脚本输出 RTL 代码。这样改表不用手改代码,也避免了抄错。比如 PC-1 表可以用一个 56 元素的数组表示,脚本遍历数组生成位选。这个方法同样适用于 S 盒,把 8 个 4×16 的表写成二维数组,脚本生成case分支。从那以后我每次做涉及查找表的 RTL 设计,都强制走一遍脚本生成加自动比对的流程,手写表再也没翻过车。希望这个思路帮到你。
本文还有配套的精品资源,点击获取