News:

Let's find out together what makes a PIC Tick!

Main Menu

I2C Alphanumeric LCD interface using a TCA6408A device

Started by top204, Feb 03, 2024, 06:15 PM

Previous topic - Next topic

top204

Sometimes, the pins available on a microcontroller are very few, and an alphanumeric 'Hitachi type' LCD can take too many pins to interface with, or the microcontroller type does not have enough pins, as in the case of 8-pin devices. So being able to interface to it using only 2 pins is ideal. The I2C interface is not known for its very high speed, however the alphanumeric Hitachi LCDs do not operate at extreme speeds, so I2C is ideal for it. The library replaces the compiler's Print, Print At, Cls, and Cursor commands, so the program listing using an LCD does not need multiple procedure calls that can confuse things.

Below is the code listing for the "TCA6408_LCD.inc" library file. I found the TCA6408 extremely nice to work with, and it operates perfectly with the LCD, or as a standard port expander:

$ifndef _TCA6408_LCD_INC_
$define _TCA6408_LCD_INC_
'
'   /\\\\\\\\\
'  /\\\///////\\\
'  \/\\\     \/\\\                                                 /\\\          /\\\
'   \/\\\\\\\\\\\/        /\\\\\     /\\\\\\\\\\     /\\\\\\\\   /\\\\\\\\\\\  /\\\\\\\\\\\  /\\\\\\\\\
'    \/\\\//////\\\      /\\\///\\\  \/\\\//////    /\\\/////\\\ \////\\\////  \////\\\////  \////////\\\
'     \/\\\    \//\\\    /\\\  \//\\\ \/\\\\\\\\\\  /\\\\\\\\\\\     \/\\\         \/\\\        /\\\\\\\\\\
'      \/\\\     \//\\\  \//\\\  /\\\  \////////\\\ \//\\///////      \/\\\ /\\     \/\\\ /\\   /\\\/////\\\
'       \/\\\      \//\\\  \///\\\\\/    /\\\\\\\\\\  \//\\\\\\\\\\    \//\\\\\      \//\\\\\   \//\\\\\\\\/\\
'        \///        \///     \/////     \//////////    \//////////      \/////        \/////     \////////\//
'                                  Let's find out together what makes a PIC Tick!
'
' Alphanumeric LCD interface routines using a TCA6408A I2C I/O expander device.
' The library replaces the compiler's alphanumeric Print library subroutine.
'
' Note. The library defaults to the TCA6408A device's ADDR pin pulled to ground, so it uses the slave address: cTCA6408_ADDR1
'
' Written for the Positron BASIC compiler by Les Johnson.
'
    #Disable Print                                                      ' Disable the compiler's Print command's library subroutines
'
' Set some default Port Pins for the I2C interface
'
$ifndef LCD_SDA_Pin
    $define LCD_SDA_Pin PORTC.4                                         ' Connects to the TCA6408 device's SDA pin
    $SendWarning "$define LCD_SDA_Pin missing from the main program, so defaulting to pin PORTC.4"
$endif
$ifndef LCD_SCL_Pin
    $define LCD_SCL_Pin PORTC.3                                         ' Connects to the TCA6408 device's SCL pin
    $SendWarning "$define LCD_SCL_Pin missing from the main program, so defaulting to pin PORTC.3"
$endif
'
' Setup the Alphanumeric LCD's constants within the compiler
'
    Declare LCD_Interface = 4                                           ' Use a 4-wire interface for the LCD
    Declare LCD_Lines     = 2                                           ' The LCD has 2 lines
    Declare LCD_Type      = Hitachi                                     ' An alphanumeric LCD
    Declare LCD_CommandUs = 2000                                        ' Delay (in us) for commands sent to the LCD
    Declare LCD_DataUs    = 50                                          ' Delay (in us) for data sent to the LCD

