refactor Disk ][ stepper motor emulation based on hardware teardown
This commit is contained in:
parent
66a4180747
commit
f310bae003
|
@ -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 \
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
epple2
|
||||
|
||||
Copyright © 2019, Christopher Alan Mosher, Shelton, CT, USA. <cmosher01@gmail.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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;
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
epple2
|
||||
|
||||
Copyright © 2019, Christopher Alan Mosher, Shelton, CT, USA. <cmosher01@gmail.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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
|
|
@ -0,0 +1,396 @@
|
|||
/*
|
||||
epple2
|
||||
|
||||
Copyright © 2019, Christopher Alan Mosher, Shelton, CT, USA. <cmosher01@gmail.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
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 <cassert>
|
||||
#include <cstdio>
|
||||
|
||||
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;
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
epple2
|
||||
|
||||
Copyright © 2019, Christopher Alan Mosher, Shelton, CT, USA. <cmosher01@gmail.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
epple2
|
||||
|
||||
Copyright © 2019, Christopher Alan Mosher, Shelton, CT, USA. <cmosher01@gmail.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "disk2steppermotorcan.h"
|
||||
#include <cassert>
|
||||
|
||||
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));
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
epple2
|
||||
|
||||
Copyright © 2019, Christopher Alan Mosher, Shelton, CT, USA. <cmosher01@gmail.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
epple2
|
||||
|
||||
Copyright © 2019, Christopher Alan Mosher, Shelton, CT, USA. <cmosher01@gmail.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
epple2
|
||||
|
||||
Copyright © 2019, Christopher Alan Mosher, Shelton, CT, USA. <cmosher01@gmail.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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
|
|
@ -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
|
||||
|
|
|
@ -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 ";
|
||||
}
|
||||
};
|
||||
|
|
|
@ -16,3 +16,72 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#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);
|
||||
}
|
||||
|
|
93
src/drive.h
93
src/drive.h
|
@ -23,13 +23,15 @@
|
|||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#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
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
#include "movable.h"
|
||||
|
||||
Movable::~Movable() = default;
|
|
@ -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
|
|
@ -1,136 +0,0 @@
|
|||
/*
|
||||
epple2
|
||||
|
||||
Copyright © 2008–2018, Christopher Alan Mosher, Shelton, CT, USA. <cmosher01@gmail.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/**
|
||||
* 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 <iostream>
|
||||
|
||||
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<std::uint8_t>(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<std::uint8_t>(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);
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
epple2
|
||||
Copyright (C) 2008 by Christopher A. Mosher <cmosher01@gmail.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef STEPPERMOTOR_H
|
||||
#define STEPPERMOTOR_H
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
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
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue