Writing your own device libraries can be time consuming but it gives you a deep insight of how the device works and full control to the hardware. Just importing LiquidCrystal.h or other LCD libraries without knowing what is underneath is not a good aproach.
The library is compatible with LCD types (passive matrix) based on KS0066U IC driver or HD44780 IC driver. What the software driver does is to send commands and data to LCD IC driver. If you take a look on the back side of the LCD the driver is there but under a black spot (see Chip on Board manufacturing method).
Firstly, the internal structure of the driver needs to be analysed.
- R/W - read or write operation
- RS - register select
- E - enable
- DB0 - DB7 - data lines
First thing to notice from the above picture is that KS0066U is handling the hard job of generating the AC drive waveforms ( a DC voltage will deteriorate the LC fluid). All we have to do is to send data/commands over parallel interface DB0 - DB7 and the CGROM (where all the character patterns are stored), parallel to serial circuit, the shift register and the segment driver will generate all the waveforms so that the net DC voltage across any pixel would be zero.
Even if there are 8 data lines (D0-D7) the controller also supports 4 bit mode (D7-D4) communication. The drawback of driving the LCD directly from GPIO pins without a SPI/I2C interface is that we can run out of GPIO pins, but using 4 bit mode can be a win-win situation.
Following the instruction set table the controller can be set in 4 bit mode if the DL bit is set to 0. In the start-up sequence I've used the following method for setting the 4 bit mode.
Even if the driver was tested on a FRDM-KEAZ128Q80 board (NXP) and STM32F4 Discovery, I will illustrate how to do it on ATmega328 microcontroller because the set of registers is well known and easy to grasp. The only difference is the init sequence that calls a different DIO (digital input/output) initialization function for each platform.
For instance for ATmega328 the function looks like this. The port number can be changed according to your needs and the RS, E, D4, D5, D6 and D7 pins can configured using preprocessor directives.
void DIO_SetPins()
{
DDRD |= (1<<RS) | (1<<E) | (1<<D4) | (1<<D5) | (1<<D6) | (1<<D7);
PORTD =0x00;
}Write "send data" commandBecause in 4-bit mode only a nibble can be sent at a time, the send data routine calls twice the LCD_Send4bitData function. The first call transmits the higher nibble and the second one the lower nibble.
void LCD_Data(uint8_t cmnd)
{
LCD_Send4bitData(cmnd);
_delay_us(2);
LCD_Send4bitData(cmnd<<4);
_delay_ms(20);
}The LCD_Sent4bitData function reproduce the write operation described in the last picture. The timing parameters tSP, tR, tPW have to be taken into consideration, so reading the datasheet is necessary.
void LCD_Send4bitData(uint8_t cmnd) {
PORTD |= (1<<RS);
//delay_ns(50); /* work also without delay */
PORTD |= (1<< E); /* Enable pulse */
PORTD &=0x0F;
//delay_ns(100); */ work also without delay */
PORTD |= (cmnd & 0xF0) | (1<<E) | (1<<RS); /* sending nibble */
_delay_us(1);
PORTD &= ~(1 << E);
_delay_us(2);
}Write "send command" commandThe single difference between sending a command and sending a data value is the state of the RS (register select) pin. Therefore the command is almost identical.
void LCD_Command(uint8_t cmnd)
{
LCD_Send4bitCmd(cmnd);
_delay_us(2);
LCD_Send4bitCmd(cmnd<<4);
_delay_ms(20);
}
void LCD_Send4bitCmd(uint8_t cmnd) {
PORTD &= ~(1<<RS);
/* delay_ns(50); */
PORTD |= (1<< E); /* Enable pulse */
PORTD &=0x0F;
/* delay_ns(100); */
PORTD |= (cmnd & 0xF0) | (1<<E); /* sending nibble */
_delay_us(1);
PORTD &= ~(1 << E);
_delay_us(2);
}From now on, everything is simple. Using LCD_Command and LCD_Data the communication between IC driver and the microcontroller is possible.
Write "init LCD" commandTaking a look at the Instruction set table the command values can be computed. For instance: 0x01 - screen clear command; 0x06 - input set command with increment mode (I/D = 1).
void init_LCD()
{
setPins();
_delay_ms(20); // Wait for more than 15 ms after VCC rises to 4.5 V
LCD_Command(0x33);
LCD_Command(0x32); /* Send for 4 bit initialization of LCD */
LCD_Command(0x28); /* 2 line, 5*7 matrix in 4-bit mode */
LCD_Command(0x0c); /* Display on cursor off */
LCD_Command(0x06); /* Increment cursor (shift cursor to right) */
LCD_Command(0x01); /* Clear display screen */
}Also, other functions can be created based on LCD_Command and LCD_Data functions. For LCD_String_xy please see the functionality of DDRAM block.
void LCD_Clear() {
LCD_Command(0x01); /* Clear display */
_delay_ms(2);
LCD_Command(0x01); /* Clear display */
}
void LCD_String(uint8_t *str) /* Send string to LCD function */
{
int i;
for (i = 0; str[i] != 0; i++) /* Send each uint8 of string till the NULL */
{
LCD_Data(str[i]);
}
}
void LCD_String_xy(uint8_t row, uint8_t pos, uint8_t *str)
{
/* Command of first row and required position<16 */
if (row == 0 && pos < 16) {
LCD_Command((pos & 0x0F) | 0x80);
LCD_Command((pos & 0x0F) | 0x80);
} else if (row == 1 && pos < 16) {
LCD_Command((pos & 0x0F) | 0xC0);
LCD_Command((pos & 0x0F) | 0xC0);
}
_delay_us(40);
LCD_String(str);
}Now, just calling these functions in the main function should give the expected result.
int main(void)
{
DDRB |= (1<<PINB0);
PORTB &= ~(1<<PINB0);
init_timer0();
uart_init(BAUD,F_CPU);
adc_init();
init_PWM();
init_LCD();
sei();
stdout = &uart_output;
LCD_String("Proiect-B. Vlad ");
for (;;) {
/* infinite loop ... */
}
}To test the code for ATmega328, Proteus can be used (pay attention to ATMEGA328P frequency setting):
- refer to CGRAM block functionality, also DDRAM
#define LCD_START_CGRAM_ADDRESS (0x40)
#define LCD_NUMBER_OF_CUSTOM_SYMBOLS (2U)
static uint8 auc_SubmenuArrow[] =
{
0B00000,
0B00000,
0B00100,
0B00010,
0B11111,
0B00010,
0B00100,
0B00000
};
static uint8 auc_SimleyFace[] =
{
0B00000,
0B00000,
0B01010,
0B00000,
0B10001,
0B01110,
0B00000,
0B00000
};
static uint8 *puc_LcdCustomCharacters[LCD_NUMBER_OF_CUSTOM_SYMBOLS] =
{
auc_SubmenuArrow,
auc_SimleyFace
};
void Lcd_CreateCustomCharacter(void)
{
uint8 uc_CGRamAddress = LCD_START_CGRAM_ADDRESS;
uint8 uc_I = 0;
uint8 uc_J = 0;
for (uc_J = 0; uc_J <= (LCD_NUMBER_OF_CUSTOM_SYMBOLS - 1); uc_J++)
{
Lcd_SendCommand(uc_CGRamAddress);
for (uc_I = 0; uc_I <= 7; uc_I++)
{
Lcd_SendData(puc_LcdCustomCharacters[uc_J][uc_I]);
}
uc_CGRamAddress = uc_CGRamAddress + 8;
}
}
void Lcd_PutCustomCharacter(uint8 uc_Row, uint8 uc_Position, uint8 uc_Index)
{
if (uc_Row == 0 && uc_Position < 16)
{
Lcd_SendCommand((uc_Position & 0x0F) | 0x80);
Lcd_SendCommand((uc_Position & 0x0F) | 0x80);
}
else if (uc_Row == 1 && uc_Position < 16)
{
Lcd_SendCmd((uc_Position & 0x0F) | 0xC0);
Lcd_SendCmd((uc_Position & 0x0F) | 0xC0);
}
_delay_us(40);
Lcd_SendData(uc_Index);
}







Comments