Project tutorial
Vacuum Fluorescent Display Controller

Vacuum Fluorescent Display Controller © GPL3+

A simple interface circuit to drive VFD displays. No fancy parts required, and runs on serial communication.

  • 24 respects

Components and supplies

Ph a000066 iso (1) ztbmubhmho
Arduino UNO
...or whatever 8 Bits Arduino
VFD Display
our buddy
DIP integrated circuit
DIP integrated circuit
...or equivalent transistor (2N3906, 2SA733, ecc) or higher voltage PNP ones
...or equivalent transistor (2N3904, 2SC945, ecc)
ubiquitous optocoupler found in most switching PSUs, bottom line part
Fairchild semiconductor 1n4004. image
1N4007 – High Voltage, High Current Rated Diode
Electrolitic Capacitors
see schematic for values and voltages
470nF Capacitor
whatever type is good
aka: paper board, protoboard, or in case your preferred assembly solution
1/4W Resistors
see schematic for values
Dupont Jumpers
...or your preferred connections and posts
thin wires for the circuit backside and also to hookup the display, don't use connectors there because pin spacing is variable and a fast soldering is just easier

Necessary tools and machines

09507 01
Soldering iron (generic)
Solder Wire
buy it at a local store, no cheap stuff for this, 0.5mm is preferred
Wire Clipper

About this project

What's the Use?

VFD displays are not trending anymore for some obvious reasons, they are made of glass ("uh cool!"), have an incandescent filament ("really??"), they are basically vacuum tubes ("interesting!"), they make a light so brilliant, sometimes colored, it needs a dark plastic on top ("uh, cool again!").

Usually you need a dedicated chip to drive em, most modules/appliances come with this chip that you can control independently via I2C or SPI (that is way better in terms of ease), in case you buy or have one of those displays and lack such chip it's not so easy to "connect and go," the ideal would be buying that chip (that comes in SMD format usually). This circuit gives a pretty decent interface for like an Arduino and can drive quite likely any VFD, including the bulky "tube" vintage ones, but matrix displays with a lot of segments are not a good idea by the way, you can expand the circuit but THOUSANDS... mhhh, maybe not.

Pros and Cons


  • almost zero cost (hopefully)
  • easy to find parts
  • high voltage capability (up to hundreds Volts on the anodes)
  • independent grids and anodes voltages
  • only 3 digital data lines needed
  • expandable (it requires more CPU work in case)
  • code is ready made for every 8-bit Arduino


  • requires a bunch of different voltages and supplies (not weird for VFDs)
  • no dimming function (except from voltage drive)
  • uses two Arduino resources, the SPI and timer1
  • you can't hang the microcontroller, otherwise the scan freezes on a grid
  • it's not a pair of resistors and Leds, it requires a bit of work
SEE IT IN.... "action" !!

Principle of Operation

A VFD usually works in a multiplexed fashion, like a 2D matrix with X and Y axis, you need to heat up the filament (the cathode, you see those thin wires in front of it), connect the ground on that filament as well, a positive voltage to a grid (control grid, one point of the X axis) and again a positive voltage on a segment pin (the anode, one point of the Y axis), at this point a segment (just one) lights up. To light up whatever segment and whatever combination, and having "few" wires around, the multiplexing selects one grid per time and at the same time configures the anodes to light up the corresponding segments under that grid, a moment after, it selects another grid and configures the anodes this time for having the corresponding segments correctly lighted up under that second grid. Speeding up this continuous repeating scan results in a movement so fast that under our eyes it doesn't select one grid per time, but looks driven at once, this is the so called POV (Persistence Of Vision).

This circuit uses two types of integrated circuits of the same family, two CD4094 and two CD4017, the 4094 drive the anodes and the 4017 drive the grids instead, the 4094 can store an high\low configuration on their outputs and are good for the anodes side, the 4017 are the classic chips with 10 sequencing outputs, perfect for the grids. Once the 4094 are loaded with the moment anodes configuration, the "ok" signal (strobe) applies this setup and at the same time switches the 4017 by one step, allowing for an automatic sequencing.

