Trying to make the worlds cheapest syringe pump/linear drive

99% of things you buy from scientific suppliers are violently overpriced. Peristaltic pumps for $2000 that only contain $50 worth of equipment. Homeothermic blankets should only cost about three fifty. But the one that has always annoyed me are syringe drivers. These are nothing more than a stepper motor, a lead screw and a bit of electronics. The budget ones tend to start at around $300, and they go up to ten times that. I’m not standing for that, and neither should you. So I wanted to make one myself.

syringe-driver

I’m not the first person to try this. There was a beautifully written article in PLoS One from another group who tried this, and their total come to just under $100US, however, their version required a 3D printer, something I unfortunately don’t own. So here are my plans for a version that costs approximately £50 or $75 US.

Let’s start with the mechanics, then move onto the electronics, and finally the code.

Mechanics

First things first, unless you have access to a milling machine and someone who knows how to use it, turn back now. If you attempt this with a standard drill press and a ruler, it is highly likely you will do nothing but waste aluminium. You only need to drill half a dozen holes accurately though, so if you don’t have an engineer on campus, it wont cost much if you take it to a local machining shop. The complete device (minus lead screw and rails) looks like this:

3D-view

The power of this comes from a stepper motor. You can use the classical NEMA 17, though I had an essentially equivalent Astrosyn MY4001 lying around. The first step is to build a plate to hold the motor to the rest, which is a simple thing made out of 5 mm thick aluminium:

Motor-plate.

My motor has a 16 mm DIAM extrustion around the spindle, on the NEMA 17 it is 22 mm, so cut that large central hole to fit, and then cut and counter sink holes for the mounting screw. Finally, there are two M3 through holes, to hold the motor plate to the “start piece”.

front-piece

The start piece is cut from a piece of 25x25x60 mm aluminium. We have two 5mm holes on each end for the rails to sit in (and tapped M3 holes for grub screws to descend into, to hold the rails). There is a large central hole for the flexible coupler to fit in. I had a Huco 724.13 which is 12.5 mm, so I cut a 13 mm hole. However if you look on ebay, you can find them for a few dollars, so just cut your hole to the size of your coupler, plus 1 mm or so.

lead-piece

The lead piece holds the lead nut and the linear bearings. I decided to make nylon bearings, and a brass nut, because nylon on steel rails is smooth, and brass holds thread well, however, if you have t,o you could skip these, and just cut straight into aluminium,. The body of the lead piece is cut from 20x60x50 mm aluminium, though there is nothing stopping you using the same 25x25x60 pieces. 12 mm holes are cut into body of this part, and pre-cut sections of 12 mm diam brass and nylon rod are inserted into the hole. This needs a bit of finesse. I suggest getting a new 12 mm cutter, so your holes are exact, and be prepared to spend a bit of time with sandpaper to get your rod to fit in nicely. You want it so go in with a touch force, but avoid hammering it is, or otherwise you will distort everything. A drop of locktite will hold them if there is any slack. Then drill/tap the holes in the plastic/brass and insure the holes are centered.

end-piece

Finally we have the end piece. This is almost identical to the start piece, except it contains no accessory holes for the motor plate to screw into, and the large central hole is large enough for a ball bearing to go into. I used a NSK Deep Groove Ball Bearing 625, which cost £2.02. I now realized that this part is largely unnecessary, unless you want to make sure your syringe driver has no black-lash (that is, when you reverse direction, there is no lag).

When be careful that the rod and threaded rod you use is straight, as some of mine had subtle kinks that seriously effected the drive.

DescriptionPrice (US/GBP)
Stepper Motor$15 / £10
9 Volt, 1000 mA DC Supply$5 / £3.80
1m of S/S M5 Threaded Rod$2 / £1.50
1m of S/S 5 mm rod$12 / £8
1m of 25 x 25 mm Aluminium Bar$15 / £10
100 mm of 12 mm Brass Rod$4.50 / £3
100 mm of 12 mm Nylon Rod$3 / £2
Flexible 5mm shaft coupler$2 / £ 1.39
5 mm ball bearing. NSK Deep Groove Ball Bearing 625.$3 / £2
EasyDriver$1 / £1
Arduino Nano$3 / £3
Misc (screws)$5 / £4
TOTAL$70.50/£49.69

Electronics

