commit 5f6e1572ab1cbb291378090c44d2dc83422ea7da
parent afef40a0af426b741289ae0e89306a97987dff72
Author: Jean Schurger <jean@schurger.org>
Date: Thu, 5 Nov 2026 14:48:56 -0500
Exeldisk : lecteur de disquettes WD1770, EXELDOS démarre, disquettes dans la bibliothèque
Étape 3. D'\''après EXELDOS.equ du devkit Jester et le ROM d'\''amorçage (exeldisk_150.rom de DCExel, versionné
dans roms/) : contrôleur WD1770 derrière P64-P67 ($0140-$0143, lecture en deux temps P66/P67, DRQ sur PA.1),
verrou DSKCTL qui choisit la carte mémoire ($8000 ROM boot/CRAM, $D000 RAM DOS/COS, $0200 RAM/cartouche),
ROM 8 Ko en $8000 et en fenêtre $C800, RAM 31,5 Ko + RAM DOS 8 Ko. Image .fd = 640 Ko à plat (80 pistes,
2 faces, 16 secteurs de 256). Classe ExelDisk (exelv_disk.{h,cpp}) : seek/restore, lecture/écriture de
secteur, WRITE TRACK décodé (formatage), read address. L'\''Exeldisk n'\''est vu que si une image est montée
(--tape x.fd), sinon le logo.
Page : groupe « Disquettes (Exeldisk, EXELDOS) » et puce dans la bibliothèque (roms/disks/, ignoré),
dépôt d'\''une image .fd, aide « L'\''Exeldisk ». Vérifié headless : ExelDOS 1.4 et les six autres disquettes
amorcent (BOOT V1.5, INIBAS.SVC, EXELDOS V1.4) et DIR liste leurs fichiers ; EXEC (Jester) se lance.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyJB9yqUAYm3wRQi4uytj9
Diffstat:
7 files changed, 737 insertions(+), 25 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -9,6 +9,7 @@ web/index.html
.DS_Store
roms/cartridges/
roms/tapes/
+roms/disks/
tools/wav2k7.js
tools/wav2k7.wasm
build-dbg/
diff --git a/patches/exl100.patch b/patches/exl100.patch
@@ -1,18 +1,26 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
-index a1ed784..3ed811e 100644
+index a1ed784..3ffea37 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
-@@ -442,7 +442,7 @@ function(dsp_add_frontend target)
+@@ -297,6 +297,7 @@ add_library(dsp_lib STATIC
+ src/drivers/computers/apple2gs.cpp
+ src/drivers/computers/exelv.cpp
+ src/drivers/computers/exelv_tape.cpp
++ src/drivers/computers/exelv_disk.cpp
+ src/drivers/computers/ql.cpp
+ src/drivers/computers/atari_st.cpp
+ src/drivers/computers/amiga.cpp
+@@ -442,7 +443,7 @@ function(dsp_add_frontend target)
"-sTOTAL_STACK=8388608"
"-sEXIT_RUNTIME=0"
"-sFORCE_FILESYSTEM=1"
- "-sEXPORTED_FUNCTIONS=['_dsp_web_start','_dsp_web_list','_dsp_web_has_embedded']"
-+ "-sEXPORTED_FUNCTIONS=['_dsp_web_start','_dsp_web_list','_dsp_web_has_embedded','_dsp_web_cram_present','_dsp_web_cram_dirty','_dsp_web_cram_save','_dsp_web_cram_load']"
++ "-sEXPORTED_FUNCTIONS=['_dsp_web_start','_dsp_web_list','_dsp_web_has_embedded','_dsp_web_cram_present','_dsp_web_cram_dirty','_dsp_web_cram_save','_dsp_web_cram_load','_dsp_web_disk_present','_dsp_web_disk_dirty','_dsp_web_disk_save','_dsp_web_disk_load']"
"-sEXPORTED_RUNTIME_METHODS=['ccall','UTF8ToString','FS']"
"-sMINIFY_HTML=0"
"--shell-file=${CMAKE_CURRENT_BINARY_DIR}/shell.html")
diff --git a/src/drivers/computers/exelv.cpp b/src/drivers/computers/exelv.cpp
-index debcb94..8891140 100644
+index debcb94..c1fad73 100644
--- a/src/drivers/computers/exelv.cpp
+++ b/src/drivers/computers/exelv.cpp
@@ -2,6 +2,7 @@
@@ -23,7 +31,27 @@ index debcb94..8891140 100644
#include <cstring>
#include "core/fs_compat.h"
#include <fstream>
-@@ -178,6 +179,30 @@ bool Exelv::load_bios(const std::string& rom_path, std::string* error) {
+@@ -94,6 +95,19 @@ Exelv::Exelv(Model model)
+ maincpu_.set_port_in(Tms7000::kPortA, [this]() { return tms7020_porta_r(); });
+ maincpu_.set_port_out(Tms7000::kPortB, [this](uint8_t v) { tms7020_portb_w(v); });
+ maincpu_.set_cycle_handler([this](int c) { on_main_cycles(c); });
++#ifdef CRAM_DBG
++ // Trace le dispatcher de commandes du 7042 ($F1C8) et l'entrée des handlers de bloc.
++ subcpu_.set_cycle_handler([this](int) {
++ const uint16_t pc = subcpu_.pc();
++ if (pc == 0xf1c8) std::fprintf(stderr, "7042 dispatch cmd=%02x cont=%02x%02x\n", unsigned(subcpu_.b()), unsigned(subcpu_.ram_at(45)), unsigned(subcpu_.ram_at(46)));
++ else if (pc == 0xf1e4) std::fprintf(stderr, "7042 RESET par cmd %02x\n", unsigned(subcpu_.b()));
++ else if (pc == 0xf006) std::fprintf(stderr, "7042 entre en RESET ($F006)\n");
++ else if (pc == 0xf2d8) std::fprintf(stderr, "7042 handler $0B (bloc)\n");
++ else if (pc == 0xf1a1) std::fprintf(stderr, "7042 poste %02x\n", unsigned(subcpu_.b()));
++ else if (pc == 0xf4ac) std::fprintf(stderr, "7041 IR code brut %02x\n", unsigned(subcpu_.ram_at(7)));
++ else if (pc == 0xf4b7) std::fprintf(stderr, "7041 IR touche R43=%02x R9=%02x R52=%02x\n", unsigned(subcpu_.ram_at(43)), unsigned(subcpu_.ram_at(9)), unsigned(subcpu_.ram_at(52)));
++ });
++#endif
+
+ subcpu_.set_memory_handlers([this](uint16_t a) { return read_sub(a); },
+ [this](uint16_t a, uint8_t v) { write_sub(a, v); });
+@@ -178,6 +192,30 @@ bool Exelv::load_bios(const std::string& rom_path, std::string* error) {
if (loader.open(rom_path, &ignored) && loader.load(kExeltelSys, sys, &ignored)) {
system_rom_ = std::move(sys);
}
@@ -54,7 +82,61 @@ index debcb94..8891140 100644
}
if (main_rom.size() < (model_ == Model::Exl100 ? 0x800u : 0x1000u)) return false;
-@@ -377,7 +402,50 @@ uint8_t Exelv::read_main(uint16_t address) {
+@@ -249,6 +287,17 @@ bool Exelv::load_media(const std::string& path, std::string* error) {
+ namespace fs = dsp::fsx;
+ std::error_code ec;
+ const std::string ext = lower_copy(fs::path(path).extension().string());
++ if (ext == ".fd") {
++ if (!read_plain_file(path, data)) {
++ if (error) *error = "cannot read disk image " + path;
++ return false;
++ }
++ if (!disk_.mount(std::move(data))) {
++ if (error) *error = "bad .fd image (expected 655360 bytes) " + path;
++ return false;
++ }
++ return true;
++ }
+ if (ext == ".k7" || ext == ".wav") {
+ if (!read_plain_file(path, data)) {
+ if (error) *error = "cannot read cassette " + path;
+@@ -286,6 +335,18 @@ bool Exelv::load_media(const std::string& path, std::string* error) {
+ }
+
+ void Exelv::reset() {
++ // Exeldisk (EXL 100) : branché si son ROM d'amorçage est dans le MEMFS (la page le retire
++ // quand aucune disquette n'est choisie, sinon le BIOS amorcerait sur le lecteur vide).
++ if (model_ == Model::Exl100 && disk_rom_.empty()) {
++ std::vector<uint8_t> rom;
++ if (read_plain_file("/roms/exeldisk.rom", rom) && rom.size() == 0x2000) {
++ disk_rom_ = std::move(rom);
++ disk_present_ = true;
++ disk_ram_.assign(0x7e00, 0);
++ dos_ram_.assign(0x2000, 0);
++ }
++ }
++ disk_.reset();
+ ram_.fill(0);
+ tms7020_portb_ = 0;
+ tms7041_portb_ = 0;
+@@ -366,6 +427,16 @@ uint8_t Exelv::read_main(uint16_t address) {
+ if (address == 0x0125) return vdp_.reg_r();
+ if (address == 0x0128) return vdp_.initptr_r();
+ if (address == 0x0130) return mailbox_wx319_r();
++ if (disk_active()) {
++ if (address == 0x0140) return disk_.read_status_port();
++ if (address == 0x0142) return disk_.start_read();
++ if (address == 0x0143) return disk_.finish_read();
++ if (address >= 0x0200 && address <= 0x7fff && !disk_.cart_at_0200()) return disk_ram_[address - 0x0200];
++ if (address >= 0x8000 && address <= 0x9fff && disk_.boot_rom_at_8000()) return disk_rom_[address - 0x8000];
++ if (address >= 0xa000 && address <= 0xbfff && disk_.boot_rom_at_8000()) return 0xff;
++ if (address >= 0xc800 && address <= 0xcfff) return disk_rom_[address - 0xc000];
++ if (address >= 0xd000 && address <= 0xefff && !disk_.cos_at_d000()) return dos_ram_[address - 0xd000];
++ }
+ if (model_ == Model::Exeltel) {
+ if (address == 0x0138) {
+ page_bit1_ = true;
+@@ -377,7 +448,55 @@ uint8_t Exelv::read_main(uint16_t address) {
}
if (address == 0x0140) return p64_;
}
@@ -82,6 +164,11 @@ index debcb94..8891140 100644
+ // directement en code page 2. RAM CRAM à $8000-$BFFF. Pas de $A5 à $C800
+ // (sinon la détection boot auto-lance $8001 = RAM vide -> plantage).
+ if (cram_present_) {
++#ifdef CRAM_AUTOSTART
++ // Expérience : octet d'autostart vu par la détection d'amorçage du 7040 (page 1, $C800 == $A5 ->
++ // BRLANC $8001) pour lancer un programme machine stocké dans la CRAM.
++ if (model_ == Model::Exeltel && address == 0xc800) return 0xa5;
++#endif
+ if (address >= 0x8000 && address <= 0xbfff) {
+#ifdef CRAM_DBG_READS
+ std::fprintf(stderr, "MR %04x pc=%04x\n", unsigned(address), unsigned(maincpu_.pc()));
@@ -105,7 +192,28 @@ index debcb94..8891140 100644
if (address >= 0xc000 && address <= 0xc7ff) return ram_[address - 0xc000];
return 0xff;
}
-@@ -409,6 +477,43 @@ void Exelv::write_main(uint16_t address, uint8_t value) {
+@@ -395,6 +514,20 @@ void Exelv::write_main(uint16_t address, uint8_t value) {
+ mailbox_wx318_w(value);
+ return;
+ }
++ if (disk_active()) {
++ if (address == 0x0140) { disk_.write_control(value); return; }
++ if (address == 0x0141) { disk_.write_register(value); return; }
++ if (address == 0x0142 || address == 0x0143) return; // choix de densité
++ if (address >= 0x0200 && address <= 0x7fff) {
++ if (!disk_.cart_at_0200()) disk_ram_[address - 0x0200] = value;
++ return;
++ }
++ if (address >= 0x8000 && address <= 0xbfff && disk_.boot_rom_at_8000()) return;
++ if (address >= 0xd000 && address <= 0xefff) {
++ if (!disk_.cos_at_d000()) dos_ram_[address - 0xd000] = value;
++ return;
++ }
++ }
+ if (model_ == Model::Exeltel) {
+ if (address == 0x0138) {
+ page_bit1_ = false;
+@@ -409,6 +542,43 @@ void Exelv::write_main(uint16_t address, uint8_t value) {
return;
}
}
@@ -149,7 +257,62 @@ index debcb94..8891140 100644
if (address >= 0xc000 && address <= 0xc7ff) ram_[address - 0xc000] = value;
}
-@@ -503,9 +608,20 @@ void Exelv::fast_load_hook() {
+@@ -416,10 +586,25 @@ uint8_t Exelv::read_sub(uint16_t) { return 0xff; }
+ void Exelv::write_sub(uint16_t, uint8_t) {}
+
+ uint8_t Exelv::mailbox_wx319_r() { return wx319_; }
+-void Exelv::mailbox_wx318_w(uint8_t data) { wx318_ = data; }
++void Exelv::mailbox_wx318_w(uint8_t data) {
++#ifdef CRAM_DBG
++ {
++ const uint8_t sp = maincpu_.sp();
++ const unsigned r1 = (unsigned(maincpu_.ram_at(uint8_t(sp - 1))) << 8) | maincpu_.ram_at(sp);
++ const unsigned r2 = (unsigned(maincpu_.ram_at(uint8_t(sp - 3))) << 8) | maincpu_.ram_at(uint8_t(sp - 2));
++ std::fprintf(stderr, "MBX main->sub %02x pc=%04x ret=%04x,%04x\n", unsigned(data), unsigned(maincpu_.pc()), r1, r2);
++ }
++#endif
++ wx318_ = data;
++}
+
+ uint8_t Exelv::tms7020_porta_r() {
+ uint8_t data = (tms7041_portb_ & 0x80) ? 0x01 : 0x00;
++ // Exeldisk : PA.1 = DRQ du 1770, PA.3 = état ROM/RAM de $0200.
++ if (disk_active()) {
++ if (disk_.drq()) data |= 0x02;
++ if (disk_.cart_at_0200()) data |= 0x08;
++ }
+ // PA.4: cassette input, high when idle.
+ if (!tape_.loaded() || tape_.level()) data |= 0x10;
+ return data;
+@@ -447,10 +632,16 @@ uint8_t Exelv::tms7041_porta_r() {
+ void Exelv::tms7041_portb_w(uint8_t data) {
+ speech_.strobe_ws_rs(data & 0x03);
+ if ((tms7041_portb_ & 0x04) && !(data & 0x04)) {
++#ifdef CRAM_DBG
++ std::fprintf(stderr, "MBX sub->main INT1 (wx319=%02x, sub pc=%04x)\n", unsigned(wx319_), unsigned(subcpu_.pc()));
++#endif
+ maincpu_.set_input_line(Tms7000::kInt1, IrqLine::Hold);
+ }
+ if (!(tms7041_portb_ & 0x40) && (data & 0x40)) {
+ wx319_ = tms7041_portc_;
++#ifdef CRAM_DBG
++ std::fprintf(stderr, "MBX sub poste %02x\n", unsigned(wx319_));
++#endif
+ // Function $01 (key/joystick 0) is followed by the key code; $04
+ // there reports the release.
+ if (last_sent_ == 0x01 && wx319_ != 0x04) last_key_ = wx319_;
+@@ -460,6 +651,10 @@ void Exelv::tms7041_portb_w(uint8_t data) {
+ }
+
+ uint8_t Exelv::tms7041_portc_r() {
++#ifdef CRAM_DBG
++ if (!(tms7041_portb_ & 0x20))
++ std::fprintf(stderr, "MBX sub lit %02x (sub pc=%04x)\n", unsigned(wx318_), unsigned(subcpu_.pc()));
++#endif
+ if (!(tms7041_portb_ & 0x20)) return wx318_;
+ return 0xff;
+ }
+@@ -503,9 +698,20 @@ void Exelv::fast_load_hook() {
const uint16_t read_byte = exl ? 0xfe82 : 0xfcbd;
const uint16_t pc = maincpu_.pc();
if (pc == sync_start) {
@@ -171,7 +334,7 @@ index debcb94..8891140 100644
if (value < 0) return; // end of tape: let the BIOS time out
maincpu_.set_a(uint8_t(value));
// RETS: the low byte of the return address is on top of the stack.
-@@ -517,9 +633,28 @@ void Exelv::fast_load_hook() {
+@@ -517,9 +723,28 @@ void Exelv::fast_load_hook() {
void Exelv::on_main_cycles(int cycles) {
main_cycles_ += uint64_t(cycles);
@@ -201,7 +364,17 @@ index debcb94..8891140 100644
const uint32_t cpu_clock = maincpu_.cpu_clock();
audio_accumulator_ += int64_t(cycles) * kSampleRate;
while (audio_accumulator_ >= cpu_clock) {
-@@ -677,6 +812,22 @@ void Exelv::tick_keyboard(int cpu_cycles) {
+@@ -601,6 +826,9 @@ void Exelv::tick_keyboard(int cpu_cycles) {
+ if (k_ch_byte_ < 2) {
+ k_channels_[0] = scan_key_channel();
+ if (k_channels_[0] != 0xff && k_ch_byte_ == 0) {
++#ifdef CRAM_DBG
++ std::fprintf(stderr, "KBD trame ch0=%02x ch1=%02x\n", unsigned(k_channels_[0]), unsigned(k_channels_[1]));
++#endif
+ if (k_channels_[1] == 0xff) k_channels_[1] = k_channels_[0];
+ k_ch_bit_ = 0;
+ k_bit_num_ = false;
+@@ -677,6 +905,22 @@ void Exelv::tick_keyboard(int cpu_cycles) {
}
void Exelv::run_frame() {
@@ -224,7 +397,7 @@ index debcb94..8891140 100644
const uint32_t cpu_clock = maincpu_.cpu_clock();
const int cycles_per_line = int(double(cpu_clock) / (kFramesPerSecond * kScanlines) + 0.5);
for (int line = 0; line < kScanlines; line++) {
-@@ -696,6 +847,28 @@ void Exelv::run_frame() {
+@@ -696,6 +940,28 @@ void Exelv::run_frame() {
tick_keyboard(slice);
remain -= slice;
}
@@ -253,24 +426,32 @@ index debcb94..8891140 100644
// Without an I/O CPU ROM, post the "I/O CPU initialized" byte ($08)
// the main CPU waits for. The INT1 handler reads it with PA.0 low,
// raises PB.1 and waits for PA.0 to follow (tms7020_portb_w).
-@@ -709,6 +882,12 @@ void Exelv::run_frame() {
+@@ -709,6 +975,12 @@ void Exelv::run_frame() {
}
}
}
+#ifdef CRAM_PC
+ if (cram_present_) {
+ static int fr = 0;
-+ std::fprintf(stderr, "FR%04d PC=%04x\n", fr++, unsigned(maincpu_.pc()));
++ std::fprintf(stderr, "FR%04d PC=%04x SUB=%04x\n", fr++, unsigned(maincpu_.pc()), unsigned(subcpu_.pc()));
+ }
+#endif
// Write a SAVE out once the BIOS has left the tape routine for a second.
if (tape_.recording()) {
if (in_tape_read()) {
diff --git a/src/drivers/computers/exelv.h b/src/drivers/computers/exelv.h
-index b3857e8..a09827f 100644
+index b3857e8..89202ab 100644
--- a/src/drivers/computers/exelv.h
+++ b/src/drivers/computers/exelv.h
-@@ -72,6 +72,22 @@ public:
+@@ -6,6 +6,7 @@
+ #include <vector>
+
+ #include "core/machine.h"
++#include "drivers/computers/exelv_disk.h"
+ #include "drivers/computers/exelv_tape.h"
+ #include "cpu/tms7000.h"
+ #include "sound/tms5220.h"
+@@ -72,6 +73,32 @@ public:
}
bool bios_loaded() const { return bios_loaded_; }
@@ -290,10 +471,20 @@ index b3857e8..a09827f 100644
+ cram_dirty_ = false;
+ return dirty;
+ }
++ // Exeldisk : présent si /roms/exeldisk.rom est là au démarrage ; image .fd montée par
++ // load_media (--tape x.fd) ou disk_mount (page web), persistance comme la CRAM.
++ bool disk_present() const { return disk_present_; }
++ // L'Exeldisk n'est vu par le BIOS que si une disquette est montée : sans elle, l'EXL 100
++ // démarre sur son logo (le ROM d'amorçage demanderait « Insert system disk »).
++ bool disk_active() const { return disk_present_ && disk_.mounted(); }
++ bool disk_mounted() const { return disk_.mounted(); }
++ const std::vector<uint8_t>& disk_image() const { return disk_.image(); }
++ bool disk_mount(std::vector<uint8_t> data) { return disk_.mount(std::move(data)); }
++ bool disk_take_dirty() { return disk_.take_dirty(); }
bool sub_present() const { return sub_present_; }
uint16_t debug_pc() const { return maincpu_.pc(); }
uint8_t debug_a() const { return maincpu_.a(); }
-@@ -125,6 +141,17 @@ private:
+@@ -125,6 +152,24 @@ private:
std::array<uint8_t, 0x800> ram_{};
std::vector<uint8_t> cart_;
std::vector<uint8_t> system_rom_;
@@ -308,11 +499,356 @@ index b3857e8..a09827f 100644
+ // logicielle (R16 = $02D7), ses variables ($0260-$02D8) et le volume A ($02DA+).
+ std::vector<uint8_t> cram_pram_;
+ bool cram_dirty_ = false; // CRAM modifiée depuis la dernière sauvegarde de la page
++ // Exeldisk : ROM d'amorçage 8 Ko ($8000-$9FFF au démarrage, fenêtre $C800-$CFFF = ROM+$0800),
++ // RAM 31,5 Ko à $0200-$7FFF (à la place de la cartouche), RAM DOS 8 Ko à $D000-$EFFF.
++ ExelDisk disk_;
++ std::vector<uint8_t> disk_rom_;
++ std::vector<uint8_t> disk_ram_;
++ std::vector<uint8_t> dos_ram_;
++ bool disk_present_ = false;
uint8_t tms7020_portb_ = 0;
uint8_t tms7041_portb_ = 0;
+diff --git a/src/drivers/computers/exelv_disk.cpp b/src/drivers/computers/exelv_disk.cpp
+new file mode 100644
+index 0000000..088411c
+--- /dev/null
++++ b/src/drivers/computers/exelv_disk.cpp
+@@ -0,0 +1,235 @@
++#include "drivers/computers/exelv_disk.h"
++
++#include <algorithm>
++
++namespace dsp {
++
++namespace {
++constexpr uint8_t kBusy = 0x01;
++constexpr uint8_t kDrq = 0x02; // type II/III ; INDEX en type I
++constexpr uint8_t kLost = 0x04; // type II/III ; TRK0 en type I
++constexpr uint8_t kCrc = 0x08;
++constexpr uint8_t kRnf = 0x10;
++constexpr uint8_t kSpinUp = 0x20; // type I ; RECORD TYPE en type II
++constexpr uint8_t kWriteProt = 0x40;
++constexpr uint8_t kMotor = 0x80;
++} // namespace
++
++void ExelDisk::reset() {
++ control_ = 0;
++ latch_ = 0xff;
++ status_ = 0;
++ track_ = 0;
++ sector_ = 0;
++ data_ = 0;
++ drq_ = intrq_ = false;
++ type1_ = true;
++ op_ = Op::None;
++ buffer_.clear();
++ pos_ = 0;
++}
++
++bool ExelDisk::mount(std::vector<uint8_t> image) {
++ if (image.size() != kImageSize) return false;
++ image_ = std::move(image);
++ dirty_ = false;
++ return true;
++}
++
++uint8_t ExelDisk::read_status_port() const {
++ // bit0 : disquette changée / absente (le ROM répond « drive not ready » si elle est à 1) ;
++ // bit1 : lecteur sélectionné ; bit2 : prêt ; bit3 : INTRQ du 1770.
++ uint8_t v = 0;
++ if (!mounted()) v |= 0x01;
++ if (control_ & 0x0c) v |= 0x02;
++ if (mounted() && (control_ & 0x0c)) v |= 0x04;
++ if (intrq_) v |= 0x08;
++ return v;
++}
++
++size_t ExelDisk::sector_offset(uint8_t track, uint8_t side, uint8_t sector) const {
++ if (track >= kTracks || side >= kSides || sector >= kSectors) return kImageSize;
++ return ((size_t(track) * kSides + side) * kSectors + sector) * kSectorSize;
++}
++
++void ExelDisk::finish(uint8_t status_bits) {
++ status_ = uint8_t((status_ & ~kBusy) | status_bits);
++ if (mounted()) status_ |= kMotor;
++ drq_ = false;
++ intrq_ = true;
++ op_ = Op::None;
++}
++
++uint8_t ExelDisk::start_read() {
++ latch_ = read_register();
++ return latch_;
++}
++
++uint8_t ExelDisk::read_register() {
++ switch (control_ & 0x03) {
++ case 0: {
++ // Lire l'état acquitte INTRQ. En type I, bit1 = index (on le fait battre), bit2 = piste 0.
++ intrq_ = false;
++ uint8_t s = status_;
++ if (type1_) {
++ s = uint8_t(s & ~(kDrq | kLost));
++ if (track_ == 0) s |= kLost;
++ if (((index_++) & 0x0f) == 0) s |= kDrq;
++ } else if (drq_) {
++ s |= kDrq;
++ }
++ return s;
++ }
++ case 1: return track_;
++ case 2: return sector_;
++ default:
++ if (op_ == Op::ReadSector || op_ == Op::ReadAddress) {
++ if (pos_ < buffer_.size()) data_ = buffer_[pos_++];
++ if (pos_ >= buffer_.size()) finish(0);
++ }
++ return data_;
++ }
++}
++
++void ExelDisk::write_register(uint8_t v) {
++ switch (control_ & 0x03) {
++ case 0: command(v); break;
++ case 1: track_ = v; break;
++ case 2: sector_ = v; break;
++ default:
++ data_ = v;
++ if (op_ == Op::WriteSector) {
++ if (pos_ < buffer_.size()) buffer_[pos_++] = v;
++ if (pos_ >= buffer_.size()) {
++ if (target_ + kSectorSize <= image_.size()) {
++ std::copy(buffer_.begin(), buffer_.end(), image_.begin() + long(target_));
++ dirty_ = true;
++ }
++ finish(0);
++ }
++ } else if (op_ == Op::WriteTrack) {
++ write_track_byte(v);
++ }
++ break;
++ }
++}
++
++void ExelDisk::command(uint8_t cmd) {
++ intrq_ = false;
++ const uint8_t side = (control_ & 0x80) ? 1 : 0;
++ // Type IV : interruption forcée (fin de l'ordre en cours).
++ if ((cmd & 0xf0) == 0xd0) {
++ op_ = Op::None;
++ drq_ = false;
++ status_ = uint8_t(status_ & ~(kBusy | kDrq));
++ if (cmd & 0x0f) intrq_ = true;
++ return;
++ }
++ if (!mounted()) {
++ type1_ = (cmd & 0x80) == 0;
++ status_ = kBusy;
++ finish(kRnf);
++ return;
++ }
++ if (!(cmd & 0x80)) {
++ // Type I : restore / seek / step. Tout se fait d'un coup.
++ type1_ = true;
++ status_ = kBusy;
++ switch (cmd >> 5) {
++ case 0:
++ if (cmd & 0x10) track_ = data_; // seek
++ else track_ = 0; // restore
++ break;
++ case 1: break; // step (direction inconnue : rien)
++ case 2: if (cmd & 0x10) track_ = uint8_t(track_ + 1); break; // step in
++ case 3: if (cmd & 0x10 && track_) track_--; break; // step out
++ }
++ if (track_ >= kTracks) track_ = uint8_t(kTracks - 1);
++ finish(kSpinUp);
++ return;
++ }
++ type1_ = false;
++ status_ = kBusy;
++ switch (cmd >> 4) {
++ case 0x8: case 0x9: { // read sector
++ const size_t off = sector_offset(track_, side, sector_);
++ if (off >= image_.size()) { finish(kRnf); return; }
++ buffer_.assign(image_.begin() + long(off), image_.begin() + long(off + kSectorSize));
++ pos_ = 0;
++ op_ = Op::ReadSector;
++ drq_ = true;
++ return;
++ }
++ case 0xa: case 0xb: { // write sector
++ target_ = sector_offset(track_, side, sector_);
++ if (target_ >= image_.size()) { finish(kRnf); return; }
++ buffer_.assign(kSectorSize, 0);
++ pos_ = 0;
++ op_ = Op::WriteSector;
++ drq_ = true;
++ return;
++ }
++ case 0xc: { // read address : piste, face, secteur, taille (1 = 256), CRC
++ buffer_ = {track_, side, sector_, 1, 0, 0};
++ pos_ = 0;
++ op_ = Op::ReadAddress;
++ drq_ = true;
++ return;
++ }
++ case 0xe: { // read track : pas utilisé par le ROM ; on rend la piste brute des secteurs
++ buffer_.clear();
++ for (size_t s = 0; s < kSectors; s++) {
++ const size_t off = sector_offset(track_, side, uint8_t(s));
++ buffer_.insert(buffer_.end(), image_.begin() + long(off), image_.begin() + long(off + kSectorSize));
++ }
++ pos_ = 0;
++ op_ = Op::ReadSector;
++ drq_ = true;
++ return;
++ }
++ case 0xf: // write track (formatage) : on décode les champs ID/données du flux
++ op_ = Op::WriteTrack;
++ wt_count_ = 0;
++ wt_state_ = 0;
++ wt_idx_ = 0;
++ drq_ = true;
++ return;
++ default:
++ finish(kRnf);
++ return;
++ }
++}
++
++void ExelDisk::write_track_byte(uint8_t v) {
++ // Flux de formatage du ROM : ... $F5 $F5 $F5 $FE piste face secteur $01 $F7 ... $F5 $F5 $F5 $FB
++ // données x256 $F7 ... ; $F5 = $A1 (sync), $F7 = CRC généré.
++ switch (wt_state_) {
++ case 0: // en attente d'un marqueur
++ if (v == 0xfe) { wt_state_ = 1; wt_idx_ = 0; }
++ else if (v == 0xfb) {
++ const size_t off = sector_offset(wt_id_[0], wt_id_[1] & 1, wt_id_[2]);
++ target_ = off;
++ buffer_.assign(kSectorSize, 0);
++ pos_ = 0;
++ wt_state_ = 2;
++ }
++ break;
++ case 1: // champ ID : piste, face, secteur, taille
++ wt_id_[wt_idx_++] = v;
++ if (wt_idx_ >= 4) wt_state_ = 0;
++ break;
++ case 2: // champ de données
++ if (pos_ < buffer_.size()) buffer_[pos_++] = v;
++ if (pos_ >= buffer_.size()) {
++ if (target_ + kSectorSize <= image_.size()) {
++ std::copy(buffer_.begin(), buffer_.end(), image_.begin() + long(target_));
++ dirty_ = true;
++ }
++ wt_state_ = 0;
++ }
++ break;
++ }
++ if (++wt_count_ >= kTrackBytes) finish(0); // impulsion d'index : fin de piste
++}
++
++} // namespace dsp
+diff --git a/src/drivers/computers/exelv_disk.h b/src/drivers/computers/exelv_disk.h
+new file mode 100644
+index 0000000..94dc47c
+--- /dev/null
++++ b/src/drivers/computers/exelv_disk.h
+@@ -0,0 +1,91 @@
++#pragma once
++
++#include <cstdint>
++#include <vector>
++
++namespace dsp {
++
++// Exeldisk : lecteur de disquettes 3"1/2 de l'EXL 100 (et de l'EXELTEL), piloté par un
++// WD1770 derrière quatre registres du fichier périphérique (d'après EXELDOS.equ du
++// devkit Jester et la ROM d'amorçage exeldisk.rom) :
++// P64 $0140 lecture : état du lecteur (bit0 disquette changée / absente, bit1 lecteur
++// sélectionné, bit2 prêt, bit3 INTRQ du 1770) ; écriture : verrou DSKCTL
++// (bits 0-1 registre du 1770, bit2/3 lecteur 1/2, bit4 $8000 = CRAM(1)/ROM
++// d'amorçage(0), bit5 $D000 = COS(1)/RAM DOS(0), bit6 $0200 = cartouche(1)/
++// RAM(0), bit7 face).
++// P65 $0141 écriture du registre du 1770 choisi par DSKCTL.
++// P66 $0142 lecture : lance la lecture du registre (verrouillée) ; écriture : double densité.
++// P67 $0143 lecture : rend l'octet verrouillé ; écriture : simple densité.
++// DRQ du 1770 arrive sur le bit 1 du port A du 7020.
++//
++// Image .fd (DCExel / webmo) : 80 pistes x 2 faces x 16 secteurs x 256 octets = 655 360
++// octets, à plat (piste, face, secteur), secteurs numérotés 0-15, double densité MFM.
++// Le ROM d'amorçage lit les secteurs 4-15 et 32-47 (le DOS) en $D000-$EFFF.
++class ExelDisk {
++public:
++ static constexpr size_t kTracks = 80;
++ static constexpr size_t kSides = 2;
++ static constexpr size_t kSectors = 16;
++ static constexpr size_t kSectorSize = 256;
++ static constexpr size_t kImageSize = kTracks * kSides * kSectors * kSectorSize;
++ // Longueur d'une piste en double densité (300 tr/min, 250 kbit/s) : fin de WRITE TRACK.
++ static constexpr int kTrackBytes = 6250;
++
++ void reset();
++ bool mount(std::vector<uint8_t> image);
++ bool mounted() const { return !image_.empty(); }
++ const std::vector<uint8_t>& image() const { return image_; }
++ bool take_dirty() {
++ const bool d = dirty_;
++ dirty_ = false;
++ return d;
++ }
++
++ // Verrou DSKCTL et ses lignes de sélection mémoire.
++ uint8_t control() const { return control_; }
++ void write_control(uint8_t v) { control_ = v; }
++ bool boot_rom_at_8000() const { return !(control_ & 0x10); }
++ bool cos_at_d000() const { return (control_ & 0x20) != 0; }
++ bool cart_at_0200() const { return (control_ & 0x40) != 0; }
++
++ uint8_t read_status_port() const; // P64
++ void write_register(uint8_t v); // P65
++ uint8_t start_read(); // P66 (verrouille le registre)
++ uint8_t finish_read() const { return latch_; } // P67
++ bool drq() const { return drq_; }
++ bool intrq() const { return intrq_; }
++
++private:
++ uint8_t read_register();
++ void command(uint8_t cmd);
++ size_t sector_offset(uint8_t track, uint8_t side, uint8_t sector) const;
++ void finish(uint8_t status_bits);
++ void write_track_byte(uint8_t v);
++
++ std::vector<uint8_t> image_;
++ bool dirty_ = false;
++ uint8_t control_ = 0;
++ uint8_t latch_ = 0xff;
++ // Registres du 1770.
++ uint8_t status_ = 0;
++ uint8_t track_ = 0;
++ uint8_t sector_ = 0;
++ uint8_t data_ = 0;
++ bool drq_ = false;
++ bool intrq_ = false;
++ bool type1_ = true; // dernier ordre de type I : bits d'état TRK0/INDEX
++ int index_ = 0; // impulsion d'index simulée (bit 1 des ordres de type I)
++ // Transfert en cours.
++ enum class Op { None, ReadSector, WriteSector, ReadAddress, WriteTrack };
++ Op op_ = Op::None;
++ std::vector<uint8_t> buffer_;
++ size_t pos_ = 0;
++ size_t target_ = 0; // offset dans l'image du secteur en écriture
++ // WRITE TRACK : analyse du flux (champs ID $FE et données $FB).
++ int wt_count_ = 0;
++ int wt_state_ = 0;
++ uint8_t wt_id_[4] = {0, 0, 0, 0};
++ int wt_idx_ = 0;
++};
++
++} // namespace dsp
diff --git a/src/main.cpp b/src/main.cpp
-index 6525a1c..3022c6e 100644
+index 6525a1c..7e7058d 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -15,6 +15,8 @@
@@ -324,7 +860,7 @@ index 6525a1c..3022c6e 100644
#include "core/embedded_media.h"
#include "frontend/sdl_app.h"
-@@ -358,6 +360,69 @@ int main(int argc, char** argv) { return run_dsp(argc, argv); }
+@@ -358,6 +360,93 @@ int main(int argc, char** argv) { return run_dsp(argc, argv); }
// The page starts the embedded media when its URL names no game.
extern "C" EMSCRIPTEN_KEEPALIVE int dsp_web_has_embedded() { return dsp_embedded_file_count > 0 ? 1 : 0; }
@@ -391,6 +927,30 @@ index 6525a1c..3022c6e 100644
+ return m && b64 && cram_b64_decode(b64, data) && m->cram_load(data) ? 1 : 0;
+}
+
++// Exeldisk : même mécanique que la CRAM (image .fd de 640 Ko en base64 côté page).
++extern "C" EMSCRIPTEN_KEEPALIVE int dsp_web_disk_present() {
++ dsp::Exelv* m = web_exelv();
++ return m && m->disk_present() ? (m->disk_mounted() ? 2 : 1) : 0;
++}
++
++extern "C" EMSCRIPTEN_KEEPALIVE int dsp_web_disk_dirty() {
++ dsp::Exelv* m = web_exelv();
++ return m && m->disk_take_dirty() ? 1 : 0;
++}
++
++extern "C" EMSCRIPTEN_KEEPALIVE const char* dsp_web_disk_save() {
++ static std::string b64;
++ dsp::Exelv* m = web_exelv();
++ b64 = m && m->disk_mounted() ? cram_b64_encode(m->disk_image()) : std::string();
++ return b64.c_str();
++}
++
++extern "C" EMSCRIPTEN_KEEPALIVE int dsp_web_disk_load(const char* b64) {
++ dsp::Exelv* m = web_exelv();
++ std::vector<uint8_t> data;
++ return m && b64 && cram_b64_decode(b64, data) && m->disk_mount(std::move(data)) ? 1 : 0;
++}
++
extern "C" EMSCRIPTEN_KEEPALIVE int dsp_web_start(const char* lines) {
static std::vector<std::string> args;
static std::vector<char*> argv;
@@ -406,6 +966,45 @@ index ab4d563..7f2f930 100644
+diff --git a/src/sound/tms5220.cpp b/src/sound/tms5220.cpp
+index e85a3d3..8ee3d1a 100644
+--- a/src/sound/tms5220.cpp
++++ b/src/sound/tms5220.cpp
+@@ -1,3 +1,4 @@
++#include <cstdio>
+ #include "sound/tms5220.h"
+
+ #include <algorithm>
+@@ -123,6 +124,9 @@ void Tms5220::chip_reset() {
+ void Tms5220::set_interrupt_state(bool state) {
+ if (state == irq_pin_) return;
+ irq_pin_ = state;
++#ifdef CRAM_DBG
++ std::fprintf(stderr, "T5220 int=%d\n", state ? 1 : 0);
++#endif
+ if (irq_cb_) irq_cb_(state);
+ }
+
+@@ -172,6 +176,9 @@ void Tms5220::data_write(uint8_t value) {
+ }
+
+ void Tms5220::process_command(uint8_t cmd) {
++#ifdef CRAM_DBG
++ std::fprintf(stderr, "T5220 cmd=%02x\n", unsigned(cmd));
++#endif
+ switch (cmd & 0x70) {
+ case 0x00:
+ case 0x20: // set rate on the TMS5220C, NOP otherwise
+@@ -320,6 +327,9 @@ int Tms5220::read_bits(int count) {
+
+ if (!ddis_) {
+ // No VSM attached: the bits float high, which decodes as a stop frame.
++#ifdef CRAM_DBG
++ { static int n = 0; if (n++ < 8) std::fprintf(stderr, "T5220 read_bits(%d) sans VSM\n", count); }
++#endif
+ return (1 << count) - 1;
+ }
+
diff --git a/src/video/tms3556.cpp b/src/video/tms3556.cpp
index 288aa7e..49487a3 100644
--- a/src/video/tms3556.cpp
diff --git a/roms/exeldisk.rom b/roms/exeldisk.rom
Binary files differ.
diff --git a/test/disk-boot.mjs b/test/disk-boot.mjs
@@ -0,0 +1,21 @@
+// Exeldisk : l'EXL 100 démarre sur une image .fd (ROM d'amorçage $8000, DOS chargé en $D000).
+// Captures à 6 s et 15 s, trace PC (build de debug) pour voir où ça bloque.
+// node test/disk-boot.mjs [image dans le MEMFS] [base-url] [préfixe]
+import { open, sleep } from './cdp.mjs';
+import { writeFileSync } from 'node:fs';
+
+const img = process.argv[2] ?? '/roms/disks/exeldos-1.4.fd';
+const base = process.argv[3] ?? 'http://127.0.0.1:8194';
+const prefix = process.argv[4] ?? '/tmp/disk';
+const url = base + '/?game=exl100&rom=/roms/exl100.zip&args=' + encodeURIComponent('--tape ' + img);
+const page = await open(url, { width: 900, height: 1100 });
+await sleep(6000);
+await page.screenshot(prefix + '-6s.png');
+await sleep(9000);
+await page.screenshot(prefix + '-15s.png');
+writeFileSync(prefix + '.log', page.logs.join('\n') + '\n');
+const fr = page.logs.map((l) => l.replace(/^\[\w+\]\s*/, '')).filter((l) => /^FR\d+ PC=/.test(l));
+console.log('statut :', await page.evaluate("document.getElementById('status').textContent"));
+console.log('trames :', fr.length, '; derniers PC :', fr.slice(-12).map((l) => l.split('PC=')[1].split(' ')[0]).join(' '));
+console.log('Captures :', prefix + '-6s.png', prefix + '-15s.png');
+page.close();
diff --git a/test/disk-cmd.mjs b/test/disk-cmd.mjs
@@ -0,0 +1,40 @@
+// Exeldisk : démarre sur une image .fd puis tape une commande EXELDOS (DIR par défaut) à l'invite.
+// Captures avant/après. Frappe CDP : 80 ms enfoncé / 400 ms d'écart (les lettres passent ainsi).
+// node test/disk-cmd.mjs [commande] [image MEMFS] [base-url] [préfixe]
+import { open, sleep } from './cdp.mjs';
+import { writeFileSync } from 'node:fs';
+
+const cmd = process.argv[2] ?? 'DIR';
+const img = process.argv[3] ?? '/roms/disks/exeldos-1.4.fd';
+const base = process.argv[4] ?? 'http://127.0.0.1:8194';
+const prefix = process.argv[5] ?? '/tmp/diskcmd';
+const url = base + '/?game=exl100&rom=/roms/exl100.zip' + (process.env.EXELMEM ? '&exelmem=1' : '') + '&args=' + encodeURIComponent('--tape ' + img);
+const page = await open(url, { width: 900, height: 1100 });
+await page.evaluate('location.reload()'); // sans ce rechargement, les touches CDP n'atteignent pas le canevas
+const ev = (type, k, code, vk, mods = 0) =>
+ page.send('Input.dispatchKeyEvent', { type, key: k, code, windowsVirtualKeyCode: vk, modifiers: mods });
+const key = async (k, code, vk, hold = 80, gap = 400) => { await ev('keyDown', k, code, vk); await sleep(hold); await ev('keyUp', k, code, vk); await sleep(gap); };
+const shifted = async (k, code, vk) => {
+ await ev('keyDown', 'Shift', 'ShiftLeft', 16); await sleep(80);
+ await ev('keyDown', k, code, vk, 8); await sleep(80); await ev('keyUp', k, code, vk, 8); await sleep(80);
+ await ev('keyUp', 'Shift', 'ShiftLeft', 16); await sleep(400);
+};
+const type = async (s) => {
+ for (const ch of s) {
+ if (ch === ':') await shifted(':', 'Period', 190);
+ else if (ch === '.') await key('.', 'Period', 190);
+ else if (ch === ' ') await key(' ', 'Space', 32);
+ else if (/[0-9]/.test(ch)) await key(ch, 'Digit' + ch, ch.charCodeAt(0));
+ else await key(ch.toLowerCase(), 'Key' + ch.toUpperCase(), ch.toUpperCase().charCodeAt(0));
+ }
+};
+await sleep(Number(process.env.BOOT_MS ?? 8000));
+await page.screenshot(prefix + '-0-boot.png');
+await page.evaluate("document.getElementById('canvas').focus(); document.activeElement && document.activeElement.id").then((r) => console.log('focus :', r));
+await type(cmd);
+await key('Enter', 'Enter', 13);
+await sleep(5000);
+await page.screenshot(prefix + '-1-cmd.png');
+writeFileSync(prefix + '.log', page.logs.join('\n') + '\n');
+console.log('Captures :', prefix + '-0-boot.png', prefix + '-1-cmd.png');
+page.close();
diff --git a/test/library-disk.mjs b/test/library-disk.mjs
@@ -0,0 +1,35 @@
+// Disquette choisie dans la bibliothèque (EXL 100) : clic sur l'entrée, amorçage sur l'Exeldisk, puis
+// commande tapée à l'invite EXELDOS (DIR par défaut) en événements clavier CDP. Captures à 10 s et après.
+// node test/library-disk.mjs [bout de titre] [commande] [base-url] [préfixe]
+import { open, sleep } from './cdp.mjs';
+
+const want = (process.argv[2] ?? 'exeldos').toLowerCase();
+const cmd = process.argv[3] ?? 'DIR';
+const base = process.argv[4] ?? 'http://127.0.0.1:8196';
+const prefix = process.argv[5] ?? '/tmp/lib-disk';
+const page = await open(base + '/', { width: 900, height: 1100 });
+await page.evaluate('location.reload()');
+await sleep(7000);
+const hit = await page.evaluate(`(() => {
+ const b = [...document.querySelectorAll('.pg')].find((b) => b.dataset.type === 'disk' && b.dataset.search.includes(${JSON.stringify(want)}));
+ if (b) b.click();
+ return b ? b.querySelector('.pg-t').textContent : null;
+})()`);
+console.log('entrée cliquée :', hit);
+await sleep(10000);
+await page.screenshot(prefix + '-boot.png');
+const ev = (type, k, code, vk, mods = 0) =>
+ page.send('Input.dispatchKeyEvent', { type, key: k, code, windowsVirtualKeyCode: vk, modifiers: mods });
+const key = async (k, code, vk) => { await ev('keyDown', k, code, vk); await sleep(80); await ev('keyUp', k, code, vk); await sleep(400); };
+for (const ch of cmd) {
+ if (ch === ' ') await key(' ', 'Space', 32);
+ else if (ch === '.') await key('.', 'Period', 190);
+ else if (/[0-9]/.test(ch)) await key(ch, 'Digit' + ch, ch.charCodeAt(0));
+ else await key(ch.toLowerCase(), 'Key' + ch.toUpperCase(), ch.toUpperCase().charCodeAt(0));
+}
+await key('Enter', 'Enter', 13);
+await sleep(6000);
+await page.screenshot(prefix + '-cmd.png');
+console.log('statut :', await page.evaluate("document.getElementById('status').textContent"));
+console.log('Captures :', prefix + '-boot.png', prefix + '-cmd.png');
+page.close();
diff --git a/web/shell.html.in b/web/shell.html.in
@@ -200,7 +200,7 @@
<button class="chip active" type="button" data-f="all">Tous</button>
<button class="chip" type="button" data-f="cart">Cartouches</button>
<button class="chip" type="button" data-f="tape">Cassettes</button>
- <button class="chip" type="button" data-f="cram">Exelmémoire</button>
+ <button class="chip" type="button" data-f="cram">Exelmémoire</button><button class="chip" type="button" data-f="disk">Disquettes</button>
<button class="chip" type="button" data-f="jeu">Jeux</button>
<button class="chip" type="button" data-f="application">Applications</button>
<button class="chip" type="button" data-f="educatif">Éducatif</button>
@@ -245,6 +245,12 @@
« Exelmémoire » dans la barre : comme la vraie cartouche, elle démarre alors à la place du logo. Son
contenu est conservé dans ce navigateur (comme le fichier <code>.cram</code> de DCExel) ; <i>Format</i>
l'efface.</p>
+ <h3>L'Exeldisk</h3>
+ <p>Le lecteur de disquettes 3"½ <b>Exeldisk</b> (contrôleur WD1770, 640 Ko par disquette) est émulé
+ d'après les équates du devkit et son ROM d'amorçage. Choisissez une disquette dans la bibliothèque ou
+ déposez une image <code>.fd</code> : l'EXL 100 amorce dessus (« exelvision BOOT V1.5 ») et charge
+ <b>EXELDOS</b> ; à l'invite <code>[A]</code>, <code>DIR</code>, <code>HELP</code>, <code>LOADRUN</code>…
+ Les écritures restent en mémoire le temps de la session.</p>
<h3>Le clavier infrarouge</h3>
<p>Le clavier et les manettes de l'EXL 100 étaient sans fil, reliés en infrarouge. Ici c'est votre clavier
d'ordinateur, aux positions de MAME. Cliquez d'abord dans l'écran pour lui donner le focus.</p>
@@ -455,6 +461,10 @@
} else if (value.startsWith('tape:')) {
args.push('--tape', '/roms/cartridges/exelbasic.bin', '--tape', '/roms/tapes/' + value.slice(5));
cassette = true;
+ } else if (value.startsWith('disk:')) {
+ // Disquette de la bibliothèque : l'Exeldisk est monté avec l'image (le pilote reconnaît .fd) et
+ // l'EXL 100 amorce dessus (BOOT V1.5 puis EXELDOS). Pas de persistance des écritures ici.
+ args.push('--tape', '/roms/disks/' + value.slice(5));
} else if (value.startsWith('cram:')) {
// Volume Exelmémoire de la bibliothèque. Programmes EXL 100 (ceux de webmo : code machine qui
// appelle ExelBASIC et le moniteur de l'EXL 100) : volume dans l'Exelmémoire, ExelBASIC, puis
@@ -560,7 +570,7 @@
b.className = 'pg';
b.dataset.value = value;
b.dataset.cat = cat || '';
- b.dataset.type = value.startsWith('cart:') ? 'cart' : value.startsWith('tape:') ? 'tape' : value.startsWith('cram:') ? 'cram' : '';
+ b.dataset.type = value.startsWith('cart:') ? 'cart' : value.startsWith('tape:') ? 'tape' : value.startsWith('cram:') ? 'cram' : value.startsWith('disk:') ? 'disk' : '';
b.dataset.search = (title + ' ' + source).toLowerCase();
const t = document.createElement('span'); t.className = 'pg-t'; t.textContent = title;
const s = document.createElement('span'); s.className = 'pg-s'; s.textContent = source;
@@ -584,6 +594,7 @@
addGroup('Cartouches', '/roms/cartridges/titles.json', 'cart:');
addGroup('Cassettes BASIC', '/roms/tapes/titles.json', 'tape:');
addGroup('Exelmémoire (volumes CROS64)', '/roms/cram/titles.json', 'cram:');
+ addGroup('Disquettes (Exeldisk, EXELDOS)', '/roms/disks/titles.json', 'disk:');
}
// Filtres combinés : texte (titre/source) + une puce (type ou catégorie), et masquage des en-têtes vides.
@@ -593,7 +604,7 @@
for (const el of programsEl.querySelectorAll('.pg')) {
const okText = q === '' || el.dataset.search.includes(q);
const okChip = chip === 'all' ||
- (chip === 'cart' || chip === 'tape' || chip === 'cram' ? el.dataset.type === chip : el.dataset.cat === chip);
+ (chip === 'cart' || chip === 'tape' || chip === 'cram' || chip === 'disk' ? el.dataset.type === chip : el.dataset.cat === chip);
el.classList.toggle('hide', !(okText && okChip));
}
let group = null, any = false;
@@ -676,7 +687,7 @@
// l'extension puis le contenu, puis lancé comme il faut : cassette = ExelBASIC + auto-chargement,
// cartouche = démarrage direct, image Exelmémoire (.cram 16 Ko, image 64 Ko DCExel, .bkp webmo = en-tête
// de 512 octets puis 64 blocs de 258 octets, 16 Ko à l'envers) = volume chargé puis rechargement,
- // disquette = étape à venir.
+ // disquette .fd = Exeldisk monté (--tape x.fd, le pilote reconnaît l'extension) et amorçage dessus.
const CART_SIZES = new Set([8192, 16384, 32256, 32768]);
function detectMedia(name, data) {
const ext = (name.toLowerCase().match(/\.([a-z0-9]+)$/) || [])[1] || '';
@@ -704,8 +715,12 @@
const data = new Uint8Array(await file.arrayBuffer());
const d = detectMedia(file.name, data);
const label = { cassette: 'Cassette', cartouche: 'Cartouche', exelmemoire: 'Exelmémoire', disquette: 'Disquette', inconnu: 'Fichier' }[d.kind];
- if (d.kind === 'disquette') {
- statusEl.textContent = label + ' détectée (' + d.why + ') : les disquettes Exeldisk ne sont pas encore prises en charge.';
+ if (d.kind === 'disquette' && d.why === 'image Teledisk') {
+ statusEl.textContent = label + ' détectée (' + d.why + ') : les images Teledisk (.td0) ne sont pas encore lues ; convertissez-la en .fd.';
+ return;
+ }
+ if (d.kind === 'disquette' && data.length !== 655360) {
+ statusEl.textContent = label + ' inattendue (' + data.length + ' octets) : une image .fd Exeldisk fait 655 360 octets (80 pistes, 2 faces, 16 secteurs de 256).';
return;
}
if (d.kind === 'inconnu') {
@@ -737,7 +752,8 @@
else args.push('--tape', path);
args.push(biosZip);
launch(args);
- statusEl.textContent = label + ' détectée (' + d.why + ') : ' + (d.kind === 'cassette' ? 'chargement dans ExelBASIC…' : 'démarrage…');
+ statusEl.textContent = label + ' détectée (' + d.why + ') : ' + (d.kind === 'cassette' ? 'chargement dans ExelBASIC…'
+ : d.kind === 'disquette' ? 'amorçage sur l’Exeldisk (la disquette doit porter EXELDOS)…' : 'démarrage…');
$('pick').textContent = '📄 ' + file.name;
if (d.kind === 'cassette') autoLoadCassette(token);
}