Verilog语言学习(一)

一·

1,集成电路发展

(1)片上系统(System on Chip,Soc):一种将完整的电子系统(包含处理器、内存、各种接口等)集成在单个芯片上的集成电路技术。包含中央处理器(CPU),图形处理器(GPU),内存与存储(RAM,ROM,FLASH),通信与接口(无线模块,USB,HDMI等物理接口),专用协处理器。

专用集成电路(Application-Specific Integrated Circuit):为某一特定用途或特定算法而专门定制设计的芯片。

(2)系统设计工作:逻辑设计(前端)->电路实现(后端)->验证

(3)根据多年的发展形成Verilog HDL和VHDL两种典型的语言。随后又发展出来System C和System Verilog语言

2,Verilog HDL和VHDL

(1)发展历史

HDL(硬件描述语言):

Verilog发展重要节点:1995年Verilog HDL的IEEE标准,2001年的Verilog HDL 1364-2001标准,2005年的Verilog HDL 1364-2005标准。

VHDL发展重要节点:IEEE标准的1076-1993版本。

(2)两者比较:

3,FPGA与CPLD

1,可编程逻辑器件(PLD,即Programmable Logic Device):由工程师通过软件工具编程来定义其内部逻辑功能的集成电路。

早期可编程逻辑器件:可编程只读存储器(PROM),紫外线可擦除只读存储器(EPROM),电可擦只读存储器(EEPROM)

PAL/GAL:

(1)PAL(Programmable Array Logic):由一个可编程的"与"阵列 和一个固定的"或"阵列组成。内部的连接点是熔丝或反熔丝。工程师通过编程器施加高压电流,将不需要的熔丝"烧断",从而定义逻辑功能,但是一旦烧断就无法恢复。如果你写错了逻辑,或者想修改设计,这颗芯片就只能报废。

(2)GAL(Generic Array Logic):与PAL类似,也是"可编程与阵列 + 固定或阵列"。但GAL最大的创新在于其输出端------**电可擦写(E²CMOS)。**如果逻辑写错了,只需要用编程器执行一次擦除操作,芯片就能恢复如新,重新编程。

(3)比较:

CPLD(复杂可编程逻辑器件,Complex Programmable Logic Device):

由GAL和PAL这类简单的PLD发展而来

(1)内部结构:

  1. 可编程互连线(PI,即Programmable Interconnect):这是CPLD的"神经网络",负责将芯片内各个逻辑块连接起来,实现信号的路由。

  2. 可编程逻辑块(LB,即Logic Block) :这是CPLD的"大脑"。每个逻辑块内部包含多个宏单元(Macrocell)

  3. I/O(输入/输出)控制块:负责管理芯片引脚,可以配置为输入、输出或双向端口,并控制其电气特性。

补充:**宏单元,**通常包含乘积项阵列(用于实现组合逻辑),可配置触发器(用于实现时序逻辑),选择器(MUX,用于配置输出是组合信号还是寄存信号)

(2)工作流程:定义逻辑->编译与适配->映射与布线->生成熔丝图->编程(烧写)->上电运行

**FPGA(现场可编程门阵列,Field-Programmable Gate Array):**当前可编程逻辑器件(PLD)家族中集成度最高、功能最强大的成员

(1)常见内部结构:

1,可配置逻辑块(CLB,即Configurable Logic Block):,分布在整个芯片上,是构成所有逻辑功能的最基本单元。

(1)查找表(LUT,即Look-Up Table):这是FPGA实现组合逻辑的核心。你可以把LUT理解为一个小的RAM(随机存取存储器),它通过"查表"的方式来实现任何输入组合对应的输出。例如,一个4输入LUT可以存储16个值,实现所有4变量的逻辑函数。

(2)触发器(FF,即Flip-Flop):用于存储状态,是实现时序逻辑(如计数器、寄存器、状态机)的基础。

