Skip to main content

A minimal, 8-bit CPU emulator designed to help developers explore low-level computing concepts.

Project description

ByteCore Emulator

ByteCore is a simple 8-bit CPU design following Von Neumann architecture (The design where both instructions and data share the same memory address space) capable of addressing 65536 (64KB) bytes of memory.

ByteCore Emulator is the Python implementation of the ByteCore CPU to test out the instruction set.

Registers

  • Accumulator (A): An 8-bit register where arithmetic and logic operations are carried out.
  • Program Counter (PC): A 16-bit register which keeps track of the address of the current instruction being executed and points to the next instruction to be executed.

Internal Registers

In addition to the Accumulator and Program Counter registers, the ByteCore CPU uses a few internal registers to manage its operations:

  • Temp Register (T): An 8-bit register used by the CPU to temporarily store data during arithmetic and logic operations.
  • Memory Address Register (MAR): A 16-bit register used by the CPU to hold the address to be accessed during memory operations.
  • Increment Register (IR): A constant register, permanently set to 1, used by the CPU specifically to increment the Program Counter.

These registers are used internally by the CPU and are not directly accessible to user programs. They are essential to the operation of the CPU and understanding their role can help users better understand the internal workings of the ByteCore CPU.

Addressing Modes

The ByteCore CPU uses a big-endian memory architecture. In this design, the most significant byte (MSB) of a multi-byte value is stored at the smallest memory address.

  • Extended Mode: Each instruction is directly followed in memory by a 16-bit address. This address is stored across two consecutive memory locations, with the most significant byte in the first location.

Note: All uninitialized memory defaults to 0x0.

Word Size

In the context of the ByteCore CPU architecture, a "word" is defined as an 8-bit unit of data, consistent with its 8-bit design. This is the primary unit of data that can be manipulated with arithmetic and logic instructions.

A "double word" is a 16-bit unit of data, or two consecutive 8-bit words. This is primarily used in memory addressing, allowing the ByteCore CPU to access 65536 ($2^{16}$) individual memory locations.

Instructions

Each instruction is represented by a unique 8-bit operation code, commonly abbreviated as "opcode". The instructions supported by the ByteCore CPU are as follows:

Opcode (Binary) Opcode (Hexadecimal) Opcode (Decimal) Instruction Description
00000000 0x0 0 HALT Stop execution.
00000001 0x1 1 LOAD Load the value from an extended memory address into the accumulator.
00000010 0x2 2 STORE Store the value in the accumulator into an extended memory address.
00000100 0x4 4 ADD Add the value from an extended memory address to the value in the accumulator.
00001000 0x8 8 SUB Subtract the value from an extended memory address from the value in the accumulator.
00010000 0x10 16 JMP Unconditional jump to an extended memory address.
00100000 0x20 32 JZ Conditional jump to an extended memory address if the accumulator is zero.

Note: The opcode for HALT being 0x0 means that any uninitialized memory (which defaults to 0x0) will also halt the CPU, providing a fail-safe for programs that exceed their intended bounds.

Note: The opcode assignments are set up so that each opcode has exactly one bit set to 1. This is chosen to simplify hardware implementation where it will be possible to use a bit-selection logic to decode the opcodes.

HALT Instruction

The HALT instruction (opcode 0x0) stops the CPU from processing further instructions. When a HALT instruction is encountered, the CPU will enter a loop where it continually fetches and decodes the HALT instruction without incrementing the Program Counter (PC). This effectively suspends the CPU's operation until an external event occurs or the emulation is manually stopped. This behavior can be particularly useful for handling error situations or stopping the CPU after a program has finished execution.

Unrecognized Opcodes

In the ByteCore CPU, any opcode that does not correspond to a defined instruction is treated as an invalid opcode. To ensure the stability and predictability of the system, the CPU defaults to executing the HALT operation when it encounters an invalid opcode.

This fail-safe mechanism prevents the execution of unintended or unknown instructions, aiding in error detection and debugging. It's important to note that when the CPU halts unexpectedly, it may be due to an unrecognized opcode in the instruction set.

Instruction Cycle

This is a description of the instruction cycle, also known as the fetch-decode-execute cycle. The steps look like this:

  1. Fetch: The CPU fetches the instruction from memory. This is the location pointed to by the program counter (PC).
  2. Decode: The CPU decodes the instruction to determine what operation to perform.
  3. Evaluate Address MSB: If the instruction involves data from memory, the CPU calculates the address of the most significant byte (MSB) of the data.
  4. Fetch Operand MSB: If the instruction involves data from memory, the CPU fetches the most significant byte (MSB) of the data.
  5. Evaluate Address LSB: The CPU calculates the address of the least significant byte (LSB) of the data.
  6. Fetch Operand LSB: The CPU fetches the least significant byte (LSB) of the data.
  7. Execute: The CPU performs the operation.
  8. Store Result: If the operation produces a result, the CPU stores it in the appropriate place (either a register or a memory location).
  9. Increment PC: The CPU increments the program counter to point to the next instruction.

