diff --git a/src/Makefile.am b/src/Makefile.am index c05d97c..2896016 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -14,6 +14,8 @@ epple2_SOURCES = a2colorsobserved.cpp addressbus.cpp analogtv.cpp apple2.cpp \ applentsc.cpp card.cpp cassette.cpp cassettein.cpp cassetteout.cpp \ clipboardhandler.cpp clockcard.cpp \ configep2.cpp cpu.cpp diskcontroller.cpp drive.cpp drivemotor.cpp \ +disk2readwritehead.cpp disk2steppermotor.cpp disk2steppermotorcan.cpp disk2steppermotorrotor.cpp \ +magneticfield.cpp movable.cpp \ emptyslot.cpp emulator.cpp firmwarecard.cpp gui.cpp hypermode.cpp \ keyboard.cpp keyboardbuffermode.cpp languagecard.cpp filterchroma.cpp \ filterluma.cpp lss.cpp main.cpp memory.cpp \ @@ -21,7 +23,7 @@ memorychip.cpp memoryrow.cpp memorystrapping.cpp memoryrandomaccess.cpp \ paddlebuttonstates.cpp \ paddles.cpp picturegenerator.cpp powerupreset.cpp raminitializer.cpp \ screenimage.cpp slots.cpp speakerclicker.cpp standardin.cpp \ -standardinproducer.cpp standardout.cpp steppermotor.cpp textcharacters.cpp \ +standardinproducer.cpp standardout.cpp textcharacters.cpp \ timable.cpp video.cpp videoaddressing.cpp videomode.cpp \ videostaticgenerator.cpp wozfile.cpp \ Circuit.cpp Common.cpp Cpu6502.cpp Cpu6502Helper.cpp Emu6502.cpp SegmentCache.cpp \ @@ -31,13 +33,15 @@ tinyfiledialogs.cpp noinst_HEADERS = a2colorsobserved.h addressbus.h analogtv.h apple2.h applentsc.h \ card.h cassette.h cassettein.h cassetteout.h \ clipboardhandler.h clockcard.h configep2.h cpu.h \ +disk2readwritehead.h disk2steppermotor.h disk2steppermotorcan.h disk2steppermotorrotor.h \ +magneticfield.h movable.h \ diskcontroller.h drive.h drivemotor.h e2const.h emptyslot.h emulator.h firmwarecard.h font3x5.h gui.h \ hypermode.h keyboardbuffermode.h keyboard.h languagecard.h filterchroma.h \ filterluma.h lss.h memory.h \ memorychip.h memoryrow.h memorystrapping.h memoryrandomaccess.h \ paddlebuttonstates.h paddles.h picturegenerator.h \ powerupreset.h raminitializer.h screenimage.h slots.h speakerclicker.h \ -standardin.h standardinproducer.h standardout.h steppermotor.h \ +standardin.h standardinproducer.h standardout.h \ textcharacterimages.h textcharacters.h timable.h util.h \ videoaddressing.h video.h videomode.h videostaticgenerator.h wozfile.h \ Circuit.h Common.h Cpu6502.h Cpu6502Helper.h Emu6502.h SegmentCache.h SegmentTypes.h \ diff --git a/src/disk2readwritehead.cpp b/src/disk2readwritehead.cpp new file mode 100644 index 0000000..ce5a0d7 --- /dev/null +++ b/src/disk2readwritehead.cpp @@ -0,0 +1,37 @@ +/* + epple2 + + Copyright © 2019, Christopher Alan Mosher, Shelton, CT, USA. + + 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 3 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 . +*/ + +#include "disk2readwritehead.h" + +Disk2ReadWriteHead::~Disk2ReadWriteHead() = default; + +int Disk2ReadWriteHead::position() const { + return this->current_position; +} + +bool Disk2ReadWriteHead::move_by(const int delta_position) { + const int candidate_position = this->current_position + delta_position; + + const bool ok_position = (0 <= candidate_position && candidate_position < C_POSITION); + if (ok_position) { + this->current_position = candidate_position; + } + + return ok_position; +} diff --git a/src/disk2readwritehead.h b/src/disk2readwritehead.h new file mode 100644 index 0000000..ce191dd --- /dev/null +++ b/src/disk2readwritehead.h @@ -0,0 +1,66 @@ +/* + epple2 + + Copyright © 2019, Christopher Alan Mosher, Shelton, CT, USA. + + 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 3 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 . +*/ + +#ifndef DISK2READWRITEHEAD_H +#define DISK2READWRITEHEAD_H + +#include "movable.h" + +/** + * @brief Disk ][ read/write head assembly + * + * Represents the read/write head assembly of a Disk ][ floppy drive. + * The assembly includes a "sled" that can move back and forth, with a + * read/write head attached to it. The sled is moved by the rotor from + * the stepper motor. + * + * Rotation of the rotor will cause the sled to move back or forth, along + * with the read/write head, positioning it over a "track" on the floppy disk. + * + * If the rotor attempts to move the sled too far, the sled will resist and + * prevent the rotor from rotating. + */ +class Disk2ReadWriteHead : public Movable { +public: + virtual ~Disk2ReadWriteHead(); + + int position() const; + virtual bool move_by(int delta_position); + +private: + /** + * Total count of possible positions for this read/write head. + * + * A position will represent a track on a floppy disk. + * The number of tracks really depends on the floppy disk. + * The Disk ][ has a "stop" at track zero, but not at the high end, + * so this is where it can vary from disk to disk. + * Typical disks have 35 tracks, and allow quarter-track precision. + * + * In this emulator, we adhere to the WOZ v2 specification for floppy disks. + * It allows for up to 40 tracks (with quarter-track precision): + * + * 0=t0, 1=t0.25, 2=t0.5, 3=t0.75, 4=t1, ... 140=t35.00 ... 159=t39.75 + */ + const static int C_POSITION = 40*4; + + int current_position = C_POSITION/2; // random start +}; + +#endif // DISK2READWRITEHEAD_H diff --git a/src/disk2steppermotor.cpp b/src/disk2steppermotor.cpp new file mode 100644 index 0000000..9e1d0c3 --- /dev/null +++ b/src/disk2steppermotor.cpp @@ -0,0 +1,396 @@ +/* + epple2 + + Copyright © 2019, Christopher Alan Mosher, Shelton, CT, USA. + + 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 3 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 . +*/ + +/* +See UA2, 9-7. + +The following is based on research, experimentation, and other +help from John Morris, Tom Greene, Michael Guidero, and Lane Roathe. + +Stepper motor has 2 cans each with a center-tapped coil, allowing +for current flow in one of either direction, causing a N/S or S/N +polarity fo the top/bottom of the can. Each top/bottom has fingers +bending down/up towards the center, the fingers from alternate surfaces +being interlased. These fingers transfer the polarity from the coil. + +Example of coil-0, controlled by phases 0 and 2, +the two possible energized states, N/S and S/N, +generated by the signal from PH0 or PH2 driving +half the coil. Phase 0 drives top half of coil one way: + +side view: top of can coil leads +----v-------v------NNN----- + N N N v-<-<-<-----PHASE-0 <--<-- ON + N S N S NNN@ + N S N S SSS@>->->--center-tap--E + N S N S S @ + S S S \-----------PHASE-2 OFF +--------^-------^--SSS----- +bottom of can + +And to cause opposing polarity, phase 2 drives the bottom half +of the coil the other way: + +side view: top of can coil leads +----v-------v------SSS----- + S S S /-----------PHASE-0 OFF + S N S N S @ + S N S N SSS@>->->--center-tap--E + S N S N NNN@ + N N N ^-<-<-<-----PHASE-2 <--<-- ON +--------^-------^--NNN----- +bottom of can + +Only one coil at a time will (typically) have current, +one causing "upward" flow in the diagram, or the other +causing "downward" flow. The flow causes either N/S +or (reverse) S/N polarity of alternating fingers. + +Inside the coils is a rotor with NSNSNS... fixed polarity magnets +(actually one cylindrical magnet with vertical stripes of polarity). + +S on a finger will attract an N on the rotor, possibly causing it to move. +The rotor is attached to the track arm. Clockwise rotor movement causes the +arm to move in one direction, and counter-clockwise the other. + +The two cans are offset rotationally from each other by one-half a finger width. +This offset is what allows the cans to alternately pull on the rotor (causing it +to rotate). + +top of can 0 coil leads +----v-------v---------- + v v @-----------PHASE-0 + v ^ v ^ @--center-tap--E + ^ ^ @-----------PHASE-2 +--------^-------^------ +bottom of can 0 + +top of can 1 coil leads +------v-------v-------- + v v @-----------PHASE-1 + v ^ v ^ @--center-tap--E + ^ ^ @-----------PHASE-3 +----------^-------^---- +bottom of can 1 + +The combination of the two cans allows for 8 positions of the rotor (in 2 tracks), +yielding quarter-track precision. +Quarter tracks are achieved by activating two fingers (one in each can) adjacent +to one another, causing the rotor to be positioned BETWEEN the two fingers, rather +than ALIGNED with one finger (as is the case for on-track or half-track positions). + +Example of two cans, with N/S charge in can 0 and no charge in can 1, +and resulting rotational position of the rotor (each one repeats 12 times around +the circle): + + /v-------\ 7 N + |N S | + \----^---/ 3 S* <-- S charge of can is on finger position 3 + + /--v-----\ 5 O + | | + \------^-/ 1 O + + |SS-NNN-S| <-- rotor, center of N pole (position 3) is aligned with + * the S (position 3) of the top can + 76543210 <-- our (arbitrary) indices of the 8 rotor/finger positions + +To calculate postion (of center of N pole), average positions of S poles in both cans, +but if only one can is on, use that one's position directly. + +But 7&1 is a special case (calculation-wise): since it's a circle, they +"wrap around", position _0_ being between them: + + /v-------\ 7 S* top of can 0 + |S N | + \----^---/ 3 N bot of can 0 + + /--v-----\ 5 N top of can 1 + | N S | + \------^-/ 1 S* bot of can 1 + + |N-SSS-NN| <-- rotor, center of N pole (position 0) is aligned between + * the two S poles in the two cans + 76543210 + + + +Adjacent fingers within a can are of reverse polarity, and are 4 positions apart. +Cans are offset from each other by 2 positions. + +PH = phase 0-4 (on or off) +CAN = can 0-1 (north, south, or off) + +CAN0 = PH0+PH2 +CAN1 = PH1+PH3 + +PHASE CAN +3210 == 10 +---- ---- ---- +0000 OO. 0/ 0 +0001 ON. 0/+1 +0010 NO. +1/ 0 +0011 NN. +1/+1 +0100 OS. 0/-1 +0101 0000 +0110 NS. +1/-1 +0111 0010 +1000 SO. -1/ 0 +1001 SN. -1/+1 +1010 0000 +1011 0001 +1100 SS. -1/-1 +1101 1000 +1110 0100 +1111 0000 + +PHASE 3 2 1 0 + | | | | + / X \ + / / \ \ + / / \ \ + / / \ \ + / E | | E \ +| | | | | | ++oo+oo+ +oo+oo+ +(CAN-1) (CAN-0) + +PHASE to CAN translation algorithm: + can[0] = can[1] = 0 + if phase 0, ++can[0] + if phase 2, --can[0] + if phase 1, ++can[1] + if phase 3, --can[1] +resulting can[i] value indicates: + +1, N north + 0, O off + -1, S south + +For each of the 8 on states, the rotor will (usually) be forced to one position. +(The one exception is if the rotor happens to be positioned in exactly the +opposing position, then the turning on of a magnet won't be able to move the +rotor into the correct position.) + +If both coils are off (OO), then the rotor is free to rotate, +and could be in any possible position (and we can't determine where). + +In terms of state transitions, we could go from/to: +from: to: +OFF any 8 ON (with random movement) +any 8 ON OFF (with no rotor movement) +any 8 ON any other 7 ON (with movement) + + +State transitions for on-states: + -+ -0 -- 0- +- +0 ++ 0+ + \to | SN: SO: SS: OS: NS: NO: NN: ON: +from \ | N-SSS-NN NN-SSS-N NNN-SSS- -NNN-SSS S-NNN-SS SS-NNN-S SSS-NNN- -SSS-NNN +--------+----------------------------------------------------------------------------------------------- +SN: | +N-SSS-NN| 0 +1 +2 +3 0 -3 -2 -1 +SO: | +NN-SSS-N| -1 0 +1 +2 +3 0 -3 -2 +SS: | +NNN-SSS-| -2 -1 0 +1 +2 +3 0 -3 +OS: | +-NNN-SSS| -3 -2 -1 0 +1 +2 +3 0 +NS: | +S-NNN-SS| 0 -3 -2 -1 0 +1 +2 +3 +NO: | +SS-NNN-S| +3 0 -3 -2 -1 0 +1 +2 +NN: | +SSS-NNN-| +2 +3 0 -3 -2 -1 0 +1 +ON: | +-SSS-NNN| +1 +2 +3 0 -3 -2 -1 0 + + 0 == no movement + +movement = (new_can[0]-old_can[0]) + (new_can[1]-old_can[1]) + +The concepts of track, half-track, and quarter-track are arbitrary, and are +of no significance to the stepper motor. As far as the motor is concerned, it's +output is just a positive or negative movement (rotation), by one or more +"steps". Also, positive and negative are arbitrary +designations; they simply indicate opposite directions of rotation. + +Input is just the 4 phase switches. + +Internal state is the position of the rotor. +We also keep the 4 phase settings in our internal state. +*/ + +#include "disk2steppermotor.h" +#include +#include + +Disk2StepperMotor::Disk2StepperMotor(Movable& movable): + can_0(0, 2), + can_1(1, 2), + rotor(2, movable) { +} + +void Disk2StepperMotor::set_phase(int p, const bool on) { + assert(0 <= p && p < 4); + + Disk2StepperMotorCan *can; + if ((p&1) == 0) { + can = &this->can_0; + } else { + can = &this->can_1; + } + can->set_lead(p/2, on); + build_magnetic_field(); + + this->rotor.pend_rotation(); +} + +void Disk2StepperMotor::build_magnetic_field() { + if (magnetized()) { + this->field.turn_on_at(magnetic_position()); + } else { + this->field.turn_off(); + } +} + +bool Disk2StepperMotor::magnetized() const { + return this->can_0.magnetized() || this->can_1.magnetized(); +} + +int Disk2StepperMotor::magnetic_position() const { + assert(magnetized()); + if (this->can_0.magnetized() && this->can_1.magnetized()) { + if (this->can_1.magnetic_position()==6 && this->can_0.magnetic_position()==0) { // special case at our "seam" + return 7; + } + // average position between poles of both cans: + return (this->can_0.magnetic_position()+this->can_1.magnetic_position())/2; + } + if (this->can_0.magnetized()) { + return this->can_0.magnetic_position(); + } + if (this->can_1.magnetized()) { + return this->can_1.magnetic_position(); + } + assert(false); +} + +void Disk2StepperMotor::tick() { + this->rotor.tick(this->field); +} + + + + + + + + + +class SpyMovable : public Movable { +public: + virtual ~SpyMovable() = default; + int position() const { return last_delta; } + virtual bool move_by(int delta) { last_delta = delta; return true; } + int last_delta = 0; +}; + +static void advance(Disk2StepperMotor &d) { + for (int i = 0; i < 1007; ++i) { + d.tick(); + } +} + +static void set_phases_to(Disk2StepperMotor &d, int p) { + d.set_phase(0, (p&1) != 0); + d.tick(); + d.set_phase(1, (p&2) != 0); + d.tick(); + d.set_phase(2, (p&4) != 0); + d.tick(); + d.set_phase(3, (p&8) != 0); + advance(d); +} + +static int test_move(SpyMovable &moved, int p_from, int p_to) { + Disk2StepperMotor d(moved); + + if (p_from == 0x4 || p_from == 0xE) { + // oops, give it a nudge first + d.set_phase(0,true); + d.set_phase(1,true); + advance(d); + // and reset + d.set_phase(0,false); + d.set_phase(1,false); + advance(d); + } + + // set up initial state + set_phases_to(d, p_from); + + // clear any movement from setup + moved.move_by(0); + + // test moving to the final state + set_phases_to(d, p_to); + + return moved.position(); // return how far we actually were moved +} + +bool Disk2StepperMotor::test() { + int phase[0x10] = {0x9,0x8,0xD,0xC,0x4,0xE,0x6,0x2,0x7,0x3,0x1,0xB,0x0,0x5,0xA,0xF}; + int move[0x0C][0x10] = { + //fr\to 9 8 D C 4 E 6 2 7 3 1 B 0 5 A F + /*9*/ { 0,-1,-1,-2,-3,-3, 0,+3,+3,+2,+1,+1, 0, 0, 0, 0}, + /*8*/ {+1, 0, 0,-1,-2,-2,-3, 0, 0,+3,+2,+2, 0, 0, 0, 0}, + /*D*/ {+1, 0, 0,-1,-2,-2,-3, 0, 0,+3,+2,+2, 0, 0, 0, 0}, + /*C*/ {+2,+1,+1, 0,-1,-1,-2,-3,-3, 0,+3,+3, 0, 0, 0, 0}, + /*4*/ {+3,+2,+2,+1, 0, 0,-1,-2,-2,-3, 0, 0, 0, 0, 0, 0}, + /*E*/ {+3,+2,+2,+1, 0, 0,-1,-2,-2,-3, 0, 0, 0, 0, 0, 0}, + /*6*/ { 0,+3,+3,+2,+1,+1, 0,-1,-1,-2,-3,-3, 0, 0, 0, 0}, + /*2*/ {-3, 0, 0,+3,+2,+2,+1, 0, 0,-1,-2,-2, 0, 0, 0, 0}, + /*7*/ {-3, 0, 0,+3,+2,+2,+1, 0, 0,-1,-2,-2, 0, 0, 0, 0}, + /*3*/ {-2,-3,-3, 0,+3,+3,+2,+1,+1, 0,-1,-1, 0, 0, 0, 0}, + /*1*/ {-1,-2,-2,-3, 0, 0,+3,+2,+2,+1, 0, 0, 0, 0, 0, 0}, + /*B*/ {-1,-2,-2,-3, 0, 0,+3,+2,+2,+1, 0, 0, 0, 0, 0, 0}, + }; + + std::printf(" %s\n", "from_phase -> to_phase ? expected_move : rotor_before +/-actual_move = rotor_after [*=failed]"); + std::printf(" %s\n", " 9 8 D C 4 E 6 2 7 3 1 B 0 5 A F"); + + SpyMovable moved; + + bool bad = false; + for (int i_from = 0; i_from < 0x0C; ++i_from) { + std::printf("%1X: ", phase[i_from]); + for (int i_to = 0; i_to < 0x10; ++i_to) { + int expected = move[i_from][i_to]; + int actual = test_move(moved,phase[i_from],phase[i_to]); + std::printf( + "%1X->%1X?%+2d:%+2d=%s ", + phase[i_from],phase[i_to],expected,actual,expected==actual?" ":"*"); + if (actual != expected) { + bad = true; + } + } + std::printf("\n"); + } + + return !bad; +} diff --git a/src/disk2steppermotor.h b/src/disk2steppermotor.h new file mode 100644 index 0000000..a84df76 --- /dev/null +++ b/src/disk2steppermotor.h @@ -0,0 +1,50 @@ +/* + epple2 + + Copyright © 2019, Christopher Alan Mosher, Shelton, CT, USA. + + 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 3 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 . +*/ + +#ifndef DISK2STEPPERMOTOR_H +#define DISK2STEPPERMOTOR_H + +#include "disk2steppermotorcan.h" +#include "disk2steppermotorrotor.h" +#include "movable.h" +#include "magneticfield.h" + +class Disk2StepperMotor { +public: + Disk2StepperMotor(Movable& movable); + + void tick(); + void set_phase(int i_phase_0_to_3, bool on); + + static bool test(); + +private: + void build_magnetic_field(); + bool magnetized() const; + int magnetic_position() const; + + Disk2StepperMotorCan can_0; + Disk2StepperMotorCan can_1; + + MagneticField field; + + Disk2StepperMotorRotor rotor; +}; + +#endif // DISK2STEPPERMOTOR_H diff --git a/src/disk2steppermotorcan.cpp b/src/disk2steppermotorcan.cpp new file mode 100644 index 0000000..03a6cf6 --- /dev/null +++ b/src/disk2steppermotorcan.cpp @@ -0,0 +1,55 @@ +/* + epple2 + + Copyright © 2019, Christopher Alan Mosher, Shelton, CT, USA. + + 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 3 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 . +*/ + +#include "disk2steppermotorcan.h" +#include + +Disk2StepperMotorCan::Disk2StepperMotorCan(int index, int total_stack): + index(index), + total_stack(total_stack) { +} + +void Disk2StepperMotorCan::set_lead(int i_lead, bool on) { + assert(0 <= i_lead && i_lead < 2); + if (i_lead == 0) { + this->lead_north = on; + } else { + this->lead_south = on; + } +} + +int Disk2StepperMotorCan::charge() const { + int coil_charge = 0; + if (this->lead_north) { + ++coil_charge; + } + if (this->lead_south) { + --coil_charge; + } + return coil_charge; +} + +bool Disk2StepperMotorCan::magnetized() const { + return this->charge() != 0; +} + +int Disk2StepperMotorCan::magnetic_position() const { + assert(magnetized()); + return this->total_stack * (this->index + 2*(this->charge() < 0)); +} diff --git a/src/disk2steppermotorcan.h b/src/disk2steppermotorcan.h new file mode 100644 index 0000000..707d8ae --- /dev/null +++ b/src/disk2steppermotorcan.h @@ -0,0 +1,42 @@ +/* + epple2 + + Copyright © 2019, Christopher Alan Mosher, Shelton, CT, USA. + + 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 3 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 . +*/ + +#ifndef DISK2STEPPERMOTORCAN_H +#define DISK2STEPPERMOTORCAN_H + +class Disk2StepperMotorCan { +public: + Disk2StepperMotorCan(int index, int total_stack); + + bool magnetized() const; + int magnetic_position() const; + + void set_lead(int index, bool on); + +private: + int charge() const; + + const int index; + const int total_stack; + + bool lead_north = false; + bool lead_south = false; +}; + +#endif // DISK2STEPPERMOTORCAN_H diff --git a/src/disk2steppermotorrotor.cpp b/src/disk2steppermotorrotor.cpp new file mode 100644 index 0000000..f83ec8b --- /dev/null +++ b/src/disk2steppermotorrotor.cpp @@ -0,0 +1,79 @@ +/* + epple2 + + Copyright © 2019, Christopher Alan Mosher, Shelton, CT, USA. + + 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 3 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 . +*/ + +#include "disk2steppermotorrotor.h" + + + +/** + * @brief positive_modulo + * @param x value to constrain + * @param n modulus + * @return constrained value + */ +static int positive_modulo(const int x, const int n) { + return (x%n + n) % n; +} + +/** + * @brief + * Constrains x to within range (-n,n) + * representing movements around a circle + * @param x value to constrain + * @param n range + * @return constrained value + */ +static int cyclic_motion(int x, const int n) { + while (x <= -n) { + x += 2*n; + } + while (+n <= x) { + x -= 2*n; + } + return x%n; +} + + + +Disk2StepperMotorRotor::Disk2StepperMotorRotor(const int total_can_stack, Movable& movable): + total_can_stack(total_can_stack), + movable(movable) { +} + +void Disk2StepperMotorRotor::pend_rotation() { + this->ticks_pending = 1000; // about 1 millisecond delay due to inertia +} + +void Disk2StepperMotorRotor::tick(const MagneticField &magnetic_field) { + if (this->ticks_pending) { + --this->ticks_pending; + if (!this->ticks_pending) { + if (magnetic_field.is_on()) { + rotate_to(magnetic_field.position()); + } + } + } +} + +void Disk2StepperMotorRotor::rotate_to(const int new_position) { + const int potential_movement = cyclic_motion(new_position-this->position, this->total_can_stack*2); + if (potential_movement && this->movable.move_by(potential_movement)) { + this->position = positive_modulo(this->position+potential_movement, this->total_can_stack*4); + } +} diff --git a/src/disk2steppermotorrotor.h b/src/disk2steppermotorrotor.h new file mode 100644 index 0000000..4e14d5d --- /dev/null +++ b/src/disk2steppermotorrotor.h @@ -0,0 +1,42 @@ +/* + epple2 + + Copyright © 2019, Christopher Alan Mosher, Shelton, CT, USA. + + 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 3 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 . +*/ + +#ifndef DISK2STEPPERMOTORROTOR_H +#define DISK2STEPPERMOTORROTOR_H + +#include "movable.h" +#include "magneticfield.h" + +class Disk2StepperMotorRotor { +public: + Disk2StepperMotorRotor(const int total_can_stack, Movable& movable); + + void pend_rotation(); + void tick(const MagneticField &field); + +private: + void rotate_to(int new_position); + + const int total_can_stack; + Movable& movable; + int position = 0; + int ticks_pending = 0; +}; + +#endif // DISK2STEPPERMOTORROTOR_H diff --git a/src/diskcontroller.cpp b/src/diskcontroller.cpp index ee3c2a4..95101a3 100644 --- a/src/diskcontroller.cpp +++ b/src/diskcontroller.cpp @@ -20,8 +20,6 @@ DiskController::DiskController(ScreenImage& gui, int slot, bool lss13): gui(gui), slot(slot), - drive1(diskBytes1,arm1), - drive2(diskBytes2,arm2), currentDrive(&this->drive1), load(false), write(false), @@ -47,7 +45,7 @@ unsigned char DiskController::io(const unsigned short addr, const unsigned char case 1: // TODO if phase-1 is on, it also acts as write-protect (UA2, 9-8) case 2: case 3: - this->currentDrive->setMagnet(q,on); + this->currentDrive->set_phase(q,on); this->gui.setTrack(this->slot, getCurrentDriveNumber(), getTrack()); break; case 4: @@ -87,8 +85,8 @@ unsigned char DiskController::io(const unsigned short addr, const unsigned char * (When the motor is on, that is.) */ void DiskController::tick() { - this->arm1.tick(); - this->arm2.tick(); + this->drive1.tick(); + this->drive2.tick(); if (this->ioStepped) { // if we already ran it, above in io(), skip here this->ioStepped = false; @@ -101,6 +99,12 @@ void DiskController::tick() { } this->motor.tick(); // only need to send tick when motor is powered on + /* + * TODO + * Every CPU clock, add 8 to your bit timing clock. If your bit timing clock is >= optimal bit timing, + * then inject the next bit and subtract the optimal bit timing from your bit timing clock. + * That will give you 125ns resolution on your bits being fed to the sequencer. + */ rotateCurrentDisk(); // run two LSS cycles = 2MHz diff --git a/src/diskcontroller.h b/src/diskcontroller.h index d4685ad..ad37fda 100644 --- a/src/diskcontroller.h +++ b/src/diskcontroller.h @@ -28,152 +28,128 @@ class DiskController : public Card { private: - ScreenImage& gui; - int slot; - WozFile diskBytes1; - StepperMotor arm1; - Drive drive1; + ScreenImage& gui; + int slot; - WozFile diskBytes2; - StepperMotor arm2; - Drive drive2; + Disk2Drive drive1; + Disk2Drive drive2; + Disk2Drive* currentDrive; - Drive* currentDrive; + bool load; // Q6 + bool write; // Q7 + bool ioStepped; - bool load; // Q6 - bool write; // Q7 - bool ioStepped; + /* + * Only one drive's motor can be on at a time, + * so we only need one instance. + */ + DriveMotor motor; - /* - * Only one drive's motor can be on at a time, - * so we only need one instance. - */ - DriveMotor motor; + // Maintain a copy of the last thing on the data bus, so it can + // be read by the LSS algorithm when needed. + std::uint8_t dataBusReadOnlyCopy; + LSS lssp6rom; // the LSS PROM P6 chip (one command per sequence/state combination) + std::uint8_t dataRegister; // C3 the controller's LS323 data register + std::uint8_t seq; // A3 sequence control LS174 (current sequence number, 0-F) + // For ease of use, we store the 4-bit seq number in the _high order_ nibble here. + // On the real Apple the read pulse goes thru this LS174 too, but we don't emulate that here. - // Maintain a copy of the last thing on the data bus, so it can - // be read by the LSS algorithm when needed. - std::uint8_t dataBusReadOnlyCopy; - LSS lssp6rom; // the LSS PROM P6 chip (one command per sequence/state combination) - std::uint8_t dataRegister; // C3 the controller's LS323 data register - std::uint8_t seq; // A3 sequence control LS174 (current sequence number, 0-F) - // For ease of use, we store the 4-bit seq number in the _high order_ nibble here. - // On the real Apple the read pulse goes thru this LS174 too, but we don't emulate that here. + std::uint8_t prev_seq; // remember previous seq, to determine if A7 changes (indicating write a 1 bit) + std::uint8_t t; // used to keep track of 4 MPU cycles - std::uint8_t prev_seq; // remember previous seq, to determine if A7 changes (indicating write a 1 bit) - std::uint8_t t; // used to keep track of 4 MPU cycles + // TODO for a rev. 0 motherboard, the disk controller will auto reset the CPU (see UA2, 9-13) - // TODO for a rev. 0 motherboard, the disk controller will auto reset the CPU (see UA2, 9-13) - - void writeBit(bool on) { - if (!this->motor.isOn()) { - return; - } - this->currentDrive->writeBit(on); + void writeBit(bool on) { + if (!this->motor.isOn()) { + return; } + this->currentDrive->writeBit(on); + } - Drive& getDrive(const unsigned char drive) - { - return (drive == 0) ? this->drive1 : this->drive2; - } + Disk2Drive& getDrive(const unsigned char drive) { + return (drive == 0) ? this->drive1 : this->drive2; + } - Drive& getOtherDrive() - { - return (this->currentDrive == &this->drive1) ? this->drive2 : this->drive1; - } + Disk2Drive& getOtherDrive() { + return (this->currentDrive == &this->drive1) ? this->drive2 : this->drive1; + } - void rotateCurrentDisk(); - void stepLss(); + void rotateCurrentDisk(); + void stepLss(); public: - DiskController(ScreenImage& gui, int slot, bool lss13); - ~DiskController(); + DiskController(ScreenImage& gui, int slot, bool lss13); + ~DiskController(); - void tick(); - virtual unsigned char io(const unsigned short address, const unsigned char data, const bool writing); + void tick(); + virtual unsigned char io(const unsigned short address, const unsigned char data, const bool writing); - void reset() - { - this->gui.setIO(this->slot,getCurrentDriveNumber(),false); - this->gui.clearCurrentDrive(this->slot,getCurrentDriveNumber()); + void reset() { + this->gui.setIO(this->slot,getCurrentDriveNumber(),false); + this->gui.clearCurrentDrive(this->slot,getCurrentDriveNumber()); - this->currentDrive = &this->drive1; - this->motor.reset(); + this->currentDrive = &this->drive1; + this->motor.reset(); - this->gui.setCurrentDrive(this->slot,getCurrentDriveNumber(),getTrack(),false); - } + this->gui.setCurrentDrive(this->slot,getCurrentDriveNumber(),getTrack(),false); + } - void loadDisk(unsigned char drive, const std::string& fnib) - { - if (!this->getDrive(drive).loadDisk(fnib)) - { - return; - } - this->gui.setDiskFile(this->slot,drive,fnib); - this->gui.setDirty(this->slot,getCurrentDriveNumber(),false); - } + void loadDisk(unsigned char drive, const std::string& fnib) { + if (!this->getDrive(drive).loadDisk(fnib)) { + return; + } + this->gui.setDiskFile(this->slot,drive,fnib); + this->gui.setDirty(this->slot,getCurrentDriveNumber(),false); + } - void unloadDisk(unsigned char drive) - { - this->getDrive(drive).unloadDisk(); - this->gui.setDiskFile(this->slot,drive,""); - this->gui.setDirty(this->slot,getCurrentDriveNumber(),false); - } + void unloadDisk(unsigned char drive) { + this->getDrive(drive).unloadDisk(); + this->gui.setDiskFile(this->slot,drive,""); + this->gui.setDirty(this->slot,getCurrentDriveNumber(),false); + } - void save(int drive) - { - this->getDrive(drive).saveDisk(); - this->gui.setDirty(this->slot,getCurrentDriveNumber(),false); - } + void save(int drive) { + this->getDrive(drive).saveDisk(); + this->gui.setDirty(this->slot,getCurrentDriveNumber(),false); + } - bool isMotorOn() - { - return this->motor.isOn(); - } + bool isMotorOn() { + return this->motor.isOn(); + } -// const WozFile& getDiskBytes(unsigned char disk) -// { -// return this->getDrive(disk).getDiskBytes(); -// } + unsigned char getTrack() { + return this->currentDrive->getTrack(); + } - unsigned char getTrack() - { - return this->currentDrive->getTrack(); - } + bool isWriting() { + return this->write; + } - bool isWriting() - { - return this->write; - } + bool isModified() { + return this->currentDrive->isModified(); + } - bool isModified() - { - return this->currentDrive->isModified(); - } + bool isModifiedOther() { + return getOtherDrive().isModified(); + } - bool isModifiedOther() - { - return getOtherDrive().isModified(); - } + bool isWriteProtected() { + return this->currentDrive->isWriteProtected(); + } - bool isWriteProtected() - { - return this->currentDrive->isWriteProtected(); - } + bool isDirty() { + return isModified() || isModifiedOther(); + } - bool isDirty() - { - return isModified() || isModifiedOther(); - } + unsigned char getCurrentDriveNumber() { + return this->currentDrive == &this->drive1 ? 0 : 1; + } - unsigned char getCurrentDriveNumber() - { - return this->currentDrive == &this->drive1 ? 0 : 1; - } + unsigned char getOtherDriveNumber() { + return 1-getCurrentDriveNumber(); + } - unsigned char getOtherDriveNumber() - { - return 1-getCurrentDriveNumber(); - } - - virtual std::string getName() { return "disk][ drive 1 drive 2 "; } + virtual std::string getName() { + return "disk][ drive 1 drive 2 "; + } }; diff --git a/src/drive.cpp b/src/drive.cpp index 9e776f2..8270a38 100644 --- a/src/drive.cpp +++ b/src/drive.cpp @@ -16,3 +16,72 @@ along with this program. If not, see . */ #include "drive.h" +Disk2Drive::Disk2Drive(): + stepper(head), + pulse(false), + bitBufferRead(0), + generator(std::chrono::system_clock::now().time_since_epoch().count()), + distribution(0,1) { +} + +bool Disk2Drive::loadDisk(const std::string& fnib) { + return this->disk.load(fnib); +} + +void Disk2Drive::unloadDisk() { + this->disk.unload(); +} +bool Disk2Drive::isLoaded() const { + return this->disk.isLoaded(); +} + +void Disk2Drive::saveDisk() { + this->disk.save(); +} + +bool Disk2Drive::isWriteProtected() const { + return this->disk.isWriteProtected(); +} + +bool Disk2Drive::isModified() const { + return this->disk.isModified(); +} + +int Disk2Drive::position() const { + return this->head.position(); +} + +void Disk2Drive::tick() { + this->stepper.tick(); +} + +void Disk2Drive::set_phase(int i_phase_0_to_3, bool on) { + this->stepper.set_phase(i_phase_0_to_3, on); +} + +int Disk2Drive::getTrack() const { + return this->head.position() >> 2; +} + +void Disk2Drive::rotateDiskOneBit() { + this->disk.rotateOneBit(this->head.position()); + + bitBufferRead <<= 1; + bitBufferRead |= this->disk.getBit(this->head.position()); + if (bitBufferRead & 0x0Fu) { + this->pulse = (bitBufferRead & 0x02u) >> 1; + } else { + this->pulse = randomBit(); + } +} + +bool Disk2Drive::readPulse() const { + return this->pulse; +} +void Disk2Drive::clearPulse() { + this->pulse = false; +} + +void Disk2Drive::writeBit(bool on) { + this->disk.setBit(this->head.position(), on); +} diff --git a/src/drive.h b/src/drive.h index c4cb075..95f0712 100644 --- a/src/drive.h +++ b/src/drive.h @@ -23,13 +23,15 @@ #include #include #include +#include "disk2steppermotor.h" +#include "disk2readwritehead.h" #include "wozfile.h" -#include "steppermotor.h" -class Drive { +class Disk2Drive { private: - WozFile& disk; - StepperMotor& arm; + Disk2StepperMotor stepper; + Disk2ReadWriteHead head; + WozFile disk; bool pulse; std::uint8_t bitBufferRead; @@ -44,74 +46,21 @@ private: } public: - Drive(WozFile& disk, StepperMotor& arm): - disk(disk), - arm(arm), - pulse(false), - bitBufferRead(0), - generator(std::chrono::system_clock::now().time_since_epoch().count()), - distribution(0,1) { - } - - ~Drive() { - } - - bool loadDisk(const std::string& fnib) { - return this->disk.load(fnib); - } - - void unloadDisk() { - this->disk.unload(); - } - bool isLoaded() const { - return this->disk.isLoaded(); - } - - void saveDisk() { - this->disk.save(); - } - - bool isWriteProtected() const { - return this->disk.isWriteProtected(); - } - - bool isModified() const { - return this->disk.isModified(); - } - - - - void setMagnet(unsigned char q, bool on) { - this->arm.setMagnet(q,on); - } - - int getTrack() const { - return this->arm.getTrack(); - } - - - void rotateDiskOneBit() { - this->disk.rotateOneBit(this->arm.getQuarterTrack()); - - bitBufferRead <<= 1; - bitBufferRead |= this->disk.getBit(this->arm.getQuarterTrack()); - if (bitBufferRead & 0x0Fu) { - this->pulse = (bitBufferRead & 0x02u) >> 1; - } else { - this->pulse = randomBit(); - } - } - - bool readPulse() const { - return this->pulse; - } - void clearPulse() { - this->pulse = false; - } - - void writeBit(bool on) { - this->disk.setBit(this->arm.getQuarterTrack(), on); - } + Disk2Drive(); + bool loadDisk(const std::string& fnib); + void unloadDisk(); + bool isLoaded() const; + void saveDisk(); + bool isWriteProtected() const; + bool isModified() const; + int position() const; + void tick(); + void set_phase(int i_phase_0_to_3, bool on); + int getTrack() const; + void rotateDiskOneBit(); + bool readPulse() const; + void clearPulse(); + void writeBit(bool on); }; #endif diff --git a/src/magneticfield.cpp b/src/magneticfield.cpp new file mode 100644 index 0000000..309d45a --- /dev/null +++ b/src/magneticfield.cpp @@ -0,0 +1,18 @@ +#include "magneticfield.h" + +bool MagneticField::is_on() const { + return this->on; +} + +int MagneticField::position() const { + return this->pos; +} + +void MagneticField::turn_off() { + this->on = false; +} + +void MagneticField::turn_on_at(const int position) { + this->on = true; + this->pos = position; +} diff --git a/src/magneticfield.h b/src/magneticfield.h new file mode 100644 index 0000000..800a44a --- /dev/null +++ b/src/magneticfield.h @@ -0,0 +1,17 @@ +#ifndef MAGNETICFIELD_H +#define MAGNETICFIELD_H + +class MagneticField { +public: + bool is_on() const; + int position() const; + + void turn_off(); + void turn_on_at(const int position); + +private: + bool on = false; + int pos = 0; +}; + +#endif // MAGNETICFIELD_H diff --git a/src/movable.cpp b/src/movable.cpp new file mode 100644 index 0000000..b78986a --- /dev/null +++ b/src/movable.cpp @@ -0,0 +1,3 @@ +#include "movable.h" + +Movable::~Movable() = default; diff --git a/src/movable.h b/src/movable.h new file mode 100644 index 0000000..61abf35 --- /dev/null +++ b/src/movable.h @@ -0,0 +1,11 @@ +#ifndef MOVABLE_H +#define MOVABLE_H + + +class Movable { +public: + virtual ~Movable(); + virtual bool move_by(int delta_position) = 0; +}; + +#endif // MOVABLE_H diff --git a/src/steppermotor.cpp b/src/steppermotor.cpp deleted file mode 100644 index 654e7cd..0000000 --- a/src/steppermotor.cpp +++ /dev/null @@ -1,136 +0,0 @@ -/* - epple2 - - Copyright © 2008–2018, Christopher Alan Mosher, Shelton, CT, USA. - - 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 3 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 . -*/ -/** - * Emulates the arm stepper motor in the Disk ][. - * - * @author Chris Mosher - */ -/* - - -mags ps magval -3210 ----- -- ------ - -Each postition is a quarter track. -One complete cycle through the 4 phases will -move the arm 2 tracks. (UA2, 9-7.) -0001 0 1 -0011 1 3 -0010 2 2 -0110 3 6 -0100 4 4 -1100 5 C -1000 6 8 -1001 7 9 - -strange, but still defined -1011 0 B -0111 2 7 -1110 4 E -1101 6 D - -all off (no movement) -0000 ? 0 - -undefined -0101 ? 5 // <-- TODO pick one at random? -1010 ? A // <-- TODO pick one at random? -1111 ? F // TODO what to do here? -*/ -#include "steppermotor.h" -#include "util.h" -#include - -StepperMotor::StepperMotor(): - quarterTrack(QTRACKS >> 1), // start in the middle of the disk... just for fun -// TODO if we want to be extremely accurate, we should save each arm's position on shutdown and restore on startup -// (because in the real-life Apple ][, the arm stays in the same position when powered off). - pos(0), - mags(0), - pendingPos(0), - pendingTicks(0) { -} - -StepperMotor::~StepperMotor() { -} - -std::int8_t StepperMotor::mapMagPos[] = {-1,0,2,1,4,-1,3,2,6,7,-1,0,5,6,4,-1}; - -void StepperMotor::calculateTrack(const std::int8_t delta) { - std::int16_t q = this->quarterTrack; - q += delta; - if (q < 0) { - q = 0; - } else if (QTRACKS <= q) { - q = QTRACKS-1; - } - this->quarterTrack = static_cast(q); -} - -void StepperMotor::moveCog() { - if (this->pendingPos >= 0) { - calculateTrack(calcDeltaPos(this->pos,this->pendingPos)); - this->pos = this->pendingPos; - } -} - -void StepperMotor::setMagnet(const std::uint8_t magnet, const bool on) { - const std::uint8_t mask = static_cast(1u << magnet); - if (on) { - this->mags |= mask; - } else { - this->mags &= ~mask; - } - - // set magnets (above), but delay actual movement of the stepper - // motor cog, to emulate force of inertia while trying to move it - // This allows Locksmith to write on the quarter-track, for example. - - this->pendingPos = mapMagPos[this->mags]; - this->pendingTicks = 1000; // about 1 millisecond -} - -void StepperMotor::tick() { - if (this->pendingTicks) { - --this->pendingTicks; - if (!this->pendingTicks) { - moveCog(); - } - } -} - - - -// TODO fix logging (due to new delayed movement algorithm) -// const std::uint8_t oldQT = this->quarterTrack; -//... -// const std::uint8_t newQT = this->quarterTrack; -// const std::int8_t deltaQT = newQT - oldQT; -// printf("ARM: ph%d %s [%c%c%c%c] T$%02X.%02d %s %+0.2f\n", -// (std::uint8_t)magnet, -// on ? "+" : "-", -// (mags&1)?'*':'.', -// (mags&2)?'*':'.', -// (mags&4)?'*':'.', -// (mags&8)?'*':'.', -// this->quarterTrack / 4, -// (this->quarterTrack % 4) * 25, -// deltaQT>0 ? "-->" : deltaQT<0 ? "<--" : " ", -// (deltaQT % 4) / 4.0); diff --git a/src/steppermotor.h b/src/steppermotor.h deleted file mode 100644 index 0da0765..0000000 --- a/src/steppermotor.h +++ /dev/null @@ -1,69 +0,0 @@ -/* - epple2 - Copyright (C) 2008 by Christopher A. Mosher - - 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 3 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 . -*/ -#ifndef STEPPERMOTOR_H -#define STEPPERMOTOR_H - -#include - -class StepperMotor { -private: - enum { QTRACKS = 160 }; - // quarter track: 0=t0, 1=t0.25, 2=t0.5, 3=t0.75, 4=t1, ... 140=t35.00 ... 159=t39.75 - // (see TMAP in WOZ2 file format spec) - std::uint8_t quarterTrack; - - std::int8_t pos; - std::uint8_t mags; - - std::int8_t pendingPos; - std::uint32_t pendingTicks; - - static std::int8_t mapMagPos[]; - - void moveCog(); - void calculateTrack(const std::int8_t delta); - - static std::int8_t calcDeltaPos(const std::int8_t cur, const std::int8_t next) { - std::int8_t d = next-cur; // -7 to +7 - - if (d == 4 || d == -4) { - d = 0; // <--- TODO pick random direction? - } else if (4 < d) { - d -= 8; - } else if (d < -4) { - d += 8; - } - - return d; - } - -public: - StepperMotor(); - ~StepperMotor(); - - void setMagnet(const std::uint8_t magnet, const bool on); - std::uint8_t getTrack() { - return this->quarterTrack >> 2; - } - std::uint8_t getQuarterTrack() { - return this->quarterTrack; - } - void tick(); -}; - -#endif diff --git a/src/wozfile.h b/src/wozfile.h index ff632fa..d475e8c 100644 --- a/src/wozfile.h +++ b/src/wozfile.h @@ -112,21 +112,21 @@ public: bool load(const std::string& filePath); - std::string getFileName() { + std::string getFileName() const { return this->fileName; } - bool isLoaded() { + bool isLoaded() const { return this->loaded; } void save(); void unload(); - bool isWriteProtected() { + bool isWriteProtected() const { return !this->writable; } - bool isModified() { + bool isModified() const { return this->modified; }