【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验九:PS/2模块③ — 键盘与多组合键

实验九: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。

至于时序方面呢 ...

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验九:PS/2模块③ — 键盘与多组合键

图9.1 含有E0的通码与断码。

如图9.1所示,当笔者按下 <RCtrl>,紧接着PS/2键盘会发送 8’hE0 8’h14的通码,完后isCtrl立旗。假设笔者立即释放 <RCtrl>,那么PS/2键盘会发送 8’hE0 8’hF0 8’h14的断码,事后isCtrl就会消除立旗状态。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验九:PS/2模块③ — 键盘与多组合键

图9.2 E0按键与组合键①。

假设笔者按下 <RCtrl> 又按下 <A>,那么 <RCtrl> 通码会导致 isCtrl立旗,<A> 通码则会导致 isDone产生高脉冲,此刻组合键 <Ctrl> + <A> 完成。假设笔者手痒,先释放 <A> 再释放 <RCtrl>,<A> 断码没有异常,反之 <RCtrl> 断码则会消除 isCtrl的立旗状态。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验九:PS/2模块③ — 键盘与多组合键

图9.3 E0按键与组合键②。

假设顽皮的笔者先按下 <RCtrl> 又按下 <LCtrl> 然后释放 <LCtrl>。首先 <RCtrl> 通码会导致 isCtrl 立旗,不过 <LCtrl> 通码会驱使 isCtrl 重复立旗,但是 <LCtrl> 断码则会消除 isCtrl的立旗状态。如果此刻笔者按下 <A>,虽然 <A> 通码使产生isDone的高脉冲,但是组合键 <Ctrl> + <A> 则没有成立。心灰意冷的笔者,于是便释放 <A>又释放 <RCtrl>,期间 <A> 断码与 <RCtrl> 断码都没有异样。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验九:PS/2模块③ — 键盘与多组合键

图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的描述难度。简言之就是实验七,还有实验八的思路却不适合实验九,为此我们需要更换一下思路。

假设实验九所针对的组合键有:

<LShift> 与 <RShift>
<LCtrl> 与 <RCtrl>
<LAlt> 与 <RAlt>

然后,我们必须事先考虑所有可能性,包括这些组合键的通码与断码,然后用常量表达出来,结果如代码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。

理解这些内容以后,我们就可以开始建模了。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验九:PS/2模块③ — 键盘与多组合键

图9.5 实验九的建模图。

图9.5是实验九的建模图,相较实验八,PS/2功能模块的 oTag 则多了3个状态,余下都一样。

ps2_funcmod.v

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验九:PS/2模块③ — 键盘与多组合键

图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编辑键,设计思路也是一样的,不过后者比较偏向软件。对此,读者只要基于实验九,再简单扩展一下即可。

细节一:完整的个体模块

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验九:PS/2模块③ — 键盘与多组合键

图9.7 PS/2键盘功能模块。

图9.7是PS/2键盘功能模块,内容基本上与PS/2功能模块一模一样,至于区别就是穿上其它马甲而已,所以怒笔者不再重复粘贴了。

上一篇:【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验八:PS/2模块② — 键盘与组合键


下一篇:线下沙龙丨阿里云无锡ACE同城会 分布式存储技术沙龙