diff --git a/Programs/testPrograms/timerBeatTest.asm b/Programs/testPrograms/timerBeatTest.asm new file mode 100644 index 0000000..cc96999 --- /dev/null +++ b/Programs/testPrograms/timerBeatTest.asm @@ -0,0 +1,48 @@ +; The timer interrupting, which is what a music routine actually wants. +; +; 125,000 cycles is a sixteenth note at 120 beats a minute. THE SCREEN CANNOT EXPRESS IT: a +; frame is 16,667 cycles, so that beat is seven and a half of them, and a program timing music +; on frames has to pick a tempo whose subdivisions happen to land on whole ones. +; +; Eight of them is a second, and what is recorded is that it took one - and that the machine +; slept through nearly all of it, which is the difference between waiting for a beat and +; counting up to it. +; +; Written by Anachronaut + +#Program +start: + ; 125,000 cycles: a sixteenth note at 120 beats a minute, which the screen's frame + ; cannot express at all. + INIA 0x01 + OUTA 0x52 + INIA 0xE8 + OUTA 0x53 + INIA 0x48 + OUTA 0x54 ; 0x01E848 = 125,000 + + INIA 0x07 + OUTA 0x51 ; Run, repeat, interrupt. + SIF + + INIB 0d8 +everyBeat: + WAIT + INIA 0d46 + OUTA 0x00 + DECB + BNB everyBeat + + CIF + RSTA + OUTA 0x51 + INIA 0d10 + OUTA 0x00 + HALT + +beat: + RETI + +#Vectors + Boot start + Device 0x50 beat diff --git a/Programs/testPrograms/timerTest.asm b/Programs/testPrograms/timerTest.asm new file mode 100644 index 0000000..794fff2 --- /dev/null +++ b/Programs/testPrograms/timerTest.asm @@ -0,0 +1,93 @@ +; A beat a program sets for itself. +; +; The only regular thing this machine had was the screen finishing a frame, sixty times a +; second and not negotiable. That is a clock a program BORROWS: every duration becomes a +; multiple of 16.67 ms, so a sixteenth note at 120 beats a minute - 125,000 cycles, which is +; seven and a half frames - cannot be asked for at all. +; +; Counted in cycles, because that is what everything else on this machine is counted in. +; +; Written by Anachronaut + +#Program + +start: + ; ---- Repeating, and polled ---- + ; + ; 100,000 cycles, a tenth of a second. Four of them, so the whole thing is 400,000 and the + ; recorded cycle count is the check that the period is what it says. + INIA 0x01 + OUTA 0x52 + INIA 0x86 + OUTA 0x53 + INIA 0xA0 + OUTA 0x54 ; 0x0186A0 + + INIA 0x03 + OUTA 0x51 ; Run, repeat, no interrupt. + + INIB 0d4 +everyTick: + INA 0x50 + PSHB + INIB 0x01 + AND + POPB + BRQ everyTick ; Not yet. + INIA 0d46 + OUTA 0x00 + DECB + BNB everyTick + + ; ---- And looking is what answered it ---- + ; + ; The bit came down when it was read, so asking again immediately says nothing has happened + ; since. A timer whose flag stayed up would look like a beat every time round the loop. + INA 0x50 + INIB 0x01 + AND + BRQ tickCleared + INIA 0d78 ; 'N' + OUTA 0x00 + BRI oneShot +tickCleared: + INIA 0d89 ; 'Y' + OUTA 0x00 + +oneShot: + ; ---- Once, and then stopped ---- + ; + ; Without the repeat bit it runs its period out and turns itself off, which the status port + ; says: bit 1 is whether it is running. + RSTA + OUTA 0x51 ; Stop first, so starting below is a start. + INIA 0x01 + OUTA 0x51 ; Run, no repeat. + +waitOnce: + INA 0x50 + INIB 0x01 + AND + BRQ waitOnce + INIA 0d46 + OUTA 0x00 + + INA 0x50 + INIB 0x02 ; RUNNING + AND + BRQ stopped + INIA 0d78 + OUTA 0x00 + BRI done +stopped: + INIA 0d89 + OUTA 0x00 + +done: + INIA 0d10 + OUTA 0x00 + HALT + +#Vectors + + Boot start diff --git a/Source/Emulator/io.c b/Source/Emulator/io.c index 88f35ea..317cef8 100644 --- a/Source/Emulator/io.c +++ b/Source/Emulator/io.c @@ -948,11 +948,121 @@ void deviceTick(unsigned long now) { // And the sound, which makes whatever samples are due by now. On the machine's clock, // so the same program makes the same sound in the same cycles. soundTick(now); + // And the timer, which is the only beat a program can choose for itself. + timerTick(now); if (diskPending && now >= diskReadyAt) { diskSettle(); } } +// ---- A beat a program sets for itself ---- +// +// COUNTED IN CYCLES, which is this machine's unit of time everywhere else: it is what the +// cost model counts and what a frame is measured in. A timer counting anything else would be +// a second thing to remember, and a prescaler would buy range that twenty four bits already +// covers - one cycle at one end and sixteen point seven seconds at the other, with 120 beats +// a minute sitting at 500,000 in the middle of it. +static uint32_t timerPeriod = 0; +static uint32_t timerLeft = 0; +static uint8_t timerControl = 0; +static uint8_t timerTicked = 0; +static unsigned long timerLast = 0; + +void timerReset(void) { + timerPeriod = 0; + timerLeft = 0; + timerControl = 0; + timerTicked = 0; + timerLast = 0; + clearInterrupt(PORT_TIMER); +} + +// ---- Caught up rather than counted ---- +// +// The same shape as the screen's frame: the machine runs in batches, so more than one period +// can pass between two looks. What is owed is worked out from how far the clock moved rather +// than by being told once per cycle, and several periods at once still mean one tick - a +// missed one is missed, which is what missing one is. +void timerTick(unsigned long now) { + const unsigned long moved = now - timerLast; + timerLast = now; + if (!(timerControl & TIMER_CONTROL_RUN) || timerPeriod == 0) { + return; + } + if ((unsigned long)timerLeft > moved) { + timerLeft -= (uint32_t)moved; + return; + } + + timerTicked = 1; + if (timerControl & TIMER_CONTROL_INTERRUPT) { + raiseInterrupt(PORT_TIMER); + } + if (timerControl & TIMER_CONTROL_REPEAT) { + // What is left over carries into the next period, so a timer asked for 1,000 cycles + // gets a tick every 1,000 and not every 1,000 plus however late anybody looked. + const unsigned long over = moved - timerLeft; + timerLeft = timerPeriod - (uint32_t)(over % timerPeriod); + } else { + timerControl = (uint8_t)(timerControl & ~TIMER_CONTROL_RUN); + timerLeft = 0; + } +} + +static uint8_t timerWrite(uint8_t value, uint8_t port) { + switch (port) { + case TIMER_CONTROL: { + const uint8_t wasRunning = timerControl & TIMER_CONTROL_RUN; + timerControl = value; + if ((value & TIMER_CONTROL_RUN) && !wasRunning) { + // Starting loads the period. Asking it to run while it already is does not, + // so a program that sets the interrupt bit half way through a period does not + // silently move the beat it was keeping. + timerLeft = timerPeriod; + } + if (!(value & TIMER_CONTROL_INTERRUPT)) { + // Asking to stop being interrupted takes down whatever was already asked for, + // the same as the screen and for the same reason. + clearInterrupt(PORT_TIMER); + } + } + break; + case TIMER_PERIOD_HIGH: + timerPeriod = (timerPeriod & 0x0000FFFFu) | ((uint32_t)value << 16); + break; + case TIMER_PERIOD_MID: + timerPeriod = (timerPeriod & 0x00FF00FFu) | ((uint32_t)value << 8); + break; + case TIMER_PERIOD_LOW: + timerPeriod = (timerPeriod & 0x00FFFF00u) | value; + break; + default: break; + } + return 0; +} + +static uint8_t timerRead(uint8_t port) { + switch (port) { + case TIMER_STATUS: { + uint8_t status = 0; + if (timerTicked) status |= TIMER_STATUS_TICKED; + if (timerControl & TIMER_CONTROL_RUN) status |= TIMER_STATUS_RUNNING; + if (timerControl & TIMER_CONTROL_INTERRUPT) status |= TIMER_STATUS_INTERRUPT; + // Looking is what answers it, the same as every other status port here: a beat + // that has been noticed is not still waiting to be, and the line goes down with + // the flag because a program that polls is not one that will answer a handler. + timerTicked = 0; + clearInterrupt(PORT_TIMER); + return status; + } + case TIMER_CONTROL: return timerControl; + case TIMER_PERIOD_HIGH: return (uint8_t)(timerPeriod >> 16); + case TIMER_PERIOD_MID: return (uint8_t)(timerPeriod >> 8); + case TIMER_PERIOD_LOW: return (uint8_t)timerPeriod; + default: return 0; + } +} + // ---- A device that brings memory ---- // // The simplest thing that owns a bank. Writing to its port fills its memory with the @@ -1011,6 +1121,7 @@ static const DeviceRecord deviceTable[] = { { PORT_DISK, DEVICE_DISK, DEVICE_FLAG_HAS_MEMORY }, { PORT_VIDEO, DEVICE_VIDEO, DEVICE_FLAG_HAS_MEMORY }, { PORT_SOUND, DEVICE_SOUND, 0 }, + { PORT_TIMER, DEVICE_TIMER, 0 }, { PORT_REGISTRY, DEVICE_REGISTRY, 0 }, }; static const int deviceCount = (int)(sizeof(deviceTable) / sizeof(deviceTable[0])); @@ -1046,6 +1157,9 @@ static const DeviceRecord *deviceOnPort(uint8_t port) { if (port > PORT_SOUND && port <= PORT_SOUND_TOP) { return deviceOnPort(PORT_SOUND); } + if (port > PORT_TIMER && port <= PORT_TIMER_TOP) { + return deviceOnPort(PORT_TIMER); + } for (int i = 0; i < deviceCount; i++) { if (deviceTable[i].port == port) { return &deviceTable[i]; @@ -1082,6 +1196,9 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) { if (Address >= PORT_SOUND && Address <= PORT_SOUND_TOP) { return soundWrite(DataByte, Address); } + if (Address >= PORT_TIMER && Address <= PORT_TIMER_TOP) { + return timerWrite(DataByte, Address); + } // This function sends the DataByte to the appropriate place based on the Port Address. switch(Address) { case CONSOLE_DATA: @@ -1216,6 +1333,9 @@ uint8_t InputHandler(uint8_t Address) { if (Address >= PORT_SOUND && Address <= PORT_SOUND_TOP) { return soundRead(Address); } + if (Address >= PORT_TIMER && Address <= PORT_TIMER_TOP) { + return timerRead(Address); + } switch(Address) { case CONSOLE_DATA: // If data is sent here, it should be read from STDIN. diff --git a/Source/Emulator/io.h b/Source/Emulator/io.h index 9932044..1ddb595 100644 --- a/Source/Emulator/io.h +++ b/Source/Emulator/io.h @@ -97,6 +97,30 @@ #define PORT_VIDEO 0x30 #define PORT_VIDEO_TOP 0x3F +// ---- The timer ---- +// +// The only regular beat this machine had was the screen finishing a frame, which is fixed at +// sixty a second. That is a clock a program borrows rather than one it sets: every duration +// becomes a multiple of 16.67 ms, so a note worth a third of a beat cannot be asked for and +// the way round it is to choose a tempo whose subdivisions happen to land on whole frames - +// which is making the music fit the hardware. +#define PORT_TIMER 0x50 +#define PORT_TIMER_TOP 0x54 + +#define TIMER_STATUS 0x50 +#define TIMER_CONTROL 0x51 +#define TIMER_PERIOD_HIGH 0x52 +#define TIMER_PERIOD_MID 0x53 +#define TIMER_PERIOD_LOW 0x54 + +#define TIMER_STATUS_TICKED 0x01 +#define TIMER_STATUS_RUNNING 0x02 +#define TIMER_STATUS_INTERRUPT 0x04 + +#define TIMER_CONTROL_RUN 0x01 +#define TIMER_CONTROL_REPEAT 0x02 +#define TIMER_CONTROL_INTERRUPT 0x04 + #define PORT_REGISTRY 0xFF // ---- The console ---- @@ -221,6 +245,7 @@ void consoleSetInputHook(int (*hook)(int mayWait)); #define DEVICE_DISK 0x13 #define DEVICE_VIDEO 0x14 #define DEVICE_SOUND 0x15 +#define DEVICE_TIMER 0x16 // What a device brings besides itself. This means memory that somebody has to register // with the controller, so the controller's own bank 2 does not count: it is already there. @@ -382,6 +407,10 @@ void clearInterrupt(uint8_t port); // never asked the device for anything. void clearAllInterrupts(void); +// The timer, which counts the machine's own cycles. +void timerReset(void); +void timerTick(unsigned long now); + // The lowest numbered port with its line up, or -1 if none of them are. int nextPendingInterrupt(void); diff --git a/Source/Emulator/machine.c b/Source/Emulator/machine.c index db97a40..97839a9 100644 --- a/Source/Emulator/machine.c +++ b/Source/Emulator/machine.c @@ -135,6 +135,7 @@ static int machineRestart(Machine *m) { } videoReset(); soundReset(); + timerReset(); consoleHome(); consoleResetInput(); // ---- And every line down ---- @@ -201,6 +202,7 @@ uint8_t machineStart(Machine *m, const EmulatorOptions *options, const char *pro // did not happen. videoReset(); soundReset(); + timerReset(); if (options->sound != NULL) { soundKeepSamples(); } diff --git a/SplitBit Programming Manual.md b/SplitBit Programming Manual.md index bfa811b..26461c6 100644 --- a/SplitBit Programming Manual.md +++ b/SplitBit Programming Manual.md @@ -551,6 +551,7 @@ If nothing is installed for the vector a device refused with, the machine stops | 0x12 | A device that owns 256 bytes of memory. Writing to its port fills that memory with the byte written, standing in for a disk controller reading a sector. Its memory is unreachable until it is registered as a bank. | 0x12 | | 0x30 - 0x3F | The screen. See The Screen. It brings video memory, which is unreachable until it is registered as a bank. | 0x14 | | 0x40 - 0x4F | The sound device. See Making A Noise. Four channels, played by writing to ports; it brings no memory. | 0x15 | +| 0x50 - 0x54 | The timer. See Keeping Time. Counts the machine's cycles and says when a period has gone by. | 0x16 | | 0xE0 - 0xEF | The memory controller. See The Memory Controller. | 0x03 | | 0xFF | The bus registry. See Asking What Is There. | 0x01 | @@ -988,7 +989,58 @@ One thing to be careful of: the registry remembers which port it was asked about | 0x13 | Disk. | | 0x14 | Screen. | | 0x15 | Sound. | -| 0x16 - 0xFF | Peripherals. | +| 0x16 | Timer. | +| 0x17 - 0xFF | Peripherals. | + +## Keeping Time: + +A period, in cycles, and a bit that says when one has gone by. + +Before this the only regular beat on the machine was the screen finishing a frame, and that is +a clock a program **borrows** rather than one it sets. A frame is 16,667 cycles and not +negotiable, so every duration becomes a multiple of it - and a sixteenth note at 120 beats a +minute is 125,000 cycles, which is seven and a half frames. It cannot be asked for at all. The +way round it is to choose a tempo whose subdivisions happen to land on whole frames, which is +making the music fit the machine. + +| Port | Register | +| --- | --- | +| 0x50 | Status. Bit 0 a period has gone by, bit 1 it is running, bit 2 it is set to interrupt. | +| 0x51 | Control. Bit 0 run, bit 1 repeat, bit 2 interrupt. | +| 0x52 - 0x54 | The period, in cycles, most significant byte first. | + +**The period is in cycles**, because that is what everything else here is counted in: it is +what the cost model counts and what a frame is measured in, so a timer counting anything else +would be a second unit to remember. Twenty four bits reaches from one cycle to sixteen and a +half seconds, and 120 beats a minute sits at 500,000 in the middle of it. There is no +prescaler, because there is no range left for one to buy. + +**Starting it loads the period.** Writing the control byte with the run bit already set does +not, so a program that turns interrupts on half way through a period does not silently move +the beat it was keeping. + +**With the repeat bit it reloads; without it, it stops** and the status port says so. What is +left over carries into the next period, so a timer asked for 1,000 cycles ticks every 1,000 +and not every 1,000 plus however late anybody looked. + +**Reading the status is what answers it**: the tick comes down when it is read, and the line +with it. A program that polls is not one that will answer a handler. + +```asm + ; A sixteenth note at 120 beats a minute, waited for rather than counted. + INIA 0x01 + OUTA 0x52 + INIA 0xE8 + OUTA 0x53 + INIA 0x48 + OUTA 0x54 ; 0x01E848, which is 125,000 + INIA 0x07 + OUTA 0x51 ; Run, repeat, interrupt + SIF + WAIT +``` + +Eight of those is one second, and a machine doing it spends 999,720 of those cycles asleep. ## The Memory Controller: diff --git a/SplitBit Test Manual.md b/SplitBit Test Manual.md index 0848619..edf7715 100644 --- a/SplitBit Test Manual.md +++ b/SplitBit Test Manual.md @@ -79,7 +79,7 @@ from `make`, not from here. ### 1. Recorded output `Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares -everything it printed against a file in `Tests/expected`. 182 tests, of which 120 run, 35 +everything it printed against a file in `Tests/expected`. 184 tests, of which 122 run, 35 only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image given at all. diff --git a/Tests/expected/timerBeatTest.out b/Tests/expected/timerBeatTest.out new file mode 100644 index 0000000..188f1f1 --- /dev/null +++ b/Tests/expected/timerBeatTest.out @@ -0,0 +1,3 @@ +........ +Execution halted. +[exit 0] diff --git a/Tests/expected/timerTest.out b/Tests/expected/timerTest.out new file mode 100644 index 0000000..1968f43 --- /dev/null +++ b/Tests/expected/timerTest.out @@ -0,0 +1,3 @@ +....Y.Y +Execution halted. +[exit 0] diff --git a/Tests/manifest b/Tests/manifest index 7329cab..5f2a1c1 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -838,6 +838,17 @@ cosmosDrivePath | CosmOS/Source/cosmos.asm | run | cosmosDri # place the shell looks. The drive it says afterwards is the check that fetching a program # did not move the person who ran it. cosmosCrossDisk | CosmOS/Source/cosmos.asm | run | cosmosCrossDisk.in | 90000000 | disks/cosmos.img+disks/other.img +# ---- A beat a program sets for itself ---- +# +# That reading the status is what takes the tick down, and that without the repeat bit it +# runs its period out and stops. NOT that the period is the length it says: settle() strips +# cycle counts from these recordings, so how long anything took cannot be checked here. +# Tests/terminal.sh measures that, which is where anything about cycles belongs. +timerTest | testPrograms/timerTest.asm | run | - | 5000000 | - +# And interrupting, which is what a music routine wants. 125,000 cycles is a sixteenth note +# at 120 beats a minute - seven and a half frames, so the screen cannot express it at all. +# Eight of them is a second, and nearly all of that second is spent asleep. +timerBeatTest | testPrograms/timerBeatTest.asm | run | - | 5000000 | - printDecimalTest | testPrograms/printDecimalTest.asm | xfail | - | - printDigitTest | testPrograms/printDigitTest.asm | xfail | - | - printHexTest | testPrograms/printHexTest.asm | xfail | - | - diff --git a/Tests/terminal.sh b/Tests/terminal.sh index 40bce14..b91b967 100755 --- a/Tests/terminal.sh +++ b/Tests/terminal.sh @@ -63,6 +63,8 @@ ASM # And the one that waits. Assembled from the repository rather than written inline, because # it is a real test program that run.sh also runs - there it proves the machine wakes up at # all, and here it proves it was asleep. +"$ROOT/Assembler" "$ROOT/Programs/testPrograms/timerBeatTest.asm" \ + -o "$BUILD/timerBeatTest.bin" >/dev/null "$ROOT/Assembler" "$ROOT/Programs/testPrograms/waitTest.asm" -o "$BUILD/waitTest.bin" \ >/dev/null 2>&1 || { echo "Could not assemble waitTest."; exit 1; } "$ROOT/SplitDisk" format "$BUILD/wait.img" 32 1 >/dev/null 2>&1 || { @@ -290,6 +292,34 @@ if os.path.exists(waitProgram) and os.path.exists(waitDisk): else: report(False, "a slow disk is waited for", "waitTest.bin or the disk is missing") +# ---- A period is a number of cycles, and this is where that can be said ---- +# +# The manifest records what a program printed with the cycle count stripped, which is right +# for everything else and useless for a clock: the whole claim the timer makes is about HOW +# LONG, and a recording that says "it printed eight dots" would pass on a timer that fired +# them all at once. +# +# Eight periods of 125,000 is a million cycles, and the program does nothing else worth +# counting. Within a couple of hundred, because starting and stopping cost a few instructions +# and the last tick is answered rather than waited for. +beatProgram = os.path.join(build, "timerBeatTest.bin") +if os.path.exists(beatProgram): + run = subprocess.run([emulator, beatProgram, "--fast", "--cycles", "5000000"], + capture_output=True, text=True) + got = re.search(r"halted after (\d+) cycles, (\d+) of them waiting", run.stdout) + if not got: + report(False, "eight beats take eight periods", "no cycle count was reported") + else: + total, idle = int(got.group(1)), int(got.group(2)) + report(abs(total - 1000000) < 500, "eight beats take eight periods", + "%d cycles against 8 x 125,000" % total) + # And it slept through them rather than counting. A timer a program has to poll is + # a timer that costs the machine everything it saves. + report(idle > total * 0.99, "and the machine slept between them", + "%d of %d idle (%.1f%%)" % (idle, total, 100.0 * idle / total)) +else: + report(False, "eight beats take eight periods", "timerBeatTest.bin is missing") + print() if problems: print("The terminal does not survive everything it should:")