- Verilog:用于描述硬件电路的并行结构、时序和寄存器传输,它的核心是并发执行的模块和门级元件。
- C:一种顺序执行的软件编程语言,用于描述算法和软件流程,它的核心是按顺序执行的指令。
“转换”的真正含义是用 C 语言来模拟或实现 Verilog 代码所描述的硬件行为,这通常分为两种主要场景:

(图片来源网络,侵删)
- 行为级建模/仿真:将 Verilog 的
always块(特别是非阻塞赋值<=)转换为 C 中的顺序逻辑,用于功能仿真。 - 寄存器传输级 建模:将 Verilog 的组合逻辑(
assign)和时序逻辑(posedge/negedgeclk)转换为 C 中的函数和状态机。
下面我将详细解释这两种转换的核心思想,并提供一个完整的示例。
核心转换思想
模块 -> 结构体
Verilog 的 module 对应 C 中的 struct(结构体),结构体将模块内部所有的线网、寄存器和输入/输出端口打包在一起。
// Verilog
module my_module (
input wire clk,
input wire rst_n,
input wire [7:0] data_in,
output reg [7:0] data_out
);
// 内部寄存器
reg [7:0] internal_reg;
// ... 逻辑 ...
endmodule
// C
#include <stdint.h> // 用于 uint8_t 等类型
// 对应 Verilog 的 module
struct my_module_t {
// 对应 input/output
uint8_t data_in;
uint8_t data_out;
// 对应内部寄存器
uint8_t internal_reg;
// 需要一个时钟标志来模拟时序
int clk;
int rst_n;
};
线网 和寄存器 -> 变量
wire:在 C 中通常用普通变量(如int)或特定类型(如uint8_t)表示。reg:在 C 中也用变量表示,但它的值会在模拟时钟的边沿被更新。
组合逻辑 (assign) -> 函数
Verilog 中持续赋值的 assign 语句描述的是组合逻辑,即输出信号是输入信号的即时函数,在 C 中,这最自然地对应一个函数。
// Verilog assign out = in1 & in2;
// C
uint8_t combinational_logic(uint8_t in1, uint8_t in2) {
return in1 & in2;
}
时序逻辑 (always @(posedge clk)) -> 函数 + 状态更新
这是转换中最关键也最容易出错的部分,Verilog 的 always @(posedge clk) 块表示在时钟的上升沿执行块内的操作,在 C 中,我们必须显式地模拟时钟的边沿。

