Microscope Z-Stage knob controlled by serial dialogue
Click on the pictures to enlarge


Circuito (disegnato con Fritzing)

Circuit (designed with Fritzing)

Obiettivi e Software di controllo


La fotomicografia e la macrofotografia beneficiano notevolmente della tecnica del focus stacking. Parlando di microscopi, le pile di immagini sono quasi esclusivamente realizzate lungo l'asse verticale. Numerose immagini vengono scattate spostando il piano focale in piccoli incrementi verticali dopo ogni scatto, di solito ruotando la manopola di messa a fuoco del microscopio. Focheggiare manualmente è difficile da due punti di vista: ottenere incrementi abbastanza piccoli e ottenere il tipo di incrementi uguali e uniformi che garantiscono la migliore qualità dell'immagine successivamente ottenuta dal focus stacking.
Il motore passo-passo può fornire la combinazione necessaria di precisione e coerenza. Sono disponibili colonne di focalizzazione motorizzate per microscopi, ma sono molto costose e sono specifiche per ogni marca e modello. Una soluzione più economica, ma costosa, può essere fornita dai tavolini traslatori verticali motorizzati. La proposta qui descritta approssima un tavolino traslatore verticale motorizzato, e può essere controllata tramite Arduino Serial Monitor, collegando Arduino al PC come di consueto, tramite il cavo USB.
Dal punto di vista del software, dato che il sistema non comprende sensori di fine corsa, non è possibile prevenire azionamenti erronei: occorrerà un'attenta vigilanza quando il tavolino traslatore si approssima alle sue posizioni estreme, per evitare che il motore passo-passo si danneggi sforzando senza potere avanzare.

Objectives and Control Software


Photomicography and macrophotography benifit greatly from the technique of focus stacking. Talking about microscopes, stacks of pictures are almost exclusively made along the vertical axis. Numerous pictures are taken by moving the focal plane in small vertical increments after each capture, usually by turning the focus knob of the microscope. Manual focusing is difficult from two points of view: obtaining increments small enough, and obtaining the kind of equal, consistent increments that grant the best quality of the image subsequently obtained by focus stacking.
Stepper motor may provide the needed combination of precision and consistency. Microscope motorized focusing columns are available, but they are very costly and are specific for each brand and model. A cheaper, yet costly, solution may be provided by motorized Z-stages. The proposal described here approximates a motorized Z-stage, and can be controlled via the Arduino Serial Monitor, by connecting Arduino to the PC as usual, via the USB cable.
From the software point of view, since the system does not include limit switch sensors, it is not possible to prevent erroneous operations: careful vigilance will be required when the moving stage approaches its extreme positions, to prevent the stepper motor getting damaged by straining without being able to drive.

Supporto del motore passo-passo


La soluzione qui descritta è rapida, sporca ed esteticamente non raffinata. La sua efficacia contrasta nettamente con il suo aspetto rudimentale. Considerando che un tavolino per microscopio motorizzato può costare centinaia di euro, si può sospendere il giudizio fino al completamento della costruzione.
La soluzione si basa su un piccolo stepper che aziona il pomello di un traslatore verticale meccanico, come si farebbe con le dita. Pertanto, l'accoppiamento meccanico tra motore e manopola deve essere realizzato con un manicotto di dimensioni adeguate. Il manicotto sarà collegato allo stepper grazie a un Pololu Universal Aluminum Mounting Hub for 5mm Shaft.
Inutile dire che qualsiasi tentativo di automazione di un traslatore verticale richiede che esso sia montato nella posizione più bassa, sotto gli stadi X e Y, altrimenti la sua posizione potrebbe variare in modo imprevedibile. Ma avere lo stadio traslatore verticale a diretto contatto con il supporto del microscopio non significa necessariamente che il relativo pomello mantenga una posizione fissa: non possiamo dare per scontato che il nostro stepper possa restare fisso e mantenere una posizione statica quando attivato.
Sarebbe il caso dei più sofisticati traslatori, basati su ingranaggi a vite senza fine, in cui il pomello rimane statico mentre gli stadi meccanici vengono alzati o abbassati. Ma la soluzione qui proposta riguarda lo scenario peggiore, in cui il pomello si allunga o accorcia quando viene ruotato nei due sensi. Per quanto possa sembrare poco elegante, la necessità di seguire il pomello richiede che lo stepper venga trascinato avanti e indietro dal pomello. Di conseguenza, abbiamo bisogno di un supporto che possa sostenere il motore in modo sicuro, ma scorrere liberamente sulla superficie del supporto da tavolo e abbastanza pesante da resistere alla modesta coppia esercitata dal motore mentre fa girare il pomello.
La soluzione più semplice è fornita da una squadretta metallica ad angolo retto.

