专注收集记录技术开发学习笔记、技术难点、解决方案
网站信息搜索 >> 请输入关键词:
您当前的位置: 首页 > Verilog

【黑金原创课程】【FPGA那些事儿-驱动篇I 】实验十四:储存模块

发布时间:2011-06-30 07:19:31 文章来源:www.iduyao.cn 采编人员:星星草
【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十四:储存模块

 

实验十四比起动手笔者更加注重原理,因为实验十四要讨论的东西,不是其它而是低级建模II之一的模块类,即储存模块。接触顺序语言之际,“储存”不禁让人联想到变量或者数组,结果它们好比数据的暂存空间。

1.    int main()
2.    {
3.        int VarA;
4.        char VarB;
5.        VarA = 20;
6.        VarB = 5;
7.    }

代码14.1

如代码14.1所示,主函数内一共声明两个变量VarA与VarB(第3~4行)。VarA是两个字节的整型变量,VarB是一个字节的字符变量,然后VarA赋值20(第5行),VarB则赋值5(第6行)。,其中 int 与 char 等字眼用来表示字节,即暂存空间的位宽,然后储存的内容仅局限于二进制,非0即1。

1.    int main()
2.    {
3.        int VarC[20];
4.        VarC[0] = 30;
5.        for( int i = 0; i < 20; i++ ) VarC[i] = i;
6.        VarC[0] = VarC[1];
7.    }

代码14.2

除了变量以外,顺序语言也有数组这个玩意,亦即一连串的变量。如代码14.2所示,主函数内声明数组VarC,数组的成员位宽是两个字节的int,数组的成员长度则是20(第3行)。然而数组常见的赋值方法除了成员直接赋值以外(第4行),也有使用for循环为逐个成员赋值的方法(第5行)。此外,还有某个数组成员为某个数组成员直接赋值的方法(第6行)。目前为止,顺序语言还有储存之间的故事就圆满结束。

人是一种自虐的生物,事情越是顺利,越是容易萌起疑心 ... 然后暗道:“储存是不是太容易理解呢?容易到让人觉得恶心!“。没错,事实的确如此。“储存”一旦投入描述语言之中,话题便会严肃起来。顺序语言是一件懒人多得的语言,它有许多底层工作都交由编译器处理,相较之下描述语言是一件多劳多得的语言,许多底层工作都必须交由我们自己声明与定义。

 

1.    reg [3:0]D1 = 4’d1;
2.    reg [3:0]D2 ;
3.    reg [3:0]D3;
4.    
5.    initial begin D2 = 4’d2; end
6.    
7.    always @ ( posedge CLOCK or negedge RESET )
8.        if( !RESET )
9.            D3 <= 4’d3;
10.        ......

代码14.3

首先让我们来理解一下初始化与复位化之间的区别。我们知道顺序语言的变量只有初始化,没有复位化这一回事 ... 反之,描述语言却不同。如代码14.3所示,笔者在第1~3行声明D1~D3三个寄存器,其中D1声明不久便立即赋予初值 4’d1。换之,D2则在第5行赋予初值 4’d2,最后D3则在第8~9行赋予复位值4’d3。

所谓初值就是初始化所给予的起始内容,反之复位值就是复位触发所给予的内容。初始化一般都是编译器的赋值活动,第1行的D1还有第5行的D2都是经由编译器的活动给予初值。反观之下,复位化不是编译器活动而是硬件活动,也是俗称的RESET,即电平变化所引起的复位触发。

如代码行第7~9所示,敏感区种含有 negedge RESET的字眼表示,如果RESET的电平由高变低并且产生下降沿触发,结果就执行一下 always 的内容。其中的内容便是复位操作,最终 D3 赋予复位值 4’d3。

clip_image002

图14.1 初始化与复位化的时序图。

如果用时序来表示的话 ... 如图14.1所示,灰色区域表示初始化状态又或者未上电状态

,当中D1与D2都赋予初值4’d1与4’d2,同样D3也给予初值 4’d0。虽然D3在代码14.3之间并没有任何初始化的痕迹,不过默认下编译器都会一视同仁,将所有暂存声明都给予初值0,除非有特别声明,例如第1行的D1与第5行的D2。上电以后,RESET电平又高变低便产生下降沿,结果复位发生了,然后D3被赋予复位值4’d3。

