/* epple2 Copyright © 2019, Christopher Alan Mosher, Shelton, CT, USA. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY, without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ /* See UA2, 9-7. The following is based on research, experimentation, and other help from John Morris, Tom Greene, Michael Guidero, and Lane Roathe. Stepper motor has 2 cans each with a center-tapped coil, allowing for current flow in one of either direction, causing a N/S or S/N polarity 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 #include Disk2StepperMotor::Disk2StepperMotor(Movable& movable): can_0(0, 2), can_1(1, 2), rotor(2, movable) { } void Disk2StepperMotor::set_phase(int p, const bool on) { assert(0 <= p && p < 4); Disk2StepperMotorCan *can; if ((p&1) == 0) { can = &this->can_0; } else { can = &this->can_1; } can->set_lead(p/2, on); build_magnetic_field(); this->rotor.pend_rotation(); } void Disk2StepperMotor::build_magnetic_field() { if (magnetized()) { this->field.turn_on_at(magnetic_position()); } else { this->field.turn_off(); } } bool Disk2StepperMotor::magnetized() const { return this->can_0.magnetized() || this->can_1.magnetized(); } int Disk2StepperMotor::magnetic_position() const { assert(magnetized()); if (this->can_0.magnetized() && this->can_1.magnetized()) { if (this->can_1.magnetic_position()==6 && this->can_0.magnetic_position()==0) { // special case at our "seam" return 7; } // average position between poles of both cans: return (this->can_0.magnetic_position()+this->can_1.magnetic_position())/2; } if (this->can_0.magnetized()) { return this->can_0.magnetic_position(); } if (this->can_1.magnetized()) { return this->can_1.magnetic_position(); } assert(false); 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; }