// controller.c // The SplitBit Memory Controller. // // This is the only thing on the machine that can write Program Memory. That is the whole // reason it exists: SplitBit is a Harvard machine and its instruction set cannot reach // its own code, which is worth keeping true of the instructions. Routing it through a // device instead makes writing code a capability, reached deliberately through a port, // rather than something every instruction stream can do by accident. // // Written by Anachronaut #include "controller.h" #include "io.h" #include "../Assembler/assembly.h" // For the fault vector numbers. #include 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; // 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); } 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); } void initializeController(uint8_t *programMemory, uint8_t *dataMemory) { memset(banks, 0, sizeof(banks)); memset(bankTable, 0, sizeof(bankTable)); for (int i = 0; i < BANK_COUNT; i++) { banks[i].ownerPort = BANK_OWNER_MACHINE; publishBank(i); } defineBank(BANK_PROGRAM, programMemory, 0x10000, 0, BANK_OWNER_MACHINE); defineBank(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); sourceBank = destBank = guardBank = 0; sourceAddress = destAddress = length = 0; guardStart = guardEnd = 0; status = 0; } // Refuses, remembering why so that Status can be read afterwards. static void refuse(uint8_t faultVector) { 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); 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)) { return 0; } if (banks[bank].flags & BANK_FLAG_READ_ONLY) { refuse(VECTOR_GUARD_VIOLATION); return 0; } if ((banks[bank].flags & BANK_FLAG_GUARDED) && address >= banks[bank].guardStart && address <= banks[bank].guardEnd) { refuse(VECTOR_GUARD_VIOLATION); return 0; } return 1; } // A 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; } // 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); return 0; } return 1; } static int rangeWritable(uint8_t bank, uint16_t address, uint32_t count) { if (!rangeReadable(bank, address, count)) { return 0; } if (banks[bank].flags & BANK_FLAG_READ_ONLY) { refuse(VECTOR_GUARD_VIOLATION); return 0; } if (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); 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 // 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. // // Misaligned falls back to a byte a cycle, which is exactly what the machine did before it // was widened, so nothing already written got slower. // // THE RULE IS VISIBLE ON PURPOSE. A program that cares can align what it moves, and a cost // a program cannot see is a cost it cannot avoid. It is also the honest thing to model: // hardware this shape really does behave this way. static int wideRun(uint32_t addressesAndLength) { return (addressesAndLength & 1u) == 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)) { return; } if (!rangeWritable(destBank, 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; // 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 // 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; } static void doFill(void) { uint32_t count = transferLength(); if (!rangeWritable(destBank, 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; } // 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) { // Banks 0 to 2 are the machine's own and are not anybody's to hand out. refuse(VECTOR_BANK_FAULT); return; } uint8_t port = (uint8_t)(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); 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; } // 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); return; } if (guardStart > 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); return; } banks[guardBank].guardStart = guardStart; banks[guardBank].guardEnd = guardEnd; banks[guardBank].flags |= BANK_FLAG_GUARDED; publishBank(guardBank); status = 0; } static void doGuardOff(void) { if (!(banks[guardBank].flags & BANK_FLAG_PRESENT)) { refuse(VECTOR_BANK_FAULT); return; } banks[guardBank].flags &= (uint8_t)~BANK_FLAG_GUARDED; publishBank(guardBank); status = 0; } uint8_t controllerWrite(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_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_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) { // 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); } destAddress++; status = 0; } break; case CTRL_COMMAND: // 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; 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); break; } break; default: // Status is read only. break; } return 0; } uint8_t controllerRead(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_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_DATA: { // A byte out of the source, stepping on the same way a write does. if (!canRead(sourceBank, sourceAddress)) { return 0; } pendingCycles++; // As above, the other way round. uint8_t value = banks[sourceBank].memory[sourceAddress]; sourceAddress++; status = 0; return value; } } return 0; }