So let me just start by apologizing for the length of this post, but I want to include as much information as possible.
So a few months ago I began tinkering with creating a small, hobby (retro) computer for my own purposes and wanted to create some kind of graphic display. I own Andre LaMothe's "Black Art of Game Console Development" book, and I know he talks about different display systems in there, so I took a stab at the "easiest" one. He has schematics and diagrams, so I copied the design as much as I could, making a few chip substitutions to cut down on the number of analog multiplexers from 2 (74HC4051) to 1 (74HCT4067). The timings of the two chips look similar, so I didn't think it would cause much of a problem. And after a week or so of timing issues, it worked.
Sort of.
The test pattern created was a series of colored bars on the TV, but the colors didn't seem stable. They would produce a small ripple or wave like effect that became very pronounced, then very subtle, in some instances disappearing completely. This pattern came in waves, which lead me to believe the issue was noise. Since the board I made was just the driver, and didn't include the ATMega328 on the board (I was using a generic, Chinese Arduino Nano clone), it would be very easy for some stray noise to get into the system and cause a jiggle or two.
So I pushed on, and created a new board design which included the Arduino Nano (just to program the ATMega328 easily) on the board itself. I also added more colors because the color palette I had picked originally was crap. The NTSC color wheel starts at yellow, but I made my calculations assuming it started at red like a standard HSV wheel. Oh well, live and learn.
When I finished the programming of the board, I was shocked to see that I had gained a saturation problem. Between colors 29 and 32, most of the color had been sucked out of the system. I attribute this to noise being added due to the 74HCT244 as the oscillator's signal passes between the VIL and VIH thresholds, which gets progressively worse the more gates it has to go through. Makes sense to me, but if any of you much more experienced guys has a clue, I'm all ears.
(Otherwise, I plan to deal with the issue by using Schmitt Trigger gates in the future.)
But my noise issue was also still there, and worse now. Both the top and bottom of my board are ground-filled as much as possible, to help cut down on noise, so it should have gotten better, right? Now, in addition to the ripple effect, I also have what looks like a rainbowing effect on some lines. I tried the timings LaMothe mentioned in his book, as well as the Nintendo timings I found here:
https://wiki.nesdev.org/w/index.php/NTSC_video as well as the timings I found here:
https://www.avrfreaks.net/sites/default/files/ntsctime.pdf. The Nintendo timings didn't work at all (though I may have messed them up) and both LaMothe's and AVR Freak's timings seem to exhibit the same problem.
Since the design I'm using is based on a copyrighted book, I'm not sure what the legality is on posting the designs directly, but for now, I'll just describe the boards (they're all pretty much the same):
A 3.579545 MHz crystal oscillator feeds directly into one of the inputs of a 74HCT244 where it ripples through all of the available inputs, as well as the inputs of 3 other chips except 1. This adds delay in order to achieve the phase shift needed for NTSC color. Then, those signals are all feed into 2 74HCT4067 chips, and the color value from the ATMega328 is used as the selector (plus an extra signal for LOW or HIGH bank). The output of that is passed through into the empty input of a 74HCT244 (this was copied from the original design...I don't think it's necessary), then piped through a 75Ohm resistor. A 1nF capacitor is used to smooth out the signal, in an attempt to get close to a sine wave, and is combined with an intensity signal (also coming from the ATMega328) through an R2R ladder (400 and 200 Ohms). A couple of potentiometers allow for adjustment of the brightness and saturation (copied from the original design), but I couldn't get the values he suggested, so mine are as close as possible (1K and 10K, both linear).
I also use a 12ns RAM chip (W24257AK-12) to handle some of the color decoding, so that I can use a single port (PORTD) to change colors, but I highly doubt that's the culprit. My first test board didn't even use it, but the issue has gotten worse, so who knows?
I've attached an image to show the issue I'm having, easily visible on the left and right sides of the image. Here is the code for the ATMega328 (all assembly to ensure timing):
;; Arduino timing:
;; 16 MHz = 62.5ns per clock
;; 16 MHz / 3 = 187.5ns per pixel
;; [url]https://www.avrfreaks.net/sites/default/files/ntsctime.pdf[/url]
;; NSTC Timing:
;; Normal pulses 253 EVEN LINES, 254 ODD LINES 14/15 TOP OVERSCAN 15 BOTTOM OVERSCAN
;; SYNC 4.7us 4700ns 25 pixels
;; BREEZEWAY 1.6us 1687.5ns 9 pixels
;; COLOR BURST 2.5us 2437.5ns 13 pixels
;; BACK PORCH 2.1us 2062.5ns 11 pixels
;; VIDEO DATA 51.2us 51187.5ns 273 pixels
;; LEFT BORDER 1.5us 1500ns 8 pixels
;; ACTIVE VIDEO 48us 48000ns 256 pixels
;; RIGHT BORDER 1.6us 1687.5ns 9 pixels
;; FRONT PORCH 1.5us 1500ns 8 pixels
;;
;; Equalizing pulses
;; BOTH 3 LINES
;; SYNC 2.3us 2250ns 12 pixels
;; BLANK 29.5us 29625ns 158 pixels
;; SYNC 2.3us 2250ns 12 pixels
;; BLANK 29.5us 29625ns 158 pixels
;; ODD 1 LINE
;; SYNC 2.3us 2250ns 12 pixels
;; BLANK 29.5us 29625ns 158 pixels
;; SYNC 27.3us 27375ns 146 pixels
;; BLANK 4.5us 4500ns 24 pixels
;;
;; Serration pulses
;; EVEN 3 LINES
;; SYNC 27.3us 27375ns 146 pixels
;; BLANK 4.5us 4500ns 24 pixels
;; SYNC 27.3us 27375ns 146 pixels
;; BLANK 4.5us 4500ns 24 pixels
;; ODD 1 LINE (2 LINES OF ABOVE)
;; SYNC 27.3us 27375ns 146 pixels
;; BLANK 4.5us 4500ns 24 pixels
;; SYNC 2.3us 2250ns 12 pixels
;; BLANK 29.5us 29625ns 158 pixels
;;
;;
;; Frame process
;; EVEN FRAME
;; 14 OVERSCAN
;; 224 ACTIVE VIDEO LINES
;; 15 OVERSCAN
;; 3 EQUALIZING PULSE
;; 1 EQUALIZING PULSE ODD
;; 2 SERRATING PULSE
;; 1 SERRATING PULSE ODD
;; 2 EQUALIZING PULSE
;; 263 LINES
;; ODD FRAME
;; 15 OVERSCAN
;; 224 ACTIVE VIDEO
;; 15 OVERSCAN
;; 3 EQUALIZING PULSE
;; 3 SERRATING PULSE
;; 3 EQUALIZING PULSE
;; 262 LINES
.def wait1 = r16
.def wait2 = r17
.def lineCount = r18
.def color = r19
.def colorCount = r20
.def voltage = r21
.def colorIncrease = r22
.def ledStatus = r23
.def ledMask = r24
.set HSYNC = $00
.set VSYNC = $00
.set BLANK = $01
.set COLOR_BURST = $09
.set BLACK = $02
.set OVERSCAN_COUNT = 15
.set OVERSCAN_COUNT_EVEN = 14
;; Takes 1 parameter (x) and waits 3x clock cycles
;; Timing: 3x clock cycles -- x pixels
;; Proof:
;; WAIT 6 => 6 pixels
;; ldi r16, 6 ;; 1 clock
;; loop: subi r16, 1 ;; 1 clock -- r16 = 5
;; brne loop ;; 2 clocks
;; loop: subi r16, 1 ;; 1 clock -- r16 = 4
;; brne loop ;; 2 clocks
;; loop: subi r16, 1 ;; 1 clock -- r16 = 3
;; brne loop ;; 2 clocks
;; loop: subi r16, 1 ;; 1 clock -- r16 = 2
;; brne loop ;; 2 clocks
;; loop: subi r16, 1 ;; 1 clock -- r16 = 1
;; brne loop ;; 2 clocks
;; loop: subi r16, 1 ;; 1 clock -- r16 = 0
;; brne loop ;; 1 clocks
;; ;; 18 clocks = 3 * 6 => 6 pixels
.macro WAIT
ldi wait1, @0 ;; 1 clock
loop: subi wait1, 1 ;; 1 clock
brne loop ;; 2 clocks, 1 if fall through
.endmacro
;; Takes 2 parameters (x, y) and waits 3x(y+1) clock cycles
;; Timing: 3x(y + 1) clock cycles -- x(y + 1) pixels
;; Proof:
;; WAIT_LONG 6, 5 => 36 pixels
;; ldi r17, 6 ;; 1 clock
;; loop: WAIT 5 ;; 15 clocks
;; subi r17, 1 ;; 1 clock -- r17 = 5
;; brne loop ;; 2 clocks
;; loop: WAIT 5 ;; 15 clocks
;; subi r17, 1 ;; 1 clock -- r17 = 4
;; brne loop ;; 2 clocks
;; loop: WAIT 5 ;; 15 clocks
;; subi r17, 1 ;; 1 clock -- r17 = 3
;; brne loop ;; 2 clocks
;; loop: WAIT 5 ;; 15 clocks
;; subi r17, 1 ;; 1 clock -- r17 = 2
;; brne loop ;; 2 clocks
;; loop: WAIT 5 ;; 15 clocks
;; subi r17, 1 ;; 1 clock -- r17 = 1
;; brne loop ;; 2 clocks
;; loop: WAIT 5 ;; 15 clocks
;; subi r17, 1 ;; 1 clock -- r17 = 0
;; brne loop ;; 1 clock
;; ;; 108 clocks = 3 * 36 => 36 pixels
.macro WAIT_LONG
ldi wait2, @0 ;; 1 clock
loop: WAIT @1 ;; 3 * @1 clocks
subi wait2, 1 ;; 1 clock
brne loop ;; 2 clocks, 1 if fall through
.endmacro
;; Takes 1 parameter (voltage)
;; Timing: 2 clock cycles
.macro SET_VOLTAGE
ldi voltage, @0
out PORTD, voltage
.endmacro
;; Takes 1 parameter (colorburst)
;; Timing: 186 clock cycles -- 62 pixels
.macro LINE_START
;; SYNC 4.7us 4700ns 25 pixels
;; BREEZEWAY 1.6us 1687.5ns 9 pixels
;; COLOR BURST 2.5us 2437.5ns 13 pixels
;; BACK PORCH 2.1us 2062.5ns 11 pixels
;; VIDEO DATA 51.2us 51187.5ns 273 pixels
;; LEFT BORDER 1.5us 1500ns 8 pixels
;; ACTIVE VIDEO 48us 48000ns 256 pixels
;; RIGHT BORDER 1.6us 1687.5ns 9 pixels
;; FRONT PORCH 1.5us 1500ns 8 pixels
;; HSYNC -- 25 pixels
nop ;; 1 clock
SET_VOLTAGE HSYNC ;; 2 clocks -- pixel
WAIT 25 ;; 72 clocks -- 24 pixels, HSYNC now over
;; BREEZEWAY -- 9 pixels
nop ;; 1 clock
SET_VOLTAGE BLANK ;; 2 clocks -- pixel
WAIT 8 ;; 24 clocks -- 8 pixels, BREEZEWAY now over
;; COLOR_BURST -- 13 pixels
nop ;; 1 clock
SET_VOLTAGE @0 ;; 2 clocks -- pixel
WAIT 12 ;; 36 clocks -- 12 pixels, COLOR_BURST now over
;; BACK PORCH -- 11 pixels
nop ;; 1 clock
SET_VOLTAGE BLANK ;; 2 clocks -- pixel
WAIT 10 ;; 30 clocks -- 10 pixels, BACK PORCH now over
;; 8 clock black pixels for left border
nop ;; 1 clock
SET_VOLTAGE BLACK ;; 2 clocks -- pixel
WAIT 7 ;; 21 clocks -- 7 pixels, LEFT BORDER now over
.endmacro
;; Takes 1 parameter (color + intensity)
.macro INTENSITY_LINE
;; 256 pixels of 1 intensity
nop ;; 1 clock
SET_VOLTAGE @0 ;; 2 clocks -- pixel
WAIT 255 ;; 255 more pixels, INTENSITY_LINE now over
.endmacro
;; Takes 1 parameter (intensity)
.macro DISPLAY_LINE
;; 8 pixels of 32 colors = 256 pixels total
ldi colorIncrease, $08 ;; 1 clock
ldi color, @0 ;; 1 clock
out PORTD, color ;; 1 clock -- pixel
ldi colorCount, 31 ;; 1 clock
nop ;; 1 clock
nop ;; 1 clock -- pixel
WAIT 6 ;; 18 clocks -- 6 pixels, next color
loop: nop ;; 1 clock
add color, colorIncrease ;; 1 clock
out PORTD, color ;; 1 clock -- pixel
WAIT 6 ;; 18 clocks -- 6 pixels
subi colorCount, 1 ;; 1 clock
brne loop ;; 2 clocks, 1 fall through
nop ;; 1 clock -- pixel, DISPLAY_VIDEO now over
.endmacro
// Takes no parameters, ends 1 pixel early, for looping
.macro LINE_END
;; RIGHT BORDER 1.6us 1687.5ns 9 pixels
;; FRONT PORCH 1.5us 1500ns 8 pixels
;; 9 black pixels for border
nop ;; 1 clock
SET_VOLTAGE BLACK ;; 2 clocks -- pixel
WAIT 8 ;; 24 clocks -- 8 pixels
;; FRONT PORCH, 8 pixels (really 7 for external looping)
nop ;; 1 clock
SET_VOLTAGE BLANK ;; 2 clocks -- pixel
WAIT 6 ;; 18 clocks -- 6 pixels
;; Skip last pixel
.endmacro
;; Takes no parameters
.macro BLACK_VIDEO
;; 256 black pixels
nop ;; 1 clock
SET_VOLTAGE BLACK ;; 2 clocks -- pixel
WAIT 255 ;; 765 clocks -- 255 pixels, BLACK_VIDEO now over
.endmacro
;; Takes no parameters, lineCount must be set before using, leaves 1 unused clock cycle
.macro OVERSCAN
loop: LINE_START BLACK
BLACK_VIDEO
LINE_END
subi lineCount, 1 ;; 1 clock
brne loop ;; 2 clocks, 1 if fall through
;; 1 unused clock
.endmacro
;; Takes 2 parameters (linecount, intensity), requires 1 extra clock cycle
.macro INTENSITY_BAR
ldi lineCount, @0 ;; 1 clock (extra clocks burned)
loop: LINE_START BLACK
INTENSITY_LINE @1
LINE_END ;; 3 unused clocks
subi lineCount, 1 ;; 1 clock
brne loop ;; 2 clocks, 1 if fall through
;; 1 unused clock
.endmacro
;; Takes 2 parameters (linecount, intensity), requires 1 extra clock cycle
.macro COLOR_BAR
ldi lineCount, @0 ;; 1 clock (extra clocks burned)
loop: LINE_START COLOR_BURST
DISPLAY_LINE @1
LINE_END ;; 3 unused clocks
subi lineCount, 1 ;; 1 clock
brne loop ;; 2 clocks, 1 if fall through
;; 1 unused clock
.endmacro
;; Takes 4 parameters (syncTime1, blankTime1, syncTime2, blankTime2)
.macro VSYNC_PULSE
nop ;; 1 clock
SET_VOLTAGE VSYNC ;; 2 clocks -- pixel
WAIT @0 ;; syncTime1 - 1 pixels
nop ;; 1 clock
SET_VOLTAGE BLANK ;; 2 clocks -- pixel
WAIT @1 ;; blankTime1 - 1 pixels
nop ;; 1 clock
SET_VOLTAGE VSYNC ;; 2 clocks -- pixel
WAIT @2 ;; syncTime2 - 1 pixels
nop ;; 1 clock
SET_VOLTAGE BLANK ;; 2 clocks -- pixel
WAIT @3 ;; blankTime2 - 1 pixels
.endmacro
;; Equalizing pulses
;; BOTH 3 LINES
;; SYNC 2.3us 2250ns 12 pixels
;; BLANK 29.5us 29625ns 158 pixels
;; SYNC 2.3us 2250ns 12 pixels
;; BLANK 29.5us 29625ns 158 pixels
;; ODD 1 LINE
;; SYNC 2.3us 2250ns 12 pixels
;; BLANK 29.5us 29625ns 158 pixels
;; SYNC 27.3us 27375ns 146 pixels
;; BLANK 4.5us 4500ns 24 pixels
;;
;; Serration pulses
;; EVEN 3 LINES
;; SYNC 27.3us 27375ns 146 pixels
;; BLANK 4.5us 4500ns 24 pixels
;; SYNC 27.3us 27375ns 146 pixels
;; BLANK 4.5us 4500ns 24 pixels
;; ODD 1 LINE (2 LINES OF ABOVE)
;; SYNC 27.3us 27375ns 146 pixels
;; BLANK 4.5us 4500ns 24 pixels
;; SYNC 2.3us 2250ns 12 pixels
;; BLANK 29.5us 29625ns 158 pixels
;; Leaves three clock cycles unused
.macro VSYNC_ODD
;; 3 EQUALIZING PULSE
VSYNC_PULSE 11, 157, 11, 157
VSYNC_PULSE 11, 157, 11, 157
VSYNC_PULSE 11, 157, 11, 157
;; 3 SERRATING PULSE
VSYNC_PULSE 145, 23, 145, 23
VSYNC_PULSE 145, 23, 145, 23
VSYNC_PULSE 145, 23, 145, 23
;; 3 EQUALIZING PULSE
VSYNC_PULSE 11, 157, 11, 157
VSYNC_PULSE 11, 157, 11, 157
VSYNC_PULSE 11, 157, 11, 156 ;; Leave 1 pixel accounted for
.endmacro
;; Leaves three clock cycles unused
.macro VSYNC_EVEN
;; 3 EQUALIZING PULSE
VSYNC_PULSE 11, 157, 11, 157
VSYNC_PULSE 11, 157, 11, 157
VSYNC_PULSE 11, 157, 11, 157
;; 1 EQUALIZING PULSE ODD
VSYNC_PULSE 11, 157, 145, 23
;; 2 SERRATING PULSE
VSYNC_PULSE 145, 23, 145, 23
VSYNC_PULSE 145, 23, 145, 23
;; 1 SERRATING PULSE ODD
VSYNC_PULSE 145, 23, 11, 157
;; 2 EQUALIZING PULSE
VSYNC_PULSE 11, 157, 11, 157
VSYNC_PULSE 11, 157, 11, 156 ;; Leave 1 pixel accounted for
.endmacro
; Replace with your application code
start: jmp frame ;; RESET
jmp halt ;; IRQ0 handler
jmp halt ;; IRQ1 handler
jmp halt ;; PCINT0 handler
jmp halt ;; PCINT1 handler
jmp halt ;; PCINT2 handler
jmp halt ;; WDT (watchdog) handler
jmp halt ;; TIM2_COMPA handler
jmp halt ;; TIM2_COMPB handler
jmp halt ;; TIM2_OVF handler
jmp halt ;; TIM1_CAPT handler
jmp halt ;; TIM2_COMPA handler
jmp halt ;; TIM2_COMPB handler
jmp halt ;; TIM2_OVF handler
jmp halt ;; SPI_STC handler
jmp halt ;; USART_RXC handler
jmp halt ;; USART_UDRE handler
jmp halt ;; USART_TXC handler
jmp halt ;; ADC handler
jmp halt ;; EE_RDY handler
jmp halt ;; ANA_COMP handler
jmp halt ;; TWI handler
jmp halt ;; SPM_RDY handler
;; [url]https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf[/url]
;; page 50
halt: jmp halt
frame: ;; Set up port D
clr voltage
sts UCSR0B, voltage
ldi voltage, 0xff ;; All outputs
out DDRD, voltage
;; Set up port B
ldi ledMask, $20 ;; bit 5 = Arduino D13 = onboard LED
out DDRB, ledMask
;;ldi ledStatus, $20
;;out PORTB, ledMask
ldi lineCount, OVERSCAN_COUNT_EVEN
jmp display_loop
;; WAIT TEST
;; 2665 pixels ~ 1kHz (but leave 2 for looping)
;; 2663 = 2662 + 1 = 22 * 121 + 1
blink_1khz: WAIT_LONG 22, 120 ;; 2662 pixels
nop
nop
nop ;; 2663 pixels
nop
eor ledStatus, ledMask
out PORTB, ledStatus ;; 2664 pixels
jmp blink_1khz ;; 2665 pixels
;; OVERSCAN TEST1
;; 1 clock + 338 pixels - 1 clock + 5 clocks = 339 px + 2 clocks = 63,687.5 ns
;; 15,701.6 kHz / 2 =
;; 7,850.83 kHz
overscan_test1: ldi lineCount, 1
OVERSCAN
eor ledStatus, ledMask
out PORTB, ledStatus
jmp overscan_test1
;; OVERSCAN TEST2
;; 438.3 Hz
overscan_test2: ldi lineCount, OVERSCAN_COUNT
OVERSCAN
eor ledStatus, ledMask
out PORTB, ledStatus
jmp overscan_test2
;; DISPLAY TEST
;; 337 pixels + 4 clocks = 338 + 1 clocks =
;; 7.874.0157 kHz
display_test: LINE_START COLOR_BURST
DISPLAY_LINE 15
LINE_END
eor ledStatus, ledMask
out PORTB, ledStatus
jmp display_test
;; COLOR_BAR TEST
;; 357.9 Hz
colorbar_test: COLOR_BAR 36, 15
eor ledStatus, ledMask
out PORTB, ledStatus
jmp colorbar_test
;; VSYNC TEST
;; 1.312 kHz
;;vsync_test: VSYNC_PULSE
;; eor ledStatus, ledMask
;; out PORTB, ledStatus
;; jmp vsync_test
display_loop:
even_frame:
OVERSCAN ;; 1 unused clock cycle
;; 224 lines by 6 intensities = 37 lines per color bar (+1 for top and bottom)
COLOR_BAR 38, 7 ;; requires 1 extra clock cycle, leaves 1 unused clock cycle
COLOR_BAR 37, 6 ;; requires 1 extra clock cycle, leaves 1 unused clock cycle
COLOR_BAR 37, 5 ;; requires 1 extra clock cycle, leaves 1 unused clock cycle
COLOR_BAR 37, 4 ;; requires 1 extra clock cycle, leaves 1 unused clock cycle
COLOR_BAR 37, 3 ;; requires 1 extra clock cycle, leaves 1 unused clock cycle
COLOR_BAR 38, 2 ;; requires 1 extra clock cycle, leaves 1 unused clock cycle
ldi lineCount, OVERSCAN_COUNT ;; 1 clock cycle, timing caught up
OVERSCAN ;; 1 unused clock cycle
ldi lineCount, OVERSCAN_COUNT ;; 1 clock cycle (required for first odd OVERSCAN)
VSYNC_EVEN ;; Perform vsync pulses for even frame
;; 3 unused clock cycles
nop ;; 1 clock
nop ;; 1 clock
nop ;; 1 clock
odd_frame:
OVERSCAN ;; 1 unused clock cycle
;; 224 lines by 6 intensities = 37 lines per color bar (+1 for top and bottom)
COLOR_BAR 38, 7 ;; requires 1 extra clock cycle, leaves 1 unused clock cycle
COLOR_BAR 37, 6 ;; requires 1 extra clock cycle, leaves 1 unused clock cycle
COLOR_BAR 37, 5 ;; requires 1 extra clock cycle, leaves 1 unused clock cycle
COLOR_BAR 37, 4 ;; requires 1 extra clock cycle, leaves 1 unused clock cycle
COLOR_BAR 37, 3 ;; requires 1 extra clock cycle, leaves 1 unused clock cycle
COLOR_BAR 38, 2 ;; requires 1 extra clock cycle, leaves 1 unused clock cycle
ldi lineCount, OVERSCAN_COUNT ;; 1 clock cycle, timing caught up
OVERSCAN ;; 1 unused clock cycle
ldi lineCount, OVERSCAN_COUNT_EVEN ;; 1 clock cycle (required for first even OVERSCAN)
VSYNC_ODD ;; Perform vsync pulses for odd frame
;; 3 unused clock cycles
jmp display_loop
If you are confused why there are "ODD" lines in the EVEN frames, it's because I adjusted the timing from the PDF linked above to create tighter code.
Frankly, at this point, I'm at a loss. I don't know if this belongs in the "beginner" section, but I think I'm missing something totally obvious. Help?