简介:面向FPGA学习者的DES加密算法Verilog实现工程包,覆盖加解密全流程与Quartus工程配置,适合数字逻辑设计入门、密码算法硬件实现及嵌入式安全方向开发者参考。包内共134个文件,核心为8个v源码文件,涵盖DES核心模块、顶层模块及仿真环境;另有mif初始化文件、qsf/qpf工程配置文件、txt说明文档、rpt综合报告与bak备份等,整体约409KB。目前已有1072人学习浏览。通过该工程可直观理解初始置换、16轮迭代、轮函数、S盒、P盒及逆初始置换等DES关键步骤,借助仿真平台比对已知向量验证加密结果;Quartus工程文件可直接打开,便于综合、布局布线与开发板在线调试。该压缩包体量紧凑但文件类型齐全,对想要同时掌握Verilog硬件描述语言与经典对称加密算法实现细节的读者,是一份值得对照研究的入门参考。
1. 从软件到硬件:Verilog 实现 DES 加密的实战起点
FPGA 开发里有个很常见的需求:把一段对称加密算法从 C 代码搬到硬件上。DES 虽然是老一代加密算法,但它的结构非常规整,拆成 IP 置换、轮函数、密钥调度三步后,特别适合用来理解「算法怎么变成可综合的电路」。我这次拆的资源就是一份完整的 Verilog 版 DES 加密工程,不是只贴几个模块的片段,而是从顶层端口到仿真测试环境齐全的一套代码。它解决的不只是「能不能加密」的问题,而是让你在 Vivado / Quartus 里能直接综合、能跑 testbench、能对着标准测试向量验证结果。
适合的人有两类:一类是想学硬件算法落地的新手,可以从这份代码里看到状态机、置换网络、S 盒 LUT 这些常规写法;另一类是在做接口加密、串口透传加密的工程师,想拿一个可参考的轮函数实现去改流水线或改成 3DES。如果你准备在 FPGA 上实现 DES,这份工程等于是一个能跑通的起点,你可以直接烧到板子上,也能顺着代码把整个算法链路捋清楚。
2. 项目定位与技术选型:为什么用 Verilog 写 DES 而不是调 IP 核
DES 全称是 Data Encryption Standard,分组长度 64 位,密钥长度 64 位(实际有效 56 位,另 8 位是奇偶校验位),一共做 16 轮迭代。与 AES 的字节代换、行移位结构不同,DES 的轮函数以置换和异或为主,非常适合用 Verilog 的比特位赋值和 case 语句表达。选用 Verilog 而不去调现成的加密 IP 核,主要有三个原因:IP 核通常是黑盒,内部结构改不动,想加流水线或者转 3DES 很麻烦;纯 RTL 实现方便精确控制时序,知道每一拍数据走到哪里;对于教学或研究性的 FPGA 项目,能一层层看穿 S 盒和密钥调度的实现,本身就是价值。
2.1 DES 算法的硬件友好结构
DES 的整体结构可以分成三段:初始置换 IP、16 轮 Feistel 网络、逆初始置换 IP⁻¹。Feistel 网络是 DES 的骨架,每一轮都做相同的操作:把 64 位数据分成左右各 32 位,右边走轮函数 F,F 的输出和左边异或,然后左右交换。这个结构天然适合硬件——每一轮的数据位宽固定,置换表可以直接写成常量数组,S 盒的查表也能用 case 语句或 ROM 实现。
从硬件角度看,DES 没有乘法、没有浮点,只有三类操作:位重排、异或、查表。这三类操作在 FPGA 里都是「组合逻辑 + 少量触发器」的组合。不追求极致性能时,用状态机复用一个轮函数单元,16 轮分时复用,资源占用非常小。追求吞吐率时,把 16 轮展开成 16 级流水,每级只是一组 LUT 和寄存器,时钟可以跑到很高。这份工程采用的是最直观的单轮迭代结构,后续改造的空间也留出来了。
2.2 模块划分与端口约定
工程文件组织分成六个模块:顶层des_encrypt负责对外接口;initial_permutation做 IP 置换;final_permutation做 IP⁻¹ 置换;round_transform做单轮 Feistel 变换;s_box_lookup实现 8 个 S 盒;key_schedule实现密钥调度。顶层端口定义如下,这也是最常用的一套总线约定:
module des_encrypt ( input wire clk, input wire rst_n, input wire start, input wire [63:0] plaintext, input wire [63:0] key, output reg [63:0] ciphertext, output reg done, output reg busy );参数说明:clk是驱动整个加密过程的时钟,建议用板载 50MHz 或 100MHz;rst_n是异步复位,低有效;start是加密请求信号,拉高一个时钟周期就启动一轮加密;plaintext是待加密的 64 位明文,key是 64 位密钥;ciphertext输出 64 位密文,done拉高表示加密完成,busy表示状态机正在运行。这样的端口设计可以直接挂在总线上,也可以接一个串口模块做数据接收。
这里有一个容易忽略的点:key尽管是 64 位输入,但实际上只用到了 56 位,每字节的第 8 位会被 PC1 置换丢弃。如果你从软件移植数据,要注意密钥的奇偶校验位不影响加密结果,去掉它反而更容易和标准向量对齐。
2.3 为什么不用现成 IP 核
很多 FPGA 厂商提供了 AES 或 3DES 的 IP 核,但 IP 核的接口通常是流式的,需要额外的 AXI 总线适配或者 FIFO 缓存,而且密钥扩展和轮函数往往被封装成黑盒。对于想要深入理解算法或者定制加密流程的开发来说,自研 RTL 最大的好处是可控——你可以精确插入流水寄存器,可以去掉不必要的缓存逻辑,可以按需求改成 CBC 模式。当然,如果你只是要在量产项目里快速实现加密,调用经过认证的 IP 核是更稳妥的选择,但作为研究学习和仿真验证,这份 Verilog 工程的价值在于它的透明性:每一个置换表、每一轮异或都能对上标准文档。
3. 核心模块拆解:一个明文分组的加密完整链路
这一章我们跟着数据流走一遍:从明文进入顶层,经过 IP 置换、16 轮迭代、IP⁻¹ 置换,最后得到密文。每个关键模块我都会给出可综合的 Verilog 代码和设计思路。建议你边读代码边对照 DES 标准中的置换表,这样能更快理解硬件写法与算法描述的映射关系。
3.1 顶层状态机与轮计数控制
顶层模块的核心是一个四状态状态机:IDLE、LOAD、ROUND、FINISH。IDLE 等待start;LOAD 拍把明文锁存到内部寄存器并完成 IP 置换;ROUND 状态执行一次轮函数,轮计数从 0 到 15;FINISH 做 IP⁻¹ 置换并把结果输出到ciphertext,同时拉高done。这样的状态机结构比较直白,也方便后续改成流水线——只需把 ROUND 状态展开成多级寄存器。
reg [1:0] state; reg [3:0] round_cnt; reg [63:0] data_reg; reg [31:0] L_reg; reg [31:0] R_reg; localparam IDLE = 2'd0; localparam LOAD = 2'd1; localparam ROUND = 2'd2; localparam FINISH = 2'd3; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; busy <= 1'b0; done <= 1'b0; round_cnt <= 4'd0; end else case (state) IDLE: if (start) begin state <= LOAD; busy <= 1'b1; done <= 1'b0; end LOAD: begin L_reg <= data_reg[63:32]; R_reg <= data_reg[31:0]; round_cnt <= 4'd0; state <= ROUND; end ROUND: begin if (round_cnt == 4'd15) begin state <= FINISH; end else begin round_cnt <= round_cnt + 1'b1; end end FINISH: begin ciphertext <= final_permutation_result; done <= 1'b1; busy <= 1'b0; state <= IDLE; end endcase end逻辑说明:这里把 IP 置换的结果暂存在data_reg,LOAD 状态拆成左右两组。ROUND 状态每拍调用一次轮函数,第 15 轮之后左右不需要交换,直接拼成 64 位送给 FINISH 状态做逆置换。一个细节:round_cnt从 0 计数到 15,控制轮函数使用的是哪一组子密钥,密钥调度模块需要按同样的序号产出key_index。
参数说明:L_reg和R_reg分别是每轮迭代的左右 32 位数据。在 Feistel 网络中,下一轮的 L 等于当前轮的 R,下一轮的 R 等于当前轮的 L 异或 F(R, K)。所以轮函数模块接收 R 和子密钥,输出 32 位结果,顶层负责异或和左右交换。这种把置换网络、轮函数、状态机分开的写法,有利于单独验证模拟的每一部分。
3.2 初始置换与逆置换:用常量数组替代连线
初始置换 IP 的作用是把 64 位输入按照固定的表重新排列。Verilog 实现方式很多,最直观的是连续赋值语句打散位号,但维护起来很麻烦。工程里采用了 generate 循环加常量表的方式,可读性和可维护性都更好:
function [63:0] do_ip_permutation; input [63:0] data_in; integer i; begin for (i = 0; i < 64; i = i + 1) begin do_ip_permutation[63 - i] = data_in[63 - ip_table[i]]; end end endfunctionip_table在工程里是一个完整的 64 元素整型数组,对应标准 IP 表。这里的索引写法要注意:标准表描述的是位序从左到右,而 Verilog 的[63:0]是 MSB 在左。如果不做 63 与 i 的翻转,加密结果会和你预期的字节序相反,这是最容易翻车的点。逆置换函数do_fp_permutation写法一致,只是换成fp_table。
逻辑说明:这个函数是纯组合逻辑,综合后会直接映射成一堆 LUT 和布线,没有时序问题。实际工程中如果追求吞吐率,可以把 IP 置换放到寄存器输出端,在 LOAD 状态一拍完成;如果追求频率,可以在置换前后插两级寄存器,避免组合路径过长。
3.3 轮函数 F 与 S 盒实现
轮函数 F 是 DES 安全性的核心,包含三步:32 位 R 扩展成 48 位 E,与 48 位子密钥异或,然后分成 8 组 6 位数据查 S 盒得到 32 位结果,再经过 P 置换。这里截取扩展和异或的关键代码:
wire [47:0] expanded_R; wire [47:0] xor_result; assign expanded_R[47:0] = {R_reg[31], R_reg[0], R_reg[1], R_reg[2], R_reg[3], R_reg[4], R_reg[3], R_reg[4], R_reg[5], R_reg[6], R_reg[7], R_reg[8], // ... 完整的E扩展表 R_reg[31]}; assign xor_result = expanded_R ^ subkey;subkey来自密钥调度模块,每轮换一组新的 48 位子密钥。S 盒部分用 case 语句实现查表。每个 S 盒有 64 个 4 位输出项,标准实现里可以写成二维数组,但 Verilog 的二维数组在综合时会消耗大量 LUT,更常见的是拆成 8 个独立函数,每个函数单独 case:
function [3:0] s1_lookup; input [5:0] index; case (index) 6'd0: s1_lookup = 4'd14; 6'd1: s1_lookup = 4'd4; // 省略中间项 6'd63: s1_lookup = 4'd2; default: s1_lookup = 4'd0; endcase endfunction逻辑说明:S 盒的行由 6 位输入的首尾两位决定,列由中间 4 位决定。但写 case 时可以直接用完整 6 位索引,把 S 盒的 64 项全列出来,综合工具自然能优化成 LUT。这种做法还有一个好处:仿真速度比用二维数组索引快得多,避免仿真器在数组边界检查上消耗时间。
参数说明:index每一组是 6 位,前 48 位异或结果刚好分为 8 组,分别进 s1 到 s8 查表,每个输出 4 位,拼接成 32 位。P 置换则和 IP 置换类似,用一个常量数组重新排列这 32 位。轮函数 F 最终输出 32 位。
这里要强调一下:S 盒的默认项必须写上default,不然综合工具会推断出锁存网表,导致功能和仿真不一致。这是 Verilog 新手在写组合逻辑时最常见的坑,也是后面避坑章节里我会专门展开记录的。
3.4 密钥调度:从 64 位密钥到 16 组子密钥
密钥调度分三步:PC1 置换得到 56 位,分成 C、D 各 28 位,每轮循环左移一位或两位,然后拼接成 56 位,再做 PC2 置换得到 48 位子密钥。工程里用一个独立的key_schedule模块一次性生成 16 组子密钥,存到寄存器数组里,轮函数直接按索引取用。
reg [55:0] key56; reg [27:0] C_reg, D_reg; reg [47:0] subkey_array [0:15]; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin key56 <= 56'd0; end else begin key56 <= do_pc1_permutation(key); end end always @(*) begin C_reg = key56[55:28]; D_reg = key56[27:0]; // 根据轮序号循环左移 for (i = 0; i < 16; i = i + 1) begin case (shift_schedule[i]) 1: begin C_reg = {C_reg[26:0], C_reg[27]}; D_reg = {D_reg[26:0], D_reg[27]}; end 2: begin C_reg = {C_reg[25:0], C_reg[27:26]}; D_reg = {D_reg[25:0], D_reg[27:26]}; end endcase subkey_array[i] = do_pc2_permutation({C_reg, D_reg}); end end逻辑说明:shift_schedule是 16 个元素的常量数组,对应 DES 标准中第 1、2、9、16 轮左移一位,其余轮次左移两位。上面的组合逻辑块把每个循环左移后的 C、D 拼接成 56 位,再经过 PC2 置换存到subkey_array。实际工程中为了节省寄存器,也可以做成每轮实时计算子密钥的状态机,但预计算 16 组数组的优点是轮函数不需要等待密钥扩展,且后续改成流水线时每个流水级可以独立读取子密钥。
参数说明:key56是去掉奇偶校验位的 56 位密钥,PC1 置换同时完成了丢弃校验位和位重排两件事。PC2 从 56 位中选出 48 位,被打乱后的顺序写在常量表里。因为子密钥每轮不重复使用,所以这个模块本质上是在密钥加载后做一次展开,16 轮共需要 16 个 48 位寄存器,资源消耗 768 比特,在 FPGA 上完全可以接受。
4. 仿真验证与综合上板:让 testbench 告诉你对错
拿到代码后第一步不是急着上板,而是先把 testbench 跑通。DES 是公开算法,网上能找到大量的标准测试向量,用标准向量做比对是最直接的验证手段。工程里包含的 testbench 覆盖两种典型场景:单组向量加密、连续多组数据加密。
4.1 testbench 编写:一个带自动比对的标准向量测试
testbench 的核心思路是:给顶层模块发送 start 信号,等待 done 拉高,然后把输出的ciphertext和预期的密文比对。为了减少手工检查的麻烦,我在测试代码里写了一个简单的错误计数逻辑:
reg [63:0] expected_cipher; reg [7:0] error_cnt; initial begin error_cnt = 8'd0; clk = 1'b0; rst_n = 1'b0; #100 rst_n = 1'b1; // 标准向量 1 plaintext = 64'h0123456789ABCDEF; key = 64'h133457799BBCDFF1; expected_cipher = 64'h85E813540F0AB405; @(posedge clk); start = 1'b1; @(posedge clk); start = 1'b0; wait (done); if (ciphertext !== expected_cipher) begin error_cnt = error_cnt + 1; $display("Test 1 failed: got %h expected %h", ciphertext, expected_cipher); end else begin $display("Test 1 passed"); end // 可继续添加更多测试向量 end采样说明:我故意用wait(done)而不是固定的延时,这样如果状态机中间多跑了一拍,测试也能等到真实完成时刻。这里要注意ciphertext是 reg 输出,在 done 拉高的同一拍它已经稳定,所以可以直接比对。错误计数error_cnt在最后通过$display打印,如果非零就返回失败。这种自检式 testbench 在回归测试里很有用。
4.2 标准测试向量怎么选
DES 最经典的测试向量就是上面代码里的那组:明文0123456789ABCDEF,密钥133457799BBCDFF1,密文85E813540F0AB405。这组数据来自 FIPS 官方示例,几乎所有的教材和开源实现都引用了它。除了这组,我一般还会再测边界用例:全 0 明文、全 0 密钥、全 0 明文加全 f 密钥。这些极端组合能帮你快速暴露位序和置换表的错误。
参数说明:如果你发现第一个标准向量对不上,不要急着改代码,先检查三点。第一,你的key和plaintext在 testbench 里的赋值顺序,Verilog 的initial块中赋值没有时延的话,应放在rst_n释放之后、start拉高之前。第二,检查 S 盒的索引方向,DES 里 S 盒的行是由第 0 位和第 5 位组成的 2 位值,如果你把行、列搞反,输出至少错 75% 的位。第三,确认轮函数最后一轮是否交换左右,Finail 轮不做左右互换,直接从最终轮的 L、R 拼出 64 位。
4.3 综合与上板:从仿真到跑起来
仿真通过后,打开工程进行综合。在 Vivado 或 Quartus 中把顶层设为des_encrypt,添加约束文件,主要约束一个时钟和复位引脚。综合频率预期:不额外优化时,单轮迭代结构通常能在 100MHz 以内稳定跑通,因为轮函数里的 S 盒 LUT 有 3 到 4 级逻辑延迟。如果你在板子上通过开关按键触发加密,记得做按键消抖,不然一次按下可能触发多次start。
上板验证的观察方式很简单:把ciphertext、done连到 LED 或者逻辑分析仪,通过串口把结果发回电脑比对。很多开发板带有 USB 转串口,一个常见做法是写一个小的串口发送模块,把 64 位密文按字节发送,这样你不需要额外的调试工具。注意done信号只维持一个时钟周期,如果要观察它,可以在顶层再做一个寄存器把done打一拍,或者直接拉长显示时间。
5. 避坑记录:时序、阻塞赋值与测试向量的那些坑
我在调试这份工程时踩过不少坑,其中有一些是 Verilog 新手一定会遇到的,把它们按「现象、原因、解决」写出来,能帮你节省大量排查时间。
坑一:加密结果前八位是错的,后八位完全正确
现象:用标准向量比对,密文和预期只在低字节部分匹配,高字节乱掉。原因:初始置换表中的索引方向写反了。DES 标准把所有位用 1 到 64 编号,1 是最高有效位,而 Verilog 数组[63:0]需要把data[63]对应到表的第 1 项。如果你的置换函数里直接写do_ip_permutation[i] = data_in[ip_table[i]],等于整体镜像了位序。解决:在函数里把所有索引翻转,写成[63 - i] = data_in[63 - ip_table[i]]。这个坑非常隐蔽,因为仿真结果不是全错,只是按位镜像后的错误,看着很像随机乱码。
坑二:S 盒输出不定态导致仿真密文为 X
现象:仿真波形中某些比特位是红色 X,且会随时间扩散到整段密文。原因:S 盒的 case 语句没有覆盖全部 64 种输入,或者 48 位异或结果中的某几位是 X,导致索引为 X,case 进入不到任何分支。解决:在所有 S 盒函数的 case 语句最后加default,并且检查密钥调度和扩展模块,确保每个比特都是确定值。如果你的仿真里出现了 X,最优先的方法是看$display里打印的中间变量,定位是第一轮出现的还是后面才出现的。
坑三:用了阻塞赋值写状态机,综合后功能不对
现象:仿真结果在 ModelSim 里是好的,但综合后上板运行时加密结果不稳定,时好时坏。原因:在always @(posedge clk)块中用=给多个状态寄存器赋值,造成同一拍内多个触发器读取尚未稳定的中间值。解决:状态机相关的寄存器赋值全部改成非阻塞赋值<=,组合逻辑块才使用阻塞赋值=。这是 Verilog 仿真和综合差异的经典案例,我刚开始写代码时经常在这里翻车,后来干脆给自己定了一条规则:时序块里所有 reg 用<=,组合函数里无所谓但尽量用临时变量。
坑四:复位后第一个标准向量验证通过,连续输入两组数据时第二组密文错误
现象:第一组明文加密正确,紧接着再一次start,第二组输出错误。原因:密钥调度模块使用了always @(*)组合逻辑,但依赖于key56寄存器,而这个寄存器只在复位时更新一次,之后没有更新时机。如果你在加载新密钥前没有把状态机回到 IDLE 并等待足够时间,组合逻辑可能会读到上一次的半更新值。解决:在 LOAD 状态下强制把key56重新执行 PC1 置换,或者把密钥扩展做成同步逻辑,每个start都重新计算子密钥数组。更稳妥的做法是在 IDLE 状态就检测start,并用一个额外的key_loaded寄存器标记密钥是否已更新。
坑五:时钟频率稍高时就出现偶发错误
现象:10MHz 时钟下测试全通过,抬高到 50MHz 后大约每几千次加密会错一次。原因:轮函数从 R 输入到 F 输出再到异或,组合路径过长,再加上 S 盒 LUT 的多级延迟,导致时序约束无法满足。解决:要么降低时钟频率,要么在轮函数的关键路径上插入一级流水寄存器,把扩展异或和 S 盒查表分开。插入一级寄存器会让整个加密时间从 16 拍变成 32 拍,但吞吐率会好很多。这是在资源与速度之间做权衡的典型场景。
6. 二次开发:从单周期 DES 到流水线与 3DES 改造
这份 DES 工程并不是只能按原样使用,它的模块划分和接口设计给二次开发留了很大空间。我做过三种方向的改造,都验证过可行,可以给你参考。第一种是把单轮迭代改成 16 级流水线。做法是把round_transform复制 16 份,每份的 R 输入接上一份的输出,子密钥从预计算的subkey_array分别取。这样每拍都能载入新的明文,加密吞吐率从「每 16 拍一个结果」变成「每拍一个结果」,但资源占用大约变成原来的 16 倍。在资源比较充裕的 FPGA 上,这是最常用的提速手段。
第二种改造是扩展成 3DES。3DES 本质就是做三次 DES,加密-解密-加密。你只需在顶层外面包一层控制逻辑,把第一次加密的密文送进一个解密模块,再用第三个模块加密。注意中间那层用的是解密,但密钥调度不变,只是轮函数里子密钥的使用顺序从 K16 到 K1。你可以复用现有的round_transform,通过一个decrypt_mode信号控制子密钥的排列方向。改造工作量不大,却能把加密强度从 DES 的 56 位有效密钥提升到 112 位。
第三种改造是改成 CBC 模式。当前工程是 ECB 模式,也就是每个分组独立加密。CBC 模式需要把每个分组加密前和前一个密文异或。实现方式是在顶层加一个 64 位反馈寄存器,在 LOAD 状态对明文做异或。要注意 CBC 模式不能并行处理多个分组,因为每个分组依赖前一个密文,这会限制流水线的吞吐率。如果你的应用场景是串口数据流加密,我建议优先考虑 CBC,但如果你只是验证算法本身,ECB 更直观。
从我的习惯来看,每次拿到一份 RTL 工程,我都会先写好自检 testbench,再动手改代码。那次把阻塞赋值混用在状态机里导致上板翻车之后,我给自己定了条规矩:凡是要沿时钟沿更新的 reg,一律先写一遍<=,代码评审时重点看时序块里有没有=。这条习惯帮我避掉了后面几个项目里不少莫名其妙的偶发故障。希望这份 DES 的 Verilog 工程能让你少走这些弯路,也给你后续做加密相关硬件设计打下一个能跑通、能改动的底子。希望帮到你。
本文还有配套的精品资源,点击获取