#include #include #include #include "bios.h" #include "cpu.h" #include "applevm.h" #include "teensy-display.h" #include "teensy-keyboard.h" #include "teensy-mouse.h" #include "teensy-speaker.h" #include "teensy-paddles.h" #include "teensy-filemanager.h" #include "teensy-usb.h" #include "appleui.h" #include "teensy-prefs.h" #include "teensy-println.h" #include "smalloc.h" //#define DEBUG_TIMING #if F_CPU < 240000000 #pragma AiiE warning: performance will improve if you overclock the Teensy to 240MHz (F_CPU=240MHz) or 256MHz (F_CPU=256MHz) #endif #if F_CPU == 600000000 #pragma AiiE suggestion: if you underclock to 528MHz (F_CPU=528MHz) then it will use significantly less power, and still perform perfectly #endif #define RESETPIN 38 #define DEBUGPIN 23 #define BATTERYLEVEL 20 // analog reading of battery voltage (scaled to half) #define BATTERYSELECT 21 // digital select that turns on the power reading ckt #include "globals.h" BIOS bios; // How many microseconds per cycle #define SPEEDCTL ((float)1000000/(float)g_speed) static time_t getTeensy3Time() { return Teensy3Clock.get(); } TeensyUSB usb; Bounce resetButtonDebouncer = Bounce(); volatile bool cpuClockInitialized = false; // The battery voltage measurement comes through a 50% ratio voltage // divider; and the analog resolution is set to 8 bits (so a max of // 256); with a fixed voltage reference of 3.3v (standard in the // Teensy 4.1). Since the voltage of a 16550 battery is 4.2v (at // 100%) to 2.5v (at 0%), that means we should expect the // currentBatteryReading to be about 97 - 163. Since this is // imperfect due to tolerance in the resistors and whatnot, we might // as well call that 100 - 160. volatile uint16_t currentBatteryReading = 0; volatile uint16_t currentBatteryCount = 0; volatile uint16_t currentBatterySum = 0; #define BATTERYMIN 100 #define BATTERYMAX 160 // how often should we read the battery level? #define BATTERYPERIOD (60 * 100000) // FIXME: abstract this into the USB code; doesn't belong in the root... #include "physicalkeyboard.h" // https://www.win.tue.nl/~aeb/linux/kbd/scancodes-14.html static uint8_t usb_scanmap[256] = { 0, 0, 0, 0, // 0-3 don't exist 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', // keycodes 4-29 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', // keycodes 30-39 PK_RET, // keycode 40 PK_ESC, // 41 PK_DEL, // 42 PK_TAB, ' ', // space bar '-', '=', '[', ']', '\\', 0, // 50 ';', '\'', '`', ',', '.', '/', PK_LOCK, // 57 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 58-69, F1-F12 keys 0, 0, 0, 0, 0, 0, 0, 0, 0, // PrtScr, scroll lock, pause, insert, home, PgUp, Delete, End, PgDown PK_RARR, PK_LARR, PK_DARR, PK_UARR, // 79-82, arrow keys 0, // 83 num lock '/', '*', '-', '+', PK_RET, '0', '1', '2', // 84-99 keypad, which we just... '3', '4', '5', '6', '7', '8', '9', '.', // ... use as their "normal" keys 0, // 100 undefined PK_RA, // 101: "application" key 0, // 102 "power" key PK_CTRL, // 103 keypad '=' but it's my left control key PK_LSHFT, // 104, "f13" but it's my left shift key PK_LA, // 105: "f14" but it's my left alt key PK_LA, // 106: "f15" but it's the windows/command key PK_CTRL, // 107: "f16" but it's my right control key PK_RSHFT, // 108: "f17" but it's my right shift key PK_RA, // 109: "f18" but it's my right alt key 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 110-119 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 120-129 0, 0, 0, // 130-132 ',', // 133: keypad , '=', // 134: keypad = 0, 0, 0, 0, 0, // 135-139 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 140-149 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 150-159 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 160-169 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 170-179 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 180-189 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 190-199 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 200-209 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 210-219 0, 0, 0, 0, // 220-223 PK_CTRL, // 224: left control (but not on my keyboard) PK_LSHFT, // 225: left shift (but not on my keyboard) PK_LA, PK_LA, // 226, 227: left alt, left GUI (but not on my keyboard) PK_CTRL, // 228: right control (but not on my keyboard) PK_RSHFT, // 229: right shift (but not on my keyboard) PK_RA, PK_RA, // 230, 231: right alt, right GUI (but not on my keyboard) 0, 0, 0, 0, 0, 0, 0, 0, // 232-239 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 240-249 0, 0, 0, 0, 0, 0 // 250-255 }; EXTMEM uint8_t keysPressed[256]; // FIXME: if we need to save RAM, make this bitflags void onKeypress(uint8_t keycode) { if (keysPressed[keycode]) return; // defeat auto-repeat if (!usb_scanmap[keycode]) return; // skip undefined keys if (keycode == 67 || keycode == 70) { // F10 or PrtSc/SysRq are interrupt buttons. Probably needs to be // configurable somehow... g_biosInterrupt = true; } else { keysPressed[keycode] = 1; ((TeensyKeyboard *)g_keyboard)->pressedKey(usb_scanmap[keycode]); } } void onKeyrelease(uint8_t keycode) { if (!keysPressed[keycode]) return; // defeat auto-repeat if (!usb_scanmap[keycode]) return; // skip undefined keys keysPressed[keycode] = 0; ((TeensyKeyboard *)g_keyboard)->releasedKey(usb_scanmap[keycode]); } void setup() { Serial.begin(230400); #if 0 // Wait for USB serial connection before booting while debugging while (!Serial) { yield(); } delay(2000); #endif delay(200); // let the power settle & serial to get its bearings if (CrashReport) { Serial.print(CrashReport); delay(5000); } pinMode(DEBUGPIN, OUTPUT); // for debugging pinMode(BATTERYSELECT, OUTPUT); digitalWrite(BATTERYSELECT, false); // leave it off by default pinMode(BATTERYLEVEL, INPUT); // enableFaultHandler(); // SCB_SHCSR |= SCB_SHCSR_BUSFAULTENA | SCB_SHCSR_USGFAULTENA | SCB_SHCSR_MEMFAULTENA; memset(keysPressed, 0, sizeof(keysPressed)); // set the Time library to use Teensy 3.0's RTC to keep time setSyncProvider(getTeensy3Time); delay(100); // don't know if we need this if (timeStatus() == timeSet) { println("RTC set from Teensy"); } else { println("Error while setting RTC"); } pinMode(RESETPIN, INPUT); digitalWrite(RESETPIN, HIGH); analogReadRes(8); // We only need 8 bits of resolution (0-255) for paddles analogReadAveraging(4); // ?? dunno if we need this or not. println("creating virtual hardware"); g_speaker = new TeensySpeaker(18, 19); // FIXME abstract constants println(" fm"); // First create the filemanager - the interface to the host file system. g_filemanager = new TeensyFileManager(); // Construct the interface to the host display. This will need the // VM's video buffer in order to draw the VM, but we don't have that // yet. println(" display"); g_display = new TeensyDisplay(); println(" UI"); g_ui = new AppleUI(); // Next create the virtual CPU. This needs the VM's MMU in order to // run, but we don't have that yet. println(" cpu"); g_cpu = new Cpu(); // Create the virtual machine. This may read from g_filemanager to // get ROMs if necessary. (The actual Apple VM we've built has them // compiled in, though.) It will create its virutal hardware (MMU, // video driver, floppy, paddles, whatever). println(" vm"); Serial.flush(); g_vm = new AppleVM(); // Now that the VM exists and it has created an MMU, we tell the CPU // how to access memory through the MMU. println(" [setMMU]"); g_cpu->SetMMU(g_vm->getMMU()); // the paddles are used by the teensy mouse println(" paddles"); g_paddles = new TeensyPaddles(A3, A2, g_invertPaddleX, g_invertPaddleY); // The keyboard reaches in to the mouse println(" mouse"); g_mouse = new TeensyMouse(); // And the physical keyboard needs hooks in to the virtual keyboard... println(" keyboard"); g_keyboard = new TeensyKeyboard(g_vm->getKeyboard()); // the usb keyboard piggybacks on g_keyboard println(" usb"); usb.init(); usb.attachKeypress(onKeypress); usb.attachKeyrelease(onKeyrelease); // Now that all the virtual hardware is glued together, reset the VM println("Resetting VM"); g_vm->Reset(); println("Reading prefs"); readPrefs(); // read from eeprom and set anything we need setting g_speaker->begin(); // let the speaker reset its volume from g_volume resetButtonDebouncer.attach(RESETPIN); resetButtonDebouncer.interval(5); // ms println("Drawing UI border"); g_display->redraw(); println("free-running"); Serial.flush(); } // FIXME: move these memory-related functions elsewhere... // This only gives you an estimated free mem size. It's not perfect. uint32_t FreeIntRamEstimate() { uint32_t heapTop; // The Teensy 4.1 has different memory regions; the stack grows down // from the top of RAM1, and the heap gros up from the start of // RAM2. The end of RAM2 is 0x20280000, so if we malloc a byte we // should be able to calculate a gross estimate (ignoring memory // holes created by fragmentation of course). void* hTop = malloc(1); heapTop = (uint32_t) hTop; free(hTop); return 0x20280000 - heapTop; } uint32_t FreeExtRamEstimate() { // EXTMEM uses a different thing entirely - the smalloc library is // embedded in TeensyDuino (as of this writing) and we should be // able to query it to see how much ram exists, is in use, and is // free. However, at some point this will break, and we'll have to // figure out what new library Teensyduino moved to... size_t total = 0, totalUser = 0, freespace = 0; int blocks; // number of blocks allocated sm_malloc_stats_pool(&extmem_smalloc_pool, &total, &totalUser, &freespace, &blocks); // total and totalUser always seem to be 0. So is blocks. But freespace might be real? return freespace; } #include "malloc.h" int heapSize(){ return mallinfo().uordblks; } void runMaintenance(uint32_t now) { static uint32_t nextRuntime = 0; if (now >= nextRuntime) { // Run maintenance at 60 Hz because the mouse will need it nextRuntime = now + 16667; if (!resetButtonDebouncer.read()) { // This is the BIOS interrupt. Wait for it to clear and process it. while (!resetButtonDebouncer.read()) resetButtonDebouncer.update(); g_biosInterrupt = true; } if (!g_biosInterrupt) { g_mouse->maintainMouse(); g_keyboard->maintainKeyboard(); usb.maintain(); } } } #define TARGET_FPS 30 void runDisplay(uint32_t now) { // When do we want to reset our expectation of "normal"? static uint32_t nextResetMicros = 0; // how many full display refreshes have we managed in this second? static uint32_t refreshCount = 0; // how many micros until the next frame refresh? static uint32_t microsAtStart = 0; static uint32_t microsForNext = micros(); static uint32_t lastFps = 0; static uint32_t displayFrameCount = 0; // If it's time to draw the next frame, then do so if (now >= microsForNext) { refreshCount++; microsForNext = microsAtStart + (1000000.0*((float)refreshCount/(float)TARGET_FPS)); { static uint32_t nextDebugTime = 0; if (millis() > nextDebugTime) { doDebugging(lastFps); nextDebugTime = millis() + 1000; } } if (!g_biosInterrupt) { // FIXME this needs some love. It could be efficient, but parts are removed, so it's doing duplicative work. 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(); } } // Once a second, start counting all over again if (now >= nextResetMicros) { uint32_t newFrameCount = ((TeensyDisplay *)g_display)->frameCount(); // There are two "FPS" counters here, actually. One is how often // we're polling the Apple //e memory to refresh the DMA buffer, // and to show that, we'd use this: // lastFps = refreshCount; // The other is how often the DMA code is refreshing the actual // display, and to show that, we'd use this: lastFps = newFrameCount - displayFrameCount; #ifdef DEBUG_TIMING // ... and this debugging code shows both. println("DMA buffer refresh at ", refreshCount, " FPS"); println("Display refresh at ", newFrameCount - displayFrameCount, " FPS"); #endif displayFrameCount = newFrameCount; nextResetMicros = now + 1000000; refreshCount = 0; microsAtStart = now; microsForNext = microsAtStart + (1000000.0*((float)refreshCount/(float)TARGET_FPS)); } } // The debouncer is used in the bios, which blocks the main loop // execution; so this function updates the debouncer instead. It used // to be a thread of its own, but now that this is single-threaded // again, it's a standalone method. void runDebouncer() { static uint32_t nextRuntime = 0; if (millis() >= nextRuntime) { nextRuntime = millis() + 10; resetButtonDebouncer.update(); } else { yield(); } } void runBIOS(uint32_t now) { static uint32_t nextResetMicros = 0; static uint32_t countSinceLast = 0; static uint32_t microsAtStart = micros(); static uint32_t microsForNext = microsAtStart + 100000; // 1/10 second if (now >= microsForNext) { microsForNext = now + 100000; // 1/10 second if (!bios.loop()) { g_biosInterrupt = false; } } } void runCPU(uint32_t now) { static uint32_t nextResetMicros = 0; static uint32_t countSinceLast = 0; static uint32_t microsAtStart = micros(); static uint32_t microsForNext = microsAtStart + (countSinceLast * SPEEDCTL); // Allow the BIOS to reset our timing if (!cpuClockInitialized) { nextResetMicros = 0; countSinceLast = 0; microsAtStart = micros(); microsForNext = microsAtStart + (countSinceLast * SPEEDCTL); cpuClockInitialized = true; } if (now >= microsForNext) { countSinceLast += g_cpu->Run(24); // The CPU runs in bursts of cycles. This '24' is the max burst we perform. ((AppleVM *)g_vm)->cpuMaintenance(g_cpu->cycles); microsForNext = microsAtStart + (countSinceLast * SPEEDCTL); } if (now >= nextResetMicros) { nextResetMicros = now + 1000000; #ifdef DEBUG_TIMING float pct = (100.0 * (float)countSinceLast) / (float)g_speed; sprintf(debugBuf, "CPU running at %f%%", pct); println(debugBuf); #endif countSinceLast = 0; microsAtStart = now; microsForNext = microsAtStart + (countSinceLast * SPEEDCTL); } } void loop() { static uint32_t readingBattery = 0; // set to millis() + a settle time constant when we start reading static uint32_t nextReadBattery = micros() + BATTERYPERIOD; uint32_t now = micros(); if (readingBattery && now >= readingBattery) { // Take 10 readings over a second and average them currentBatterySum += analogRead(BATTERYLEVEL); readingBattery = now + 100000; // 100 ms if (++currentBatteryCount >= 10) { currentBatteryReading = currentBatterySum / currentBatteryCount; readingBattery = 0; digitalWrite(BATTERYSELECT, false); nextReadBattery = now + BATTERYPERIOD; // Set up the displayed battery level if (currentBatteryReading < BATTERYMIN) currentBatteryReading = BATTERYMIN; if (currentBatteryReading > BATTERYMAX) currentBatteryReading = BATTERYMAX; ((AppleUI *)g_ui)->drawBatteryStatus(map(currentBatteryReading, BATTERYMIN, BATTERYMAX, 0, 100)); } } else if (!readingBattery && now >= nextReadBattery) { // start reading the battery readingBattery = now + 1 * 1000000; // let it settle for 1 second currentBatterySum = 0; currentBatteryCount = 0; digitalWrite(BATTERYSELECT, true); } static bool wasBios = false; // so we can tell when it's done if (g_biosInterrupt) { runBIOS(now); wasBios = true; } else { if (wasBios) { // bios has just exited writePrefs(); // Also might have changed the paddles state TeensyPaddles *tmp = (TeensyPaddles *)g_paddles; tmp->setRev(g_invertPaddleX, g_invertPaddleY); // if we turned off debugMode, make sure to clear the debugMsg if (g_debugMode == D_NONE) { g_display->debugMsg(""); } // Drain the speaker queue (FIXME: a little hacky) g_speaker->maintainSpeaker(-1, -1); // Force the display to redraw g_display->redraw(); // Redraw the UI ((AppleDisplay*)(g_vm->vmdisplay))->modeChange(); // force a full re-draw and blit // Poll the keyboard before we start, so we can do selftest on startup g_keyboard->maintainKeyboard(); // Reset the CPU clock so it doesn't fast-forward cpuClockInitialized = false; // Reset the speaker so it picks up its new volume (FIXME kinda hacky) g_speaker->begin(); wasBios = false; } } if (!g_biosInterrupt) { runCPU(now); } runDisplay(now); runMaintenance(now); runDebouncer(); } void doDebugging(uint32_t lastFps) { switch (g_debugMode) { case D_SHOWFPS: sprintf(debugBuf, "%lu FPS", lastFps); g_display->debugMsg(debugBuf); break; case D_SHOWMEMFREE: sprintf(debugBuf, "%lu %lu", FreeIntRamEstimate(), FreeExtRamEstimate()); g_display->debugMsg(debugBuf); break; case D_SHOWPADDLES: sprintf(debugBuf, "%u %u", g_paddles->paddle0(), g_paddles->paddle1()); g_display->debugMsg(debugBuf); break; case D_SHOWPC: sprintf(debugBuf, "%X", g_cpu->pc); g_display->debugMsg(debugBuf); break; case D_SHOWCYCLES: sprintf(debugBuf, "%llX", g_cpu->cycles); g_display->debugMsg(debugBuf); break; case D_SHOWBATTERY: sprintf(debugBuf, "B: %d %d%% ", currentBatteryReading, map(currentBatteryReading, BATTERYMIN, BATTERYMAX, 0, 100)); g_display->debugMsg(debugBuf); break; case D_SHOWTIME: sprintf(debugBuf, "%.2d:%.2d:%.2d", hour(), minute(), second()); g_display->debugMsg(debugBuf); break; case D_SHOWDSK: { uint8_t sd = ((AppleVM *)g_vm)->disk6->selectedDrive(); sprintf(debugBuf, "s %d t %d", sd, ((AppleVM *)g_vm)->disk6->headPosition(sd)); g_display->debugMsg(debugBuf); } break; } } void readPrefs() { TeensyPrefs np; prefs_t p; if (np.readPrefs(&p)) { g_volume = p.volume; g_displayType = p.displayType; g_debugMode = p.debug; g_speed = (p.speed * (1023000/2)); // steps of half normal speed if (g_speed < (1023000/2)) g_speed = (1023000/2); if (p.disk1[0]) { ((AppleVM *)g_vm)->insertDisk(0, p.disk1); } if (p.disk2[0]) { ((AppleVM *)g_vm)->insertDisk(1, p.disk2); } if (p.hd1[0]) { ((AppleVM *)g_vm)->insertHD(0, p.hd1); } if (p.hd2[0]) { ((AppleVM *)g_vm)->insertHD(1, p.hd2); } g_luminanceCutoff = p.luminanceCutoff; g_invertPaddleX = p.invertPaddleX; g_invertPaddleY = p.invertPaddleY; } else { // Set some defaults! g_volume = 7; g_displayType = 3; // FIXME constant g_debugMode = D_NONE; g_speed = 1023000; g_luminanceCutoff = 127; g_invertPaddleX = g_invertPaddleY = false; } // Update the paddles with the new inversion state ((TeensyPaddles *)g_paddles)->setRev(g_invertPaddleX, g_invertPaddleY); } void writePrefs() { TeensyPrefs np; prefs_t p; p.magic = PREFSMAGIC; p.magicFooter = PREFSMAGIC; p.prefsSize = sizeof(prefs_t); p.version = PREFSVERSION; p.invertPaddleX = g_invertPaddleX; p.invertPaddleY = g_invertPaddleY; p.volume = g_volume; p.displayType = g_displayType; p.luminanceCutoff = g_luminanceCutoff; p.debug = g_debugMode; p.speed = g_speed / (1023000/2); strcpy(p.disk1, ((AppleVM *)g_vm)->DiskName(0)); strcpy(p.disk2, ((AppleVM *)g_vm)->DiskName(1)); strcpy(p.hd1, ((AppleVM *)g_vm)->HDName(0)); strcpy(p.hd2, ((AppleVM *)g_vm)->HDName(1)); bool ret = np.writePrefs(&p); }