我们知道容器有大有小,所以储存空间也有大小之分,然而决定空间大小就是位宽这种东西。位宽一般是指数据的长度,顺序语言会用 int 或者 char 等关键字表示位宽,反之描述语言会直接声明位宽的大小,如 reg[3:0]D1。在此,顺序语言的位宽区别都是一个字节一个字节比较,反之描述语言比较随意。

1.    reg [3:0]D1;  // Verilog
2.    reg var [3:0]D2;    // Sytem Verilog
3.    logic var [3:0]D3;  // System Verilog

代码14.4

除了位宽以外,我们还要理解什么是储存内容。描述语言相较顺序语言,储存内容的花样较多一些 ... 顺序语言由于比较比较偏向软件,所以储存内容仅有两态,即1与0而已。反之描述语言是介于硬件与软件之间,所以储存内容除了0与1两态之外,也有高阻态z与切开x。

如代码14.4所示,当我们声明D1的时候,除了需要强调位宽以外,我们还要强调储存内容 ... 以Verilog为例的话,关键字 reg 的用意并非强调储存资源是基于寄存器,而是表示储存内容有0有1,还有z与x等四个状态。相反的,SystemVerilog在这方面却做得很好,如代码行2~3所示,var 关键字表示对象是储存空间,reg 关键字表示对象的储存内容有4态,logic关键字则表示对象的储存内容有2态。

1.    char VarA;      // 变量(内存)
2.    char VarB[4]    // 数组(内存)
3.    reg [7:0] D1;   // 寄存器
4.    reg [31:0] D2;  // 寄存器

代码14.5

我们知道顺序语言有所谓的变量与数组,储存资源一般都是基于内存,例如第1行的VarA与第2行的VarB。反之,描述语言不仅可以用寄存器资源建立变量,寄存器资源也能建立数组,例如与第3行的D1与第4行的D2。(虽然顺序语言偶尔也会用到寄存器型的储存资源,不过该储存资源对处理器来说太珍贵了,如果不是特殊条件,一般都不会随意使用)

1. reg [3:0] RAM [15:0]; // 片上内存

代码14.6

此外,描述语言还有另外一种叫做片上内存的储存资源,声明方法如代码14.6所示。FPGA的片上内存与单片机的内存虽然都是内存,但是两者之间却是不同性质的内存。简单而言,单片机的内存是经过烧烤的熟肉,随时可以享用 ... 反之,FPGA的片上内存则是未经过烧烤的生肉,享用之前必须做好事先准备。为此,FPGA的片上内存无法像级单片机内存那样,随便赋值,随意调用。

1. int main()

2. {

3. char VarC[4];

4. for( int i = 0; i < 3; i++ ) VarC[i] = VarC[i+1]

5. }

代码14.7

如代码14.7所示,笔者先建立一个 char 类型的数组 VarC长度并且为4,紧接着利用for循环为数组的整组成员赋值,其中VarC[i] 的赋予内容是 VarC[i+1] 的结果。代码

14.7算是顺序语言常见的例子,期间初始化也好,还是利用for循环为数组赋值也好,许多底层的工作都交由编译器去作,我们只要翘脚把代码照顾好就行。

1. reg [7:0] RAM [3:0]

2. reg [3:0]i;

3.

4. always @ ( posedge CLOCK ) // 错误

5. for( i = 0; i < 3; i = i + 1 ) RAM[i] = RAM[i+1];

代码14.8

换做描述语言,如代码14.8所示 ... 笔者在第1~2行当中先声明位宽为8长度为4的RAM,随之又声明i。假设RAM要实现代码14.7同样的赋值操作,首先最常见的错误就是第4~5行的例子 ... 许多人会直接将关键字for套用在 always 块里,这种赋值操作有两种问题:

其一,编译器并不清楚我们到底要利用空间实现for,还是利用时钟实现for。默认下,编译器会选择前者,后果就是吃光逻辑资源。

其二,RAM[i] = RAM[i+1] 这种赋值操作会搞砸综合,结果片上内存的布线状况会变得非常复杂,从而导致综合失败。

