简介:本资源是东南大学信息学院《数字系统设计》短学期课程的FPGA实践项目——自动售货机硬件控制系统,面向电子工程、计算机科学及相关专业本科生与FPGA初学者,聚焦数字逻辑建模、时序控制与硬件协同设计能力培养。压缩包含160个文件,总大小1.46MB,以10个VHDL源文件(vhd)为核心实现状态机与控制逻辑,辅以11个约束文件(xdc)、14个综合报告(rst)、11个实现报告(rpt)及多个DPC/DCP工程文件,完整覆盖从代码编写、约束配置、综合布局布线到仿真验证的全流程;bat与tcl脚本支持一键编译调试。已有534人学习下载,资源提供可直接在Vivado中打开的完整工程(含Lock_Top_routed.dcp等关键实现文件),附带详细日志与备份记录,便于理解FPGA开发闭环、复现设计流程并快速定位时序违例等典型问题。
1. 为什么用 FPGA 做自动售货机,比用单片机更值得在数字系统设计课上死磕?
这不是一个“用 Verilog 写个状态机就交作业”的玩具项目——东南大学信息学院《数字系统设计》课程里这个“自动售货机”实验,本质是一次对时序建模能力、异步信号处理边界、资源约束意识和硬件调试直觉的集中拷问。学生常卡在:按键消抖后状态跳变、找零逻辑一加就错、硬币识别信号毛刺导致多计数、VGA 显示刷新和控制逻辑抢时钟……这些不是仿真波形能轻易骗过的“玄学翻车”。它要求你真正理解:FPGA 不是跑代码的 CPU,而是用逻辑门实时编织一张响应网络;每一个always @(posedge clk)都在和建立/保持时间赛跑;每一条assign都可能成为时序路径上的黑匣子。适合刚学完组合/时序电路、正啃 Verilog 基础、但还没被综合报错和时序违例毒打过的新手;也适合想把课本里的 Mealy/Moore 状态机、同步复位、跨时钟域握手这些概念,焊进真实引脚电平里的熟手。它不追求商业功能完整,而追求每个模块都能独立验证、每个信号都能用逻辑分析仪抓到、每次修改都能预判资源变化——这才是数字系统设计的肌肉记忆起点。
2. 从顶层框图到模块拆解:为什么必须用同步设计+状态机驱动核心流程
自动售货机的物理行为天然符合有限状态机(FSM)建模:投币 → 选择商品 → 判定金额 → 出货 → 找零 → 复位。但直接写一个大而全的 FSM 容易失控。我们采用分层同步设计法:顶层只做时钟域划分与信号路由,核心控制逻辑下沉到vending_ctrl模块,用 Moore 型状态机主导主干流程,再用独立的coin_detector、dispense_logic、display_driver模块并行处理外设。关键理由有三:
第一,避免异步输入直接进主状态机。硬币传感器、按键开关都是机械触点,存在毫秒级抖动,若直接接进always @(posedge clk)的敏感列表,一次按下可能触发多次状态跳转。必须先经两级寄存器同步(synchronizer),再送入边沿检测电路。
第二,分离数据通路与时序控制。金额累加、找零计算属于纯组合逻辑,若混在状态机里,会拉长关键路径,导致时序收敛失败。应单独用adder_subtractor模块实现,其输出通过寄存器打拍后供状态机读取。
第三,为后续扩展留出明确接口。比如未来加红外扫码模块,只需替换product_select子模块,不碰主控状态机。这种解耦不是教条,而是某高校实验室在迭代 7 版教学 Demo 后血泪经验:模块越小,仿真越快,出错定位越准。
2.1 顶层模块top_vending:时钟、复位与端口定义的硬性规范
// top_vending.v module top_vending ( input wire clk_50m, // 50MHz 板载晶振,所有逻辑以此为基准 input wire rst_n, // 低电平异步复位(注意:必须用异步复位!) // 输入:硬币信号(高电平有效)、按键(高电平有效)、商品选择拨码开关 input wire coin_in, // 单枚硬币脉冲,宽度约 10ms,需同步 input wire [3:0] key_in, // 4x4 矩阵键盘扫描结果,或简化为 4 位拨码 input wire [2:0] sel_sw, // 3 位拨码开关,对应 8 种商品 // 输出:LED 指示、数码管段码、VGA 控制信号、电机驱动使能 output reg [7:0] led_out, // 8 位 LED,显示当前状态码(如 0x01=等待投币) output reg [7:0] seg_data, // 共阴极数码管段码(0-9,A-F) output reg [3:0] seg_sel, // 数码管位选(4 位) output reg vga_hsync, output reg vga_vsync, output reg [2:0] vga_rgb, // 简化 VGA,仅 3 位 RGB output reg motor_en // 出货电机使能(高电平启动) ); // 时钟分频:生成 1kHz 用于消抖和状态机节拍 wire clk_1k; clk_divider #(.DIV_CNT(50000)) uut_clk_div ( .clk_in(clk_50m), .rst_n(rst_n), .clk_out(clk_1k) ); // 同步化硬币输入(两级寄存器防亚稳态) wire coin_sync; synchronizer #(.STAGES(2)) uut_coin_sync ( .clk(clk_1k), .rst_n(rst_n), .async_in(coin_in), .sync_out(coin_sync) ); // 主控制器实例化(核心!) vending_ctrl uut_ctrl ( .clk(clk_1k), .rst_n(rst_n), .coin_pulse(coin_sync), // 已同步的硬币脉冲 .key_in(key_in), .sel_sw(sel_sw), .led_out(led_out), .seg_data(seg_data), .seg_sel(seg_sel), .vga_hsync(vga_hsync), .vga_vsync(vga_vsync), .vga_rgb(vga_rgb), .motor_en(motor_en) ); endmodule逻辑说明:顶层不参与业务逻辑,只做三件事——分频、同步、连线。
clk_1k是整个系统节奏基准,所有状态机跳转、数码管扫描、VGA 行场同步都基于它。synchronizer模块必须用#(.STAGES(2))显式例化,不能手写两级reg,否则综合工具可能优化掉中间寄存器,失去同步意义。rst_n必须是异步复位,因为上电瞬间各模块寄存器初值不确定,同步复位无法保证所有 FF 同时清零。
2.2 核心控制器vending_ctrl:Moore 状态机的 6 个状态与转移条件
Moore 型状态机的输出只取决于当前状态,不依赖输入组合,抗干扰强、时序干净。本设计定义 6 个状态:
| 状态名 | 含义 | 输出特征 | 转移条件 |
|---|---|---|---|
IDLE | 等待投币 | LED=0x01,数码管显示余额 0 | coin_pulse == 1 |
ACCUMULATING | 累加金额 | LED=0x02,数码管动态更新余额 | coin_pulse == 1或key_in != 0 |
SELECTING | 商品选择中 | LED=0x03,VGA 显示商品菜单 | key_in有效且余额足够 |
DISPENSING | 出货中 | LED=0x04,motor_en=1,VGA 显示"OUT" | sel_sw选定且确认 |
GIVING_CHANGE | 找零中 | LED=0x05,数码管倒计时找零额 | 出货完成且找零 > 0 |
RESET_WAIT | 复位等待 | LED=0x00,所有输出清零 | key_in[0]长按 2s |
// vending_ctrl.v(节选关键状态转移部分) localparam IDLE = 3'b000, ACCUMULATING = 3'b001, SELECTING = 3'b010, DISPENSING = 3'b011, GIVING_CHANGE = 3'b100, RESET_WAIT = 3'b101; reg [2:0] state, next_state; reg [7:0] balance; // 当前余额(单位:角) // 状态寄存器(同步时序) always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= IDLE; else state <= next_state; end // 下一状态逻辑(组合逻辑) always @(*) begin next_state = state; // 默认保持 case (state) IDLE: if (coin_pulse) next_state = ACCUMULATING; ACCUMULATING: begin if (key_in != 4'b0000) next_state = SELECTING; else if (coin_pulse) next_state = ACCUMULATING; // 继续投币 end SELECTING: if (valid_selection && balance >= price[sel_sw]) next_state = DISPENSING; DISPENSING: if (motor_done) begin if (change_amt > 0) next_state = GIVING_CHANGE; else next_state = IDLE; end GIVING_CHANGE: if (change_cnt == 0) next_state = IDLE; RESET_WAIT: if (key_in[0] && reset_timer > 2000) next_state = IDLE; endcase end // Moore 输出逻辑(仅由 state 决定) always @(posedge clk or negedge rst_n) begin if (!rst_n) begin led_out <= 8'h00; seg_data <= 8'h00; seg_sel <= 4'h0; vga_rgb <= 3'h0; motor_en <= 1'b0; end else begin case (state) IDLE: begin led_out <= 8'h01; seg_data <= bcd_encode(balance); // BCD 编码函数 seg_sel <= 4'h1; // 选最低位数码管 vga_rgb <= 3'h0; motor_en <= 1'b0; end DISPENSING: begin led_out <= 8'h04; seg_data <= 8'h3F; // "0" seg_sel <= 4'h0; vga_rgb <= 3'h7; // 白色 motor_en <= 1'b1; end // 其他状态类似... endcase end end参数说明:
balance用 8 位寄存器存储(最大 255 角 = 25.5 元),覆盖教学需求;price[sel_sw]是 3-bit 地址索引的 ROM 查表,存 8 种商品单价(如 5, 8, 10, 12... 单位:角);bcd_encode()是自定义函数,将二进制转为 2 位 BCD 供数码管显示。关键细节:motor_done信号来自电机驱动模块的反馈(如光电开关检测到位),绝不能用固定延时替代——这是硬件闭环思维的分水岭。
3. 三大外设模块的落地细节:消抖、显示、驱动一个都不能少
自动售货机的“真实感”来自外设交互。学生最容易在这里翻车:数码管乱闪、VGA 图像撕裂、电机狂转不停。问题根源往往不在算法,而在时序匹配与驱动能力。
3.1 硬币检测与消抖:两级同步 + 边沿检测的黄金组合
硬币传感器输出的是机械抖动脉冲(典型 10ms 宽,含 1~5ms 毛刺)。直接采样会导致coin_pulse多触发。必须用同步化 + 边沿检测两步走:
// coin_detector.v module coin_detector ( input wire clk, input wire rst_n, input wire coin_raw, // 传感器原始输出 output reg coin_pulse // 单周期高脉冲,宽度=1个clk_1k周期 ); reg [1:0] coin_sync; // 两级同步寄存器 reg coin_prev; // 同步化(已包含在顶层,此处为完整性展示) always @(posedge clk or negedge rst_n) begin if (!rst_n) coin_sync <= 2'b00; else coin_sync <= {coin_sync[0], coin_raw}; end // 边沿检测:上升沿产生单周期脉冲 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin coin_prev <= 1'b0; coin_pulse <= 1'b0; end else begin coin_prev <= coin_sync[1]; // 同步后的稳定信号 coin_pulse <= ~coin_prev & coin_sync[1]; // 上升沿 end end endmodule为什么必须两级?单级同步无法消除亚稳态(Metastability):当
coin_raw在clk采样沿附近变化,FF 可能进入中间电压态,持续数纳秒才稳定,导致后续逻辑误判。两级寄存器将亚稳态概率降至工程可接受水平(<1e-12)。血泪经验:某同学用单级同步,在 50MHz 下测试 1000 次投币,第 873 次出现双计数,用逻辑分析仪抓到亚稳态波形才信服。
3.2 数码管动态扫描:4 位共阴极的时序精确控制
开发板常用 4 位共阴极数码管,需动态扫描(每位点亮约 2ms,4 位轮询,总周期 8ms ≈ 125Hz)。关键在位选信号与段码信号严格同步,否则出现“鬼影”。
// seg_display.v module seg_display ( input wire clk_1k, input wire rst_n, input wire [7:0] data_in, // 待显示的 8 位数据(BCD 编码) output reg [3:0] seg_sel, // 位选(低电平有效) output reg [7:0] seg_data // 段码(共阴极,高电平点亮) ); reg [1:0] digit_cnt; // 0-3 计数,选择当前位 reg [7:0] seg_buf [3:0]; // 4 个缓冲区,存每位段码 // 位计数器(1kHz 分频后,每位显示 250 个周期 ≈ 2ms) always @(posedge clk_1k or negedge rst_n) begin if (!rst_n) digit_cnt <= 2'b00; else digit_cnt <= digit_cnt + 1'b1; end // 段码缓冲(将 8 位输入拆成 4 个 BCD 位) always @(posedge clk_1k or negedge rst_n) begin if (!rst_n) begin seg_buf[0] <= 8'h00; seg_buf[1] <= 8'h00; seg_buf[2] <= 8'h00; seg_buf[3] <= 8'h00; end else begin // data_in 是 2 位 BCD(如 0x12 表示 12 角),高位在 data_in[7:4] seg_buf[0] <= bcd_to_seg(data_in[3:0]); // 个位 seg_buf[1] <= bcd_to_seg(data_in[7:4]); // 十位 seg_buf[2] <= 8'h3F; // "0",百位(教学版不显示) seg_buf[3] <= 8'h3F; // "0",千位 end end // 动态扫描输出 always @(posedge clk_1k or negedge rst_n) begin if (!rst_n) begin seg_sel <= 4'b1111; seg_data <= 8'h00; end else begin case (digit_cnt) 2'b00: begin seg_sel <= 4'b1110; seg_data <= seg_buf[0]; end // 位0 2'b01: begin seg_sel <= 4'b1101; seg_data <= seg_buf[1]; end // 位1 2'b10: begin seg_sel <= 4'b1011; seg_data <= seg_buf[2]; end // 位2 2'b11: begin seg_sel <= 4'b0111; seg_data <= seg_buf[3]; end // 位3 endcase end end // BCD 转段码查表(共阴极) function [7:0] bcd_to_seg; input [3:0] bcd; case (bcd) 4'h0: bcd_to_seg = 8'b00111111; // 0 4'h1: bcd_to_seg = 8'b00000110; // 1 4'h2: bcd_to_seg = 8'b01011011; // 2 // ... 其他数字 default: bcd_to_seg = 8'b00000000; endcase endfunction endmodule关键参数:
digit_cnt用 2 位计数器,确保 4 位轮询;seg_sel低电平有效,所以4'b1110表示选中第 0 位(最右);seg_data必须在seg_sel切换后立即更新,否则位选与段码错位。避坑提示:若发现某位数码管亮度明显偏低,大概率是该位seg_sel有效时间过短(计数器溢出或复位异常),用示波器测seg_sel[0]和seg_data时序即可定位。
3.3 VGA 显示驱动:640x480@60Hz 的最小可行方案
教学板 VGA 接口通常只接 R/G/B 各 2 位(共 6 位色深),无需复杂帧缓存。我们用字符模式:定义 8x16 点阵字体,用 1KB RAM 存 128 个字符,VGA 控制器实时查表生成像素。
// vga_controller.v(简化版,仅行场同步+字符地址生成) module vga_controller ( input wire clk_25m, // VGA 需要 25.175MHz,用 PLL 从 50MHz 分频 input wire rst_n, input wire [2:0] char_data, // 当前显示字符 ASCII 码(0-127) output reg vga_hsync, output reg vga_vsync, output reg [1:0] vga_r, output reg [1:0] vga_g, output reg [1:0] vga_b ); // VGA 时序参数(640x480@60Hz) localparam H_TOTAL = 800, // 行总周期 H_SYNC = 96, // 行同步脉宽 H_BP = 48, // 行后沿 H_ACT = 640, // 有效显示区 H_FP = 16; // 行前沿 localparam V_TOTAL = 525, // 帧总周期 V_SYNC = 2, // 场同步脉宽 V_BP = 33, // 场后沿 V_ACT = 480, // 有效显示区 V_FP = 10; // 场前沿 reg [9:0] h_cnt, v_cnt; // 行/场计数器 reg [9:0] x, y; // 当前像素坐标(0-639, 0-479) // 行计数器 always @(posedge clk_25m or negedge rst_n) begin if (!rst_n) h_cnt <= 0; else if (h_cnt == H_TOTAL-1) h_cnt <= 0; else h_cnt <= h_cnt + 1; end // 场计数器 always @(posedge clk_25m or negedge rst_n) begin if (!rst_n) v_cnt <= 0; else if ((h_cnt == H_TOTAL-1) && (v_cnt == V_TOTAL-1)) v_cnt <= 0; else if (h_cnt == H_TOTAL-1) v_cnt <= v_cnt + 1; end // 像素坐标(仅在有效区计算) always @(posedge clk_25m or negedge rst_n) begin if (!rst_n) begin x <= 0; y <= 0; end else if (h_cnt < H_ACT && v_cnt < V_ACT) begin x <= h_cnt; y <= v_cnt; end else begin x <= 0; y <= 0; end end // 行/场同步信号生成 assign vga_hsync = (h_cnt >= H_ACT + H_BP) && (h_cnt < H_ACT + H_BP + H_SYNC); assign vga_vsync = (v_cnt >= V_ACT + V_BP) && (v_cnt < V_ACT + V_BP + V_SYNC); // 字符显示逻辑:将屏幕分为 80x30 字符格(80*8=640, 30*16=480) reg [6:0] char_addr; // 字符内存地址(0-127) reg [3:0] x_char, y_char; // 字符内坐标(0-7, 0-15) always @(*) begin char_addr = (y[9:4] * 80) + x[9:3]; // 行号*80 + 列号 x_char = x[2:0]; y_char = y[3:0]; end // 字体 ROM 查表(简化:用 case 代替 ROM) reg [1:0] pixel_color; always @(*) begin pixel_color = 2'b00; // 黑色背景 if (x_char < 8 && y_char < 16) begin case (char_data) 8'h20: ; // 空格,全黑 8'h30: if (font_0[y_char][x_char]) pixel_color = 2'b11; // '0' 字模 // ... 其他字符 endcase end end // 输出 RGB(2 位色深) assign vga_r = (pixel_color == 2'b11) ? 2'b11 : 2'b00; assign vga_g = (pixel_color == 2'b11) ? 2'b11 : 2'b00; assign vga_b = (pixel_color == 2'b11) ? 2'b11 : 2'b00; endmodule为什么用 25.175MHz?这是 VGA 640x480@60Hz 的标准像素时钟,误差超过 0.1% 会导致图像滚动或撕裂。开发板 PLL 必须精确配置,不能用近似分频。实操技巧:先用示波器测
vga_hsync频率是否为 31.469kHz(640x480 的行频),再测vga_vsync是否为 59.94Hz,达标再调字符显示。
4. 避坑指南:数字系统设计课上最常踩的 5 个坑及后悔药
这些不是理论假设,而是某高校连续三年实验报告里高频出现的错误,附带现场抓到的波形证据和一招解决法。
4.1 现象:按键按下一次,状态机跳了两次
原因:按键未消抖,或消抖逻辑写在状态机内部,导致key_in在一个时钟周期内多次变化,被always @(posedge clk)采样多次。
解决:必须用独立key_debouncer模块,输出key_valid信号(单周期脉冲),且该信号必须经同步器后再送入主状态机。验证方法:用逻辑分析仪抓key_raw和key_valid,确认key_valid是干净单脉冲。
4.2 现象:数码管显示数字正确,但某一位始终不亮或常亮
原因:seg_sel信号未用寄存器打拍,或seg_data更新与seg_sel切换不同步,导致位选无效时段码已改变。
解决:seg_sel和seg_data必须在同一always @(posedge clk)块中赋值,且seg_sel切换后至少延迟 1 个周期再更新seg_data。关键代码:always @(posedge clk_1k) begin ... case(digit_cnt) ... end中,seg_sel和seg_data必须同块赋值。
4.3 现象:VGA 显示图像正常,但文字边缘模糊或错位
原因:字符坐标计算用了x[9:0]直接除法(如x/8),综合工具将其转为组合逻辑,引入长路径延迟,导致像素时序偏移。
解决:改用移位实现整除(x >> 3),或用计数器生成字符地址(如char_x_cnt每 8 个像素加 1)。教训:FPGA 里除法是面积/时序黑洞,能移位绝不除。
4.4 现象:综合后资源占用爆表(LUT > 90%),根本无法布局布线
原因:在状态机里写了大数组(如reg [7:0] price[7:0]),或用了未初始化的reg数组,综合工具推断为 RAM,但教学板无 Block RAM。
解决:价格表改用case语句查表(case(sel_sw) 3'b000: p=8; ... endcase);所有reg数组必须显式初始化。检查命令:看综合报告.rpt文件中的Inferred Memory条目,非零即危险。
4.5 现象:电机启动后不转,或转几秒就停,逻辑分析仪显示motor_en一直为高
原因:motor_en是使能信号,但电机驱动芯片(如 L298N)需要 PWM 调速,或需外部限流电阻。单纯高电平无法驱动。
解决:增加pwm_generator模块,将motor_en作为使能,输出 5kHz PWM 波(占空比 80%);在motor_en为高时启动 PWM,为低时强制 PWM=0。硬件验证:用万用表测电机两端电压,应为 PWM 平均电压(如 5V*0.8=4V)。
5. 时序约束与引脚分配:让代码从仿真走向真实开发板的临门一脚
写完 Verilog 只完成了 70%,剩下 30% 是让工具链理解你的硬件意图。不加约束的代码,综合后可能在仿真里完美,上板就时序违例——这是数字设计从“能跑”到“可靠”的分水岭。
5.1 为什么必须写 XDC 约束文件?一个反例说明
某同学的clk_1k分频器在仿真中输出精准 1kHz,但上板后用示波器实测为 987Hz。原因:综合工具将分频计数器优化成了组合逻辑链,实际路径延迟导致频率漂移。解决方案是添加时钟约束,强制工具将其综合为寄存器链,并指定时序目标。
# constraints.xdc # 创建主时钟(50MHz) create_clock -period 20.000 -name clk_50m [get_ports clk_50m] # 创建衍生时钟(1kHz),基于 clk_50m 的分频器输出 create_generated_clock -name clk_1k -source [get_pins clk_divider/UUT/clk_out_reg/C] -divide_by 50000 [get_pins vending_ctrl/clk] # 约束关键输入:硬币信号为异步输入,需设置输入延迟 set_input_delay -clock clk_1k -max 10.0 [get_ports coin_in] set_input_delay -clock clk_1k -min 0.0 [get_ports coin_in] # 约束输出:数码管段码需在位选后 5ns 内稳定 set_output_delay -clock clk_1k -max 5.0 [get_ports seg_data] set_output_delay -clock clk_1k -min 0.0 [get_ports seg_data]参数解读:
-period 20.000对应 50MHz(1/50e6=20ns);-divide_by 50000告诉工具clk_1k是clk_50m的 50000 分频,工具会据此计算时序路径;set_input_delay告诉工具coin_in最晚在clk_1k上升沿前 10ns 到达,避免建立时间违例。没有这些,工具就按“最快路径”优化,结果就是“最不可靠路径”。
5.2 引脚分配的生死线:VGA、数码管、按键的物理映射原则
开发板原理图是唯一真理。常见错误是凭记忆分配引脚,导致 VGA 的R0/R1接反,显示全红。必须严格按以下三步:
- 查原理图:找到 VGA 接口的
R0,R1,G0,G1,B0,B1,HSYNC,VSYNC对应的 FPGA 引脚号(如Y10,Z10,W10,U10,Y9,W9,T8,T9); - 查开发板手册:确认这些引脚支持 LVCMOS33 电平(VGA 标准),且未被其他功能复用;
- XDC 中精确绑定:
# VGA 引脚(以某款开发板为例) set_property PACKAGE_PIN Y10 [get_ports vga_r[0]] set_property IOSTANDARD LVCMOS33 [get_ports vga_r[0]] set_property PACKAGE_PIN Z10 [get_ports vga_r[1]] set_property IOSTANDARD LVCMOS33 [get_ports vga_r[1]] set_property PACKAGE_PIN W10 [get_ports vga_g[0]] set_property IOSTANDARD LVCMOS33 [get_ports vga_g[0]] # ... 其他引脚同理 # 数码管位选(共阴极,低电平有效) set_property PACKAGE_PIN U13 [get_ports seg_sel[0]] # 位0 set_property PACKAGE_PIN T13 [get_ports seg_sel[1]] # 位1 set_property PACKAGE_PIN T14 [get_ports seg_sel[2]] # 位2 set_property PACKAGE_PIN U14 [get_ports seg_sel[3]] # 位3 # 按键(高电平有效,内部上拉) set_property PACKAGE_PIN T18 [get_ports key_in[0]] # KEY0 set_property PACKAGE_PIN U18 [get_ports key_in[1]] # KEY1 set_property PACKAGE_PIN U17 [get_ports key_in[2]] # KEY2 set_property PACKAGE_PIN T17 [get_ports key_in[3]] # KEY3致命细节:
seg_sel是低电平有效,但开发板原理图中位选引脚可能接了反相器,此时 XDC 中需标注INVERTED属性;key_in若为按键,必须确认开发板已内置上拉电阻,否则需在 XDC 中加PULLUP。验证方法:约束后运行Report IO Planning,检查所有端口是否成功绑定,无Unconstrained报告。
5.3 时序报告解读:看懂 Critical Path 和 Slack
综合实现后,打开report_timing_summary.rpt,重点关注三列:
| 项目 | 含义 | 健康值 | 风险提示 |
|---|---|---|---|
| WNS (Worst Negative Slack) | 最差负裕量 | ≥ 0ps | < 0ps 表示时序违例,必修! |
| TNS (Total Negative Slack) | 总负裕量 | = 0ps | > 0ps 表示多条路径违例 |
| Critical Path | 关键路径描述 | clk_1k to vending_ctrl/motor_en | 定位到具体模块和信号 |
例如报告中:
Slack (MET) : 1.234ns (required time - arrival time) Source: vending_ctrl/coin_pulse_reg/C Destination: vending_ctrl/state_reg[0]/D Path Group: clk_1k说明从coin_pulse_reg到state_reg[0]的路径有 1.234ns 富余,安全。若为-0.876ns,则需优化:在路径中插入寄存器(register retiming),或降低clk_1k
本文还有配套的精品资源,点击获取