实验九:PS/2模块③ — 键盘与多组合键
笔者曾经说过,通码除了单字节以外,也有双字节通码,而且双字节通码都是 8’hE0开头,别名又是 E0按键。常见的的E0按键有,<↑>,<↓>,<←>,<→>,<HOME>,<PRTSC> 等编辑键。除此之外,一些组合键也是E0按键,例如 <RCtrl> 或者 <RAlt>
。所以说,当我们设计组合键的时候,除了考虑“左边”的组合键以外,我们也要考虑“右边”的组合键。<Ctrl> 为例:
<LCtrl> 通码是 8’h14;
<RCtrl> 通码则是 8’hE0 8’h14。
E0按键除了通码携带 8’hE0字节以外,E0按键的断码同样也会携带 8’hE0字节。<Ctrl>继续为例:
<LCtrl> 断码是 8’hF0 8’h14;
<RCtrl> 断码是 8’hE0 8’F0 8’h14。
至于时序方面呢 ...
图9.1 含有E0的通码与断码。
如图9.1所示,当笔者按下 <RCtrl>,紧接着PS/2键盘会发送 8’hE0 8’h14的通码,完后isCtrl立旗。假设笔者立即释放 <RCtrl>,那么PS/2键盘会发送 8’hE0 8’hF0 8’h14的断码,事后isCtrl就会消除立旗状态。
图9.2 E0按键与组合键①。
假设笔者按下 <RCtrl> 又按下 <A>,那么 <RCtrl> 通码会导致 isCtrl立旗,<A> 通码则会导致 isDone产生高脉冲,此刻组合键 <Ctrl> + <A> 完成。假设笔者手痒,先释放 <A> 再释放 <RCtrl>,<A> 断码没有异常,反之 <RCtrl> 断码则会消除 isCtrl的立旗状态。
图9.3 E0按键与组合键②。
假设顽皮的笔者先按下 <RCtrl> 又按下 <LCtrl> 然后释放 <LCtrl>。首先 <RCtrl> 通码会导致 isCtrl 立旗,不过 <LCtrl> 通码会驱使 isCtrl 重复立旗,但是 <LCtrl> 断码则会消除 isCtrl的立旗状态。如果此刻笔者按下 <A>,虽然 <A> 通码使产生isDone的高脉冲,但是组合键 <Ctrl> + <A> 则没有成立。心灰意冷的笔者,于是便释放 <A>又释放 <RCtrl>,期间 <A> 断码与 <RCtrl> 断码都没有异样。
图9.4 E0按键与组合键③。
为了解决这个问题,我们必须把 isCtrl 旗标区分为 isLCtrl 与 isRCtrl 为两种旗标。如图9.4所示,同样的按键过程,不过却有不同的按键结果。期间,<RCtrl> 通码立旗 isRCtrl,换之 <LCtrl> 通码立旗 isLCtrl。虽然 <LCtrl> 断码消除 isLCtrl的立旗状态,但是 <A> 通码还有isRCtrl 立旗因为合作无间,结果造就组合键 <Ctrl> + <A> 完成。 事后 <RCtrl> 断码再消除 isRCtrl 的立旗状态。
为此,我们 isLCtrl 与 isRCtrl 之间的关系可以这样表示:
wire isCtrl = isLCtrl | isRCtrl;
除此之外, isLShift,isRShift,isLAlt 与 isRAlt也是同样的道理。
我们虽然已经解决 E0按键还有组合键之间的问题,但是还有根本性的问题在等待我们。实验七~八之际,解读一帧数据,数据要么就是通码,数据要么就是断码 ... 换句话说,检测数据的时候,我们只要检测1×2等两种可能性而已,即8’hF0或者非 8’hF0。如果数据是 8’hF0,那么数据就是断码,否则就是通码。
一旦 E0按键乱入搅局,检测的可能性也从原来的 1×2等两种可能性,变成 1×2×3 等8种可能性,这个事实无疑会加剧Verilog的描述难度。简言之就是实验七,还有实验八的思路却不适合实验九,为此我们需要更换一下思路。
假设实验九所针对的组合键有:
与
与
与
然后,我们必须事先考虑所有可能性,包括这些组合键的通码与断码,然后用常量表达出来,结果如代码9.1所示:
parameter MLSHIFT = 24'h00_00_12, MLCTRL = 24'h00_00_14, MLALT = 24'h00_00_11;
parameter BLSHIFT = 24'h00_F0_12, BLCTRL = 24'h00_F0_14, BLALT = 24'h00_F0_11;
parameter MRSHIFT = 24'h00_00_59, MRCTRL = 24'hE0_00_14, MRALT = 24'hE0_00_11;
parameter BRSHIFT = 24'h00_F0_59, BRCTRL = 24'hE0_F0_14, BRALT = 24'hE0_F0_11;
代码9.1
如代码9.1所示,M××表示通码,B××表示断码 ... 如果算计字节 8’hF0与 8’hE0,
所有组合键的通码与断码都可以使用3字节来表达。期间<RCtrl> 与 <RAlt> 的通码与断码都有8’hE0的字眼。此外,我们知道低级建模II是追求表达能力的建模技巧,凡事力求直观 ... 为此,我们必须建立3个步骤,而且每个步骤处理单一情况,结果如代码9.2所示:
1. 1: // E0_xx_xx & E0_F0_xx Check
2. if( T == 8'hE0 ) begin D1[23:16] <= T; i <= FF_Read; Go <= i; end
3. else if( D1[23:16] == 8'hE0 && T == 8'hF0 ) begin D1[15:8] <= T; i <= FF_Read; Go <= i; end
4. else if( D1[23:8] == 16'hE0_F0 ) begin D1[7:0] <= T; i <= CLEAR; end
5. else if( D1[23:16] == 8'hE0 && T != 8'hF0 ) begin D1[15:0] <= {8'd0, T}; i <= SET; end
6. else i <= i + 1'b1;
7.
8. 2: // 00_F0_xx Check
9. if( T == BREAK ) begin D1[23:8] <= {8'd0,T}; i <= FF_Read; Go <= i; end
10. else if( D1[23:8] == 16'h00_F0 ) begin D1[7:0] <= T; i <= CLEAR; end
11. else i <= i + 1'b1;
12.
13. 3: // 00_00_xx Check
14. begin D1 <= {16'd0,T}; i <= SET; end
代码9.2
如代码9.2所示:
步骤1处理 E0_××_×× 或者 E0_F0_××,亦即针对E0通码与E0断码。
步骤2处理 00_F0_××,亦即针对一般断码。
步骤3处理 00_00_××,亦即针对一般通码。
接下来,让让我们详细理解一下各个步骤的内容:
步骤1:
第2行 if( T == 8'hE0 ) 表示,如果第一字节是 8’hE0便将8’hE0暂存在 D1[23:16],即E0按键,然后步骤指向伪函数读取第二字节,并且Go返回当前步骤。
第3行 if( D1[23:16] == 8'hE0 && T == 8'hF0 ) 表示,如果 D1[23:16] 的内容是 8’hE0并且第二字节是8’hF0,即E0断码。为此,F0暂存在 D1[15:8],然后步骤指向伪函数读取第三字节,Go则返回当前步骤。
第4行 if( D1[23:8] == 16'hE0_F0 ) 表示,如果 D1[23 :8] 为 16’hE0_F0,那么第三字节也是断码。为此,D1[7:0] 暂存第三字节,然后步骤指向 Clear (消除步骤)。
第5行 if( D1[23:16] == 8'hE0 && T != 8'hF0 ) 表示, D1[23:16] 为 8’hE0,但是第二字节不是8’hF0,即E0通码。为此,D1[16:8] 赋值 8’h00,D1[7:0] 则暂存第二字节,然后步骤指向 SET (设置步骤)。
第6行,当什么都不是则表示对象不是E0按键,i递增以示下一个步骤。
步骤2:
第9行,if( T == BREAK ) 表示,第一字节为 8’hF0,即是一般断码。为此,D1[23:18] 赋值8’h00,D1[16:8] 则暂存 8’hF0,然后i指向伪函数读取第二字节,Go返回当前步骤。
第10行,if( D1[23:8] == 16'h00_F0 ) 表示,第一字节8’hF0已经读取完毕,现正准备断码的后续字节。为此,D1[7:0] 暂存第二字节,然后i指向 Clear(消除步骤)。
第11行,当什么都是则表示对象只是一般通码而已。
步骤3:
第14行,D1[23:8] 赋值16’h00_00 然后 D1[7:0] 暂存第一字节,然后i指向 SET (设置步骤)。
1. 4: // Set state
2. if( D1 == MRSHIFT ) begin isTag[5] <= 1'b1; D1 <= 24'd0; i <= 5'd0; end
3. else if( D1 == MRCTRL ) begin isTag[4] <= 1'b1; D1 <= 24'd0; i <= 5'd0; end
4. else if( D1 == MRALT ) begin isTag[3] <= 1'b1; D1 <= 24'd0; i <= 5'd0; end
5. else if( D1 == MLSHIFT ) begin isTag[2] <= 1'b1; D1 <= 24'd0; i <= 5'd0; end
6. else if( D1 == MLCTRL ) begin isTag[1] <= 1'b1; D1 <= 24'd0; i <= 5'd0; end
7. else if( D1 == MLALT ) begin isTag[0] <= 1'b1; D1 <= 24'd0; i <= 5'd0; end
8. else i <= DONE;
9.
10. 5: // Clear state
11. if( D1 == BRSHIFT ) begin isTag[5] <= 1'b0; D1 <= 24'd0; i <= 5'd0; end
12. else if( D1 == BRCTRL ) begin isTag[4] <= 1'b0; D1 <= 24'd0; i <= 5'd0; end
13. else if( D1 == BRALT ) begin isTag[3] <= 1'b0; D1 <= 24'd0; i <= 5'd0; end
14. else if( D1 == BLSHIFT ) begin isTag[2] <= 1'b0; D1 <= 24'd0; i <= 5'd0; end
15. else if( D1 == BLCTRL ) begin isTag[1] <= 1'b0; D1 <= 24'd0; i <= 5'd0; end
16. else if( D1 == BLALT ) begin isTag[0] <= 1'b0; D1 <= 24'd0; i <= 5'd0; end
17. else begin D1 <= 24'd0; i <= 5'd0; end
代码9.3
当第一至第三字节经由步骤1~3分析并且整理完毕以后,就会路由步骤4(SET)或者步骤5(CLEAR)。
步骤4,第2~7行是用来立旗,如果D1的内容是组合键,那么相关的标志位 isTag[n] 就会立旗,D1清空,然后i返回步骤0;否则,对象只是一般字符按键的通码而已,结果i指向 DONE并且产生完成信号(第8行)。
步骤5,第11~16行是用来消除立旗,如果D的内容是组合键,那么相关的标志位 isTag[n]就会被消除,D1清空,i返回步骤0。否则,对象只是一般的字符按键的断码而已,结果D1清零,i则返回步骤0。
理解这些内容以后,我们就可以开始建模了。
图9.5 实验九的建模图。
图9.5是实验九的建模图,相较实验八,PS/2功能模块的 oTag 则多了3个状态,余下都一样。
ps2_funcmod.v
图9.6 PS/2功能模块的建模图。
同样,PS/2功能模块相较实验八,oTag增多了3个位宽,此外内容也发生不少改变。
1. odule ps2_funcmod
2. (
3. input CLOCK, RESET,
4. input PS2_CLK, PS2_DAT,
5. output oTrig,
6. output [7:0]oData,
7. output [5:0]oTag
8. );
以上内容为相关的出入端声明。
9. parameter MLSHIFT = 24'h00_00_12, MLCTRL = 24'h00_00_14, MLALT = 24'h00_00_11;
10. parameter BLSHIFT = 24'h00_F0_12, BLCTRL = 24'h00_F0_14, BLALT = 24'h00_F0_11;
11. parameter MRSHIFT = 24'h00_00_59, MRCTRL = 24'hE0_00_14, MRALT = 24'hE0_00_11;
12. parameter BRSHIFT = 24'h00_F0_59, BRCTRL = 24'hE0_F0_14, BRALT = 24'hE0_F0_11;
13. parameter BREAK = 8'hF0;
14. parameter FF_Read = 5'd8, DONE = 5'd6, SET = 5'd4, CLEAR = 5'd5;
以上内容为组合键的常量声明(三字节)。第13行是BREAK的常量声明。第14行是伪函数,SET步骤与CLEAR步骤等入口地址声明。
16. /*******************************/ // sub1
17.
18. reg F2,F1;
19.
20. always @ ( posedge CLOCK or negedge RESET )
21. if( !RESET )
22. { F2,F1 } <= 2'b11;
23. else
24. { F2, F1 } <= { F1, PS2_CLK };
25.
26. /*******************************/ // core
27.
28. wire isH2L = ( F2 == 1'b1 && F1 == 1'b0 );
以上内容是检测电平的周边操作,第28行则是下降沿的即时声明。
29. reg [7:0]T;
30. reg [23:0]D1;
31. reg [5:0]isTag; // [5]isRShift, [4]isRCtrl, [3]isRAlt, [2]isLShift, [1]isLCtrl, [0]isLAlt;
32. reg [4:0]i,Go;
33. reg isDone;
34.
35. always @ ( posedge CLOCK or negedge RESET )
36. if( !RESET )
37. begin
38. T <= 8'd0;
39. D1 <= 24'd0;
40. isTag <= 6'd0;
41. i <= 5'd0;
42. Go <= 5'd0;
43. isDone <= 1'b0;
44. end
45. else
以上内容是是相关寄存器的声明以及复位操作。T用于伪函数的暂存空间,D1用来暂存按键数据,isTag用来标示各个组合按键的状态,i指向步骤,Go返回步骤,isDone则标示有效按键。
46. case( i )
47.
48. 0: // Read Make
49. begin i <= FF_Read; Go <= i + 1'b1; end
50.
51. 1: // E0_xx_xx & E0_F0_xx Check
52. if( T == 8'hE0 ) begin D1[23:16] <= T; i <= FF_Read; Go <= i; end
53. else if( D1[23:16] == 8'hE0 && T == 8'hF0 ) begin D1[15:8] <= T; i <= FF_Read; Go <= i; end
54. else if( D1[23:8] == 16'hE0_F0 ) begin D1[7:0] <= T; i <= CLEAR; end
55. else if( D1[23:16] == 8'hE0 && T != 8'hF0 ) begin D1[15:0] <= {8'd0, T}; i <= SET; end
56. else i <= i + 1'b1;
57.
58. 2: // 00_F0_xx Check
59. if( T == BREAK ) begin D1[23:8] <= {8'd0,T}; i <= FF_Read; Go <= i; end
60. else if( D1[23:8] == 16'h00_F0 ) begin D1[7:0] <= T; i <= CLEAR; end
61. else i <= i + 1'b1;
62.
63. 3: // 00_00_xx Check
64. begin D1 <= {16'd0,T}; i <= SET; end
65.
以上内容为部分核心操作,过程如下:
步骤0,进入伪函数以致读取第一字节数据。
步骤1处理 E0_××_×× 或者 E0_F0_××,亦即针对E0通码与E0断码。
步骤2处理 00_F0_××,亦即针对一般断码。
步骤3处理 00_00_××,亦即针对一般通码。
66. 4: // Set state
67. if( D1 == MRSHIFT ) begin isTag[5] <= 1'b1; D1 <= 24'd0; i <= 5'd0; end
68. else if( D1 == MRCTRL ) begin isTag[4] <= 1'b1; D1 <= 24'd0; i <= 5'd0; end
69. else if( D1 == MRALT ) begin isTag[3] <= 1'b1; D1 <= 24'd0; i <= 5'd0; end
70. else if( D1 == MLSHIFT ) begin isTag[2] <= 1'b1; D1 <= 24'd0; i <= 5'd0; end
71. else if( D1 == MLCTRL ) begin isTag[1] <= 1'b1; D1 <= 24'd0; i <= 5'd0; end
72. else if( D1 == MLALT ) begin isTag[0] <= 1'b1; D1 <= 24'd0; i <= 5'd0; end
73. else i <= DONE;
74.
75. 5: // Clear state
76. if( D1 == BRSHIFT ) begin isTag[5] <= 1'b0; D1 <= 24'd0; i <= 5'd0; end
77. else if( D1 == BRCTRL ) begin isTag[4] <= 1'b0; D1 <= 24'd0; i <= 5'd0; end
78. else if( D1 == BRALT ) begin isTag[3] <= 1'b0; D1 <= 24'd0; i <= 5'd0; end
79. else if( D1 == BLSHIFT ) begin isTag[2] <= 1'b0; D1 <= 24'd0; i <= 5'd0; end
80. else if( D1 == BLCTRL ) begin isTag[1] <= 1'b0; D1 <= 24'd0; i <= 5'd0; end
81. else if( D1 == BLALT ) begin isTag[0] <= 1'b0; D1 <= 24'd0; i <= 5'd0; end
82. else begin D1 <= 24'd0; i <= 5'd0; end
83.
以上内容为部分核心操作,过程如下:
步骤4,用来立旗组合键。
步骤5,则用来消除组合键。
84. 6: // DONE
85. begin isDone <= 1'b1; i <= i + 1'b1; end
86.
87. 7:
88. begin isDone <= 1'b0; i <= 5'd0; end
以上内容为部分核心操作,步骤6~7用来产生完成信号。
90. /****************/ // PS2 Read Function
91.
92. 8: // Start bit
93. if( isH2L ) i <= i + 1'b1;
94.
95. 9,10,11,12,13,14,15,16: // Data byte
96. if( isH2L ) begin i <= i + 1'b1; T[ i-9 ] <= PS2_DAT; end
97.
98. 17: // Parity bit
99. if( isH2L ) i <= i + 1'b1;
100.
101. 18: // Stop bit
102. if( isH2L ) i <= Go;
103.
104. endcase
105.
以上内容为部分核心操作。步骤8~18是读取一帧数据的伪函数。
106. assign oTrig = isDone;
107. assign oData = D1[7:0];
108. assign oTag = isTag;
109.
110. endmodule
以上内容是输出驱动声明。
ps2_demo.v
组合模块 ps2_demo 的连线部署请浏览图9.5。
1. module ps2_demo
2. (
3. input CLOCK, RESET,
4. input PS2_CLK, PS2_DAT,
5. output [7:0]DIG,
6. output [5:0]SEL
7. );
8. wire [7:0]DataU1;
9. wire [5:0]TagU1;
10.
11. ps2_funcmod U1
12. (
13. .CLOCK( CLOCK ),
14. .RESET( RESET ),
15. .PS2_CLK( PS2_CLK ), // < top
16. .PS2_DAT( PS2_DAT ), // < top
17. .oTrig(),
18. .oData( DataU1 ), // > U2
19. .oTag( TagU1 ) // > U2
20. );
21.
22. smg_basemod U2
23. (
24. . CLOCK( CLOCK ),
25. .RESET( RESET ),
26. .DIG( DIG ), // > top
27. .SEL( SEL ), // > top
28. .iData( { 8'h00, 1'b0,TagU1[5:3], 1'b0,TagU1[2:0], DataU1 } ) // < U1
29. );
30.
31. endmodule
上述代码基本上没有什么难点,除了第28行的联合驱动。8’h00表示无视第1~2位的数码管。1’b0 + TagU1[5:3] 表示第3位数码管显示 <RCtrl> <RShift> 还有 <RAlt> 等立旗状态。1’b0 + TagU1[2:0] 表示第4位数码管显示 <LCtrl> <LShift> 还有 <LAlt> 等立旗状态。DataU1则表示第5~6位数码管显示通码。
编译完毕便下载程序。如果读者同时按下 <RCtrl> <RShift> 还有 <RAlt>,第3位数码管就会显示4’h7,即4’b0111。如果同时按下 <LCtrl> 还有 <LShift> ,第4位数码管就会显示4’h6,即4’b0110。如果按下 <A>,第5~6位数码管则会显示 8’h1C。注意,千万不要太贪心,同时按下6个以上的按键,PS/2键盘会因此而罢工的 ...
本实验结束之前,让我们来聊聊一些八卦 ... 实验九的 PS/2功能模块虽然支持 E0按键,但是仅限 E0的组合键而已。至于那些 E0 编辑键,如 <↑> 或者 <↓>,则需要进一步扩展,不过该要求已经超出本书的讨论范围。不管对象是 E0组合键,还是E0编辑键,设计思路也是一样的,不过后者比较偏向软件。对此,读者只要基于实验九,再简单扩展一下即可。
细节一:完整的个体模块
图9.7 PS/2键盘功能模块。
图9.7是PS/2键盘功能模块,内容基本上与PS/2功能模块一模一样,至于区别就是穿上其它马甲而已,所以怒笔者不再重复粘贴了。