代码14.8算是新手最容易犯下的问题之一,代码14.8虽然没有语法上的错误,而且仿真也会通过,但是综合却万万不可。为此,代码14.8需要更动一下。

1. reg [7:0] RAM [3:0]

2. reg [3:0]i;

3.

4. always @ ( posedge CLOCK ) // 错误

5. case( i )

6. 0,1,2:

7. begin RAM[i] <= RAM[i+1]; i <= i + 1’b1; end

8. endcase

代码14.9

如代码14.9所示,笔者舍弃关键字 for,取而代之却利用仿顺序操作充当循环,这是一种利用时钟实现for的方法 (伪循环)。不过代码14.9依然不被综合器接受,结果报错 ... 因为片上内存并不支持类似 RAM[i] <= RAM[i+1] 的赋值方式,因为综合期间会导致布线复杂化,并且进一步搞砸综合。为此,代码14.9需要继续更动。

1. reg [7:0] RAM [3:0]

2. reg [3:0]i;

3. reg [7:0]D1;

4.

5. always @ (posedge CLOCK) // 正确

6. case( i )

7. 0,2,4:

8. begin D1 <= RAM[i<<1]; i <= i + 1’b1; end

9. 1,3,5:

10. begin RAM[ (i<<1)+1 ] <= D1; i <= i + 1’b1; end

11. endcase

代码14.10

如代码14.10所示,笔者多建立一个作为暂存作用的寄存器D1,然后利用两组步骤移动RAM之间的数据。步骤0,2与4将RAM[i] 的内容暂存至D1,步骤1,3与5则将D1的内容赋予 RAM[i+1]。如此一来,片上内存成员与成员之间的数据移动便大功告成。事实上代码14.7也干同样的事情,不过事实却被编译器隐藏了 ... 如果读者读者打开代码14.7的编译结果,读者会看见类似的汇编语言,结果如代码14.11所示:

T0 Load RAM[1] => R0;

T1 Load R0 => RAM[0];

T2 Load RAM[2] => R0;

T3 Load R0 => RAM[1];

T4 Load RAM[3] => R0;

T5 Load R0 => RAM[2];

代码14.11

如代码14.11所示,汇编内容会重复使用 Load 指令将某个RAM的内容先暂存至通用寄存器R0,然后又从R0移至另某个RAM当中。至于代码14.11的正确性,笔者不能确保什么,毕竟距离上一次接触汇编语言已经是N年前的事情。不过感觉上差不多就是那样 ... 这就是被编译器所隐藏的底层工作之一,代码14.10不过是将其模仿而已。

讲到这里,我们开始接触重点了。上述的例子告诉我们,编译器不会帮忙描述语言处理底层操作。所以,变量与数组之间的储存操作不及顺序语言那么便捷,而且模仿起来也非常麻烦 ... 不过,我们也不用那么灰心,良驹有良驹的跑法,歪驹有歪驹的走法,我们只要换个角度去面对情况,不视问题,问题自然迎刃而解。

根据笔者的妄想,储存有“储存资源“ 还有“储存方式”之分。描述语言可用的储存资源有寄存器还有片上内存,然而变量与数组也是最简单也是最基础的“储存方式”。基于这些 ... 事实上,描述语言可以描述各种各样的“储存方式”。

1. module rom( input [1:0]iAddr, output [7:0]oData );

2. reg [7:0]D1;

3. always @ (*) 

4. if( iAddr == 2’b00 ) D1 = 8’hA;

5. else if( iAddr == 2’b01 ) D1 = 8’hB;

6. else if( iAddr == 2’b10 ) D1 = 8’hC;

7. else if( iAddr == 2’b11 ) D1 = 8’hD;

8. else D1 = 8’dx;

9.

10. assign oData = D1;

11.

12. endmodule

代码14.12

例如一个简单的静态ROM模块,它可以基于寄存器或者片上内存,结果如代码14.12与14.13所示。代码14.12是基于寄存器的静态ROM,它有2位iAddr与8位的oData

,其中第3~8行是ROM的内容定义,第10行则是输出驱动,为此oData会根据iAddr的输入产生不同的输出。

