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!