FPGA设计入门:VHDL、Verilog与SystemVerilog核心对比与实战指南
在数字电路设计的广阔天地里FPGA现场可编程门阵列以其高度的灵活性和并行处理能力已成为现代电子系统不可或缺的核心。然而对于许多初学者而言从软件思维跨越到硬件描述语言HDL的世界常常感到无从下手。无论是面对VHDL的严谨语法还是Verilog的C语言风格亦或是SystemVerilog的强大扩展如何选择并高效入门是第一个拦路虎。本文将以科罗拉多大学相关课程与工程实践为脉络系统梳理FPGA设计中的三大硬件描述语言VHDL、Verilog和SystemVerilog。我们将从核心概念对比入手逐步深入到语法精要、设计范式、仿真验证以及实际项目中的应用。无论你是电子工程专业的学生还是希望转向硬件开发的软件工程师都能通过本文构建清晰的FPGA-HDL知识体系并掌握从代码编写到功能验证的完整流程。1. 硬件描述语言与FPGA设计基础在深入具体语言之前我们必须理解硬件描述语言在FPGA设计中的根本角色。这不同于用C或Python编写顺序执行的软件程序。1.1 什么是硬件描述语言硬件描述语言是一种用于描述数字电路结构和行为的专用计算机语言。你可以把它理解为“电路的蓝图”。开发者使用HDL编写代码然后通过专门的综合工具如Vivado、Quartus内的综合器将这些代码“翻译”成由逻辑门、触发器等基本单元构成的网表最终映射到FPGA的可编程逻辑资源上。核心思想是“描述硬件”而非“编写算法”。你的代码中的每一条语句都可能对应着FPGA芯片内部的一条连线、一个逻辑门或一个寄存器。因此HDL代码具有强烈的并行性和时序性。1.2 VHDL、Verilog与SystemVerilog的定位与选择这是初学者最常困惑的问题。三者并非简单的版本迭代关系而是各有侧重。VHDL全称VHSIC Hardware Description Language。语法严谨、强类型类似于Ada语言。它强制要求明确定义信号的数据类型和位宽编译时检查严格有助于在早期发现设计错误。在欧洲和国防航天领域应用广泛。其设计风格更偏向于“系统级”和“文档化”描述。Verilog语法更接近C语言相对简洁灵活学习曲线较平缓。在工业界特别是在亚洲和北美拥有极高的占有率。它非常适合描述门级和RTL级电路。但正因其灵活也更容易写出不可综合或存在隐含错误的代码。SystemVerilog它不仅仅是Verilog的扩展更是一次重大的进化。SystemVerilog在Verilog的基础上引入了强大的验证特性如面向对象、约束随机、功能覆盖率和增强的设计特性如接口、枚举、包。目前在大型复杂FPGA/ASIC项目中设计部分常用Verilog-2005标准而验证环节则广泛采用SystemVerilog。它已成为现代数字验证的事实标准。如何选择学术与入门根据你的教材、课程或实验室环境选择。两者都掌握最佳但初期精通一门即可。Verilog因语法简单常被推荐为第一门HDL。工业与项目关注目标公司或行业的惯例。通信、消费电子多使用Verilog/SystemVerilog航空航天、欧洲企业可能更倾向VHDL。对于验证工程师SystemVerilog是必选项。个人发展建议先掌握Verilog语法和RTL设计思想然后迅速扩展到SystemVerilog用于验证再根据需要了解VHDL。这样的路径性价比最高。1.3 FPGA设计流程概览理解HDL编码在整体流程中的位置至关重要。设计输入使用HDLVHDL/Verilog或原理图进行设计描述。功能仿真使用仿真工具如ModelSim, VCS验证代码的逻辑正确性不考虑时序。综合将HDL代码转换为由基本逻辑单元与门、或门、触发器等构成的网表。实现包含翻译、映射、布局布线。将网表映射到特定FPGA芯片的具体资源上并优化布线路径。时序仿真在布局布线后加入实际的器件延时和布线延时进行仿真验证时序是否满足要求。生成比特流与下载生成可以配置FPGA的二进制文件通过JTAG等方式下载到芯片中运行。2. 环境准备与工具链搭建工欲善其事必先利其器。一个高效的开发环境能极大提升学习和工作效率。2.1 操作系统与硬件操作系统Windows、Linux、macOS均可。工业界Linux服务器环境常见个人学习Windows更方便。FPGA开发板入门推荐带有基础外设如LED、按键、数码管、串口的板卡如Xilinx的Basys3、Zybo或Intel的DE10系列。这不是必须的前期仿真学习可以不用板卡。2.2 核心软件工具FPGA开发离不开厂商提供的集成环境或第三方工具。Xilinx (AMD) 阵营Vivado当前主流的全功能设计套件支持VHDL、Verilog、SystemVerilog。集成了设计、综合、实现、仿真、调试功能。对于初学者其自带的仿真器足够使用。安装从AMD官网下载Vivado HLx WebPACK版本这是免费的支持大部分主流器件。Intel (Altera) 阵营Quartus PrimeIntel FPGA的主开发软件功能与Vivado类似。ModelSim - Intel FPGA Starter Edition通常与Quartus捆绑安装是一款优秀的仿真工具。第三方仿真工具ModelSim/QuestaSimMentor Graphics现Siemens EDA出品行业标准仿真器对SystemVerilog支持好。VCSSynopsys出品高性能仿真器常用于大型ASIC验证。代码编辑器VS Code轻量且强大通过插件如Verilog-HDL/SystemVerilog、VHDL、TerosHDL可以获得优秀的代码高亮、自动补全、 linting语法检查和代码跳转体验强烈推荐。2.3 推荐初学者环境配置对于刚入门的学习者建议采用以下组合以降低初期环境搭建的复杂度安装 Vivado WebPACK选择安装Vivado HL WebPACK并在安装组件中确保勾选“Vivado Simulator”。安装 VS Code从官网下载安装。配置 VS Code 插件搜索并安装Verilog-HDL/SystemVerilog/Bluespec SystemVerilog插件。搜索并安装VHDL插件。可选安装TerosHDL插件它提供了更强大的项目管理、linting和集成功能。这样你可以用VS Code愉快地写代码用Vivado进行综合、实现和仿真形成一个完整的学习闭环。3. Verilog HDL 核心语法与设计入门我们以Verilog为例因为它语法简洁易于建立硬件思维。请记住所有示例都旨在展示硬件描述的思想。3.1 模块Module—— 设计的基本单元模块是Verilog的基本构建块代表一个电路功能单元类似于软件中的函数但本质是描述了一个硬件黑盒。// 文件名and_gate.v // 模块定义一个2输入与门 module and_gate ( input wire a, // 输入端口a wire表示连线类型 input wire b, // 输入端口b output reg y // 输出端口y reg表示寄存器类型这里用于过程赋值 ); // 逻辑功能描述持续赋值 assign y a b; endmodulemodule和endmodule定义了一个模块。端口列表声明了模块与外部世界的接口。input/output定义方向。wire和reg是两种主要的数据类型。简单理解wire表示物理连线其值由驱动源决定reg表示存储单元可以在always或initial块中被赋值。3.2 数据格式与运算符数值表示位宽进制数值如8‘b1100_00118位二进制、4’ha4位十六进制、16‘d25516位十进制。常用运算符按位运算符与|或~非^异或逻辑运算符||!关系运算符!拼接运算符{a, b}将a和b的位拼接起来。移位运算符左移右移3.3 行为描述always块与时序逻辑数字电路分为组合逻辑和时序逻辑。always块是描述它们的关键。组合逻辑示例无时钟// 一个简单的2选1多路选择器 module mux2to1 ( input wire sel, input wire din0, input wire din1, output reg dout ); // always (*) 表示对块内所有输入信号敏感任何输入变化立即执行 always (*) begin if (sel 1b0) dout din0; else dout din1; end endmodule时序逻辑示例有时钟// 一个带异步复位、同步使能的D触发器 module d_flip_flop ( input wire clk, // 时钟 input wire rst_n, // 低电平有效的异步复位 input wire en, // 同步使能 input wire d, // 数据输入 output reg q // 数据输出 ); // always (posedge clk or negedge rst_n) 对时钟上升沿和复位下降沿敏感 always (posedge clk or negedge rst_n) begin if (!rst_n) // 如果复位有效 q 1b0; // 输出清零 else if (en) // 复位无效且使能有效 q d; // 在时钟上升沿锁存输入d // 如果使能无效q保持原值 end endmodule关键区别在时序逻辑的always块中我们使用非阻塞赋值。这模拟了寄存器在时钟边沿同时更新的行为。而在组合逻辑的always块中我们使用阻塞赋值。敏感列表always (...)括号内的信号列表决定了该块何时被执行。posedge表示上升沿negedge表示下降沿。3.4 可综合与不可综合代码并非所有Verilog语法都能被综合工具转换成实际的电路。写可综合代码是RTL设计工程师的核心技能。可综合assign,always(敏感列表需为边沿或*)if-else,case, 基本的运算符模块实例化等。不可综合主要用于仿真initial块除用于初始化寄存器外#延时语句$display等系统任务force/release 复杂的文件操作。4. VHDL 核心语法与设计范式VHDL风格迥异其强类型和显式声明要求设计者更严谨地思考硬件结构。4.1 实体与结构体VHDL设计单元由“实体”和“结构体”组成。-- 文件名and_gate.vhd -- 库声明 library IEEE; use IEEE.STD_LOGIC_1164.ALL; -- 实体声明定义模块接口 entity and_gate is Port ( a : in STD_LOGIC; -- 输入端口类型为标准逻辑 b : in STD_LOGIC; y : out STD_LOGIC -- 输出端口 ); end and_gate; -- 结构体声明定义模块内部行为或结构 architecture Behavioral of and_gate is begin -- 数据流描述方式 y a and b; end architecture Behavioral;entity定义了模块的“引脚”。architecture定义了这些引脚之间的内部连接和行为。一个实体可以有多个结构体代表不同的实现方式。STD_LOGIC是VHDL中用于表示单比特逻辑值的标准类型有‘0’ ‘1’ ‘Z’高阻 ‘X’未知等9种状态。4.2 数据类型与强类型系统VHDL的数据类型系统非常严格不同类型间不能直接赋值必须进行类型转换。STD_LOGIC/STD_LOGIC_VECTOR最常用的逻辑类型和逻辑向量类型。INTEGER整数类型。UNSIGNED/SIGNED来自IEEE.NUMERIC_STD库的无符号和有符号数类型用于算术运算。use IEEE.NUMERIC_STD.ALL; -- 必须使用这个库来进行算术运算 signal data_vec : STD_LOGIC_VECTOR(7 downto 0); signal data_uns : UNSIGNED(7 downto 0); signal data_int : INTEGER; -- 错误示例 data_vec data_uns; -- 类型不匹配无法直接赋值 -- 正确做法 data_vec STD_LOGIC_VECTOR(data_uns); -- 类型转换 -- 正确做法 data_uns UNSIGNED(data_vec);4.3 进程与时序控制VHDL中使用process块来描述顺序行为类似于Verilog的always块。-- 带异步复位的D触发器 entity d_ff is Port ( clk : in STD_LOGIC; rst_n : in STD_LOGIC; d : in STD_LOGIC; q : out STD_LOGIC ); end d_ff; architecture Behavioral of d_ff is begin -- 进程 proc_dff : process(clk, rst_n) begin if rst_n 0 then -- 异步复位 q 0; elsif rising_edge(clk) then -- 检测时钟上升沿 q d; -- 同步数据锁存 end if; end process proc_dff; end Behavioral;process的敏感信号列表放在括号内。rising_edge(clk)是检测时钟上升沿的标准方法。5. SystemVerilog 进阶为验证与设计赋能SystemVerilog极大地提升了验证效率和设计描述能力。这里我们聚焦于对设计者最有用的部分。5.1 新的数据类型logic 一个四态数据类型0,1,Z,X可以替代reg和wire在大多数场景中使用简化了声明。logic [7:0] data_bus; // 可以用于连续赋值也可以用于过程赋值enum 枚举类型提高代码可读性。typedef enum logic [1:0] {IDLE, START, DATA, STOP} state_t; state_t current_state, next_state;structunion 结构体和联合体方便组织相关信号。typedef struct packed { logic valid; logic [31:0] addr; logic [63:0] data; } packet_t; packet_t tx_packet;5.2 接口——简化模块互连在大型设计中模块间大量连线容易出错。SystemVerilog的interface可以将一组相关的信号、协议封装起来。// 定义一个简单的存储器接口 interface mem_if (input logic clk); logic wr_en; logic rd_en; logic [15:0] addr; logic [31:0] wdata; logic [31:0] rdata; logic ready; // 可以在接口内定义时钟块用于同步驱动和采样 clocking cb (posedge clk); default input #1ns output #1ns; // 指定时序偏移 output wr_en, rd_en, addr, wdata; input rdata, ready; endclocking // 也可以定义任务和函数 task automatic do_write(input [15:0] a, input [31:0] d); cb.addr a; cb.wdata d; cb.wr_en 1b1; (cb); while(!cb.ready) (cb); cb.wr_en 1b0; endtask endinterface // 使用接口的顶层模块 module top; logic clk; // 实例化接口 mem_if mem_bus(clk); // 实例化使用该接口的模块 memory_controller u_ctrl (.bus(mem_bus)); sram_model u_sram (.bus(mem_bus)); endmodule在子模块中端口声明简化为module memory_controller (mem_if.bus bus); // 直接使用 bus.wr_en, bus.cb.addr 等 endmodule接口极大地提升了代码的模块化、可维护性和复用性。5.3 断言——内建的检查器断言用于在仿真中主动检查设计是否遵循特定的协议或属性是验证的利器。// 立即断言 always (posedge clk) begin assert (data_valid || !data_ready) else $error(“data_ready asserted without data_valid!”); end // 并发断言 - 更强大常用于形式验证 property p_no_glitch; (posedge clk) disable iff (!rst_n) $rose(req) |- ##[1:3] $rose(ack); // req上升沿后ack在1-3个周期内必须出现上升沿 endproperty assert property (p_no_glitch);6. 完整实战从设计到仿真——一个简单的串行数据接收器让我们用一个完整的微型项目串联所学知识。我们将设计一个简单的串行接收器接收8位数据1位起始位01位停止位1无奇偶校验。6.1 需求分析与模块划分顶层模块uart_rx功能检测起始位在数据位中点采样拼接数据检测停止位输出有效数据。子模块/状态需要一个状态机IDLE, START, DATA, STOP一个波特率时钟生成器或计数器。6.2 Verilog 实现// 文件名uart_rx.v timescale 1ns / 1ps module uart_rx #( parameter CLK_FREQ 100_000_000, // 系统时钟频率 100MHz parameter BAUD_RATE 115200 // 波特率 )( input wire clk, input wire rst_n, input wire rx_serial, // 串行输入数据线 output reg [7:0] rx_data, // 接收到的并行数据 output reg rx_data_valid // 数据有效脉冲高电平一个周期 ); // 计算波特率分频计数值 localparam integer BAUD_CNT_MAX CLK_FREQ / BAUD_RATE; localparam integer SAMPLE_POINT BAUD_CNT_MAX / 2; // 在数据位中点采样 // 定义状态机状态 typedef enum logic [2:0] { IDLE, START_BIT, DATA_BITS, STOP_BIT } state_t; state_t current_state, next_state; reg [15:0] baud_counter; // 波特率计数器 reg [2:0] bit_index; // 数据位索引 (0-7) reg [7:0] data_reg; // 数据移位寄存器 reg sample_en; // 采样使能信号 // 状态机主进程 always (posedge clk or negedge rst_n) begin if (!rst_n) begin current_state IDLE; baud_counter 0; bit_index 0; data_reg 0; rx_data 0; rx_data_valid 0; sample_en 0; end else begin current_state next_state; rx_data_valid 1b0; // 默认无效 // 波特率计数器逻辑 if (baud_counter 0) begin if (sample_en) begin baud_counter BAUD_CNT_MAX - 1; end end else begin baud_counter baud_counter - 1; end // 状态机具体逻辑 case (current_state) IDLE: begin sample_en 1b0; if (rx_serial 1b0) begin // 检测到起始位低电平 next_state START_BIT; sample_en 1b1; // 启动波特率计数 baud_counter BAUD_CNT_MAX - 1; end else begin next_state IDLE; end end START_BIT: begin if (baud_counter 0) begin // 应在起始位中点验证是否为低电平 // 简单实现中我们直接进入数据位 next_state DATA_BITS; bit_index 0; end else begin next_state START_BIT; end end DATA_BITS: begin if (baud_counter 0) begin // 在数据位中点采样 data_reg[bit_index] rx_serial; if (bit_index 3d7) begin // 已采样完8位 next_state STOP_BIT; end else begin bit_index bit_index 1; next_state DATA_BITS; end end else begin next_state DATA_BITS; end end STOP_BIT: begin if (baud_counter 0) begin // 停止位中点期望为高电平 // 如果为高接收成功 rx_data data_reg; rx_data_valid 1b1; next_state IDLE; sample_en 1b0; end else begin next_state STOP_BIT; end end default: next_state IDLE; endcase end end endmodule6.3 编写Testbench进行仿真设计完成后必须通过仿真验证其功能。我们使用SystemVerilog风格的Testbench。// 文件名tb_uart_rx.sv timescale 1ns / 1ps module tb_uart_rx; // 测试参数 localparam CLK_PERIOD 10; // 100MHz时钟周期10ns localparam BIT_PERIOD 1000000000 / 115200; // 波特率115200对应的位周期约8680ns // 连接被测模块的信号 logic clk; logic rst_n; logic rx_serial; logic [7:0] rx_data; logic rx_data_valid; // 实例化被测模块 uart_rx #( .CLK_FREQ(100_000_000), .BAUD_RATE(115200) ) u_uart_rx ( .clk(clk), .rst_n(rst_n), .rx_serial(rx_serial), .rx_data(rx_data), .rx_data_valid(rx_data_valid) ); // 生成时钟 initial begin clk 1b0; forever #(CLK_PERIOD/2) clk ~clk; end // 发送一个字节的UART数据任务 task automatic send_byte(input [7:0] data); integer i; begin rx_serial 1b1; // 空闲状态为高电平 #BIT_PERIOD; // 发送起始位 rx_serial 1b0; #BIT_PERIOD; // 发送8位数据位LSB first for (i0; i8; ii1) begin rx_serial data[i]; #BIT_PERIOD; end // 发送停止位 rx_serial 1b1; #BIT_PERIOD; // 额外空闲时间 #(BIT_PERIOD * 2); end endtask // 主测试过程 initial begin // 初始化 rst_n 1b0; rx_serial 1b1; #100; rst_n 1b1; #100; $display([%0t] Testbench started., $time); // 测试用例1发送数据 8‘h55 (01010101) send_byte(8h55); wait (rx_data_valid 1b1); if (rx_data 8h55) $display([%0t] PASS: Received 0x55, $time); else $display([%0t] FAIL: Expected 0x55, got 0x%h, $time, rx_data); #1000; // 测试用例2发送数据 8‘hAA (10101010) send_byte(8hAA); wait (rx_data_valid 1b1); if (rx_data 8hAA) $display([%0t] PASS: Received 0xAA, $time); else $display([%0t] FAIL: Expected 0xAA, got 0x%h, $time, rx_data); #1000; // 测试用例3发送随机数据 for (int k0; k5; k) begin logic [7:0] random_data; random_data $urandom_range(0, 255); send_byte(random_data); wait (rx_data_valid 1b1); if (rx_data random_data) $display([%0t] PASS: Received 0x%h, $time, random_data); else $display([%0t] FAIL: Expected 0x%h, got 0x%h, $time, random_data, rx_data); #1000; end $display([%0t] All tests completed., $time); $finish; end // 波形dump用于在Vivado/ModelSim中查看波形 initial begin $dumpfile(“wave.vcd”); $dumpvars(0, tb_uart_rx); end endmodule6.4 运行仿真与结果分析在Vivado中将uart_rx.v和tb_uart_rx.sv添加到项目。将tb_uart_rx设置为顶层仿真模块。运行行为仿真。你将在TCL控制台看到打印的测试结果[100] Testbench started. [1086940] PASS: Received 0x55 [2186940] PASS: Received 0xAA ... [xxxxxxx] All tests completed.同时可以打开波形窗口查看rx_serial,rx_data,rx_data_valid,current_state等信号的时序关系直观验证状态机跳转和数据采样是否正确。7. 常见问题与调试技巧7.1 仿真与综合行为不一致现象仿真结果正确但下载到板卡后功能异常。原因未初始化的寄存器在仿真中寄存器可能是X未知但综合后上电状态不确定。务必使用复位信号对所有寄存器进行初始化。时序违例组合逻辑路径延时太大导致建立/保持时间不满足。仿真未考虑实际布线延时。代码不可综合在设计中使用了initial、#delay等仿真语句。解决检查所有always块中的寄存器是否在复位条件下有明确赋值。综合实现后必须查看时序报告确保无时序违例。严格遵守可综合代码风格。7.2 锁存器推断现象综合报告中出现未预期的Latch锁存器可能导致毛刺和静态时序分析困难。原因在组合逻辑的always块或VHDL的process中if或case语句未能覆盖所有可能的输入分支导致输出在某种条件下需要“保持”之前的值综合工具便推断出锁存器。示例// 错误缺少else会推断出锁存器 always (*) begin if (en) q d; end// 正确为所有分支赋值 always (*) begin if (en) q d; else q 1‘b0; // 或保持其他默认值 end解决编写组合逻辑时确保所有输入条件下输出都有明确的赋值。可以为所有信号在always块开头设置一个默认值。7.3 阻塞赋值与非阻塞赋值混用现象仿真行为怪异与预期不符但语法检查无错误。黄金法则在描述时序逻辑的always (posedge clk)块中使用非阻塞赋值。在描述组合逻辑的always (*)块中使用阻塞赋值。不要在同一个always块中混合使用两种赋值方式极其特殊的优化场景除外。解决严格遵循上述法则这是避免难以调试的竞争冒险问题的关键。7.4 仿真速度慢原因Testbench中使用了#延时或者设计本身时钟周期太短。优化在验证主要功能时可以适当提高时钟频率在合理范围内。使用(posedge clk)代替固定的#延时来同步激励。对于需要长时间运行才能触发的场景可以加入断言或检查点一旦触发就结束仿真而不是傻等。8. 最佳实践与工程化建议掌握语法后写出稳健、可维护、高效的RTL代码是下一个目标。8.1 代码风格与可读性命名规范使用有意义的名称如wr_addr而非wa。低电平有效信号以_n结尾如rst_n。时钟信号建议以clk开头复位信号以rst开头。采用统一的命名前缀如axi_开头的信号属于AXI总线。注释在每个模块开头注释其功能、端口说明、参数含义。对复杂的逻辑段或状态机进行注释。参数化设计使用parameter或localparam将代码中的常数如数据位宽、深度、计数器最大值参数化提高模块的复用性。模块划分遵循“高内聚、低耦合”原则。一个模块最好只完成一个明确的功能。模块大小适中便于管理和验证。8.2 可综合设计原则同步设计尽可能使用单一的全局时钟和同步复位。异步复位需要做同步释放处理以避免亚稳态。避免组合逻辑反馈组合逻辑环路会导致振荡和不可预测的行为综合工具也可能报错。小心使用门控时钟门控时钟虽然省电但会带来时钟偏移和毛刺问题增加时序分析复杂度。初学者应使用时钟使能信号代替。寄存器输出模块的输出信号尽量用寄存器打一拍再输出。这有助于改善时序将关键路径限制在模块内部。8.3 验证策略分层验证先对每个子模块进行单元测试再集成进行系统测试。自动化测试编写自检查的Testbench利用assert和$error自动判断测试结果而不是人工看波形。功能覆盖率在SystemVerilog中使用覆盖组收集代码覆盖率和功能覆盖率量化验证进度。形式验证对于控制密集型模块如仲裁器、状态机可以使用形式验证工具进行穷举证明补充仿真验证的不足。8.4 资源与时序优化理解资源报告综合和实现后仔细查看资源利用率报告LUT、FF、BRAM、DSP分析瓶颈。流水线设计对于长组合逻辑路径如大位宽加法器、乘法器插入寄存器进行流水线切割提高系统时钟频率。资源共享如果多个地方使用相同的复杂运算如乘法考虑使用一个共享的运算单元分时复用以面积换速度。使用厂商IP核对于常用功能如FIFO、RAM、DSP、SerDes优先使用FPGA厂商提供的经过高度优化的IP核而不是自己用RTL描述这能保证最佳的性能和可靠性。学习FPGA设计是一个从描述语言到硬件思维再到系统工程的漫长旅程。从理解VHDL/Verilog的每一个语法点到能独立完成一个包含状态机、数据通路和接口的模块再到能搭建一个完整的、通过验证的、满足时序的系统每一步都需要大量的实践和思考。建议从模仿开始多读优秀的开源代码多动手实现小项目并养成严谨的仿真和调试习惯。当你第一次看到自己编写的代码在FPGA开发板上闪烁LED或通过串口打印出“Hello World”时那种软硬结合的成就感将是独一无二的。