Stepper support


The solution described here is quick, dirty and esthetically unrefined. Its effectiveness contrasts sharply with its rudimentary look. Considering that a motorized microscope stage may cost hundreds of dollars, one might suspend judgment until the construction is completed.
The solution is based on a small stepper driving the knob of a mechanical Z-stage, in the same way that one would do with his/her fingers. Thus, the mechanical coupling between motor and knob must be made with a small sleeve of suitable size. The sleeve will be fixed to the stepper thanks to a Pololu Universal Aluminum Mounting Hub for 5mm Shaft.
Needless to say, any attempt at the automation of a Z-stage requires that it is mounted in the lowest position, under the X and Y stages - otherwise its position may vary unpredictably. But having the Z-stage in direct contact with the microscope stand does not necessarily mean that its knob keeps a fixed position: we cannot take for granted that our stepper can be fixed and keep a static position when activated.
That would be the case of the more sophisticated Z-stages, based on worm gears, where the knob remains static while the mechanical stage is raised or lowered. But the solution proposed here deals with the worst scenario, where the knob gets longer or shorter when turned in each direction. Inelegant as it may seem, the need to follow the knob requires that the stepper is dragged back and forth by the knob.
Consequently, we need an assembly that can hold the motor firmly, yet slide freely on the surface of the table stand, and heavy enough to resist the modest torque exerted by the motor when turning the knob. The easiest solution is provided by a right-angled metal brace.

Connessione tra Arduino e motore passo-passo


La squadretta angolare giacerà (e slitterà) sul tavolino del microscopio come mostrato dalla fotografia.

Connection between Arduino and stepper motor


The angled brace will lay (and slide) on the microscope table stand as illustrated in the picture.

Componenti della manica di accoppiamento con il pomello


L'immagine è autoesplicativa e mostra il lato interno dei componenti del manicotto.

Components of the knob-coupling sleeve


The picture is self-explanatory and shows the inner side of the sleeve components.

Componenti della manica di accoppiamento con il pomello


L'immagine è autoesplicativa e mostra il lato esterno dei componenti del manicotto.

Components of the knob-coupling sleeve


The picture is self-explanatory and shows the outer side of the sleeve components.

Dettaglio dell'accoppiamento meccanico


L'immagine è autoesplicativa e mostra il manicotto montato tra motore passo-passo e pomello.

Detail of the mechanical copuling


The picture is self-explanatory and shows the sleeve mounted between the stepper and the knob.

Setup completo


Ecco il setup completo, pronto per l'azione.

Full setup


Here is the complete setup, ready for action.

Source code