There are lots of possible ways to drive this, but nowadays I realized you want to avoid reinventing the wheel, so just buy an EasyDriver. This is a premade board that will give you simple control of a constant current microstepping driver, and you can get them off Ebay for a few dollars. All you do is give the board a TTL pulse, and you get one (micro)step. The end. One tip though, put a heatsink on it, as they have a tendency to get hot and go into thermal shutdown. I initially designed a system that used the audio out of your PC, which was fed into a simple non-inverting amplifier (with a 220 Ohm feedback resistor and a 1 kOhm ground resistor = ~5.5 gain), and then into the EasyDriver. This did work, but it was temperamental at low speeds, and different computers behaved slightly differently, however, the Python code is available here if you want a look. It’s nice enough OO code, so you should be able to understand it even though it is only partially commented.

At this point I found out you could get knock-off Arduino nanos for a few bucks on Ebay, so I moved on to doing this in a slight less hacky fashion. So the electronics could hardly be more simple.

linear-driver4

The MS1 and MS2 pins have pull up resistors, and can be left floating, which puts the driver into 8th stepping mode. This provides the highest torque, most accurate and quietest stepping, but limits the maximum speed. On the picture I have a capacitor across the power rails, but it’s really not necessary.

Code

The full Arduino code is available here, and the Python GUI is here.

Sometimes coding for Arduino upsets me, and this is one of those times. The goal was to minimize the duration of the main loop so that maximum velocity could be achieved, and this resulted in a lot of nasty code. The principle of the whole thing is that Python is going to send the Arduino a triplet of digits via serial port. These are going to be (distance, duration, direction), in microns, microseconds and 1/-1 respectively. The main Arduino loop checks if it has data in its serial buffer, and if it does, it uses the new data to update it’s settings, if it doesn’t, it runs the drive function, which simply asks “Am I where I want to be?” and if not moves. Let’s dive in. We start with a mountain of global variable declarations

//DRIVE PARAMETERS - USER SHOULD SET
const int STEPS_PER_REV = 200; // Steps per revolution of the motor
const int PULSES_PER_STEP = 8; // Because we're 8th stepping
const int SCREW_PITCH = 800; // The pitch of the lead screw. M5 = 800 micron per revolution
const long TOTAL_LENGTH_MICRONS = 65000; //Total length of useable screw in microns
int PULSES_PER_MICRON = (STEPS_PER_REV * PULSES_PER_STEP)/SCREW_PITCH;
long TOTAL_LENGTH_PULSES = TOTAL_LENGTH_MICRONS * PULSES_PER_MICRON;

//CONNECTION PARAMETERS - USER SHOULD CHECK
const int pulse_pin = 11; // Which Arduio pin is connected to the 'STEP' pin
const int dir_pin = 12; // Arduino pin connected to the 'DIR' pin

//Movement Parameters - user should leave
long current_pos = 0;
long final_pos = 0; // in pulses
long time = 0;  //microseconds
int directn = 1; //1 = forwards, -1 = reverse
unsigned long last_step_time = 0;
int micros_per_pulse = 0;

The first block contains user settable variables that reflect the nature of the motor, the microstepping setting, the pitch of the thread, and finally the usable travel length. In the next block you find the pin definitions. Finally there is a section for global variables used to keep track of position and timing.

void setup() {
  Serial.begin(115200);
  Serial.write("X"); // write back to python to confirm connection
  pinMode(pulse_pin, OUTPUT); 
  pinMode(dir_pin, OUTPUT);
  digitalWrite(pulse_pin, LOW);
  digitalWrite(dir_pin, LOW);
}

The setup block is simple. We begin the serial connection, which when setup, writes an arbitrary code back to Python. This works because when devices initialize serial connections with the Arduino, it restarts, and then enters the setup block. We also set the pin modes.

void loop() {
  if (Serial.available() > 5) { //min is 6 e.g. 1,2,3,
      long new_length = Serial.parseInt(); 
      long new_time = Serial.parseInt(); 
      int new_directn = Serial.parseInt();
  
      if (new_length == 9999 && new_time == 9999 && new_directn == 9999) {
        stopdriver(); //Emergency stop
      } else if (new_length == 8888 && new_time == 8888 && new_directn == 8888) {
        move_to_zero(); //Move to preset zero
      } else if (new_length == 7777 && new_time == 7777 && new_directn == 7777) {
        start_zeroing(); // Begin moving towards the beginning
      } else if (new_length == 6666 && new_time == 6666 && new_directn == 6666) {
        stop_zeroing(); // Stop the driver, and set current position as zero
      } else {
        set_driver(new_length, new_time, new_directn);
      }
  } else {
    drive();
  }
}

This could hardly be more self explanatory. Every loop the arduino performs, it checks if there is a serial command, or drives the stepper. The serial command can either be of the form or one of 4 special codes, that perform useful functions.

