diff --git a/.gitignore b/.gitignore index c795b05..3971f66 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -build \ No newline at end of file +build +tools/sdcard.img +tools/OTTERNAV.EXE diff --git a/README.md b/README.md index 13cc58c..c31cc21 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # DOSBox-VirtIsa -This fork focuses on building an emulation environment for a custom ISA card. +This fork focuses on building an emulation environment for a +[custom ISA card](https://github.com/ifilot/slot-otter). ## Purpose of this fork diff --git a/docs/dosbox.1 b/docs/dosbox.1 index 88f65ee..5edc697 100644 --- a/docs/dosbox.1 +++ b/docs/dosbox.1 @@ -65,7 +65,7 @@ at the end of .RB "inside " dosbox ) .TP .B \-userconf -Load the configuration file located in ~/.dosbox. Can be combined with +Load the configuration file located in ~/.dosbox-virtisa. Can be combined with .RB "the " \-conf " option." .TP .BI \-scaler " scaler" @@ -286,7 +286,7 @@ If no configfile is specified at the commandline, a file named loaded automatically. If a configfile is specified at the commandline that one will be used instead. If no configfile is specified or found in the current directory -.RB " then dosbox will load one from " ~/.dosbox/ ". It will try to create " +.RB " then dosbox will load one from " ~/.dosbox-virtisa/ ". It will try to create " one if there is none. .SH "SPECIAL KEYS" .TP 12m diff --git a/src/dosbox.cpp b/src/dosbox.cpp index f82345d..116aed1 100644 --- a/src/dosbox.cpp +++ b/src/dosbox.cpp @@ -419,6 +419,9 @@ void DOSBOX_Init(void) { Pstring = secprop->Add_path("captures",Property::Changeable::Always,"capture"); Pstring->Set_help("Directory where things like wave, midi, screenshot get captured."); + Pstring = secprop->Add_path("isa_sd_image", Property::Changeable::OnlyAtStart, ""); + Pstring->Set_help("Path to SD-card image file used by the custom ISA interface on port 330h."); + #if C_DEBUG LOG_StartUp(); #endif diff --git a/src/isa/isa.cpp b/src/isa/isa.cpp index eeb0ea9..5c33b36 100644 --- a/src/isa/isa.cpp +++ b/src/isa/isa.cpp @@ -3,67 +3,461 @@ * * This file is intentionally separated from core DOSBox files to make * ISA card emulation behavior easy to tune for this fork. + * + * This is a hardware level implementation of SlotOtter: + * https://github.com/ifilot/slot-otter */ +#include +#include +#include +#include + #include "dosbox.h" #include "inout.h" #include "setup.h" #include "isa/isa.h" -static const Bitu kIsaPort = 0x330; -static const Bitu kIsaShift = 1; +static const Bitu kIsaPortBase = 0x330; +static const Bitu kIsaPortCount = 4; // lower two bits (A0 and A1) +static const bool kIsaDebugTrace = true; class ISA330Port : public Module_base { +private: + static ISA330Port *instance; // Singleton pointer used by static I/O callbacks + + std::ifstream sd_image; + bool has_image; + bool app_cmd_pending; + bool sdcs; + bool sdout; + Bit8u tx_shift_reg; + Bit8u rx_shift_reg; + std::deque mosi_queue; + std::deque miso_queue; + public: - ISA330Port(Section *configuration) : Module_base(configuration) { - /* - * VirtIsa custom insertion: - * Register read/write handlers for I/O port 0x330 so this fork can - * emulate a minimal ISA card command/data interaction point. - */ - IO_RegisterReadHandler(kIsaPort, ReadPort, IO_MB); - IO_RegisterWriteHandler(kIsaPort, WritePort, IO_MB); - LOG_MSG("[DEBUG]: Registered custom ISA port handler at 0x%03X", (unsigned int)kIsaPort); + /** + * Constructor + * + * Initializes the ISA device, optionally opens an SD-card image, + * and registers I/O port handlers with DOSBox. + * + * @param configuration DOSBox configuration section + * @param image_path Path to SD card image file (may be null/empty) + */ + ISA330Port(Section *configuration, const char *image_path) + : Module_base(configuration), + has_image(false), + app_cmd_pending(false), + sdcs(false), + sdout(true), + tx_shift_reg(0xFF), + rx_shift_reg(0xFF) { + + if (image_path && image_path[0]) { + sd_image.open(image_path, std::ios::in | std::ios::binary); + has_image = sd_image.good(); + if (has_image) { + LOG_MSG("[ISA] : SD image loaded from '%s'", image_path); + } else { + LOG_MSG("[ISA] : Failed to open SD image '%s' (SD emulation disabled)", image_path); + } + } else { + LOG_MSG("[ISA] : No SD image configured (set isa_sd_image=...)"); + } + + // Register I/O handlers for the configured port range + for (Bitu port = kIsaPortBase; port < (kIsaPortBase + kIsaPortCount); ++port) { + IO_RegisterReadHandler(port, ReadPort, IO_MB); + IO_RegisterWriteHandler(port, WritePort, IO_MB); + } + + // Store singleton instance for static callbacks + instance = this; + + LOG_MSG("[ISA] : Registered ISA SD-card handler range 0x%03X-0x%03X", + (unsigned int)kIsaPortBase, + (unsigned int)(kIsaPortBase + kIsaPortCount - 1)); } + /** + * Destructor + * + * Unregisters I/O handlers and clears singleton instance. + */ ~ISA330Port() { - IO_FreeReadHandler(kIsaPort, IO_MB); - IO_FreeWriteHandler(kIsaPort, IO_MB); + for (Bitu port = kIsaPortBase; port < (kIsaPortBase + kIsaPortCount); ++port) { + IO_FreeReadHandler(port, IO_MB); + IO_FreeWriteHandler(port, IO_MB); + } + if (instance == this) { + instance = 0; + } } - static Bitu ReadPort(Bitu /*port*/, Bitu /*iolen*/) { - LOG_MSG("[DEBUG]: read from 0x%03X -> 0x%02X", - (unsigned int)kIsaPort, (unsigned int)shifted_value); - return shifted_value; + /** + * Static I/O read handler + * + * Called by DOSBox when the emulated CPU reads from one of the ISA ports. + * Dispatches to the active instance via the singleton pointer. + * + * @param port I/O port being read + * @param iolen Length of read (unused) + * @return Byte returned to emulated CPU + */ + static Bitu ReadPort(Bitu port, Bitu /*iolen*/) { + if (!instance) { + return 0xFF; + } + + const Bitu offset = port - kIsaPortBase; + Bit8u out = 0xFF; + + // if (kIsaDebugTrace) { + // LOG_MSG("[ISA] IN 0x%03X (0x%02X)", (unsigned int)port, (unsigned int)out); + // } + + switch (offset) { + case 0: + out = instance->rx_shift_reg; + return out; + case 1: + out = instance->rx_shift_reg; + instance->pulse_8clocks(); + return out; + case 2: + instance->set_sdcs(true); + return 0xFF; // actually undefined, though most likely pulled to high because of TTL characteristics + case 3: + instance->set_sdcs(false); + return 0xFF; // actually undefined, though most likely pulled to high because of TTL characteristics + default: + return 0xFF; + } } - static void WritePort(Bitu /*port*/, Bitu val, Bitu /*iolen*/) { + /** + * Static I/O write handler + * + * Called by DOSBox when the emulated CPU writes to one of the ISA ports. + * Updates transmit state and triggers SPI clocking. + * + * @param port I/O port being written + * @param val Value written + * @param iolen Length of write (unused) + */ + static void WritePort(Bitu port, Bitu val, Bitu /*iolen*/) { + if (!instance) + return; + const Bit8u input = (Bit8u)(val & 0xff); - shifted_value = (Bit8u)(input << kIsaShift); - LOG_MSG("[DEBUG]: write to 0x%03X value 0x%02X -> shifted/stored 0x%02X", - (unsigned int)kIsaPort, - (unsigned int)input, (unsigned int)shifted_value); + const Bitu offset = port - kIsaPortBase; + + switch (offset) { + case 0: + // if (kIsaDebugTrace) { + // LOG_MSG("[ISA] OUT 0x%03X (0x%02X)", (unsigned int)port, (unsigned int)input); + // } + instance->tx_shift_reg = input; + instance->pulse_8clocks(); + break; + case 1: + // if (kIsaDebugTrace) { + // LOG_MSG("[ISA] OUT 0x%03X (PULSE)", (unsigned int)port); + // } + instance->pulse_8clocks(); + break; + case 2: + instance->sdout = true; + break; + case 3: + instance->sdout = false; + break; + default: + break; + } } private: - static Bit8u shifted_value; + /** + * Set SD card chip-select state + * + * Updates SDCS line. !!Currently only logged and not used for logic!! + * + * @param state New chip-select state + */ + void set_sdcs(const bool state) { + if (sdcs == state) + return; + + sdcs = state; + } + + /** + * Simulate 8 SPI clock cycles + * + * Transfers one byte from TX shift register into MOSI queue, + * processes commands, and updates RX shift register from MISO queue. + */ + void pulse_8clocks() { + mosi_queue.push_back(tx_shift_reg); + tx_shift_reg = 0xFF; + + // this function scans for any command words and parses those + digest_sd(); + + if (!miso_queue.empty()) { + rx_shift_reg = miso_queue.front(); + miso_queue.pop_front(); + } else { + rx_shift_reg = 0xFF; + } + } + + /** + * Process accumulated MOSI bytes as SD commands + * + * Parses 6-byte command frames and generates appropriate responses + * into the MISO queue. + */ + void digest_sd() { + // digest sd command, ignores any leading "cleansing bytes" (0xFF) + if (mosi_queue.size() == 1 && mosi_queue.front() == 0xFF) { + mosi_queue.pop_front(); + return; + } + + while (mosi_queue.size() >= 6) { + Bit8u cmd[6]; + for (Bitu i = 0; i < 6; ++i) { + cmd[i] = mosi_queue.front(); + mosi_queue.pop_front(); + } + + // CMD00 + if (match_cmd(cmd, 0x40, 0x00, 0x00, 0x00, 0x00, 0x95)) { + app_cmd_pending = false; + load_response(make_response_cmd00()); + if (kIsaDebugTrace) { + LOG_MSG("[ISA] CMD00"); + } + continue; + } + + // CMD08 + if (match_cmd(cmd, 0x48, 0x00, 0x00, 0x01, 0xAA, 0x87)) { + load_response(make_response_cmd08()); + if (kIsaDebugTrace) { + LOG_MSG("[ISA] CMD08"); + } + continue; + } + + // CMD55 + if (match_cmd(cmd, 0x77, 0x00, 0x00, 0x00, 0x00, 0x65)) { + app_cmd_pending = true; + load_response(make_response_cmd55()); + if (kIsaDebugTrace) { + LOG_MSG("[ISA] CMD55"); + } + continue; + } + + // ACMD41 + if (app_cmd_pending && match_cmd(cmd, 0x69, 0x40, 0x00, 0x00, 0x00, 0x77)) { + app_cmd_pending = false; + load_response(make_response_acmd41()); + if (kIsaDebugTrace) { + LOG_MSG("[ISA] ACMD41"); + } + continue; + } + + // CMD58 + if (match_cmd(cmd, 0x7A, 0x00, 0x00, 0x00, 0x00, 0x01)) { + load_response(make_response_cmd58()); + if (kIsaDebugTrace) { + LOG_MSG("[ISA] CMD58"); + } + continue; + } + + // CMD17 + if (cmd[0] == (0x40 | 17) && cmd[5] == 0x01) { + const Bit32u addr = (Bit32u)cmd[1] << 24 | (Bit32u)cmd[2] << 16 | + (Bit32u)cmd[3] << 8 | (Bit32u)cmd[4]; + load_response_cmd17(addr); + if (kIsaDebugTrace) { + LOG_MSG("[ISA] CMD17: 0x%08lX", addr); + } + continue; + } + + load_response(make_response_illegal_command()); + } + } + + /** + * Compare a 6-byte command frame against expected values + */ + static bool match_cmd(const Bit8u cmd[6], + Bit8u b0, Bit8u b1, Bit8u b2, + Bit8u b3, Bit8u b4, Bit8u b5) { + return cmd[0] == b0 && cmd[1] == b1 && cmd[2] == b2 && + cmd[3] == b3 && cmd[4] == b4 && cmd[5] == b5; + } + + /** Generate response for CMD0 (reset) */ + static std::vector make_response_cmd00() { + const Bit8u resp[] = {0xFF, 0x01}; + return std::vector(resp, resp + sizeof(resp)); + } + + /** Generate response for CMD8 (interface condition) */ + static std::vector make_response_cmd08() { + const Bit8u resp[] = {0xFF, 0x01, 0x00, 0x00, 0x01, 0xAA}; + return std::vector(resp, resp + sizeof(resp)); + } + + /** Generate response for CMD55 (app command prefix) */ + static std::vector make_response_cmd55() { + const Bit8u resp[] = {0xFF, 0x01}; + return std::vector(resp, resp + sizeof(resp)); + } + + /** Generate response for ACMD41 (init) */ + static std::vector make_response_acmd41() { + const Bit8u resp[] = {0xFF, 0x00}; + return std::vector(resp, resp + sizeof(resp)); + } + + /** Generate response for CMD58 (read OCR) */ + static std::vector make_response_cmd58() { + const Bit8u resp[] = {0xFF, 0x00, 0xC0, 0xFF, 0x80, 0x00}; + return std::vector(resp, resp + sizeof(resp)); + } + + /** Generate response for illegal/unsupported command */ + static std::vector make_response_illegal_command() { + const Bit8u resp[] = {0xFF, 0x04}; + return std::vector(resp, resp + sizeof(resp)); + } + + /** + * Append response bytes to MISO queue + */ + void load_response(const std::vector &resp) { + for (size_t i = 0; i < resp.size(); ++i) { + miso_queue.push_back(resp[i]); + } + } + + /** + * Handle CMD17 (read single block) + * + * Reads a 512-byte sector from the SD image and pushes it + * into the MISO queue along with protocol framing and CRC. + * + * @param addr Block address (sector index) + */ + void load_response_cmd17(Bit32u addr) { + if (!has_image || !sd_image.is_open()) { + load_response(make_response_illegal_command()); + return; + } + + sd_image.clear(); + sd_image.seekg((std::streamoff)(addr * 512UL), std::ios::beg); + + if (!sd_image.good()) { + load_response(make_response_illegal_command()); + return; + } + + Bit8u buffer[512]; + sd_image.read((char *)buffer, sizeof(buffer)); + + if (sd_image.gcount() != (std::streamsize)sizeof(buffer)) { + load_response(make_response_illegal_command()); + return; + } + + // Response + data token + data + CRC + miso_queue.push_back(0xFF); + miso_queue.push_back(0x00); + miso_queue.push_back(0xFF); + miso_queue.push_back(0xFF); + miso_queue.push_back(0xFF); + miso_queue.push_back(0xFF); + miso_queue.push_back(0xFE); + + for (Bitu i = 0; i < 512; ++i) { + miso_queue.push_back(buffer[i]); + } + + const Bit16u checksum = crc16_xmodem(buffer, 512); + miso_queue.push_back((Bit8u)((checksum >> 8) & 0xFF)); + miso_queue.push_back((Bit8u)(checksum & 0xFF)); + } + + /** + * Compute CRC16 (XMODEM variant) + * + * Used for SD card data block integrity. + * + * @param data Input buffer + * @param len Length in bytes + * @return Computed CRC16 + */ + static Bit16u crc16_xmodem(const Bit8u *data, size_t len) { + Bit16u crc = 0x0000; + for (size_t i = 0; i < len; ++i) { + crc ^= (Bit16u)data[i] << 8; + for (int j = 0; j < 8; j++) { + if (crc & 0x8000) + crc = (Bit16u)((crc << 1) ^ 0x1021); + else + crc <<= 1; + } + } + return crc; + } }; -Bit8u ISA330Port::shifted_value = 0; +ISA330Port *ISA330Port::instance = 0; static ISA330Port *isa_port_handler; +static std::string isa_sd_image_path; +/** + * Cleanup function for ISA module + * + * Deletes the global ISA port handler instance. + */ static void ISA_Destroy(Section * /*sec*/) { delete isa_port_handler; isa_port_handler = 0; } +/** + * Initialize ISA module + * + * Creates (or recreates) the ISA device and registers it with DOSBox. + * Also preserves SD image configuration across reinitializations. + * + * @param sec DOSBox configuration section + */ void ISA_Init(Section *sec) { - /* - * VirtIsa custom insertion: - * Initialize ISA 0x330 emulation module during DOSBox startup. - */ - LOG_MSG("[DEBUG]: initializing ISA 0x330 handler (this can override MPU-401 at the same port)."); - isa_port_handler = new ISA330Port(sec); + Section_prop *section = static_cast(sec); + const char *configured_path = section->Get_string("isa_sd_image"); + + if (configured_path && configured_path[0]) + isa_sd_image_path = configured_path; + + if (isa_port_handler) + delete isa_port_handler; + + isa_port_handler = new ISA330Port(sec, isa_sd_image_path.c_str()); sec->AddDestroyFunction(&ISA_Destroy, true); -} +} \ No newline at end of file diff --git a/src/isa/isa.h b/src/isa/isa.h index 23f68d2..79dec95 100644 --- a/src/isa/isa.h +++ b/src/isa/isa.h @@ -1,3 +1,13 @@ +/* + * DOSBox-VirtIsa custom ISA card emulation hook. + * + * This file is intentionally separated from core DOSBox files to make + * ISA card emulation behavior easy to tune for this fork. + * + * This is a hardware level implementation of SlotOtter: + * https://github.com/ifilot/slot-otter + */ + #ifndef DOSBOX_VIRTISA_ISA_H #define DOSBOX_VIRTISA_ISA_H diff --git a/src/misc/cross.cpp b/src/misc/cross.cpp index 72be6c5..e57d145 100644 --- a/src/misc/cross.cpp +++ b/src/misc/cross.cpp @@ -59,12 +59,12 @@ static void W32_ConfDir(std::string& in,bool create) { void Cross::GetPlatformConfigDir(std::string& in) { #ifdef WIN32 W32_ConfDir(in,false); - in += "\\DOSBox"; + in += "\\DOSBox-VirtIsa"; #elif defined(MACOSX) in = "~/Library/Preferences"; ResolveHomedir(in); #else - in = "~/.dosbox"; + in = "~/.dosbox-virtisa"; ResolveHomedir(in); #endif in += CROSS_FILESPLIT; @@ -72,11 +72,11 @@ void Cross::GetPlatformConfigDir(std::string& in) { void Cross::GetPlatformConfigName(std::string& in) { #ifdef WIN32 -#define DEFAULT_CONFIG_FILE "dosbox-" VERSION ".conf" +#define DEFAULT_CONFIG_FILE "dosbox-virtisa-" VERSION ".conf" #elif defined(MACOSX) #define DEFAULT_CONFIG_FILE "DOSBox " VERSION " Preferences" #else /*linux freebsd*/ -#define DEFAULT_CONFIG_FILE "dosbox-" VERSION ".conf" +#define DEFAULT_CONFIG_FILE "dosbox-virtisa-" VERSION ".conf" #endif in = DEFAULT_CONFIG_FILE; } @@ -84,14 +84,14 @@ void Cross::GetPlatformConfigName(std::string& in) { void Cross::CreatePlatformConfigDir(std::string& in) { #ifdef WIN32 W32_ConfDir(in,true); - in += "\\DOSBox"; + in += "\\DOSBox-VirtIsa"; mkdir(in.c_str()); #elif defined(MACOSX) in = "~/Library/Preferences/"; ResolveHomedir(in); //Don't create it. Assume it exists #else - in = "~/.dosbox"; + in = "~/.dosbox-virtisa"; ResolveHomedir(in); mkdir(in.c_str(),0700); #endif diff --git a/tools/.gitignore b/tools/.gitignore new file mode 100644 index 0000000..14034d9 --- /dev/null +++ b/tools/.gitignore @@ -0,0 +1,3 @@ +* +!create_sd.sh +!DEBUG.COM diff --git a/tools/create_sd.sh b/tools/create_sd.sh new file mode 100644 index 0000000..c1854c7 --- /dev/null +++ b/tools/create_sd.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +loremipsum () { + if [ "${1}" = "" ] || [ "${2}" = "" ]; then + echo "Usage: loremipsum [paragraphs, sentences] [integer]" + else + curl -s http://metaphorpsum.com/"${1}"/"${2}" && printf "\n" + fi +} + +set -e # Exit on error + +# delete any existing files and folders +rm -rf *.img *.zip software-main + +# Configuration +IMAGE_NAME="sdcard.img" +PARTITION_SIZE="128MiB" +FILESYSTEM="fat32" +LABEL="MSDOS" + +# Step 1: Create an empty image file +echo "Creating an empty image file of size $PARTITION_SIZE..." +dd if=/dev/zero of=$IMAGE_NAME bs=1M count=$(echo $PARTITION_SIZE | sed 's/MiB//') + +# Step 2: Partition the image +echo "Partitioning the image..." +parted --script $IMAGE_NAME mklabel msdos +parted --script $IMAGE_NAME mkpart primary $FILESYSTEM 1MiB 100% + +# Step 3: Associate the image with a loop device +LOOP_DEVICE=$(losetup --show -fP $IMAGE_NAME) +echo "Image attached to loop device: $LOOP_DEVICE" + +# Step 4: Format the partition as FAT32 +echo "Formatting the partition as FAT32..." +mkfs.vfat -F 32 -n $LABEL "${LOOP_DEVICE}p1" + +# Step 5: Mount and populate partitions +echo "Mounting partitions..." +ROOT_MOUNT=$(mktemp -d) + +# Step 6: mount device +mount "${LOOP_DEVICE}p1" $ROOT_MOUNT + +# Step 8: Populate the partitions +echo "Populating root partition..." +cp -rv /mnt/d/GAMES $ROOT_MOUNT + +# Step 9: Cleanup +echo "Unmounting and cleaning up..." +umount $ROOT_MOUNT +losetup -d $LOOP_DEVICE +rmdir $ROOT_MOUNT + +echo "SD card image creation completed: $IMAGE_NAME"