LoginSignup
0
0

Hackコンピューター(nand2tetris)のアセンブラを作る

Last updated at Posted at 2023-06-17

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. 実行結果

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0