Skip to content

Latest commit

 

History

History
356 lines (258 loc) · 9.31 KB

难点解决.md

File metadata and controls

356 lines (258 loc) · 9.31 KB

难点和解决

Logisim

如何搭建有限状态机

  1. 规划状态,确定是Mealy型还是Moore型,这决定了状态数多一个还是少一个。
  2. 画出状态转移图
  3. 规划输出端口是Moore型还是Mealy型
  4. 确定端口的延迟,如果带上寄存器,可以延迟1个周期

Moore和Mealy型状态机

以字符串匹配“abc”为例,
如果是Moore型状态机,一共4个状态,可以记为S0 -> Sa -> Sb -> Sc
如果是Mealy型状态机,一共3个状态,可以记为S0 -> Sa -> Sb
Moore型会多一个Done(匹配完成)的状态,就是Sc,这个状态直接和输出关联;
Mealy型在Sb状态时,input == c ,output 直接等于1,
这就叫做:“当输入为c时,输出直接为1”
当一个时钟过来后,c被“打”进去,此时状态反而回到原点。

Logisim 的同步和异步复位

首先,异步复位是简单的
Logisim异步复位到0
当CLR(clear)为1时,reg直接变成0

正确的同步复位如下:
Logisim同步复位
使用了MUX和一个0常量

