website statistics

Generating NTSC composite video with an FPGA and two resistorsΒΆ

_images/tcircuit.jpg _images/tntsc.jpg

Inspired by all the people who have got composite video working on PIC microcontrollers, I tried to do the same thing using a FPGA in VHDL. The FPGA I used happens to be a Xilinx Spartan II XC2S200, but almost any FPGA could do it.

The hardware uses 2 output pins running in LVTTL (3.3v) mode. The circuit looks much like this one, although I adjusted the resistor values for 3.3v instead of 5v: 300 and 600 ohms seem to work.

The output is monochrome, but has a 16-level grayscale using pulse width modulation. The FPGA is running off an 80MHz oscillator, and that’s what clocks this circuit; I guess you could run at 160MHz through a DLL and get 32 shades of gray.

The picture looks pretty stable, which is probably more of a testament to the electronics in the TV than my VHDL skills.

There is a white bar across the top of the screen, gray bars, then 4096 bits (256 x 16) of bitmapped area, which I update via the host interface. The bitmap RAM is dual-ported: 1-bit wide on the scanout side, 8-bit wide on the host interface side.

Things not done:

  • I’m not sure if color is possible or not.
  • Interlace: I hate interlace, and I wish it would just die. So I didn’t implement it.

Here is the VHDL:

...

signal ntscLinecount : std_logic_vector(8 downto 0);
signal ntscLinecountN : std_logic_vector(8 downto 0);
signal ntscHPixelcount : std_logic_vector(11 downto 0);
signal ntscHPixelcountN : std_logic_vector(11 downto 0);
signal ntscPixelcount : std_logic_vector(12 downto 0);
signal ntscPixelcountN : std_logic_vector(12 downto 0);
signal ntscSignal : std_logic;
signal ntscLevel, ntscEq, ntscSe, ntscBl, ntscAc : std_logic;
signal ntscBitmap : std_logic_vector(0 downto 0);

signal ntscAddr : std_logic_vector(8 downto 0);
signal ntscData : std_logic_vector(7 downto 0);

...

-- ************************************************************************
--                          NTSC OUT
-- ************************************************************************
bram0: RAMB4_S1_S8
      port map (ADDRA => ntscLineCount(4 downto 1) & ntscPixelcount(11 downto 4),
                ADDRB => ntscAddr,
                DIA => "0",
                DIB => ntscData,
                DOA => ntscBitmap, WEA => '0',
                WEB => '1',  CLKA => clock, CLKB => hostCLK,
                RSTA => hostReset, RSTB => hostReset, ENA => '1', ENB => '1');

-- Derive all other signals from ntscLinecount, ntscPixelcount and ntscBitmap
process (ntscLinecount, ntscPixelcount, ntscBitmap(0))
begin
  if (hostReset = '1') then
    ntscHPixelcountN <= "000000000000";
    ntscPixelcountN <= "0000000000000";
    ntscLinecountN <= "000000000";
  else
    if (ntscHPixelcount = "100111110000") then
      ntscHPixelcountN <= "000000000000";
    else
      ntscHPixelcountN <= ntscHPixelcount + 1;
    end if;
    if (ntscPixelcount = "1001111100000") then
      ntscPixelcountN <= "0000000000000";
      if (ntscLinecount = "100000111") then
        ntscLinecountN <= "000000000";
      else
        ntscLinecountN <= ntscLinecount + 1;
      end if;
    else
      ntscPixelCountN <= ntscPixelcount + 1;
      ntscLinecountN <= ntscLinecount;
    end if;
  end if;

  -- ntscEq is the equalization pulse
  if (ntscHPixelcount < "000010111000") then
    ntscEq <= '0';
  else
    ntscEq <= '1';
  end if;

  -- ntscSe is the serration pulse
  if (ntscHPixelcount < "100010001000") then
    ntscSe <= '0';
  else
    ntscSe <= '1';
  end if;

  -- ntscBl is the blanking pulse
  if (ntscPixelcount < "0000101111000") then
    ntscBl <= '0';
  else
    ntscBl <= '1';
  end if;

  -- ntscAc is high when raster is in active (i.e. displayed) part of scan
  if (("000010101" < ntscLinecount) AND
      ("0001101101000" < ntscPixelcount) AND
      (ntscPixelcount < "1001101101000")) then
    ntscAc <= '1';
  else
    ntscAc <= '0';
  end if;

  -- logic to generate equalization, serration, and blanking pulses at start of every frame
  if (ntscLinecount(8 downto 4) = "00000") then
    case ntscLinecount(3 downto 0) is
    when X"0" =>
        ntscSignal <= ntscEq;
    when X"1" =>
        ntscSignal <= ntscEq;
    when X"2" =>
        ntscSignal <= ntscEq;
    when X"3" =>
        ntscSignal <= ntscSe;
    when X"4" =>
        ntscSignal <= ntscSe;
    when X"5" =>
        ntscSignal <= ntscSe;
    when X"6" =>
        ntscSignal <= ntscEq;
    when X"7" =>
        ntscSignal <= ntscEq;
    when X"8" =>
        ntscSignal <= ntscEq;
    when others =>
        ntscSignal <= ntscBl;
    end case;
  else
    ntscSignal <= ntscBl;
  end if;

  -- logic to generate ntsc level (i.e. the actual picture) during active part of frame
  if (ntscLinecount = "0001000XX") then
    -- white bar across top of screen
    ntscLevel <= '1';
  elsif (ntscLinecount = "1XXXXXXXX") then
    -- black border at bottom of screen
    ntscLevel <= '0';
  elsif (ntscLinecount = "0111XXXXX") then
    -- bitmap in lower part of screen
    ntscLevel <= ntscBitmap(0);
  else
    -- gray scale over rest of screen
    if (ntscPixelcount(10 downto 7) >= (ntscPixelcount(3 downto 0))) then
      ntscLevel <= '1';
    else
      ntscLevel <= '0';
    end if;
  end if;

end process;

process
begin
  wait until clock'event and clock = '1';
  ntscHPixelcount <= ntscHPixelcountN;
  ntscPixelcount <= ntscPixelcountN;
  ntscLinecount <= ntscLinecountN;
  output_01 <= ntscSignal;             -- sync pulses
  output_02 <= ntscAc AND ntscLevel;   -- black vs white
end process;

Note

The project above is a 2003 implementation using composite video. 1For a more a more recent color VGA interface in an FPGA, see The J1 Forth CPU

_images/invaders.jpg

More VHDL, Verilog and FPGA notes.