【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十五:FIFO储存模块(同步)

实验十五:FIFO储存模块(同步)

笔者虽然在实验十四曾解释储存模块,而且也演示奇怪的家伙,但是实验十四只是一场游戏而已。至于实验十五,笔者会稍微严肃一点,手动建立有规格的储存模块,即同步FIFO。那些看过《时序篇》的同学一定对同步FIFO不会觉得陌生吧?因为笔者曾在《时序篇》建立基于移位寄存器的同步FIFO。不过那种同步FIFO只是用来学习的玩具而已。因此,这回笔者可要认真了!

事实告诉笔者,同步FIFO的利用率远胜其它储存模块,几乎所有接口模块都会出现它的身影。早期的时候,笔者都会利用官方准备的同步FIFO(官方插件模块),大伙都知道官方插件模块都非常傲娇,心意(内容)不仅不容易看透,而且信号也不容易捉摸,最重要是无法随心所欲摆布它们。与其跪下向它求救,笔者还不如创建自己专属的同步FIFO。

故名思议,“同步”表示相同频率的时钟源,“FIFO”表示先进先出的意思。FIFO的用意一般都是缓冲数据,另模块独立,让模块回避调用的束缚。同步FIFO是RAM的亚种,它基于RAM,再加上先进先出的机制,学习同步FIFO就是学习如何建立先进先出的机制。

图15.1 同步FIFO建模图(常规)。

常规上,同步FIFO的建模图如图15.1所示,左边有写入请求 ReqW,写入数据 DataW,还有写满标示Full。换之,右边则有读出请求ReqR,读出数据DataR,还有读空标示 Empty。写入方面,ReqW必须拉高DataW才能写入,一旦FIFO写满,那么Full就会拉高。至于读出方面,ReqR 必须拉高,数据才能经由DataR读出,一旦FIFO读空,Empty就会拉高。不过图15.1可以稍微更动一下,另它更加接近低级建模II的形象。

图15.2 同步FIFO建模图(低级建模II)。

如图15.2所示,Req× 改为沟通信号 En,其中En[1] 表示写入使能, En[0]表示读出使能。Data× 改为数据信号Data,iData为写入数据,oData为读出数据。Full与Empty 则改为状态信号 Tag[1] 与 Tag[0] 。

1. module fifo_savemod

2. ( 

3. input CLOCK, RESET,

4. input [1:0]iEn,

5. input [3:0]iData,

6. ouptut [3:0]oData

7. output [1:0]oTag

8. );

9. ......

10. assign oTag[1] = ...; // Full

11. assign oTag[0] = ...; // Empty

12.

13. endmodule

代码15.1

同步FIFO大致的外皮如代码15.1所示,第3~7行是相关的出入端声明,第10~11行则是相关的输出驱动声明。理解这些以后,接下来我们要学习先进先出这个机制。

图15.3 读空状态。

假设笔者建立位宽为4,深度为4的ram,然后又建立位宽为3的写指针WP与读指正RP。同学一定会好奇道,既然ram只有4个深度,那么指针只要2位宽(22 = 4)即不是可以访问所有深度呢?话虽如此,为了利用指正表示写满与读空状态,指针必须多出一位 ... 因此,指针的最高位常常也被称为方向位。

如图15.3所示,一开始的时候,写指针与读指针同样指向地址0,而且ram里边也是空空如也,为此读空状态“叮咚”亮着红灯。为此,我们可以暂时这样表示读空的逻辑关系:

Empty = (WP == RP);

图15.4 写入中①。

当火车开动以后,首先数据 4’hA 写入地址0,然后写指针从原来的 3’b0_00 递增为 3’b0_01,并且指向地址1。此刻,ram再也不是空空入也,所示读空状态消除红灯,结果如图15.4 所示。

图15.5 写入中②。

紧接着,数据 4’hB 写入地址1,然后写指针从原来的 3’b0_01 递增为 3’b0_10,并且指向地址2,结果如图15.5所示。

