Microfluidics control system

An all-in-one solution for controlling microfluidic chips

Similar projects worth following
Microfluidics are a highly useful and increasingly popular tool for research in biology (and other fields). Miniaturizing experiments makes it possible to have a tighter control on experimental conditions, run many assays in parallel and even diminish costs compared to conventional techniques.

Unfortunately, the hardware needed to control these microfludic chips is prohibitively expensive, especially for labs (or DIY biologists!) that are considering trying out microfluidics without committing tens of thousands of dollars to equipment.

The ultimate goal of this project is to make microfluidics more accessible by providing a simple, open source, all-in-one control solution for a much lower cost than comparable commercial controllers. It is suited to some of the most common types of microfluidic chips, and offers both manual and automated control via a PC or tablet interface.

Hopefully, this will help lower the entry barrier to microfluidics for labs and DIY biologists alike.


A brief overview of microfluidics

Microfluidics are often hailed as a revolution in biology, akin to how the transistor and ICs revolutionized electronics and computing. Chips can be designed to run many different kinds of lab experiments, with the advantage that they use much lower volumes of reagents, and are usually faster and more accurate than "conventional" assays. 

Some common applications of microfluidics include diagnostics (e.g. detection of disease markers from a single drop of blood), drug screening, DNA sequencing and cell culture. This Wikipedia article gives a good introduction to the field.

Practically, fluids have to be pushed around the chips somehow. There are several ways to do this. One is to use passive flow, which relies on the affinity of the fluid and the substrate to pull the fluid along. This is used for example in glucose test strips, which are basically strips of paper that wick a drop of blood to an electrochemical sensor. 

Another -- very cool -- way of moving fluids around a chip is electrowetting, which in a nutshell consists of applying a high voltage to electrodes on the surface of the chip to make a given electrode more hydrophilic, thus allowing you to pull a droplet of water along. A great example of this is OpenDrop.

Finally, fluids can be pushed around by air pressure. This is usually accomplished either with a syringe pump, which establishes a given flow rate, or with a source such as a pump and pressure regulator, which impose a given pressure.

There are of course other methods, but these are the most common. Without going into too many details, it is this last method -- pressure-driven flow -- which we need to operate our microfluidic chips. In our lab, we use these chips to culture cells in a way that simulates "real-life" tissue. This allows us to carry out experiments to better understand how cells interact in vivo, while keeping them in a highly controlled environment, and allowing us to run many experiments in parallel with relatively little manual labor. 

If you are interested in learning more about microfluidics and how they can be applied to biology, here is a great introductory lecture on the subject. The type of chips that we use (and is mentioned in that lecture) are constructed of two layers of PDMS. These are great for our application since they make it easy to integrate valves into our designs, allowing us to route fluids to specific parts of a chip, and run many different experiments in parallel on a single chip.

Control systems for PDMS chips

In short, to operate our chips, we need:

  1. A positive pressure source
  2. A vacuum source
  3. A way to regulate pressure (at three different pressure levels, including vacuum)
  4. Valves to switch pressure on & off on up to 32 lines independently
  5. A way to automate everything

While this is specific to our application, it should be noted that most of these requirements (especially numbers 1 and 3, and to some extent 5) are common to every lab using similar microfluidic chips, so this project could benefit many other labs and even DIY biologists.

Now, there are commercial solutions for controlling microfluidics. You get either a modular system with separate pressure sources, valves and so on, or a nice all-in-one unit. Unfortunately, all of these tend to be rather expensive (5-10k USD or more), and not always suited to one's exact application. For example, you might find commercial controllers with 16 valves; if you need just a few more, then you have to shell out an extra few thousand dollars for a second valve control module.

Therefore, many labs go for a more DIY approach.

Typically, the pressure sources are either central compressed air for the fancy labs, or large pumps and tanks for the plumbing-impaired labs. The pressure is then adjusted using a manual regulator and the air is split by a manifold to the many different lines, each of which is controlled either with manual valves, or solenoid valves...

Read more »

  • 1 × ESP32-DevKitC Espressif's official ESP32 development board
  • 1 × PCA9698 I2C 40-channel IO expander
  • 1 × Parker Hannifin 990-005203-005 Pressure regulator, 0 - 345 mmHg
  • 1 × Parker Hannifin H085-11 Pump (for positive pressure, up to 23 PSI)
  • 1 × Parker Hannifin E129-12-120 Vacuum pump

