一天一个设计实例-PS/2键盘及鼠标的应用设计
PS/2键盘及鼠标的应用设计
1.1.1键盘与单次操作
PS/2接口用于许多现代的鼠标和键盘,由IBM最初开发和使用。物理上的PS/2接口有两种类型的连接器:5脚的DIN和6脚的mini-DIN。图1就是两种连接器的引脚定义。使用中,主机提供+5V电源给鼠标,鼠标的地连接到主机电源地上。
图5‑25 PS/2 的接口连接器引脚定义
PS2 的接口如上图,除了 Pin 5 和 Pin 1 其他的引脚对解码没有什么意义。而下图是 PS2协议的时序图。PS2 协议对数据的读取是“Clock 的下降沿”有效。PS2 时钟的频率比较慢,大约是 10Khz 左右。
图5‑26 PS/2协议的时序图
表5‑11 PS/2协议的时序图说明
第 N 位 |
属性 |
0 |
开始位 |
1~8 |
数据位 |
9 |
校验位 |
10 |
结束位 |
PS2 的一帧是 11 位。对 PS2 进行解码时,除了第 1~8 位数据位以外,其余的位都可以无视。
对编码键盘“键盘码”的简单认识:
普通计算机采用的都是“编码键盘”,但是“编码键盘”的“编码方式”有分为“第一套”“第二套”和“第三套”。“第二套”的编码使用较为普遍,大致的民用键盘都是采用“第二套”编码方式。
图5‑27 键盘码
键盘的编码有“通码”(Make)和“断码”(Break)之分。看得简单一点就是,“通码”是某按键的“按下事件”,“断码”是某按键的“释放事件”。
假设,按下“W”键不放,每秒大约会输出 10 个“0x1d”的“通码”。然后释放“W”键,就会输出 “0xF0 0x1d” 的“断码”。编码键盘还有一个老规则,就是一次只能有一个输出而已 (多个按键同时按下,只有其中一个有效)。
再假设按下“W”键不放,然后我再按下“X”键不放,那么会输出“0x1d”“0x22”“0x22” ........ 的通码。如果此时笔者放开“X”键,就会输出“0xf0 0x22”的“断码”,此时“W”键的“通码”已经无效。但是当笔者释放“W”键的时候,依然会输出“0xf0 0x1d”, “W”键的“断码”。
接下来需要详细了解 PS/2 键盘的按键行为。
图5‑28 PS/2 键盘,按一下又释放
假设轻按一下然后又释放,如图5‑28所示, PS/2 键盘先会发送一帧 8’h1C 的通码,然后又发送两帧 8’hF0 8’h1C 的断码,这是 PS/2 键盘最常见的按键行为。
图5‑29 PS/2 键盘,长按又释放
如果长按 键不放,如图5‑29所示, PS/2 键盘会不停发送通码,直至释放才发送断码。至于长按期间,通码的发送间隔大约是 100ms,亦即 1 秒内发送 10 个通码。
图5‑30 PS/2 键盘,有效通码
一般而言,我们都会选择通码放弃断码,为了表示一次性,而且也是有效性的通码。每当一帧通码完成接收, isDone 就会产生一个高脉冲,以示一次性而且有效的通码已经接收完毕。
检测下降沿,利用上面介绍的电平检测模块就可以了,所以我们可以建立如下的结构图:
图5‑31 PS/2 单次操作的建模图
图5‑32 PS/2 解码模块的建模图
图5‑32是 PS/2 解码模块的建模图,左方是 H2L_Sig 与 PS2_DAT 顶层信号的输入,右方则是 1 位 oTrig 与 8 位 oData。具体内容,让我们来瞧瞧代码:
代码5‑9 PS/2 解码模块代码
1.//****************************************************************************// 2.//# @Author: 碎碎思 3.//# @Date: 2019-06-15 00:15:16 4.//# @Last Modified by: zlk 5.//# @WeChat Official Account: OpenFPGA 6.//# @Last Modified time: 2019-06-15 00:32:45 7.//# Description: 8.//# @Modification History: 2014-05-09 17:16:16 9.//# Date By Version Change Description: 10.//# ========================================================================= # 11.//# 2014-05-09 17:16:16 12.//# ========================================================================= # 13.//# | | # 14.//# | OpenFPGA | # 15.//****************************************************************************// 16.module ps2_decode_module 17.( 18. CLOCK, RST_n, 19. isH2L, 20. PS2_DAT, 21. oTrig, 22. oData 23.); 24. input CLOCK, RST_n; 25. input isH2L; 26. input PS2_DAT; 27. output oTrig; 28. output [7:0]oData; 29. 30. parameter BREAK = 8'hF0; 31. parameter FF_Read = 5'd4; 32. 33. /******************/ // core 34. 35. reg [7:0]D1; 36. reg [4:0]i,Go; 37. reg isDone; 38. 39. always @ ( posedge CLOCK or negedge RST_n ) 40. if( !RST_n ) 41. begin 42. D1 <= 8'd0; 43. i <= 5'd0; 44. Go <= 5'd0; 45. isDone <= 1'b0; 46. end 47. else 48. case( i ) 49. 50. 0: 51. begin i <= FF_Read; Go <= i + 1'b1; end 52. 53. 1: 54. if( D1 == BREAK ) begin i <= FF_Read; Go <= 5'd0; end 55. else i <= i + 1'b1; 56. 57. 2: 58. begin isDone <= 1'b1; i <= i + 1'b1; end 59. 60. 3: 61. begin isDone <= 1'b0; i <= 5'd0; end 62. 63. /*************/ // PS2 read function 64. 65. 4: // Start bit 66. if( isH2L ) i <= i + 1'b1; 67. 68. 5,6,7,8,9,10,11,12: // Data byte 69. if( isH2L ) begin D1[ i-5 ] <= PS2_DAT; i <= i + 1'b1; end 70. 71. 13: // Parity bit 72. if( isH2L ) i <= i + 1'b1; 73. 74. 14: // Stop bit 75. if( isH2L ) i <= Go; 76. 77. endcase 78. 79. /************************************/ 80. 81. assign oTrig = isDone; 82. assign oData = D1; 83. 84. /*************************************/ 85. 86.endmodule |
核心操作的具体步骤如下。其中步骤 4~14是读取 1 帧数据的伪函数,入口地址是 4。步骤 0~3 则是主要操作,过程如下:
步骤 0,进入伪函数准备读取第一帧数据。读完第一帧数据以后便返回步骤 1。
步骤 1,判断第一帧数据是否为断码?是,进入伪函数,完整第二帧数据的读取,然后返回步骤指向为 0。否,继续步骤。
步骤 2~3,产生完成的触发信号,然后返回步骤 0。
完成后的RTL的电路如下:
图5‑33 完成后的RTL电路
接下来测试下代码,结构如下:
图5‑34 单次操作测试框图
其中,数码管基础模块直接借用1.8节,在1.8节基础上增加了A B C D E F的编码,整体结构不变,在此不再赘述。
完整的代码如下:
代码5‑10 单次操作测试代码
1.//****************************************************************************// 2.//# @Author: 碎碎思 3.//# @Date: 2019-06-15 00:35:19 4.//# @Last Modified by: zlk 5.//# @WeChat Official Account: OpenFPGA 6.//# @Last Modified time: 2019-06-15 00:50:06 7.//# Description: 8.//# @Modification History: 2019-06-15 00:48:57 9.//# Date By Version Change Description: 10.//# ========================================================================= # 11.//# 2019-06-15 00:48:57 12.//# ========================================================================= # 13.//# | | # 14.//# | OpenFPGA | # 15.//****************************************************************************// 16.module ps2_testmodule 17.( 18. CLOCK, RST_n, 19. PS2_CLK, PS2_DAT, 20. SMG_Data, 21. Scan_Sig 22.); 23. input CLOCK, RST_n; 24. input PS2_CLK, PS2_DAT; 25. output [7:0]SMG_Data; 26. output [5:0]Scan_Sig; 27. 28. 29. wire [7:0]DataU1; 30. 31. ps2_funcmod U1 32. ( 33. .CLOCK( CLOCK ), 34. .RST_n( RST_n ), 35. .PS2_CLK( PS2_CLK ), // input - from top 36. .PS2_DAT( PS2_DAT ), // input - from top 37. .oData( DataU1 ), // output - to U2 38. .oTrig() 39. ); 40. 41. smg_basemod U2 42. ( 43. .CLOCK( CLOCK ), 44. .RST_n( RST_n ), 45. .SMG_Data( SMG_Data ), // output - to top 46. .Scan_Sig( Scan_Sig ), // output - to top 47. .iData( { 16'hFFFF, DataU1 } ) // input - from U1 48. ); 49. 50.endmodule |
如果按下 键,数码管便会显示 1C;如果笔者释放 键,数码管也是显示1C,期间也会发生一丝的闪耀。由于 ps2_funcmod 的暂存空间 D 直切驱动 oData,所以数码管事实反映 ps2_funcmod 的读取状况。从演示上来看的确如此,不过在时序身上,唯有通码读取成功以后,才会产生触发信号。
完成后的RTL电路如下:
图5‑35 单次操作测试完成后的RTL电路
1.1.2键盘与组合操作
当我们使用键盘的时候,如果 5~6 按键同时按下,电脑随之便会发出“哔哔”的警报声,键盘立即失效。这是键盘限制设计,不同产品也有不同限制的按键数量。默认下,最大按键数量是 5~7 个。所谓组合键就是两个以上的按键所产生的有效按键。举例而言,按下按键 输出“字符 a”,按下+便输出“字符 A”。不过要实现组合键,我们必须深入了解键盘的按键行为不可。
图5‑36 按下又立即释放
PS/2 键盘最常见的按键行为是按下以后又立即释放,假设按下键又立即释放键,那么 PS/2 键盘便会产生类似图5‑36的时序。如图5‑36所示,当笔者按下 的时候, PS/2 键盘便会发送 8’h1C 的通码;反之,如果 被释放, PS2 键盘也会立即发送 8’hF0 8’h1C 的断码。
多键行为不同单键行为,因为多键行为同时存在两个以上的按键被按下,因此多键行为便有先按后放,先按先放等次序。假设先按下,然后又按下,随之 PS/2键盘便会接续发送通码 8’h1C 与 8’h12。如果笔者想要撒手,必须事先释放,再者是 ,结果 PS/2 键盘便会连续发送 8’hF0 8’h12 与 8’hF0 8’h1C 的断码。
图5‑37 多键行为,先按后放②
再假设先按下 后按下以后并没有立即释放任何按键,作为最后按下的按键,它可以得到执行权。如图5‑37所示,先是按下 然后又按下,那么 PS/2 键盘便会接续发送 8’h1C 与 8’h12 等通码。假设手指麻痹没有立即释放任何按键,那么 就会得到执行权,结果保持长按状态。此刻, PS/2 键盘便会不停发送 的通码。
一旦手指回复知觉,然后按照先按后放的次序,先行释放 然后释放 ,结果 PS/2 键盘便会接续发送 8’hF0 8’h12 与 8’hF0 8’h1C 等断码。
图5‑38 多键行为,先按先放
如果不是按照先按后放,而是先按先放的次序,先按下 ,后按下的话 ... 如图5‑38所示,假设先按下 ,然后又按下,此刻 PS/2 键盘便会接续发送 8’h1C 与 8’h12 等通码。期间,忽然手痒,觉得先按先放比较好玩,于是故意松开 ,此刻 PS/2 键盘便会发送 8’hF0 8’h1C 的断码。
同一时刻, 亦然保持按下的姿势, PS/2 键盘发送完毕 的断码以后, PS/2键盘也会不停发送 的通码 ... 直至笔者释放, PS/2 键盘发送 8’hF08’h12 的断码为止。
多键行为的终点就在于“先按后放”还是“先按先放”。 不管是哪一种次序,下一刻按键都会抢夺上一刻按键的执行权与长按状态。不过根据习惯,先按后放固然已经成为主流,唯有意外或者那个神经不协调的人才会选择先按先放的次序。当我们理解 PS/2键盘的多键行为以后,我们便可以开始实现组合键。
PS/2 键盘也有按键分类,如:,还有等按键,它们都是常见的组合(补助)按键。除此之外,笔记本或者一些特殊键盘也有不同的组合键,如: 与按键。一般而言,我们都认为组合键是软件的工作,虽然这是不择不扣的事实,不过我们只要换个思路, Verilog 也可以实现组合键。对此,我们只要将一只组合键视为一个立旗状态,所有难题都能迎刃而解
图5‑39 组合键与立旗状态
假设先按下 又按下, PS/2 键盘发送完毕的通码以后,isCtrl 便会立旗。紧接着 PS/2 键盘又会发送的通码,随后 isShift 也会立旗。事后,笔者先释放 再释放,那么 PS/2 键盘便会接续发送与的断码。断码发送完毕以后, isShift 便会消除立旗。同样断码发送完毕以后 isCtrl 也会消除立旗。
图5‑40 有效的组合键①
为了表示有效的组合键,我们依然需要 isDone 这个高脉冲,我们虽然知道 isDone 产生高脉冲都是一般通码输出以后。不过在此,组合键不被认为是一般通码。如图5‑40所示,假设笔者先按下 又按下 ,通码发送完毕以后便立旗 isShift; 通码 发送完毕以后便拉高一会 isDone。如果此刻 isShift 为拉高状态,而且通码 又有效,那么有效的组合键+ 便产生。
完后,先释放 在释放, PS/2 键盘便会接续发送 与的断码。 的断码没有产生任何效果,反之的断码则消除 isShift 的立旗状态。
图5‑41 有效的组合键②
为了产生各种各样的有效组合键,我们不可能不断按下又释放组合键 ... 换言之,不断切换的家伙只有非组合键而已,组合键则一直保持有效的状态,直至发送断码为止。如图5‑41所示,假设笔者先按下又按下 ,通码使 isShift 立旗, 通码使 isDone 产生高脉冲,对此组成键+ 完成。
随后,释放 , PS/2 键盘便发送 断码。不一会,笔者又按下 , 通码使 isDone 产生高脉冲,结果完成组合键+ 。事后,笔者释放 又释放 , PS/2 键盘便会接续发送断码 与, 断码没有异样,断码则消除 isShift 的立旗状态。
图5‑42 多状态有效组合键
除了当个组合键(一个立即状态)以外,同样的道理也能实现多个组合键(多个立旗状态)。如图 8.9 所示,笔者先是按下又按下,通码立旗 isCtrl状态, 通码则立旗 isShift 状态。紧接着笔者又按下 , 通码导致isDone 产生一个高脉冲,此刻组合键++ 已经完成。然后笔者释放 使其产生 断码。
不一会,笔者又按下 ,结果 通码驱使 isDone 又产生另一个高脉冲,此刻组合键 ++ 已经完成。心满意足的笔者接续释放 ,还有。 断码没有任何异样,断码消除 isShift 立旗状态,断码则消除 isCtrl 立旗状态。
一般而言,组合键最多可以达到 3 级,亦即+++ ?。话虽如此,除非对方的手指比猴子更灵活,不然要同时按照次序按下 4 个按键是一件容易伤害手指的蠢事。换之,一级与两级的组合键已经足够应用。理论上, Verilog 要实现多少级组合键也没有问题,但是过多的功能只是浪费而已。
检测下降沿,利用上面介绍的电平检测模块就可以了,所以我们可以建立如下的结构图:
图5‑43 组合操作建模图
图5‑44 PS/2 解码模块的建模图
图5‑44是 PS/2 解码模块的建模图,左方是 H2L_Sig 与 PS2_DAT 顶层信号的输入,右方则是 1 位 oTrig 与 8 位 oData。具体内容,让我们来瞧瞧代码:
代码5‑11 组合操作PS/2 解码模块代码
1.//****************************************************************************// 2.//# @Author: 碎碎思 3.//# @Date: 2019-06-15 00:15:16 4.//# @Last Modified by: zlk 5.//# @WeChat Official Account: OpenFPGA 6.//# @Last Modified time: 2019-06-16 22:04:55 7.//# Description: 8.//# @Modification History: 2014-05-09 17:16:16 9.//# Date By Version Change Description: 10.//# ========================================================================= # 11.//# 2014-05-09 17:16:16 12.//# ========================================================================= # 13.//# | | # 14.//# | OpenFPGA | # 15.//****************************************************************************// 16.module ps2_decode_module 17.( 18. CLOCK, RST_n, 19. isH2L, 20. PS2_DAT, 21. oTrig, 22. oData, 23. oState 24.); 25. input CLOCK, RST_n; 26. input isH2L; 27. input PS2_DAT; 28. output oTrig; 29. output [7:0]oData; 30. output [2:0]oState; 31. 32. parameter LSHIFT = 8'h12, LCTRL = 8'h14, LALT = 8'h11, BREAK = 8'hF0; 33. parameter RDFUNC = 5'd5; 34. 35. reg [7:0]D; 36. reg [2:0]S; // [2] isShift, [1] isCtrl, [0] isAlt 37. reg [4:0]i,Go; 38. reg isDone; 39. 40. always @ ( posedge CLOCK or negedge RST_n ) 41. if( !RST_n ) 42. begin 43. D <= 8'd0; 44. S <= 3'd0; 45. i <= 5'd0; 46. Go <= 5'd0; 47. isDone <= 1'b0; 48. end 49. else 50. case( i ) 51. 52. 0: // Read Make 53. begin i <= RDFUNC; Go <= i + 1'b1; end 54. 55. 1: // Set Flag 56. if( D == LSHIFT ) begin S[2] <= 1'b1; D <= 8'd0; i <= 5'd0;end 57. else if( D == LCTRL ) begin S[1] <= 1'b1; D <= 8'd0; i <= 5'd0; end 58. else if( D == LALT ) begin S[0] <= 1'b1; D <= 8'd0; i <= 5'd0; end 59. else if( D == BREAK ) begin i <= RDFUNC; Go <= i + 5'd3; end 60. else begin i <= i + 1'b1; end 61. 62. 2: 63. begin isDone <= 1'b1; i <= i + 1'b1; end 64. 65. 3: 66. begin isDone <= 1'b0; i <= 5'd0; end 67. 68. 4: // Clear Flag 69. if( D == LSHIFT ) begin S[2] <= 1'b0; D <= 8'd0; i <= 5'd0; end 70. else if( D == LCTRL ) begin S[1] <= 1'b0; D <= 8'd0; i <= 5'd0; end 71. else if( D == LALT ) begin S[0] <= 1'b0; D <= 8'd0; i <= 5'd0; end 72. else begin D <= 8'd0; i <= 5'd0; end 73. 74. /****************/ // PS2 Read Function 75. 76. 5: // Start bit 77. if( isH2L ) i <= i + 1'b1; 78. 79. 6,7,8,9,10,11,12,13: // Data byte 80. if( isH2L ) begin i <= i + 1'b1; D[ i-6 ] <= PS2_DAT; end 81. 82. 14: // Parity bit 83. if( isH2L ) i <= i + 1'b1; 84. 85. 15: // Stop bit 86. if( isH2L ) i <= Go; 87. 88. endcase 89. 90. assign oTrig = isDone; 91. assign oData = D; 92. assign oState = S; 93. 94.endmodule |
步骤 0,进入伪函数等待读取通码,并且 Go 指向下一个步骤。
步骤 1,检测组合键与断码,如果是 LShift 那么 isTag[2]立旗,然后返回步骤 0;如果是 LCTRL 那么 isTag[1] 立旗,然后返回步骤 0;如果是 LALT 那么 isTag[0] 立旗,然后返回步骤 0。如果是 BREAK 便进入伪函数,然后 Go 指向步骤 4。如果什么都不是便进入步骤 2~3。
步骤 2~3,产生完成信号,然后返回步骤 0。
步骤 4,用来消除立旗状态。步骤 1 为 BREAK 便会进入这里,如果断码为 LSHIFT 便会消除 isTag[2], LCTRL 消除 isTag[1], LALT 消除 isTag[0],无视其它断码。最后返回步骤 0。
换句话说,无视数码管的 1~3位,第 4 位数码管显示组合键状态,第 5~6 位数码管则显示通码。编译完后便下载程序。如果同时按下++,第 4 位数码管便会显示 4’h7,亦即 4’b0111,或者说 isTag[2..0] 皆为立旗状态。如果按下其它按键,如,那么第 5~6 位的数码管便会显示 8’h1C。假设释放,第 4 位数码管便会显示 4’h3,亦即 4’b0011,或者说 isTag[1..0] 皆为立旗状态。释放 ,第 5~6 位数码管则会显示 8’h00。
1.1.3键盘与多组合操作
通码除了单字节以外,也有双字节通码,而且双字节通码都是 8’hE0开头,别名又是 E0 按键。常见的的 E0 按键有, <↑>, <↓>, <←>, <→>,,等编辑键。除此之外,一些组合键也是 E0 按键,例如或者。所以说,当我们设计组合键的时候,除了考虑“左边”的组合键以外,我们也要考虑“右边”的组合键。 为例:
通码是 8’h14;
通码则是 8’hE0 8’h14。
E0 按键除了通码携带 8’hE0 字节以外,E0 按键的断码同样也会携带 8’hE0 字节。继续为例:
断码是 8’hF0 8’h14;
断码是 8’hE0 8’F0 8’h14。
图5‑45 含有 E0 的通码与断码
如图5‑45所示,当按下,紧接着 PS/2 键盘会发送 8’hE0 8’h14 的通码,完后 isCtrl 立旗。假设笔者立即释放,那么 PS/2 键盘会发送 8’hE0 8’hF0 8’h14的断码,事后 isCtrl 就会消除立旗状态。
图5‑46 E0 按键与组合键①
假设笔者按下 又按下 ,那么通码会导致 isCtrl 立旗, 通码则会导致 isDone 产生高脉冲,此刻组合键+ 完成。假设笔者手痒,先释放 再释放, 断码没有异常,反之断码则会消除 isCtrl 的立旗状态。
图5‑47 E0 按键与组合键②
假设先按下 又按下然后释放。首先通码会导致 isCtrl 立旗,不过通码会驱使 isCtrl 重复立旗,但是断码则会消除 isCtrl 的立旗状态。如果此刻笔者按下 ,虽然 通码使产生 isDone的高脉冲,但是组合键 + 则没有成立。心灰意冷的笔者,于是便释放 又释放 ,期间 断码与断码都没有异样。
图5‑48 E0 按键与组合键③
为了解决这个问题,我们必须把 isCtrl 旗标区分为 isLCtrl 与 isRCtrl 为两种旗标。如图5‑48所示,同样的按键过程,不过却有不同的按键结果。期间,通码立旗isRCtrl,换之通码立旗 isLCtrl。虽然断码消除 isLCtrl 的立旗状态,但是 通码还有 isRCtrl 立旗因为合作无间,结果造就组合键+ 完成。事后 断码再消除 isRCtrl 的立旗状态。为此,我们 isLCtrl 与 isRCtrl 之间的关系可以这样表示:
wire isCtrl = isLCtrl | isRCtrl;
除此之外, isLShift, isRShift, isLAlt 与 isRAlt 也是同样的道理。
图5‑49 多组合操作建模图
图5‑50 键盘多组合解码建模图
代码5‑12 键盘多组合解码代码
1.//****************************************************************************// 2.//# @Author: 碎碎思 3.//# @Date: 2019-06-15 00:15:16 4.//# @Last Modified by: zlk 5.//# @WeChat Official Account: OpenFPGA 6.//# @Last Modified time: 2019-06-17 21:18:21 7.//# Description: 8.//# @Modification History: 2014-05-09 17:16:16 9.//# Date By Version Change Description: 10.//# ========================================================================= # 11.//# 2014-05-09 17:16:16 12.//# ========================================================================= # 13.//# | | # 14.//# | OpenFPGA | # 15.//****************************************************************************// 16.module ps2_decode_module 17.( 18. CLOCK, RST_n, 19. isH2L, 20. PS2_DAT, 21. oTrig, 22. oData, 23. oState 24.); 25. input CLOCK, RST_n; 26. input isH2L; 27. input PS2_DAT; 28. output oTrig; 29. output [7:0]oData; 30. output [5:0]oState; 31. 32. parameter MLSHIFT = 24'h00_00_12, MLCTRL = 24'h00_00_14, MLALT = 24'h00_00_11; 33. parameter BLSHIFT = 24'h00_F0_12, BLCTRL = 24'h00_F0_14, BLALT = 24'h00_F0_11; 34. parameter MRSHIFT = 24'h00_00_59, MRCTRL = 24'hE0_00_14, MRALT = 24'hE0_00_11; 35. parameter BRSHIFT = 24'h00_F0_59, BRCTRL = 24'hE0_F0_14, BRALT = 24'hE0_F0_11; 36. parameter BREAK = 8'hF0; 37. parameter FF_Read = 5'd8, DONE = 5'd6, SET = 5'd4, CLEAR = 5'd5; 38. 39. reg [7:0]T; 40. reg [23:0]D1; 41. reg [5:0]isTag; // [2]isRShift, [1]isRCtrl, [0]isRAlt, [2]isLShift, [1]isLCtrl, [0]isLAlt; 42. reg [4:0]i,Go; 43. reg isDone; 44. 45. always @ ( posedge CLOCK or negedge RST_n) 46. if( !RST_n ) 47. begin 48. T <= 8'd0; 49. D1 <= 24'd0; 50. isTag <= 6'd0; 51. i <= 5'd0; 52. Go <= 5'd0; 53. isDone <= 1'b0; 54. end 55. else 56. case( i ) 57. 58. 0: // Read Make 59. begin i <= FF_Read; Go <= i + 1'b1; end 60. 61. 1: // E0_xx_xx & E0_F0_xx Check 62. if( T == 8'hE0 ) begin D1[23:16] <= T; i <= FF_Read; Go <= i; end 63. else if( D1[23:16] == 8'hE0 && T == 8'hF0 ) begin D1[15:8] <= T; i <= FF_Read; Go <= i; end 64. else if( D1[23:8] == 16'hE0_F0 ) begin D1[7:0] <= T; i <= CLEAR; end 65. else if( D1[23:16] == 8'hE0 && T != 8'hF0 ) begin D1[15:0] <= {8'd0, T}; i <= SET; end 66. else i <= i + 1'b1; 67. 68. 2: // 00_F0_xx Check 69. if( T == BREAK ) begin D1[23:8] <= {8'd0,T}; i <= FF_Read; Go <= i; end 70. else if( D1[23:8] == 16'h00_F0 ) begin D1[7:0] <= T; i <= CLEAR; end 71. else i <= i + 1'b1; 72. 73. 3: // 00_00_xx Check 74. begin D1 <= {16'd0,T}; i <= SET; end 75. 76. 4: // Set state 77. if( D1 == MRSHIFT ) begin isTag[5] <= 1'b1; D1 <= 24'd0; i <= 5'd0; end 78. else if( D1 == MRCTRL ) begin isTag[4] <= 1'b1; D1 <= 24'd0; i <= 5'd0; end 79. else if( D1 == MRALT ) begin isTag[3] <= 1'b1; D1 <= 24'd0; i <= 5'd0; end 80. else if( D1 == MLSHIFT ) begin isTag[2] <= 1'b1; D1 <= 24'd0; i <= 5'd0; end 81. else if( D1 == MLCTRL ) begin isTag[1] <= 1'b1; D1 <= 24'd0; i <= 5'd0; end 82. else if( D1 == MLALT ) begin isTag[0] <= 1'b1; D1 <= 24'd0; i <= 5'd0; end 83. else i <= DONE; 84. 85. 5: // Clear state 86. if( D1 == BRSHIFT ) begin isTag[5] <= 1'b0; D1 <= 24'd0; i <= 5'd0; end 87. else if( D1 == BRCTRL ) begin isTag[4] <= 1'b0; D1 <= 24'd0; i <= 5'd0; end 88. else if( D1 == BRALT ) begin isTag[3] <= 1'b0; D1 <= 24'd0; i <= 5'd0; end 89. else if( D1 == BLSHIFT ) begin isTag[2] <= 1'b0; D1 <= 24'd0; i <= 5'd0; end 90. else if( D1 == BLCTRL ) begin isTag[1] <= 1'b0; D1 <= 24'd0; i <= 5'd0; end 91. else if( D1 == BLALT ) begin isTag[0] <= 1'b0; D1 <= 24'd0; i <= 5'd0; end 92. else begin D1 <= 24'd0; i <= 5'd0; end 93. 94. 6: // DONE 95. begin isDone <= 1'b1; i <= i + 1'b1; end 96. 97. 7: 98. begin isDone <= 1'b0; i <= 5'd0; end 99. 100. /****************/ // PS2 Read Function 101. 102. 8: // Start bit 103. if( isH2L ) i <= i + 1'b1; 104. 105. 9,10,11,12,13,14,15,16: // Data byte 106. if( isH2L ) begin i <= i + 1'b1; T[ i-9 ] <= PS2_DAT; end 107. 108. 17: // Parity bit 109. if( isH2L ) i <= i + 1'b1; 110. 111. 18: // Stop bit 112. if( isH2L ) i <= Go; 113. 114. endcase 115. 116. assign oTrig = isDone; 117. assign oData = D1[7:0]; 118. assign oState = isTag; 119. 120.endmodule |
图5‑51 键盘多组合解码综合后RTL
编译完毕便下载程序。如果读者同时按下 还有,第 3 位数码管就会显示 4’h7,即 4’b0111。如果同时按下还有,第 4 位数码管就会显示 4’h6,即 4’b0110。如果按下 ,第 5~6 位数码管则会显示 8’h1C。
1.1.4普通鼠标操作
学习 PS/2 键盘以后,接下来就要学习 PS/2 鼠标。PS/2 鼠标相较 PS/2 键盘,驱动难度稍微高了一点点,因为 FPGA(从机)不仅仅是从 PS/2 鼠标哪里读取数据, FPGA 还要往鼠标里写数据 ... 反之, FPGA 只要对 PS/2 键盘读取数据即可。
图5‑52 从机视角,从机读数据
为了方便理解,经由从机的视角去观察 PS/2 的读写时序。图5‑52是从机视角的读时序,从机都是皆由 PS2_CLK 的下降沿读取 1 帧为 11 位的数据 ... 期间,有 11个下降沿。
图5‑53 从机视角,从机写数据,第一帧
首先必须明白, PS2_CLK 与 PS2_DAT 信号是双向的 IO 口,由于 FPGA 是从机的关系,所以 FPGA 一开始都处于输入状态,而 PS/2 鼠标一开始则处于输出状态。假设isQ1 是 FPGA 针对 PS2_CLK 的输出控制, isQ2 是 FPGA 针对 PS2_DAT 的输出控制,然而复位状态(初始化)都为拉低状态。
FPGA 为了获取数据的发送权,首先它必须摘取 PS2_CLK,亦即拉高 isQ1 然后又拉低PS2_CLK 100us。事后, FPGA 必须释放 PS2_CLK,亦即关闭 IO 或者拉低 isQ1,期间顺手拉高 isQ2,让 PS2_DAT 成为输出状态。那么重点来了,读者是否看见黄色的圈圈?当 FPGA 释放PS2_CLK 瞬间, PS/2 鼠标早已拉低 PS2_CLK, FPGA 也因此错过 PS2_CLK 第一次珍贵的下降沿。在此,读者需要好好注意,因为许多资料都忽略这点。
读者需要知道,根据 PS/2 传输协议,从机( FPGA)不管怎样挣扎也没有 PS2_CLK 的拥有权。换句话说,无论从机( FPGA)是写数据还是读数据,从机( FPGA)都必须借助主机( PS/2 鼠标)发送过来的 PS2_CLK 下降沿。如果我们不小心忽略第一个下降沿,余下几个下降沿,我们也会搞错次序,最终造成数据读取错位的悲剧。
FPGA 无论是写数据还是读数据,从机都必须借用 PS2_CLK 的下降沿。FPGA 释放PS2_CLK 之后, isQ2 立即拉高, PS2_DAT 也随之拉低以示一帧数据的起始位 ... 直至下一个下降沿到来为此。
FPGA 一共用了 10 个下降沿,即 T0~T9 发送 8 位数据位, 1 位校验位,还有 1 位停止位。 T9 过去不久, PS2_CLK 就会引来上升沿,此刻 PS/2 鼠标则会反馈 1 位应答位,不过此刻也无暇招呼它。 T10 之际,那是最后一个下降沿, FPGA 拉低 isQ2 让 PS2_DAT成为输入状态,并且读取应答位,然而懒惰的笔者决定无视它。就这样从机写一帧数据就结束了。
这一过程步骤如下:
步骤 32,来拉高 isQ1 又拉低 rCLK 持续 100us。
步骤 33,拉低 isQ1 释放 PS2_CLK,然后又拉高 isQ2 准备发送数据。
步骤 34,重点!由于错过第一个下降沿,我们只能拉低 rDAT 产生起始位。
步骤 35~43,发送 8 位数据位,还有 1 位校验位。
步骤 44,发送结束位。
步骤 45,无视应答位。
步骤 46,拉低 isQ2,让 PS2_CLK 为输入状态。
图5‑54 主机视角,从机写数据,第一帧
接下来,让我们经由主机的视角去观察,主机如何读取从机发送过来的数据。初始状态,主机亦即 PS/2 鼠标掌握 PS2_CLK 与 PS2_DAT。一旦从机即 FPGA 载取 PS2_CLK,并且拉低输出 100us,立刻 PS/2 鼠标已经理解从机准备发送数据。100us 过后, FPGA释放 PS2_CLK 并且载取 PS2_DAT, PS/2 鼠标便开始产生时钟。如图5‑54所示, PS/2鼠标都是借用上升沿读取 FPGA 发送过来的数据。大约 11 个上升沿过后, PS/2 鼠标就结束读取动作,并且反馈应答位。
图5‑55 主机视角,从机写数据,第二帧
每当主机接收完毕一帧数据,就会反馈一帧数据。如图5‑55所示,那是图5‑52的下半部分,依然是从主机视角观察从机写数据。图中显示,每当 PS/2 鼠标(主机)接收完毕一帧数据以后, PS/2 鼠标便会反馈一帧数据,亦即 PS/2 鼠标会再度借用 10 个上升沿发送一帧数据。期间, FPGA(从机)的 PS2_CLK 与 PS2_DAT 都都处于输入状态。
图5‑56 主机视角,从机写数据,完整时序
图 10.5 主机视角,从机写数据,完整时序。
图5‑56是图5‑54与图5‑55的完整时序(主机视角)。
图5‑57 从机视角,从机写数据,第二帧
图 10.6 从机视角,从机写数据,第二帧。
既然主机反馈一帧数据,那么从机也不能无视,如图5‑57所示,那是经由从机视角观察从机如何读取下一帧数据。期间,从机借用 11 个下降沿读取 1 一帧数据。
图5‑58 从机视角,从机写数据,完整时序
图5‑58是图5‑53与图5‑56的完整时序(从机视角)。
将上面的步骤进行一下扩展:
步骤 32~46即从机发送一帧数据。余下步骤 47~57(共有 11 个步骤),主要用来过滤主机反馈过来的下一帧数据,步骤 58 则是步骤返回。不过为什么步骤47~57 不是读取数据,而是过滤数据?别着急,答案很快就会揭晓,暂时忍耐一下。
呼! PS/2 传输协议的写数据(从机视角)解释起来真有够呛。最后让我们来总结一下,主机无论是输出数据,还是读取数据都是借用 PS2_CLK 的上升沿。反之,从机无论是输出数据,还是读取数据都是借用 PS2_CLK 的下降沿。PS/2 传输协议是可谓是爷爷级别的传输协议吧,因为近代的传输协议不管对象是从机还是主机,或者是写还是读,一般都是上升沿设置数据下降沿锁存数据。很少情况是主机使用一个时间沿,从机使用另一个时间沿,例如 SPI 传输协议就是最好的例子。
说完 PS/2 传输协议,接下来我们要进入 PS/2 鼠标的主题了。
鼠标也有普通鼠标与滚扩展鼠标之分 ... 所谓普通鼠标就是包含左键,中建还有右键;所谓扩展鼠标则包含左键,中建,右键还有滚轮,扩展鼠标也称为滚轮鼠标,不过一般的扩展鼠标只有一个滚轮而已。实验十的实验目的就是驱动普通鼠标。 PS/2 鼠标不像PS/2 键盘, PS/2 鼠标即使一上电也不会立即工作,期间从机必须将它使能才行。
普通鼠标一旦上电就便会立即复位,然后得到默认化参数,并且进入 Steam 模式。所谓Steam 模式,即位置状况或者按键状况一有变化就会鼠标便会立即发送报告。话虽然那么说,实际上 Stream 模式还要依赖采集频率,默认的采集频率是 100 次/秒,即采集间隔为 10ms,也就是说鼠标在一秒内会检测 100 次位置状况还有按键状况。
举例而言,假设笔者按着左键不放,那么鼠标在一秒内会发送 100 次“左键好疼!左键好疼!“,直至笔者释放左键为止。再假设鼠标不小心被笔者退了一下,然后鼠标在 10ms内向左移动 10mm,当鼠标察觉位置状况发生变化以后,鼠标便会发送“哎呀!被人推向左边 10mm 了!”。
图5‑59 鼠标的位置标示
鼠标为了标示位置,内建 2 组 9 位的寄存器 X 与 Y,结果如图5‑59所示。默认下,鼠标的分辨率为 4 计数/mm。此外,鼠标也有能力辨识 4 处的移动方向,例如左移 10mm寄存器 X 便计数 -40,右移 10mm 寄存器 X 便计数 +40,上移 10mm 寄存器 Y 便计数+40,下移 10mm 寄存器 Y 便计数 -40。鼠标每隔 10ms(默认采集频率)便会清零一次寄存器 X 与 Y 的内容。
PS/2 鼠标不像 PS/2 键盘一上电便立即工作,我们必须事先发送命令 8’hF4 即“使能鼠标发送数据”,开启数据的水龙头。每当鼠标接收一帧数据,鼠标便会反馈一帧数据,为此 ... 从机每次向鼠标写入一帧数据,就必须接收一帧反馈数据。反馈数据为 8’hFA表示“数据接收成功”,反馈数据为 8’hFE 表示“第一帧数据接收失败”,反馈数据为8’hFC 则表示“第二帧数据接收成功”(有些命令是由 2 帧或者以上组成)。
图5‑60 从机发送 “使能报告”命令,鼠标反馈接收成功
为了驱使鼠标工作, PS/2 鼠标上电以后,从机必须发送命令 8’hF4,并且接收反馈8’hFA。如果一切顺利,那么鼠标就会开始工作,结果如图5‑60所示。鼠标“使能”以后,鼠标便处于就绪状态,采集便开始 ... 此刻,如果鼠标的位置状况或者按键状况发生变化,鼠标就会发送 3 帧,亦即 3 字节的报告。
图5‑61 鼠标发送报告
至于报告的内容如表5‑12 普通鼠标的报告所示:
表5‑12 普通鼠标的报告
字节/位 |
[7] |
[6] |
[5] |
[4] |
[3] |
[2] |
[1] |
[0] |
字节一 |
Y 溢出位 |
X 溢出位 |
Y[8]符号位 |
X[8]符号位 |
保留 |
中键 |
右键 |
左键 |
字节二 |
X[7:0] |
|||||||
字节三 |
Y[7:0] |
如表5‑12 普通鼠标的报告所示,字节一的第四位表示按键状况以外,字节一的高四位也与内部寄存器 X与 Y 有关。字节二为寄存器 X 的内容,字节三位寄存器 Y 的内容。字节一, [0]标示左键, 1 表示左键按下;[1]标示右键, 1 表示右键按下;[2]标示中键,1 表示中键按下;[4]为字寄存器的最高位也是符号位;[5]为寄存器最高位也是符号位;节二是寄存器 X 的低八位,字节三是寄存器 Y 的低八位,因此寄存器 X 与 Y 的位宽为9。这样作的目的是为了使用补码表示鼠标的移动状况。
图5‑62 鼠标的有效位置
假设寄存器 X 的内容为 9’b1_1111_1100, [8]为 1’b1 表示鼠标正在左移, [7:0]为8’b1111_1100 也是 8 个计数,亦即移动 2mm 的距离。因此 9’b1_1111_1100 表示鼠标左移 2mm 的举例。再假设寄存器 Y 的内容为 9’b0_0001_0000, [8]为 0 表示鼠标正在上移, [7:0] 为 8’b0001_0000 也是 16 个计数,亦即移动 4mm。因此 9’b0_0001_0000 表示鼠标上移 4mm。结果如图5‑62所示。
图5‑63 组合模块 ps2_demo
组合模块 ps2_demo 内部包含, PS/2 初始化功能模块,PS/2 读功能模块,数码管基础模块,然后中间还要正直化的即时操作。顾名思义, PS/2初始化功能模块主要负责初始化的工作,简言之就是发送命令 8’hF4,完后便拉高 oEn使能 PS/2 读功能模块。PS/2 读功能模块接收 iEn 拉高便会开始读取 3 字节的报告,并且经由 oData 将其输出。
稍微注意一下 PS2_CLK 还有 PS2_DAT 顶层信号,由于 PS/2 初始化功能模块需要双向访问 PS/2 鼠标,为此该顶层信号皆是出入状态( IO)。反之, PS/2 读功能模块只有接收数据而已,因此该顶层信号只是出入状态。 PS/2 读功能模块的 oData,其中[2:0]是 3 只按键的状况,并且直接驱动三位 LED 资源。至于[23:4]则是寄存器 X 与寄存器 Y 的内容,它们经由即时操作正直化以后便联合驱动数码管基础模块,然后再显示内容。
图5‑64 PS/2 鼠标初始化模块
由于该模块需要来问读写 PS/2 鼠标,因此顶层信号 PS2_CLK 与 PS2_DAT 都是双向,亦即 IO 口。此外,一旦该模块完成初始化的工作, oEn 就会一直拉高。
步骤 0~1 是主操作,主要发送命令 8’hF4,然后拉高 isEn。
第 45 行{ 1'b0, 8'hF4 },其中 1’b0 是校验位, PS/2 的校验位是“奇校验”,如果“1”的数量为单数,那么校验位便是 0。如第 45 所示, 8’hF4 有 5 个“1”所示,校验位为 0。
代码5‑13 PS/2 鼠标初始化操作代码
1.module ps2_init_funcmod 2.( 3. input CLOCK, RST_n, 4. inout PS2_CLK, 5. inout PS2_DAT, 6. output oEn 7.); 8. parameter T100US = 13'd5000; 9. parameter FF_Write = 7'd32; 10. 11. /*******************************/ // sub1 12. 13. reg F2,F1; 14. 15. always @ ( posedge CLOCK or negedge RST_n ) 16. if( !RST_n ) 17. { F2,F1 } <= 2'b11; 18. else 19. { F2, F1 } <= { F1, PS2_CLK }; 20. 21. /*******************************/ // Core 22. 23. wire isH2L = ( F2 == 1'b1 && F1 == 1'b0 ); 24. reg [8:0]T; 25. reg [6:0]i,Go; 26. reg [12:0]C1; 27. reg rCLK,rDAT; 28. reg isQ1,isQ2,isEn; 29. 30. always @ ( posedge CLOCK or negedge RST_n ) 31. if( !RST_n ) 32. begin 33. T <= 9'd0; 34. C1 <= 13'd0; 35. { i,Go } <= { 7'd0,7'd0 }; 36. { rCLK,rDAT } <= 2'b11; 37. { isQ1,isQ2,isEn } <= 3'b000; 38. end 39. else 40. case( i ) 41. 42. /***********/ // INIT Normal Mouse 43. 44. 0: // Send F4 1111_0100 45. begin T <= { 1'b0, 8'hF4 }; i <= FF_Write; Go <= i + 1'b1; end 46. 47. 1: 48. isEn <= 1'b1; 49. 50. /****************/ // PS2 Write Function 51. 52. 32: // Press low CLK 100us 53. if( C1 == T100US -1 ) begin C1 <= 13'd0; i <= i + 1'b1; end 54. else begin isQ1 = 1'b1; rCLK <= 1'b0; C1 <= C1 + 1'b1; end 55. 56. 33: // Release PS2_CLK and set in, PS2_DAT set out 57. begin isQ1 <= 1'b0; rCLK <= 1'b1; isQ2 <= 1'b1; i <= i + 1'b1; end 58. 59. 34: // start bit 60. begin rDAT <= 1'b0; i <= i + 1'b1; end 61. 62. 35,36,37,38,39,40,41,42,43: // Data byte 63. if( isH2L ) begin rDAT <= T[ i-35 ]; i <= i + 1'b1; end 64. 65. 44: // Stop bit 66. if( isH2L ) begin rDAT <= 1'b1; i <= i + 1'b1; end 67. 68. 45: // Ack bit 69. if( isH2L ) begin i <= i + 1'b1; end 70. 71. 46: // PS2_DAT set in 72. begin isQ2 <= 1'b0; i <= i + 1'b1; end 73. 74. 47,48,49,50,51,52,53,54,55,56,57: // 1 Frame 75. if( isH2L ) i <= i + 1'b1; 76. 77. 58: // Return 78. i <= Go; 79. 80. endcase 81. 82. assign oEn = isEn; 83. assign PS2_CLK = isQ1 ? rCLK : 1'bz; 84. assign PS2_DAT = isQ2 ? rDAT : 1'bz; 85. 86.endmodule |
图5‑65 PS/2 读化功能模块的建模图
PS/2 读功能模块,如果 iEn 不拉高就不工作。此外,该模块也只是读入 3 字节的报告而已,完后便经由 oTrig 产生完成信号,报告内容则经由 oData。
if( iEn ) 表示, iEn 不拉高核心操作就不运行。步骤 0~1 是读取第一字节,步骤 2~3 是读取第二字节,步骤 4~5 是读取第三字节,步骤6~7 则是反馈完成信号,以示一次性的报告读取已经完成。完后, i 便指向步骤 0。
代码5‑14 PS/2 读化功能模块代码
1.module ps2_read_funcmod 2.( 3. input CLOCK, RST_n, 4. input PS2_CLK,PS2_DAT, 5. input iEn, 6. output oTrig, 7. output [23:0]oData 8.); 9. parameter FF_Read = 7'd32; 10. 11. /*******************************/ // sub1 12. 13. reg F2,F1; 14. 15. always @ ( posedge CLOCK or negedge RST_n ) 16. if( !RST_n ) 17. { F2,F1 } <= 2'b11; 18. else 19. { F2, F1 } <= { F1, PS2_CLK }; 20. 21. /*******************************/ // core 22. 23. wire isH2L = ( F2 == 1'b1 && F1 == 1'b0 ); 24. reg [23:0]D1; 25. reg [7:0]T; 26. reg [6:0]i,Go; 27. reg isDone; 28. 29. always @ ( posedge CLOCK or negedge RST_n ) 30. if( !RST_n ) 31. begin 32. D1 <= 24'd0; 33. T <= 8'd0; 34. { i,Go } <= { 7'd0,7'd0 }; 35. isDone <= 1'b0; 36. end 37. else if( iEn ) 38. case( i ) 39. 40. /*********/ // Normal mouse 41. 42. 0: // Read 1st byte 43. begin i <= FF_Read; Go <= i + 1'b1; end 44. 45. 1: // Store 1st byte 46. begin D1[7:0] <= T; i <= i + 1'b1; end 47. 48. 2: // Read 2nd byte 49. begin i <= FF_Read; Go <= i + 1'b1; end 50. 51. 3: // Store 2nd byte 52. begin D1[15:8] <= T; i <= i + 1'b1; end 53. 54. 4: // Read 3rd byte 55. begin i <= FF_Read; Go <= i + 1'b1; end 56. 57. 5: // Store 3rd byte 58. begin D1[23:16] <= T; i <= i + 1'b1; end 59. 60. 6: 61. begin isDone <= 1'b1; i <= i + 1'b1; end 62. 63. 7: 64. begin isDone <= 1'b0; i <= 7'd0; end 65. 66. /****************/ // PS2 Write Function 67. 68. 32: // Start bit 69. if( isH2L ) i <= i + 1'b1; 70. 71. 33,34,35,36,37,38,39,40: // Data byte 72. if( isH2L ) begin T[i-33] <= PS2_DAT; i <= i + 1'b1; end 73. 74. 41: // Parity bit 75. if( isH2L ) i <= i + 1'b1; 76. 77. 42: // Stop bit 78. if( isH2L ) i <= Go; 79. 80. endcase 81. 82. assign oTrig = isDone; 83. assign oData = D1; 84. 85.endmodule |
组合模块 ps2_demo.v 的建模图就不再重复粘贴了。
第 34~35 行是正直化的即时声明。第 43 行是 U3 的联合驱动,其中 3’d0, DataU2[5] 表示第 1 位数码管显示 Y 的符号位, Y 表示第 2~3 位的数码管显示 Y 的正直结果, 3’d0,DataU2[4] 表示第 4 位数码管显示 X 的符号位, X 表示第 5~6 位数码管显示 X 的正直结果。第 46 行则是各个按键情况直接驱动 LED 资源。
编译完成并且下载程序。假设笔者按下左键,那么 LED[0]便会点亮,释放则消灭。再假设笔者向左移动鼠标,那么鼠标第 4 位数码管会显示 1,第 5~6 数码管则会显示 X 的内容,亦即鼠标移动的举例。
1.1.5扩展鼠标操作操作
扩展鼠标即滚轮鼠标就诞生了,就是实现滚轮鼠标的驱动。
图5‑66 命令 F3,设置采样频率
命令 F3 也是 Set Sample Rate,主要是用来设置采集频率。笔者曾经说过,采集频率就是鼠标采集按键状况还有位置状况的间隔时间,默认下是 100 次/秒。如图5‑66所示,FPGA 先发送命令数据 8’hF3,事后鼠标会反馈 8’hFA 以示接收成功,余下 FPGA 再发送参数数据 8’d200,鼠标接收成功后也会反馈 8’hFA。如此一来,鼠标的采集频率从原本的 100 次/秒,变成 200 次/秒。
图5‑67 命令 E8,设置分辨率
命令 E8 也是 Set Resolution,主要是用来设置分辨率。所谓分辨率就是位置对应寄存器计数的单位,默认下是 4 计数/mm,亦即 1mm 的距离,鼠标计数 4 下。如图5‑67所示,FPGA 先发送命令数据 8’hE8,鼠标接收以后便反馈 8’hFA, FPGA 随之也会发送参数数据 8’h01,鼠标接收以后也会反馈数据 8’hFA。完后,鼠标的分辨从原本的 4 计数/mm变成 2 计数/mm。参数数据所对应的分辨率如表5‑13所示:
表5‑13 参数数据所对应的分辨率
参数数据 |
分辨率 |
8’ h00 |
1 计数/mm |
8’ h01 |
2 计数/mm |
8’ h02 |
4 计数/mm |
8’ h03 |
8 计数/mm |
图5‑68 命令 F6,使用默认参数
假设笔者手痒,不小心打乱鼠标内部的参数数据,此刻笔者可以发送命令 F6,即 SetDefaults 将参数数据回复成原来的缺省值。如图5‑68所示, FPGA 先发送命令数据 8’hF6,鼠标完成接收以后便会反馈 8’hFA。
图5‑69 命令 F4 使能报告,命令 F5 关闭报告
PS/2 鼠标不像 PS/2 键盘,上电并且完成初始化以后它便会陷入发呆状态,如果不发送命令数据 8’hF4(即 Enable Data Report)手动开启鼠标的水龙头,鼠标是不会发送报告(即夹杂按键状况与位置状况的数据)。如图5‑69所示, FPGA 先发送命令数据 8’hF4,鼠标接收以后便会反馈 8’hFA,事后鼠标立即处于就绪状态,一旦按键状况或者位置状况发生改变,鼠标就会发送报告。
假设读者觉得鼠标太唠叨,什么大事小事都报告,笔者可以发送命令数据 8’hF5(即Disable Data Report)为了使其闭嘴。如图5‑69所示, FPGA 先发送命令数据 8’hF4,鼠标接收完毕以后便会反馈 8’hFA,事后鼠标就成为闭嘴状态,大事小事再也不会烦人。如果读者觉得寂寞,读者可以再度发送命令数据 8’hF4,让鼠标再度唱歌。
图5‑70 命令 F2,读取鼠标 ID
为了区分鼠标是普通鼠标还是扩展鼠标,期间我们必须使用命令 8’hF2,即 Get DeviceID。如图5‑70所示, FPGA 发送命令数据 8’hF2,鼠标接收以后先反馈 8’hFA,再来便发送鼠标 ID。如果内容是 8’h00,则表示该鼠标只是普通鼠标 ... 反之,如果内容是8’h03,那么该鼠标就是扩展鼠标。
滚轮鼠标也是扩展鼠标,上电以后也不会立即变成扩展鼠标,如果扩展鼠标不经过核对暗语,扩展鼠标也是一只普通的 3 键鼠标而已 ... 反之,如果完成暗语核对,扩展鼠标才会发挥滚轮功能。
图5‑71 设置扩展鼠标的暗语
如图5‑71所示,那是设置扩展鼠标的暗语:
发送命令数据 8’hF3,接收反馈 8’hFA,再发送参数数据 8’hC8,在接收反馈 8’hFA;
发送命令数据 8’hF3,接收反馈 8’hFA,再发送参数数据 8’h64,在接收反馈 8’hFA;
发送命令数据 8’hF3,接收反馈 8’hFA,再发送参数数据 8’h50,在接收反馈 8’hFA;
发送命令数据 8’hF2,接收反馈 8’hFA,再接收鼠标 ID8’h03。
完后,鼠标便成为扩展鼠标,内部也自动初始化,然后进入默认模式。
图5‑72 扩展鼠标标示的位置
图 11.7 扩展鼠标标示的位置。
普通鼠标相较扩展鼠标,它多了滚轮功能,即鼠标除了标示左键,中键,右键, X 还有Y 以外,扩展还会标示 Z。如图5‑72所示, X 与 Y 可以看成面积,至于 Z 则可以看成上下。当鼠标向西移动, X 呈现正直,反之负值;当鼠标向北移动, Y 呈现正直,反之负值;当滚动向下活动, Z 呈现正直,反之负值。
图5‑73 扩展鼠标的报告长度
为此,扩展鼠标相较普通鼠标,报告长度则多了一个字节。如图5‑73所示,当鼠标察觉变化以后,鼠标便会发送 4 个字节长度的报告,然而字节之间的位分配如表5‑14 所示:
表5‑14 Device ID 为 8’h03 的报告内容
字节/位 |
[7] |
[6] |
[5] |
[4] |
[3] |
[2] |
[1] |
[0] |
字节一 |
Y 溢出位 |
X 溢出位 |
Y[8]符号位 |
X[8]符号位 |
保留 |
中键 |
右键 |
左键 |
字节二 |
X[7:0] |
|||||||
字节三 |
Y[7:0] |
|||||||
字节四 |
保留 |
保留 |
保留 |
保留 |
Z[3]符号位 |
Z[2] |
Z[1] |
Z[0] |
由于早期 Intel 称王,所以扩展鼠标标准都是 Intel 说话算话,Device ID 为 8’h03 就是其中一种扩展标准。如表 11.1 所示,字节一至字节三基本上变化不大,反之字节四则稍微不同。字节四的[2..0]位是 Z[2:0],字节四的[3]是 Z[3],也是 Z 的符号位。换句话说,寄存器 Z 有 4 位,内容用补码表示。
图5‑74 扩展鼠标的位置范围
图5‑74表示扩展鼠标的位置范围, X 与 Y 与普通鼠标一样, Z 比较畸形一点,因为 Z向上不是正直而是负值,反之亦然。 Z 的有效范围是 4’b1001~4’b0111 或者 -7~7,也就是说滚轮向下活动,寄存器 Z 就递增,向上滚动,寄存器 Z 就递减。
图5‑75 扩展鼠标建模图
如图5‑75所示,组合模块 ps2_demo 包含的内容与实验十相差不了多少,不过却少了正直化的即时操作。期间, PS/2 初始化功能模块的 oEn 有两位, oEn[1] 拉高表示鼠标为扩展鼠标,oEn[0] 拉高表示鼠标为普通鼠标。PS/2 读取功能模块的 oData[2:0] 直接驱动 LED 资源, oData[27:4]则驱动数码管基础模块的 iData。
图5‑76 PS/2 初始化功能建模
如图5‑76所示, PS/2 初始化功能模块有两位 oEn, [1]拉高表示鼠标为扩展鼠标, [0]拉高则表示鼠标为普通鼠标。
步骤 0~9 是主操作,步骤 0~6 则是发送用来开启扩展鼠标的暗语,步骤 7 用来判断鼠标返回的 Device ID 是否为 8’h03,如果是 isEx 立旗,否则 isEx 消除立旗。步骤 8 用来使能鼠标。步骤 9 根据 isEx 的状态再来决定 isEn 的结果, 如果isEx 为 1 isEn[1] 便拉高,否则 isEx 拉高,完后步骤停留。
步骤 32~58 是部分伪函数,内容则是发送一帧数据,再读取一帧反馈,完后便进入步骤 58 判断,发送的命令是否为 8’hF2,如果是便继续步骤,否则便返回步骤。
代码5‑15 PS/2 初始化功能代码
1.module ps2_init_funcmod 2.( 3. input CLOCK, RST_n, 4. inout PS2_CLK, 5. inout PS2_DAT, 6. output [1:0]oEn 7.); 8. parameter T100US = 13'd5000; 9. parameter FF_Write = 7'd32; 10. 11. /*******************************/ // sub1 12. 13. reg F2,F1; 14. 15. always @ ( posedge CLOCK or negedge RST_n ) 16. if( !RST_n ) 17. { F2,F1 } <= 2'b11; 18. else 19. { F2, F1 } <= { F1, PS2_CLK }; 20. 21. /*******************************/ // core 22. 23. wire isH2L = ( F2 == 1'b1 && F1 == 1'b0 ); 24. reg [8:0]T; 25. reg [6:0]i,Go; 26. reg [12:0]C1; 27. reg rCLK,rDAT; 28. reg isQ1,isQ2,isEx; 29. reg [1:0]isEn; 30. 31. always @ ( posedge CLOCK or negedge RST_n ) 32. if( !RST_n ) 33. begin 34. T <= 9'd0; 35. C1 <= 13'd0; 36. { i,Go } <= { 7'd0,7'd0 }; 37. { rCLK,rDAT } <= 2'b11; 38. { isQ1,isQ2,isEx } <= 3'b000; 39. isEn <= 2'b00; 40. end 41. else /// odd 0 , even 1 42. case( i ) 43. 44. /***********/ // INIT Mouse 45. 46. 0: // Send F3 1111_0011 47. begin T <= { 1'b1, 8'hF3 }; i <= FF_Write; Go <= i + 1'b1; end 48. 49. 1: // Send C8 1100_1000 50. begin T <= { 1'b0, 8'hC8 }; i <= FF_Write; Go <= i + 1'b1; end 51. 52. 2: // Send F3 1111_0011 53. begin T <= { 1'b1, 8'hF3 }; i <= FF_Write; Go <= i + 1'b1; end 54. 55. 3: // Send 64 0110_1000 56. begin T <= { 1'b0, 8'h64 }; i <= FF_Write; Go <= i + 1'b1; end 57. 58. 4: // Send F3 1111_0011 59. begin T <= { 1'b1, 8'hF3 }; i <= FF_Write; Go <= i + 1'b1; end 60. 61. 5: // Send 50 0101_0000 62. begin T <= { 1'b1, 8'h50 }; i <= FF_Write; Go <= i + 1'b1; end 63. 64. 6: // Send F2 1111_0010 65. begin T <= { 1'b0, 8'hF2 }; i <= FF_Write; Go <= i + 1'b1; end 66. 67. 7: // Check Mouse ID 00(normal), 03(extend) 68. if( T[7:0] == 8'h03 ) begin isEx <= 1'b1; i <= i + 1'b1; end 69. else if( T[7:0] == 8'h00 ) begin isEx <= 1'b0; i <= i + 1'b1; end 70. 71. 8: // Send F4 1111_0100 72. begin T <= { 1'b0, 8'hF4 }; i <= FF_Write; Go <= i + 1'b1; end 73. 74. 9: 75. if( isEx ) isEn[1] <= 1'b1; 76. else if( !isEx ) isEn[0] <= 1'b1; 77. 78. /****************/ // PS2 Write Function 79. 80. 32: // Press low PS2_CLK 100us 81. if( C1 == T100US -1 ) begin C1 <= 13'd0; i <= i + 1'b1; end 82. else begin isQ1 = 1'b1; rCLK <= 1'b0; C1 <= C1 + 1'b1; end 83. 84. 33: // release PS2_CLK and set in ,PS2_DAT set out 85. begin isQ1 <= 1'b0; rCLK <= 1'b1; isQ2 <= 1'b1; i <= i + 1'b1; end 86. 87. 34: // start bit 1 88. begin rDAT <= 1'b0; i <= i + 1'b1; end 89. 90. 35,36,37,38,39,40,41,42,43: // data bit 9 91. if( isH2L ) begin rDAT <= T[ i-35 ]; i <= i + 1'b1; end 92. 93. 44: // stop bit 1 94. if( isH2L ) begin rDAT <= 1'b1; i <= i + 1'b1; end 95. 96. 45: // Ack bit 97. if( isH2L ) begin i <= i + 1'b1; end 98. 99. 46: // PS2_DAT set in 100. begin isQ2 <= 1'b0; i <= i + 1'b1; end 101. 102. /***********/ // Receive 1st Frame 103. 104. 47,48,49,50,51,52,53,54,55,56,57: // Ingnore 105. if( isH2L ) i <= i + 1'b1; 106. 107. 58: // Check comd F2 108. if( T[7:0] == 8'hF2 ) i <= i + 1'b1; 109. else i <= Go; 110. 111. /***********/ // Receive 2nd Frame 112. 113. 59: // Start bit 1 114. if( isH2L ) i <= i + 1'b1; 115. 116. 60,61,62,63,64,65,66,67,68: // Data bit 9 117. if( isH2L ) begin T[i-60] <= PS2_DAT; i <= i + 1'b1; end 118. 119. 69: // Stop bit 1 120. if( isH2L ) i <= Go; 121. 122. endcase 123. 124. assign PS2_CLK = isQ1 ? rCLK : 1'bz; 125. assign PS2_DAT = isQ2 ? rDAT : 1'bz; 126. assign oEn = isEn; 127. 128.endmodule |
图5‑77 PS/2 读功能模块的建模图
与普通鼠标操作的 PS/2 读功能模块相比,左边的 iEn 出入多出一位以外,右边的 oData也多出一个字节。
以上内容为部分核心操作。第 37 行的 if( iEn[1] ) 表示下面所有内容都是扩展鼠标的核心操作。步骤 0~7 则是读取 4 个字节的数据,步骤 8~9 用来产生完成信号以示一次性的报告已经接收完毕。
以上内容为部分核心操作。第 87 行的 if( iEn[0] ) 表示下面的内容均为普通鼠标的核心操作。步骤 0~5 用来读取 3 个字节的内容,步骤 6~7 则用来产生完成信号以示一次性的报告已经读取完毕。
代码5‑16 PS/2 读功能模块代码
1.module ps2_read_funcmod 2.( 3. input CLOCK, RST_n, 4. input PS2_CLK,PS2_DAT, 5. input [1:0]iEn, 6. output oTrig, 7. output [31:0]oData 8.); 9. parameter FF_Read = 7'd32; 10. 11. /*******************************/ // sub1 12. 13. reg F2,F1; 14. 15. always @ ( posedge CLOCK or negedge RST_n ) 16. if( !RST_n ) 17. { F2,F1 } <= 2'b11; 18. else 19. { F2, F1 } <= { F1, PS2_CLK }; 20. 21. /*******************************/ // core 22. 23. wire isH2L = ( F2 == 1'b1 && F1 == 1'b0 ); 24. reg [31:0]D1; 25. reg [7:0]T; 26. reg [6:0]i,Go; 27. reg isDone; 28. 29. always @ ( posedge CLOCK or negedge RST_n ) 30. if( !RST_n ) 31. begin 32. D1 <= 32'd0; 33. T <= 8'd0; 34. { i,Go } <= { 7'd0,7'd0 }; 35. isDone <= 1'b0; 36. end 37. else if( iEn[1] ) 38. case( i ) 39. 40. /***********/ // Extend Mouse Read Data 41. 42. 0: // Read Data 1st byte 43. begin i <= FF_Read; Go <= i + 1'b1; end 44. 45. 1: // Store Data 1st byte 46. begin D1[7:0] <= T; i <= i + 1'b1; end 47. 48. 2: // Read Data 2nd byte 49. begin i <= FF_Read; Go <= i + 1'b1; end 50. 51. 3: // Store Data 2nd byte 52. begin D1[15:8] <= T; i <= i + 1'b1; end 53. 54. 4: // Read Data 3rd byte 55. begin i <= FF_Read; Go <= i + 1'b1; end 56. 57. 5: // Store Data 3rd byte 58. begin D1[23:16] <= T; i <= i + 1'b1; end 59. 60. 6: // Read Data 4rd byte 61. begin i <= FF_Read; Go <= i + 1'b1; end 62. 63. 7: // Store Data 4rd byte 64. begin D1[31:24] <= T; i <= i + 1'b1; end 65. 66. 8: 67. begin isDone <= 1'b1; i <= i + 1'b1; end 68. 69. 9: 70. begin isDone <= 1'b0; i <= 7'd0; end 71. 72. /****************/ // PS2 Write Function 73. 74. 32: // Start bit 75. if( isH2L ) i <= i + 1'b1; 76. 77. 33,34,35,36,37,38,39,40: // Data byte 78. if( isH2L ) begin T[i-33] <= PS2_DAT; i <= i + 1'b1; end 79. 80. 41: // Parity bit 81. if( isH2L ) i <= i + 1'b1; 82. 83. 42: // Stop bit 84. if( isH2L ) i <= Go; 85. 86. endcase 87. else if( iEn[0] ) 88. case( i ) 89. 90. /***********/ // Normal Mouse Read Data 91. 92. 0: // Read Data 1st byte 93. begin i <= FF_Read; Go <= i + 1'b1; end 94. 95. 1: // Store Data 1st byte 96. begin D1[7:0] <= T; i <= i + 1'b1; end 97. 98. 2: // Read Data 2nd byte 99. begin i <= FF_Read; Go <= i + 1'b1; end 100. 101. 3: // Store Data 2nd byte 102. begin D1[15:8] <= T; i <= i + 1'b1; end 103. 104. 4: // Read Data 3rd byte 105. begin i <= FF_Read; Go <= i + 1'b1; end 106. 107. 5: // Store Data 3rd byte 108. begin D1[23:16] <= T; i <= i + 1'b1; end 109. 110. 6: 111. begin isDone <= 1'b1; i <= i + 1'b1; end 112. 113. 7: 114. begin isDone <= 1'b0; i <= 7'd0; end 115. 116. /****************/ // PS2 Write Function 117. 118. 32: // Start bit 119. if( isH2L ) i <= i + 1'b1; 120. 121. 33,34,35,36,37,38,39,40: // Data byte 122. if( isH2L ) begin T[i-33] <= PS2_DAT; i <= i + 1'b1; end 123. 124. 41: // Parity bit 125. if( isH2L ) i <= i + 1'b1; 126. 127. 42: // Stop bit 128. if( isH2L ) i <= Go; 129. 130. endcase 131. 132. assign oTrig = isDone; 133. assign oData = D1; 134. 135.endmodule |
笔者就不重复粘贴建模图了,具体内容我们还是来看代码吧。
期间第 39 行的 2’d0, DataU2[5],DataU2[4] 表示数码管的第一位显示 X 与 Y 的符号位;DataU2[27:24] 表示数码管的第二位显示 Z 的内容;DataU2[23:16] 表示数码管的第三至第四位显示 Y 的内容;DataU2[15:8] 表示数码管的第五至第六位显示 X 的内容。第 42 行则表示 LED[2]显示右键, LED[1]显示中键, LED[0]显示左键。
编译完毕并且下载程序。当鼠标向西南方移动的时候,第一位数码管便会显示 4’h3,即 4’b0011,也就是说 X 与 Y 的符号位都是拉高状态(负值)。当滚轮向上滚动的时候,第二位数码管便会显示 4’hF,即 4’b1111,也就是 Z 为负值 -1(只要滚动速度够快,负值还能更小)。至于数码管第 3~4 显示 Y 的内容(补码形式),数码管 5~6 则显示 X 的内容(补码形式)。