You: an 80s-style cyberpunk hacker who's got to spread your video message to the people. Jam this tiny, battery powered video nub shank into a state-of-the-art composite video input of a "television" and run!

Use the QT Py ESP32 Pico to build a stylish vaporwave clock, NTSC SPMTE color bar test pattern generator, or other lo-fi/hi-style video applications.

Thanks to the incredible coding efforts of rossumur, bitluni and marciot, it's possible to generate an analog composite video signal with the original ESP32 chip's 8-bit DACs.

Due to 'creative' uses of the DAC on the ESP32 original chip, this code doesn't work on ESP32-S2, C2, S3 or newer chips. Only the ESP32 as found on the QT Py ESP32 Pico, Feather ESP32 v2, and others build on the original architecture.

Parts

Angled shot of purple square-shaped microcontroller.
This dev board is like when you're watching a super-hero movie and the protagonist shows up in a totally amazing costume in the third act and you're like 'OMG! That's...
$14.95
In Stock
Video of a person with white painted nails unplugging a USB cable from a small, black, square-shaped lipo battery breakout board soldered to a similarly shaped microcontroller, which is also connected to a monochrome OLED display breakout. The OLED breakout displays battery and power data.
Is your QT Py all alone, lacking a friend to travel the wide world with? When you were a kid you may have learned...
$4.95
In Stock
USB Type A to Type C cable - 6" long
As technology changes and adapts, so does Adafruit. This  USB Type A to Type C cable will help you with the transition to USB C, even if you're still...
$2.95
In Stock
RCA Male Plug Terminal Block adapter
One truth about working with A/V is you always need the cable or adapter you don't have in your toolbox. That's why we love these terminal-block RCA connectors so...
$1.50
In Stock

or, alternatively, use a jack and cable instead of a plug:

RCA Female Jack Terminal Block adapter
One truth about working with A/V is you always need the cable or adapter you don't have in your toolbox. That's why we love these terminal-block RCA connectors so...
$2.50
In Stock
Angled shot of a RCA (Composite Video, Audio) Cable 6 feet.
This cable comes with two nice gold-plated RCA connectors. Perfect for use with component/composite video and audio, such as the YBox...
Out of Stock
Slim Lithium Ion Polymer Battery 3.7v 400mAh with JST 2-PH connector and short cable
Lithium-ion polymer (also known as 'lipo' or 'lipoly') batteries are thin, light, and powerful. The output ranges from 4.2V when completely charged to 3.7V. This...
$6.95
In Stock
5 pieces of Extra-long break-away 0.1 inch 16-pin strip male header
Breakaway header is like the duct tape of electronics, and this header is one better with extra long pins on both sides. This makes it great for connecting things together that...
$3.00
In Stock
Five pack of 20-pin 0.1 Female Header - Yellow plastic
Female header is like the duct tape of electronics. It's great for connecting things together, soldering to perf-boards, sockets for wires or break-away header, etc. We go through...
$2.50
In Stock
Break-away 0.1 inch 36-pin strip male header - yellow plastic
In this world nothing can be said to be certain, except we need headers, headers, and more headers!Each pack contains ten yellow 36-pin 0.1"...
$4.95
In Stock

You'll need a TV or monitor with a composite video input -- this is nearly always a yellow RCA jack.

Some modern displays have compositve video inputs, but this looks particularly awesome on an old CRT TV!

The most basic setup is to simply wire the QT Py GND to the negative (sleeve) conductor of the video plug, and the QT Py A0 pin to positive (tip), as shown below.

To get a bit fancier, you can create a small sandwich of QT Py, video plug, and BFF charger board, with a side of LiPo battery. This way, you'll be able to run from battery, or plug into USB C for power and charging.

You can choose to use the plug or jack version of the video terminal block breakout. These can also be swapped later.

Or, get a socket-socket adapter to allow the nub to be used in cable mode.

Assemble Headers, Wires

Solder the extra long header pins to the QT Py as shown -- Note, the addition of a second set of spacers is highly optional, it only serves to look cool.

Solder the header sockets to the underside of the BFF.

Solder a short black wire from the QT Py GND to the composite video plug's negative (-) terminal and a yellow wire from the QT Py A0 to the video plug's positive (+) terminal as shown.