1. module rom( input [1:0]iAddr, output [7:0]oData );

2. reg [7:0] RAM [3:0];

3. initial begin

4. RAM[0] = 8’hA;

5. RAM[1] = 8’hB;

6. RAM[2] = 8’hC;

7. RAM[3] = 8’hD;

8. end

9.

10. assign oData = RAM[ iAddr ];

11.

12. endmodule

代码14.13

反之,代码14.13是基于片上内存的静态ROM,它也有2位iAddr与8位oData,第3~7行是内容的定义也是初始化片上内存,第10行则是输出驱动,oData会根据iAddr的输出产生不同的输出。

代码14.12与代码14.13虽然都是静态ROM,不过却有根本性的不同,因为两者源于不同的储存资源,其中最大的证据就是第10行的输出驱动,前者由寄存器驱动,后者则由片上内存驱动。不同的储存资源也有不同的性质,例如寄存器操作简单,而且布线有余,不过不支持大容量的储存行为。换之,片上内存虽然操作麻烦,布线也紧凑,可是却支持大容量的储存行为。

储存方式相较储存资源理解起来稍微抽象一点,而且想象范围也非常广大 ... 如果储存资源是“容器的种类”,那么储存方式就是“容器的用法”。举例而言,一个简单静态ROM,根据需要它还可以演变成为其它亚种,例如常见的单口ROM或者双口ROM或等。

1. module rom( input CLOCK,input [1:0]iAddr, output [7:0]oData );

2. reg [7:0] RAM [3:0];

3. initial begin

4. RAM[0] = 8’hA;

5. RAM[1] = 8’hB;

6. RAM[2] = 8’hC;

7. RAM[3] = 8’hD;

8. end

9.

10. reg [1:0] D1;

11. always @ ( posedge CLOCK)

12. D1 <= iAddr;

13.

14. assign oData = RAM[ D1 ];

15.

16. endmodule

代码14.14

如代码14.14所示,那是单口ROM的典型例子,然而单口ROM与静态ROM之间的差别就在于前者有时钟信号,后者没有时钟信号。期间,代码14.14用D1暂存iAddr,然后再由D1充当RAM的寻址工具。

1. module rom( input CLOCK,input [1:0]iAddr1, iAddr2,output [7:0]oData1,oData2 );

2. reg [7:0] RAM [3:0];

3. initial begin

4. RAM[0] = 8’hA;

5. RAM[1] = 8’hB;

6. RAM[2] = 8’hC;

7. RAM[3] = 8’hD;

8. end

9.

10. reg [1:0] D1;

11. always @ ( posedge CLOCK)

12. D1 <= iAddr1;

13.

14. assign oData1 = RAM[ D1 ];

15.

16. reg [1:0] D2;

17. always @ ( posedge CLOCK)

18. D2 <= iAddr2;

19.

20. assign oData2 = RAM[ D2 ];

21.

22. endmodule

代码14.15

如代码14.15所示,那是双口ROM的典型例子,如果将其比较单口ROM,它则多了一组 iAddr与oData而已,即iAddr1与oData1,iAddr2与oData2。第10~14行是第一组(第一口),第16~20行则是第二组(第二口),不过两组 iAddr 与 oData 都从同样的RAM资源哪里读取结果。

事实上,ROM还会根据更多不同要求产生更多亚种,而且亚种的种类也绝非局限在于专业规范,因为亚种的储存模块会依照设计者的欲望——有多畸形就多畸形,死守传统只会固步自封而已。无论模块对象是静态ROM,单口ROM还是双口ROM等 ... 笔者眼中,它们都是任意的“储存方式”而已。

根据笔者的妄想,储存方式的覆盖范围非常之广。简单而言,凡是模块涉及数据的储存操作,低级建模II都视为储存类。举例而言,ROM模块储存自读不写的数据; RAM模块储存又读又写的数据;FIFO模块储存先写先读的数据。

为此,我们可以这样命名它们:

rom_savemod.v // rom储存模块

ram_savemod.v // ram储存模块

fifo_savemod.v // fifo储存模块

