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;
}
}

Saturday, January 2, 2010

im-me hacking

I came across this blog describing hacking the im-me, a small wireless console. It looked interesting, so I went ahead and bought a couple.  For £8 you get a console and a USB radio dongle. It turns out that they are a bargain for what they contain, provided you can discover how to "re-purpose" the hardware.

I became much more interested when I discovered Travis Goodspeed's blog describing how to implement the debug port on a related processor.

The console

The console is really simple but powerful, containing:
  • A CC1110F32 radio/processor: 32k flash, 4k ram, 900MHz transceiver, with a debug port exposed.
  • An 8x24 LCD display, with backlight. The LCD has an SPI interface.
  • A full qwerty keyboard.
  • A piezoelectric sounder.
  • A voltage regulator, and a small number of sundry SMD parts - transistors, Rs, and Cs.
The Wireless USB Dongle

The dongle contains:
  • A CC1110 radio/processor identical to that in the console.
  • A CY7C63803 processor with USB interface.
The FCC ID is Grantee code: PIY, product code: L7281D1

The Console Debug Port

The debug port on the CC1110 is conveniently wired out to some test pads that are accessible via holes in the bottom of the battery tray. Connections are (from left to right):
  • (square pad) RESET_N
  • P2_1 = debug data
  • P2_2 = debug clock
  • +2.5 volts
  • Gnd
Here's a photo showing the wires I've soldered to the test pads that connect to the Arduino via a breadboard.  (I don't use the 2.5v connection as I supply power via the battery compartment, and rely on the on-board regulator to generate 2.5v.)


I wanted to drive this port via an Arduino, but couldn't drive it directly since the CC1110 runs off 2.5v as compared to the Arduino's +5volts.  The CC1110 is also intolerant of voltages more than 0.3v above VDD.  The solution was simple - the outputs from the Arduino are fed into voltage dividers (The DATA pin on the CC1110 is both an input and output so I decided to use relatively high value 3.3K resistors to present minimal load when it is an output).  To read the data line back into the Arduino, I wired the debug data pin to the Arduino's positive comparator input too, and set the comparator's negative input to about 1.7 volts.


(A word of warning - keep the lead lengths short - I had variable results when I initially just used test leads to connect everything.)

I wrote some Arduino interfacing code, using Travis's blog, the official documentation for the debug port, and CCFlasher (another implementation) for inspiration.  By executing the "READ_STATUS" command it became evident that the "DEBUG_LOCKED" bit is set.  This bit prevents the firmware from being read out, so it's not surprising that it is set.  However, a "CHIP_ERASE" can be executed, which erases the entire flash memory, and enables the full debug port capability, which is what I've done on one of my consoles.  I'm now able to download data to the chip and to execute programs in RAM or in flash.  The full hardware capabilities of the console are now at my disposal.

The Arduino code is still very much work in progress, but provides some basic facilities to allow me to experiment via the Serial Monitor interface of the Arduino IDE.  I'm now working on some facilities to allow me to download and flash programs more easily.  More later...

Console CC1110 Processor Development

Software development for the processor can be carried via this tool: http://supp.iar.com/Download/SW/?item=EW8051-KS4 This "kickstart" free version of the commercial tool can have its code size limits increased if you follow the guidelines in Section 9.1 of http://focus.ti.com/lit/ug/swru236a/swru236a.pdf.


The IDE can be configured to generate it's output in the simple Intel format, which is ideal for me to download to the processor with via the Arduino.


Console Keyboard


The 40 odd keyboard buttons are wired as a matrix, but the designers use some tricks to minimise the number of i/o pins.  The i/o pins used are:

  • "row" pins: P0_1, P1_2, P1_3, P1_4, P1_5, P1_6, P1_7 - input and output.  Normally high, regular very short pulses to 0v.
  • P0_6, P0_7 - input only.
The key connections are shown in the table below.

One keyboard sensing algorithm would be:
  • Set all keyboard i/o pins to inputs, with pullups to Vcc.  Read all inputs, and if any pin is zero, it is because one of the keys connected to Gnd is pressed.  For instance, if P1_2 is zero, it means the "O" key is pressed.
  • Set P0_1 as an output, and 0v.  Read all inputs (except P0_1 of course), and if any any pin is zero, it is because one of the keys on the "P0_1" row is pressed.  For instance, if P0_6 is zero, it means the "Menu" key is pressed.  Return the P0_1 to an input.
  • In a similar manner, take each of P1_2, P1_3, P1_4, P1_6, P1_7 low in turn, checking each time for a key press.
  • Add bebounce logic - e.g. repeatedly go through the steps above, and only report a key-press if the same key is pressed on 2 consecutive loops. 

Console Radio

Some information about the radio can be gathered from the FFC submission.  FCC submission details are found by searching for PIY/L7281H1 on the FCC site. There are internal photos, radio frequency details, and transmission spectra.

The standard unit transmits/receives on up to 16 different frequencies between 908.45MHz and 920.6MHz.

I've been using my trusty Tektronix 7L14 Spectrum Analyser to see how the units currently communicate.  On startup the console polls on all 16 channels to make contact with the wireless USB dongle.

Once communication has been established on one of the channels, the console and dongle continue to use just that channel thereafter.  The console polls the dongle about every 0.2 seconds.  Here's a typical sequence:


The console polls the dongle, and the dongle responds about 10 milliseconds later (the dongle is physically further away from the SA input and sothe signal is weaker).  The dongle then sends a message (a poll?) about 3 milliseconds later.  Finally the console replies to the poll after about 10 milliseconds.  This sequence repeats every 0.2 seconds, but the total transmission time for the console is 4 milliseconds, so the portion of time spent transmitting (and therefore consuming battery power) is just 2%.

If each poll/ack message is about minimal length, it corresponds to around 96 bits by the time you take into account the preamble, sync, length, address, data, and CRC.  96 bits transmitted in 2 ms is 50kbps.  So the minimum radio tx/rx data rate is 50 kbps, but could easily be higher.

The photo above also gives us a clue to the type of modulation being used.  The fact that the signal strength remains the same during the transmission period indicates that the amplitude modulation options supported by the CC1110 - On-Off Keying (OOK) and Amplitude Shift Keying (ASK) - aren't being used.

A plot of the transmission spectrum shows that transmission is on two frequencies 300kHz apart:


Zooming x10 into the twin peaks reveals this spectrum:




The smaller peaks at 50kHz intervals between the two main peaks are likely the result of the data rate setting, and gives further evidence that the data rate is 50kbps.  The very narrow peaks also hint that there is no modulation smoothing, which rules out the chip's Minimum Shift Keying (MSK) and the Gaussian filtering option available with  Frequency Shirt Keying (FSK).  This leaves just the 2-FSK modulation option, with the deviation set at +/- 150kHz.

LCD Interface

The LCD is connected via a 20 way ribbon cable.  The connector is on the left ofn the photo below:



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)
  • +2.5v
  • Gnd
  • (the remaining 13 pins are connected via capacitors either together or to ground, and are part of a high voltage generator circuit in the LCD)
All the data connections are easily accessible via the test points between the connector and the processor.

Other Processor Connections

The only remaining i/o port connections on the processor are:
  • P0_0 - decoupled voltage divider input (to measure 2.5v to detect battery low condition?)
  • P1_0 - digital output to transducer
  • P2_0 - digital input/output to backlight circuit?
  • P2_3 - digital output to status LED (red?)
  • P2_4 - digital output to status LED (green?)