Zynq基础与FPGA开发入门 - 第3节
一、数字逻辑基础:从与或非门到触发器
各位同学,咱们今天聊聊FPGA的根基——数字逻辑。说实话,我刚开始学FPGA时,总觉得这些基础电路离实际开发很远。后来做了几个项目才发现,很多bug的根源,恰恰是这些最基础的东西没搞透。
1.1 与或非门:数字世界的原子
数字电路里,与门、或门、非门就是最基本的"原子"。你想想看,再复杂的处理器,拆到最底层也就是这些门的组合。
- 与门(AND):两个输入都为1,输出才为1。用乘法表示:Y = A · B
- 或门(OR):只要有一个输入为1,输出就是1。用加法表示:Y = A + B
- 非门(NOT):输入取反。Y = ¬A
我个人的小习惯:写Verilog时,我更喜欢用逻辑运算符 &、|、~ 而不是直接用门级原语。代码可读性会好很多,尤其是做复杂逻辑时。
举个实际例子。我在做某个通信接口时,需要检测一个8位总线是否全为0。用与门组合?太麻烦。直接写 assign all_zero = ~|data_bus; 一行搞定。这就是组合逻辑的魅力。
1.2 触发器:让电路记住状态
组合逻辑只能处理当前输入,但实际系统需要"记忆"。触发器就是干这个的。
最常见的D触发器,说白了就是一个"采样保持器"。时钟上升沿来的时候,它把输入D的值"拍"到输出Q上,然后保持住,直到下一个时钟沿。
避坑指南:我曾经在项目里犯过一个低级错误——把触发器的时钟和复位信号搞反了极性。结果仿真全对,上板全错。后来花了整整两天才定位到问题。记住:时钟沿和复位电平一定要和你的设计意图一致。
触发器的几个关键参数,我建议你记牢:
| 参数 | 含义 | 典型值 |
|---|---|---|
| 建立时间(tsu) | 数据在时钟沿前必须稳定的时间 | 0.2~0.5 ns |
| 保持时间(th) | 数据在时钟沿后必须稳定的时间 | 0.1~0.3 ns |
| 时钟到输出延迟(tco) | 时钟沿到输出稳定的时间 | 0.3~0.8 ns |
嗯,这些时序参数在Vivado的时序报告里会反复出现。你跑一次综合、实现,就能看到它们。
二、Verilog HDL基础语法:从零开始写代码
Verilog,说白了就是描述数字电路的"语言"。它不像C语言那样顺序执行,而是描述硬件结构。这个思维转变,是很多初学者最难受的地方。
2.1 模块:最小的设计单元
每个Verilog设计都以 module 开始,endmodule 结束。模块就是你的"黑盒子"——定义输入输出,内部实现功能。
module counter (
input wire clk, // 时钟
input wire rst_n, // 复位,低有效
output reg [3:0] count // 4位计数器输出
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'd0;
else
count <= count + 1'b1;
end
endmodule
这段代码,我估计你以后会写几百遍。一个简单的计数器,包含了时钟、复位、寄存器、赋值——这些都是FPGA设计的核心要素。
2.2 数据类型:wire 和 reg
初学者最容易搞混的就是 wire 和 reg。我直接说结论:
- wire:组合逻辑连线。用
assign赋值。 - reg:寄存器或组合逻辑变量。在
always块里赋值。
注意:reg 不一定是寄存器!在组合逻辑的 always 块里,reg 综合出来只是连线。这个坑我见过太多人踩了。判断标准很简单:看 always 块的敏感列表里有没有时钟沿。有时钟沿才是寄存器,否则就是组合逻辑。
2.3 运算符:别小看这些符号
Verilog的运算符和C语言很像,但有几个需要特别注意:
| 运算符 | 含义 | 示例 |
|---|---|---|
| & | 按位与 | 4'b1010 & 4'b1100 = 4'b1000 |
| | | 按位或 | 4'b1010 | 4'b1100 = 4'b1110 |
| ^ | 按位异或 | 4'b1010 ^ 4'b1100 = 4'b0110 |
| && | 逻辑与 | 用于条件判断 |
| || | 逻辑或 | 用于条件判断 |
| {} | 拼接运算符 | {a, b} 把a和b拼起来 |
我个人最常用的是拼接运算符。比如要生成一个8位的独热码,assign one_hot = 1'b1 << position; 比写一堆 case 语句方便多了。
三、组合逻辑与时序逻辑:理解本质区别
这个知识点,我建议你花点时间彻底搞懂。因为FPGA里90%的bug,都出在混淆了这两种逻辑。
3.1 组合逻辑:输入决定输出
组合逻辑的输出只取决于当前输入。没有记忆,没有时钟。典型的例子:加法器、多路选择器、译码器。
// 组合逻辑:4选1多路选择器
module mux4 (
input [1:0] sel,
input [3:0] data_in,
output reg data_out
);
always @(*) begin
case (sel)
2'b00: data_out = data_in[0];
2'b01: data_out = data_in[1];
2'b10: data_out = data_in[2];
2'b11: data_out = data_in[3];
default: data_out = 1'b0;
endcase
end
endmodule
注意看,always 块的敏感列表是 @(*),表示所有输入信号变化都会触发。这就是组合逻辑的标志。
3.2 时序逻辑:时钟驱动一切
时序逻辑的输出不仅取决于当前输入,还取决于之前的状态。说白了,它有"记忆"。触发器、计数器、状态机都是时序逻辑。
// 时序逻辑:带使能的8位移位寄存器
module shift_reg (
input wire clk,
input wire rst_n,
input wire en,
input wire din,
output reg [7:0] dout
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
dout <= 8'd0;
else if (en)
dout <= {dout[6:0], din}; // 左移,低位补入新数据
end
endmodule
关键区别:组合逻辑用 =(阻塞赋值),时序逻辑用 <=(非阻塞赋值)。这个规则,我建议你当成铁律来遵守。我在代码评审时,看到混用赋值方式的代码,基本都会打回去重写。
四、Vivado工程创建与仿真:动手实践
理论说完了,咱们来点实际的。Vivado是Xilinx的集成开发环境,咱们用它来创建工程、写代码、跑仿真。
4.1 创建工程
步骤很简单,但有几个细节要注意:
- 打开Vivado,点击 Create Project
- 选择 RTL Project,勾选 Do not specify sources at this time(后面再添加文件)
- 选择芯片型号。咱们课程用 xc7z020clg400-1(Zynq-7020)
- 点击 Finish,工程就创建好了
我的建议:工程命名最好有规律,比如 lab1_counter、lab2_uart。别用 test、project1 这种名字。不然三个月后你自己都找不到哪个工程是干嘛的。
4.2 添加设计文件与仿真
在工程里右键 Add Sources,选择 Add or create design sources,然后新建一个Verilog文件。把咱们刚才的计数器代码写进去。
仿真才是验证设计的核心。Vivado自带的仿真器够用,咱们写个testbench:
`timescale 1ns / 1ps
module tb_counter;
reg clk;
reg rst_n;
wire [3:0] count;
// 实例化被测试模块
counter uut (
.clk (clk),
.rst_n (rst_n),
.count (count)
);
// 生成时钟:10ns周期,100MHz
always #5 clk = ~clk;
// 测试激励
initial begin
clk = 0;
rst_n = 0;
#20;
rst_n = 1;
#200;
$finish;
end
endmodule
跑仿真时,记得在 Simulation 里选择 Run Behavioral Simulation。然后就能看到波形了。
我曾经犯过的错:仿真通过就以为设计没问题,直接上板。结果板子跑起来完全不对。后来发现是testbench里忘了给复位信号加足够的延迟。记住:仿真通过只是第一步,时序约束、上板调试才是真正的考验。
本章知识体系
下面这张图,是我自己总结的本章知识脉络。你把它存下来,学完这章后对照着回顾一遍,效果会好很多。
好了,第一章的内容就到这里。数字逻辑是FPGA的基石,Verilog是描述工具,Vivado是实践平台。这三者缺一不可。下一章咱们会深入讲Verilog的进阶语法和模块化设计,到时候见。