Symbol cTCA6408_ADDR1 = ($20 << 1)                                      ' TCA6408 I2C address if the ADDR pin is pulled to ground
Symbol cTCA6408_ADDR2 = ($21 << 1)                                      ' TCA6408 I2C address if the ADDR pin is connected to VDD
'
' TCA6408 Registers
'
$define cTCA6408_INPUT              $00                                 ' Read from the expander port bits
$define cTCA6408_OUTPUT             $01                                 ' Make the expander port bits low or high
$define cTCA6408_POLARITY_INVERSION $02
$define cTCA6408_CONFIGURATION      $03                                 ' Make the expander port bits inputs or outputs
'
' Bit patterns for the LCD via the I2C I/O extension port
' Change as required
'
' The LCD's RS pin is connected to bit-5 of the TCA chip's port
' The LCD's EN pin is connected to bit-4 of the TCA chip's port
' The LCD's D4 pin is connected to bit-3 of the TCA chip's port
' The LCD's D5 pin is connected to bit-2 of the TCA chip's port
' The LCD's D6 pin is connected to bit-1 of the TCA chip's port
' The LCD's D7 pin is connected to bit-0 of the TCA chip's port
'
$define cLCD_I2C_RS_Bit  5                                              ' The LCD's RS line bit
$define cLCD_I2C_EN_Bit  4                                              ' The LCD's EN line bit

$define cLCD_I2C_D4_Bit  3                                              ' The LCD's D4 line bit
$define cLCD_I2C_D5_Bit  2                                              ' The LCD's D5 line bit
$define cLCD_I2C_D6_Bit  1                                              ' The LCD's D6 line bit
$define cLCD_I2C_D7_Bit  0                                              ' The LCD's D7 line bit

$define cLCD_SendControl 0                                              ' Signal to send a control byte to the LCD
$define cLCD_SendData    1                                              ' Signal to send a data byte to the LCD
'
' Create some global variables
'
    Dim BPF                  As Byte System                             ' Create a compiler system variable
    Dim LCD_I2C_tInitialised As BPF.0                                   ' Holds 1 after the LCD has been initialised
    Dim LCD_I2C_tComOrData   As BPF.1                                   ' Holds 0 if data is to be sent to the LCD, else 1 for a command
    Dim LCD_I2C_tRSFlag      As BPF.2                                   ' Holds the state of the LCD's RS line
    Dim LCD_I2C_tEN_Param    As BPF.3                                   ' Used as a paremeter in LCD_I2C_Write to signal the state of the LCD's EN line

'---------------------------------------------------------------------------------------------------
    GoTo _LCD_I2C_Main_                                                 ' Jump over the replacement Print library subroutine

'---------------------------------------------------------------------------------------------------
' A replacement subroutine of the compiler's Print library, that uses a TCA6408 for I2C coms
' Input     : WREG holds the byte to send to the LCD
' Output    : WREG still holds the byte sent
' Notes     : If LCD_I2C_tInitialised is 0, the LCD will be initialised (is set to 1 after the LCD is initialised)
'           : Calls the LCD_I2C_Print() procedure
'
Sub __Print_()
    LCD_I2C_Print()
EndSub

