Text VGA module on VHDL
In this article, I will introduce a VGA text module written in VHDL. This module can be useful when debugging the board, and takes up relatively little space.
The module operates at a clock frequency of 50 MHz.
It produces a picture with a resolution of 640x480 with a frequency of 60Hz.
The character size is 8x16 pixels. The screen has 80x25 characters.
Palette for 32 colors.
Resources occupied in FPGA:
The interface of the module includes the input of the clock generator (clk), the output to the VGA-connector (r, g, b, vsync, hsync) and the output to the processor bus (addr, data, iowr, dout). The board I worked with has 8-bit DACs in the amount of three pieces (one for each color), so 8-bit lines in colors come from the module. Taking the most significant bit of color, we get a working version for boards without a DAC.
The monitor requires 3 sync signals and 3 color signals. To generate synchronization signals, VCounter and HCounter counters are used. Synchronization signals are issued at the moments shown in the diagram.
Counters Code:
Generation of synchronization signals:
The size of one character is 8x16 pixels. In the character array there will be 256 cells of 128 bits each. Each bit is responsible for whether a point is in the current position or not. Location of points in the memory cell:
In FPGA there is a block memory configuration of 1024x18 bit (18432 bit), which we will focus on. CAD automatically implements almost any selected configuration using such memory blocks. So, for character memory you need 256x128 bits = 32768 bits of memory, which will take 2 memory blocks (BRAM).
The symbol memory is initialized during design and can be changed during operation.
Another necessary memory is the memory of the screen (visible area). In this memory, we encode the symbol number, symbol color, and background color. To save color memory, encode the number in the palette. Based on the configuration of the memory with a cell of 18 bits, we assign 8 bits to the symbol number, 5 bits to the symbol color, and 5 bits to the background color. Under the palette, 32 cells are allocated 24 bits each (8 bits per color).
Based on the resolution and size of the character, 80x30 characters fit on the screen. But it takes up 2400 cells (3 memory blocks) in memory. Since, from the very beginning, the task of the minimum size of this module was set, the 5 bottom lines were cut. As a result, we get 80x25 displayed characters, which occupies 2000 memory cells (2 memory blocks).
Character memory is generated in the coregenerator. The memory connection to the module is as follows:
Screen memory is created by writing boilerplate code (although you can do the same as with character memory).
It is also necessary to set the color table of the palette, which is as follows.
All the necessary memory is formed, now you can start implementing the preparation of a pixel for display on the screen.
Current FPGA design trends imply fully synchronous designs. We will not deviate from these trends, and we will implement a pipeline to prepare the next pixel. At the input there will be HCount and VCount counters that determine the current position on the screen, and at the output there will be the color of the pixel that needs to be displayed.
Conveyor Stages:
We will work with the module through the bus with the interface
The bus is controlled by a fort processor implemented on the same FPGA. On the OUTPORT command, the Addr, Data lines are set and the Iowr line rises to “1” line. At the INPORT command, the processor sets the Addr line and collects data from the Dout line.
Example of working with a bus:
In the same way, most of the commands for working with the module are implemented.
A complete listing of the module is available here.
Listing of the module
To initialize the
UPD symbol memory :
Basic functions
300 - write / read from the screen memory
301 - address for writing to the screen memory
302 - set the X coordinate of the carriage (GotoX)
303 - set the coordinate of the carriage (GotoY)
304 - set the color of the symbol ( SetColor)
305 - set the background color (SetBgColor)
306 - character output and carriage movement (EMIT)
307 - switch mode
1 - LowLevel mode. 300, 301 functions available for direct memory operation
0 - Normal mode. Function 306 is available to set a character with a given color to a given position and move the carriage.
308 - CR
309 - VGA_SetData (A ++)
Cursor Parameters
310 - set cursor parameters.
0 bit - lower bar in the cursor
1 bit - black square
2 bit - blinking
3 bit - vertical bar
311 - setting bit No. 0 in the cursor parameters
...
314 - setting bit No. 3 in the cursor parameters
320 - setting the color of the cursor
321 - setting the blinking time cursor
322 - setting the time of the burning cursor
Working with the palette
330 - Setting the address of the color palette
331 - Record the color at the set address
Working with the symbol table
335 - Setting the address in the symbol table
337 - Writing NewSymbol to the specified address
340 - Setting the lower 32 bits of the NewSymbol signal (NewSymbol (31: 0))
341 - Setting the next 32 bits of NewSymbol (63:32)
342 - NewSymbol ( 95:64)
343 - NewSymbol (127: 96)
Thank you for your attention.
General characteristics
The module operates at a clock frequency of 50 MHz.
It produces a picture with a resolution of 640x480 with a frequency of 60Hz.
The character size is 8x16 pixels. The screen has 80x25 characters.
Palette for 32 colors.
Resources occupied in FPGA:
Implementation
The interface of the module includes the input of the clock generator (clk), the output to the VGA-connector (r, g, b, vsync, hsync) and the output to the processor bus (addr, data, iowr, dout). The board I worked with has 8-bit DACs in the amount of three pieces (one for each color), so 8-bit lines in colors come from the module. Taking the most significant bit of color, we get a working version for boards without a DAC.
entity vga_text is
Port (clk: in STD_LOGIC;
iowr: in STD_LOGIC;
addr: in STD_LOGIC_VECTOR (31 downto 0);
data: in STD_LOGIC_VECTOR (31 downto 0);
dout: out std_logic_vector (31 downto 0);
r: out: 7 downto 0);
g: out STD_LOGIC_VECTOR (7 downto 0);
b: out STD_LOGIC_VECTOR (7 downto 0);
vga_blank: out STD_LOGIC;
vsync: out STD_LOGIC;
hsync: out STD_LOGIC);
end vga_text;
The monitor requires 3 sync signals and 3 color signals. To generate synchronization signals, VCounter and HCounter counters are used. Synchronization signals are issued at the moments shown in the diagram.
Counters Code:
architecture Behavioral of vga_text is
signal VCounter: integer range 0 to 520: = 0;
signal HCounter: integer range 0 to 800: = 0;
signal div: std_logic: = '0';
begin
process (clk)
begin
if rising_edge (clk) then
div <= not (div);
if div = '1' then
if HCounter = 799 then
HCounter <= 0;
if VCounter = 520 then
VCounter <= 0;
else
VCounter <= VCounter + 1;
end if;
else
HCounter <= HCounter + 1;
end if;
end if;
end if;
end process;
end Behavioral;
Generation of synchronization signals:
vsync <= '0' when VCounter <2 else '1';
hsync <= '0' when HCounter <96 else '1';
vga_blank <= '1' when ((HCounter> 143) and (HCounter <784) and
(VCounter> 30) and (VCounter <511)) else '0';
The size of one character is 8x16 pixels. In the character array there will be 256 cells of 128 bits each. Each bit is responsible for whether a point is in the current position or not. Location of points in the memory cell:
In FPGA there is a block memory configuration of 1024x18 bit (18432 bit), which we will focus on. CAD automatically implements almost any selected configuration using such memory blocks. So, for character memory you need 256x128 bits = 32768 bits of memory, which will take 2 memory blocks (BRAM).
The symbol memory is initialized during design and can be changed during operation.
Another necessary memory is the memory of the screen (visible area). In this memory, we encode the symbol number, symbol color, and background color. To save color memory, encode the number in the palette. Based on the configuration of the memory with a cell of 18 bits, we assign 8 bits to the symbol number, 5 bits to the symbol color, and 5 bits to the background color. Under the palette, 32 cells are allocated 24 bits each (8 bits per color).
Based on the resolution and size of the character, 80x30 characters fit on the screen. But it takes up 2400 cells (3 memory blocks) in memory. Since, from the very beginning, the task of the minimum size of this module was set, the 5 bottom lines were cut. As a result, we get 80x25 displayed characters, which occupies 2000 memory cells (2 memory blocks).
Character memory is generated in the coregenerator. The memory connection to the module is as follows:
signal SAddrA: std_logic_vector (7 downto 0);
signal SAddrRead, SAddrWrite: integer range 0 to 255;
signal SDataA, SDinA: std_logic_vector (127 downto 0);
signal SWeA: std_logic;
- ...
component vga640_symbols
port (
addr: IN std_logic_VECTOR (7 downto 0);
clk: IN std_logic;
din: IN std_logic_VECTOR (127 downto 0);
dout: OUT std_logic_VECTOR (127 downto 0);
we: IN std_logic);
end component;
- ...
begin
- ...
vga640_symbols_0: vga640_symbols
Port map (
addr => SAddrA,
clk => clk,
din => NewSymbol,
dout => SDataA,
we => SWeA
);
SWeA <= '1' when (isLowLevel = '1') and (conv_integer (addr) = 337 and iowr = '1') else '0';
SAddrA <= conv_std_logic_vector (SAddrWrite, 8) when (isLowLevel = '1') and (conv_integer (addr) = 337 and iowr = '1') else conv_std_logic_vector (SAddrRead, 8);
Screen memory is created by writing boilerplate code (although you can do the same as with character memory).
type TScreen is array (0 to 1999) of std_logic_vector (17 downto 0);
signal Screen: TScreen;
signal scrAddrA, scrAddrB: integer range 0 to 1999;
signal scrDataA, scrDataB, scrDinB: std_logic_vector (17 downto 0);
signal scrWeB: std_logic;
- ...
begin
- ...
process (clk)
begin
if rising_edge (clk) then
if scrWeB = '1' then
Screen (scrAddrB) <= scrDinB;
end if;
scrDataA <= Screen (scrAddrA);
scrDataB <= Screen (scrAddrB);
end if;
end process;
scrDinB <= scrDinBLowLevel when isLowLevel = '1' else scrDinBNormal;
scrWeb <= scrWeBLowLevel when isLowLevel = '1' else scrWebNormal;
scrAddrB <= scrAddrBLowLevel when isLowLevel = '1' else scrAddrBNormal;
scrDinBLowLevel <= data (17 downto 0);
scrWeBLowLevel <= '1' when (conv_integer (addr) = 300 or conv_integer (addr) = 309) and iowr = '1' else '0';
scrAddrBNormal <= GotoY * 80 + GotoX;
scrDiNBNormal <= BgColor & Color & data (7 downto 0);
scrWebNormal <= '1' when conv_integer (addr) = 306 and iowr = '1' else '0';
It is also necessary to set the color table of the palette, which is as follows.
type TColor is array (0 to 31) of std_logic_vector (23 downto 0);
signal PalleteColor: TColor: =
(
0 => x "000000",
1 => x "0000FF",
2 => x "00FF00",
3 => x "00FFFF",
4 => x "FF0000",
5 => x "FF00FF",
6 => x "FFFF00",
7 => x "FFFFFF",
others => x "FFFFFF"
);
signal PalleteAddr: integer range 0 to 31;
All the necessary memory is formed, now you can start implementing the preparation of a pixel for display on the screen.
Current FPGA design trends imply fully synchronous designs. We will not deviate from these trends, and we will implement a pipeline to prepare the next pixel. At the input there will be HCount and VCount counters that determine the current position on the screen, and at the output there will be the color of the pixel that needs to be displayed.
Conveyor Stages:
- Calculation of scrX, scrY signals that determine the position in the visible part of the screen.
- Calculation of signals defining a cell in the screen memory the necessary symbol
textX = scrX / 8
textY = scrY / 16
Calculation of signals defining a desired pixel symbol
SymbolX = scrX mod 8 - 1
SymbolY = scrY mod 16
where mod - remainder of the division. - Calculation of addresses on signals X, Y.
- Getting the current character.
- Getting the current pixel and color formation.
process (clk)
begin
if rising_edge (clk) then
if div = '1' then
- 1
scrX <= conv_std_logic_vector (HCounter - 139, 10);
scrY <= conv_std_logic_vector (VCounter - 70, 10);
- 2
textX <= conv_integer (scrX (9 downto 3));
textY <= conv_integer (scrY (9 downto 4));
SymbolX <= scrX (2 downto 0) - 1;
SymbolY <= scrY (3 downto 0);
- 3
scrAddrA <= textY * 80 + textX;
SymbolPoint <= conv_integer (SymbolY) * 8 + 7 - conv_integer (SymbolX);
- 4
SAddrRead <= conv_integer (scrDataA (7 downto 0));
scrData <= scrDataA;
-- 5
if (VCounter> 69) and (VCounter <470) then
if SDataA (SymbolPoint) = '1' then
curColor <= PalleteColor (conv_integer (scrData (12 downto 8)));
else
curColor <= PalleteColor (conv_integer (scrData (17 downto 13)));
end if;
else
curColor <= x "000000";
end if;
end if;
end if;
end process;
b <= curColor (23 downto 16) when (vga_blank = '1') else "00000000";
g <= curColor (15 downto 8) when (vga_blank = '1') else "00000000";
r <= curColor (7 downto 0) when (vga_blank = '1') else "00000000";
We will work with the module through the bus with the interface
- Addr (32 bit) - Address
- Data (32 bit) - data
- Iowr (1 bit) - Record (OUTPORT)
- Dout (32 bit) - response data (INPORT)
The bus is controlled by a fort processor implemented on the same FPGA. On the OUTPORT command, the Addr, Data lines are set and the Iowr line rises to “1” line. At the INPORT command, the processor sets the Addr line and collects data from the Dout line.
Example of working with a bus:
process (clk)
begin
if rising_edge (clk) then
- position the cursor at position X (GOTOX)
if conv_integer (addr) = 302 and iowr = '1' then
if conv_integer (data)> 79 then
GotoX <= 0;
else
GotoX <= conv_integer (data);
end if;
end if;
- setting the cursor to position Y (GOTOY)
if conv_integer (addr) = 303 and iowr = '1' then
if conv_integer (data)> 24 then
GotoY <= 0;
else
GotoY <= conv_integer (data);
end if;
end if;
end if;
end process;
In the same way, most of the commands for working with the module are implemented.
A complete listing of the module is available here.
Listing of the module
To initialize the
UPD symbol memory :
Basic functions
300 - write / read from the screen memory
301 - address for writing to the screen memory
302 - set the X coordinate of the carriage (GotoX)
303 - set the coordinate of the carriage (GotoY)
304 - set the color of the symbol ( SetColor)
305 - set the background color (SetBgColor)
306 - character output and carriage movement (EMIT)
307 - switch mode
1 - LowLevel mode. 300, 301 functions available for direct memory operation
0 - Normal mode. Function 306 is available to set a character with a given color to a given position and move the carriage.
308 - CR
309 - VGA_SetData (A ++)
Cursor Parameters
310 - set cursor parameters.
0 bit - lower bar in the cursor
1 bit - black square
2 bit - blinking
3 bit - vertical bar
311 - setting bit No. 0 in the cursor parameters
...
314 - setting bit No. 3 in the cursor parameters
320 - setting the color of the cursor
321 - setting the blinking time cursor
322 - setting the time of the burning cursor
Working with the palette
330 - Setting the address of the color palette
331 - Record the color at the set address
Working with the symbol table
335 - Setting the address in the symbol table
337 - Writing NewSymbol to the specified address
340 - Setting the lower 32 bits of the NewSymbol signal (NewSymbol (31: 0))
341 - Setting the next 32 bits of NewSymbol (63:32)
342 - NewSymbol ( 95:64)
343 - NewSymbol (127: 96)
Thank you for your attention.