Building Machines In Code – Part 6

This entry is part 6 of 9 in the series Building Machines in Code

Last issue we built a simple assembler for our TIny-P processor emulator. In this installment, we will build a loader. But what is a loader?

Loaders are small programs that load other programs into system memory and prepare them for execution. Most loaders are part of an Operating System however, in the embedded world, there often is no operating system and so a free-standing loader is typical.

Loaders vary in complexity and functionality. The loader we will build for the Tiny-P will be minimal and only support loading our bin files generated by the assembler into the Tiny-P’s program memory.

To load the program memory of the Tiny-P, the loader has to have access to it. We give our loader that access by passing in the CPU. The loader can then call the CPU’s program() method to store the program into PROM (Programmable Read-Only Memory). Before doing this, however, our loader should make sure the CPU is ready to be programmed by calling the init_rom() and reset() methods.

The Tiny-P loader is a very small application and so I will show the complete “loader” portion of the program below:

# Tiny-P Machine Code Loader
# Assumes machine code is stored in
# a *.bin file and is formatted as:
# <address> <opcode>
# Where the address is a 3-digit decimal
# value and the opcode is a 3-digit
# decimal value.

import sys, getopt

from cpu import CPU


class Loader:
    def __init__(self, cpu: CPU, code_text: str):
        self.machine_code = code_text
        self.code = self.machine_code.split('\n')
        self.cpu = cpu

    def cpu_init(self):
        self.cpu.init_rom()
        self.cpu.reset()

    def load(self):
        for line in self.code:
            code = line.split()
            if len(code) == 2:
                addr = int(code[0])
                opcode = int(code[1])
                self.cpu.program(addr, opcode)

The __init__() method takes in two arguments, a CPU and a string of text. The text is the contents of our bin file and the CPU is an instance of our Tiny-P CPU. Because the CPU and text are instantiated outside the Loader class, we are free to pre-process them as needed.

You can see the cpu_init() method simply readies the CPU for programming while the load() method walks through each line of text separating the address from the opcode and storing the opcode in PROM at the associated address. The one item to take note of is that the address and opcode are also converted to integer values before the call to the program() method.

Adding Polish

As with the assembler, our loader will receive a little polish to make it easier to use and look a bit more professional. We will use nearly identical code to that used in the assembler; using getopt to parse command-line arguments and pass them to our loader. Add a main() function below and outside the Loader() class as shown below:

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# Tiny-P Machine Code Loader
# Assumes machine code is stored in
# a *.bin file and is formatted as:
# <address> <opcode>
# Where the address is a 3-digit decimal
# value and the opcode is a 3-digit
# decimal value.

import sys, getopt

from cpu import CPU


class Loader:
    def __init__(self, cpu: CPU, code_text: str):
        self.machine_code = code_text
        self.code = self.machine_code.split('\n')
        self.cpu = cpu

    def cpu_init(self):
        self.cpu.init_rom()
        self.cpu.reset()

    def load(self):
        for line in self.code:
            code = line.split()
            if len(code) == 2:
                addr = int(code[0])
                opcode = int(code[1])
                self.cpu.program(addr, opcode)


def main(argv):
    inputfile = ''
    usage_message = "Usage: assembler.py -i <inputfile> "

    try:
        opts, args = getopt.getopt(argv, "hi:", ["help", "ifile="])
    except getopt.GetoptError:
        print(usage_message)
        sys.exit(2)
    for opt, arg in opts:
        if opt in ('-h', '--help'):
            print(usage_message)
            sys.exit()
        elif opt in ('-i', '--ifile'):
            inputfile = arg

    if not inputfile:
        print(usage_message)
        sys.exit(2)

    with open(inputfile, 'r') as ifh:
        program_text = ifh.read()
    ifh.close()

    # Loader program
    cpu = CPU()
    loader = Loader(cpu, program_text)
    loader.cpu_init()
    loader.load()

    # Exit message
    print(f"Loader: {inputfile} loaded in to cpu.")
    print(f"Ready to run!")

    # Run the program
    cpu.run()
    ...

Our main() function here is mostly for demonstration and exercise purposes. The last line in the code above calls cpu.run() to execute the program loaded into the Tiny-P. You can replace this line with calls to cpu.step() and a display_dump() method to display the state of the CPU if desired:

# Exit message
    print(f"Loader: {inputfile} loaded in to cpu.")
    print(f"Ready to run!")

    # Run the program
    cpu.step()
    dump(cpu)
    cpu.step()
    dump(cpu)
    cpu.step()
    dump(cpu)
...

The dump() function might look something like this:

def dump(cpu: CPU):
    print(f"ACC: {cpu.acc}, PC: {cpu.pc}, Z: {cpu.zero_flag}, P: {cpu.pos_flag}")
    dump_mem(cpu.mem)
    print()
    dump_mem(cpu.prog)
    print('\n')


def dump_mem(mem: list):
    for i, data in enumerate(mem):
        if i % 15 == 0: print(f"\n{i} : ", end='')
        print(f" {data}, ", end='')

Breaking out the dump_memory() function and passing in the memory object we want to display keeps us from cutting and pasting similar code for the program and data memories. The loop in dump_memory() uses the enumerate() method to get both the address (index into the list) and the value. We can then use the address i and the modulo operator to format the display into lines of up to 16 memory locations per row. I did neglect to remove the trailing comma but this is trivial and left as an exercise for the reader.

Our file needs to contain the standard if __name__ == “__main__” cluase at the bottom if the file:

...

if __name__ == "__main__":
    main(sys.argv[1:])

Don’t forget to import sys at the top of the file. If copy the cpu.py file and the example.bin file generated by the assembler from part-5, on *nix you can run the loader like this:

>$ ./loader.py -i example.bin

Run the program and make sure you get no errors and that the output is as expected.

If you only plan on using the loader from other files as a library, you can remove the main function. But I prefer to leave it for testing anytime I make changes. You can still use it as a library by simply importing it as such. The magic line if __name__ == “__main__”: will only be true and call main() if you execute the file directly. Otherwise, it’s a library module, and the main function won’t be called.

Conclusion

This has been a short post compared to parts 5 and 6. Loaders simply automate a task we would have to write over and over again with each new application if we hadn’t created a library for it. When possible, you should seek to automate any task that you expect to do many times, providing the time needed to automate far exceeds the time to complete the tasks manually or if the task is complex and error-prone. Our loader will ease our burden and took very little effort on our part.

Until next time, Happy Coding!

Resources

You can find the code for this series of posts on GitHub at: https://github.com/Monotoba/Building-Machines-In-Code

Series Navigation<< Building Machines In Code – Part 5Building Machines In Code – Part 7 >>

Leave a Reply

Your email address will not be published.