Lego Arduino (28BYJ48 stepper motor) nanometer xy positioning table

This is a followup on "How to build high precision (3µm accuracy) linear actuator" thread:
https://forum.arduino.cc/index.php?topic=645745.0

I had a wonderful idea on how to reduce 1.6µm average step width of that thread to nanometer range!

The motor shaft (back to front) was vertical to moving direction (bottom to top), and we got 1mm operational range with that.

Now think that motor shaft direction would be bottom to up as well. Nothing would happen, because the 1mm move would happen vertical to "bottom to top" direction. So 1mm for 0°, and 0mm for 90°, that is cosine function for 1mm radius! The trick is to mount the stepper motor shaft with angle just below 90°.

Animation was created with gifenc.sh from 4 seconds of smartphone video.
In youtube video you see stepper position moves 300->600->300, in animation only 300->600 repeatedly:

I took Raspberry v1 camera video with this command:

$ raspivid -md 1 -w 1920 -h 1080 -p 22,50,864,648 -t 0 -o tst.h264 -fps 10 -o tst.h264

I made the video 3 times faster for complete (unmodified) upload to youtube:
$ ffmpeg -r 30 -i tst.h264 -c copy tst.30fps.mp4

Because the motion is slow and minimal in x-direction, I made animation 5 times faster, and cropped 500x200 part from the video, created with gifenc.sh. Although faster, movement is still slow and minimal in x-direction:
$ ffmpeg -r 50 -i tst.h264 -filter:v "crop=500:200:0:0" tst.mp4

I took leftmost frame

and rightmost frame

You see 0.01mm resolution micrometer. I measured 360 pixel between two big divisions 10*10µm=100µm apart. And I counted between 4 and 6 pixels (only!) movement between a markers leftmost and rightmost position. For being on safe side let us assume the movement from left to right is 7 pixels.

First we can compute the operational range for the 300 steps, it is 2µm(!) in total:

$ echo "scale=6; 100/360*7" | bc -ql
1.944439
$

Dividing by 300 (steps) gives 6.5nm(!) average step width:
$ echo "scale=6; 100/3607/3001000" | bc -ql
6.481000
$

I built a Leo frame around a 1$ mirror from drugstore.
And I created 4 Lego springs from ballpoint springs:

I did drill 2mm diameter holes into the Lego pieces with my Dremel, screwed the stepper motor with M3x16 screws:

Because the motor shaft is not in stepper motor middle, I can make motor sit at an arbitrary angle by just screwing left and right screws in or out a bit:

The Lego piece with flat top (it moves on a flat top piece below it as well) did not reach the mirror at the given angle, so I just inserted a 1.5mm thick small piece of plastic between movable Lego piece and mirror:

As you could see above, the angle of stepper motor can be increased arbitrarily. That will reduce the step width below 1nm, but I have no idea on how to see/verify that, as the Raspberry Pi camera cannot sharply display stuff even in low single digit micrometer range. Maybe my 0.001mm electronic micrometer can help with that later:

Today I learned that it is safe to power two 28BYJ48 stepper motors with half-stepping from USB powered Arduino 5V power rail. So the whole xy positioning table consists of some Lego pieces, small mirror, two stepper motors, two ULN2003 drivers and an Arduino Uno!
https://forum.arduino.cc/index.php?topic=648859.msg4377804

By increasing the angle and getting nearer and nearer to 90°, arbitrary low average step width can be achieved.
On the other hand operational range drops and drops (from 1mm with motor shaft vertical to moving axis, to only 2um as shown above).

This posting discusses adding a motor shaft coupler to the game and increasing operational range by factor of 9:
https://forum.arduino.cc/index.php?topic=645745.msg4383884#msg4383884

Operational range can be increased further by placing the metallic piece more distant to center. Doing so with Lego xy positioning table would only require a bigger movable Lego piece that is pressing against the mirror.

I used a long M3 screw with nut to get 0.125um/step(!) (for each step) for linear actuator:
https://forum.arduino.cc/index.php?topic=645745.msg4387462#msg4387462!%5B%5D(upload://fZlmwEFCXdt9c0Co710VwQS5Bez.png)

