PIC18F4550 + 16x2 LCD code query

Thread Starter

GrafZeppelin

Joined May 3, 2013
11
I'm a beginner in MCU programming and also very eager to learn lots about it. Recently I have acquired the "Aptinex development board rev.6" which has many features already attached. I also bought a PIC18F4550 to plug it in the board. I've managed to understand how the basics of I/O operations work, but now I have stumbled on the LCD, which is attached to the development board. I found the code bellow in one of many tutorials pages all over google, and decided to try it out because it seemed simple enough.
Code:
//File name 'LCD_Display_String_Original.c'
#define _XTAL_FREQ 8000000
#define RS LATD0
#define EN LATD1
#define D4 LATD2
#define D5 LATD3
#define D6 LATD4
#define D7 LATD5

#include <xc.h>
#include "lcd.h"

void delay_ms(unsigned int delay_value)
{
while(delay_value-- > 0)
// While desired delay value is more than 0, decrement that value by 1 after each 10ms
{
    __delay_ms(1);
    // Wait 10ms
}                      
}

int main()
{
TRISD = 0b00000000;
Lcd_Init();
while(1)
{
    Lcd_Clear();
    Lcd_Set_Cursor(1,1);
    Lcd_Write_String("Hello world!");
    delay_ms(1000);

}
}
The question that I have is, could someone kindly please explain what is going on in the functionLcd_port (char a);, located in the lcd.h file bellow. Can't understand what conditions are tested and how they are true or false.
Code:
//File name 'lcd.h'
#include <xc.h>
#include <stdio.h>

void Lcd_Port(char a)
{
if(a & 1)
    D4 = 1;
else
    D4 = 0;

if(a & 2)
    D5 = 1;
else
    D5 = 0;

if(a & 4)
    D6 = 1;
else
    D6 = 0;

if(a & 8)
    D7 = 1;
else
    D7 = 0;
}

void Lcd_Cmd(char a)
{
RS = 0;             // => RS = 0
Lcd_Port(a);
EN  = 1;             // => E = 1
    __delay_ms(4);
    EN  = 0;             // => E = 0
}

Lcd_Clear()
{
Lcd_Cmd(0);
Lcd_Cmd(1);
}

void Lcd_Set_Cursor(char a, char b)
{
char temp,z,y;
if(a == 1)
{
  temp = 0x80 + b - 1;
    z = temp>>4;
    y = temp & 0x0F;
    Lcd_Cmd(z);
    Lcd_Cmd(y);
}
else if(a == 2)
{
    temp = 0xC0 + b - 1;
    z = temp>>4;
    y = temp & 0x0F;
    Lcd_Cmd(z);
    Lcd_Cmd(y);
}
}

void Lcd_Init()
{
Lcd_Port(0x00);
__delay_ms(20);
Lcd_Cmd(0x03);
__delay_ms(5);
Lcd_Cmd(0x03);
__delay_ms(11);
Lcd_Cmd(0x03);

Lcd_Cmd(0x02);
Lcd_Cmd(0x02);
Lcd_Cmd(0x08);
Lcd_Cmd(0x00);
Lcd_Cmd(0x0C);
Lcd_Cmd(0x00);
Lcd_Cmd(0x06);
}

void Lcd_Write_Char(char a)
{
char temp,y;
temp = a&0x0F;
y = a&0xF0;
RS = 1;             // => RS = 1
Lcd_Port(y>>4);             //Data transfer
EN = 1;
__delay_us(40);
EN = 0;
Lcd_Port(temp);
EN = 1;
__delay_us(40);
EN = 0;
}

void Lcd_Write_String(char *a)
{
int i;
for(i=0;a[i]!='\0';i++)
   Lcd_Write_Char(a[i]);
}

void Lcd_Shift_Right()
{
Lcd_Cmd(0x01);
Lcd_Cmd(0x0C);
}

void Lcd_Shift_Left()
{
Lcd_Cmd(0x01);
Lcd_Cmd(0x08);
}
As mentioned before, the code works perfectly fine. I just really need someone to explain how that part works. Any help will be highly appreciated!
 

dannyf

Joined Sep 13, 2015
2,197
It takes the lower four bits and output it in d3..d0.

