简介:本资源是一套基于Xilinx FPGA平台实现PS/2键盘与鼠标控制器的Verilog源码工程,面向FPGA初学者及数字系统设计学习者,解决外设接口协议解析、时序控制与硬件中断生成等典型实践问题。压缩包共115个文件,涵盖9个核心Verilog源文件(.v)、4个ISE/Vivado工程文件(.prj)、2个配置约束文件(.ucf/.pcf)、2个比特流文件(.bit)及多个综合、布局布线与日志文件(.ngc/.ncd/.log等),完整覆盖从代码编写、仿真、综合到FPGA下载验证的全流程。资源包大小仅1.5MB,结构清晰,含keyboard_test1_top等可直接上板运行的顶层模块,已通过真实Xilinx开发板验证,支持PS/2协议扫描、按键去抖、扫描码解析与LED状态反馈等功能。目前已有142人学习下载,适合作为FPGA课程实验、毕业设计参考或嵌入式外设接口开发的入门范例。
1. 项目概述:一个来自实践验证的FPGA键鼠接口设计
最近在整理硬盘里的老项目,翻出来一个有些年头的FPGA设计,是关于键盘和鼠标接口的。这个项目不是一个简单的仿真练习,而是当年在一块Xilinx Spartan-6开发板上实际跑通、验证过的完整工程。压缩包里包含了所有的Verilog源码、约束文件以及一些简单的测试文档。我觉得它对于正在学习FPGA数字接口设计,尤其是想弄明白PS/2这类经典低速串行协议如何用硬件描述语言实现的朋友来说,会是一个不错的参考。很多教程只讲协议理论或者给个仿真模型,而这个项目是从引脚电平开始,一直到在FPGA内部稳定解析出键值或鼠标位移数据的完整流程,里面有不少在实际调试中积累下来的细节处理。
这个设计的核心,就是用FPGA来实现一个PS/2接口的主机端控制器。PS/2接口虽然现在新电脑上不多见了,但在嵌入式系统、工控面板或者一些特定设备上仍有应用,其协议本身理解起来相对清晰,是学习同步串行通信和状态机设计的经典案例。通过这个项目,你不仅能掌握Verilog描述状态机、消抖、时钟同步等基本技能,更能理解如何将一个实际的物理接口协议,用纯数字逻辑的方式“翻译”成系统可用的数据。无论是用于学习,还是作为你自己项目中需要接入一个旧键盘或鼠标时的设计起点,这份经过验证的代码都有其价值。
2. 设计思路与核心模块拆解
整个工程的设计思路遵循典型的自底向上方法。PS/2协议是一种双向同步串行协议,但在这个基础版本中,我们主要实现接收功能,即FPGA作为主机接收来自键盘或鼠标的数据。发送功能(例如设置鼠标采样率)逻辑类似但方向相反,可以作为进阶练习。
2.1 系统顶层架构与模块划分
整个系统可以划分为三个主要层次:物理层接口、协议解析层和数据应用层。在Verilog中,这通常对应几个主要模块。
- 顶层模块(ps2_top):负责模块间的互联和时钟/复位信号的全局分发。它将外部的PS/2数据线(PS2_DATA)和时钟线(PS2_CLK)引入,并输出解析后的有效数据(如键值
keycode或鼠标数据包mouse_data)以及数据有效标志(data_valid)。 - PS/2接口同步与滤波模块(ps2_interface):这是最底层也是最关键的模块。它的任务有两个:一是将来自外部设备的、异步且带有毛刺的PS2_CLK和PS2_DATA信号,同步到FPGA的内部时钟域,并滤除毛刺;二是检测时钟线的下降沿,作为一位数据采样的触发点。这里通常会用到同步器(两级D触发器链)和滤波器(如计数器实现的消抖)。
- PS/2协议解析模块(ps2_rx):该模块是一个状态机核心。它接收来自接口模块的、已同步和滤波后的时钟信号与数据信号,按照PS/2协议的帧格式(起始位、8位数据位、奇偶校验位、停止位)逐位接收并组装成一个完整的数据字节。它需要检查起始位和停止位的正确性,通常也会忽略奇偶校验(对于学习而言),并在成功接收一帧后,输出数据字节和有效脉冲。
- 键盘/鼠标解码模块(keyboard_decoder / mouse_decoder):协议解析模块输出的是原始的扫描码(Scan Code)或鼠标数据包。这个模块负责根据键盘的扫描码集(通常是Set 2)将其转换为标准的ASCII码或通码/断码状态;对于鼠标,则负责解析三字节数据包,计算出X、Y方向的位移量和按键状态。
2.2 关键设计决策与考量
为什么采用这样的结构?主要是为了清晰和可维护性。将物理信号处理与协议逻辑分离,符合数字设计的“分而治之”思想。ps2_interface模块专注于解决信号完整性问题,这是一个与具体FPGA型号、板卡布局、外部设备特性相关的问题。而ps2_rx模块专注于纯协议逻辑,其行为是确定性的,便于仿真验证。
在信号滤波部分,我并没有使用复杂的数字滤波器,而是采用了经典的“计数器消抖法”。具体来说,当检测到时钟线电平变化时,启动一个计数器,在连续的若干个系统时钟周期内,如果该电平保持稳定,则确认这次变化有效。这个“若干个周期”的取值是关键,它需要大于可能出现的毛刺宽度,但又不能太长以至于影响对合法信号沿的响应。通常,对于PS/2设备(时钟频率约10-20kHz),在50MHz的系统时钟下,设置计数器阈值对应10-20us的滤波时间窗口是一个合理的起点。
状态机的设计采用了经典的三段式写法,清晰地将状态转移、状态寄存和输出逻辑分开。状态包括IDLE(等待起始位)、RECEIVE_DATA(接收8位数据)、RECEIVE_PARITY(接收奇偶位)和RECEIVE_STOP(接收停止位)。在IDLE状态,持续检测数据线是否为低电平(起始位标志),并且只有在时钟下降沿到来时才进行状态转移,确保采样点准确。
3. 核心代码解析与实现细节
让我们深入到几个核心模块的代码细节,看看具体是如何实现的。
3.1 接口同步与滤波模块(ps2_interface)
这个模块的输入是原始的ps2_clk_i和ps2_data_i,输出是滤波后的ps2_clk_f和ps2_data_f,以及一个时钟下降沿标志ps2_clk_negedge。
module ps2_interface ( input wire sys_clk, // 系统时钟,如50MHz input wire sys_rst_n, // 系统复位,低有效 input wire ps2_clk_i, // 原始PS2时钟输入 input wire ps2_data_i, // 原始PS2数据输入 output reg ps2_clk_f, // 滤波后的PS2时钟 output reg ps2_data_f, // 滤波后的PS2数据 output wire ps2_clk_negedge // PS2时钟下降沿标志 ); // 同步器:两级D触发器链,消除亚稳态 reg [1:0] ps2_clk_sync; reg [1:0] ps2_data_sync; always @(posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) begin ps2_clk_sync <= 2'b11; // PS2时钟空闲为高 ps2_data_sync <= 2'b11; end else begin ps2_clk_sync <= {ps2_clk_sync[0], ps2_clk_i}; ps2_data_sync <= {ps2_data_sync[0], ps2_data_i}; end end // 滤波逻辑:检测到变化后,持续采样一段时间,若稳定则更新输出 localparam FILTER_CNT_MAX = 16; // 滤波计数器最大值,对应约0.32us@50MHz reg [3:0] filter_cnt_clk; reg [3:0] filter_cnt_data; reg ps2_clk_stable; reg ps2_data_stable; always @(posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) begin filter_cnt_clk <= 0; filter_cnt_data <= 0; ps2_clk_stable <= 1'b1; ps2_data_stable <= 1'b1; ps2_clk_f <= 1'b1; ps2_data_f <= 1'b1; end else begin // 时钟线滤波 if (ps2_clk_sync[1] != ps2_clk_stable) begin // 检测到潜在变化,开始计数 if (filter_cnt_clk == FILTER_CNT_MAX) begin // 计满,确认变化有效 ps2_clk_stable <= ps2_clk_sync[1]; ps2_clk_f <= ps2_clk_sync[1]; filter_cnt_clk <= 0; end else begin filter_cnt_clk <= filter_cnt_clk + 1; end end else begin // 状态稳定,计数器清零 filter_cnt_clk <= 0; end // 数据线滤波(逻辑同时钟线) if (ps2_data_sync[1] != ps2_data_stable) begin if (filter_cnt_data == FILTER_CNT_MAX) begin ps2_data_stable <= ps2_data_sync[1]; ps2_data_f <= ps2_data_sync[1]; filter_cnt_data <= 0; end else begin filter_cnt_data <= filter_cnt_data + 1; end end else begin filter_cnt_data <= 0; end end end // 下降沿检测:对滤波后的时钟信号进行边沿检测 reg ps2_clk_f_dly; always @(posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) ps2_clk_f_dly <= 1'b1; else ps2_clk_f_dly <= ps2_clk_f; end assign ps2_clk_negedge = ps2_clk_f_dly & (~ps2_clk_f); // 检测到下降沿时输出一个时钟周期的高脉冲 endmodule注意:
FILTER_CNT_MAX的值需要根据你的系统时钟频率和实际信号质量调整。如果系统时钟是100MHz,这个值可能需要增大到32左右,以维持相似的滤波时间窗口。最好的方法是用示波器观察实际的PS2_CLK信号,测量毛刺的宽度,然后设置计数器值使其大于毛刺宽度但小于正常脉冲宽度的1/10。
3.2 协议解析模块(ps2_rx)
这是状态机核心,它接收滤波后的信号和下降沿标志。
module ps2_rx ( input wire sys_clk, input wire sys_rst_n, input wire ps2_clk_f, // 滤波后时钟 input wire ps2_data_f, // 滤波后数据 input wire ps2_clk_negedge, // 时钟下降沿标志 output reg [7:0] rx_data, // 接收到的8位数据 output reg rx_valid, // 数据有效脉冲 output reg rx_error // 接收错误标志(可选) ); // 状态定义 localparam [2:0] IDLE = 3'b000; localparam [2:0] RECV_DATA = 3'b001; localparam [2:0] RECV_PARITY = 3'b010; localparam [2:0] RECV_STOP = 3'b011; localparam [2:0] DONE = 3'b100; reg [2:0] current_state, next_state; reg [3:0] bit_cnt; // 位计数器,用于计数接收的数据位 reg [7:0] data_shift; // 数据移位寄存器 // 状态寄存器(第一段) always @(posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) current_state <= IDLE; else current_state <= next_state; end // 下一状态逻辑与位计数(第二段) always @(*) begin next_state = current_state; case (current_state) IDLE: begin if (ps2_clk_negedge && !ps2_data_f) // 下降沿且数据为低(起始位) next_state = RECV_DATA; end RECV_DATA: begin if (ps2_clk_negedge) begin if (bit_cnt == 4'd7) // 已收满8位数据 next_state = RECV_PARITY; // 否则保持在RECV_DATA状态,由计数器控制 end end RECV_PARITY: begin if (ps2_clk_negedge) next_state = RECV_STOP; end RECV_STOP: begin if (ps2_clk_negedge) next_state = DONE; end DONE: begin next_state = IDLE; // 完成一帧,回到空闲 end default: next_state = IDLE; endcase end // 输出逻辑与移位寄存器(第三段) always @(posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) begin bit_cnt <= 0; data_shift <= 8'h00; rx_data <= 8'h00; rx_valid <= 1'b0; rx_error <= 1'b0; end else begin rx_valid <= 1'b0; // 默认拉低,只在成功接收时产生一个脉冲 rx_error <= 1'b0; case (current_state) IDLE: begin bit_cnt <= 0; data_shift <= 8'h00; end RECV_DATA: begin if (ps2_clk_negedge) begin data_shift <= {ps2_data_f, data_shift[7:1]}; // 低位在先,右移 bit_cnt <= bit_cnt + 1; end end RECV_PARITY: begin if (ps2_clk_negedge) begin // 此处可添加奇偶校验逻辑,本例略过 // parity_received = ps2_data_f; // parity_calculated = ^data_shift; // 按位异或计算奇偶 // if (parity_received != parity_calculated) rx_error <= 1'b1; end end RECV_STOP: begin if (ps2_clk_negedge) begin if (ps2_data_f) begin // 停止位应为高电平 // 接收成功 end else begin rx_error <= 1'b1; // 停止位错误 end end end DONE: begin rx_data <= data_shift; // 锁存数据 rx_valid <= 1'b1; // 产生有效脉冲 // 状态机将在下一个时钟周期自动跳回IDLE end endcase end end endmodule这个三段式状态机是FPGA设计的常见模式,它将时序逻辑(状态转移)和组合逻辑(次态判断、输出)分离,代码清晰且易于综合。在RECV_DATA状态,我们利用bit_cnt来控制接收的位数,每来一个时钟下降沿就右移一位数据(PS2协议是LSB先发)。
3.3 键盘解码模块示例
ps2_rx输出的rx_data是键盘的扫描码。标准键盘(Scan Code Set 2)的按键通常会产生一个“通码”(Make Code),释放时会产生一个“断码”(Break Code),断码通常是通码前缀0xF0。解码模块需要处理这个序列。
module keyboard_decoder ( input wire sys_clk, input wire sys_rst_n, input wire [7:0] scan_code, input wire scan_valid, output reg [7:0] ascii_code, // 转换后的ASCII码,简单映射 output reg key_make, // 按键按下脉冲 output reg key_break // 按键释放脉冲 ); reg [7:0] prev_scan_code; reg is_break_code; always @(posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) begin prev_scan_code <= 8'h00; is_break_code <= 1'b0; ascii_code <= 8'h00; key_make <= 1'b0; key_break <= 1'b0; end else if (scan_valid) begin key_make <= 1'b0; key_break <= 1'b0; if (scan_code == 8'hF0) begin // 接收到断码前缀 is_break_code <= 1'b1; end else begin if (is_break_code) begin // 当前扫描码是断码的后半部分(真正的键码) key_break <= 1'b1; // 可以根据prev_scan_code或scan_code更新状态,如释放特定键 is_break_code <= 1'b0; end else begin // 接收到的是通码 key_make <= 1'b1; prev_scan_code <= scan_code; // 简单的扫描码到ASCII映射(示例:A键) case (scan_code) 8'h1C: ascii_code <= 8'h41; // A -> 'A' 8'h32: ascii_code <= 8'h42; // B -> 'B' 8'h21: ascii_code <= 8'h43; // C -> 'C' // ... 其他键映射 8'h29: ascii_code <= 8'h20; // Space -> Space 8'h66: ascii_code <= 8'h08; // Backspace -> BS default: ascii_code <= 8'h00; // 未定义 endcase end end end end endmodule这是一个简化的解码器,实际应用中可能需要处理扩展键(0xE0前缀)、按键状态保持(如Caps Lock)等更复杂的情况。但基本框架如此:通过一个状态位is_break_code来区分当前接收的是通码还是断码。
4. 工程实现与上板验证要点
有了代码,下一步就是将其在真实的FPGA上跑起来。这个过程涉及到工程创建、约束、仿真和调试。
4.1 Vivado工程创建与约束文件编写
在Xilinx Vivado中,你需要创建一个新项目,添加所有Verilog源文件。最关键的一步是编写约束文件(XDC),将设计中的端口映射到实际FPGA芯片的物理引脚上。
假设你使用的是一块带有PS/2接口的FPGA开发板(比如很多板子的PMOD口可以接PS/2适配器)。你需要知道PS/2接口的时钟线和数据线连接到了FPGA的哪两个引脚。例如:
# 假设系统时钟引脚 set_property PACKAGE_PIN E3 [get_ports sys_clk] set_property IOSTANDARD LVCMOS33 [get_ports sys_clk] create_clock -period 20.000 -name sys_clk -waveform {0.000 10.000} [get_ports sys_clk] # PS/2接口引脚(示例,请根据你的板卡手册修改) set_property PACKAGE_PIN J15 [get_ports ps2_clk_i] set_property IOSTANDARD LVCMOS33 [get_ports ps2_clk_i] set_property PULLUP true [get_ports ps2_clk_i] # PS/2时钟线通常需要上拉 set_property PACKAGE_PIN J16 [get_ports ps2_data_i] set_property IOSTANDARD LVCMOS33 [get_ports ps2_data_i] set_property PULLUP true [get_ports ps2_data_i] # PS/2数据线通常需要上拉 # 输出指示引脚,例如LED,用于显示接收成功或键值 set_property PACKAGE_PIN H17 [get_ports rx_valid] set_property IOSTANDARD LVCMOS33 [get_ports rx_valid] set_property PACKAGE_PIN K15 [get_ports {rx_data[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {rx_data[0]}] # ... 为rx_data[7:1]也分配LED引脚注意:
PULLUP true这一句非常重要。PS/2协议规定,时钟线和数据线在空闲时由主机(此处是FPGA)通过上拉电阻拉到高电平。在FPGA约束中使能内部弱上拉,可以模拟这个行为,省去外部电阻。但有些板子可能已经在外部接了上拉电阻,这时就需要根据实际情况调整。
4.2 仿真测试与调试技巧
在烧录到板子之前,强烈建议进行仿真。你可以编写一个简单的测试平台(Testbench),模拟PS/2设备发送数据。
`timescale 1ns / 1ps module tb_ps2_rx(); reg sys_clk; reg sys_rst_n; reg ps2_clk_sim; reg ps2_data_sim; wire [7:0] rx_data; wire rx_valid; // 实例化被测模块 ps2_top uut ( .sys_clk(sys_clk), .sys_rst_n(sys_rst_n), .ps2_clk_i(ps2_clk_sim), .ps2_data_i(ps2_data_sim), .rx_data(rx_data), .rx_valid(rx_valid) ); // 生成50MHz系统时钟 always #10 sys_clk = ~sys_clk; // 20ns周期 // 模拟PS/2设备发送一个字节(例如 0x5A) task ps2_send_byte; input [7:0] data; integer i; reg parity; begin // 计算奇偶位(奇校验) parity = ~(^data); // 发送起始位(0) ps2_data_sim = 1'b0; #50000; // 等待约50us,模拟设备准备 // 发送8位数据(LSB first) for (i=0; i<8; i=i+1) begin ps2_clk_sim = 1'b1; #30000; // 时钟高电平时间 ps2_clk_sim = 1'b0; ps2_data_sim = data[i]; #50000; // 时钟低电平时间(数据在下降沿被主机采样) end // 发送奇偶位 ps2_clk_sim = 1'b1; #30000; ps2_clk_sim = 1'b0; ps2_data_sim = parity; #50000; // 发送停止位(1) ps2_clk_sim = 1'b1; #30000; ps2_clk_sim = 1'b0; ps2_data_sim = 1'b1; #50000; // 释放总线(回到高阻,由上拉拉到高) ps2_clk_sim = 1'b1; #100000; // 帧结束后的空闲时间 end endtask initial begin // 初始化 sys_clk = 0; sys_rst_n = 0; ps2_clk_sim = 1'b1; // 空闲高电平 ps2_data_sim = 1'b1; #100; sys_rst_n = 1; #200000; // 等待系统稳定 // 测试发送 ps2_send_byte(8'h5A); // 发送一个字节 #500000; $finish; end endmodule在仿真中,你可以观察rx_data和rx_valid信号,看是否在正确的时刻捕获到发送的数据0x5A。通过仿真,你可以提前发现状态机逻辑错误、时序问题等。
4.3 上板验证与调试实录
仿真通过后,就可以生成比特流并下载到FPGA了。上板验证是发现问题最多的环节。
无反应:按键后LED或显示器无任何变化。
- 检查约束:首先双查PS/2的时钟和数据线引脚分配是否正确,
IOSTANDARD是否匹配板卡电压(通常是3.3V LVCMOS)。 - 检查上拉:确认约束中或板卡上PS/2信号是否有上拉。没有上拉,信号无法回到高电平,设备无法工作。用万用表量一下空闲时引脚电压是否为高。
- 检查时钟:用示波器或逻辑分析仪探测PS2_CLK引脚。当按下按键时,应该能看到一串约10-20kHz的时钟脉冲。如果没有,可能是键盘/鼠标没供电,或接口线序接反(PS/2接口的时钟和数据脚序是固定的)。
- 内部信号探测:使用Vivado的ILA(集成逻辑分析仪)IP核,将
ps2_clk_f、ps2_data_f、ps2_clk_negedge以及状态机状态current_state等内部信号抓出来。这是最强大的调试手段,可以直观地看到滤波后的信号是否干净,状态机是否按预期跳转。
- 检查约束:首先双查PS/2的时钟和数据线引脚分配是否正确,
数据错误:有反应,但接收到的键值不对。
- 采样点问题:PS/2协议规定主机在时钟下降沿采样数据。确保你的下降沿检测逻辑正确。在ILA中查看
ps2_clk_negedge脉冲是否准确出现在滤波后时钟的下降沿。 - 位顺序问题:确认你的移位寄存器方向是否正确。PS/2是LSB(最低位)先发送。代码示例中
{ps2_data_f, data_shift[7:1]}是右移,符合LSB先入的惯例。 - 滤波过重:如果
FILTER_CNT_MAX设置得太大,可能会滤掉正常的时钟沿,导致漏掉数据位。尝试减小该值,或在ILA中对比滤波前后的信号,看是否把正常信号也滤掉了。
- 采样点问题:PS/2协议规定主机在时钟下降沿采样数据。确保你的下降沿检测逻辑正确。在ILA中查看
不稳定,时好时坏:
- 亚稳态:这是跨时钟域问题的典型表现。虽然PS/2时钟频率不高,但相对于FPGA系统时钟仍是异步的。确保使用了至少两级同步器(如代码中的
ps2_clk_sync)。 - 电源噪声:确保FPGA和键盘/鼠标供电稳定。尤其是使用一些简单的USB转PS/2适配器时,其电源可能噪声较大。
- 时序约束:为异步输入的PS/2信号设置正确的时序约束(
set_input_delay),虽然对于这种低速信号通常不严格要求,但严谨的做法可以避免一些边际问题。
- 亚稳态:这是跨时钟域问题的典型表现。虽然PS/2时钟频率不高,但相对于FPGA系统时钟仍是异步的。确保使用了至少两级同步器(如代码中的
5. 常见问题与进阶优化指南
在实际使用和教学过程中,我总结了一些常见问题和可以让设计更稳健的进阶方法。
5.1 典型问题速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 完全无响应,LED不亮 | 1. 引脚约束错误 2. 电源或时钟未连接 3. PS/2端口无上拉 | 1. 检查XDC文件引脚号、电平标准。 2. 测量系统时钟是否输入,复位信号是否有效。 3. 测量PS/2_CLK/DATA空闲时电压,应为~3.3V,否则在约束中启用 PULLUP或外接上拉电阻。 |
| 按键有时灵有时不灵 | 1. 信号毛刺干扰 2. 滤波参数不合适 3. 亚稳态 | 1. 用ILA观察原始ps2_clk_i信号,看是否有毛刺。调整滤波器计数器阈值。2. 确保同步器(两级D触发器)存在。 3. 检查板卡接地和电源质量。 |
| 接收到的数据总是错误 | 1. 数据位采样点错误 2. LSB/MSB顺序弄反 3. 起始位/停止位判断逻辑有误 | 1. 确认在ps2_clk_f的下降沿采样ps2_data_f。2. 检查移位寄存器方向,PS/2是LSB先发。 3. 在ILA中对比发送的数据波形和状态机接收的数据。 |
| 只能接收第一个字节,后续字节收不到 | 状态机未能正确回到IDLE状态 | 检查状态机DONE状态后的跳转逻辑,确保在输出rx_valid后下一个周期能回到IDLE,并清空位计数器。 |
| 使用某些键盘/鼠标不正常 | 1. 设备兼容性问题(如发送Set3码) 2. 设备需要初始化 | 1. 大多数现代USB键盘的PS/2模式兼容Set2。可尝试更换设备测试。 2. 部分鼠标需要主机发送初始化指令(0xFF)后才能开始报告数据。这需要实现发送功能。 |
5.2 设计进阶与优化建议
当你掌握了基本接收功能后,可以考虑以下扩展,让这个设计更实用、更健壮:
双向通信实现:完整的PS/2主机需要能向设备发送命令。这需要实现一个
ps2_tx模块,其状态机控制将数据线拉低产生起始位,然后在时钟线的上升沿(由设备产生)切换数据位。需要处理好主机和设备对总线控制的切换(时钟线由设备驱动,但在主机发送起始位前,需要主动拉低时钟线至少100us以抑制设备通信)。FIFO缓冲:在协议解析模块和数据应用模块(如显示、串口转发)之间加入一个FIFO(先入先出存储器)。这样当应用端处理速度较慢时,不会丢失快速按键产生的扫描码。Xilinx IP Catalog中的
FIFO GeneratorIP可以很方便地实现。去抖优化:之前的滤波主要是针对信号线上的毛刺。对于按键本身产生的机械抖动,可以在解码器层面增加软件去抖逻辑,例如,只有当同一个键的“通码”被稳定识别多次后才认为有效按下,避免一次物理按下产生多个字符。
协议扩展:实现对鼠标三字节数据包的完整解析,计算出带符号的X、Y位移量(数据包中的位移是9位补码形式),并解析左、中、右键的状态。这需要解码模块能缓存连续三个字节,并识别数据包的起始(通常第一个字节的某些位有特定模式)。
应用集成:将这个PS/2控制器作为一个IP核,集成到更大的系统中。例如,将键值通过UART发送到电脑串口助手显示;或者用键盘控制七段数码管显示数字;更复杂的,可以作为一个简单文本输入接口,配合VGA显示模块,在屏幕上实现字符输入。
这个项目麻雀虽小,五脏俱全。它涉及了FPGA开发中从底层信号处理、状态机设计、协议实现到上板调试的完整流程。理解并亲手实现它,对你构建更复杂的数字系统,比如SPI、I2C、UART等接口控制器,会是一个极好的铺垫。希望这份来自实际项目的代码和总结,能帮你少走些弯路。
本文还有配套的精品资源,点击获取