Neste tutorial, vais aprender a implementar um relógio digital num CrowPanel 1.28″ Round Display com um GC9A01 TFT usando a biblioteca Adafruit_GC9A01A. O código de exemplo para o relógio também te mostrará como usar o ecrã tátil CST816D, o buzzer, o motor de vibração, o relógio em tempo real (RTC) BM8563, o modo deep sleep e a sincronização do tempo pela internet.
Peças Necessárias
Obviamente, vais precisar do módulo CrowPanel ESP32 1.28-inch Round Display para este projeto. Além de um cabo USB-C, que normalmente vem com o módulo de ecrã, não são necessárias outras peças.

CrowPanel ESP32 1.28″ Round Display
Características do CrowPanel 1.28″ Round Display
O módulo CrowPanel 1.28″ é composto por um ecrã TFT redondo (GC9A01) com resolução de 240×240 pixels, um ecrã tátil capacitivo (CST816D) e um ESP32-C3 integrado. A imagem abaixo mostra a frente do módulo:

Além do ESP32, o módulo contém um buzzer, motor de vibração, um relógio em tempo real (BM8563) com bateria de reserva, suporte para bateria LiPo com carregamento e vários botões. A imagem seguinte mostra a parte traseira do módulo, onde podes ver a maioria das peças:

Note que há também um pequeno encoder com suporte a botão (à esquerda), que essencialmente te permite adicionar uma coroa para ajustar a hora como num relógio mecânico de pulso – desde que escrevas o código para isso. Mas, como é totalmente programável, podes controlar todo o tipo de outras funções ou interações da interface de utilizador com ele também.
Para programação (e alimentação) há uma porta USB-C e botões Reset e Boot na parte traseira. Se uma bateria LiPo estiver ligada, será carregada pela porta USB-C.
A tabela seguinte resume as principais características do módulo de ecrã:
| Chip Principal | ESP32-C3 |
| Processador | Processador RISC-V de 32 bits, núcleo único, até 160 MHz |
| Memória | 384 KB ROM, 400 KB SRAM (16 KB para cache), 8 KB SRAM no RTC |
| Tamanho | 1.28 polegadas |
| Resolução | 240*240 |
| Interface de Sinal | SPI |
| Tipo de Toque | Toque Capacitivo |
| Tipo de Painel | TFT LCD, Painel IPS |
| Profundidade de Cor | 262K |
| Brilho | 350 cd/m² |
| Ângulo de Visualização | 178° |
| Botões | Botão Reset, Botão Boot, Botão Personalizado |
| Interface | Interface Tipo-C, Interface para Bateria |
| Encoder | Com função de botão, sem pino (Tamanho do pino: 0.8mm*0.8mm) |
| Alimentação | Módulo: DC5V, Chip Principal: 3.3V |
| Área Ativa | 32.51*32.51mm |
| Dimensões | 42*42*9.8mm |
| Peso Líquido | 15g |
Relógio Digital no CrowPanel 1.28″ Round Display
Nesta secção vamos implementar um Relógio Digital no CrowPanel 1.28″ Round Display. O relógio mostrará o nome do dia atual, a hora e a data. Também poderemos alternar entre o formato 24h e 12h tocando num botão no ecrã. A imagem abaixo mostra o mostrador do relógio com as suas funcionalidades.

