NSLU2-Linux
view · edit · print · history

Interfacing to the LCD display

I wanted to interface a generic HD44780 based LCD display to my NSLU2. There are many different displays of different dimensions available meeting this standard, and its relatively easy to programme. These LCDs are relatively cheap, starting at a few dollars for a small device up to a few tens of dollars for the larger ones.

When thinking about connecting any hardware peripheral to the NSLU2, the problem is there are no general purpose I/O pins available. I've seen projects using the USB interface to connect LCDs, but only hints of projects using the I2C? bus. In this article, I'm going to detail how I implemented an I2C? LCD interface.

Typical LCDs don't come with I2C? interfaces built in. However cheap PIC microcontrollers can implement an I2C? bus for a couple of dollars. I chose to use a 16F88, an 18 pin DIP with two eight bit ports available. My design uses two of these 16 I/O pins for the I2C? interface, seven for the LCD interface and leaves seven for you to implement other cool stuff such as A/D inputs, keypad reading inputs or general purpose outputs. Using these other 16F88 I/O lines, I've added a battery voltage monitor and some general purpose switches accessible from the NSLU2 software through the I2C? bus.

Interfacing the I2C? to the NSLU2

There are other pages on this wiki that describe where the I2C? bus can be accessed on the NSLU2. I'm not going to repeat the information here. However there's no information yet on this wiki about implementing the I2C? interface in hardware. That's what I'm providing.

The schematic diagram is really simple. Just the 16F88, a diode, capacitor and a few resistors. I assembled them and some sockets onto a small piece of matrix board and now constructed several of them for my slugs.

<<INSERT PHOTO OF CIRCUIT DIAGRAM HERE - HOW DOES ONE UPLOAD A PHOTO??>>

In the interim, without a circuit diagram, here are the 16F88 pin assignments:

 Pin 17: PORT A bit 0 = D4 on LCD
 Pin 18: PORT A bit 1 = D5 on LCD
 Pin 1: PORT A bit 2 = D6 on LCD
 Pin 2: PORT A bit 3 = Analogue Input - tied to 3.3V rail with 22k
 Pin 3: PORT A bit 4 = Analogue Input - Battery Voltage via 1:2 voltage divider
 Pin 4: PORT A bit 5 = pulled to VDD via 22k (This is normally reset input)
 Pin 15: PORT A bit 6 = NC
 Pin 16: PORT A bit 7 = D7 on LCD

 Pin 6: PORT B bit 0 = E on LCD
 Pin 7: PORT B bit 1 = SD on I2C?  (I added my own 4k7 pullup to 3.3V)
 Pin 8: PORT B bit 2 = RW on LCD
 Pin 9: PORT B bit 3 = RS on LCD
 Pin 10: PORT B bit 4 = CK on I2C?  (I added my own 4k7 pullup to 3.3V)
 Pin 11: PORT B bit 5 = Switch3 input. Pulled to VDD via 22k. Switched low.
 Pin 12: PORT B bit 6 = Switch2 input. Pulled to VDD via 22k. Switched low.
 Pin 13: PORT B bit 7 = Switch1 input. Pulled to VDD via 22k. Switched low.

 Pin 14: VDD connected to NSLU2 +5V USB supply via a 1N4001 diode
 Pin 5: VSS connected to NSLU2 0 Volt supply

