PIC18 Q43 NTSC B/W video signal demo

Thread Starter

nsaspook

Joined Aug 27, 2009
8,382
A little something I've been playing around with to test the limits of DMA on this series of 8-bit controller.

Generating NTSC signals for simple games with a 8-bit controller is nothing new or novel but I wanted a way that was mainly independent from controller cpu processing limits. The transfer of 1 byte of SRAM (a C array) to a 1 byte SFR register (PORTB) is the hard dynamic signal resolution limit on this chip using DMA . That's about 100ns per signal transition with a 64MHz FOSC to the processor controlling a NSTC FSM (scanline state machine).



At this speed we can easily encode, at relatively good fidelity, an NTSC scanline into a memory buffer using a few bits per byte for timing pulses and video B/W pixels to be clocked to a I/O port for simple mixing into a analog video signal.



Yellow: NTSC camera video as a signal standard.
Red: Q43 DMA generated video.
Green: CPU debugging timing pulses.


Static pattern controller generated testing video:

A moving line, changing pattern test.

The remaining problem is dynamic updates to the video memory. All of the memory bandwidth (something in short supply with the simple DMA architectures seen in most 8-bit controllers) is being used by DMA so excessive processing during DMA will affect H/V timing causing tearing and loss of sync. The fix for that is to only process mainline code during the scanlines with no video around the H sync pulse. For that we need a simple task manager that uses the high/low priority interrupt structure and a 'hold' flag.

Setup a Low priority timer ISR as the 'idle' loop; This idle loop can be interrupted by High priority DMA completion and other module interrupt requests so those time critical processes get the cpu they need during the NTSC FSM. The 'idle' loop can also set a task time duration for the main task that will interrupt the main task back to the low priority idle loop during the critical timing periods. This means I can step into the main application code execution when I want it to run it and control how long I want one processing time slice to be.

Application setup and application code loop in the while (true) loop.
C:
#pragma warning disable 520
#pragma warning disable 1498

#include <stdlib.h>
#include <stdbool.h>
#include "mcc_generated_files/mcc.h"
#include "mcc_generated_files/tmr5.h"
#include "mcc_generated_files/tmr4.h"
#include "qconfig.h"
#include "vtouch.h"
#include "vtouch_build.h"
#include "timers.h"
#include "eadog.h"
#include "ntsc.h"

volatile uint16_t tickCount[TMR_COUNT];
char buffer[256];

void led_flash(void);

/*
 *             Main application
 */
void main(void)
{

    // Initialize the device
    SYSTEM_Initialize();
    TMR4_Stop();
    TMR5_SetInterruptHandler(led_flash);

    // Enable high priority global interrupts
    INTERRUPT_GlobalInterruptHighEnable();

    // Enable low priority global interrupts.
    INTERRUPT_GlobalInterruptLowEnable();

    SPI1CON0bits.EN = 1;
    init_display();
    sprintf(buffer, "%s ", build_version);
    eaDogM_WriteStringAtPos(0, 0, buffer);
    sprintf(buffer, "%s ", build_date);
    eaDogM_WriteStringAtPos(1, 0, buffer);
    sprintf(buffer, "%s ", build_time);
    eaDogM_WriteStringAtPos(2, 0, buffer);
    BLED_SetLow();

    StartTimer(TMR_DIS, 500);

    TMR6_Stop(); // disable software timers to stop scan-line jitter
    ntsc_init();

    while (true) {
        // Add your application code
        BLED_Toggle(); // application code blink LED
        task_hold = true; // set the idle ISR hold flag
        while (task_hold) { // wait until idle ISR return flag is cleared to reduce CPU/IO usage during DMA
        };
        scan_line++; // back from idle ISR 
        ntsc_flip = !ntsc_flip;
    }
}

/*
 * This runs in the timer5 ISR
 */
void led_flash(void)
{
    LED2_Toggle(); // ISR code blink LED
}
/**
 End of File
 */

Basic NTSC state machine parameters.
C:
/* 
 * File:   ntsc.h
 * Author: root
 *
 * Created on December 20, 2020, 10:05 AM
 */

#ifndef NTSC_H
#define    NTSC_H

