FT8XX et l'interface homme-machine capacitif
Comment bien relier l'homme et la machine électronique pour que l'appareil soit utilisés correctement, efficacement et sans acharnement de l'usager?
La majorité des contrôleurs d'écrans bien documentés, compatibles avec une bonne majorité des microcontrôleurs populaires (ATMega, STM32, PIC, MSP430, etc) ne gèrent que les panneaux tactiles résistifs, nécessitent un framebuffer et fonctionent à un taux de rafraîchissement très rapide. Ces défis de conception n'ont souvent pas leur place dans une application à base de microcontrôleur 8bit puisque l'espace programme et la RAM sont extrêmement limités : l'application graphique prends tout l'espace, et le taux de rafraîchissement souvent élevé ampute la vitesse de traitement de l'application principale. Dans un environnement à microcontrôleur, chaque byte compte, donc la saine gestion de l'application graphique et la flexibilité de l'interface de communication sont des aspects cruciaux au succès d'une application fluide et fonctionnelle.
Après avoir développé beaucoup d'application à base d'un convertisseur USB-UART, je découvre que FTDI développe depuis 2 ans des contrôleurs graphiques embarqués, dénommés “EVE” pour “Embedded video engine”. Destiné pour une faible empreinte mémoire, embarquant les ports de communication I2C, SPI, QSPI et fonctionnant au principe d'une liste, le contrôleur est qualifié pour le playback de vidéo AVI. Au fil des recherches, je découvre la compagnie Riverdi Displays qui fabrique des écrans tactiles embarquant le contrôleur EVE FT813.
Voici donc les étapes de développement de l'API de contrôle du FT813.
Basé sur un dsPIC33EP512GM706, fonctionnel sur l'entièreté de la famille PIC (PIC8/16/18, dsPIC, PIC32). Une base de fonction d'accès bas niveau (SPI, I2C, etc) sont prévues pour porter l'API vers le STM32 d'ici 3018.
Tour rapide du FT8XX :
- Support de communication SPI/DualSPI/QSPI/I2C**
- 1MB de RAM graphique embarqué, éliminant le besoin d'une RAM externe
- Fonctionnement basse-latence, éliminant le besoin d'un microcontrôleur 32 bit
- Incorpore différents widget de base, configurable par registres
- Playback audio mono disponible
- Possibilité de brancher des display de résolution allant jusqu'à 800x600
Je vais donc résumer l'essentiel du travail de programmation réalisé jusqu'à présent (8 avril 2017) et discuter des améliorations / ajouts à venir dans les prochaines semaines (le temps libre du stagiaire!).
Détails technique de la librairie :
- Support des modules I2C / SPI hardware de la famille PIC16/PIC18/dsPIC/PIC32
- Définition de la quantitée d'éléments graphiques nécessaires, et ajustement de la compilation pour générer le code le plus léger possible
- Vitesse de communication maximale de 25MHz SPI / 2.7MHz I2C
- Création / destruction / affichage de la liste graphique simplifié par l'API
- Définition et modification des éléments graphiques via l'API
- Fichier de définitions général pour porter la librairie vers de nouveaux MCU
- Gestion de l'écran tactile (TAG) hardware (5 tag disponible) ou software (qté TAG limitée par la mémoire du MCU)
Explication des fonctions actuelles
Un nombre grandissant de fonctions est déja implanté pour simplifier le contrôle du processeur graphique. Voyons en surface la plupart d'entres-elles et leurs différentes utilitées.
Fonction d'initialisation du contrôleur graphique
Elle permet de définir les paramètres de base du contrôleur pour l'interface avec le panneau LCD : Front/Back porch, horizontal display lines, data mode, clock polarity, etc
actuellement en révision pour l'ajout de #defines dans le fichier de définitions général pour ajuster les réglages avant la compilation
void FT_init (void)
{
UC duty = 0, gpio, reg_id_value;
POWER_DOWN_PIN = 1;//Clear FT8XX registers
__delay_ms(50);
POWER_DOWN_PIN = 0;
__delay_ms(50);
POWER_DOWN_PIN = 1;
__delay_ms(50);
FT_write_command(FT800_ACTIVE); //FT801 wake_up command
__delay_ms(10);
FT_write_command(FT800_ACTIVE); //FT801 wake_up command
__delay_ms(10);
FT_write_command(FT800_CLKINT); //Set clock to internal oscillator
__delay_ms(10);
FT_write_command(FT800_CLK48M); //FT801 clock set to 48MHz
__delay_ms(10);
FT_write_command(FT800_CORERST);//reset FT801 core CPU
__delay_ms(10);
FT_write_command(FT800_GPUACTIVE);//activate GPU
__delay_ms(100);
reg_id_value = FT_read_8bit(REG_ID);
while (reg_id_value != 0x7C)//Check if clock switch was performed
{
reg_id_value = FT_read_8bit(REG_ID);
}
//Clock switch was a success, initialize FT801 display parameters
FT_write_8bit(REG_PCLK, 0); // no PCLK on init, wait for init done
FT_write_8bit(REG_PWM_DUTY, 0);// no backlight until init done
FT_write_16bit(REG_HCYCLE, 928);//Hor total line count
FT_write_16bit(REG_HSIZE, 800); //active display width
FT_write_16bit(REG_HOFFSET, 88); //start of active line
FT_write_16bit(REG_HSYNC0, 40); //start of horizontal sync pulse
FT_write_16bit(REG_HSYNC1, 88); //end of horizontal sync pulse
FT_write_16bit(REG_VCYCLE, 525);//Vert total line count
FT_write_16bit(REG_VSIZE, 480); //active display height
FT_write_16bit(REG_VOFFSET, 32); //start of active screen
FT_write_16bit(REG_VSYNC0, 13); //start of vertical sync pulse
FT_write_16bit(REG_VSYNC1, 16); //end of vertical sync pulse
FT_write_8bit(REG_SWIZZLE, 0); //FT800 output to LCD - pin order
FT_write_8bit(REG_PCLK_POL, 0); //PCLK polarity (fixed to LCD bezel)
FT_write_8bit(REG_VOL_PB, ZERO);//No audio volume
FT_write_8bit(REG_VOL_SOUND, ZERO);
FT_write_16bit(REG_SOUND, 0x6000);//Auio syth muted
//***************************************
// Write Initial Display List & Enable Display (clear screen, set ptr to 0)
FT_start_new_dl();
FT_clear_screen(BLACK);
FT_update_screen_dl();
gpio = FT_read_8bit(REG_GPIO);//Read the FT800 GPIO
gpio = gpio | 0x80; //Enable display signal
FT_write_8bit(REG_GPIO, gpio); //Enable the DISP signal to the LCD
FT_write_8bit(REG_PCLK, 2); //Now start clocking data to the LCD
for(duty = 0; duty < 127; duty++)
{
FT_write_8bit(REG_PWM_DUTY,duty);//Backlight turning on
__delay_ms(1);
}
FT_write_8bit(REG_CTOUCH_MODE, 3); //Touch enabled
FT_write_8bit(REG_CTOUCH_EXTENDED, 1);//Compatibility mode
}
La majorité des fonctions bas niveau utilisées dans ces fonctions gèrent par défaut les 2 modes de communication : i2c / spi. Le fichier de définition général permet de sélectionner ce mode de communication.
Fonction de contrôle bas-niveau des accès aux commandes du périphérique
Permet d'écrire la commande spécifiée en paramètre au contrôleur graphique. Prend en compte les 2 modes de communication.
Tel que constaté, la majorité des transactions réalisées sur les bus physiques passent par une structure contenant l'ensemble des paramètres des trames à envoyer et reçevoir, permettant une gestion simple et efficace des fonctions d'interruption pour le contrôle en tandem avec le microcontrôleur.
À venir, l'ajout des définitions des bit d'interruptions pour les différentes familles PIC
void FT_write_command (UC command)
{
#ifdef MODE_SPI
while(IEC0bits.SPI1IE);//Wait for previous transaction to end
spi_struct.spi_tx_data[0] = command;//Command at index 0
spi_struct.spi_tx_data[1] = 0x00; //Null bytes
spi_struct.spi_tx_data[2] = 0x00; //
spi_struct.spi_chip = FT_8XX; //Select FT801 CS
spi_struct.spi_rd_cnt = 0; //Clear RD_cnt var
spi_struct.spi_free = 0; //clear SPI_free
spi_struct.wr_length = 3; //set write_length
IEC0bits.SPI1IE = 1; //enable SPI interrupt
spi_assert_cs(spi_struct.spi_chip); //assert FT801 cs
SPI1BUF = spi_struct.spi_tx_data[0];//send first SPI byte
#endif
#ifdef MODE_I2C
while(IEC1bits.MI2C1IE==1); //Wait for previous transaction to end
i2c_struct.i2c_adress = FT801_ADR;
i2c_struct.i2c_tx_data[0] = command;//Command at index 0
i2c_struct.i2c_tx_data[1] = 0x00; //Null bytes
i2c_struct.i2c_tx_data[2] = 0x00; //
i2c_struct.i2c_message_mode = I2C_WRITE; // write
i2c_struct.i2c_done = 1; //busy i2c
i2c_struct.i2c_message_length = 3; //set write_length
IEC1bits.MI2C1IE = 1; //enable ssp interrupt
I2C1CONbits.SEN = 1; //this line sets SSP1IF
#endif
}
Le même principe est utilisé pour lire/écrire des données vers le contrôleur graphque :
void FT_write_8bit (UL adr, UC data)
{
#ifdef MODE_SPI
while(IEC0bits.SPI1IE);
spi_struct.spi_tx_data[0] = (UC)((adr >> 16) | MEM_WRITE);//Write ADR
spi_struct.spi_tx_data[1] = (UC)(adr>>8);//
spi_struct.spi_tx_data[2] = adr; //
spi_struct.spi_tx_data[3] = data; //Data into spi buffer
spi_struct.spi_chip = FT_8XX; //Select FT801 CS
spi_struct.spi_rd_cnt = 0; //Reset RD cnt to 0
spi_struct.spi_free = 0; //spi bus is busy
spi_struct.wr_length = 4; //set write length
IEC0bits.SPI1IE = 1; //enable spi interrupt
spi_assert_cs(spi_struct.spi_chip); //assert FT801 cs
SPI1BUF = spi_struct.spi_tx_data[0];//send first SPI byte
#endif
#ifdef MODE_I2C
while(IEC1bits.MI2C1IE ==1);
i2c_struct.i2c_adress = FT801_ADR;
i2c_struct.i2c_tx_data[0] = (UC)((adr >> 16) | MEM_WRITE); //Write ADR
i2c_struct.i2c_tx_data[1] = (UC)(adr>>8);//
i2c_struct.i2c_tx_data[2] = adr; //
i2c_struct.i2c_tx_data[3] = data; //
i2c_struct.i2c_message_mode = I2C_WRITE;//write
i2c_struct.i2c_done = 1; //busy i2c
i2c_struct.i2c_message_length = 4; //set write_length
IEC1bits.MI2C1IE = 1;
I2C1CONbits.SEN = 1;
#endif
}
La magie de la compilation conditionnelle commence dans le fichier de définitions général, où l'utilisateur de la librairie définit son mode de communication ainsi que les widgets graphiques utilisés. Par la suite, les fonctions nécessaires deviennent accessibles et l'utilisateur peut commencer à coder l'interface. Dans le cas de l'utilisation d'un widget non déclaré dans le fichier général, le compilateur génère une erreur de compilation spécifiant que les fonctions utilisées sont inexistantes, rappelant au programmeur de sélectionner la bonne quantité de primitive avant de compiler:
//Definition des primitives à compiler
#define MAX_STR_LEN 20
#define MAX_RECT_NB 0
#define MAX_WINDOW_NB 0
#define MAX_SLIDER_NB 0
#define MAX_BUTTON_NB 0
#define MAX_TEXT_NB 0
#define MAX_NUMBER_NB 0
#define MAX_TOGGLESW_NB 0
#define MAX_DIAL_NB 0
#define MAX_PROGRESS_NB 0
#define MAX_SCROLLER_NB 0
#define MAX_CLOCK_NB 1
#define MAX_GAUGE_NB 0
#define MAX_KEYS_NB 0
#if MAX_KEYS_NB > 0
typedef struct
{
UI x;
UI y;
UI w;
UI h;
UI f;
UI opt;
UC len;
C str[MAX_STR_LEN];
}STKeys;
extern STKeys st_Keys[MAX_KEYS_NB];
#endif
Ci-haut, la compilation conditionnelle de la primitive “Key”, qui affiche des touches avec le texte associé. Si la quantitée du #define reste à 0, toutes les fonctions faisant appel à la structure STKeys deviennent inaccessibles. Ceci permet de générer un code source léger qui inclut seulement les fonctions nécessaires à la gestion de l'interface conçue par le programmeur.
La création d'éléments graphique est somme toute simple, tout les paramètres possibles des primitives graphiques sont nécessaires lors de l'appel de la fonction d'initialisation. Les données sont sauvegardées dans une structure propre à chaque éléments graphiques. Voici un exemple de ces fonctions
#if MAX_SLIDER_NB > 0
void init_slider (UC number, UI x, UI y, UI w, UI h, UI opt, UI v, UI r)
{
st_Slider[number].X1 = x;
st_Slider[number].Y1 = y;
st_Slider[number].Width = w;
st_Slider[number].Height = h;
st_Slider[number].opt = opt;
st_Slider[number].Value = v;
st_Slider[number].Range = r;
}
Le paramètre “number” est dynamiquement géré par le programme pour l'accès aux paramètres des différents éléments graphiques. P.S notez bien le #if présent avant le prototype de la fonction.
Par la suite, la fonction de l'affichage d'éléments graphique est appelée en spécifiant l'adresse de la structure de l'élément à afficher. Dans un programme complet, une seule variable par primitive permet de gérer cette option.
void FT_draw_slider (STSlider *st_Slider)
{
FT_write_dl(CMD_SLIDER);
FT_write_dl_int(st_Slider->X1); // x
FT_write_dl_int(st_Slider->Y1); // y
FT_write_dl_int(st_Slider->Width); // width
FT_write_dl_int(st_Slider->Height); // height
FT_write_dl_int(st_Slider->opt); // option
FT_write_dl_int(st_Slider->Value); // 16 bit value
FT_write_dl(st_Slider->Range); // 32 bit range
}
Finalement, la majorité des primitives vont interagir et se modifier lorsque l'usager appuiera sur l'écran tactile. Il faut donc prévoir ces interactions et ajuster le graphisme de la primitive en fonction du dernier toucher effectué par l'usager.
Prenons l'exemple de la primitive du “Slider” :
Dans le document “FT8XX programmers guide” publié par FTDI, on y observe les paramètres de la primitive ainsi que son allure visuelle. Habitué par les interfaces tactiles d'Android et de iOS, un usager agile tentera de déplaçer le “bouton” de la barre pour en changer la valeur.
L'API gère déja ces cas d'utilisations. Voici la fonction qui calcule la nouvelle position du “bouton” de la barre et modifie l'élément graphique :
unsigned int FT_slider_update (STTouch touch_read, STSlider *st_Slider)
{
static unsigned int old_value=0, new_value=0;
UI yMin = (st_Slider->Y1 - (3*st_Slider->Height));//min y value
UI yMax = (st_Slider->Y1 + (3*st_Slider->Height));//max y value
UI xMin = (st_Slider->X1);//min x value
UI xMax = (st_Slider->X1 + st_Slider->Width);//max x value
UI step = ((xMax - xMin) / st_Slider->Range);//values step
//UI step = (st_Slider->Range / st_Slider->Width);//values step
//Verify that actual touch position is inside specified slider positions
//if its the case, calculate new value proportionnal to range
if ((touch_read.Y0 >= yMin) && (touch_read.Y0 <= yMax))
{
if ((touch_read.X0 >= xMin) && (touch_read.X0 <= xMax))
{
old_value = new_value;//calculate new value
new_value = ((touch_read.X0 - xMin)/step);//
st_Slider->Value = new_value;//
return (new_value);//
//st_Slider->Value = new_value;
}
else {st_Slider->Value = old_value; return (old_value);}
}
else {st_Slider->Value = old_value; return (old_value);}
}
#endif //#if MAX_SLIDER_NB > 0
La fonction est appelée dans le gestionnaire d'interface :
if (st_Window[9].ucReadOK)
{
slide_value_6 = FT_slider_update (touch_data, &st_Slider[5]);
FT_modify_number(&st_Number[5], slide_value_6);
}
La condition “if (st_Window[9].ucReadOK)” est le threshold logiciel qui indique si une fenêtre tactile est actuellement appuyé ou en veille.
La fonction FT_slider_update est appelée dans le cas où l'usager appuie dans l'espace tactile du slider[5]. Elle retourne un Uint représentant l'emplacement du point sur la barre. L'appel de la fonction “FT_modify_number” permet de modifier la valeur d'une primitive “number” qui affiche la valeur slide_value en pourcentage (0-100).
Notez bien le #endif à la fin de la fonction. Tel qu'énoncé plus haut, si le nombre de primitives de type “Slider” est à 0, les fonctions ci-dessus ne compilent pas, réduisant ainsi la quantitée de mémoire utilisée.
La création de la liste graphique est simplifiée par l'appel de fonction de base :
FT_start_new_dl();
FT_clear_screen(BLACK);
FT_set_fcolor (FOREST);
FT_draw_text(&st_Text[0]);
FT_draw_button(&st_Button[0]);
FT_draw_button(&st_Button[1]);
FT_draw_button(&st_Button[2]);
FT_draw_button(&st_Button[3]);
FT_draw_slider(&st_Slider[0]);
FT_draw_number(&st_Number[0]);
FT_draw_slider(&st_Slider[1]);
FT_draw_number(&st_Number[1]);
FT_draw_slider(&st_Slider[2]);
FT_draw_number(&st_Number[2]);
FT_draw_slider(&st_Slider[3]);
FT_draw_number(&st_Number[3]);
FT_draw_slider(&st_Slider[4]);
FT_draw_number(&st_Number[4]);
FT_draw_slider(&st_Slider[5]);
FT_draw_number(&st_Number[5]);
FT_update_screen_dl();
La fonction “FT_start_new_dl()” initialise le pointeur d'écriture à la valeur par défaut du ring bufgfer du FT813.
La fonction “FT_clear_screen(BLACK)” initialise l'écran, de couleur noir. La valeur “BLACK” est un unsigned long qui accepte les valeur de couleurs RGB 16M sur 24 bit.
La fonction “FT_set_fcolor(FOREST)” initiale le “foreground color scheme” à la couleur vert forêt. La valeur “FOREST” est un unsigned long qui accepte les valeur de couleurs RGB 16M sur 24 bit.
Les différentes fonctions de type “draw” écrit les valeurs des primitives vers le FT813. Voici l'exemple d'un string :
void FT_draw_text (STText *st_Text)
{
unsigned char c=0;
FT_write_dl(CMD_TEXT); // FT text command
FT_write_dl_int(st_Text->x); // x position on screen
FT_write_dl_int(st_Text->y); // y position on screen
FT_write_dl_int(st_Text->font);// font parameter
FT_write_dl_int(st_Text->opt); // FT text primitives options
while (c < st_Text->len) // write text until eos
{
FT_write_dl_char(st_Text->str[c]);
c++;
}
}
Somme toute simple, elle inscrit les paramètre de la structure spécifié vers les registres du FT813. À une vitesse de communication de 25mbps, le tout répond extrêmement vite.
La fonction “FT_update_screen_dl()” permet d'écrire la taille de la liste d'affichage vers le contrôleur:
void FT_update_screen_dl (void)
{
FT_write_dl(DISPLAY()); // Request display swap
FT_write_dl(CMD_SWAP); // swap internal display list
FT_write_16bit(REG_CMD_WRITE, cmdOffset); // Write list to display
}
Voici un exemple de gestionnaire tactile et l'affichage sur l'écran :
touch_data = FT_touchpanel_read(touch_data); //read touch data
for (c=0; c<MAX_WINDOW_NB; c++) //scan through windows
{
st_Window[c].ucReadOK = ucCheckTouchWindow(&st_Window[c], touch_data);
}
for (c=0; c<MAX_WINDOW_NB; c++)
{
if (st_Window[c].ucReadOK){CHANGE_HAPPENED++;}
else{st_Window[c].one_touch=0;}
}
if (CHANGE_HAPPENED>0)
{
CHANGE_HAPPENED = 0;
FT_start_new_dl();
FT_clear_screen(BLACK);
FT_set_fcolor (FOREST);
FT_draw_text(&st_Text[0]);
FT_draw_button(&st_Button[0]);
FT_draw_button(&st_Button[1]);
FT_draw_button(&st_Button[2]);
FT_draw_button(&st_Button[3]);
//Default screen drawing at each timebase
if (st_Window[0].ucReadOK)
{
//WHEELS_forward(30, 30);
LED0 = LED_ON;
}
if (st_Window[1].ucReadOK)
{
//WHEELS_backward(30, 30);
LED1 = LED_ON;
}
if (st_Window[2].ucReadOK)
{
//WHEELS_left(30, 30);
LED2 = LED_ON;
}
if (st_Window[3].ucReadOK)
{
//WHEELS_right(30, 30);
LED3 = LED_ON;
}
if (st_Window[4].ucReadOK)
{
slide_value_1 = FT_slider_update (touch_data, &st_Slider[0]);
FT_modify_number(&st_Number[0], slide_value_1);
PWM_change_duty(PWM_1L, slide_value_1);
}
if (st_Window[5].ucReadOK)
{
slide_value_2 = FT_slider_update (touch_data, &st_Slider[1]);
FT_modify_number(&st_Number[1], slide_value_2);
PWM_change_duty(PWM_1H, slide_value_2);
}
if (st_Window[6].ucReadOK)
{
slide_value_3 = FT_slider_update (touch_data, &st_Slider[2]);
FT_modify_number(&st_Number[2], slide_value_3);
PWM_change_duty(PWM_2L, slide_value_3);
}
if (st_Window[7].ucReadOK)
{
slide_value_4 = FT_slider_update (touch_data, &st_Slider[3]);
FT_modify_number(&st_Number[3], slide_value_4);
PWM_change_duty(PWM_2H, slide_value_4);
}
if (st_Window[8].ucReadOK)
{
slide_value_5 = FT_slider_update (touch_data, &st_Slider[4]);
FT_modify_number(&st_Number[4], slide_value_5);
PWM_change_duty(PWM_6L, slide_value_5);
}
if (st_Window[9].ucReadOK)
{
slide_value_6 = FT_slider_update (touch_data, &st_Slider[5]);
FT_modify_number(&st_Number[5], slide_value_6);
PWM_change_duty(PWM_6H, slide_value_6);
}
FT_draw_slider(&st_Slider[0]);
FT_draw_number(&st_Number[0]);
FT_draw_slider(&st_Slider[1]);
FT_draw_number(&st_Number[1]);
FT_draw_slider(&st_Slider[2]);
FT_draw_number(&st_Number[2]);
FT_draw_slider(&st_Slider[3]);
FT_draw_number(&st_Number[3]);
FT_draw_slider(&st_Slider[4]);
FT_draw_number(&st_Number[4]);
FT_draw_slider(&st_Slider[5]);
FT_draw_number(&st_Number[5]);
FT_update_screen_dl();
}
Le résultat de l'affichage de cette liste à l'écran :
La librairie est en constante évolution, et avec l'arrivée de ma carte de développement dsPIC, le tout avance rapidement. Le but de ce projet est d'avancer cette librairie pour en faire un élément central de mes projets futurs, simplifier le débugging pour en faire une sortie standard uart (microcontrôleur dédié bridge UART / SPI) ou pour contrôler un robot à distance. Les possibilités sont larges et l'imagination est infinie.