图15.6写入中③

然后,数据 4’hC 写入地址2,然后写指针从原来的 3’b0_10 递增为 3’b0_11,并且指向地址3,结果如图15.6所示。

图15.7 写满状态。

接着,数据 4’hD 写入地址3,然后写指针从原来的 3’b0_11 递增为 3’b1_00,并且重新指向地址0。此刻写指针的最高位为1,这表示写指针已经绕弯ram一圈又回来原点,反之读指针从刚才开始一动也不动,结果最高为0 ... 所以我们可以说写指针与读指针目前处于不同的方向。在此ram已经写满,所以写满状态便“叮咚”亮红灯,结果如图15.6所示。写满状态的逻辑关系则可以这样表示:

FULL = (WP[2] ^ RP[2] && WP[1:0] == RP[1:0]);

图15.8 读出中①。

从现在开始,另一头火车才开始走动 ... 首先数据4’hA从地址0读出来,读指针也从原本的 3’b0_00 递增为 3’b0_01,并且指向地址1。此刻ram再也不是吃饱饱的状态,所以写满状态被消除红灯,结果如图15.8所示。

图15.9 读出中②。

接下来,数据4’hB 从地址1哪里读出,读指针也从原本的 3’b0_01 递增为 3’b0_10,并且指向地址2,结果如图15.9所示。

图15.10 读出中③。

随之,数据4’hC 从地址2哪里读出,读指针也从原本的 3’b0_10 递增为 3’b0_11,并且指向地址3,结果如图15.10所示。

图15.11读空状态。

最后,数据4’hD 从地址3哪里读出,读指针也从原本的 3’b0_11 递增为 3’b1_00,并且重新指向地址0。当读指针绕弯一圈又回到原点的时候,读者的最高位也成为值1,换句话说 ... 此刻的读指针与写指针也处于同样的位置。同一个时候,ram也是空空如也,所以读空状态便“叮咚”亮起红灯,结果如图15.11所示。为此,读空状态的逻辑关系可以这样表示:

Empty = (WP == RP);

总结而言,当我们设置N位位宽的时候,读写指针的位宽便是 N + 1。此外,读空状态为写指针等价读指针。反之,写满状态是两个指针方向一致(异或状态),然后地址一致。理解先进先出的机制以后,接下来我们便可以填充一下FIFO储存模块的内容。

1. module fifo_savemod

2. (

3. input CLOCK, RESET, 

4. input [1:0]iEn,

5. input [3:0]iData,

6. output [3:0]oData,

7. output [1:0]oTag

8. );

9. reg [3:0] RAM [3:0]; 

10. reg [3:0]D1;

11. reg [2:0]C1,C2; // N+1

12.

13. always @ ( posedge CLOCK or negedge RESET )

14. if( !RESET )

15. begin

16. C1 <= 3‘d0;

17. end

18. else if( iEn[1] ) 

19. begin

20. RAM[ C1[1:0] ] <= iData; 

21. C1 <= C1 + 1‘b1; 

22. end

23.

24. always @ ( posedge CLOCK or negedge RESET )

25. if( !RESET )

26. begin

27. D1 <= 4‘d0;

28. C2 <= 3‘d0;

29. end

30. else if( iEn[0] )

31. begin 

32. D1 <= RAM[ C2[1:0] ]; 

33. C2 <= C2 + 1‘b1; 

34. end

35.

36. assign oData = D1;

37. assign oTag[1] = ( C1[2]^C2[2] & C1[1:0] == C2[1:0] ); // Full

38. assign oTag[0] = ( C1 == C2 ); // Empty

39.

40. endmodule

代码15.2

笔者在第9~11行创建相关的寄存器,C1取代WP,C2取代RP。第13~22行是写操作,内容非常单纯,即 iEn[1] 拉高便将 iData 写入 C1[1:0] 指定的地方,然后C1递增。

第24~34行是读操作,内容也是一样单纯,iEn[0] 拉高便将 C2[1:0] 指定的数据暂存至 D1,随后C2递增,最后由D驱动oData。第37~38行是写满状态与读空状态的逻辑关系。