The power part is basically only some BC557 transistors (or equivalent) that allow for a more wide voltage swing on the anodes, because these displays require an higher voltage than the 5V of the Arduino. The grids are driven by the 4017 directly, a PC817 optocoupler allows for a grater voltage than 5V around the 4017 and also a different voltage level from the CD4094, this simplifies A LOT the whole.

The Arduino must provide all the orchestration, it means that must store all the segments configuration and load the circuit with the anode setup at every grid switch, this means it really requires a clever code to do it beyond the user action. The code I made sets up a timer-based interrupt that re-loads the 4094 chips at every step, actually around 1000 times per second, so for 10 grids it gives a refresh rate of 100Hz, that is good. There's an array of data that stores the segments configuration and can be modified in-code, without particular procedures or subsequent actions, the interrupt routine will upload the data by it's own.

Power Supplies

The grids voltage on the 4017 chips can be from 5Vdc to 18Vdc and at least 50mA of current, it doesn't need to be regulated. Usually (as far as I saw) 12V will suffice for every situation, increasing it doesn't appear to increase brightness much (if it's a vacuum tube, the grid doesn't require huge voltages).

The anodes voltage can be from literally 0V up to whatever your transistors can withstand (50Vdc for the BC557), usually if the display is perfect a 20-30Vdc will do the job perfectly, not regulated is good. For a normal setup 50mA of supply is more than enough.

The digital power can be 5Vdc or also 3.3Vdc in case of those types of Arduinos or MCUs (not tried yet), this requires like 100mA of power (hopefully less), in case the CD4094 become sluggish you can reduce the SPI clock in code and\or use the "Q'S" output from the first 4094 for a more consistent communication.

The filament supply must provide at least 5V 200-300mA of current, if you already have a DC source you can avoid the bridge rectifier and 1000uF capacitor, but you CANNOT use the same digital supply for\from the Arduino. The actual filament in case of a not so big appliance display runs on like 3V and can draw 150mA.

