A blit cost ten cycles, which were the five port writes that set it up. The quarter of a kilobyte that moved cost nothing, and no hardware moves a quarter of a kilobyte for nothing. BANKS ARE SEPARATE MEMORIES, AND THAT IS WHAT SETS THE RATE. A move between two of them can overlap its read and its write - fetch the next byte while the last one is stored - so it settles at a byte a cycle. A move within one bank cannot, and costs two. A fill has nothing to read and costs one whatever the banks are. The odd cycle on each is the pipeline filling. That is not a modelling choice so much as a reading of the structure the machine already has: a Program to Data blit is inherently twice the rate of a Data to Data one, and it is legible why. Measured: 256 bytes is 297 cycles across banks and 518 within one, both including the instructions that ask for it. WHAT IT TAUGHT, which was not what I expected. Charging for movement costs the native assembler 0.4 per cent and costs directory work 13.4. The assembler reads a block and then thinks about it for a long time, so the move is amortised into nothing; the filesystem reads a block in order to look at it and does nothing else in between. So the case for a blitter that runs alongside the CPU is weaker than it sounds. Concurrency pays when there is other work to do during the transfer, and the place that spends its time moving memory is exactly the place with nothing else to do - it blits a block precisely so that it can read it. What that workload wants is a FASTER controller, not a concurrent one: a wider data path halves the wait, and the machine is waiting either way. Video is the case that would still want concurrency, since a frame can be moved while the next one is worked out. That is an argument about software nobody has written yet, and it is now an argument with numbers on the other side of it. The byte at a time port is charged too, for the byte it moves beyond reaching the port. Nothing polls CTRL_STATUS, so the transfer stalls whoever asked for it, which is the conservative reading and the one the software already assumes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
114 lines
4.2 KiB
C
114 lines
4.2 KiB
C
// controller.h
|
|
// The SplitBit Memory Controller.
|
|
// Written by Anachronaut
|
|
|
|
#ifndef CONTROLLER_H
|
|
#define CONTROLLER_H
|
|
|
|
#include <stdint.h>
|
|
|
|
// ---- Ports ----
|
|
//
|
|
// Sixteen registers, one to a port, written and read directly. A register file behind a
|
|
// single port would be smaller but stateful, and losing your place in a device that
|
|
// moves memory corrupts memory rather than an answer.
|
|
|
|
#define CONTROLLER_PORT_BASE 0xE0
|
|
#define CONTROLLER_PORT_TOP 0xEF
|
|
|
|
#define CTRL_SOURCE_BANK 0xE0
|
|
#define CTRL_SOURCE_HIGH 0xE1
|
|
#define CTRL_SOURCE_LOW 0xE2
|
|
#define CTRL_DEST_BANK 0xE3
|
|
#define CTRL_DEST_HIGH 0xE4
|
|
#define CTRL_DEST_LOW 0xE5
|
|
#define CTRL_LENGTH_HIGH 0xE6
|
|
#define CTRL_LENGTH_LOW 0xE7
|
|
#define CTRL_COMMAND 0xE8
|
|
#define CTRL_DATA 0xE9
|
|
#define CTRL_STATUS 0xEA
|
|
#define CTRL_GUARD_BANK 0xEB
|
|
#define CTRL_GUARD_START_HIGH 0xEC
|
|
#define CTRL_GUARD_START_LOW 0xED
|
|
#define CTRL_GUARD_END_HIGH 0xEE
|
|
#define CTRL_GUARD_END_LOW 0xEF
|
|
|
|
// ---- Commands ----
|
|
//
|
|
// Written to the Command port, which performs them at once. A blit is instantaneous from
|
|
// the CPU's point of view: waiting belongs to the peripheral that has something to wait
|
|
// for, not to the moving of bytes.
|
|
|
|
#define COMMAND_BLIT 0x01
|
|
#define COMMAND_FILL 0x02
|
|
|
|
// Gives a bank number to the memory owned by a device. How big it is comes from the
|
|
// device, not from software: a program asserting a hardware fact could only ever be
|
|
// wrong about it.
|
|
#define COMMAND_REGISTER_BANK 0x03
|
|
|
|
// Raises and lowers the fence over the bank named by GuardBank. Any program may do
|
|
// either: this is a fence rather than a wall, and nobody is ever told no. What it stops
|
|
// is walking into something by accident, not walking into it on purpose.
|
|
#define COMMAND_GUARD_ON 0x10
|
|
#define COMMAND_GUARD_OFF 0x11
|
|
|
|
// ---- Banks ----
|
|
//
|
|
// Program and Data are banks like any other; being banks 0 and 1 is the only thing
|
|
// special about them. Bank 2 is the controller's own memory, and the bank table lives
|
|
// in it, which is how anything finds out what banks exist without a second protocol.
|
|
|
|
#define BANK_PROGRAM 0
|
|
#define BANK_DATA 1
|
|
#define BANK_TABLE 2
|
|
#define BANK_COUNT 256
|
|
|
|
// Eight bytes each, so bank n's record begins at n * 8.
|
|
//
|
|
// 0 Flags
|
|
// 1 The port that owns it, or the machine itself for banks 0 to 2
|
|
// 2 - 3 Capacity, where zero means the whole 64K
|
|
// 4 - 5 First guarded address
|
|
// 6 - 7 Last guarded address
|
|
//
|
|
// What is published is a description. The pointer a bank really holds is never in here:
|
|
// a program that could write one would be setting a host address, which means nothing on
|
|
// hardware and everything to the emulator running it.
|
|
#define BANK_RECORD_BYTES 8
|
|
#define BANK_TABLE_BYTES (BANK_COUNT * BANK_RECORD_BYTES)
|
|
|
|
#define BANK_FLAG_PRESENT 0x01
|
|
#define BANK_FLAG_READ_ONLY 0x02
|
|
#define BANK_FLAG_GUARDED 0x04
|
|
|
|
// Banks 0 to 2 belong to the machine rather than to any device.
|
|
#define BANK_OWNER_MACHINE 0xFF
|
|
|
|
// ---- What the controller's work costs ----
|
|
//
|
|
// The controller moves memory, and memory takes time to move: a byte has to be read from
|
|
// somewhere and written somewhere else. A blit is not free just because the machine issues
|
|
// it with one instruction, and pretending otherwise made a quarter of a millisecond of
|
|
// work look like ten cycles.
|
|
//
|
|
// Banks are separate memories, which is what decides the rate. A move between two of them
|
|
// can overlap its read and its write - fetch the next byte while the last one is stored -
|
|
// so it settles at a byte a cycle. A move WITHIN one bank cannot, and costs two. A fill has
|
|
// nothing to read and costs one whatever the banks are.
|
|
//
|
|
// Returned and cleared, so the caller adds it to whatever it is charging for. The CPU picks
|
|
// it up after each port access, which makes the transfer a stall: the machine issues a blit
|
|
// and waits for it. Whether real hardware would let the two run at once is a live question -
|
|
// the memories are separate, so it plausibly could - and the answer wants measuring before
|
|
// it is designed.
|
|
unsigned long controllerTakeCycles(void);
|
|
|
|
void initializeController(uint8_t *programMemory, uint8_t *dataMemory);
|
|
|
|
uint8_t controllerWrite(uint8_t value, uint8_t port);
|
|
|
|
uint8_t controllerRead(uint8_t port);
|
|
|
|
#endif // CONTROLLER_H
|