Jump to content

Stepper Motor Zeroing


CorporalCarrot

Recommended Posts

Hi,

 

I'm researching the use of stepper motors for gauges. I've built an A-10 engine panel using servo's... It works, but is a bit clunky as I've bought cheap servo's...

 

I have some x27.168's on the way and looking through the code it shouldn't be difficult to get them to work, but I'm stumped how they are zero'd? As a stepper motor, there is no positional feedback, so how does the code find zero? Lot's of people seem to use the code WarHog wrote with Ian, and there are a few lines in the code regarding zeroing on start up, but I'm struggling to understand how it works. Can someone enlighten me, please?

 

It looks like it is reading a zero from another pin, but I'm not clear how this is working? Is a micro switch used, or perhaps an optical switch sending the pin to ground?

 

Thanks in advance...

 

#define DCSBIOS_IRQ_SERIAL

#include <AccelStepper.h>
#include "DcsBios.h"

struct StepperConfig {
 unsigned int maxSteps;
 unsigned int acceleration;
 unsigned int maxSpeed;
};


class Vid60Stepper : public DcsBios::Int16Buffer {
 private:
   AccelStepper& stepper;
   StepperConfig& stepperConfig;
   inline bool zeroDetected() { return digitalRead(irDetectorPin) == 1; }
   unsigned int (*map_function)(unsigned int);
   unsigned char initState;
   long currentStepperPosition;
   long lastAccelStepperPosition;
   unsigned char irDetectorPin;
   long zeroOffset;
   bool movingForward;
   bool lastZeroDetectState;

   long normalizeStepperPosition(long pos) {
     if (pos < 0) return pos + stepperConfig.maxSteps;
     if (pos >= stepperConfig.maxSteps) return pos - stepperConfig.maxSteps;
     return pos;
   }

   void updateCurrentStepperPosition() {
     // adjust currentStepperPosition to include the distance our stepper motor
     // was moved since we last updated it
     long movementSinceLastUpdate = stepper.currentPosition() - lastAccelStepperPosition;
     currentStepperPosition = normalizeStepperPosition(currentStepperPosition + movementSinceLastUpdate);
     lastAccelStepperPosition = stepper.currentPosition();
   }
 public:
   Vid60Stepper(unsigned int address, 
   AccelStepper& stepper, 
   StepperConfig& stepperConfig, 
   unsigned char irDetectorPin, 
   long zeroOffset, unsigned int (*map_function)(unsigned int)): Int16Buffer(address), 
   stepper(stepper), 
   stepperConfig(stepperConfig), 
   irDetectorPin(irDetectorPin), 
   zeroOffset(zeroOffset), 
   map_function(map_function), 
   initState(0), 
   currentStepperPosition(0), 
   lastAccelStepperPosition(0) 
   {
   }

   virtual void loop() {
     if (initState == 0) { // not initialized yet
       pinMode(irDetectorPin, INPUT);
       stepper.setMaxSpeed(stepperConfig.maxSpeed);
       stepper.setSpeed(1000);
       
       initState = 1;
     }
     if (initState == 1) {
       // move off zero if already there so we always get movement on reset
       // (to verify that the stepper is working)
       if (zeroDetected()) {
         stepper.runSpeed();
       } else {
           initState = 2;
       }
     }
     if (initState == 2) { // zeroing
       if (!zeroDetected()) {
         stepper.runSpeed();
       } else {
           stepper.setAcceleration(stepperConfig.acceleration);
           stepper.runToNewPosition(stepper.currentPosition() + zeroOffset);
           // tell the AccelStepper library that we are at position zero
           stepper.setCurrentPosition(0);
           lastAccelStepperPosition = 0;
           // set stepper acceleration in steps per second per second
           // (default is zero)
           stepper.setAcceleration(stepperConfig.acceleration);
           
           lastZeroDetectState = true;
           initState = 3;
       }
     }
     if (initState == 3) { // running normally
       
       // recalibrate when passing through zero position
       bool currentZeroDetectState = zeroDetected();
       if (!lastZeroDetectState && currentZeroDetectState && movingForward) {
         // we have moved from left to right into the 'zero detect window'
         // and are now at position 0
         lastAccelStepperPosition = stepper.currentPosition();
         currentStepperPosition = normalizeStepperPosition(zeroOffset);
       } else if (lastZeroDetectState && !currentZeroDetectState && !movingForward) {
         // we have moved from right to left out of the 'zero detect window'
         // and are now at position (maxSteps-1)
         lastAccelStepperPosition = stepper.currentPosition();
         currentStepperPosition = normalizeStepperPosition(stepperConfig.maxSteps + zeroOffset);
       }
       lastZeroDetectState = currentZeroDetectState;
       
       
       if (hasUpdatedData()) {
           // convert data from DCS to a target position expressed as a number of steps
           long targetPosition = (long)map_function(getData());

           updateCurrentStepperPosition();
           
           long delta = targetPosition - currentStepperPosition;
           
           // if we would move more than 180 degree counterclockwise, move clockwise instead
           if (delta < -((long)(stepperConfig.maxSteps/2))) delta += stepperConfig.maxSteps;
           // if we would move more than 180 degree clockwise, move counterclockwise instead
           if (delta > (stepperConfig.maxSteps/2)) delta -= (long)stepperConfig.maxSteps;

           movingForward = (delta >= 0);
           
           // tell AccelStepper to move relative to the current position
           stepper.move(delta);
           
       }
       stepper.run();
     }
   }
};

