P0-logisim

CO-P0-logisim ​ 写在前面:10.9晚上机有三道题目,笔者只侥幸通过了其中两道,记录一下思路,分享一下拙见QAQ,如有错误还请指正!(os:上机还是很辛苦的……) 1.找最小 1.思路探究 ​ 题目的大意为,输入五个八位二进制数字,完成电路,输出没有出现过的最小正整数,例如,输入为:0,3,2,1,7,那么最小的没有出现过的正整数为4. ​ 题意分析:如果能够想到将输入转换为独热码就可以比较直观地理解问题!我们考虑:这个所求的最小正整数最大是多少?可以想到,找最小正整数的过程类似于找一个“空隙”,如果我们将五位输入按从小到大顺序排好,记为a[n],那么最小正整数会出现的条件: 1 a[n-1] + 1 < a[n] ​ 我们可以发现:最小正整数最大的时候即为五个输入为1,2,3,4,5;此时最小正整数大小为6.故我们可以知道最小正整数的出现范围为16。这一点对我们的解题过程很关键,因为这一认知告诉我们:无论输入如何,我们只需要去找从16之间没有出现过的最小数即为我们所求的最小正整数。 ​ 然后:我们怎么知道数字有没有出现过呢?常规的二进制编码可能较难实现,但是选择独热码是一个比较直观的选择,将每个输入转换为独热码,最后或起来…….到这里思路已经明晰,实现电路吧! 2.电路设计 1.转换为独热码电路 one-hot encoder ​ 这里做出判断,大于6就当作0处理,需要注意的是,八位独热码中0也被编码,即为图中所示,因为要求是正整数,所以无论是否输入0,第0位应该被默认占位,这一点在主电路中也有处理。 2.main ​ 在主电路的处理中,我们添加一个“输入0”,将第0位占位,保证是正整数。 2.回字迷宫 ​ 从迷宫中的1位置出发,绕回字迷宫行走,00向北,01向南,10向西,11向东,如果当前输入的方向有位置,就输出进入位置的编号,若没有位置,就输出当前位置编号,使用mealy状态机实现,注意1位置的编号为1。 ​ 这个题目比较简单,无脑的把状态逻辑和输出逻辑分开列真值表就可以解决……题目里一共八个状态,建议还是使用四位编码吧,从0001开始编码,这样可以在输出逻辑时好看一些,笔者采用三位编码,导致输出时的数字要比状态数字大1,有点费心神。 ​ 打表技巧:将输入利用splitter分解成1位输入到模块中,这样在模块中便于进行analyze功能和debug……毕竟将多位传输进模块,在打完表之后要进行位拼接,这个过程走线比较逆天,另外如果逻辑错了不好进行debug,需要将拼接好的位数再展开……. 3.十六进制匹配 ​ 此题笔者是通过打真值表实现,比较麻烦,下面附上状态转移图 ​ 课程组提供的标准解法中利用三个寄存器存储最近输入的状态,这种做法比较巧妙,引用讨论区中助教的回答

October 9, 2023 · 1 min · sudo

P0课下提交