Além disso, o relógio pode ser colocado em modo deep sleep ao pressionar o botão físico à direita (traseira) do módulo. Um segundo pressionar acorda o relógio novamente. Isto é essencial se quiseres usar o relógio com bateria.
Também emitiremos um som a cada hora e forneceremos feedback tátil ao pressionar o botão tátil ativando o motor de vibração.
Finalmente, o relógio sincronizará automaticamente o Relógio em Tempo Real (RTC) interno com um servidor de tempo da internet (SNTP) se o WiFi estiver disponível.
Com isso, explorámos a maioria das funcionalidades técnicas do CrowPanel Display – exceto Bluetooth e o Encoder.
Código para Relógio Digital no Ecrã GC9A01
O ecrã do CrowPanel 1.28″ é um GC9A01 com um ecrã tátil CST816D. Usaremos a biblioteca Adafruit_GC9A01A para desenhar no ecrã, pois é muito mais simples que a biblioteca LVGL usada para o demo code que acompanha o CrowPanel.
No entanto, se quiseres implementar uma interface de utilizador mais complexa com menus drop-down e outras funcionalidades, a biblioteca LVGL é a melhor opção.
Abaixo está o código para o Relógio Digital. É um código extenso e sugiro que apenas o explores para teres uma visão geral. Vamos discutir os detalhes a seguir.
#include "WiFi.h"
#include "Adafruit_GFX.h"
#include "Adafruit_GC9A01A.h"
#include "I2C_BM8563.h"
#include "CST816D.h"
// I2C
#define SDA 4
#define SCL 5
#define PI4IO_I2C_ADDR 0x43
// TFT display
#define TFT_CS 10
#define TFT_DC 2
#define TFT_MOSI 7
#define TFT_SCLK 6
#define TFT_RST 4
#define TFT_BKL 2
#define DW 240
#define DH 240
// Touch panel
#define TP_INT 0
#define TP_RST 3
#define BUTTON 1
#define BUZZER 3
#define MOTOR 0
const char* SSID = "SSID";
const char* PWD = "PASSWORD";
bool is_24h = true;
const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";
const char* DAYSTR[] = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
const char* MONTHSTR[] = { "Jan", "Feb", "Mar", "Apr", "May",
"Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
// touch button
const int bx = DW / 2;
const int by = DH - 30;
const int br = 10;
Adafruit_GC9A01A tft(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK);
I2C_BM8563 rtc(I2C_BM8563_DEFAULT_ADDRESS, Wire);
CST816D touch(SDA, SCL, TP_RST, TP_INT);
void init_ioex() {
Wire.begin(SDA, SCL);
Wire.beginTransmission(PI4IO_I2C_ADDR);
Wire.write(0x01); // test register
Wire.endTransmission();
Wire.requestFrom(PI4IO_I2C_ADDR, 1);
uint8_t rxdata = Wire.read();
Wire.beginTransmission(PI4IO_I2C_ADDR);
Wire.write(0x03);
Wire.write((1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4));
Wire.endTransmission();
Wire.beginTransmission(PI4IO_I2C_ADDR);
Wire.write(0x07);
Wire.write(~((1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4)));
Wire.endTransmission();
}
void write_ioex(uint8_t pin_number, bool value) {
Wire.beginTransmission(PI4IO_I2C_ADDR);
Wire.write(0x05); // test register
Wire.endTransmission();
Wire.requestFrom(PI4IO_I2C_ADDR, 1);
uint8_t rxdata = Wire.read();
Wire.beginTransmission(PI4IO_I2C_ADDR);
Wire.write(0x05); // Output register
if (!value)
Wire.write((~(1 << pin_number)) & rxdata);
else
Wire.write((1 << pin_number) | rxdata);
Wire.endTransmission();
Wire.beginTransmission(PI4IO_I2C_ADDR);
Wire.write(0x05); // test register
Wire.endTransmission();
Wire.requestFrom(PI4IO_I2C_ADDR, 1);
rxdata = Wire.read();
}
void set_rtc_time() {
rtc.begin();
struct tm t;
getLocalTime(&t);
I2C_BM8563_TimeTypeDef ts;
ts.hours = t.tm_hour;
ts.minutes = t.tm_min;
ts.seconds = t.tm_sec;
rtc.setTime(&ts);
I2C_BM8563_DateTypeDef ds;
ds.weekDay = t.tm_wday;
ds.month = t.tm_mon + 1;
ds.date = t.tm_mday;
ds.year = t.tm_year + 1900;
rtc.setDate(&ds);
}
void sync_time() {
WiFi.begin(SSID, PWD);
for (int i = 0; (i < 5) && (WiFi.status() != WL_CONNECTED); i++)
delay(100);
if (WiFi.status() == WL_CONNECTED) {
configTzTime(TIMEZONE, "pool.ntp.org");
set_rtc_time();
}
}
void display_time() {
static char buf[16];
static I2C_BM8563_DateTypeDef ds;
static I2C_BM8563_TimeTypeDef ts;
rtc.getDate(&ds);
rtc.getTime(&ts);
tft.setTextColor(GC9A01A_LIGHTGREY, GC9A01A_BLACK);
tft.setCursor(80, 60);
tft.setTextSize(4);
tft.print(DAYSTR[ds.weekDay]);
if (is_24h) {
sprintf(buf, "%02d:%02d:%02d", ts.hours, ts.minutes, ts.seconds);
} else {
int display_hours = ts.hours % 12;
display_hours = display_hours ? display_hours : 12;
const char* period = ts.hours >= 12 ? "pm" : "am";
sprintf(buf, "%02d:%02d %s", display_hours, ts.minutes, period);
}
tft.setTextColor(is_24h ? GC9A01A_WHITE : GC9A01A_YELLOW, GC9A01A_BLACK);
tft.setCursor(45, 110);
tft.setTextSize(3);
tft.print(buf);
sprintf(buf, "%2d %s %04d", ds.date, MONTHSTR[ds.month], ds.year);
tft.setTextColor(GC9A01A_WHITE, GC9A01A_BLACK);
tft.setCursor(45, 150);
tft.setTextSize(2);
tft.print(buf);
}
void sound_hour() {
static I2C_BM8563_TimeTypeDef ts;
rtc.getTime(&ts);
if (ts.minutes == 0 && ts.seconds == 0) {
tone(BUZZER, 1000);
delay(50);
tone(BUZZER, 0);
delay(500);
}
}
void check_touch() {
uint8_t gesture;
uint16_t x, y;
bool touched = touch.getTouch(&x, &y, &gesture);
if (touched) {
if (abs(x - bx) < 2*br && abs(y - by) < 2*br) {
write_ioex(MOTOR, true);
delay(50);
write_ioex(MOTOR, false);
is_24h = !is_24h;
display_touch();
delay(500);
}
}
}
void display_touch() {
if (is_24h) {
tft.fillCircle(bx, by, br, GC9A01A_BLACK);
tft.drawCircle(bx, by, br, GC9A01A_DARKGREY);
} else {
tft.fillCircle(bx, by, br, GC9A01A_DARKGREY);
}
}
void check_button() {
if (digitalRead(BUTTON) == LOW) {
tft.fillScreen(GC9A01A_BLACK);
write_ioex(TFT_BKL, false); // Switch off TFT backlight
delay(300);
esp_deep_sleep_start();
}
}
void init_deep_sleep() {
pinMode(BUTTON, INPUT);
esp_deep_sleep_enable_gpio_wakeup(1ULL << BUTTON, ESP_GPIO_WAKEUP_GPIO_LOW);
}
void init_buzzer() {
pinMode(BUZZER, OUTPUT);
digitalWrite(BUZZER, LOW);
}
void init_tft() {
tft.begin();
tft.setRotation(0);
tft.fillScreen(GC9A01A_BLACK);
display_touch();
}
void set_ioex_pins() {
write_ioex(TFT_RST, true); // TFT Reset
write_ioex(TP_RST, true); // Touch panel reset
write_ioex(TFT_BKL, true); // TFT backlight
}
void setup() {
Serial.begin(115200);
init_ioex();
set_ioex_pins();
touch.begin();
init_buzzer();
init_deep_sleep();
sync_time();
init_tft();
}
void loop() {
static unsigned long st = millis();
if (millis() - st > 300) {
display_time();
sound_hour();
st = millis();
}
check_touch();
check_button();
yield();
}
Vamos dividir o código em componentes para uma compreensão mais clara.
Bibliotecas
O código começa por incluir as bibliotecas necessárias para WiFi, gráficos, controlo do ecrã, relógio em tempo real (RTC) e funcionalidade do painel tátil.
#include "WiFi.h" #include "Adafruit_GFX.h" #include "Adafruit_GC9A01A.h" #include "I2C_BM8563.h" #include "CST816D.h"
Terás de instalar a biblioteca Adafruit_GC9A01A. Basta abrir o Library Manager, procurar por “Adafruit_GC9A01A” e clicar em “INSTALL”. A imagem abaixo mostra como deve ficar após a instalação:

Além disso, precisarás dos ficheiros para o Relógio em Tempo Real I2C_BM8563 e para o ecrã tátil CST816D. Descarrega o Arduino Demo Code, descompacta-o e vai para a pasta ESP32_1.28_Arduino_Demo. Deverá conter as seguintes subpastas:

Nas subpastas LvglWidgets e RTC encontrarás os ficheiros necessários. Copia-os para a pasta do projeto do teu código do relógio digital. O conteúdo da pasta do projeto deverá ser assim:

Como podes ver, nomeei o sketch Arduino como “CrowPanel-Digital-Clock-1-28-Inch.ino” e, como o nome da pasta deve coincidir com o do sketch, é “CrowPanel-Digital-Clock-1-28-Inch”. Mas podes nomear de outra forma, só certifica-te que o nome do sketch e da pasta coincidem.
No entanto, para facilitar, também zippei o meu projeto e podes descarregá-lo usando o seguinte link:
Constantes
De seguida, definimos constantes para a comunicação I2C, pinos do ecrã TFT e pinos do painel tátil.
#define SDA 4 #define SCL 5 #define PI4IO_I2C_ADDR 0x43 #define TFT_CS 10 #define TFT_DC 2 #define TFT_MOSI 7 #define TFT_SCLK 6 #define TFT_RST 4 #define TFT_BKL 2 #define DW 240 #define DH 240 // Touch panel #define TP_INT 0 #define TP_RST 3 #define BUTTON 1 #define BUZZER 3 #define MOTOR 0
Podes encontrar estas definições de pinos no código de exemplo e no Schematics of the CrowPanel 1.28″ Display. Especialmente interessante é a secção abaixo que mostra o conector do TFT e do painel tátil:

É importante notar que o módulo CrowPanel contém um expansor GPIO (PI4IOE5V6408), pois o ecrã TFT utiliza a maioria dos pinos do ESP32. Isso significa que alguns pinos são GPIO do ESP32, por exemplo (BUZZER) e outros são controlados via o expansor GPIO. Especificamente, LCD_RESET (TFT_RST=4), TP_RESET (TP_RST=3), o LED de backlight LED_P2 (TFT_BKL=2) e o motor de vibração MOTOR_P0 (MOTOR=0). Dá uma vista de olhos nos esquemas do expansor GPIO abaixo:

Verás mais tarde no código que os pinos do ESP32 e do expansor GPIO precisam de ser controlados de forma diferente.
Variáveis Globais
Declaramos várias variáveis globais, incluindo credenciais WiFi, definições de fuso horário e arrays para nomes dos dias e meses. Também definimos uma variável booleana para alternar entre os formatos 12h e 24h.
const char* SSID = "SSID";
const char* PWD = "PASSWORD";
bool is_24h = true;
const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";
const char* DAYSTR[] = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
const char* MONTHSTR[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
Note que terás de substituir as credenciais WiFi (SSID, PWD) pelas credenciais da tua rede WiFi.
Muito provavelmente, também quererás alterar o TIMEZONE. Eu uso AEST-10AEDT,M10.1.0,M4.1.0/3, que corresponde a Melbourne, Austrália. Esta string indica o desfasamento do horário padrão e as regras para o horário de verão.
As partes desta definição de fuso horário são as seguintes
- AEST: Australian Eastern Standard Time
- -10: Desfasamento UTC de 10 horas à frente do Tempo Universal Coordenado (UTC)
- AEDT: Australian Eastern Daylight Time
- M10.1.0: A transição para o horário de verão ocorre no 1º domingo de outubro
- M4.1.0/3: A transição de volta para o horário padrão ocorre no 1º domingo de abril, com uma diferença de 3 horas em relação ao UTC.
Para outras definições de fuso horário, consulta o Posix Timezones Database. Basta copiar a string que lá encontras e alterar a constante TIMEZONE em conformidade.
As restantes constantes são apenas strings para os nomes dos dias e meses. Podes substituí-las por strings noutra língua, por exemplo. Mas terás de manter comprimentos semelhantes.
Funções de Inicialização
O código contém várias funções para inicializar vários componentes. A função init_ioex() configura a comunicação I2C e o expansor I/O.
void init_ioex() {
Wire.begin(SDA, SCL);
Wire.beginTransmission(PI4IO_I2C_ADDR);
Wire.write(0x01); // test register
...
}
Este código origina-se do ficheiro de código demo LvglWidgets.ino. Alterei ligeiramente o nome e removi algumas instruções de impressão, mas de resto é o mesmo código.
O mesmo para a função relacionada write_ioex() abaixo, que permite escrever nos pinos GPIO do expansor IO:
void write_ioex(uint8_t pin_number, bool value) {
Wire.beginTransmission(PI4IO_I2C_ADDR);
Wire.write(0x05); // test register
...
}
A função init_tft() define as configurações padrão para desenhar no ecrã TFT e também desenha o círculo branco que representa o botão tátil chamando display_touch():
void init_tft() {
tft.begin();
tft.setRotation(0);
tft.fillScreen(GC9A01A_BLACK);
display_touch();
}
A função init_deep_sleep() configura o botão que desperta o ESP32 do modo deep sleep:
void init_deep_sleep() {
pinMode(BUTTON, INPUT);
esp_deep_sleep_enable_gpio_wakeup(1ULL << BUTTON, ESP_GPIO_WAKEUP_GPIO_LOW);
}
E finalmente, a função init_buzzer() configura o pino do buzzer.
void init_buzzer() {
pinMode(BUZZER, OUTPUT);
digitalWrite(BUZZER, LOW);
}
RTC e Sincronização de Tempo
A função set_rtc_time() inicializa o RTC e define a hora e data atuais com base na hora local obtida pela ligação WiFi.
void set_rtc_time() {
rtc.begin();
struct tm t;
getLocalTime(&t);
// Set RTC time and date...
}
Essa hora local é sincronizada com um servidor de tempo da internet (servidor SNTP) via a função sync_time():
void sync_time() {
WiFi.begin(SSID, PWD);
for (int i = 0; (i < 5) && (WiFi.status() != WL_CONNECTED); i++)
delay(100);
if (WiFi.status() == WL_CONNECTED) {
configTzTime(TIMEZONE, "pool.ntp.org");
set_rtc_time();
}
}
Note que a função tenta 5 vezes ligar ao WiFi e depois desiste. Isto é intencional! Se usares o relógio fora da tua rede WiFi, ele não consegue conectar nem sincronizar ao reiniciar. Isso é aceitável, pois o RTC manterá o controlo do tempo.
No entanto, como só sincronizamos ao reiniciar o ESP32 e o RTC não tem noção de horário de verão, o relógio não ajustará automaticamente a mudança entre DST e ST e vice-versa. Existem soluções para isso. Consulta o tutorial Real-Time-Clock DS3231 with ESP32.
Exibição do Tempo
A função display_time() obtém a hora e data atuais do RTC e exibe-as no ecrã TFT nos locais apropriados. Dependendo do estado da flag is_24h, formata a hora em 12 ou 24 horas.
void display_time() {
static char buf[16];
static I2C_BM8563_DateTypeDef ds;
static I2C_BM8563_TimeTypeDef ts;
...
if (is_24h) {
sprintf(buf, "%02d:%02d:%02d", ts.hours, ts.minutes, ts.seconds);
} else {
int display_hours = ts.hours % 12;
display_hours = display_hours ? display_hours : 12;
const char* period = ts.hours >= 12 ? "pm" : "am";
sprintf(buf, "%02d:%02d %s", display_hours, ts.minutes, period);
}
...
}
Note que não é fácil usar uma fonte diferente para exibir hora e data. A biblioteca Adafruit_GC9A01A permite definir uma cor de fundo ao imprimir texto, o que apaga a hora e data previamente impressas.
tft.setTextColor(GC9A01A_WHITE, GC9A01A_BLACK); ... tft.print(buf);
No entanto, isto só funciona para a fonte padrão, não para fontes proporcionais. Para mais informações, vê o Adafruit GFX Graphics Library Documentation. Uma solução seria usar um Canvas, mas isso é mais complexo e a atualização do ecrã TFT pode ser lenta.
Botão Tátil
A função check_touch() deteta eventos de toque no painel tátil. Se o toque estiver próximo da área definida do botão (o círculo branco), ativa o motor de vibração por um curto período para fornecer feedback tátil.
void check_touch() {
uint8_t gesture;
uint16_t x, y;
bool touched = touch.getTouch(&x, &y, &gesture);
if (touched) {
if (abs(x - bx) < 2*br && abs(y - by) < 2*br) {
write_ioex(MOTOR, true);
delay(50);
write_ioex(MOTOR, false);
is_24h = !is_24h;
display_touch();
delay(500);
}
}
}
Depois alterna o formato da hora e atualiza o ecrã. A imagem seguinte mostra os dois estados do ecrã:

O pequeno círculo branco na parte inferior do ecrã representa o botão tátil e é desenhado pela função display_touch(). Dependendo do seu estado, é desenhado como um círculo preenchido ou vazio:
void display_touch() {
if (is_24h) {
tft.fillCircle(bx, by, br, GC9A01A_BLACK);
tft.drawCircle(bx, by, br, GC9A01A_DARKGREY);
} else {
tft.fillCircle(bx, by, br, GC9A01A_DARKGREY);
}
}
Botão Deep Sleep
A função check_button() verifica se o botão físico na lateral do módulo está pressionado. Se estiver, coloca o dispositivo em modo deep sleep. O mais importante é que também desliga o LED de backlight do ecrã TFT para poupar bateria.
void check_button() {
if (digitalRead(BUTTON) == LOW) {
tft.fillScreen(GC9A01A_BLACK);
write_ioex(TFT_BKL, false); // Switch off TFT backlight
delay(300);
esp_deep_sleep_start();
}
}
Um segundo pressionar no botão acorda o ESP32 novamente. Note que este botão tem um resistor pullup interno e, por isso, fica LOW quando pressionado. Vê os esquemas abaixo:

Funções de Configuração
A função setup() inicializa a comunicação serial, expansor I/O, painel tátil, buzzer e ecrã TFT, e sincroniza a hora.
void setup() {
Serial.begin(115200);
init_ioex();
...
}
Ou seja, o relógio só é sincronizado com a hora da internet ao reiniciar. Isto poupa bateria, pois podes desligar o WiFi após o reinício. No entanto, podes decidir sincronizar com mais frequência, por exemplo, uma vez por hora para captar a mudança para o horário de verão.
Função Loop
A função loop() atualiza continuamente a exibição da hora e data a cada 300 milissegundos e verifica eventos de toque e o estado do botão. Podes adicionar um pequeno delay de 50ms em vez da chamada yield().
void loop() {
static unsigned long st = millis();
if (millis() - st > 300) {
display_time();
sound_hour();
st = millis();
}
check_touch();
check_button();
yield();
}
Demonstração do Relógio Digital
O vídeo curto seguinte mostra o relógio em ação. Podes ver a hora a avançar, a alternância entre os formatos 24h e 12h ao pressionar o botão tátil, e o modo deep sleep:

E está feito! Explorámos a maioria das funcionalidades do CrowPanel 1.28″ Display e agora deves estar numa boa posição para implementar o teu próprio relógio com as suas funcionalidades únicas.
Conclusões
Neste tutorial aprendeste a implementar um relógio digital num CrowPanel 1.28″ Display Module. Usámos muitas funcionalidades do módulo de ecrã, incluindo o ecrã tátil, buzzer, motor de vibração e RTC. Também aprendemos a sincronizar o RTC com um servidor de tempo da internet e a colocar o relógio em modo deep sleep.
Se quiseres aprender mais sobre sincronização de tempo, vê o tutorial How to synchronize ESP32 clock with SNTP server, e o tutorial Real-Time-Clock DS3231 with ESP32 ajudará a implementar suporte para horário de verão.
E, se quiseres implementar um Relógio Analógico, o Analog Clock on e-Paper Display terá informações úteis, apesar de ser direcionado para um ecrã E-Paper e não TFT.
Caso contrário, há muitas funções que podes adicionar ao teu relógio. Uma óbvia seria a exibição de informações meteorológicas. Vê o tutorial Simple ESP32 Internet Weather Station para um exemplo. Também há informações sobre como integrar o módulo connect the CrowPanel no Home Assistant.
Se tiveres dúvidas, sente-te à vontade para deixá-las na secção de comentários.
Divirta-te e boas criações ; )
Links
Abaixo alguns links que achei úteis ao escrever este tutorial:

