FabGL
ESP32 Display Controller and Graphics Library
VGA/PCEmulator/PCEmulator.ino

A i8086 based IBM PC emulator (runs FreeDOS, MS-DOS, CPM86, Linux-ELK, Windows 3.0)

/*
Created by Fabrizio Di Vittorio (fdivitto2013@gmail.com) - <http://www.fabgl.com>
Copyright (c) 2019-2021 Fabrizio Di Vittorio.
All rights reserved.
* Please contact fdivitto2013@gmail.com if you need a commercial license.
* This library and related software is available under GPL v3.
FabGL is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FabGL is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FabGL. If not, see <http://www.gnu.org/licenses/>.
*/
/* Instructions:
- to run this application you need an ESP32 with PSRAM installed and an SD-CARD slot (ie TTGO VGA32 v1.4 or FabGL Development Board with WROVER)
- open this with Arduino and make sure PSRAM is DISABLED
- partition scheme must be: Huge App
- compile and upload the sketch
*/
#pragma message "This sketch requires Tools->Partition Scheme = Huge APP"
#include <memory>
#include "esp32-hal-psram.h"
extern "C" {
#include "esp_spiram.h"
}
#include "esp_sntp.h"
#include <Preferences.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include "fabgl.h"
#include "mconf.h"
#include "machine.h"
using std::unique_ptr;
using fabgl::StringList;
using fabgl::imin;
using fabgl::imax;
Preferences preferences;
InputBox ibox;
Machine * machine;
// noinit! Used to maintain datetime between reboots
__NOINIT_ATTR static timeval savedTimeValue;
static bool wifiConnected = false;
static bool downloadOK = false;
// try to connected using saved parameters
bool tryToConnect()
{
bool connected = WiFi.status() == WL_CONNECTED;
if (!connected) {
char SSID[32] = "";
char psw[32] = "";
if (preferences.getString("SSID", SSID, sizeof(SSID)) && preferences.getString("WiFiPsw", psw, sizeof(psw))) {
ibox.progressBox("", "Abort", true, 200, [&](fabgl::ProgressForm * form) {
WiFi.begin(SSID, psw);
for (int i = 0; i < 32 && WiFi.status() != WL_CONNECTED; ++i) {
if (!form->update(i * 100 / 32, "Connecting to %s...", SSID))
break;
delay(500);
if (i == 16)
WiFi.reconnect();
}
connected = (WiFi.status() == WL_CONNECTED);
});
// show to user the connection state
if (!connected) {
WiFi.disconnect();
ibox.message("", "WiFi Connection failed!");
}
}
}
return connected;
}
bool checkWiFi()
{
wifiConnected = tryToConnect();
if (!wifiConnected) {
// configure WiFi?
if (ibox.message("WiFi Configuration", "Configure WiFi?", "No", "Yes") == InputResult::Enter) {
// repeat until connected or until user cancels
do {
// yes, scan for networks showing a progress dialog box
int networksCount = 0;
ibox.progressBox("", nullptr, false, 200, [&](fabgl::ProgressForm * form) {
form->update(0, "Scanning WiFi networks...");
networksCount = WiFi.scanNetworks();
});
// are there available WiFi?
if (networksCount > 0) {
// yes, show a selectable list
StringList list;
for (int i = 0; i < networksCount; ++i)
list.appendFmt("%s (%d dBm)", WiFi.SSID(i).c_str(), WiFi.RSSI(i));
int s = ibox.menu("WiFi Configuration", "Please select a WiFi network", &list);
// user selected something?
if (s > -1) {
// yes, ask for WiFi password
char psw[32] = "";
if (ibox.textInput("WiFi Configuration", "Insert WiFi password", psw, 31, "Cancel", "OK", true) == InputResult::Enter) {
// user pressed OK, connect to WiFi...
preferences.putString("SSID", WiFi.SSID(s).c_str());
preferences.putString("WiFiPsw", psw);
wifiConnected = tryToConnect();
// show to user the connection state
if (wifiConnected)
ibox.message("", "Connection succeeded!");
} else
break;
} else
break;
} else {
// there is no WiFi
ibox.message("", "No WiFi network found!");
break;
}
WiFi.scanDelete();
} while (!wifiConnected);
}
}
return wifiConnected;
}
// handle soft restart
void shutdownHandler()
{
// save current datetime into Preferences
gettimeofday(&savedTimeValue, nullptr);
}
void updateDateTime()
{
// Set timezone
setenv("TZ", "CET-1CEST,M3.5.0,M10.5.0/3", 1);
tzset();
// get datetime from savedTimeValue? (noinit section)
if (esp_reset_reason() == ESP_RST_SW) {
// adjust time taking account elapsed time since ESP32 started
savedTimeValue.tv_usec += (int) esp_timer_get_time();
savedTimeValue.tv_sec += savedTimeValue.tv_usec / 1000000;
savedTimeValue.tv_usec %= 1000000;
settimeofday(&savedTimeValue, nullptr);
return;
}
if (checkWiFi()) {
// we need time right now
ibox.progressBox("", nullptr, true, 200, [&](fabgl::ProgressForm * form) {
sntp_setoperatingmode(SNTP_OPMODE_POLL);
sntp_setservername(0, (char*)"pool.ntp.org");
sntp_init();
for (int i = 0; i < 12 && sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED; ++i) {
form->update(i * 100 / 12, "Getting date-time from SNTP...");
delay(500);
}
sntp_stop();
ibox.setAutoOK(2);
ibox.message("", "Date and Time updated. Restarting...");
esp_restart();
});
} else {
// set default time
auto tm = (struct tm){ .tm_sec = 0, .tm_min = 0, .tm_hour = 8, .tm_mday = 14, .tm_mon = 7, .tm_year = 84 };
auto now = (timeval){ .tv_sec = mktime(&tm) };
settimeofday(&now, nullptr);
}
}
// download specified filename from URL
bool downloadURL(char const * URL, FILE * file)
{
downloadOK = false;
char const * filename = strrchr(URL, '/') + 1;
ibox.progressBox("", "Abort", true, 380, [&](fabgl::ProgressForm * form) {
form->update(0, "Preparing to download %s", filename);
HTTPClient http;
http.begin(URL);
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
if (file) {
int tlen = http.getSize();
int len = tlen;
auto buf = (uint8_t*) SOC_EXTRAM_DATA_LOW; // use PSRAM as buffer
WiFiClient * stream = http.getStreamPtr();
int dsize = 0;
while (http.connected() && (len > 0 || len == -1)) {
size_t size = stream->available();
if (size) {
int c = stream->readBytes(buf, size);
auto wr = fwrite(buf, 1, c, file);
if (wr != c) {
dsize = 0;
break; // writing failure!
}
dsize += c;
if (len > 0)
len -= c;
if (!form->update((int64_t)dsize * 100 / tlen, "Downloading %s (%.2f / %.2f MB)", filename, (double)dsize / 1048576.0, tlen / 1048576.0))
break;
}
}
downloadOK = (len == 0 || (len == -1 && dsize > 0));
}
}
http.end();
});
return downloadOK;
}
// return filename if successfully downloaded or already exist
char const * getDisk(char const * url)
{
FileBrowser fb(SD_MOUNT_PATH);
char const * filename = nullptr;
if (url) {
if (strncmp("://", url + 4, 3) == 0) {
// this is actually an URL
filename = strrchr(url, '/') + 1;
if (filename && !fb.exists(filename, false)) {
// disk doesn't exist, try to download
if (!checkWiFi())
return nullptr;
auto file = fb.openFile(filename, "wb");
bool success = downloadURL(url, file);
fclose(file);
if (!success) {
fb.remove(filename);
return nullptr;
}
}
} else {
// this is just a file
if (fb.filePathExists(url))
filename = url;
}
}
return filename;
}
// user pressed SYSREQ (ALT + PRINTSCREEN)
void sysReqCallback()
{
machine->graphicsAdapter()->enableVideo(false);
ibox.begin(VGA_640x480_60Hz, 500, 400, 4);
int s = ibox.menu("", "Select a command", "Restart (Boot Menu);Continue;Mount Disk");
switch (s) {
// Restart
case 0:
esp_restart();
break;
// Mount Disk
case 2:
{
int s = ibox.menu("", "Select Drive", "Floppy A (fd0);Floppy B (fd1)");
if (s > -1) {
constexpr int MAXNAMELEN = 256;
unique_ptr<char[]> dir(new char[MAXNAMELEN + 1] { '/', 'S', 'D', 0 } );
unique_ptr<char[]> filename(new char[MAXNAMELEN + 1] { 0 } );
if (machine->diskFilename(s))
strcpy(filename.get(), machine->diskFilename(s));
if (ibox.fileSelector("Select Disk Image", "Image Filename", dir.get(), MAXNAMELEN, filename.get(), MAXNAMELEN) == InputResult::Enter) {
machine->setDriveImage(s, filename.get());
}
}
break;
}
// Continue
default:
break;
}
ibox.end();
PS2Controller::keyboard()->enableVirtualKeys(false, false); // don't use virtual keys
machine->graphicsAdapter()->enableVideo(true);
}
void setup()
{
Serial.begin(115200); delay(500); printf("\n\n\nReset\n\n");// DEBUG ONLY
disableCore0WDT();
delay(100); // experienced crashes without this delay!
disableCore1WDT();
preferences.begin("PCEmulator", false);
// uncomment to clear preferences
//preferences.clear();
ibox.begin(VGA_640x480_60Hz, 500, 400, 4);
ibox.setBackgroundColor(RGB888(0, 0, 0));
ibox.onPaint = [&](Canvas * canvas) { drawInfo(canvas); };
// we need PSRAM for this app, but we will handle it manually, so please DO NOT enable PSRAM on your development env
#ifdef BOARD_HAS_PSRAM
ibox.message("Warning!", "Please disable PSRAM to improve performance!");
#endif
// note: we use just 2MB of PSRAM so the infamous PSRAM bug should not happen. But to avoid gcc compiler hack (-mfix-esp32-psram-cache-issue)
// we enable PSRAM at runtime, otherwise the hack slows down CPU too much (PSRAM_HACK is no more required).
if (esp_spiram_init() != ESP_OK)
ibox.message("Error!", "This app requires a board with PSRAM!", nullptr, nullptr);
#ifndef BOARD_HAS_PSRAM
esp_spiram_init_cache();
#endif
if (!FileBrowser::mountSDCard(false, SD_MOUNT_PATH, 8)) // @TODO: reduce to 4?
ibox.message("Error!", "This app requires a SD-CARD!", nullptr, nullptr);
// uncomment to format SD!
//FileBrowser::format(fabgl::DriveType::SDCard, 0);
esp_register_shutdown_handler(shutdownHandler);
updateDateTime();
// machine configurations
MachineConf mconf;
// show a list of machine configurations
ibox.setAutoOK(6);
int idx = preferences.getInt("dconf", 0);
for (bool showDialog = true; showDialog; ) {
loadMachineConfiguration(&mconf);
StringList dconfs;
for (auto conf = mconf.getFirstItem(); conf; conf = conf->next)
dconfs.append(conf->desc);
dconfs.select(idx, true);
ibox.setupButton(0, "Browse Files");
ibox.setupButton(1, "Options", "Edit;New;Remove", 52);
auto r = ibox.select("Machine Configurations", "Please select a machine configuration", &dconfs, nullptr, "Run");
idx = dconfs.getFirstSelected();
switch (r) {
case InputResult::ButtonExt0:
// Browse Files
ibox.folderBrowser("Browse Files", SD_MOUNT_PATH);
break;
case InputResult::ButtonExt1:
// Options
switch (ibox.selectedSubItem()) {
// Edit
case 0:
editConfigDialog(&ibox, &mconf, idx);
break;
// New
case 1:
newConfigDialog(&ibox, &mconf, idx);
break;
// Remove
case 2:
delConfigDialog(&ibox, &mconf, idx);
break;
};
break;
case InputResult::Enter:
// Run
showDialog = false;
break;
default:
break;
}
// next selection will not have timeout
ibox.setAutoOK(0);
}
idx = imax(idx, 0);
preferences.putInt("dconf", idx);
// setup selected configuration
auto conf = mconf.getItem(idx);
char const * diskFilename[DISKCOUNT];
downloadOK = true;
for (int i = 0; i < DISKCOUNT && downloadOK; ++i)
diskFilename[i] = getDisk(conf->disk[i]);
if (!downloadOK || (!diskFilename[0] && !diskFilename[2])) {
// unable to get boot disks
ibox.message("Error!", "Unable to get system disks!");
esp_restart();
}
if (wifiConnected) {
// disk downloaded from the Internet, need to reboot to fully disable wifi
ibox.setAutoOK(2);
ibox.message("", "Disks downloaded. Restarting...");
esp_restart();
}
ibox.end();
machine = new Machine;
machine->setBaseDirectory(SD_MOUNT_PATH);
for (int i = 0; i < DISKCOUNT; ++i)
machine->setDriveImage(i, diskFilename[i], conf->cylinders[i], conf->heads[i], conf->sectors[i]);
machine->setBootDrive(conf->bootDrive);
/*
printf("MALLOC_CAP_32BIT : %d bytes (largest %d bytes)\r\n", heap_caps_get_free_size(MALLOC_CAP_32BIT), heap_caps_get_largest_free_block(MALLOC_CAP_32BIT));
printf("MALLOC_CAP_8BIT : %d bytes (largest %d bytes)\r\n", heap_caps_get_free_size(MALLOC_CAP_8BIT), heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));
printf("MALLOC_CAP_DMA : %d bytes (largest %d bytes)\r\n\n", heap_caps_get_free_size(MALLOC_CAP_DMA), heap_caps_get_largest_free_block(MALLOC_CAP_DMA));
heap_caps_dump_all();
*/
machine->setSysReqCallback(sysReqCallback);
machine->run();
}
#if FABGLIB_VGAXCONTROLLER_PERFORMANCE_CHECK
namespace fabgl {
extern volatile uint64_t s_vgapalctrlcycles;
}
using fabgl::s_vgapalctrlcycles;
#endif
void loop()
{
#if FABGLIB_VGAXCONTROLLER_PERFORMANCE_CHECK
static uint32_t tcpu = 0, s1 = 0, count = 0;
tcpu = machine->ticksCounter();
s_vgapalctrlcycles = 0;
s1 = fabgl::getCycleCount();
delay(1000);
printf("%d\tCPU: %d", count, machine->ticksCounter() - tcpu);
printf(" Graph: %lld / %d (%d%%)\n", s_vgapalctrlcycles, fabgl::getCycleCount() - s1, (int)((double)s_vgapalctrlcycles/240000000*100));
++count;
#else
vTaskDelete(NULL);
#endif
}