P0课下提交 ​ 本次P0课下提交部分全部为logisim有关内容,五个电路题目我没有一题是一遍过的QAQ,在做每个题目的过程中或多或少都会发现一些疏漏点,本篇笔记的初衷是记录一下做题思路(毕竟.circ文件不支持添加笔记)以及在做题过程中遇到的一些坑点。 1.CRC校验码的生成 名称 方向 描述 A[7 : 0] I 8位原数据帧 B[3 : 0] I 除数 C[10 : 0] O 8位原数据帧+3位余数 1.被除数的生成 ​ 被除数为8位原数据帧 + (除数位数-1)位0,在题目中具体下来即补全为11位被除数。这时我们发现,按照题目中的提示,按照4位除法来搭建电路,而一个11位数应当可以进行8次4位补位除法(同样参考商的位数是8位可以得到答案,不要被竖式计算过程中似乎计算除法模块小于8次迷惑,做出难以名状的事情)。 2.模二除法 ​ 在此题目中涉及到模二除法的使用,这是一个新概念,模二除法在结果上等于两位进行异或的答案,但是进行模二除法的前提是最高位需要为1(已经保证除数的最高位为1),即保证被除数与除数的最高位相同。这样我们可以知道,能进行除法的四位数为```1xxx```,这样得到的余数为```0xxx```,型为```0xxx```的数字不满足进行模二除法的条件,需要进行借位,直到最高位变为1才进行计算,这是根据题目中给出的样例得到的。 3.电路设计 ​ 通过以上分析我们知道,当前补全的四位能否进行模二除法的关键在于它的最高位,如果当前四位数字的最高位为1则进行模二除法,并传递余数到下一级,如果当前最高位为0则将数字左移一位,传递给下一级,如此传递直到最高位为1满足进行除法的条件。由于我们知道余数一定是三位且在进行下一级除法前需要拼接被除数的下一位,因此在四位除法中输出设计为3位。下面给出电路。 2.主电路搭建 ​ 主电路主要实现8级除法的连接(传入下一位),这里需要注意的是在进行输出输出时都需要进行处理,输入时在原数据后补加3位0,输出时在原数据后补加3位余数。此题用到许多的splitter。 2.实现GRF ​ 这一题主体上的功能比较单一,即对寄存器进行简单的读写操作,但是这个题目中却有很多的细节值得细细品味。而且此题的电路图过于复杂且重复,因此只展示部分电路连接。 1.一个弱智问题MUX与DMX ​ 对于MUX与DMX我要好好品味,毕竟Ppre挂掉就是因为对多路选择器的功能不够熟悉QAQ!。 1.DMX ​ DMX通常用于输入端选择输入到哪里的情况,短边连接输入信号,长边连接多个可以被选择的输入到的位置。 端口说明: 短边连接输入信号(data) 长边连接多个输入路径选择 腰上一个使能端口(include enable),一个选择输入到第几个路径的输入信号(select),这里需要注意的是一些情况下可以不选择启用使能端口,在启用使能端口时,腰上会出现两个接口点,如何去区分功能?端口上有一个灰色点的是select! 2.MUX ​ MUX通常用于选择多方数据中的一个来进行输出,长边连接多个可以进行输出的信号,短边进行输出。 端口说明: 短边进行输出(output) 长边上连接多个可供进行输出的信号 腰上一个使能端口(include enable),一个选择输出第几路数据的信号(select),同样,上面标记有灰色点的为选择信号。 3.总结 ​ DMX用于输入到哪里的选择,MUX用于输出哪个的选择。选择信号为腰上标记灰色点的端口。DMX与MUX在此题中配对放置。 使能端的勾选视情况而定。 2.DMX的three-state ​ 在我进行电路搭建的过程中,我习惯性的将three-state设置为no,在此次搭建过程中,由于需要向32个不同的寄存器中写入数据,我发现在写入数据时每当我向新的寄存器写入数据,之前写过的寄存器会被洗掉变回0,这是一个很奇怪的现象,知道我看了讨论区,才知道要将DMX的three-state设置为yes.这背后的原因是什么呢? ...

October 2, 2023 · 1 min · sudo

Pre上机logisim部分——俄罗斯方块

