Home
Welcome
Information


FPGA projects - Basic
Music box
LED displays
Pong game
R/C servos
Text LCD module
Quadrature decoder
PWM and one-bit DAC
Debouncer
Crossing clock domains
The art of counting
External contributions

Interfaces
RS-232
JTAG
I2C
EPP
SPI
PCI
PCI Express
Ethernet
HDMI
SDRAM

Advanced
Digital oscilloscope
Graphic LCD panel
Direct Digital Synthesis
CNC steppers
Spoc CPU core

Hands-on
A simple oscilloscope


FPGA introduction
What are FPGAs?
How FPGAs work
Internal RAM
FPGA pins
Clocks and global lines
Download cables
Configuration
Learn more

FPGA software
Design software
Design-entry
Simulation
Pin assignment
Synthesis and P&R

FPGA electronic
SMD technology
Crystals and oscillators

HDL info
HDL tutorials
Verilog tips
VHDL tips

Quick-start guides
ISE
Quartus-II

Site
Links
HDL tutorials
Forum


Graphic LCD panel - Text

Let's try to display characters on our panel. This way, the panel can be used as a text terminal.
Our 480x320 sample panel can be used as a 80 columns x 40 lines console (using 6x8 characters font) or 60 columns x 40 lines console (using 8x8 character font). We are going to use a "character generator" technique.

Character generator

Let's assume the word "Hello" is somewhere on the screen. In ASCII, that uses 5 bytes (0x48, 0x65, 0x6C, 0x6C, 0x6F). Our simple character generator uses one RAM to hold the characters to display, and one ROM to hold the font.

"Font ROM" contains the representation of each possible characters. Here're sample sets:

Changing the content of the "Character RAM" makes characters appear on the panel.

8x8 font implementation

On the panel, a 6x8 font looks somewhat nicer than a 8x8 font, but 8x8 is easier to implement, so that's what we try first.

By using a 2Kbytes RAM for the "Character RAM", we can have 32 lines of 64 characters... so 5 bits to count the lines and 6 bits to count the columns.
By keeping all the numbers a power of two, the implementation stays as simple as possible.

Here's what we got:

We use 6 bits from CounterX, and 8 bits from CounterY (5 bits for the "Character RAM", plus 3 bits for the "Font ROM"). "Font ROM" uses a total of 11 bits.

The design goes as follow:

wire [7:0] CharacterRAM_dout;

ram8x2048 CharacterRAM(
	.clk(clk),
	.rd_adr({CounterY[7:3],CounterX[6:1]}),
	.data_out(CharacterRAM_dout)
);

wire [7:0] raster8;
rom8x2048 FontROM(
	.clk(clk),
	.rd_adr({CharacterRAM_dout, CounterY[2:0]}),
	.data_out(raster8)
);

wire [3:0] LCDdata = CounterX[0] ? raster8[7:4] : raster8[3:0];

A few details:

Here's a shot of the LCD with an 8x8 font:


6x8 font implementation

The 6x8 font allows for more characters to be displayed (and looks better on the panel too!). My particular panel width is 480 pixels, which translates conveniently into 80 columns.

The 6x8 font is more complicated to handle than 8x8 because the font width (6) is not a power of 2.
That means we don't display the same part of a character at each clock cycle anymore.

That is done using a simple case statement

wire [3:0] charfont0, charfont1;

always @(posedge clk)
begin
  case(cnt_mod3)
    2'b00: LCDdata <=  charfont0;
    2'b01: LCDdata <= {charfont0[3:2], charfont1[3:2]};
    2'b10: LCDdata <= {charfont1[3:2], charfont0[1:0]};
  endcase
end

with a modulus-3 counter (it counts 0, 1, 2, 0, 1, 2, 0, 1, ...)

reg [1:0] cnt_mod3;
always @(posedge clk) if(cnt_mod3==2) cnt_mod3 <= 0; else cnt_mod3 <= cnt_mod3 + 1;

but we also need a character-counter for the "Character RAM"

// character-counter (increments only twice every 3 clocks)
reg [6:0] cnt_charbuf;
always @(posedge clk) if(cnt_mod3!=1) cnt_charbuf <= cnt_charbuf + 1;

wire [11:0] CharacterRAM_rdaddr = CounterY[8:3]*80 + cnt_charbuf;
wire [7:0] CharacterRAM_dout;

ram8x2048 CharacterRAM(
	.clk(clk),
	.rd_adr(CharacterRAM_rdaddr),
	.data_out(CharacterRAM_dout)
);

and two "Font ROMs".

// remember the previous character displayed
reg [7:0] RAM_charbuf_dout_last;
always @(posedge clk) CharacterRAM_dout_last <= CharacterRAM_dout;

// because we need it when we display 2 half characters at once
wire [10:0] readaddr0 = {CharacterRAM_dout, CounterY[2:0]};
wire [10:0] readaddr1 = {CharacterRAM_dout_last, CounterY[2:0]};

// The font ROMs are split in two blockrams, holding 4 pixels each
// (half of the second ROM is not used, since we need only 6 pixels)
rom4x2048 FontROM0(.clk(clk), .rd_adr(readaddr0), .data_out(charfont0));
rom4x2048 FontROM1(.clk(clk), .rd_adr(readaddr1), .data_out(charfont1));

Here's a shot of the LCD with an 6x8 font:


Hardware cursor

Let's implement a blinking cursor that can be placed on any character on the panel.

First we use a video-frame counter to "time" the blinking. Using 6 bits, the cursor blinks every 64 frames.

reg [5:0] cnt_cursorblink;
always @(posedge clk) if(vsync & hsync) cnt_cursorblink <= cnt_cursorblink + 1;
wire cursorblinkstate = cnt_cursorblink[5];  // cursor on for 32 frames, off for 32 frames

Now let's assume the cursor address position is available in a "CursorAddress" register.

// Do we have a cursor-character match?
wire cursorblink_adrmatch = cursorblinkstate & (CharacterRAM_rdaddr==CursorAddress);

// When we have a match, "invert" the character
// First for charfont0
wire [3:0] charfont0_cursor = charfont0 ^ {4{cursorblink_adrmatch}};

// Next for charfont1
reg cursorblink_adrmatch_last;
always @(posedge clk) cursorblink_adrmatch_last <= cursorblink_adrmatch;
wire [3:0] charfont1_cursor = charfont1 ^ {4{cursorblink_adrmatch_last}};

Whoa, we can display graphics and text!

All this is applicable to monochrome and color LCDs.
Here's what happens when we get carried away on a color LCD...

Your turn to experiment!

Links




>>> HOME: Graphic LCD panel >>>



This page was last updated on October 19 2008.