图15.12 调用FIFO储存模块。

创建同步FIFO基本上没有什么难度,但是调用FIFO倒是一件难题。如图15.12所示,笔者建立一支核心操作尝试调用 FIFO储存模块,至于核心操作的内容如代码15.3所示:

1. case( i ) // Core

2. 0:

3. if( iTag[1]! ) begin oEn[1] <= 1’b1; oData <= 4’hA; i <= i + 1’b1; end

4. 1:

5. if( iTag[1]! ) begin oEn[1] <= 1’b1; oData <= 4’hB; i <= i + 1’b1; end

6. 2:

7. if( iTag[1]! ) begin oEn[1] <= 1’b1; oData <= 4’hC; i <= i + 1’b1; end

8. 3:

9. if( iTag[1]! ) begin oEn[1] <= 1’b1; oData <= 4’hD; i <= i + 1’b1; end

10. 4:

11. begin oEn[1] <= 1’b0; i <= i + 1’b1; end

12. 5:

13. if( iTag[0]! ) begin oEn[0] <= 1’b1; i <= i + 1’b1; end

14. 6:

15. if( iTag[0]! ) begin oEn[0] <= 1’b1; i <= i + 1’b1; end

16. 7:

17. if( iTag[0]! ) begin oEn[0] <= 1’b1; i <= i + 1’b1; end

18. 8:

19. if( iTag[0]! ) begin oEn[0] <= 1’b1; i <= i + 1’b1; end

20. 9:

21. begin oEn[0] <= 1’b0; i <= i + 1’b1; end

22. endcase

代码15.3

如代码15.3所示,步骤0~3用来一边检测 Tag[1] 是否为高,一边向储存模块写入数据 4’hA~4‘hD,步骤4则用来拉低 oEn[1] 并且歇息一下。步骤5~8用来一边检测 Tag[0] 是否为高,一边从储存模块哪里读出数据,步骤9则用来拉低 oEn[0]并且偷懒一下。

图15.13 读写FIFO储存模块的理想时序图。

图15.13是代码15.3所生产的理想时序图,同时也是核心操作作为视角的时序,至于C1~C2是FIFO储存模块作为视角的时序。各个视角的时序过程如下:

核心操作视角:

l T0,isTag[1]为低(即时值),拉高oEn[1](未来值),并且发送数据 4’hA(未来值)。

l T1,isTag[1]为低(即时值),拉高oEn[1](未来值),并且发送数据 4’hB(未来值)。

l T2,isTag[1]为低(即时值),拉高oEn[1](未来值),并且发送数据 4’hC(未来值)。

l T3,isTag[1]为低(即时值),拉高oEn[1](未来值),并且发送数据 4’hD(未来值)。

l T4,isTag[1]为高(即时值),拉低oEn[1](未来值)。

l T5,isTag[0]为低(即时值),拉高oEn[1](未来值)。

l T6,isTag[0]为低(即时值),拉高oEn[1](未来值),数据4’hA读出(过去值)。

l T7,isTag[0]为低(即时值),拉高oEn[1](未来值),数据4’hB读出(过去值)。

l T8,isTag[0]为低(即时值),拉高oEn[1](未来值),数据4’hC读出(过去值)。

l T9,isTag[0]为高(即时值),拉低oEn[1](未来值),数据4’hD读出(过去值)。

l T10,isTag[0]为高(即时值)。

FIFO储存模块视角:

l T0,oEn[1]为低(过去值)。C1等价C2为读空状态,iTag[0]拉高(即时值)。

l T1,oEn[1]为高(过去值),读取数据4’hA(过去值),递增C1。C1不等价C2,iTag[0]拉低(即时值)。

l T2,oEn[1]为高(过去值),读取数据4’hB(过去值),递增C1。

l T3,oEn[1]为高(过去值),读取数据4’hC(过去值),递增C1。

