Compare commits

...

37 Commits

Author SHA1 Message Date
David Given
500fcde21b Merge. 2024-05-14 21:41:30 +02:00
David Given
eb363a4b2a Update Tartu documentation. 2024-05-14 21:40:50 +02:00
David Given
8a78e609b0 And fix everywhere else... 2024-05-13 23:41:37 +02:00
David Given
15c67b8cc1 Bash into workingness on OSX. 2024-05-13 23:27:53 +02:00
David Given
00e9c5a07f Add support for updating file metadata (only the SRA bits, really). 2024-05-13 21:44:58 +02:00
David Given
7643457374 Add support for renaming files. 2024-05-13 21:12:42 +02:00
David Given
78d5584e21 Add creeate, put and delete support to the CP/M filesystem driver. 2024-05-13 00:32:57 +02:00
David Given
1d1143a893 Merge from master. 2024-05-10 00:19:57 +02:00
David Given
91093e1304 Merge pull request #754 from davidgiven/greaseweazle
Correctly twiddle DTR on Linux/OSX when changing baud rates.
2024-05-01 19:11:57 +02:00
David Given
1175a06f3d Merge from master. 2024-05-01 16:23:59 +02:00
David Given
6e5abd1189 Merge from master. 2024-05-01 16:23:38 +02:00
David Given
34f97384e7 Merge pull request #753 from davidgiven/osx
Fix OSX build problems.
2024-05-01 16:23:09 +02:00
David Given
653a6a0189 Be more consistent about DTR toggling (needed to reset serial devices). 2024-05-01 12:54:22 +02:00
David Given
f0b1b61eac Merge pull request #749 from p-j-b/fix-hang-windows-adafruit-floppy-greaseweazle
Set DTR after calling SetCommState
2024-05-01 12:46:08 +02:00
David Given
c0fd121bdf Restore build script to normal. 2024-05-01 00:25:41 +02:00
David Given
b805b86ddb Fix truncate arg ordering because of stupid OSX. 2024-05-01 00:25:10 +02:00
David Given
654e7e750c Fix truncate arg ordering because of stupid OSX. 2024-05-01 00:25:10 +02:00
David Given
7501fcfe8b Looks like compiling protobuf files now requires access to the protobuf
libraries.
2024-05-01 00:18:18 +02:00
David Given
fdb7837e03 Looks like compiling protobuf files now requires access to the protobuf
libraries.
2024-05-01 00:18:18 +02:00
David Given
1c57cea483 Try and debug the OSX build failure. 2024-05-01 00:00:30 +02:00
David Given
0c8e8d4d69 Remember to mark the 40-track format as being such. 2024-04-30 23:09:45 +02:00
David Given
8876aae2cc Calculate gaps in bits, not bytes (more accurate). Pad the end of the track to
avoid weirdness reading the last sector.
2024-04-30 23:09:30 +02:00
David Given
3e053b32e2 Display a useful command to repeat a test if one fails. 2024-04-30 23:07:10 +02:00
David Given
0611728537 Don't try to change the build system just yet. 2024-04-30 21:32:58 +02:00
David Given
a84cf83ce5 Add a prototype Tartu encoder. 2024-04-30 00:56:26 +02:00
David Given
c064aa7862 Merge pull request #751 from davidgiven/tartu
Add support for the Tartu Palivere.
2024-04-23 22:21:05 +02:00
David Given
195f7126cc Update link. 2024-04-23 21:58:42 +02:00
David Given
50d466c9c1 Update Tartu documentation. 2024-04-23 21:56:42 +02:00
David Given
5763574634 Update documentation. 2024-04-21 01:18:17 +02:00
David Given
2da568b3e8 Update the Tartu documentation. 2024-04-21 00:35:39 +02:00
David Given
2732d9aec8 Get the Tartu checksums working, and hook up the CP/M filesystem code. 2024-04-21 00:17:11 +02:00
David Given
15d34aff15 Work-in-progress Tartu decoder. 2024-04-20 01:20:49 +02:00
David Given
af3e257c78 Add boilerplate for the Tartu. 2024-04-19 21:10:49 +02:00
p-j-b
c2248c7e4a Added CLRDTR and SETDTR to setBaudRate
Fixes hang in Windows with Adafruit Floppy GreaseWeazle
2024-04-02 13:05:29 +01:00
David Given
a7967b6dc3 More release script tweaks. 2024-03-31 22:50:55 +02:00
David Given
c1f47921e6 Adjust release script. 2024-03-31 22:38:21 +02:00
David Given
cda93d516b Merge pull request #748 from davidgiven/windows
Switch from MSYS builds to WSL/Fedora builds.
2024-03-31 22:31:36 +02:00
25 changed files with 1100 additions and 113 deletions

View File

