Ron Kreymborg
It is often necessary to provide a readout device for embedded systems, and while more complex systems are now using graphic displays, there is still a place for simple ASCII readouts. An area where I use one of these most often is during program development for monitoring various parameters in the running program, particularly where the end product has no readout device of its own. The module I will describe here stands alone and can be linked in during development and debugging. Or it can form the basis of your standard system's code.
The standard LCD display has two lines of sixteen characters (2x16). There are three control lines and eight data lines. Because they can also be driven with four data lines, this is the natural mode to use as the complete interface fits in a single 8-bit port. Initialisation is a little more complex as are data transfers but not that you would notice. I refer you to some of the excellent sites that describe the ubiquitous HD44780 chip and its clones for details and timing diagrams.
Although you can read the display contents, here I will only provide a write capability. Sending control bytes configures the display. Once configured you can send it data bytes for display. For normal displays these are usually ASCII characters. The three control lines are E, RS and R/W. The RS is high during a command transfer and low for data. The R/W is low for writing. The E line is used to clock data into and out of the display on the high to low transition.
Setup and clock times are such that a 4 Mhz AVR will be too fast using bit banging without some inter-transition delay. The following code uses an inline function that is an asm macro containing a few nops. An alternative is to replace this with an empty function. In most cases the gcc overhead of the function call is all that is required. If you are using a '103 or similar with a clock divider (the XDIV register), you could remove these calls and simply bracket the display code with XDIV writes with a consequent reduction in code size.
Initialisation needs to include some hefty delays. In the interest of making this module stand alone I have included a simple loop delay. You can implement this another way of course, including a sequence of delayed tasks in TaskR2, but remember you cannot make an lcd function call until the initialisation is complete.
I use defines for the port addresses to simplify subsequent moving to another physical port. These are defined in the module header along with the control pins. The table shows the particular pin connections I use but this is of course, quite arbitrary. Note that if you do change these pins, you will need to review the hardcoded masks in the Send function.
| AVR port | LCD pin | Function |
| D6 | 6 | E |
| D5 | 4 | RS |
| D4 | 5 | R/W |
| D3 | 14 | D7 |
| D2 | 13 | D6 |
| D1 | 12 | D5 |
| D0 | 11 | D4 |
#define PORT PORTD // allows easy port change #define DDR DDRD #define PIN PIND #define E_PIN 6 #define RS_PIN 5 #define RW_PIN 4
With these defined the initialisation function is:
void InitLCD(void)
{
// Initialise the port to use.
//
outp(0x00, PORT); // all low
outp(0xff, DDR); // all output
// The following are sent as though it is an 8-bit
// interface. The specific bit patterns configure the
// chip for subsequent 4-bit transfers.
//
Delay(50); // power up 50 mSec wait
outp(0x03, PORT);
Delay(5);
outp(0x03, PORT);
Delay(5);
outp(0x03, PORT);
Delay(5);
outp(0x02, PORT);
Delay(5);
// The chip is now expecting 4-bit communication. The next
// commands configure the display for 2 lines of 16 chars.
//
SendCommand(0x28);
SendCommand(0x08);
SendCommand(0x0c);
SendCommand(0x01);
SendCommand(0x06);
}
The SendCommand and SendData functions set the static global commandFlag respectively and then call the common Send function
void SendCommand(BYTE value)
{
commandFlag = 0;
Send(value);
}
static void SendData(BYTE value)
{
commandFlag = 1;
Send(value);
}
The Send function implements the bit banging associated with sending a byte to the display four bits at a time. The connections are as shown in the table. The first task is to check the chip is not busy. In 4-bit mode the HD44780 puts the busy bit on its bit-7 during the first nibble, and the address counter during the second nibble. For the AVR, this means switching the R/W line high for read, switching the four data lines to input, recording the state of bit-7 during the first clock, providing a second clock but ignoring the data, then examining the bit-7 state to decide whether to do the status loop again or continue. Once the chip is ready, the R/W line is taken low, the data lines reset as outputs, and the output byte sent most significant nibble first.
static void Send(BYTE value)
{
BYTE state;
outp(0xf0, DDR); // low 4 bits input
outp(0x1f, PORT); // R/W high, pullup on
do
{
Tick();
sbi(PORT, E_PIN); // clock high
sbi(PORTA, 1);
Tick();
state = inp(PIN);
cbi(PORT, E_PIN);
cbi(PORTA, 1);
Tick();
sbi(PORT, E_PIN);
sbi(PORTA, 1);
Tick();
cbi(PORT, E_PIN);
cbi(PORTA, 1);
} while (state & 0x08);
Tick();
outp(0x00, PORT); // R/W low
outp(0xff, DDR); // low 4 bits output again
SendNibble(value>>4); // send high nibble
SendNibble(value); // send low nibble
}
The Tick function is implemented as an inline function.
static inline void Tick(void)
{
asm("nop\n\tnop"::);
}
How you implement this will depend on your cpu clock speed. Too many nops and the code size increases above what would be required to simply call an empty function. The HD44780 seems to be happy with clock periods down to about 1 uSec.
The SendNibble function uses the global flag to decide whether the byte is data or a command.
static void SendNibble(BYTE value)
{
value &= 0x0f; // mask off high nibble
if (commandFlag) // or in the RS bit
value |= 1<<RS_PIN;
Tick();
outp(value, PORT); // output low nibble
Tick();
}
After initialisation the display is set up and ready to receive data. Characters to be sent are preceded by a command byte that includes the start address. For a 2x16 display, these addresses are 0x00 for the first character in the top row, and 0x40 for the first character of the second row. Thus the address of the 5th character in the second row would be 69. The command byte itself is 0x80. The SendString function demonstrates this sequence well. This function is called with a pointer to the null terminated string to display, the line number (1 or 2), and the starting character position in that line.
void SendString(char *pt, int line, int position)
{
if (line == 1)
SendCommand(0x80 + position);
else
SendCommand(0xc0 + position);
while (*pt)
SendData(*pt++);
}
The SendString function is the basis of all other Send functions. The SendInt function to display an integer is
void SendInt(int number, int line, int position)
{
char temp[10];
itoa(number, temp, 10);
SendString(temp, line, position);
}
The SendFloat function uses the non-ansi dtostre function to convert a floating point value to a string using scientific notation.
void SendFloat(float value, int line, int position)
{
char temp[13];
dtostre(value, temp, 4, 3);
SendString(temp, line, position);
}
Finally the SendLong function needs to provide its own long to string conversion, as this is not yet part of the avr-gcc implementation. The following shows one way this could be done.
void SendLong(long number, int line, int position)
{
char temp[15];
sprintl(number, temp);
SendString(temp, line, position);
}
void sprintl(long number, char *pt)
{
int i, a, flag;
long divisor = 1000000000L;
if (number < 0L) // prepend a minus sign for negative
*pt++ = '-';
if (number == 0L)
{
*pt++ = '0';
}
else
{
flag = 0; // leading zero flag
for (i=1; i<11; i++)
{
a = (int)(number / divisor);
if (a > 0)
{
flag = 1;
number = number - (long)a * divisor;
}
if (flag)
{
*pt++ = '0' + (char)a;
}
divisor /= 10L;
}
}
*pt = '\0'; // trailing null
}
The code described above has been collected in a module called lcd16.c. You can download this module along with the corresponding header files, makefile and an example main program.