'------------------------------------------------------------------------
' Send a byte to the alphanumeric LCD using a TCA6408 I2C port expander device
' Input     : WREG holds the byte to send
' Output    : WREG still holds the byte sent
' Notes     : If LCD_I2C_tInitialised is 0, the LCD will be initialised (is set to 1 after the LCD is initialised)
'
Proc LCD_I2C_Print()
    Dim wFSR0 As FSR0L.Word                                             ' Create a 16-bit SFR of FSR0L and FSR0H
    Dim wSaveFSR0 As Word Heap                                          ' Holds a copy of FSR0L and FSR0H
    Dim PP3 As Byte System                                              ' \
Global Dim LCD_I2C_bValue As PP3 Shared                                 ' / Holds the value to send to the LCD
    Dim PP3H As Byte System                                             ' \
    Dim bWregStore As PP3H                                              ' / Holds a copy of WREG
    bWregStore = WREG                                                   ' Save the contents of WREG
    wSaveFSR0 = wFSR0                                                   ' Save the contents of FSR0L and FSR0H because I2Cout uses them

    If LCD_I2C_tInitialised = 0 Then                                    ' Has the LCD been inititalised?
        TCA6408_SetAsOutputs()                                          ' No. So set the TCA chip's port as all outputs
        LCD_I2C_tComOrData = cLCD_SendControl                           ' Signal control bytes are being sent to the LCD
        LCD_I2C_Write(%00110000, 1)                                     ' EN line high and send command nibble
        LCD_I2C_Write(%00110000, 0)                                     ' EN line low and send command nibble again
        DelayMS 5                                                       ' A small delay after the command
        LCD_I2C_Write(%00110000, 1)
        LCD_I2C_Write(%00110000, 0)                                     ' EN line low
        DelayUS 100                                                     ' A small delay after the command
        LCD_I2C_Write(%00100000, 1)                                     ' EN line high and send command nibble
        LCD_I2C_Write(%00100000, 0)                                     ' EN line low
        DelayUS 100                                                     ' A small delay after the command
        LCD_I2C_Send(%00101000)                                         ' Send a control byte for '4-bit mode and n lines' to the LCD
        LCD_I2C_Send(%00001100)                                         ' Send a control byte for 'Enable display, cursor and blink off' to the LCD
        LCD_I2C_Send(%00000110)                                         ' Send a control byte for 'Move cursor after each write' to the LCD
        LCD_I2C_Send(%10000000)                                         ' Send a control byte for: 'Goto column 1 line 1' to the LCD
        LCD_I2C_tInitialised = 1                                        ' Indicate that the LCD is initialised
    EndIf

    LCD_I2C_bValue = bWregStore                                         ' Restore the value to write to the LCD
    If LCD_I2C_bValue = $FE Then                                        ' Is it a command byte?
        LCD_I2C_tRSFlag = 1                                             ' Yes. So change a flag so we know the next byte will be a command
    Else                                                                ' Otherwise...
        If LCD_I2C_tRSFlag = 1 Then                                     ' Is this is a command byte?
            LCD_I2C_tComOrData = cLCD_SendControl                       ' Yes. So signal a control bytes is being sent to the LCD
            LCD_I2C_Send(LCD_I2C_bValue)                                ' Send a control byte to the LCD
            #ifdef __LCD_COMMANDUS
                DelayUS __LCD_COMMANDUS                                 ' A delay between control bytes sent to the LCD
            #else
                DelayUS 2000                                            ' A default delay between control bytes sent to the LCD
            #endif
        Else                                                            ' Otherwise... It is a data byte
            LCD_I2C_tComOrData = cLCD_SendData                          ' Signal a control byte is being sent to the LCD
            LCD_I2C_Send(LCD_I2C_bValue)                                ' So... Send data to the LCD
            #ifdef __LCD_DATAUS
                DelayUS __LCD_DATAUS                                    ' A delay between data bytes sent to the LCD
            #else
                DelayUS 50                                              ' A default delay between data bytes sent to the LCD
            #endif
        EndIf
        LCD_I2C_tRSFlag = 0                                             ' Change a flag so the next byte will be send as data
    EndIf
    wFSR0 = wSaveFSR0                                                   ' Restore the contents of FSR0L and FSR0H because I2Cout used them
    WREG = bWregStore                                                   ' Restore the contents of WREG
EndProc

'---------------------------------------------------------------------------------------------------
' Send a data or control byte to the LCD
' Input     : LCD_I2C_tComOrData holds 0 for a data byte, 1 for a control byte
'           : pValue (LCD_I2C_bValue) holds the data value to send
' Output    : None
' Notes     : None
'
Proc LCD_I2C_Send(pValue As LCD_I2C_bValue)
    Dim PP0      As Byte System                                         ' \
    Dim bHighVal As PP0                                                 ' / Holds the high nibble data for the LCD
    Dim PP0H     As Byte System                                         ' \
    Dim bLowVal  As PP0H                                                ' / Holds the low nibble data fro the LCD
    Dim PP3      As Byte System                                         ' \
Global Dim LCD_I2C_bValue As PP3 Shared                                 ' / Holds the value to send to the LCD

    bHighVal = pValue & %11110000                                       ' Extract the upper nibble bits
    bLowVal = pValue << 4                                               ' Shift left for the lower nibble bits

    LCD_I2C_Write(bHighVal, 1)                                          ' EN line high and send the high nibble to the LCD
    LCD_I2C_Write(bHighVal, 0)                                          ' EN line low and send the high nibble byte to the LCD
    LCD_I2C_Write(bLowVal, 1)                                           ' EN line high and send the low nibble to the LCD
    LCD_I2C_Write(bLowVal, 0)                                           ' EN line low and send the low nibble byte to the LCD
EndProc

