Give the memory controller to a bus rather than to the machine
The third and largest piece of the peripheral core, and like the two before it nothing changes: 169 tests, and the two that would notice a misroute pass loudest of all. A CONTROLLER IS THE MOST STATEFUL THING ON THIS MACHINE - a source bank and address, a destination, a length, five guard registers and a table describing every bank it can reach. Two processors sharing one would interleave into nonsense: one sets a source, the other sets a destination, the first issues a blit and moves the wrong bytes somewhere else again. No arbitration fixes that, because there is nothing to arbitrate - both writes were legal and the result belongs to neither of them. So it is a struct threaded through all seventeen functions that touch it, rather than a pointer to a current one set on the way in. The smaller change was tempting and is the shape that produced the stale reset flag and the editor's IsNew surviving a second run, both this week: state that has to be set on the right path before anything reads it. A controller is where that goes wrong quietly rather than loudly. THE STATICS WERE DELETED RATHER THAN LEFT, which is what made this safe to do mechanically. A missed reference is a compile error rather than a variable that still exists and serves the wrong core, so "did I catch all 165?" stopped being a judgement and became a question the compiler answered. Two things the transformation nearly got wrong, both caught by reading rather than by building. guardStart and guardEnd are fields of Bank as well as registers of the controller, so banks[n].guardStart had to keep its name while a bare guardStart changed - the difference between a fence and the register about to be written into one. And a definition and a call look alike enough that the first attempt turned publishBank(number) into publishBank(Controller *c, number); definitions start at column zero here and calls never do. Tests/agree.sh is what says this is right, more than the count does. It builds the same disk with SplitDisk and with CosmOS and compares byte for byte, and every filesystem operation on the machine goes through the controller - so a blit that went to the wrong place would corrupt a disk that is checked against one built by different code entirely. Tests/cycles.sh covers the other half, since what a transfer costs depends on pendingCycles landing in the right one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
co-authored by
Claude Opus 5
parent
5be5bea994
commit
33afc20abc
+150
-172
@@ -14,152 +14,130 @@
|
||||
#include "../Assembler/assembly.h" // For the fault vector numbers.
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
uint8_t *memory; // Never published. See the note in controller.h.
|
||||
uint32_t capacity; // In bytes. A full bank is 65536, which is why this is not 16 bit.
|
||||
uint8_t flags;
|
||||
uint8_t ownerPort;
|
||||
uint16_t guardStart;
|
||||
uint16_t guardEnd;
|
||||
} Bank;
|
||||
|
||||
static Bank banks[BANK_COUNT];
|
||||
|
||||
// Bank 2's contents: the description of every bank, for anything that wants to read it.
|
||||
static uint8_t bankTable[BANK_TABLE_BYTES];
|
||||
|
||||
// The registers, exactly as the ports name them.
|
||||
static uint8_t sourceBank, destBank, guardBank;
|
||||
static uint16_t sourceAddress, destAddress, length;
|
||||
static uint16_t guardStart, guardEnd;
|
||||
static uint8_t status;
|
||||
|
||||
// Writes a bank's description into the table that bank 2 publishes. Called whenever
|
||||
// anything about a bank changes, so the published table and the real one cannot drift.
|
||||
static void publishBank(int number) {
|
||||
uint8_t *record = bankTable + number * BANK_RECORD_BYTES;
|
||||
record[0] = banks[number].flags;
|
||||
record[1] = banks[number].ownerPort;
|
||||
static void publishBank(Controller *c, int number) {
|
||||
uint8_t *record = c->bankTable + number * BANK_RECORD_BYTES;
|
||||
record[0] = c->banks[number].flags;
|
||||
record[1] = c->banks[number].ownerPort;
|
||||
// Zero means the whole 64K, the same convention Length uses, because a capacity of
|
||||
// nothing is never what anyone meant.
|
||||
record[2] = (uint8_t)((banks[number].capacity >> 8) & 0xFF);
|
||||
record[3] = (uint8_t)(banks[number].capacity & 0xFF);
|
||||
record[4] = (uint8_t)(banks[number].guardStart >> 8);
|
||||
record[5] = (uint8_t)(banks[number].guardStart & 0xFF);
|
||||
record[6] = (uint8_t)(banks[number].guardEnd >> 8);
|
||||
record[7] = (uint8_t)(banks[number].guardEnd & 0xFF);
|
||||
record[2] = (uint8_t)((c->banks[number].capacity >> 8) & 0xFF);
|
||||
record[3] = (uint8_t)(c->banks[number].capacity & 0xFF);
|
||||
record[4] = (uint8_t)(c->banks[number].guardStart >> 8);
|
||||
record[5] = (uint8_t)(c->banks[number].guardStart & 0xFF);
|
||||
record[6] = (uint8_t)(c->banks[number].guardEnd >> 8);
|
||||
record[7] = (uint8_t)(c->banks[number].guardEnd & 0xFF);
|
||||
}
|
||||
|
||||
static void defineBank(int number, uint8_t *memory, uint32_t capacity, uint8_t flags, uint8_t owner) {
|
||||
banks[number].memory = memory;
|
||||
banks[number].capacity = capacity;
|
||||
banks[number].flags = flags | BANK_FLAG_PRESENT;
|
||||
banks[number].ownerPort = owner;
|
||||
banks[number].guardStart = 0;
|
||||
banks[number].guardEnd = 0;
|
||||
publishBank(number);
|
||||
static void defineBank(Controller *c, int number, uint8_t *memory, uint32_t capacity, uint8_t flags, uint8_t owner) {
|
||||
c->banks[number].memory = memory;
|
||||
c->banks[number].capacity = capacity;
|
||||
c->banks[number].flags = flags | BANK_FLAG_PRESENT;
|
||||
c->banks[number].ownerPort = owner;
|
||||
c->banks[number].guardStart = 0;
|
||||
c->banks[number].guardEnd = 0;
|
||||
publishBank(c, number);
|
||||
}
|
||||
|
||||
void initializeController(uint8_t *programMemory, uint8_t *dataMemory) {
|
||||
memset(banks, 0, sizeof(banks));
|
||||
memset(bankTable, 0, sizeof(bankTable));
|
||||
void initializeController(Controller *c, uint8_t *programMemory, uint8_t *dataMemory) {
|
||||
memset(c->banks, 0, sizeof(c->banks));
|
||||
memset(c->bankTable, 0, sizeof(c->bankTable));
|
||||
for (int i = 0; i < BANK_COUNT; i++) {
|
||||
banks[i].ownerPort = BANK_OWNER_MACHINE;
|
||||
publishBank(i);
|
||||
c->banks[i].ownerPort = BANK_OWNER_MACHINE;
|
||||
publishBank(c, i);
|
||||
}
|
||||
|
||||
defineBank(BANK_PROGRAM, programMemory, 0x10000, 0, BANK_OWNER_MACHINE);
|
||||
defineBank(BANK_DATA, dataMemory, 0x10000, 0, BANK_OWNER_MACHINE);
|
||||
defineBank(c, BANK_PROGRAM, programMemory, 0x10000, 0, BANK_OWNER_MACHINE);
|
||||
defineBank(c, BANK_DATA, dataMemory, 0x10000, 0, BANK_OWNER_MACHINE);
|
||||
// The table describes itself, so a program that walks it finds bank 2 in there along
|
||||
// with everything else. It is read only, which is what keeps RegisterBank the only
|
||||
// way to change what the controller routes through.
|
||||
defineBank(BANK_TABLE, bankTable, BANK_TABLE_BYTES, BANK_FLAG_READ_ONLY, BANK_OWNER_MACHINE);
|
||||
defineBank(c, BANK_TABLE, c->bankTable, BANK_TABLE_BYTES, BANK_FLAG_READ_ONLY, BANK_OWNER_MACHINE);
|
||||
|
||||
sourceBank = destBank = guardBank = 0;
|
||||
sourceAddress = destAddress = length = 0;
|
||||
guardStart = guardEnd = 0;
|
||||
status = 0;
|
||||
c->sourceBank = c->destBank = c->guardBank = 0;
|
||||
c->sourceAddress = c->destAddress = c->length = 0;
|
||||
c->guardStart = c->guardEnd = 0;
|
||||
c->status = 0;
|
||||
}
|
||||
|
||||
// Refuses, remembering why so that Status can be read afterwards.
|
||||
static void refuse(uint8_t faultVector) {
|
||||
status = faultVector;
|
||||
static void refuse(Controller *c, uint8_t faultVector) {
|
||||
c->status = faultVector;
|
||||
refuseAccess(faultVector);
|
||||
}
|
||||
|
||||
// Is this somewhere the controller can read? A bank has to be there, and the address has
|
||||
// to be inside it.
|
||||
static int canRead(uint8_t bank, uint16_t address) {
|
||||
if (!(banks[bank].flags & BANK_FLAG_PRESENT) || address >= banks[bank].capacity) {
|
||||
refuse(VECTOR_BANK_FAULT);
|
||||
static int canRead(Controller *c, uint8_t bank, uint16_t address) {
|
||||
if (!(c->banks[bank].flags & BANK_FLAG_PRESENT) || address >= c->banks[bank].capacity) {
|
||||
refuse(c, VECTOR_BANK_FAULT);
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The same, and then the two reasons a write in particular gets turned away.
|
||||
static int canWrite(uint8_t bank, uint16_t address) {
|
||||
if (!canRead(bank, address)) {
|
||||
static int canWrite(Controller *c, uint8_t bank, uint16_t address) {
|
||||
if (!canRead(c, bank, address)) {
|
||||
return 0;
|
||||
}
|
||||
if (banks[bank].flags & BANK_FLAG_READ_ONLY) {
|
||||
refuse(VECTOR_GUARD_VIOLATION);
|
||||
if (c->banks[bank].flags & BANK_FLAG_READ_ONLY) {
|
||||
refuse(c, VECTOR_GUARD_VIOLATION);
|
||||
return 0;
|
||||
}
|
||||
if ((banks[bank].flags & BANK_FLAG_GUARDED)
|
||||
&& address >= banks[bank].guardStart && address <= banks[bank].guardEnd) {
|
||||
refuse(VECTOR_GUARD_VIOLATION);
|
||||
if ((c->banks[bank].flags & BANK_FLAG_GUARDED)
|
||||
&& address >= c->banks[bank].guardStart && address <= c->banks[bank].guardEnd) {
|
||||
refuse(c, VECTOR_GUARD_VIOLATION);
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// A length of zero means the whole 64K, because a transfer of no bytes is never what
|
||||
// A c->length of zero means the whole 64K, because a transfer of no bytes is never what
|
||||
// anyone meant, and 65536 does not fit in the two bytes that carry it.
|
||||
static uint32_t transferLength(void) {
|
||||
return (length == 0) ? 0x10000u : (uint32_t)length;
|
||||
static uint32_t transferLength(Controller *c) {
|
||||
return (c->length == 0) ? 0x10000u : (uint32_t)c->length;
|
||||
}
|
||||
|
||||
// Everything a transfer will touch is checked before any of it moves. A blit that ran
|
||||
// out of bank halfway would leave memory in a state no program asked for, and the
|
||||
// diagnostic would arrive after the damage rather than instead of it. So these answer
|
||||
// for the whole range or refuse the whole thing.
|
||||
static int rangeReadable(uint8_t bank, uint16_t address, uint32_t count) {
|
||||
if (!(banks[bank].flags & BANK_FLAG_PRESENT)
|
||||
|| (uint32_t)address + count > banks[bank].capacity) {
|
||||
refuse(VECTOR_BANK_FAULT);
|
||||
static int rangeReadable(Controller *c, uint8_t bank, uint16_t address, uint32_t count) {
|
||||
if (!(c->banks[bank].flags & BANK_FLAG_PRESENT)
|
||||
|| (uint32_t)address + count > c->banks[bank].capacity) {
|
||||
refuse(c, VECTOR_BANK_FAULT);
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int rangeWritable(uint8_t bank, uint16_t address, uint32_t count) {
|
||||
if (!rangeReadable(bank, address, count)) {
|
||||
static int rangeWritable(Controller *c, uint8_t bank, uint16_t address, uint32_t count) {
|
||||
if (!rangeReadable(c, bank, address, count)) {
|
||||
return 0;
|
||||
}
|
||||
if (banks[bank].flags & BANK_FLAG_READ_ONLY) {
|
||||
refuse(VECTOR_GUARD_VIOLATION);
|
||||
if (c->banks[bank].flags & BANK_FLAG_READ_ONLY) {
|
||||
refuse(c, VECTOR_GUARD_VIOLATION);
|
||||
return 0;
|
||||
}
|
||||
if (banks[bank].flags & BANK_FLAG_GUARDED) {
|
||||
if (c->banks[bank].flags & BANK_FLAG_GUARDED) {
|
||||
uint32_t last = (uint32_t)address + count - 1;
|
||||
// Any overlap at all with the fence, not just a write that starts inside it.
|
||||
if (!(last < banks[bank].guardStart || address > banks[bank].guardEnd)) {
|
||||
refuse(VECTOR_GUARD_VIOLATION);
|
||||
if (!(last < c->banks[bank].guardStart || address > c->banks[bank].guardEnd)) {
|
||||
refuse(c, VECTOR_GUARD_VIOLATION);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// What the moves below have cost since anybody last asked.
|
||||
static unsigned long pendingCycles = 0;
|
||||
|
||||
// ---- Sixteen bits wide, when the addresses let it be ----
|
||||
//
|
||||
// The controller reaches bank memory two bytes at a time, so an aligned transfer moves two
|
||||
// bytes in the time a misaligned one moves one. A word is read at an even address and
|
||||
// written at an even address, which is why the source, the destination AND the length must
|
||||
// written at an even address, which is why the source, the destination AND the c->length must
|
||||
// all be even: an odd anything would have the controller shifting bytes across word
|
||||
// boundaries to line them up, and that is a second design rather than this one.
|
||||
//
|
||||
@@ -173,140 +151,140 @@ static int wideRun(uint32_t addressesAndLength) {
|
||||
return (addressesAndLength & 1u) == 0;
|
||||
}
|
||||
|
||||
unsigned long controllerTakeCycles(void) {
|
||||
unsigned long taken = pendingCycles;
|
||||
pendingCycles = 0;
|
||||
unsigned long controllerTakeCycles(Controller *c) {
|
||||
unsigned long taken = c->pendingCycles;
|
||||
c->pendingCycles = 0;
|
||||
return taken;
|
||||
}
|
||||
|
||||
static void doBlit(void) {
|
||||
uint32_t count = transferLength();
|
||||
if (!rangeReadable(sourceBank, sourceAddress, count)) {
|
||||
static void doBlit(Controller *c) {
|
||||
uint32_t count = transferLength(c);
|
||||
if (!rangeReadable(c, c->sourceBank, c->sourceAddress, count)) {
|
||||
return;
|
||||
}
|
||||
if (!rangeWritable(destBank, destAddress, count)) {
|
||||
if (!rangeWritable(c, c->destBank, c->destAddress, count)) {
|
||||
return;
|
||||
}
|
||||
// memmove rather than memcpy, because source and destination may be the same bank
|
||||
// and may overlap. Sliding a buffer along itself is an ordinary thing to want, and
|
||||
// getting it silently wrong is exactly the sort of failure this machine keeps
|
||||
// designing against.
|
||||
memmove(banks[destBank].memory + destAddress,
|
||||
banks[sourceBank].memory + sourceAddress, count);
|
||||
// A word read and a word written. Two banks are two memories and the pair overlaps;
|
||||
memmove(c->banks[c->destBank].memory + c->destAddress,
|
||||
c->banks[c->sourceBank].memory + c->sourceAddress, count);
|
||||
// A word read and a word written. Two c->banks are two memories and the pair overlaps;
|
||||
// one bank is one memory and they do not. The odd cycle is the pipeline filling.
|
||||
//
|
||||
// Wide when everything is even, so an aligned move between banks settles at two bytes a
|
||||
// Wide when everything is even, so an aligned move between c->banks settles at two bytes a
|
||||
// cycle and an aligned move within one at a byte a cycle - each twice what it was.
|
||||
unsigned long moves = wideRun(sourceAddress | destAddress | count) ? count / 2 : count;
|
||||
pendingCycles += (sourceBank == destBank) ? 2 * moves + 1 : moves + 1;
|
||||
sourceAddress = (uint16_t)(sourceAddress + count);
|
||||
destAddress = (uint16_t)(destAddress + count);
|
||||
status = 0;
|
||||
unsigned long moves = wideRun(c->sourceAddress | c->destAddress | count) ? count / 2 : count;
|
||||
c->pendingCycles += (c->sourceBank == c->destBank) ? 2 * moves + 1 : moves + 1;
|
||||
c->sourceAddress = (uint16_t)(c->sourceAddress + count);
|
||||
c->destAddress = (uint16_t)(c->destAddress + count);
|
||||
c->status = 0;
|
||||
}
|
||||
|
||||
static void doFill(void) {
|
||||
uint32_t count = transferLength();
|
||||
if (!rangeWritable(destBank, destAddress, count)) {
|
||||
static void doFill(Controller *c) {
|
||||
uint32_t count = transferLength(c);
|
||||
if (!rangeWritable(c, c->destBank, c->destAddress, count)) {
|
||||
return;
|
||||
}
|
||||
// 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 - including for the alignment, which
|
||||
// asks only about where the bytes are going and how many there are.
|
||||
pendingCycles += (wideRun(destAddress | count) ? count / 2 : count) + 1;
|
||||
memset(banks[destBank].memory + destAddress, (int)(sourceAddress & 0xFF), count);
|
||||
destAddress = (uint16_t)(destAddress + count);
|
||||
status = 0;
|
||||
c->pendingCycles += (wideRun(c->destAddress | count) ? count / 2 : count) + 1;
|
||||
memset(c->banks[c->destBank].memory + c->destAddress, (int)(c->sourceAddress & 0xFF), count);
|
||||
c->destAddress = (uint16_t)(c->destAddress + count);
|
||||
c->status = 0;
|
||||
}
|
||||
|
||||
// DestBank is the number being given out, and SourceLow says which port owns the memory.
|
||||
// The capacity is asked of the device rather than supplied, because how big a bank is
|
||||
// was settled when the machine was built.
|
||||
static void doRegisterBank(void) {
|
||||
if (destBank <= BANK_TABLE) {
|
||||
static void doRegisterBank(Controller *c) {
|
||||
if (c->destBank <= BANK_TABLE) {
|
||||
// Banks 0 to 2 are the machine's own and are not anybody's to hand out.
|
||||
refuse(VECTOR_BANK_FAULT);
|
||||
refuse(c, VECTOR_BANK_FAULT);
|
||||
return;
|
||||
}
|
||||
uint8_t port = (uint8_t)(sourceAddress & 0xFF);
|
||||
uint8_t port = (uint8_t)(c->sourceAddress & 0xFF);
|
||||
uint32_t capacity = 0;
|
||||
uint8_t *memory = deviceMemory(port, &capacity);
|
||||
if (memory == NULL) {
|
||||
// Either nothing is on that port or what is there brings no memory. Registering
|
||||
// it would put a bank in the table that leads nowhere.
|
||||
refuse(VECTOR_BANK_FAULT);
|
||||
refuse(c, VECTOR_BANK_FAULT);
|
||||
return;
|
||||
}
|
||||
// Registering over a bank that already has something in it is allowed. Which number
|
||||
// a device's memory answers to is the OS's business, and nothing was allocated that
|
||||
// could be lost by changing its mind.
|
||||
defineBank(destBank, memory, capacity, 0, port);
|
||||
status = 0;
|
||||
defineBank(c, c->destBank, memory, capacity, 0, port);
|
||||
c->status = 0;
|
||||
}
|
||||
|
||||
// The guard registers stage a range; this is what commits it. Raising a fence over a
|
||||
// bank that is not there would protect nothing while looking like it protected
|
||||
// something, so it is refused rather than quietly accepted.
|
||||
static void doGuardOn(void) {
|
||||
if (!(banks[guardBank].flags & BANK_FLAG_PRESENT)) {
|
||||
refuse(VECTOR_BANK_FAULT);
|
||||
static void doGuardOn(Controller *c) {
|
||||
if (!(c->banks[c->guardBank].flags & BANK_FLAG_PRESENT)) {
|
||||
refuse(c, VECTOR_BANK_FAULT);
|
||||
return;
|
||||
}
|
||||
if (guardStart > guardEnd) {
|
||||
if (c->guardStart > c->guardEnd) {
|
||||
// No address can be inside a range that ends before it starts, so this fence
|
||||
// would catch nothing. A program that raised one would believe it was protected
|
||||
// and would not be, which is worse than having no fence at all.
|
||||
refuse(VECTOR_BANK_FAULT);
|
||||
refuse(c, VECTOR_BANK_FAULT);
|
||||
return;
|
||||
}
|
||||
banks[guardBank].guardStart = guardStart;
|
||||
banks[guardBank].guardEnd = guardEnd;
|
||||
banks[guardBank].flags |= BANK_FLAG_GUARDED;
|
||||
publishBank(guardBank);
|
||||
status = 0;
|
||||
c->banks[c->guardBank].guardStart = c->guardStart;
|
||||
c->banks[c->guardBank].guardEnd = c->guardEnd;
|
||||
c->banks[c->guardBank].flags |= BANK_FLAG_GUARDED;
|
||||
publishBank(c, c->guardBank);
|
||||
c->status = 0;
|
||||
}
|
||||
|
||||
static void doGuardOff(void) {
|
||||
if (!(banks[guardBank].flags & BANK_FLAG_PRESENT)) {
|
||||
refuse(VECTOR_BANK_FAULT);
|
||||
static void doGuardOff(Controller *c) {
|
||||
if (!(c->banks[c->guardBank].flags & BANK_FLAG_PRESENT)) {
|
||||
refuse(c, VECTOR_BANK_FAULT);
|
||||
return;
|
||||
}
|
||||
banks[guardBank].flags &= (uint8_t)~BANK_FLAG_GUARDED;
|
||||
publishBank(guardBank);
|
||||
status = 0;
|
||||
c->banks[c->guardBank].flags &= (uint8_t)~BANK_FLAG_GUARDED;
|
||||
publishBank(c, c->guardBank);
|
||||
c->status = 0;
|
||||
}
|
||||
|
||||
uint8_t controllerWrite(uint8_t value, uint8_t port) {
|
||||
uint8_t controllerWrite(Controller *c, uint8_t value, uint8_t port) {
|
||||
switch (port) {
|
||||
case CTRL_SOURCE_BANK: sourceBank = value; break;
|
||||
case CTRL_SOURCE_HIGH: sourceAddress = (uint16_t)(value << 8) | (sourceAddress & 0x00FF); break;
|
||||
case CTRL_SOURCE_LOW: sourceAddress = (sourceAddress & 0xFF00) | value; break;
|
||||
case CTRL_DEST_BANK: destBank = value; break;
|
||||
case CTRL_DEST_HIGH: destAddress = (uint16_t)(value << 8) | (destAddress & 0x00FF); break;
|
||||
case CTRL_DEST_LOW: destAddress = (destAddress & 0xFF00) | value; break;
|
||||
case CTRL_LENGTH_HIGH: length = (uint16_t)(value << 8) | (length & 0x00FF); break;
|
||||
case CTRL_LENGTH_LOW: length = (length & 0xFF00) | value; break;
|
||||
case CTRL_SOURCE_BANK: c->sourceBank = value; break;
|
||||
case CTRL_SOURCE_HIGH: c->sourceAddress = (uint16_t)(value << 8) | (c->sourceAddress & 0x00FF); break;
|
||||
case CTRL_SOURCE_LOW: c->sourceAddress = (c->sourceAddress & 0xFF00) | value; break;
|
||||
case CTRL_DEST_BANK: c->destBank = value; break;
|
||||
case CTRL_DEST_HIGH: c->destAddress = (uint16_t)(value << 8) | (c->destAddress & 0x00FF); break;
|
||||
case CTRL_DEST_LOW: c->destAddress = (c->destAddress & 0xFF00) | value; break;
|
||||
case CTRL_LENGTH_HIGH: c->length = (uint16_t)(value << 8) | (c->length & 0x00FF); break;
|
||||
case CTRL_LENGTH_LOW: c->length = (c->length & 0xFF00) | value; break;
|
||||
|
||||
case CTRL_GUARD_BANK: guardBank = value; break;
|
||||
case CTRL_GUARD_START_HIGH: guardStart = (uint16_t)(value << 8) | (guardStart & 0x00FF); break;
|
||||
case CTRL_GUARD_START_LOW: guardStart = (guardStart & 0xFF00) | value; break;
|
||||
case CTRL_GUARD_END_HIGH: guardEnd = (uint16_t)(value << 8) | (guardEnd & 0x00FF); break;
|
||||
case CTRL_GUARD_END_LOW: guardEnd = (guardEnd & 0xFF00) | value; break;
|
||||
case CTRL_GUARD_BANK: c->guardBank = value; break;
|
||||
case CTRL_GUARD_START_HIGH: c->guardStart = (uint16_t)(value << 8) | (c->guardStart & 0x00FF); break;
|
||||
case CTRL_GUARD_START_LOW: c->guardStart = (c->guardStart & 0xFF00) | value; break;
|
||||
case CTRL_GUARD_END_HIGH: c->guardEnd = (uint16_t)(value << 8) | (c->guardEnd & 0x00FF); break;
|
||||
case CTRL_GUARD_END_LOW: c->guardEnd = (c->guardEnd & 0xFF00) | value; break;
|
||||
|
||||
case CTRL_DATA:
|
||||
// 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) {
|
||||
if (canWrite(c, c->destBank, c->destAddress)) {
|
||||
c->pendingCycles++; // The byte itself, beyond reaching the port.
|
||||
c->banks[c->destBank].memory[c->destAddress] = value;
|
||||
if (c->destBank == BANK_TABLE) {
|
||||
// Unreachable while the table is read only, and here so that it stays
|
||||
// true if that ever changes: the published bytes are a description,
|
||||
// and nothing may write through them into a real bank.
|
||||
publishBank(BANK_TABLE);
|
||||
publishBank(c, BANK_TABLE);
|
||||
}
|
||||
destAddress++;
|
||||
status = 0;
|
||||
c->destAddress++;
|
||||
c->status = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -314,16 +292,16 @@ uint8_t controllerWrite(uint8_t value, uint8_t port) {
|
||||
// Both leave the addresses past whatever they touched and Length as it was,
|
||||
// so asking again carries straight on from where the last one stopped.
|
||||
switch (value) {
|
||||
case COMMAND_BLIT: doBlit(); break;
|
||||
case COMMAND_FILL: doFill(); break;
|
||||
case COMMAND_REGISTER_BANK: doRegisterBank(); break;
|
||||
case COMMAND_GUARD_ON: doGuardOn(); break;
|
||||
case COMMAND_GUARD_OFF: doGuardOff(); break;
|
||||
case COMMAND_BLIT: doBlit(c); break;
|
||||
case COMMAND_FILL: doFill(c); break;
|
||||
case COMMAND_REGISTER_BANK: doRegisterBank(c); break;
|
||||
case COMMAND_GUARD_ON: doGuardOn(c); break;
|
||||
case COMMAND_GUARD_OFF: doGuardOff(c); break;
|
||||
default:
|
||||
// Refusing an unknown command is better than ignoring it, since a
|
||||
// program that asked for something is entitled to find out that it
|
||||
// did not happen.
|
||||
refuse(VECTOR_BANK_FAULT);
|
||||
refuse(c, VECTOR_BANK_FAULT);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
@@ -335,33 +313,33 @@ uint8_t controllerWrite(uint8_t value, uint8_t port) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t controllerRead(uint8_t port) {
|
||||
uint8_t controllerRead(Controller *c, uint8_t port) {
|
||||
switch (port) {
|
||||
case CTRL_SOURCE_BANK: return sourceBank;
|
||||
case CTRL_SOURCE_HIGH: return (uint8_t)(sourceAddress >> 8);
|
||||
case CTRL_SOURCE_LOW: return (uint8_t)(sourceAddress & 0xFF);
|
||||
case CTRL_DEST_BANK: return destBank;
|
||||
case CTRL_DEST_HIGH: return (uint8_t)(destAddress >> 8);
|
||||
case CTRL_DEST_LOW: return (uint8_t)(destAddress & 0xFF);
|
||||
case CTRL_LENGTH_HIGH: return (uint8_t)(length >> 8);
|
||||
case CTRL_LENGTH_LOW: return (uint8_t)(length & 0xFF);
|
||||
case CTRL_STATUS: return status;
|
||||
case CTRL_SOURCE_BANK: return c->sourceBank;
|
||||
case CTRL_SOURCE_HIGH: return (uint8_t)(c->sourceAddress >> 8);
|
||||
case CTRL_SOURCE_LOW: return (uint8_t)(c->sourceAddress & 0xFF);
|
||||
case CTRL_DEST_BANK: return c->destBank;
|
||||
case CTRL_DEST_HIGH: return (uint8_t)(c->destAddress >> 8);
|
||||
case CTRL_DEST_LOW: return (uint8_t)(c->destAddress & 0xFF);
|
||||
case CTRL_LENGTH_HIGH: return (uint8_t)(c->length >> 8);
|
||||
case CTRL_LENGTH_LOW: return (uint8_t)(c->length & 0xFF);
|
||||
case CTRL_STATUS: return c->status;
|
||||
|
||||
case CTRL_GUARD_BANK: return guardBank;
|
||||
case CTRL_GUARD_START_HIGH: return (uint8_t)(guardStart >> 8);
|
||||
case CTRL_GUARD_START_LOW: return (uint8_t)(guardStart & 0xFF);
|
||||
case CTRL_GUARD_END_HIGH: return (uint8_t)(guardEnd >> 8);
|
||||
case CTRL_GUARD_END_LOW: return (uint8_t)(guardEnd & 0xFF);
|
||||
case CTRL_GUARD_BANK: return c->guardBank;
|
||||
case CTRL_GUARD_START_HIGH: return (uint8_t)(c->guardStart >> 8);
|
||||
case CTRL_GUARD_START_LOW: return (uint8_t)(c->guardStart & 0xFF);
|
||||
case CTRL_GUARD_END_HIGH: return (uint8_t)(c->guardEnd >> 8);
|
||||
case CTRL_GUARD_END_LOW: return (uint8_t)(c->guardEnd & 0xFF);
|
||||
|
||||
case CTRL_DATA: {
|
||||
// A byte out of the source, stepping on the same way a write does.
|
||||
if (!canRead(sourceBank, sourceAddress)) {
|
||||
if (!canRead(c, c->sourceBank, c->sourceAddress)) {
|
||||
return 0;
|
||||
}
|
||||
pendingCycles++; // As above, the other way round.
|
||||
uint8_t value = banks[sourceBank].memory[sourceAddress];
|
||||
sourceAddress++;
|
||||
status = 0;
|
||||
c->pendingCycles++; // As above, the other way round.
|
||||
uint8_t value = c->banks[c->sourceBank].memory[c->sourceAddress];
|
||||
c->sourceAddress++;
|
||||
c->status = 0;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user