Beefy Boxes and Bandwidth Generously Provided by pair Networks
Your skill will accomplish
what the force of many cannot
 
PerlMonks  

i2c attached LCD Character Display for a Raspberry Pi

by anita2R (Scribe)
on May 24, 2016 at 16:28 UTC ( [id://1163993]=CUFP: print w/replies, xml ) Need Help??

I was unable to find a Perl script to make my SainSmart LCD character display with i2c 'backpack' work from my Raspberry Pi.

Code for using these devices is available mainly for the Arduino. There is also code in C and python, but I could not find a simple working example in Perl. There is a module for an HTBackpackV2 (HiPi::Interface::HTBackpackV2), but I wasn't able to get this working with the SainSmart LCD.

I found a python script written by Matt Hawkins here: https://bitbucket.org/MattHawkinsUK/rpispy-misc/raw/master/python/lcd_i2c.py and I used his python script as the basis for my Perl Script - thanks Matt.

This script only provides basic display functionality and is run by calling it with two parameters, -s a string of text to display and -l the line number to display the text on. Hopefully it will provide both a way for others to get one of these LCD's working as well as provide the basis for more comprehensive implementations. The subroutines writeByte and sendByte are the key ones, together with the initialization sequence in the init subroutine.

The script must be run as root to set up the i2c object, but drops back to the regular user immediately after - enter your user name and user group in the two variables $user and $group which you will find at lines 39 & 40.

When called with no text and no line parameter the LCD initialization code is run.

After the code I have added some explanation about the way the data is transferred from the backpack to the LCD, including the transition from the power-up 8-bit mode to 4-bit mode. It took me some time to get my head round this!

In addition to -l 1 & -l 2 for the two lines, -l 50 & -l 51 turn the display off and back on. When the display is turned off with -l 50, the data remains in the LCD's DDRAM memory and the previous text will be displayed by -l 51 (a simple flashing display can be implemented)

If no line number is given, the text is printed on line 1 and overflows to line 2 if required. All existing text is cleared.

Examples (run as root/sudo):

lcd_i2c.pl => initialize
lcd_i2c.pl -s "" => initialize
lcd_i2c.pl -s "A string of characters" => prints line 1: 'A string of' & line 2: 'characters' (note: splits on word break)
lcd_i2c.pl -s "A string of characters" -l 1 => prints line 1: 'A string of char' & line 2 unchanged ('characters')
lcd_i2c.pl -l 50 => display turned off
lcd_i2c.pl -l 51 => display turned on. Last data on both lines displayed again
lcd_i2c.pl -s " " => clears display (note: -s "" initializes display)


Code for a Raspberry Pi with SainSmart 2 line, 16 character LCD display with i2c backpack at 0x3f, attached to the Pi's default i2c pins.

#!/usr/bin/perl # # lcd_i2c.pl # anita2R # Version 1.00, 24th May 2016 # # A script to display data on a SainSmart LCD character display # connected via the i2c bus # # The data to be displayed is passed to the routine as parameter -s # The line number is passed as parameter -l # # If -s is empty or null (""), the initialization code is run # If there is a text string and a line number the text is displayed # on that line and is truncated/padded to fit # If there is no line number, the text is displayed on line 1 and # overflowed onto line 2 - existing text on both lines is overwritten # Other line parameters: # -l 50 turns display off # -l 51 turns display on # use Getopt::Std; use HiPi::Utils; use HiPi::BCM2835::I2C qw( :all ); # use strict; # # get the parameters (-s, -l) getopt('sl:'); # pass parameters to meaningful variables our ( $opt_s, $opt_l ); my $lnParam = $opt_l; my $strParam = $opt_s; # # setup bus number, speed and i2c 'backpack' address my $i2cBus = BB_I2C_PERI_1; # bus #1 (vers.1 used bus #0) my $i2cSpeed = BB_I2C_CLOCK_100_KHZ; # said to be reliable on RPi my $i2cAddr = 0x3f; # SainSmart lcd backpack at 0x3f # # setup regular user & group id's - for permission drop-back my $user = <your user name>; my $group = <your user group>; # # setup values specific to the 2 row 16 character SainSmart i2c LCD my $width = 16; # Maximum characters per line my $lines = 2; # LCD lines my $dataMode = 0x01; # Mode - send data my $cmdMode = 0x00; # Mode - send command my $line1 = 0x80; # Address for the 1st line my $line2 = 0xC0; # Address for the 2nd line my $blStatus = 0x08; # Backlight mask On=0x08 - Off=0x00 my $loMask = 0xF0; # Masks off low-order bits in byte my $sEN = 0x04; # 0000 0100 - mask to set enable bit my $cEN = 0x0B; # 0000 1011 - mask to clear enable bit & data # # hold/wait times are in the code but do not seem to be required # perhaps the delay inherent in 12c serial to parallel conversion # is sufficient my $enHold = 0; # hold enable high (microseconds) my $wait = 0; # wait before next write (microseconds) # # Test if initialization required # (no text in -s & line not 50 or 51 (display off / on) my $initFlag = 0; if (( $strParam eq "" && $lnParam != 50 && $lnParam != 51 )) { $initFlag = 1; } # # Change line number (if any) to line address / exit if not valid # line = 50 display off, line = 51 display on, 0 = no -l parameter if ( $lnParam != 0 ) { if ( $lnParam == 1 ) { $lnParam = $line1; } elsif ( $lnParam == 2 ) { $lnParam = $line2; } elsif (( $lnParam != 50 && $lnParam != 51 )) { # non valid -l value exit 1; } } # # create an i2c device object my $objI2c = HiPi::BCM2835::I2C->new( peripheral => $i2cBus, address => $i2cAddr ); HiPi::Utils::drop_permissions_name( $user, $group ); $objI2c->set_baudrate( $i2cSpeed ); # # # ****************************************** # # ************** Main Program ************** # # if ( $initFlag ) { # init flag = 1, initialise display &init; } elsif ( $lnParam == 50 ) { # line = 50, turn display off &sendByte( 0x08, $cmdMode ); } elsif ( $lnParam == 51 ) { # line = 51, turn display on &sendByte( 0x0C, $cmdMode ); } else { # there is text - print it, depending on line number my ( $data1, $data2 ); if ( $lnParam == 0 ) { # no line specified - print on line 1 & overflow to line 2 if ( length($strParam) > $width ) { # overflow onto line 2 # get substring as long as display width $data1 = substr( $strParam, 0, $width ); # cut at last space so as not to split a word ($data1) = split /\s+(?=\S*+$)/, $data1; # remainder of string on second line $data2 = substr( $strParam, length($data1) + 1 ); } else { # text fits on line 1 $data1 = $strParam; $data2 = ""; } # pad / truncate both lines $data1 = &padTrStr( $data1 ); $data2 = &padTrStr( $data2 ); # print both lines &prntStr( $data1, $line1 ); &prntStr( $data2, $line2 ); } else { # pad / truncate then print on one (specified) line $data1 = &padTrStr( $strParam ); &prntStr( $data1, $lnParam ); } } # exit 0; # # # ****************************************** # # ************** Subroutines *************** # # # *************** Initialize *************** # sub init { # 8-bit write (no LCD data in lower nibble) &writeByte( 0x30, $cmdMode ); # 0011 xxxx Sets 8-bit mode &writeByte( 0x30, $cmdMode ); # repeat in case LCD in 4-bit # mode but out of sync &writeByte( 0x20, $cmdMode ); # 0010 xxxx Sets 4-bit mode # *** now in 4-bit mode *** # both nibbles of data sent to LCD using sendByte &sendByte( 0x28, $cmdMode ); # 0010 1000 2 lines & small chars. &sendByte( 0x0C, $cmdMode ); # 0000 1100 Display On, no cursor &sendByte( 0x01, $cmdMode ); # 0000 0001 Clear display } # # *************** Write Byte *************** # sub writeByte { my $byte = $_[0]; # # writes byte to i2c object (LCD) # Enable 'control port' toggled high-low # # write data with enable set $objI2c->bus_write( $byte | $sEN ); $objI2c->delayMicroseconds($enHold); # clear enable, clear data, keep backlight & mode $objI2c->bus_write( $byte | $cEN ); $objI2c->delayMicroseconds( $wait ); } # # **************** Send byte *************** # sub sendByte { my $data = $_[0]; my $mode = $_[1]; # # splits data into high & low-order # puts each nibble into high-order bits # then adds mode & backlight status bits into low-order bits # # mask off 4 low-order bits & 'add' mode and backlight my $data_high = (( $data & $loMask ) | $mode | $blStatus ); # shift 4 low bits to high bits, mask-off low order bits # & 'add' mode and backlight my $data_low = ((( $data << 4 ) & $loMask ) | $mode | $blStatus ); # Send both nibbles of data to write routine &writeByte( $data_high ); &writeByte( $data_low ); } # # ************* Print a String ************* # sub prntStr { my $message = $_[0]; my $line = $_[1]; my $i; # # send address for required line to LCD &sendByte( $line, $cmdMode ); # iterate through message string for ( $i = 0 ; $i < $width ; $i++ ) { # send bytes to be displayed (use character values) &sendByte( ord( substr( $message, $i, 1 )), $dataMode ); } } # # ********* Pad/Truncate a String ********** # sub padTrStr { my $string = $_[0]; # truncate $string = substr( $string, 0, $width ); # pad message with spaces - so it fills the line $string = sprintf( "%-${width}s\n", $string ); return ($string); }

Mode of operation & initialization

The i2c backpack transfers the 8 bits of data sent to it over the i2c bus as follows:
high-order bits go to the LCD high-order data lines
low-order bits go to the LCD control ports

3 2 1 0 Backlight Enable Read/Write Register select

When the LCD is in 8-bit mode it never gets any data on the low-order data lines from the backpack.
The command to switch the LCD to 4-bit mode is binary 0010 xxxx where x is 'don't care', so 4-bit mode can be set even when the low-order data lines are not connected.

Once in 4-bit mode the data presented to the high order data lines is alternately transferred by the LCD's controller to the low-order data lines, so recreating the original 8-bit commands or 8-bit character data.

On power-up, the LCD device is in 8-bit mode, but it could already have been initialized and in 4-bit mode and it could possibly be in 4-bit mode but out of sync so that the first nibble sent goes to the low-order data lines.

To account for these three possible states the initialization sequence uses the writeByte subroutine which sends single bytes to the backpack.

Binary 0011 0000 is sent twice - only 0011 appears on the high-order data lines and the low-order lines are undefined.
If the LCD is in 8-bit mode it will only receive 0011 on its high-order data lines - and this will reset 8-bit mode (twice!)
If the LCD is already in 4-bit mode the binary data 0011 0000 will place 0011 on the high-order data lines and 0011 on the low-order data lines, as the LCD controller moves the second nibble of data to the low-order lines. This will set 8-bit mode.
If the LCD is in 4-bit mode, but out of sync, the first 0011 will be sent to the high-order data lines but transferred by the LCD's controller to the low-order data lines. As the preceding data on the high-order data lines is unknown, the result of this write is undetermined. The second write of 0011 will be to the high-order data lines and will set the device to 8-bit mode.

The third write places 0010 on the high-order data lines and this places the device in 4-bit mode.

From here on the data is sent using sendByte which sends both nibbles of the data in the sequential 4-bit mode with the LCD controller putting alternate nibbles onto the low-order data lines.

Binary 0010 1000 Sets 2 lines & small characters (and 4-bit again)
The last initialization command is binary 0000 1100 which turns the display On as well as setting it to 'no cursor'. Modify as required.
The bits are:

7 6 5 4 3 2 1 0 0 0 0 0 1 C B x x=don't care Bit 3=1 display on, C=1 cursor on, B=1 blink cursor

All data written to the LCD must be set by toggling the enable control port high then low. The minimum high time (as seen on one manufacturer's datasheet) was 300 nanoseconds
The next enable high after enable is set low requires at least a further 200 nanoseconds wait for a combined high-low cycle of 500 nanoseconds.

The code has enable high delays and wait delays after writes, but in practice with the i2c serial transmission, the backpack serial to parallel conversion and other delays inherent in the code and a Raspberry Pi, the program works with the delays set to zero. If implemented in faster environments, delays might become necessary, so the delay code has been left in place.

Replies are listed 'Best First'.
Re: i2c attached LCD Character Display for a Raspberry Pi
by jmlynesjr (Deacon) on May 25, 2016 at 20:48 UTC

    anita2R Nice Work!

    I plan to look at your code in detail to learn something new about the RPI. I have used the chipKIT UNO32 with a Sparkfun I2C backpack.

    How does the 4/8 bit mode stuff come into play over an I2C backpack interface? I thought 4/8 bit mode related to using a 4 or 8 bit parallel interface. This should be handled by the backpack processor.

    I think RPI users could be a source of new Perl users. More posts like yours might help this along.

    Update: After reading your code it appears that the SainSmart LCD you are using has a much lower level interface than the Sparkfun unit that I used. The Sparkfun backpack hides all of the bit manipulation from the user.

    James

    There's never enough time to do it right, but always enough time to do it over...

      I wasn't aware that the SparkFun module provided more than just serial to parallel conversion.

      The SainSmart backpack is a simple (and cheap) device. I just looked, their 2 x 16 LCD with i2c backpack is under $10 US. There are other similar backpacks out there that you can wire up yourself to an LCD display which are now really cheap. The disadvantage is that the information on using them is poor.

      While trying to get mine working I read numerous posts from people who could not get theirs to work. In the end I went back to basics, and worked out how the backpacks work and how the LCD 4-bit mode works, and then avoided the high level libraries. Simple i2c communication using the HiPi::BCM2835::I2C was all that was needed. The module is described as: The HiPi::BCM2835::I2C module provides direct access to the Broadcom I2C Serial peripheral.

      After that it came down to understanding the commands and the way the parallel side of the i2c backpack was connected to the LCD.

      I will write up some more detailed notes on the 8-bit/4-bit issue and initialization and post it later

      I have a Perl script that collects data from an spi bus-attached ADC that monitors some voltages. I should tidy up the script and post that as well

        anita2R

        I went back and looked at my chipKIT code. Turns out my Sparkfun LCD is a SPI interface. My SF 4 digit LED display uses I2C. In either case neither required any bit twiddling. Their backpack has an on-board PIC that abstracts out the HD44780 handling. Ease of use and no soldering comes at a higher price(double) than you have with the SainSmart product.

        Update:I read through the HiPi docs last night. Good stuff! The developer, Mark Dootson, has done much work on wxPerl and I believe that wxPerl will run on the RPI, if you need to do any GUI work.

        For Example:

        // SerLCDTest.pde - Serial Interface Version for use with the chipKIT +UNO // // This display uses the HD44780 LCD controller and a PIC16F688 serial + interface // // Test file for the Sparkfun Serial LCD used with the chipKIT UNO // Uses UART1(pin 40 TX) so UART0/USB can be used for debug message +s // Combined header and library files for initial testing of the dis +play. // Hope to split into a library after testing. // // James M. Lynes, Jr. // Last Modified: December 24,2012 // // This code is public domain, but you buy me a beer(or Dr. Pepper) // if you use this and we meet some day(Beerware License). // // Constant Definitions // // Display Commands // #define COMMAND 0xFE // Command Code #define CLEARDISP 0x01 // Clear the display #define CURSORHOME 0x02 // Send the cursor home #define CURSORRIGHT 0x14 // Move the cursor right one pos +ition #define CURSORLEFT 0x10 // Move cursor left one position #define SCROLLRIGHT 0x1C // Scroll the display right #define SCROLLLEFT 0x18 // Scroll the display left #define DISPLAYON 0x0C // Turn the display on #define DISPLAYOFF 0x08 // Turn the display off #define UCURSORON 0x0E // Turn underline cursor on #define UCURSOROFF 0x0C // Turn underline cursor off #define BOXCURSORON 0x0D // Turn box cursor on #define BOXCURSOROFF 0x0C // Turn box cursor off #define LINE1 0x80 // Row 1 COl 1 Address #define LINE2 0xC0 // Row 2 COl 1 Address #define RESET 0x12 // Reset code, send at 9600 baud i +mmediately after POR // // Configuration Commands // #define CONFIG 0x7C // Configuration Code #define B2400 0x0B // Set 2400 baud #define B4800 0x0C // Set 4800 baud #define B9600 0x0D // Set 9600 baud #define B144K 0x0E // Set 14400 baud #define B192K 0x0F // Set 19200 baud #define B384K 0x10 // Set 38400 baud #define SAVESPLASH 0x0A // Save a new splash message #define TOGGLESPLASH 0x09 // Toggle splash screen on and +off #define BACKLIGHTOFF 0x80 // Turn the backlight off #define BACKLIGHTMED 0x8F // Turn the backlight to 50% #define BACKLIGHTHIGH 0x9D // Turn the backlight to 100% // // Misc Defines // #define LCDTYPE 0x03 // Type 2 line x 16 characters #define LCDDelay 0x10 // General delay timer #define LCDDelay2 0x200 // Scroll timer // // Function Protypes // void LCDClear(); // Clear the display void LCDSelectLineOne(); // Position cursor to the beginning + of line 1 void LCDSelectLineTwo(); // Position cursor to the beginning + of Line 2 void LCDGoTo(int position); // Position cursor to a specific + character 0-15, 16-31 void LCDBacklightOn(); // Turn the backlight on void LCDBacklightOff(); // Turn the backlight off void LCDCursorHome(); // Position cursor to home void LCDCursorRight(); // Set cursor to move left to rght void LCDCursorLeft(); // Set cursor to move right to left void LCDScrollRight(); // Scroll the display right void LCDScrollLeft(); // Scroll the dispaly left void LCDDisplayOn(); // Turn the display on void LCDDisplayOff(); // Turn the display off void LCDUCursorOn(); // Turn the underline cursor on void LCDUCursorOff(); // Turn the underline cursor off void LCDBoxCursorOn(); // Turn the box cursor on void LCDBoxCursorOff(); // Turn the box cousor off void LCDBlackStart(); // Restart from an unknown state void LCDSaveSplash(char*, char*); // Save a new spash display void LCDToggleSplash(); // Toggle the splash display on/off/ +on void LCDSetBrightness(int); // Set the screen brightness 128 +-157(0x80-0x9D) void LCDSetBaud(int); // Set a new baud rate // // Test Function Prototypes // void tstwrite(); // Write out two lines of text void tstmoves(); // Try some cursor moves void tstscroll(); // Try some scrolling void tstbrightness(); // Cycle the backlight high to off // // Main Program // void setup() { delay(3000); // Time to allow the splash screen to disp +lay Serial.begin(9600); // Start serial communications for debu +g messages Serial.println("Initialized UNO Serial/USB Port"); Serial1.begin(9600); // Initialize the UART1 hardware Serial.println("Initialized UNO UART1"); LCDClear(); // Reset the LED display, cursor to positio +n 1 Serial.println("Cleared the LCD Display"); LCDSetBrightness(BACKLIGHTHIGH);// Set display to max brightness } void loop() { Serial.println("Mainloop"); Serial.println("Writes"); tstwrite(); delay(2000); Serial.println("Moves"); tstmoves(); delay(10000); Serial.println("Scrolls"); tstscroll(); delay(2000); // Serial.println("Brightness"); // tstbrightness(); // delay(2000); } // Test Subroutines void tstwrite() { LCDClear(); LCDSelectLineOne(); Serial1.print("Merry Christmas"); LCDSelectLineTwo(); Serial1.print(" to my Sweetie "); } void tstmoves() { LCDClear(); LCDSelectLineOne(); Serial1.print("789ABCDEF6543210"); LCDSelectLineTwo(); LCDGoTo(24); Serial1.print("@"); LCDCursorRight(); LCDCursorRight(); Serial1.print("#"); LCDCursorHome(); Serial1.print("$"); LCDCursorHome(); LCDCursorLeft(); // LCDCursorLeft(); Serial1.print("%"); } void tstscroll() { LCDClear(); LCDSelectLineOne(); Serial1.print("0123456789ABCDEF"); delay(LCDDelay); for(int i=1 ; i<9; i++){ LCDScrollLeft(); } for(int i=1 ; i<9; i++){ LCDScrollRight(); } } void tstbrightness() { tstwrite(); LCDSetBrightness(BACKLIGHTHIGH); LCDSetBrightness(BACKLIGHTMED); LCDSetBrightness(BACKLIGHTOFF); } // Library Subroutines void LCDClear() { Serial1.write(COMMAND); Serial1.write(CLEARDISP); // Clear the display delay(LCDDelay); } void LCDSelectLineOne() { Serial1.write(COMMAND); Serial1.write(LINE1); // Select line 1 delay(LCDDelay); } void LCDSelectLineTwo() { Serial1.write(COMMAND); Serial1.write(LINE2); // Select line 2 delay(LCDDelay); } void LCDGoTo(int position) { if(position < 16) // Go to specific position 0-31 { Serial1.write(COMMAND); Serial1.write(position + 128); } else if(position < 32) { Serial1.write(COMMAND); Serial1.write(position + 48 + 128); } else { LCDGoTo(0); } delay(LCDDelay); } void LCDBacklightOn() { Serial1.write(CONFIG); Serial1.write(BACKLIGHTHIGH); // Turn the backlight on delay(LCDDelay); } void LCDBacklightOff() { Serial1.write(CONFIG); Serial1.write(BACKLIGHTOFF); // Turn the backlight off delay(LCDDelay); } void LCDCursorHome() { Serial1.write(COMMAND); Serial1.write(CURSORHOME); // Position the cursor to home delay(LCDDelay); } void LCDCursorRight() { Serial1.write(COMMAND); Serial1.write(CURSORRIGHT); // Move cursor right one positi +on delay(LCDDelay); } void LCDCursorLeft() { Serial1.write(COMMAND); Serial1.write(CURSORLEFT); // Move cursor left one position delay(LCDDelay); } void LCDScrollRight() { Serial1.write(COMMAND); Serial1.write(SCROLLRIGHT); // Scroll the display right one + position delay(LCDDelay2); } void LCDScrollLeft() { Serial1.write(COMMAND); Serial1.write(SCROLLLEFT); // Scroll the display left one p +osition delay(LCDDelay2); } void LCDDisplayOn() { Serial1.write(COMMAND); Serial1.write(DISPLAYON); // Turn the display on delay(LCDDelay); } void LCDDisplayOff() { Serial1.write(COMMAND); Serial1.write(DISPLAYOFF); // Turn the display off delay(LCDDelay); } void LCDUCursorOn() { Serial1.write(COMMAND); Serial1.write(UCURSORON); // Turn the underline cursor on delay(LCDDelay); } void LCDUCursorOff() { Serial1.write(COMMAND); Serial1.write(UCURSOROFF); // Turn the underline cursor off delay(LCDDelay); } void LCDBoxCursorOn() { Serial1.write(COMMAND); Serial1.write(BOXCURSORON); // Turn the box cursor on delay(LCDDelay); } void LCDBoxCursorOff() { Serial1.write(COMMAND); Serial1.write(BOXCURSOROFF); // Turn the box cursor off delay(LCDDelay); } void LCDBlackStart() { Serial1.write(RESET); // Restart from unknown condition delay(LCDDelay); } void LCDSaveSplash(char *l1, char *l2) { LCDSelectLineOne(); // Replace the splash screen Serial1.print(l1); LCDSelectLineTwo(); Serial1.print(l2); Serial1.write(CONFIG); Serial1.write(SAVESPLASH); delay(LCDDelay); } void LCDToggleSplash() { Serial1.write(CONFIG); // Toggle the splash screen on/o +ff/on Serial1.write(TOGGLESPLASH); delay(LCDDelay); } void LCDSetBrightness(int level) { Serial1.write(CONFIG); // Change the brightness level Serial1.write(level); delay(LCDDelay); } void LCDSetBaud(int rate) { Serial1.write(CONFIG); // Change the baud rate Serial1.write(rate); delay(LCDDelay); }

        James

        There's never enough time to do it right, but always enough time to do it over...

Re: i2c attached LCD Character Display for a Raspberry Pi
by toolz (Initiate) on Aug 11, 2023 at 21:21 UTC
    Thank you for posting this. I spent way too much time trying to translate the Python libraries I found online, and then way, way too much time trying to get it to work in 8-bit mode. I wrote it in module format and plan to submit it to cpan.org.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: CUFP [id://1163993]
Approved by Corion
Front-paged by ww
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others chanting in the Monastery: (6)
As of 2024-03-28 16:51 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found