Before trying to add that to xy positioning table, I was amazed by my latest buy on aliexpress.
After I did my purchase, I was shown below <2$ xy positioning table, inclusive stepper motors.
I think the operational range is mislabled (because of other photos with hand), should be 2.4cm.
I did order three for 8.52$ incl. shipping to minimize shipping costs:
https://www.aliexpress.com/item/32972295033.html

HermannSW:
I think the operational range is mislabled (because of other photos with hand), should be 2.4cm.

Operational range is really 2.4mm × 2.4mm.
Now that I have µm precision Raspberry HQ camera microscope under control ...
https://www.raspberrypi.org/forums/viewtopic.php?f=43&t=210605&p=1673492#p1673710

... I will have to get this micro-stepping two-axis sliding table working:
https://www.raspberrypi.org/forums/viewtopic.php?f=43&t=276084&p=1674056#p1674056

This is 100% view of part of 12MP photo with 0.21µm/px:
(micrometer diversions are 10µm apart, measured line is 150µm long, and 714 pixels)

I did solder 4 breadboard cables to A+, A-, B+, B- of one motor of sliding table yesterday:

I tried to drive it with uln2003 stepper driver without success.

Today I tried to drive another "Mini Two-phase Four-wire 5mm Stepper Motor with Planetary Gearbox, Metal Gears, Metal Screw Slide Precision Lifting Motor" I had bought at the same time, without success.

I read about the pattern to send to the 4 wires:
https://www.tigoe.com/pcomp/code/circuits/motors/stepper-motors/

Step wire 1 wire 2 wire 3 wire 4
1 High low high low
2 low high high low
3 low high low high
4 high low low high

AND I read the warning:

Like other motors, stepper motors require more power than a microcontroller can give them, so you’ll need a separate power supply for it.

Since I was not able to get the motor driver working, and because it is a "mini stepper" only, I did drive it directly from Arduino Uno pins 2-5. And it worked! I did 350 loops, with 4 steps in each, into one direction, and then the same in the other direction. That did move exactly 10mm. That means 10000µm/(4*350)=7.143µm per step(!!). Here is the not nice code that I used for the video:

void setup() {
  pinMode(2, OUTPUT);
  pinMode(3, OUTPUT);
  pinMode(4, OUTPUT);
  pinMode(5, OUTPUT);
  digitalWrite(2, 0);
  digitalWrite(3, 0);
  digitalWrite(4, 0);
  digitalWrite(5, 0);
}

int d=1, N=350;


void loop() {
  for(int i=0; i<N; ++i)
  {
  digitalWrite(2, 1);
  digitalWrite(3, 0);
  digitalWrite(4, 1);
  digitalWrite(5, 0);
  delay(d);
  digitalWrite(2, 0);
  digitalWrite(3, 1);
  digitalWrite(4, 1);
  digitalWrite(5, 0);
  delay(d);
  digitalWrite(2, 0);
  digitalWrite(3, 1);
  digitalWrite(4, 0);
  digitalWrite(5, 1);
  delay(d);
  digitalWrite(2, 1);
  digitalWrite(3, 0);
  digitalWrite(4, 0);
  digitalWrite(5, 1);
  delay(d);
  }
  delay(1000);
  for(int i=0; i<N; ++i)
  {
  digitalWrite(2, 1);
  digitalWrite(3, 0);
  digitalWrite(4, 1);
  digitalWrite(5, 0);
  delay(d);
  digitalWrite(2, 1);
  digitalWrite(3, 0);
  digitalWrite(4, 0);
  digitalWrite(5, 1);
  delay(d);
  digitalWrite(2, 0);
  digitalWrite(3, 1);
  digitalWrite(4, 0);
  digitalWrite(5, 1);
  delay(d);
  digitalWrite(2, 0);
  digitalWrite(3, 1);
  digitalWrite(4, 1);
  digitalWrite(5, 0);
  delay(d);
  }
  delay(1000);
}

Will be interesting to see what resolution the differently looking sliding desk micro stepping motors have ...

I am not sure whether 2-phase stepper motors would allow for half-stepping as well, will test that later.

This is the video of setup and action:

P.S:
Forgot to add product link for the motor I tested (less than 2$/pc with <2$ shipping):

Half stepping works as well — now with 10000µm/(8*350) =3.57µm per step!

