Skip to the content
PCBKitNew board
2 layers76 × 91 mmMeniscus controller, the printed circuit board in 3D

Meniscus controller

EdSmithe/meniscus-controller

Closed-loop meniscus pressure controller for inkjet printing: custom KiCad PCB, Arduino control firmware, Python GUI

Size
76 × 91 mm
Copper layers
2
Thickness
1.6 mm
Parts
10
Nets
36
Pads
64
Tracks, vias
89, 1
Pours
1
Routed
Yes
KiCad
KiCad 9.0 (file 20241229)
Created
14 Sep 2026
Updated
14 Sep 2026

Overview

Meniscus controller is an open-source printed circuit board by EdSmithe, from the EdSmithe/meniscus-controller repository on GitHub. It is a 2-layer board of 76 × 91 mm with 10 parts on 36 nets, routed with 89 tracks and 1 via.

It is built around the MPXV5100DP and 0PTF0078P. Its parts include 5 connectors, 3 ICs and 1 capacitor. It belongs with the AVR (ATmega, ATtiny) boards of this library.

Main components

2

README

README.md

Meniscus Pressure Controller

A closed-loop pressure controller that holds the ink meniscus in a drop-on-demand inkjet nozzle. It runs a peristaltic pump forwards or backwards to hold a pressure setpoint between about −35 mbar and +15 mbar. It's built around a custom two-layer PCB that I designed in KiCad. This was my first PCB, and I learned KiCad from scratch to make it.

V1 controller board during assembly V1 board, milled in the department, during assembly. Including and OLED display and a rotary encoder.

Pump Design

PurposeHold the meniscus pressure at a piezo inkjet nozzle, with additional controls needed for experimental printing: Fill (vacuum) and Purge (positive pressure)
Control100 Hz loop, a two-mode controller with separate gain sets for vacuum and positive pressure, and pulse-density control below the motor's stall threshold
SensorNXP MPXV5100DP differential pressure sensor, read ratiometrically by a 10-bit ADC
ActuatorWilliamson 200 Series peristaltic pump (24 V DC), driven by an L298N H-bridge module
MCUArduino Nano (ATmega328P)
PCB2 layers, 76 × 91 mm, 1.6 mm FR-4, KiCad 9. V1 was milled in the department, V2 was made by PCBWay
InterfacesOLED and rotary encoder on the unit; text commands over USB serial; Python/browser GUI with live plotting and CSV logging
MechanicalSolidWorks mount that holds the whole pump assembly for lab use

How it's used on the rig

The printer deposits highly viscous inks, usually liquid crystals, through a piezo drop-on-demand nozzle. Inks like these leave very little margin on nozzle pressure. A few mbar too positive and ink weeps out of the nozzle; a few mbar too negative and air is drawn in and the nozzle de-primes. The working window is a slight vacuum that holds the meniscus flat, just inside the nozzle.

This controller holds that pressure in the ink reservoir and the line feeding the nozzle. Prints run for hours, and the pressure drifts as ink leaves the reservoir, so holding a level meniscus over a long print is the whole point of closing the loop rather than setting a head height and leaving it. Two manual functions cover the rest of the workflow: Fill draws ink through to prime the nozzle, and Purge pushes ink forwards to clear a bubble or a blockage.

What this project taught me

  • PCB design from zero. I learned schematic capture, footprint selection, a custom footprint for the encoder module, layout, design-rule checks, Gerber export and ordering from a PCB supplier, then took the board from perfboard to an in-house prototype to a professional board.
  • Keeping power away from the analogue signal. The motor's H-bridge sits off the board. The 24 V power path is grouped along one edge, and the sensor output runs only ~16 mm to the ADC pin.
  • Power-entry design. The 24 V input goes through a 5×20 mm cartridge fuse. A 470 µF bulk decoupling capacitor sits at the motor-driver supply terminals, sized for the pump's current peaks.
  • Reading the datasheet before choosing a part. My first 24 V → 5 V converter only holds its output at 5 V with a significant load. My board draws very little, so I had to redesign the power stage (details below).
  • Building a proper BoM, with reference designators, manufacturer part numbers, footprints, and on-board and off-board parts listed separately (hardware/bom.csv).
  • Tuning a nonlinear controller. Pumping into positive pressure and drawing a vacuum behave very differently. The final firmware uses proportional gains almost three orders of magnitude apart for the two directions, plus mode switching, gain scheduling and a pulse scheme for the motor's dead zone (control write-up).