@@ -33,20 +33,21 @@ jobs:
- uses: actions/checkout@v4
with:
repository: 'davidgiven/fluxengine'
path: 'fluxengine'
- name: run
run: |
wsl sh -c 'make BUILDTYPE=windows -j$(nproc)'
wsl sh -c 'cd fluxengine && make BUILDTYPE=windows -j$(nproc)'
- name: nsis
run: |
wsl sh -c 'strip fluxengine.exe -o fluxengine-stripped.exe'
wsl sh -c 'strip fluxengine-gui.exe -o fluxengine-gui-stripped.exe'
wsl sh -c 'makensis -v2 -nocd -dOUTFILE=fluxengine-installer.exe extras/windows-installer.nsi'
wsl sh -c 'cd fluxengine && strip fluxengine.exe -o fluxengine-stripped.exe'
wsl sh -c 'cd fluxengine && strip fluxengine-gui.exe -o fluxengine-gui-stripped.exe'
wsl sh -c 'cd fluxengine && makensis -v2 -nocd -dOUTFILE=fluxengine-installer.exe extras/windows-installer.nsi'
- name: zip
run: |
wsl sh -c 'zip -9 fluxengine-windows.zip fluxengine.exe fluxengine-gui.exe upgrade-flux-file.exe brother120tool.exe brother240tool.exe FluxEngine.cydsn/CortexM3/ARM_GCC_541/Release/FluxEngine.hex fluxengine-installer.exe'
wsl sh -c 'cd fluxengine && zip -9 fluxengine-windows.zip fluxengine.exe fluxengine-gui.exe upgrade-flux-file.exe brother120tool.exe brother240tool.exe FluxEngine.cydsn/CortexM3/ARM_GCC_541/Release/FluxEngine.hex fluxengine-installer.exe'
- name: date
run: |
@@ -57,6 +58,7 @@ jobs:
with:
tag-name: dev
force-branch: false
git-directory: 'fluxengine'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -75,8 +77,8 @@ jobs:
with:
name: Development build ${{ env.RELEASE_DATE }}
files: |
fluxengine.zip
fluxengine-installer.exe
fluxengine/fluxengine.zip
fluxengine/fluxengine-installer.exe
tag_name: dev
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -103,45 +103,46 @@ particular filesystem and can read (and sometimes write, support varies) files
directly from disks, flux files or disk images. Some formats have multiple
choices because they can store multiple types of file system.
<!-- FORMATSSTART -->
<!-- This section is automatically generated. Do not edit. -->
| Profile | Format | Read? | Write? | Filesystem? |
|:--------|:-------|:-----:|:------:|:------------|
| [`acornadfs`](doc/disk-acornadfs.md) | Acorn ADFS: BBC Micro, Archimedes | 🦖 | | |
| [`acorndfs`](doc/disk-acorndfs.md) | Acorn DFS: Acorn Atom, BBC Micro series | 🦄 | | ACORNDFS |
| [`aeslanier`](doc/disk-aeslanier.md) | AES Lanier "No Problem": 616kB 5.25" 77-track SSDD hard sectored | 🦖 | | |
| [`agat`](doc/disk-agat.md) | Agat: 840kB 5.25" 80-track DS | 🦖 | 🦖 | |
| [`amiga`](doc/disk-amiga.md) | Amiga: 880kB 3.5" DSDD | 🦄 | 🦄 | AMIGAFFS |
| [`ampro`](doc/disk-ampro.md) | Ampro Little Board: CP/M | 🦖 | | CPMFS |
| [`apple2`](doc/disk-apple2.md) | Apple II: Prodos, Appledos, and CP/M | 🦄 | 🦄 | APPLEDOS CPMFS PRODOS |
| [`atarist`](doc/disk-atarist.md) | Atari ST: Almost PC compatible | 🦄 | 🦄 | |
| [`bk`](doc/disk-bk.md) | BK: 800kB 5.25"/3.5" 80-track 10-sector DSDD | 🦖 | 🦖 | |
| [`brother`](doc/disk-brother.md) | Brother word processors: GCR family | 🦄 | 🦄 | BROTHER120 FATFS |
| [`commodore`](doc/disk-commodore.md) | Commodore: 1541, 1581, 8050 and variations | 🦄 | 🦄 | CBMFS |
| [`eco1`](doc/disk-eco1.md) | VDS Eco1: CP/M; 1210kB 77-track mixed format DSHD | 🦖 | | CPMFS |
| [`epsonpf10`](doc/disk-epsonpf10.md) | Epson PF-10: CP/M; 3.5" 40-track DSDD | 🦖 | | CPMFS |
| [`f85`](doc/disk-f85.md) | Durango F85: 461kB 5.25" 77-track SS | 🦖 | | |
| [`fb100`](doc/disk-fb100.md) | Brother FB-100: 100kB 3.5" 40-track SSSD | 🦖 | | |
| [`hplif`](doc/disk-hplif.md) | Hewlett-Packard LIF: a variety of disk formats used by HP | 🦄 | 🦄 | LIF |
| [`ibm`](doc/disk-ibm.md) | IBM PC: Generic PC 3.5"/5.25" disks | 🦄 | 🦄 | FATFS |
| [`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 | 🦄 | 🦄 | |
| [`psos`](doc/disk-psos.md) | pSOS: 800kB DSDD with PHILE | 🦄 | 🦄 | PHILE |
| [`rolandd20`](doc/disk-rolandd20.md) | Roland D20: 3.5" electronic synthesiser disks | 🦄 | 🦖 | ROLAND |
| [`rx50`](doc/disk-rx50.md) | Digital RX50: 400kB 5.25" 80-track 10-sector SSDD | 🦖 | 🦖 | |
| [`smaky6`](doc/disk-smaky6.md) | Smaky 6: 308kB 5.25" 77-track 16-sector SSDD, hard sectored | 🦖 | | SMAKY6 |
| [`tids990`](doc/disk-tids990.md) | Texas Instruments DS990: 1126kB 8" DSSD | 🦖 | 🦖 | |
| [`tiki`](doc/disk-tiki.md) | Tiki 100: CP/M | | | CPMFS |
| [`victor9k`](doc/disk-victor9k.md) | Victor 9000 / Sirius One: 1224kB 5.25" DSDD GCR | 🦖 | 🦖 | |
| [`zilogmcz`](doc/disk-zilogmcz.md) | Zilog MCZ: 320kB 8" 77-track SSSD hard-sectored | 🦖 | | ZDOS |
{: .datatable }
<!-- FORMATSSTART -->
<!-- This section is automatically generated. Do not edit. -->
| Profile | Format | Read? | Write? | Filesystem? |
|:--------|:-------|:-----:|:------:|:------------|
| [`acornadfs`](doc/disk-acornadfs.md) | Acorn ADFS: BBC Micro, Archimedes | 🦖 | | |
| [`acorndfs`](doc/disk-acorndfs.md) | Acorn DFS: Acorn Atom, BBC Micro series | 🦄 | | ACORNDFS |
| [`aeslanier`](doc/disk-aeslanier.md) | AES Lanier "No Problem": 616kB 5.25" 77-track SSDD hard sectored | 🦖 | | |
| [`agat`](doc/disk-agat.md) | Agat: 840kB 5.25" 80-track DS | 🦖 | 🦖 | |
| [`amiga`](doc/disk-amiga.md) | Amiga: 880kB 3.5" DSDD | 🦄 | 🦄 | AMIGAFFS |
| [`ampro`](doc/disk-ampro.md) | Ampro Little Board: CP/M | 🦖 | | CPMFS |
| [`apple2`](doc/disk-apple2.md) | Apple II: Prodos, Appledos, and CP/M | 🦄 | 🦄 | APPLEDOS CPMFS PRODOS |
| [`atarist`](doc/disk-atarist.md) | Atari ST: Almost PC compatible | 🦄 | 🦄 | |
| [`bk`](doc/disk-bk.md) | BK: 800kB 5.25"/3.5" 80-track 10-sector DSDD | 🦖 | 🦖 | |
| [`brother`](doc/disk-brother.md) | Brother word processors: GCR family | 🦄 | 🦄 | BROTHER120 FATFS |
| [`commodore`](doc/disk-commodore.md) | Commodore: 1541, 1581, 8050 and variations | 🦄 | 🦄 | CBMFS |
| [`eco1`](doc/disk-eco1.md) | VDS Eco1: CP/M; 1210kB 77-track mixed format DSHD | 🦖 | | CPMFS |
| [`epsonpf10`](doc/disk-epsonpf10.md) | Epson PF-10: CP/M; 3.5" 40-track DSDD | 🦖 | | CPMFS |
| [`f85`](doc/disk-f85.md) | Durango F85: 461kB 5.25" 77-track SS | 🦖 | | |
| [`fb100`](doc/disk-fb100.md) | Brother FB-100: 100kB 3.5" 40-track SSSD | 🦖 | | |
| [`hplif`](doc/disk-hplif.md) | Hewlett-Packard LIF: a variety of disk formats used by HP | 🦄 | 🦄 | LIF |
| [`ibm`](doc/disk-ibm.md) | IBM PC: Generic PC 3.5"/5.25" disks | 🦄 | 🦄 | FATFS |
| [`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 | 🦄 | 🦄 | |
| [`psos`](doc/disk-psos.md) | pSOS: 800kB DSDD with PHILE | 🦄 | 🦄 | PHILE |
| [`rolandd20`](doc/disk-rolandd20.md) | Roland D20: 3.5" electronic synthesiser disks | 🦄 | 🦖 | ROLAND |
| [`rx50`](doc/disk-rx50.md) | Digital RX50: 400kB 5.25" 80-track 10-sector SSDD | 🦖 | 🦖 | |
| [`smaky6`](doc/disk-smaky6.md) | Smaky 6: 308kB 5.25" 77-track 16-sector SSDD, hard sectored | 🦖 | | SMAKY6 |
| [`tartu`](doc/disk-tartu.md) | Tartu: The Palivere and variations | 🦄 | | CPMFS |
| [`tids990`](doc/disk-tids990.md) | Texas Instruments DS990: 1126kB 8" DSSD | 🦖 | 🦖 | |
| [`tiki`](doc/disk-tiki.md) | Tiki 100: CP/M | | | CPMFS |
| [`victor9k`](doc/disk-victor9k.md) | Victor 9000 / Sirius One: 1224kB 5.25" DSDD GCR | 🦖 | 🦖 | |
| [`zilogmcz`](doc/disk-zilogmcz.md) | Zilog MCZ: 320kB 8" 77-track SSSD hard-sectored | 🦖 | | ZDOS |
{: .datatable }
<!-- FORMATSEND -->
### Notes

View File

@@ -19,6 +19,7 @@ proto(
"./northstar/northstar.proto",
"./rolandd20/rolandd20.proto",
"./smaky6/smaky6.proto",
"./tartu/tartu.proto",
"./tids990/tids990.proto",
"./victor9k/victor9k.proto",
"./zilogmcz/zilogmcz.proto",

View File

@@ -1,6 +1,4 @@
syntax = "proto2";
import "lib/common.proto";
message Smaky6DecoderProto {}

84
arch/tartu/decoder.cc Normal file
View File

@@ -0,0 +1,84 @@
#include "lib/globals.h"
#include "lib/decoders/decoders.h"
#include "arch/tartu/tartu.h"
#include "lib/crc.h"
#include "lib/fluxmap.h"
#include "lib/decoders/fluxmapreader.h"
#include "lib/sector.h"
#include <string.h>
constexpr uint64_t HEADER_BITS = 0xaaaaaaaa44895554LL;
constexpr uint64_t DATA_BITS = 0xaaaaaaaa44895545LL;
static const FluxPattern HEADER_PATTERN(64, HEADER_BITS);
static const FluxPattern DATA_PATTERN(64, DATA_BITS);
const FluxMatchers ANY_RECORD_PATTERN {
&HEADER_PATTERN,
&DATA_PATTERN
};
class TartuDecoder : public Decoder
{
public:
TartuDecoder(const DecoderProto& config):
Decoder(config),
_config(config.tartu())
{
}
void beginTrack() override
{
}
nanoseconds_t advanceToNextRecord() override
{
return seekToPattern(ANY_RECORD_PATTERN);
}
void decodeSectorRecord() override
{
if (readRaw64() != HEADER_BITS)
return;
auto bits = readRawBits(16 * 4);
auto bytes = decodeFmMfm(bits).slice(0, 4);
ByteReader br(bytes);
uint8_t track = br.read_8();
_sector->logicalTrack = track >> 1;
_sector->logicalSide = track & 1;
br.skip(1); /* seems always to be 1 */
_sector->logicalSector = br.read_8();
uint8_t wantChecksum = br.read_8();
uint8_t gotChecksum = ~sumBytes(bytes.slice(0, 3));
if (wantChecksum == gotChecksum)
_sector->status = Sector::DATA_MISSING;
_sector->status = Sector::DATA_MISSING;
}
void decodeDataRecord() override
{
if (readRaw64() != DATA_BITS)
return;
const auto& bits = readRawBits(129 * 16);
const auto& bytes = decodeFmMfm(bits).slice(0, 129);
_sector->data = bytes.slice(0, 128);
uint8_t wantChecksum = bytes.reader().seek(128).read_8();
uint8_t gotChecksum = ~sumBytes(_sector->data);
_sector->status = (wantChecksum == gotChecksum) ? Sector::OK : Sector::BAD_CHECKSUM;
}
private:
const TartuDecoderProto& _config;
};
std::unique_ptr<Decoder> createTartuDecoder(const DecoderProto& config)
{
return std::unique_ptr<Decoder>(new TartuDecoder(config));
}

114
arch/tartu/encoder.cc Normal file
View File

@@ -0,0 +1,114 @@
#include "lib/globals.h"
#include "lib/decoders/decoders.h"
#include "lib/encoders/encoders.h"
#include "arch/tartu/tartu.h"
#include "lib/crc.h"
#include "lib/fluxmap.h"
#include "lib/sector.h"
#include <string.h>
class TartuEncoder : public Encoder
{
public:
TartuEncoder(const EncoderProto& config):
Encoder(config),
_config(config.tartu())
{
}
std::unique_ptr<Fluxmap> encode(std::shared_ptr<const TrackInfo>& trackInfo,
const std::vector<std::shared_ptr<const Sector>>& sectors,
const Image& image) override
{
_clockRateUs = _config.clock_period_us();
int bitsPerRevolution =
(_config.target_rotational_period_ms() * 1000.0) / _clockRateUs;
const auto& sector = *sectors.begin();
_bits.resize(bitsPerRevolution);
_cursor = 0;
writeFillerRawBitsUs(_config.gap1_us());
bool first = true;
for (const auto& sectorData : sectors)
{
if (!first)
writeFillerRawBitsUs(_config.gap4_us());
first = false;
writeSector(sectorData);
}
if (_cursor > _bits.size())
error("track data overrun");
writeFillerRawBitsUs(_config.target_rotational_period_ms() * 1000.0);
std::unique_ptr<Fluxmap> fluxmap(new Fluxmap);
fluxmap->appendBits(_bits,
calculatePhysicalClockPeriod(_clockRateUs * 1e3,
_config.target_rotational_period_ms() * 1e6));
return fluxmap;
}
private:
void writeBytes(const Bytes& bytes)
{
encodeMfm(_bits, _cursor, bytes, _lastBit);
}
void writeRawBits(uint64_t data, int width)
{
_cursor += width;
_lastBit = data & 1;
for (int i = 0; i < width; i++)
{
unsigned pos = _cursor - i - 1;
if (pos < _bits.size())
_bits[pos] = data & 1;
data >>= 1;
}
}
void writeFillerRawBitsUs(double us)
{
unsigned count = (us / _clockRateUs) / 2;
for (int i = 0; i < count; i++)
writeRawBits(0b10, 2);
};
void writeSector(const std::shared_ptr<const Sector>& sectorData)
{
writeRawBits(_config.header_marker(), 64);
{
Bytes bytes;
ByteWriter bw(bytes);
bw.write_8(
(sectorData->logicalTrack << 1) | sectorData->logicalSide);
bw.write_8(1);
bw.write_8(sectorData->logicalSector);
bw.write_8(~sumBytes(bytes.slice(0, 3)));
writeBytes(bytes);
}
writeFillerRawBitsUs(_config.gap3_us());
writeRawBits(_config.data_marker(), 64);
{
Bytes bytes;
ByteWriter bw(bytes);
bw.append(sectorData->data);
bw.write_8(~sumBytes(bytes.slice(0, sectorData->data.size())));
writeBytes(bytes);
}
}
private:
const TartuEncoderProto& _config;
double _clockRateUs;
std::vector<bool> _bits;
unsigned _cursor;
bool _lastBit;
};
std::unique_ptr<Encoder> createTartuEncoder(const EncoderProto& config)
{
return std::unique_ptr<Encoder>(new TartuEncoder(config));
}

8
arch/tartu/tartu.h Normal file
View File

@@ -0,0 +1,8 @@
#ifndef TARTU_H
#define TARTU_H
extern std::unique_ptr<Decoder> createTartuDecoder(const DecoderProto& config);
extern std::unique_ptr<Encoder> createTartuEncoder(const EncoderProto& config);
#endif

27
arch/tartu/tartu.proto Normal file
View File

@@ -0,0 +1,27 @@
syntax = "proto2";
import "lib/common.proto";
message TartuDecoderProto {}
message TartuEncoderProto {
optional double clock_period_us = 1
[ default = 2.0, (help) = "clock rate on the real device (for MFM)" ];
optional double target_rotational_period_ms = 2
[ default=200, (help) = "rotational period of target disk" ];
optional double gap1_us = 3
[ default = 1200,
(help) = "size of gap 1 (the post-index gap)" ];
optional double gap3_us = 4
[ default = 150,
(help) = "size of gap 3 (the pre-data gap)" ];
optional double gap4_us = 5
[ default = 180,
(help) = "size of gap 4 (the post-data or format gap)" ];
optional uint64 header_marker = 6
[ default = 0xaaaaaaaa44895554,
(help) = "64-bit raw bit pattern of header record marker" ];
optional uint64 data_marker = 7
[ default = 0xaaaaaaaa44895545,
(help) = "64-bit raw bit pattern of data record marker" ];
}

View File

@@ -143,6 +143,8 @@ cxxlibrary(
"./arch/northstar/encoder.cc",
"./arch/rolandd20/decoder.cc",
"./arch/smaky6/decoder.cc",
"./arch/tartu/decoder.cc",
"./arch/tartu/encoder.cc",
"./arch/tids990/decoder.cc",
"./arch/tids990/encoder.cc",
"./arch/victor9k/decoder.cc",
@@ -175,6 +177,7 @@ cxxlibrary(
"arch/micropolis/micropolis.h": "./arch/micropolis/micropolis.h",
"arch/c64/data_gcr.h": "./arch/c64/data_gcr.h",
"arch/c64/c64.h": "./arch/c64/c64.h",
"arch/tartu/tartu.h": "./arch/tartu/tartu.h",
"lib/a2r.h": "./lib/a2r.h",
"lib/bitmap.h": "./lib/bitmap.h",
"lib/bytes.h": "./lib/bytes.h",
@@ -278,6 +281,8 @@ else:
("mac", "scripts/mac800_test.textpb", "--800"),
("n88basic", "", ""),
("rx50", "", ""),
("tartu", "", "--390 40track_drive"),
("tartu", "", "--780"),
("tids990", "", ""),
("victor9k", "", "--612"),
("victor9k", "", "--1224"),

View File

@@ -11,7 +11,7 @@ from build.ab import (
)
from build.c import cxxlibrary
from types import SimpleNamespace
import build.pkg
from build.pkg import package
emit(
"""
@@ -22,6 +22,7 @@ endif
"""
)
lib = package(name="protobuf_lib", package="protobuf")
@Rule
def proto(self, name, srcs: Targets = None, deps: Targets = None):
@@ -67,6 +68,6 @@ def protocc(self, name, srcs: Targets = None, deps: Targets = None):
cxxlibrary(
replaces=self,
srcs=[r],
deps=targetswithtraitsof(deps, "cheaders"),
deps=targetswithtraitsof(deps, "cheaders") + [lib],
hdrs=headers,
)

48
doc/disk-tartu.md Normal file
View File

@@ -0,0 +1,48 @@
tartu
====
## The Palivere and variations
<!-- This file is automatically generated. Do not edit. -->
The Tartu Palivere is a 1988 Z80-based computer from Estonia. It is a CP/M
machine with 64kB of RAM, running off a 2MHz ꃣ0e30
clone; it operated off punched tape, cassette, external hard drive or floppy, and was notable as being the first ever computer with an Estonian keyboard.
<div style="text-align: center">
<img src="tartu.jpg" alt="The Tartu computer's developer Leo Humal working with one."/>
</div>
From a floppy disk perspective, it is interesting because the floppy drive
interface is almost entirely handled in software --- necessary at the time as
the usual floppy disk interface chip at the time, the ⎲fba5
of the WD1793), was hard to find. Instead, the floppy controller board was
implemented entirely using TTL logic. Despite this, the encoding is fairly high
density, using MFM and with up to 780kB on a double-sided 80 track disk.
<div style="text-align: center">
<img src="tartu-fdc.jpg" alt="The Tartu FDC with Soviet TTL logic chips."/>
</div>
FluxEngine supports reading Tartu disks with CP/M filesystem access.
## Options
- Format variants:
- `390`: 390kB 5.25" 40-track DSDD
- `780`: 780kB 5.25" 80-track DSDD
## Examples
To read:
- `fluxengine read tartu --390 -s drive:0 -o tartu.img`
- `fluxengine read tartu --780 -s drive:0 -o tartu.img`
To write:
- `fluxengine write tartu --390 -d drive:0 -i tartu.img`
- `fluxengine write tartu --780 -d drive:0 -i tartu.img`
## References
- [The Estonia Museum of Electronics](https://www.elektroonikamuuseum.ee/tartu_arvuti_lugu.html)

BIN
doc/tartu-fdc.jpg Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

BIN
doc/tartu.jpg Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -18,6 +18,7 @@
#include "arch/northstar/northstar.h"
#include "arch/rolandd20/rolandd20.h"
#include "arch/smaky6/smaky6.h"
#include "arch/tartu/tartu.h"
#include "arch/tids990/tids990.h"
#include "arch/victor9k/victor9k.h"
#include "arch/zilogmcz/zilogmcz.h"
@@ -51,6 +52,7 @@ std::unique_ptr<Decoder> Decoder::create(const DecoderProto& config)
{DecoderProto::kNorthstar, createNorthstarDecoder },
{DecoderProto::kRolandd20, createRolandD20Decoder },
{DecoderProto::kSmaky6, createSmaky6Decoder },
{DecoderProto::kTartu, createTartuDecoder },
{DecoderProto::kTids990, createTids990Decoder },
{DecoderProto::kVictor9K, createVictor9kDecoder },
{DecoderProto::kZilogmcz, createZilogMczDecoder },
@@ -89,7 +91,7 @@ std::shared_ptr<TrackDataFlux> Decoder::decodeToSectors(
Fluxmap::Position recordStart = fmr.tell();
_sector->clock = advanceToNextRecord();
if (fmr.eof() || !_sector->clock)
return _trackdata;
break;
/* Read the sector record. */
@@ -108,28 +110,26 @@ std::shared_ptr<TrackDataFlux> Decoder::decodeToSectors(
{
/* The data is in a separate record. */
for (;;)
_sector->headerStartTime = before.ns();
_sector->headerEndTime = after.ns();
_sector->clock = advanceToNextRecord();
if (fmr.eof() || !_sector->clock)
break;
before = fmr.tell();
decodeDataRecord();
after = fmr.tell();
if (_sector->status != Sector::DATA_MISSING)
{
_sector->position = before.bytes;
_sector->dataStartTime = before.ns();
_sector->dataEndTime = after.ns();
pushRecord(before, after);
}
else
{
_sector->headerStartTime = before.ns();
_sector->headerEndTime = after.ns();
_sector->clock = advanceToNextRecord();
if (fmr.eof() || !_sector->clock)
break;
before = fmr.tell();
decodeDataRecord();
after = fmr.tell();
if (_sector->status != Sector::DATA_MISSING)
{
_sector->position = before.bytes;
_sector->dataStartTime = before.ns();
_sector->dataEndTime = after.ns();
pushRecord(before, after);
break;
}
fmr.skipToEvent(F_BIT_PULSE);
resetFluxDecoder();
}
@@ -142,6 +142,8 @@ std::shared_ptr<TrackDataFlux> Decoder::decodeToSectors(
_trackdata->sectors.push_back(_sector);
}
}
return _trackdata;
}
void Decoder::pushRecord(

View File

@@ -15,13 +15,14 @@ import "arch/mx/mx.proto";
import "arch/northstar/northstar.proto";
import "arch/rolandd20/rolandd20.proto";
import "arch/smaky6/smaky6.proto";
import "arch/tartu/tartu.proto";
import "arch/tids990/tids990.proto";
import "arch/victor9k/victor9k.proto";
import "arch/zilogmcz/zilogmcz.proto";
import "lib/fluxsink/fluxsink.proto";
import "lib/common.proto";
//NEXT: 32
//NEXT: 33
message DecoderProto {
optional double pulse_debounce_threshold = 1 [default = 0.30,
(help) = "ignore pulses with intervals shorter than this, in fractions of a clock"];
@@ -50,6 +51,7 @@ message DecoderProto {
NorthstarDecoderProto northstar = 24;
RolandD20DecoderProto rolandd20 = 31;
Smaky6DecoderProto smaky6 = 30;
TartuDecoderProto tartu = 32;
Tids990DecoderProto tids990 = 16;
Victor9kDecoderProto victor9k = 17;
ZilogMczDecoderProto zilogmcz = 18;

View File

@@ -11,6 +11,7 @@
#include "arch/macintosh/macintosh.h"
#include "arch/micropolis/micropolis.h"
#include "arch/northstar/northstar.h"
#include "arch/tartu/tartu.h"
#include "arch/tids990/tids990.h"
#include "arch/victor9k/victor9k.h"
#include "lib/encoders/encoders.pb.h"
@@ -24,8 +25,8 @@ std::unique_ptr<Encoder> Encoder::create(const EncoderProto& config)
static const std::map<int,
std::function<std::unique_ptr<Encoder>(const EncoderProto&)>>
encoders = {
{EncoderProto::kAmiga, createAmigaEncoder },
{EncoderProto::kAgat, createAgatEncoder },
{EncoderProto::kAmiga, createAmigaEncoder },
{EncoderProto::kApple2, createApple2Encoder },
{EncoderProto::kBrother, createBrotherEncoder },
{EncoderProto::kC64, createCommodore64Encoder},
@@ -33,6 +34,7 @@ std::unique_ptr<Encoder> Encoder::create(const EncoderProto& config)
{EncoderProto::kMacintosh, createMacintoshEncoder },
{EncoderProto::kMicropolis, createMicropolisEncoder },
{EncoderProto::kNorthstar, createNorthstarEncoder },
{EncoderProto::kTartu, createTartuEncoder },
{EncoderProto::kTids990, createTids990Encoder },
{EncoderProto::kVictor9K, createVictor9kEncoder },
};

View File

@@ -9,6 +9,7 @@ import "arch/ibm/ibm.proto";
import "arch/macintosh/macintosh.proto";
import "arch/micropolis/micropolis.proto";
import "arch/northstar/northstar.proto";
import "arch/tartu/tartu.proto";
import "arch/tids990/tids990.proto";
import "arch/victor9k/victor9k.proto";
@@ -27,5 +28,6 @@ message EncoderProto
Victor9kEncoderProto victor9k = 11;
Apple2EncoderProto apple2 = 12;
AgatEncoderProto agat = 13;
TartuEncoderProto tartu = 14;
}
}

View File

@@ -42,11 +42,9 @@ public:
commtimeouts.ReadIntervalTimeout = 100;
SetCommTimeouts(_handle, &commtimeouts);
if (!EscapeCommFunction(_handle, CLRDTR))
error("Couldn't clear DTR: {}", get_last_error_string());
Sleep(200);
if (!EscapeCommFunction(_handle, SETDTR))
error("Couldn't set DTR: {}", get_last_error_string());
/* Toggle DTR to reset the device. */
toggleDtr();
PurgeComm(_handle,
PURGE_RXABORT | PURGE_RXCLEAR | PURGE_TXABORT | PURGE_TXCLEAR);
@@ -58,6 +56,15 @@ public:
}
public:
void toggleDtr() override
{
if (!EscapeCommFunction(_handle, CLRDTR))
error("Couldn't clear DTR: {}", get_last_error_string());
Sleep(200);
if (!EscapeCommFunction(_handle, SETDTR))
error("Couldn't set DTR: {}", get_last_error_string());
}
ssize_t readImpl(uint8_t* buffer, size_t len) override
{
DWORD rlen;
@@ -97,6 +104,8 @@ public:
.Parity = NOPARITY,
.StopBits = ONESTOPBIT};
SetCommState(_handle, &dcb);
toggleDtr();
}
private:
@@ -157,12 +166,7 @@ public:
/* Toggle DTR to reset the device. */
int flag = TIOCM_DTR;
if (ioctl(_fd, TIOCMBIC, &flag) == -1)
error("cannot clear DTR on serial port: {}", strerror(errno));
usleep(200000);
if (ioctl(_fd, TIOCMBIS, &flag) == -1)
error("cannot set DTR on serial port: {}", strerror(errno));
toggleDtr();
/* Flush pending input from a generic greaseweazel device */
tcsetattr(_fd, TCSAFLUSH, &t);
@@ -174,6 +178,16 @@ public:
}
public:
void toggleDtr() override
{
int flag = TIOCM_DTR;
if (ioctl(_fd, TIOCMBIC, &flag) == -1)
error("cannot clear DTR on serial port: {}", strerror(errno));
usleep(200000);
if (ioctl(_fd, TIOCMBIS, &flag) == -1)
error("cannot set DTR on serial port: {}", strerror(errno));
}
ssize_t readImpl(uint8_t* buffer, size_t len) override
{
ssize_t rlen = ::read(_fd, buffer, len);
@@ -198,6 +212,8 @@ public:
tcgetattr(_fd, &t);
cfsetspeed(&t, baudRate);
tcsetattr(_fd, TCSANOW, &t);
toggleDtr();
}
private:

View File

@@ -11,6 +11,7 @@ public:
virtual ssize_t readImpl(uint8_t* buffer, size_t len) = 0;
virtual ssize_t write(const uint8_t* buffer, size_t len) = 0;
virtual void setBaudRate(int baudRate) = 0;
virtual void toggleDtr() = 0;
void read(uint8_t* buffer, size_t len);
void read(Bytes& bytes);

View File

@@ -1,19 +1,24 @@
#include "lib/globals.h"
#include "lib/vfs/vfs.h"
#include "lib/config.pb.h"
#include <fmt/format.h>
#include <regex>
class CpmFsFilesystem : public Filesystem
class CpmFsFilesystem : public Filesystem, public HasBitmap, public HasMount
{
class Entry
{
public:
Entry(const Bytes& bytes, int map_entry_size)
Entry(const Bytes& bytes, int map_entry_size, unsigned index):
index(index)
{
if (bytes[0] == 0xe5)
deleted = true;
user = bytes[0] & 0x0f;
{
std::stringstream ss;
ss << (char)(user + '0') << ':';
for (int i = 1; i <= 8; i++)
{
@@ -64,13 +69,117 @@ class CpmFsFilesystem : public Filesystem
}
}
Bytes toBytes(int map_entry_size) const
{
Bytes bytes(32);
ByteWriter bw(bytes);
if (deleted)
{
for (int i = 0; i < 32; i++)
bw.write_8(0xe5);
}
else
{
bw.write_8(user);
/* Encode the filename. */
for (int i = 1; i < 12; i++)
bytes[i] = 0x20;
for (char c : filename)
{
if (islower(c))
throw BadPathException();
if (c == '.')
{
if (bw.pos >= 9)
throw BadPathException();
bw.seek(9);
continue;
}
if ((bw.pos == 9) || (bw.pos == 12))
throw BadPathException();
bw.write_8(c);
}
/* Set the mode. */
if (mode.find('R') != std::string::npos)
bytes[9] |= 0x80;
if (mode.find('S') != std::string::npos)
bytes[10] |= 0x80;
if (mode.find('A') != std::string::npos)
bytes[11] |= 0x80;
/* EX, S1, S2, RC */
bw.seek(12);
bw.write_8(extent & 0x1f); /* EX */
bw.write_8(0); /* S1 */
bw.write_8(extent >> 5); /* S2 */
bw.write_8(records); /* RC */
/* Allocation map. */
switch (map_entry_size)
{
case 1:
for (int i = 0; i < 16; i++)
bw.write_8(allocation_map[i]);
break;
case 2:
for (int i = 0; i < 8; i++)
bw.write_le16(allocation_map[i]);
break;
}
}
return bytes;
}
void changeFilename(const std::string& filename)
{
static std::regex FORMATTER("(?:(1?[0-9]):)?([^ .]+)\\.?([^ .]*)");
std::smatch results;
bool matched = std::regex_match(filename, results, FORMATTER);
if (!matched)
throw BadPathException();
std::string user = results[1];
std::string stem = results[2];
std::string ext = results[3];
if (stem.size() > 8)
throw BadPathException();
if (ext.size() > 3)
throw BadPathException();
this->user = std::stoi(user);
if (this->user > 15)
throw BadPathException();
if (ext.empty())
this->filename = stem;
else
this->filename = fmt::format("{}.{}", stem, ext);
}
std::string combinedFilename() const
{
return fmt::format("{}:{}", user, filename);
}
public:
unsigned index;
std::string filename;
std::string mode;
unsigned user;
unsigned extent;
unsigned records;
std::vector<unsigned> allocation_map;
bool deleted = false;
};
public:
@@ -83,7 +192,8 @@ public:
uint32_t capabilities() const override
{
return OP_GETFSDATA | OP_LIST | OP_GETFILE | OP_GETDIRENT;
return OP_GETFSDATA | OP_LIST | OP_GETFILE | OP_PUTFILE | OP_DELETE |
OP_GETDIRENT | OP_CREATE | OP_MOVE | OP_PUTATTRS;
}
std::map<std::string, std::string> getMetadata() override
@@ -94,7 +204,7 @@ public:
for (int d = 0; d < _config.dir_entries(); d++)
{
auto entry = getEntry(d);
if (!entry)
if (entry->deleted)
continue;
for (unsigned block : entry->allocation_map)
@@ -112,6 +222,17 @@ public:
return attributes;
}
void create(bool, const std::string&) override
{
auto& start = _config.filesystem_start();
_filesystemStart =
getOffsetOfSector(start.track(), start.side(), start.sector());
_sectorSize = getLogicalSectorSize(start.track(), start.side());
_directory = Bytes{0xe5} * (_config.dir_entries() * 32);
putCpmBlock(0, _directory);
}
FilesystemStatus check() override
{
return FS_OK;
@@ -127,15 +248,15 @@ public:
for (int d = 0; d < _config.dir_entries(); d++)
{
auto entry = getEntry(d);
if (!entry)
if (entry->deleted)
continue;
auto& dirent = map[entry->filename];
auto& dirent = map[entry->combinedFilename()];
if (!dirent)
{
dirent = std::make_unique<Dirent>();
dirent->path = {entry->filename};
dirent->filename = entry->filename;
dirent->filename = entry->combinedFilename();
dirent->path = {dirent->filename};
dirent->mode = entry->mode;
dirent->length = 0;
dirent->file_type = TYPE_FILE;
@@ -173,6 +294,42 @@ public:
throw FileNotFoundException();
}
void putMetadata(const Path& path,
const std::map<std::string, std::string>& metadata) override
{
mount();
if (path.size() != 1)
throw BadPathException();
/* Only updating MODE is supported. */
if (metadata.empty())
return;
if ((metadata.size() != 1) || (metadata.begin()->first != MODE))
throw UnimplementedFilesystemException();
auto mode = metadata.begin()->second;
/* Update all dirents corresponding to this file. */
bool found = false;
for (int d = 0; d < _config.dir_entries(); d++)
{
std::unique_ptr<Entry> entry = getEntry(d);
if (entry->deleted)
continue;
if (path[0] == entry->combinedFilename())
{
entry->mode = mode;
putEntry(entry);
found = true;
}
}
if (!found)
throw FileNotFoundException();
unmount();
}
Bytes getFile(const Path& path) override
{
mount();
@@ -190,9 +347,9 @@ public:
for (int d = 0; d < _config.dir_entries(); d++)
{
entry = getEntry(d);
if (!entry)
if (entry->deleted)
continue;
if (path[0] != entry->filename)
if (path[0] != entry->combinedFilename())
continue;
if (entry->extent < logicalExtent)
continue;
@@ -201,7 +358,7 @@ public:
break;
}
if (!entry)
if (entry->deleted)
{
if (logicalExtent == 0)
throw FileNotFoundException();
@@ -236,8 +393,160 @@ public:
return data;
}
private:
void mount()
public:
void putFile(const Path& path, const Bytes& bytes) override
{
mount();
if (path.size() != 1)
throw BadPathException();
/* Test to see if the file already exists. */
for (int d = 0; d < _config.dir_entries(); d++)
{
std::unique_ptr<Entry> entry = getEntry(d);
if (entry->deleted)
continue;
if (path[0] == entry->combinedFilename())
throw CannotWriteException();
}
/* Write blocks, one at a time. */
std::unique_ptr<Entry> entry;
ByteReader br(bytes);
while (!br.eof())
{
unsigned extent = br.pos / 0x4000;
Bytes block = br.read(_config.block_size());
/* Allocate a block and write it. */
auto bit = std::find(_bitmap.begin(), _bitmap.end(), false);
if (bit == _bitmap.end())
throw DiskFullException();
*bit = true;
unsigned blocknum = bit - _bitmap.begin();
putCpmBlock(blocknum, block);
/* Do we need a new directory entry? */
if (!entry ||
entry->allocation_map[std::size(entry->allocation_map) - 1])
{
if (entry)
{
entry->records = 0x80;
putEntry(entry);
}
entry.reset();
for (int d = 0; d < _config.dir_entries(); d++)
{
entry = getEntry(d);
if (entry->deleted)
break;
entry.reset();
}
if (!entry)
throw DiskFullException();
entry->deleted = false;
entry->changeFilename(path[0]);
entry->extent = extent;
entry->mode = "";
std::fill(entry->allocation_map.begin(),
entry->allocation_map.end(),
0);
}
/* Hook up the block in the allocation map. */
auto mit = std::find(
entry->allocation_map.begin(), entry->allocation_map.end(), 0);
*mit = blocknum;
}
if (entry)
{
entry->records = ((bytes.size() & 0x3fff) + 127) / 128;
putEntry(entry);
}
unmount();
}
void moveFile(const Path& oldPath, const Path& newPath) override
{
mount();
if ((oldPath.size() != 1) || (newPath.size() != 1))
throw BadPathException();
/* Check to make sure that the file exists, and that the new filename
* does not. */
bool found = false;
for (int d = 0; d < _config.dir_entries(); d++)
{
auto entry = getEntry(d);
if (entry->deleted)
continue;
auto filename = entry->combinedFilename();
if (filename == oldPath[0])
found = true;
if (filename == newPath[0])
throw CannotWriteException();
}
if (!found)
throw FileNotFoundException();
/* Now do the rename. */
for (int d = 0; d < _config.dir_entries(); d++)
{
auto entry = getEntry(d);
if (entry->deleted)
continue;
auto filename = entry->combinedFilename();
if (filename == oldPath[0])
{
entry->changeFilename(newPath[0]);
putEntry(entry);
}
}
unmount();
}
void deleteFile(const Path& path) override
{
mount();
if (path.size() != 1)
throw BadPathException();
/* Remove all dirents for this file. */
bool found = false;
for (int d = 0; d < _config.dir_entries(); d++)
{
auto entry = getEntry(d);
if (entry->deleted)
continue;
if (path[0] != entry->combinedFilename())
continue;
entry->deleted = true;
putEntry(entry);
found = true;
}
if (!found)
throw FileNotFoundException();
unmount();
}
public:
void mount() override
{
auto& start = _config.filesystem_start();
_filesystemStart =
@@ -268,26 +577,71 @@ private:
_blocksPerLogicalExtent = 16384 / _config.block_size();
_directory = getCpmBlock(0, _dirBlocks);
/* Create the allocation bitmap. */
_bitmap.clear();
_bitmap.resize(_filesystemBlocks);
for (int d = 0; d < _dirBlocks; d++)
_bitmap[d] = true;
for (int d = 0; d < _config.dir_entries(); d++)
{
std::unique_ptr<Entry> entry = getEntry(d);
if (entry->deleted)
continue;
for (unsigned block : entry->allocation_map)
{
if (block >= _filesystemBlocks)
throw BadFilesystemException();
if (block)
_bitmap[block] = true;
}
}
}
void unmount()
{
putCpmBlock(0, _directory);
}
private:
std::unique_ptr<Entry> getEntry(unsigned d)
{
auto bytes = _directory.slice(d * 32, 32);
if (bytes[0] == 0xe5)
return nullptr;
return std::make_unique<Entry>(bytes, _allocationMapSize);
return std::make_unique<Entry>(bytes, _allocationMapSize, d);
}
Bytes getCpmBlock(uint32_t number, uint32_t count = 1)
void putEntry(std::unique_ptr<Entry>& entry)
{
unsigned sector = number * _blockSectors;
ByteWriter bw(_directory);
bw.seek(entry->index * 32);
bw.append(entry->toBytes(_allocationMapSize));
}
unsigned computeSector(uint32_t block) const
{
unsigned sector = block * _blockSectors;
if (_config.has_padding())
sector += (sector / _config.padding().every()) *
_config.padding().amount();
return sector;
}
Bytes getCpmBlock(uint32_t block, uint32_t count = 1)
{
return getLogicalSector(
sector + _filesystemStart, _blockSectors * count);
computeSector(block) + _filesystemStart, _blockSectors * count);
}
void putCpmBlock(uint32_t block, const Bytes& bytes)
{
putLogicalSector(computeSector(block) + _filesystemStart, bytes);
}
public:
std::vector<bool> getBitmapForDebugging() override
{
return _bitmap;
}
private:
@@ -303,6 +657,7 @@ private:
uint32_t _blocksPerLogicalExtent;
int _allocationMapSize;
Bytes _directory;
std::vector<bool> _bitmap;
};
std::unique_ptr<Filesystem> Filesystem::createCpmFsFilesystem(

View File

@@ -277,4 +277,18 @@ public:
static std::unique_ptr<Filesystem> createFilesystemFromConfig();
};
/* Used for tests only. */
class HasBitmap
{
public:
virtual std::vector<bool> getBitmapForDebugging() = 0;
};
class HasMount
{
public:
virtual void mount() = 0;
};
#endif

View File

@@ -8,9 +8,9 @@ script="$4"
flags="$5"
dir="$6"
srcfile=$dir.$format.src.img
fluxfile=$dir.$format.$ext
destfile=$dir.$format.dest.img
srcfile=$dir/src.img
fluxfile=$dir/flux.$ext
destfile=$dir/dest.img
dd if=/dev/urandom of=$srcfile bs=1048576 count=2 2>&1
@@ -21,9 +21,11 @@ if [ ! -s $destfile ]; then
exit 1
fi
truncate $srcfile -r $destfile
truncate -r $destfile $srcfile
if ! cmp $srcfile $destfile; then
echo "Comparison failed!" >&2
echo "Run this to repeat:" >&2
echo "./scripts/encodedecodetest.sh \"$1\" \"$2\" \"$3\" \"$4\" \"$5\" \"$6\"" >&2
exit 1
fi
exit 0

View File

@@ -34,6 +34,7 @@ formats = [
"rx50",
"shugart_drive",
"smaky6",
"tartu",
"tids990",
"tiki",
"victor9k",

105
src/formats/tartu.textpb Normal file
View File

@@ -0,0 +1,105 @@
shortname: 'Tartu'
comment: 'The Palivere and variations'
read_support_status: UNICORN
write_support_status: DINOSAUR
documentation:
<<<
The Tartu Palivere is a 1988 Z80-based computer from Estonia. It is a CP/M
machine with 64kB of RAM, running off a 2MHz КP580ВМ80А, a Soviet Union 8080
clone; it operated off punched tape, cassette, external hard drive or floppy, and was notable as being the first ever computer with an Estonian keyboard.
<div style="text-align: center">
<img src="tartu.jpg" alt="The Tartu computer's developer Leo Humal working with one."/>
</div>
From a floppy disk perspective, it is interesting because the floppy drive
interface is almost entirely handled in software --- necessary at the time as
the usual floppy disk interface chip at the time, the КR1818VG93 (a Soviet clone
of the WD1793), was hard to find. Instead, the floppy controller board was
implemented entirely using TTL logic. Despite this, the encoding is fairly high
density, using MFM and with up to 780kB on a double-sided 80 track disk.
<div style="text-align: center">
<img src="tartu-fdc.jpg" alt="The Tartu FDC with Soviet TTL logic chips."/>
</div>
FluxEngine supports reading and writing Tartu disks with CP/M filesystem access.
>>>
documentation:
<<<
## References
- [The Estonia Museum of Electronics](https://www.elektroonikamuuseum.ee/tartu_arvuti_lugu.html)
>>>
image_writer {
filename: "tartu.img"
type: IMAGETYPE_IMG
}
layout {
layoutdata {
sector_size: 128
physical {
start_sector: 1
count: 39
}
filesystem {
start_sector: 1
count: 39
skew: 3
}
}
}
encoder {
tartu {}
}
decoder {
tartu {}
}
option_group {
comment: "$formats"
option {
name: "390"
comment: '390kB 5.25" 40-track DSDD'
config {
layout {
format_type: FORMATTYPE_40TRACK
tracks: 40
sides: 2
}
}
}
option {
name: "780"
comment: '780kB 5.25" 80-track DSDD'
config {
layout {
format_type: FORMATTYPE_80TRACK
tracks: 80
sides: 2
}
}
}
}
filesystem {
type: CPMFS
cpmfs {
filesystem_start {
track: 1
}
block_size: 2048
dir_entries: 128
}
}

View File

@@ -37,14 +37,50 @@ namespace
};
}
namespace snowhouse
{
template <>
struct Stringizer<std::vector<bool>>
{
static std::string ToString(const std::vector<bool>& vector)
{
std::stringstream stream;
stream << '{';
bool first = true;
for (const auto& item : vector)
{
if (!first)
stream << ", ";
stream << item;
first = false;
}
stream << '}';
return stream.str();
}
};
template <>
struct Stringizer<Bytes>
{
static std::string ToString(const Bytes& bytes)
{
std::stringstream stream;
stream << '\n';
hexdump(stream, bytes);
return stream.str();
}
};
}
static Bytes createDirent(const std::string& filename,
int extent,
int records,
const std::initializer_list<int> blocks)
const std::initializer_list<int> blocks,
int user = 0)
{
Bytes dirent;
ByteWriter bw(dirent);
bw.write_8(0);
bw.write_8(user);
bw.append(filename);
while (bw.pos != 12)
bw.write_8(' ');
@@ -69,6 +105,21 @@ static void setBlock(
sectors->put(block, 0, i)->data = data.slice(i * 256, 256);
}
static Bytes getBlock(
const std::shared_ptr<SectorInterface>& sectors, int block, int length)
{
Bytes bytes;
ByteWriter bw(bytes);
for (int i = 0; i < (length + 127) / 128; i++)
{
auto sector = sectors->get(block, 0, i);
bw.append(sector->data);
}
return bytes;
}
static void testPartialExtent()
{
auto sectors = std::make_shared<TestSectorInterface>();
@@ -113,6 +164,143 @@ static void testLogicalExtents()
AssertThat(data[0x4000 * 2], Equals(3));
}
static void testBitmap()
{
auto sectors = std::make_shared<TestSectorInterface>();
auto fs = Filesystem::createCpmFsFilesystem(
globalConfig()->filesystem(), sectors);
setBlock(sectors,
0,
createDirent("FILE", 1, 128, {1, 0, 0, 0, 0, 0, 0, 0, 2}) +
createDirent("FILE", 2, 128, {4}) + (blank_dirent * 62));
dynamic_cast<HasMount*>(fs.get())->mount();
std::vector<bool> bitmap =
dynamic_cast<HasBitmap*>(fs.get())->getBitmapForDebugging();
AssertThat(bitmap,
Equals(std::vector<bool>{
1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}));
}
#if 0
static void testPutGet()
{
auto sectors = std::make_shared<TestSectorInterface>();
auto fs = Filesystem::createCpmFsFilesystem(
globalConfig()->filesystem(), sectors);
fs->create(true, "volume");
fs->putFile(Path("0:FILE1"), Bytes{1, 2, 3, 4});
fs->putFile(Path("0:FILE2"), Bytes{5, 6, 7, 8});
dynamic_cast<HasMount*>(fs.get())->mount();
std::vector<bool> bitmap =
dynamic_cast<HasBitmap*>(fs.get())->getBitmapForDebugging();
AssertThat(bitmap,
Equals(std::vector<bool>{
1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}));
auto directory = getBlock(sectors, 0, 256).slice(0, 64);
AssertThat(directory,
Equals(createDirent("FILE1", 0, 1, {1}) +
createDirent("FILE2", 0, 1, {2})));
auto file1 = getBlock(sectors, 1, 8).slice(0, 8);
AssertThat(file1, Equals(Bytes{1, 2, 3, 4, 0, 0, 0, 0}));
auto file2 = getBlock(sectors, 2, 8).slice(0, 8);
AssertThat(file2, Equals(Bytes{5, 6, 7, 8, 0, 0, 0, 0}));
}
static void testPutBigFile()
{
auto sectors = std::make_shared<TestSectorInterface>();
auto fs = Filesystem::createCpmFsFilesystem(
globalConfig()->filesystem(), sectors);
fs->create(true, "volume");
Bytes filedata;
ByteWriter bw(filedata);
while (filedata.size() < 0x9000)
bw.write_le32(bw.pos);
fs->putFile(Path("0:BIGFILE"), filedata);
auto directory = getBlock(sectors, 0, 256).slice(0, 64);
AssertThat(directory,
Equals(createDirent("BIGFILE",
0,
0x80,
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}) +
createDirent("BIGFILE", 2, 0x20, {17, 18})));
}
static void testDelete()
{
auto sectors = std::make_shared<TestSectorInterface>();
auto fs = Filesystem::createCpmFsFilesystem(
globalConfig()->filesystem(), sectors);
fs->create(true, "volume");
fs->putFile(Path("0:FILE1"), Bytes{1, 2, 3, 4});
fs->putFile(Path("0:FILE2"), Bytes{5, 6, 7, 8});
fs->deleteFile(Path("0:FILE1"));
auto directory = getBlock(sectors, 0, 256).slice(0, 64);
AssertThat(directory,
Equals((Bytes{0xe5} * 32) + createDirent("FILE2", 0, 1, {2})));
}
static void testMove()
{
auto sectors = std::make_shared<TestSectorInterface>();
auto fs = Filesystem::createCpmFsFilesystem(
globalConfig()->filesystem(), sectors);
fs->create(true, "volume");
fs->putFile(Path("0:FILE1"), Bytes{0x55} * 0x9000);
fs->putFile(Path("0:FILE2"), Bytes{5, 6, 7, 8});
fs->moveFile(Path("0:FILE1"), Path("1:FILE3"));
auto directory = getBlock(sectors, 0, 256).slice(0, 32 * 3);
AssertThat(directory,
Equals(createDirent("FILE3",
0,
0x80,
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},
1) +
createDirent("FILE3", 2, 0x20, {17, 18}, 1) +
createDirent("FILE2", 0, 1, {19})));
}
static void testPutMetadata()
{
auto sectors = std::make_shared<TestSectorInterface>();
auto fs = Filesystem::createCpmFsFilesystem(
globalConfig()->filesystem(), sectors);
fs->create(true, "volume");
fs->putFile(Path("0:FILE1"), Bytes{0x55} * 0x9000);
fs->putFile(Path("0:FILE2"), Bytes{5, 6, 7, 8});
fs->putMetadata(Path("0:FILE1"),
std::map<std::string, std::string>{
{"mode", "SRA"}
});
auto directory = getBlock(sectors, 0, 256).slice(0, 32 * 3);
AssertThat(directory,
Equals(createDirent("FILE1 \xa0\xa0\xa0",
0,
0x80,
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}) +
createDirent("FILE1 \xa0\xa0\xa0", 2, 0x20, {17, 18}) +
createDirent("FILE2", 0, 1, {19})));
}
#endif
int main(void)
{
try
@@ -124,7 +312,7 @@ int main(void)
layout {
format_type: FORMATTYPE_80TRACK
tracks: 10
tracks: 20
sides: 1
layoutdata {
sector_size: 256
@@ -148,6 +336,14 @@ int main(void)
testPartialExtent();
testLogicalExtents();
#if 0
testBitmap();
testPutGet();
testPutBigFile();
testDelete();
testMove();
testPutMetadata();
#endif
}
catch (const ErrorException& e)
{