2,可编程互连资源(Interconnect) :这是FPGA的"神经网络",它将芯片上所有的CLB、存储器和硬核IP按照你设计的需求连接起来。这些互连线由许多可编程开关(如多路复用器、传输管) 组成,通过配置这些开关的通断来决定信号的路径。

3,输入/输出块(IOB,即Input/Output Block):这是FPGA与外部世界的"接口",负责管理芯片的物理引脚。它可以被配置为输入、输出或双向端口,并支持不同的电压标准和驱动电流。

4,片内存储器(BRAM,即Block RAM):这是FPGA内部的"仓库",是专用的、大块的存储资源,用于高效地存储大量数据(如缓存、FIFO队列),通常以块为单位(例如18Kb或36Kb)。

5,时钟管理单元(CMT,即Clock Management Tile):这是FPGA的"心脏节律器",包含锁相环(PLL)和数字时钟管理器(DCM),可以对输入时钟进行分频、倍频、相位偏移等操作,为芯片不同区域提供精准、低抖动的时钟信号。

6,硬核IP(Hard IP) :这是FPGA的"专精特长",是固化在硅片上的专用硬件电路。例如,用于高速乘法累加运算的 DSP(数字信号处理器)块 ,用于高速通信的 PCIe(高速串行计算机扩展总线标准)、以太网、SerDes(串行解串器) 收发器等。硬核IP相比用LUT搭建的"软核"性能更高、功耗更低。

(2)特点:

并行处理能力:CPU是串行执行的,一秒算完一个数再算下一个。而FPGA内部可以有成千上万个独立的逻辑块,它们可以同时运算

硬件可重构:可以通过重写配置,让同一块FPGA在一小时内从"高清视频编码器"变成"比特币矿机控制器",再变成"5G基带处理器"。这种灵活性是ASIC(专用集成电路)无法比拟的。

(3)与CPLD的区别

SOPC(片上可编程系统,System on a Programmable Chip):处理器(CPU)、存储器、I/O接口 以及各种用户自定义的数字逻辑模块 ,全部集成到同一片FPGA芯片 中的设计技术。即SOPC = FPGA硬件 + 处理器软核/硬核 + 可裁剪的外设系统

二,Verilog HDL基础

Verilog HDL(Verilog Hardware Description Language),即Verilog硬件描述语言 ,是用于描述数字电路和系统的结构、行为的一种专业语言。

1,基本单元-模块(module)

用于描述某个设计的功能或结构及其与其他模块通信的外部接口。一个模块的定义包含接口与内部实现,实际意义是硬件电路上的逻辑实体,可以是从简单的门道整个的大系统。

cpp 复制代码
module <模块名> ( <端口列表> );
    // 1. 端口声明 (方向与数据类型)
    // 2. 参数声明 (可选, 用于增强灵活性)
    // 3. 内部信号声明 (wire, reg等)
    // 4. 功能描述 (assign语句、always块、实例化子模块等)
endmodule

注意:1,模块取名要避免采用Verilog的保留字和Quartus内置门的名字。

2,每个模块以关键字module开始,以endmodule结尾。

3,模块名称后面的括号内是模块的端口列表,有输入端口和输出端口,要在模块的开始部分就全部列出来。

4,逻辑功能描述部分,最基本的描述方式:always,assign和创建模块实例。一个模块中允许使用一种或多种方法描述逻辑功能。

5,每一条语句必须以分号结尾。

6,可以用/*···*/和//···对Verilog HDL程序任何部分进行注释,前者为多行注释符,后者为单行注释符。多行注释不可嵌套。

模块之间是并行运行的,且模块实例化使得模块具备层次化,可以构建一个简单的系统。

高层模块可以通过调用,连接低层模块来实现复杂功能,各模块连接需要用一个顶层模块(Top-module)完成整个系统。

Verilog HDL程序文件的后缀都是.v

2,Verilog HDL程序的基本结构