void setup() {
  pinMode(2, OUTPUT);
  pinMode(3, OUTPUT);
  pinMode(4, OUTPUT);
  pinMode(5, OUTPUT);
  digitalWrite(2, 0);
  digitalWrite(3, 0);
  digitalWrite(4, 0);
  digitalWrite(5, 0);
}

int d=1, N=350;

void step(int a, int b, int c, int d, int D)
{
  digitalWrite(2, a); digitalWrite(3, b);
  digitalWrite(4, c); digitalWrite(5, d);
  delay(D);
}

void loop() {
  for(int i=0; i<N; ++i)
  {
    step(1,0,0,0,d);
    step(1,0,1,0,d);
    step(0,0,1,0,d);
    step(0,1,1,0,d);
    step(0,1,0,0,d);
    step(0,1,0,1,d);
    step(0,0,0,1,d);
    step(1,0,0,1,d);
  }
  delay(1000);
  for(int i=0; i<N; ++i)
  {
    step(1,0,0,1,d);
    step(0,0,0,1,d);
    step(0,1,0,1,d);
    step(0,1,0,0,d);
    step(0,1,1,0,d);
    step(0,0,1,0,d);
    step(1,0,1,0,d);
    step(1,0,0,0,d);
  }
  delay(1000);
}

This time half-stepping pattern can be clearly seen:

…
    step(1,0,0,0,d);
    step(1,0,1,0,d);
    step(0,0,1,0,d);
    step(0,1,1,0,d);
    step(0,1,0,0,d);
    step(0,1,0,1,d);
    step(0,0,0,1,d);
    step(1,0,0,1,d);
…

3.57µm per step allows for 14 steps per 50µm, and that is the side length of the center micrometer squares.

Unlike the current mini stepper motor, the two sliding table motors have tiny springs attached, so positioning there might be more precise than with currently tested mini stepper motor.

As sneak preview I did first video of sliding-table µm movement.
Under Raspberry HQ camera 0.21µm/px microscope view of scene.
5 loops of 4 full-steps making 142µm is 7.1µm per full-step (micrometer diversions are 10µm apart).
https://www.raspberrypi.org/forums/viewtopic.php?f=43&t=210605&p=1677348#p1677348

16.6% size 30fps animation of first 6 seconds of video:

This is kind of a hello world sketch for two-axis microscope sliding-table.
It does 40 half-steps to the right, then 40 half-steps up, and finally 40 half-steps of both stepper motors in the other dicrection, together:

int M[2][4]={{8,9,10,11},{4,5,2,3}};

int P[8][4]={{1,0,0,0},{1,0,1,0},{0,0,1,0},{0,1,1,0},
             {0,1,0,0},{0,1,0,1},{0,0,0,1},{1,0,0,1}};

#define forall_motors  for(m=0; m<2; ++m)
#define forall_pins    for(p=0; p<4; ++p)

int m, p, x, y, d=1, N=40;

void step(int m, int i, int D)
{
  forall_pins  digitalWrite(M[m][p], P[i%8][p]);
  delay(D);
}

void setup() {
  forall_motors  forall_pins  pinMode(M[m][p], OUTPUT);  
  forall_motors  forall_pins  digitalWrite(M[m][p], P[0][p]);  
  x=y=0;
}

void loop() {
  delay(1000);  for(; x<N; ++x)  step(0,x+1,d);    
  delay(1000);  for(; y<N; ++y)  step(1,y+1,d);    
  delay(1000);  for(; x>0; --x,--y)  { step(0,x-1,0); step(1,y-1,d); }    
}

I did upload 17s 1920x1080@30fps smartphone video of Raspberry microscope view with Figure of Merit:

Right side of Figure of Merit rectangle moves from position 21 to 41 with 40 half-stepper steps. That is 200µm/40=5µm per half-step.

New demo.
4×zoom of Raspberry HQ camera preview made pixels of 12MP frame correspond to (1360x768) HDMI display pixels 1:1.
Now moving 20 steps or 100µm right, then up and finally diagonally down to start (center of micrometer diversions 10µm apart, or 48 pixles):
https://www.raspberrypi.org/forums/viewtopic.php?f=43&t=210605&p=1678566#p1678566