/*
 
Stepper-based microscope Z-stage

Circuit and comments: 
See http://www.cesarebrizio.it/Arduino/Z-Stage.html
Circuit is as illustrated here:
https://www.tdegypt.com/wp-content/uploads/2017/08/stepper-motor-wiring.png
the only exception being that the sketch uses digital outputs 4 - 5 - 6 - 7
while the Fritzing diagram uses digital outputs 8 - 9 - 10 - 11
 
 created 9 Jan 2021
 modified ----
 by Cesare Brizio
 
This example code is in the public domain.

This sketch controls the rotation of the knob of a microscope Z-stage 
based on a stepper motor and custom-made mechanical coupling.
Stepper model is 28BYJ48, controlled via the X113647 Stepper Motor 
Driver (based on the ULN2003A IC) normally purchased with the stepper.  

The knob can rotate 360 degrees in both direction. 
Commands for incremental rotation are provided.
The platform is managed by serial dialogue, managed via Serial Monitor 
or Tera Term.
The following commands are implemented:
---> continuous rotation
R = Rotate clockwise continuously (meaning: for several turns)
L = Rotate counter-clockwise continuously (meaning: for several turns)
S = Stop rotating
---> 360 degrees  continuous rotation
r = Rotate for 360 degrees  clockwise
l = Rotate for 360 degrees  counter-clockwise
---> rotation by small increments
f = rotate 5 degrees  clockwise
F = rotate 5 degrees  counter-clockwise
t = rotate 10 degrees  clockwise
T = rotate 10 degrees  counter-clockwise
w = rotate 20 degrees  clockwise
W = rotate 20 degrees  counter-clockwise
q = rotate 40 degrees  clockwise
Q = rotate 40 degrees  counter-clockwise

It's very obvious that the source code could be optimized, but I preferred to keep 
a repetitive structure with a separate IF for each possible value of incomingByte.
 
Sources of information:
Small stepper control: http://arduino-info.wikispaces.com/SmallSteppers

*/

/*-----( Import needed libraries )-----*/
#include <AccelStepper.h>

/*-----( Declare Constants and Pin Numbers )-----*/
/* NEVER PUT ; AFTER A #define statement!!!! */
// motor pins
#define motorPin1  4     // Blue   - 28BYJ-48 pin 1
#define motorPin2  5     // Pink   - 28BYJ-48 pin 2
#define motorPin3  6     // Yellow - 28BYJ-48 pin 3
#define motorPin4  7     // Orange - 28BYJ-48 pin 4
                        // Red    - 28BYJ-48 pin 5 (VCC)
                        // Blue   - 28BYJ-48 pin GND  
#define STEPS_PER_TURN 2048 // number of steps in 360 degrees 
#define STEPS_PER_FIVE 28 // number of steps in 5 degrees 
#define STEPS_PER_TEN 57 // number of steps in 10 degrees 
#define STEPS_PER_TWENTY 114 // number of steps in 20 degrees 
#define STEPS_PER_FORTY 228 // number of steps in 40 degrees 

int motorSpeed = 500; // High speeds (800 and above) may cause erratic behavior in 28BYJ-48
int motorAccel = 400; // As above: better avoiding extreme accelerations
int myPos = 0; // will be used to define a starting point for 360 degrees  rotations
int LeftTurnUp = 0; // Couple of flags to determine rotation direction
int RightTurnDown = 0; // Couple of flags to determine rotation direction
int Continuous = 0; // used below to discriminate single rotation commands
int incomingByte = 0; // for incoming serial data
int STEPS_TO_DO = 0; // to allocate the number of steps needed to perform the required rotation

/*-----( Objects for stepper control )-----*/
// Set up the stepper as 4 wire bipolar on pin 4,5,6,7
// NOTE: The sequence 1-3-2-4 is required for proper sequencing of 28BYJ48
AccelStepper stepper(4,motorPin1,motorPin3,motorPin2,motorPin4); 

void setup()
{
  Serial.begin(9600);
  stepper.setMinPulseWidth(20); // Advisable setting to avoid that pulses from Arduino
                                // are too quick to be decoded
  stepper.setMaxSpeed(motorSpeed);
  stepper.setSpeed(motorSpeed);
  stepper.setAcceleration(motorAccel);
  // the following two lines reset "step zero" to the current position
  stepper.setCurrentPosition(stepper.currentPosition());
  stepper.runToPosition();
  Serial.println("Available commands:");
  Serial.println("====> Continuous rotation");
  Serial.println("R = continuous clockwise rotation");
  Serial.println("L = continuous counter-clockwise rotation");
  Serial.println("S = stop rotation");
  Serial.println("====> 360 degrees  Rotation");
  Serial.println("o = 360 degrees  clockwise rotation");
  Serial.println("O = 360 degrees  counter-clockwise rotation");
  Serial.println("====> Rotation by small increments");
  Serial.println("f = rotate 5 degrees  clockwise");
  Serial.println("F = rotate 5 degrees  counter-clockwise");
  Serial.println("t = rotate 10 degrees  clockwise");
  Serial.println("T = rotate 10 degrees  counter-clockwise");
  Serial.println("w = rotate 20 degrees  clockwise");
  Serial.println("W = rotate 20 degrees  counter-clockwise");
  Serial.println("q = rotate 40 degrees  clockwise");
  Serial.println("Q = rotate 40 degrees  counter-clockwise");
}

