Creating the scaffold
So, to avoid losing more time than I did just trying to wrap my head around the idea of not being able of even run an "add" without having to write it myself, I did't lost to much time and created the folders, it's index.html and bytepusher.js.
The HTML couldn't be simpler:
<body>
<canvas
width="256"
height="256"
style="transform: scale(4); transform-origin: top left"
id="canvas"
></canvas>
<script src="bytepusher.js"></script>
</body>Basically, I'm just creating a canvas that would be used for the screen and scaling it some inline css in order to better see the pixels. Then, I'm importing a script that would be the BytePusher(BP) itself.
For now, the JS file is empty.
Understanding the specifications
From the Wiki page, the BP Specifications are:
| Component | Specification |
|---|---|
| Framerate | 60 frames per second |
| CPU | ByteByteJump with 3-byte addresses |
| CPU speed | 65536 instructions per frame (3932160 instructions per second, ~3.93 MHz) |
| Byte ordering | Big-endian |
| Memory | 16 MiB RAM |
| Graphics | 256*256 pixels, 1 byte per pixel, 216 fixed colors |
| Sound | 8-bit mono, signed values. 256 samples per frame (15360 samples per second), single-channel |
| Keyboard | 16 keys, organized into 4 rows by 4 columns (which I'll map to arrows, ASDFZXCVB, Enter, Esc, Space and LeftShift) |
So, based on this, we should have 5 main devices to work on:
- The CPU itself
- The Memory
- The 256*256 display
- Soundboard
- Keyboard
And we have to program it by ourselves. Yay!
The Memory Map
In order to properly programm the CPU, we have to understand what does it access at each loop, so here is the memory map:
| Address | Bytes | Description |
|---|---|---|
| 0 | 2 | Keyboard state. Each bit corresponds to a key on the keyboard, with bit X representing key X. A value of 1 means the key is pressed down, while 0 means it is not. |
| 2 | 3 | Program counter fetch address. This address is fetched at the beginning of each frame and used as the starting point for program execution. |
| 5 | 1 | Pixel address. The value ZZ represents the page number, and XX and YY represent the horizontal and vertical coordinates of the pixel respectively. For a given pixel at coordinates (XX, YY) on page ZZ, the address can be calculated as ZZYYXX. |
| 6 | 2 | Audio sample address. The value XXYY represents the address of an audio sample, with XX and YY giving the upper and lower bits of the address respectively. The third byte ZZ represents the actual audio sample data stored at that address. (Won't do now) |
And the outer loop is:
- Wait for the next timer tick (60 ticks are generated per second).
- Poll the keys and store their states as a 2-byte value at address 0.
- Fetch the 3-byte program counter from address 2, and execute exactly 65536 instructions.
- Send the 64-KiB pixeldata block designated by the byte value at address 5 to the display device. Send the 256-byte sampledata block designated by the 2-byte value at address 6 to the audio device.
- Go back to step 1.
Or, in other words, for every second:
- Create a let tick = 60;
- Create a decresing
forloop fortick; - Iterate through all the current pressed keys and store their state at their respective memory;
- Create another for loop, this time for each instruction;
- After leaving the for loop, send the address that starts the 64kb byte array (256 * 256) for the Pixel Address to draw in the display
- Soon after it, send the address that starts the 256 byte array for the Audio Address to play it on the sound device
- Wait until next frame
- Go to the step 1.
Based on it, let's create the main loop on the bytepusher.js:
function main() {
setInterval(() => {
let ticksLeft = 60;
while (ticksLeft) {
ticksLeft--;
}
}, 1000);
}Is not much, but is honest work.
Right after this, I created the memories:
const MEMORY_MAP = new Uint8Array(2 ** 6);
const MEMORY_ROM = new Uint8Array(2 ** 27);
const PAGE_SIZE = 2 ** 3;And update the loop function to be somewhat more readable;
function main() {
setInterval(() => {
const KEYS_MAP = MEMORY_MAP.slice(0x0, PAGE_SIZE * 2);
const INSTRUCTIONS_MAP = MEMORY_MAP.slice(
KEYS_MAP.length,
KEYS_MAP.length + PAGE_SIZE * 3
);
const GFX_MAP = MEMORY_MAP.slice(
INSTRUCTIONS_MAP.length,
INSTRUCTIONS_MAP.length + PAGE_SIZE * 1
);
const SOUND_MAP = MEMORY_MAP.slice(
GFX_MAP.length,
GFX_MAP.length + PAGE_SIZE * 2
);
let ticksLeft = 60;
while (ticksLeft) {
ticksLeft--;
}
drawToCanvas();
}, 1000);
}INFO
Worth noting that I have no clue of what I'm doing, but I keep coding as long as it seems to work and make sense (in my actual mind).
Then I took the oportunity to create an initMemory() function for default values;
Okay. Nevermind.
While I was trying to solve, the code changed a lot as I found out way better ways to achieve something.
Instead of writing a tutorial or whatever, I'll wait until I get a working display and explain the code here.