含有ST7565P 芯片的液晶,是没有文库支持的功能,但是没有就没有啦!液晶可以给我画画,那么它就是好东西了。
液晶的“显示”,液晶的“扫描次序”全部都与CGRAM分配有很大的关系。我们先了解“扫描次序”吧。
宏观上一副液晶是 “64高 x 128宽”。微观上由芯片 ST7565P驱动的一副 12864 液晶是由“8个8 高 x 128宽的页”组成。 至于液晶的“扫描次序”就与4个命令有关系。
上图表示了,当命令为0xA0列扫描是“自左向右”,如果命令式 0xA1列扫描是“自右向左”。总归,这两个命令控制了“列扫描次序”
除了控制列扫描的命令以外,当然还有控制“页扫描次序”的命令。如上图,命令 0xC0 控制页扫描是“从下至上”,然而命令 0xc8 控制页扫描“又上至下”。无论页扫描的次序是“从上至下”还是“从下至上”,然而每一页的列填充,都是“低位开始高位结束”
关于列扫描就有列填充的问题。我们知道每“一页”都是由“8 高 x 128宽”组成。换句话说,这里没有“行扫描”的概念,因为“一页”都是由“一个字节数据,列填充128次”成为一页。如上图中所示。
假设“页扫描次序”是由上至下,填充的值是 0x0f,那么经过 128次的“列扫描”以后,一页的扫描结果会是如上图所示。
关于 ST7565P 芯片,命令,和液晶扫描它们之间的关系而已,我们简单来总结一下:
(一)CGRAM分布是由8页组成。
(二)每一页是 由 一个字节填充 和128次列扫描 组成。
(三)列扫描次序与命令 0xA0 与 0xA1有关。
(四)页扫描次序与命令 0xC0 与 0xC8 有关。
(五)列填充字节的高位低位关系与页扫描命令有关。
(六)不存在行扫描概念。
上图所示是 “页扫描”由上至下,“列扫描”由左至右,列填充值是 0x0f。
在CGRAM分布方面。CGRAM 可以说是由 8 bits x 1024 words,如果以“页”去分配,也就是说 8 page x 8 bits x 128 words , 那么“页”的偏移量就是 128。这一点要好好的记住。
那么关于“列地址”和“页地址”又是如何呢?
事实上 CGRAM 的建立不可能是 8 page x 8 bits x 128 words 那么完美的,必定有而外的列和页是不在显示的范围内,亦即第8页和第128~131列(如果页和列从0开始计算)。
虽然说完成一次列填充,列地址会自动递增,然而 ST7565P 对于列地址的控制显得很笨蛋。
假设一开始我们设置“页地址0和列地址0作为起始地址”,当列填充到127(如果从0开始计算),列地址会自动递增至128, 这显然不是显示范围了(红色部分)。所以呀,每一次完成128次的列填充,就要“重新设置列起始地址和下一个页地址”。
关于设置也地址的命令很简单,就是 0xb?。“?”页地址的设置。假设输入0xb0, 也就是页地址0。
那么关于设置列地址的命令是 0x1?, 和 0x0?。命令 0x1?的“ ?”是列地址的“高四位”,0x0?的“?”是列地址的“低四位”。假设输入 0x10, 0x00, 也就是说列地址是 8'b 0000_0000, 亦即0。
假设我要设置页地址1(0000 0001),和列地址65(0100 0001)。那么我需要输入:
0xb1;
0x14;
0x01;
通过几页的内容,我只是要读者明白 ST7565P 芯片驱动液晶的规则和一些基本的概念,真正的好戏儿在后头。
上图是在黑金开发板上的12864 液晶原理图。对于串行输入模式的液晶来说,重要的引脚有 P/S,CS,A0,DB6(SCL)和 DB7(SDI)而已。ST7565P芯片可以支持3种传输模式,当然最简单的传输模式还是SPI模式,然而控制“传输模式的引脚”就是 P/S 。当 P/S 被拉低时就是表示“串行传输模式”。
CS是使能信号(低电平有效)。A0 是命令或者数据决定信号(0 = 命令,1 = 数据 )。SCL是串行时钟信号,SI是串行输入信号。
至于其他的引脚属性自己去查相关的数据手册吧,这里只说重要的引脚而已。
上图是ST7565P芯片,SPI传输的时序图。从图中我们可以明白,SI读取数据都是在SCL信号的上升沿。在这里我再重复一下:
CS是使能信号。SI是串行数据输入信号。SCL是串行时钟信号。AO是决定当前的SI信号上的是命令还是数据(1=数据,0=命令)。
在顺序操作上(以C语言为例),ST7565P芯片液晶的简易驱动概念如下:
// 建立最基本的传输函数 SPI_Send{ unsigned char Data } {} //建立传输数据函数 Send_Data( unsigned char Data) { A0 = 1; SPI_Send( Data ); ...... } //建立传输命令函数 Send_Command( unsigned char Data ) { A0 = 0; SPI_Send( Data ); ...... } //建立初始化函数 Initial_Function() { //液晶显示初始化配置 Send_Command( 0xaf ); //液晶使能 Send_Command( 0x40 ); //开始显示 Send_Command( 0xa6 ); //此命令表达 1 = 点亮,0 = 点灭 //扫描次序配置 Send_Command( 0xa0 ); //列扫描向左至右 Send_Command( 0xc8 ); //也扫描从上至下 //内部电源配置 Send_Command( 0xa4 ); Send_Command( 0xa2 ); Send_Command( 0x2f ); Send_Command( 0x24 ); Send_Command( 0x81 ); //背光LED配置命令 Send_Command( 0x24 ); //背光LED配置值 } //绘图函数 Draw_Fucntion() { for( int page = 0; page < 8; page++ ) { Send_Command( 0xb0 | page ); //设置页地址 Send_Command( 0x10 ); //设置列地址“高四位”- 0000 Send_Command( 0x00 ); //设置列地址“第四位”- 0000 for( int x = 0 ; x < 128; x ++ ) Send_Data( *pic++ ); } } //主函数 int main( void ) { Initial_Function(); Draw_Function(); whiel(1); //停止 }
在顺序操作中,我们会先建立最基本的 SPI_Send() 函数,然后基于 SPI_Send() 函数又建立 Send_Data() 和 Send_Command() 等函数。接下来,会基于 Send_Command() 函数建立 Initial_Function() 函数,和基于 Send_Data() 函数建立 Draw_Fucntion() 函数。最后在主函数中调用 Initial_Function() 和 Draw_Function() 函数。
在4-1章我说过了,顺序操作如同吃饭那样,有“步骤的概念”,然而顺序操作的语言都是偏向高级语言,所以在编辑上占到许多好处。很多重要的指令都是被隐性处理,如函数的调用指令和返回指令等。在上述的内容中,一些高级函数无视了许多隐性指令,只要简单的多次嵌入低层函数,就能形成Initial_Function() 和 Draw_Function() 等高级函数。此外函数的调用也有很方便。
那么Verilog HDL语言要如何模仿顺序操作呢?
上图所示是要建立的功能模块,spi_write_module.v 亦即spi发送模块。为了最大发挥 Verilog HDL语言特性,SPI_Data 和 SPI_Out 的位配置如下:
SPI_Data |
||
[9] |
[8] |
[7 .. 0] |
CS |
A0 |
Data |
SPI_Out |
|||
[3] |
[2] |
[1] |
[0] |
CS |
A0 |
SCL |
SI |
在这里需要重申几个常常容易被疏忽的重点:
我们知道SPI的时钟信号在“上升沿”的时候是“锁存数据”,在时钟信号的“下降沿”是“设置数据”。但是在单片机上编写SPI写函数,或者调用单片机SPI硬件资源来执行SPI写操作,我们常常会忽略了这些具体的细节。
(那些有关使用单片机SPI硬件资源的事儿,我什么都不想说,因为这样的做法什么也学不到。)
SPI_Send( unsigned char Data ) { CS = 0; SCL = 0;
for( int i = 0; i < 8; i++ ) { if( Data & 0x80 ) SI = 1; else SI = 0; Data <<= 1; SCL = 0; SCL = 1; } } |
SPI_Send( unsigned char Data ) { CS = 0; SCL = 1;
for( int i = 0; i < 8; i++ ) { SCL = 0; if( Data & (7-i) ) SI = 1; else SI = 0;
SCL = 1; } } |
上面有两个SPI_Send 函数,左边的写法是最常用,但是也是最容易忽略小细节。相比右边的写法比较谨慎,以最低的方法去符合一写小细节。
对于SPI时钟信号,在空闲的时候总是处于高电平(几乎所有与上升沿有关的信号,在默认状态下都是处于高电平)。SPI时钟信号在下降沿“设置”SI数据(主机数据移位操作),反之SPI时钟信号在上升沿“锁存”数据(从机读取数据操作)。很明显左边的写法没有符合这个规则,然而右边的写法却符合这个规则。
无论是左边的写法还是右边的写法,都忽略了一个致命的细节,两种写法都无法确定SPI时钟信号的时钟频率。当然可以基于上述的写法产生更笨拙的写法,如下:
SPI_Send( unsigned char Data ) { CS = 0; SCL = 1; for( int i = 0; i < 8; i++ ) { SCL = 0; Delay_US(10); //添加延迟函数 if( Data & (7-i) ) SI = 1; else SI = 0; SCL = 1; Delay_US(10); //添加延迟函数 } }
哦!这样的此法只会浪费单片机宝贵的处理资源 ... 除非这个单片机有置入实时操作系统,否则那样的活儿将会是非常的糟糕。
虽然顺序操作的语言在“结构性”和“简易性”上,远远领先 Verilog HDL语言。但是你别忘了我们可以利用 Verilog HDL 语言来“模仿”顺序操作。可能读者会误会“仿顺序操作”只是在外形上模仿“顺序操作”而已。但是实际上,我们可以借与Verilog HDL语言本身的特性,只要稍微用心去发挥一下,读者不仅可以模仿“顺序操作”的“操作概念”,而且还可以发挥出超越“顺序操作”本身的极限。
虽然spi_write_module.v 终究仅是模仿 SPI_Send() 函数这个部分而已,但是这不是代表我们可以拥有“只要目的,不要细节”这种盲目的态度。
SCL的时钟频率定义为1Mhz , 也就是说一个周期是 1us ,半周期就是 0.5us 。如果以20Mhz来定时,那么计数的结果是10。在19行定义了0.5us
第23~33行是0.5us的定时器。但是比较不同的是,这个定时器平时不工作,当Start_Sig 拉高的时候才开始计数(第30行)。
第37~64行是 spi_write_module.v 的核心功能。I寄存器表示操作步骤,rCLK寄存器表示SCL 然而 rDO寄存器表示 SI 。如同前面所述那样,SCL时钟信号,处于空闲状态时是出于高电平,所以 rCLK 复位与逻辑1(46行)。
在这里稍微提醒一下:
SPI_Data : 第9位表示CS,第8位表示A0,第7 .. 0 位表示一字节数据。
SPI_Out : 第3位表示CS,第2位表示A0,第1位表示SCL,第0位表示SI。
当 Start_Sig 拉高的同时,定时器开始计数(30行),该模块也开始执行(50行)。
当第一个定时产生的时候(54行),也就是第一个时钟的前半周期,亦即下降沿,rCLK设置为逻辑0。根据SPI传输的规则,下降沿的时候主机设置数据,rDO赋予 SPI_Data 信号的第7位(SPI传输是从最高位开始,最低位结束),最后 i递增以示下一个步骤。
当i等于1的时候并且定时产生(56行),这表示第一个时钟的后半周期,亦即上升沿,rCLK设置为逻辑1。在SPI传输的规则中上升沿的时候,从机锁存数据,从机自己单纯的将rCLK拉高即可。然后i递增以示下一个步骤。
上述的步骤会一直重复到第八次,直到一字节的数据发送完毕。最后会产生一个完成信号(59~63行)。
这里有一个表达式需要说明一下:
i >> 1 : 表示 i 除与 2。因为右移操作也是代表 除与j^2, j是右移次数。
假设 8 >> 2 ,亦即 8 / 2^2 等于2。 8 >> 3, 亦即 8 / 2^3 等于 1。
最后还有一个重点就是 SPI_Out 的驱动(70行)。在上面我已经重复过 SPI_Out 是占4位的输出。而且每一个位都有意义。
SPI_Out 第3位:表示了CS,所以直接由 SPI_Data的第9位驱动。
SPI_Out 第2位:表示了A0,同样也是直接由 SPI_Data 的第8位驱动。
SPI_Out 第1位:表示了SCL,以寄存器rCLK来驱动。
SPI_Out 第0位:表示了SI,以寄存器rDO来驱动。
这样的目的是简化连线的复杂度。我们知道Verilog HDL语言的位操作是很强大。懂得善用,会对建模提到很大的帮助。
乍看 initial_module.v 既包含了 initial_control_module.v 和 spi_write_module.v。spi_write_module.v 前面已经说过了, 至于 initial_control_module.v 吗 ~ 我们知道我们需要一个控制模块来执行,初始化的步骤,而该模块就是这个初衷。
第11~17行定义了输出和输入口相关的信息,具体和图形一样。在22行定义了 rData寄存器,它是用来驱动 SPI_Data (94行)。第23行定义了 isSPI_Start 标志寄存器,如命名般一样,是用来驱动 SPI_Start_Sig, 换句话就是 SPI发送模块的是能信号。
第26~88是该模块的核心部分。当上一层将 Start_Sig 拉高的时候(注意:initial_control_module.v 的 Start_Sig外部连线是Initial_Start_Sig),该模块就开始工作(35行)。全核心部分都是使用“仿顺序操作”的写法。
前三个命令是液晶的“显示配置命令”(38~48行),然而我们知道要对液晶写数据的时候,CS和A0都必须拉低,由于 SPI_Data 位分配的关系。rData寄存器第9 .. 8 位都是赋予 2'b00。
假设 i 等于 0。那么机会发送第一个命令,亦即 0xaf,
(39行)一开始由于条件if没有达到,(40行)rData会被赋予 2'b00 , 8'haf, 并且 isSPI_Start 会设置位逻辑1,这时候 SPI发送模块就会开始工作。直到SPI发送模块发送一字节数据,并且反馈一个完成信号的高脉冲(SPI_Done_Sig),if条件就会成立(39行),然后 isSPI_Start就会被设置为逻辑0,然后i递增以示下一步步骤。
类似上面的操作会一直重复,直到完成发送 3个“显示配置命令”,2个“扫描次序配置命令”,和6个“内部电源配置命令”(38~80行)。直到最后该模块会反馈一个完成信号给上一层模块(82~86行),并且(83行)复位 rData寄存器(前两位必须设置为逻辑1,而后八位可以是任意值)。
initial_module.v 是 initial_control_module.v 和 spi_write_module 的组合模块。连线关系基本上和“图形一样”。
有一点可能会使读者们困惑。因为“低级建模”的全部功能不可能在一个模块中完成,多多少少,读者们会对模块与模块之间的关系会有“不解”的情况。笔者在这里要求读者们要保持平常心去理解,因为Verilog HDL语言的建模本来就需要很强的逻辑性。目前面对的难题就当做是为日后的修行吧。
draw_module.v 是一个组合模块,同样 draw_module.v 有包含 spi_write_module.v 。
此外draw_module.v也含有draw_control_module.v和pika_rom_module.v,pika_rom_module.v 是一个 8 bits x 1024 words 的rom。
draw_control_module.v 控制模块主要是控制绘图的所有操作步骤,然而 pika_rom_module.v 包含了所需要的图片资料。该控制模块对 spi_write_module.v 的链接也和 initial_control_module.v 一样。
第13~20行的定义基本上都和“图形”一样,除了Start_Sig 和 Done_Sig 比较特别,它们在外部的连线时 Draw_Start_Sig 和 Draw_Done_Sig。
第31~67行是该模块的核心部分,但是别被它吓到了,它不过是充气胖子。在这里我们简单复习一下在“顺序操作”中的 Draw_Function() 的操作。
Draw_Fucntion() { for( int page = 0; page < 8; page++) { Send_Command( 0xb0|page); //页地址配置 Send_Command( 0x10 ); //列地址高四位配置 Send_Command( 0x00 ); //列地址第四位配置 for( int x = 0; x < 128; x++ ) Send_Data( *p++ ); //发送128次列填充 } }
上述的一段函数代码中,一个Draw_Function()函数的功能表达的一了百了,而且该函数中最大作用就是for循环,很可惜 Verilog HDL语言是不推荐使用 for循环。(不要问我为什么,很多的参考书上都是这样写的,如果以我的角度说,我表示for循环不适合Verilog HDL语言的风格)。在Draw_Function() 函数之中,第一个for循环控制 page,亦即页。并且在每一个页的开始都重新配置列地址。至于第二个for循环是用于控制 128次的列填充。
那么 Verilog HDL语言该如何呢?
在34~39行中,i控制执行步骤,x控制列扫描地址(列填充次数),y控制页扫描次序,rData是用来驱动 SPI_Out (73行),而 isSPI_Start是用来驱动 SPI_Start_Sig。当 Start_Sig被拉高的时候(41行),该控制模块就开始工作。
我们先假设一个情况:
当i等于0的时候,由于if条件不成立(45行)。由于“顺序操作”关系,必须先设置页地址,rData 被赋予 2'b00 ( CS=0, A0 = 0, 亦即发送命令 ) 和 y寄存器的值,Y寄存器复位值是 8'd0。然后isSPI_Start寄存器被设置为逻辑1(46行)。此时SPI发送模块开始工作。
当SPI发送完一字节的数据,就会反馈一个高脉冲至完成信号SPI_Done_Sig。此时if条件就会成立(45行),isSPI_Start 寄存器被设置为0,然后i递增以示下一步步骤。
当页地址设置完毕后,接下来的操作就要设置列地址。48~50行是设置列地址的高四位,52~54行是设置列地址的第四位。具体操作和设置也地址一样。不一样的是,每一次设置“新一页”,列地址都必须复原为0。
当页地址和列地址设置okay后,接下来就是128次的列填充操作了。x寄存是用来控制“列填充次数”,同期间也充当“列扫描地址”。一开始的时候,由于if条件和else if条件同样无法达成(57~58行),rData会被赋予 2'b01(CS = 0, A0 = 1, 亦即数据)和八位数据,然而八位数据 Draw_Data 的取值是来至 pika_rom_module.v的地址0的值。同期间 isSPI_Start 被设置为逻辑1,亦即 SPI发送模块被时能。
在这里我们先暂停一下!
为了迎合CGRAM 的分配,pika_rom_module.v 同样也是采用一样的分配方式。pika_rom_module.v 是 8 bits x 1024 words 的分配,如果迎合 CGRAM 的分配方式 pika_rom_module.v 可以这样定义 8 pages x 8 bits x 128 words,这也就说每一页之间的页偏移量是 128个words。在72行是 Rom_Addr 的输出,至于为什么驱动的表达式是 x + ( y << 7 )呢?“ y << 7 ”等价于“y * 128”,我们知道,y寄存器代表了液晶设置的当前页, x寄存器代表了液晶当前的列填充次数。
为了从 pika_rom_module.v 中读取到正确的 Draw_Data 值,“x + ( y << 7 )”表达式也成为了 Rom 地址的转换表达式。
我假设一个情况:
当 y寄存器等于0值时,亦即液晶正在就绪列填充第0页。而第0页的列填充值是在 pika_rom_module.v 的 0~127中。假设模块开始填充第0列,经过表达式转换后 :
( 0 + ( 0 << 7 ) )= 0; 也就是说第0页第0列的填充值在pika_rom_module.v的地址0。
再假设一个情况:
当 y寄存器的值等于3,亦即液晶正在填充第3页,然而第3页的列填充值是在 pika_rom_module.v 的 384~ 491中。假设列填充在63开始,经过表达式转换后 :
( 63 + ( 3 << 7 ) )= 447; 也就是说第3页第63列的填充值在pika_rom_module.v的地址447。
好了继续上面的话题。
直到SPI发送模块完成一字节数据的发送后,就会反馈一个高脉冲的完成信号。此时(58行)else if 条件成立,isSPI_Start 被设置为逻辑0,x寄存器递增,以示下一个列填充。这个时候,会再一次进入 else(59行),isSPI_Start被设置为逻辑1,rData被赋值为 2'b01 和 Draw_Data, 由于72行表达式的关系,这时候列填充的值是来至 pika_rom_module.v 地址1的值。
上述的内容会一直重复到 x 递增至 128 次,直到 if条件成立(57行)y寄存器递增,以示下一个页地址,然后x寄存器被赋值为0,最后 i递增以示下一个步骤。
“设置页地址,设置初始列地址,128次的列填充”这样的过程会一直重复,直到完成液晶全部8个页的所有列填充,draw_control_module.v 就会产生一个完成信号(61~65行),同期间也会复位 rData 和 y 寄存器的值(62行)。
就这样一副 12864 液晶扫描完毕。
Draw_module.v 是一个组合模块,具体上和“图形”一样。
上图中的 lcd_module.v 是已经完成的组合模块。Lcd_control_module.v 顾名思义就是控制液晶操作的控制模块。我们来看一下该模块与“顺序操作”的关系。
main()
{
Initial_Function();
Draw_Function();
while(1);
}
在主函数中,先调用 Initial_Function(), 然后再调用 Draw_Function()。然而液晶控制模块的操作却是如此,先使能initial_module.v 然后再使能 draw_module.v ,最后保持沉默。
至于选择器与4-1章的一样。无论是 initial_module.v 或者 draw_module.v 它们各自都拥有 spi_write_module.v 。为了协调它们共享输出资源,选择器是必须的。
整个组合模块比较简单,只要照“顺序操作”的思路,就能理解它们。
在16~17行定义了使能初始化模块和绘图模块的标志寄存器 isInit 和 isDraw。在27~40行是该控制模块的核心部分。一开始先使能初始化模块(29~31行),当初始化完成后(30行),便使能绘图模块(33~35行)。绘图完成后(34行),便进入停止(37~38行)。
lcd_module.v 组合模块基本上和“图形”一样,自己看着办吧。
这个实验是模仿“顺序操作”的“低级建模”。如 Initial_module.v 组合模块,包含了 initial_control_module.v 和 spi_write_module.v ,initial_control_module.v 控制初始化的次序,spi_write_module 却将数据已SPI模式输出。
draw_module.v 组合模块包含了draw_control_module.v , pika_rom_module.v 和 spi_write_module.v。Rom模块储存了 64 x 128 的图片信息,控制模块控制了显示图片的步骤,最后SPI发送模块将数据已SPI模式输出。
lcd_module.v 同时拥有 initial_module.v 和 draw_module.v 这两个组合模块,然后利用自身的 lcd_control_module.v 去控制“先使能 iniital_module.v 还是 draw_module.v?”。
宏观上,lcd_module.v都是由一层一层不同功能的组合模块组合而成,实际上它却是遵守“顺序操作”的执行步骤建模而成。
“仿顺序操作”始终有一个避免不了的问题,那就是“两仪性或者多义性”,initial_module.v 或者 draw_module.v 它们自身都拥有 spi_write_module.v ,两个模块不可能同时拥有一样的输出资源,这时候就需要选择器的帮助。
完成后的扩展图:
initial_module.v
draw_module.v
lcd_module.v
在宏观上“仿顺序操作”是利用建模模仿“顺序操作”。在微观上“仿顺序操作”是利用Verilog HDL语言本身的特性去模仿“顺序操作”。如 intial_module.v 和 draw_module.v 的 spi_write_module.v。在上述的内容中,“顺序操作”语言都无法顾及 spi传输的小细节,但是 Verilog HDL语言却顾及到这些小细节。还有一点,虽然Verilog HDL在设计上却不如“顺序语言”般的方便,所以在建模的时候应该“多善用位操作”来填补写问题。
最后我们来讨论一个问题:
Verilog HDl代码的设计是不是应该尽量简化!?虽然在网上,众多的人们的答案是“不是”。如果你的代码没有固定的结构(代码风格的问题),自然而然模块也不会执行也太稳定。相反的,如果你拥很强的经验能力,当然你可以忽略这样的问题,但是另一个问题是,你的代码只有你看得懂,别人却看不懂!(这种感受,估计很多新手都尝受过 ... )
如果给笔者选择,笔者会选择中规中矩的做法。笔者要求“代码风格”理应受到维护,但是代码量是在精简和臃肿之间,代码的设计是倾向“理解”的方向。
“低级建模”与“仿顺序操作”之间的优点,笔者就不多说了,读者可以从试验中直接的了解到。但是有一点笔者必须强调“仿顺序操作”不仅只是利用模块来模仿“顺序操作” 而已,设计者应该多利用Verilog HDL语言本身的特性去完成建模设计。
所以说,“仿顺序操作”许多时候也不是完全的模仿“顺序操作”。在顺序操作的语言之中,常常也使用多函数方式,去完成编程。毕竟,Verilog HDL语言与顺序操作语言是两个世界的东西,Verilog HDL更能顾及底层的小细节。
但是也就是这个原因给 Verilog HDL 语言带来许多麻烦。如同笔者在第一章所说,Verilog HDL语言如同乐高积木那样,它“太细”了,如果没有“手段”和“技巧”,是组合不了好的积木模型。换句话说,Verilog HDL语言的建模,如果不存在着结构性,是建立不了好的模块。然而这个“手段”或者“技巧”就是“低级建模”的存在。