In a previous post we have presented a [»] lab-made syringe pump but without its controller. In this post, I will present how to assemble the pump controller from Arduino shields. There are no PCB to etch and only a few parts to solder, so you should have no problem building it.
You will need an Arduino Uno rev3 controller, an Adafruit LCD shield and the Adafruit Motorshield v2 as well as some stacking header for the Arduino Uno rev3 and a 12V power supply. All of them are available at [∞] Robotshop for a total $50US.
The first thing you will have to do is to solder the Motorshield and LCD shield according to Adafruit instructions. Use stacking headers for the motor shield because we will stack the LCD shield on top of it.
Then, stack the motor shield on your Arduino Uno and connect your stepper wires on the second stepper slot (M3/M4), just like on Figure 1. You want to connect the wires from the ST35 motor in the following order: yellow, bue, red + brown, pink, orange. Yellow/blue is the motor phase 1 while pink/orange is the phase 2. The red and brown wires are the ground of each phase.
In a later post I will show you how to connect a motorized flow selection valve, but it’s not for today so don’t pay attention to the three wires at the top of Figure 1.
Next, you will have to put some stacking headers directly onto the motor shield. Here is the tricky part because you need to solder the push buttons from the syringe pump such that they will shunt pin 8 and pin 12 to ground. Check Figure 2 to see how it looks like. The button from the front part of the syringe pump should be connected to pin 8 and ground and the button from the back part of the syringe pump (the one close to the motor) should be tied to pin 12 and ground. If you are not comfortable soldering directly the wire on the stacking header you can use a Proto Shield for the Arduino Uno rev3.
The idea is that once the syringe pump triggers one of the button, the corresponding pin will be set to low voltage while in the absence of trigger, the pin is internally (in the Arduino chip) connected to +5V through a 20 kΩ resistor (weak pull-up). That way, we can detect a stop condition to prevent damaging the pump hardware.
Finally, just plug the LCD shield on top and plug the 12V power supply into the Arduino. After uploading the program given below in the resources section, you should get the result of Figure 3. If nothing is displayed on the LCD screen, try turning the contrast potentiometer of the LCD shield as recommended by Adafruit.
You can travel the different options in the menu by pressing the up/down buttons. Then, press the left/right buttons to change the values. To start or stop the pump, press the select button.
If you are using a syringe with a diameter different than mine (12 mm) or a different thread pitch than M6 (1 mm), you can tune the value in the menu using the “Diameter” and “Pitch” vars. You can then select a flow rate, an injection volume and a working mode. There are three different working modes: push (inject), pull (withdrawn) and cycles which alternates push and pulls. In push/pull mode, the pump will automatically stop when the specified volume has been reached (e.g.: 5.000 ml on Figure 3) or when the stop buttons are triggered. In cycle mode, it’s a bit different because the stop conditions will not stop the pump but alternate the push/pull modes. So if you set cycles with 1.000 ml volume, the pump will first inject 1 ml then withdrawn 1 ml, inject another 1 ml and so on. By the way all the values you configure are stored in the Arduino EEPROM memory every time you press the select button.
Let’s do some math now.
We know that the ST35 motor requires 2,040 ticks for one full shaft revolution so the volume injected per tick is:
with d the syringe piston diameter in millimetre, p the pitch of the thread in millimetre and ∆V the volume per ticks in microlitres. Note that this is only valid for full steps, if we are using half steps we have to divide this by two and if we use micro-stepping we have to divide this by the number of microsteps in one full step (usually 16). This is why I prefer talking about “volume per tick” and not “volume per step” which my be misleading in the implementation.
Next, we know that the flow rate is the rate at which we dispend the volume ∆V:
with ∆t the interval between two ticks. Q is given in microlitres/h and ∆t in millisecond.
This flow rate will be limited by the maximum pulse rate of the ST35 which is about 800 ticks/s. However, I have found that the software I developed is too slow to manage both the LCD and the stepper motor at that rate. I have measured a minimum ∆tmin of 6 ms required. The maximum flow rate is then equal to:
Conversely, the minimum flow rate will be limited by the maximum time step between two ticks:
I recommend not going above 1 second for this but this may change depending on your particular application. In the software here, I have considered a maximum 1 second which then limits the dynamic range to 6/1,000 ≈ 1:167. For a 12 mm syringe piston and a M6 pitch, this gives a range from 0.200 ml/h to 33.264 ml/h. Other thread pitches and syringe will give different values but the ratio between the slowest and fastest rate will always be 167x in our current implementation.
Any flow rates between Qmin and Qmax can be obtained by using a time step of:
Finally, I would like to discuss a small trick that I have used in the software to expand the range slightly. So far, we have considered constant step sizes for a tick. This allows to achieve larger flow rate (one large step will always dispend more volume than a small one) but it restricts lower flow rates due to the maximum time steps imposed. Smaller step sizes, on the other hand, allow lower flow rates to be more accurate but we cannot do as fast as with the larger steps due to the minimum time imposed by the software loop.
The trick consists of changing the step size depending on the flow required. Fast rate (small ∆t) will use full steps while smaller rate (longer ∆t) will use half-steps or even micro-steps. Here, I am switching from full steps to half-steps only because the micro-steps gave strange results with the ST35 motor.
You now have everything on hand to understand the software, so have fun!
To program your Arduino with our software you will first have to download the Arduino IDE and install the libraries for the LCD and Motor Shield as described by Adafruit and Arduino. Check their respective websites for tutorials on this.
Once you have configured your Arduino IDE, create a new file and copy/past the following code:
#include <Wire.h>
#include <EEPROM.h>
#include <Servo.h>
#include <Adafruit_MotorShield.h>
#include <Adafruit_MCP23017.h>
#include <Adafruit_RGBLCDShield.h>
#define MODE_PUSH 0
#define MODE_PULL 1
#define MODE_CYCLES 2
#define MAX_MENUS 5
class TuneVar
{
public:
TuneVar(long iDefaultValue, long iMinValue, long iMaxValue, long iLargeStepInc, long iSmallStepInc)
{
this->m_iVar = iDefaultValue;
this->m_iMin = iMinValue;
this->m_iMax = iMaxValue;
this->m_iLargeStep = iLargeStepInc;
this->m_iSmallStep = iSmallStepInc;
}
void inc(bool bLargeStep)
{
setValue(getValue() + (bLargeStep ? this->m_iLargeStep : this->m_iSmallStep));
}
void dec(bool bLargeStep)
{
setValue(getValue() - (bLargeStep ? this->m_iLargeStep : this->m_iSmallStep));
}
long getValue(void) const
{
return this->m_iVar;
}
void setValue(long iValue)
{
if(iValue < this->m_iMin)
iValue = this->m_iMin;
if(iValue > this->m_iMax)
iValue = this->m_iMax;
this->m_iVar = iValue;
}
void setMax(long iMax)
{
this->m_iMax = iMax;
setValue(getValue());
}
long getMax(void) const
{
return this->m_iMax;
}
void setMin(long iMin)
{
this->m_iMin = iMin;
setValue(getValue());
}
long getMin(void) const
{
return this->m_iMin;
}
void setSteps(long iLargeStepInc, long iSmallStepInc)
{
this->m_iLargeStep = iLargeStepInc;
this->m_iSmallStep = iSmallStepInc;
}
void autoConfig(void)
{
setMax(this->m_iMin * (this->m_iMax / this->m_iMin));
this->m_iSmallStep = this->m_iMin;
this->m_iLargeStep = 9 * this->m_iMin;
}
private:
long m_iVar, m_iMin, m_iMax, m_iLargeStep, m_iSmallStep;
};
Adafruit_MotorShield AFMS = Adafruit_MotorShield();
Adafruit_RGBLCDShield lcd = Adafruit_RGBLCDShield();
TuneVar g_cMode(0, 0, 2, 1, 1);
TuneVar g_cRate(1000, 100, 16500, 900, 100);
TuneVar g_cVolume(5000, 10, 5000, 90, 10);
TuneVar g_cDiameter(12, 1, 40, 6, 1);
TuneVar g_cPitch(1000, 200, 5000, 90, 10);
TuneVar g_cMenu(0, 0, 4, 1, 1);
TuneVar *g_pMenuVars[MAX_MENUS] = {&g_cRate, &g_cMode, &g_cVolume, &g_cDiameter, &g_cPitch};
bool g_bRunning = false;
unsigned long g_ulStepSize = 0;
unsigned long g_ulCurrentVolume = 0;
char g_cCurrentDirection = 0;
double g_fMicroliterPerTicks = 0;
unsigned long g_ulNumTicks = 0;
TuneVar *g_pCurrentVar = g_pMenuVars[0];
Adafruit_StepperMotor *myStepper = AFMS.getStepper(2040, 2);
Servo g_cStopCock;
unsigned long g_ulDetachServo = 0;
#define IDENT_VERSION 0xC0
struct save_s
{
uint8_t uIdent;
uint8_t uChecksum;
uint8_t uNumMenus;
long iValues[MAX_MENUS];
};
uint8_t checksum(byte *pStream, uint8_t nSize)
{
uint8_t ret = 0;
while(nSize--)
{
ret += ~(*pStream);
pStream ++;
}
return ~ret;
}
void writeEEPROM(byte *pStream, uint8_t nSize)
{
for(uint8_t i=0;i<nSize;i++)
EEPROM.write(i, pStream[i]);
}
void readEEPROM(byte *pStream, uint8_t nSize)
{
for(uint8_t i=0;i<nSize;i++)
pStream[i] = EEPROM.read(i);
}
void save(void)
{
struct save_s s;
for(uint8_t i=0;i<MAX_MENUS;i++)
s.iValues[i] = g_pMenuVars[i]->getValue();
s.uIdent = IDENT_VERSION;
s.uNumMenus = MAX_MENUS;
s.uChecksum = 0;
s.uChecksum = checksum((byte*)&s, sizeof(s));
writeEEPROM((byte*)&s, sizeof(s));
}
void load(void)
{
struct save_s s;
readEEPROM((byte*)&s, sizeof(s));
if(s.uIdent == IDENT_VERSION && s.uNumMenus == MAX_MENUS)
{
uint8_t oldChecksum = s.uChecksum;
s.uChecksum = 0;
if(checksum((byte*)&s, sizeof(s)) == oldChecksum)
{
for(uint8_t i=0;i<MAX_MENUS;i++)
g_pMenuVars[i]->setValue(s.iValues[i]);
}
}
reconfigure();
}
void reconfigure(void)
{
double pitch = 0.001 * (double)g_cPitch.getValue();
double d = (double)g_cDiameter.getValue();
g_fMicroliterPerTicks = 1.924995499e-4 * pitch * d * d;
double fK = 1385.996759 * pitch * d * d;
double fMaxVolume = 39.26990817 * pitch * d * d;
g_cVolume.setMin(ceil(0.001 * fMaxVolume));
g_cVolume.setMax(floor(fMaxVolume));
g_cVolume.autoConfig();
g_cRate.setMin(ceil(fK / 2000.0));
g_cRate.setMax(floor(fK / 6.0));
g_cRate.autoConfig();
}
bool compareTime(unsigned long ulAction)
{
return ulAction <= millis();
}
bool isPushing(void)
{
return (g_cMode.getValue() == MODE_PUSH) || (g_cMode.getValue() == MODE_CYCLES && g_cCurrentDirection == +1);
}
bool isPulling(void)
{
return (g_cMode.getValue() == MODE_PULL) || (g_cMode.getValue() == MODE_CYCLES && g_cCurrentDirection == -1);
}
void updateStopCock(void)
{
if(isPushing())
servoGoto(0);
else if(isPulling())
servoGoto(180);
}
void setup(void)
{
pinMode(8, INPUT_PULLUP);
pinMode(12, INPUT_PULLUP);
AFMS.begin();
g_cStopCock.detach();
lcd.begin(16, 2);
lcd.setBacklight(0x1);
load();
// stupid hack
load();
printValues();
}
void servoGoto(int iPos)
{
g_ulDetachServo = millis() + 2500;
g_cStopCock.attach(10);
g_cStopCock.write(iPos);
}
void stopCondition(void)
{
if(g_cMode.getValue() == MODE_CYCLES)
{
if(g_cCurrentDirection == +1)
g_cCurrentDirection = -1;
else if(g_cCurrentDirection == -1)
g_cCurrentDirection = +1;
updateStopCock();
g_ulCurrentVolume = 0;
g_ulNumTicks = 0;
printRun();
}
else
{
g_bRunning = false;
printValues();
}
}
void timerInterrupt(void)
{
static unsigned long ulNextStep = 0;
if(!g_bRunning)
return;
if(!compareTime(ulNextStep))
return;
if(g_ulStepSize > 10000)
ulNextStep = millis() + g_ulStepSize / 2;
else
ulNextStep = millis() + g_ulStepSize;
bool bPushing = isPushing();
bool bPulling = isPulling();
if(((digitalRead(8) == 0) && bPushing) || ((digitalRead(12) == 0) && bPulling))
{
stopCondition();
return;
}
if(bPushing)
{
if(g_ulStepSize > 10000)
myStepper->onestep(FORWARD, INTERLEAVE);
else
myStepper->onestep(FORWARD, SINGLE);
}
else if(bPulling)
{
if(g_ulStepSize > 10000)
myStepper->onestep(BACKWARD, INTERLEAVE);
else
myStepper->onestep(BACKWARD, SINGLE);
}
if(g_ulStepSize > 10000)
g_ulNumTicks ++;
else
g_ulNumTicks += 2;
g_ulCurrentVolume = (unsigned long)(g_fMicroliterPerTicks * (double)g_ulNumTicks);
if(g_ulCurrentVolume >= g_cVolume.getValue())
stopCondition();
}
bool checkCommand(void)
{
static unsigned long ulLastButtonTime = 0;
static uint8_t lastButtonState = 0;
uint8_t buttons = lcd.readButtons();
/* do not accept multiple button press */
if(buttons != 0 && (buttons & (buttons-1)) != 0)
return false;
if((buttons & BUTTON_SELECT) != 0)
{
g_bRunning = !g_bRunning;
if(g_bRunning)
{
g_ulNumTicks = 0;
g_ulCurrentVolume = 0;
g_cCurrentDirection = +1;
updateStopCock();
save();
}
else
g_cStopCock.detach();
delay(500);
return true;
}
if(g_bRunning)
{
lastButtonState = buttons;
return false;
}
if((buttons & BUTTON_LEFT) != 0)
{
if(compareTime(ulLastButtonTime + 1000) && (lastButtonState & BUTTON_LEFT) != 0)
g_pCurrentVar->dec(true);
else if(lastButtonState == 0)
{
g_pCurrentVar->dec(false);
ulLastButtonTime = millis();
}
delay(50);
}
else if((buttons & BUTTON_RIGHT) != 0)
{
if(compareTime(ulLastButtonTime + 1000) && (lastButtonState & BUTTON_RIGHT) != 0)
g_pCurrentVar->inc(true);
else if(lastButtonState == 0)
{
g_pCurrentVar->inc(false);
ulLastButtonTime = millis();
}
delay(50);
}
else if((buttons & BUTTON_UP) != 0)
{
if((lastButtonState & BUTTON_UP) == 0)
g_cMenu.dec(false);
delay(50);
}
else if((buttons & BUTTON_DOWN) != 0)
{
if((lastButtonState & BUTTON_DOWN) == 0)
g_cMenu.inc(false);
delay(50);
}
if(buttons == 0)
ulLastButtonTime = millis();
lastButtonState = buttons;
return buttons != 0;
}
void printMode(void)
{
long iMode = g_cMode.getValue();
lcd.print("Mode: ");
switch(iMode)
{
case MODE_PUSH:
lcd.print("PUSH ");
break;
case MODE_PULL:
lcd.print("PULL ");
break;
case MODE_CYCLES:
lcd.print("CYCLES ");
break;
}
}
void printRate(void)
{
long iRate = g_cRate.getValue();
lcd.print(" ");
if(iRate >= 1000000)
lcd.print((iRate / 1000000) % 10);
if(iRate >= 100000)
lcd.print((iRate / 100000) % 10);
if(iRate >= 10000)
lcd.print((iRate / 10000) % 10);
lcd.print((iRate / 1000) % 10);
lcd.print(".");
lcd.print((iRate / 100) % 10);
lcd.print((iRate / 10) % 10);
lcd.print(iRate % 10);
lcd.print(" ml/h ");
if(iRate < 1000000)
lcd.print(" ");
if(iRate < 100000)
lcd.print(" ");
if(iRate < 10000)
lcd.print(" ");
}
void printVolume(void)
{
long iVolume = g_cVolume.getValue();
lcd.print("Vol.: ");
if(iVolume >= 10000)
lcd.print(iVolume / 10000);
lcd.print((iVolume / 1000) % 10);
lcd.print(".");
lcd.print((iVolume / 100) % 10);
lcd.print((iVolume / 10) % 10);
if(iVolume < 10000)
lcd.print(iVolume % 10);
lcd.print(" ml ");
}
void printDiameter(void)
{
long iDiameter = g_cDiameter.getValue();
lcd.print("Diameter: ");
if(iDiameter >= 10)
lcd.print((iDiameter / 10) % 10);
lcd.print(iDiameter % 10);
lcd.print(" mm");
if(iDiameter < 10)
lcd.print(" ");
}
void printPitch(void)
{
long iPitch = g_cPitch.getValue();
lcd.print("Pitch: ");
lcd.print((iPitch / 1000) % 10);
lcd.print(".");
lcd.print((iPitch / 100) % 10);
lcd.print((iPitch / 10) % 10);
lcd.print(iPitch % 10);
lcd.print(" mm");
}
void printMenu(uint8_t menu)
{
switch(menu)
{
case 0:
printRate();
break;
case 1:
printMode();
break;
case 2:
printVolume();
break;
case 3:
printDiameter();
break;
case 4:
printPitch();
break;
}
}
void printValues(void)
{
uint8_t menu = g_cMenu.getValue();
g_pCurrentVar = g_pMenuVars[menu];
if(menu < (MAX_MENUS-1))
{
lcd.setCursor(0, 0);
lcd.print("\xff");
printMenu(menu);
lcd.setCursor(0, 1);
lcd.print("\xfe");
printMenu(menu+1);
}
else
{
lcd.setCursor(0, 0);
lcd.print("\xfe");
printMenu(menu-1);
lcd.setCursor(0, 1);
lcd.print("\xff");
printMenu(menu);
}
}
void printRun(void)
{
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Running... ");
lcd.setCursor(0, 1);
if(isPushing())
lcd.print("\x7E");
else if(isPulling())
lcd.print("\x7F");
}
void loop(void)
{
timerInterrupt();
if(compareTime(g_ulDetachServo) && g_cStopCock.attached())
g_cStopCock.detach();
if(checkCommand())
{
switch(g_cMenu.getValue())
{
case 0:
g_ulStepSize = (unsigned long)(g_fMicroliterPerTicks / (double)g_cRate.getValue());
break;
case 3:
case 4:
reconfigure();
break;
}
if(!g_bRunning)
printValues();
else
printRun();
}
}
Then plug your Arduino to your computer using a USB cable and upload the program. It should compile without errors. If not, check that you have installed the Adafruit libraries correctly. Don’t forget the external power supply because it won’t run from the USB power alone.
You may also like:
[»] Ultra-low Flow Rates with DIY Syringes Pumps
[»] Airsoft Electronic Shooting Targets Game
[»] Adding an actuated flow selector valve to our syringe pump