Pre上机logisim部分——俄罗斯方块 一.题目的回忆 1.关于输入输出 name width input 8 reset 1 clk 1 output 2 2.题意 ​ 我们利用mealy型状态机实现俄罗斯方块的模拟。假设我们有一个1行8列的空间,在每个时钟周期进行一个8位的输入,这个输入以独热码形式,如00000001表示在第一块空间放入方块,对于放置方块的输出有如下要求: 若尝试放置处已有方块,则当前想要放入的方块被阻挡,输出01. 若尝试放置处无方块且其他位置处至少有一处无方块,则成功放入方块,输出10。 若尝试放置处无方块,此外每个位置都有方块,则清空所有方块,输出得分 11。 3.电路模块外观 ​ 这部分对于题意倒是无关紧要 后续题解补充,唯一的坑点在于我们搭建好的电路可能与标准要求的电路外观不同,涉及到修改子电路外观。 二.题意理解 ​ 对于这道题目,由于要求搭建状态机,我的思路被局限在pre教程中提示过的利用真值表的解法。事实上,mealy状态机的下一状态逻辑和输出逻辑的输入是相同的,即电路的上一状态和当前输入。我们粗略的考虑一下打表的复杂度,输入为独热码,共八种状态,可能的状态有2^8-1种,即除去满方块的状态,这样打表的复杂度是2^11显然是不合理的。 所以我们应当摒弃打表这种想法,进一步思考题目的要求。 ​ 题目中只涉及到三种状态的判断,一是放置位置处已经有方块,这时放置失败,输出01,并将原来的状态更新为只有将要放置的这一块地方有方块(即相当于输入的一行把原来的状态顶替掉),二是想要放置的位置没有方块,且其他位置至少还有一个空块,这时输出10,三是想要放置的地方没有方块,且放置后刚好满一行,清空这一行,输出得分11。 ​ 我们考虑如何判断放置位置处有没有方块呢?我们知道输入为8位独热码,只有表示方块的那一位为1,如果此时状态中那一位已经有方块,我们知道,这两位的与运算为1。经过分析我们知道,判断放置位置处是否为空可以用与运算,而且当前输入与状态的与运算八位中最多只会有一位出现1。 ​ 我们考虑如何更新状态呢?我们想要的更新状态是在可以放入方块的情况下,而这“放入”的操作是可以通过位运算“或”来实现的,注意,在填入方块后,我们还需要考虑是不是每一位都是1,如果都是1,则说明满足情况3,需要清除所有方块。 ​ 在大体明白的情况下,考虑一下细节。如何判断与运算中是否有1?将与运算的结果的8位进行或运算。如何判断或运算是否已经填满?使用与运算。 ​ 在大致逻辑明白的情况下,我们可以进行搭建电路。 三.电路搭建 ​ 这里我附上mealy状态机的原型图 ​ 我们可以知道下方的或门是用来更新状态,或门之后的与门用来判断是否清空。clk与reset信号则直接控制寄存器。这里只有一个状态更新是由上面部分的电路提供的,即放置位置已有方块,这时更新为input的状态。多路选择器选择1引脚输出,大部分情况下(2,3)都是选择0引脚输出,即下方电路产生的下一状态。同时我们可以发现,电路的输出是控制选择常量输出。 四.编辑子电路外观 在我们搭建好的电路中,默认的外观为 ​ 题目要求的外观为输出锚点在右上角,这就需要我们修改子电路外观。 ​ 如上即可正确测评。

September 24, 2023 · 1 min · sudo

OOpre_HW3