l T4,oEn[1]为高(过去值),读取数据4’hA(过去值),递增C1。C1等价C2为写满状态,iTag[1]拉高(即时值)。

l T5,oEn[1]为低(过去值)。

l T6,oEn[0]为高(过去值),读出数据4’hA(未来值),递增C2。C1不等价C2,isTag[1]拉低(即时值)。

l T7,oEn[0]为高(过去值),读出数据4’hB(未来值),递增C2。

l T8,oEn[0]为高(过去值),读出数据4’hC(未来值),递增C2。

l T9,oEn[0]为高(过去值),读出数据4’hD(未来值),递增C2。C1等价C2为读空状态,isTag[0]拉高(即时值)。

l T10,oEn[0]为低(过去值)。

读者是不是一边浏览一边捏蛋蛋呢?什么过去值,又什么未来值,又又什么即时值的 ... 没错,同步FIFO的设计原理虽然简单,但是时序解读却让人泪流满面。因为同步FIFO夹杂两种时序表现——时间点事件还有即时事件。如图15.13 所示,除了 iTag 信号是触发即时事件以外,所有信号都是触发时间点事件。读过《时序篇》或者《工具篇II》的朋友一定知晓,即时值不仅比过去值优先,而且即时值也会无视时钟。

好奇的同学可能困惑道:“为什么iTag不能设计成为时间点事件呢?”。笔者曾在《时序篇》建立基于移位寄存器的FIFO,其中iTag就是设计成为时间点事件,结果FIFO的写满状态或者读空状态都来不及反馈,因此发生调用上的混乱。

图15.14 读写FIFO储存模块的即时事件。

为了理解重点,首先让我们来焦距写数据的部分。如图15.14所示,关键的地方就是发生在T4——这只时钟沿。T4之际,FIFO储存模块读取oEn[1]的过去值,C1也因此递增,即时事件就在这个瞬间发生了。写满状态成立,iTag[1]也随之拉高即时值。从时序上来看,C1的更新(C1为4’b100)是发生在T4之后,不过那也无关紧要,因为即时值是更新在小小的时间沿之间,也是即时层 ... 然而,即时层是无法显示在时序之上。

图15.15 迟到的写满状态。

假设,iTag[1]不是经由即时事件触发而是事件点事件,那么iTag就会反馈迟到的写满状态。如图15.15所示,T4之际 oEn[1] 为高,C1也因此递增为 3’b100。T5之际,C1与C2的过去值均为 3’b100 与 3’b000,然后拉高 iTag[1]。由于时间点事件的关系,所以iTag[1]迟一拍被拉高 ... 读者千万别小看这样慢来一拍,它是搞乱调用的罪魁祸首。

如果核心操作在T5继续写操作的话,此刻iTag[1]的过去值为0,它会认为FIFO未满,然后不管三七二十一执行写操作,结果FIFO发生错乱随之机能崩溃。从某种程度来看,即时事件的偷时钟能力,是建立同步FIFO的关键。

fifo_savemod.v

图15.16 fifo储存模块的建模图。

图15.16基本上与图15.2没什么两样,不过FIFO储存模块的位宽还有深度发生改变而已。此外,图15.16的信号布局虽然有点违规低,不过这点小细节读者就不要太计较了。建模技巧毕竟不是暴力规范,用不着死守,反之随机应变才是本意。

1. module fifo_savemod

2. (

3. input CLOCK, RESET, 

4. input [1:0]iEn,

5. input [7:0]iData,

6. output [7:0]oData,

7. output [1:0]oTag

8. );

以上内容是相关的出入端声明。

9. reg [7:0] RAM [15:0]; 

10. reg [7:0]D1;

11. reg [4:0]C1,C2; // N+1

12.

以上内容是相关的内存与寄存器声明。第9行,RAM声明为8位宽还有24=16个深度。为此,第11行的写指针C1与读指针C2声明为5个位宽。

13. always @ ( posedge CLOCK or negedge RESET )

14. if( !RESET )