I completed soldering of 2nd stepper motor from two-axis microscope sliding table and superglued 8 female headers to an 8×1 connector:
https://www.raspberrypi.org/forums/viewtopic.php?f=43&t=210605&start=75#p1678566

I did not measure current of tiny stepper motor coils I drive directly from Arduino digital pins until yesterday. I was lucky, the rmeasured 21mA should be OK (Uno datasheets says digital pins can deliver 20mA).

This is part from 16MP smartphone detail photo of Raspberry HQ camera microscope in use, scaled to 50%, 100% here:
https://pbs.twimg.com/media/EabwN_KWsAInSfd?format=jpg&name=largeShard with micrometer superglued onto white cardboard superglued on two-axis sliding-table superglued on yellow light box for height superglued on heavy black box for keeping everything in place. The black box is placed on aluminum alloy microscope stand, and then moved around on that for getting micrometer into initial view of HQ camera microscope. Moving to target positions <10µm away by hand is a puzzle sometimes:
Creating a RPI-assisted microscope - Page 5 - Raspberry Pi Forums

I do program Arduino Uno with IDE from my 4GB Raspberry Pi4B for some days now. I do not like Serial monitor input capabilities, so I use a Linux "screen" command session against /dev/ttyUSB0 for input/output to Arduino Serial:

$ screen /dev/ttyUSB0 115200,cs8

You can see screen session at left bottom of 1360x768 screenshots (right click for 100% view):

I started with Examples->Strings->StringToInt, this is the sketch that now allows to move sliding table in 5µm steps in X and Y directions:

int M[2][4]={{8,9,10,11},{4,5,2,3}};

int P[8][4]={{1,0,0,1},{1,0,0,0},{1,0,1,0},{0,0,1,0},
             {0,1,1,0},{0,1,0,0},{0,1,0,1},{0,0,0,1}};

#define forall_motors  for(m=0; m<2; ++m)
#define forall_pins    for(p=0; p<4; ++p)

int m, p, x, y, d=2;

void step(int m, int i)
{
  forall_pins  digitalWrite(M[m][p], P[(i%8+8)%8][p]);
}

void setup() {
  forall_motors  forall_pins  pinMode(M[m][p], OUTPUT);  
  forall_motors  forall_pins  digitalWrite(M[m][p], P[0][p]);  
  x=y=0;
  
  Serial.begin(115200);
  while (!Serial)  { }

  Serial.println("\nmicrometer moves:");  
}

String inString = "";
int nx, ny, s=1;

void loop() {
  while (Serial.available() > 0) {
    int inChar = Serial.read();
    if (isDigit(inChar)) {
      inString += (char)inChar; 
      Serial.print((char)inChar);
    } else if (inChar == 45) {
      s = -1;
      Serial.print((char)inChar);
    } else if (inChar == 13) {
      ny = s * inString.toInt();

      while ((x!=nx) || (y!=ny)) {
        if (x<nx)  step(0, ++x);
        if (x>nx)  step(0, --x);
        if (y<ny)  step(1, ++y);
        if (y>ny)  step(1, --y);
        delay(d);
      }
      
      Serial.println();
      inString = ""; 
      s = 1;
    } else {
      nx = s * inString.toInt();
      inString = "";
      s = 1;
      Serial.write(inChar);
    }
  }
}

After night I powered on the Arduino, and the slightly more than 10µm size object that was inside Figure of Merit rectangle last night was a bit outside today (just putting wireless keyboard on desk has potential to move scene few 10s of micrometers). As you can see in screen session on left bottom Arduino sketch allows to get it into the rectangle easily:

Next step:
Implement microstepping. Currently sketch uses 5µm per step half-stepping.
7.1µm per step full-steps are more reliable, so I will go with them.
Sketch uses pins 2-5 and 8-11, Uno digital pins 3,5,7 and 9 allow for PWM.
I will use String.toFloat() which allows for 2 decimal digits precision after point only.
But 7.1µm/100 = 0.071µm(!) per step in X and Y direction of sliding-table should be more than good enough :wink:
[the 12MP frame has 0.21µm/px, so a single stepper motor step would move less than 1 pixel wide ...]

I did the next but one step first.

