完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
1、假设读者对硬件数字电路熟悉,比如自己可以用74芯片做跑马灯
2、C语言都比较熟悉,因为下面用的Verilog语言就跟它很类似,暂时规避晦涩的VHDL 我打算分几个部分 1、Verilog语法 2、组合逻辑设计 3、时序逻辑设计 4、阻塞和非阻塞 5、同步和异步设计 6、有限状态机 7、设计一个只有4条指令的CPU 1、Verilog语法 没错,我们就是拿C语言照猫画虎,下面是一个“老虎”的模型。 我们一个个看他跟“猫”不一样的地方 module nand( input in1, input in2, output out ); wire tmp; assign tmp = in1 & in2; assign out = ~tmp; endmodule 模块定义跟C语言的函数很相似吧 1、模块必须使用“module”关键字,他也没有返回值。 2、模块没有beginmodule,只有endmodule 3、模块对外接口有input,output,inout,但为了入门着想,只谈input和output 模块内部还有个中间变量耶,是不是看见了tmp就有很熟悉的感觉了。 没错,他就是中间“变量”,在硬件上他就是一根导线,wire望文生义即可。 看见了“=”就应该猜到这是赋值语句了,没错,但Verilog的语法要求前面必须有个苦B的assign关键字 至于“&”和“~”这2个运算符号,就不讲了吧,C语法搞不清的兄弟,对不住了 有人会说,你这“变量”到底是int还是long还是flot抑或double呢? 好了,咱继续照猫画虎,不过老虎毕竟跟猫是不一样的,比如老虎会虎啸,猫只会喵喵。 wire[7:0] tmp; 这一下子把tmp从一根线,扩展成了8根线,觉得是7根线的自己去看C语言课本去。 好了,我们要虎啸了,同时喵喵几下,对比着看 wire[7:0] tmp; wire[3:0] high; assign high = tmp[7:4]; //虎啸的Verilog high = tmp<<4; //喵喵的C语言 硬件就是硬件,可以随意飞线,你甚至可以把tmp里面的bit6,bit3,bit1,bit7组成一个Nibble 不知道Nibble不要紧,它就是Half Byte的 assign high = {tmp[6],tmp[3],tmp[1],tmp[7]}; //虎啸的Verilog high = (tmp & 0x40) ? 0x08 : 0; //喵喵的C语言 high |= (tmp & 0x08) ? 0x04 : 0; //喵喵的C语言 high |= (tmp & 0x02) ? 0x02 : 0;; //喵喵的C语言 high |= (tmp & 0x80) ? 0x01 : 0;; //喵喵的C语言 这下知道喵喵跟虎啸的差距了吧,C语言,把如猫添翼?表达式都用上了,还是4行代码才表达出自己的意图。 当然,Verilog也有他的?表达式,那用上了,就真的是如虎添翼了 C语言的switch/case语句 switch(tmp) { case 1: high =1; break; case 3: high =5; break; case 5: high =2; break; case 9: high =1; break; default: high =11; } Verilog的case语句 case(tmp) 1: high =1; 2: high =5; 3: high =1; 4: high =1; default: high = 1; 发现了没,首先打字要少敲很多case了吧,case已经升级当主管了,小罗罗们直接跟这冒号就可以了。 细心的文艺青年,应该发现了一个大秘密,那个四处张扬,到处留种的break居然不见了。 Verilog不需要break了,它默认每个语句自动break,这时有人又担心,那我有2个语句咋办? 问得好,又有2个keyword要粉末登场了,begin/end 学会Pascal语言的朋友,肯定认得他俩,在C语言中被{和}所替代 Verilog本来也想用{和}的,毕竟写代码是要敲键盘的,能少敲谁也不愿意多敲。 可惜{和}被用掉了,用在了哪里?到上面找去, case(tmp) 1,2,3,4: begin high =1; high1 =3; high8 =9; end default: high = 1; 这个排版,是不是又点更像C语言的风格了 你也许已经看到了,C语言中多个case项公用一段代码的情况,在Verilog里面也有,而且更TMD的简洁 if/else语句就不讲了,这方面猫和老虎太像了,照猫画虎就八九不离十了。 好了,下面有个用得非常多的always语句 always(tmp1, tmp2) begin out1 = tmp1 ^ tmp2; out2 = tmp1 + tmp2; end 又是喵喵和虎啸的区别了,C语言的while也是always的意思,但while不如always忠诚。 C的while语句,是随着CPU的时钟节奏,一步一步的走,然后Loop循环回来,直到永远或者有人叫她出台(霸王的break或者while条件不满足了) Verilog的always可就忠诚多了,只要tmp1和tmp2中的任何一个变动,out1和out2都跟着动,clk来不来都会工作,这就是主动和被动的差别 2、组合逻辑设计 2、组合逻辑设计 组合逻辑是神马? 所谓组合逻辑就是,一堆输入注定了一个(或多个)输出,明天你再送同样的这一堆输入,可以得到跟今天完全相同的结果。 或者说,输出的值跟先前任何状态没有一毛钱的关系,只跟当前的输入有关系。 来个最简单的: assign out = in1 & in2; 这是个与门,out的值只跟in1和in2有关。 这时候?语句很有作用了,比如 assign out = sel ? in1 : in2; 这是一个二选一的选择器。 你肯定觉得二选一太简单了,来个4选一看看 assign out = (sel==2'b00) ? in0 : (sel==2'b01) ? in1 : (sel==2'b10) ? in2 : (sel==2'b11) ? in3; 不知道你感觉如何,反正我从第二个问号开始花眼,咋办? 首先一个办法,?表达式的复合,我们可以用括号来区分层次,但仍然感觉很不直观。 想到了什么,C语言的switch/case,OK,我们就用Verilog的case语句写一下 always @(sel or in0 or in1 or in2) case(sel) 2'b00: out = in0; 2'b01: out = in0; 2'b10: out = in0; 2'b11: out = in0; endcase OK,我们看看这个case语句是什么?没错,他就是那个真值表的美丽化身。 怎么,你还想到了卡诺图辅助逻辑表达式化简,当年读书时候,整天对着田字格横看竖看的,很神奇的。 现在我们有Verilog语言了,化简的事情交给综合器好了。 啥,你不知道综合器是啥?C语言的C编译器,你知道吧,他俩基本是一个地位的。 always的小老鼠后面的括号里不是有很多“变量”吗,那叫敏感信号。 只要敏感信号任何一个有变动,下面的语句就执行一次,其实这是个形象的说法,几乎是专门给C语言工程师定制的一个解释。 说到逻辑电路,我们找个非典型的用途吧-----地址译码 CPU就说是8051吧,其实其他的也一样 我们有个外设,内部有8个寄存器,我们打算把它安排到地址0xF080~0xF0087,设计它的片选信号 比较笨的方法: assign sel = (addr==16'hF080) | (addr==16'hF081) | ............... 我就不敲完了,8个啊,复制完了,还要一个个修改,还要校对,够苦B的了 always @(addr) case(addr) 16'hF080, 16'hF081, 16'hF082, 16'hF083, 16'hF084, 16'hF085, 16'hF086, 16'hF087: sel = 1; default: sel = 0; endcase 这个case语句简单多了吧,16'hF080的含义就是此数据有16个bit,h表示后面是十六进制表示的。 F080你要不知道什么意思,估计你们学校是体育老师讲计算机基础课,当然也可能是政治老师。 其实,case里面连续写8个项,然后一个冒号,感觉也很是苦B 那我们手工分析下0xF080~0xF0087的特征,高位的3个Nibble是F08,低位Nibble是0~7 我们再用二进制的方式看看: 1111 0000 1000 0000 1111 0000 1000 0001 1111 0000 1000 0010 1111 0000 1000 0011 1111 0000 1000 0100 1111 0000 1000 0101 1111 0000 1000 0110 1111 0000 1000 0111 --------------------------------------- 1111 0000 1000 0xxx 这3个小x的含义我就不说了,这点归纳的逻辑都没有的话,您真不适合做工程师,适合做公务员。 always @(addr) casex(addr) 16'b1111_0000_1000_0xxx: sel = 1; default: sel = 0; endcase 是不是帅呆了,如果你不是太粗心的话,应该看到了这个是casex而不是case 其实帅不是目的,泡到妞才是根本。看一个assign语句就可以搞定的事,何必罗嗦这么多呢。 assign sel = (addr==16'b1111_0000_1000_0xxx); 估计你还看到了一个小东西,那下划线。 没错,他就是个摆设,增加可读性的摆设,去掉也可以,不过我舍不得去掉。 C语言里如果要判断这个sel信号,一般用bit mask的方法 sel = ((addr&0xFFF8)==0xF0808); 看来照猫画虎,这招也可以用在这里,虽然代码稍微差异,思路近似。 关于case语句的一个注意事项,就是所谓的full-case,这个很重要 always @(addr) casex(addr) 16'b1111_0000_1000_0xxx: sel = 1; endcase 那么,当地址不落在我们指定范围内的时候,没有语句来处理。 没来命令,就是要原地驻军。 其实相当于, always @(addr) casex(addr) 16'b1111_0000_1000_0xxx: sel = 1; default: sel = sel; endcase 天啊,sel=sel,这就是传说中的意外的latch,这跟我们的意图不一致 即使逻辑上可以完成指定功能,他也额外用掉了一个寄存器。 记住,用case语句的时候一定要full-case处理,除非你设计的就是寄存器 寄存器的latch是时序逻辑里面的内容 |
|
相关推荐
|
|
4、阻塞和非阻塞
话说大禹治水,因为他老爹治水失败被咔咔了,他不得已去顶缸。 他也琢磨啊,其父也不是等闲之辈,没搞定,说明必须得换个法子,否则自己也得被大哥给嗝屁了。 大禹父子治水,分别用的是阻塞和非阻塞的方法,下面我们就扯一下逻辑电路中的阻塞和非阻塞。 通常所说的阻塞和非阻塞,指的是always块中的语句。 always语句中有时序逻辑,也有组合逻辑。前者用非阻塞,后者用阻塞。 其实“阻塞”这个术语,也是专门给软件出身的电工看的,硬电工才懒得管你阻不阻的呢。 reg[7:0] in1; reg[7:0] out; always @ (posedge clk) begin in1 <= in1+8'h01; out <= in1; end endmodule 先从容易的下刀,我们先看看这个非阻塞的语句,它很简单,就是in1的自身完成一个自加一 注意这个“<=”,是不是又想起了C语言里用来搞指针的“->”,不过真的没有一毛钱的关系。 in1拿到的是clk上升沿之前的“in1”值再加1,跟clk上升沿之后的in1没有关系了。 正如,已毕业的小明的时候对还在读大四GF小芳承诺说,哥等你大学毕业就讨你做LP了。 时光如箭,日月如梭,时间如白驹过隙,学校7月份小芳走出了象牙塔的大学校门。 小明履行承诺,娶小芳为妻。话说,无巧不成书。 小芳大学毕业了,但大三的同学也该升级读大四了,正好里面也有个女娃的名字也叫小芳。 抢答开始,问:小明,娶的是哪个小芳? 答案是,去年读大四今年毕业的那个小芳,而不是去年大三今年大四的那个小芳。 您感觉拗口吗,反正我有点绕口令的感觉了。 非阻塞操作也是这个效果,你娶的是毕业(clk上升沿)之前的那个大四的小芳。 我们知道硬件是并行执行的,所以,上面的代码,这么写,效果一样。 in1 <= in1+8'h01; //老小芳毕业,新小芳升级大四 out <= in1; //小明娶老婆 但,如果把非阻塞改为阻塞的,那小明娶的老婆,到底是谁?且看分析。 in1 <= in1+8'h01; //老小芳毕业,新小芳升级大四 out <= in1; //小明娶老婆 所谓阻塞,就是一步一步来,就是写软件的那个思路,小明顺利娶他昔日的恋人为妻。 我们要调整语句顺序了,再看看小明的执行结果咋样 in1 = in1+8'h01; //老小芳毕业,新小芳升级大四 out = in1; //小明娶老婆 要顺序执行的哦,先完成“老小芳毕业,新小芳升级大四”,然后“小明娶老婆”。 小明娶到了刚刚大三升大四的小芳,你完全可以认定,小明是一个喜新厌旧的文艺青年。 而如果,做下语句的调整,就像下面这样 out = in1; //小明娶老婆 in1 = in1+8'h01; //老小芳毕业,新小芳升级大四 小明喜新厌旧的企图,被强大的阻塞语句,给堵回去了。 一般用阻塞语句来实现assign语句描述困难的组合逻辑,一般情况下代码块会比较小。 非阻塞的一般是用于时序逻辑,时序逻辑往往比较复杂,有时候复杂得有些变态。 如果月老执行Verilog语句的时候,一不小心,小明就娶错了老婆。 阻塞,有个地方用起来很方便,也许你也猜到了,testbench tb代码本身,就不被用来综合到电路,所以,可以大胆使用阻塞语句 #10 rst = 1; #10 clk = 0; #10 clk = 1; #10 clk = 0; #10 clk = 1; #10 rst = 0; repeat(100) begin #10 clk = 0; #10 clk = 1; end 这是一段,模拟单片机复位释放以及振荡器启动的激励。 反正是顺序执行的,就拿这写软件的脑袋来理解就够了,估计软电工都喜欢。 |
|
|
|
|
|
5、同步和异步设计
前面已有铺垫,同步就是与时钟同步。 同步就是走正步,一二一,该迈哪个脚就迈那个脚,跑的快的要等着跑的慢的。 异步就是搞赛跑,各显神通,尽最大力量去跑,谁跑得快,谁拿奖牌。 我们举个例子,SPI接口,他是一个低成本的单端的高速串行数据传输协议。 四个信号,nCS、SCK、MISO、MOSI 下面是一个Slave SPI的接口部分,简化了, model mySPI(input nCS, input SCK, input MOSI, output MISO); reg[3:0] bitcnt; reg[7:0] shift_in; //写入 reg[7:0] shift_out; //读出 reg[7:0] data_wt; reg[7:0] data_rd; always @(posdge SCK) if(nCS) bitcnt <= 0; else begin if(bit_cnt!=4'h7) begin bitcnt <= bitcnt+4'h1; shift_in <= {shift_in[6:0], MOSI}; shift_out <= {shift_out[6:0], 0}; end else begin bitcnt <= 4'h0; ........... data_wt <= deshift_in; shift_out <= data_rd; end end endmodule 这段代码是同步的还是异步的? 其实,他远看是同步的,近看是异步的,仔细一看还是同步的。 大致一看,丫的还配时钟呢,按钟点走步,八成是同步的。 然后一想,不对啊,SPI的SCK是Master提供的,跟自家的全局时钟没有必然关系啊,异步的。 思索一阵,假如俺系统全局时钟都靠SCK不就是同步的了吗? 实际情况如何呢? 举个例子,SPI Flash,比如25系列,其实就是同步的,SCK就是全局时钟。 比如某ARM core的MCU内置SPI模块,为了简化问题,我们只谈Slave的情况,问题就来了。 ARM MCU肯定有其自家的时钟,SPI的Master又送来一个时钟,咋办呢? 当你发愁的时候,你该庆幸自己遇到了几乎所有入门的人都必须解决的问题----多时钟系统。 多时钟,各自都是同步,放在一起就是异步。 正如两队人马,都在走正步,**走得快,国军走的更快,他们各自都是同步的,扯蛋到一块就是异步。 咋办呢? 丛林法则要起作用了,单一时钟同步化处理,势力小的听势力大的人安排。 model mySPI(input clk, input nCS, input SCK, input MOSI, output MISO); always @(clk) ................................... endmodule clk是自家的全局CLK信号,对方的SCK信号,只在自家CLK触发才看一眼对方的各个信号,包括SCK信号。 这就是强者的统战部,你家的可汗(SCK),见到我家皇帝(clk),也是称臣子。 当然,这个处理方法是有前提的,就是clk的频率要远远高于SCK信号。 所谓远远高于,就是即使我clk的上升沿,瞄你一眼,就不会漏掉你所有的表现。 根据XXXOOO定律,要达到采样不丢信息,尼玛的频率至少是人家的2倍,实际应用中一般保证4倍,或更高。 就好比有4个小弟的人,叫只有一个小弟的人,对自己称臣,听话大家就,不听就干你。 前面有朋友谈到了复位信号的同步化处理,最简单的就是复位和释放都同步处理,我前面几个帖子有用到。 复位,是什么,是杀头,复位释放是什么,是重新投胎。 你跟情敌斗志斗勇的时候,想到了制胜的一招时候,你觉得是立马去执行,还是等下一次例行见面时再执行。 当然是立马执行了,这不就是异步把情敌给复位了嘛。 你击败情敌之后,要对全班同学宣布的你胜利,是每天早会宣布呢,还是里面召集同学宣布呢? 此时大势已定,当然是按CLK四平八稳来得妥当,大家会认为你是一个做事不鲁莽,有步骤的,电工十佳青年。 所以,我们称之为,异步复位,同步释放。 always @ (posedge clk or negedge nRST) if (!nRST) 击败情敌; else 把击败情敌的战果宣布; 再举个例子,Memory的访问,为了简化,我们做个ROM,这样只有读的一种情况,适合理解记忆 model memory8(input[7:0] addr, output[7:0] dat) reg[7:0] rom[255:0]; assign dat = rom[addr]; endmodule model memory8(input clk, input[7:0] addr, output[7:0] dat) reg[7:0] rom[255:0]; reg[7:0] outbuf; assign dat = outbuf; always @(posedge clk) outbuf <= rom[addr]; endmodule 简单的是异步的,只要地址变化了,输出立马就表现。 我们实际使用的27系列的EPROM,61系列的异步SRAM,都是这样的,始终把OE信号置于有效即可。 复杂的就是同步的了,我不管你地址变了没,在CLK上升沿到来之前,我懒的理你。 异步的SRAM,刚查了下,也有61系列的; 看来不能以前缀来瞎子摸象了。 一般实际用的时候,异步SRAM肯定比同步的好用,同步的老要CLK,你是IO口模拟呢,还是怎么输出呢? 但同步的Memory也不是吃素的,吃的多必然长得壮,同步可以提高更高的传输速度。 该往回说了,为何同步电路可以提高更高的速度呢? 异步,就是赛跑,速度以跑得慢的人为准,团队精神嘛,这不能平均,只能搞木桶原理。 同步,就是大家按一个节奏,你慢的话,就用2个节奏完成,但必须按节奏。 这样负责协调的那个人,就是喊一二一的那个人(clk),可以把握全局的节奏来达到速度最大化。 所以一般FPGA里面都有全局时钟,强大的扇出能力,最小的传输延迟,因为他是老大,好资源他先挑的。 他就好比系统的原子铯钟,他很精确,我们每天跟他对一下时间,我们自家的表,就不会产生误差积累。 异步,2个队伍,各自有自家的老大,比如一个是地址线,一个是数据先,某个时刻,主控一抓。 可能地址线跑得快,数据线跑的慢,就会出现数据错位的情况,数字电路上叫竞争。 你作为运筹帷幄的总统,不能断定2个队伍能同事到达,你仍然用这个方法,你就是在冒险。 作为设计而言,应尽量避免竞争冒险。 如果系统简单,工期紧,速度要求低,逻辑简单,用异步的。 如果系统庞大,速度要求越高越好,逻辑交叉错节,坚决用同步的。 同步设计就是个工具,让智商90的人可以干智商120的人的工作。 Asynchronous 和 Synchronous 这两个单词我老是分不清 后来学软件学逻辑电路,给记住了,带A的要要冒尖的,是异步的 |
|
|
|
|
|
7、设计一个只有4条指令的CPU 我们要设计一个简单的CPU 既然做CPU,我们要做流水线的,要简单,做2级流水线就够了。 为了实例的简单,我们选择设计一个8bit的MCU的内核 仍然我们要简单,所以选择RISC的内核,类似PIC的结构 还是为了要简化,我们只支持4条指令 继续为了要简化,我们不考虑Status寄存器 有人会问,只有4条指令,你还加减法都有,有一个不就可以了。 这也是我有意的,你想,假设ALU只能做加法,你不觉得ALU这个名称太不名副其实了吗。 mov A,#35H 把立即数mov到A寄存器 add A,#42H (A) + 12 -> A sub A,#62H (A) - 12 -> A JMP imd 跳转到某地址 我们先给他们做机器编码,我们用16bit宽度的指令集编码 0x0035 00是MOV的OP code 0x0142 01是ADD的OP Code 0x0265 02是SUB的OP code 0x8000 80是JMP的OP CODE 我们继续看,指令集,用Verilog的方式来描述 16'b0000_0000_????_???? MOV 16'b0000_0001_????_???? ADD 16'b0000_0010_????_???? SUB 16'b1???_????_????_???? JMP 我们可以看到JMP的跳转地址范围是15个bit地址,也就是32K地址范围 有人说ALU很重要,好,我们就先来看ALU的组成,因为只有加减2种情况,所有ALU的OP代码只用1个bit表示 op为1的时候,做加法,为0的时候做减法。 module alu(input op, input[7:0] in1, input[7:0] in2, output[7:0] out) assign out = op ? (in1+in2) : (in1-in2); 看到上面的代码,估计不少人大跌眼镜,莫非传说中的alu就这么简陋。 没错,如果你只要做加法和减法,而且不考虑进位和溢出的ALU,就是这么easy的。 好了,cpu的运转过程,包括加载指令,解码指令,执行指令,大家都知道。 我们还要使用流水线技术,虽然这里不用也许更简单,但我们的目标是学习。 一 | 加载指令1 | 加载指令2 | 加载指令3 | .......... ----------+---------------+----------------+----------------+----------------- 二 | | 解码1 执行1 | 解码2 执行2 | 解码3 执行3 我们可以看到加载和解码和执行,并没有在一个周期中完成,而是分开了 在运行第二条指令的时候,CPU正在加载第三条指令,一心二用,事事不耽搁。 clkcnt; always @(posdge clk) if(nCS) clkcnt <= 0; instr <= 0; else instr <= rom_dat_out; 下面是CPU的解码和执行过程 always @(posdge clk) if(!nCS) casex(instr) 16'b0000_0000_????_????: //MOV begin acc <= instr[7:0]; pc <= pc + 16'h0001; end 16'b0000_0001_????_????: //ADD begin acc <= aluout; pc <= pc + 16'h0001; end 16'b0000_0010_????_????: //SUB begin acc <= aluout; pc <= pc + 16'h0001; end 16'b1???_????_????_????: //JMP begin pc <= instr[14:0]; pc <= pc + 16'h0001; end 下面完成CPU核心和ALU之间的连线 assign aluop = (instr[15:8]==8'h01); assign aluin1 = acc; assign aluin2 = instr[7:0]; alu alu1(aluop, aluin1, aluin2, aluout); 有人说,只看到执行指令,没看到解码指令的过程,有木有啊?当然有 16'b0000_0000_????_???? MOV 16'b0000_0001_????_???? ADD 16'b0000_0010_????_???? SUB 16'b1???_????_????_???? JMP 这几个逐个的case不就是在做解码?只是没有独立的解码步骤而已,因为太简单了嘛。 还有个地方,我故意做了遗漏,就是JMP指令的处理。 所谓流水线,就是取指和执行是同时的,但JMP的到来,带来了异常。 正常都是PC加一,所以取指其实一直在取下一条指令,而JMP的目标是不确定的,所以取的指令就不对了 我们一般称之为预测失败,然后继续取JMP目标地址的指令,但执行部分,会有一个空的指令周期。 从CPU的用户角度看,就是JMP指令要使用2个指令周期。 CPU的设计基本到此结束了。 |
|
|
|
|
|
1476 浏览 1 评论
1266 浏览 0 评论
矩阵4x4个按键,如何把识别结果按编号01-16(十进制)显示在两个七段数码管上?
1471 浏览 0 评论
920 浏览 0 评论
2272 浏览 0 评论
1436 浏览 35 评论
5626 浏览 113 评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-11-23 20:10 , Processed in 0.747816 second(s), Total 77, Slave 58 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号