diff --git a/bios.cpp b/bios.cpp index 664c6ed..9d4f758 100644 --- a/bios.cpp +++ b/bios.cpp @@ -7,6 +7,7 @@ #include "cpu.h" #ifdef TEENSYDUINO +#include #include "teensy-paddles.h" #endif @@ -147,14 +148,7 @@ bool BIOS::runUntilDone() break; case ACT_SPEED: currentCPUSpeedIndex++; -#ifdef TEENSYDUINO - // The Teensy doesn't have any overhead to spare. Allow slowing - // down the virtual CPU, but not speeding it up... - currentCPUSpeedIndex %= 2; -#else - // Other variants can support double and quad speeds. currentCPUSpeedIndex %= 4; -#endif switch (currentCPUSpeedIndex) { case CPUSPEED_HALF: g_speed = 1023000/2; @@ -296,16 +290,18 @@ uint8_t BIOS::GetAction(int8_t selection) #ifndef TEENSYDUINO usleep(100) #endif + threads.delay(1); ; // Wait for either a keypress or the reset button to be pressed } #ifdef TEENSYDUINO + // FIXME: debounce! if (digitalRead(RESETPIN) == LOW) { // wait until it's no longer pressed while (digitalRead(RESETPIN) == HIGH) ; - delay(100); // wait long enough for it to debounce + threads.delay(100); // wait long enough for it to debounce // then return an exit code return ACT_EXIT; } diff --git a/globals.cpp b/globals.cpp index 0222e48..90045d7 100644 --- a/globals.cpp +++ b/globals.cpp @@ -12,7 +12,6 @@ VMui *g_ui; int8_t g_volume = 15; uint8_t g_displayType = 3; // FIXME m_perfectcolor VMRam g_ram; -volatile bool g_inInterrupt = false; volatile uint8_t g_debugMode = D_NONE; bool g_prioritizeDisplay = false; volatile bool g_biosInterrupt = false; diff --git a/globals.h b/globals.h index 7be57bb..5c18710 100644 --- a/globals.h +++ b/globals.h @@ -47,11 +47,14 @@ extern VMui *g_ui; extern int8_t g_volume; extern uint8_t g_displayType; extern VMRam g_ram; -extern volatile bool g_inInterrupt; extern volatile uint8_t g_debugMode; extern bool g_prioritizeDisplay; extern volatile bool g_biosInterrupt; extern uint32_t g_speed; extern bool g_invertPaddleX; extern bool g_invertPaddleY; + +#include +extern Threads::Mutex spi_lock; + #endif diff --git a/teensy/teensy.ino b/teensy/teensy.ino index b58007d..257f26e 100644 --- a/teensy/teensy.ino +++ b/teensy/teensy.ino @@ -1,7 +1,8 @@ #include #include #include -#include +#include +#include #include "bios.h" #include "cpu.h" #include "applevm.h" @@ -26,9 +27,6 @@ #include "globals.h" #include "teensy-crash.h" -uint32_t nextInstructionMicros; -uint32_t startMicros; - BIOS bios; // How many microseconds per cycle @@ -38,6 +36,17 @@ static time_t getTeensy3Time() { return Teensy3Clock.get(); } TeensyUSB usb; +int cpuThreadId; +int displayThreadId; +int maintenanceThreadId; +int biosThreadId = -1; + +Bounce resetButtonDebouncer = Bounce(); +Threads::Mutex cpulock; // For the BIOS to suspend CPU cleanly +Threads::Mutex displaylock; // For the BIOS to shut down the display cleanly + +volatile bool g_writePrefsFromMainLoop = false; + void onKeypress(int unicode) { Serial.print("onKeypress:"); @@ -186,24 +195,29 @@ void setup() println("Reading prefs"); readPrefs(); // read from eeprom and set anything we need setting - startMicros = nextInstructionMicros = micros(); - // Debugging: insert a disk on startup... //((AppleVM *)g_vm)->insertDisk(0, "/A2DISKS/UTIL/mock2dem.dsk", false); //((AppleVM *)g_vm)->insertDisk(0, "/A2DISKS/JORJ/disk_s6d1.dsk", false); // ((AppleVM *)g_vm)->insertDisk(0, "/A2DISKS/GAMES/ALIBABA.DSK", false); - // pinMode(56, OUTPUT); - // pinMode(57, OUTPUT); - Serial.print("Free RAM: "); println(FreeRamEstimate()); + resetButtonDebouncer.attach(RESETPIN); + resetButtonDebouncer.interval(5); // ms + println("free-running"); - Timer1.initialize(3); - Timer1.attachInterrupt(runCPU); - Timer1.start(); + threads.setMicroTimer(); // use a 100uS timer instead of a 1mS timer + cpuThreadId = threads.addThread(runCPU); + displayThreadId = threads.addThread(runDisplay); + maintenanceThreadId = threads.addThread(runMaintenance); + // Set the relative priorities of the threads by defining how long a "slice" + // is for each (in 100uS "ticks") + // At a ratio of 50:10:1, we get about 30FPS and 100% CPU speed. + threads.setTimeSlice(displayThreadId, 100); + threads.setTimeSlice(cpuThreadId, 20); + threads.setTimeSlice(maintenanceThreadId, 1); } // FIXME: move these memory-related functions elsewhere... @@ -234,156 +248,229 @@ int heapSize(){ void biosInterrupt() { - Timer1.stop(); + // Make sure the CPU and display don't run while we're in interrupt. + Threads::Scope lock1(cpulock); + Threads::Scope lock2(displaylock); + Serial.println("Waiting for button to be released"); // wait for the interrupt button to be released - while (digitalRead(RESETPIN) == LOW) + while (!resetButtonDebouncer.read()) ; + Serial.println("Invoking BIOS"); // invoke the BIOS if (bios.runUntilDone()) { // if it returned true, we have something to store persistently in EEPROM. - writePrefs(); + // The EEPROM doesn't like to be written to from a thread? + Serial.println("Writing prefs"); + g_writePrefsFromMainLoop = true; + while (g_writePrefsFromMainLoop) { + Serial.println("Waiting for prefs to be written"); + delay(100); + // wait for write to complete + } // Also might have changed the paddles state + Serial.println("Updating paddle state"); TeensyPaddles *tmp = (TeensyPaddles *)g_paddles; tmp->setRev(g_invertPaddleX, g_invertPaddleY); - } + Serial.println("Cleaning up"); // if we turned off debugMode, make sure to clear the debugMsg if (g_debugMode == D_NONE) { g_display->debugMsg(""); } // clear the CPU next-step counters + #if 0 + // FIXME: this is to prevent the CPU from racing to catch up, and we need sth in the threads world g_cpu->cycles = 0; nextInstructionMicros = micros(); startMicros = micros(); + #endif // Drain the speaker queue (FIXME: a little hacky) g_speaker->maintainSpeaker(-1, -1); + Serial.println("Forcing display redraw"); // Force the display to redraw g_display->redraw(); // Redraw the UI ((AppleDisplay*)(g_vm->vmdisplay))->modeChange(); // force a full re-draw and blit + Serial.println("re-priming keyboard"); // Poll the keyboard before we start, so we can do selftest on startup g_keyboard->maintainKeyboard(); - - Timer1.start(); } //bool debugState = false; //bool debugLCDState = false; +// FIXME: how often does this really need to run? We can threads.yield() when we're running too quickly +void runMaintenance() +{ + uint32_t nextRuntime = 0; + + while (1) { + if (millis() > nextRuntime) { + nextRuntime = millis() + 100; // FIXME: what's a good time here + + if (biosThreadId == -1) { + // bios is not running; see if it should be + if (!resetButtonDebouncer.read()) { + // This is the BIOS interrupt. We immediately act on it. + + biosThreadId = threads.addThread(biosInterrupt); + } + } else if (threads.getState(biosThreadId) != Threads::RUNNING) { + // When the BIOS thread exits, we clean up + Serial.println("Cleaing up bios thread"); + threads.wait(biosThreadId); + Serial.println("BIOS thread is cleaned"); + biosThreadId = -1; + } + + g_keyboard->maintainKeyboard(); + usb.maintain(); + + static unsigned long nextBattCheck = millis() + 30;// debugging + static int batteryLevel = 0; // static for debugging code! When done + // debugging, this can become a local + // in the appropriate block below + if (millis() >= nextBattCheck) { + // FIXME: what about rollover? + nextBattCheck = millis() + 3 * 1000; // check every 3 seconds + + // This is a bit disruptive - but the external 3.3v will drop along with the battery level, so we should use the more stable (I hope) internal 1.7v. + // The alternative is to build a more stable buck/boost regulator for reference... + + batteryLevel = analogRead(BATTERYPIN); + + /* LiIon charge to a max of 4.2v; and we should not let them discharge below about 3.5v. + * With a resistor voltage divider of Z1=39k, Z2=10k we're looking at roughly 20.4% of + * those values: (10/49) * 4.2 = 0.857v, and (10/49) * 3.5 = 0.714v. Since the external + * voltage reference flags as the battery drops, we can't use that as an absolute + * reference. So using the INTERNAL 1.1v reference, that should give us a reasonable + * range, in theory; the math shows the internal reference to be about 1.27v (assuming + * the resistors are indeed 39k and 10k, which is almost certainly also wrong). But + * then the high end would be 172, and the low end is about 142, which matches my + * actual readings here very well. + * + * Actual measurements: + * 3.46v = 144 - 146 + * 4.21v = 172 + */ +#if 0 + Serial.print("battery: "); + println(batteryLevel); +#endif + + if (batteryLevel < 146) + batteryLevel = 146; + if (batteryLevel > 168) + batteryLevel = 168; + + batteryLevel = map(batteryLevel, 146, 168, 0, 100); + g_ui->drawPercentageUIElement(UIePowerPercentage, batteryLevel); + } + } else { + threads.delay(10); + // threads.yield(); + } + } +} + +// FIXME: figure out how to limit this to 30 FPS (or whatver) so we can +// appropriately use threads.yield() +void runDisplay() +{ + while (1) { + { + Threads::Scope lock(displaylock); + doDebugging(); + + // FIXME: this is sometimes *VERY* slow. + uint32_t startDisp = millis(); + uint32_t cpuBefore = g_cpu->cycles; + g_ui->blit(); + g_vm->vmdisplay->lockDisplay(); + if (g_vm->vmdisplay->needsRedraw()) { // necessary for the VM to redraw + // Used to get the dirty rect and blit just that rect. Could still do, + // but instead, I'm just wildly wasting resources. MWAHAHAHA + // AiieRect what = g_vm->vmdisplay->getDirtyRect(); + g_vm->vmdisplay->didRedraw(); + // g_display->blit(what); + } + g_display->blit(); // Blit the whole thing, including UI area + g_vm->vmdisplay->unlockDisplay(); + uint32_t dispTime = millis() - startDisp; + uint32_t cpuAfter = g_cpu->cycles; + if (dispTime > 75) { + Serial.print("Slow blit: "); + Serial.print(dispTime); + Serial.print(" cpu ran: "); + Serial.println(cpuAfter - cpuBefore); + } + } + } +} void runCPU() { - g_inInterrupt = true; - // Debugging: to watch when the speaker is triggered... - // static bool debugState = false; - // debugState = !debugState; - // digitalWrite(56, debugState); - - // Relatively critical timing: CPU needs to run ahead at least 4 - // cycles, b/c we're calling this interrupt (runCPU, that is) just - // about 1/3 as fast as we should; and the speaker is updated - // directly from within it, so it needs to be real-ish time. - if (micros() > nextInstructionMicros) { - // Debugging: to watch when the CPU is triggered... - // static bool debugState = false; - // debugState = !debugState; - // digitalWrite(56, debugState); - - uint8_t executed = g_cpu->Run(24); + uint32_t nextInstructionMicros; + uint32_t startMicros; + startMicros = nextInstructionMicros = micros(); - // The CPU of the Apple //e ran at 1.023 MHz. Adjust when we think - // the next instruction should run based on how long the execution - // was ((1000/1023) * numberOfCycles) - which is about 97.8%. - nextInstructionMicros = startMicros + ((double)g_cpu->cycles * (double)SPEEDCTL); - - ((AppleVM *)g_vm)->cpuMaintenance(g_cpu->cycles); + uint32_t startMillis = millis(); + + Serial.println("CPU thread is started"); + while (1) { + // Relatively critical timing: CPU needs to run ahead at least 4 + // cycles, b/c we're calling this interrupt (runCPU, that is) just + // about 1/3 as fast as we should; and the speaker is updated + // directly from within it, so it needs to be real-ish time. + if (micros() >= nextInstructionMicros) { + uint32_t expectedCycles = (micros() - startMicros) * SPEEDCTL; + + uint8_t executed; + cpulock.lock(); // Blocking; if the BIOS is running, we stall here + executed = g_cpu->Run(24); + cpulock.unlock(); + + // The CPU of the Apple //e ran at 1.023 MHz. Adjust when we think + // the next instruction should run based on how long the execution + // was ((1000/1023) * numberOfCycles) - which is about 97.8%. + if (expectedCycles > g_cpu->cycles) { + nextInstructionMicros = micros(); +#if 0 + // show a warning on serial about our current performance + double percentage = ((double)g_cpu->cycles / (double)expectedCycles) * 100.0; + static uint32_t nextWarningTime = 0; + if (millis() > nextWarningTime) { + static char buf[100]; + sprintf(buf, "CPU running at %f%% of %d", percentage, g_speed); + Serial.println(buf); + nextWarningTime = millis() + 1000; + } +#endif + } else { + nextInstructionMicros = startMicros + ((double)g_cpu->cycles * (double)SPEEDCTL); + } + + ((AppleVM *)g_vm)->cpuMaintenance(g_cpu->cycles); + } else { + // threads.yield(); + threads.delay(1); + } } - - g_inInterrupt = false; } void loop() { - if (digitalRead(RESETPIN) == LOW) { - // This is the BIOS interrupt. We immediately act on it. - biosInterrupt(); - } + resetButtonDebouncer.update(); - g_keyboard->maintainKeyboard(); - usb.maintain(); - - //debugLCDState = !debugLCDState; - //digitalWrite(57, debugLCDState); - - doDebugging(); - - // FIXME: this is sometimes *VERY* slow. - uint32_t startDisp = millis(); - uint32_t cpuBefore = g_cpu->cycles; - g_ui->blit(); - g_vm->vmdisplay->lockDisplay(); - if (g_vm->vmdisplay->needsRedraw()) { // necessary for the VM to redraw - // Used to get the dirty rect and blit just that rect. Could still do, - // but instead, I'm just wildly wasting resources. MWAHAHAHA - // AiieRect what = g_vm->vmdisplay->getDirtyRect(); - g_vm->vmdisplay->didRedraw(); - // g_display->blit(what); - } - g_display->blit(); // Blit the whole thing, including UI area - g_vm->vmdisplay->unlockDisplay(); - uint32_t dispTime = millis() - startDisp; - uint32_t cpuAfter = g_cpu->cycles; - if (dispTime > 30) { - Serial.print("Slow blit: "); - Serial.print(dispTime); - Serial.print(" cpu ran: "); - Serial.println(cpuAfter - cpuBefore); - } - - static unsigned long nextBattCheck = millis() + 30;// debugging - static int batteryLevel = 0; // static for debugging code! When done - // debugging, this can become a local - // in the appropriate block below - if (millis() >= nextBattCheck) { - // FIXME: what about rollover? - nextBattCheck = millis() + 3 * 1000; // check every 3 seconds - - // This is a bit disruptive - but the external 3.3v will drop along with the battery level, so we should use the more stable (I hope) internal 1.7v. - // The alternative is to build a more stable buck/boost regulator for reference... - - batteryLevel = analogRead(BATTERYPIN); - - /* LiIon charge to a max of 4.2v; and we should not let them discharge below about 3.5v. - * With a resistor voltage divider of Z1=39k, Z2=10k we're looking at roughly 20.4% of - * those values: (10/49) * 4.2 = 0.857v, and (10/49) * 3.5 = 0.714v. Since the external - * voltage reference flags as the battery drops, we can't use that as an absolute - * reference. So using the INTERNAL 1.1v reference, that should give us a reasonable - * range, in theory; the math shows the internal reference to be about 1.27v (assuming - * the resistors are indeed 39k and 10k, which is almost certainly also wrong). But - * then the high end would be 172, and the low end is about 142, which matches my - * actual readings here very well. - * - * Actual measurements: - * 3.46v = 144 - 146 - * 4.21v = 172 - */ -#if 1 - Serial.print("battery: "); - println(batteryLevel); -#endif - - if (batteryLevel < 146) - batteryLevel = 146; - if (batteryLevel > 168) - batteryLevel = 168; - - batteryLevel = map(batteryLevel, 146, 168, 0, 100); - g_ui->drawPercentageUIElement(UIePowerPercentage, batteryLevel); + if (g_writePrefsFromMainLoop) { + Serial.println("Writing prefs"); + writePrefs(); + g_writePrefsFromMainLoop = false; } } @@ -481,10 +568,7 @@ void writePrefs() TeensyPrefs np; prefs_t p; - g_display->clrScr(); - g_display->drawString(M_SELECTED, 80, 100,"Writing prefs..."); - g_display->flush(); - + Serial.println("writePrefs()"); p.magic = PREFSMAGIC; p.prefsSize = sizeof(prefs_t); p.version = PREFSVERSION; @@ -502,7 +586,5 @@ void writePrefs() strcpy(p.hd1, ((AppleVM *)g_vm)->HDName(0)); strcpy(p.hd2, ((AppleVM *)g_vm)->HDName(1)); - Timer1.stop(); bool ret = np.writePrefs(&p); - Timer1.start(); }