aiie/teensy/teensy.ino

534 lines
15 KiB
Arduino
Raw Normal View History

2017-02-20 18:55:16 +00:00
#include <Arduino.h>
#include <SPI.h>
#include <TimeLib.h>
2020-07-08 20:37:26 +00:00
#include <TeensyThreads.h>
#include <Bounce2.h>
2017-02-20 18:55:16 +00:00
#include "bios.h"
#include "cpu.h"
#include "applevm.h"
#include "teensy-display.h"
#include "teensy-keyboard.h"
#include "teensy-speaker.h"
#include "teensy-paddles.h"
#include "teensy-filemanager.h"
2020-07-07 10:25:04 +00:00
#include "teensy-usb.h"
2018-01-07 19:43:17 +00:00
#include "appleui.h"
#include "teensy-prefs.h"
2020-07-04 12:03:58 +00:00
#include "teensy-println.h"
2020-07-12 04:40:11 +00:00
//#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
2020-07-09 18:02:18 +00:00
#define RESETPIN 38
2020-07-11 23:14:30 +00:00
#define DEBUGPIN 23
2017-02-20 18:55:16 +00:00
#include "globals.h"
#include "teensy-crash.h"
BIOS bios;
// How many microseconds per cycle
#define SPEEDCTL ((float)1000000/(float)g_speed)
2017-02-20 18:55:16 +00:00
static time_t getTeensy3Time() { return Teensy3Clock.get(); }
2020-07-07 10:25:04 +00:00
TeensyUSB usb;
2020-07-08 20:37:26 +00:00
int cpuThreadId;
int displayThreadId;
int maintenanceThreadId;
2020-07-10 00:31:51 +00:00
int speakerThreadId;
2020-07-08 20:37:26 +00:00
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
2020-07-11 23:14:30 +00:00
Threads::Mutex speakerlock;
2020-07-08 20:37:26 +00:00
volatile bool g_writePrefsFromMainLoop = false;
2020-07-07 10:25:04 +00:00
void onKeypress(int unicode)
{
/*
shift/control/command are automatically applied
caps lock is oemkey 57
set the keyboard LED w/ ::capsLock(bool)
modifiers are <<8 bits for the right side:
command: 0x08; option/alt: 0x04; shift: 0x02; control: 0x01
F1..F12 are 194..205
Arrows: l/r/u/d 216/215/218/217
Delete: 127 (control-delete is 31)
home/pgup/down/delete/end: 210,211,214,212,213
numlock: oem 83
keypad: 210..218 as arrows &c, or digit ascii values w/ numlock on
enter: 10
*/
2020-07-07 10:25:04 +00:00
// vmkeyboard->keyDepressed(keypad.key[i].kchar);
}
void onKeyrelease(int unicode)
{
// vmkeyboard->keyReleased(keypad.key[i].kchar);
}
2017-02-20 18:55:16 +00:00
void setup()
{
Serial.begin(230400);
2020-07-08 21:53:51 +00:00
#if 0
2020-07-06 20:46:14 +00:00
// Wait for USB serial connection before booting while debugging
2020-06-28 13:21:27 +00:00
while (!Serial) {
yield();
2020-07-06 20:46:14 +00:00
}
#endif
delay(120); // let the power settle
2020-07-11 23:14:30 +00:00
pinMode(DEBUGPIN, OUTPUT); // for debugging
2020-07-06 20:46:14 +00:00
// enableFaultHandler();
SCB_SHCSR |= SCB_SHCSR_BUSFAULTENA | SCB_SHCSR_USGFAULTENA | SCB_SHCSR_MEMFAULTENA;
2017-02-20 18:55:16 +00:00
// 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) {
2020-07-04 12:03:58 +00:00
println("RTC set from Teensy");
2017-02-20 18:55:16 +00:00
} else {
2020-07-04 12:03:58 +00:00
println("Error while setting RTC");
2017-02-20 18:55:16 +00:00
}
pinMode(RESETPIN, INPUT);
digitalWrite(RESETPIN, HIGH);
2020-07-09 18:02:18 +00:00
analogReadRes(8); // We only need 8 bits of resolution (0-255) for paddles
2017-02-20 18:55:16 +00:00
analogReadAveraging(4); // ?? dunno if we need this or not.
2020-07-04 12:03:58 +00:00
println("creating virtual hardware");
2020-07-10 00:31:51 +00:00
g_speaker = new TeensySpeaker(18, 19); // FIXME abstract constants
2017-02-20 18:55:16 +00:00
2020-07-04 12:03:58 +00:00
println(" fm");
2017-02-20 18:55:16 +00:00
// 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.
2020-07-04 12:03:58 +00:00
println(" display");
2017-02-20 18:55:16 +00:00
g_display = new TeensyDisplay();
2020-07-04 12:03:58 +00:00
println(" UI");
2018-01-07 19:43:17 +00:00
g_ui = new AppleUI();
2017-02-20 18:55:16 +00:00
// Next create the virtual CPU. This needs the VM's MMU in order to
// run, but we don't have that yet.
2020-07-04 12:03:58 +00:00
println(" cpu");
2017-02-20 18:55:16 +00:00
g_cpu = new Cpu();
2020-07-07 10:25:04 +00:00
println(" usb");
usb.init();
usb.attachKeypress(onKeypress);
usb.attachKeyrelease(onKeyrelease);
2017-02-20 18:55:16 +00:00
// 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).
2020-07-04 12:03:58 +00:00
println(" vm");
2020-07-08 21:44:25 +00:00
Serial.flush();
2017-02-20 18:55:16 +00:00
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.
2020-07-08 21:44:25 +00:00
println(" [setMMU]");
2017-02-20 18:55:16 +00:00
g_cpu->SetMMU(g_vm->getMMU());
// And the physical keyboard needs hooks in to the virtual keyboard...
2020-07-04 12:03:58 +00:00
println(" keyboard");
2017-02-20 18:55:16 +00:00
g_keyboard = new TeensyKeyboard(g_vm->getKeyboard());
2020-07-04 12:03:58 +00:00
println(" paddles");
2020-07-09 18:02:18 +00:00
g_paddles = new TeensyPaddles(A3, A2, g_invertPaddleX, g_invertPaddleY);
2017-02-20 18:55:16 +00:00
// Now that all the virtual hardware is glued together, reset the VM
2020-07-04 12:03:58 +00:00
println("Resetting VM");
2017-02-20 18:55:16 +00:00
g_vm->Reset();
2020-07-04 12:03:58 +00:00
println("Reading prefs");
2017-02-20 18:55:16 +00:00
readPrefs(); // read from eeprom and set anything we need setting
// Debugging: insert a disk on startup...
2020-07-06 20:46:14 +00:00
//((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);
2017-02-20 18:55:16 +00:00
2020-07-08 20:37:26 +00:00
resetButtonDebouncer.attach(RESETPIN);
resetButtonDebouncer.interval(5); // ms
2020-07-04 12:03:58 +00:00
println("free-running");
2020-07-08 21:44:25 +00:00
Serial.flush();
2018-01-07 19:43:17 +00:00
2020-07-08 20:37:26 +00:00
threads.setMicroTimer(); // use a 100uS timer instead of a 1mS timer
2020-07-12 04:40:11 +00:00
// threads.setSliceMicros(5);
2020-07-08 20:37:26 +00:00
cpuThreadId = threads.addThread(runCPU);
displayThreadId = threads.addThread(runDisplay);
maintenanceThreadId = threads.addThread(runMaintenance);
2020-07-10 00:31:51 +00:00
speakerThreadId = threads.addThread(runSpeaker);
2020-07-08 20:37:26 +00:00
// Set the relative priorities of the threads by defining how long a "slice"
// is for each (in 100uS "ticks")
2020-07-11 23:14:30 +00:00
// At a ratio of 50:10:1, we get about 30FPS and 100% CPU speed using 100uS ticks.
// After adding an I2C DAC (what a terrible idea!) - 40:10:1:10 gets us about 70%
// CPU during disk activity, and 22 FPS, with a speaker that, well, makes a good deal
// of noise. It's not ideal, but it proves that it's possible; using a real SPI
// DAC here would probably work.
threads.setTimeSlice(displayThreadId, 40);
2020-07-12 04:40:11 +00:00
threads.setTimeSlice(cpuThreadId, 20);
2020-07-08 20:37:26 +00:00
threads.setTimeSlice(maintenanceThreadId, 1);
2020-07-12 04:40:11 +00:00
threads.setTimeSlice(speakerThreadId, 20); // guessing at a good value
2017-02-20 18:55:16 +00:00
}
// FIXME: move these memory-related functions elsewhere...
// This only gives you an estimated free mem size. It's not perfect.
uint32_t FreeRamEstimate()
{
uint32_t stackTop;
uint32_t heapTop;
// current position of the stack.
stackTop = (uint32_t) &stackTop;
// current position of heap.
void* hTop = malloc(1);
heapTop = (uint32_t) hTop;
free(hTop);
// The difference is the free, available ram.
return stackTop - heapTop;
}
#include "malloc.h"
int heapSize(){
return mallinfo().uordblks;
}
void biosInterrupt()
{
2020-07-08 20:37:26 +00:00
// Make sure the CPU and display don't run while we're in interrupt.
Threads::Scope lock1(cpulock);
Threads::Scope lock2(displaylock);
2020-07-11 23:14:30 +00:00
Threads::Scope lock3(speakerlock);
2017-02-20 18:55:16 +00:00
// wait for the interrupt button to be released
2020-07-08 20:37:26 +00:00
while (!resetButtonDebouncer.read())
2017-02-20 18:55:16 +00:00
;
// invoke the BIOS
if (bios.runUntilDone()) {
// if it returned true, we have something to store persistently in EEPROM.
2020-07-08 20:37:26 +00:00
// The EEPROM doesn't like to be written to from a thread?
g_writePrefsFromMainLoop = true;
while (g_writePrefsFromMainLoop) {
delay(100);
// wait for write to complete
}
// Also might have changed the paddles state
TeensyPaddles *tmp = (TeensyPaddles *)g_paddles;
tmp->setRev(g_invertPaddleX, g_invertPaddleY);
2017-02-20 18:55:16 +00:00
}
// if we turned off debugMode, make sure to clear the debugMsg
if (g_debugMode == D_NONE) {
2017-02-20 18:55:16 +00:00
g_display->debugMsg("");
}
// clear the CPU next-step counters
2020-07-08 20:37:26 +00:00
#if 0
// FIXME: this is to prevent the CPU from racing to catch up, and we need sth in the threads world
2017-02-20 18:55:16 +00:00
g_cpu->cycles = 0;
nextInstructionMicros = micros();
startMicros = micros();
2020-07-08 20:37:26 +00:00
#endif
// Drain the speaker queue (FIXME: a little hacky)
g_speaker->maintainSpeaker(-1, -1);
2017-02-20 18:55:16 +00:00
// Force the display to redraw
g_display->redraw(); // Redraw the UI
((AppleDisplay*)(g_vm->vmdisplay))->modeChange(); // force a full re-draw and blit
2017-02-20 18:55:16 +00:00
// Poll the keyboard before we start, so we can do selftest on startup
g_keyboard->maintainKeyboard();
}
2020-07-10 00:31:51 +00:00
void runSpeaker()
{
2020-07-12 04:40:11 +00:00
uint32_t nextResetMillis = 0;
uint32_t refreshCount = 0;
uint32_t microsAtStart = 0;
uint32_t microsForNext = micros() + 1000000/SAMPLERATE; // (1000000 us/second) / (frames/second) = us/frame
2020-07-10 00:31:51 +00:00
while (1) {
2020-07-12 04:40:11 +00:00
if (micros() >= microsForNext) {
refreshCount++;
microsForNext = microsAtStart + ((1000000*refreshCount)/SAMPLERATE);
{
Threads::Scope lock(speakerlock);
2020-07-11 23:14:30 +00:00
2020-07-12 04:40:11 +00:00
((TeensySpeaker *)g_speaker)->maintainSpeaker();
}
2020-07-11 23:14:30 +00:00
} else {
2020-07-12 04:40:11 +00:00
while (micros() < microsForNext)
threads.yield();
2020-07-10 00:31:51 +00:00
}
2020-07-12 04:40:11 +00:00
if (millis() >= nextResetMillis) {
nextResetMillis = millis() + 1000;
#ifdef DEBUG_TIMING
static char buf[25];
float pct = (100.0 * (float)refreshCount) / (float)SAMPLERATE;
sprintf(buf, "Speaker running at %f%%", pct);
println(buf);
#endif
refreshCount = 0;
microsAtStart = micros();
microsForNext = microsAtStart + ((1000000*refreshCount)/SAMPLERATE);
}
2020-07-10 00:31:51 +00:00
}
}
2017-02-20 18:55:16 +00:00
2020-07-08 20:37:26 +00:00
void runMaintenance()
2017-02-20 18:55:16 +00:00
{
2020-07-08 20:37:26 +00:00
uint32_t nextRuntime = 0;
while (1) {
2020-07-12 04:40:11 +00:00
if (millis() >= nextRuntime) {
2020-07-08 20:37:26 +00:00
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
threads.wait(biosThreadId);
biosThreadId = -1;
}
g_keyboard->maintainKeyboard();
usb.maintain();
} else {
2020-07-12 04:40:11 +00:00
while (millis() < nextRuntime)
threads.yield();
2020-07-08 20:37:26 +00:00
}
}
2020-07-08 20:37:26 +00:00
}
2020-07-12 04:40:11 +00:00
#define TARGET_FPS 30
2020-07-08 20:37:26 +00:00
void runDisplay()
{
2020-07-08 21:44:25 +00:00
g_display->redraw(); // Redraw the UI; don't blit to the physical device
2020-07-12 04:40:11 +00:00
// When do we want to reset our expectation of "normal"?
uint32_t nextResetMillis = 0;
// how many full display refreshes have we managed in this second?
uint32_t refreshCount = 0;
// how many micros until the next frame refresh?
uint32_t microsAtStart = 0;
uint32_t microsForNext = micros() + 1000000/TARGET_FPS; // (1000000 us/second) / (frames/second) = us/frame
uint32_t lastFps = 0;
2020-07-08 21:44:25 +00:00
2020-07-08 20:37:26 +00:00
while (1) {
2020-07-12 04:40:11 +00:00
// If it's time to draw the next frame, then do so
if (micros() >= microsForNext) {
refreshCount++;
microsForNext = microsAtStart + ((1000000*refreshCount)/TARGET_FPS);
{
Threads::Scope lock(displaylock);
doDebugging(lastFps);
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();
2020-07-08 20:37:26 +00:00
}
2020-07-12 04:40:11 +00:00
} else {
// We're running faster than needed, so give other threads some time
while (micros() < microsForNext)
threads.yield();
}
// Once a second, start counting all over again
if (millis() >= nextResetMillis) {
lastFps = refreshCount;
#ifdef DEBUG_TIMING
println("Display running at ", lastFps, " FPS");
#endif
nextResetMillis = millis() + 1000;
refreshCount = 0;
microsAtStart = micros();
microsForNext = microsAtStart + ((1000000*refreshCount)/TARGET_FPS);
2020-07-08 20:37:26 +00:00
}
}
}
2020-07-08 20:37:26 +00:00
void runCPU()
{
2020-07-12 04:40:11 +00:00
uint32_t nextResetMillis = 0;
uint32_t countSinceLast = 0;
uint32_t microsAtStart = micros();
uint32_t microsForNext = microsAtStart + (countSinceLast * SPEEDCTL);
2020-07-08 20:37:26 +00:00
while (1) {
2020-07-12 04:40:11 +00:00
if (micros() >= microsForNext) {
2020-07-08 20:37:26 +00:00
cpulock.lock(); // Blocking; if the BIOS is running, we stall here
2020-07-12 04:40:11 +00:00
countSinceLast += g_cpu->Run(24); // The CPU runs in bursts of cycles. This '24' is the max burst we perform.
2020-07-08 20:37:26 +00:00
((AppleVM *)g_vm)->cpuMaintenance(g_cpu->cycles);
2020-07-12 04:40:11 +00:00
cpulock.unlock();
microsForNext = microsAtStart + (countSinceLast * SPEEDCTL);
2020-07-08 20:37:26 +00:00
} else {
2020-07-12 04:40:11 +00:00
while (micros() < microsForNext)
threads.yield();
}
if (millis() >= nextResetMillis) {
nextResetMillis = millis() + 1000;
#ifdef DEBUG_TIMING
static char buf[25];
float pct = (100.0 * (float)countSinceLast) / (float)g_speed;
sprintf(buf, "CPU running at %f%%", pct);
println(buf);
#endif
countSinceLast = 0;
microsAtStart = micros();
microsForNext = microsAtStart + (countSinceLast * SPEEDCTL);
2020-07-08 20:37:26 +00:00
}
2020-07-12 04:40:11 +00:00
2020-07-08 20:37:26 +00:00
}
}
void loop()
{
resetButtonDebouncer.update();
if (g_writePrefsFromMainLoop) {
writePrefs();
g_writePrefsFromMainLoop = false;
2017-02-20 18:55:16 +00:00
}
}
2020-07-12 04:40:11 +00:00
void doDebugging(uint32_t lastFps)
{
char buf[25];
switch (g_debugMode) {
case D_SHOWFPS:
2020-07-12 04:40:11 +00:00
sprintf(buf, "%lu FPS", lastFps);
g_display->debugMsg(buf);
break;
case D_SHOWMEMFREE:
sprintf(buf, "%lu %u", FreeRamEstimate(), heapSize());
g_display->debugMsg(buf);
break;
case D_SHOWPADDLES:
sprintf(buf, "%u %u", g_paddles->paddle0(), g_paddles->paddle1());
g_display->debugMsg(buf);
break;
case D_SHOWPC:
sprintf(buf, "%X", g_cpu->pc);
g_display->debugMsg(buf);
break;
case D_SHOWCYCLES:
sprintf(buf, "%lX", g_cpu->cycles);
g_display->debugMsg(buf);
break;
case D_SHOWTIME:
sprintf(buf, "%.2d:%.2d:%.2d", hour(), minute(), second());
g_display->debugMsg(buf);
break;
2020-07-04 12:03:58 +00:00
case D_SHOWDSK:
{
uint8_t sd = ((AppleVM *)g_vm)->disk6->selectedDrive();
sprintf(buf, "s %d t %d",
sd,
((AppleVM *)g_vm)->disk6->headPosition(sd));
g_display->debugMsg(buf);
}
break;
2017-02-20 18:55:16 +00:00
}
}
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);
2017-02-20 18:55:16 +00:00
}
if (p.disk2[0]) {
((AppleVM *)g_vm)->insertDisk(1, p.disk2);
2017-02-20 18:55:16 +00:00
}
if (p.hd1[0]) {
((AppleVM *)g_vm)->insertHD(0, p.hd1);
}
2017-02-20 18:55:16 +00:00
if (p.hd2[0]) {
((AppleVM *)g_vm)->insertHD(1, p.hd2);
}
}
g_invertPaddleX = p.invertPaddleX;
g_invertPaddleY = p.invertPaddleY;
// Update the paddles with the new inversion state
((TeensyPaddles *)g_paddles)->setRev(g_invertPaddleX, g_invertPaddleY);
2017-02-20 18:55:16 +00:00
}
void writePrefs()
{
TeensyPrefs np;
prefs_t p;
2017-02-20 18:55:16 +00:00
p.magic = PREFSMAGIC;
p.prefsSize = sizeof(prefs_t);
p.version = PREFSVERSION;
2017-02-20 18:55:16 +00:00
p.invertPaddleX = g_invertPaddleX;
p.invertPaddleY = g_invertPaddleY;
p.volume = g_volume;
p.displayType = g_displayType;
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);
2017-02-20 18:55:16 +00:00
}