/* modify below this line */

/* define stepper parameters
  multiple Vid60Stepper instances can share the same StepperConfig object */
struct StepperConfig stepperConfig = 
 {
 400,  // maxSteps
 2200, // maxSpeed
 1000 // acceleration
 };


// define AccelStepper instance
AccelStepper stepper(AccelStepper::DRIVER, 9, 8);
// define Vid60Stepper class that uses the AccelStepper instance defined in the line above
//           v-- arbitrary name
Vid60Stepper alt100ftPointer(0x107e,          // address of stepper data
                            stepper,         // name of AccelStepper instance
                            stepperConfig,   // StepperConfig struct instance
                            11,              // IR Detector Pin (must be HIGH in zero position)
                            0,               // zero offset
                            [](unsigned int newValue) -> unsigned int {
 /* this function needs to map newValue to the correct number of steps */
 return map(newValue, 0, 65535, 0, stepperConfig.maxSteps-1);
                                                          });


void setup() {
 DcsBios::setup();
 pinMode(13, OUTPUT);
}

void loop() {
 PORTB |= (1<<5);
 PORTB &= ~(1<<5);
 DcsBios::loop();

Link to comment
Share on other sites

If it's the same as mine, and HanSolo is the real guru for this, apparently the logic in the script makes it run all the way to the stop in one direction, and that sets the zero. Every time I start mine, it does this, so the clever chaps who wrote the script made it an automated zero process.

 

Cheers

 

Les

Link to comment
Share on other sites

The clue might be here

 

inline bool zeroDetected() { return digitalRead(irDetectorPin) == 1; }

 

 

 

irDetectorPin will return a 1 or a zero depending shaft position from some form of opto and an interrupter or IR detector. It could be a switch but ir likely indicates it's and infer red optical device of some sort.

 

The code looks for a zero position presumably the position of an interrupter or mark on the stepper shaft it may well be an integral feature (component assembly) on the stepper. This provides rotational position information which the initialisation code runs through to set the shaft zero position and sets the stepper motor state.

 

 

 

Something like this only as an example

Control is an illusion which usually shatters at the least expected moment.

Gazelle Mini-gun version is endorphins with rotors. See above.

 

Currently rolling with a Asus Z390 Prime, 9600K, 32GB RAM, SSD, 2080Ti and Windows 10Pro, Rift CV1. bu0836x and Scratch Built Pedals, Collective and Cyclic.

Link to comment
Share on other sites

Thanks Chaps,

 

Just to dig a little deeper, it seems this is the key line of code:

 

inline bool zeroDetected() { return digitalRead(irDetectorPin) == 1; }

 

I was about to ask whether I should replace 'irDetectorPin' with the pin no. of the sensor, as I couldn't see this setup in the code before this line (which is what I was expecting), but I've just noticed the following code towards the end:

 

Vid60Stepper alt100ftPointer(0x107e,          // address of stepper data
                            stepper,         // name of AccelStepper instance
                            stepperConfig,   // StepperConfig struct instance
                            11,              // IR Detector Pin (must be HIGH in zero position)
                            0,               // zero offset
                            [](unsigned int newValue) -> unsigned int {
 /* this function needs to map newValue to the correct number of steps */
 return map(newValue, 0, 65535, 0, stepperConfig.maxSteps-1);
                                                          });

 

I presume this code sets up the zeroing device on the input of pin 11 of the Arduino? The 'zero offset' seems to be a metric if the zeroing device isn't actually at the zero position? Is this a number of steps? Finally, in a few gauges I'm making the pointer will need to be limited to 180 degrees of sweep. I can see this code runs the zeroing routine;

 

      if (initState == 2) { // zeroing
       if (!zeroDetected()) {
         stepper.runSpeed();
       } else {
           stepper.setAcceleration(stepperConfig.acceleration);
           stepper.runToNewPosition(stepper.currentPosition() + zeroOffset);
           // tell the AccelStepper library that we are at position zero
           stepper.setCurrentPosition(0);
           lastAccelStepperPosition = 0;
           // set stepper acceleration in steps per second per second
           // (default is zero)
           stepper.setAcceleration(stepperConfig.acceleration);

 

Will the stepper motor always be driven in the same direction (i.e. clockwise)? In which case I would imagine that the zeroing sensor needs to be towards the fully clockwise extreme of needle sweep to ensure that the position is sensed every time and doesn't go off and foul the end-stop or another gauge...?

 

Space is limited in my setup, which isn't a replica cockpit, but a pit I share with my F1 wheel (I use the TS-PC pedals as rudders!), so I've ordered some hall effect sensors and some tiny magnets, and I'm going to try to 3D print a gauge pointer with an integrated boss on the rear face for the magnet to superglued in place and then mount the hall effect sensor behind the gauge panel (3mm opal acrylic) and hope the magnet is strong enough to trip the sensor... The magnets are neodymium with a pull of 0.1kg, so I'm expecting this is enough...

 

Will keep you posted on how I get on...

Link to comment
Share on other sites

  • 4 weeks later...

Panel Finished!

 

Hi Guys,

 

Just a quick update with my fuel/flap panel using stepper motors. I ended up using some cheap 28BYJ-48 steppers for the flaps and fuel gauges. They're not paticuarily fast but they're fast enough for these gauges. I used a hall sensor on each gauge with a tiny 2mm diameter / 1mm thick neodymium magnet superglued to the 3D printed pointer shaft for zeoing/calibration. I'[m pretty happy with this panel, but frustrated with the back lighting on the flap gauge. Due to some physical structure of my rig, I had to put the back lights really close to the panel (petty much touching), resulting in an uneven light distribution. This is the panel booting up, zeroing the steppers and then me using DCS Bios debug too to move the fuel gauges;

 

 

The left and right fuel gauges are running off 1 Arduino nano, and the flap gauge, fuel selector knob, test button and digital fuel display is run off a second nano. As you can see, 1 nano is able to move two steppers at once with no issues. The whole board is connected to the PC via an RS485 netword using a single cat5e cable.

 

I ended up writing my own code, more or less from scatch. I found the zeroing/cal routine a little cumbersome, and didn't use the more efficient 'move quick to the sensor and then move off slowly' to get a fine calibration. Here's my code for the nano controlling both fuel gauges;

 

#define DCSBIOS_RS485_SLAVE 14
#define TXENABLE_PIN 2

#include <AccelStepper.h>
#include "DcsBios.h"

/* L Fuel Gauge */
AccelStepper stepper1(AccelStepper::HALF4WIRE, 7, 9, 8, 10);

long initial_homingL=-1;
int pinposL = 0;

void onFuelQtyLChange(unsigned int newValue) {
 pinposL = map (newValue, 0, 65535, 0, 1920);
 stepper1.moveTo(pinposL);
}
DcsBios::IntegerBuffer fuelQtyLBuffer(0x10ca, 0xffff, 0, onFuelQtyLChange);


/* R Fuel Gauge */
AccelStepper stepper2(AccelStepper::HALF4WIRE, 3, 5, 4, 6);

long initial_homingR=-1;
int pinposR = 0;

void onFuelQtyRChange(unsigned int newValue) {
 pinposR = map (newValue, 0, 65535, 1920, 0);
 stepper2.moveTo(pinposR);
}
DcsBios::IntegerBuffer fuelQtyRBuffer(0x10cc, 0xffff, 0, onFuelQtyRChange);

void setup() {
 
 /* Left Cal */
 
 pinMode(11,INPUT_PULLUP);
 
 stepper1.setMaxSpeed(600.0);
 stepper1.setAcceleration(1000.0);
 
 stepper1.move(150);
 stepper1.runToPosition();
 stepper1.move(-1000000);
 while (!digitalRead(11)) {  // Make the Stepper move CCW until the switch is activated   
 stepper1.run();  // Start moving the stepper

}

 stepper1.setCurrentPosition(0);  // Set the current position as zero for now
 stepper1.setMaxSpeed(100.0);      // Set Max Speed of Stepper (Slower to get better accuracy)
 stepper1.setAcceleration(100.0);  // Set Acceleration of Stepper
 initial_homingL=-1;
 delay (1000);

 while (digitalRead(11)) { // Make the Stepper move CW until the switch is deactivated
   stepper1.moveTo(initial_homingL);  
   stepper1.run();
   initial_homingL--;

 }
 stepper1.setMaxSpeed(600.0);
 stepper1.setAcceleration(1000.0);
 stepper1.move(908);
 stepper1.runToPosition();
 stepper1.setCurrentPosition(960);
 
 /* Right Cal */
 pinMode(12,INPUT_PULLUP);
 
 stepper2.setMaxSpeed(600.0);
 stepper2.setAcceleration(1000.0);
 
 stepper2.move(-500);
 stepper2.runToPosition();
 
 stepper2.move(1000000);
 while (!digitalRead(12)) {  // Make the Stepper move CCW until the switch is activated   
 stepper2.run();  // Start moving the stepper
}

 stepper2.setCurrentPosition(0);  // Set the current position as zero for now
 stepper2.setMaxSpeed(100.0);      // Set Max Speed of Stepper (Slower to get better accuracy)
 stepper2.setAcceleration(100.0);  // Set Acceleration of Stepper
 initial_homingR=1;
 delay (1000);

 while (digitalRead(12)) { // Make the Stepper move CW until the switch is deactivated
   stepper2.moveTo(initial_homingR);  
   stepper2.run();
   initial_homingR++;

 }
 stepper2.setMaxSpeed(600.0);
 stepper2.setAcceleration(1000.0);
 stepper2.move(-945);
 stepper2.runToPosition();
 stepper2.setCurrentPosition(960);

 DcsBios::setup();
}

void loop() {
 DcsBios::loop();
 stepper1.run();
 stepper2.run();
}

 

Things I would do differently for the next time;

 

1) Either drill a hole in the pointer, or put one in the print, for the magnet to sit flush with with the surface, meaning potentially better alignment of the magnet - I had to use trial and error to get the right spot and then use tweezers to drop it in place.

 

2) I notice these steppers drift a little on the zeroing. I'm not sure whether it is the hall sensors or the steppers, but sometimes it's a little out (around 2 or 3 degrees). There are free analogue pins on the Nano, that I could use trim pots for the zeroing and put a tiny hole in the front panel to access them. Either use 1 trim pot to set the neutral postion (50% FSD) or could use 2 trim pots to set either end of the scale within the map function. i.e.

 

pinposL = map (newValue, 0, 65535, A, B)

 

where A and B are values detemined from the trim pot.

 

3) Use male pin headers! I was lazy and used female pin headers ('cos I find it easier to assemble male connectors), but they are weaker than male pin headers. I had to repair my PCB around several headers because of this!

 

4) Make sure there is space to backlight the switch tags, instead of using printed labels!

 

This is my last A-10C panel, meaning once it is installed, the layout is complete. It's not a replica cockpit, but something usable in the space I had. However I now have cleared some other space in my house and work on a 1:1 replica of an F/A-18 cockpit using a curved screen and projectors starts in the new year!

 

See you in the skies!

 

Cpl. Carrot. :pilotfly:


Edited by CorporalCarrot
Link to comment
Share on other sites

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...