
简介南京邮电大学脉冲按键拨号电路FPGA设计课程设计资料包面向电子通信类专业学生及FPGA初学者完整实现0~9按键输入、串行脉冲序列输出与动态显示功能并支持按键切换基本/扩展指标为课堂项目或课设提供可复现的完整工程方案。资源共215个文件包含Verilog/VHDL源文件c、Xilinx工程文件xise/ucf/bit、仿真报告log/html以及原理图文件sch、位流配置文件bit等压缩包约897KB便于下载与快速部署。已有128人学习下载。工程已经测试验收可在开发板上直接运行读者可基于源码理解有限状态机、按键消抖、动态扫描等数字电路核心设计也可参考其基本指标与扩展指标的功能切换思路用于课程答辩或二次开发。对准备数字逻辑实验或FPGA课设的学生来说是一份具实操价值的参考资料。1. 脉冲按键拨号电路FPGA设计把随机按键整理成一串标准脉冲课程设计拿到这个题目第一反应多半是“电话都进博物馆了还拨什么脉冲”。但脉冲按键拨号电路练的并不是电话而是 FPGA 入门最该掌握的一套组合动作按键消抖、边沿检测、计数器分频、时序状态机。真实世界的按键是异步的按下瞬间电平乱跳而拨号协议要求每个号码变成一串严格定宽的脉冲交换机的计数继电器才能识别。用 FPGA 把“一头随机、一头严格”的两端接起来正好把数字电路和 Verilog 的核心技巧全过一遍而且结果能用 LED、示波器或逻辑分析仪直接验收。这个题目在南邮的现代数字系统课程设计里反复出现做法不唯一但验收点很统一脉冲个数不能错每个脉冲高电平 40ms、低电平 60ms号码之间有 300ms 以上的位间隔。2. 脉冲拨号的时序规格与FPGA顶层模块划分2.1 脉冲拨号协议一位数字就是一串“断断续续”脉冲拨号是电话交换机最早的自动选线方式。老式旋转电话机上有个带弹簧的拨号盘拨到指定挡位后松手机械触点按固定节奏断开、闭合断开一次就算一个脉冲。交换机那边用计数继电器把脉冲数量翻译成电话号码的一位。这个“断开—闭合”的动作输出到电路上就是一个严格周期的高/低电平序列高电平 40ms、低电平 60ms合成一个 100ms 的脉冲周期也就是 10pps 的拨号速率。号码里的每个数字都对应固定的脉冲个数拨 5线路就断 5 次拨 0 最有意思它不是 0 个脉冲而是拨 10 个脉冲为了与“没拨号”区分开。如果把旋转拨号盘换成按键输入FPGA 要做的事情就变成用户按一个数字键在 dial_out 引脚上输出对应数量的标准脉冲每个脉冲都是 40ms 高电平加 60ms 低电平号码发完后再维持至少 300ms 的静默作为位间隔交换机看到静默就知道一位号码结束。整个过程可以浓缩成下面的对应关系号码脉冲数发完耗时不含位间隔说明11100ms最短的号码位22200ms依次递增55500ms中间档99900ms接近 1 秒0101000ms10 个脉冲最耗时连续按 3 个数字键就得在拨号线上输出三组这样的脉冲串中间用位间隔隔开。如果两组脉冲之间没有间隔交换机会把前后两串当成一个号码比如“34”可能被识别成“7”。这个“串起来”的节奏控制正是状态机存在的意义。2.2 顶层模块划分按键、编码、发脉冲、显示各管一段FPGA 课程设计不能只写一个大 always 块工程需要按功能拆模块。很多同学刚上手把全部逻辑堆在顶层仿真时一旦时序不对根本说不清是消抖没做干净、计数器溢出还是状态机跳飞了。处理“按键转脉冲”这类题目我一般会拆成四个模块key_debounce把按键输入两级同步化、滤掉抖动输出稳定后的电平以及按下次瞬间的单周期脉冲number_encoder把按键的 BCD 码映射成脉冲数量0 对应 101~9 对应自身pulse_dial_fsm拨号状态机输入 start 和 BCD 码输出拨号脉冲与 busy 信号display把当前拨出号码显示到数码管板级调试时能直观看到“号码有没有被正确锁存”。顶层例化时模块之间只有几根线逻辑分析仪要抓的信号也一目了然。典型例化结构如下// pulse_dial_top.v 顶层例化框架 wire key_pulse; // 按键消抖后单周期脉冲 wire [3:0] key_code; // 按键 BCD 码 wire dial_out; // 拨号脉冲输出 wire busy; // 忙信号 key_debounce u_debounce ( .clk (clk_50m), .rst_n (rst_n), .key_in (key_row), // 原始按键输入 .key_out (key_stable), // 消抖后稳定电平 .key_pulse (key_pulse) // 按下瞬间的单周期脉冲 ); pulse_dial_fsm u_dial ( .clk_50m (clk_50m), .rst_n (rst_n), .start (key_pulse), .key_num (key_code), .dial_pulse (dial_out), .busy (busy) );参数说明key_code 是 4 位 BCD 码对应数字 0~9。这里特意把按键解码单独留给键盘扫描模块而不是让状态机直接处理 16 个按键的扫描编码好处是后面从独立按键换成 4x4 矩阵键盘状态机代码一行都不用改。模块间的握手规则是key_pulse 是单周期脉冲busy 为高期间状态机正在发脉冲新按键应该被忽略busy 拉低后下一拍才能接受新的 start。这个约定必须在设计文档里写清楚否则仿真时很容易出现“拨号还没结束就触发第二次按键”的竞争。3. 按键消抖与号码编码映射的FPGA实现3.1 用两级同步器和20ms计数器完成按键消抖按键按下和松开的过程中机械触点会弹跳波形上表现为几百微秒到十几毫秒的毛刺。如果不处理状态机可能把一次按下当成好几下拨号脉冲数就会凭空多出来。课程设计里最常用的消抖办法是“20ms 电平稳定判定”检测到按键状态变化后开始计数只有持续 20ms 保持同一电平才认为状态稳定。除了抖动脉冲还有亚稳态问题。按键相对时钟是异步信号直接进 always 块的触发条件寄存器的建立/保持时间可能被违反。所以第一步应该是两级同步器把异步信号变成与时钟对齐的信号再交给消抖状态机。下面这段消抖代码可以直接放进工程// key_debounce.v module key_debounce ( input wire clk, // 50MHz 系统时钟 input wire rst_n, // 低电平复位 input wire key_in, // 异步按键输入按下为高 output reg key_out, // 消抖后的稳定电平 output reg key_pulse // 按下边沿产生的单周期脉冲 ); localparam CNT_MAX 1_000_000; // 20ms 50MHz 20 * 50000 reg [19:0] cnt; reg key_sync0, key_sync1; // 两级同步器 reg key_prev; // 第一段两级同步器消除亚稳态 always (posedge clk or negedge rst_n) begin if (!rst_n) begin key_sync0 1b0; key_sync1 1b0; end else begin key_sync0 key_in; key_sync1 key_sync0; end end // 第二段20ms 稳定计数 always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 20d0; key_out 1b0; end else if (key_sync1 key_out) begin // 与当前稳定输出一致说明没有跳变 cnt 20d0; end else begin // 检测到新电平开始累计稳定时间 if (cnt CNT_MAX) begin key_out key_sync1; // 电平稳定更新输出 cnt 20d0; end else begin cnt cnt 1; end end end // 第三段边沿检测按键按下的瞬间拉高一个时钟周期 always (posedge clk or negedge rst_n) begin if (!rst_n) begin key_prev 1b0; key_pulse 1b0; end else begin key_prev key_out; key_pulse key_out ~key_prev; end end endmodule逻辑说明key_sync1 key_out 判断的是“新输入与当前输出是否一致”。一致说明按键没有变化计数器清零不一致说明电平跳变了开始累计 20ms。20ms 之后 key_out 才翻转同时计数器清零为下一次跳变做准备。key_prev 保存 key_out 上一拍的值当 key_out 从 0 变 1 时key_pulse 拉高一个 clk 周期这个脉冲可以直接作为状态机的启动条件。参数说明CNT_MAX 20ms / 1 个时钟周期 20 × 50_000 1_000_000计数器宽度 20 位刚好够。如果板子时钟不是 50MHz例如 100MHz就要改成 2_000_000。消抖时间也不是越大越好20ms 是行业常见值大部分机械按键的抖动都在 5~15ms 内结束。如果觉得按键反馈迟钝可以压到 10ms但不要低于 5ms否则抖动量大的按键会失效。3.2 数字到脉冲数的编码映射表状态机真正要的不是按键编号而是这个键要发几个脉冲。号码到脉冲数的映射是拨号协议的核心也是仿真里最容易查的一项。0 对应 10 个脉冲、1~9 对应自身具体的映射关系如下按键BCD 码脉冲数十六进制04b0000104hA14b000114h124b001024h234b001134h344b010044h454b010154h564b011064h674b011174h784b100084h894b100194h9这个映射用 Verilog 写比 case 更简洁0 特殊处理一下就是完整逻辑wire [3:0] pulse_count_target; assign pulse_count_target (key_num 4d0) ? 4d10 : key_num;参数说明pulse_count_target 是 4 位无符号数最大 10最小 1不存在 0 值。状态机只要进入拨号流程必然至少发一个脉冲。最容易写错的是直接把 key_num 当成脉冲数这样拨 0 时一个脉冲都没有交换机根本认不出这个号码而仿真里看似“正常”实际违背了拨号协议。如果板上的键盘带的是 4x4 矩阵按键扫描模块先输出行列编码再经译码器转成 BCD 码这张映射表的位置不变状态机不用关心键盘物理结构。4. 拨号脉冲发生器的时序控制状态机4.1 三个必调参数MAKE、BREAK、GAP状态机的任务把“脉冲数量”变成一个真实波形。波形由三段组成MAKE脉冲高电平导通时间标准 40msBREAK脉冲低电平断开时间标准 60msGAP整个号码发完后、下一个号码开始前的静默时间标准不小于 300ms。10pps 的脉冲周期由 MAKEBREAK 决定40ms 60ms 正好 100ms。在 50MHz 时钟下各段时间对应的计数器值如下参数时间时钟周期数允许计数器位宽MAKE_TICKS40ms2_000_000至少 21 bitBREAK_TICKS60ms3_000_000至少 22 bitGAP_TICKS300ms15_000_000至少 24 bitGAP_TICKS 最大值为 15_000_000小于 2^24 16_777_216所以 timer 取 24 位最稳妥。如果板级调试想加快节奏可以把这三个参数临时缩小 1000 倍拨号过程就从 1 秒量级缩到 1ms 量级方便示波器观察综合下载前记得改回来。4.2 拨号控制状态机的可综合实现状态机使用 4 个状态S_IDLE 等待按键、S_MAKE 输出高电平、S_BREAK 输出低电平、S_GAP 位间隔。pulse_count 寄存器记录剩余脉冲个数进入 S_BREAK 时递减递减到 0 后进入 S_GAP。核心代码如下// pulse_dial_fsm.v module pulse_dial_fsm ( input wire clk_50m, input wire rst_n, input wire start, // key_debounce 输出的单周期脉冲 input wire [3:0] key_num, // BCD 码 0~9 output reg dial_pulse, // 拨号脉冲输出 output reg busy // 高电平表示正在拨号 ); // 50MHz 时钟时间参数 localparam MAKE_TICKS 2_000_000; // 40ms localparam BREAK_TICKS 3_000_000; // 60ms localparam GAP_TICKS 15_000_000; // 300ms localparam S_IDLE 2d0; localparam S_MAKE 2d1; localparam S_BREAK 2d2; localparam S_GAP 2d3; reg [1:0] state; reg [23:0] timer; reg [3:0] pulse_count; // 剩余脉冲数 reg [3:0] dial_num; // 锁存的号码 wire [3:0] target_pulses (dial_num 4d0) ? 4d10 : dial_num; always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin state S_IDLE; timer 24d0; pulse_count 4d0; dial_num 4d0; dial_pulse 1b0; busy 1b0; end else begin case (state) // 空闲等待按键锁存号码 S_IDLE: begin dial_pulse 1b0; busy 1b0; if (start) begin dial_num key_num; pulse_count target_pulses; busy 1b1; timer 24d0; state S_MAKE; end end // 脉冲高电平阶段 S_MAKE: begin dial_pulse 1b1; if (timer MAKE_TICKS - 1) begin timer 24d0; state S_BREAK; end else begin timer timer 1; end end // 脉冲低电平阶段结束后判断是否有下一个脉冲 S_BREAK: begin dial_pulse 1b0; if (timer BREAK_TICKS - 1) begin timer 24d0; pulse_count pulse_count - 1; if (pulse_count 4d1) begin // 最后一个脉冲进入位间隔 state S_GAP; end else begin state S_MAKE; end end else begin timer timer 1; end end // 位间隔号码结束后静默 300ms S_GAP: begin dial_pulse 1b0; if (timer GAP_TICKS - 1) begin timer 24d0; busy 1b0; state S_IDLE; end else begin timer timer 1; end end endcase end end endmodule逻辑说明S_IDLE 把 key_num 锁存到 dial_num同时 pulse_count 载入 target_pulses。这里 target_pulses 由“上一拍的 dial_num”计算得到第一次按键时 dial_num 是复位值 0映射出来是 10会导致拨 3 却发 10 个脉冲。这是时序逻辑里常见的“当前值取到上一拍旧值”问题修复方式是在 S_IDLE 判断里用 key_num 直接计算而不是用 dial_numif (start) begin dial_num key_num; pulse_count (key_num 4d0) ? 4d10 : key_num; busy 1b1; timer 24d0; state S_MAKE; endS_BREAK 的判定逻辑要注意pulse_count 4d1判断的是“当前正处于最后一个脉冲”。进入 S_BREAK 前 pulse_count 等于 1则这个 BREAK 结束后递减成 0并进入 S_GAP如果 pulse_count 大于 1递减后继续回到 S_MAKE 发下一个脉冲。这样脉冲总数始终等于号码映射值不会多也不会少。参数说明这套状态机可以直接替换成 DTMF 双音多频方案只需把脉冲输出改为并行输出两个正弦波频率。不过课程设计做脉冲拨号状态机本身就是验收重点所以不要在 DTMF 上展开。想改成 20pps 拨号速率时把 MAKE_TICKS 减半为 1_000_000、BREAK_TICKS 减为 1_500_000其他逻辑不变但交换机侧的拨号速率是双方约定课程设计一般都固定 10pps不需要做成可配置参数。5. 仿真验证与板级联调的三个关键技巧5.1 用 Testbench 数脉冲个数顺便检查脉宽状态机跑完整拨号流程需要几百毫秒仿真时间在 50MHz 时钟下大约 3000 万个周期ModelSim、Vivado Simulator 都能承受。Testbench 里最该做两件事数 dial_pulse 的上升沿个数以及测量每个高电平持续的时间。数上升沿可以直接用一个计数器// tb_pulse_dial.v 片段 reg [3:0] edge_cnt; reg dial_pulse_d; wire pos dial_pulse ~dial_pulse_d; always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin dial_pulse_d 1b0; edge_cnt 4d0; end else begin dial_pulse_d dial_pulse; if (pos) edge_cnt edge_cnt 1; end end激励文件里连续给两组号码观察边缘计数是否与协议一致initial begin rst_n 1b0; repeat(10) (posedge clk_50m); rst_n 1b1; (posedge clk_50m); key_num 4d3; start 1b1; (posedge clk_50m); start 1b0; wait (busy 1b0); // 等待第一组号码拨完 $display(first dial edge_cnt %d, edge_cnt); // 期望是 3 key_num 4d0; start 1b1; (posedge clk_50m); start 1b0; wait (busy 1b0); $display(second dial edge_cnt %d, edge_cnt); // 期望是 10 end这里的 wait (busy 1b0) 是仿真时同步“上一组号码已经拨完”的方式。实际板级运行时不存在这种等待句法如果要在拨完后触发其他动作应该用 busy 下降沿采样。参数说明edge_cnt 在每次拨号前没有自动清零上面演示里第二次计数会叠加第一次的 3所以实际工程里应该在 start 拉高前手动清零或者把计数结果与起始值做差才能得到本组号码的脉冲数。5.2 数码管动态显示和缩小时间参数的板级验收下载到 FPGA 板卡后最直观的验收方式是把 dial_pulse 接到 LED拨号时能看到 LED 以 40ms 亮、60ms 灭的节奏闪烁。拨 5灯就闪 5 次拨 0灯闪 10 次。这个现象足够证明脉冲计数正确但没法定量确认脉宽。有条件的话把 dial_pulse 用跳线引到示波器或逻辑分析仪看到的应该是一串 100ms 周期的方波占空比 40%后面跟着 300ms 低电平。没有示波器时把 busy 接到另一个 LED 也能辅助判断位间隔是否结束busy 亮代表拨号中灭掉后 300ms 才允许下一组开始。板级调试最实用的技巧是临时把时间参数缩小比如把 MAKE_TICKS 改成 2_000、BREAK_TICKS 改成 3_000、GAP_TICKS 改成 15_000这样整个拨号过程从秒级变成毫秒级按键反应快示波器上一屏就能看到完整序列。改完参数后综合下载观察 LED 闪动节奏是否均匀连同数码管动态显示当前号码一起验证。另一个亲身踩过的坑是矩阵键盘的消抖位置。如果板子只提供了 4x4 矩阵键盘不要对 16 个按键各做一路消抖而是在行列扫描之后共享一个消抖通道。扫描到某一行某一列的电平稳定后统一进消抖模块再输出 BCD 码给状态机。显示屏上如果显示 5但状态机发出 10 个脉冲问题一定出在键盘扫描编码与 BCD 码的对应关系上跟拨号状态机无关。把问题边界切到模块边界排错会快很多。最后强调一个容易忽略的时序约束dial_pulse 直接由状态机寄存器输出没有任何组合逻辑不需要在 XDC/SDC 里加特殊约束。如果板子上 dial_pulse 要通过 PCB 走线驱动继电器或三极管注意查一下 FPGA IO 的驱动能力推挽输出默认即可要驱动真正的电话线必须经外部三极管放大不能拿 FPGA 引脚直接接负载。本文还有配套的精品资源点击获取