refactor Disk ][ stepper motor emulation based on hardware teardown

This commit is contained in:
Christopher Mosher 2019-06-22 21:21:27 -04:00
parent 66a4180747
commit f310bae003
20 changed files with 1019 additions and 406 deletions

View File

@ -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 \

View File

@ -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;
}

66
src/disk2readwritehead.h Normal file
View File

@ -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

396
src/disk2steppermotor.cpp Normal file
View File

@ -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;
}

50
src/disk2steppermotor.h Normal file
View File

@ -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

View File

@ -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));
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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

View File

@ -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

View File

@ -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 ";
}
};

View File

@ -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);
}

View File

@ -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

18
src/magneticfield.cpp Normal file
View File

@ -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;
}

17
src/magneticfield.h Normal file
View File

@ -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

3
src/movable.cpp Normal file
View File

@ -0,0 +1,3 @@
#include "movable.h"
Movable::~Movable() = default;

11
src/movable.h Normal file
View File

@ -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

View File

@ -1,136 +0,0 @@
/*
epple2
Copyright © 20082018, 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);

View File

@ -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

View File

@ -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;
}