(图片来源网络,侵删)
核心技巧:
- 在结构体中保存上一时刻的时钟值(
clk_prev)。 - 在每个仿真步骤,检查当前时钟值和上一时刻时钟值的关系。
clk为高且clk_prev为低,则检测到了上升沿。- 执行时序逻辑块中的代码(通常是非阻塞赋值
<=的逻辑)。 - 更新
clk_prev为当前clk的值。
非阻塞赋值 (<=) 的处理:
非阻塞赋值的语义是“在时钟边沿计算所有右值,然后在时钟边沿结束时更新所有左值”,为了模拟这个行为:
- 在结构体中为每个
reg创建一个“下一状态”变量(internal_reg_next)。 - 在检测到时钟边沿时,将计算结果存入“下一状态”变量。
- 在时钟边沿的末尾,将“下一状态”变量的值复制到“当前状态”变量中。
完整示例:一个带同步复位的4位计数器
让我们通过一个完整的例子来理解这个过程。
Verilog 代码
// counter.v
module counter (
input clk, // 时钟
input rst_n, // 低电平有效复位
output reg [3:0] count // 4位计数器输出
);
// 在时钟上升沿,如果复位无效,则计数器加1
// 如果复位有效,则计数器清零
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count <= 4'b0000; // 复位时清零
end else begin
count <= count + 1; // 计数
end
end
endmodule
C 语言转换
我们将创建一个 C 程序来模拟这个计数器的行为。
// counter_sim.c
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
// 1. 定义对应 module 的结构体
typedef struct {
// 输入/输出端口
uint8_t clk;
uint8_t rst_n;
uint8_t count; // 当前状态
// 用于模拟时序的内部变量
uint8_t clk_prev; // 上一时刻的时钟值
uint8_t count_next; // 计算出的下一状态值
} Counter;
// 2. 初始化函数,对应 Verilog 的复位
void counter_init(Counter* c) {
c->clk = 0;
c->clk_prev = 0;
c->rst_n = 0; // 初始时复位有效
c->count = 0;
c->count_next = 0;
}
// 3. 核心时序逻辑处理函数
void counter_update(Counter* c) {
// --- 检测时钟边沿 ---
// 上升沿条件: 当前 clk 为 1, 上一时刻 clk 为 0
bool is_posedge = (c->clk == 1 && c->clk_prev == 0);
// --- 执行时序逻辑 (always 块内的代码) ---
if (is_posedge) {
// 复位优先级更高
if (!c->rst_n) {
c->count_next = 0; // 计算下一状态
} else {
c->count_next = c->count + 1; // 计算下一状态
}
}
// --- 更新状态 ---
// 在时钟边沿结束时,将下一状态赋给当前状态
if (is_posedge) {
c->count = c->count_next;
}
// --- 更新时钟历史 ---
c->clk_prev = c->clk;
}
// 4. 主仿真循环
int main() {
Counter my_counter;
counter_init(&my_counter);
printf("Time | Clk | Rst_n | Count\n");
printf("-----|-----|-------|------\n");
// 模拟10个时钟周期
for (int time = 0; time < 20; time++) {
// 生成时钟信号 (0, 1, 0, 1, ...)
my_counter.clk = (time % 2 == 0) ? 0 : 1;
// 在第5个周期释放复位
if (time == 5) {
my_counter.rst_n = 1;
}
// 在第15个周期再次拉低复位
if (time == 15) {
my_counter.rst_n = 0;
}
// 调用更新函数,模拟一个时钟周期的行为
counter_update(&my_counter);
// 打印当前状态
printf("%4d | %d | %d | %d\n", time, my_counter.clk, my_counter.rst_n, my_counter.count);
}
return 0;
}
编译和运行
gcc counter_sim.c -o counter_sim ./counter_sim
预期输出
你会看到计数器在复位有效时保持为0,复位释放后在时钟上升沿开始计数,再次被复位后又清零。
Time | Clk | Rst_n | Count
-----|-----|-------|------
0 | 0 | 0 | 0
1 | 1 | 0 | 0 (上升沿,复位有效,清零)
2 | 0 | 0 | 0
3 | 1 | 0 | 0 (上升沿,复位有效,清零)
4 | 0 | 0 | 0
5 | 1 | 1 | 0 (上升沿,复位释放,count+1 -> 1)
6 | 0 | 1 | 1
7 | 1 | 1 | 2 (上升沿,count+1 -> 2)
8 | 0 | 1 | 2
9 | 1 | 1 | 3 (上升沿,count+1 -> 3)
10 | 0 | 1 | 3
11 | 1 | 1 | 4 (上升沿,count+1 -> 4)
12 | 0 | 1 | 4
13 | 1 | 1 | 5 (上升沿,count+1 -> 5)
14 | 0 | 1 | 5
15 | 1 | 0 | 5 (上升沿,复位有效,清零 -> 0)
16 | 0 | 0 | 0
17 | 1 | 0 | 0 (上升沿,复位有效,清零)
18 | 0 | 0 | 0
19 | 1 | 0 | 0 (上升沿,复位有效,清零)
高级主题和注意事项
-
initial块:Verilog 的initial块在仿真开始时执行一次,在 C 中,这通常对应main函数开头的初始化代码或一个单独的初始化函数。 -
阻塞赋值 () vs. 非阻塞赋值 (
<=):- 阻塞赋值 ():在
always块中按顺序执行,类似于 C 的赋值语句,在组合逻辑always @(*)块中必须使用。 - 非阻塞赋值 (
<=):在时序逻辑always @(posedge clk)块中使用,用于描述寄存器的行为。必须使用“下一状态”变量的方法来模拟,否则会得到错误的结果(相当于锁存器行为)。
- 阻塞赋值 ():在
-
for循环和while循环:- 在 Verilog 中,
for/while循环是可综合的,但它们会被展开成硬件(通常是多路选择器或移位寄存器),并且循环次数必须是编译时常数。 - 在 C 中,循环就是软件循环。
- 转换时,Verilog 的硬件展开循环对应 C 的循环,但如果 Verilog 循环用于生成重复硬件(如多位加法器),C 中可能需要用循环来逐位处理。
- 在 Verilog 中,
-
任务 (
task) 和函数 (function):- Verilog 的
function返回一个值,可以直接转换为 C 的函数。 - Verilog 的
task可以有输入/输出/双向端口,并且可以包含时间控制(如#1),转换为 C 时,它也变成一个函数,但需要通过结构体指针来传递和修改状态。#1这样的延迟在 C 仿真中通常用sleep()或简单的空循环来模拟,但这会改变行为的时序。
- Verilog 的
-
模拟器 vs. 实现:
- 上述 C 代码是一个行为级模型,用于验证功能是否正确,但它不能被综合成硬件。
- 如果你想将 Verilog 直接编译成可以在 CPU 上运行的 C 代码,你需要使用高层次综合 工具,HLS 工具接受一个 RTL 或更高层次的 Verilog/VHDL 描述,并自动将其转换为 C/C++ 或其他软件代码,同时优化性能和面积,这是一个比手动转换复杂得多的自动化过程。
手动将 Verilog 转换为 C 的关键步骤可以总结为:
- 分析 Verilog 代码:识别出模块、组合逻辑、时序逻辑、复位逻辑。
- 定义 C 结构体:为每个 Verilog 模块创建一个 C 结构体,包含所有寄存器、线网和端口。
- 处理组合逻辑:将
assign和组合always块转换为 C 函数。 - 处理时序逻辑:
- 在结构体中添加
clk_prev变量。 - 添加“下一状态”变量(如
reg_next)来模拟非阻塞赋值。 - 编写一个更新函数,在其中检测时钟边沿。
- 在边沿内计算下一状态,并在边沿结束时更新当前状态。
- 在结构体中添加
- 编写仿真主循环:在
main函数中生成测试激励(时钟、复位、数据),并循环调用更新函数来推进仿真。
通过这种方法,你可以用 C 语言有效地模拟和理解 Verilog 代码的硬件行为。