cpp 复制代码
// ============================================
// 1. 文件头注释(工程规范,非语法必需)
// ============================================
// 文件名    : counter_top.v
// 功能      : 顶层计数器模块,包含时钟分频与LED显示
// 作者      : 
// 创建日期  : 
// 修改记录  : 
// ============================================

// ============================================
// 2. `timescale 编译指令(通常放在模块定义之前)
// ============================================
`timescale 1ns / 1ps    // 定义仿真时间单位与精度,仅对仿真有效

// ============================================
// 3. 模块定义(核心部分)
// ============================================
module counter_top (     // 顶层模块名
    // 端口列表:模块与外部世界的所有接口
    input  wire       clk,      // 系统时钟
    input  wire       rst_n,    // 异步复位(低有效)
    output wire [3:0] led       // 4位LED输出
);

    // --- 3a. 内部信号声明 ---
    wire        clk_div;        // 分频后的时钟信号
    reg  [3:0]  cnt;            // 计数器值

    // --- 3b. 底层模块实例化(结构描述) ---
    // 实例化一个时钟分频器模块
    clk_divider u_div (
        .clk_in  (clk),
        .rst_n   (rst_n),
        .clk_out (clk_div)
    );

    // 实例化一个4位计数器模块
    counter_4bit u_cnt (
        .clk     (clk_div),
        .rst_n   (rst_n),
        .count   (cnt)
    );

    // --- 3c. 数据流描述(组合逻辑赋值) ---
    assign led = cnt;           // 将计数值直接输出到LED

    // --- 3d. 行为描述(时序/组合逻辑 always块) ---
    // 可选的其他逻辑...

endmodule                   // 模块结束

// ============================================
// 4. 其他辅助模块定义(可在一个文件里定义多个模块)
// ============================================
module clk_divider ( ... );
    // ...
endmodule

module counter_4bit ( ... );
    // ...
endmodule

(1)模块声明

标准格式包含三个主要部分:模块名端口列表 (可选)和 参数声明(可选)。

cpp 复制代码
module <module_name> #(  
    // 参数声明 (Parameter List) - 可选,用于使模块可配置
    parameter WIDTH = 8
) (
    // 端口列表 (Port List) - 可选,没有端口的模块通常是顶层测试模块
    input  wire             clk,    // 输入端口:时钟
    input  wire             rst_n,  // 输入端口:复位
    input  wire [WIDTH-1:0] data_in, // 输入端口:数据输入,位宽由参数控制
    output reg  [WIDTH-1:0] data_out  // 输出端口:数据输出
);
// ... 内部功能描述 ...
endmodule

注意事项:

1,端口列表可以为空

cpp 复制代码
module tb_my_design;   // 无端口列表
    // 内部生成时钟、复位等激励信号,并实例化待测模块
endmodule

2,inout端口:用于双向数据总线(如I2C的SDA、数据总线),设计时应谨慎使用,通常需要三态控制逻辑。

3,逗号和分号: 端口列表中,每个端口声明后使用逗号(,)分隔 (最后一个端口不加逗号),整个端口列表结束时使用分号(;)。

cpp 复制代码
module test (
    input  wire a,   // 逗号分隔
    input  wire b,   // 逗号分隔
    output wire c    // 最后一个不用逗号
);                   // 分号结束端口列表

4,层次化设计中顶层模块的声明是特殊的,它的端口会映射到FPGA芯片的物理引脚 或ASIC的芯片引脚 。顶层模块可以实例化多个子模块,形成一个设计树。顶层模块通常不声明参数(或参数仅在内部使用),因为它的功能对于整个系统是固定的。

cpp 复制代码
// 顶层模块:端口直接对应FPGA引脚
module top (
    input  wire       sys_clk,   // 来自晶振
    input  wire       sys_rst_n, // 来自复位按键
    output wire       tx,        // UART发送引脚
    input  wire       rx         // UART接收引脚
);
    // 实例化子模块
    uart_ctrl u_uart (
        .clk  (sys_clk),
        .rst_n(sys_rst_n),
        .tx   (tx),
        .rx   (rx)
    );
endmodule

(2)端口定义

本质上是在声明模块的输入(input)输出(output)双向(inout) 引脚,同时指定这些引脚的数据类型和位宽。

Verilog-2001 ANSI-C风格:所有端口属性(方向、数据类型、位宽、名称)整合在一行中声明。

cpp 复制代码
module counter (
    input  wire       clk,     // 输入端口,wire类型(可省略),1位
    input  wire       rst_n,   // 输入端口,wire类型
    output reg  [7:0] cnt      // 输出端口,reg类型,8位宽(在always块中赋值)
);
    // ... 功能描述
endmodule
端口方向:

信号在模块边界上的流向:

input端口 :在模块内部只能被读取,不能被赋值。它必须始终是wire类型(默认)。

output端口 :在模块内部可以被驱动(赋值)。如果需要在always块中赋值,必须声明为reg类型;如果由assign语句或子模块实例化驱动,则声明为wire类型(默认)。

inout端口 :在模块内部必须连接到三态缓冲器(用assign控制高阻态z),不能直接赋值。初学者应谨慎使用,优先使用inputoutput组合替代。

位宽

位宽定义了信号是标量(1位) 还是向量(多位)。

(1)标量 :不指定位宽,默认1位。例如 input wire clk

(2)向量 :使用 [MSB:LSB] 语法,定义从最高位(MSB)到最低位(LSB)的范围。通常LSB为0 ,例如 [7:0] 表示8位,从bit7到bit0。

端口连接规则:

1,模块内部使用端口:

(1)input端口 :在模块内部视为只读信号 ,可以直接使用,但不能出现在赋值语句的左侧。

cpp 复制代码
wire sum = a + b;   // 正确:a和b都是input,被读取
// assign a = 1'b0; // 错误:不能给input赋值

(2)output端口 :在模块内部视为驱动信号,必须被赋值。

cpp 复制代码
assign result = a & b;      // 正确:result是output,由assign驱动
// 或者
always @(posedge clk) begin
    result <= a;            // 正确:result声明为reg,在always中赋值
end

(3)数据类型说明

cpp 复制代码
module example (
    input  wire       a,        // input 只能是 wire,显式写出更清晰
    output wire       b,        // wire 类型,由 assign 驱动
    output reg        c         // reg 类型,由 always 块驱动
);
    assign b = ~a;              // wire 类型用 assign 赋值
    
    always @(posedge clk) begin
        c <= a;                 // reg 类型在 always 块中赋值
    end
endmodule

3,逻辑功能描述

(1)assign连续赋值语句

可理解为理解为在硬件上直接"焊接"导线。它描述了一种持续性的、实时的驱动关系:只要等式右边的信号发生变化,等式左边的信号就会立即(在仿真中无延迟) 跟着变化

本质:assign 语句用于驱动线网(net)类型 的信号(主要是wire)。它表示一种组合逻辑关系,不涉及时钟,没有存储功能。

cpp 复制代码
assign <线网类型信号> = <表达式>;

注意:1,assign左侧(LHS) 必须是一个线网类型(net) ,最常见的就是 wire不能是 reg 类型

2,assign右侧(RHS) 可以是任何表达式,包含 wirereg、常量、操作符等。

3,assign 语句是并行执行的,不依赖于代码书写顺序(这与软件编程语言有本质区别)。

基本用法

(1)基本逻辑门

cpp 复制代码
wire        a, b;
wire        y_and, y_or, y_not;

assign y_and = a & b;      // 与门
assign y_or  = a | b;      // 或门
assign y_not = ~a;         // 非门(取反)

(2)算数运算

cpp 复制代码
wire [7:0]  A, B;
wire [7:0]  sum, diff;

assign sum  = A + B;       // 8位加法器
assign diff = A - B;       // 8位减法器

(3)条件选择(三目运算符):(?:

cpp 复制代码
wire        sel;
wire [7:0]  data_in_0, data_in_1;
wire [7:0]  data_out;

assign data_out = sel ? data_in_1 : data_in_0;
// 等价于一个2选1多路选择器(MUX)

(4)拼接与截取({}

cpp 复制代码
wire [3:0]  nibble_high, nibble_low;
wire [7:0]  byte_data;

assign byte_data = {nibble_high, nibble_low};   // 拼接成8位
assign {nibble_high, nibble_low} = byte_data;  // 拆分赋值(需要两侧位宽一致)

(5)三态缓冲器(双向端口)

assign 是实现双向端口(inout)的唯一方式,通过控制高阻态 z 实现。

cpp 复制代码
wire        oe;       // 输出使能
wire [7:0]  data_out; // 内部输出数据
wire [7:0]  data_pad; // 双向引脚

assign data_pad = oe ? data_out : 8'bz;  // 当oe为1时输出数据,否则输出高阻

(2)调用元件

调用元件(Module Instantiation)结构建模(Structural Modeling) 的核心手段。它指的是在一个模块(称为父模块上层模块 )内部,引用并生成另一个模块(称为子模块下层模块 )的一个或多个实例(Instance)

模块本身不直接工作,只有被实例化(调用)后,才会在芯片上生成对应的物理电路。

语法格式:
cpp 复制代码
<子模块名> #(
    // 参数传递(Parameter Override),可选
    .参数名1 (值或上层信号),
    .参数名2 (值或上层信号)
) <实例名> (
    // 端口连接
    .端口名1 (连接到的上层信号或线网),
    .端口名2 (连接到的上层信号或线网)
);

注意:1,实例名必须唯一,同一父模块中,实例名不能重复。

2,端口名前的点号(.)是必须的,表示'这是子模块的端口'。

3,括号内是连接到该端口的信号 ,可以是上层模块的wirereg、常量或未连接的悬空端口。

4,顺序与声明顺序无关:端口连接的顺序可以随意排列,编译器根据端口名匹配。

5,每条端口连接以逗号(,)结尾,最后一条不加逗号。

端口连接方式:
三种连接方式:

(1)正常连接:信号连接到上层线网

cpp 复制代码
wire [7:0] data_from_sub;
my_module u_my (
    .data_out (data_from_sub)  // 子模块输出连接到上层 wire
);

(2)未连接:端口悬空

如果某个端口在实例化时不需要连接 ,可以在括号内留空,或使用逗号占位

  • 输入端口(input)悬空 :综合工具会将其视为未驱动 ,默认值为z(高阻),但在综合时可能会产生警告或错误。

  • 输出端口(output)悬空:通常合法,表示我们不关心该输出信号。

cpp 复制代码
my_module u_my (
    .data_in (),   // 输入端口悬空------通常不推荐,容易造成未知状态
    .data_out (),  // 输出端口悬空------合法,表示不使用该输出
    .clk   (sys_clk)
);

(3)连接常量:恒定电平

在调试或测试时,可以将子模块的端口连接到固定的01

cpp 复制代码
my_module u_my (
    .enable (1'b1),       // 连接常量高电平
    .mode   (2'b00),      // 连接常量2位0
    .clk    (sys_clk)
);
参数传递:

元件调用不仅能连接端口,还能通过参数传递(#())覆盖子模块中定义的parameter,实现高度可重用的设计。

cpp 复制代码
// 子模块定义(带参数)
module fifo #(
    parameter DATA_WIDTH = 8,
    parameter DEPTH = 16
)(
    input  wire clk,
    input  wire wr_en,
    input  wire [DATA_WIDTH-1:0] wr_data,
    // ...
);

// 顶层实例化------覆盖参数
fifo #(
    .DATA_WIDTH (32),   // 改为32位数据宽度
    .DEPTH      (64)    // 改为深度64
) u_fifo_32x64 (
    .clk    (clk_100M),
    .wr_en  (wr_en),
    .wr_data(data_in_32)
);

// 另一个实例------使用不同的参数
fifo #(
    .DATA_WIDTH (8),
    .DEPTH      (128)
) u_fifo_8x128 ( ... );

层级路径与信号的"层级访问"

在Verilog中,实例化会形成一个树状层次结构 。每个模块实例都有一个唯一的层级路径(Hierarchical Path)

访问规则

  • 设计(RTL) 中,不能跨层级引用信号(综合工具不支持)。

  • 仿真(Testbench) 中,可以通过全路径名(包含所有实例名) 访问任意层级内部的信号,方便调试。

cpp 复制代码
// 顶层 testbench 实例化 top
top u_top (...);

// 在 testbench 的 initial 块中访问 top 内部 u_counter 模块的 cnt 寄存器
initial begin
    #100;
    $display("Counter value = %d", u_top.u_counter.cnt);
end

(3)always过程块赋值

负责描述那些依赖时钟边沿(时序逻辑)电平变化(组合逻辑) 的行为。与assign那种"持续连线"不同,always块描述的是事件驱动的行为:当某个事件发生时,执行块内的语句。

本质:always 块就像一个永远循环执行的硬件进程 ,它只在敏感列表(Sensitivity List) 中指定的事件发生时,才会"醒来"执行一次块内的代码,执行完毕后继续等待下一次事件。

cpp 复制代码
always @(敏感列表) begin
    // 过程赋值语句(只能对 reg 类型赋值)
end
敏感列表

敏感列表决定了always块何时被执行,它是always块最关键的部分。主流的编码风格分为以下三种:

1. 时序逻辑:边沿触发(posedge / negedge):用于描述寄存器

cpp 复制代码
// 时钟上升沿触发,异步复位低有效
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end

一个always块中,最多只能有一个时钟 和一个异步复位信号,且复位信号必须在敏感列表中。

2. 组合逻辑:电平敏感(@(*)

cpp 复制代码
always @(*) begin
    case (sel)
        2'b00: y = a;
        2'b01: y = b;
        2'b10: y = c;
        default: y = 1'b0;
    endcase
end

如果敏感列表不完整(例如只写了@(a or b)但块中读取了c),综合工具会推断出锁存器(Latch) ,而这通常不是设计者的意图。使用@(*)可以彻底避免这个问题。

3. 仿真专用:initial(不可综合)

initial块只执行一次,常用于仿真初始化。不可综合,仅用于Testbench

cpp 复制代码
initial begin
    clk = 1'b0;
    rst_n = 1'b0;
    #100 rst_n = 1'b1;
end
相关推荐
一只小菜鸡..2 小时前
南京大学 操作系统 (JYY) 学习笔记:访问操作系统的对象与“一切皆文件”
笔记·学习
寒月小酒7 小时前
第五章 生成集成 和第六章 RAG评估(all-in-rag学习)
数据库·学习
魔城烟雨7 小时前
从零开始学习betaflight《5-坐标系》
学习
笔夏7 小时前
【移芯平台CAT1模组】烧写固件
stm32·单片机·学习
憧憬成为java架构高手的小白7 小时前
黑马八股--spring框架学习
java·学习·spring
晓梦林8 小时前
[极客大挑战 2019]PHP学习笔记
笔记·学习·php
茯苓gao9 小时前
嵌入式开发笔记:QSerialPort 完整使用指南——从基础 API 到工程实战
笔记·嵌入式硬件·qt·学习
一起努力啊~9 小时前
DataWhale组队学习笔记--llm-algo-leetcode(七)
笔记·学习·llm
min(a,b)10 小时前
学习第 6 天:类型注解、装饰器与高级特性
python·学习