Then, sandwich the plug between the boards as shown. Delicious.

Add Battery

Plug the battery into the BFF.

Use a small loop of Kapton tape or a small piece of double-sided foam tape to secure the battery to the header.

Use It

Inject Video

You can plug right into the composite video jack of a TV or monitor, flip the BFF power switch to on, and set the input to "composite" or "video".

USB Powered

If your monitor has a USB port you may be able to power (and charge) the video nub shank using a short USB C to USB A cable.

Adapt

Here's how to adapt from shank mode to cable mode.

One excellent demo for video on ESP32 is the esp32-dali-clock by marciot. This is an Arduino sketch and supporting libraries, so make sure you have the Arduino IDE installed and can successfully run the Blink sketch before proceeding. This guide shows how, but is for the newer ESP32-S2 -- we'll update this once the QT Py ESP32 Pico guide is ready.

Download

To run this demo, first download the .zip file and uncompress it.

Move the esp32-dali-clock folder into your Arduino sketches directory.

Settings, Compile, Upload

In order to compile and upload to the board, first select Tools > Board > ESP32 Arduino > Adafruit QT Py ESP32.

Then, match the other settings shown here. Very important: Click Tools > PSRAM > Disabled. If you don't disable the PSRAM the video will suffer vertical sync issues and jiggle up and down!

When you're ready, click Sketch > Upload (or use the upload button) to compile and upload the code to the board.

The board will reset after upload and begin immediately running the code. Plug the nub into a composite video input on your TV or monitor and you'll see the awesome vaporwave clock.

Set the Clock

The ESP32's built in WiFi makes it possible to host a small web server in order to configure the clock.

On your computer or mobile device, choose the "ESP32 Dali Clock" network. You'll be taken to a setup page where you can configure the graphics style, date/time, and local network settings.

You can even set the color theme to a specific choice or allow it to change based on the time of day.

Another nifty use for the Video Nub Shank is to create a test pattern generator for a composite TV or display.

Ladyada tests out little displays we're building, and wanted a way to easily check the geometry, rotation, orientation, and colors.

I created a SPMTE NTSC color bar test pattern in code using the Adafruit GFX library running the ESP_8_BIT_composite library by Roger Cheng. While the colors are not totally accurate to SMPTE standards due to the RGB332 bit depth used, it's close enough to do the job.

Also, it looks really rad on an old CRT.

The library used here defaults to output video over DAC_CHANNEL_1 which is pin A1 on the QT Py ESP32. Should you need to switch to pin A0, edit the ESP_8_BIT_composite.cpp file in the library folder, line 116 to DAC_CHANNEL_2

Install Library and Code

To run the Test Pattern Generator, you'll need to download the ESP_8_BIT_composite library, uncompress the .zip, and then move the folder to your Arduino libraries directory.

Rename the folder from ESP_8_BIT_composite-master to ESP_8_BIT_composite.

In the Arduino IDE, open the File > Examples > ESP_8_BIT Color Composite Video Library > SMPTE_NTSC_Color_Bars.ino file.

Settings, Compile, Upload

In order to compile and upload to the board, first select Tools > Board > ESP32 Arduino > Adafruit QT Py ESP32.

Then, match the other settings shown here. Very important: Click Tools > PSRAM > Disabled. If you don't disable the PSRAM the video will suffer vertical sync issues and jiggle up and down!

When you're ready, click Sketch > Upload (or use the upload button) to compile and upload the code to the board.

Code

/*
 Adafruit NTSC SMPTE color bars
 copyright: 2022 John Park for Adafruit Industries
 License: MIT
 resolution is 256x224, colors are approximate at best
uses composite video generator library on ESP32 by Roger Cheng
Connect GPIO25 (A0 on QT Py ESP32 Pico) to signal line, usually the center of composite video plug.
Please disable PSRAM option before uploading to the board, otherwise there may be vertical jitter.
*/

#include <ESP_8_BIT_GFX.h>


// Create an instance of the graphics library
ESP_8_BIT_GFX videoOut(true /* = NTSC */, 8 /* = RGB332 color */);

