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.

This page (NTSC Test Pattern Generator) was last updated on Sep 03, 2022.

Text editor powered by tinymce.