Note that because the NSLU2 I2C? bus uses 3V3 (i.e. 3.3V) levels and the 16F88 and LCD devices are 5V, you need to pull a trick to interface the two. Thankfully, the I2C? bus is open drain, and the 16F88 and LCD devices work well down to a little over 4V (at least that's what I've found on the ones I've built). So by picking 5V off the USB port on the NSLU2, dropping this by 0V6 through a diode, you can power the LCD and 16F88 at 4V4. At this reduced voltage, the 16F88 will recognise the 3V3 I2C? signalling just fine. Nothing further needs to be done to translate levels.

Compiling an image with your I2C? code embedded

As mentioned above, I chose the OpenWRT? distribution for my NSLU2. I had some trouble compiling a working kernel so I will make some notes here to get you going. I have still never been able to compile a working kernel with the svn source. If somebody has a working kernel .config file they'd like to post - please do so! (The downloaded openwrt-kamikaze comes with a working .config which is easy to tweak to your requirements)

  1. I started with the OpenWRT tarball and customised it.
  2. tar zxf sources/openwrt-kamikaze_7.09.tgz
  3. ln -s packages/utils/i2c-tools/ openwrt-kamikaze_7.09/package/i2c-tools
  4. mkdir openwrt-kamikaze_7.09/dl
  5. cp IPL_ixp400NpeLibrary-2_4.zip openwrt-kamikaze_7.09/dl (You downloaded this from the Intel site, right? Refer to the instructions on this wiki.
  6. cd openwrt-kamikaze_7.09
  7. make menuconfig (Enable utilities->i2c-tools i.e. type 'y' so that it shows with an asterisk)
  8. make kernel_menuconfig (Need to enable i2c in the kernel)
  9. make (This step will take a LONG time. If the build works, the image file will be found in openwrt-kamikaze_7.09/bin - its the big file - and can be flashed to the NSLU2 using upslug)

Now its time to add your LCD code and compile it into the build

  1. cd .. (change to the level above the OpenWRT? tree you just compiled)
  2. mkdir my-additions my-additions/files my-additions/src (these are directories where you're going to install your I2C? source code. I create it in the level above the OpenWRT? tree and link it in so that I can blow away the OpenWRT? tree and start again without blowing away my LCD/I2C? code)
  3. cp top-Makefile my-additions (provided below)
  4. cp src-Makefile echo_lcd.c i2c-dev.h my-additions/src (The first two are provided below. The i2c-dev.h file can be copied from openwrt-kamikaze_7.09/build_armeb/i2c-tools-3.0.0/include/linux after you've compiled openwrt the first time)
  5. ln -s my-additions openwrt-kamikaze_7.09/package/my-additions (this links your code into the OpenWRT? structure so that it gets built)
  6. cd openwrt-kamikaze_7.09 && make menuconfig (enable base system -> my-additions i.e. type 'y' so that it shows with an asterisk)
  7. make (this time, the i2c code will be compiled into the image)

Programme to write to an LCD connected via I2C? to the NSLU2 (echo_lcd.c)

I found several example C programmes both on this wiki as well as generally on the web, but none of them worked without hacking on my OpenWRT? based NSLU2. In any case, all the examples seemed to be for interfacing I2C? EEPROMs?, and I wanted to be able to echo strings to the I2C? bus rather like you'd echo strings to an RS-232 port.

Here's an example of a programme to echo strings to the I2C? bus, similar to echo in shell script.

 #include <stdio.h>
 #include <stdlib.h>
 #include <unistd.h>
 #include <fcntl.h>
 #include <errno.h>
 #include <string.h>
 #include <sys/types.h>
 //include <linux/i2c-dev.h>
 #include "i2c-dev.h"

 #define I2C?_ADDRESS 0x32
 #define BUFSIZE 256

 int
 main (int argc, char **argv)
 {
  int fd;
  int length ;
  int n, i;
  char *s ;
  unsigned char buf[BUFSIZE+1];

  if (argc < 2)
  {
    fprintf (stderr, "Usage: \s STRING [STRING] ...\n", argv[0]);
    exit (1) ;
  }

  if ((fd = open("/dev/i2c-0", O_RDWR)) < 0)
  {
    printf("Error opening i2c port!\n");
    exit (1);
  }

  if (ioctl(fd,I2C?_SLAVE,I2C?_ADDRESS) < 0)
  {
    printf("Error setting i2c address!\n");
    exit (1);
  }

  if (ioctl(fd,I2C?_RETRIES,2) < 0)
  {
    printf("Error setting i2c retries!\n");
    exit (1);
  }

  for (i=1 ; i < argc ; i++)
  {
    if (strlen(argv[i]) == 0)
      continue ;

    length = 0 ;

    if (i > 1))
      buf[length++]=' ' ;

    for (s=argv[i] ; *s && (length < BUFSIZE) ;)
    {
      if (*s == '\\')
      {
        switch (*++s)
        {
          case '0': buf[length++]=0x00 ; s++ ; break ;
          case '1': buf[length++]=0x01 ; s++ ; break ;
          case '2': buf[length++]=0x02 ; s++ ; break ;
          case '3': buf[length++]=0x03 ; s++ ; break ;
          case '4': buf[length++]=0x04 ; s++ ; break ;
          case 'n': buf[length++]=0x0a ; s++ ; break ;
          case 'r': buf[length++]=0x0d ; s++ ; break ;
          default: ; break ; //Ignore the slash if following char is unknown
        }
      }
      else
        buf[length++]=*s++ ;
    }

    n = write(fd, buf, length);
    printf ("Writing \%d characters: \%s\n", length, argv[i]) ;

    if (n != length)
    {
      printf("Write error! Returned \%d instead of \%d - \%s\n", n, length, strerror(errno));
      exit (1);
    }
  }

  close (fd);
  return 0 ;
 }

Programme to read data from an I2C? device connected to the NSLU2 (read_i2c.c)

Here's an example of a programme to read a data structure off the I2C? bus. This is tailored to interpret the data structure returned by the PIC code below.

 #include <stdio.h>
 #include <stdlib.h>
 #include <unistd.h>
 #include <fcntl.h>
 #include <errno.h>
 #include <string.h>
 #include <sys/types.h>
 //include <linux/i2c-dev.h>
 #include "i2c-dev.h"

 #define I2C?_ADDRESS 0x32
 #define BUFSIZE 10

 int
 main (int argc, char **argv)
 {
  int fd;
  int n, i;
  unsigned char buf[BUFSIZE+1];

  if ((fd = open("/dev/i2c-0", O_RDWR)) < 0)
  {
    printf("Error opening i2c port!\n");
    exit (1);
  }

  if (ioctl(fd,I2C?_SLAVE,I2C?_ADDRESS) < 0)
  {
    printf("Error setting i2c address!\n");
    exit (1);
  }

  if (ioctl(fd,I2C?_RETRIES,2) < 0)
  {
    printf("Error setting i2c retries!\n");
    exit (1);
  }

  for (i=1 ; i < 8 ; i++)
  {
    sleep (1) ;
    n = read (fd, buf, BUFSIZE);

    if (n != BUFSIZE)
      printf("\%d: Read error! Returned \%d instead of \%d\n", i, n, BUFSIZE);

    printf("\%d: ADC1?=\%04x, ADC2?=\%04x", i, (buf[0]<<8 + buf[1]), (buf[2]<<8 + buf[3]));
    printf(", SW1?(=\%c count=\%d)", (buf[4] & 0x80)?'1':'0', buf[4]&0x7f);
    printf(", SW2?(=\%c count=\%d)", (buf[5] & 0x80)?'1':'0', buf[5]&0x7f);
    printf(", SW3?(=\%c count=\%d)", (buf[6] & 0x80)?'1':'0', buf[6]&0x7f);

    for (n=7 ; n < BUFSIZE ; n++)
      printf(", \%02x", buf[n]);

    printf ("\n");
  }

  close (fd);
  return 0 ;
 }

top-Makefile

This makefile goes in the top directory of your i2c application directory. Note that Makefiles require tabs and not space characters on the indented lines!!!

 include $(TOPDIR)/rules.mk

 PKG_NAME:=my-additions
 PKG_RELEASE:=1

 PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)

 include $(INCLUDE_DIR)/package.mk

 define Package/my-additions
        SECTION:=utils
        CATEGORY:=Base system
        TITLE:=My i2c additions to the NSLU2
 endef

 define Package/my-additions/description
        This package contains all my i2c additions to the system
 endef

 define Build/Prepare
        mkdir -p $(PKG_BUILD_DIR)
        $(CP) ./src/* $(PKG_BUILD_DIR)/
 endef

 define Build/Compile
        $(MAKE) -C $(PKG_BUILD_DIR) LINUX="$(LINUX_DIR)" CC="$(TARGET_CC)" STAGING_DIR="$(STAGING_DIR)"
 endef

 define Package/my-additions/install
        $(INSTALL_DIR) $(1)/etc/rc.d
        $(INSTALL_BIN) ./files/S* $(1)/etc/rc.d
        $(INSTALL_DIR) $(1)/bin
        $(INSTALL_BIN) $(PKG_BUILD_DIR)/read_i2c $(1)/bin/
        $(INSTALL_BIN) $(PKG_BUILD_DIR)/echo_lcd $(1)/bin/
        $(INSTALL_BIN) $(PKG_BUILD_DIR)/test_i2c $(1)/bin/
 endef

 $(eval $(call BuildPackage?,my-additions))

Note the section starting with define Package/my-additions/install. This is where you compiled and static files get copied into the openwrt image. The lines involving the /etc/rc.d directory copy startup scripts which will be run when the NSLU2 boots. The filenames should conform with the general format of the first three characters being Sxx or Kxx (xx being a number between 00 and 99, and S or K denoting that the script should be run at startup time, or Kill (shutdown) time).

src-Makefile

This makefile goes in the src directory of your i2c application directory. Note that Makefiles require tabs and not space characters on the indented lines!!!

 BINS = echo_lcd read_i2c
 DESTDIR =
 CFLAGS += -O2

 prefix := /usr/local
 incdir := $(prefix)/include

 INSTALL := install
 INSTALL_BIN := $(INSTALL) -c -m 755
 INSTALL_DATA := $(INSTALL) -c -m 644
 INSTALL_DIR := $(INSTALL) -m 755 -d

 RM := rm -rf

 all: $(BINS)

 clean: $(RM) $(wildcard *.o *.so *.a $(BINS)) .depend

 install: $(INSTALL_BIN) $(BINS) $(DESTDIR)/bin/

 echo_lcd: echo_lcd.o
 read_i2c: read_i2c.o

Firmware for a 16F88 PIC

I've written assembler code for the 16F88 before and frankly, its a pain to debug. This time I decided that I'd have a crack at writing the LCD interface in C. There's several freely downloadable C compilers for the PIC available and I chose to use one called MikroC?, available at the mikroElektronika(approve sites) web site.

As written, the I2C? is configured into 7 bit addressing mode (the normal mode). The I2C? message consists of a leading address byte (containing the 7 bit address and 1 bit write command), followed by one or more ascii characters to display on the LCD.

There are several special characters:

  • Zero (0x00) will clear the LCD and move the cursor to position 1 on line 1.
  • One (0x01) through two (0x02) or four (0x04) will move the cursor to position 1 of the numbered line. All the characters on that line will be blanked.
  • Newline (0x0a) will move the cursor to position 1 on the next line and blank that line. If the cursor was already on the bottom line, it will wrap back to the first line.
  • Carriage Return (0x0d) will take the cursor back to the beginning of the current line and blank the current line.
  • If you attempt to print more characters onto a line than the width of the LCD supports, the cursor will be moved onto the beginning of the next line, the character will be printed in the 1st position on that line and the rest of that line will be blanked.

In order for these functions to work properly, you need to customise the C code with the dimensions of the LCD you are going to use. Use the #define statements near the top of the code to specify the number of lines and the number of characters per line. Choose an i2c address for the PIC using the #define near the top of the file also. Its possible to have several different I2C? devices connected to your NSLU2 at the same time, each with different addresses.

When you power-on the 16F88, it will initialise the LCD and display the size of LCD compiled into it, and it will also display the I2C? address it is listening to. As I have a range of different LCD display sizes, this helped me realise when I'd connected the wrong PIC to the wrong LCD!

Finally, I experimented with PIC clock speed to see if I could reduce power consumption by reducing the clock. The I2C? routines seem to work perfectly down to a clock speed of 2 MHz?. If you try to clock the PIC slower than this, characters start to be dropped because the I2C? interrupt routine isn't entered quickly enough or because of I2C? input buffer overflows (because the last character isn't emptied quickly enough).

 // 16F88 programme to drive a hitachi compatible LCD display, read switches
 // and monitor some voltages..... Interface to NSLU2 or host micro via I2C? bus

 // Define the I2C? address for this device
 #define I2C?_ADDRESS 0x32

 // Define the number of lines and columns on the LCD display
 #define LINES 4
 #define COLUMNS 20
 //////////////////////////////////////////////////////////////////////
 // Copyright Stefan Keller-Tuberg (C) 2008
 //
 //    This program is free software: you can redistribute it and/or modify
 //    it under the terms of the GNU General Public License as published by
 //    the Free Software Foundation, either version 3 of the License, or
 //    (at your option) any later version.
 //
 //    This program is distributed in the hope that it will be useful,
 //    but WITHOUT ANY WARRANTY; without even the implied warranty of
 //    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 //    GNU General Public License for more details.
 //
 // Visit <http://www.gnu.org/licenses/> to view the terms of the license.
 //////////////////////////////////////////////////////////////////////
 // PIC 16F88 pin assignments as follows:
 //
 // PORT A bit 0 = D4 on LCD
 // PORT A bit 1 = D5 on LCD
 // PORT A bit 2 = D6 on LCD
 // PORT A bit 3 = Analogue Input - tied to 3.3V rail with 22k
 // PORT A bit 4 = Analogue Input - Battery Voltage via 1:2 voltage divider
 // PORT A bit 5 = pulled to VDD via 22k (This is normally reset input)
 // PORT A bit 6 = NC
 // PORT A bit 7 = D7 on LCD
 //
 // PORT B bit 0 = E on LCD
 // PORT B bit 1 = SD on I2C?  (I added my own 4k7 pullup to 3.3V - didn't work without)
 // PORT B bit 2 = RW on LCD
 // PORT B bit 3 = RS on LCD
 // PORT B bit 4 = CK on I2C?  (I added my own 4k7 pullup to 3.3V - didn't work without)
 // PORT B bit 5 = Switch3 input. Pulled to VDD via 22k. Switched low.
 // PORT B bit 6 = Switch2 input. Pulled to VDD via 22k. Switched low.
 // PORT B bit 7 = Switch1 input. Pulled to VDD via 22k. Switched low.
 //
 // VDD connected to +5V USB port supply via a 1N4001 diode - to drop voltage so
 // I2C? inputs will work OK when pulled to only 3.3V
 //////////////////////////////////////////////////////////////////////
 // Fx is mikroC nomenclature for bit x

 // Define PORTB bits used for LCD control
 #define RS_BIT F3
 #define WR_BIT F2
 #define E_BIT  F0

 // Define PORTA bits used for the four LCD data bits
 #define LCD4?   F0
 #define LCD5?   F1
 #define LCD6?   F2
 #define LCD7?   F7

 // Define PORTA bits used for Analogue inputs
 #define ANALOGUE0?  F3
 #define ANALOGUE1?  F4

 // Define PORTB bits used for Digital inputs
 #define SW1?    F5
 #define SW2?    F6
 #define SW3?    F7
 #define SWITCH_MASK 0xe0

 void
 write_nibble_to_lcd (const unsigned char c, const unsigned char RS)
 {
   TRISA.LCD4? = 0;
   TRISA.LCD5? = 0;
   TRISA.LCD6? = 0;
   TRISA.LCD7? = 0;
   PORTA.LCD4? = c.F0 ;
   PORTA.LCD5? = c.F1 ;
   PORTA.LCD6? = c.F2 ;
   PORTA.LCD7? = c.F3 ;

   PORTB.E_BIT = 0 ;

   if (RS)
     PORTB.RS_BIT = 1 ;
   else
     PORTB.RS_BIT = 0 ;

   PORTB.WR_BIT = 0 ;
   Delay_us (1);
   PORTB.E_BIT = 1 ;  //E High, WR low, RS set to selected value
   Delay_us (1);
   PORTB.E_BIT = 0 ;

   // Exit with E Low, WR Low, RS set to selected value
 }

 // Assumes Port A bits 7, 2-0 are the ONLY outputs (ie bits 6-3 are INPUTS)
 void
 write_byte_to_lcd (const unsigned char c, const unsigned char RS)
 {
   unsigned char busy ;

   TRISA.LCD4? = 1;
   TRISA.LCD5? = 1;
   TRISA.LCD6? = 1;
   TRISA.LCD7? = 1;

   PORTB.E_BIT = 0 ;
   PORTB.RS_BIT = 0 ;
   PORTB.WR_BIT = 1 ;
   Delay_us(1);

   // Wait for LCD to become unbusy
   // BUSY FLAG polled on falling edge of E

   do  // Need to set E high and low twice - two four bit reads
   {
     PORTB.E_BIT = 1;    //WR High=Read, E High
     Delay_us(1);
     busy = PORTA ;      //Read the busy flag in bit 7 (bit 7 of LCD)
     PORTB.E_BIT = 0 ;   //WR High, E Clocks low
     Delay_us(1);
     PORTB.E_BIT = 1;    //WR High=Read, E High
     Delay_us(1);
     PORTB.E_BIT = 0 ;   //WR High, E Clocks low
     Delay_us(1);
   } while (busy.LCD7?);

   // Now the LCD is ready, write byte in two 4 bit nibbles
   write_nibble_to_lcd (c >> 4, RS) ;
   write_nibble_to_lcd (c, RS) ;

   // Exit with E Low, WR Low, RS set to selected value
 }

 void
 string_to_lcd (const unsigned char *s)
 {
   while (*s)
     write_byte_to_lcd (*s++, 1) ;
 }

 void
 decbyte_to_lcd (unsigned char c)
 {
   char *s ;
   unsigned char text[4] ;

   ByteToStr? (c, text);

   for (s=text; *s ;)
     write_byte_to_lcd (*s++, 1) ;
 }

 void
 hexnibble_to_lcd (unsigned char c)
 {
   c &= 0x0f ;

   if (c > 9)
       c += 'A' - 10 ;
   else
     c += '0' ;

   write_byte_to_lcd (c, 1) ;
 }

 void hexbyte_to_lcd (unsigned char c)
 {
    hexnibble_to_lcd (c >> 4) ;
    hexnibble_to_lcd (c) ;
 }
 //==============================================================================
 // BUFFER_POWER must be >= 2
 #define BUFFER_LEN 64
 #define BUFFER_MASK (BUFFER_LEN-1)

 unsigned char rxbuf [BUFFER_LEN] ;
 unsigned char rx_head, rx_tail, rx_len ;

 // The following is a bogus structure definition....
 // the array is fed back to the NSLU2 when read off I2C?
 #define BATT_VOLT  0              // 2 byte ADC count
 #define REF_VOLT   (BATT_VOLT+2)  // 2 byte ADC count
 #define SWITCH1?    (REF_VOLT+2)   // 1 byte count of debounced presses
 #define SWITCH2?    (SWITCH1?+1)    // 1 byte count of debounced presses
 #define SWITCH3?    (SWITCH2?+1)    // 1 byte count of debounced presses
 #define SWITCHES   (SWITCH3?+1)    // 1 byte count current state
 #define STRUCT_LEN (SWITCHES+1)   // Should be total of 8

 unsigned char txbuf [STRUCT_LEN] ;
 unsigned char tx_index ;

 // counters for debugging purposes
 unsigned char crashed, crash_cause, info, last_i2c ;

 void
 interrupt ()
 {
   unsigned char stat;

   if (PIR1?.F3 == 1) // is this an i2c interrupt
   {
     SSPCON.F4 = 0 ;         // Hold clock low - (ie stretch clock while we work)
     stat = SSPSTAT & 0x2d ; // mask out bits we're not interestted in
                             // 5=(1=data,0=addr), 3=(1=start detected),
                             // 2=(1=read,0=write), 0=(1=sspbuf full)

     if (stat == 0x09) // I2C? Write: Byte received was an address
     {
       last_i2c = SSPBUF ; // Do a dummy read of the sspbuf
     }
     else if (stat == 0x29) // I2C? Write: Byte received was data
     {
       rxbuf[rx_head++] = SSPBUF ;
       rx_head &= BUFFER_MASK ;

       if (rx_len++ >= BUFFER_LEN)
         crashed = 1 ;  //flag error
     }
     else if (stat == 0x0c) // I2C? Read: Byte received was an address
     {
       tx_index = 1 ;   // byte to be transmitted
       while (SSPSTAT.F0 == 1) ; // keep waiting while buffer full

            do
       {
              SSPCON.F7 = 0 ; // Clear the write collision flag
         SSPBUF = txbuf[0] ; // Return first byte of array
       } while (SSPCON.F7 == 1) ; // Loop if there was a write collision
     }
     else if (stat == 0x2c) // I2C? Read: Byte to be transmitted will be data
     {
       while (SSPSTAT.F0 == 1) ; // keep waiting while buffer full

       do
       {
         SSPCON.F7 = 0 ; // Clear the write collision flag
         SSPBUF = txbuf[tx_index] ; // Return next byte of array
       } while (SSPCON.F7 == 1) ; // Loop if there was a write collision

       tx_index++ ; // point to next byte in the array next time
     }
     else  // NACK received 0x28 - or another condition - Do a reset
     {
       last_i2c = SSPBUF ; // Do a dummy read of the sspbuf
       SSPCON.F5=0;    // Temporarily disable
       SSPCON = 0x36 ; // F7=0 WCOL Write Collision Detect bit
                       // F6=0 SSPOV Receive Overflow Indicator bit
                       // F5=1 Synchronous Serial Port Enable
                       // F4=1 When set at 0, I2C? clock pin held low to stretch clock
                       // F3-0=0110: I2C? slave mode: 7 bits address
       SSPSTAT = 0 ;   // Clear all status bits
     }

     SSPCON.F4 = 1 ; // Release the clock (finish stretching it)
     PIR1?.F3 = 0 ;   // Clear the interrupt
   }
   else // unknown interrupt!!
   {
     crashed = 3;
     info = PIR1? ;
   }
 }
 //==============================================================================
 #define CLEAR_AND_FIRST_ROW 0x01
 #define FIRST_ROW           0x02
 #define SECOND_ROW          (0x80 | 0x40)
 #define THIRD_ROW           (0x80 | 0x14)
 #define FOURTH_ROW          (0x80 | 0x54)

 unsigned char line, column ;

 void
 linefeed ()
 {
   unsigned char cmd, i ;

   // Blank the appropriate line.
   switch (line)
   {
     case 1:  cmd = FIRST_ROW ; break ;
     case 2:  cmd = SECOND_ROW ; break ;
     case 3:  cmd = THIRD_ROW ; break ;
     default: cmd = FOURTH_ROW ; break ;
   }

   write_byte_to_lcd (cmd, 0); // Go to start of line

   for (i=0 ; i<COLUMNS ; i++)      // Blank the entire line
     write_byte_to_lcd (' ', 1);

   write_byte_to_lcd (cmd, 0); // Return to start of line
   column = 0 ;
 }

 void
 printchar (const unsigned char c)
 {
   if (c == 0)
   {
     // Clear display entirely
     write_byte_to_lcd (CLEAR_AND_FIRST_ROW, 0);
     line = 1 ;
     column = 0 ;
   }
   else if (c == '\n')
   {
     // linefeed and wrap around to line 0
     line++ ;

     if (line > LINES)
       line = 1 ;

     linefeed() ;
   }
   else if (c <= LINES)
   {
     // move to line 1..4
     line = c ;
     linefeed() ;
   }
   else if (c == '\r')
   {
     linefeed() ;
   }
   else
   {
     if (column >= COLUMNS)
     {
       // need to force a linefeed
       line++ ;

       if (line > LINES)
         line = 1 ;

       linefeed() ;
     }

     // Display the numbered character
     write_byte_to_lcd (c, 1);
     column++ ;
   }
 }

 // Enqueue_string will print anything BUT NOT the \000 CLEAR command!!!!
 void
 enqueue_char (unsigned char c)
 {
   PIE1?.F3 = 0;  // Disable int F3=I2C? int
   rxbuf[rx_head++] = c ;
   rx_head &= BUFFER_MASK ;
   rx_len++ ;
   PIE1?.F3 = 1;  // Enable int F3=I2C? int

   if (rx_len > BUFFER_LEN)
   {
     crashed = 4 ;  //flag error
     return ;
   }
 }

 void
 enqueue_string (const unsigned char *s)
 {
   while (*s)
     enqueue_char (*s++) ;
 }

 void
 enqueue_number (const unsigned char c)
 {
   unsigned char text[4] ;
   unsigned char *s ;

   ByteToStr? (c, text);

   for (s=text ; *s ; s++)
   {
     if (*s ==  ' ')
       continue ;

     rxbuf[rx_head++] = *s ;
     rx_head &= BUFFER_MASK ;
     rx_len++ ;

     if (rx_len > BUFFER_LEN)
       crashed = 5 ;  //flag error
   }
 }

 void
 flush_display_buffer()
 {
   asm {
     CLRWDT  // clear the watchdog timer
   }

   if (rx_len)
   {
     printchar (rxbuf[rx_tail++]) ;  //THIS IS ONLY PLACE WE TOUCH rx_tail
     rx_tail &= BUFFER_MASK ;
     PIE1?.F3 = 0;  // Disable int F3=I2C? int
     rx_len-- ;
     PIE1?.F3 = 1;  // Enable int F3=I2C? int
   }
 }
 //==============================================================================
 unsigned char debounce ;

 void
 Check_ADC()
 {
   unsigned char i ;

   if (ADCON0?.F2 == 0) //zero means converstion finished
   {
     i = ADCON0? ;
     i &= 0x38 ;    // Only interested in the channel select bits

     if (i == 0x18)
     {
       txbuf [REF_VOLT]   = ADRESH ;
       txbuf [REF_VOLT+1] = ADRESL ;
       ADCON0?=0xa1 ;//  convert voltage on A4 next
     }
     else
     {
       txbuf [BATT_VOLT]   = ADRESH ;
       txbuf [BATT_VOLT+1] = ADRESL ;
       ADCON0?=0x99 ;//  else convert voltage on A3 next
     }

     Delay_us (100); // Wait a bit for analogue input to settle
     ADCON0?.F2 = 1; // Start the conversion
   }
 }

 void
 Long_Delay (unsigned char time)
 {
   unsigned char i ;

   for (i=0 ; i < time ; i++)
   {
     asm {
       CLRWDT         // clear the watchdog timer
     }

     Delay_ms (100);
   }
 }
 //==============================================================================
 #define MAGIC       0x3d

 unsigned char num_boots ;
 unsigned char magic ; // Will hold a magic number so we know a cold boot

 void
 main()
 {
   char i, j ;

   OSCCON=0x7c; // Internal osc: 0x4c=1MHz, 0x5c=2MHz, 0x6c=4MHz, 0x7c=8MHz
   while (OSCCON.F2 == 0) ; // Wait for osc to stabilise

 restart:
   OPTION_REG=0x03; //0x03=Prescaler=1:256   0x80=low..pullup enabled
   INTCON=0;     //Disable interrupts
   PIR1?=0;       //Clear all interrupt flags

   WDTCON =0x17 ;// Enable watchdog with max timeout value = approx 2s

   TRISA=0;      //Set Port A as outputs, except analogue inputs
   TRISA.ANALOGUE0?=1 ;
   TRISA.ANALOGUE1?=1 ;

   TRISB=0x12 | SWITCH_MASK;   //B4 and B1 are inputs (I2C?)

   PORTA=0x00;
   PORTB=SWITCH_MASK;

   ANSEL=0;      //Set Port A as outputs, except analogue inputs
   ANSEL.ANALOGUE0?=1 ;
   ANSEL.ANALOGUE1?=1 ;
   ADCON1?=0xc0 ; // Bit7=1=right justify, 6=1=clokc/2, 5,4=00=Vref
   ADCON0?=0x81 ; // Bit7,6=10 Osc/32, Bit5-3=Chan select, bit0=1 A-D enabled

   SSPCON = 0x36 ; // F7=0 WCOL Write Collision Detect bit
                   // F6=0 SSPOV Receive Overflow Indicator bit
                   // F5=1 Synchronous Serial Port Enable
                   // F4=1 When set at 0, I2C? clock pin held low to stretch clock
                   // F3-0=0110: I2C? slave mode: 7 bits address
   SSPADD = I2C?_ADDRESS << 1 ; // 7 bit address in bits 7--1
   SSPSTAT = 0 ;   // Clear all status bits
   PIE1?.F3 = 1;    // F6=ADC int, F3=I2C? int

   if (magic != MAGIC)
   {
     crash_cause=0 ;  // used for debugging interrupt routine with print statements
     num_boots = 0 ;
     magic = MAGIC ;
   }
   else
     num_boots++ ;

   crashed = 0 ;   // We're not crashed at the moment!
   rx_head = rx_tail = rx_len = 0 ;

   INTCON.F6 = 1 ; // enable peripheral interrupts
   INTCON.F7 = 1 ; // enable all unmasked interrupts

   TXSTA = 0 ;     // Turn off transmit from UART
   RCSTA = 0 ;
   CMCON = 0x07 ;  // Disable comparators

   Long_Delay (2) ;  // Wait 200ms seconds

   txbuf[SWITCHES] = PORTB & SWITCH_MASK ; // read the initial switch inputs
   txbuf[SWITCH1?] = 0 ;
   txbuf[SWITCH2?] = 0 ;
   txbuf[SWITCH3?] = 0 ;
   debounce=0 ; // Initialise debouncing algorithm OFF

   write_nibble_to_lcd (0x03,0);// Function set 8 bit mode
   Delay_ms (5);                // Wait at least 4.1ms
   write_nibble_to_lcd (0x03,0);// Function set 8 bit mode
   Delay_ms (1);                // Wait at least 100us
   write_nibble_to_lcd (0x03,0);// Function set 8 bit mode
   Delay_ms (1);                // Wait at least 100us
   write_nibble_to_lcd (0x02,0);// NOW FINALLY Function set 4 bit mode
   Delay_ms (1);                // Wait at least 100us

   write_byte_to_lcd (0x28, 0); //And repeat, doing full 8 bits of function cmd
   write_byte_to_lcd (0x0c, 0); //Display ON (04), Curson ON(02), Blinking(01)
   write_byte_to_lcd (0x01, 0); //Clear display
   write_byte_to_lcd (0x06, 0); //Entry mode Increment(02) Noshift(01)
   write_byte_to_lcd (CLEAR_AND_FIRST_ROW, 0);
   line = 1;
   column = 0 ;

   // Give some indication if there's been a crash
   j=1 ;
   enqueue_number (LINES) ;
   enqueue_char ('x') ;
   enqueue_number (COLUMNS) ;
   enqueue_string (" lcd @addr ") ;
   enqueue_number (I2C?_ADDRESS) ;

   if (num_boots)
   {
     enqueue_string ("\nBoot count: ");
     enqueue_number (num_boots) ;
     j=2 ;
   }

   for (; j < LINES ; j++) // visual test for data line wiring errors...
   {
     enqueue_char ('\n');

     for (i=0; i < COLUMNS ; i++)
       enqueue_char (' '+i+(j*COLUMNS));

     while (rx_len) // empty out what we've just queued
       flush_display_buffer();
   }

   Long_Delay (50) ;  // Wait 5 seconds
   enqueue_char ('\0') ;
   enqueue_string ("Booting Linux.......");

   for (;;)
   {
     if (debounce) // are we currently debouncing?
     {
       debounce-- ;

       if (debounce == 0)
       {
         i = PORTB & SWITCH_MASK ; // read the current switch inputs

         if ((i.SW1? == 0) && (txbuf[SWITCHES].SW1? == 1))  // Switch 1 pressed
           txbuf[SWITCH1?]++ ;

         if ((i.SW2? == 0) && (txbuf[SWITCHES].SW2? == 1))  // Switch 2 pressed
           txbuf[SWITCH2?]++ ;

         if ((i.SW3? == 0) && (txbuf[SWITCHES].SW3? == 1))  // Switch 3 pressed
           txbuf[SWITCH3?]++ ;

         txbuf[SWITCH1?].F7 = i.SW1? ;
         txbuf[SWITCH2?].F7 = i.SW2? ;
         txbuf[SWITCH3?].F7 = i.SW3? ;
         txbuf[SWITCHES] = i ; // Remember state and END of debounce
       }
     }
     else  // we are NOT currently debouncing - so read the switches
     {
       i = PORTB & SWITCH_MASK ; // read the current switch inputs

       if (txbuf[SWITCHES] != i)  // need to debounce??
         debounce-- ;
     }

     if (crashed)
     {
       crash_cause = crashed ; // Remember why we crashed
       write_byte_to_lcd (CLEAR_AND_FIRST_ROW, 0);
       string_to_lcd ("Cause:") ;
       hexbyte_to_lcd (crash_cause);
       string_to_lcd (" last:") ;
       hexbyte_to_lcd (last_i2c) ;
       string_to_lcd (" info:") ;
       hexbyte_to_lcd (info);
       string_to_lcd (" boots:") ;
       hexbyte_to_lcd (num_boots);

       write_byte_to_lcd (SECOND_ROW, 0);
       string_to_lcd ("Buf len:") ;
       decbyte_to_lcd (rx_len);
       write_byte_to_lcd (' ', 1);

       for (i=0 ; i < 14 ; i++)
         hexbyte_to_lcd (rxbuf[(rx_tail+i)&BUFFER_MASK]);

       // The following kludge is to get around the non-reentrant version of C
       // If there's an error inside the Interrupt, this flag will be set
       // That lets us display something - and this loop will cause an infinte wait
 //      while (crashed) ; // Infinite loop if an error has been set in the ISR
                         // This will trip the watchdog timer some time later

       Long_Delay (150) ; // Delay for 15 seconds
       goto restart ;
     }

     Check_ADC();
     flush_display_buffer();  //Print any characters received on I2C?
   }
 }

Big Tip: NSLU2 I2C? drivers can only drive short flying leads unless you pull them up externally

I could not get the I2C? drivers on the NSLU2 to speak with anything (such as the PIC described above) on a cable longer than about 50mm (ie really short) unless I pulled the I2C? clock and data lines to 3.3V on my external board using a 4k7 resistors. I know that the NSLU2 has its own pull up resistors - but they appear to be insufficient when driving anything other than a trivially short I2C? cable.

view · edit · print · history · Last edited by stefan keller-tuberg.
Based on work by ByronT and stefan keller-tuberg.
Originally by stefan keller-tuberg.
Page last modified on July 04, 2008, at 07:12 AM