15. begin

16. C1 <= 5‘d0;

17. end

18. else if( iEn[1] ) 

19. begin

20. RAM[ C1[3:0] ] <= iData; 

21. C1 <= C1 + 1‘b1; 

22. end

23.

以上内容是fifo的写操作,第18行的 iEn[1] 每拉高一个时钟, 第20行的iData 便写入C1[3:0]指定的位置,随后第21行写指针也递增。

24. always @ ( posedge CLOCK or negedge RESET )

25. if( !RESET )

26. begin

27. D1 <= 8‘d0;

28. C2 <= 5‘d0;

29. end

30. else if( iEn[0] )

31. begin 

32. D1 <= RAM[ C2[3:0] ]; 

33. C2 <= C2 + 1‘b1; 

34. end

35.

以上内容是fifo的读操作,第30行的 iEn[0] 每拉高一个时钟, 第32行的D1便赋予C2[3:0]指定的数据,随后第33行的读指针也递增。

36. assign oData = D1;

37. assign oTag[1] = ( C1[4]^C2[4] & C1[3:0] == C2[3:0] ); // Full

38. assign oTag[0] = ( C1 == C2 ); // Empty

39.

40. endmodule

以上内容是相关输出驱动声明,其中第37行是写满状态,第38行是读空状态。在此,读者需要注意一下 ... 第36行相较第37~38行 ,前者由寄存器D1驱动,即oData信号为时间点事件。反之,后者由组合逻辑驱动,即 oTag[1:0] 信号为即时事件。为此,该储存模块的内部状态是以即时的方式反馈出去。

tx_rx_demo.v

图15.17 实验十五的建模图。

实验十五是实验十三的延续 ... 实验十三之际,RX功能模块接收并且失败发送一连串的数据,因为发送方不仅来不及,而且接收成功的数据也没有地方缓冲。如今实验十五多了一只FIFO储存模块作为缓冲空间。注意,图15.17虽然是实验十五的建模图,可是却与实际的连线部署有一点出入,不过大意上都是差不多的。

RX功能模块接收一连串的数据,然后经由周边操作协调,事后再将数据缓冲至FIFO储存模块。至于核心操作会不停从FIFO储存模块哪里读取数据,然后再调用TX功能模块将数据发送出去。

1. module tx_rx_demo

2. (

3. input CLOCK, RESET, 

4. input RXD,

5. output TXD

6. ); 

以上内容是相关的出入端声明。

7. wire DoneU1;

8. wire [7:0]DataU1;

9.

10. rx_funcmod U1

11. (

12. .CLOCK( CLOCK ),

13. .RESET( RESET ),

14. .RXD( RXD ), // < top

15. .iCall( isRX ), // < sub

16. .oDone( DoneU1 ), // > U2

17. .oData( DataU1 ) // > U2

18. );

19.

以上内容是RX功能模块的实例化。第15行表示 isRX 充当使能。

20. reg isRX;

21.

22. always @ ( posedge CLOCK or negedge RESET ) // sub

23. if( !RESET ) isRX <= 1‘b0;

24. else if( DoneU1 ) isRX <= 1‘b0; 

25. else isRX <= 1‘b1; 

26.

以上内容是周边操作,它主要重复调用RX功能模块。

27. wire [1:0]TagU2;

28. wire [7:0]DataU2;

29.

30. fifo_savemod U2

31. (

32. .CLOCK( CLOCK ),

33. .RESET( RESET ),

34. .iEn ( { DoneU1 , isRead } ), // < U1 & Core

35. .iData ( DataU1 ), // < U1

36. .oData ( DataU2 ), // > U3

37. .oTag ( TagU2 ) // > core

38. );

39.

以上内容是FIFO储存模块的实例化。第34行表示,DoneU1充当写入使能,isRead充当读出使能。

40. wire DoneU3;

41.

42. tx_funcmod U3

