This is a four part series:
- Part 1 – Electronics
- Part 2 – Display Driver
- Part 3 – Algorithms
- Part 4 – Programming
Talking to the LCD module
First, the pin mappings as shown in the previous diagram:
// Description Pin on LCD display
// LCD Vcc .... Pin 1 +3.3V (up to 7.4 mA) Chip power supply
#define PIN_SCLK 2 // LCD SPIClk . Pin 2 Serial clock line of LCD // Was 2 on 1st prototype
#define PIN_SDIN 5 // LCD SPIDat . Pin 3 Serial data input of LCD // Was 5 on 1st prototype
#define PIN_DC 3 // LCD Dat/Com. Pin 4 (or sometimes labelled A0) command/data switch // Was 3 on 1st prototype
// LCD CS .... Pin 5 Active low chip select (connected to GND)
// LCD OSC .... Pin 6 External clock, connected to vdd
// LCD Gnd .... Pin 7 Ground for VDD
// LCD Vout ... Pin 8 Output of display-internal dc/dc converter - Left floating - NO WIRE FOR THIS. If we added a wire, we could connect to gnd via a 100nF capacitor
#define PIN_RESET 4 // LCD RST .... Pin 9 Active low reset // Was 4 on prototype
#define PIN_BACKLIGHT 6 // - Backlight controller. Optional. It's connected to a transistor that should be connected to Vcc and gnd
#define LCD_C LOW
#define LCD_D HIGH
We need to initialize the LCD:
void LcdClear(void)
{
for (int index = 0; index < LCD_X * LCD_Y / 8; index++)
LcdWrite(LCD_D, 0x00);
}
void LcdInitialise(void)
{
// pinMode(PIN_SCE, OUTPUT);
pinMode(PIN_RESET, OUTPUT);
pinMode(PIN_DC, OUTPUT);
pinMode(PIN_SDIN, OUTPUT);
pinMode(PIN_SCLK, OUTPUT);
pinMode(PIN_BACKLIGHT, OUTPUT);
digitalWrite(PIN_RESET, LOW); // This must be set to low within 30ms of start up, so don't put any long running code before this
delay(30); //The res pulse needs to be a minimum of 100ns long, with no maximum. So technically we don't need this delay since a digital write takes 1/8mhz = 125ns. However I'm making it 30ms for no real reason
digitalWrite(PIN_RESET, HIGH);
digitalWrite(PIN_BACKLIGHT, HIGH);
LcdWrite(LCD_C, 0x21 ); // LCD Extended Commands.
LcdWrite(LCD_C, 0x80 + 0x31 ); // Set LCD Vop (Contrast). //0x80 + V_op The LCD voltage is: V_lcd = 3.06 + V_op * 0.06
LcdWrite(LCD_C, 0x04 + 0x0 ); // Set Temp coefficent. //0 = Upper Limit. 1 = Typical Curve. 2 = Temperature coefficient of IC. 3 = Lower limit
LcdWrite(LCD_C, 0x10 + 0x3 ); // LCD bias mode 1:48. //0x10 + bias mode. A bias mode of 3 gives a "recommended mux rate" of 1:48
LcdWrite(LCD_C, 0x20 ); // LCD Basic Commands
LcdWrite(LCD_C, 0x0C ); // LCD in normal mode.
}
/* Write a column of 8 pixels in one go */
void LcdWrite(byte dc, byte data)
{
digitalWrite(PIN_DC, dc);
shiftOut(PIN_SDIN, PIN_SCLK, MSBFIRST, data);
}
/* gotoXY routine to position cursor
x - range: 0 to 84
y - range: 0 to 5
*/
void gotoXY(int x, int y)
{
LcdWrite( 0, 0x80 | x); // Column.
LcdWrite( 0, 0x40 | y); // Row.
}
So this is pretty straightforward. We can’t write per pixel, but must write a column of 8 pixels at a time.
This causes a problem – we don’t have enough memory to make a framebuffer (we have only 2kb in total!), so all drawing must be calculated on the fly, with the whole column of 8 pixels calculated on the fly.
Snake game
The purpose of all of this is to create a game of snake. In our game, we want the snake to be 3 pixels wide, with a 1 pixel gap. It must be offset by 2 pixels though because of border.
The result is code like this:
/* x,y are in board coordinates where x is between 0 to 19 and y is between 0 and 10, inclusive*/
void update_square(const int x, const int y)
{
/* Say x,y is board square 0,0 so we need to update columns 1,2,3,4
* and rows 1,2,3,4 (column and row 0 is the border).
* But we have actually update row 0,1,2,3,4,5,6,7 since that's how the
* lcd display works.
*/
int col_start = 4*x+1;
int col_end = col_start+3; // Inclusive range
for(int pixel_x = col_start; pixel_x <= col_end; ++pixel_x) {
int pixelrow_y_start = y/2;
int pixelrow_y_end = (y+1)/2; /* Inclusive. We are updating either 1 or 2 lcd block, each with 2 squares */
int current_y = pixelrow_y_start*2;
for(int pixelrow_y = pixelrow_y_start; pixelrow_y <= pixelrow_y_end; ++pixelrow_y, current_y+=2) {
/* pixel_x is between 0 and 83, and pixelrow_y is between 0 and 5 inclusive */
int number = 0; /* The 8-bit pixels for this column */
if(pixelrow_y == 0)
number = 0b1; /* Top border */
else
number = get_image(x, current_y-1, (pixel_x-1)%4) >> 3;
if( current_y < ARENA_HEIGHT) {
number |= (get_image(x, current_y, (pixel_x-1)%4) << 1);
number |= (get_image(x, current_y+1, (pixel_x-1) %4) << 5);
}
gotoXY(pixel_x, pixelrow_y);
LcdWrite(1,number);
}
}
}
int get_image(int x, int y, int column)
{
if( y >= ARENA_HEIGHT)
return 0;
int number = 0;
if( y == ARENA_HEIGHT-1 )
number = 0b0100000; /* Bottom border */
if(board.hasSnake(x,y))
return number | get_snake_image(x, y, column);
if(food.x == x && food.y == y)
return number | get_food_image(x, y, column);
return number;
}
int get_food_image(int x, int y, int column)
{
if(column == 0)
return 0b0000;
if(column == 1)
return 0b0100;
if(column == 2)
return 0b1010;
if(column == 3)
return 0b0100;
}
/* Column 0 and row 0 is the gap between the snake*/
/* Column is 0-3 and this returns a number between
* 0b0000 and 0b1111 for the pixels for this column
The MSB (left most digit) is drawn below the LSB */
int get_snake_image(int x, int y, int column)
{
if(column == 0) {
if(board.snakeFromLeft(x,y))
return 0b1110;
else
return 0;
}
if(board.snakeFromTop(x,y))
return 0b1111;
else
return 0b1110;
}
Pretty messy code, but necessary to save every possible byte of memory.
Now we can call
update_square(x,y)
when something changes in board coordinates x,y, and this will redraw the screen at that position.
So now we just need to implement the snake logic to create a snake and get it to play automatically. And do so in around 1 kilobyte of memory!