System overview

mermaid
flowchart LR
    subgraph PCB["Controller PCB"]
        FUSE["5×20 mm fuse"] --> R24(("24 V"))
        R24 --- CB["470 µF bulk cap"]
        R24 --> REG["OKI-78SR<br/>5 V switching regulator"]
        REG --> R5(("5 V"))
        R5 --> MCU["Arduino Nano"]
        R5 --> SEN["MPXV5100DP<br/>pressure sensor"]
        SEN -->|"VOUT → A0"| MCU
        OLED["128×64 OLED"] <-->|"I²C"| MCU
        ENC["Rotary encoder"] --> MCU
    end

    PSU["24 V DC supply"] --> FUSE
    R24 --> DRV["L298N H-bridge<br/>(off-board)"]
    MCU -->|"ENA (PWM), IN1, IN2"| DRV
    DRV --> PUMP["Peristaltic pump"]
    PUMP -->|"pressure line"| SEN
    MCU <-->|"USB serial, 115200 baud"| PC["Python GUI"]

Hardware

StageDateWhat happened
Perfboard prototypeAutumn 2025Nano, sensor and L298N module hand-wired. Used to develop the firmware and the PC GUI
First schematicJan 2026Started with an L298 H-bridge IC on the board, then moved the driver off-board to keep motor switching currents away from the sensor
Power stageJan 2026Added a 24 V barrel jack, Murata CRE1S2405SC isolated converter, fuse holder, bulk capacitor, and sockets for the encoder and display. First routing pass
V1 PCBFeb 2026Replaced the regulator with an OKI-78SR. Board milled and assembled in the department, then checked and validated
V2 PCBMar 2026Minor copper and silkscreen revisions after V1, then ordered from PCBWay
Pump mountJun 2026SolidWorks mount for the complete pump assembly

Layout Considerations

  • Power along one edge. The barrel jack, fuse holder, bulk capacitor, regulator and 24 V motor header sit together on the left-hand side of the board. The high-current path never crosses the sensing area.
  • Motor driver off the board. The L298N module's switching currents and the brushed motor's noise stay on the module. My board only sends it three logic lines (ENA, IN1, IN2) plus 5 V and ground.
  • Short analogue path. The sensor sits right next to the Nano's analogue pins, and its output trace to A0 is about 16 mm long.
  • Solid ground plane. All tracks are on the top layer, with one via on the whole board. The bottom layer is a continuous ground pour.
  • Track widths sized to current. 1.0 mm on the 24 V input, 0.8 mm on the 5 V rail, 0.2–0.5 mm for signals.
  • Bulk decoupling at the motor supply. A 470 µF / 35 V electrolytic at the motor-driver supply terminals delivers the pump's PWM current peaks locally, so they don't pull down the rail the regulator runs from.
  • Input protection. A 5×20 mm cartridge fuse in a Littelfuse PCB fuse holder sits on the 24 V input.

My regulator mistake

My first choice for the 24 V → 5 V stage was a Murata CRE1S2405SC, a 1 W isolated DC-DC module. This type of converter only holds its output near 5 V when it carries a significant load. The controller (Nano, sensor, OLED and the L298N logic) draws very little, so the 5 V rail wasn't regulated properly. I also didn't need isolation, since everything shares one ground.

I replaced it with a Murata OKI-78SR, a non-isolated switching regulator. It has no minimum-load requirement and accepts 7–36 V in. The lesson was to check load regulation and minimum-load figures on the datasheet, not just the input and output voltages.

Bill of materials

The full BoM is in hardware/bom.csv. It's split into parts soldered to the board and off-board modules and assemblies:

