简介:这份资源提供了一套完整的SHA256算法Verilog硬件实现源码,面向希望深入理解密码学算法与FPGA/ASIC数字电路设计的工程师、学生及研究人员。SHA256作为SSL/TLS、PGP等安全协议的核心散列函数,其硬件实现涉及数据填充、初始化变量、消息块处理、压缩函数与最终哈希值计算等环节,本资源将上述步骤逐一转化为可综合的Verilog模块,便于读者对照源码理解消息调度、主循环与并行吞吐优化等关键逻辑。压缩包共18个文件,约13KB,以10个.v源文件为主体,辅以3个.rs文件、1个.toml、1个.lock、1个.md说明、1个Makefile及1个.py脚本,覆盖RTL设计、Rust工程配置与构建脚本,目录结构清晰。目前已有146人学习,适合作为硬件编程与密码学算法的实践参考,帮助读者掌握资源受限条件下的电路设计思路与验证方法。
1. 从一次时序违例说起:这套 SHA256 的 Verilog 源码到底值不值得拆
去年帮一个做安全模块的朋友看板子,综合报告里一条路径的 slack 是负的,起点是sha256_core里的加法器树,终点是寄存器组。他当时用的是网上随手找的一份 SHA256 实现,代码能跑仿真,但一上板就出问题。后来我们换了一份结构更清晰的源码,把关键路径拆开、插了流水线寄存器,才把时序收住。这件事让我意识到,SHA256 的 Verilog 实现看着简单——不就是移位、异或、加法吗——但真正决定能不能用的,是消息调度和压缩函数那两块的状态机怎么组织、关键路径怎么切。
这次要拆的这套源码,就是一套完整的 SHA256 Verilog 实现,覆盖了从消息填充、消息调度到压缩函数、最终摘要输出的全流程。它适合两类人:一类是刚学完 Verilog 语法、想找一个「不太大但足够完整」的项目练手的同学;另一类是在做安全加速、需要把 SHA256 往 FPGA 上搬的工程师,想先拿一份能综合、能仿真的参考实现摸清结构。整套代码的模块划分比较规整,顶层、核心、常量表、测试平台都有,拿来改参数、加流水线、做面积和速度的权衡都方便。
我打算按「先看懂结构、再跑通仿真、然后拆关键模块、最后聊时序和验证」的顺序来写。中间会给出可以直接抄的仿真命令、测试平台写法、参数修改位置,也会把我在综合和上板时踩过的几个坑摊开讲。如果你手上正好有一份 SHA256 的 Verilog 代码但不确定写得对不对,或者想自己从零搭一个,这篇可以当参考。
2. 拆开这套源码:模块划分与数据通路怎么读
拿到一份 RTL 源码,我习惯先不看代码,先看目录和模块例化关系。这套 SHA256 实现的结构不算复杂,但该有的层次都有。先把整体框架理清楚,后面看具体逻辑才不会迷路。
2.1 顶层到核心的层次关系
这套代码大致分四层。最上面是顶层模块,负责把输入数据、时钟复位、启动信号接进来,把最终摘要送出去。往下一层是核心模块,也就是 SHA256 的压缩函数主体,包含 64 轮迭代的状态更新逻辑。再往下是消息调度模块,负责把 512 位的消息块扩展成 64 个 32 位字 W[0] 到 W[63]。最底层是常量表和一些组合逻辑辅助模块,比如加法器、选择器。
常见做法是把消息调度和压缩函数放在同一个核心模块里,用同一个时钟推进。这套源码也是这么处理的,sha256_core里同时例化了消息扩展和轮函数更新逻辑。这样做的好处是控制简单,一个状态机就能管住整个流程;代价是关键路径可能偏长,因为消息扩展的 W 值计算和轮函数的加法会串在一起。如果你后面要做高频设计,可以考虑把消息调度单独提出来做成流水线,这是后话。
读代码的时候,建议从顶层端口开始,顺着start信号往下追。start拉高后,核心模块进入初始化状态,把 SHA256 的八个初始哈希值 H0 到 H7 加载进去,然后按 512 位一块地处理输入消息。每处理完一块,H 值更新一次;所有块处理完,把 H0 到 H7 拼接输出,就是最终摘要。
2.2 消息填充与 512 位分块的处理方式
SHA256 对输入消息有个硬性要求:先补一个 1,再补若干个 0,最后附上 64 位的原始消息长度,使得总长度是 512 位的整数倍。这套源码里,消息填充这部分通常放在测试平台或者上层控制逻辑里做,核心模块假设进来的就是已经填好、按 512 位对齐的数据。
这里有个容易翻车的地方:填充规则里那个「1」是紧跟在原始消息最后一位之后的,不是补一个完整的字节。很多新手写测试的时候直接按字节补0x80,如果原始消息长度正好是字节对齐的,这样写没问题;但如果消息长度不是 8 的倍数,就会出错。SHA256 的填充是按位来的,0x80只在字节对齐时等价于「先补 1 再补 7 个 0」。所以测试平台里生成激励的时候,要么保证消息是整字节的,要么老老实实按位拼。
核心模块内部处理 512 位块时,会把它拆成 16 个 32 位字,存进一个 16 深、32 位宽的寄存器组或者用移位寄存器实现。然后消息调度模块基于这 16 个字,用递推公式扩展出剩下的 48 个字。这个递推公式里有循环右移和异或,是组合逻辑里比较深的一段,综合时要注意。
2.3 关键寄存器与状态机的状态划分
这套源码的状态机不算复杂,大致有 IDLE、INIT、ROUND、DONE 几个状态。IDLE 等待start;INIT 加载初始 H 值和第一组 W;ROUND 跑 64 轮;DONE 输出结果并拉高done信号。
我一般会重点看两个地方。一个是轮计数器的位宽和复位逻辑,64 轮需要至少 7 位计数器(0 到 63 是 64 个值,6 位不够)。另一个是 H 值更新用的临时变量 a 到 h,这八个变量在每轮结束时整体移位,最后一轮还要和初始 H 值做加法。如果这里移位顺序写错,仿真可能不报错,但结果就是不对,属于典型的「玄学 bug」。
提示:读这套代码时,先把
sha256_core的端口和状态机画在纸上,再对照轮函数的八变量更新顺序看一遍,比直接啃代码快得多。
3. 跑通仿真:从测试平台到波形对照
代码看懂了不算数,得跑起来。这一章给出一套可以直接用的仿真流程,包括测试平台怎么写、常用仿真器怎么跑、结果怎么和标准值对照。我用的工具是常见的开源仿真器,命令通用,换成商业仿真器也大同小异。
3.1 测试平台的基本结构
测试平台要做三件事:产生时钟和复位、喂激励、检查输出。下面是一个简化但完整的测试平台骨架,可以直接套。
`timescale 1ns/1ps module tb_sha256; reg clk; reg rst_n; reg start; reg [511:0] msg_block; // 一个 512 位消息块 wire [255:0] digest; wire done; // 例化核心模块 sha256_core u_core ( .clk (clk), .rst_n (rst_n), .start (start), .msg_block (msg_block), .digest (digest), .done (done) ); // 时钟:10ns 周期,100MHz initial clk = 0; always #5 clk = ~clk; initial begin rst_n = 0; start = 0; msg_block = 512'd0; #100; rst_n = 1; #20; // 喂一个已知消息块,这里用全 0 块做示例 msg_block = 512'h0; start = 1; #10; start = 0; // 等待 done wait (done == 1'b1); #10; $display("digest = %h", digest); $finish; end endmodule这段测试平台的关键点有三个。第一,start只拉高一个时钟周期,核心模块靠边沿检测启动,如果拉高太久,状态机会重复触发。第二,msg_block是 512 位,对应一个已经填充好的消息块,实际使用时你需要先做填充和分块。第三,wait (done)是阻塞等待,仿真里没问题,但如果你要连续喂多个块,得改成状态机或者用@(posedge done)配合计数。
3.2 用开源仿真器跑一遍
假设文件结构是sha256_core.v、sha256_constants.v、tb_sha256.v,用 Icarus Verilog 跑仿真的命令如下。
# 编译 iverilog -o sha256_sim sha256_core.v sha256_constants.v tb_sha256.v # 运行并生成波形 vvp sha256_sim # 如果需要看波形,在测试平台里加 $dumpfile / $dumpvars如果测试平台里加了$dumpfile("sha256.vcd")和$dumpvars(0, tb_sha256),跑完会生成 VCD 文件,用 GTKWave 打开就能看波形。我一般会重点看state、round_cnt、a到h这几个信号,确认 64 轮是否跑满、每轮移位是否正确。
3.3 和标准摘要值对照
光看波形不够,得拿标准值验证。SHA256 对空字符串的摘要是一个固定值,对"abc"也有固定值。你可以用 Python 的hashlib生成参考值,再和仿真输出对比。
import hashlib # 空字符串 print(hashlib.sha256(b"").hexdigest()) # 输出: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 # "abc" print(hashlib.sha256(b"abc").hexdigest()) # 输出: ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad注意,仿真里喂的是填充后的 512 位块,不是原始字符串。以空字符串为例,填充后是一个 512 位块:最高位是 1,后面跟 447 个 0,最后 64 位是长度 0。你需要在测试平台里把这个块拼出来,或者写一个小函数生成。这一步是新手最容易出错的地方,填充错了,后面逻辑再对也白搭。
注意:不同仿真器对
wait语句和$dumpvars的支持略有差异,如果波形出不来,先检查测试平台里有没有正确调用$dumpfile。
4. 消息调度与压缩函数:参数怎么改、路径怎么切
仿真跑通只是第一步。真正要把这套代码用起来,得知道哪些参数可以改、改了会怎样,以及关键路径在哪里、怎么优化。这一章拆消息调度和压缩函数两块,给出具体的修改位置和优化思路。
4.1 消息调度的递推公式与实现选择
SHA256 的消息调度用下面这个递推公式,从 W[0] 到 W[15] 扩展出 W[16] 到 W[63]:
W[t] = σ1(W[t-2]) + W[t-7] + σ0(W[t-15]) + W[t-16]其中 σ0 和 σ1 是循环右移和异或的组合。这套源码里,消息调度有两种常见实现方式:一种是纯组合逻辑,每个周期算一个 W;另一种是移位寄存器,每个周期把窗口往前推一格。纯组合逻辑面积小,但关键路径长;移位寄存器面积大,但时序好收。
我一般会先看源码用的是哪种。如果是组合逻辑,综合报告里那条从 W 计算到轮函数加法的路径往往就是最长的。想改的话,可以在 W 计算后面插一级寄存器,把消息调度和轮函数分成两级流水。代价是每轮多一个周期,但频率能上去。
4.2 压缩函数的八变量更新与加法器树
压缩函数每轮更新 a 到 h 八个变量,核心是下面这组赋值:
// 一轮更新的简化写法 t1 = h + Σ1(e) + Ch(e,f,g) + K[t] + W[t]; t2 = Σ0(a) + Maj(a,b,c); h = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2;这段逻辑里,t1的计算串了五个加法,是典型的长路径。综合工具会把它综合成一棵加法器树,但树的结构取决于你写的顺序。常见做法是用括号显式分组,让综合工具知道哪些加法可以并行。比如把h + Σ1(e)和Ch + K[t]先各自算,再合并,能缩短关键路径。
参数方面,K[t] 是 64 个常量,存在sha256_constants.v里。这套源码的常量表是完整的,不需要改。如果你要做多消息块连续处理,需要关注的是 H 值在块之间的保持逻辑,确保上一块的输出正确加载到下一块的初始值。
4.3 面积与速度的权衡:流水线级数怎么选
这套源码默认应该是非流水的,一个块 64 轮加初始化和收尾,大概几十个周期。如果你要跑高吞吐,可以考虑两种改法。第一种是轮内流水,把每轮拆成两级,一级算 t1,一级算 t2 和更新,周期数翻倍但频率能提高。第二种是轮间流水,同时处理多个消息块,面积成倍增加,但吞吐能上去。
我一般会先跑一次综合,看默认实现的 Fmax 和面积,再决定要不要改。如果 Fmax 已经满足需求,就别动,改流水线引入的 bug 可能比收益还大。如果差一点,优先考虑在 W 计算和 t1 之间插寄存器,这是收益最明显的一刀。
提示:改流水线之前,先把非流水版本的综合报告和时序报告存一份,改完对比,不然很容易改着改着忘了基线在哪。
5. 避坑与排查:仿真对、上板错的那几件事
这套代码在仿真里跑通不难,但从仿真到综合再到上板,中间有几个坑我踩过不止一次。这一章按「现象 → 原因 → 解决」列出来,你遇到类似问题时可以对照排查。
5.1 仿真结果对,综合后结果错
现象:RTL 仿真输出和 Python 参考值一致,综合后上板或者门级仿真结果不对。
原因:最常见的是组合逻辑里的锁存器推断。消息调度或者状态机里如果有 if 分支没写全,综合工具会推断出锁存器,仿真时因为初始值不确定可能碰巧对,综合后就暴露了。
解决:综合后看警告列表,搜latch关键字。有锁存器就回去补全 if-else 或者 case 的 default 分支。这套源码里状态机的 case 语句要确认每个状态都有明确跳转,不要留悬空分支。
5.2 时序违例集中在加法器路径
现象:综合报告里 slack 为负,路径起点在 W 计算或者 t1 计算,终点在 a 到 h 寄存器。
原因:加法器树太深,组合逻辑延迟超过时钟周期。
解决:两个方向。一是降低时钟频率,先保证功能正确;二是在加法器树中间插寄存器,做流水线。如果不想改架构,可以试试让综合工具做 retiming,但效果不稳定,不如手动插寄存器可控。
5.3 多块消息处理时 H 值没更新
现象:单块消息摘要正确,连续喂多个块时,第二块开始结果就错了。
原因:H 值在块与块之间没有正确保持,或者初始化逻辑在每块开始时把 H 值重置成了初始常量,而不是上一块的输出。
解决:检查核心模块里 H 值的加载条件。正确做法是第一块用初始常量,后续块用上一块的输出。状态机里要有一个标志位区分「第一块」和「后续块」,这个标志位在复位后置位,处理完第一块后清零。
5.4 填充逻辑在非字节对齐消息上出错
现象:消息长度是整字节时结果对,长度不是 8 的倍数时结果错。
原因:填充时直接按字节补0x80,没有按位处理。
解决:填充逻辑改成按位操作。先补一个 1,再补 0 到 511 个 0,最后附 64 位长度。如果嫌按位麻烦,可以在上层把消息统一转成字节流,但长度字段仍然要按位计算。
5.5 仿真器对阻塞和非阻塞赋值的处理差异
现象:换一个仿真器,结果不一样。
原因:时序逻辑里混用了阻塞赋值,不同仿真器对竞争的处理可能不同。
解决:时序逻辑统一用非阻塞赋值<=,组合逻辑用阻塞赋值=。这套源码里如果看到always @(posedge clk)里用了=,改成<=。这是 Verilog 的基本功,但混用的时候仿真可能不报错,换工具就翻车。
6. 进阶验证:用随机激励和覆盖率把黑匣子照亮
仿真跑通、上板能用之后,还有一件事值得做:验证。SHA256 这种算法,输入空间巨大,靠几个固定测试向量不够。我一般会加一层随机激励和覆盖率收集,把「黑匣子」照亮,确认各种边界情况都覆盖到。
6.1 随机激励生成与自动比对
思路很简单:用 Python 或者 SystemVerilog 生成随机长度的随机消息,算出参考摘要,喂给 RTL,自动比对输出。下面是一个用 Python 生成激励文件、再用 Verilog 读入的简化流程。
import hashlib import random def pad_message(msg: bytes) -> bytes: """SHA256 填充:补 1、补 0、附 64 位长度""" length = len(msg) * 8 msg += b'\x80' while len(msg) % 64 != 56: msg += b'\x00' msg += length.to_bytes(8, 'big') return msg with open("stimulus.txt", "w") as f: for _ in range(100): n = random.randint(0, 200) msg = bytes(random.getrandbits(8) for _ in range(n)) padded = pad_message(msg) ref = hashlib.sha256(msg).hexdigest() # 每个块一行,格式:512 位十六进制 + 参考摘要 for i in range(0, len(padded), 64): block = padded[i:i+64] f.write(block.hex() + "\n") f.write("REF " + ref + "\n")这段脚本生成 100 组随机消息,每组按 512 位分块写入文件,最后附上参考摘要。Verilog 测试平台读这个文件,逐块喂给核心模块,处理完一组后比对摘要。
6.2 覆盖率收集与边界场景
随机激励之外,还要有针对性的边界场景。我一般会覆盖这几类:
| 场景 | 说明 | 为什么重要 |
|---|---|---|
| 空消息 | 长度为 0 | 填充后只有一个块,长度字段全 0 |
| 单字节消息 | 长度 1 字节 | 填充跨块边界,容易出错 |
| 55 字节消息 | 填充后刚好一个块 | 长度字段紧跟在 1 之后,边界紧凑 |
| 56 字节消息 | 填充后需要两个块 | 触发多块处理逻辑 |
| 64 字节消息 | 正好一个块 | 填充后需要两个块,长度字段在第二块 |
| 长消息 | 几百字节 | 验证多块连续处理的 H 值保持 |
这些场景用固定激励跑一遍,比纯随机更能暴露问题。覆盖率方面,至少要看状态机的每个状态是否都到过、轮计数器是否跑满 64、H 值更新路径是否都激活过。
6.3 形式验证的轻量替代:等价性检查
如果你改了流水线或者优化了加法器树,怎么确认改完和改之前功能一致?跑随机激励是一种办法,但更省事的是做等价性检查。常见做法是用综合工具或者专用工具,把改前和改后的网表做逻辑等价对比。如果没有这类工具,退而求其次,用同一组随机激励跑两个版本,比对输出。我一般会保留一份「黄金版本」的 RTL,任何优化都拿它做基准。
从那以后我每次改这套 SHA256 的流水线或者加法器结构,都强制走一遍「随机激励 + 边界场景 + 黄金版本比对」的流程,哪怕只改了一行。希望帮到你。
本文还有配套的精品资源,点击获取