void set_driver(long new_length, long new_time, int new_directn) {
  int msg = check_setting(new_length, new_time, new_directn);
  if (msg == 1) {
    final_pos = current_pos + (new_length * new_directn * PULSES_PER_MICRON);
    micros_per_pulse = new_time / (new_length * PULSES_PER_MICRON);
    directn = new_directn;
    if (directn == 1) {
      digitalWrite(dir_pin, LOW); //FOWARD
    } else {
      digitalWrite(dir_pin, HIGH); //BACKWARD
    }
  }
  Serial.println(msg); //TELL PYTHON THE SITUATION    
}

In the loop() function, if the user sends in a serial command that isn’t a special code, then the set_driver() function is called. The first thing it does is check that the command is legal using the check_setting() function (described below). The check_setting() returns either a 1 for okay, or a variety of negative error codes. If the returned value is 1, then we convert the serial command into the final position in number of TTL pulses that we will need to deliver (final_pos). Remember, that if the EasyDriver is in 8th stepping mode, then we need to deliver 8 TTL pulses to produce a single true step in the motor. We then calculate the number of microseconds that need to be between each pulse (micros_per_pulse). Finally, we set the direction pin. We also return whether the serial command was okay back to python.

int check_setting(long new_length, long new_time, int new_directn) {
  // Function to check settings.
  if (new_length/(float(new_time)/1000) > PULSES_PER_STEP) { //Arbitrary speed limit
    return -1; //too fast
  }
  if (new_directn == 1) {
    if (current_pos + new_length * PULSES_PER_MICRON > TOTAL_LENGTH_PULSES) {
      return -2; //off end
    } 
  }
  if (new_directn == -1) {
    if (current_pos - new_length * PULSES_PER_MICRON < 0) {
      return -3; //off start
    } 
  } 
  return 1;
}

The check_setting() function checks whether the serial command will, in the first if statement, drive the motor too fast. In the second if statement, whether it will drive the lead section of the end of the drive. And in the final if statement, reverse the lead section of the beginning of the drive. The first if statement is set kinda arbitrarily, as it will be a function not just the motor, but also how easily your whole lead section moves. If while you're playing around, your motor locks up, try multiplying the right hand side by 2 or 3. Everything else is clear enough. If you take the current position, add or subtract what the command says to do, and that puts the lead stage off the end (final_pos > TOTAL_LENGTH_PULSES) or off the beginning (final_pos < 0) return an appropriate error code. Otherwise return 1.

void drive() {
  if (abs(current_pos - final_pos) > 0) {
    if (micros() - last_step_time > micros_per_pulse ) {
      digitalWrite(pulse_pin, HIGH);
      digitalWrite(pulse_pin, LOW);
      current_pos += directn;
      last_step_time = micros();
    }
  }
}

This is super clear. We say, if the current position isn't where we want to be AND if the time since the last pulse is longer than the time between pulses, then we make a pulse.

Finally, we have a collection of simple functions, that either start the drive moving, or stop the drive moving. Note, they achieve motion simply by setting the final position to something, and the drive() function achieves the rest.

void stopdriver() {
  final_pos = current_pos;  
}

void start_zeroing() {
  digitalWrite(dir_pin, HIGH);
  directn = -1;
  micros_per_pulse = 200;
  final_pos = 2147483647;
}

void stop_zeroing() {
  final_pos = 0;
  current_pos = 0;
}

void move_to_zero() {
  digitalWrite(dir_pin, HIGH);
  directn = -1;
  micros_per_pulse = 200;
  final_pos = 0;
}

I'm not going to spend a lot of time going over the Python code. I have the Stepper_gui class, which sets up the GUI, and calls methods of the Simple_serial. The Simple_serial is a very shallow wrapper for the standard python serial module, but it provides a connection check that interfaces with the Arduino, and it also has a readline method.

Performance

I measured the distance of travel with some digital callipers. To be honest, the result was far better than I anticipated. The average distance travelled was between 1 and 2% less than the command, except for the smallest 50 micron travel, which averaged only 40 microns of movement.

Stepper Performance

Finally, when you put it all together, you'll need something to hold your syringe. I just tooled up something quickly out of a block of vinyl. If you've gotten this far you wont find it a problem. If anyone does make this, I'd love to see it, and know about your improvements.

2 thoughts on “Trying to make the worlds cheapest syringe pump/linear drive

  1. An elegant design and described very nicely. Congratulations. I am unable to find if there are any limit switches on the extreme ends of the slider.

    • Hi,

      No I didn’t include any limit switches due to the increase expense, and that they are largely not needed due to the use of the stepper motor. You programmatically enter the two limits when you set it up. I found that small limit switches were actually rather expensive, and ended up increasing the cost of the whole thing by about 20%.

Leave a Reply to Bill Connelly Cancel reply

Your email address will not be published. Required fields are marked *