Now that X and Y direction of sliding-table can be addressed with 5µm steps, I found it annoying that Z axis for focusing is done manually. With 180° M12 lens mounted reversed as macro lens, not only a big maginification is achieved (0.21µm/px in 12MP photo), but also the depth of view is reduced. Even with microscope stand wheel it is not easy to do the needed moves and not overshoot.

I looked at my microscope stand and thought about superglueing 28BYJ48 stepper motor to center of wheel. It turned out that there was no plastic in order to allow access to inner screw. After removing thin plastic the screw became visible. I have some 5mm to 3mm couplers, and it was possible to superglue the 3mm side of coupler centered onto the microscope stand screw nut. I did superglue some Lego pieces to stand as well, allowing to finally superglue stepper motor onto the Lego pieces with motor shaft freely inside 5mm side of coupler. After waiting some time to complete superglueing, I did screw in two tiny screws that fixated the coupler to stepper motor shaft (in case I do not want to use the stepper motor, just unscrewing the two screws a bit allows the coupler to turn without being affected by stepper motor):

Then I connected stepper to uln2003 motor driver, and connected unused pins 6, 7, 12 and 13 to IN1-4 of uln2003 stepper driver. Simple full rotation example is what you can see in video uploaded to youtoube. 2048 full-steps down, then 2048 steps up, repeat. I did mark highest and lowest position reached on microscope stand and let the sketch alone for an hour. Stepper is moving fine, and maximal and minimal positions have not changed.

I increased speed to 10rpm in sample sketch I found somewhere:

#include <Stepper.h>
const int stepsPerRevolution = 2048;
Stepper myStepper = Stepper(stepsPerRevolution, 6,12,7,13);
void setup() {
  myStepper.setSpeed(10);  // 10rpm
  
  Serial.begin(9600);
}
void loop() {
  Serial.println("clockwise");
  myStepper.step(stepsPerRevolution);
  delay(500);
  
  Serial.println("counterclockwise");
  myStepper.step(-stepsPerRevolution);
  delay(500);
}

I measured 18mm as vertical difference for a full rotation of wheel, with 2048 full-steps per rotation that is 8.8µm per step(!) in Z direction.

Here is the youtube video proving that stepper motor does the job:

I wanted to do a quick test with fine grain Z-axis movement and live microscope view. I modified sliding-table sketch for one input only, and use stepper library:

#include <Stepper.h>
const int stepsPerRevolution = 2048;
Stepper myStepper = Stepper(stepsPerRevolution, 6,12,7,13);

void setup() {  
  myStepper.setSpeed(10);  // 10rpm

  Serial.begin(115200);
  while (!Serial)  { }

  Serial.println("\nZ micrometer moves:");  
}

String inString = "";
int nz, s=1, z=0;

void loop() {
  while (Serial.available() > 0) {
    int inChar = Serial.read();
    if (isDigit(inChar)) {
      inString += (char)inChar; 
      Serial.print((char)inChar);
    } else if (inChar == 45) {
      s = -1;
      Serial.print((char)inChar);
    } else if (inChar == 13) {
      nz = s * inString.toInt();

      myStepper.step(nz-z);
      z=nz;
      
      Serial.println();
      inString = ""; 
      s = 1;
    }
  }
}

I started Rapberr Pi HQ camera microscope view, and in left bottom terminal you see my inputs. I did move 1 stepper motor step deeper each time, that means 8.8µm per step. I took 1360x768 screenshots after each move and created animated .gif from captured frames. Right click to see animation in 100% size:

P.S:
Further work will be posted in new thread:
"Uno" µm xyz positioning system for 0.21µm/pixel microscope(Raspberry HQ camera)
https://forum.arduino.cc/index.php?topic=691123.0

I have described above how to add stepper motor to microscope stand:

Recently I bought a 50$ cheap telescope and wanted to use this technique for touchless focusing. First there was too much torque needed, so not even 12V/1A/0.13Nm Nema17 stepper worked. Finally I got it working, after having loosened two screws fixating focus knob to telescope tube:
https://www.raspberrypi.org/forums/viewtopic.php?f=43&t=290331&p=1760806#p1760368

Telescope looks at millimeter ruler 2m distant (with that magnification moon diameter is larger than 12MP photo width, so parts of moon can be inspected):