FPGA实现PS/2键盘解码与串口通信:从协议解析到系统集成
2026/6/5 14:16:37 网站建设 项目流程

1. 项目概述:从PS/2接口到串口输出的FPGA键盘解码器

最近在整理一些FPGA的入门项目,翻到了之前做的一个PS/2键盘解码的小玩意儿。这个项目的核心目标很直接:用一块FPGA开发板,接上一个老式的PS/2键盘,当你按下键盘上的字母键时,FPGA能识别出是哪个键,然后通过串口把对应的ASCII码发送到电脑上,在串口调试助手里实时显示出来。听起来像是把两个“古董”协议(PS/2和UART)用现代的可编程逻辑连了起来,但恰恰是这种“桥梁”项目,最能锻炼你对时序逻辑、协议解析和系统集成的理解。

PS/2接口虽然现在用得少了,但在嵌入式系统和一些工控场景里还能见到,它的通信协议本身非常经典,是一种同步串行协议。而UART(串口)更是嵌入式开发者的“老朋友”,调试必备。用FPGA来实现这两个协议的对接,你不仅能搞懂它们各自是怎么工作的,更能深刻体会硬件描述语言(Verilog)如何“安排”硬件资源去精准地捕捉信号、处理数据。这个项目适合已经学过Verilog基础语法、搞过LED闪烁和按键消抖,想要挑战一下具体通信协议实现的同学。下面,我就把当时的设计思路、代码细节,以及调试过程中踩过的坑,系统地梳理一遍。

2. 核心设计思路与模块划分

整个系统的数据流非常清晰:PS/2键盘产生按键扫描码 -> FPGA接收并解码 -> 将扫描码转换为ASCII码 -> 通过UART TX发送出去。基于这个流程,我采用了经典的“自顶向下”模块化设计,将系统划分为三个核心功能模块和一个顶层整合模块。

2.1 顶层模块:系统的“接线图”

顶层模块(我命名为ps2_key)不实现复杂逻辑,它的主要职责是“接线”和“调度”。它定义了整个系统对外的引脚(与物理世界交互的接口)和对内的互连(模块间的数据与控制流)。我们来看一下它的代码骨架:

