I always wanted a TFT GUI with a touchscreen interface and it should behave like a smartphone. Pretty ambitious at first, but as I looked into it I came up with simplifications and ways to actually make it happen.
The screen
The tft-controller is the most important part of the project and it needs to have some kind of hardware-scrolling build into it. I started with the very popular ILI9325 and most of the functionality was developed under this controller. But this controller has only a basic implementation of the hardware-scrolling. It is capable of rolling scrolling without any fixed areas. So there is no easy way of adding permanent buttons or text to the screen.
I searched some time for a more suitable controller and settled on the more advanced ILI9341.
This controller can perform vertical rolling scrolling with a top- and a bottom-fixed-area.
Because I wanted the fasted updated rate possible I need a parallel interface to the screen, which was pretty hard to find. I managed to find one on ebay.
The micro
All my projects so far used a pic-microcontroller and I like them because of their rich hardware features. There are hundreds of PICs available, so it is a challenge to pick the right one. I used the 18f47j53 because of several features:
- 8-bit parallel port
- SPI port with DMA
- 12 MIPS
This micro has an 8-bit parallel port but the screen i picked is configured for 16bit data.
In the datasheet of the PIC there is a mode for multiplexing 16bit data onto one 8bit bus, but in this mode the PIC sends two wr-strobs.
I ended up with a bit of a hack. I used the 8 bit data mode with multiplexed adress. In this mode the PIC sends on adress-latch-enable ALE and one wr_strobe, which is perfect.
My final schematic looks like this.
If you have the parallel-port configured, the data transfer looks like this:
//------------------------------------------------------------------------------
//
// Sends a byte of data to the tft
//
// @param data data which should be send
//
void TFTData(uns16 data)
{
while(BUSY);
TFT_DATA;
PMADDRL = data.low8;
PMDIN1L = data.high8;
}
But if you are tansferring a stream of data, you can set the mode to TFT_DATA once and stream out data.
On a PIC a RAM to SFR transfer like this compiles to MOVFF instruction which takes two cycles(8 clock cycles). In the above drawing you can see that with minimal waiting times the parallel-port takes 8 clock cycles for a transfer.
The transfer starts on writing to PMDIN1L. So, by the time you have written the next byte to PMADDRL the parallel-port finished the transfer. So you don't need to check the BUSY-bit.
One last thing. The data in PMADDRL remains in the register after the transfer.
Why is this important?
Because all graphics functions rely on single pixeles and rectangles. And it is important to get these functions as fast as possible.
The DrawFullRect function is implemented by unrolling the pixel loop and sending blocks of pixels with the same color
#define START_PIXEL while(BUSY); TFT_DATA; PMADDRL = color.low8;
#define NEXT_PIXEL PMDIN1L = color.high8;
#define END_PIXEL
.
.
.
// Set up 16bit transfer
START_PIXEL
// Send the 64 pixel blocks
uns8 i;
for(; blocks>0; blocks--)
{
// 64 pixels/block / 16 pixels/pass
for(i=4; i>0; i--)
{
NEXT_PIXEL
NEXT_PIXEL
NEXT_PIXEL
NEXT_PIXEL
NEXT_PIXEL
NEXT_PIXEL
NEXT_PIXEL
NEXT_PIXEL
NEXT_PIXEL
NEXT_PIXEL
NEXT_PIXEL
NEXT_PIXEL
NEXT_PIXEL
NEXT_PIXEL
NEXT_PIXEL
NEXT_PIXEL
}
}
// Send all remaining pixels
for(; rest>0; rest--)
{
NEXT_PIXEL
}
END_PIXEL
All of this allows a pixel-transferrate of 6MHz for pixels with the same color. A screenfill takes about 13ms.
Graphics
The scrolling works by using the hardware scrolling function. First I am moving the content by some movment in y direction. Then I set up some boundaries for the graphics library. So now it is possible to draw a complet page everytime, but only the slice which wrapped around and therefor has to be updated, gets drawn by the graphics functions.
Here is a simple example of what I mean.
//------------------------------------------------------------------------------
//
// Draws a single pixle onto the screen
//
// @param x x position
// @param y y position
// @param color color of the screen
//
void DrawPixel(glcdPos x, glcdPos y, glcdColor color)
{
// check if pixel is on screen
if(x > GetWidth()) return;
if(y > bounds_y_max) return;
if(y < bounds_y_min) return;
SetBox(x,y, x+1,y+1);
PixData(color);
}
This works for all drawing function: pix, line, rect, circle and bmp.
You only have to construct some function which can draw a specific page to the screen and by calling the scrolling function just befor it, it handles all the scrolling and sets up those boundaries.
How to use
Here is a small example programm, which shows how simple a list is integrated. This app displays a scolling list with no fixed areas. Every page has a different color and some text. There are 4 pages.
void testScrollPage(uns8 page)
{
if(page == 0) return;
static const glcdColor color_table[] = {BLUE, GREEN, RED, BROWN};
glcdColor color = color_table[page-1];
// Draw BG
ClearLcd(color);
printf(" PAGE: %1u",page,120,150,WHITE,color,X_CENTER|BOLD,2);
}
void testDrawPage(uns8 page)
{}
void testApp(void)
{
while(1)
{
// get a valid touch
EventManager();
uns16 x @ e.touch_data.x;
uns16 y @ e.touch_data.y;
switch(e.type)
{
case BACK_APP:
case CLOSE_APP:
e.type = NONE;
return;
break;
case OPEN_APP:
// OPEN_APP event must be cleared by app
e.type = NONE;
// 4 pages, listheight is 320px per page, no fixed areas
InitScroller(TEST, 4, 320*4,0,0);
ScrollPageTo(0);
break;
case TOUCH_DOWN:
break;
case TOUCH_MOVE:
break;
case TOUCH_UP:
break;
case NONE:
break;
}
ScrollPageBy(e.dy);
}
}
Here is a more usefull example programm of a list with tiles. Each tile has it's own title and you could add more to the data struct if you want. e.g. some description or a path to an icon. On top of the screen is a fixed area for the actionbar. You can place your app title or some button there.
In the code below you can see that there are two functions which draw pages.
The first(testListScrollPage) and most important draws the page with the above described sclice methode.
The second(testListDrawPage) one can always draw on the screen. This on is for dynamic changes like the highlighting of the tiles or if you want to update a clock which is displayed on some tile.
If you touch on one tile the tileDetails function gets called but you could call a diffrent function for every tile or call the same function and use parameters.
For example every tile has a title and a value. And you draw a slider based on that value. When you touch on a tile you can use the x-pos of the touch as input and change the slider and the value.
typedef struct str_TEST_LIST_ITEM
{
const size2 char *text;
} TEST_LIST_ITEM;
const TEST_LIST_ITEM test_list_data[] = {
// PAGE_1
{"Tile 1"},
{"Tile 2"},
{"Tile 3"},
{"Tile 4"},
{"Tile 5"},
{"Tile 6"},
// PAGE_2
{"Tile 7"},
{"Tile 8"},
{"Tile 9"},
{"Tile 10"},
{"Tile 11"},
{"Tile 12"},
// PAGE_3
{"Tile 13"},
{"Tile 14"},
{"Tile 15"},
{"Tile 16"},
{"Tile 17"},
{"Tile 18"},
};
#include "test_list.h"
//------------------------------------------------------------------------------
//
// Draws the specified page
//
// @param page page whisch shouled be drawn
//
void testListScrollPage(uns8 page)
{
if(page == 0) return;
// Calculate the first and last tile on screen
uns8 tile_cnt = (page-1)*TEST_LIST_TILES_PER_PAGE;
uns8 tile_cnt_max = tile_cnt+TEST_LIST_TILES_PER_PAGE;
if(tile_cnt_max > test_list.numb_entries) tile_cnt_max = test_list.numb_entries;
// Set drawing pos
uns16 screen_pos = TEST_LIST_TOP_OF_LIST;
// Draw all tile on this page
for(; tile_cnt<tile_cnt_max; tile_cnt++)
{
screen_pos -= TEST_LIST_TILE_HEIGHT;
const size2 char *text = test_list_data[tile_cnt].text;
// Draw tile BG
DrawFullRect(0, screen_pos+1, 240, screen_pos+TEST_LIST_TILE_HEIGHT, TEST_LIST_BG);
// Draw tile text
printf(text,0, TEST_LIST_TEXT_XPOS, screen_pos+TEST_LIST_TEXT_YOFFSET,
CONTACT_LIST_TEXT_COLOR, CONTACT_LIST_BG, BOLD,1);
// Draw seperator
DrawFullRect(0, screen_pos, 240 , screen_pos+1, TEST_LIST_SEPERATOR_COLOR);
}
}
void testListDrawPage(uns8 page)
{
// pages must be bigger than 0
if(page == 0) return;
// Scrolling part is done
scroller.status = NONE;
// Calculate the first and last tile on screen
uns8 tile_cnt = (page-1)*TEST_LIST_TILES_PER_PAGE;
uns8 tile_cnt_max = tile_cnt+TEST_LIST_TILES_PER_PAGE;
if(tile_cnt_max > test_list.numb_entries) tile_cnt_max = test_list.numb_entries;
// Set drawing pos
uns16 screen_pos = TEST_LIST_TOP_OF_LIST;
// Draw all tile on this page
for(; tile_cnt<tile_cnt_max; tile_cnt++)
{
screen_pos -= TEST_LIST_TILE_HEIGHT;
if((tile_cnt == test_list.tile_number_old)||(tile_cnt == test_list.tile_number))
{
glcdColor color;
if(tile_cnt == test_list.tile_number) color = TEST_LIST_HIGHLIGHT_COLOR;
else if(tile_cnt == test_list.tile_number_old) color = TEST_LIST_BG;
// Draw the border
DrawRect(0, screen_pos+1, 240 , screen_pos+TEST_LIST_TILE_HEIGHT-1,0, color, NORMAL);
}
}
}
//------------------------------------------------------------------------------
//
// Is this a touch on the scrolling list
//
// @param y y pos of the touch event
//
bit testListTouchOnList(uns16 y)
{
if(y < TEST_LIST_TOP_OF_LIST) return TRUE;
return FALSE;
}
//------------------------------------------------------------------------------
//
// Caluclate the touched tile from the relative coordinates from the touchpanel
//
// @param x, y touch coords.
//
// @return returns the tile
//
uns8 testListFindTile(glcdPos x, glcdPos y)
{
return (GetAbsTouch(y) / TEST_LIST_TILE_HEIGHT);
}
void tileDetails(uns8 index)
{
while(1)
{
// get a valid touch
EventManager();
glcdPos x @ e.touch_data.x;
glcdPos y @ e.touch_data.y;
switch(e.type)
{
case BACK_APP:
case CLOSE_APP:
e.type = NONE;
return;
break;
case OPEN_APP:
// OPEN_APP event must be cleared by app
e.type = NONE;
// Draw BG
OpenBounds();
DrawFullRect(0, 0, 240 , 320-ACTION_BAR_HEIGHT, TEST_LIST_BG);
// Draw the action Bar
DrawFullRect(0, 320-ACTION_BAR_HEIGHT, 240, 320, TEST_LIST_ACTION_BAR_COLOR);
printf(test_list_data[index].text,0, 10,283, WHITE, WIN_BLUE,BOLD,2);
break;
case TOUCH_DOWN:
break;
case TOUCH_MOVE:
break;
case TOUCH_UP:
break;
case NONE:
break;
}
}
}
//------------------------------------------------------------------------------
//
// Basic scrolling list with some functionality behind each tile
//
void testListApp(void)
{
while(1)
{
// get a valid touch
EventManager();
glcdPos x @ e.touch_data.x;
glcdPos y @ e.touch_data.y;
switch(e.type)
{
case BACK_APP:
case CLOSE_APP:
e.type = NONE;
return;
break;
case OPEN_APP:
{
// OPEN_APP event must be cleared by app
e.type = NONE;
// Draw BG
OpenBounds();
DrawFullRect(0, 0, 240 , 320-ACTION_BAR_HEIGHT, TEST_LIST_BG);
// Draw the action Bar
DrawFullRect(0, 320-ACTION_BAR_HEIGHT, 240, 320, TEST_LIST_ACTION_BAR_COLOR);
printf("Test list",0, 10,283, WHITE, WIN_BLUE,BOLD,2);
test_list.tile_number_old = -2;
test_list.tile_number = -1;
test_list.numb_entries = sizeof(test_list_data)/sizeof(TEST_LIST_ITEM);
uns8 page = (test_list.numb_entries+(TEST_LIST_TILES_PER_PAGE-1))/TEST_LIST_TILES_PER_PAGE;
InitScroller(TEST_LIST,
page,
(uns16)TEST_LIST_TILE_HEIGHT*test_list.numb_entries,
ACTION_BAR_HEIGHT,
0);
ScrollPageTo(-ACTION_BAR_HEIGHT);
}
break;
case TOUCH_DOWN:
{
if(testListTouchOnList(y))
{
// mark tile
test_list.tile_number_old = test_list.tile_number;
test_list.tile_number = testListFindTile(x, y);
}
}
break;
case TOUCH_MOVE:
{
// moved to far
if((e.dx_down > 8)||(e.dy_down > 8))
{
// unmark tile
test_list.tile_number_old = test_list.tile_number;
test_list.tile_number = -1;
}
}
break;
case TOUCH_UP:
// tile selected?
if(test_list.tile_number != -1)
{
// open an app: blocking function
e.type = OPEN_APP;
tileDetails(test_list.tile_number);
// reopen this app
e.type = OPEN_APP;
continue;
}
break;
case NONE:
break;
}
ScrollPageBy(e.dy);
UpdateVisiblePages();
}
}