Sunday, January 31, 2010

im-me LCD Interface Hacked

Good news everyone, I've reverse engineered the im-me's LCD Display interface and now have working code it drive it.  Here's a photo showing a variation on "Hello World":


It was quite a journey and took much longer to reverse engineer than I expected.  First I'll describe the steps I took, and then I'll summarise the interface.

I started by using an oscilloscope to probe the 5x wires interfacing the CC1110 to the LCD interface.  The signal waveforms gave some clues, but not the complete picture.  For instance it was pretty clear from the photo below that the upper trace with the 8 regular pulses was the SPI clock (P0_5, SCK) , and that the lower trace was the data from Master to Slave (P0_3, MOSI - Master Out Slave In):



This waveform also told me that the clock SPI clock speed was 2.5MHz, and that the data changes on the trailing edge of the clock (it's difficult to see the faint transitions in the photo above).  So the LCD must be reading the data on the leading edge of the clock.

I pondered how I could spy on the SPI traffic, and concluded that the simplest way to deal with the high speed and the 2.5v signal levels would be to use a second im-me!  I used fine wire-wrap wire to connect the SCK and MOSI signals from a fully-functioning im-me to my spy im-me, and wrote a simple program to capture the traffic.  The spy program was simple - it would read the data into a memory buffer, and stop when it was full.  I'd then use the debug interface to read the results out of the buffer.  To write the spy program I used for guidance TI's Design Note DN113 and the code in CC1110, CC2510 Basic Software Examples.  Although the program started capturing SPI output, the results were inconsistent each time I ran it; data was being dropped.  It turns out that on reset the CPU runs on an internal RC clock which is half the speed of the Xtal Oscillator; I added some code to the spy program to select the higher speed clock source and it started giving consistent results.

By interacting with the fully-functioning im-me I was able to get a wide variety of spy traffic and to piece together a lot of how it worked.  For instance there seemed to be commands to direct the on-screen cursor, and the graphics was being written 8 vertical bits at a time per byte transferred.  It was also clear from the addressing that the data was being sent MSB first (the spy program was reading it LSB first).

But there was a problem - I couldn't understand how the LCD could distinguish between the command bytes and the data bytes.  I would have expected either escape characters to enter/exit data mode, or byte-lengths for the data, but there were none.

I resorted to the internet, and searched for SPI LCD interface specifications for clues.  After many blind alleys I found that Sitronix's range of SPI-based LCD Driver ICs.  The ST7565S was particularly interesting because (a) it supports a similar size LCD to the im-me, (b) it has an on-board PSU requiring capacitors wired exactly like the capacitors surrounding the LCD connector on the im-me, (c) many of the commands I had decoded from the SPI spy were identical.

The ST7565S data sheet also gave me the final clue I needed - an additional wire to the driver IC (A0) indicates whether the byte is a command or data - solving the problem I was struggling with earlier.  With a quick modification to the spy program I was able to confirm that P0_2 is the output from the CC1110 to the LCD driver's A0 input.

So the connections (from the top of the connector to the bottom) are, using ST7565S terminology:

  • P0_4 - /CS -- hold low while transferring data (SSN in SPI terminology).
  • P1_1 - /RESET (I think) -- Pulse low at power up, keep high all the time otherwise
  • P0_2 - A0 -- Low for a command byte, high for a data byte
  • P0_5 - SCL -- SPI clock (SCK in SPI terminology)
  • P0_3 - SI -- Serial Data (MOSI in SPI terminology)
(Note, my guesses for these signals in my original post were wrong).

Now that I had confirmed what the signals and commands were, it was easy to write a small test program for the im-me that would bring the LCD to life.  The test program verifies all basic operation.  It also exercises commands documented in the ST7565 data sheet that the im-me doesn't use (Display reversed, and Display start line set).  It doesn't yet demonstrate contrast adjust (Electronic Volume Mode Set), and I haven't tested Power Save mode either.

The only thing remaining unexplained is a couple of commands in the initialisation sequence that don't appear in the ST7565 data sheet.

Here's the test program's source code I developed using the sdcc compiler.

#include
#include "cc1110-ext.h"

#define LOW 0;
#define HIGH 1;

void sleepMillis(int ms) {
int j;
while (--ms > 0) { 
for (j=0; j<1200;j++); // about 1 millisecond
};
}

void xtalClock() { // Set system clock source to 26 Mhz
    SLEEP &= ~SLEEP_OSC_PD; // Turn both high speed oscillators on
    while( !(SLEEP & SLEEP_XOSC_S) ); // Wait until xtal oscillator is stable
    CLKCON = (CLKCON & ~(CLKCON_CLKSPD | CLKCON_OSC)) | CLKSPD_DIV_1; // Select xtal osc, 26 MHz
    while (CLKCON & CLKCON_OSC); // Wait for change to take effect
    SLEEP |= SLEEP_OSC_PD; // Turn off the other high speed oscillator (the RC osc)
}


// IO Port Definitions:
#define A0 P0_2
#define SSN P0_4
#define LCDRst P1_1
#define LED_RED P2_3
#define LED_GREEN P2_4
// plus SPI ports driven from USART0 are:
// MOSI P0_3
// SCK P0_5

void setIOPorts() {
//No need to set PERCFG or P2DIR as default values on reset are fine
    P0SEL |= (BIT5 | BIT3 ); // set SCK and MOSI as peripheral outputs
P0DIR |= BIT4 | BIT2; // set SSN and A0 as outputs
P1DIR |= BIT1; // set LCDRst as output
P2DIR = BIT3 | BIT4; // set LEDs  as outputs
LED_GREEN = LOW; // Turn the Green LED on (LEDs driven by reverse logic: 0 is ON)
}

// Set a clock rate of approx. 2.5 Mbps for 26 MHz Xtal clock
#define SPI_BAUD_M  170
#define SPI_BAUD_E  16

void configureSPI() {
U0CSR = 0;  //Set SPI Master operation
     U0BAUD =  SPI_BAUD_M; // set Mantissa
U0GCR = U0GCR_ORDER | SPI_BAUD_E; // set clock on 1st edge, -ve clock polarity, MSB first, and exponent
}
void tx(unsigned char ch) {
U0DBUF = ch;
while(!(U0CSR & U0CSR_TX_BYTE)); // wait for byte to be transmitted
U0CSR &= ~U0CSR_TX_BYTE;         // Clear transmit byte status
}

void txData(unsigned char ch) {
A0 = HIGH;
tx(ch);
}

void txCtl(unsigned char ch){
A0 = LOW;
tx(ch);
}

void LCDReset(void) {
LCDRst = LOW; // hold down the RESET line to reset the display
sleepMillis(1);
LCDRst = HIGH;
SSN = LOW;
// send the initialisation commands to the LCD display
txCtl(0xe2); // RESET cmd
txCtl(0x24); // set internal resistor ratio
txCtl(0x81); // set Vol Control
txCtl(0x60); // set Vol Control - ctd
txCtl(0xe6); // ?? -- don't know what this command is
txCtl(0x00); // ?? -- don't know what this command is
txCtl(0x2f); // set internal PSU operating mode
txCtl(0xa1); // LCD bias set
txCtl(0xaf); // Display ON
SSN = HIGH;
}

void LCDPowerSave() { // not tested yet; taken from spi trace
txCtl(0xac); // static indicator off cmd
txCtl(0xae); // LCD off
txCtl(0xa5); // Display all Points on cmd = Power Save when following LCD off
}

void setCursor(unsigned char row, unsigned char col) {
txCtl(0xb0 + row); // set cursor row
txCtl(0x00 + (col & 0x0f)); // set cursor col low
txCtl(0x10 + ( (col>>4) & 0x0f)); // set cursor col high
}

void setDisplayStart(unsigned char start) {
txCtl(0x40 | (start & 0x3f)); // set Display start address
}

void setNormalReverse(unsigned char normal) {  // 0 = Normal, 1 = Reverse
txCtl(0xa6 | (normal & 0x01) );
}

unsigned int i;
unsigned char row;
unsigned char col;
unsigned char displayStart;
static const unsigned char helloWorld[] = {
0x7f, 0x08, 0x08, 0x08, 0x7f, 0x00, //H
0x38, 0x54, 0x54, 0x54, 0x18, 0x00, //e
0x00, 0x41, 0x7f, 0x40, 0x00, //0x00, //l
0x00, 0x41, 0x7f, 0x40, 0x00, //0x00, //l
0x38, 0x44, 0x44, 0x44, 0x38, 0x00, //o
0x00, 0x00, 0x00, 0x00, 0x00, //0x00,
0x7f, 0x08, 0x08, 0x08, 0x7f, 0x00, //H
0x20, 0x54, 0x54, 0x54, 0x78, 0x00, //a
0x38, 0x44, 0x44, 0x44, 0x20, 0x00, //c
0x7f, 0x10, 0x28, 0x44, 0x00, //0x00, //k
0x20, 0x54, 0x54, 0x54, 0x78, 0x00, //a
0x38, 0x44, 0x44, 0x48, 0x3f, 0x00, //d
0x20, 0x54, 0x54, 0x54, 0x78, 0x00, //a
0x0c, 0x50, 0x50, 0x50, 0x3c, 0x00, //y
0x00, 0x00, 0x00, 0x00, 0x00, //0x00,
0x3f, 0x40, 0x38, 0x40, 0x3f, 0x00, //W
0x38, 0x44, 0x44, 0x44, 0x38, 0x00, //o
0x7c, 0x08, 0x04, 0x04, 0x08, 0x00, //r
0x00, 0x41, 0x7f, 0x40, 0x00, //0x00, //l
0x38, 0x44, 0x44, 0x48, 0x3f, 0x00//, //d
//0x01, 0x02, 0x03, 0x04, 0x05, 0x06, // temp
//0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
//0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12 // total = 23*6 -6 = 138 - 6 = 132 pixels, or 22 chars
};
void main(void) {
xtalClock(); // select the xtal clock for fastest transfer speed
setIOPorts();
configureSPI();
LCDReset();

    while (1) {
SSN = LOW;
setDisplayStart(displayStart++);
setNormalReverse(displayStart); // screen will alternate between normal and reverse on each scroll
for (row=0; row<=8; row++) { // LCD is 65 pixels high, or 9 rows; clear all of them
setCursor(row, 0);
for (i=0; i<132; i++) { // clear every row on the line
txData(0x00);
};
if (row != 8) { // Don't paint the 9th row as only the top row of its pixels are displayed
setCursor(row, row<<1);  // indent each successive row by two pixels
for (col=0; col
txData(helloWorld[col]);
};
}
}
SSN = HIGH;
sleepMillis(1000);
LED_RED = !LED_RED;
}
}

4 comments:

  1. Again, incredibly neighborly work. I've finally gotten around to wiring up an IM ME and starting on a replacement application.

    LCDPowerSave() seems to work fine, except that the pixels need to be turned out of "all on" mode when waking the unit.
    txCtl(0xa4);

    Thank you kindly,
    --Travis

    ReplyDelete
  2. Fantastic work! A completely fresh and original hack, I love it. On the protocol driver implementation side I've been trying to collect all the reverse engineered drivers into a sourceforge project im-megpldrivers (page, c driver, and svn up so far). If you're interested in posting anything up, I think it'd be fantastically helpful to other hackers. Either way, love the blog, keep up the good work!

    ReplyDelete
  3. Amazing! Truly amazing!
    Especially I was the one who told Hack A Day that the radio devices used by these pink widgets were indeed the Chip Con devices. And of course they are now sold by TI.

    Hunter he's also one up on you.

    ReplyDelete
  4. Planning to look into this. Any chance of this working peer to peer? (No pc in the middle?)

    ReplyDelete