module ps2_key( input clk, // 50MHz 系统主时钟 input rst_n, // 低电平有效的全局复位信号 input ps2k_clk, // PS/2接口的时钟线 input ps2k_data, // PS/2接口的数据线 output rs232_tx // UART发送数据线 ); // 内部连线声明 wire [7:0] ps2_byte; // 从键盘接收到的1字节键值(扫描码) wire ps2_state; // 按键状态标志,高电平表示有新的有效键值 wire bps_start; // 波特率发生器启动信号 wire clk_bps; // 波特率时钟,用于UART发送的位定时 // 实例化三个子模块 ps2scan u_ps2scan ( .clk(clk), .rst_n(rst_n), .ps2k_clk(ps2k_clk), .ps2k_data(ps2k_data), .ps2_byte(ps2_byte), .ps2_state(ps2_state) ); speed_select u_speed_select ( .clk(clk), .rst_n(rst_n), .bps_start(bps_start), .clk_bps(clk_bps) ); my_uart_tx u_my_uart_tx ( .clk(clk), .rst_n(rst_n), .clk_bps(clk_bps), .rx_data(ps2_byte), // 注意:这里将接收到的键值作为发送数据 .rx_int(ps2_state), // 按键状态作为发送启动信号 .rs232_tx(rs232_tx), .bps_start(bps_start) ); endmodule

设计要点解析:

  1. 接口定义clkrst_n是数字系统的标配。ps2k_clkps2k_data需要连接到FPGA的普通IO引脚,注意PS/2接口是5V电平,而多数FPGA是3.3V LVCMOS电平,中间可能需要电平转换或使用带钳位保护的IO(有些FPGA引脚兼容5V输入)。
  2. 模块互联ps2_state信号是关键的控制流。当ps2scan模块检测到有键按下时,它拉高ps2_state,这个信号同时连接到my_uart_tx模块的rx_int(接收中断,这里复用为发送请求)和speed_select模块的bps_start。这意味着,一旦有键值,就同时启动波特率时钟生成和UART发送流程。
  3. 数据流:解码后的8位键值ps2_byte直接传递给UART发送模块作为待发送数据rx_data

注意:这种设计是一种“触发即发送”的简单模式。它没有缓冲区,如果按键速度过快(快于UART发送一个字节的时间),会导致数据丢失。对于键盘输入这种低速场景,通常够用,但这是理解后续可能优化方向(如添加FIFO)的基础。

2.2 PS/2扫描模块:协议解码的核心

这是整个项目最核心也最有趣的部分,ps2scan模块负责与PS/2键盘对话,理解它发来的每一帧数据。PS/2协议是双向的,但这里我们只实现主机(FPGA)接收设备(键盘)数据的功能。

2.2.1 PS/2协议基础与硬件连接

PS/2接口使用6针的Mini-DIN连接器,但我们实际只用到其中4针:VCC(+5V)、GND、Data(数据线)、Clock(时钟线)。时钟线由键盘产生,频率通常在10kHz到16.67kHz之间。数据传输格式是每帧11位:1位起始位(总是0)、8位数据位(LSB先发)、1位奇校验位、1位停止位(总是1)。时钟下降沿锁存数据。

在FPGA侧,我们需要用两个普通IO口来连接ps2k_clkps2k_data。由于键盘时钟是异步于FPGA系统时钟的,直接用它来驱动FPGA内部的时序逻辑会引入亚稳态问题。因此,标准的做法是使用FPGA的高速系统时钟(如50MHz)来对这两根信号线进行同步和边沿检测。

2.2.2 代码逐行解析与设计考量

让我们深入ps2scan模块的代码,看看如何用Verilog实现上述逻辑。

module ps2scan( input clk, // 50MHz系统时钟 input rst_n, input ps2k_clk, // 来自键盘的时钟 input ps2k_data, // 来自键盘的数据 output reg [7:0] ps2_byte, // 解码出的键值 output reg ps2_state // 按键状态标志 );

第一部分:时钟同步与边沿检测这是处理异步信号的黄金步骤。

reg ps2k_clk_r0, ps2k_clk_r1, ps2k_clk_r2; wire neg_ps2k_clk; // 下降沿标志 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin ps2k_clk_r0 <= 1'b1; // 注意:空闲时PS/2时钟线为高电平 ps2k_clk_r1 <= 1'b1; ps2k_clk_r2 <= 1'b1; end else begin ps2k_clk_r0 <= ps2k_clk; // 第一级同步 ps2k_clk_r1 <= ps2k_clk_r0; // 第二级同步,消除亚稳态 ps2k_clk_r2 <= ps2k_clk_r1; // 用于边沿检测 end end assign neg_ps2k_clk = ps2k_clk_r2 & ~ps2k_clk_r1; // 检测下降沿
  • 为什么用三级寄存器?ps2k_clk_r0ps2k_clk_r1构成一个经典的“双寄存器同步器”,极大降低了来自异步时钟域的信号ps2k_clk在进入FPGA时钟域(clk)时引发亚稳态的概率。ps2k_clk_r2则是为了干净地检测边沿。
  • 边沿检测逻辑neg_ps2k_clk = r2 & ~r1。当上一拍r1为高,当前拍r2为低时(即r1=1, r2=0),说明在clk的采样视角下,ps2k_clk出现了从高到低的变化,我们便产生一个时钟周期宽的高电平脉冲作为下降沿标志。这个脉冲将作为我们读取数据位的使能信号。

第二部分:数据位采集与帧组装这是状态机思想的体现,用一个计数器num来追踪当前正在接收的是第几位。

reg [3:0] num; // 0~10,共11位 reg [7:0] temp_data; // 临时存储接收到的8位数据 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin num <= 4'd0; temp_data <= 8'd0; end else if (neg_ps2k_clk) begin // 只在时钟下降沿处理 case (num) 4'd0: num <= num + 1'b1; // 起始位,忽略 4'd1: begin num <= num + 1'b1; temp_data[0] <= ps2k_data; end // LSB 4'd2: begin num <= num + 1'b1; temp_data[1] <= ps2k_data; end ... // 类似地处理第2到第7位 (num=3~8) 4'd9: begin num <= num + 1'b1; end // 奇偶校验位,本例忽略 4'd10: begin num <= 4'd0; end // 停止位,接收完毕,计数器复位 default: num <= 4'd0; endcase end end
  • LSB First:注意赋值顺序,num=1时采集的是temp_data[0],这符合PS/2协议LSB(最低位)先发的约定。
  • 采样点:我们在检测到ps2k_clk下降沿时采样ps2k_data。根据协议,数据在时钟下降沿前后是稳定的,此时采样最可靠。
  • 忽略校验位:为了简化,本例没有实现奇偶校验。在产品级设计中,校验是必要的,可以增加数据的可靠性。

第三部分:键值处理与状态生成PS/2键盘的按键消息是“通码(Make)”和“断码(Break)”组合。通常,按下时发送一个通码(如A键是0x1C),释放时先发送一个0xF0,再跟一个通码。我们需要区分按下和释放。

reg key_f0; // 断码标志 reg [7:0] ps2_byte_r; // 存储最终有效的键值(通码) always @(posedge clk or negedge rst_n) begin if (!rst_n) begin key_f0 <= 1'b0; ps2_state_r <= 1'b0; ps2_byte_r <= 8'd0; end else if (num == 4'd10) begin // 一帧(11位)接收完成 if (temp_data == 8'hf0) begin key_f0 <= 1'b1; // 收到断码前缀,置位标志 end else begin if (!key_f0) begin // 之前没有收到F0,说明是按下事件 ps2_state_r <= 1'b1; // 产生按键有效脉冲 ps2_byte_r <= temp_data; // 锁存键值 end else begin // 之前收到了F0,现在是通码,说明是释放事件 ps2_state_r <= 1'b0; key_f0 <= 1'b0; // 清除断码标志 end end end else begin ps2_state_r <= 1'b0; // 状态信号只维持一个处理周期 end end
  • 状态机逻辑key_f0标志位构成了一个简单的状态机。状态0:等待;收到0xF0进入状态1(期待通码);在状态1下收到通码,则识别为释放事件,清除状态。在状态0下收到非0xF0的通码,则识别为按下事件。
  • 脉冲信号ps2_state_r在识别到按下事件时,仅在一个时钟周期内拉高。这个脉冲信号用于通知其他模块(如UART):“有新的键值可用了!”

第四部分:扫描码到ASCII码的转换这是应用层逻辑。PS/2键盘输出的是位置扫描码(Scan Code Set 2),我们需要将其映射为ASCII码。本例只映射了大写字母A-Z。

reg [7:0] ps2_asci; // 转换后的ASCII码 always @(*) begin // 使用组合逻辑,键值改变立即转换 case (ps2_byte_r) 8'h1c: ps2_asci = 8'h41; // A -> 'A' (0x41) 8'h32: ps2_asci = 8'h42; // B -> 'B' ... // 其他字母映射 8'h3a: ps2_asci = 8'h4d; // M -> 'M' default: ps2_asci = 8'h00; // 非字母键,输出空(或可定义其他值) endcase end assign ps2_byte = ps2_asci; assign ps2_state = ps2_state_r;
  • 组合逻辑映射:使用always @(*)块实现一个查找表(LUT)。当ps2_byte_r变化时,ps2_asci立即更新。
  • 扩展性:这个case语句很容易扩展。要支持小写字母,需要结合Caps Lock或Shift键的状态;要支持数字和符号键,只需添加更多的映射条目。

实操心得:在编写这部分代码时,最大的坑在于PS/2时钟的消抖和亚稳态处理。如果同步寄存器级数不够,或者边沿检测逻辑不严谨,在键盘时钟边沿附近采样数据,极易导致错位,接收到的数据全是乱的。务必确保neg_ps2k_clk脉冲的干净和准确。另外,扫描码表一定要查对,不同键盘或设置可能略有差异,但Set 2是PC最常用的。

3. 波特率生成与UART发送模块详解

PS/2解码模块产出了数据和触发信号,下一步就是通过串口将其发送出去。UART发送部分由两个模块协作完成:speed_select(波特率时钟生成器)和my_uart_tx(发送器)。

3.1 波特率时钟生成模块

UART通信的核心是精准的波特率。我们需要从FPGA的高频系统时钟(如50MHz)中,分频产生一个符合波特率要求的低频时钟信号clk_bps。这个时钟的上升沿(或下降沿)应对齐到每个数据位的中间时刻进行采样或输出。

module speed_select( input clk, // 50MHz input rst_n, input bps_start, // 启动信号,高电平有效 output clk_bps // 波特率时钟脉冲 );

设计思路:通常我们不生成连续的波特率时钟,而是生成一个周期与波特率时钟周期相同、但宽度仅为一个系统时钟周期的脉冲信号。这个脉冲在需要发送/接收每一位数据时出现一次,作为“节拍”。

假设系统时钟clk频率为clk_freq = 50_000_000 Hz,目标波特率bps = 9600

  • 波特率周期:T_bps = 1 / 9600 ≈ 104166.67 ns
  • 系统时钟周期:T_clk = 1 / 50e6 = 20 ns
  • 每个波特率时钟周期包含的系统时钟数:N = T_bps / T_clk = 104166.67 / 20 ≈ 5208.33,取整为5208。

那么,我们需要一个计数器,从0计数到5207(共5208个周期),当计数器达到5207时,产生一个脉冲(clk_bps拉高一个clk周期),同时计数器归零,重新开始计数。这个脉冲的周期就是5208 * 20ns = 104160ns,对应的波特率约为9600.6,误差极小,完全满足要求。

parameter BPS_PARA = 13'd5208; // 50MHz / 9600 ≈ 5208 reg [12:0] cnt; reg clk_bps_r; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 13'd0; clk_bps_r <= 1'b0; end else if (bps_start) begin // 只有启动信号有效时才计数 if (cnt == BPS_PARA - 1) begin cnt <= 13'd0; clk_bps_r <= 1'b1; // 产生一个时钟周期的脉冲 end else begin cnt <= cnt + 1'b1; clk_bps_r <= 1'b0; end end else begin // 没有启动信号,计数器保持 cnt <= 13'd0; clk_bps_r <= 1'b0; end end assign clk_bps = clk_bps_r;
  • 参数化设计BPS_PARA使用参数定义,方便切换不同的波特率(如115200、19200等)。计算方法是BPS_PARA = clk_freq / bps
  • 门控计数:计数器仅在bps_start为高时工作。当PS/2模块产生ps2_state脉冲时,bps_start被拉高,启动波特率时钟生成。发送模块会在发送完一个字节后,将bps_start拉低,停止计数,节省功耗。

3.2 UART发送器模块

发送器模块在波特率时钟clk_bps的节拍下,将8位并行数据rx_data按照UART帧格式(1位起始位+8位数据位+1位停止位)串行输出到rs232_tx线上。

module my_uart_tx( input clk, input rst_n, input clk_bps, // 波特率时钟脉冲 input [7:0] rx_data, // 待发送数据(来自PS/2键值) input rx_int, // 发送启动信号(连接ps2_state) output reg rs232_tx, output reg bps_start // 控制波特率生成器的启停 ); reg [3:0] num; // 发送位计数器 (0~10) reg [7:0] tx_data; // 发送数据寄存器 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin bps_start <= 1'b0; rs232_tx <= 1'b1; // 空闲时TX为高电平 num <= 4'd0; tx_data <= 8'd0; end else if (rx_int) begin // 收到发送请求 bps_start <= 1'b1; // 启动波特率时钟 tx_data <= rx_data; // 锁存待发送数据 end else if (clk_bps) begin // 波特率时钟脉冲到来 case (num) 4'd0: begin rs232_tx <= 1'b0; // 起始位 num <= num + 1'b1; end 4'd1, 4'd2, 4'd3, 4'd4, 4'd5, 4'd6, 4'd7, 4'd8: begin rs232_tx <= tx_data[num-1]; // 发送数据位,LSB first num <= num + 1'b1; end 4'd9: begin rs232_tx <= 1'b1; // 停止位 num <= num + 1'b1; end 4'd10: begin bps_start <= 1'b0; // 发送完毕,关闭波特率时钟 num <= 4'd0; // rs232_tx 保持高电平(空闲) end endcase end end endmodule
  • 发送状态机num计数器清晰地定义了发送过程的11个状态(0起始位,1-8数据位,9停止位,10结束)。
  • 数据锁存:在rx_int(即ps2_state)有效时,立刻锁存rx_datatx_data。这是必须的,因为rx_data可能随时变化,而发送过程需要持续约10个波特率周期。
  • LSB First:UART协议也是LSB先发。注意发送数据位时的索引tx_data[num-1]。当num=1时,发送tx_data[0]
  • 流程控制:模块通过bps_start信号控制波特率生成器。发送开始时启动,发送完毕后停止,实现了按需生成时钟,降低了系统动态功耗。

注意事项:UART发送模块的clk_bps信号必须非常干净,且严格对准每一位数据的中间。如果clk_bps的脉冲产生有偏差(如计数器初值设置不当),会导致发送数据的位宽不一致,在接收端产生误码。仿真时一定要仔细检查clk_bpsrs232_tx的时序关系。

4. 系统集成、仿真与板级调试实录

模块代码写完并不意味着成功,仿真和调试才是重头戏。这里分享我从仿真到上板调试的全过程。

4.1 测试平台的搭建与仿真

我使用ModelSim或Vivado自带的仿真工具进行测试。首先需要编写一个testbench来模拟键盘的行为。

1. 模拟PS/2键盘发送数据:在testbench中,我需要模拟产生PS/2协议的时钟和数据信号。例如,模拟发送一个A键的通码8‘h1C

// 示例:发送 8‘h1C (二进制 00011100, LSB先发为 00111000) task send_ps2_byte; input [7:0] data; integer i; reg parity; begin // 计算奇校验位 (本例忽略,但模拟时应包含) parity = ^data; // 按位异或,计算奇偶性 // 产生起始位 (0) ps2k_data = 0; #50000 ps2k_clk = 0; // 时钟低电平 #30000 ps2k_clk = 1; // 产生下降沿,对应用户代码的采样点 #20000; // 保持高电平一段时间 // 发送8位数据 for (i=0; i<8; i=i+1) begin ps2k_data = data[i]; #50000 ps2k_clk = 0; #30000 ps2k_clk = 1; #20000; end // 发送校验位 (本例代码忽略,但模拟时发送) ps2k_data = parity; #50000 ps2k_clk = 0; #30000 ps2k_clk = 1; #20000; // 发送停止位 (1) ps2k_data = 1; #50000 ps2k_clk = 0; #30000 ps2k_clk = 1; #20000; // 总线释放 ps2k_data = 1'bz; ps2k_clk = 1'bz; end endtask

在initial块中调用send_ps2_byte(8‘h1C);来模拟按下A键。

2. 观察关键信号:在仿真波形中,我需要重点关注:

  • ps2k_clkps2k_data的波形是否符合协议。
  • ps2scan模块内部的neg_ps2k_clk下降沿脉冲是否准确。
  • 计数器num是否从0递增到10。
  • temp_data寄存器是否在正确的num时刻锁存了正确的数据位。
  • ps2_byte_r是否最终锁存为8‘h1C
  • ps2_state是否产生了一个单一脉冲。
  • clk_bps脉冲是否在ps2_state后开始规律出现。
  • rs232_tx线上输出的波形是否是一个标准的UART帧:先拉低(起始位),接着是8位数据01000001(ASCII ‘A’ 的二进制,LSB first),最后拉高(停止位)。

3. 仿真发现的问题与解决:

  • 问题一:数据错位。最初发现temp_data接收的数据和发送的不一致。检查发现是边沿检测逻辑写反了,neg_ps2k_clk = ~ps2k_clk_r1 & ps2k_clk_r2这个条件在r1从1变0时并不成立。修正为ps2k_clk_r2 & ~ps2k_clk_r1
  • 问题二:ps2_state脉冲过宽。在最初的代码里,ps2_state_r在一个大的else块里被赋值,导致它在一个帧接收周期内可能多次被置位或保持。修正为仅在num==4‘d10且是按下事件时,产生一个时钟周期的高脉冲,其他时候置低。
  • 问题三:UART发送不完整。仿真发现有时只发了7位数据就停止了。原因是num计数器在clk_bps脉冲下从0走到10,但我的case语句只写到了9。补充4‘d10的状态处理逻辑。

4.2 板级调试与实物连接

仿真通过后,就可以进行综合、布局布线并生成比特流文件,下载到FPGA开发板了。

1. 硬件连接:

  • PS/2接口:需要一个PS/2母座。连接时注意引脚顺序:通常1脚是Data,3脚是GND,4脚是VCC(+5V),5脚是Clock。务必确认开发板IO电压!如果FPGA BANK电压是3.3V,直接接5V的PS/2信号有风险。稳妥的做法是使用一个简单的电平转换电路(如两个电阻分压,或用74LVC4245这类电平转换芯片),或者选择支持5V容忍IO的FPGA型号并正确配置。
  • UART接口:FPGA的rs232_tx引脚连接到USB转串口模块的RX引脚,GND对接。USB转串口模块的另一端插电脑。

2. 调试工具:

  • 串口调试助手:电脑端打开串口调试助手(如Putty、SecureCRT、或者各种嵌入式IDE自带的工具),设置正确的COM口、波特率(与代码中一致,如9600)、数据位8、停止位1、无校验。
  • 逻辑分析仪:这是硬件调试的利器。我用的是Saleae Logic,将探头连接到FPGA的ps2k_clkps2k_datars232_tx引脚。可以直观地看到协议波形,对比仿真结果,快速定位是PS/2解码问题还是UART发送问题。

3. 常见实物问题与排查:

  • 现象:串口助手无任何显示。

    • 检查1:电源和连接。确认键盘PS/2口有电(键盘指示灯亮吗?),确认USB转串口模块驱动已安装,COM口选择正确。
    • 检查2:波特率。确认代码中的波特率参数与串口调试助手设置完全一致。9600波特率下,BPS_PARA计算错误一位都会导致无法通信。
    • 检查3:信号捕捉。用逻辑分析仪抓取rs232_tx信号。如果根本没有波形,说明UART发送模块没工作,回溯bps_startclk_bps信号。如果有波形但不对,看起始位、停止位、数据位是否符合预期。
    • 检查4:PS/2解码。用逻辑分析仪同时抓ps2k_clkps2k_data。按下一个键,看是否有标准的11位波形发出。再观察FPGA内部的ps2_byteps2_state信号(可以通过引出到LED或虚拟IO查看),确认解码是否正确。
  • 现象:按下键,显示乱码或错误字符。

    • 排查1:ASCII映射表。这是最常见的原因。确认你按下的键的扫描码是否与case语句中的值匹配。例如,原代码中8‘h1z显然是笔误,应该是8‘h1a(Z键的扫描码)。务必使用标准的Scan Code Set 2表进行核对
    • 排查2:UART位序。确认发送模块是LSB first。如果弄反了,发送的01000001(A)会被接收端解读为10000010,变成乱码。
    • 排查3:电平极性。RS-232标准是负逻辑:高电平(-3V~-15V)代表逻辑1,低电平(+3V~+15V)代表逻辑0。而我们的FPGA输出是正逻辑(0V/3.3V)。USB转串口芯片(如CH340、CP2102、FT232)通常自动兼容这两种逻辑。但有些老式串口可能需要MAX232这类芯片进行电平转换。确保你的硬件连接是正确的电平转换方式。
  • 现象:连续快速按键会丢字。

    • 原因分析:这是预期内的,因为当前设计没有缓冲。UART发送一个字节需要约1ms (1/9600 * 10 bits),而快速打字时按键间隔可能小于1ms。
    • 解决方案:在ps2scanmy_uart_tx之间加入一个FIFO(先入先出)缓冲区。当PS/2解码出一个键值,就写入FIFO;UART发送模块空闲时,从FIFO读取数据发送。FIFO的深度可以根据需要设置(如16、32),这样就能容忍短时间的突发按键。

5. 项目优化与扩展思路

这个基础版本跑通后,你可以从多个方向进行优化和扩展,让它变得更实用、更强大。

5.1 功能扩展

  1. 支持小写字母与Shift键:PS/2键盘会为修饰键(Shift, Ctrl, Alt, Caps Lock)也发送独立的通码和断码。你需要增加状态机来跟踪这些修饰键的状态。例如,当收到左Shift键的通码(0x12)时,置位一个shift_pressed标志;收到其断码(0xF0后跟0x12)时清除。在扫描码转ASCII时,根据shift_pressedcaps_lock标志,选择输出大写或小写字母的ASCII码。
  2. 支持更多按键:扩展case语句,加入数字键、符号键、功能键(F1-F12)、方向键、回车、退格等的映射。对于非字符键,可以定义一套自己的输出协议,比如用特殊的转义序列表示。
  3. 添加LED状态控制:PS/2协议允许主机控制键盘上的Num Lock, Caps Lock, Scroll Lock指示灯。你可以实现向键盘发送命令的功能(例如,在Caps Lock按下时,发送命令点亮对应LED),这需要实现PS/2协议的“主机到设备”通信部分,稍微复杂一些。
  4. 实现键盘缓冲区(FIFO):如上所述,用FPGA内部的Block RAM或分布式RAM实现一个同步FIFO,解决丢键问题。这是提升产品可用性的关键一步。

5.2 性能与可靠性优化

  1. 添加奇偶校验:在ps2scan模块的接收端,实现奇偶校验计算。如果校验错误,可以丢弃该帧数据,或者通过一个错误信号输出,增加系统的鲁棒性。
  2. 更稳健的边沿检测与消抖:虽然PS/2时钟信号质量通常较好,但为了应对极端情况,可以增加更复杂的数字滤波器。例如,连续采样多次,只有当多次采样值一致时才确认状态变化,以滤除毛刺。
  3. 参数化与可配置性:将系统时钟频率、目标波特率、FIFO深度等设计为模块参数(parameter),这样代码只需稍作修改就能适配不同的开发板(时钟频率不同)或通信需求(波特率不同)。
  4. 使用状态机重构:当前的ps2scan模块使用计数器num和标志位key_f0,本质上是一个状态机,但写法比较分散。可以用更清晰的always @(posedge clk)块配合statenext_state寄存器,使用case语句明确写出“空闲”、“接收数据”、“等待断码”等状态,使代码更易读和维护。

5.3 系统集成应用

这个PS/2解码器可以作为一个IP核,集成到更大的系统中:

  • 嵌入式输入系统:与VGA/HDMI显示控制器结合,在屏幕上实现一个简单的键盘输入终端。
  • 硬件密码锁:将键盘输入与密码验证逻辑结合,实现一个纯硬件的密码输入装置。
  • 游戏控制器:将方向键、功能键映射为游戏控制信号。
  • 工业HMI:作为低成本工控面板的输入设备。

调试这个项目的过程,让我对硬件时序的理解上了一个台阶。看着逻辑分析仪上规整的协议波形,和串口助手里跳出的字符,那种“硬件在按我写的代码运行”的成就感,是软件编程难以比拟的。希望这份详细的拆解,能帮你少走些弯路,顺利打通从协议文档到硬件实现的这条路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询