43. (

44. .CLOCK( CLOCK ), 

45. .RESET( RESET ),

46. .TXD( TXD ), // > top

47. .iCall( isTX ), // < core

48. .oDone( DoneU3 ), // > core

49. .iData( DataU2 ) // < U2

50. );

51.

以上内容是TX功能模块的实例化。第47行表示 isTX充当使能。第49行表示,该模块的iData直接经由DataU2驱动。

52. reg [3:0]i;

53. reg isRead;

54. reg isTX;

55.

56. always @ ( posedge CLOCK or negedge RESET ) // core

57. if( !RESET )

58. begin 

59. i <= 4‘d0;

60. isRead <= 1‘b0;

61. isTX<= 1‘b0;

62. end

以上内容是核心操作的相关寄存器声明与复位操作。

63. else

64. case( i )

65.

66. 0:

67. if( !TagU2[0] ) begin isRead <= 1‘b1; i <= i + 1‘b1; end

68.

69. 1:

70. begin isRead <= 1‘b0; i <= i + 1‘b1; end

71.

72. 2:

73. if( DoneU3 ) begin isTX <= 1‘b0; i <= 4‘d0; end

74. else isTX <= 1‘b1;

75.

76. endcase

77.

78. endmodule

以上内容是操作操作。步骤0用来判断FIFO是否读空,否则就拉高isRead。步骤1则拉低isRead,然后FIFO就会读出数据。步骤2则使能TX功能模块,并且将方才读出的数据发送出去。

编译完毕并下载程序。此刻,串口便可以支持一连串的数据发送与接收,为了避免部分数据凭空消失的怪事,数据流的容量必须配合FIFO的缓冲容量(深度)。此外,实验十五还有许多优化的空间,然而这些都是交由读者的功课。(注意,某些串口调试助手必须把检验位设置为标志位才能显示字符)

细节一:完整的个体模块

该实验的 fifo_savemod.v 已经是完整的个体。

时间: 2024-11-05 20:44:12

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十五:FIFO储存模块(同步)的相关文章

【黑金原创教程】【FPGA那些事儿-驱动篇I 】连载导读

前言: 无数昼夜的来回轮替以后,这本<驱动篇I>终于编辑完毕了,笔者真的感动到连鼻涕也流下来.所谓驱动就是认识硬件,还有前期建模.虽然<驱动篇I>的硬件都是我们熟悉的老友记,例如UART,VGA等,但是<驱动篇I>贵就贵在建模技巧的升华,亦即低级建模II. 话说低级建模II,读过<建模篇>的朋友多少也会面熟几分,因为它是低级建模的进化形态.许久以前,笔者就有点燃低级建模II的念头,但是懒惰的性格让笔者别扭许久.某天,老大忽然说道:"让咱们大干一场吧

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验六:数码管模块

实验六:数码管模块 有关数码管的驱动,想必读者已经学烂了 ... 不过,作为学习的新仪式,再烂的东西也要温故知新,不然学习就会不健全.黑金开发板上的数码管资源,由始至终都没有改变过,笔者因此由身怀念.为了点亮多位数码管从而显示数字,一般都会采用动态扫描,然而有关动态扫描的信息请怒笔者不再重复.在此,同样也是动态扫描,但我们却用不同的思路去理解. 图6.1 6位数码管. 如图6.1所示,哪里有一排6位数码管,其中包好8位DIG信号还有6位SEL信号.DIG为digit,即俗称的数码管码,如果数码管

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验三:按键模块② — 点击与长点击

实验三:按键模块② - 点击与长点击 实验二我们学过按键功能模块的基础内容,其中我们知道按键功能模块有如下操作: l 电平变化检测: l 过滤抖动: l 产生有效按键. 实验三我们也会z执行同样的事情,不过却是产生不一样的有效按键: l 按下有效(点击): l 长按下有效(长点击). 图3.1 按下有效,时序示意图. 图3.2 长按下有效,时序示意图. 如图3.1所示,按下有效既是"点击",当按键被按下并且消抖完毕以后,isSClick信号就有被拉高一个时钟(Short Click).

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验二:按键模块① - 消抖

