关闭

一周入门FPGA

1376人阅读 评论(0) 收藏 举报
分类:

原文:http://www.amobbs.com/thread-5476115-1-1.html
这是一个大任务,但我打算只是引门外汉入门,大约7个帖子来完成,一周入门FPGA。

1、假设读者对硬件数字电路熟悉,比如自己可以用74芯片做跑马灯
2、C语言都比较熟悉,因为下面用的Verilog语言就跟它很类似,暂时规避晦涩的VHDL

我打算分几个部分
1、Verilog语法
2、组合逻辑设计
3、时序逻辑设计
4、阻塞和非阻塞
5、同步和异步设计
6、有限状态机
7、设计一个只有4条指令的CPU


如果你不需要自己设计,只需要看懂一些代码,理解Team中别人的意思。
这个帖子就是教你一周学会XXOO的

如果你已经入门而登堂入室了,这个帖子真的不应该看了,否则会退步的。


禁止转载,最讨厌那些用别人帖子,装衬一个克隆论坛的了。
没那人气的,就别折腾论坛,做论坛也要下本钱才行。
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来不来都会工作,这就是主动和被动的差别


好了,看到这里,你应该知道,文艺青年和苦B青年其实也有很多共同之处的,如果你认识文艺青年,那跟苦B青年交朋友也不难了。
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是时序逻辑里面的内容
欲知后市如何,请听下回分解。
3、时序逻辑设计

所谓时序逻辑,简而言之,就是CLK驱动,不来时钟不干活,同时能自我保持。
最简单的例子,跑马灯

model led_led(input rst, input clk, output out0, output out1, output out2, output out3);
reg ary[3:0];

assign out0 = ary[0];
assign out1 = ary[1];
assign out2 = ary[2];
assign out3 = ary[3];
always @(clk)
begin
if(rst)
ary <= 4'h00;
else
begin
ary[3] <= ary[2];
ary[2] <= ary[1];
ary[1] <= ary[0];
ary[0] <= ary[3];
end
end
endmodele

有人会说那个ary的“中间变量”是不是可以省掉,还真省不了,因为它不仅仅是“临时变量”。
它是个锁存器,寄存器-----------数据保持是它的特点。而wire信号是无法保持的。

当然那4个信号单独赋值,挺形象,但是不够紧凑,C语言可以用ary = ary<<1来表示,那Verilog就如下:
model led_led(input rst, input clk, output out[3:0]);
reg ary[3:0];
assign out = ary;
always @(clk)
begin
if(rst)
ary <= 4'h00;
else
ary <= {ary[2:0],ary[3]};
end

跑马灯,用组合逻辑做不出来,是因为它的输出不仅仅跟输入有关,也跟前一个状态有关。

看到时序逻辑,很多人发现了无比强大的clk信号,到处都有他的影子,他才是真正的到处留种的风流才子。

“输出不仅仅跟输入有关,也跟前一个状态有关”,这句话,应该很熟悉吧。 -----没错,就是状态机。
后面有个专门贴讲FSM,这是一个非常有效的解决问题的工具------智商99的人可以做到智商120的设计。
如果天才用了,那肯定要更加newbility了。

下面看个十进制的计数器,是前几天给一个初学者的sample,但愿这小哥真的去看懂了,而不是搞完毕设就完蛋鸟。

module bcd_counter(rst, clk, qout);
input rst;
input clk;
output[7:0] qout;

reg [3:0] low;
reg [3:0] high;

assign qout ={high,low};

always @(posdge clk)
if(rst)
begin
low <= 4'h0;
high <= 4'h0;
end
else
begin
case(low)
0,1,2,3,4,5,6,7,8:
low <= low+4'h1;
9:
begin
low <= 4'h0;
case(high)
0,1,2,3,4,5,6,7,8:
high <= high+4'h1;
9:
high <= 0;
endcase
end
endcase
end
end module

这里面有2个十进制数据,分别占用一个nibble---4个bit,其实就是BCD码。
使用了case语句来完成,而且两个case还套起来用了,跟C语言一个样,照猫画虎即可。

细心的文艺青年,应该发现了,module的定义貌似跟以前不一样了。
不用貌似,就是不一样了,这是2种风格,其实苦B的C语言也有这个风格的。
bool fun1(X,Y)
int X, Y;
{
}
C语言的古老风格,现在很少人用了,毕竟多敲键盘,不代表多干活。
那时候C语言还没有国际标准,此K&R C是以2个工程师的名字首字母命名的。
后来才有ANSI C和ISO C(C89/C90/C99),这巨人的肩膀好高啊,搞技术就不能有恐高症。

Verilog亦是如此,建议使用输入输出定义在括号里面的风格,至少要少码好几个字母不是。


不知道有没有人注意到,上面几个帖子用的都是always @(posdge clk),敏感信号列表中不见rst的身影。
下面的判断rst的信号,决定复位还是干活,这叫做同步复位。

同步复位,你要理解成rst一有效就复位就错了。同步的含义,与CLK同步,类似于与裆中央保持一致。

