aiie/teensy/teensy.ino
2022-02-02 07:17:05 -05:00

662 lines
20 KiB
C++

#include <Arduino.h>
#include <TimeLib.h>
#include <Bounce2.h>
#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);
}