OOpre_HW3 and JUnit 一.关于OO_checkstyle的新发现 只能采用驼峰命名法命名变量 方法行数不超过60行(后续重构代码将操作与处理输入分离的主要原理,虽然只有两分) 每行字数不超过100(方法传参时发现) 其余关于空格的问题省略 二.增量开发的思路 在此次作业中,新增了“食物”、“背包”等概念。food作为与equipment和bottle同级物品,背包则负责容纳这些物品。 新增操作: 尝试携带(放入背包)某物品(保证尝试携带的物品冒险者已经拥有) 尝试使用某物品(该物品必须被携带才能够使用) ​ 实现逻辑:我们需要明白“携带”与“使用”的业务逻辑。 我的第一版代码实现思路 ​ 我第一版代码中,按照题目描述,将food与package作为新建类处理,冒险者与背包之间的关系使用哈希表处理,建立起<advid,package>的映射,在背包中建立三个容器分别存储瓶子,装备和食物。对于加入背包,我的理解是,为冒险者增加物品是将物品放在冒险者对应的类adventure.java中对应的总库三个容器中,加入背包需要将物品从总库移动到与冒险者对应的背包,从物理角度来看是对物品进行了移动。这导致实现起来非常麻烦,例如统计数量等都需要考虑两个部分。这与题意不符,具体体现在中测最后一个数据点不过。 第二版代码 ​ 经过与助教的沟通,我理解到: 放入背包是一个概念的问题,而不是一个物理上的问题。放入背包并不需要将物品从总库中删除,只需要加入背包。 一开始处理中建立冒险者与背包对应哈希表的想法并不符合面向对象的逻辑,这是面向过程的思路,如果想要具体实现背包应该建立在冒险者类中 既然放入背包是一个概念问题,那么我们完全可以不去实现背包实体,而只需要进行概念上的判断。例如给每个物品增加一个属性 1 private boolean becarried ; 初始时设置为false即不在背包中,放入背包即建立方法将属性设置为true.这个思路在实现代码上是十分简便的,具体体验到的优势如下: 不需要新建数据结构存储放在背包中的物品 判断该物品是否在背包中只需要获取属性becarried 删除物品只需要在adventure类中的总库删除,实现简洁 获取物品数量是需要获取总库中的数量 三.代码架构与重构 ​ 经过checkstyle与JUnit对于代码架构的步步限制,我经历了三次代码重构,第一次是在编写过程中发现方法的行数不能超过60行,第二次是在传参时受到限制,选择将定义的静态方法从operation.java移动到inputhandler.java,第三次是编写JUnit过程中由于不能进行输入输出重定向等测评机认为的违法操作,这样只能将输入集中到一个类中,后续在方法中进行读取已经存储好的输入。 ​ 对于输入的类,原码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import java.util.Arrays; import java.util.Scanner; import java.util.ArrayList; public class Main { public static void main(String [] args) { ArrayList<ArrayList<String>> inputInfo = new ArrayList<>(); // 解析后的输入将会存进该容器中, 类似于c语言的二维数组 Scanner scanner = new Scanner(System.in); int n = Integer.parseInt(scanner.nextLine().trim()); // 读取行数 for (int i = 0; i < n; ++i) { String nextLine = scanner.nextLine(); // 读取本行指令 String[] strings = nextLine.trim().split(" +"); // 按空格对行进行分割 inputInfo.add(new ArrayList<>(Arrays.asList(strings))); // 将指令分割后的各个部分存进容器中 } InputHandler inputHandler = new InputHandler(inputInfo); inputHandler.solve(n); } } ​ 这样所有的操作指令被以分割的字符串的方式存入inputinfo,后续将inputinfo传入inputhandler类进行处理,所有的变量从这个形式上的二维数组中读取。这样可以避免在编写JUnit时无法控制台输入导致无法测试方法导致覆盖率不够,第二部分任务寄掉的问题。 ...

September 22, 2023 · 2 min · sudo

OOpre_HW2

OOpre_HW2 第一次进行类的编写(冒险者故事的开端) 1.什么是面向对象(Object Oriented) ​ 对象能够直接反映现实生活中的事物,例如人、车、小鸟等,将其表示为程序中的对象,每个对象都有各自的状态特征(属性)以及行为特征(方法),除了可以存储数据外还可以对自身进行操作,相当于结构体与函数的封装。 ​ 面向对象就是把构成问题的事物分解成一个一个的对象,建立对象不是为了实现一个步骤,而是描述某个事物在解决问题中的行为。 ​ 类是面向对象中的一个很重要的概念,类是很多个具有相同属性和行为特征的对象所抽象出来的,对象是类的一个实例。 2. OO三大特征 封装 继承 多态 3.类与对象 ​ 类表示一个共性的产物,是一个综合的产物,而对象是一个个性的产物,类必须通过对象才可以使用,对象的所有操作都在类中定义 类由属性和方法组成 属性:特征 方法:行为 ​ 一个类想真正地进行操作则必须依靠对象 1 2 3 4 5 //对象的定义 classname objectname = new classname();//所有类的对象都是通过new关键字创建 //访问类中的属性或方法 objectname.id //访问属性 objectname.func(parameter1,parameter2)//调用方法 类的编写规则 类必须编写在.java文件中 一个.java文件中可以存在多个类,但只能存在一个public修饰的类 .java文件名必须与public修饰的类名相同 同一个包中不能有重名的类 4.构造方法 ​ 在创建对象时,调用构造方法,所有的JAVA类中都至少存在一个构造方法(除了主类),如果一个类中没有明确的编写构造方法,编译器会自动生成一个无参的构造方法,构造方法中没有任何的代码!如果自行编写了构造方法,则编译器不会生成无参的构造方法。 构造方法的定义格式 构造方法名称必须与类名相同 没有返回值类型的声明 1 2 3 4 5 6 7 //创建一个对象就要调用构造方法 //一个自定义构造方法的例子 public person(String name, int age) { this.name = name; this.age = age; } this关键字 this指当前对象 程序中非静态方法可以使用this关键字 指向当前代码运行时所处于的对象空间 引用当前对象的实例变量 目前只在构造方法中接触this关键字 static关键字 static修饰变量为静态变量,也成称为类变量,静态变量属于类本身,而不是属于对象实例。该类的所有对象共享同一个静态变量的值,不会开辟出多块内存空间,可以通过<类名>.<变量名>来访问静态变量,但此时的变量需要被public修饰而不是private ...

September 15, 2023 · 6 min · sudo

MIPS中自动存入地址的指令

向$ra寄存器中自动存入地址的指令 ​ 在进行编写MIPS部分矩阵转化一题时,我误以为beq等分支指令也会将下一条指令的地址存入$ra寄存器,这导致出现访存bug. ​ 在MIPS架构的汇编指令中,只有 jal: 会将当前指令的地址存入$ra中,并跳转到目标地址执行 jalr:会将要跳转的地址存入目标寄存器,并将当前指令的地址存入$ra. beq与bne等条件分支指令则不会有将当前地址存入$ra寄存器的行为。 4560

September 14, 2023 · 1 min · sudo

MIPS中的函数调用

函数调用 一.调用初印象 ​ 最早接触到函数调用是在选择排序程序中,教学视频中代码块来换回拼接导致我看了好几遍视频! 下面附上源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 .data array: .space 400 // 申请数组空间 message_input_n: .asciiz "please input an integer as the length of the sequence\n" message_input_array: .asciiz "please input an integer followed with a line breaker\n" message_output_array: .asciiz "the sorted sequence is:\n" space: .asciiz " " stack: .space 100 // 申请栈空间 .globl main // 在代码段起始位置声明main为全局符号 .text input: la $a0,message_input_n li $v0,4 syscall li $v0,5 // number of integers to be sorted syscall // the number is stored in $v0 move $t0, $v0 // set $t0 to the contents of $v0 li $t1, 0 // 循环变量 for_1_begin: slt $t2, $t1, $t0 // t2=1 if t1 < t0 beq $t2, $zero, for_1_end nop // 目标指令紧跟分支指令 增加延时槽防止并行性引起错误 //下面这段代码实际上是在计算存入地址 // 存入地址 = 首地址 + 循环变量 * 4 la $t2, array / 将数组首地址存入t2 li $t3, 4 mult $t3, $t1 // t3 * t1 高位存入 hi,低位存入lo 事实上一般的乘法,只要结果不超过32位,lo中的值就是完整的答案 mflo $t3 // 将lo寄存器中移动到t3(所有的move指令都相当于赋值语句,复制后原值不会改变,而不是“移动”) addu $t2, $t2, $t3 //在首地址t2基础上加上偏移量t3 la $a0,message_input_array //输出前导字符串 li $v0,4 syscall li $v0, 5 //输入数字 syscall sw $v0, 0($t2) // 从寄存器存入数组中对应的地址 这里的t2就是刚刚计算过的地址 addi $t1, $t1, 1 // 循环变量++ j for_1_begin nop for_1_end: move $v0, $t0 jr $ra //跳回到主程序,跳转语句的下一条语句 nop //与input同理 无需多言! output: move $t0, $a0 li $t1,0 la $a0,message_output_array li $v0,4 syscall for_2_begin: slt $t2, $t1, $t0 beq $t2, $zero, for_2_end nop la $t2, array li $t3, 4 mult $t3, $t1 mflo $t3 addu $t2, $t2, $t3 lw $a0,0($t2) li $v0,1 syscall la $a0,space li $v0,4 syscall addi $t1, $t1, 1 j for_2_begin nop for_2_end: jr $ra nop sort: addiu $sp,$sp,-32 //向低地址移动32字节 move $t0,$a0 //此时a0值即为元素个数 li $t1,0 //循环变量 for_4_begin: slt $t2, $t1, $t0 //选择排序外层循环n-1趟 beq $t2, $zero, for_4_end nop //计算地址 la $t2, array li $t3, 4 mult $t1, $t3 mflo $t3 addu $t2, $t2, $t3 move $a0, $t0 move $a1, $t1 //父函数维护t寄存器 入栈 需要注意的是多层调用时$ra的维护 sw $t2, 28($sp) sw $t1, 24($sp) sw $t0, 20($sp) sw $ra, 16($sp) //这时的ra值需要保存 这里的ra值记录的是返回到主函数的指令地址 经过调用findmin后会变为返回到sort的地址! jal findmin //调用子函数 nop //出栈 lw $ra, 16($sp) lw $t0, 20($sp) lw $t1, 24($sp) lw $t2, 28($sp) //交换值 v0地址与t2地址处存储的值,两个寄存器分别存储 lw $t3, 0($v0) lw $t4, 0($t2) sw $t3, 0($t2) sw $t4, 0($v0) addi $t1,$t1,1 //更新循环变量 j for_4_begin nop for_4_end: addiu $sp,$sp,32 //将申请的栈空间退回,栈指针回到高地址 jr $ra //回到主程序 nop findmin: //从sort中传入 a0值为n, a1值为外层循环变量i la $t0,array sll $a0,$a0,2 subi $a0,$a0,4 //需要注意的是在 * 4的基础上需要减去4, addu $t0,$t0,$a0 //当前的地址是数组中最后一个元素的地址 lw $t1, 0($t0) // t1=a[n-1] move $t2,$t0 move $t3,$t0 // t3 = t2 = t0 = 最后一个元素地址 // a[i+1] la $t0,array sll $a1,$a1,2 addu $t0,$t0,$a1// t0此时为a[i+1]地址 for_3_begin: sge $t4,$t3,$t0 // t4 = 1 if t3 >= t0 beq $t4,$zero,for_3_end nop lw $t5,0($t3) // t5=a[n-1] // 进入查找时 t3为最后一个元素地址 ,t0为a[i+1]地址 //这里寻找最小值的操作实际上是从末尾开始的,先记t1=a[n-1]为最小值,之后由哨兵 t5=遍历到的值 从n-1逐步向前遍历直到 i+1 //不断更新t1的值作为新的最小值,并在t2中保存最小值地址 t3 //这里t1被设置为保存最小值 如果当前遍历的元素小于t1最小值则进入下方更新最小值操作,否则进入if_1_else,顺序进入if_1_end遍历 //下一个元素 slt $t6,$t5,$t1 // t6 = 1 if t5 < t1 第一次运行时 t5 = t1 直接跳转到 if_1_else beq $t6,$zero,if_1_else nop move $t1,$t5 //更新最小值 move $t2,$t3 //保存最小值在数组中的的地址 j if_1_end nop if_1_else:// if_1_else后误操作则直接执行 if_1_end 在标签之间无跳转时按照顺序执行 if_1_end: subi $t3,$t3,4 //t3从末尾向前移动一个元素,更新遍历元素 j for_3_begin nop for_3_end: move $v0,$t2 jr $ra //回到主程序 nop main: la $sp, stack addiu $sp,$sp,100 //此处sp+100是将指针向高地址移动 addiu $sp,$sp,-20 //-20向低地址移动 jal input //跳转到input nop move $t0,$v0 //回到位置 move $a0,$t0 sw $t0, 16($sp) //向高地址偏移量为16 调用者维护t寄存器,将t0入栈 jal sort nop lw $t0,16($sp) // t0出栈 move $a0,$t0 jal output nop addiu $sp,$sp,20 li $v0,10 //程序结束 syscall ​ 上述选择排序算法较为复杂,主体结构为main调用input,sort,output函数,在sort中又调用findmin子函数 ...

September 13, 2023 · 9 min · sudo

MIPS基础编程

MIPS编程 1.各部分寄存器的功能 $t0 ~ $t9: 临时变量 (调用者保存,否则容易丢失) $s0 ~ $s7: 保存变量(被调用者保存) $0 :常量0,存不了数据 $at : 保留给汇编器(不可以随便用) $v0 ~ $v1 : 函数调用返回值 $a0 ~ $a3 : 函数调用参数 $sp : 栈指针 $ra : 返回地址,用于子程序的调用 2.调用子过程时对于s,t寄存器的操作 1.被调用者维护s寄存器 ​ 对于s寄存器而言,被调用者需要保证s寄存器中的值在调用前后不能发生改变, 实际操作中,如果想要编写一个子函数,那么在这个子函数中使用的所有s寄存器,都必须要在函数的开头入栈、在函数的结尾出栈,确保s寄存器的值在函数调用前后不会发生变化。 2.调用者维护t寄存器 ​ 对于t寄存器,编写子函数中用到t寄存器的地方无需做任何保存,维护t寄存器是上层函数(调用者维护),调用者将t寄存器压入栈中,函数调用结束之后再弹回来,只需要借助$sp指针。 需要注意的是,对于s,t寄存器的维护都要通过在数据区开辟栈空间来实现! 3.调用关键字 jal:跳转到子过程 jr:跳转到父过程 4.栈的使用 过程自身需要满足栈的结构 过程调用子过程时需满足栈的结构 子过程执行前后移动栈指针 $sp MIPS中栈由高地址向低地址延申,即优先使用高地址,父过程栈帧高,子过程栈帧低 子过程的栈帧图 高地址 临时变量 ​ 返回地址 ​ 需要保存的寄存器 ​ 其他变量 低地址 参数0~3,传给子子过程($a0~$a3) 叶子函数:可以省去参数和返回地址(无子子函数) 栈的具体使用 计算好栈帧大小 即保存这些变量需要的字节大小 栈指针减少表示向低地址移动,栈指针增加表示向高地址移动 栈指针始终指向栈顶,栈指针初始时在高地址 过程开始时分配栈空间 addiu $sp,$sp,-32(需要32字节,将栈指针向低地址移动32字节) 过程结束时回收栈空间 addiu $sp,$sp,32 以栈指针为基址进行存取 sw $t0,24($sp) ,偏移量的单位是字节 偏移量为正向高地址偏移,偏移量为负向低地址偏移 5.nop延时槽 ​ 在MIPS编程中,延时槽是指指令执行的时间间隔。当某个条件分支指令(如条件跳转、函数调用等)的目标指令紧跟在该分支指令后面时,需要插入一个延时槽指令,以免出现错误的指令结果,常用```nop```. ...

September 13, 2023 · 2 min · sudo

MIPS与计算机内存

COpre——MIPS汇编指令与机器码、内存 ​ 在笔者进行COpre中MIPS部分的学习时,对于MIPS指令及其机器码转换,MIPS对应的内存计算等等感到十分头痛,这一篇文章主要目的是零碎的记录一些知识。 一. 数制 ​ 在计组学习中最常用到的数值即为二进制(binary/BIN)和十六进制(hexadecimal/HEX),例如MIPS汇编指令机器码用二进制表示,而计算机内存的表示通常为十六进制。一位十六进制数相当于四位二进制数。一位二进制数通常又称为比特位(bit),关联出计算机系统中经典的换算关系。 ​ 在32位系统中: 1字节(byte)=8比特位(bit) 计算机中最小的寻址单位即为字节/B 1个字(word)=4字节(byte)=32bits 即一个字就是32位,同样64位系统中一个字是64位 1KB=1024B 计算机中K的概念是2^10即1024 1MB=1024KB 1G=1024MB 二. 浅析MIPS架构 ​ MIPS是一种经典的RISC架构,具有精简的指令集,32位定长指令,五级流水线,延迟槽,32个通用寄存器等特点。这里我们主要谈及32位定长指令、32个通用寄存器、指令集等内容,其他的部分会在日后涉及。这里需要特殊说明的是我们的MIPS是32位系统 1.32位定长编码 ​ 定长指令的优点是简化指令解析,减少解析时间。但同样的,由于指令为定长32位,这对于内存是不友好的。我们在写MIPS汇编程序时,编写的每一行指令代码均为32位,四个字节,一个字。这里指令具体的分为R型、I型、J型指令 R型指令(register type) R型指令用于寄存器之间的操作,常用于算术运算、逻辑操作和寄存器之间的数据传输,如add,sub.and.or等。 I型指令(immediate type) ​ I型指令用于立即数(常数)与寄存器之间的操作,通常用于家在常熟、内存读写、分支跳转等操作。例如,addi,lw,sw,beq等指令 J型指令(jump type) ​ J型指令用于无条件跳转到目标地址,常用于函数调用、循环跳转等控制流程的修改。例如,j,jar指令。 对于以上指令,我们只需要记住他们都占32位,4个字节,其他的具体用法详见《MIPS-C指令集》。 ​ 2.32个通用寄存器 ​ MIPS寄存器是32位寄存器,每个部分的功能如图所示 ​ 需要注意的是,每个寄存器既可以用名字表示,也可以用编号表示,如**$to<==>$8**。 3.特殊寄存器 1. HI(high)与LO(low) ​ HI与LO是MIPS中用于处理乘除法的特殊寄存器,在MIPS汇编指令中,乘除法指令的结果最多为64位,夫需要设置特殊寄存器进行保存。在乘法中,HI保存高32位,LO保存低32位;在除法中HI保存余数,LO保存商。 2.PC程序计数器 ​ PC(program counter)程序计数器,是计算机系统中的一个寄存器,用于存储下一条指令的地址。程序计数器指向执行中的指令的内存地址,当处理器执行完当前指令后会自动将程序计数器的值增加,使其指向下一条指令的地址。具体地,在MIPS汇编指令中,每执行完一条指令 PC=PC+4,这是由于在MIPS中每一条指令所占的内存空间都是4个字节。程序计数器的初始值一般为程序的入口地址(首条指令的地址 最常见的为0x0000_3000)。同时分支指令也可以使程序计数器进行跳转。总的来说,PC相当于程序运行中的内存监控,通过PC可以了解程序的流程。 1 2 3 4 5 6 7 8 9 //在我翻阅 MIPS-C指令集时,发现了如下出场率极高的代码 //BEQ: beq rs,rt,offset //描述:if rs==rt then 转移 //以BEQ:相等时转移为例,功能的C语言描述为 if(GPR[rs]==GPR[rt]) PC=PC+4+sign_extend(offset||0^2);//代表在offset后补两位0 即乘四 else PC=PC+4; //我们可以发现,该指令至少会进行一条语句的跳转(四个字节),这也是我查询PC的起始 三.COpre中提供的部分题目具体分析 1.下列指令中需要在立即数后拼接两位0的是 1 答案: beq $s2,$s3,4 在立即数后拼接两位0,即将原立即数向左移动两位,立即数4,代表着按4对齐。在beq中,立即数n的意义是跳转到第n条指令,而实际操作中,一条指令占用四字节,地址访存的话需要跳转4n字节,所以需要拼接两位0。 ...

September 12, 2023 · 2 min · sudo

MIPS基本语法

MIPS代码的基本语法 1. .data数据段 1 2 3 .data fibs: .space 48 size: .word 12 ​ 在数据段进行变量的定义,<变量名> : .<伪指令> <变量内容> 2. .test代码段 ​ 定义程序的代码段,指令后为代码 1 2 .test add $t0,&t1,$t2 3. .macro ​ 使用宏可以避免相同代码多次复用 不带参数定义的宏 macro <macro_name> # # # .end_macro 常用的场景是可以替换程序结束的两行代码 1 2 li $v0,10 syscall 如果我们定义出代表这两行定义的宏 1 2 3 4 macro DONE li $v0,10 syscall .end_macro 这样我们在需要调用这两行代码的时候只需一行 ...

September 12, 2023 · 1 min · sudo