1. 概要
「VHDLの練習としてnand2tetrisのHackコンピューターをMAX10 FPGAで作る」の続き。今度はアセンブラを作る。テキストどおりに作業を進める。Pythonを使う。
2. Parserクラスを作る
コードを見る/隠す
Parser.py
class Parser:
def __init__(self, file_path):
self.file = open(file_path, "r")
self.EOF_POS = self.__get_EOF_position(self.file)
self.current_inst = ""
def has_more_lines(self):
return self.file.tell() != self.EOF_POS
def advance(self):
self.current_inst = self.__readline_then_strip()
return self
def inst_type(self):
if not self.current_inst:
return ""
elif "@" in self.current_inst:
return "A"
elif "(" in self.current_inst and ")" in self.current_inst:
return "L"
else:
return "C"
def symbol(self):
if self.inst_type() == "A":
return self.__get_right_hand(self.current_inst, "@")
elif self.inst_type() == "L":
return self.current_inst[1:-1] # (LABEL)のLABELだけを抜いて返す。
else:
return ""
def dest(self):
# dest=comp;jump
if self.inst_type() == "C" and self.__has_destination():
return self.__get_left_hand(self.current_inst, "=") # destだけを抜いて返す。
else:
return ""
def comp(self):
# dest=comp;jump
if self.inst_type() == "C":
comp_part = self.__get_right_hand(self.current_inst, "=") # dest=を切り落として、
comp_part = self.__get_left_hand(comp_part, ";") # さらに;jumpを切り落としてcompだけを抜いて返す。
return comp_part
else:
return ""
def jump(self):
# dest=comp;jump
if self.inst_type() == "C" and self.__has_jump_to():
return self.__get_right_hand(self.current_inst, ";") # jumpだけを抜いて返す。
else:
return ""
def close(self):
self.file.close()
return self
def __readline_then_strip(self):
line = self.file.readline()
line = line.strip() # 先頭の空白と末尾の改行記号とを削除する。
line = self.__get_left_hand(line, "//") # コメントを削除する
line = line.replace(" ", "") # 文字列中の半角スペースを削除する。
line = line.replace(" ", "") # 文字列中の全角スペースを削除する。
line = line.replace("\t", "") # 文字列中のタブ文字を削除する。
return line if line else ""
def __has_jump_to(self):
return (";" in self.current_inst)
def __has_destination(self):
return ("=" in self.current_inst)
def __get_left_hand(self, strings, delimiter):
return strings.split(delimiter)[0]
def __get_right_hand(self, strings, delimiter):
return strings.split(delimiter)[-1]
def __get_EOF_position(self, file):
file.seek(0, 2) # ファイル末尾に移動して、
pos = file.tell() # その位置を取得して、
self.file.seek(0) # 再度ファイル先頭に戻って、
return pos # 取得したファイル末尾位置を返す。
if __name__ == "__main__":
s = Parser("test.asm")
while (s.has_more_lines()):
s.advance()
print("--------------------------")
print("命令 :", s.current_inst)
print("命令タイプ :", s.inst_type())
print("シンボル :", s.symbol())
print("格納先 :", s.dest())
print("ALU出力 :", s.comp())
print("ジャンプ命令:", s.jump())
s.close()
3. Codeクラスを作る
コードを見る/隠す
Code.py
class Code:
def __init__(self):
pass
# dest=comp;jumpのdestをバイナリコードに変える。
def dest(self, mnemonic):
adm = 0
if "A" in mnemonic: adm |= 1 << 2
if "D" in mnemonic: adm |= 1 << 1
if "M" in mnemonic: adm |= 1
return bin(adm)[2:].zfill(3)
# dest=comp;jumpのcompをバイナリコードに変える。
def comp(self, mnemonic):
cccccc = ""
if mnemonic == "0" : cccccc = "101010"
if mnemonic == "1" : cccccc = "111111"
if mnemonic == "-1" : cccccc = "111010"
if mnemonic == "D" : cccccc = "001100"
if mnemonic == "A" or mnemonic == "M" : cccccc = "110000"
if mnemonic == "!D" : cccccc = "001101"
if mnemonic == "!A" or mnemonic == "!M" : cccccc = "110001"
if mnemonic == "-D" : cccccc = "001111"
if mnemonic == "-A" or mnemonic == "-M" : cccccc = "110011"
if mnemonic == "D+1" : cccccc = "011111"
if mnemonic == "A+1" or mnemonic == "M+1": cccccc = "110111"
if mnemonic == "D-1" : cccccc = "001110"
if mnemonic == "A-1" or mnemonic == "M-1": cccccc = "110010"
if mnemonic == "D+A" or mnemonic == "D+M": cccccc = "000010"
if mnemonic == "D-A" or mnemonic == "D-M": cccccc = "010011"
if mnemonic == "A-D" or mnemonic == "M-D": cccccc = "000111"
if mnemonic == "D&A" or mnemonic == "D&M": cccccc = "000000"
if mnemonic == "D|A" or mnemonic == "D|M": cccccc = "010101"
acccccc = ""
if "M" in mnemonic: acccccc = "1" + cccccc
else : acccccc = "0" + cccccc
return acccccc
# dest=comp;jumpのjumpをバイナリコードに変える。
def jump(self, mnemonic):
jjj = [
"",
"JGT",
"JEQ",
"JGE",
"JLT",
"JNE",
"JLE",
"JMP",
]
jjj_index = jjj.index(mnemonic)
return bin(jjj_index)[2:].zfill(3)
#########################################################################
if __name__ == "__main__":
inst = Code()
print(inst.dest(""))
print(inst.dest("M"))
print(inst.dest("D"))
print(inst.dest("DM"))
print(inst.dest("A"))
print(inst.dest("AM"))
print(inst.dest("AD"))
print(inst.dest("ADM"))
print("")
print(inst.comp("0"))
print(inst.comp("A"))
print(inst.comp("!A"))
print(inst.comp("!M"))
print(inst.comp("D|A"))
print(inst.comp("D|M"))
print("")
print(inst.jump(""))
print(inst.jump("JGT"))
print(inst.jump("JEQ"))
print(inst.jump("JGE"))
print(inst.jump("JLT"))
print(inst.jump("JNE"))
print(inst.jump("JLE"))
print(inst.jump("JMP"))
print("")
4. Symbol_Tableクラスを作る
ここではPythonの辞書を使った。
コードを見る/隠す
Symbol_Table.py
class Symbol_Table:
def __init__(self, screen_address=8192, keyboard_address=8576, out_port_address=8577):
self.symbol_dict = {
"SP":0,
"LCL":1,
"ARG":2,
"THIS":3,
"THAT":4,
"SCREEN":screen_address,
"KBD":keyboard_address,
"OUT_PORT":out_port_address}
self.ram_last_address = 15
# 疑似汎用レジスタとして使うR0~R15とアドレスとのペアをシンボルテーブルに追加する。
for i in range(self.ram_last_address+1):
self.symbol_dict["R" + str(i)] = i
def add_entry(self, symbol, address):
if not self.contains(symbol):
self.symbol_dict[symbol] = address
else:
print("symbol already exists")
return self
def contains(self, symbol):
return symbol in self.symbol_dict
def get_address(self, symbol):
return self.symbol_dict[symbol]
def get_symbol_dict(self):
return self.symbol_dict
def get_ram_last_address(self):
return self.ram_last_address
def increment_ram_last_address(self):
self.ram_last_address += 1
return self
##################################################
if __name__ == "__main__":
s = Symbol_Table(screen_address=8192, keyboard_address=8576, out_port_address=8577)
print("初期化直後:", s.get_symbol_dict())
s.add_entry("LOOP",10)
s.increment_ram_last_address()
s.add_entry("sum", s.get_ram_last_address())
s.increment_ram_last_address()
s.add_entry("i", s.get_ram_last_address())
s.add_entry("LOOP2",20)
print("エントリ追加後: ", s.get_symbol_dict())
5. Hack_Assemblerを作る
これまでに作った3つのクラスをアセンブラにまとめる。これで.hack
ファイルが生成できるようになった。以上でテキストの内容は終わりである。
コードを見る/隠す
Hack_Assembler.py
from Parser import *
from Code import *
from Symbol_Table import *
class Hack_Assembler:
def __init__(self,
asm_file_path,
screen_address=8192,
keyboard_address=8576,
out_port_address=8577):
# 出力ファイル名は入力ファイル名の拡張子を.hackに変えたものにする。
self.output_file_path = self.__replace_file_extension(asm_file_path, "hack")
self.output_file = open(self.output_file_path, "w")
self.parser = Parser(asm_file_path)
self.code = Code()
self.symbol_table = Symbol_Table(
screen_address=screen_address,
keyboard_address=keyboard_address,
out_port_address=out_port_address)
self.hack_file = [] # .hackファイルを出力するだけでなくリストとしても持っておく。
self.__first_pass()
self.__second_pass()
def __first_pass(self):
self.parser.file.seek(0) # ファイルの先頭に移動して、
line_number = -1 # 行番号の初期値を設定して、
while (self.parser.has_more_lines()):
self.parser.advance() # 次の行に移動して、
if (self.parser.inst_type() == "C") or (self.parser.inst_type() == "A"):
line_number += 1 # A命令なら何もせずに行番号を先へ進めるだけにして、
elif self.parser.inst_type() == "L":
self.symbol_table.add_entry(self.parser.symbol(), line_number + 1) # L命令ならラベルをシンボルテーブルに追加する。
def __second_pass(self):
self.parser.file.seek(0) # ファイルの先頭に移動して、
while (self.parser.has_more_lines()):
self.parser.advance() # 次の行に移動して、
if self.parser.inst_type() == "C":
self.__second_pass_for_C_inst() # C命令ならそれに応じた処理をして、
elif self.parser.inst_type() == "A":
self.__second_pass_for_A_inst() # A命令ならそれに応じた処理をして、
self.parser.close()
self.close()
def __second_pass_for_C_inst(self):
# dest=comp;jumpの各部をバイナリコードに変えて、
comp_part = self.code.comp(self.parser.comp())
dest_part = self.code.dest(self.parser.dest())
jump_part = self.code.jump(self.parser.jump())
# 16ビットのC命令に変えてファイルに書き込む。
processed_string = "111" + comp_part + dest_part + jump_part
self.output_file.write(processed_string + "\n")
self.hack_file.append(processed_string)
def __second_pass_for_A_inst(self):
address_part = self.parser.symbol()
try:
int(address_part)
except:
# シンボルがシンボルテーブルに登録済みであったら
if self.symbol_table.contains(address_part):
# それを取り出すが、
address_part = self.symbol_table.get_address(address_part)
# シンボルテーブルに登録していなかったら、
else:
# そのシンボルと、空いているRAMのうち一番若いRAMの番地とをペアにして
# シンボルテーブルに登録して、
self.symbol_table.increment_ram_last_address()
self.symbol_table.add_entry(address_part, self.symbol_table.get_ram_last_address())
#address_part = self.symbol_table.ram_last_address
address_part = self.symbol_table.get_ram_last_address()
# 16ビットのA命令に変えてファイルに書き込む。
processed_string = "0" + bin(int(address_part))[2:].zfill(15)
self.output_file.write(processed_string + "\n")
self.hack_file.append(processed_string)
def get_hack_file(self):
return self.hack_file
def close(self):
self.output_file.close()
return self
def __replace_file_extension(self, file_name, new_extension):
return file_name.split(".")[0] + "." + new_extension
####################################################################
if __name__ == "__main__":
Hack_Assembler("test.asm")
6. .asmファイルから一気に.mifファイルを生成する
.asm
ファイルから.mif
ファイルが一気に生成できるようにする。.mif
ファイルというのは、MAX10のメモリーブロックに作ったROMを初期化するためのファイルのこと。.asm
ファイルをdrop_here_asm.bat
にドラッグ&ドロップすればrom_int.mif
ファイルが生成される。
コード(asm2mif.py)を見る/隠す
asm2mif.py
import sys
from Hack_Assembler import *
class asm2mif:
def __init__(
self,
asm_file_path,
mif_name = "rom_init.mif",
rom_words = 4096,
screen_address = 8192,
keyboard_address = 8576,
out_port_address = 8577):
file_path = ""
try:
file_path = sys.argv[1]
except:
file_path = asm_file_path
hack_assembler = Hack_Assembler(
asm_file_path = file_path,
screen_address = screen_address,
keyboard_address = keyboard_address,
out_port_address = out_port_address)
self.hack_file = hack_assembler.get_hack_file()
self.__generate_mif(self.hack_file,
mif_name,
rom_words = rom_words)
def __generate_mif(self, bit_strings, mif_path, rom_words):
f = open(mif_path, "w")
f.write("WIDTH=%d;\n" % 16)
f.write("DEPTH=%d;\n" % rom_words)
f.write("ADDRESS_RADIX=UNS;\n")
f.write("DATA_RADIX=BIN;\n")
f.write("CONTENT BEGIN\n")
for i in range(len(bit_strings)):
f.write("%10d: %s;\n" % (i, bit_strings[i]))
f.write("END;\n")
f.close()
if __name__ == "__main__":
asm2mif(
asm_file_path = "test.asm",
mif_name = "rom_init.mif",
rom_words = 4096,
screen_address = 8192,
keyboard_address = 8576,
out_port_address = 8577)
バッチファイル(drop_here_asm.bat)を見る/隠す
drop_here_asm.bat
for %%i in (%*) do (
python.exe asm2mif.py %%i
)
テストした.asmファイル(test.asm)を見る/隠す
test.asm
////////////////////////////////////
// 1からRAM[0]までの総和を求める。//
////////////////////////////////////
// ディップスイッチで設定した任意の値をRAM[0]に格納する。
@KBD
D=M
@R0
M=D
// 加算する値の初期値i=1
@i
M=1
// 総和の初期値sum=0
@sum
M=0
(LOOP)
@i
D=M
// 途中の何かをOUT_PORTに出力する。
@OUT_PORT
M=D
@R0
D=D-M
// 途中の何かをOUT_PORTに出力する。
@OUT_PORT
M=D
// if i-RAM[0] > 0: goto (STOP)
// すなわち計算し終わったらループを抜けて(STOP)へ飛ぶ。
@STOP
D;JGT
// sum+=i
@sum
D=M
// 途中の何かをOUT_PORTに出力する。
@OUT_PORT
M=D
@i
D=D+M
// 途中の何かをOUT_PORTに出力する。
@OUT_PORT
M=D
@sum
M=D
// i++
@i
M=M+1
@LOOP
0;JMP
(STOP)
// R1=sum
@sum
D=M
// 最終結果をOUT_PORTに出力する。
@OUT_PORT
M=D
(END)
@END
0;JMP
生成された.mifファイル(rom_init.mif)を見る/隠す
WIDTH=16;
DEPTH=4096;
ADDRESS_RADIX=UNS;
DATA_RADIX=BIN;
CONTENT BEGIN
0: 0010000110000000;
1: 1111110000010000;
2: 0000000000000000;
3: 1110001100001000;
4: 0000000000010000;
5: 1110111111001000;
6: 0000000000010001;
7: 1110101010001000;
8: 0000000000010000;
9: 1111110000010000;
10: 0010000110000001;
11: 1110001100001000;
12: 0000000000000000;
13: 1111010011010000;
14: 0010000110000001;
15: 1110001100001000;
16: 0000000000100000;
17: 1110001100000001;
18: 0000000000010001;
19: 1111110000010000;
20: 0010000110000001;
21: 1110001100001000;
22: 0000000000010000;
23: 1111000010010000;
24: 0010000110000001;
25: 1110001100001000;
26: 0000000000010001;
27: 1110001100001000;
28: 0000000000010000;
29: 1111110111001000;
30: 0000000000001000;
31: 1110101010000111;
32: 0000000000010001;
33: 1111110000010000;
34: 0010000110000001;
35: 1110001100001000;
36: 0000000000100100;
37: 1110101010000111;
END;
7. 実行結果