1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-07-07 08:28:57 +00:00

Merge pull request #865 from TomHarte/ADL

Electron: adds support for the ADL file format, and logic for AP6 and sideways RAM selection
This commit is contained in:
Thomas Harte 2021-01-31 09:37:24 -05:00 committed by GitHub
commit d54085c7fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 231 additions and 31 deletions

View File

@ -50,7 +50,10 @@ std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetDFSCatalogue(const std::s
new_file.name = name;
new_file.load_address = uint32_t(details->samples[0][file_offset] | (details->samples[0][file_offset+1] << 8) | ((details->samples[0][file_offset+6]&0x0c) << 14));
new_file.execution_address = uint32_t(details->samples[0][file_offset+2] | (details->samples[0][file_offset+3] << 8) | ((details->samples[0][file_offset+6]&0xc0) << 10));
new_file.is_protected = names->samples[0][file_offset + 7] & 0x80;
if(names->samples[0][file_offset + 7] & 0x80) {
// File is locked; it may not be altered or deleted.
new_file.flags |= File::Flags::Locked;
}
long data_length = long(details->samples[0][file_offset+4] | (details->samples[0][file_offset+5] << 8) | ((details->samples[0][file_offset+6]&0x30) << 12));
int start_sector = details->samples[0][file_offset+7] | ((details->samples[0][file_offset+6]&0x03) << 8);
@ -69,11 +72,16 @@ std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetDFSCatalogue(const std::s
new_file.data.insert(new_file.data.end(), next_sector->samples[0].begin(), next_sector->samples[0].begin() + length_from_sector);
data_length -= length_from_sector;
}
if(!data_length) catalogue->files.push_back(new_file);
if(!data_length) catalogue->files.push_back(std::move(new_file));
}
return catalogue;
}
/*
Primary resource used: "Acorn 8-Bit ADFS Filesystem Structure";
http://mdfs.net/Docs/Comp/Disk/Format/ADFS
*/
std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetADFSCatalogue(const std::shared_ptr<Storage::Disk::Disk> &disk) {
auto catalogue = std::make_unique<Catalogue>();
Storage::Encodings::MFM::Parser parser(true, disk);
@ -101,5 +109,73 @@ std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetADFSCatalogue(const std::
case 3: catalogue->bootOption = Catalogue::BootOption::ExecBOOT; break;
}
// Parse the root directory, at least.
for(std::size_t file_offset = 0x005; file_offset < 0x4cb; file_offset += 0x1a) {
// Obtain the name, which will be at most ten characters long, and will
// be terminated by either a NULL character or a \r.
char name[11];
std::size_t c = 0;
for(; c < 10; c++) {
const char next = root_directory[file_offset + c] & 0x7f;
name[c] = next;
if(next == '\0' || next == '\r') break;
}
name[c] = '\0';
// Skip if the name is empty.
if(name[0] == '\0') continue;
// Populate a file then.
File new_file;
new_file.name = name;
new_file.flags =
(root_directory[file_offset + 0] & 0x80 ? File::Flags::Readable : 0) |
(root_directory[file_offset + 1] & 0x80 ? File::Flags::Writable : 0) |
(root_directory[file_offset + 2] & 0x80 ? File::Flags::Locked : 0) |
(root_directory[file_offset + 3] & 0x80 ? File::Flags::IsDirectory : 0) |
(root_directory[file_offset + 4] & 0x80 ? File::Flags::ExecuteOnly : 0) |
(root_directory[file_offset + 5] & 0x80 ? File::Flags::PubliclyReadable : 0) |
(root_directory[file_offset + 6] & 0x80 ? File::Flags::PubliclyWritable : 0) |
(root_directory[file_offset + 7] & 0x80 ? File::Flags::PubliclyExecuteOnly : 0) |
(root_directory[file_offset + 8] & 0x80 ? File::Flags::IsPrivate : 0);
new_file.load_address =
(uint32_t(root_directory[file_offset + 0x0a]) << 0) |
(uint32_t(root_directory[file_offset + 0x0b]) << 8) |
(uint32_t(root_directory[file_offset + 0x0c]) << 16) |
(uint32_t(root_directory[file_offset + 0x0d]) << 24);
new_file.execution_address =
(uint32_t(root_directory[file_offset + 0x0e]) << 0) |
(uint32_t(root_directory[file_offset + 0x0f]) << 8) |
(uint32_t(root_directory[file_offset + 0x10]) << 16) |
(uint32_t(root_directory[file_offset + 0x11]) << 24);
new_file.sequence_number = root_directory[file_offset + 0x19];
const uint32_t size =
(uint32_t(root_directory[file_offset + 0x12]) << 0) |
(uint32_t(root_directory[file_offset + 0x13]) << 8) |
(uint32_t(root_directory[file_offset + 0x14]) << 16) |
(uint32_t(root_directory[file_offset + 0x15]) << 24);
uint32_t start_sector =
(uint32_t(root_directory[file_offset + 0x16]) << 0) |
(uint32_t(root_directory[file_offset + 0x17]) << 8) |
(uint32_t(root_directory[file_offset + 0x18]) << 16);
new_file.data.reserve(size);
while(new_file.data.size() < size) {
const Storage::Encodings::MFM::Sector *const sector = parser.get_sector(start_sector / (80 * 16), (start_sector / 16) % 80, start_sector % 16);
if(!sector) break;
const auto length_from_sector = std::min(size - new_file.data.size(), sector->samples[0].size());
new_file.data.insert(new_file.data.end(), sector->samples[0].begin(), sector->samples[0].begin() + ssize_t(length_from_sector));
++start_sector;
}
catalogue->files.push_back(std::move(new_file));
}
return catalogue;
}