好奇的朋友一定会觉得疑惑,笔者究竟是为了定义储存类呢?事情说来话长,笔者也是经过多番考虑以后才狠下心肠去决定的。首先,让我们继续从顺序语言的角度去理解吧:

1. unsigned char Variable;

2. void FunctionA( unsinged char A ) { Variable = A; }

3. unsinged char FunctionB( void ) { return Variable; } 

4. int main()

5. {

6. unsigned char D1;

7. FunctionA( 0x0A ); 

8. D1 =FunctionB();

9. ......

10. }

代码14.16

假设有N个函数想共享数据,一般而言我们都会建立全局变量(数组)。如代码14.16所示,笔者先建立全局变量Variable,然后又声明函数A为Variable 赋值,反之函数B则返回Variable的内容。完后,再编辑主函数的操作 ... 期间,主函数先声明变量D,然后调用函数A,并且传递参数 0x0A,完后便调用函数B,并且将返回的内容赋予D。

函数之间之所以可以共享数据,那是因为编译器在后面大力帮忙,并且处理底层操作才得以实现。换之,描述语言虽然没有类似的好处,但是描述语言可以模仿。

1. reg [7:0]Variable;

2. reg [7:0]T,D1;

3. reg [3:0]i,Go;

4. always @ ( posedge CLOCk ) // 核心操作

5. case(i)

6. 0: // 主操作

7. begin T <= 8’h0A; i <= 4’d8; Go <= i + 1’b1; end

8. 1:

9. begin i <= 4’d9; Go <= i + 1’b1; end

10. 2:

11. begin D1 <= T; i <= i + 1’b1; end

12. ......

13. 8// Fake Function A 伪函数A

14. begin Variable = T; i <= Go; end

15. 9: // Fake Function B 伪函数B

16. begin T = Variable; i <= Go; end

17. endcase

代码14.17

如代码14.17所示,笔者先建立Variable,然后又建立T与D,还有i与Go。Variable模仿全局变量,T则是伪函数的暂存空间(数据传递),i指向步骤,Go则是指向返回步骤。步骤0~2,我们可以视为主函数,步骤8~9则是伪函数A与伪函数B。

步骤0,i将指向伪函数A的入口,T赋予 8’h0A,Go则指向下一个步骤。

步骤8,Variable 赋予 T 的内容,然后返回步骤。

步骤1,i将指向伪函数B的入口,Go则指向下一个步骤。

步骤9,T赋予Varibale 的内容,然后返回步骤。

步骤2,D1赋予Varibale的内容,然后操作结束。

如果我们将代码14.16与代码14.17互相比较的话,它们存在几处区别甚微的地方。

其一,代码14.17的代码量比代码14.16还要多;

其二,代码14.16的Variable是真正意义上的全局变量,反之代码14.17则是山寨。

除此之外,代码14.17还是一只核心操作组成,或者代码14.17是有一只函数而已。

如果主函数,函数A还有函数B之间只有简单操作,而且数据的传递量也不多的话,那么仅有一只核心操作也没有什么问题。相反的,如果函数之间不仅有复杂的操作,而且数据的传递量也很多的话,独秀的核心操作就要举白旗投降了。为此,我们必须借助多模块的力量来解决复杂的操作,但是多模块之间又如何共享数据呢?首先,让我们换个思路思考问题。

1. unsigned char Variable; // 储存类

2. void FunctionA( unsinged char A ) { Variable = A; } // 功能类

3. unsinged char FunctionB( void ) { return Variable; } // 功能类 

4. int main() { ...... } // 控制类

代码14.18

如代码14.18所示,全局变量视为储存类,函数A与函数B视为功能类,至于主函数视为控制类。

clip_image004

图14.2 代码14.18的建模图。

代码14.18经过分类以后,大致的建模布局如图14.2所示。一只名为main的控制模块充当中介,次序调度,协调者等角色。其中,A功能模块与B功能模块负责最基本的操作,variable储存模块则负责储存操作。余下,所有模块都经由问答信号联系起来,至于Verilog则可以这样表示:

1. module ( ... );

2.

3. wire [2:0]CallU1;

4. main_ctrlmod U1

5. ( 

6. .oCall( CallU1 ),

7. .iDone( { DoneU1, DoneU2, DoneU3 } ),

8. ...

9. );