#ifdef    __cplusplus
extern "C" {
#endif
#include <xc.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
#include "mcc_generated_files/dma5.h"
#include "mcc_generated_files/pin_manager.h"
#include "mcc_generated_files/tmr4.h"

#define DMA_M        0x04    // DMA modules number

#define SYNC_LEVEL    0 // clear all PORTB bits
#define BLANK_LEVEL    1 // PORTB bit 0 set
#define BLACK_LEVEL    1 // "
#define VIDEO_LEVEL    2 // PORTB bit 1

#define DMA_B        473    // timing adjustment of H sync pulses for 63.2us
#define V_BUF_SIZ    512    // data buffer array size
#define S_COUNT        247    // scanlines 
#define H_SYNC        3    // number of H sync lines
#define H_COUNT        12    // post H sync scanlines

#define S_END        37    // H scan pulse    
#define B_START        48    // H front-porch
#define V_START        48    // Video start
#define V_DOTS        160    // scanline video dot position
#define V_END        400    // Video end
#define V_H        DMA_B/2

    enum s_mode_t {
        sync0, sync1, sync2, sync3, sync_error
    };

    extern uint8_t vsync[V_BUF_SIZ];
    extern uint8_t hsync[V_BUF_SIZ];
    extern volatile uint8_t vbuffer[V_BUF_SIZ], *vbuf_ptr;
    extern volatile uint32_t vcounts;
    extern volatile uint8_t vfcounts, scan_line;
    extern volatile bool ntsc_vid, ntsc_flip, task_hold;
    extern volatile enum s_mode_t s_mode;

    void ntsc_init(void);

    /*
     * NTSC state machine options
     * 
     * scan_line: Set to zero to display data on all scan lines or [1..S_COUNT] to display only on that scan line
     * nstc_vid: Blank video when scan_line is set to zero for all lines
     * ntsc_flip: use alternative scan_line buffer
     */

#ifdef    __cplusplus
}
#endif

#endif    /* NTSC_H */
NTSC state machine setup, processing and task manager code
C:
#include "ntsc.h"

volatile uint32_t vcounts = 0;
volatile uint8_t vfcounts = 0, scan_line = 0;
volatile bool ntsc_vid = true, ntsc_flip = false, task_hold = true;

volatile enum s_mode_t s_mode;
volatile uint8_t vbuffer[V_BUF_SIZ], *vbuf_ptr;

void vcntd(void);
void vcnts(void);

/*
 * setup the data formats and hardware for the DMA engine
 */
void ntsc_init(void)
{
    uint16_t count = 0;

    /*
     * Interrupt driven task manager
     * after the H sync pulse there are V syncs with no video
     * main-line code runs for the duration of one timer 4 interrupt period
     * ~200us for testing, then goes back to idle
     * until re-triggered the next H sync cycle
     */
    TMR4_Stop();
    TMR4_SetInterruptHandler(vcntd);

    /*
     * DMA hardware registers data setup
     */
    DMA5_StopTransfer();
    vbuf_ptr = vsync;
    SLRCONB = 0xff; // reduce PORTB slewrate
    DMA5_SetDMAPriority(0);
    DMA5_SetDCNTIInterruptHandler(vcnts);
    DMASELECT = DMA_M;
    DMAnCON0bits.EN = 0;
    DMAnSSA = (volatile uint24_t) vbuf_ptr;
    DMAnSSZ = DMA_B;
    DMAnDSZ = DMAnSSZ;
    DMAnCON0bits.EN = 1;

    /*
     * setup the static V, H and video patterns for DMA transfer engine to PORTB
     */
    for (count = 0; count < B_START; count++) {
        vsync[count] = SYNC_LEVEL;
        vbuffer[count] = SYNC_LEVEL;
        hsync[count] = SYNC_LEVEL;
    }

    for (count = S_END; count < B_START; count++) {
        vsync[count] = BLANK_LEVEL;
        vbuffer[count] = BLANK_LEVEL;
        hsync[count] = SYNC_LEVEL;
    }

    for (count = V_START; count < V_END; count++) {
        vsync[count] = BLANK_LEVEL;
        vbuffer[count] = BLANK_LEVEL;
        hsync[count] = SYNC_LEVEL;
        if ((count % 8)) { // add a bit of default texture
            if (count > V_DOTS)
                vsync[count] += VIDEO_LEVEL; // set bit 1 of PORTB
        } else {
            if (!(count % 8)) { // add a bit of default texture
                if (count > V_DOTS)
                    vbuffer[count] += VIDEO_LEVEL; // set bit 1 of PORTB
            }
        }
    }
    for (count = V_END; count < (V_BUF_SIZ - 1); count++) {
        vsync[count] = BLANK_LEVEL;
        vbuffer[count] = BLANK_LEVEL;
        hsync[count] = SYNC_LEVEL;
    }

    for (count = (DMA_B - 5); count < (V_BUF_SIZ - 1); count++) {
        hsync[count] = BLANK_LEVEL;
    }

    for (count = V_H; count < (V_H + 10); count++) {
        hsync[count] = BLANK_LEVEL; // double speed H pulses
    }

    // default scan mode to all lines
    s_mode = sync1;

    /*
     * kickstart the DMA engine
     */
    DMA5_StartTransfer();
    TMR4_StartTimer();
}

