Random Quote Board

Understanding the LCD Display: Programming with the Arduino, Part 2

Gary Schafer, June 2024

My first post covering programming a LCD screen with an Arduino really did not talk about the actual programming. It merely covered how using an Arduino program could more quickly and easily allow you to set up a LCD screen. This time, I'm going to actually cover programming. Plus, I'm going to provide a header file for Arduino programming that allows you to send long text strings to the LCD display. The header file provides several functions that allow you to parse the text into chunks, and send each chunk individually to the LCD screen and have it displayed over several lines. As part of the parsing, it also takes into account word wrapping, so that words are not split between lines.

LCD Review

Here's a quick review of the LCD and, more specifically, the Hitachi HD44780U chip that drives it. The LCD comes with 16 pins.

  1. GND: This pin should be connected directly to ground.
  2. Vcc: the voltage that drives the LCD. This is typically +5 VDC.
  3. Contrast adjustment: This pin should get a variable voltage, typically provided by the center pin of an adjustable potentiometer. By adjusting this voltage, you can adjust the contrast of the screen itself.
  4. RS (Register Select): The LCD has two states. These are instructions and data. This pin tells the system whether its for instructions (the level is set to 0 or ground), or for data (the level is set to 1 or +5 volts).
  5. RW (Read / Write): When this is set low (0 / ground), the system is set to write data to the LCD. When this is set high (1 or +5 V), then it is set to read from the LCD.
  6. EN (Enable): This pin has a special purpose. Think of this as the traffic cop. The LCD doesn't do anything unless and until this pin has first been set HIGH (+5 VDC), then back LOW (ground). This up / down cycle is like the traffic cop waving his hands and blowing his whistle for traffic to start moving. When this pin is cycled HIGH / LOW, it looks at pins RS, RW and DB0 - DB7. It then uses their binary values (either HIGH or LOW) to perform whatever has been ordered. In general, if the RS pin has been set LOW (0 or ground), then the pins DB0 - DB7 are considered instructions. If the RS pin is set HIGH (1, or +5 volts), then DB0 - DB7 are considered data. This is followed by the RW pin. If it's set LOW (0 or ground), then the pins DB0 - DB7 are considered data that is to be written to the LCD; otherwise, those same pins are to read from the LCD.
  7. DB0: Data pin. This and all of the other data pins should be set either to ground (LOW or 0) or +5 volts DC (HIGH or 1).
  8. DB1: Data pin.
  9. DB2: Data pin.
  10. DB3: Data pin.
  11. DB4: Data pin.
  12. DB5: Data pin.
  13. DB6: Data pin.
  14. DB7: Data pin.
  15. LED+: This is the positive voltage for the backlight of the LCD.
  16. LED-: This is the ground for the backlight of the LCD.

The HD44780-based LCDs can be programmed either using 4 or 8 pins. Programming with 4 pins would use DB4 - DB7, while programming with 8 pins would use all pins from DB0 - DB7. In my first post on Arduino programming, I looked at programming using only 4 data pins (DB7 - DB4, i.e. the higher order bits). My current programming assumes that all 8 data pins (DB0 - DB7) are in use. This includes the functions in the header file; they, too, assume all 8 pins are used.

Basic Assumptions

The HD44780U chipset, the basis for almost every small LCD screen such as this one, has a lot of capability. You can write text as 1-line or 2-line, fonts can be 5x8 or 5x10 pixels, the display can shift everything left or right, and so on. For this setup, I'm going to make the following assumptions:

The Arduino Setup

For this effort, I'm again using the Arduino Uno. That's because it comes with an attached breadboard that makes wiring up the LCD and accompanying circuitry that much easier. Unlike my first post, I'm also going to keep the pins from the Arduino to the LCD streamlined. The connections are as follows:

Arduino PinLCD Pin
2Register Select (RS)
3Read / Write (RW)
4Enable (EN)
5Data Bus 0 (DB0)
6Data Bus 1 (DB1)
7Data Bus 2 (DB2)
8Data Bus 3 (DB3)
9Data Bus 4 (DB4)
10Data Bus 5 (DB5)
11Data Bus 6 (DB6)
12Data Bus 7 (DB7)

My header file assumes that the connections between the Arduino and the LCD will be in the order shown in the table above (RS, RW, EN, DB0 - dB7). Further, they assume that the RS (register select) connection will be the lowest numbered one. If you read through the Arduino "LiquidCrystal" library, figuring out the connections between the Arduino and the LCD screen is one of the biggest problems. Given that there are 11 connections , that gives 11! (11 factorial) possible combinations, which is just shy of 40 million. It makes sense to simplify.

