2019-06-23 01:21:27 +00:00
|
|
|
/*
|
|
|
|
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
|
2022-12-05 03:45:35 +00:00
|
|
|
polarity of the top/bottom of the can. Each top/bottom has fingers
|
2019-06-23 01:21:27 +00:00
|
|
|
bending down/up towards the center, the fingers from alternate surfaces
|
2022-12-05 03:45:35 +00:00
|
|
|
being interlaced. These fingers transfer the polarity from the coil.
|
2019-06-23 01:21:27 +00:00
|
|
|
|
|
|
|
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.
|
|
|
|
|
2022-12-05 03:45:35 +00:00
|
|
|
PH = phase 0-3 (on or off)
|
2019-06-23 01:21:27 +00:00
|
|
|
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);
|
2022-12-05 03:45:35 +00:00
|
|
|
return 0; // quell compiler warning "control reaches end of non-void function"
|
2019-06-23 01:21:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Disk2StepperMotor::tick() {
|
|
|
|
this->rotor.tick(this->field);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2019-06-23 02:24:21 +00:00
|
|
|
// everything below here is for unit testing only
|
2019-06-23 01:21:27 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2019-06-23 02:24:21 +00:00
|
|
|
static int test_move(int p_from, int p_to) {
|
|
|
|
SpyMovable moved;
|
2019-06-23 01:21:27 +00:00
|
|
|
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},
|
|
|
|
};
|
|
|
|
|
2019-06-23 02:24:21 +00:00
|
|
|
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");
|
2019-06-23 01:21:27 +00:00
|
|
|
|
|
|
|
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];
|
2019-06-23 02:24:21 +00:00
|
|
|
int actual = test_move(phase[i_from],phase[i_to]);
|
2019-06-23 01:21:27 +00:00
|
|
|
std::printf(
|
|
|
|
"%1X->%1X?%+2d:%+2d=%s ",
|
2019-06-23 02:24:21 +00:00
|
|
|
phase[i_from],phase[i_to],expected,actual,expected==actual?".":"*");
|
2019-06-23 01:21:27 +00:00
|
|
|
if (actual != expected) {
|
|
|
|
bad = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
std::printf("\n");
|
|
|
|
}
|
|
|
|
|
|
|
|
return !bad;
|
|
|
|
}
|