异步复位,是立即的。异步复位,同步释放。这个话题就比较远了,后面有个帖子,会专门说这个话题。


后面的会稍微有些难度,但我会为了入门的学习台阶,尽量把难的赶跑,容易的留下-----“男的赶跑,女的留下”。


预知后市如何,明天再看红盘绿盘。
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上升沿)之前的那个大四的小芳。

我们知道硬件是并行执行的,所以,上面的代码,这么写,效果一样。
out <= in1; //小明娶老婆
in1 <= in1+8'h01; //老小芳毕业,新小芳升级大四



但,如果把非阻塞改为阻塞的,那小明娶的老婆,到底是谁?且看分析。
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的要要冒尖的,是异步的

明天要讲的有限状态机,是以同步设计为基础的设计方式,然后我们就可以用90的智商做150智商的工作了。
6、有限状态机

状态机,只要C代码写过2年的人,估计无人不识君,稍微复杂的逻辑都可以借助状态机来简化问题。

为了方便,我们使用前面用过的一个例子,来说明状态机的应用,也就是说我们前面已经有意无意的用过状态机了。

我们以SPI的Slave接口,为例,来说明状态机的使用


为了简化问题
1、我们没有把信号同步到本地时钟
2、把其他信号同步到SCK
3、我们把SPI暂时按照单向来分析

下面,我们分析SPI通讯
1、nCS高电平时候,总线是空闲的
2、nCS低电平时传输数据
3、满了8个bit,凑够了一个字节,要保存当前已经收到的字节,并准备收下一个;

nCS高电平的时候,我们称之为idel态(IDEL)
接受0~7逐个bit的时候,称之为bit接受态(BIT_RECV)
收满一个字节,称之为字节转存态(BYTE_SAVE)

我们开始画状态转移图


model SlaveSPI(input nCS, input SCK, input MOSI, output MISO);

parameter IDEL = 0,
BIT_RECV = 1,
BIT_SAVE = 2;

reg[3:0] bitcnt;
reg[7:0] shift_in; //写入
reg[7:0] shift_out; //读出

reg[7:0] data;

reg[1:0] state;
reg[1:0] next_state;

always @(*)
begin
case(state)
IDEL:
if(nCS==1'b1)
next_state = IDEL;
else
next_state = BIT_RECV;
BIT_RECV:
if(nCS==1'b0)
bgein
if(bitcnt<4'h8)
next_state = BIT_RECV;
else
next_state = BYTE_SAVE;
end
else
next_state = IDEL;
BYTE_SAVE:
if(nCS==1'b0)
next_state = BIT_RECV;
else
next_state = IDEL;
defalut:
next_state = IDEL;
endcase
end


always @(posdge SCK)
if(nCS)
bitcnt=0;
else
state = next_state


always @(posdge SCK)
case(state)
BIT_RECV:
begin
bitcnt <= bitcnt+4'h1;
shift_in <= {shift_in[6:0], MOSI};
end
BYTE_SAVE:
begin
bitcnt <= 4'h0;
data <= deshift_in;
end
endcase

我用了所谓的三段式,来描述这个状态机,用了3个always语句
第一个always用来描述状态转移的条件
第二个always用来描述状态转移
第三个always用来描述状态机的输出,也就是状态机实际要干的活


与前面帖子(同步和异步设计)中的SPI代码相比,是不是冗长了很多。
冗长,并不代表着脱裤子放屁,自找麻烦。您难道没有反显,代码很容易读懂了吗?

没错,这就是空间换时间的策略,我们写更长的代码来增强可分析性。

状态机的代码撰写一般不复杂,复杂的是状态机的构建过程,这个过程中,我们要分析
实际遇到的各个情况,同时把状态机进行优化,某些状态进行合并,某些状态去掉。

有人问,为何某些状态要去掉?
这要说下FSM的来头了,有限状态机,是有限的状态机。
自然界实际的状态机,往往起状态的数量,是非常大的,直接建模使用简直是劳民伤财。
而且,现实的往往是无限的状态机,这根本无法用于工程实现,所以有限状态机就横空出世了。

正如,软件算法中的DAG(Directed Acyclic Graph)有向无环图,比纯粹的图有实用价值。
二叉树,比多叉树,也更容易实现。

越说,软件和FPGA越近了。可谓是天下大势,分久必合,合久必分。

好了,这篇比较仓促,周六晚上专门来办公室写这个帖子。

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的设计基本到此结束了。
关于FPGA,我也没有能力做太深入的讲解了,否则误人子弟,岂不是背离了我的目标。

我的blog基本上也是很少更新,你点进去看也没多少货。
有个哥们说得好,MP的博客,写得比别人的微薄还短小。

至此,这个帖子连载终于完成了,总算没有辜负自己和一些网友。

1
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:179102次
    • 积分:2470
    • 等级:
    • 排名:第15494名
    • 原创:29篇
    • 转载:215篇
    • 译文:7篇
    • 评论:9条
    文章分类
    最新评论