一天一个设计实例-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 的内容(补码形式)。

(0)

相关推荐

  • 二进制转BCD码

    应用: 用fpga实现对数码管显示,以前通常的方法是进行整除和取余进行运算,但是fpga并不擅长乘法除法运算,所以可以用BCD码来转换. BCD码:通俗的可以理解为用四位二进制数表示一位十进制数字.例 ...

  • 一天一个设计实例-基于FPGA的模数、数模转换器应用设计

    基于FPGA的模数.数模转换器应用设计 本节设计采用黑金ADDA模块,模块硬件结构如下: 图7‑32 硬件结构 数模转换( DA) 电路 如硬件结构图所示, DA 电路由高速 DA 芯片. 7 阶巴特 ...

  • 一天一个设计实例-基于FPGA的数模转换器应用设计

    基于FPGA的数模转换器应用设计 1.1.1带 EEPROM 存储器的 12 位MCP4725应用设计 7.4.1.1 MCP4725简介 MCP4725 是低功耗.高精度.单通道的 12 位缓冲电压 ...

  • 一天一个设计实例-基于FPGA的模数转换器应用设计

    基于FPGA的模数转换器应用设计 1.1.1八通道AD7606应用设计 7.3.1.1 AD7606简介 AD7606 是一款集成式 8 通道数据采集系统,片内集成输入放大器.过压保护电路.二阶模拟抗 ...

  • 一天一个设计实例-AD转换器原理

    AD转换器原理 A/D 转换器的基本原理 图7‑2 A/D转换器功能示意图 A/D转换器(Analog-to-Digital Converter)又叫模/数转换器,即是将模拟信号(电压或是电流的形式) ...

  • 一天一个设计实例-LCD12864的应用设计

    LCD12864的应用设计 LCD12864的应用基本和LCD1602的自定义字库非常类似,下面就简单介绍下LCD12864. 12864 中文 汉字图形点阵液晶显示模块,可显示汉字及图形,内置 81 ...

  • 一天一个设计实例-​LCD1602的应用设计

    LCD1602的应用设计 1.1.1LCD1602的简介 工业字符型液晶,能够同时显示16x02即32个字符.(16列2行) 图6‑16 LCD1602实物图 注:为了表示的方便 ,后文皆以1表示高电 ...

  • 一天一个设计实例-LED显示模块设计

    LED显示模块设计 LED点阵模块指的是利用封装8*8的模块组合点元板形成模块,而LED模组应用中一般指两类产品:一种是用插灯或表贴封装做成的单元板,常用户外门头单红屏.户外全彩屏,室内全彩屏等:另外 ...

  • 一天一个设计实例-GPIO PWM应用

    GPIO PWM 1.1.1PWM简介 脉冲宽度调制脉冲宽度调制(PWM),是英文"Pulse Width Modulation"的缩写,简称脉宽调制,是利用微处理器的数字输出来对 ...

  • 一天一个设计实例-矩阵开关的应用

    矩阵键盘又叫行列式键盘.用带IO口的线组成行列结构,按键设置在行列的交点上.例如用4×4的行列式结构可以构成16个键的键盘.这样,当按键数量平方增长时,I/O口只是线性增长,这样就可以节省I/O口.矩 ...