View all 14 components

  • PC code

    Craiga day ago 0 comments

    Now that most of the hardware- and microcontroller-related stuff has been covered, let's take a look at the PC code.

    You can find the repository here:

    As a reminder, the requirements for this part were, mainly, to be able to communicate with the ESP32 over USB (and eventually bluetooth), to show a usable and if possible not-too-ugly user interface, and to be cross-platform, including Android and/or iOS.

    I have quite a lot of experience with Qt, and it happens to be one of the best frameworks for this application: first of all, it is cross-platform, making it very easy to adapt the code to run on Windows, Linux or Mac but also mobile devices; it has libraries for serial communication (which are, again, cross-platform) and some good libraries for GUI creation. Another important aspect is that is coded in C++, just like the ESP32 (so there is no need to know two different programming languages so start working on this project).

    Here again, I tried to make everything as modular as possible. The serial communication, routines, and GUI back-end are almost completely independent of one-another. So if one part needs to be rewritten, there is no need to refactor the whole codebase.


    Right now, everything but the routines (i.e., pre-programmed experiments) is functional. Serial communication works, and the user interface allows to both send commands to the ESP32, and display status of all the components. The interface still needs some work, but it is at least functional and close to a final design. Here's what it looks like:

    Behind the scenes, the main classes are:


    The main class. This provides the interface between the various components of the backend (serial communication, routine controller) and the GUI, written in QML.


    Handles serial communication. A few functions can be called to set a given component to a given value (for example, setValve()); it also reads incoming data to get status updates on components. When that happens, a signal is emitted. Signals and slots are very useful to communicate between different classes.

    PCHelper, ValveSwitchHelper 

    These two little classes make it easier to interface between the QML front-end and the rest of the backend. They are essentially a backend for the buttons shown on screen. Without going into too many details here, these classes make it possible to keep the QML code very simple, while also allowing the backend (ApplicationController) to keep track of which button on the screen corresponds to which physical component.

    For example, to define the buttons (1-23) shown in the above screenshot, one simply has to declare:

    ValveSwitch {
        valveNumber: 17

    And at runtime, the backend will be informed of the existence of this button, and update its status whenever the Communicator class tells it that valve number 17 has been toggled.


    I am a firm believer that anything that can be automated should be automated, and so I intend to have my control system run my experiments for me. This will be accomplished by the RoutineController class, which is partially coded but not yet accessible from the GUI.

    The comments in the source code explain the concept quite thoroughly, which I'll just summarize here.

    As a compromise between user-friendlyness and ease of development, I decided to have routines in a text format, which is parsed at run-time. (The two ends of the spectrum would be a complete GUI-based solution, to be super user-friendly, on one end, and a routine defined entirely in C++ on the other).

    The format is easy to understand and edit:

    # Comments begin with a number sign.
    # Empty lines are ignored
    pressure 1 7.5 psi # This sets pressure regulator 1 to 7.5 psi
    pressure 2 20 # If units aren't specified, the default is used. Here, it is pounds per square inch (PSI)
    valve 2 close
    valve 3 close
    valve 4 close
    valve 14 open
    wait 30 seconds # pauses can be in units of seconds, minutes or even hours
    valve 14 close
    Read more »

  • Hardware assembly

    Craiga day ago 0 comments

    Ideally, I'd like a nice enclosure for this project, perhaps made out of sheet metal. 

    For now however, I just needed something to hold all the components together, with enough space to access everything, and connect tubes and cables easily.

    So I ordered a piece of acrylic on Amazon, threw together an assembly in Inventor, printed out the drawing, taped it to said piece of acrylic (when it finally arrived a week or two later), and got to work on the drill press:

    This makerspace has a CNC, a waterjet cutter and several laser cutters, but I wanted this done quickly, and I didn't want to take the time to learn how to use those machines. For a temporary solution, this did the job perfectly.

    A couple of hours later, I had this shiny base plate:

    Thankfully, my drilling was precise enough and I was able to screw everything in:

    While I was at it, I made myself a little debugging aid:

    These are WS2812 LEDs, which I can use to indicate the status of each solenoid valve. Green = open, red = closed. This is pretty useful to test the system (especially the PC interface and serial communication) without having to check whether compressed air is coming out of whatever valve was just toggled.

    Plus, blinky lights make every project better.

  • ADC & DAC calibration

    Craiga day ago 0 comments

    One important aspect of the ESP32 code that wasn't mentioned in the previous log concerns the analog <-> digital conversion.

    Long-term, the idea is to integrate pressure regulators into the design of the PCB, but for now we use commercial ones, which require an analog voltage between 0 and 5 volts for the setpoint, and indicate the current measured pressure in the same way.

    This was mostly important for the hardware design, as I explained in the selection of PCB components, and the PCB design. However, there were also software consequences.

    It turns out that the ESP32 ADC isn't quite linear. Same goes for the DAC, especially given that the output is amplified. Therefore, some calibration was necessary.

    For the ADC part, this consisted simply in connecting a variable power supply to the ADC pins, applying voltages between 0 and 5V (in 0.5V increments), and outputting the value of analogRead() to serial using a short sketch I wrote for this purpose. I wrote down the values and plotted them in Excel, which gave the following results:

    PR1, PR2 and PR3 are the three ADC pins; the values were virtually identical for all three.

    To correct the error, I fit a polynomial to these values, and this polynomial is used when reading the current pressure:

    uint8_t PressureController::getValue()
        int val = analogRead(mMeasurementPin);
        double x = val;
        double y = -3e-12*pow(x, 3) - 7e-10*pow(x, 2) + 0.0003*x + 0.0169;
        mMeasuredValue = std::min(255, std::max(0, int(round(y*UINT8_MAX))));
        return mMeasuredValue;

    This actually works pretty well, and the measured value is now close enough to the actual value to be usable.

    For the DAC part, the process was similar, with a short sketch that read a value sent over serial and applied that voltage to all three analog outputs.

    The ESP32 doesn't yet have an implementation of Arduino's analogWrite() function. Instead, it has dacWrite, for the two channels of the built-in DAC, and ledCWrite, which is pretty much identical to Arduino's analogWrite() and drives the PWM output.

    The error was much smaller than with the ADC:

    Still, it's enough to warrant some correction, so the PressureController::setValue function was modified to correct the output, again based on a polynomial fit done in Excel.

    The complete spreadsheet, including values post-correction, are available on the Github repository, here.

  • ESP32 code

    Craig10/13/2017 at 18:39 0 comments

    The code is hosted on GitHub, here:

    First, a few words about the repository structure.


    One handy advantage of the ESP32 is that it (partially) supports Arduino libraries. This makes it quite a bit more accessible to people who are unfamiliar with microcontrollers, and was one of the reasons why I chose to use it for this project. It also makes it possible to use C++. As you will notice if you go through the code, I am quite fond of object-oriented programming, and have made an effort to compartmentalize the code as much as possible, separating logical units of it into different classes.

    Programming the ESP32 is made even easier through the use of PlatformIO, an IDE that handles all the complicated business of downloading the correct toolchains, managing libraries, and building and uploading code with minimal effort. This is why the repository has a "lib" folder and a "platformio.ini" file in its root. All of the actual code is in "src" and consists of just a few files which are explained below.

    Code structure

    If you are familiar with the Arduino way, you will know that all programs, or sketches, center around two functions: the setup() function, which is executed once when the microcontroller starts up, and the loop() function, which is executed over and over. While this is the case here, we actually offload all the functionality to the Controller class. This class has its equivalent of setup and loop, and has member variables to keep track of all information that must persist across calls to loop. This is a neater solution than the usual Arduino way of declaring global variables in the main sketch file.

    So, the Controller class is the main class; it handles all the top-level functionality, as well as serial communication (which is very simple, and handled by 2-3 short functions).

    The microcontroller can receive a few simple instructions from the computer: either a request for the status of a given component (say, the current pressure recorded by pressure controller number two), or a request to set a component to a given state. For example, switch pump 2 off, open valve 12, or set pressure 1 to 5 PSI. In these latter cases, the request is very similar for all components, and the general behavior of the microcontroller is the same (set this component to that value). However, the details of how to accomplish that will vary from one component to the next. For example, opening a valve will require setting a given I/O pin either to HIGH (+3.3V) or LOW (GND); while setting a pressure will require setting a certain analog voltage on another pin.

    Therefore, to separate the high-level logic from the implementation, the valves, pumps, and pressure controllers are each represented by a different class, all of which inherit from the same base class. This base class presents all that the controller needs: a function to set a certain state, and a function to retrieve the current state. The derived classes then implement these functions in different ways, depending on the specific components.

    This way, when the Controller receives a request to set Component X to value Y, it simply calls X's setValue function, passing it Y, and lets X handle the specifics. It doesn't need to know whether X is a valve or pump, or what Y means and how to translate it.

    All the hard-coded constants, such as the ones indicating which component is connected to which IO pin, are found in the constants.h file.

  • Software overview

    Craig10/13/2017 at 15:24 0 comments

    In parallel with the PCB design (or rather, while waiting for the boards to arrive), I worked on the software design.

    Here is roughly what the overall software architecture looks like:

    Basically, the idea is to have a GUI that allows manual control of each electromechanical component, displays the status of each component (e.g on/off for the pumps, open/closed for the valves), and can store sequences of actions ("routines", in the diagram) as a way to automate experiments.

    For each action, e.g "open valve 12", the PC sends a command to the microcontroller which executes it, then sends back the component's new value. The microcontroller also sends the current recorded pressures at regular intervals, whether or not the setpoints have changed.

    I decided to have the PC side handle routines, as that would make it easier for end-users to work with them (no need to reprogram the microcontroller to modify an experiment) and to simplify the serial communication. This means that there is a fairly small number of things that can be sent over serial. The PC can request:

    • To open or close valves 1 through 32
    • To switch either of the two pumps on or off
    • To set a given pressure regulator to a given pressure
    • A status update, to obtain the current value of all components

    And the microcontroller can send the current status of a component.

    I'm sure there are many ways of doing this; I wanted to make the communication as fast as possible while still being somewhat human-readable and easy to modify. 

    The way I settled on was to send two bytes for each command: the first byte designates the component, and the second byte the value. The same values can be used by the PC to request a component change, and by the microcontroller to indicate a component's current status. This way, I would transmit a very small amount of data each time, and keep processing times low -- at least compared to something like parsing a JSON request.

    For readability, a file defines a list of constants, defining e.g VALVE12, PUMP2 and so on. Of course, this file has to be on both the microcontroller and PC side, which means there is an extra step if these values need to be modified, but I felt that this was a low price to pay for speed.

    In the next log, we'll take a closer look at the microcontroller side of things.

  • PCB assembly

    Craig10/05/2017 at 20:37 0 comments

    With the PCB design finished, double-checked and triple-checked, I ordered some boards on DirtyPCBs.

    This is a ridiculously cheap service; it was even cheaper than OSHPark and any other manufacturer I found. I got around 10 boards for 29$ + 6$ shipping.

    One concern I had with the manufacturing was the minimum soldermask size. The PCA9698 (IO expander) has a TSSOP-56 package, which has 28 pins on each side, with a pitch of 0.5mm. That leaves very little space for error, and having soldermask between the pins helps to avoid bridging when soldering.

    I thought my PCBs would have this soldermask, but either I was mistaken when reading through DirtyPCBs's specs, or I made an error in my designs, because they arrived with no soldermask between these pins.

    Luckily it turned out to be easier to solder than I expected, and a little wick was enough to fix the few errors I made. This step, by the way, would not have been possible without a microscope to help with soldering and inspecting. Thankfully, our university has an amazingly well-equipped makerspace that has some good irons and a couple of microscopes.

    Soldering the rest of the components was rather straightforward, and so after a couple of hours, I was able to turn this:

    Into this:

    The ESP32 is sitting in some female headers I put in so I could easily make modifications or plug other things in. As you may be able to tell from the jumper wires, that came in quite handy.

    Just after soldering the last components, I noticed this:

    PSA: check the orientation of your datasheet when you design a footprint.

    Luckily for me, that side is only used for a few pins: the SDA and SCL lines for I2C, and a couple of ground pins. So the fix was a relatively simple matter of desoldering the headers, cutting them and putting just a few back, for the actual SDA & SCL lines, and using some jumper to connect the old pins to the new, correct ones.

    The yellow and green wires correct my poor design skills; the blue wire, to the left, was added after I accidentally nicked a trace while repairing some other issues.

    The other hardware issues were:

    1) The pins labeled as GPIO9 and 10 on the ESP32 datasheet cannot actually be used as GPIO. They serve only for Flash. This is why you can see jumpers in the second image, above.

    2) I connected the ESP32 dev board's 5V to the shield's 5V, which was silly.  The dev board doesn't like to be powered both by USB and an external supply at the same time (and in fact now that I write this, I seem to remember that it can't be powered by external 5V at all; but I'm not sure about this). So I also removed this pin, and power the ESP through USB, which is fine for now.

    After fixing these issues, everything worked as intended, and I was able to continue with assembling the hardware, and programming everything.

  • PCB design

    Craig10/04/2017 at 19:31 0 comments

    As explained previously, the PCB is a shield for an ESP32 development board. There are many development boards out there, each with varying shapes and pin layouts. I chose to use the "official" dev kit C, rather than an obscure 3rd party one, assuming that the official one would be easier for other people to source, should anyone want to build their own control system.

    I also wanted to make the pins accessible, to easily add components or fix bad connections with jumper wires (which turned out to be a good decision, as you will read later...). 

    The other connectors are:

    • 4 2x5 headers for solenoid valves (each connector being used for 8 valves, with two common +12V pins)
    • 3 1x4 headers for the pressure regulators
    • 2 1x2 headers for the pumps
    • 1 1x3 header for the external 12->24V step-up regulator
    • 1 barrel jack

    And the other components (all surface-mount, except for the first one):

    • 12V->5V regulator (Recom R-78)
    • 1 PCA9698 GPIO expander (TSSOP 56 package)
    • 5 ULN2803 darlington arrays (SOIC 18 package)
    • 1 TLV2374 quad op-amp (SOIC 14 package)
    • Various resistors and capacitors (all 0603 packages)

    The full schematic is available in the "files" section and is mostly quite simple, so I won't go over the whole thing; just some of the circuits that were discussed in the last log.

    Still, here is low-res overview of the schematic:

    First, the ULN2803 used to power the two pumps (bottom-left of the schematic):

    This is very straightforward: two digital pins from the ESP connect to four inputs of the darlington array each; the four corresponding outputs will be connected to the pumps' negative leads.

    The solenoid valves are wired very similarly. The only differences are that only one pin of the ULN2803s are necessary for each valve, and that they are connected to the PCA9698 rather than to the ESP32 directly. 

    Besides the obvious power and ground pins, and the I/O pins, this is the important part of the PCA9698 schematic:

    SDA and SCL are for I2C communication; AD0, 1 and 2 are used to specify the chip's address: you could actually use up to 8 of these (if for some reason you need 320 I/O pins for your project), in which case you could set the address of the PCA9698 by tying those pins either to HIGH or LOW. This one has the address 0 0 0.

    OE (output enable) and RESET are connected to two pins of the ESP32. This is actually optional; they could also be tied to GND (see the introduction section of the XIO library reference).

    Finally, analog inputs & outputs, used to set the pressure regulators' setpoints and read their current pressure, respectively:

    PR[N]_SET is the input to the pressure regulator (i.e. setpoint); PR[N]_OUT is the output from the regulator (i.e. current measured pressure). The same circuit is used for the first two pressure regulators, except for the low-pass filter on the left, which is used only for this one (see previous log).

    I decided to use a TLV2374 quad op-amp because it provides enough op amps in a small package; it is rail-to-rail so I can power it with 5V and get an output of (almost) 5V, and most importantly, it was in the parts drawer of the electronics lab next door.

    I will skip over the details of the PCB layout, and just hit you with the final design:

    The front has most of the components and connectors. The ESP32 dev board plugs in on the left (with a double row of headers to make it easy to connect other stuff to it later on). In the center-top is the power input and 5V regulator, as well as the headers for the 12->24V step-up. The PCA9698, in the center, branches out to four ULN2803s, each connecting to headers for 8 valves.

    At the center-bottom are the connectors for the pressure regulators and pumps.

    The back has a ground plane, as well as a few components that wouldn't fit on the front:

    This does make it impossible (or complicated) to reflow, but I was planning on soldering everything by hand anyway, so this allowed me to keep the board quite small.

  • PCB components

    Craig10/04/2017 at 16:41 0 comments

    Now that we know what pneumatic components and microcontroller we are using, let's go over how to interface the two. 

    The solenoid valves are controlled individually: apply 12V to power them on; open the circuit to shut them off (or vice-versa, for a normally-open valve). So I needed one digital output pin and one transistor per valve, as well as a 12V power supply.

    Given that the ESP32 only has around 20 usable GPIO pins, I also needed an I/O expander. The best option I found was the PCA9698. It is controlled over I2C, has 40 I/O pins and, to make my life extra-easy, there is an Arduino library to use it, thanks to Iowa Scaled Engineering. Not too bad for 4$.

    So, with just the SDA, SCL and a couple other pins, the ESP32 can easily control a ton of outputs. 

    Next up are the transistors.  ULN2803s  are perfect for this: they have 8 darlington pairs which can sink up to 500mA each (at up to 50V), and can switch inductive loads such as solenoids thanks to the included flyback diode. I was hoping to find large darlington arrays controlled over I2C or SPI, instead of using separate components for this, but I was unable to find anything good. Still, the PCA9698 + ULN2803 combination is fairly compact and works great. 

    The ULN2803 is also useful for switching the pumps: although they may require more than 500mA, one can simply use several pins of the ULN in parallel to sink more current.

    So far, we have one PCA9698 and five ULN2803s (four to drive the 32 solenoid valves, and one for the two pumps).

    Next up are the pressure regulators. They are powered by 24V, and have an analog input and output (see previous log). Unfortunately, the analog voltages are between 0 and 5V, and the ESP32 functions on 3.3V. Another minor inconvenience is that the ESP has 2 DAC's, and I needed 3 pressure regulators. 

    The 5V -> 3.3V conversion is very straightforward: a simple voltage divider does the trick here. For the 3.3V -> 5V, I settled on using op-amps to make simple non-inverting amplifiers. This way, the two DAC pins can be used to control two of the pressure regulators. For the third, a PWM output of the ESP32 combined with an RC low-pass filter does a fairly good job of outputting an analog 0->3.3V voltage, which is then amplified to 0-5V just like the other two.

    Finally, power. The ESP32 requires 3.3V, but the development board (which I decided to use for this PCB, which is basically a shield for the dev board) has a regulator to power itself from USB. So we already have 5V and 3.3V. In order not to require USB (since bluetooth support is planned at some point), I decided to also include a 5V regulator.

    The solenoid valves and pumps, which require by far the most power of all the components here, require 12V. Therefore I decided to have a 12V power input (with a standard barrel jack) and use regulators for the other voltages I needed (5V, as mentioned, and 24V to power the pressure regulators).

    The 12->24V part was actually a little problematic. I couldn't find a compact, board-mount  regulator capable of delivering enough power -- up to 28W or so -- so in the interest of getting a working version done as soon as possible, I just went with an external regulator.

    This version of the control system isn't intended to be final: hence the use of the ESP32 development board, external pressure regulators, etc. It is a bit of a platform for experimentation, and I hope to build on this to make a more polished version.

  • Pneumatic components

    Craig10/02/2017 at 22:29 0 comments

    The first step is to find suitable components for the controller. As mentioned in the project description, we need to control pressure sources, regulate them at least moderately precisely, and be able to switch pressure on and off to many different tubes (~20-30 for our particular chips).

    In case you are not familiar with microfluidics, here is a quick summary to provide a little context for this project.

    The microfluidic chips we use are made of PDMS (polydimethylsiloxane, a transparent and flexible polymer). The way we create channels (through which we flow water, cells, and so on) in the chips is by "soft lithography" a.k.a. replica molding: the PDMS, which is a viscous liquid similar to epoxy when uncured, is poured onto a negative mold and allowed to cure in place. After this, the PDMS is peeled off of the mold and the chip is assembled: we use two layers of PDMS bonded to a glass substrate.

    The channels are on the order of tens of microns wide and tall; and to get fluids onto the chip, we punch holes in the PDMS and connect thin tubing to them. This is our chip-to-lab interface: it is to these tubes that we need to apply pressure to push stuff around on the chip, and open and close elastomeric valves on the chip.

    In the rest of this post, I will give an overview of the main components needed for the control system. 

    Read more »

View all 9 project logs

Enjoy this project?



Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates