diff --git a/README.md b/README.md index 7870220b..dd2cc632 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,7 @@ choices because they can store multiple types of file system. | [`icl30`](doc/disk-icl30.md) | ICL Model 30: CP/M; 263kB 35-track DSSD | 🦖 | | CPMFS | | [`mac`](doc/disk-mac.md) | Macintosh: 400kB/800kB 3.5" GCR | 🦄 | 🦄 | MACHFS | | [`micropolis`](doc/disk-micropolis.md) | Micropolis: 100tpi MetaFloppy disks | 🦄 | 🦄 | | +| [`ms2000`](doc/disk-ms2000.md) | : MS2000 Microdisk Development System | | | MICRODOS | | [`mx`](doc/disk-mx.md) | DVK MX: Soviet-era PDP-11 clone | 🦖 | | | | [`n88basic`](doc/disk-n88basic.md) | N88-BASIC: PC8800/PC98 5.25" 77-track 26-sector DSHD | 🦄 | 🦄 | | | [`northstar`](doc/disk-northstar.md) | Northstar: 5.25" hard sectored | 🦄 | 🦄 | | diff --git a/lib/build.mk b/lib/build.mk index 6e73483d..c8fc1c5f 100644 --- a/lib/build.mk +++ b/lib/build.mk @@ -81,6 +81,7 @@ LIBFLUXENGINE_SRCS = \ lib/vfs/imagesectorinterface.cc \ lib/vfs/lif.cc \ lib/vfs/machfs.cc \ + lib/vfs/microdos.cc \ lib/vfs/philefs.cc \ lib/vfs/prodos.cc \ lib/vfs/roland.cc \ diff --git a/lib/utils.cc b/lib/utils.cc index 9d4f295d..72b24317 100644 --- a/lib/utils.cc +++ b/lib/utils.cc @@ -197,6 +197,17 @@ bool doesFileExist(const std::string& filename) return f.good(); } +int countSetBits(uint32_t word) +{ + int b = 0; + while (word) + { + b += word & 1; + word >>= 1; + } + return b; +} + uint32_t unbcd(uint32_t bcd) { uint32_t dec = 0; diff --git a/lib/utils.h b/lib/utils.h index e6373c93..489da6a9 100644 --- a/lib/utils.h +++ b/lib/utils.h @@ -19,6 +19,7 @@ extern std::string quote(const std::string& s); extern std::string unhex(const std::string& s); extern std::string tohex(const std::string& s); extern bool doesFileExist(const std::string& filename); +extern int countSetBits(uint32_t word); extern uint32_t unbcd(uint32_t bcd); template diff --git a/lib/vfs/microdos.cc b/lib/vfs/microdos.cc new file mode 100644 index 00000000..94b75266 --- /dev/null +++ b/lib/vfs/microdos.cc @@ -0,0 +1,223 @@ +#include "lib/globals.h" +#include "lib/vfs/vfs.h" +#include "lib/config.pb.h" +#include "lib/utils.h" +#include + +/* See https://www.hp9845.net/9845/projects/hpdir/#lif_filesystem for + * a description. */ + +static void trimZeros(std::string s) +{ + s.erase(std::remove(s.begin(), s.end(), 0), s.end()); +} + +class MicrodosFilesystem : public Filesystem +{ + struct SDW + { + unsigned start; + unsigned length; + }; + + class MicrodosDirent : public Dirent + { + public: + MicrodosDirent(MicrodosFilesystem& fs, Bytes& bytes) + { + file_type = TYPE_FILE; + + ByteReader br(bytes); + auto stem = trimWhitespace(br.read(6)); + auto ext = trimWhitespace(br.read(3)); + filename = fmt::format("{}.{}", stem, ext); + + br.skip(1); + ssn = br.read_be16(); + attr = br.read_8(); + + Bytes rib = fs.getLogicalSector(ssn); + ByteReader rbr(rib); + for (int i = 0; i < 57; i++) + { + unsigned w = rbr.read_be16(); + if (w & 0x8000) + { + /* Last. */ + sectors = w & 0x7fff; + break; + } + else + { + /* Each record except the last is 24 bits long. */ + w = (w << 8) | rbr.read_8(); + sdws.emplace_back(SDW{w & 0xffff, (w >> 16) + 1}); + } + } + rbr.seek(500); + lastSectorBytes = rbr.read_be16(); + loadSectors = rbr.read_be16(); + loadAddress = rbr.read_be16(); + startAddress = rbr.read_be16(); + + length = sectors * 512; + + mode = ""; + path = {filename}; + + attributes[Filesystem::FILENAME] = filename; + attributes[Filesystem::LENGTH] = std::to_string(length); + attributes[Filesystem::FILE_TYPE] = "file"; + attributes[Filesystem::MODE] = mode; + attributes["microdos.ssn"] = std::to_string(ssn); + attributes["microdos.attr"] = fmt::format("0x{:x}", attr); + attributes["microdos.sdw_count"] = std::to_string(sdws.size()); + attributes["microdos.total_sectors"] = std::to_string(sectors); + attributes["microdos.lastSectorBytes"] = + std::to_string(lastSectorBytes); + attributes["microdos.loadSectors"] = std::to_string(loadSectors); + attributes["microdos.loadAddress"] = + fmt::format("0x{:x}", loadAddress); + attributes["microdos.startAddress"] = + fmt::format("0x{:x}", startAddress); + } + + public: + unsigned ssn; + unsigned attr; + std::vector sdws; + unsigned sectors; + unsigned lastSectorBytes; + unsigned loadSectors; + unsigned loadAddress; + unsigned startAddress; + }; + +public: + MicrodosFilesystem( + const MicrodosProto& config, std::shared_ptr sectors): + Filesystem(sectors), + _config(config) + { + } + + uint32_t capabilities() const + { + return OP_GETFSDATA | OP_LIST | OP_GETFILE | OP_GETDIRENT; + } + + FilesystemStatus check() override + { + return FS_OK; + } + + std::map getMetadata() override + { + mount(); + + std::map attributes; + + attributes[VOLUME_NAME] = _volumeLabel; + attributes[TOTAL_BLOCKS] = std::to_string(_totalBlocks); + attributes[USED_BLOCKS] = std::to_string(_usedBlocks); + attributes[BLOCK_SIZE] = "512"; + return attributes; + } + + std::shared_ptr getDirent(const Path& path) override + { + mount(); + if (path.size() != 1) + throw BadPathException(); + + return findFile(path.front()); + } + + std::vector> list(const Path& path) override + { + mount(); + if (!path.empty()) + throw FileNotFoundException(); + + std::vector> result; + for (auto& de : _dirents) + result.push_back(de); + return result; + } + + Bytes getFile(const Path& path) override + { + mount(); + if (path.size() != 1) + throw BadPathException(); + + auto dirent = findFile(path.front()); + + Bytes data; + ByteWriter bw(data); + for (const auto& sdw : dirent->sdws) + bw += getLogicalSector(sdw.start, sdw.length); + + return data.slice(512); + } + +private: + void mount() + { + _rootBlock = getLogicalSector(0); + _catBlock = getLogicalSector(9); + Bytes directory = getLogicalSector(1, 8); + + ByteReader rbr(_rootBlock); + rbr.seek(20); + _volumeLabel = trimWhitespace(rbr.read(44)); + + _dirents.clear(); + ByteReader dbr(directory); + while (!dbr.eof()) + { + Bytes direntBytes = dbr.read(16); + if ((direntBytes[0] != 0) && (direntBytes[0] != 0xff)) + { + auto dirent = + std::make_unique(*this, direntBytes); + _dirents.push_back(std::move(dirent)); + } + } + + ByteReader cbr(_catBlock); + _totalBlocks = 630; + _usedBlocks = 0; + for (int i = 0; i < _totalBlocks / 8; i++) + { + uint8_t b = cbr.read_8(); + _usedBlocks += countSetBits(b); + } + } + + std::shared_ptr findFile(const std::string filename) + { + for (const auto& dirent : _dirents) + { + if (dirent->filename == filename) + return dirent; + } + + throw FileNotFoundException(); + } + +private: + const MicrodosProto& _config; + Bytes _rootBlock; + Bytes _catBlock; + std::string _volumeLabel; + unsigned _totalBlocks; + unsigned _usedBlocks; + std::vector> _dirents; +}; + +std::unique_ptr Filesystem::createMicrodosFilesystem( + const FilesystemProto& config, std::shared_ptr sectors) +{ + return std::make_unique(config.microdos(), sectors); +} diff --git a/lib/vfs/vfs.cc b/lib/vfs/vfs.cc index f1146fd1..0de71064 100644 --- a/lib/vfs/vfs.cc +++ b/lib/vfs/vfs.cc @@ -216,6 +216,9 @@ std::unique_ptr Filesystem::createFilesystem( case FilesystemProto::LIF: return Filesystem::createLifFilesystem(config, image); + case FilesystemProto::MICRODOS: + return Filesystem::createMicrodosFilesystem(config, image); + case FilesystemProto::ZDOS: return Filesystem::createZDosFilesystem(config, image); diff --git a/lib/vfs/vfs.h b/lib/vfs/vfs.h index 43e33899..a9a0090b 100644 --- a/lib/vfs/vfs.h +++ b/lib/vfs/vfs.h @@ -264,6 +264,8 @@ class Filesystem const FilesystemProto& config, std::shared_ptr image); static std::unique_ptr createLifFilesystem( const FilesystemProto& config, std::shared_ptr image); + static std::unique_ptr createMicrodosFilesystem( + const FilesystemProto& config, std::shared_ptr image); static std::unique_ptr createZDosFilesystem( const FilesystemProto& config, std::shared_ptr image); static std::unique_ptr createRolandFsFilesystem( diff --git a/lib/vfs/vfs.proto b/lib/vfs/vfs.proto index 9b7a46c9..6ba11004 100644 --- a/lib/vfs/vfs.proto +++ b/lib/vfs/vfs.proto @@ -88,6 +88,9 @@ message LifProto [ default = 256, (help) = "LIF filesystem block size" ]; } +message MicrodosProto {} + +// NEXT_TAG: 16 message ZDosProto { message Location @@ -110,7 +113,7 @@ message RolandFsProto [ (help) = "number of directory entries", default = 79 ]; } -// NEXT_TAG: 17 +// NEXT_TAG: 18 message FilesystemProto { enum FilesystemType @@ -128,8 +131,9 @@ message FilesystemProto APPLEDOS = 10; PHILE = 11; LIF = 12; - ZDOS = 13; - ROLAND = 14; + MICRODOS = 13; + ZDOS = 14; + ROLAND = 15; } optional FilesystemType type = 10 @@ -147,8 +151,9 @@ message FilesystemProto optional Smaky6FsProto smaky6 = 11; optional PhileProto phile = 13; optional LifProto lif = 14; - optional ZDosProto zdos = 15; - optional RolandFsProto roland = 16; + optional MicrodosProto microdos = 15; + optional ZDosProto zdos = 16; + optional RolandFsProto roland = 17; optional SectorListProto sector_order = 9 [ (help) = "specify the filesystem order of sectors" ]; diff --git a/src/formats/build.mk b/src/formats/build.mk index 0a6297d5..c9455dd3 100644 --- a/src/formats/build.mk +++ b/src/formats/build.mk @@ -21,6 +21,7 @@ FORMATS = \ icl30 \ mac \ micropolis \ + ms2000 \ mx \ n88basic \ northstar \ diff --git a/src/formats/ms2000.textpb b/src/formats/ms2000.textpb new file mode 100644 index 00000000..0e85b60d --- /dev/null +++ b/src/formats/ms2000.textpb @@ -0,0 +1,58 @@ +comment: 'MS2000 Microdisk Development System' + +documentation: +<<< +The RCA MicroDisk Development System MS2000 is a highly obscure (i.e. I gather +that single digit numbers of original machines exist) development system for the +RCA1802 series of CPUs, as made famous by the Cosmac ELF. It was a fairly +straightforward big bag o'RAM system with a 2kB boot ROM, 62kB of RAM, twin +floppy drives and a serial terminal --- CP/M users will find it very familiar. + +Read and writing disks is currently not supported by FluxEngine, but there is +basic support for the MicroDisk operating system's file system. This should +allow files to be read from MS2000 disk images. + +The disks are normal DD 3.5" disks, using a 70-track, single sided variation of +the venerable IBM floppy disk scheme, so allowing 315kB of storage per disk. + +If you have access to flux files for MS2000 disks, please [get in +touch](https://github.com/davidgiven/cpm65/issues/new) --- I would like to add +better support for these. +>>> + +documentation: +<<< +## References + + - [The EMMA-02 emulator](https://www.emma02.hobby-site.com/ms2000.html), which + supports the MS2000 and provides information on it. +>>> + +image_reader { + filename: "ms2000.img" + type: IMAGETYPE_IMG +} + +image_writer { + filename: "ms2000.img" + type: IMAGETYPE_IMG +} + +layout { + tracks: 70 + sides: 1 + tpi: 135 + layoutdata { + sector_size: 512 + physical { + start_sector: 1 + count: 9 + } + } +} + +filesystem { + type: MICRODOS +} + +