一·
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)内部结构:
-
可编程互连线(PI,即Programmable Interconnect):这是CPLD的"神经网络",负责将芯片内各个逻辑块连接起来,实现信号的路由。
-
可编程逻辑块(LB,即Logic Block) :这是CPLD的"大脑"。每个逻辑块内部包含多个宏单元(Macrocell)。
-
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),不能直接赋值。初学者应谨慎使用,优先使用input和output组合替代。
位宽:
位宽定义了信号是标量(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) 可以是任何表达式,包含 wire、reg、常量、操作符等。
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,括号内是连接到该端口的信号 ,可以是上层模块的wire、reg、常量或未连接的悬空端口。
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)连接常量:恒定电平
在调试或测试时,可以将子模块的端口连接到固定的0或1。
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