10.

11. wire DoneU2;

12. a_funcmod U2

13. ( 

14. .iCall( CallU1[0] ), 

15. .oDone( DoneU2 ),

16. ...

17. );

18.

19. wire DoneU3;

20. b_funcmod U3

21. ( 

22. .iCall( CallU1[1] ), 

23. .oDone( DoneU3 ),

24. ...

25. );

26.

27. wire DoneU4;

28. varibale_savemod U1

29. ( 

30. .iCall( CallU1[2] ), 

31. .oDone( DoneU4 ),

32. ...

33. );

34.

35. endmodule

代码14.18

如代码14.18所示,组合模块的内容包含,main控制模块为实例U1,a功能模块与b功能模块为实例U2~U3,variable储存模块为实例 U4。最后,各个模块经由问答信号 Call/Done 联系起来。

前面的例子告诉我们,描述语言在变量上的运用,远远不及顺序语言那么便捷,毕竟描述语言没有底层补助,而且模仿它人也超麻烦。话虽如此,这是描述语言的缺点也是优点 ... 优点?笔者有没有搞错?那么麻烦还称为优点,笔者是不是脑子进水了?这位同学别猴急,笔者会慢慢解释的。

1. unsigned char LUT[4] = { 10, 20, 30, 40 };

2. int main()

3. {

4. int D1;

5. D1 = LUT[1] + LUT[2];

6. ...

7. }

代码14.19

如代码14.19所示,第1行声明位宽为8,长度为4的LUT查表,第2~7行则是查表的运用。表面上,顺序语言虽有惊人的便捷性,不过底子里却是一片死残,尤其是时钟的利用率更是惨不忍睹。那些写过算法的同学一定知道,查表常常用来优化算法的运算速度 ... 简单来说,查表就是顺序语言“空间换速度”的优化手段。

查表既是ROM也是一种储存方式。如果把话说难听一点,所谓查表也不过是顺序语言在利用数组模仿ROM而已,它除了便捷性好以外,无论是资源的消耗,还是时钟的消耗等效率都远远不及描述语言的ROM。顺序语言偶尔虽然也有山寨的FIFO,Shift等储存方式,不过性能却是差强人意。

顺序语言之所以那么逊色,那是因为被钢铁一般坚固的顺序结构绑得死死。述语言是自由的语言,结构也是自由。虽然自由结构为人们带来许多麻烦,但是“储存方式”可以描述的范畴,绝对超乎人们的估量。归根究底,究竟是顺序语言好,还是描述语言模比较厉害呢?除了见仁见智以外,答案也只有天知晓。

随着时代不断变迁,“储存方式”的需求也逐渐成长,例如50年代需要rom,60年代需要ram,70年代需要 fifo。二十一世纪的今天,保守的规范再也无法压抑“储存方式”的放肆衍生,例如rom衍生出来静态rom,单口rom,双口rom等许多亚种;此外,fifo也衍生出同步fifo或者异步fifo等亚种。至于ram的亚种,比前两者更加恐怖!不管怎么样,大伙都是笔者的好孩子,亦即 ××_savemod。

虽然伟大的官方早已准备数之不尽的储存模块,但是笔者还是强调手动建模比较好,因为官方的东西有太多限制了。此刻,可能有人跳出来反驳道:“为什么不用官方插件模块,它们既完整又便捷,那个白痴才不吃天上掉下来的馅饼!笔者是呆子!蠢货!“。话说这位同学也别那么激动,如果读者一路索取它人的东西,学习只会本末倒置而已。

除此之外,官方插件模块是商业的产物,不仅自定义有限内容也是隐性,而且还是不择不扣的快餐。快餐即美味也方便,偶尔吃下还不错,但是长期食用就会危害健康,危害学习。

“fifo插件的数据位宽能不能设为11位?”,某人求救道。

“ram插件怎样调用?怎样仿真?”,某人求救道。

类似问题每月至少出现数十次,而且还是快餐爱好者提问的。笔者也有类似的经验,所以非常明白这种心境。年轻的笔者就是爱好快餐,凡事拿来主义,伸手比吃饭更多。渐渐地,笔者愈来愈懒,能不增反降,最终变成只会求救的肥仔而已。后悔以后,笔者才脚踏实地自力建模,慢慢减肥。

