zoukankan      html  css  js  c++  java
  • 【连载】【FPGA黑金开发板】Verilog HDL那些事儿12864(ST7565P)液晶驱动(十三)

    声明:本文为原创作品,版权归akuei2及黑金动力社区(http://www.heijin.org)共同所有,如需转载,请注明出处http://www.cnblogs.com/kingst/

    5

    4.2 实验十二:12864(ST7565P)液晶驱动

    显示概念

    含有ST7565P 芯片的液晶,是没有文库支持的功能,但是没有就没有啦!液晶可以给我画画,那么它就是好东西了。

    液晶的“显示”,液晶的“扫描次序”全部都与CGRAM分配有很大的关系。我们先了解“扫描次序”吧。

    clip_image002[4]

    宏观上一副液晶是 “64高 x 128宽”。微观上由芯片 ST7565P驱动的一副 12864 液晶是由“8个8 高 x 128宽的页”组成。 至于液晶的“扫描次序”就与4个命令有关系。

    clip_image004[4]

    上图表示了,当命令为0xA0列扫描是“自左向右”,如果命令式 0xA1列扫描是“自右向左”。总归,这两个命令控制了“列扫描次序”

    clip_image006[4]

    除了控制列扫描的命令以外,当然还有控制“页扫描次序”的命令。如上图,命令 0xC0 控制页扫描是“从下至上”,然而命令 0xc8 控制页扫描“又上至下”。无论页扫描的次序是“从上至下”还是“从下至上”,然而每一页的列填充,都是“低位开始高位结束”

    clip_image008[4]

    关于列扫描就有列填充的问题。我们知道每“一页”都是由“8 高 x 128宽”组成。换句话说,这里没有“行扫描”的概念,因为“一页”都是由“一个字节数据,列填充128次”成为一页。如上图中所示。

    假设“页扫描次序”是由上至下,填充的值是 0x0f,那么经过 128次的“列扫描”以后,一页的扫描结果会是如上图所示。

    关于 ST7565P 芯片,命令,和液晶扫描它们之间的关系而已,我们简单来总结一下:

    (一)CGRAM分布是由8页组成。

    (二)每一页是 由 一个字节填充 和128次列扫描 组成。

    (三)列扫描次序与命令 0xA0 与 0xA1有关。

    (四)页扫描次序与命令 0xC0 与 0xC8 有关。

    (五)列填充字节的高位低位关系与页扫描命令有关。

    (六)不存在行扫描概念。

    clip_image010[4]

    上图所示是 “页扫描”由上至下,“列扫描”由左至右,列填充值是 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 对于列地址的控制显得很笨蛋。

    clip_image012[4]

    假设一开始我们设置“页地址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 芯片驱动液晶的规则和一些基本的概念,真正的好戏儿在后头。

    clip_image014[4]

    上图是在黑金开发板上的12864 液晶原理图。对于串行输入模式的液晶来说,重要的引脚有 P/S,CS,A0,DB6(SCL)和 DB7(SDI)而已。ST7565P芯片可以支持3种传输模式,当然最简单的传输模式还是SPI模式,然而控制“传输模式的引脚”就是 P/S 。当 P/S 被拉低时就是表示“串行传输模式”。

    CS是使能信号(低电平有效)。A0 是命令或者数据决定信号(0 = 命令,1 = 数据 )。SCL是串行时钟信号,SI是串行输入信号。

    至于其他的引脚属性自己去查相关的数据手册吧,这里只说重要的引脚而已。

    clip_image016[4]

    上图是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发送模块

    clip_image018[4]

    上图所示是要建立的功能模块,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 函数,左边的写法是最常用,但是也是最容易忽略小细节。相比右边的写法比较谨慎,以最低的方法去符合一写小细节。

    clip_image019[4]

    对于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() 函数这个部分而已,但是这不是代表我们可以拥有“只要目的,不要细节”这种盲目的态度。

    spi_write_module.v

    clip_image021[4]

    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语言的位操作是很强大。懂得善用,会对建模提到很大的帮助。

    初始化模块

    clip_image023[4]

    乍看 initial_module.v 既包含了 initial_control_module.v 和 spi_write_module.v。spi_write_module.v 前面已经说过了, 至于 initial_control_module.v 吗 ~ 我们知道我们需要一个控制模块来执行,初始化的步骤,而该模块就是这个初衷。

    initial_control_module.v

    clip_image025[4]

    clip_image026[4]

    第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

    clip_image028[4]

    clip_image029[4]

    initial_module.v 是 initial_control_module.v 和 spi_write_module 的组合模块。连线关系基本上和“图形一样”。

    有一点可能会使读者们困惑。因为“低级建模”的全部功能不可能在一个模块中完成,多多少少,读者们会对模块与模块之间的关系会有“不解”的情况。笔者在这里要求读者们要保持平常心去理解,因为Verilog HDL语言的建模本来就需要很强的逻辑性。目前面对的难题就当做是为日后的修行吧。

    绘图模块

    clip_image031[4]

    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 一样。

    draw_control_module.v

    clip_image033[4]

    clip_image034[4]

    第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

    clip_image036[4]

    clip_image037[4]

    Draw_module.v 是一个组合模块,具体上和“图形”一样。

    液晶模块

    clip_image039[4]

    上图中的 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 。为了协调它们共享输出资源,选择器是必须的。

    整个组合模块比较简单,只要照“顺序操作”的思路,就能理解它们。

    lcd_control_module.v

    clip_image041[4]

    在16~17行定义了使能初始化模块和绘图模块的标志寄存器 isInit 和 isDraw。在27~40行是该控制模块的核心部分。一开始先使能初始化模块(29~31行),当初始化完成后(30行),便使能绘图模块(33~35行)。绘图完成后(34行),便进入停止(37~38行)。

    lcd_module.v

    clip_image043[4]

    clip_image044[4]

    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 ,两个模块不可能同时拥有一样的输出资源,这时候就需要选择器的帮助。

    完成后的扩展图:

    clip_image046[4]

    initial_module.v

    clip_image048[4]

    draw_module.v

    clip_image050[4]

    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语言的建模,如果不存在着结构性,是建立不了好的模块。然而这个“手段”或者“技巧”就是“低级建模”的存在。

  • 相关阅读:
    108. Convert Sorted Array to Binary Search Tree
    How to check if one path is a child of another path?
    Why there is two completely different version of Reverse for List and IEnumerable?
    在Jenkins中集成Sonarqube
    如何查看sonarqube的版本 how to check the version of sonarqube
    Queue
    BFS广度优先 vs DFS深度优先 for Binary Tree
    Depth-first search and Breadth-first search 深度优先搜索和广度优先搜索
    102. Binary Tree Level Order Traversal 广度优先遍历
    How do I check if a type is a subtype OR the type of an object?
  • 原文地址:https://www.cnblogs.com/kingst/p/1834252.html
Copyright © 2011-2022 走看看