实验二:按键模块① - 消抖 按键消抖实验可谓是经典中的经典,按键消抖实验虽曾在<建模篇>出现过,而且还惹来一堆麻烦.事实上,笔者这是在刁难各位同学,好让对方的惯性思维短路一下,但是惨遭口水攻击 ... 面对它,笔者宛如被甩的男人,对它又爱又恨.不管怎么样,如今 I'll be back,笔者再也不会重复一样的悲剧. 按键消抖说傻不傻说难不难.所谓傻,它因为原理不仅简单(就是延迟几下下而已),而且顺序语言(C语言)也有无数不尽的例子.所谓难,那是因为人们很难从单片机的思维跳出来 ... 此外,

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验四:按键模块③ — 单击与双击

实验四:按键模块③ — 单击与双击 实验三我们创建了“点击”还有“长点击”等有效按键的多功能按键模块.在此,实验四同样也是创建多功能按键模块,不过却有不同的有效按键.实验四的按键功能模块有以下两项有效按键: l 单击(按下有效): l 双击(连续按下两下有效). 图4.1 单击有效按键,时序示意图. 实验四的“单击”基本上与实验三的“点击”一模一样,既按键被按下,经过消抖以后isSClick信号被拉高一个时钟,结果如图4.1所示,过程非常单调.反之,“双击”实现起来,会比较麻烦一些,因为我们还要

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验五:按键模块④ — 点击,长点击,双击

实验五:按键模块④ — 点击,长点击,双击 实验二至实验四,我们一共完成如下有效按键: l 点击(按下有效) l 点击(释放有效) l 长击(长按下有效) l 双击(连续按下有效) 然而,不管哪个实验都是只有两项“功能”的按键模块而已,如今我们要创建三项“功能”的按键模块,亦即点击(按下有效),长击,还有双击.实验继续之前,让我们先来复习一下各种有效按键. 图5.1 点击(按下有效). 如图5.1所示,所谓点击(按下有效)就是按键按下以后,isSClick信号(Single Click) 产生一

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

实验九:PS/2模块③ — 键盘与多组合键 笔者曾经说过,通码除了单字节以外,也有双字节通码,而且双字节通码都是 8’hE0开头,别名又是 E0按键.常见的的E0按键有,<↑>,<↓>,<←>,<→>,<HOME>,<PRTSC> 等编辑键.除此之外,一些组合键也是E0按键,例如 <RCtrl> 或者 <RAlt> .所以说,当我们设计组合键的时候,除了考虑“左边”的组合键以外,我们也要考虑“右边”的组合键.&

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验七:PS/2模块① — 键盘

实验七:PS/2模块① — 键盘 实验七依然也是熟烂的PS/2键盘.相较<建模篇>的PS/2键盘实验,实验七实除了实现基本的驱动以外,我们还要深入解PS/2时序,还有PS/2键盘的行为.不过,为了节省珍贵的页数,怒笔者不再重复有关PS/2的基础内容,那些不晓得的读者请复习<建模篇>或者自行谷歌一下. 市场上常见的键盘都是应用第二套扫描码,各种扫描码如图7.2所示.<建模篇>之际,笔者也只是擦边一下PS/2键盘,简单读取单字节通码与断码而已.所谓单字节通码,就是有效的按下

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

实验八:PS/2模块② — 键盘与组合键 实验七之际,我们学习如何读取PS/2键盘发送过来的通码与断码,不过实验内容也是一键按下然后释放,简单按键行为而已.然而,实验八的实验内容却是学习组合键的按键行为. 不知读者是否有类似的经历?当我们使用键盘的时候,如果5~6按键同时按下,电脑随之便会发出“哔哔”的警报声,键盘立即失效.这是键盘限制设计,不同产品也有不同限制的按键数量.默认下,最大按键数量是5~7个.所谓组合键就是两个以上的按键所产生的有效按键.举例而言,按下按键 <A> 输出“字符a”,