简易 MIPS 汇编器

  1. 简介
  2. 源代码
  3. 示例

1. 简介

本质是使用 ply 库实现的词法分析器,再将词法分析得到的 token 直接转换为对应的二进制码

结果用十六进制 ascii 码表示,可以使用 verilog 的相关函数用来初始化内存

写的目的是计算机组成实验要用…(要写一个支持 mips 指令的 CPU,为了得到 mips 指令才写的这个)

支持的指令:

指令类型 指令 指令格式类型
存储器访问指令 LW Rt,Imme16(Rs) I 型
存储器访问指令 SW Rt,Imme16(Rs) I 型
算术逻辑运算指令 ADD Rd,Rs,Rt R 型
算术逻辑运算指令 SUB Rd,Rs,Rt R 型
算术逻辑运算指令 OR Rd,Rs,Rt R 型
算术逻辑运算指令 AND Rd,Rs,Rt R 型
算术逻辑运算指令 SLT Rd,Rs,Rt R 型
程序转移指令 BEQ Rs,Rt,Addr16 I 型
程序转移指令 J Addr16 J 型

存在一个无关紧要的移进归约冲突,解决方法是优先移进。(其实优先移进和优先归约结果是相同的)

如果出现这个警告不用太在意。

效果图:

闲来无事加了个注释功能,关键字与 python 一致,转换后注释内容原封不动放进输出文件
目的是使得可以同时编写多段不同用处的代码,并用注释标明,这样就不用写一段删一段了
(存在两个问题,最后一行必须是指令或者空行,指令和注释不能同行)

效果图:

2. 源代码

  1. 词法分析器
  2. 语法分析器
  3. 测试文件

2.1 词法分析器

lexer.py

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
# -- coding: utf-8 --**
# ---------
# 简易mips指令汇编器 词法分析
# author:bibibi
# ---------

import ply.lex as lex

CODE_dict = {
'LW': '100011',
'SW': '101011',
'ADD': '000000',
'SUB': '000000',
'OR': '000000',
'AND': '000000',
'SLT': '000000',
'BEQ': '000100',
'J': '000010'
}

CAL_dict = {
'ADD': '100000',
'SUB': '100010',
'OR': '100101',
'AND': '100100',
'SLT': '101010',
}

tokens = (
'OPCODE', # 指令码
'REG', # 寄存器
'IMME', # 立即数
'LPAREN', # 左括号
'RPAREN', # 右括号
'COMMENTS'
)
t_LPAREN = r'\('
t_RPAREN = r'\)'


def t_COMMENTS(t):
r'\#[^\n]*'
t.value = ["comments",t.value]
return t

# 操作码


def t_OPCODE(t):
r'(LW)|(SW)|(ADD)|(SUB)|(OR)|(AND)|(SLT)|(BEQ)|(J)'
# 如果是数字逻辑运算型指令,则需要加上对应的操作码
if t.value in CAL_dict:
t.value = [CODE_dict[t.value], CAL_dict[t.value]]
else:
t.value = CODE_dict[t.value]
return t

# 寄存器


def t_REG(t):
r'\$\d+'
# 寄存器号码直接转化为对应的二进制表示
t.value = "%05d" % int(bin(int(t.value[1:]))[2:])
# t.value = bin(int(t.value[1:]))[2:]
return t


def t_IMME(t):
r'\d+'
# 由于数字可能是I型指令的16位,也可能是J型指令的26位
# 保存原数字的值,在语法分析种再进行转换
# t.value = "%05d"%int(bin(int(t.value))[2:])
# t.value = bin(int(t.value))[2:]
# t.value = (t.value[0]=='-')?(-1*int(t.value)):int(t.value);
t.value = int(t.value)
return t


def t_newline(t):
r'\n+'
t.lexer.lineno += len(t.value)


t_ignore = ', \t'


def t_error(t):
print("Illegal character '%s'" % t.value[0])
t.lexer.skip(1)


lexer = lex.lex()


if __name__ == "__main__":
with open("./data.txt", 'r', encoding='utf-8') as file:
data = file.read()
lexer.input(data)
while True:
tok = lexer.token()
if not tok:
break
print(tok)