'---------------------------------------------------------------------------------------------------
' Write an 8-bit value to the LCD via the TCA6408 I2C expander device
' Input     : pValue (LCD_I2C_bValue) holds the 8-bit value to write
'           : pEN (LCD_I2C_tEN_Param) holds 1 if the EN line is to be made high, and 0 for low
'           : LCD_I2C_tComOrData holds 1 if the RS line is to be set for a data byte, and 0 for a control byte
' Output    : None
' Notes     : None
'
Proc LCD_I2C_Write(pValue As LCD_I2C_bValue, pEN As LCD_I2C_tEN_Param)
    Dim PP1     As Byte System                                          ' \
    Dim bShadow As PP1 = 0                                              ' / Holds the bits to write from the TCA6408 device
    Dim PP3     As Byte System                                          ' \
Global Dim LCD_I2C_bValue As PP3 Shared                                 ' / Holds the value to send to the LCD
'
' Move the bits of the value around to suit the pins used on the TCA6408 to the LCD
'
    bShadow.cLCD_I2C_D4_Bit = pValue.4                                  ' The LCD's D4 line bit
    bShadow.cLCD_I2C_D5_Bit = pValue.5                                  ' The LCD's D5 line bit
    bShadow.cLCD_I2C_D6_Bit = pValue.6                                  ' The LCD's D6 line bit
    bShadow.cLCD_I2C_D7_Bit = pValue.7                                  ' The LCD's D7 line bit
    bShadow.cLCD_I2C_RS_Bit = LCD_I2C_tComOrData                        ' The LCD's RS line bit (data or command)
    bShadow.cLCD_I2C_EN_Bit = pEN                                       ' The LCD's EN line bit
    TCA6408_hWrite8(cTCA6408_OUTPUT, bShadow)                           ' Write the value to the LCD from the TCA6408 device
EndProc

'-------------------------------------------------------------------------------------------------
' Initialise the TCA6408 device
' Input     : None
' Output    : None
' Notes     : Toggles the TCA device's Reset pin if the '$define TCA6408_Reset_Pin' is used in the main program
'
Proc TCA6408_Init()
$ifdef TCA6408_Reset_Pin                                                ' Has the TCA6408_Reset_Pin been defined?
    TCA6408_Reset()                                                     ' Yes. So reset the TCA6408 chip
$endif
    TCA6408_SetAsOutputs()                                              ' Set the TCA6408 chip's port as all outputs
EndProc

'-------------------------------------------------------------------------------------------------
' Reset the TCA6408 device
' Input     : None
' Output    : None
' Notes     : None
'
Proc TCA6408_Reset()
$ifdef TCA6408_Reset_Pin                                                ' Has the TCA6408_Reset_Pin been defined?
    PinLow TCA6408_Reset_Pin                                            ' Yes. So pull the reset pin to ground
    DelayMS 100                                                         ' Wait a short time for the reset to take place
    PinSet TCA6408_Reset_Pin                                            ' Bring the reset pin back to VDD
    DelayMS 10                                                          ' A short delay to allow the TCA device to stabilise
$endif
EndProc

'-------------------------------------------------------------------------------------------------
' Write bits to the expander port
' Input     : pValue (TCA6408_bValParam) holds the value to place on the TCA device's port
' Output    : None
' Notes     : None
'
Proc TCA6408_Out(pValue As TCA6408_bValParam)
Global Dim TCA6408_bValParam As Byte Shared

    TCA6408_hWrite8(cTCA6408_OUTPUT, pValue)
EndProc

'-------------------------------------------------------------------------------------------------
' Setup the TCA6408 as all outputs
' Input     : None
' Output    : None
' Notes     : None
'
Proc TCA6408_SetAsOutputs()
    TCA6408_hWrite8(cTCA6408_CONFIGURATION, %00000000)
EndProc

'-------------------------------------------------------------------------------------------------
' Write to the TCA6408 device via I2C
' Input     : pReg holds the register to write too
'           : pValue (TCA6408_bValParam) holds the value to write to the register
' Output    : None
' Notes     : None
'
Proc TCA6408_hWrite8(pReg As Byte, pValue As TCA6408_bValParam)
Global Dim TCA6408_bValParam As Byte Shared

    I2COut LCD_SDA_Pin, LCD_SCL_Pin, cTCA6408_ADDR1, pReg, [pValue]
EndProc

'---------------------------------------------------------------------------------------------------
_LCD_I2C_Main_:

$endif