uint8_t WHITE = 0xFF ;
uint8_t DIM_WHITE = 0xB6 ;
uint8_t YELLOW =  0xF4 ;
uint8_t TEAL = 0x1C ;
uint8_t GREEN = 0x70 ;
uint8_t MAGENTA =  0x83 ;
uint8_t RED = 0x82 ;
uint8_t BLUE =  0x0B ;
uint8_t DARK_BLUE = 0x06 ;
uint8_t PURPLE = 0x23 ;
uint8_t BLACK =  0x00 ;
uint8_t GRAY =  0x24 ;
uint8_t LIGHT_GRAY = 0x29 ;

uint8_t height_tall = 149 ;
uint8_t height_squat = 19 ;
uint8_t height_med = 56 ;

uint8_t width_med = 36 ;
uint8_t width_large = 46;
uint8_t width_skinny = 12 ;

uint8_t row2_y = height_tall ;
uint8_t row3_y = height_tall + height_squat ;


void setup() {
  // Initial setup of graphics library
  videoOut.begin();
}

void loop() {
    // Wait for the next frame to minimize chance of visible tearing
    videoOut.waitForFrame();

    // Clear screen
    videoOut.fillScreen(0);

    // Draw  rectangles
    //row 1
    videoOut.fillRect(0, 0, width_med, height_tall, DIM_WHITE);
    videoOut.fillRect(width_med, 0, width_med, height_tall, YELLOW);
    videoOut.fillRect(width_med*2, 0, width_med, height_tall, TEAL);
    videoOut.fillRect(width_med*3, 0, width_med, height_tall, GREEN);
    videoOut.fillRect(width_med*4, 0, width_med, height_tall, MAGENTA);
    videoOut.fillRect(width_med*5, 0, width_med, height_tall, RED);
    videoOut.fillRect(width_med*6, 0, width_med, height_tall, BLUE);
    //row 2
    videoOut.fillRect(0, row2_y, width_med, height_squat, BLUE);
    videoOut.fillRect(width_med, row2_y, width_med, height_squat, GRAY);
    videoOut.fillRect(width_med*2, row2_y, width_med, height_squat, MAGENTA);
    videoOut.fillRect(width_med*3, row2_y, width_med, height_squat, GRAY);
    videoOut.fillRect(width_med*4, row2_y, width_med, height_squat, TEAL);
    videoOut.fillRect(width_med*5, row2_y, width_med, height_squat, GRAY);
    videoOut.fillRect(width_med*6, row2_y , width_med, height_squat, DIM_WHITE);
    //row 3
    videoOut.fillRect(0, row3_y, width_large, height_med, DARK_BLUE);
    videoOut.fillRect(width_large, row3_y, width_large, height_med, WHITE);
    videoOut.fillRect(width_large*2, row3_y, width_large, height_med, PURPLE);
    videoOut.fillRect(width_large*3, row3_y, width_large, height_med, GRAY);
    videoOut.fillRect(width_large*4, row3_y, width_skinny, height_med, BLACK);
    videoOut.fillRect(width_large*4+width_skinny, row3_y, width_skinny, height_med, GRAY);
    videoOut.fillRect(((width_large*4)+(width_skinny*2)), row3_y , width_skinny, height_med, LIGHT_GRAY);
    videoOut.fillRect(width_med*6, row3_y , width_med, height_med, GRAY);


    // Draw text
     videoOut.setCursor(144, 180);
     videoOut.setTextColor(0xFF);
     videoOut.print("Adafruit NTSC");
     videoOut.setCursor(144, 190);
     videoOut.setTextColor(0xFF);
     videoOut.print("composite video");

}

How it Works

The ESP_8_BIT_composite library makes it easy to use the Adafruit GFX library commands to display images on screen.

videoOut Creation

First, you'll import the library and create and instance of ESP_8_BIT_GFX called videoOut. The true argument chooses NTSC (vs. PAL), and the 8 chooses 8-bit RGB332 color.

#include <ESP_8_BIT_GFX.h>

// Create an instance of the graphics library
ESP_8_BIT_GFX videoOut(true /* = NTSC */, 8 /* = RGB332 color */);

Colors and Sizes

Next, a number of colors are defined to approximate the NTSC color bars. This was accomplished with the incredibly cool 3D color picker Roger Cheng created here, which outputs hex values for RGB332 color space.

Colors and Sizes