Setup of LCD (HM2004A, in front) and Arduino Uno (rear). This is a 20 character x 4 line display.
Sideview of LCD (right) and Arduino Uno (left). The connections between the Uno and the LCD screen are between pins 2 - 12 (Uno) and RS, RW, EN, DB0 - DB7 (LCD), respectively. Also note the 10 kΩ resistors between ground and the LCD screen. These ensure that, when the Uno sets the value to 0, any stray currents will be shunted to ground.
This shows the connections of the Arduino Uno going to the LCD screen.
This is the circuit with a 16 character x 2 line LCD. Other than some minor adjustments to the programming code, there are no other changes required between this LCD and a 20 character x 4 line display.

Programming the Arduino

Programming the LCD requires binary commands. Programming the Arduino is similar to programming in C code, but with several, specific commands unique to the Arduino. For example, the Arduino has the digitalWrite statement. This sets a specific pin to either a HIGH level (+5 V) or LOW (ground). That's what makes the Arduino perfect for programming the LCD.

For example, performing a "Function Set" to set the LCD to 8-bit transfers (control and data), 2 lines, and 5x8 character fonts would be a set of digitalWrite statements, along with a single delay statement, as shown below.

digitalWrite(rs,0); // \ digitalWrite(rw,0); // \ digitalWrite(d7,0); // -> These bits tell the LCD // / this is a "Function Set". digitalWrite(d6,0); // / digitalWrite(d5,1); // / digitalWrite(d4,1); // Set to 8-bit control and data. digitalWrite(d3,1); // Set to two lines. digitalWrite(d2,0); // Set to 5x8 dot characters. digitalWrite(d1,0); // This setting is irrelevant. digitalWrite(d0,0); // This setting is irrelevant. digitalWrite(en,1); delay(dly); digitalWrite(en,0);

There are 13 lines in this code. The first line (RS) tells the LCD that this will be an instruction, meaning that the LCD is being set for some condition. The second line (RW) tells the system it will be written to, meaning data will be coming into the LCD and not read out. The next, three pins (D7 - D5), tell the system that this is the "Function Set" command. This means it will know how to interpret the pins D4 - D2. The last, two data lines (D1 - D0) are irrelevant. They can be either 1 or 0 and it won't matter. The last three lines tell the LCD to read those 10 lines (RS, RW, D7 - D0) into the LCD. It does this by setting the ENABLE line high, waiting "dly" number of milliseconds, then setting it LOW.

The LCD Character Set

These LCDs, both the 2-line and 4-line, are based on the Hitachi HD44780U chipset. When you want to display a certain character, there are two ways to do it. The first, and most common, is to send a number to this chip that maps to a certain character. This uses the built-in ROM to display characters. The Hitachi chipset comes with two character ROMs. These are the A00 (Japanese standard font) and the A02 (European standard font). Strangely, all of the LCDs I have are the A00 (Japanese) version.

Table showing codes for ROM A00. This the only ROM that has come with my various (2-line and 4-line) LCDs. Source: HD44780U Dot Matrix Liquid Crystal Display Controller / Driver, Rev. 0.0, Hitachi, 1999.
Table showing codes for ROM A02. This has more characters than the A00, but is not easily available for LCDs from any of the sources I've checked. Source: HD44780U Dot Matrix Liquid Crystal Display Controller / Driver, Rev. 0.0, Hitachi, 1999.

The A00 has fewer characters available than the A02. Regardless, to get a character, you send a binary number to it. For example, if you want to display the uppercase letter "B", you send binary numeric equivalent of decimal 66. A lowercase "j" would be 106. Both type of ROM (A00 and A02) have several characters in common, from 32 (the space character) to 125 (which appears to be a character that has all of the 5x8 dots turned on). These are also the same values as for ASCII characters. This makes it easier to write text to the screen.

The second way to display characters is to make them yourself. This involves using the "Character Generator Random Access Memory" or "CGRAM". I'll cover that in my next post. Promise.[1]

Overview of the Problem: Writing Text

What I want to do is to write a program that will take a long string of text and do the following:

  1. Parse out the words of the string so that they will fit on one line of the LCD without going off the end of the screen, and also not have any word split between lines.
  2. Write the lines of text to the screen.
  3. Pause when the bottom line is written so that a reader will have time to read the text.
  4. Clear the screen.
  5. Go back to Step #1 and continue on the string of text. If the end of the string is reached, write the last line and stop.