2.2 语法分析器

yacc.py

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
# coding:utf-8
# ---------
# 简易mips指令汇编器 语法分析
# author:bibibi
# ---------

from lexer import lexer, tokens
import ply.yacc as yacc

# 存储器访问指令,即I型指令
# 将立即数转换为16位二进制数


def p_operation_mem(p):
'operation : OPCODE REG IMME LPAREN REG RPAREN'
p[0] = p[1] + p[5] + p[2] + "%016d" % int(bin(int(p[3]))[2:])
p[0] = "%08x" % (int(p[0], base=2)) + '\n'

# 算术逻辑运算,即R型指令
# 需要根据在指令的最后加上操作码


def p_operation_cal(p):
'operation : OPCODE REG REG REG'
p[0] = p[1][0] + p[3] + p[4] + p[2] + '00000' + p[1][1]
p[0] = "%08x" % (int(p[0], base=2)) + '\n'


# BEQ指令,I型指令
# 将立即数转换为16位二进制数


def p_operation_beq(p):
'operation : OPCODE REG REG IMME'
p[0] = p[1] + p[2] + p[3] + "%016d" % int(bin(int(p[4]))[2:])
p[0] = "%08x" % (int(p[0], base=2)) + '\n'

# J指令,J型指令
# 将立即数转换为26位二进制数


def p_operation_j(p):
'operation : OPCODE IMME'
p[0] = p[1] + "%026d" % int(bin(int(p[2]))[2:])
p[0] = "%08x" % (int(p[0], base=2)) + '\n'

# 若干条指令合并成一段连续的代码


def p_program(p):
'operation : operation operation'
p[0] = p[1] + p[2]

# 处理注释
def p_comments(p):
'''operation : COMMENTS operation
| operation COMMENTS'''
# 存在两个问题:注释不能和代码放在同一行,注释不能放在结尾
# 但是又不是不能用哈哈哈哈哈哈哈
# 这么费劲主要是想让源码中的行号与输出文件中的行号相对应
if type(p[1]) == list:
p[0] = p[1][1] + '\n' + p[2]
else:
p[0] = p[1] + p[1][1] + '\n'


def p_error(p):
print("Syntax error in input!", p)


parser = yacc.yacc()


def compile(data):
result = parser.parse(data)
return result

2.3 测试入口

test.py

1
2
3
4
5
6
7
8
9
10
11
12
13
from yacc import compile

if __name__ == "__main__":
file_name = "temp"
with open(file_name + ".txt",'r',encoding='utf-8') as file:
data = file.read()

result = compile(data)

with open(file_name + ".out",'w',encoding='utf-8') as file:
file.write(result)

print(result)

3. 示例

各种不同的指令

data.txt

1
2
3
4
5
6
7
8
9
10
11
LW $1,3($0)
LW $2,4($0)
J 11
ADD $3,$1,$2
SUB $4,$1,$2
OR $5,$1,$2
AND $6,$1,$2
SLT $7,$1,$2
SLT $7,$2,$1
BEQ $1,$2,65532
BEQ $1,$1,11

data.out

1
2
3
4
5
6
7
8
9
10
11
8c010003
8c020004
0800000b
00221820
00222022
00222825
00223024
0022382a
0041382a
1022fffc
1021000b

附赠一段用来测试流水线的代码

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
# 1.RAW数据冲突
# 即计算得到的结果需要在送回寄存器之前被其他指令用(以R型指令为例,I型指令同理)
# 1.1 从另一个寄存器转移到另一个寄存器
# 该冲突使用数据旁路解决
LW $1,0($0)
LW $2,1($0)
ADD $3,$0,$0
ADD $4,$0,$0
ADD $5,$0,$0
ADD $6,$0,$0
ADD $7,$0,$0
# ($1,$2寄存器初始化完成,同时流水线中不再使用$1,$2寄存器)
# 开始测试
# 正确结果为$8,$9,$10寄存器中的结果为数据存储器0,1地址数之和
ADD $1,$1,$2
ADD $8,$1,$0
ADD $9,$1,$0
ADD $10,$1,$0
# 1.2 从数据存储器中取数
# 该冲突使用插入一个气泡解决
# 正确结果为$3寄存器中的结果为数据存储器0,1地址数之和
LW $1,0($0)
LW $2,1($0)
ADD $3,$1,$2

