FPGA 设计实例 基于 FPGA 的图形液晶显示面板应用 Graphic LCD panel FPGAs make great video controllers and can easily control graphic LCD panels. This project is split in 4 parts: 1. Introduction 2. Video generator 3. Graphics 4. Text Here's a view of one LCD panel used for this project:
Graphic LCD panel - Introduction Graphic LCD panels use a digital interface and are easy to interface to FPGAs. The different interfaces Graphic LCD panels come in 2 flavors: 1. With a Video-like interface 2. With a CPU-peripheral-like interface Advantages 1. Video-like interface Low cost & widely available (used in notebook computers for example) Easy to control from an FPGA Freedom on what is displayed 2. CPU-peripheral-like interface Easy to attach to microcontrollers A frame-buffer memory is integrated
(frame-buffer/on-the-fly video/sprites/hardware mouse cursor... or any combination of these) to the panel Disadvantages Requires a video controller (FPGA or dedicated chip) Requires some external memory (for the frame-buffer, character generator,...) Higher cost & lower availability For frame-buffer applications only How it works Uses a video signal - with HSync (horizontal sync) and VSync (vertical sync) pulses Similar to a VGA signal, but with digital RGB video signals The panel appears like a memory (SRAM) to the microcontroller The CPU writes to the SRAM and the data appears on the screen This project uses "type-1" panels - since we use FPGAs here, that's the most appropriate. Graphic LCD panel - video generator Prior to be able to display anything on the panel, we need to generate video-sync signals (H-sync and V-sync). The LCD used for this project has the following characteristics: Monochrome, with a 480x320 resolution (about 150000 pixels). Synchronous interface, 4-bits data interface (4 pixels are entered for each clock). No off-screen period. With a 4-bits data input, we need 480/4=120 clocks horizontally. With 320 lines, a complete video frame takes 120x320=38400 clocks. The code looks like that: parameter ScreenWidth = 480; parameter ScreenHeight = 320;
reg [6:0] CounterX; // counts from 0 to 119 reg [8:0] CounterY; // counts from 0 to 319 wire CounterXmaxed = (CounterX==ScreenWidth/4-1); wire CounterYmaxed = (CounterY==ScreenHeight-1); always @(posedge clk) begin if(counterxmaxed) CounterX <= 0; else CounterX <= CounterX + 1; end always @(posedge clk) if(counterxmaxed) begin if(counterymaxed) CounterY <= 0; else CounterY <= CounterY + 1; end reg HSync, VSync; always @(posedge clk) begin HSync <= CounterXmaxed; VSync <= CounterYmaxed; end Now, let's provide data to be displayed - first some graphics, then some text. Graphic LCD panel - Graphics Let's investigate 3 ways to generate graphical video data.
Rasterized bitmaps The classical (and easy) way to display graphics on an LCD is to hold rasterized bitmap data into a RAM. We are going to use a blockram here (see here to learn what are blockrams). We display a small 128x32 pixels bitmap here (fits nicey into a 4Kbits blockram): // Use a blockram to hold the graphical data wire [7:0] BitmapData; blockram_8x512 RAM_bitmap(.clk(clk),.rd_adr({CounterY[4:0],CounterX[4:1]}),.data_out(Bit mapdata)); // Let's say we need 4 bits at a time wire [3:0] LCD_Bitmap4 = CounterX[0]? BitmapData[3:0] : BitmapData[7:4]; // Display the data into a chessboard pattern wire [3:0] LCD_BitmapChessboard = (CounterY[5] ^ CounterX[5])? 4'b000 : LCD_Bitmap4 ^ {4{CounterY[5]}}; Not shown above is how the RAM is written. The easiest is to treat it as a ROM (the RAM content is part of the FPGA configuration, and doesn't change while the FPGA is running). Here's a macro shot:
The drawback of rasterized bitmaps is that you require a large enough RAM to hold the state of each pixel of the bitmap. That's expensive using internal FPGA RAM, so external RAM is often used instead. Now, let's explore more original ways to create graphics. Curve y=f(x) Let's say we want to display an y=f(x) waveform, like a sine. That's surprisingly easy to do. We hold the "Y" values into a blockram and generate the picture line by line by reading the RAM and comparing the values against "CounterY" (the current line number). // We assume CounterX and CounterY are available: // CounterX is the pixel number of the current line // CounterY is the line number // We use a RAM to hold the "Y" values // Y=F(CounterX) wire [7:0] RAM_Y_value;
blockram_8x512 RAM_FXY(.clk(clk),.rd_adr(CounterX),.data_out(RAM_Y_value)); // check for equality between the "Y" values and "CounterY" reg grcpeq1; always @(posedge clk) grcpeq1 <= (RAM_Y_value==CounterY); reg grcpeq2; always @(posedge clk) grcpeq2 <= grcpeq1; // check for "greater-than" between the "Y" values and "CounterY" reg grcp1; always @(posedge clk) grcp1 <= (RAM_Y_value>CounterY); reg grcp2; always @(posedge clk) grcp2 <= grcp1; // display a pixel if equality, or if "CounterY" is between 2 successive "Y" values wire FXpix= grcpeq2 (grcp1 ^ grcp2); Here's the result using F(x)=cos(x*2*pi/480)*sin(x*2*pi/480*4):
Rotozoom A Rotozoom is an efficient method to display a bitmap with linear geometric distortions. In particular, that allows easy rotating and zooming of a picture. In the following implementation, we display a rotated chessboard pattern on the screen. reg [15:0] X0, Y0, X1, Y1; always @(posedge clk) if(vsync) begin X0 <= 0; Y0 <= 0; X1 <= 0; Y1 <= 0; end else if(hsync) begin X0 <= X1-100; Y0 <= Y1 + 400; X1 <= X1-100; Y1 <= Y1 + 400; end else begin X0 <= X0 + 400; Y0 <= Y0 + 100; end // Display a chessboard pattern by XOR'ing the MSB of X and Y counters // You could also display a rotozoomed bitmap by feeding X and Y to a bitmap in a RAM wire rotozoom_pix = X0[15] ^ Y0[15];
This is simplified because the increment values are fixed (400 and 100 above). You'd want to vary them in a real implementation. By varying the coefficients, you rotate and zoom the chessboard. Here's the result, mixed with the previous waveform: You can then mix that with some text... 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: Because my LCD panel takes 4-pixels per clock, we need 2 clocks for 1 character width (1 character width=8 pixels). That's why "Character RAM" uses "CounterX[6:1]" instead of "CounterX[5:0]" above. In an FPGA, it makes sense that the "Font ROM" is actually a RAM. This way, the font can be loaded or changed at runtime. The mechanism to write the RAMs is not shown above.
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. With CounterX=0, we display the first 4 pixels of CHAR[0] With CounterX=1, we display the last 2 pixels of CHAR[0] and the first 2 pixels of CHAR[1] With CounterX=2, we display the last 4 pixels of CHAR[1] With CounterX=3, we display the first 4 pixels of CHAR[2] With CounterX=4, we display the last 2 pixels of CHAR[2] and the first 2 pixels of CHAR[3]... 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!