Jeremy Kierstead
kierstea@ee.ualberta.ca
Dave McMillan
dmcmilla@gpu.srv.ualberta.ca
The purpose of this documentation is to provide a relatively simple approach of writing data to an LCD using VHDL. To make things even simpler (and hence, of course, minimize the pain and anguish), sample source code will also be provided. The code will work for an Emerging Technologies Display Corporation LCD Model ED(C) 16210 (16x2) when a system clock frequency of 1 kHz is used. However, the code can be tweaked to accomodate different clock frequencies. It should also be noted that the code should also work for a Sharp 16x2 LCD, as the instruction set is identical to that of the Emerging Technologies' LCD, but there may be some differences in regards to timing, character addresses, etc.
Even though this code will work for a 16x2 display, that does not imply that the code uses both lines to display messages. Actually, it only uses the first line. The reasons for this will be discussed later. Likewise, even though the code doesn't use the second line at all does not mean that it can easily be ported over to a one line display. The reasons behind this are basically the same as the reasons why the second line was not used.
It is still the responsibility of anyone who uses this code to verify that the code properly simulates and meets all timing and functional requirements before programming the FPGA.
As previously mentioned, the method used in this design to write to an LCD
is relatively simple and straightforward. It's not the smartest method by
far, and it might not even be the most efficient method; however, believe
it or not, it does work, in a quick and dirty sort of way. The main idea
is as follows:
1. Home the cursor. 2. Write the next character to the LCD using the 8-bit data bus. 3. Increment the cursor. 4. Repeat steps 2 and 3 until entire message is displayed. 5. Repeat steps 1 through 4 indefinitely.Of course, this is an oversimplified description. In actuality, there may be more than one message that needs to be displayed, and the correct message needs to be selected. Furthermore, there are three additional signals on the LCD that has to be properly set for data transfer. And finally, LCD delays in instruction processing, as well as other timing delays that might exist, need to be taken into account. But other than that...
Okay, this is pretty basic stuff. However, it does serve as a good starting point for the discussions that are to follow.
The entity can be declared as follows:
entity LCD is port( clock: in std_logic; reset: in std_logic; lcd_data: out std_logic_vector( 0 to 7 ); lcd_select: out std_logic; lcd_rw: out std_logic; lcd_enable: out std_logic; message: in std_logic_vector( 0 to 3 ) ); end LCD;The clock signal refers to the system clock. Likewise, the reset signal refers to the system reset.
The four message signals provide a method of selecting the desired message to be displayed from sixteen possible different messages. Of course, this can be tweaked to provide two message signals rather than four, etc.
The eight lcd_data signals refer to the eight data lines that interfaces directly with the LCD. However, in order to save I/O pins, some LCDs do give the option for 4-bit interfacing, rather than 8-bit. However, it is recommended that 8-bit transfer be used if possible because it is easier to deal with, and it results in a faster refresh for the LCD.
The lcd_select signal refers to the LCD's register select signal, which is used to select either the data register (used for writing a character to the display, or reading a character from the display) or the instruction register (used for writing an instruction to the LCD). This signal must be high when selecting the data register, and low when selecting the instruction register.
The lcd_rw signal must be high when reading a character from the LCD display, and low when writing a character to the LCD display. In the sample code, data is never read from the display, so lcd_rw is always assigned a logic value of zero. This signal could actually be tied to ground, without any intervention of the controller - it really makes no difference except for a gain of one I/O pin, but that's about all.
For the Emerging Technologies' LCD, a falling-edge (high to low transition) lcd_enable signal allows the LCD to process the next instruction.
It should be noted that for the vectored signals, "0 to n" was used (as opposed to "n downto 0") because it was the method that *worked*. Otherwise, it was found that the bits were completely reversed after the design was compiled using actmapw and simulated. And we weren't the only ones with such wonderful problems...
Well, that's enough for the general high-level discussion. On with the code!
library ieee; use ieee.std_logic_1164.all; entity LCD is port( clock: in std_logic; reset: in std_logic; lcd_data: out std_logic_vector( 0 to 7 ); lcd_select: out std_logic; lcd_rw: out std_logic; lcd_enable: out std_logic; message: in std_logic_vector( 0 to 3 ) ); end LCD; -- Architecture Description architecture Display of LCD is subtype charcode is std_logic_vector( 0 to 7 ); -- THE HEAP O' CONSTANTS -- -- These are 8-bit patterns which may or may not be used in interfacing -- to the LCD 8-bit data bus. They are here to improve readability in the -- code, which I suppose is the idea of constants in the first place. Oh, -- and to improve maintainability, but this really only applies to the -- CLOCK_COUNT constant, as the other constants are pretty well set in -- stone, unless if another type of LCD is being used. -- Clear screen. constant CLR: charcode := "00000001"; -- Display ON, with cursor. constant DON: charcode := "00001110"; -- Set Entry Mode to increment cursor automatically after each character -- is displayed. constant SEM: charcode := "00000110"; -- Home cursor constant HOME: charcode := "00000010"; -- Function set for 8-bit data transfer and 2-line display constant SET: charcode := "00111000"; -- The alphabet - UPPER CASE only. constant A: charcode := "01000001"; constant B: charcode := "01000010"; constant C: charcode := "01000011"; constant D: charcode := "01000100"; constant E: charcode := "01000101"; constant F: charcode := "01000110"; constant G: charcode := "01000111"; constant H: charcode := "01001000"; constant I: charcode := "01001001"; constant J: charcode := "01001010"; constant K: charcode := "01001011"; constant L: charcode := "01001100"; constant M: charcode := "01001101"; constant N: charcode := "01001110"; constant O: charcode := "01001111"; constant P: charcode := "01010000"; constant Q: charcode := "01010001"; constant R: charcode := "01010010"; constant S: charcode := "01010011"; constant T: charcode := "01010100"; constant U: charcode := "01010101"; constant V: charcode := "01010110"; constant W: charcode := "01010111"; constant X: charcode := "01011000"; constant Y: charcode := "01011001"; constant Z: charcode := "01011010"; -- Some potentially useful puctuation. constant SP: charcode := "00100000"; -- Space constant BRL: charcode := "00101000"; -- Left Bracket constant BRR: charcode := "00101001"; -- Right Bracket constant DASH: charcode := "00101101"; -- Dash, as in hypen constant COLON: charcode := "00111010"; -- Colon: : constant APO: charcode := "00100111"; -- Apostrophe -- Assuming a clock cycle of 1 kHz, 2 clock cycles should be enough to -- ensure that the instruction sent to the LCD has enough time to be -- processed (max process time is 1.64 ms). This number is therefore used -- to set the upper limit of the counter. constant CLOCK_COUNT: integer := 1; -- Will be used to store the current message to be sent to the LCD. This -- includes the HOME instruction to home the cursor for the next message. -- -- NOTE: 15 characters plus HOME instruction provides a substantial -- benefit over using 16 characters (plus HOME instruction) because of the -- reduced number of modules used, as well as the shorter delay path that -- results. type lcd_char is array( 0 to 15 ) of charcode; signal lcd_ch: lcd_char; signal ch: charcode; begin -- This process simply selects the next message depending on the -- value of the message signals. -- -- Note that an instruction exists at the beginning of each -- message. The HOME instruction homes the cursor so that each -- written message starts at the beginning of the LCD display. load: process( message ) begin case message is when "0000" | "1000" => lcd_ch( 0 ) <= HOME; lcd_ch( 1 ) <= W; lcd_ch( 2 ) <= H; lcd_ch( 3 ) <= A; lcd_ch( 4 ) <= T; lcd_ch( 5 ) <= E; lcd_ch( 6 ) <= V; lcd_ch( 7 ) <= E; lcd_ch( 8 ) <= R; lcd_ch( 9 ) <= SP; lcd_ch( 10 ) <= SP; lcd_ch( 11 ) <= SP; lcd_ch( 12 ) <= SP; lcd_ch( 13 ) <= SP; lcd_ch( 14 ) <= SP; lcd_ch( 15 ) <= SP; when "0001" => lcd_ch( 0 ) <= HOME; lcd_ch( 1 ) <= H; lcd_ch( 2 ) <= E; lcd_ch( 3 ) <= R; lcd_ch( 4 ) <= E; lcd_ch( 5 ) <= APO; lcd_ch( 6 ) <= S; lcd_ch( 7 ) <= SP; lcd_ch( 8 ) <= A; lcd_ch( 9 ) <= N; lcd_ch( 10 ) <= O; lcd_ch( 11 ) <= T; lcd_ch( 12 ) <= H; lcd_ch( 13 ) <= E; lcd_ch( 14 ) <= R; lcd_ch( 15 ) <= SP; -- "0010" is being used a reset message. This -- message MUST be selected first in order to -- initialize the LCD. when "0010" => lcd_ch( 0 ) <= SET; lcd_ch( 1 ) <= DON; lcd_ch( 2 ) <= SEM; lcd_ch( 3 ) <= CLR; lcd_ch( 4 ) <= SP; lcd_ch( 5 ) <= SP; lcd_ch( 6 ) <= SP; lcd_ch( 7 ) <= SP; lcd_ch( 8 ) <= SP; lcd_ch( 9 ) <= SP; lcd_ch( 10 ) <= SP; lcd_ch( 11 ) <= SP; lcd_ch( 12 ) <= SP; lcd_ch( 13 ) <= SP; lcd_ch( 14 ) <= SP; lcd_ch( 15 ) <= SP; -- You get the idea. when others => lcd_ch( 0 ) <= HOME; lcd_ch( 1 ) <= SP; lcd_ch( 2 ) <= SP; lcd_ch( 3 ) <= SP; lcd_ch( 4 ) <= SP; lcd_ch( 5 ) <= SP; lcd_ch( 6 ) <= SP; lcd_ch( 7 ) <= SP; lcd_ch( 8 ) <= SP; lcd_ch( 9 ) <= SP; lcd_ch( 10 ) <= SP; lcd_ch( 11 ) <= SP; lcd_ch( 12 ) <= SP; lcd_ch( 13 ) <= SP; lcd_ch( 14 ) <= SP; lcd_ch( 15 ) <= SP; end case; end process load; -- This process retrieves the next character for display. This -- character is then used in the previously described -- send_instruction process. -- -- There is one interesting problem with this code. The counter -- does not reset when the status is changed. To do so would -- require a) more modules, and b) more time to get the code -- working (a first attempt worked well in initial simulation, -- but alas, Actmapw did not like it). However, with the entire -- display being refreshed about every 30 ms, the average person -- most likely would not be able to see this tiny faux-pas. And if -- they could, then it would probably make an interesting -- conversation piece, which is okay with us. get_next_char: process( clock, message, lcd_ch, reset ) variable count: integer range 0 to 15; variable time_count: integer range 0 to CLOCK_COUNT; begin if ( ( clock'event ) and ( clock = '1' ) ) then if reset = '1' then time_count := 0; count := 0; elsif time_count = CLOCK_COUNT then if count = 15 then ch <= lcd_ch( 15 ); count := 0; else ch <= lcd_ch( count ); count := count + 1; end if; time_count := 0; else time_count := time_count + 1; end if; end if; end process get_next_char; -- This process writes a byte to the lcd data bus. send_instruction: process( ch, clock, reset ) variable time_count: integer range 0 to CLOCK_COUNT; begin if ( ( clock'event ) and ( clock = '1' ) ) then if reset = '1' then time_count := 0; elsif time_count = CLOCK_COUNT then if ((ch < "00100000") or (ch ="00111000")) then -- Must be an instruction lcd_select <= '0'; else lcd_select <= '1'; end if; lcd_rw <= '0'; lcd_data <= ch; -- Falling-Edge enable. Set and cleared -- according to time_count in order to -- create a 2 ms pulse. lcd_enable <= '1'; time_count := 0; else time_count := time_count + 1; -- Falling-edge occurs. No new data being -- written, so data on bus should be valid -- data, and it should be a succesful -- write to the LCD. lcd_enable <= '0'; end if; end if; end process send_instruction; end Display;
I tried to make the code as self-documenting as possible, so it shouldn't be too hard to understand (hopefully). However, there are some necessary items that need to be pointed out, and these will be explained in the next section entitled Notes.
For us, the LCD module represented approximately 170 modules out of the 536 total modules that we used on our Actel 1020B (which has a total capacity of 547 modules). As we were able to fit our entire design onto the chip, we didn't spend much time optimizing this code, so it probably could be done fairly easily. Even so, if you're using a 1010B, you might want to consider using LEDs instead in order to make room for your primary controller.
It should be noted that in our actual implemented design, the CLR instruction was not among the initialization instructions for our initialization message. It was purposefully left out because we were left with the impression from the specification sheets that the Display On instruction fills the entire screen with the "Space" code, which would nicely clear out display before writing to it. Unfortunately, Murphy's Law came into effect, and it didn't quite work that way. As a result, we ended up covering our second line with black hockey tape to cover up the unwanted characters. An elegant and creative approach as you might imagine, but nonetheless, the addition of the CLR instruction would have eliminated the need for this wonderful innovation. As such, this version should work better than ours did by clearing the screen upon initialization.
If you plan on using a different clock speed than 1 kHz (yeah, it's slow, but our project didn't need a fast clock), you will probably need to adjust the CLOCK_COUNT constant. Keep in mind that you want to have all signals hold their values for at least 1.64 ms (or whatever the maximum time it takes your particular LCD to process an instruction). So for example, if you have a system clock set at a blistering 10 kHz, you will probably want to set your CLOCK_COUNT constant to at least 16 (17 ms process time). Etc.
If you REALLY want to optimize you LCD performance, you can note that most instructions only take about 40 us. Then, you can maybe alter the code so that it can get the next instruction in an optimal amount of time. However, our LCD still refreshed its display fairly quickly, and further optimizations may result in more time spent, and more modules.
Unfortunately, the second line of a 2-Line display does not continue from the first line at the next sequential address - it skips some addresses. Hence, you can't just increment the cursor and expect it to magically appear at the beginning of the next line - you have to set the new address of the cursor. Likewise, for a 1-Line display, the last eight characters do not continue where the first eight characters left off - you have to set the cursor address. So how do you do this? We don't know. We didn't really bother to find out. However, it WILL involve another instruction, so keep this in mind if you attempt to find out.
Even though this code should work for most standard character dot-matrix LCDs, it is still vitally important that you physically test out the actual LCD in a lab, in order to verify the instruction sequence. We probably would have had a rising-edge enable in our code if we didn't do this, and this probably would have resulted in a completely non-functional LCD. However, since we were able to verify a falling-edge enable in the lab, we were able to change the code. Two lines of code could have very well meant the difference between bitter, disappointing failure, and sweet sweet success. Hint: since we never actually used the CLR instruction, this might be a good thing to test out in the lab...
THIS IS IMPORTANT. To use this code, you must disable Actgen Macros
(switch the option to "Off" - don't use "Black Box"). Otherwise, you will
probably get an error message about halfway through compilation saying
something like:
FATAL INTERNAL ERROR - Please call your hot-line.
This documentation has been entitled "How to Write To An LCD With Minimal Pain and Anguish". Note that it has not been entitled "How to Write To An LCD With NO Pain and Anguish". Admittedly, in some respects, this code is horrific to maintain. Especially the first process block entitled "load". This code assumes that each written message is 15 characters long (plus the HOME instruction). Chances are, you may want to write only 8 characters to the LCD. Or 20 (if you have a 20 character display). Or maybe you want to actually use the second line. Or maybe you want to take advantage of the display shift features. As you can probably tell, this could potentially mean modifying virtually every single line of code in the load process. Obviously, that's a lot of code to modify. Unfortunately, this appears to be one of the "pains" of programming an LCD.
Hopefully, that is the only troublesome item that remains.