从键盘敲击到屏幕显示:手把手用Verilog在HDLbits上实现一个简易PS/2键盘数据包解析器
从键盘敲击到屏幕显示:手把手用Verilog在HDLbits上实现一个简易PS/2键盘数据包解析器
当你按下键盘上的某个按键时,这个简单的物理动作背后隐藏着一系列复杂的数字通信过程。作为FPGA开发者,理解并实现这一过程不仅能加深对硬件描述语言的理解,更能为自定义输入设备开发打下坚实基础。本文将带你从零开始,在HDLbits平台上构建一个完整的PS/2键盘数据包解析系统,涵盖从状态机设计到数据路径处理的完整流程。
1. PS/2协议基础与项目架构
PS/2接口虽然已被USB取代,但其简洁的同步串行协议仍是学习数字通信的理想案例。每个按键动作会触发键盘发送包含3个字节的数据包:
- 起始字节:最高位为1(通常为0xE0或0xF0)
- 扫描码字节:表示实际按键位置
- 结束字节:校验和或附加功能标识
// 典型PS/2数据包示例 8'hE0 // 扩展键标识 8'h1F // 左Ctrl键扫描码 8'h00 // 保留字节在FPGA中处理这类数据流需要解决三个核心问题:
- 时序同步:PS/2时钟频率为10-16.7kHz,需正确采样数据线
- 数据完整性:检测起始位、校验停止位
- 状态管理:跟踪数据包接收进度
提示:HDLbits的Fsm_ps2题目已简化流程,假设数据已正确采样到in[7:0]总线,我们只需关注协议解析逻辑。
2. 有限状态机设计精要
2.1 状态机拓扑选择
对于PS/2解析器,最适合采用Mealy型状态机,因为输出(done信号)不仅取决于当前状态,还与输入数据(in[3])直接相关。基本状态应包括:
| 状态 | 功能描述 | 转移条件 |
|---|---|---|
| IDLE | 等待数据包开始 | in[3]==1 → BYTE1 |
| BYTE1 | 接收第一个数据字节 | 自动→ BYTE2 |
| BYTE2 | 接收第二个数据字节 | 自动→ BYTE3 |
| BYTE3 | 接收第三个数据字节 | 自动→ DONE |
| DONE | 标记数据包接收完成 | in[3]==1 → BYTE1 |
parameter IDLE = 0, BYTE1 = 1, BYTE2 = 2, BYTE3 = 3, DONE = 4; reg [2:0] state; always @(posedge clk) begin if (reset) state <= IDLE; else case(state) IDLE: state <= in[3] ? BYTE1 : IDLE; BYTE1: state <= BYTE2; BYTE2: state <= BYTE3; BYTE3: state <= DONE; DONE: state <= in[3] ? BYTE1 : IDLE; endcase end2.2 关键设计技巧
- 同步复位优先:确保状态机从已知初始状态启动
- 组合逻辑分离:状态转移条件单独处理提升可读性
- 输出寄存器化:避免组合逻辑输出导致的毛刺
注意:HDLbits的Fsm_ps2题目特别要求done信号在DONE状态有效,这需要严格同步输出逻辑。
3. 数据路径扩展实现
在Fsm_ps2data题目中,我们需要扩展基本状态机以捕获完整数据包。此时需注意:
- 数据锁存时机:每个字节应在状态转移时存储
- 字节序管理:24位输出需按[23:16],[15:8],[7:0]组织
- 输出使能:仅当done有效时更新输出总线
reg [23:0] packet_data; always @(posedge clk) begin if (next_state == BYTE2) packet_data[23:16] <= in; else if (next_state == BYTE3) packet_data[15:8] <= in; else if (next_state == DONE) packet_data[7:0] <= in; end assign out_bytes = done ? packet_data : 24'b0;常见问题排查:
- 数据错位:检查状态机与锁存逻辑的同步性
- 重复捕获:确保每个字节只锁存一次
- 输出抖动:添加done信号作为输出使能
4. 系统集成与调试技巧
4.1 七段数码管显示集成
将解析出的键值映射到数码管显示是验证工作的有效方式。典型实现包括:
- 扫描码转换:建立查找表将原始数据转为ASCII或自定义编码
- 显示驱动:分时复用多个数码管
- 消抖处理:添加20ms延迟防止重复触发
// 简化的扫描码到七段码转换 always @(*) begin case(out_bytes[15:8]) // 取中间字节作为主键值 8'h1C: seg_data = 8'b01100011; // 'A' 8'h32: seg_data = 8'b01100011; // 'B' // ...其他键值映射 default: seg_data = 8'b00000000; // 空白 endcase end4.2 在线调试策略
- 仿真验证:使用$display实时监控状态转移
always @(state) begin $display("[%t] State change: %d", $time, state); end - 分段测试:先验证状态机再添加数据路径
- 边界测试:特别测试数据包边界条件(如连续快速按键)
5. 性能优化进阶
对于需要处理高速输入的系统,可考虑以下优化:
- 流水线架构:分离协议解析与数据处理阶段
- 双缓冲技术:避免显示更新期间的数据冲突
- 时钟域交叉:使用FIFO处理异步PS/2时钟
// 双缓冲实现示例 reg [23:0] active_buffer, shadow_buffer; always @(posedge clk) begin if (done) begin shadow_buffer <= packet_data; if (display_idle) active_buffer <= shadow_buffer; end end实际项目中,我曾遇到快速连续按键导致数据覆盖的问题。通过添加数据有效标志位和简单的流控机制,最终实现了稳定的1000Hz按键采样率——这远高于PS/2协议的标准速率,为特殊应用场景提供了充足余量。