在此,笔者滔滔不绝只想告知读者 ... 自由结构虽然麻烦,不过这是将想象力具体化的关键因素,储存模块的潜能远超保守的规范。规范有时候就像一粒绊脚石,让人不经意跌倒一次又一次,阻碍人们前进,限制人们想象,最后让人成为不动手即不动脑的懒人。最后,让我们建立一只不规格又畸形的储存模块作为本实验的句号。

clip_image006

图14.3 实验十四的建模图。

图14.3是实验十四的建模图,组合模块 savemod_demo 的内容包括一支核心操作,一只数码管基础模块,还有一只名字帅到掉渣的储存模块。核心操作会拉高 oEn,并且将相关的 Addr 与 Data 写入储存模块,紧接着该储存模块会经由 oData驱动数码管基础模块。事不宜迟,让我们先来瞧瞧推挤位移储存模块这位帅哥。

pushshift_savemod.v

clip_image008

图14.4 推挤位移储存模块的建模图。

顾名思义,该模块是推挤功能再加上位移功能的储存模块,左边是储存模块常见的iEn,iAddr与iData,右边则是超乎常规的oData。

1. module pushshift_savemod

2. (

3. input CLOCK,RESET,

4. input iEn,

5. input [3:0]iAddr,

6. input [3:0]iData,

7. output [23:0]oData

8. );

第3~7行是相关的出入端声明。

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

10. reg [23:0] D1;

11.

12. always @ ( posedge CLOCK or negedge RESET )

13. if( !RESET )

14. begin

15. D1 <= 24'd0; 

16. end

第9行是片上内存RAM的声明,第10行则是寄存器D1的声明。第15行则是D1的复位操作。

17. else if( iEn )

18. begin

19. RAM[ iAddr ] <= iData;

20. D1[3:0] <= RAM[ iAddr ];

21. D1[7:4] <= D1[3:0];

22. D1[11:8] <= D1[7:4];

23. D1[15:12] <= D1[11:8];

24. D1[19:16] <= D1[15:12];

25. D1[23:20] <= D1[19:16];

26. end

27.

28. assign oData = D1;

29.

30. endmodule

第17行表示 iEn不拉高该模块就不工作。第18~26行是该模块的核心操作,第19行表示RAM将iData储存至 iAddr指定的位置;第20行表示,RAM将iAddr指定的内容赋予D1[3:0]。如此一来,第19行与第20行的结合就成为推挤功能。至于第21~25行则是6个深度的位移功能(即4位宽为一个深度), iEn每拉高一个时钟,D1的内容就向左移动一个深度。

savemod_demo.v

该组合模块的连线部署根据图14.3,具体内容我们还是来看代码吧。

1. module savemod_demo

2. (

3. input CLOCK,RESET,

4. output [7:0]DIG,

5. output [5:0]SEL

6. );

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

7. reg [3:0]i;

8. reg [3:0]D1,D2; // D1 for Address, D2 for Data

9. reg isEn;

10.

11. always @ ( posedge CLOCK or negedge RESET ) // Core

12. if( !RESET )

13. begin

14. i <= 4'd0;

15. { D1,D2 } <= 8'd0;

16. isEn <= 1'b0;

17. end

18. else

以上内容是相关的寄存器声明以及复位操作。其中D1用来暂存地址数据,D2用来暂存读写数据。第12~17行是这些寄存器的复位操作。

19. case( i )

20.

21. 0:

22. begin isEn <= 1'b1; D1 <= 4'd0; D2 <= 4'hA; i <= i + 1'b1; end

23.

24. 1:

25. begin isEn <= 1'b1; D1 <= 4'd0; D2 <= 4'hB; i <= i + 1'b1; end

26.

27. 2:

28. begin isEn <= 1'b1; D1 <= 4'd0; D2 <= 4'hC; i <= i + 1'b1; end

29.

30. 3:

31. begin isEn <= 1'b1; D1 <= 4'd0; D2 <= 4'hD; i <= i + 1'b1; end