Next, a number of colors are defined to approximate the NTSC color bars. This was accomplished with the incredibly cool 3D color picker Roger Cheng created here, which outputs hex values for RGB332 color space.

The pixel dimensions are defined for the different sizes of bars.

uint8_t WHITE = 0xFF ;
uint8_t DIM_WHITE = 0xB6 ;
uint8_t YELLOW =  0xF4 ;
uint8_t TEAL = 0x1C ;
uint8_t GREEN = 0x70 ;
uint8_t MAGENTA =  0x83 ;
uint8_t RED = 0x82 ;
uint8_t BLUE =  0x0B ;
uint8_t DARK_BLUE = 0x06 ;
uint8_t PURPLE = 0x23 ;
uint8_t BLACK =  0x00 ;
uint8_t GRAY =  0x24 ;
uint8_t LIGHT_GRAY = 0x29 ;

uint8_t height_tall = 149 ;
uint8_t height_squat = 19 ;
uint8_t height_med = 56 ;

uint8_t width_med = 36 ;
uint8_t width_large = 46;
uint8_t width_skinny = 12 ;

uint8_t row2_y = height_tall ;
uint8_t row3_y = height_tall + height_squat ;

Begin Video

In the setup() loop, the videoOut is started.

void setup() {
  // Initial setup of graphics library
  videoOut.begin();
}

Main Loop

In the main loop, the library waits for a frame, then clears the screen, and draws the bars, based on some procedural rules.

// Wait for the next frame to minimize chance of visible tearing
    videoOut.waitForFrame();

    // Clear screen
    videoOut.fillScreen(0);

    // Draw  rectangles
    //row 1
    videoOut.fillRect(0, 0, width_med, height_tall, DIM_WHITE);
    videoOut.fillRect(width_med, 0, width_med, height_tall, YELLOW);
    videoOut.fillRect(width_med*2, 0, width_med, height_tall, TEAL);
    videoOut.fillRect(width_med*3, 0, width_med, height_tall, GREEN);
    videoOut.fillRect(width_med*4, 0, width_med, height_tall, MAGENTA);
    videoOut.fillRect(width_med*5, 0, width_med, height_tall, RED);
    videoOut.fillRect(width_med*6, 0, width_med, height_tall, BLUE);
    //row 2
    videoOut.fillRect(0, row2_y, width_med, height_squat, BLUE);
    videoOut.fillRect(width_med, row2_y, width_med, height_squat, GRAY);
    videoOut.fillRect(width_med*2, row2_y, width_med, height_squat, MAGENTA);
    videoOut.fillRect(width_med*3, row2_y, width_med, height_squat, GRAY);
    videoOut.fillRect(width_med*4, row2_y, width_med, height_squat, TEAL);
    videoOut.fillRect(width_med*5, row2_y, width_med, height_squat, GRAY);
    videoOut.fillRect(width_med*6, row2_y , width_med, height_squat, DIM_WHITE);
    //row 3
    videoOut.fillRect(0, row3_y, width_large, height_med, DARK_BLUE);
    videoOut.fillRect(width_large, row3_y, width_large, height_med, WHITE);
    videoOut.fillRect(width_large*2, row3_y, width_large, height_med, PURPLE);
    videoOut.fillRect(width_large*3, row3_y, width_large, height_med, GRAY);
    videoOut.fillRect(width_large*4, row3_y, width_skinny, height_med, BLACK);
    videoOut.fillRect(width_large*4+width_skinny, row3_y, width_skinny, height_med, GRAY);
    videoOut.fillRect(((width_large*4)+(width_skinny*2)), row3_y , width_skinny, height_med, LIGHT_GRAY);
    videoOut.fillRect(width_med*6, row3_y , width_med, height_med, GRAY);

Text

In order to make it simple to verify that a display is showing the video in the proper orientation/rotation a small bit of text is printed to the screen. You can adjust this text to suit your needs.

// Draw text
     videoOut.setCursor(144, 180);
     videoOut.setTextColor(0xFF);
     videoOut.print("Adafruit NTSC");
     videoOut.setCursor(144, 190);
     videoOut.setTextColor(0xFF);
     videoOut.print("composite video");

This guide was first published on May 25, 2022. It was last updated on 2022-06-14 12:04:18 -0400.