#include <TFT_eSPI.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/semphr.h>
#include <esp_task_wdt.h>
#include "image.h"
#include "music.h"
#include "nes_audio.h"
// ----------------------
// Configuração de pinos
// ----------------------
#define BUZZER_PIN 26 // Pino do buzzer (speaker)
#define SELECTOR_PIN 27 // Pino do botão
// ----------------------
// Configuração do PWM
// ----------------------
#define backlightChannel 0
#define buzzerChannel 2
// Macro para calcular o tamanho de um array
#define array_length(x) (sizeof(x) / sizeof(x[0]))
// ----------------------
// Instância da classe Cartridge para reprodução de áudio NES
// Utiliza o pino do buzzer para saída de áudio
// ----------------------
Cartridge player(BUZZER_PIN);
// Flag para controle do estado do botão
bool press = false;
// ----------------------
// Configuração da tela e sprites
// ----------------------
TFT_eSPI tft = TFT_eSPI();
TFT_eSprite img = TFT_eSprite(&tft); // Sprite para imagem (ex: nave)
TFT_eSprite txt = TFT_eSprite(&tft); // Sprite para textos
// Número máximo de linhas para o texto rolante
#define maxLine 11
// ----------------------
// Variáveis para a tarefa de música e geração de números pseudoaleatórios
// ----------------------
TaskHandle_t TaskHandle0 = NULL;
uint8_t za, zb, zc, zx;
// Texto a ser exibido de forma rolante
const String scrollText[maxLine] = {
"[ Real Time OS + Sprite Testing ]",
"Music runs on CORE-0 / Graphic runs on CORE-1",
"SPRITE: Text scrolling / X-Wing Fighter",
"=============================================",
"The dead speak! The galaxy has heard a mysterious",
"broadcast, a threat of REVENGE, in the",
"sinister voice of the late EMPEROR PALPATINE.",
"GENERAL LEIA ORGANA dispatches secret agents",
"to gather intelligence, while REY, the last",
"hope of the Jedi, trains for battle against",
"the diabolical FIRST ORDER."
};
// ---------------------------------------------------------
// Função para geração de números pseudoaleatórios
// Utiliza operações bit a bit para "embaralhar" os valores
// ---------------------------------------------------------
uint8_t rng() {
zx++;
za = (za ^ zc ^ zx);
zb = (zb + za);
zc = ((zc + (zb >> 1)) ^ za);
return zc;
}
// ---------------------------------------------------------
// Tarefa para reprodução contínua da música NES em loop
// Essa tarefa é executada no CORE-0
// ---------------------------------------------------------
void TaskPlayMusic(void *pvParameters) {
// Inicializa o watchdog timer com timeout de 63 segundos (desabilitado)
// esp_task_wdt_init(63, false);
for(;;) {
// Reproduz a música definida em 'starwars', em loop, com volume 0.5
player.play_nes(starwars, true, 0.5);
// A função de callback 'frame_counter_cb' pode ser utilizada para efeitos de luz (comentada)
// player.frame_counter_cb(danceLight);
}
}
// ---------------------------------------------------------
// Função que cria o efeito "Star Field" (campo de estrelas)
// Exibe animação com estrelas se movendo, nave (X-Wing) e textos rolantes
// ---------------------------------------------------------
void StarField() {
// Cria os sprites para a imagem (nave) e para o texto
img.createSprite(124, 59);
txt.createSprite(320, 120);
// Configuração de troca de bytes (importante para o correto funcionamento em algumas plataformas)
#ifdef roll
// Se a macro 'roll' estiver definida, outras configurações podem ser aplicadas
#else
img.setSwapBytes(true);
txt.setSwapBytes(true);
#endif
// Exibe a sprite da nave (X-Wing) na posição definida
img.pushImage(2, 2, 120, 55, xwing);
// Variáveis para controle da posição da nave e do texto rolante
uint8_t curx = 98;
uint8_t cury = 145;
int scrolly = 80;
// ------------------------------
// Configuração do campo de estrelas
// ------------------------------
#define NSTARS 512 // Número de estrelas
uint8_t sx[NSTARS] = {0}; // Coordenada X de cada estrela
uint8_t sy[NSTARS] = {0}; // Coordenada Y de cada estrela
uint8_t sz[NSTARS] = {0}; // "Profundidade" de cada estrela
// Inicializa os valores para a geração pseudoaleatória
za = random(256);
zb = random(256);
zc = random(256);
zx = random(256);
// Limpa a tela com fundo preto
tft.fillScreen(TFT_BLACK);
// Loop principal do efeito star field (enquanto o botão não for pressionado para sair)
while (press) {
// Variável para variar a profundidade das estrelas ao "spawn"
uint8_t spawnDepthVariation = 127;
// Atualiza cada estrela
for (int i = 0; i < NSTARS; ++i) {
if (sz[i] <= 1) {
// Se a estrela "morreu", reinicializa suas coordenadas e profundidade
sx[i] = 160 - 120 + rng();
sy[i] = rng();
sz[i] = spawnDepthVariation--;
} else {
// Calcula as posições antigas com efeito de perspectiva
int old_screen_x = ((int)sx[i] - 160) * 256 / sz[i] + 160;
int old_screen_y = ((int)sy[i] - 120) * 256 / sz[i] + 120;
// Apaga o pixel antigo desenhando-o em preto
tft.drawPixel(old_screen_x, old_screen_y, TFT_BLACK);
// Atualiza a profundidade (simulando movimento em direção ao espectador)
sz[i] -= 2;
if (sz[i] > 1) {
// Calcula as novas posições com base na profundidade atualizada
int screen_x = ((int)sx[i] - 160) * 256 / sz[i] + 160;
int screen_y = ((int)sy[i] - 120) * 256 / sz[i] + 120;
// Se a posição estiver dentro da tela, desenha o pixel com brilho variável
if (screen_x >= 0 && screen_y >= 0 && screen_x < 320 && screen_y < 240) {
uint8_t r, g, b;
r = g = b = 255 - sz[i];
tft.drawPixel(screen_x, screen_y, tft.color565(r, g, b));
} else {
// Se a estrela sair da tela, ela "morre"
sz[i] = 0;
}
}
}
}
// ------------------------------
// Atualização da posição da nave (X-Wing)
// ------------------------------
int8_t rolls = random(-1, 2); // Gera um pequeno deslocamento rotacional
curx = curx + rolls;
// Limita a movimentação da nave para que ela não saia da tela
if (curx < 20) curx = 20;
if (curx > 178) curx = 178;
cury = cury + random(-1, 2);
if (cury < 130) cury = 130;
if (cury > 165) cury = 165;
#ifdef roll
// Se a macro 'roll' estiver definida, ajusta a rotação da nave
tft.setPivot(curx + 62, cury + 29); // Define o ponto de rotação
img.pushRotated(rolls); // Exibe a sprite com rotação
#else
// Exibe a sprite da nave sem rotação
img.pushSprite(curx, cury);
#endif
// ------------------------------
// Desenho de elementos gráficos adicionais
// ------------------------------
// Linha horizontal verde na parte inferior da tela
tft.drawFastHLine(0, 219, 320, TFT_GREEN);
// Exibe texto "RTOS TEST" com cor aleatória
tft.setTextColor(random(0xffff));
tft.drawString("RTOS TEST", 0, 221, 4);
// Exibe um aviso de pausa no canto inferior direito com fundo vermelho
tft.setTextColor(TFT_WHITE, TFT_RED);
tft.drawRightString("[- Press button to pause -]", 319, 225, 2);
// ------------------------------
// Atualiza o texto rolante utilizando o sprite 'txt'
// ------------------------------
// Apaga o texto antigo
txt.setTextColor(TFT_BLACK);
for (int i = 0; i < maxLine; i++) {
if (scrolly + i * 25 > 0) {
txt.drawCentreString(scrollText[i], 159, scrolly + i * 25, 2);
}
}
// Move o texto para cima
scrolly--;
if (scrolly < -200) scrolly = 120; // Reinicia o ciclo do texto
// Desenha o texto em cor laranja para destaque
txt.setTextColor(TFT_ORANGE);
for (int i = 0; i < maxLine; i++) {
if (scrolly + i * 25 > 0) {
txt.drawCentreString(scrollText[i], 159, scrolly + i * 25, 2);
}
}
// Atualiza o sprite de texto na tela
txt.pushSprite(0, 0);
// Se o botão for pressionado, finaliza o loop do efeito star field
if (digitalRead(SELECTOR_PIN) == LOW) {
press = false;
}
}
// Após sair do loop, libera a memória dos sprites
img.deleteSprite();
txt.deleteSprite();
}
// ---------------------------------------------------------
// Função setup: executada uma vez ao iniciar o dispositivo
// Configura pinos, PWM, tela e exibe mensagem inicial
// ---------------------------------------------------------
void setup() {
// Configuração dos pinos
pinMode(BUZZER_PIN, OUTPUT); // Configura o pino do buzzer como saída
pinMode(SELECTOR_PIN, INPUT_PULLUP); // Configura o pino do botão com resistor de pull-up
// Configuração do PWM para o buzzer e backlight
ledcSetup(buzzerChannel, 1500, 10); // Buzzer: frequência de 1500 Hz, resolução 10 bits
ledcSetup(backlightChannel, 12000, 8); // Backlight: frequência de 12000 Hz, resolução 8 bits
ledcAttachPin(BUZZER_PIN, buzzerChannel); // Associa o pino do buzzer ao canal PWM
// Inicialização da comunicação serial
Serial.begin(115200);
// Inicializa e configura a tela TFT
tft.init();
tft.setRotation(1);
tft.fillScreen(TFT_BLACK);
tft.setSwapBytes(true);
// Mensagem inicial centralizada
tft.drawCentreString("Push button to start", 159, 120, 4);
// Desativa o tom do buzzer inicialmente
ledcWriteTone(buzzerChannel, 0);
// Exibe o tamanho do array "starwars" (definido em music.h) via Serial
Serial.println(array_length(starwars));
}
// ---------------------------------------------------------
// Função loop: executada continuamente após o setup()
// Gerencia a leitura do botão e inicia as animações e tarefas
// ---------------------------------------------------------
void loop() {
// Se o botão for pressionado
if (digitalRead(SELECTOR_PIN) == LOW) {
press = true;
if (press) {
// Pequeno atraso para debouncing
delay(500);
// Exibe mensagem de abertura na tela
tft.fillScreen(TFT_BLACK);
tft.setTextColor(0x043f);
tft.drawString("A long time ago in a galaxy far,", 0, 100, 4);
tft.drawString("far away....", 0, 140, 4);
delay(3000);
Serial.println("PLAY");
// Cria a tarefa para reprodução de música no CORE-0
xTaskCreatePinnedToCore(
&TaskPlayMusic, // Função da tarefa
"play music in core 0", // Nome da tarefa (para depuração)
4096, // Tamanho da stack da tarefa
NULL, // Parâmetro da tarefa
2, // Prioridade da tarefa
&TaskHandle0, // Handle da tarefa
0 // Número do core (CORE-0)
);
// Exibe imagem relacionada ao tema Star Wars
tft.fillScreen(TFT_BLACK);
tft.pushImage(39, 69, STAR_WARS_WIDTH, STAR_WARS_HEIGHT, STAR_WARS);
delay(5000);
// Inicia o efeito star field (animação contínua até o botão ser pressionado)
StarField();
// Após a pausa, exibe mensagem de "PAUSE"
tft.setTextColor(TFT_BLACK, TFT_VIOLET);
tft.drawCentreString("PAUSE", 159, 110, 4);
Serial.println("PAUSE");
// Encerra a tarefa de reprodução de música
vTaskDelete(TaskHandle0);
// Reconfigura o pino do buzzer (garante que o pino continue funcional)
pinMode(BUZZER_PIN, OUTPUT);
ledcAttachPin(BUZZER_PIN, buzzerChannel);
}
// Aguarda para evitar múltiplas ativações seguidas
delay(500);
}
// Permite a execução de outras tarefas do FreeRTOS
yield();
}