Two decoders are added: 1) floppy_flux: Generates a raw bitstream from the floppy drive's flux pulse output (RDATA pin).
2) floppy_ibm_pc: Decodes the MFM-encoded raw bitstream, annotates all the fields, and provides an output stream containing each sector's data. Using these two decoders coupled with an ASIX Sigma2, I have captured all tracks of a Fedora 1.0 boot disk, decoded all sectors, and generated a complete floppy disk image file that was able to boot under qemu. I expect to publish the scripts that perform data capture and perform image reconstitution in due course. --- decoders/floppy_flux/__init__.py | 32 ++++ decoders/floppy_flux/pd.py | 76 ++++++++ decoders/floppy_ibm_pc/__init__.py | 37 ++++ decoders/floppy_ibm_pc/pd.py | 273 +++++++++++++++++++++++++++++ 4 files changed, 418 insertions(+) create mode 100644 decoders/floppy_flux/__init__.py create mode 100644 decoders/floppy_flux/pd.py create mode 100644 decoders/floppy_ibm_pc/__init__.py create mode 100644 decoders/floppy_ibm_pc/pd.py diff --git a/decoders/floppy_flux/__init__.py b/decoders/floppy_flux/__init__.py new file mode 100644 index 000000000000..87126f7cf74e --- /dev/null +++ b/decoders/floppy_flux/__init__.py @@ -0,0 +1,32 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2019 Stephen Warren <s-sig...@wwwdotorg.org> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, see <http://www.gnu.org/licenses/>. +## + +''' +Many floppy disk drives emit a negative pulse whenever there is a change in +magnetic flux under the disk head. Each time unit either contains or does not +contain a pulse (or flux transition). This yields a series of 1 and 0 bits. + +This decoder extracts series of (FM-/MFM-encoded) bits from the series of flux +transitions. + +Further decoders may interpret this bit series as FM or MFM encoded data, and +further derive the encoded data. +''' + +from .pd import Decoder diff --git a/decoders/floppy_flux/pd.py b/decoders/floppy_flux/pd.py new file mode 100644 index 000000000000..525b3c6ec520 --- /dev/null +++ b/decoders/floppy_flux/pd.py @@ -0,0 +1,76 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2019 Stephen Warren <s-sig...@wwwdotorg.org> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, see <http://www.gnu.org/licenses/>. +## + +import sigrokdecode as srd + +class Decoder(srd.Decoder): + api_version = 3 + id = 'floppy_flux' + name = 'Floppy Flux' + longname = 'Floppy magnetic flux transitions' + desc = 'Floppy disk magnetic flux transitions to MFM' + license = 'gplv2+' + inputs = ['logic'] + outputs = ['mfm_raw'] + channels = ( + {'id': 'flux', 'name': 'FLUX', 'desc': 'Flux pulses'}, + ) + options = ( + {'id': 'frequency', 'desc': 'Bit frequency', 'default': 1000000}, + ) + annotations = ( + ('bits', 'Raw bits'), + ('labels', 'Labels'), + ) + annotation_rows = ( + ('bits', 'Raw bits', (0, )), + ('labels', 'Labels', (1, )), + ) + + def __init__(self): + self.reset() + + def reset(self): + pass + + def metadata(self, key, value): + if key == srd.SRD_CONF_SAMPLERATE: + self.samplerate = value + self.samples_per_tick = int(self.samplerate / float(self.options['frequency'])) + + def start(self): + self.out_ann = self.register(srd.OUTPUT_ANN) + self.out_python = self.register(srd.OUTPUT_PYTHON) + + def decode(self): + self.wait({0: 'f'}) + prev_edge = self.samplenum + while True: + self.wait({0: 'f'}) + this_edge = self.samplenum + periods = int(round((this_edge - prev_edge) / self.samples_per_tick)) + start = prev_edge + for period in range(periods): + end = min(start + self.samples_per_tick, this_edge) + self.put(start, end, self.out_ann, [1, ['period', ]]) + val = 1 if (period == 0) else 0 + self.put(start, end, self.out_ann, [0, [str(val), ]]) + self.put(start, end, self.out_python, val) + start = end + prev_edge = this_edge diff --git a/decoders/floppy_ibm_pc/__init__.py b/decoders/floppy_ibm_pc/__init__.py new file mode 100644 index 000000000000..fed470313d3f --- /dev/null +++ b/decoders/floppy_ibm_pc/__init__.py @@ -0,0 +1,37 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2019 Stephen Warren <s-sig...@wwwdotorg.org> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, see <http://www.gnu.org/licenses/>. +## + +''' +IBM PC floppy disks store many sectors of 512 bytes, each located in a specific +track/cylinder, written by a certain head or side of the disk. Each sector is +encoded as a sequence of (address mark, sector data), where the address mark is +represented as the series of fields (sync bytes, address mark, cylinder number, +head number, sector number, sector size, CRC) and sector data is represented as +the series of fields (sync bytes, address mark, sector data, CRC). Finally, the +data is MFM encoded and written to the disk as a series of flux transitions. + +This decoder extracts and annotates all the data structures mentioned above, and +additionally provides Python output representating each sector's address and +data fields. This output is sent to both the Python object output stream for use +by further protocol decoders, and to the binary output stream (which provides a +textual representation of the Python data) to allow applications to stream data +out from sigrok-cli -B. +''' + +from .pd import Decoder diff --git a/decoders/floppy_ibm_pc/pd.py b/decoders/floppy_ibm_pc/pd.py new file mode 100644 index 000000000000..e7bcf453ae05 --- /dev/null +++ b/decoders/floppy_ibm_pc/pd.py @@ -0,0 +1,273 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2019 Stephen Warren <s-sig...@wwwdotorg.org> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, see <http://www.gnu.org/licenses/>. +## + +from abc import ABCMeta, abstractmethod +import sigrokdecode as srd + +def calc_crc16(data): + poly = 0x1021 + crc = 0xffff + for d in data: + d <<= 9 + for _ in range(8): + crc <<= 1 + if (crc ^ d) & 0x10000: + crc ^= poly + d <<= 1 + return crc & 0xffff + +class SyncDetector(object): + def __init__(self): + self.ss_es_hist = [] + self.bit_hist = 0 + + def decode(self, ss, es, data): + self.bit_hist <<= 1 + self.bit_hist |= data + self.bit_hist &= 0xffff + self.ss_es_hist.append((ss, es)) + self.ss_es_hist = self.ss_es_hist[-16:] + return self.bit_hist == 0x4489 + +class ByteChunker(object): + def __init__(self, decoder, prev_bit): + self.d = decoder + + self.phase = 0 + self.prev_two_bits = prev_bit + self.prev_ss_es = [] + self.bit_hist = prev_bit + self.bit_hist_len = 0 + self.ss_es_hist = [] + + def decode(self, ss, es, data): + self.phase ^= 1 + + if not self.phase: + prev_data = (self.prev_two_bits >> 1) & 1 + prev_clk = self.prev_two_bits & 1 + expected_clk = 1 if ((prev_data == 0) and (data == 0)) else 0 + if prev_clk != expected_clk: + self.d.put(*self.prev_ss_es, self.d.out_ann, [0, ["Err"]]) + self.prev_two_bits <<= 1 + self.prev_two_bits |= data + self.prev_two_bits &= 3 + self.prev_ss_es = (ss, es) + + if self.phase: + return + + self.bit_hist <<= 1 + self.bit_hist |= data + self.bit_hist &= 0xff + self.bit_hist_len += 1 + self.ss_es_hist.append((ss, es)) + self.ss_es_hist = self.ss_es_hist[-8:] + if self.bit_hist_len < 8: + return + + self.bit_hist_len = 0 + self.d.on_byte(self.bit_hist) + +class StateAddressMark(object): + def __init__(self, decoder): + self.d = decoder + + def on_byte(self, ss, es, data): + self.d.put(ss, es, self.d.out_ann, [1, ["%02X" % data]]) + if data == 0xFB: + if self.d.id_size_decoded is None: + self.d.put(ss, es, self.d.out_ann, [3, ['Data without ID']]) + return None + self.d.put(ss, es, self.d.out_ann, [2, ['Data address mark']]) + return StateData(self.d) + elif data == 0xFE: + self.d.put(ss, es, self.d.out_ann, [2, ['ID address mark']]) + return StateIdTrack(self.d) + else: + self.d.put(ss, es, self.d.out_ann, [2, ['Error']]) + return None + +class StateByteSequence(metaclass=ABCMeta): + def __init__(self, decoder, seq_len, seq_name, next_state_class): + self.d = decoder + self.seq_len = seq_len + self.seq_name = seq_name + self.next_state_class = next_state_class + self.count = 0 + self.data = [] + + def on_byte(self, ss, es, data): + if self.count == 0: + self.ss = ss + self.data.append(data) + self.count += 1 + if self.count < self.seq_len: + return self + self.d.put(self.ss, es, self.d.out_ann, [2, [self.seq_name]]) + self.on_sequence(self.ss, es, self.data) + if not self.next_state_class: + return None + return self.next_state_class(self.d) + + @abstractmethod + def on_sequence(self, ss, es, data): + pass + +class StateIdTrack(StateByteSequence): + def __init__(self, decoder): + super().__init__(decoder, 1, 'ID Track', StateIdSide) + + def on_sequence(self, ss, es, data): + self.d.id_track = data[0] + +class StateIdSide(StateByteSequence): + def __init__(self, decoder): + super().__init__(decoder, 1, 'ID Side', StateIdSector) + + def on_sequence(self, ss, es, data): + self.d.id_side = data[0] + +class StateIdSector(StateByteSequence): + def __init__(self, decoder): + super().__init__(decoder, 1, 'ID Sector', StateIdSize) + + def on_sequence(self, ss, es, data): + self.d.id_sector = data[0] + +class StateIdSize(StateByteSequence): + def __init__(self, decoder): + super().__init__(decoder, 1, 'ID Size', StateIdCRC) + + def on_sequence(self, ss, es, data): + if data[0] > 6: + self.d.put(ss, es, self.d.out_ann, [3, ['Invalid']]) + self.next_state_class = None + return + self.d.id_size = data[0] + self.d.id_size_decoded = 128 * (2 ** self.d.id_size) + self.d.put(ss, es, self.d.out_ann, [3, [str(self.d.id_size_decoded)]]) + +class StateIdCRC(StateByteSequence): + def __init__(self, decoder): + super().__init__(decoder, 2, 'ID CRC', None) + + def on_sequence(self, ss, es, data): + found_crc = (data[0] << 8) | data[1] + # FIXME: We should really capture the sync/address-mark bytes rather than assuming them + calc_crc = calc_crc16([ + 0xa1, 0xa1, 0xa1, 0xfe, + self.d.id_track, + self.d.id_side, + self.d.id_sector, + self.d.id_size]) + if found_crc == calc_crc: + self.d.put(ss, es, self.d.out_ann, [3, ['OK']]) + else: + self.d.put(ss, es, self.d.out_ann, [3, ['Err (%x)' % calc_crc]]) + +class StateData(StateByteSequence): + def __init__(self, decoder): + super().__init__(decoder, decoder.id_size_decoded, 'Data', StateDataCRC) + + def on_sequence(self, ss, es, data): + self.d.sector_data = data + +class StateDataCRC(StateByteSequence): + def __init__(self, decoder): + super().__init__(decoder, 2, 'Data CRC', None) + + def on_sequence(self, ss, es, data): + found_crc = (data[0] << 8) | data[1] + # FIXME: We should really capture the sync/address-mark bytes rather than assuming them + calc_crc = calc_crc16([0xa1, 0xa1, 0xa1, 0xfb] + self.d.sector_data) + if found_crc == calc_crc: + self.d.put(ss, es, self.d.out_ann, [3, ['OK']]) + else: + self.d.put(ss, es, self.d.out_ann, [3, ['Err (%x)' % calc_crc]]) + chs_data = (self.d.id_track, self.d.id_side, self.d.id_sector, bytes(self.d.sector_data), found_crc, calc_crc) + chs_data_repr = (repr(chs_data) + "\n").encode('UTF-8') + self.d.put(ss, es, self.d.out_python, chs_data) + self.d.put(ss, es, self.d.out_binary, [0, chs_data_repr]) + +class Decoder(srd.Decoder): + api_version = 3 + id = 'floppy_ibm_pc' + name = 'Floppy (IBM PC)' + longname = 'IBM PC Floppy disk MFM' + desc = 'IBM PC Floppy disk MFM' + license = 'gplv2+' + inputs = ['mfm_raw'] + outputs = ['floppy_sectors'] + annotations = ( + ('mfm_err', 'MFM encoding errors'), + ('bytes', 'Raw bytes'), + ('labels', 'Labels'), + ('labels2', 'Labels 2'), + ) + annotation_rows = ( + ('mfm_err', 'MFM encoding errors', (0, )), + ('bytes', 'Raw bytes', (1, )), + ('labels', 'Labels', (2, )), + ('labels2', 'Labels 2', (3, )), + ) + binary = ( + ('sectors', 'Sector data'), + ) + + def __init__(self): + self.reset() + + def reset(self): + self.sync_detector = SyncDetector() + self.state = None + self.id_size_decoded = None + + def start(self): + self.out_ann = self.register(srd.OUTPUT_ANN) + self.out_python = self.register(srd.OUTPUT_PYTHON) + self.out_binary = self.register(srd.OUTPUT_BINARY) + + def decode(self, ss, es, data): + if self.sync_detector.decode(ss, es, data): + self.put( + self.sync_detector.ss_es_hist[-6][0], + self.sync_detector.ss_es_hist[-6][1], + self.out_ann, + [0, ["Err"]]) + self.put( + self.sync_detector.ss_es_hist[1][0], + self.sync_detector.ss_es_hist[-1][1], + self.out_ann, + [1, ["~A1"]]) + self.put( + self.sync_detector.ss_es_hist[1][0], + self.sync_detector.ss_es_hist[-1][1], + self.out_ann, + [2, ['A1 sync']]) + self.chunker = ByteChunker(self, 1) + self.state = StateAddressMark(self) + elif self.state: + self.chunker.decode(ss, es, data) + + def on_byte(self, data): + ss = self.chunker.ss_es_hist[0][0] + es = self.chunker.ss_es_hist[-1][1] + self.put(ss, es, self.out_ann, [1, ["%02x" % data]]) + self.state = self.state.on_byte(ss, es, data) -- 2.17.1 _______________________________________________ sigrok-devel mailing list sigrok-devel@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/sigrok-devel