threads conversion

This commit is contained in:
Jorj Bauer 2020-07-08 16:37:26 -04:00
parent a78b4ff203
commit e1288db403
4 changed files with 213 additions and 133 deletions

View File

@ -7,6 +7,7 @@
#include "cpu.h" #include "cpu.h"
#ifdef TEENSYDUINO #ifdef TEENSYDUINO
#include <TeensyThreads.h>
#include "teensy-paddles.h" #include "teensy-paddles.h"
#endif #endif
@ -147,14 +148,7 @@ bool BIOS::runUntilDone()
break; break;
case ACT_SPEED: case ACT_SPEED:
currentCPUSpeedIndex++; 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; currentCPUSpeedIndex %= 4;
#endif
switch (currentCPUSpeedIndex) { switch (currentCPUSpeedIndex) {
case CPUSPEED_HALF: case CPUSPEED_HALF:
g_speed = 1023000/2; g_speed = 1023000/2;
@ -296,16 +290,18 @@ uint8_t BIOS::GetAction(int8_t selection)
#ifndef TEENSYDUINO #ifndef TEENSYDUINO
usleep(100) usleep(100)
#endif #endif
threads.delay(1);
; ;
// Wait for either a keypress or the reset button to be pressed // Wait for either a keypress or the reset button to be pressed
} }
#ifdef TEENSYDUINO #ifdef TEENSYDUINO
// FIXME: debounce!
if (digitalRead(RESETPIN) == LOW) { if (digitalRead(RESETPIN) == LOW) {
// wait until it's no longer pressed // wait until it's no longer pressed
while (digitalRead(RESETPIN) == HIGH) 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 // then return an exit code
return ACT_EXIT; return ACT_EXIT;
} }

View File

@ -12,7 +12,6 @@ VMui *g_ui;
int8_t g_volume = 15; int8_t g_volume = 15;
uint8_t g_displayType = 3; // FIXME m_perfectcolor uint8_t g_displayType = 3; // FIXME m_perfectcolor
VMRam g_ram; VMRam g_ram;
volatile bool g_inInterrupt = false;
volatile uint8_t g_debugMode = D_NONE; volatile uint8_t g_debugMode = D_NONE;
bool g_prioritizeDisplay = false; bool g_prioritizeDisplay = false;
volatile bool g_biosInterrupt = false; volatile bool g_biosInterrupt = false;

View File

@ -47,11 +47,14 @@ extern VMui *g_ui;
extern int8_t g_volume; extern int8_t g_volume;
extern uint8_t g_displayType; extern uint8_t g_displayType;
extern VMRam g_ram; extern VMRam g_ram;
extern volatile bool g_inInterrupt;
extern volatile uint8_t g_debugMode; extern volatile uint8_t g_debugMode;
extern bool g_prioritizeDisplay; extern bool g_prioritizeDisplay;
extern volatile bool g_biosInterrupt; extern volatile bool g_biosInterrupt;
extern uint32_t g_speed; extern uint32_t g_speed;
extern bool g_invertPaddleX; extern bool g_invertPaddleX;
extern bool g_invertPaddleY; extern bool g_invertPaddleY;
#include <TeensyThreads.h>
extern Threads::Mutex spi_lock;
#endif #endif

View File