/*
 * low priority idle task for timer 4 ISR
 * waits checking task_hold until its false (set to false in DMA state machine)
 * this task is interruptible from all high pri interrupts and control returns after high pri processing
 * when it exits the hold loop, program flow returns to main for application processing
 * until another timer 4 interrupt returns program flow to here.
 */
void vcntd(void) // each timer 4 interrupt
{
    IO_RB4_Toggle();
    TMR4_Stop();
    task_hold = true;
    while (task_hold) {
    };
    IO_RB4_Toggle();
}

/*
 * NTSC DMA state machine
 * ISR triggered by the completed DMA transfer of the data buffer to PORTB
 * Generates the required HV sync for fake-progressive NTSC scanning on most modern TV sets
 * http://people.ece.cornell.edu/land/courses/ece5760/video/gvworks/GV%27s%20works%20%20NTSC%20demystified%20-%20B&W%20Video%20and%20Sync%20-%20Part%201.htm
 */
void vcnts(void) // each scan line interrupt, 262 total for scan lines and V sync
{
    vfcounts++;

    switch (s_mode) {
    case sync0: // H sync and video, one line
        if (vfcounts >= S_COUNT) { // 243
            vfcounts = 0;
            s_mode = sync2;
            DMASELECT = DMA_M;
            DMAnCON0bits.EN = 0;
            DMAnSSA = (volatile uint24_t) & hsync;
            DMAnSSZ = DMA_B;
            DMAnDSZ = DMAnSSZ;
            DMAnCON0bits.EN = 1;
        } else {
            if (vfcounts == scan_line) {
                IO_RB1_SetDigitalOutput();
            } else {
                IO_RB1_SetDigitalInput();
            }
        }
        break;
    case sync1: // H sync and video, all lines
        if (vfcounts >= S_COUNT) {
            vfcounts = 0;
            s_mode = sync2;
            DMASELECT = DMA_M;
            DMAnCON0bits.EN = 0;
            DMAnSSA = (volatile uint24_t) & hsync;
            DMAnSSZ = DMA_B;
            DMAnDSZ = DMAnSSZ;
            DMAnCON0bits.EN = 1;
        } else {
            if (ntsc_vid) {
                IO_RB1_SetDigitalOutput();
            } else {
                IO_RB1_SetDigitalInput();
            }
        }
        break;
    case sync2: // V sync and no video
        if (vfcounts >= H_SYNC) {
            vfcounts = 0;
            s_mode = sync3;
            DMASELECT = DMA_M;
            DMAnCON0bits.EN = 0;
            DMAnSSA = (volatile uint24_t) vbuf_ptr;
            DMAnSSZ = DMA_B;
            DMAnDSZ = DMAnSSZ;
            DMAnCON0bits.EN = 1;
            IO_RB1_SetDigitalInput(); // turn-off video bits
            /*
             * trigger main task processing using the task manager
             */
            task_hold = false; // clear idle routine run flag
            TMR4_StartTimer(); // run in main for timer 4 interrupt period then back to idle
        }
        break;
    case sync3: // H sync and no video
        if (vfcounts >= H_COUNT) {
            vfcounts = 0;
            if ((bool) scan_line) {
                s_mode = sync0;
            } else {
                s_mode = sync1;
            }
            DMASELECT = DMA_M;
            DMAnCON0bits.EN = 0;
            if (ntsc_flip) {
                vbuf_ptr = vbuffer;
            } else {
                vbuf_ptr = vsync;
            }
            DMAnSSA = (volatile uint24_t) vbuf_ptr;
            DMAnSSZ = DMA_B;
            DMAnDSZ = DMAnSSZ;
            DMAnCON0bits.EN = 1;
        }
        break;
    default:
        vfcounts = 0;
        s_mode = sync1;
        DMASELECT = DMA_M;
        DMAnCON0bits.EN = 0;
        ntsc_flip = false;
        vbuf_ptr = vsync;
        DMAnSSA = (volatile uint24_t) vbuf_ptr;
        DMAnSSZ = DMA_B;
        DMAnDSZ = DMAnSSZ;
        DMAnCON0bits.EN = 1;
        IO_RB1_SetDigitalOutput(); // video bits, on
        break;
    }

    /*
     * re-trigger the state machine for a new scanline
     */
    DMA5_StartTransfer();
}
This runs on one of my standard 47Q43 boards on a simple DAC mixer of two 1K pots. MPLABX MCC XC8
 

Thread Starter

nsaspook

Joined Aug 27, 2009
8,382
A little more work on the display code for a general character rom.
NTSC composite input to TV.




Two (upper/lower 4-bits of the display memory) 256 byte display memory banks are used for each line. (a single line is repeated here) I could use one bank per line with a 1 sync bit and 7-bit character encoding but for 8x8 block graphics two banks are needed.

https://raw.githubusercontent.com/nsaspook/vtouch_v2/eadogs/q43_board/q43_ntsc.X/ntsc.c
 
Top