LCD HD44780u
There are many libraries out there that you can find and use them easily without any worry about what happens on the pins of the LCD (Hitachi HD44780). For example, in Arduino you just need to set up some pieces of line code and print string of characters there, really easy and much time save. But for you who want to now a bit more about what data/address is actually accessed to the LCD and how to do it with a microcontroller directly (in this case atemega8535 is used), and (may be) how. this post may help.
There are many libraries out there that you can find and use them easily without any worry about what happens on the pins of the LCD (Hitachi HD44780). For example, in Arduino you just need to set up some pieces of line code and print string of characters there, really easy and much time save. But for you who want to now a bit more about what data/address is actually accessed to the LCD and how to do it with a microcontroller directly (in this case atemega8535 is used), and (may be) how. this post may help.
Schematic
Circuit
So
bellow is our schematic circuit diagram
Figure 1 Configuration of atmega8535 and lm016 (proteus) |
Note
that all of our configuration that we are going to discuss bellow is
based on the above schematic. In other words changing any pin or PORT
(e.g., from PORTC like above to PORTB) will make these code useless
(unless you know what should be changed on PORT configuration). The
discussion only emphasizes about memory accessing to the LCD with
atmega8535 MCU without ‘dynamic’ configuration prepared – like
on Arduino.
Files
Needed to prepare
After
the schematic we have, next creating our file hierarchy where we type
our scripts certainly. Bellow they are:
--build
----main.c
----lcd_reg.h
----lcd_reg.c
----Makefile
There
are four files we need to create. For the compiler, I use GNU gcc
compiler of course (and may be always…) with help of Makefile that
I create to compile.
As seen on the above files, there are four of them with a ‘build’
folder where all compiled files are put. I deliberately put the
compiled files (such as .hex, .asm and etc.) separately, so that our
original destination files (the four files) keeps clean. Now just
take a look on the main.c file:
Content of main.c File
#include "lcd_reg.h"
int main()
{
lcd_init();
lcd_print("hello lcd");
while(1);
return 0;
}
From the above lines of code, the purpose may easily be guessed that
the string of “hello lcd” will be sent to the LCD to be displayed
on the its screen. To do that, the first thing we need to do is
‘include’-ing our header file (that we are going to discussed
later). All of parameters and functions prototypes are there in the
header file: lcd_reg.h, that are then to be included in main file,
where our main(){...} definition exists.
In main() function, lcd_init() is called. This is where all LCD
addresses configuration takes place on which our discussion focuses.
The next line is lcd_print() that simply transferring string of
characters to the LCD.
Now lets take a glance on the header file, i.e., lcd_reg.h.
Content of lcd_reg.h File
#ifndef LCD_REG_HEADER_
#define LCD_REG_HEADER_
#include <avr/io.h>
//macros defined
#define DATA_PINS PORTC //port aliase
#define RS 0x04 //pin lcd to mcu
#define EN 0x08
//parameters taken from hd44780u datasheet
//table 6 on page 24
#define FUNCTION_SET 0x20
#define DISP_2LINES 0x08
#define CLEAR_DISPLAY 0x01
#define DISP_CONTROL 0x08
#define DISP_ON 0x04
#define ENTRY_MODESET 0x04
#define ENTRYLEFT 0x02
void send(uint8_t val);
void command(uint8_t val);
void write(uint8_t val);
void enablePin_pulse(void);
void send_4line(uint8_t ch);
void lcd_init(void);
void clear_disp(void);
void lcd_print(const char *s);
#endif
The contents of this file are some macro variables followed by
function prototypes needed by lcd_reg.c file and any other files that
may call it. For the three lines:
#define DATA_PINS PORTC //port aliase
#define RS 0x04 //pin lcd to mcu
#define EN 0x08
is where our pins configuration for the atmega8535 MCU pin/port
register is set. As seen on the schematic figure 1 above that PORTC
is used to interface the LCD. The higher nibble (i.e., PC4, PC5, PC6,
and PC7) of PORTC is used to transfer data-bit into D4, D5, D6, and
D7 respectively. Therefore to ease the semantic of the code,
“DATA_PINS” is used as an alias for PORTC. The next two lines are
RS and EN aliases for PORTC’s pins that we use for the transfer
control. You can see on the figure 1 that PC2 and PC3 are connected
to RS and E of LCD pins respectively. For the hexadecimal values of
0x04 and 0x08, about how to define those values according to the
PORTx of the MCU, you can see it on the related posts bellow.
Posts related:
Note:
The ‘nibble’ term in this case is parts of a byte in data-memory.
Figure 2 bellow depicts what I mean:
Next lines are macro definitions of:
#define FUNCTION_SET 0x20
#define DISP_2LINES 0x08
…
All hexadecimal values of each line of the macros above refer to the
data-sheet of Hitachi HD44780u LCD. These values can be found on page
24 in table 6. These aliases are then passed as argument functions
that call them later.
The lines:
void send(uint8_t val);
void command(uint8_t val);
…
are function prototypes that are also included along with the macros
above. Without these prototypes, main() function cannot call
lcd_init() and lcd_print() functions.
For lcd_reg.c is rather long to attach at once. Therefore each of
function definitions will be explained step-by-step begun from
lcd_init() function.
Content / Definitions of lcd_reg.c File
lcd_init definition
void lcd_init(void)
{
configure_pins();
//since on the schematic, the 4-line connection is used
//we set the lcd mode into 4-bit
//the steps are according to hitachi HD44780 flow-chart, see page 45 - 46
//starting with 8-bit mode
send_4line(0x03);
_delay_us(4100); //wait for 4.1ms
//next step
send_4line(0x03);
_delay_us(4100); //wait for 4100us
//finally
send_4line(0x03);
_delay_us(150);
send_4line(0x02); //for 4-bit mode
//set # lines, font size, etc.
command(FUNCTION_SET | DISP_2LINES);
_delay_us(4200); //waitmore than 4.1ms
//turn the display on
command(DISP_CONTROL | DISP_ON);
//clear the display
clear_disp();
//set the entry mode
command(ENTRY_MODESET | ENTRYLEFT);
}
to ease to explain, we refer to the flow of the function above. Note
that the documentations of the code above may ‘describe’ each
line of the code ‘aim’. And the explanation for next section will
also refer to the documentation.
So the first line of the code calls the configure_pins() function in
which the definition is:
configure_pins Definition
void configure_pins(void)
{
DDRC |= 0xfc; //activate pins according to our schematic
//according to datasheet, we need to wait more than 4.5ms after Vcc rises to 2.7v
_delay_us(50000);
//to begin the command, RW, EN, and RS pins need to be low
//based on our schematic, RW is directly connected to ground
//that automatically has the low state
DATA_PINS &= ~(RS | EN);
}
The definition above aims to activate PORTC on PC2 to PC7, and pull
PC2 and PC3 low for RS and EN pins respectively to start instruction
of the LCD.
send_4line Definition
void send_4line(uint8_t ch)
{
DATA_PINS &= 0x0C; //mask for RS and EN bits
DATA_PINS |= (ch << 4); //shift value according to our schematic
enablePin_pulse();
}
the first two lines of function definition above simply send 4-bit
data via high-nibble of PORTC with 2-bit (for RS an EN pin) data-sate
is retained by masking it with 0x0C. Then the next line
enablePin_pulse() is for pulse generator that is needed by the EN pin
of LCD in each data transferred.
enablePin_pulse Definition
void enablePin_pulse(void)
{
DATA_PINS &= ~EN; //mask by its own value
_delay_us(20);
DATA_PINS |= EN;
_delay_us(20); //enable pulse must be more than 450ns
DATA_PINS &= ~EN;
_delay_us(100); //commands need to be more than 37us
//see table 6 of page 24 for details
}
by referring to its data-sheet that each step in this process needs a
delay at least 450ns. When this process is finished, we need to delay
the execution once more for at least 37us before going to the next
execution / process or before leaving this function.
command Definition
void command(uint8_t val) //command mode
{
DATA_PINS &= ~RS; //rs on lcd pin needs to be low
send(val);
}
by pulling RS line low while transferring any data (by calling
send(val) above), this means that we are transferring in command
mode, otherwise we are transferring in data mode.
send Definition
void send(uint8_t val)
{
send_4line(val >> 4);
send_4line(val);
}
This is where 8-bit data is transferred by way of transferring 4-bit
(or nibble) data twice. On the first send_4line() function, we need
to shift four-bit to the left to get the high nibble of data-bit to
transfer first, and followed by the low nibble of data-bit on the
second call of send_4line(). Note that the HD44780u LCD in 4-bit
mode, the higher nibble of byte needed to be received first, and
followed by the lower nibble.
clear_disp Definition
void clear_disp(void)
{
//clear entire display and set DDRAM to 0 on address counter
command(CLEAR_DISPLAY);
_delay_ms(2);
}
By passing alias value of CLEAR_DISPLAY (or simply 0x02) in command
mode, the display of the LCD and the DDRAM address counter are
cleared and set to 0 respectively. Note that before continuing the
next step, the LCD needs to wait about 2ms for the process to be
done.
lcd_print Definition
void lcd_print(const char *s)
{
while(*s)
write(*s++);
}
This function definition simply prints each character of the string
with write() function. The process is done in while() loop by
incrementing pointer-value / memory-address-value to point to each
character of char array (or simply string).
write Definition
void write(uint8_t val) //write mode
{
DATA_PINS |= RS; //rs on lcd pin needs to be high
send(val);
}
by pulling RS line high while transferring any data (by calling
send(val) above), this means that we are transferring in data mode,
otherwise we are transferring in command mode.
Compiling and Run
Before going to the compiling process, for this project to be able to
run, you need to save all the definition above in a file called
lcd_reg.c. Then just make the file hierarchy like we have already
discussed above with the ‘build’ folder created (the name must be
the same, i.e., ‘build’, because it refers to our Makefile
configuration).
Finally after the hierarchy file you have, like the top of this post,
open your terminal and navigate to this four files destination, and
type
make
then, you would see the message like bellow:
avr-gcc -c -Wall -Os -std=c11 -g -mmcu=atmega8535 -DF_CPU=2000000UL lcd_reg.c -o build/lcd_reg.c.o
avr-gcc -c -Wall -Os -std=c11 -g -mmcu=atmega8535 -DF_CPU=2000000UL main.c -o build/main.c.o
avr-gcc -g -mmcu=atmega8535 -Wl,-Map,main.map -o build/main.c.out build/main.c.o build/lcd_reg.c.o
avr-objdump -h -S build/main.c.out > build/main.lst
avr-size build/main.c.out
text data bss dec hex filename
314 10 0 324 144 build/main.c.out
avr-objcopy -O ihex -R .flash build/main.c.out build/main.c.hex
for the .hex file, you can find it in the build folder.
So here is our Makefile
Makefile Content
MCU=atmega8535
CPU=2000000UL
MCU_FLAG=-g -mmcu=$(MCU)
CPU_FLAG=-DF_CPU=$(CPU)
CC=avr-gcc
main.c.hex : main.c.out
avr-objdump -h -S build/main.c.out > build/main.lst
avr-size build/main.c.out
avr-objcopy -O ihex -R .flash build/main.c.out build/main.c.hex
main.c.out : main.c.o
$(CC) $(MCU_FLAG) -Wl,-Map,main.map -o build/main.c.out build/main.c.o build/lcd_reg.c.o
main.c.o : main.c
$(CC) -c -Wall -Os -std=c11 $(MCU_FLAG) $(CPU_FLAG) lcd_reg.c -o build/lcd_reg.c.o
$(CC) -c -Wall -Os -std=c11 $(MCU_FLAG) $(CPU_FLAG) main.c -o build/main.c.o
For this Makefile, we will discuss on the next post.
That’s it! How we can use the LCD based on address configuration.
Thanks for reading, and if you have any question, just ask bellow on
the comment.
0 Comments
Post a Comment