Below is a demo program listing for a PIC18F26K40 device, using the above library. However, the library can be used on many PIC devices because it uses the software (bit-bashed) I2C command:

'
'   /\\\\\\\\\
'  /\\\///////\\\
'  \/\\\     \/\\\                                                 /\\\          /\\\
'   \/\\\\\\\\\\\/        /\\\\\     /\\\\\\\\\\     /\\\\\\\\   /\\\\\\\\\\\  /\\\\\\\\\\\  /\\\\\\\\\
'    \/\\\//////\\\      /\\\///\\\  \/\\\//////    /\\\/////\\\ \////\\\////  \////\\\////  \////////\\\
'     \/\\\    \//\\\    /\\\  \//\\\ \/\\\\\\\\\\  /\\\\\\\\\\\     \/\\\         \/\\\        /\\\\\\\\\\
'      \/\\\     \//\\\  \//\\\  /\\\  \////////\\\ \//\\///////      \/\\\ /\\     \/\\\ /\\   /\\\/////\\\
'       \/\\\      \//\\\  \///\\\\\/    /\\\\\\\\\\  \//\\\\\\\\\\    \//\\\\\      \//\\\\\   \//\\\\\\\\/\\
'        \///        \///     \/////     \//////////    \//////////      \/////        \/////     \////////\//
'                                  Let's find out together what makes a PIC Tick!
'
' Alphanumeric LCD interface demonstration using a TCA6408 I2C I/O expander device on a PIC18F26K40 device.
' The library replaces the compiler's alphanumeric Print library subroutine.
'
' Note. The library defaults to the TCA6408A device's ADDR pin pulled to ground, so it uses the slave address: cTCA6408_ADDR1
'
' Written for the Positron8 BASIC compiler by Les Johnson.
'
    Device = 18F26K40                                       ' Tell the compiler what device to compile for
    Declare Xtal = 64                                       ' Tell the compiler what frequency the device is operating at (in MHz)
    Declare Float_Display_Type = Fast                       ' Enable the faster, and more accurate, floating point to ASCII library routine
    Declare Auto_Variable_Bank_Cross = On                   ' Make sure all multi-byte variables remain within a single RAM bank for more efficiency
'
' Setup the I2C pins that connect to the TCA6408 device
'
$define LCD_SCL_Pin PORTC.3                                 ' Connects to the TCA6408 device's SCL pin
$define LCD_SDA_Pin PORTC.4                                 ' Connects to the TCA6408 device's SDA pin

$define LCD_Backlight_Pin PORTB.7                           ' Enables/Disables the LCD's backlight (active high)

    Include "TCA6408_LCD.inc"                               ' Load the TCA6408, Print replacement library into the program
'
' Create some variables for the demo
'
    Dim dCounter As Dword = 0
    Dim fCounter As Float = 0

'------------------------------------------------------------------------------------------------
' The main program starts here
' Display incrementing large variables on the LCD using the TCA6408 device as the I2C interface to it
'
Main:
    Setup()                                                 ' Setup the program and peripherals

    Cls                                                     ' Clear the LCD's display
    Do                                                      ' Create a loop
        Print At 1, 1, "Dword: ", Dec dCounter, "     ",    ' \ Display the values on the LCD
              At 2, 1, "Float: ", Dec1 fCounter             ' /
        Inc(dCounter, 100)                                  ' \ Increment the variables
        Inc(fCounter, 0.1)                                  ' /
        DelayMS 100                                         ' A small delay so the values can be seen incrementing
    Loop                                                    ' Do it forever

'------------------------------------------------------------------------------------------------
' Setup the program and peripherals
' Input     : None
' Output    : None
' Notes     : None
'
Proc Setup()
    Oscillator_64MHz()                                      ' Set the microcontroller to internal 64MHz operation
    DelayMs 100                                             ' Allow the LCD to stabilise
    PinHigh LCD_Backlight_Pin                               ' Illuminate the LCD's backlight
    TCA6408_Init()                                          ' Initialise the TCA6408 device
EndProc

'------------------------------------------------------------------------------------------------
' Set the microcontroller to internal 64MHz operation with an HFINTOSC_1MHZ fuse
' Input     : None
' Output    : None
' Notes     : None
'
Proc Oscillator_64MHz()
    OSCCON1 = %01100000
    OSCCON3 = %00000000
    OSCEN   = %00000000
    OSCFRQ  = %00001000             ' 64MHz
    OSCTUNE = %00000000
    Repeat: Until OSCSTATbits_HFOR = 1