RefPartRole
A1Arduino Nano (ATmega328P)Controller
IC1NXP MPXV5100DPDifferential pressure sensor
IC2Murata OKI-78SR-5/1.5-W36-C24 V → 5 V switching regulator
U2 + F1Littelfuse 0PTF0078P + 5×20 mm fuseInput protection
C1470 µF 35 V electrolyticBulk decoupling, 24 V rail
J1–J5Headers, sockets, DC jackEncoder, display, power input, motor-driver control and power
–L298N module, SSD1306 OLED, HW-040 encoder, Williamson 200 Series pumpOff-board

Wiring and connector pinouts: docs/wiring.md.

Firmware

firmware/meniscus_controller/ is an Arduino sketch for the Nano. The full explanation is in docs/control-system.md. In short:

  1. Measurement. Five ADC samples are median-filtered and converted ratiometrically using the sensor's transfer function. A two-point calibration and a first-order low-pass filter are then applied.
  2. Control. A PID with derivative-on-measurement, conditional-integration anti-windup and error-based gain scheduling. It switches with hysteresis between an APPROACH mode and a softer HOLD mode near the setpoint.
  3. Asymmetric plant. Vacuum and positive-pressure setpoints use separate gain sets, and the vacuum side adds a feedforward term.
  4. Motor dead zone. The pump won't turn reliably below ~65 % PWM duty. For smaller corrections the firmware fires fixed 80 ms pulses at the minimum reliable duty, scheduled by a sigma-delta accumulator so the average effort matches what the controller asks for.
  5. Protection. A trip on implausible readings, clamps on the positive pressure limit, PWM slew-rate limiting, and a dead time when the H-bridge changes direction.
  6. User interface. Setpoint and units (mbar, kPa, Pa, bar, psi, mmHg) are set on the OLED with the encoder. There are Fill, Purge and Revert actions, and the setpoint is saved to EEPROM with wear levelling.

Serial commands (115200 baud, newline-terminated):

CommandExampleEffect
SET <value> [unit]SET -25 mbar, SET -0.36 psiNew setpoint (unit defaults to the display unit)
GETGETReport the current setpoint
HELPHELPList commands

Telemetry. One CSV line at 20 Hz: setpoint_mbar,measured_mbar,pwm,pulse,vcc_mV,adc, for example -25,-24.812,0,1,5012,231. Everything else the controller sends is plain text, so the GUI can tell status messages and telemetry apart.

PC interface

gui/ is a small Python app built with Eel that serves a browser UI. You can connect to the controller, send setpoints, watch a live pressure plot (Chart.js), read the device's terminal output and log telemetry to timestamped CSV files.

Mechanical

hardware/mechanical/solidworks/ holds the SolidWorks assembly (PumpAssem.SLDASM). It mounts the pump, pump head and controller board together for use in the lab. The pump-head models come from the manufacturer.

What I'd change in the next revision

  • Better pressure resolution. The MPXV5100DP spans 0–100 kPa (1000 mbar), but the system works over about 50 mbar. At 45 mV/kPa on a 10-bit, 5 V ADC, one ADC count is about 1.1 mbar, so the filtering does a lot of the work. A ±1 psi (±69 mbar) sensor, ideally a digital one, would give several times finer resolution.
  • Local decoupling. Add capacitors at the sensor and the regulator, as NXP and Murata recommend, alongside the bulk capacitor.
  • Reverse-polarity protection on the 24 V input.
  • Bring the motor driver onto the board. Use a modern MOSFET H-bridge instead of the L298 (much lower losses), with a split between power ground and analogue ground.

Repository layout

text
meniscus-controller/
├── firmware/meniscus_controller/   Arduino sketch
├── gui/                            Python + browser GUI (Eel)
├── hardware/
│   ├── pcb/                        KiCad 9 project + 3D STEP of the board
│   ├── gerbers/v1-prototype/       Fabrication files, department-milled board
│   ├── gerbers/v2-pcbway/          Fabrication files, PCBWay board
│   ├── mechanical/solidworks/      Pump mount assembly
│   └── bom.csv                     Bill of materials
└── docs/                           Control write-up, wiring, images