Since d3..d0 are specified by the user, this approach allows arbitrary arrangement between mcu dataa pins and LCDs data pins.

I use an identical sgeme in my code as well.


Also, it toggles direction register to output 1 and 0.
 

Thread Starter

GrafZeppelin

Joined May 3, 2013
11
Thanks for the answer dannyf ! I think I almost got it, just need a bit more clarity. So lets say:
  1. Lcd_Port() receives a value of 'a' = '0x03' from the Lcd_Cmd() function, binary 0b0011
  2. Decimal '1' is 0b0001
  3. After the logical AND operation the result is 0b0001, which makes the condition TRUE, because it is more than zero, thus setting the corresponding pin D4 high.
And in this case, if 'a' was 0b1100, the condition would be false then, because 0b1100 AND 0b0001 would produce 0b0000, thus setting the pin D4 low. Is this the correct way to see the Lcd_Port() function?
 

dannyf

Joined Sep 13, 2015
2,197
Yes. Effectively, the lowest four bits of lcd_port()'s input are being mapped to D7..D4. The beauty of this approach is that it allows you to define D7..D4 arbitrarily, even on different ports, greatly simplifying pcb layout.
 

Thread Starter

GrafZeppelin

Joined May 3, 2013
11
@ dannyf, thanks again mate! Couple of more questions (apologies for being such a newbie). In my understanding, and please correct me if I'm wrong, when working with the LCD in 4-bit mode, the data is first sent bit by bit (Bit8 > Bit7 > Bit6 > Bit5) or in our case (0>0>1>1) in 4-bit higher nibble (D7-MSB,D6,D5,D4-LSB) while masking (ignoring) the lower 4-bit nibble (D3-MSB, D2, D1, D0-LSB), and then sending the lower 4-bit nibble while masking the higher 4-bit nibble. What confuses me, is how are we masking those nibbles exactly? How are we influencing the bits that are not even connected to the MCU (D3,D2,D1 and D0). Also, I am not so sure if I'm right about the bits 8-4 and 5-1 being MSB and LSB, respectively.
I hope my questions are clear. Could you please help me with this?
 

dannyf

Joined Sep 13, 2015
2,197
A byte needs to be sent twice, the highest 4 bits and then the lowest 4 bits.

You can take a look at Lcd_Write_Char() to see how it is done (quite inefficiently I may add).
 

nsaspook

Joined Aug 27, 2009
13,086
I don't see much use in 'masking' a into two temp upper/lower nibble variables before sending the correct nibble to Lcd_Port().
The original XLCD code with direct 8 bit port writes needed to mask off the 4 bits (LOWER in this case) not used for LCD data from modification by the routines with something like this.

Code:
#define UPPER
/* DATA_PORT defines the port to which the LCD data lines are connected */
#define DATA_PORT PORTH
...
data = some_char;
...
DATA_PORT &= 0x0f; // set the upper bits to zeros and leave the lower ones alone
DATA_PORT |= data & 0xf0; // OR in upper data
...
DATA_PORT &= 0x0f;
DATA_PORT |= ((data << 4)&0xf0); // OR in lower data after nibble shift
It looks like someone just left in unneeded code with the rewrite to indirect port bits in Lcd_Port();
 

MMcLaren

Joined Feb 14, 2010
861
Since the Lcd_Port() function is bit testing bits 3 through 0 of the data, there's really no need to mask off the unused bits before calling the function. You just need to make sure the correct nibble is in the b3..b0 bit position. Also, if you need a delay to stretch the 'E' pulse width, 1-uS should be sufficient (ref: HD44780 datasheet).

Good luck on your project. Cheerful regards, Mike
Code:
void Lcd_Write_Char(char a)
{ RS = 1;           // RS = 1 (data)
  Lcd_Port(a>>4);   // send hi nibble
  EN = 1;           // strobe 'E'
  EN = 0;           //  "
  Lcd_Port(a);      // send lo nibble
  EN = 1;           // strobe 'E'
  EN = 0;           //  "
}
 

Thread Starter

GrafZeppelin

