简介:本资源是一套基于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验证过的键盘鼠标接口设计
最近在整理硬盘里的老项目,翻出来一个压箱底的宝贝——一个用Verilog写的、在Xilinx FPGA上验证通过的键盘和鼠标接口源码。这玩意儿是我当年刚入行FPGA时,为了彻底搞懂PS/2协议,硬着头皮从零开始撸出来的。现在回头看,代码风格可能有点“稚嫩”,但整个设计思路清晰,模块划分明确,最关键的是它真的能跑起来,键盘按键和鼠标移动都能被FPGA正确识别。对于正在学习数字电路设计、Verilog语言,特别是想亲手实现一个外设接口的FPGA新手来说,这个项目是个非常不错的“麻雀”,五脏俱全,值得拆开细细研究。它不涉及复杂的算法,核心就是状态机、时序逻辑和协议解析,能把书本上那些枯燥的理论瞬间变成板子上闪烁的LED和串口打印出来的键值,这种成就感是看多少遍教程都换不来的。
2. 核心设计思路与协议解析
2.1 为什么选择PS/2接口?
现在市面上USB一统天下,为什么这个源码还是基于PS/2协议?这里有几个很实际的考虑。首先,协议简单。PS/2是典型的同步串行协议,时钟线和数据线分开,通信速率不高(通常10-20kHz),时序要求相对宽松,非常适合作为FPGA初学者的第一个通信协议实践项目。你不需要处理USB那种复杂的包结构、CRC校验和复杂的枚举过程。其次,便于调试。你可以用逻辑分析仪甚至FPGA上的ILA(集成逻辑分析仪)轻松抓取时钟和数据信号,直观地看到每一位的传输过程,对理解通信原理有巨大帮助。最后,资源占用极低。整个接口逻辑可能只需要几十个Slice,在任何一款入门级FPGA(比如Spartan-6、Artix-7)上都能轻松实现,让你专注于逻辑设计本身,而不是为资源不够而发愁。
2.2 PS/2协议核心要点拆解
PS/2协议的精髓可以概括为“设备产生时钟,主机读取数据”。但作为主机端的FPGA,我们需要模拟这个“读取”的过程。
电气与连接:键盘或鼠标通过一根6针的Mini-DIN接口与主机连接。我们真正关心的只有四根线:VCC(+5V)、GND、CLK(时钟)和DATA(数据)。在FPGA端,CLK和DATA需要被设置为双向口(inout),但通常我们初始化为输入,以监听设备发送的数据。这里有一个关键细节:PS/2总线是“线与”的,空闲时为高电平。设备通过将时钟线或数据线拉低来发起通信。
数据帧格式:每一帧数据包含11位。
- 起始位:总是为0(低电平)。
- 8位数据位:从最低位(LSB)开始发送。
- 奇偶校验位:奇校验。即数据位中“1”的个数,加上校验位本身,“1”的总数应为奇数。这是一个简单的错误检测机制。
- 停止位:总是为1(高电平)。
- 应答位:在设备到主机的通信中,主机在收到数据后,需要在第11个时钟周期将数据线拉低作为应答。但很多设计(包括本例)中,FPGA作为主机只负责接收,不发送应答,设备也能正常工作。不过,一个健壮的设计应该实现应答。
时序是关键:设备在CLK的下降沿(高电平到低电平的变化)改变DATA的数据。而主机(我们的FPGA)应该在CLK的上升沿(低电平到高电平的变化)采样DATA数据,以确保数据稳定。这个“设备下降沿输出,主机上升沿采样”的规则必须严格遵守。
注意:虽然协议规定设备驱动时钟,但在实际接收时,FPGA内部的逻辑必须用时钟的上升沿来触发采样。这个时钟是外部输入的,所以你的采样逻辑本质上是一个对输入时钟敏感的同步逻辑。处理好这个外部异步时钟与FPGA内部全局时钟的关系,是避免亚稳态问题的关键。
3. 源码模块深度解析与实操要点
拿到keyboard_mouse_interface.v这个源码文件,我们不要急着看整体,先把它拆成几个功能明确的模块来理解。一个结构清晰的设计通常包含以下部分:
3.1 顶层模块(Top Module):系统的指挥官
顶层模块叫ps2_host,它就像项目的总接线图。它的主要任务有三个:
- 端口定义:声明与外部世界连接的所有信号。包括FPGA的全局时钟
clk、复位rst_n、PS/2的时钟和数据线ps2_clk、ps2_data,以及输出给其他模块使用的键盘扫描码key_code、鼠标数据包mouse_data和相应的数据有效标志key_valid、mouse_valid。 - 模块实例化:它内部会实例化两个核心子模块——
ps2_keyboard和ps2_mouse,分别处理键盘和鼠标的数据流。这里体现了模块化设计的思想:功能分离,各司其职。 - 时钟域处理(关键!):这是最容易出问题的地方。PS/2的时钟(~20kHz)和FPGA的系统时钟(比如50MHz)是不同源的。顶层模块需要负责将
ps2_clk这个慢速的异步信号安全地引入到FPGA的时钟系统中。常见的做法是使用两级D触发器进行同步,也就是所谓的“同步器”,来大幅降低亚稳态传播的风险。代码里可能会有一个ps2_clk_sync的信号,它就是经过同步化处理后的“干净”时钟,供后续逻辑使用。
3.2 键盘接口模块(ps2_keyboard):解码按键语言
这个模块是解析键盘扫描码的核心。它的状态机是设计的灵魂。
状态机设计:通常是一个有限状态机(FSM),包含以下几个状态:
IDLE:空闲状态,等待时钟线被设备拉低(起始位开始)。START:检测到起始位,准备接收数据。DATA:循环接收8个数据位。这里会用一个计数器(bit_cnt)来控制。PARITY:接收奇偶校验位,并进行校验计算。STOP:接收停止位。如果一切正常,将接收到的8位数据(扫描码)输出,并拉高key_valid信号一个时钟周期。ERROR:如果校验失败或帧格式错误,进入错误状态,丢弃本帧数据并等待重新开始。
扫描码处理:键盘的扫描码分为通码(Make)和断码(Break)。按下按键时发送通码,释放时先发送
0xF0,再发送该键的通码。因此,一个完整的按键事件需要解析多帧数据。这个模块需要包含一个小的缓存逻辑,来组合判断是按下事件还是释放事件,并输出一个统一的、易于上层应用的键值代码。消抖处理:虽然PS/2协议层面已经比较稳定,但按键的机械抖动可能仍然存在。一个更稳健的设计会在收到有效键值后,进行一个短时间的延时(例如10-20ms)再确认,或者使用简单的滤波逻辑,避免一次按键被误识别为多次。
3.3 鼠标接口模块(ps2_mouse):追踪光标移动
鼠标协议比键盘稍复杂一点,因为它默认工作在流模式,会持续不断地发送数据包。
数据包结构:一个标准的PS/2鼠标数据包包含3个字节,连续发送。
- Byte 1:状态字节。包含按键信息(左、右、中键按下状态)和溢出标志,以及最重要的——X、Y位移量的符号位(第4、5位)。
- Byte 2:X方向位移量。是一个9位有符号数的低8位,最高位(符号位)在状态字节中。正值表示向右移动。
- Byte 3:Y方向位移量。同样是一个9位有符号数的低8位,最高位在状态字节中。注意:Y轴方向与屏幕通常相反,正值表示向下移动,但鼠标移动时是向上,所以实际坐标累加时可能需要取反。
模块工作流程:该模块也需要一个状态机,但它的状态循环是:等待包起始 -> 接收字节1 -> 接收字节2 -> 接收字节3 -> 输出完整数据包 -> 继续等待下一个包。因为鼠标是连续发送的,所以它的
IDLE状态可能只是在等待一个包内字节间的短暂间隔。坐标累加器:一个实用的鼠标接口模块,不会只输出原始的位移字节。它内部通常会维护两个寄存器:
x_pos和y_pos。每当收到一个完整的3字节包,就将有符号的位移量累加到当前位置寄存器上,并将累加后的坐标值输出。这样,上层应用拿到的是绝对的坐标信息,而不是需要自己处理的相对位移。
3.4 测试与调试模块(Simulation/Testbench)
源码包里很可能还包含一个tb_ps2_host.v的文件,这是用于在Modelsim或Vivado Simulator中进行仿真的测试平台。学习看测试平台和看设计代码一样重要。
- 如何模拟PS/2设备:测试平台里会有一个
task或initial块,来模拟键盘或鼠标的行为。例如,它会按照PS/2时序,精确地在时钟下降沿改变数据线的值,发送一帧预设的扫描码(比如字母‘A’的通码0x1C)。 - 查看波形:通过仿真,你可以在波形图中清晰地看到:
ps2_clk和ps2_data上的模拟波形。- 状态机(
state)是如何一步步跳转的。 bit_cnt如何从0计数到10。- 最终,
key_code和key_valid信号是否在正确的时间点输出预期的值。
- 自我验证:尝试修改测试平台,发送一个错误的校验位数据包,观察你的设计是否能跳转到
ERROR状态并妥善处理。
4. 基于源码的FPGA上板验证全流程
有了源码和仿真信心,下一步就是让它在一块真实的FPGA开发板上跑起来。这里以常见的Xilinx Artix-7开发板(如Basys3、Nexys4)为例。
4.1 硬件连接与约束文件(.xdc)
这是从代码到硬件的桥梁,一步错,满盘输。
- 物理连接:你需要一根PS/2连接线(通常是母头),将其时钟和数据线通过杜邦线连接到FPGA的普通IO引脚上。务必查阅开发板原理图,确认你连接的引脚没有其他特殊功能(如专用时钟输入)。VCC和GND也需要接好,为键盘/鼠标供电。
- 编写XDC约束:这是最关键的一步。你需要告诉Vivado工具每个端口对应FPGA的哪个物理引脚,以及其电气标准。
# 示例:假设将ps2_clk连接到开发板上的JXADC接口的某个引脚(具体查原理图) set_property PACKAGE_PIN “J15” [get_ports ps2_clk] set_property IOSTANDARD LVCMOS33 [get_ports ps2_clk] set_property PACKAGE_PIN “H15” [get_ports ps2_data] set_property IOSTANDARD LVCMOS33 [get_ports ps2_data] # 系统时钟和复位引脚 set_property PACKAGE_PIN “W5” [get_ports clk] # 假设是100MHz时钟 set_property IOSTANDARD LVCMOS33 [get_ports clk] create_clock -period 10.000 -name sys_clk -waveform {0.000 5.000} [get_ports clk]踩坑记录:
IOSTANDARD一定要设对!3.3V的开发板就设LVCMOS33,1.8V的就设LVCMOS18。设错了要么检测不到信号,要么可能损坏设备。PACKAGE_PIN的编号必须百分百准确,一个数字错了就连不上。
4.2 Vivado中的综合、实现与下载
- 创建工程与添加源码:在Vivado中新建项目,选择正确的FPGA型号(例如xc7a35tcsg324-1)。将所有的
.v源文件、测试文件以及刚写好的.xdc约束文件添加到工程中。 - 综合(Synthesis):点击Run Synthesis。这个过程将你的Verilog代码转换成门级网表。重点关注“Messages”窗口,不要有“Critical Warning”或“Error”。常见的警告如未连接的端口,需要根据情况处理。
- 实现(Implementation):综合成功后,运行Run Implementation。这一步包括布局布线(Place & Route),将逻辑门映射到FPGA的实际资源上。完成后,可以打开“Implemented Design”查看资源利用率报告。我们这个设计应该只占用极少的LUT和FF。
- 生成比特流(Generate Bitstream):实现无误后,生成
.bit文件。 - 下载与调试:
- 连接好JTAG下载器,给开发板上电。
- 在Vivado中打开Hardware Manager,自动识别设备,然后Program Device,选择生成的
.bit文件进行下载。 - 最直接的验证:将源码中输出的
key_code连接到开发板的LED灯上(比如低8位接8个LED)。按下键盘按键,观察LED显示的二进制数是否与该键的扫描码对应(例如,按键‘A’,LED可能显示00011100即0x1C)。对于鼠标,可以将x_pos和y_pos的某些位接到LED上,移动鼠标看LED变化。
4.3 使用ILA进行在线调试
如果LED显示不正常,或者你想更深入地观察内部信号,Vivado的ILA(集成逻辑分析仪)是你的终极武器。
- 标记调试网络(Mark Debug):在综合后的网表或RTL源码中,右键点击你想观察的信号(如
state、bit_cnt、ps2_clk_sync、key_valid),选择“Mark Debug”。 - 重新综合与实现:因为添加了调试逻辑,需要重新运行综合和实现。
- 设置ILA触发条件:下载包含ILA核的比特流后,在Hardware Manager中设置触发条件。例如,可以设置为当
key_valid信号从0变为1时触发捕获。 - 抓取波形分析:触发后,ILA会捕获一段时间内这些信号的实际波形。你可以像看仿真波形一样,检查状态机跳转、数据采样点是否与预期完全一致。这是定位时序问题的黄金手段。
5. 项目进阶与优化方向
这个基础项目跑通后,你可以尝试以下方向进行深化和扩展,这会让你的学习从“会用”上升到“理解”和“创造”。
5.1 协议扩展:从标准PS/2到更复杂的交互
- 实现主机到设备的发送:目前的设计是只读的。尝试增加FPGA向键盘/鼠标发送命令的功能,例如初始化鼠标、设置采样率、点亮键盘指示灯(Num Lock, Caps Lock, Scroll Lock)。这需要你实现主机驱动时钟线和数据线的逻辑,状态机会变得更加复杂。
- 解析扩展键盘扫描码:标准101/102键键盘使用一套扫描码集(Scan Code Set 2)。尝试让你的解析器支持更多的功能键。
- 鼠标滚轮支持:许多PS/2鼠标支持滚轮,其数据包会扩展为4个字节。尝试修改鼠标解析模块,支持解析第4个字节的滚轮信息。
5.2 系统集成:构建一个微型输入系统
- 添加消抖与按键状态机:在键盘解码器之后,增加一个按键处理模块。它维护一个按键状态映射表(比如128位的寄存器,每一位代表一个扫描码是否被按下),并产生“按下”、“保持”、“释放”等更高级的事件信号,方便上层应用(如游戏)使用。
- VGA显示字符或光标:将键盘输入的字符显示在VGA显示器上,或者用鼠标控制一个屏幕上的光标。这需要你学习VGA时序控制,并建立一个帧缓冲区(Frame Buffer)。这是一个经典的综合性项目,能把输入、处理、输出完整串联起来。
- 通过UART上传到PC:将FPGA识别到的键值或鼠标坐标,通过UART串口发送到电脑,在串口助手上显示。这练习了另一个常用通信协议——异步串口。
5.3 代码质量与可靠性提升
- 添加参数化设计:使用
parameter或localparam来定义时钟频率、去抖延时时间等,让代码更容易移植到不同时钟频率的FPGA上。 - 完善的错误处理:当前设计可能只是简单地丢弃错误帧。可以增加错误计数器,当连续错误超过一定阈值时,通过一个
error信号通知系统进行复位或重新初始化。 - 跨时钟域处理(CDC)规范化:对于
key_valid这类从慢速PS/2时钟域到快速系统时钟域的单脉冲信号,仅用同步器可能不够。可以考虑使用“脉冲同步器”或“握手协议”来安全地传递控制信号,避免数据丢失。
6. 常见问题与调试技巧实录
在实际调测中,你几乎一定会遇到下面这些问题。我把我的踩坑记录和解决方法分享出来,希望能帮你节省大量时间。
| 问题现象 | 可能原因 | 排查思路与解决方法 |
|---|---|---|
| 上电后,按键无任何反应,LED不亮 | 1. 硬件连接错误(线接反、虚焊)。 2. 约束文件错误(引脚号、电平标准)。 3. 键盘/鼠标未供电或损坏。 | 1.万用表检查:测量PS/2接口的VCC和GND之间是否有~5V电压。测量CLK和DATA引脚在空闲时是否为高电平(~3.3V或~5V)。 2.双重检查约束:逐字核对.xdc文件中的引脚编号与原理图是否一致。 3.替换法:换一个已知好的键盘或鼠标试试。 |
| 按键时LED有反应,但显示的值乱七八糟,或每次值都不同 | 1.时序采样点错误(最常见)。在时钟的下降沿采样了数据。 2. 亚稳态导致采样值错误。 3. 消抖逻辑有问题,一次按键被识别成多次。 | 1.ILA抓波形:这是最有效的办法。抓取ps2_clk和ps2_data,放大看一个数据位。确认你的采样点(代码中always@(posedge ps2_clk_sync)内的操作)是否发生在ps2_clk的上升沿,并且此时ps2_data已经稳定。2.检查同步器:确保对输入的 ps2_clk使用了至少两级D触发器进行同步。3.简化测试:在测试平台中发送一个固定的扫描码(如0xAA,二进制10101010),看波形是否完美匹配,先排除硬件问题。 |
| 鼠标能检测到按键,但移动时坐标变化不对或不动 | 1. Y坐标符号处理错误。 2. 位移量拼接错误(符号位从状态字节取错)。 3. 鼠标未正确初始化(可能需要发送初始化命令)。 | 1.打印/显示原始数据包:将鼠标输出的3个字节原始数据通过UART发送到电脑,与已知的鼠标数据包格式对比。移动鼠标,看第二个字节(X位移)的变化是否符合预期(右移增大,左移减小)。 2.检查符号位提取:确认从状态字节(Byte1)提取X、Y符号位(第4、5位)的代码是否正确。 3.查阅鼠标数据手册:确认你的鼠标默认的数据报告模式。有些鼠标可能需要上电后由主机发送 0xF4(使能数据报告)命令。 |
| 仿真完全正确,但下板后行为异常 | 1. 约束中的时钟频率与实际板载晶振频率不符。 2. 输入引脚未设置上拉电阻,PS/2总线空闲时为高阻态而非高电平。 3. 物理信号质量问题(导线过长,干扰大)。 | 1.检查时钟约束:create_clock语句中的-period值是否与板卡实际晶振频率匹配(例如,100MHz晶振,周期是10ns)。2.启用内部上拉:在XDC约束中为 ps2_clk和ps2_data添加上拉属性:set_property PULLUP TRUE [get_ports ps2_clk]。3.缩短连接线,并确保连接牢固。如果可能,使用带屏蔽的PS/2连接线直接连接到板载接口(如果板子有的话)。 |
| 资源利用率异常高 | 1. 代码中存在不可综合的语句(如initial块用于寄存器赋值,#延时)。2. 状态机编码方式效率低(如独热码One-Hot在状态多时耗资源)。 3. 不必要的寄存器被综合出来。 | 1.检查综合报告:看哪些模块占用资源多。确保所有用于硬件实现的代码都是可综合的。 2.状态机编码:对于小状态机(<8个状态),二进制编码(Binary)或格雷码(Gray)比独热码更省资源。可以通过 (* fsm_encoding = “gray” *)等综合属性指导工具。3.优化代码:移除调试用的、不影响功能的中间寄存器。 |
这个项目源码的价值,远不止几段可运行的Verilog代码。它更像一个精心设计的教学样本,完整展示了一个数字接口从协议理解、状态机设计、RTL编码、功能仿真到最终上板调试的全过程。我建议你不要满足于直接使用它,而是以它为蓝图,亲手重新敲一遍代码,过程中故意制造一些上述的“常见问题”,然后利用仿真和ILA工具去定位和解决。这个过程里积累的调试经验和对时序的深刻理解,才是你从“FPGA学习者”迈向“FPGA开发者”最关键的一步。当你能够游刃有余地修改、扩展并稳定运行这个项目时,你会发现再去学习SPI、I2C、UART等其他接口协议,思路都会清晰很多,因为底层的同步、采样、状态机思想都是相通的。
本文还有配套的精品资源,点击获取