Getting started

Firmware. In the Arduino IDE, install the Adafruit SSD1306 and Adafruit GFX libraries. Select Arduino Nano (ATmega328P), open firmware/meniscus_controller/meniscus_controller.ino and upload it.

GUI. You need Python 3.10 or newer.

bash
cd gui
pip install -r requirements.txt
# set PumpPort in config.txt, e.g. 'COM9' on Windows or '/dev/tty.usbserial-XXXX' on macOS
python meniscus_gui.py        # opens http://localhost:8000

Hardware files. Open hardware/pcb/meniscus-controller.kicad_pro in KiCad 9. The schematic and board carry their own copies of every symbol and footprint, so they open without extra libraries. Third-party library files aren't included here.

Context

Built for use in the lab at the University of Oxford, as part of my work on a piezo-driven inkjet printing system in the Oxford Fluid Dynamics Laboratory.

Edward Smith · MEng Engineering Science, University of Oxford · LinkedIn

Parts

10
QtyValueFootprintReferences
10PTF0078PFuse1:0PTF0078PU2
124v MotorConnector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_VerticalJ5
1Arduino_Nano_EveryModule:Arduino_NanoA1
1C_PolarizedCapacitor_THT:CP_Radial_Tantal_D10.5mm_P5.00mmC1
1DisplayConnector_PinSocket_2.54mm:PinSocket_1x04_P2.54mm_HorizontalJ2
1Encoder SocketLibrary:EncoderConnector-ThroughmountJ1
1Jack-DCConnector_BarrelJack:BarrelJack_Kycon_KLDX-0202-xC_HorizontalJ3
1MPXV5100DPPressure Sensor:MPXV5100DPIC1
1MotorConnector_PinHeader_2.54mm:PinHeader_1x05_P2.54mm_VerticalJ4
1OKI-78SR-3.3_1.5-W36-CDC/DC:OKI78SR3315W36CIC2

Questions

How big is the Meniscus controller PCB?

It measures 76 × 91 mm and has 2 copper layers, 1.6 mm thick.

What parts does the Meniscus controller use?

10 parts, built around the MPXV5100DP and 0PTF0078P. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.

Where can I download the Meniscus controller KiCad files?

From the EdSmithe/meniscus-controller repository on GitHub, at the commit this page was made from: the board is hardware/pcb/meniscus-controller.kicad_pcb and its schematic hardware/pcb/meniscus-controller.kicad_sch. PCBKit opens the board in the browser, with nothing to install.

Can I use the Meniscus controller design in my own project?

Its repository states no licence, so its author keeps all rights: ask EdSmithe before reusing the design.

Similar boards

  • 2 layers160 × 238.5 mmEdurob, the printed circuit board in 3D

    Edurob

    IDiAL-IMSL/Edurob-PCB

    PCB-Files for Edurob

    139 parts1BSD-3-Clause
  • 2 layers89 × 160.1 mmBachelors PCB, the printed circuit board in 3D

    Bachelors PCB

    davidstrasak/Bachelors_PCB

    43 parts
  • 4 layers79.9 × 80 mmVidyut, the printed circuit board in 3D

    Vidyut

    D-Coder-42/Vajra-Team-Indra

    All codes and files for Team B of thrustMIT subscale

    94 parts1MIT
  • 4 layers75 × 38.6 mmHuffAndPuff, the printed circuit board in 3D

    HuffAndPuff

    dmanla/AccessibilityHeadsetDevkit

    A development kit for the creation of an accessibility driven platform.

    149 parts
  • 4 layers67.7 × 97.2 mmTele board, the printed circuit board in 3D

    Tele board

    ParasGore/Rocket-Team---NDTU

    140 parts
  • 2 layers57 × 55.2 mmLogicAnalyzer, the printed circuit board in 3D

    LogicAnalyzer

    gusmanb/logicanalyzer

    24 channel, 100Msps logic analyzer hardware and software

    10 parts5.1kGPL-3.0