Epple-II/src/disk2steppermotor.cpp

397 lines
13 KiB
C++

/*
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 of the top/bottom of the can. Each top/bottom has fingers
bending down/up towards the center, the fingers from alternate surfaces
being interlaced. 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-3 (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);
return 0; // quell compiler warning "control reaches end of non-void function"
}
void Disk2StepperMotor::tick() {
this->rotor.tick(this->field);
}
// everything below here is for unit testing only
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(int p_from, int p_to) {
SpyMovable moved;
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 : +/-actual_move = result [.=pass; *=fail]");
std::printf(" %s\n", " 9 8 D C 4 E 6 2 7 3 1 B 0 5 A F");
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(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;
}