错误的同步复位:
Logisim错误的同步复位
当clk为1时,要是CLR来个小抖动,直接复位,和要求“在clk上升沿时复位不同” :(

异步复位到非0状态:
这个地方就有trick了,直接贴上答案:
异步复位到非0状态
这时的register存储的不一定是真正的state,下一个state计算好后,等着时钟周期一来,更新register

Verilog

如何设计一个状态机

和Logisim不同,Verilog配合ISE,允许我们设计更加复杂的状态机。
我们设计时,要注意下列点:

  1. 设计好状态转移。当我们使用Verilog时, 设计的状态机比Logisim复杂更多,通过analyze circuit来构建状态转移逻辑太麻烦,
    所以要我们先设计好状态

  2. 设计好内置的reg,这些reg会在一定的条件下被触发,
    而往往result就是这些reg的组合逻辑输出。

同步复位和异步复位的写法

这个和Logisim正好反过来,异步复位写的麻烦一点
rst_n 指的是 reset when negative,即低有效的复位
同步复位,which is easy

always @(posedge clk)  
    if(!rst_n) begin  
        RESET;  
    end  
    else begin  
        show time;  
    end  

异步复位

这里以reset when negative为例
posedge clk后面接着的是 negedge rst_n
如果reset when positive,接着的是posedge rst

always @(posedge clk or negedge rst_n)
    if(!rst_n) begin
        RESET;
    end
    else begin
        show time;
    end

函数用法

function <返回值的类型或范围>(函数名);
    <端口说明语句>          // input XXX
    <变量类型说明语句>      // reg YYY
    ......
    begin
        <语句>
        ......
        函数名 = ZZZ;       // 函数名就相当于输出变量;
    end
endfunction

for循环

for循环的写法

integer i;
for (i = 0; i < MAX; i = i + 1) begin
    // do what u want
end

这里,我并不是要强调for循环的语法规则,
而是想说两个重点:

  1. for循环一般在组合逻辑中作为一种函数出现,
    和always(*)相伴,如果不放在always块中,而是直接出现,
    会报错。

  2. for循环一般用于计数,而计数时,会有中间变量,
    那么每次启动for循环用于计数时,
    要保证中间变量被清零了。
    P2上有同学栽了跟头,我P4直接挂在这上面。

当然,可以用函数来代替for循环,
不过,经我测试,函数的for循环,
中间变量在loop中,也得清零,否则会累加。

附上一个正常的计数代码段,
功能是判断RegRead1这个wire里“1”的个数是不是奇数:

wire odd;   // is odd??
reg[31:0] num;   // how many "1" in RegRead1??
initial begin
    num = 0;
end
integer i;
always@(*) begin
    num = 0;    
    // loop start, intermediate variable set to 0
    for(i = 0; i < 32; i = i + 1) begin
        if(RegRead1[i] == 1) begin
            num = num + 1;
        end
    end
end
assign odd = num[0]

MIPS

.data的使用

我们平常用什么?:
.space
专门用来分配int型的变量,后面的单位是字节(byte)
比如:

array:  .space 40

40个字节,足够存10个integer了。

.asciiz 专门用来存一个字符串,这个和python的想法类似,没有char类型的变量
如:

str:    .asciiz "Hello World"

把字符串的声明放在最后。
因为前面放了array,常常用到lw、sw来操作array,array的地址必须是4的倍数

if-else语句实现

具体的流程是这样的:if-then-else
实际用label实现是这样的:if-(else->end)-(then->end)
这和C语言的不同之处在于:
C是条件判断为False时,跳转到else
MIPS是条件为True时跳转到then,else直接接在判断语句后面

if:
    beq $, $, then
else:
    do sth
    j   if_end
then:
    do sth
    j   if_end
if_end:

值得一提的是,实现跳转,直接由beq、bgt,而不必使用中间变量,
千万不要忘了,最后的if_end

循环的实现


for(i = 0; i < n; i++>){} 为例

.text
    li      $t0, 0
Loop:
    beq     $t0, $a0, Loop_end
    do sth
    add     $t0, $t0, 1
    j       Loop
Loop_end:
    done

循环是从load immediate开始,再以Loop_end结束
循环不是从Loop开始的,这是认知的一个误区

一定不要忘记了:

  1. 整个循环以 li $t0, 0开始的

  2. do sth 后,一定要记得i++, 然后再jump到开头的Loop去

  3. 同样地,判断和跳转直接用扩展指令

一维数组的使用

  1. 使用了.space声明了array。单位是byte

  2. store word和load word

  3. store byte和load byte

用了数组,肯定存很多数,然后再取出来
展示一下如何存integer:

Store:
    beq     $t0, $s0, Store_end
    li      $v0, 5
    syscall
    sll     $t1, $t0, 2         # t1 = t0*4
    sw      $v0, array($t1)
    add     $t0, $t0, 1
    j       Store

在C语言中一个i,就能完成 int 或者char 的存取,
在MIPS中
integer要中间变量i*4,
char可以直接用i来存取
因为MIPS最小的存储单元是byte

多重循环的实现

多重循环,很容易让人看不清结构 现在以两个嵌套的for循环为例

for(i=0; i<n; i++){
    for(j=0; j<n; j++){
        Do Something
    }
}

.text


    li      $t0, 0
Loop_i:
    beq     $t0, $s0, Loop_i_end

    li      $t1, 0
Loop_j:
    beq     $t1, $s0, Loop_j_end
    Do Something
    add     $t1, 1
    j       Loop_j
Loop_j_end:

    add     $t0, $t0, 1
    j       Loop_i
Loop_i_end:
    End

还是要强调,循环不是从Loop开始, 而是从load immediate开始

函数的实现

寄存器的分配:

  1. 传递参数的寄存器:$a0 - $a3

  2. 传递返回值的寄存器:$v0 - $v1

  3. 返回地址的寄存器,保存调用者的地址

调用函数的需求

将程序员变量 $s 存到 $a

$a 传入,返回的值存储在 $v0 里。 调用的前后,$s 的值不变。
所以:

  1. 调用前,自己保留好 $s

  2. 调用后,把改变好的 $a 请回来。

函数内部实现:

  1. 通过 $a 计算好返回值;

  2. 把返回值存到 $v0

递归的实现

  1. 不建议用 $v0$a0 ,因为可能会有输入和输出。

  2. store word 和 load word 不要写反了

最关键的地方,在于子函数。
子函数就是函数内部再进入一级的函数

为了把C翻译成汇编,首要的就是,写出一个“清晰”的C代码。

什么是“清晰”的C代码?
我们不需要关心C代码如何实现了我们的需求,
只要关心C代码翻译成汇编时是否易懂。

Factorial-1:

int fac(n):
{
    if(n == 1){
        return 1;
    }else{
        return n*fac(n-1)
    }
}

或许这是一个明了的C代码,
但对于翻译汇编的人而言,这并不是一个“清晰”的C代码

Factorial-2:

int fac(n){
    if(n == 1){
        return 1;
    }else{
        t = n;          // n : $a
                        // this step : store $a
        n = fac(n-1);   // after a jal-ra, n will change
        n = n * t;      // what we need return is orignal n muti fac(n)
        return n;
    }
}

递归总是有终止条件,终止条件翻译成汇编比较简单

我们为什么很难把递归的C和汇编联系在一起?
在我看来,是因为:
递归在汇编中,“传值”
递归在C语言中,“传参”

本来是作为参数的 $a ,最后成为了函数返回值的容器
这样的“弊端”是,传入递归的参数个数不能超过寄存器个数 而在C语言中,从栈的角度考虑,参数传进去,返回来的值和参数无关

如果使用 $v 来保存最后的返回值,那么 $a 也还是会被改变

Ref

可以在“推荐阅读.md”中获取相应资料对应的互联网链接