diff --git a/README.md b/README.md index 781997d..e8ed6c0 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,18 @@ anybody could build - and it is the emulator's job to be the thing the hardware against. The average SplitBit instruction costs 3.72 cycles, measured over the native assembler -assembling a program. Whether real hardware would overlap a fetch with the end of the +assembling a program. + +**The memory controller is charged for what it moves**, on the same terms. Banks are +separate memories, and that is what sets the rate: a move between two of them can overlap +its read and its write, so it settles at a byte a cycle, while a move within one bank cannot +and costs two. A fill has nothing to read and costs one. So a 256 byte block is 257 cycles +between banks and 513 within one, against the ten it used to cost - which was the five port +writes that set it up and nothing for the quarter of a kilobyte that moved. + +The transfer stalls the program that asked for it. Whether hardware would let the two run at +once is left open, the same way pipelining is: the memories are separate, so it plausibly +could, and the measurements say it would buy less than it sounds like. Whether real hardware would overlap a fetch with the end of the previous instruction is left open, and deliberately: this is the conservative model, and pipelining is a decision to make while drawing the hardware rather than one to inherit from an emulator. diff --git a/Source/Emulator/controller.c b/Source/Emulator/controller.c index e3182cc..396b466 100644 --- a/Source/Emulator/controller.c +++ b/Source/Emulator/controller.c @@ -152,6 +152,15 @@ static int rangeWritable(uint8_t bank, uint16_t address, uint32_t count) { return 1; } +// What the moves below have cost since anybody last asked. +static unsigned long pendingCycles = 0; + +unsigned long controllerTakeCycles(void) { + unsigned long taken = pendingCycles; + pendingCycles = 0; + return taken; +} + static void doBlit(void) { uint32_t count = transferLength(); if (!rangeReadable(sourceBank, sourceAddress, count)) { @@ -166,6 +175,9 @@ static void doBlit(void) { // designing against. memmove(banks[destBank].memory + destAddress, banks[sourceBank].memory + sourceAddress, count); + // A byte read and a byte written. Two banks are two memories and the pair overlaps; + // one bank is one memory and they do not. The odd cycle is the pipeline filling. + pendingCycles += (sourceBank == destBank) ? 2 * count + 1 : count + 1; sourceAddress = (uint16_t)(sourceAddress + count); destAddress = (uint16_t)(destAddress + count); status = 0; @@ -178,6 +190,7 @@ static void doFill(void) { } // A fill has nowhere to read from, only a value, so SourceLow carries the byte and // the rest of the source registers mean nothing here. + pendingCycles += count + 1; memset(banks[destBank].memory + destAddress, (int)(sourceAddress & 0xFF), count); destAddress = (uint16_t)(destAddress + count); status = 0; @@ -261,6 +274,7 @@ uint8_t controllerWrite(uint8_t value, uint8_t port) { // A byte into the destination, and the address steps on so that writing a // run of bytes is a loop over one instruction rather than four. if (canWrite(destBank, destAddress)) { + pendingCycles++; // The byte itself, beyond reaching the port. banks[destBank].memory[destAddress] = value; if (destBank == BANK_TABLE) { // Unreachable while the table is read only, and here so that it stays @@ -321,6 +335,7 @@ uint8_t controllerRead(uint8_t port) { if (!canRead(sourceBank, sourceAddress)) { return 0; } + pendingCycles++; // As above, the other way round. uint8_t value = banks[sourceBank].memory[sourceAddress]; sourceAddress++; status = 0; diff --git a/Source/Emulator/controller.h b/Source/Emulator/controller.h index 004caa5..c3bdafe 100644 --- a/Source/Emulator/controller.h +++ b/Source/Emulator/controller.h @@ -85,6 +85,25 @@ // 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); diff --git a/Source/Emulator/cpu.c b/Source/Emulator/cpu.c index 27f6f59..d8f1262 100644 --- a/Source/Emulator/cpu.c +++ b/Source/Emulator/cpu.c @@ -5,6 +5,7 @@ #include "cpu.h" #include "io.h" +#include "controller.h" #include "../Assembler/assembly.h" // For the vector table layout, which both tools share. uint16_t shiftRegister; @@ -38,11 +39,16 @@ static inline void writeData(CPURegisters *cpu, uint16_t at, uint8_t value) { static inline void portOut(CPURegisters *cpu, uint8_t value, uint8_t port) { cpu->busCycles++; OutputHandler(value, port); + // And whatever memory that made the controller move. The machine waits for it, which + // is the conservative reading: a blit stalls the program that asked for one. + cpu->busCycles += controllerTakeCycles(); } static inline uint8_t portIn(CPURegisters *cpu, uint8_t port) { cpu->busCycles++; - return InputHandler(port); + uint8_t value = InputHandler(port); + cpu->busCycles += controllerTakeCycles(); + return value; } static uint16_t readVector(const uint8_t *programMemory, uint16_t base, uint8_t index) {