The Text

The text I started with was this long string:

Hello, my name is Gary. I'm programming this Arduino to write text to a small LCD screen. In order for this to work, I have to figure out how to make it properly display over several lines, with proper word wrapping. We'll see how it goes.

It's 239 characters making up 46 words divided into four sentences. My plan is to create reusable functions that will perform the subtasks outlined above. The functions perform the following:

  1. Find the "space" character (ASCII decimal value 32) that comes before the end of a single line, and note its position. The lines are either 20 characters (for the 4x20 LCD screens) or 16 characters (for the 2x16 LCD screens). For a 4x20 screen, we want the space that comes just before the 20th character. In the text above, the first space would be between the "is" (fourth word) and "Gary" (fifth word).
  2. Pull out every character that came before that space. If this is after a line has already been pulled, then use the space from the previous line as the starting point. Regardless, every character gets placed into another, smaller string.
  3. Write that string to the screen as a single line. Increment the line number. If the line number is now 5 (for the 4-line screen) or 3 (for the 2-line screen), pause, clear the screen, and start over at the top.
  4. Once the end of the string is reached, write the last characters to the screen and stop.

Finding the End of the Line

The first step is to identify enough words that will fill up a line on the LCD screen, but not have any word split between lines. This means identifying the space just before the end of the line. But how to do this? The easiest way would be to simply copy all of the text to a function, find the space, and return that value. But given the paltry amount of memory in the Arduino Uno (my test board), that didn't sound like a good idea. Instead, I decided to bite the bullet and use pointers.

Why a pointer? Because I'm using a function. A function will not know any of the variables outside of itself. If I passed a variable to the function, it would have to be a copy of the entire string. That would be a fair amount of memory, especially with the Uno, which does not have a lot of memory to begin with. A function, however, can still access the memory of the device it is on. A pointer is just a memory address. So, by passing a pointer (memory address), I'm saying, "Here's the variable itself, not just a copy."

Quick review: A string is an array. An array, in turn, is a collection of numbers. (Yes, I realize that we're talking a string, which is letters, but they're represented in the computer by numbers. So there.) Computer progams keep arrays in memory as a collection of consecutive memory bins. For example, if I have the word "Hello" as a string, that's six numbers. Five numbers represent the letters; the sixth is the null character that marks the end of the string. If the memory address of the first letter, "H", is 804 (decimal value), then the remaining characters will be 805 ("e"), 806 ("l"), 807 ("l"), 808 ("o") and 809 (null character). If you want to pass an array to a function, you don't need to pass all of the array. Just pass the memory address of the first array character, and all of the rest will be available by adding to that number. This is perfect for passing the memory address (a pointer) of the first character of the string.

