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:
- Fetch: The CPU fetches the instruction from memory. This is the location pointed to by the program counter (PC).
- Decode: The CPU decodes the instruction to determine what operation to perform.
- Evaluate Address MSB: If the instruction involves data from memory, the CPU calculates the address of the most significant byte (MSB) of the data.
- Fetch Operand MSB: If the instruction involves data from memory, the CPU fetches the most significant byte (MSB) of the data.
- Evaluate Address LSB: The CPU calculates the address of the least significant byte (LSB) of the data.
- Fetch Operand LSB: The CPU fetches the least significant byte (LSB) of the data.
- Execute: The CPU performs the operation.
- Store Result: If the operation produces a result, the CPU stores it in the appropriate place (either a register or a memory location).
- 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:
-
Execute a single step of the current instruction cycle:
byte_core.step()
-
Complete the current instruction cycle:
byte_core.cycle()
-
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
Built Distribution
File details
Details for the file bytecore-1.1.2.tar.gz
.
File metadata
- Download URL: bytecore-1.1.2.tar.gz
- Upload date:
- Size: 21.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/5.1.0 CPython/3.12.4
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | ab97b07ead947e0f91d14bbf5f40a28725c709782f6ac70240f40cfdb239fabe |
|
MD5 | 3bc2a8b5589721816d117349d8f77c70 |
|
BLAKE2b-256 | 7f4b8fc90aac1267bdf51a5f480bc93bc9fc325c2e46f3ef85e9514285b23ef8 |
File details
Details for the file bytecore-1.1.2-py3-none-any.whl
.
File metadata
- Download URL: bytecore-1.1.2-py3-none-any.whl
- Upload date:
- Size: 16.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/5.1.0 CPython/3.12.4
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 04c3b53657d604d85976df84aea0f020b381d5a7cf027ce5b9d75f7b64aa7cd5 |
|
MD5 | 151d91013e4121a5fe5de2fbc9b31b3f |
|
BLAKE2b-256 | f548848ead04df4a16f8e27857a0decc0d1c860817b548a704620e2f2d3cec5f |