These nine steps form one complete instruction cycle. The speed at which the CPU can complete these steps is determined by its clock speed. Each tick of the clock allows the CPU to move one step forward in the cycle.

Boundary Conditions

The ByteCore CPU handles certain edge cases as follows:

  • Accumulator Overflow and Underflow: The accumulator (A register) is an 8-bit register. If the result of an operation would be greater than 0xFF or less than 0x00, the value wraps around to fit within 8 bits. For example, if the accumulator holds the value 0xFF and an ADD instruction adds 1 to it, the result will be 0x00 (overflow). Similarly, if the accumulator holds the value 0x00 and a SUB instruction subtracts 1 from it, the result will be 0xFF (underflow).
  • Memory Address Overflow: If an instruction attempts to read or write to an address that's outside the valid memory range (greater than 0xFFFF), the address wraps around to start from 0 again. For example, if a LOAD or STORE instruction tries to access the address 0x10000, it will instead access address 0x0000.

Running the Emulator

Follow these steps to run the emulator.

Manual Setup

Ensure that you have Python 3.11 or newer installed on your system.

Clone the repository and navigate into the root directory. You may want to create a Python virtual environment to isolate the project's dependencies.

To install the necessary dependencies, run:

pip3 install -r requirements.txt

Gitpod Setup

You can also use Gitpod to run the emulator by clicking here.

Running the Emulator

Create a new Python file (e.g., example.py) in the src directory and add the following import statements:

from bytecore.memory import Memory
from bytecore.byte import Byte
from bytecore.opcode import Opcode
from bytecore.emulator import ByteCore

Next, initialize a 64KB array of bytes as the memory:

memory_bytes = Memory.get_default_memory_bytes()

You can then load your program into memory. Here is a simple example which loads the HALT instruction:

memory_bytes[0] = Opcode.HALT

Instantiate the emulator with the provided memory:

byte_core = ByteCore(memory_bytes)

You now have three options for running the emulator:

  1. Execute a single step of the current instruction cycle:

    byte_core.step()
    
  2. Complete the current instruction cycle:

    byte_core.cycle()
    
  3. Execute instructions until the HALT state is reached:

    byte_core.cycle_until_halt()
    

To inspect the state of memory at any point, use:

dump = byte_core.dump()
memory_dump = dump.memory

You can print the entire memory dump, though please note that this will print 64KB of data to your screen:

print(memory_dump)

For a more manageable output, consider printing a slice of the memory:

print(memory_dump[0:20])

Finally, to run your example.py file, use:

python src/example.py

Example Programs

Below, you'll find two example programs demonstrating how to utilize the ByteCore emulator. The first is a basic introduction, while the second one engages all the opcodes.

Example 1: Simple Program

This program begins with a simple syntax using hexadecimal numbers for ease of drafting. It's then converted into a format the emulator understands, which uses decimal numbers (0 to 255) encapsulated in a Byte class.

# Initial draft in hexadecimal:
# 00 00 LOAD
# 00 01 00
# 00 02 0A
# 00 03 ADD
# 00 04 00
# 00 05 0B
# 00 06 STORE
# 00 07 FF
# 00 08 FF
# 00 09 HALT
# 00 0A 14; 20
# 00 0B 1E; 30

Conversion to a format that the emulator understands:

from bytecore.memory import Memory
from bytecore.byte import Byte
from bytecore.opcode import Opcode
from bytecore.emulator import ByteCore

memory_bytes = Memory.get_default_memory_bytes()

memory_bytes[0] = Opcode.LOAD
memory_bytes[1] = Byte(0)
memory_bytes[2] = Byte(10)
memory_bytes[3] = Opcode.ADD
memory_bytes[4] = Byte(0)
memory_bytes[5] = Byte(11)
memory_bytes[6] = Opcode.STORE
memory_bytes[7] = Byte(255)
memory_bytes[8] = Byte(255)
memory_bytes[9] = Opcode.HALT
memory_bytes[10] = Byte(20)
memory_bytes[11] = Byte(30)

byte_core = ByteCore(memory_bytes)
byte_core.cycle_until_halt()
dump = byte_core.dump()

dump.memory[-1]  # equals 50

To aid understanding, here is a Python equivalent of the ByteCore program:

output = 0