int lastSpace(byte *myText, int startPt, int lineLength) { // This function will return the value in the text array of the // that is the last space before the end of the line. // *myText = address of first character in text string // startPt = first character in text string to place on LCD // lineLength = length of one line of LCD. Should be 16 // for 2x16 screens, and 20 for 4x20 screens. int sp=myText; // Copy the "myText" address value into "sp" to // give an initial value for the final location // of the last space before the end point. int initPt=myText; // This tracks the initial point and is used // by the "while" loop. // The "while" loop tracks the address value of "myText". // The loop continues until it finds the last space before // the "lineLength" has been reached. while(myText<(initPt+startPt+lineLength)) { // Increment the "myText" memory address in order to // cycle through all of the characters of the text string. myText++; // Check whether you've reached the end of the text. // This is done by checking whether the character // is the null character (ASCII value = 0) if(char(*myText)==0) { sp=myText; break; } // Value 32 is the ASCII decimal value for the space character // If it finds this value, that's a space. The "sp" variable // is given the current counter value. if(char(*myText)==32) { sp=myText; } } // This returns the length of the amount of text between // the "startPt" and a space that is less than the // startPt+lineLength. return (sp-initPt); }

Passing the beginning of the text to this routine will have it start going through each character. Every time it finds a space, it copies the memory address of that character into a temporary variable. When it reaches the end of a line (determined by whether you're working with a 20-character LCD or a 16-character LCD), the function calculates the difference between the memory address passed into the function, and the memory address of the last space it found. This difference is returned from the function.

For example, looking at my text and assuming a 4x20 character LCD screen, the result would be that shown below. The first line is the memory address of each character. (NOTE: Your addresses may be different. This is just an example.) The second line is the actual characters in that address. The last line is the result of the function looking to determine where the spaces are located. Each "^" is a "space" character, with the last one showing "^^^" as that is the address that will be the last space before the loop ends. It takes this address (821, in this example), subtracts the initial memory address (804), and returns the difference (821 - 804 = 17). Thus, the space is the 17th character from the starting point (the very beginning of this string, in this example).

|804|805|806|807|808|809|810|811|812|813|814|815|816|817|818|819|820|821|822|823| | H | e | l | l | o | , | | m | y | | n | a | m | e | | i | s | | G | a | | | | | | | | ^ | | | ^ | | | | | ^ | | |^^^| | |

Pulling Out the Text

Now that we know how many characters to pull out, let's create a function that will pull out that number of characters.

String pullText(byte *myText, int startPt, int endPt) { // This function will pull out a string of text from // a larger string. It does this by using the memory // address of the very first character of the larger // string, adding an offset (startPt), and pulling out // all characters between this first offset and a // second offset (endPt). // *myText = pointer for first character of text string. // This is actually the value of the variable. // startPt = first character to write to the LCD. The first // position is 0. // endPt = Value of the last character to pull from the // main text string. // NOTE 1: The length of the string pulled will be // endPt - startPt. // NOTE 2: This function assumes that the input text will not // end before the value endPt is reached. // Set the starting memory address to the memory address // of the first character of the overall string, plus // the value of the starting point. myText=myText+startPt; String myString = ""; // Pull out the characters from the large string and add // them to the variable "myString". This is the string that // will be returned at the completion of this function. for (int i=0; i<(endPt-startPt); i++) { myString += char(*myText); myText++; } return myString; }

Using my original text as an example along with a 4x20 screen, the value returned from the "lastSpace" function was 17. This means that the last space before the end of the line is 17 characters from the start. Sending the pointer of the first character of the long string to this function, "pullText", along with this value, 17, this function will send back 17 characters as one string. In this case, it would be "Hello, my name is". NOW we're ready to actually put that on the screen itself.

Writing to the LCD Screen

We now have a short bit of text, a small string that will fit on one line of our LCD screen. We need to write it to the screen. For this, I created a third function called "lcdText". This function is as follows:

void lcdText(String myText, int lineNum, int colNum, int startPin) { // This function takes in a bit of text and writes it to a 2-line // or 4-line LCD. // This function uses the following input values: // myText = the text to be written to the LCD. // lineNum = the value of the line to be written to (1 - 4) // colNum = cursor position (1 - 20) // startPin = the value of the RS pin, and assumes that the pins // are then consecutively numbered as RS, RW, EN, DB0 // - DB7. // Pins from the Arduino to the LCD are numbered starting with // "startPin", corresponding to the RS pin, and increment in // value with the following order: RS, RW, EN, DB0 - DB7. Thus, // if the RS pin is pin 2, then RW will be pin 3, EN will be // pin 4, DB0 will be pin 5, up through DB7, which will be pin // 12. int rs=startPin; // Pin used for Register Select int rw=startPin+1; // Pin used for read-write select int en=startPin+2; // Pin used for enable int d0=startPin+3; // Pin used for Data 0 pin int d1=startPin+4; // " " " " 1 pin int d2=startPin+5; // " " " " 2 pin int d3=startPin+6; // " " " " 3 pin int d4=startPin+7; // " " " " 4 pin int d5=startPin+8; // " " " " 5 pin int d6=startPin+9; // " " " " 6 pin int d7=startPin+10; // " " " " 7 pin // Set the position of the cursor on the display // based on the input values. Start with the line // number and set the position based on that. The // value for the position is based on the decimal // values of the LCD. // Line 1: Column numbers = 128 ... 143 (2x16 LCD) / 147 (4x20 LCD) // Line 2: Column numbers = 192 ... 207 (2x16 LCD) / 211 (4x20 LCD) // Line 3: Column numbers = 148 ... 167 (4x20 LCD) // Line 4: Column numbers = 212 ... 231 (4x20 LCD) if(lineNum==1) { // Line #1 colNum=colNum+127; } if(lineNum==2) { // Line #2 colNum=colNum+191; } if(lineNum==3) { // Line #3 colNum=colNum+147; } if(lineNum==4) { // Line #4 colNum=colNum+211; } // Set the cursor position on the screen // Setting both the RS and RW to 0 means // that the LCD will use the settings in // DB0 - DB7 as instructions to the LCD. // Tells the LCD this is an instruction. digitalWrite(rs,0); // Set the LCD to be written to. digitalWrite(rw,0); // The next set of bits are the instructions. // The "bitRead" statement allows you // to create a simple decimal number, then // pull out the bit for each position // of the binary number. digitalWrite(d7,bitRead(colNum,7)); digitalWrite(d6,bitRead(colNum,6)); digitalWrite(d5,bitRead(colNum,5)); digitalWrite(d4,bitRead(colNum,4)); digitalWrite(d3,bitRead(colNum,3)); digitalWrite(d2,bitRead(colNum,2)); digitalWrite(d1,bitRead(colNum,1)); digitalWrite(d0,bitRead(colNum,0)); //***************** digitalWrite(en,1); delay(2); digitalWrite(en,0); int textLen=myText.length(); // calculate the number of characters // to print to the LCD display int ascVal=0; // ASCII value of single character of text for (int i=0; i<textLen; i++) { ascVal=(int)myText[i]; // Pull out the ASCII value of a // single character of text. // Write the character to the LCD screen // Set RS pin HIGH for writing to the screen. digitalWrite(rs,1); // Set RW pin LOW for writing to the screen. digitalWrite(rw,0); // Write bit 7 of chracter byte. digitalWrite(d7,bitRead(ascVal,7)); // Write bit 6 of chracter byte. digitalWrite(d6,bitRead(ascVal,6)); // Write bit 5 of chracter byte. digitalWrite(d5,bitRead(ascVal,5)); // Write bit 4 of chracter byte. digitalWrite(d4,bitRead(ascVal,4)); // Write bit 3 of chracter byte. digitalWrite(d3,bitRead(ascVal,3)); // Write bit 2 of chracter byte. digitalWrite(d2,bitRead(ascVal,2)); // Write bit 1 of chracter byte. digitalWrite(d1,bitRead(ascVal,1)); // Write bit 0 of chracter byte. digitalWrite(d0,bitRead(ascVal,0)); digitalWrite(en,1); delay(6); digitalWrite(en,0); } return; }

This function can be used with any size LCD (2x16 or 4x20), so long as the text fits onto one line.

The Code to Call the Functions

The last part is to write some code to actually use these functions. So I wrote a program in standard Arduino code (a setup() function and loop() function). I made sure the header file was in the same folder as this code. Once I loaded the code onto the Arduino, the result for both the 2x16 LCD and 4x20 LCD can be seen below.


I wrote these functions for the specific purpose of being able to send text to the screen of a small LCD. However, there might be other uses for them, ones that I cannot imagine. I'll leave it up to others to figure that out.

See you in the next post!


[1]: I was actually in the process of including CGRAM in this post when I realized it was consuming all of my time. Better to leave it to its own post.

Setting Up the HM2004A Liquid Crystal Display

Gary Schafer, June 2024

A bit of a change-up from my usual lineup. This will not be a post discussing either software defined radios (SDR) or their accompanying software. I'm working on a post on capturing Russian Meteor M2 satellite signals, but that's still a work-in-progress. Instead, this post will hark back almost ten years to some posts I did in November 2014 talking about the LCM1602C LCD.

This post will cover a slightly different display, the HM2004A. This LCD is a 4-line x 20-character display. The LCM1602C is a 2-line x 16 character display. There is an important difference between the two. Further, there's a "Duh!" moment that I want to share.

The HM2004A LCD

The HM2004A liquid crystal display (LCD) is sold by Adafruit as well as several other entities. I bought these several years ago and have been playing around with them (a bit), or letting them sit on the shelf (a lot) while I played around with my SDRs instead. They're a 4 line, 20 character display using white text on a blue background. They're actually very nice LCD displays.

Top view of the HM2004A liquid crystal display
Bottom view of the HM2004A liquid crystal display

Again, this is 4-line x 20-character display, as shown below.

This is the display active, but with the contrast set so that all characters are visible. This is not how the contrast would be set if the LCD was actually being used.

Setting the Display Address

The text is displayed based on the display data random access memory (DD RAM). This is an 8-bit hex value, with the MSB set to "1", that sets where the characters will be placed on the display. Again, this display has 4 lines with 20 characters per line. The DD RAM sets whether you will start your text on the first line at the left-most character, or the third line near the center, or wherever. I cannot find a decent document covering how to set the address for the HM2004A. The closest I found was for the TC2004A LCD as offered by Adafruit.com.

DD RAM addressing map, in hexadecimal values, for the TC2004A-01 LCD. Note how the addresses for the first and third lines are the same. This doesn't work for the HM2004A LCD.

The HM2004A doesn't have the map addressing as shown above. Note how the first and third line addresses are the same, as are the second and fourth line addresses. I decided to use "trial and error". Based on that, I've been able to figure out the addresses, which are as follows:

Display addressing, in hexadecimal values, for the HM2004A LCD. This was based on trial-and-error of the possible address space. Note how the first line continues onto the third, while the second line continues onto the fourth.

A couple of points. The first line continues onto the third, while the second line continues onto the fourth. The first address starts with "0x80" (hex value of 80, or a decimal value of 128). That's because, in order to set the address, this highest-order bit (DB7) must be set to "1". The values of the lowest order bits (DB3 - DB0) then go from 0000 to 1111 (0x0 to 0xF). Then the highest order bits are set to 0x9, and the lowest order bits are again run from 0x0 to 0xF. When the number increments from 0x93 to 0x94, it jumps from the end of line one down to the beginning of line 3. From there, the next increment of the higher order bits is 0xA. But the lower order bits only go up to 0x7.

Going back to the second line, the higher order bits don't start at 0xB. Rather, they start at 0xC. For lines 2 and 4, the higher order bits go from 0xC to 0xE. And, again, the lower order bits for the last value (0xE) stop at 0x7.

Which leads to the issue of "How does the system know where to place the characters if there is overflow from one line to the next?" That's a problem. It doesn't. If you write to this LCD and the characters go past the end of, say, the first line, the overflow will go down to the third line. And once they get to the end of the third line, the overflow jumps back to the beginning of the second line.

This is a demonstration of starting of writing characters at the top, left corner (address 0x80), then continuing to write over 2 1/2 lines. The overflow of the first line drops down to the beginning of the third line. And the overflow of the third line jumps back up to the second line.

So, yeah. If you're using this LCD, your software has to account for that. I imagine that the Arduino (which is what I'm using to drive the LCD here) has a library that accounts for that. I don't know. I'm not using the library because I want to ensure I understand how the LCD works before I use any "automagic" software library. That's why my Arduino programming is still at the stage of setting each line separately. (Yes, the file is quite long with a lot of "digitalWrite" statements.)

I'm aiming to use this experience to create my own functions that will do two things. First, I want to be able to continuously write text but properly go consecutively from lines 1 - 4, and not 1 to 3, 3 to 2, then 2 to 4. Second, I want to be able to do automatically word wrapping so that a word won't be split between two lines.

Setting for Four Lines

There's also the problem of the setting the function of the display for four lines. It doesn't exist. You only have the options of one or two lines. If you perform a "function set" with DB3 set to "0" (LOW), this will set the display to just one line. This will not allow you to use lines 2 or 4. You will only be able to access lines 1 and 3. If you set DB3 to "1" (HIGH), this will give you access to all of the lines, though only in the manner prescribed above.

Back to Basics: Pull-Down Resistors

When I was an engineering undergraduate, I had to build a digital circuit. The class TA told us students, quite emphatically, to make sure that any circuits that were set between HIGH and LOW (1 and 0, respectively) needed to have either pull-up or pull-down resistors connected. A pull-up resistor is a relatively high impedance (1 kΩ or higher) resistor connected between that point and the power supply voltage (say, +9 VDC). A pull-down resistor is the same, but connected to ground.

Well, me being the idiot that I can be sometimes, I didn't add these. They didn't seem to be necessary, and the circuit seemed to be working just fine without them.

Until I had to go to the TA's office to show that my circuit was working.

Yes, at that point, it didn't work as it did before. Why? Well, without those pull-up / pull-down paths, the currents could be affected by stray currents pretty much from anywhere. The TA knew what the problem was immediately. And I'll never forget the look he gave me. The best I can describe it is as "intense annoyance".

You'd think I would have learned.

This afternoon, as I was working on determining the address mapping of the HM2004A, I discovered that the system wasn't acting consistently. Sometimes the exact, same program loaded three different times would produce three, different results. That's when the face of my old TA came back to haunt remind me, "Put in some pull-down resistors." Soooo, I added 10 kΩ resistors between the RS, RW, EN, and DB7 to DB0 connections and ground. This ensured that, when the values for those various connections were set to LOW (0), if there was any stray voltage, it would be safely shunted to ground. Further, when set to HIGH (1), the resistors would have enough resistance that the current from the supply voltage to ground would be low (500 uA each, in this case). As soon as I installed those resistors, lo and behold, the circuit started working properly and consistently.

My TA would be proud. Or maybe intensely annoyed again. Let's go with both.

Here's a Random Fact...