View File

@ -19,19 +19,38 @@ namespace Acorn {
struct File {
std::string name;
uint32_t load_address;
uint32_t execution_address;
bool is_protected;
uint32_t load_address = 0;
uint32_t execution_address = 0;
enum Flags: uint16_t {
Readable = 1 << 0,
Writable = 1 << 1,
Locked = 1 << 2,
IsDirectory = 1 << 3,
ExecuteOnly = 1 << 4,
PubliclyReadable = 1 << 5,
PubliclyWritable = 1 << 6,
PubliclyExecuteOnly = 1 << 7,
IsPrivate = 1 << 8,
};
uint16_t flags = Flags::Readable | Flags::Readable | Flags::PubliclyReadable | Flags::PubliclyWritable;
uint8_t sequence_number = 0;
std::vector<uint8_t> data;
/// Describes a single chunk of file data; these relate to the tape and ROM filing system.
/// The File-level fields contain a 'definitive' version of the load and execution addresses,
/// but both of those filing systems also store them per chunk.
///
/// Similarly, the file-level data will contain the aggregate data of all chunks.
struct Chunk {
std::string name;
uint32_t load_address;
uint32_t execution_address;
uint16_t block_number;
uint16_t block_length;
uint8_t block_flag;
uint32_t next_address;
uint32_t load_address = 0;
uint32_t execution_address = 0;
uint16_t block_number = 0;
uint16_t block_length = 0;
uint32_t next_address = 0;
uint8_t block_flag = 0;
bool header_crc_matched;
bool data_crc_matched;

View File

@ -12,6 +12,8 @@
#include "Tape.hpp"
#include "Target.hpp"
#include <algorithm>
using namespace Analyser::Static::Acorn;
static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>>
@ -77,8 +79,8 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &me
if(!files.empty()) {
bool is_basic = true;
// protected files are always for *RUNning only
if(files.front().is_protected) is_basic = false;
// If a file is execute-only, that means *RUN.
if(files.front().flags & File::Flags::ExecuteOnly) is_basic = false;
// check also for a continuous threading of BASIC lines; if none then this probably isn't BASIC code,
// so that's also justification to *RUN
@ -108,15 +110,37 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &me
dfs_catalogue = GetDFSCatalogue(disk);
if(dfs_catalogue == nullptr) adfs_catalogue = GetADFSCatalogue(disk);
if(dfs_catalogue || adfs_catalogue) {
// Accept the disk and determine whether DFS or ADFS ROMs are implied.
target->media.disks = media.disks;
target->has_dfs = !!dfs_catalogue;
target->has_adfs = !!adfs_catalogue;
target->has_dfs = bool(dfs_catalogue);
target->has_adfs = bool(adfs_catalogue);
// Check whether a simple shift+break will do for loading this disk.
Catalogue::BootOption bootOption = (dfs_catalogue ?: adfs_catalogue)->bootOption;
if(bootOption != Catalogue::BootOption::None)
if(bootOption != Catalogue::BootOption::None) {
target->should_shift_restart = true;
else
} else {
target->loading_command = "*CAT\n";
}
// Check whether adding the AP6 ROM is justified.
// For now this is an incredibly dense text search;
// if any of the commands that aren't usually present
// on a stock Electron are here, add the AP6 ROM and
// some sideways RAM such that the SR commands are useful.
for(const auto &file: dfs_catalogue ? dfs_catalogue->files : adfs_catalogue->files) {
for(const auto &command: {
"AQRPAGE", "BUILD", "DUMP", "FORMAT", "INSERT", "LANG", "LIST", "LOADROM",
"LOCK", "LROMS", "RLOAD", "ROMS", "RSAVE", "SAVEROM", "SRLOAD", "SRPAGE",
"SRUNLOCK", "SRWIPE", "TUBE", "TYPE", "UNLOCK", "UNPLUG", "UROMS",
"VERIFY", "ZERO"
}) {
if(std::search(file.data.begin(), file.data.end(), command, command+strlen(command)) != file.data.end()) {
target->has_ap6_rom = true;
target->has_sideways_ram = true;
}
}
}
}
}

View File

@ -109,7 +109,12 @@ static std::unique_ptr<File> GetNextFile(std::deque<File::Chunk> &chunks) {
file->name = file->chunks.front().name;
file->load_address = file->chunks.front().load_address;
file->execution_address = file->chunks.front().execution_address;
file->is_protected = !!(file->chunks.back().block_flag & 0x01); // I think the last flags are the ones that count; TODO: check.
// I think the final chunk's flags are the ones that count; TODO: check.
if(file->chunks.back().block_flag & 0x01) {
// File is locked, which in more generalised terms means it is
// for execution only.
file->flags |= File::Flags::ExecuteOnly;
}
// copy all data into a single big block
for(File::Chunk chunk : file->chunks) {

View File

@ -20,6 +20,8 @@ namespace Acorn {
struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl<Target> {
bool has_adfs = false;
bool has_dfs = false;
bool has_ap6_rom = false;
bool has_sideways_ram = false;
bool should_shift_restart = false;
std::string loading_command;
@ -27,6 +29,8 @@ struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl<
if(needs_declare()) {
DeclareField(has_adfs);
DeclareField(has_dfs);
DeclareField(has_ap6_rom);
DeclareField(has_sideways_ram);
}
}
};

View File

@ -98,6 +98,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
Format("81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // 81
Format("a26", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Atari2600) // A26
Format("adf", result.disks, Disk::DiskImageHolder<Storage::Disk::AcornADF>, TargetPlatform::Acorn) // ADF
Format("adl", result.disks, Disk::DiskImageHolder<Storage::Disk::AcornADF>, TargetPlatform::Acorn) // ADL
Format("bin", result.cartridges, Cartridge::BinaryDump, TargetPlatform::AllCartridge) // BIN (cartridge dump)
Format("cas", result.tapes, Tape::CAS, TargetPlatform::MSX) // CAS
Format("cdt", result.tapes, Tape::TZX, TargetPlatform::AmstradCPC) // CDT

View File

@ -72,6 +72,10 @@ class ConcreteMachine:
if(target.has_dfs) {
required_roms.emplace_back(machine_name, "the 1770 DFS ROM", "DFS-1770-2.20.rom", 16*1024, 0xf3dc9bc5);
}
const size_t ap6_rom_position = required_roms.size();
if(target.has_ap6_rom) {
required_roms.emplace_back(machine_name, "the 8kb Advanced Plus 6 ROM", "AP6v133.rom", 8*1024, 0xe0013cfc);
}
const auto roms = rom_fetcher(required_roms);
for(const auto &rom: roms) {
@ -82,6 +86,15 @@ class ConcreteMachine:
set_rom(ROM::BASIC, *roms[0], false);
set_rom(ROM::OS, *roms[1], false);
/*
ROM slot mapping applied:
* the keyboard and BASIC ROMs occupy slots 8, 9, 10 and 11;
* the DFS, if in use, occupies slot 1;
* the ADFS, if in use, occupies slots 4 and 5;
* the AP6, if in use, occupies slot 15; and
* if sideways RAM was asked for, all otherwise unused slots are populated with sideways RAM.
*/
if(target.has_dfs || target.has_adfs) {
plus3_ = std::make_unique<Plus3>();
@ -94,6 +107,18 @@ class ConcreteMachine:
}
}
if(target.has_ap6_rom) {
set_rom(ROM::Slot15, *roms[ap6_rom_position], true);
}
if(target.has_sideways_ram) {
for(int c = 0; c < 16; c++) {
if(rom_inserted_[c]) continue;
if(c >= int(ROM::Keyboard) && c < int(ROM::BASIC)+1) continue;
set_sideways_ram(ROM(c));
}
}
insert_media(target.media);
if(!target.loading_command.empty()) {
@ -517,8 +542,20 @@ class ConcreteMachine:
rom_ptr += size_to_copy;
}
if(int(slot) < 16)
if(int(slot) < 16) {
rom_inserted_[int(slot)] = true;
}
}
/*!
Enables @c slot as sideways RAM; ensures that it does not currently contain a valid ROM signature.
*/
void set_sideways_ram(ROM slot) {
std::memset(roms_[int(slot)], 0xff, 16*1024);
if(int(slot) < 16) {
rom_inserted_[int(slot)] = true;
rom_write_masks_[int(slot)] = true;
}
}
// MARK: - Work deferral updates.

View File

@ -4,13 +4,41 @@ Expected files:
basic.rom
os.rom
plus1.rom
DFS-1770-2.20.rom
ADFS-E00_1.rom
DFS-1770-2.20.rom — used only if the user opens a DFS disk image
ADFS-E00_1.rom — used only if the user opens an ADFS disk image
ADFS-E00_2.rom
AP6v133.rom — used only if the user opens a disk image that makes use of any of the commands given below.
Likely to be desired in the future:
Possibly to be desired in the future:
* adfs.rom
* ElectronExpansionRomPresAP2-v1.23.rom
* os300.rom
adfs.rom
ElectronExpansionRomPresAP2-v1.23.rom
os300.rom
Commands that trigger a request for the AP6v133 ROM:
* *AQRPAGE
* *BUILD
* *DUMP
* *FORMAT
* *INSERT
* *LANG
* *LIST
* *LOADROM
* *LOCK
* *LROMS
* *RLOAD
* *ROMS
* *RSAVE
* *SAVEROM
* *SRLOAD
* *SRLOCK
* *SRPAGE
* *SRSAVE
* *SRUNLOCK
* *SRWIPE
* *TUBE
* *TYPE
* *UNLOCK
* *UNPLUG
* *UROMS
* *VERIFY
* *ZERO

View File

@ -18,12 +18,13 @@ namespace {
using namespace Storage::Disk;
AcornADF::AcornADF(const std::string &file_name) : MFMSectorDump(file_name) {
// very loose validation: the file needs to be a multiple of 256 bytes
// and not ungainly large
// Check that the disk image contains a whole number of sector.
if(file_.stats().st_size % off_t(128 << sector_size)) throw Error::InvalidFormat;
// Check that the disk image is at least large enough to hold an ADFS catalogue.
if(file_.stats().st_size < 7 * off_t(128 << sector_size)) throw Error::InvalidFormat;
// check that the initial directory's 'Hugo's are present
// Check that the initial directory's 'Hugo's are present.
file_.seek(513, SEEK_SET);
uint8_t bytes[4];
file_.read(bytes, 4);
@ -33,6 +34,10 @@ AcornADF::AcornADF(const std::string &file_name) : MFMSectorDump(file_name) {
file_.read(bytes, 4);
if(bytes[0] != 'H' || bytes[1] != 'u' || bytes[2] != 'g' || bytes[3] != 'o') throw Error::InvalidFormat;
// Pick a number of heads; treat this image as double sided if it's too large to be single-sided.
head_count_ = 1 + (file_.stats().st_size > sectors_per_track * off_t(128 << sector_size) * 80);
// Announce disk geometry.
set_geometry(sectors_per_track, sector_size, 0, true);
}
@ -41,9 +46,9 @@ HeadPosition AcornADF::get_maximum_head_position() {
}
int AcornADF::get_head_count() {
return 1;
return head_count_;
}
long AcornADF::get_file_offset_for_position(Track::Address address) {
return address.position.as_int() * (128 << sector_size) * sectors_per_track;
return (address.position.as_int() * head_count_ + address.head) * (128 << sector_size) * sectors_per_track;
}

View File

@ -34,6 +34,7 @@ class AcornADF: public MFMSectorDump {
private:
long get_file_offset_for_position(Track::Address address) final;
int head_count_ = 1;
};
}