void loop()
{

  if (Serial.available() > 0) 
  {
    incomingByte = Serial.read();
    {
      if (incomingByte == 'R')
      {
        Serial.println("received R - activating continuous clockwise rotation");
        // The two lines that follow allow to send commands in any sequence:
        // before execution, a quick stop is performed
        stepper.stop(); // Stop as fast as possible: sets new target
        stepper.runToPosition(); // Now stopped after quickstop
        // The following couple of flags determines rotation direction
        LeftTurnUp = 0;
        RightTurnDown = 1;
        Continuous = 1; // used below to discriminate single rotation commands
        stepper.setCurrentPosition(stepper.currentPosition()); // Set step 0 "here"
        stepper.setSpeed(motorSpeed); // Previous commands have reset the speed
      } 
      
      if (incomingByte == 'L')
      {
        Serial.println("received L - activating continuous counter-clockwise rotation");
        // The two lines that follow allow to send commands in any sequence:
        // before execution, a quick stop is performed
        stepper.stop(); // Stop as fast as possible: sets new target
        stepper.runToPosition(); // Now stopped after quickstop
        // The following couple of flags determines rotation direction
        RightTurnDown = 0;
        LeftTurnUp = 1;
        Continuous = 1; // used below to discriminate single rotation commands
        stepper.setCurrentPosition(stepper.currentPosition()); // Set step 0 "here"
        stepper.setSpeed(motorSpeed); // Previous commands have reset the speed
      }

      if (incomingByte == 'S')
      {
        Serial.println("received S - stopping rotation");
        // Reset the flags that determine rotation direction
        LeftTurnUp = 0;
        RightTurnDown = 0;
        stepper.stop(); // Stop as fast as possible: sets new target
        stepper.runToPosition(); // Now stopped after quickstop
      } 

      if (incomingByte == 'o')
      {
        Serial.println("received o - activating single clockwise rotation");
        // The two lines that follow allow to send commands in any sequence:
        // before execution, a quick stop is performed
        stepper.stop(); // Stop as fast as possible: sets new target
        stepper.runToPosition(); // Now stopped after quickstop
        // The following couple of flags determines rotation direction
        LeftTurnUp = 0;
        RightTurnDown = 1;
        Continuous = 0; // used below to discriminate single rotation commands
        stepper.setCurrentPosition(stepper.currentPosition()); // Set step 0 "here"
        stepper.setSpeed(motorSpeed); // Previous commands have reset the speed
        // Since I will have to step forward 2047 steps, I store my current 
        // position as starting point of the rotation
        myPos=stepper.currentPosition();
        STEPS_TO_DO = STEPS_PER_TURN;
      } 
      
      if (incomingByte == 'O')
      {
        Serial.println("received O - activating single counter-clockwise rotation");
        // The two lines that follow allow to send commands in any sequence:
        // before execution, a quick stop is performed
        stepper.stop(); // Stop as fast as possible: sets new target
        stepper.runToPosition(); // Now stopped after quickstop
        // The following couple of flags determines rotation direction
        RightTurnDown = 0;
        LeftTurnUp = 1;
        Continuous = 0; // used below to discriminate single rotation commands
        stepper.setCurrentPosition(stepper.currentPosition()); // Set step 0 "here"
        stepper.setSpeed(motorSpeed); // Previous commands have reset the speed
        // Since I will have to step backwards 2047 steps, I store my current 
        // position as starting point of the rotation
        myPos=stepper.currentPosition();
        STEPS_TO_DO = STEPS_PER_TURN;
      }


      if (incomingByte == 'f')
      {
        Serial.println("received f - activating 5 degrees  clockwise rotation");
        // The two lines that follow allow to send commands in any sequence:
        // before execution, a quick stop is performed
        stepper.stop(); // Stop as fast as possible: sets new target
        stepper.runToPosition(); // Now stopped after quickstop
        // The following couple of flags determines rotation direction
        LeftTurnUp = 0;
        RightTurnDown = 1;
        Continuous = 0; // used below to discriminate single rotation commands
        stepper.setCurrentPosition(stepper.currentPosition()); // Set step 0 "here"
        stepper.setSpeed(motorSpeed); // Previous commands have reset the speed
        // Since I will have to step forward STEPS_PER_FIVE steps, I store my current 
        // position as starting point of the rotation
        myPos=stepper.currentPosition();
        STEPS_TO_DO = STEPS_PER_FIVE;
      } 
      
      if (incomingByte == 'F')
      {
        Serial.println("received F - activating 5 degrees  counter-clockwise rotation");
        // The two lines that follow allow to send commands in any sequence:
        // before execution, a quick stop is performed
        stepper.stop(); // Stop as fast as possible: sets new target
        stepper.runToPosition(); // Now stopped after quickstop
        // The following couple of flags determines rotation direction
        RightTurnDown = 0;
        LeftTurnUp = 1;
        Continuous = 0; // used below to discriminate single rotation commands
        stepper.setCurrentPosition(stepper.currentPosition()); // Set step 0 "here"
        stepper.setSpeed(motorSpeed); // Previous commands have reset the speed
        // Since I will have to step backwards STEPS_PER_FIVE steps, I store my current 
        // position as starting point of the rotation
        myPos=stepper.currentPosition();
        STEPS_TO_DO = STEPS_PER_FIVE;
      }


      if (incomingByte == 't')
      {
        Serial.println("received t - activating 10 degrees  clockwise rotation");
        // The two lines that follow allow to send commands in any sequence:
        // before execution, a quick stop is performed
        stepper.stop(); // Stop as fast as possible: sets new target
        stepper.runToPosition(); // Now stopped after quickstop
        // The following couple of flags determines rotation direction
        LeftTurnUp = 0;
        RightTurnDown = 1;
        Continuous = 0; // used below to discriminate single rotation commands
        stepper.setCurrentPosition(stepper.currentPosition()); // Set step 0 "here"
        stepper.setSpeed(motorSpeed); // Previous commands have reset the speed
        // Since I will have to step forward STEPS_PER_TEN steps, I store my current 
        // position as starting point of the rotation
        myPos=stepper.currentPosition();
        STEPS_TO_DO = STEPS_PER_TEN;
      } 
      
      if (incomingByte == 'T')
      {
        Serial.println("received T - activating 10 degrees  counter-clockwise rotation");
        // The two lines that follow allow to send commands in any sequence:
        // before execution, a quick stop is performed
        stepper.stop(); // Stop as fast as possible: sets new target
        stepper.runToPosition(); // Now stopped after quickstop
        // The following couple of flags determines rotation direction
        RightTurnDown = 0;
        LeftTurnUp = 1;
        Continuous = 0; // used below to discriminate single rotation commands
        stepper.setCurrentPosition(stepper.currentPosition()); // Set step 0 "here"
        stepper.setSpeed(motorSpeed); // Previous commands have reset the speed
        // Since I will have to step backwards STEPS_PER_TEN steps, I store my current 
        // position as starting point of the rotation
        myPos=stepper.currentPosition();
        STEPS_TO_DO = STEPS_PER_TEN;
      }



      if (incomingByte == 'w')
      {
        Serial.println("received w - activating 20 degrees  clockwise rotation");
        // The two lines that follow allow to send commands in any sequence:
        // before execution, a quick stop is performed
        stepper.stop(); // Stop as fast as possible: sets new target
        stepper.runToPosition(); // Now stopped after quickstop
        // The following couple of flags determines rotation direction
        LeftTurnUp = 0;
        RightTurnDown = 1;
        Continuous = 0; // used below to discriminate single rotation commands
        stepper.setCurrentPosition(stepper.currentPosition()); // Set step 0 "here"
        stepper.setSpeed(motorSpeed); // Previous commands have reset the speed
        // Since I will have to step forward STEPS_PER_TWENTY steps, I store my current 
        // position as starting point of the rotation
        myPos=stepper.currentPosition();
        STEPS_TO_DO = STEPS_PER_TWENTY;
      } 
      
      if (incomingByte == 'W')
      {
        Serial.println("received W - activating 20 degrees  counter-clockwise rotation");
        // The two lines that follow allow to send commands in any sequence:
        // before execution, a quick stop is performed
        stepper.stop(); // Stop as fast as possible: sets new target
        stepper.runToPosition(); // Now stopped after quickstop
        // The following couple of flags determines rotation direction
        RightTurnDown = 0;
        LeftTurnUp = 1;
        Continuous = 0; // used below to discriminate single rotation commands
        stepper.setCurrentPosition(stepper.currentPosition()); // Set step 0 "here"
        stepper.setSpeed(motorSpeed); // Previous commands have reset the speed
        // Since I will have to step backwards STEPS_PER_TWENTY steps, I store my current 
        // position as starting point of the rotation
        myPos=stepper.currentPosition();
        STEPS_TO_DO = STEPS_PER_TWENTY;
      }


      if (incomingByte == 'q')
      {
        Serial.println("received q - activating 40 degrees  clockwise rotation");
        // The two lines that follow allow to send commands in any sequence:
        // before execution, a quick stop is performed
        stepper.stop(); // Stop as fast as possible: sets new target
        stepper.runToPosition(); // Now stopped after quickstop
        // The following couple of flags determines rotation direction
        LeftTurnUp = 0;
        RightTurnDown = 1;
        Continuous = 0; // used below to discriminate single rotation commands
        stepper.setCurrentPosition(stepper.currentPosition()); // Set step 0 "here"
        stepper.setSpeed(motorSpeed); // Previous commands have reset the speed
        // Since I will have to step forward STEPS_PER_TWENTY steps, I store my current 
        // position as starting point of the rotation
        myPos=stepper.currentPosition();
        STEPS_TO_DO = STEPS_PER_FORTY;
      } 
      
      if (incomingByte == 'Q')
      {
        Serial.println("received Q - activating 40 degrees  counter-clockwise rotation");
        // The two lines that follow allow to send commands in any sequence:
        // before execution, a quick stop is performed
        stepper.stop(); // Stop as fast as possible: sets new target
        stepper.runToPosition(); // Now stopped after quickstop
        // The following couple of flags determines rotation direction
        RightTurnDown = 0;
        LeftTurnUp = 1;
        Continuous = 0; // used below to discriminate single rotation commands
        stepper.setCurrentPosition(stepper.currentPosition()); // Set step 0 "here"
        stepper.setSpeed(motorSpeed); // Previous commands have reset the speed
        // Since I will have to step backwards STEPS_PER_TWENTY steps, I store my current 
        // position as starting point of the rotation
        myPos=stepper.currentPosition();
        STEPS_TO_DO = STEPS_PER_FORTY;
      }






    }
  }

  if (Continuous == 1) // continuous rotation 
    {
    if (LeftTurnUp == 1)  //left turn
      {
        stepper.moveTo(10000); //move many steps - more then mechanical needed
    }

    if (RightTurnDown == 1)  //right turn
      {
       stepper.moveTo(-10000); //move many steps - more then mechanical needed
    }
    stepper.run();
  }
  if (Continuous == 0) // continuous rotation 
    {
    if (LeftTurnUp == 1)  //left turn
      {
        stepper.moveTo(myPos+STEPS_TO_DO); // number of  steps in 5, 10, 20 or 360 degrees 
    }

    if (RightTurnDown == 1)  //right turn
      {
       stepper.moveTo(myPos-STEPS_TO_DO); // number of  steps in 5, 10, 20 or 360 degrees 
    }
  }
  stepper.run();
}