EndProc

'------------------------------------------------------------------------------------------------
' Setup the fuses to use the internal oscillator on a PIC18F26K40, with the OSC pins set as I/O pins
'
Config_Start
    RSTOSC = HFINTOSC_1MHZ          ' With HFFRQ = 4MHz and CDIV = 4:1
    FEXTOSC = Off                   ' External Oscillator not enabled
    CLKOUTEN = Off                  ' CLKOUT function is disabled
    CSWEN = On                      ' Writing to NOSC and NDIV is allowed
    FCMEN = Off                     ' Fail-Safe Clock Monitor disabled
    MCLRE = EXTMCLR                 ' MCLR pin is MCLR
    PWRTE = On                      ' Power up timer enabled
    LPBOREN = Off                   ' LPBOREN disabled
    BOREN = On                      ' Brown-out Reset enabled and controlled by software (BORCONbits_SBOREN is enabled)
    BORV = VBOR_285                 ' Brown-out Reset Voltage set to 2.85V
    ZCD = Off                       ' ZCD disabled. ZCD can be enabled by setting the ZCDSEN bit of ZCDCON
    PPS1WAY = Off                   ' PPSLOCK bit can be set and cleared repeatedly (subject to the unlock sequence)
    STVREN = Off                    ' Stack full/underflow will not cause Reset
    Debug = Off                     ' Background debugger disabled
    XINST = Off                     ' Extended Instruction Set and Indexed Addressing Mode disabled
    SCANE = Off                     ' Scanner module is Not available for use. SCANMD bit is ignored
    LVP = Off                       ' Low Voltage programming disabled
    WDTE = Off                      ' WDT disabled
    WDTCPS = WDTCPS_13              ' Watchdog Divider ratio 1:x (8 seconds)
    WDTCWS = WDTCWS_7               ' Window always open (100%). Software control. Keyed access not required
    WDTCCS = LFINTOSC               ' WDT input clock selector->WDT reference clock is the 31.2kHz HFINTOSC output
    WRT0 = Off                      ' Block 0 (000800-001FFF) not write-protected
    WRT1 = Off                      ' Block 1 (002000-003FFF) not write-protected
    WRTC = Off                      ' Configuration registers (300000-30000B) not write-protected
    WRTB = Off                      ' Boot Block (000000-0007FF) write-protected
    WRTD = Off                      ' Data EEPROM not write-protected
    Cp = Off                        ' UserNVM code protection disabled
    CPD = Off                       ' DataNVM code protection disabled
    EBTR0 = Off                     ' Block 0 (000800-001FFF) not protected from table reads executed in other blocks
    EBTR1 = Off                     ' Block 1 (002000-003FFF) not protected from table reads executed in other blocks
    EBTRB = Off                     ' Boot Block (000000-0007FF) not protected from table reads executed in other blocks
Config_End

The LCD's pin connections to the TCA6408 device are listed in the "TCA6408_LCD.inc" file, and are:

' The LCD's RS pin is connected to bit-5 of the TCA chip's port
' The LCD's EN pin is connected to bit-4 of the TCA chip's port
' The LCD's D4 pin is connected to bit-3 of the TCA chip's port
' The LCD's D5 pin is connected to bit-2 of the TCA chip's port
' The LCD's D6 pin is connected to bit-1 of the TCA chip's port
' The LCD's D7 pin is connected to bit-0 of the TCA chip's port


However, they can easily be changed to suit by altering the values in the appropriate $defines within the "TCA6408_LCD.inc" file:

$define cLCD_I2C_RS_Bit  5    ' The LCD's RS line bit
$define cLCD_I2C_EN_Bit  4    ' The LCD's EN line bit
$define cLCD_I2C_D4_Bit  3    ' The LCD's D4 line bit
$define cLCD_I2C_D5_Bit  2    ' The LCD's D5 line bit
$define cLCD_I2C_D6_Bit  1    ' The LCD's D6 line bit
$define cLCD_I2C_D7_Bit  0    ' The LCD's D7 line bit


The library source file and the demo source file are attached below, along with the TCA6408A datasheet: