Microcontroller - A Beginners Guide - Displaying Numbers on the LCD Display
So, we know how to put characters and even strings of information to the LCD. The
most useful thing to display on the LCD would be a number. Now, I'm sure you are
thinking, well, we could just pass a number in quotes, like this: "632" using the
Send_A_String command (that we created in the
last tutorial ! Well, sure, but what if it is a number from a sensor stored
in a variable, or something like that! We actually do want the number in those quotes,
but how we get it there is where the magic comes in... and it is REALLY simple.
If we want the numbers to be displayed at the same locations every time (and not
just fill the entire screen with a list of numbers), we will need to move the cursor
to the desired locations every time we want to send the number to the LCD. This
can also be used to locate strings and characters.
The magic of moving the cursor to any position on the LCD display is in the 0x80
number. That number represents the first location on the display, at the top left
corner. If you've been paying attention to the binay tutorial, you would know that
0x80 represents 0b10000000 in binary. The "1" is in the 8th place, leaving a full
7 digits left for something wonderful. 7 binary digits can hold 128 numbers (0-127).
As you already know, LCD screens are divided up into many columns and rows. For
the LCD in the tutorial, there are 4 lines (rows) and 20 columns, hence the 20x4
that is usually shown in the description. Add them all up and you have 80 total
positions on the LCD. Hmm... 80 is less than 128. This tells us that there may be
LCDs out there that have more lines and columns, becuase there are that many possibilities
using this command.
If you sent a command using Send_A_Command and inserted 0x80 to get processed by
the LCD, the cursor position would appear at the top left. If you inserted 0x8A,
then the cursor would be positioned 10 spaces right of the top left corner. It would
actually be in the middle, since the LCD is 20 columns wide. But there is a catch!
One would think that if this number kept increasing, the cursor would wrap to the
next line, but it doesn't. The cursor wraps to the third line instead. This is due
to how it uses the 128 reserved spaces. Because of the various LCD displays out
there with different line and column counts, the first line will start from "0"
and fill lines 1 and three up to number 63, which is 1 minum half of 128. Now it
is starting to come together, isn't it. The 2nd and fourth line start from 64 and
go on towards the end. So half of the 128 is reserved for the first and third line,
and the other half is reserved for the second and fourth line. That way, all LCD
will start from 0 on the first line, and start from 64 on the second line, even
on 2 line LCDs.
This makes great sense, because if you have an LCD with only two lines that were
64 columns long (using all of the 128 reserved positions), then the LCD would still
be compatible with an LCD with 2 lines but with only 20 or so columns. You could
expect to see the same information on the LCD, at least as far as the selected LCD.
However, trying to put the cursor in a desired location using a number within 0
to 64 and 64 to 128 not really knowing which line you may be affecting is as easy
as it could be. We can create a new routine that allows you to simply state the
x and y coordinate locations would be much easier. If you wanted to display something
on the 4th line and 6 spaces from the start of the line could be written link GotoLocation(6,
4); That's easier than stating the location in this form: Send_A_Command(90); 90
(in decimal, not hex) just happens to be that position on a 20x4 LCD display. 90
is derived this way: 64 is the 2nd line. The 2nd line wraps to the 4th line by adding
20 to 64 (20+64 = 84). knowing that you are on the 4th line, just add 6 and you
have the numer 90. But forget all that, because we will implement the easy way.
Creating a routine that you can just enter the X and Y component is pretty easy.
First, you will need to create a variable that stores all of the initial column
numbers. In the case of the 20x4 LCD, the numbers are 0 for the first line, 64 for
the second line, 20 for the third line and 84 for the forth line. The variable declaration
might look like this:
char firstColumnPositionsForMrLCD[4] = {0, 64, 20, 84};
I am using the "char" data type because the numbers are not that big. The "char"
data type can range from 0 to 128. The variable name "firstColumnPositionsForMrLCD"
is LONG. This is actually intentional. First, for best programming practice and
readability, and second, so later on, the possibility for this name to be used for
another use would be very unlikely. Remember, we will have this varialble in another
file as a library and we wont see it (while working in our main .c file), but it
will be present. The numbers between the squiggly brackets are the initial positions
for each line. This way, all we need to do is put the "Y" component in this array
and it will tell us the initial position for that line. Then we add the "X" component
to that number and we will have the correct position on the LCD.
The array could also be a bit more systematic and could look like this:
#define numberOfColumns 20;
char firstColumnPositionsForMrLCD[4] = {0, 64, numberOfColumns , 64 + numberOfColumns
};
This way, all you would need to do is specify the LCD's number of columns and you
would not need to go to that obfuscatious data sheet (or should I say data BOOK).
So, armed with these glorious instructions and new knowledge that you thought you
would never know, how might the actual GotoLocation routine look like? Well, here
you go:
void GotoMrLCDsLocation(uint8_t x, uint8_t y)
{
Send_A_Command(0x80 + firstColumnPositionsForMrLCD[y-1] + (x-1));
}
Notice the 0x80... That still needs to reside in the Send_A_Command since it is
the command that represents the first position. then the array calls up the correct
number and then the "X" is added. You may have also noticed the y-1 and x-1. This
is added because the array is indexed from 0 and the screen positions are also indexed
from 0 and not 1. The small subrraction is just to be able to call line #1 "1" instead
of "0" and line #2 "2" instead of "1", and so on. For the "X", i like calling the
first column "1" not "0". It's like being in Paris, all of the first floors are
considered the ground, or "0" floor and the floor just above is considered the first
floor. Not that it is wrong, but I'm just not accustomed to that. If you like your
first column and first line to be "0", simple remove the subtraction and you are
set.
Now, for the desert. Making a numeric variable appear on the LCD screen. The secret
to doing this is to have a function that converts a number (numeric variable) into
a string. We already know ow to put a string to the LCD, now all we need is to have
a string that represents the number in a variable. Fortunately, there is a function
that is already made for this. It's called
itoaand
dtostrf. The "i" in itoa means integer. The "d" in dtostrf means double.
You already know what an integer is. If not, its just a number that doesn't have
any decimal places. You might have guessed that double is a number that can contain
decimal places. Double refers to double precision. Both of these functions convert
a value to a string and they require the stdlib.h library to be included ( #include
<stdlib.h> ). The following shows the proper usage:
itoa(integer value, string that will store the numbers, base);
dtostr(double precision value, width, precision, string that will store the numbers);
Value is either a direct value plugged into this place, or a variable to
contains a value.
Width that is used with dtostrf is the number of characters in the number
that includes the negative sign (-). For instance, if the number is -532.87, the
width would be 7 including the negative sign and the decimal point.
Precision is how many numbers would be after the decimal point in the dtostrf
usage.
Base is the maximum number of values per digit. For instance, 2 is for binary
representation (2 possible values for each digit - 0 or 1); 10 is for the common
human number system (10 possible values for each digit - 0, 1, 2, 3, 4, 5, 6, 7,
8, or 9); 16 is for hexadecimal where there are 16 possible values for each digit
- 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, or F. It should be understood that
if the base is larger, then the alphabet will just get larger.
String is the variable that accepts alphanumeric characters such as a char
array. After the function is executed, the string will have the number stored within
the array. For instance, if an integer variable contains the number 643, then the
string variable will contain "634".
This declaration would be somewhere in program.
char aNumberAsString[4];
int x = 432;
This would be where you needed to convert the number to the string:
itoa(x, aNumberAsString, 10);
This would be where you would want to display the number to the LCD:
Send_A_String(aNumberAsString);
Here is the entire program that pretty much has all of the routines that we need
for the LCD:
#include <avr/io.h>
#include <util/delay.h>
#include <stdlib.h>
#define MrLCDsCrib PORTB
#define DataDir_MrLCDsCrib DDRB
#define MrLCDsControl PORTD
#define DataDir_MrLCDsControl DDRD
#define LightSwitch 5
#define ReadWrite 7
#define BiPolarMood 2
char firstColumnPositionsForMrLCD[4] = {0, 64, 20, 84};
void Check_IF_MrLCD_isBusy(void);
void Peek_A_Boo(void);
void Send_A_Command(unsigned char command);
void Send_A_Character(unsigned char character);
void Send_A_String(char *StringOfCharacters);
void GotoMrLCDsLocation(uint8_t x, uint8_t y);
int main(void)
{
DataDir_MrLCDsControl |= 1<<LightSwitch | 1<<ReadWrite | 1<<BiPolarMood;
_delay_ms(15);
Send_A_Command(0x01); //Clear Screen 0x01 = 00000001
_delay_ms(2);
Send_A_Command(0x38);
_delay_us(50);
Send_A_Command(0b00001110);
_delay_us(50);
char positionString[4];
while(1)
{
for(int y = 1; y<=4; y++)
{
for(int x = 1;x<=20;x++)
{
itoa(x, positionString, 10);
GotoMrLCDsLocation(12, 3);
Send_A_String("X = ");
Send_A_String(positionString);
itoa(y, positionString, 10);
GotoMrLCDsLocation(12, 4);
Send_A_String("Y = ");
Send_A_String(positionString);
GotoMrLCDsLocation(x, y);
Send_A_String("x");
_delay_ms(50);
GotoMrLCDsLocation(x, y);
Send_A_String(" ");
itoa(x, positionString, 10);
GotoMrLCDsLocation(12, 3);
Send_A_String(" ");
itoa(y, positionString, 10);
GotoMrLCDsLocation(12, 4);
Send_A_String(" ");
}
}
}
}
void Check_IF_MrLCD_isBusy()
{
DataDir_MrLCDsCrib = 0;
MrLCDsControl |= 1<<ReadWrite;
MrLCDsControl &= ~1<<BiPolarMood;
while (MrLCDsCrib >= 0x80)
{
Peek_A_Boo();
}
DataDir_MrLCDsCrib = 0xFF; //0xFF means 0b11111111
}
void Peek_A_Boo()
{
MrLCDsControl |= 1<<LightSwitch;
asm volatile ("nop");
asm volatile ("nop");
MrLCDsControl &= ~1<<LightSwitch;
}
void Send_A_Command(unsigned char command)
{
Check_IF_MrLCD_isBusy();
MrLCDsCrib = command;
MrLCDsControl &= ~ ((1<<ReadWrite)|(1<<BiPolarMood));
Peek_A_Boo();
MrLCDsCrib = 0;
}
void Send_A_Character(unsigned char character)
{
Check_IF_MrLCD_isBusy();
MrLCDsCrib = character;
MrLCDsControl &= ~ (1<<ReadWrite);
MrLCDsControl |= 1<<BiPolarMood;
Peek_A_Boo();
MrLCDsCrib = 0;
}
void Send_A_String(char *StringOfCharacters)
{
while(*StringOfCharacters > 0)
{
Send_A_Character(*StringOfCharacters++);
}
}
void GotoMrLCDsLocation(uint8_t x, uint8_t y)
{
Send_A_Command(0x80 + firstColumnPositionsForMrLCD[y-1] + (x-1));
}