Joined May 3, 2013
11
Since the Lcd_Port() function is bit testing bits 3 through 0 of the data, there's really no need to mask off the unused bits before calling the function. You just need to make sure the correct nibble is in the b3..b0 bit position. Also, if you need a delay to stretch the 'E' pulse width, 1-uS should be sufficient (ref: HD44780 datasheet).

Good luck on your project. Cheerful regards, Mike
Code:
void Lcd_Write_Char(char a)
{ RS = 1;           // RS = 1 (data)
  Lcd_Port(a>>4);   // send hi nibble
  EN = 1;           // strobe 'E'
  EN = 0;           //  "
  Lcd_Port(a);      // send lo nibble
  EN = 1;           // strobe 'E'
  EN = 0;           //  "
}
Thanks for the input. I was just about to ask something, but got an "Eureka!" moment just as I was writing the question. Thanks a lot!
 

nsaspook

Joined Aug 27, 2009
13,086
Another thing I might do is to convert the bit tests in Lcd_Port() to a direct assignment expression with the ternary operator instead of an if statement. The code generated by a good compiler will likely be the same but the C syntax is cleaner but maybe a bit more human-readable than repeated multi-line expressions.
Code:
// convert each if statement of this form
    if (a & 1)
        D4 = 1;
    else
        D4 = 0;

    // to

    D4 = (a & 1) ? 1 : 0;
 

nsaspook

Joined Aug 27, 2009
13,086
Let's see what XC8 'free' does with each.:)

Code:
16:               if (a & 1)
7FD4  A001     BTFSS a, 0, ACCESS
7FD6  D002     BRA 0x7FDC
17:                   D4 = 1;
7FD8  848C     BSF LATD, 2, ACCESS
7FDA  D001     BRA 0x7FDE
18:               else
19:                   D4 = 0;
7FDC  948C     BCF LATD, 2, ACCESS
20:               // to
21:               D4 = (a & 1) ? 1 : 0;
7FDE  A001     BTFSS a, 0, ACCESS
7FE0  D002     BRA 0x7FE6
7FE2  848C     BSF LATD, 2, ACCESS
7FE4  D001     BRA 0x7FE8
7FE6  948C     BCF LATD, 2, ACCESS
22:               //
23:               D4 = 0; if(a & 1) D4 = 1;    //
7FE8  948C     BCF LATD, 2, ACCESS
7FEA  B001     BTFSC a, 0, ACCESS
7FEC  848C     BSF LATD, 2, ACCESS
24:               //
25:               D4 = (a & 1);
7FEE  A001     BTFSS a, 0, ACCESS
7FF0  D002     BRA 0x7FF6
7FF2  848C     BSF LATD, 2, ACCESS
7FF4  D001     BRA 0x7FF8
7FF6  948C     BCF LATD, 2, ACCESS
26:               //
 

MMcLaren

Joined Feb 14, 2010
861
Yeah, it really works, on real hardware (lol).

Slightly off-topic... how about an 8-bit mode LCD backpack with a 2 pin interface where the shift register <clock> and <data> signals as well as the LCD 'RS' signal are derived from a single pin?

K8LH 2-Pin Backpacks.png
Code:
  /*                                                                *
   *  K8LH 2-Pin 8-Bit 74HC595 LCD low level driver                 *
   *                                                                */
   void PutLCD(char work)       // write byte to 74HC595 & LCD
   { char bitctr = 8;           //
     do                         // load 74HC595 shift register
     { if(!(work&128) clk = 0;  // if a '0', set clk = 0
       _delay_us(9);            // charge or drain cap (3*tau)
       clk = 0; clk = 1;        // clock bit into shift register
       work <<= 1;              // shift next bit into b7
     } while(--bitctr);         // until all 8 bits clocked out
     if(rs == 0) clk = 0;       // make clk pin = rs flag
     lat = 1; lat = 0;          // latch 595, pulse lcd 'E' pin
     clk = 1;                   // leave clk pin high
   }
It's a bit slow (about ~100-uS per character) but that's because the RC time constant was selected to allow operation on a PIC with a clock as low as 4-MHz. If you need a faster interface, you could get rid of the RC and use a 3 pin interface.

rc timing.png
 
Last edited:
Top