Tips and Notes

  • before hooking up the wires, if you don't know what pin does what, it's better to try the display first, usually a pair of AA batteries in series will go ok for the filament (usually it's the side contacts), while a pair of 9V batts in series will provide the positive polarization for both grids and anodes; grids and anodes are often grouped, a good idea is to find the actual "anode zero" because it's quite likely the first segment on the numeric or alphanumeric part, often the same and with the same order for all the grids, putting the wires in reverse order by accident makes the software more clumsy at the end, grids instead, apparently, follow the pin order
  • go easy on the filament supply, increase current little by little, stay in darkness and the moment you start seeing the filaments becoming slightly incandescent just reduce a bit and you are done, a power potentiometer is perfect, you find these sometimes in old TVs but it's difficult nowadays
  • increase voltages on grids and anodes in a sane way, the phosphors can burn irreparably, the circuit also works scanning, so if the system hangs you'll provide too much power on a single grid section continuously
  • play with a compromise on the grids\anodes voltages, mind that the grids could consume some current, not worth pushing on that, better mind the anodes
  • using a single transformer for your entire project\setup is ideal, also some mixed solutions can be used, but switching PSUs and especially grounded ones can play you a bad prank, that's why for experimenting it's ALWAYS a good idea to use classic transformer supplies
  • you can expand this driver adding some CD4094 and\or CD4017, of course if you need 8 anodes or less you can rid of the second 4094, same scenario for the 4017, but if you want the driver ready at hand just build it as much as complete possible
  • there are no resistors on the bases of the anodes transistors as you can see, this gives superfast drive and narrows the parts list but makes the CD4094 consume some power, they will not heat up almost at all but some specimens of chips may have a too strong output, check current consumption in case, the chips will not blow, because the power dissipated is INSIDE the maximum allowable, current can be in the 4mA range per pin at 5V supply
  • old tired displays can be renewed by some current shocks on the filaments, if it's the entire display that is dim and not just a burnt segment, they say that you bring the wires visibly incandescent (yellow) like five times in a row for a pair of seconds each time, never tried it but it may require twice the rated voltage of the filament, this cleans em but apparently it's more useful for very old\antique ones... and it's risky, those are not light bulbs, you could snap the wires


Arduino code with working routineArduino
    Versatile VFD Display Hrdware Interface Arduino Program
    Copyright (C) 2019  Genny A. Carogna

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <>.

/* this code uses resources from the Arduino in a "behind the curtains" way, the timer1 and SPI modules are both used and busy
 * , so you may have big problems using SPI, SPI pins, and some functionalities based upon timer1... the circuit proposed
 * doesn't have an onboard memory, so the segment setup must be "scanned" continuosly, every grid, 100 times per second
 * , from the Arduino segments array, this translates also on the so-called "overhaed" CPU work, that, for you to know, it's
 * no more than regular use of a micro at the end, it may hit around 3% of CPU work in terms of occupied time, you can also try
 * to use the regular SPI tools of Arduino but it needs an indepth analisys of the outtake and procedure
 * this code works with whatever 8 bit Arduino, also 3.3V should work ok, maybe reduce the SCK frequency, considering the CD4094 are
 * powered along it and may "undervolt" (yes, exactly the overclock principles to fry your beloved PC)

#define ledLed 13  // it's the led pin that leds a led on to led you know that a led on means some led related stuff (scientist mumbojumbo of course)

#define strobePin 4  // the Arduino pin to dedicate to the "strobe" line
#define strobeHold 10  // microseconds // holding time to switch the optocoupler... not megahertz-fast but acceptable

// data for my displays (a video cassette recorder, an audiophoolery CD reader and a compact stereo)... dont mind, delete, and just define "gridAmount" and "anodeAmount"
#define JVC

#define mitsubishiGrids 10
#define mitsubishiAnodes 9
#define marantzGrids 9
#define marantzAnodes 14
#define JVCGrids 11
#define JVCAnodes 19

#if defined mitsubishi
#define gridAmount mitsubishiGrids
#define anodeAmount mitsubishiAnodes

#elif defined marantz
#define gridAmount marantzGrids
#define anodeAmount marantzAnodes

#elif defined JVC
#define gridAmount JVCGrids
#define anodeAmount JVCAnodes

// this array, segments[x], stores the segment bits
// segments[0] means grid zero, and the least significative bit is segment zero in that grid
// you can access and modify this array as you like, from wherever, without doing anything else, the effect is immediately visible on
// the display, and stays on as long you don't change the bits, for more than 8 segments per grid displays, you need a
// 16 or 32 bit container per grid, this is managed automatically from the #define(s) above
#if anodeAmount > 16
volatile uint32_t segments[gridAmount] = {0};  // more than 16 segments per grid (32 bit needed)
#elif anodeAmount > 8
volatile uint16_t segments[gridAmount] = {0};  // more than 8 segments per grid (16 bit, twin transfer)
volatile uint8_t segments[gridAmount] = {0};  // 8 or less segments per grid (8 bit)

void setup(){
  // turns SPI pins and some functional ones as output in a fast\direct way
  *portModeRegister(digitalPinToPort(PIN_SPI_SS)) |= digitalPinToBitMask(PIN_SPI_SS);
  *portModeRegister(digitalPinToPort(PIN_SPI_MOSI)) |= digitalPinToBitMask(PIN_SPI_MOSI);
  *portModeRegister(digitalPinToPort(PIN_SPI_SCK)) |= digitalPinToBitMask(PIN_SPI_SCK);
  *portModeRegister(digitalPinToPort(strobePin)) |= digitalPinToBitMask(strobePin);
  *portOutputRegister(digitalPinToPort(strobePin)) &= ~digitalPinToBitMask(strobePin);  // put it low
  *portModeRegister(digitalPinToPort(ledLed)) |= digitalPinToBitMask(ledLed);
  *portOutputRegister(digitalPinToPort(ledLed)) &= ~digitalPinToBitMask(ledLed);  // put it low

  delay(800);  // some delay to wait for the CD4017 reset to complete (you need to run the code AFTER powering up the 4017s, this makes it automatic in case you power all in once)

  cli();  // disables interrupts to let us tweak some stuff without cats and dogs escaping everywhere

  // timer1 configuration, it makes an interrupt happen "grids times 100" per second... this gives a 100Hz total refresh that is ok
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;
  OCR1A = 160000 / gridAmount;
  TCCR1B |= (1 << WGM12);
  TCCR1B |= (1 << CS10);
  TIMSK1 |= (1 << OCIE1A);

  // SPI configuration, we cannot use the Arduino SPI tools bcs they rely on interrupts AND we
  // need the communication happen INSIDE an interrupt..
  // change SPR0 for SPR1 and uncomment the last line to have half SPI clock speed
  SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR0);  // 1MHz stock SCK speed
  // SPSR = (1 << SPI2X);  // uncomment this and mod above for half clock speed

  sei();  // interrupts are a go again

  // it's better to put your addictional setup() code from here

// you can do whatever in loop(), delays, pollings, but disabling interrupts or making the micro halt completely will
// freeze the display on a grid, and if the voltage is at top power you may also burn some segments (!!)
void loop() {
  show();  // kinda screensaver, you can delete

// interrupt service routine, this happens at every grid scan switch and times 100 per second
  ATTENTION: i dunno why i need to start "turn" from 1 for proper functionality (i power the Arduino after and it may flip a grid), in case it keeps missing the correct grid at statup, vary this
  static uint8_t turn = 1;  // indestructible variable to remind at what grid we were the next interrupt
#if anodeAmount > 24
  SPDR = ~uint8_t(segments[turn] >> 24);  // we need negated bits on the CD4094 (cos of the transistors arrangement)
  while (!(SPSR & (1 << SPIF))) ;  // lets wait it completes the transfer
#if anodeAmount > 16
  SPDR = ~uint8_t(segments[turn] >> 16);  // we need negated bits on the CD4094 (cos of the transistors arrangement)
  while (!(SPSR & (1 << SPIF))) ;  // lets wait it completes the transfer
#if anodeAmount > 8
  SPDR = ~uint8_t(segments[turn] >> 8);  // we need negated bits on the CD4094 (cos of the transistors arrangement)
  while (!(SPSR & (1 << SPIF))) ;  // lets wait it completes the transfer
  SPDR = ~uint8_t(segments[turn]);  // we need negated bits on the CD4094 (cos of the transistors arrangement)
  while (!(SPSR & (1 << SPIF))) ;  // lets wait it completes the transfer... yyyYYYAAAAWWWWNNNnnn...........

  // strobing (finally!!) this also switches the grid on the CD4017s
  *portOutputRegister(digitalPinToPort(strobePin)) |= digitalPinToBitMask(strobePin);  // fast pin drive ON
  *portOutputRegister(digitalPinToPort(strobePin)) &= ~digitalPinToBitMask(strobePin);  // fast pin drive OFF

  // grid turn reminder at rotation
  turn ++;
  if (turn == gridAmount) turn = 0;


// this is just a demo, delete as you like
void show() {
  // numbers flip
#ifdef mitsubishi
  for (uint8_t i = 0; i < 9; i ++) for (uint8_t j = 0; j < 11; j ++) {
    mitsubishiPrintNum(j, i);
  for (uint8_t i = 0; i < 9; i ++) for (uint8_t j = 0; j < 11; j ++) {
    mitsubishiPrintNum(j, i);
#elif defined marantz
  for (uint8_t i = 0; i < 6; i ++) for (uint8_t j = 0; j < 11; j ++) {
    marantzPrintNum(j, i);
  for (uint8_t i = 0; i < 6; i ++) for (uint8_t j = 0; j < 11; j ++) {
    marantzPrintNum(j, i);
#elif defined JVC /*
  for (uint8_t i = 0; i < 9; i ++) for (uint8_t j = 0; j < 11; j ++) {
    JVCPrintNum(j, i);
  for (uint8_t i = 0; i < 9; i ++) for (uint8_t j = 0; j < 11; j ++) {
    JVCPrintNum(j, i);


  // segments flip
  for (uint8_t i = 0; i < gridAmount; i ++) for (uint8_t j = 0; j < anodeAmount + 1; j ++) {
    segments[i] = (uint32_t(1) << j);


  // all the display blinkng
  for (uint8_t i = 0; i < 5; i ++) {
    for (uint8_t j = 0; j < gridAmount; j ++) {
      segments[j] = 0xFFFFFFFF;
    for (uint8_t j = 0; j < gridAmount; j ++) {
      segments[j] = 0;


// easy peasy seven segment number visualization
void mitsubishiPrintNum(uint8_t val, uint8_t pos) {
  if (pos > 2) pos ++;
  uint16_t mask = 0xFF80;
  uint8_t numbers[] = {B00111111, B00000110, B01011011, B01001111, B01100110, B01101101, B01111101, B00100111, B01111111, B01101111, B00000000};
  switch (pos) {
    case 0: case 1: case 4: case 5: case 6: case 7: case 8: segments[pos] &= mask; segments[pos] |= numbers[val]; break;
    case 2: if (val == 1) segments[1] |= (1 << 8); else segments[1] &= ~(uint16_t(1) << 8); break;
    case 9: segments[9] &= 0xFFF0; if (val == 1) segments[9] |= 3; else if (val == 2) segments[9] |= B1101;

// easy peasy seven segment number visualization
void marantzPrintNum(uint8_t val, uint8_t pos) {
  if (pos > 5) return;
  pos ++;
  pos ++;
  uint16_t mask = 0xFF80;  // bitmask to delete the digit
  uint8_t numbers[] = {B00111111, B00000110, B01011011, B01001111, B01100110, B01101101, B01111101, B00100111, B01111111, B01101111, B00000000};
  segments[pos] &= mask;  // delete whatever digit
  segments[pos] |= numbers[val];  // set digit

// easy peasy seven segment number visualization
void JVCPrintNum(uint8_t val, uint8_t pos) {
  // to come (you really don't care)



General Schematic
I avoided drawing ALL the BC557 transistors because they are just there connected on all the CD4094 outputs, all the same.

There's a jumper selector (running on the red line) you must configure on the 4017 chips, it resets the two 4017 at the completion of the grids scanning, you must connect (or solder) the flying jumper on the output pin AFTER the last grid, so if your display has 10 grids the jumper goes on the 11th output.

The Arduino connections are outlined in red, you can use the USB power for it but it's strongly advised to use a regular old-style transformer for the remaining supplies if you already have a grounded power supply like your PC. These supplies must be all DC except maybe the filament supply, I added some diodes because it's likely you need a separate transformer with a low voltage secondary. The filament power usually ranges around 3V 150mA, a 5V AC transformer will suffice. The "ballast" thing will possibly be a wirevound power potentiometer of like 100ohms, or a fixed resistor, or also some 1N4007 diodes to reduce the voltage.

The BC557 transistors pull the anodes up to anodic voltage and the 100kohm resistors will let the voltage drive fall when the segment is off, while the CD4094 remain at 5V normally.

The CD4017s will be powered with grid voltage and don't require additional transistors. There's a "ghetto" reset circuit for the 4017 that lasts like one tenth of a second, you have to wait for it to settle before running the code.
The "original" schematic for cascading some CD4017 required other logic gates chips, I used instead an NPN transistor and the optocoupler itself to replicate an AND gate, it's fast enough at closing so it's perfect for the task, at releasing it's instead a bit slower but we don't care, especially because the clock inputs on the 4017 are schmitt triggered and the required speed is not "scary" there.

There are various grounds for the chips and stuff as you can see, take a good look.
Vfd driver zao9xlrps3


Similar projects you might like

GPS Location Display With GPS And TFT Display Shields

Project tutorial by Boian Mitov

  • 36 respects

Multi-Dashboard Display with Arduino Controller

Project showcase by Colin O'Dell

  • 80 respects

Soil Moisture Sensor With LCD Display

Project tutorial by Patel Darshil

  • 56 respects

Crazy Arduino Hose Display

Project showcase by hwhardsoft

  • 71 respects

Arduino MIDI Controller with Encoder + OLED Display + EEPROM

Project in progress by yilmazyurdakul

  • 28 respects

Lightpipe 7-Segment Display

Project tutorial by Brian Lough

  • 31 respects
Add projectSign up / Login