@ -1,7 +1,8 @@
#include <Arduino.h> #include <Arduino.h>
#include <SPI.h> #include <SPI.h>
#include <TimeLib.h> #include <TimeLib.h>
#include <TimerOne.h> #include <TeensyThreads.h>
#include <Bounce2.h>
#include "bios.h" #include "bios.h"
#include "cpu.h" #include "cpu.h"
#include "applevm.h" #include "applevm.h"
@ -26,9 +27,6 @@
#include "globals.h" #include "globals.h"
#include "teensy-crash.h" #include "teensy-crash.h"
uint32_t nextInstructionMicros;
uint32_t startMicros;
BIOS bios; BIOS bios;
// How many microseconds per cycle // How many microseconds per cycle
@ -38,6 +36,17 @@ static time_t getTeensy3Time() { return Teensy3Clock.get(); }
TeensyUSB usb; 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) void onKeypress(int unicode)
{ {
Serial.print("onKeypress:"); Serial.print("onKeypress:");
@ -186,24 +195,29 @@ void setup()
println("Reading prefs"); println("Reading prefs");
readPrefs(); // read from eeprom and set anything we need setting readPrefs(); // read from eeprom and set anything we need setting
startMicros = nextInstructionMicros = micros();
// Debugging: insert a disk on startup... // Debugging: insert a disk on startup...
//((AppleVM *)g_vm)->insertDisk(0, "/A2DISKS/UTIL/mock2dem.dsk", false); //((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/JORJ/disk_s6d1.dsk", false);
// ((AppleVM *)g_vm)->insertDisk(0, "/A2DISKS/GAMES/ALIBABA.DSK", false); // ((AppleVM *)g_vm)->insertDisk(0, "/A2DISKS/GAMES/ALIBABA.DSK", false);
// pinMode(56, OUTPUT);
// pinMode(57, OUTPUT);
Serial.print("Free RAM: "); Serial.print("Free RAM: ");
println(FreeRamEstimate()); println(FreeRamEstimate());
resetButtonDebouncer.attach(RESETPIN);
resetButtonDebouncer.interval(5); // ms
println("free-running"); println("free-running");
Timer1.initialize(3); threads.setMicroTimer(); // use a 100uS timer instead of a 1mS timer
Timer1.attachInterrupt(runCPU); cpuThreadId = threads.addThread(runCPU);
Timer1.start(); 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... // FIXME: move these memory-related functions elsewhere...
@ -234,117 +248,89 @@ int heapSize(){
void biosInterrupt() 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 // wait for the interrupt button to be released
while (digitalRead(RESETPIN) == LOW) while (!resetButtonDebouncer.read())
; ;
Serial.println("Invoking BIOS");
// invoke the BIOS // invoke the BIOS
if (bios.runUntilDone()) { if (bios.runUntilDone()) {
// if it returned true, we have something to store persistently in EEPROM. // 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 // Also might have changed the paddles state
Serial.println("Updating paddle state");
TeensyPaddles *tmp = (TeensyPaddles *)g_paddles; TeensyPaddles *tmp = (TeensyPaddles *)g_paddles;
tmp->setRev(g_invertPaddleX, g_invertPaddleY); tmp->setRev(g_invertPaddleX, g_invertPaddleY);
} }
Serial.println("Cleaning up");
// if we turned off debugMode, make sure to clear the debugMsg // if we turned off debugMode, make sure to clear the debugMsg
if (g_debugMode == D_NONE) { if (g_debugMode == D_NONE) {
g_display->debugMsg(""); g_display->debugMsg("");
} }
// clear the CPU next-step counters // 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; g_cpu->cycles = 0;
nextInstructionMicros = micros(); nextInstructionMicros = micros();
startMicros = micros(); startMicros = micros();
#endif
// Drain the speaker queue (FIXME: a little hacky) // Drain the speaker queue (FIXME: a little hacky)
g_speaker->maintainSpeaker(-1, -1); g_speaker->maintainSpeaker(-1, -1);
Serial.println("Forcing display redraw");
// Force the display to redraw // Force the display to redraw
g_display->redraw(); // Redraw the UI g_display->redraw(); // Redraw the UI
((AppleDisplay*)(g_vm->vmdisplay))->modeChange(); // force a full re-draw and blit ((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 // Poll the keyboard before we start, so we can do selftest on startup
g_keyboard->maintainKeyboard(); g_keyboard->maintainKeyboard();
Timer1.start();
} }
//bool debugState = false; //bool debugState = false;
//bool debugLCDState = false; //bool debugLCDState = false;
// FIXME: how often does this really need to run? We can threads.yield() when we're running too quickly
void runCPU() void runMaintenance()
{ {
g_inInterrupt = true; uint32_t nextRuntime = 0;
// 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 while (1) {
// cycles, b/c we're calling this interrupt (runCPU, that is) just if (millis() > nextRuntime) {
// about 1/3 as fast as we should; and the speaker is updated nextRuntime = millis() + 100; // FIXME: what's a good time here
// 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); if (biosThreadId == -1) {
// bios is not running; see if it should be
// The CPU of the Apple //e ran at 1.023 MHz. Adjust when we think if (!resetButtonDebouncer.read()) {
// 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);
}
g_inInterrupt = false;
}
void loop()
{
if (digitalRead(RESETPIN) == LOW) {
// This is the BIOS interrupt. We immediately act on it. // This is the BIOS interrupt. We immediately act on it.
biosInterrupt();
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(); g_keyboard->maintainKeyboard();
usb.maintain(); 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 unsigned long nextBattCheck = millis() + 30;// debugging
static int batteryLevel = 0; // static for debugging code! When done static int batteryLevel = 0; // static for debugging code! When done
// debugging, this can become a local // debugging, this can become a local
@ -372,7 +358,7 @@ void loop()
* 3.46v = 144 - 146 * 3.46v = 144 - 146
* 4.21v = 172 * 4.21v = 172
*/ */
#if 1 #if 0
Serial.print("battery: "); Serial.print("battery: ");
println(batteryLevel); println(batteryLevel);
#endif #endif
@ -385,6 +371,107 @@ void loop()
batteryLevel = map(batteryLevel, 146, 168, 0, 100); batteryLevel = map(batteryLevel, 146, 168, 0, 100);
g_ui->drawPercentageUIElement(UIePowerPercentage, batteryLevel); 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()
{
uint32_t nextInstructionMicros;
uint32_t startMicros;
startMicros = nextInstructionMicros = micros();
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);
}
}
}
void loop()
{
resetButtonDebouncer.update();
if (g_writePrefsFromMainLoop) {
Serial.println("Writing prefs");
writePrefs();
g_writePrefsFromMainLoop = false;
}
} }
void doDebugging() void doDebugging()
@ -481,10 +568,7 @@ void writePrefs()
TeensyPrefs np; TeensyPrefs np;
prefs_t p; prefs_t p;
g_display->clrScr(); Serial.println("writePrefs()");
g_display->drawString(M_SELECTED, 80, 100,"Writing prefs...");
g_display->flush();
p.magic = PREFSMAGIC; p.magic = PREFSMAGIC;
p.prefsSize = sizeof(prefs_t); p.prefsSize = sizeof(prefs_t);
p.version = PREFSVERSION; p.version = PREFSVERSION;
@ -502,7 +586,5 @@ void writePrefs()
strcpy(p.hd1, ((AppleVM *)g_vm)->HDName(0)); strcpy(p.hd1, ((AppleVM *)g_vm)->HDName(0));
strcpy(p.hd2, ((AppleVM *)g_vm)->HDName(1)); strcpy(p.hd2, ((AppleVM *)g_vm)->HDName(1));
Timer1.stop();
bool ret = np.writePrefs(&p); bool ret = np.writePrefs(&p);
Timer1.start();
} }