a = 20
b = 30
accumulator = a       # LOAD
accumulator += b      # ADD
output = accumulator  # STORE
#                     # HALT

print(output)  # prints 50

Example 2: Advanced Program

This advanced example begins similarly, drafting in hexadecimal. The transition to the emulator's format benefits from a custom builder class, streamlining the assembly of Byte instances.

# Initial draft in hexadecimal:
# 00 00 JMP
# 00 01 FE
# 00 02 00

# 01 00 37; 55
# 01 01 14; 20
# 01 02 02;  2
# 01 03 01;  1

# FE 00 LOAD
# FE 01 01
# FE 02 00
# FE 03 ADD
# FE 04 01
# FE 05 02
# FE 06 STORE
# FE 07 01
# FE 08 00
# FE 09 LOAD
# FE 0A 01
# FE 0B 01
# FE 0C SUB
# FE 0D 01
# FE 0E 03
# FE 0F STORE
# FE 10 01
# FE 11 01
# FE 12 LOAD
# FE 13 01
# FE 14 01
# FE 15 JZ
# FE 16 FF
# FE 17 00
# FE 18 JMP
# FE 19 FE
# FE 1A 00

# FF 00 LOAD
# FF 01 01
# FF 02 00
# FF 03 STORE
# FF 04 FF
# FF 05 FF
# FF 06 HALT

Translation to the emulator-friendly format:

from bytecore.memory_bytes_builder import MemoryBytesBuilder
from bytecore.emulator import ByteCore

memory_bytes = MemoryBytesBuilder()\
    .msb('00').lsb('00').jmp()\
    .msb('00').lsb('01').data('FE')\
    .msb('00').lsb('02').data('00')\
    \
    .msb('01').lsb('00').data('37').comment('55')\
    .msb('01').lsb('01').data('14').comment('20')\
    .msb('01').lsb('02').data('02').comment(' 2')\
    .msb('01').lsb('03').data('01').comment(' 1')\
    \
    .msb('FE').lsb('00').load()\
    .msb('FE').lsb('01').data('01')\
    .msb('FE').lsb('02').data('00')\
    .msb('FE').lsb('03').add()\
    .msb('FE').lsb('04').data('01')\
    .msb('FE').lsb('05').data('02')\
    .msb('FE').lsb('06').store()\
    .msb('FE').lsb('07').data('01')\
    .msb('FE').lsb('08').data('00')\
    .msb('FE').lsb('09').load()\
    .msb('FE').lsb('0A').data('01')\
    .msb('FE').lsb('0B').data('01')\
    .msb('FE').lsb('0C').sub()\
    .msb('FE').lsb('0D').data('01')\
    .msb('FE').lsb('0E').data('03')\
    .msb('FE').lsb('0F').store()\
    .msb('FE').lsb('10').data('01')\
    .msb('FE').lsb('11').data('01')\
    .msb('FE').lsb('12').load()\
    .msb('FE').lsb('13').data('01')\
    .msb('FE').lsb('14').data('01')\
    .msb('FE').lsb('15').jz()\
    .msb('FE').lsb('16').data('FF')\
    .msb('FE').lsb('17').data('00')\
    .msb('FE').lsb('18').jmp()\
    .msb('FE').lsb('19').data('FE')\
    .msb('FE').lsb('1A').data('00')\
    \
    .msb('FF').lsb('00').load()\
    .msb('FF').lsb('01').data('01')\
    .msb('FF').lsb('02').data('00')\
    .msb('FF').lsb('03').store()\
    .msb('FF').lsb('04').data('FF')\
    .msb('FF').lsb('05').data('FF')\
    .msb('FF').lsb('06').halt()\
    .build()

byte_core = ByteCore(memory_bytes)
byte_core.cycle_until_halt()
dump = byte_core.dump()

dump.memory[-1]  # equals 95

For further clarity, here's the Python equivalent of the ByteCore advanced program:

output = 0

#                         # JMP to 1
a = 55
b = 20
c = 2
d = 1
while True:
    #                     # 1
    accumulator = a       # LOAD
    accumulator += c      # ADD
    a = accumulator       # STORE
    accumulator = b       # LOAD
    accumulator -= d      # SUB
    b = accumulator       # STORE
    accumulator = b       # LOAD
    if accumulator == 0:  # JZ to 2
        break
    #                     # JMP to 1

#                         # 2
accumulator = a           # LOAD
output = accumulator      # STORE
#                         # HALT

print(output)  # prints 95

License

This project is licensed under the terms of the MIT License. See the LICENSE file for the full text.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bytecore-1.1.1.tar.gz (21.2 kB view hashes)

Uploaded Source

Built Distribution

bytecore-1.1.1-py3-none-any.whl (16.6 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page