# 2. BEQ指令数据冲突
# 得到BEQ指令需要用到但是还没送回RF的数据
# 由于BEQ指令在D阶段就需要用数据,所以不同于R型指令
# 该冲突同时使用数据旁路和出入气泡解决
# 2.1 需要用的数据在 W 阶段==>由于寄存器组支持同时读写所以没有冲突
# 2.2 需要用的数据在 M 阶段==>使用数据旁路解决
# 2.3 需要用的数据在 E 阶段==>插入一个气泡后使用2.2方法解决
LW $1,0($0)
LW $2,1($0)
ADD $3,$0,$0
ADD $4,$0,$0
ADD $5,$0,$0
ADD $6,$0,$0
ADD $7,$0,$0
# ($1,$2寄存器初始化完成,同时流水线中不再使用$1,$2寄存器)
# 正确结果为完成跳转($4寄存器中的值不变)
ADD $3,$1,$0
BEQ $1,$3,4
ADD $4,$3,$0
ADD $5,$3,$0
ADD $6,$3,$0

# 3. 控制冒险
# 即指令执行顺序发生改变
# 3.1 J指令跳转
# 正确结果为$3,$4寄存器中的值未发生改变
LW $1,0($0)
LW $2,1($0)
J 20
LW $3,2($0)
LW $4,3($0)
LW $5,4($0)
LW $6,5($0)
# 3.2 BEQ指令跳转
# 正确结果为$2,$3寄存器中的值发生改变,$4,$5寄存器中的值未发生改变
LW $1,0($0)
BEQ $1,$0,8
LW $2,1($0)
LW $3,2($0)
BEQ $1,$1,8
LW $4,3($0)
LW $5,4($0)
LW $6,5($0)

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
# 1.RAW数据冲突
# 即计算得到的结果需要在送回寄存器之前被其他指令用(以R型指令为例,I型指令同理)
# 1.1 从另一个寄存器转移到另一个寄存器
# 该冲突使用数据旁路解决
8c010000
8c020001
00001820
00002020
00002820
00003020
00003820
# ($1,$2寄存器初始化完成,同时流水线中不再使用$1,$2寄存器)
# 开始测试
# 正确结果为$8,$9,$10寄存器中的结果为数据存储器0,1地址数之和
00220820
00204020
00204820
00205020
# 1.2 从数据存储器中取数
# 该冲突使用插入一个气泡解决
# 正确结果为$3寄存器中的结果为数据存储器0,1地址数之和
8c010000
8c020001
00221820
# 2. BEQ指令数据冲突
# 得到BEQ指令需要用到但是还没送回RF的数据
# 由于BEQ指令在D阶段就需要用数据,所以不同于R型指令
# 该冲突同时使用数据旁路和出入气泡解决
# 2.1 需要用的数据在 W 阶段==>由于寄存器组支持同时读写所以没有冲突
# 2.2 需要用的数据在 M 阶段==>使用数据旁路解决
# 2.3 需要用的数据在 E 阶段==>插入一个气泡后使用2.2方法解决
8c010000
8c020001
00001820
00002020
00002820
00003020
00003820
# ($1,$2寄存器初始化完成,同时流水线中不再使用$1,$2寄存器)
# 正确结果为完成跳转($4寄存器中的值不变)
00201820
10230004
00602020
00602820
00603020
# 3. 控制冒险
# 即指令执行顺序发生改变
# 3.1 J指令跳转
# 正确结果为$3,$4寄存器中的值未发生改变
8c010000
8c020001
08000014
8c030002
8c040003
8c050004
8c060005
# 3.2 BEQ指令跳转
# 正确结果为$2,$3寄存器中的值发生改变,$4,$5寄存器中的值未发生改变
8c010000
10200008
8c020001
8c030002
10210008
8c040003
8c050004
8c060005