32.

33. 4:

34. begin isEn <= 1'b1; D1 <= 4'd0; D2 <= 4'hE; i <= i + 1'b1; end

35.

36. 5:

37. begin isEn <= 1'b1; D1 <= 4'd0; D2 <= 4'hF; i <= i + 1'b1; end

38.

39. 6:

40. begin isEn <= 1'b1; D1 <= 4'd0; D2 <= 4'h0; i <= i + 1'b1; end

41.

42. 7:

43. begin isEn <= 1'b0; i <= i; end

44.

45. endcase

46.

以上内容为核心操作,操作过程如下:

步骤0为地址0写入数据 4’hA;,将原本的数据挤出来,并且发生位移。

步骤1为地址0写入数据 4’hB;,将4’hA挤出来,并且发生位移。

步骤2为地址0写入数据 4’hC;,将4’hB挤出来,并且发生位移。

步骤3为地址0写入数据 4’hD;,将4’hC挤出来,并且发生位移。

步骤4为地址0写入数据 4’hE;,将4’hD挤出来,并且发生位移。

步骤5为地址0写入数据 4’hF,将4’hE挤出来,并且发生位移。

步骤6为地址0写入数据 4’d0,将4’hF挤出来,并且发生位移。

步骤7结束操作。

clip_image010

图14.5 savemod_demo 部分时序图。

图14.5是 savemod_demo 部分重要的理想时序图,其中isEn,D1与D2 是核心操作所发送的数据,至于RAM[0]与oData是推挤位移储存模块的内部状况与输出结果。时序过程如下:

T0,核心操作拉高isEn,发送4’d0地址数据与4’hA读写数据。

T1,核心操作拉高isEn,发送4’d0地址数据与4’hB读写数据。储存模块将4’hA载入地址0。

T2,核心操作拉高isEn,发送4’d0地址数据与4’hC读写数据。储存模块将4’hB载入地址0,并且将数据 4’hA挤出,oData的结果为 24’h00000A。

T3,核心操作拉高isEn,发送4’d0地址数据与4’hD读写数据。储存模块将4’hC载入地址0,并且将数据 4’hB挤出,同时发生位移,oData的结果为 24’h0000AB。

T4,核心操作拉高isEn,发送4’d0地址数据与4’hE读写数据。储存模块将4’hD载入地址0,并且将数据 4’hC挤出,同时发生位移,oData的结果为 24’h000ABC。

T5,核心操作拉高isEn,发送4’d0地址数据与4’hF读写数据。储存模块将4’hE载入地址0,并且将数据 4’hD挤出,同时发生位移,oData的结果为 24’h00ABCD。

T6,核心操作拉高isEn,发送4’d0地址数据与4’d0读写数据。储存模块将4’hF载入地址0,并且将数据 4’hE挤出,同时发生位移,oData的结果为 24’h0ABCDE。

T7,储存模块将4’d0载入地址0,并且将数据 4’hF挤出,同时发生位移,oData的结果为 24’hABCDEF。

47. wire [23:0]DataU1;

48.

49. pushshift_savemod U1

50. (

51. .CLOCK( CLOCK ),

52. .RESET( RESET ),

53. .iEn( isEn ), // < Core

54. .iAddr( D1 ), // < Core

55. .iData( D2 ), // < Core

56. .oData( DataU1 ) // > U2

57. );

58.

第47~58行是该储存模块的实例化。

59. smg_basemod U2

60. (

61. .CLOCK( CLOCK ),

62. .RESET( RESET ),

63. .DIG( DIG ), // top

64. .SEL( SEL ), // top

65. .iData( DataU1 ) // < U1

66. );

67.

68. endmodule

第59~66行是数码管基础模块的实例化。编译完毕便下载程序,如果数码管从左至右显示“ABCDEF”,那么表示实验成功。最后还是要强调一下,推挤位移目前是没有意义的储存模块,可是实验十四的目的也非常清楚,就是解释储存模块,演示畸形的储存模块。

友情提示:
信息收集于互联网,如果您发现错误或造成侵权,请及时通知本站更正或删除,具体联系方式见页面底部联系我们,谢谢。

其他相似内容:

热门推荐: