display and gem lgic seperate cpu cores, audio fixed, display scaling implemented, psram aktivatet for threads in core
This commit is contained in:
+19
-2
@@ -3,15 +3,32 @@ idf_component_register(
|
||||
"main.c"
|
||||
INCLUDE_DIRS
|
||||
"."
|
||||
"include"
|
||||
REQUIRES
|
||||
driver
|
||||
esp_psram
|
||||
nvs_flash
|
||||
fatfs
|
||||
spi_flash
|
||||
gnuboy
|
||||
st7789
|
||||
nfc_manager
|
||||
link_cable
|
||||
potentiometer_manager
|
||||
esp_hw_support
|
||||
vfs
|
||||
peanut-gb
|
||||
)
|
||||
# GNUBoy specific compiler flags
|
||||
target_compile_options(${COMPONENT_LIB} PRIVATE
|
||||
-Wno-unused-variable
|
||||
-Wno-unused-function
|
||||
-Wno-pointer-sign
|
||||
-Wno-implicit-function-declaration
|
||||
-Wno-int-conversion
|
||||
-Wno-incompatible-pointer-types
|
||||
-Wno-format
|
||||
)
|
||||
|
||||
# GNUBoy defines
|
||||
target_compile_definitions(${COMPONENT_LIB} PRIVATE
|
||||
USE_ESP32=1
|
||||
)
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
/**
|
||||
* @file hardware_config.h
|
||||
* @brief Hardware pin configuration for Waveshare ESP32-S3-Touch-LCD-2
|
||||
*
|
||||
* This file contains ALL pin definitions for the LEGO GameBoy project.
|
||||
* Centralized configuration - change pins here only!
|
||||
*
|
||||
* Hardware: Waveshare ESP32-S3-Touch-LCD-2 (2.0" ST7789 240x320)
|
||||
* - ESP32-S3-WROOM-1 module
|
||||
* - 16MB Flash, 8MB PSRAM
|
||||
* - 2.0" ST7789 TFT Display (240x320)
|
||||
* - Integrated touch controller
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// ============================================
|
||||
// DISPLAY PINS (ST7789 - 2.0" 240x320)
|
||||
// SPI Interface
|
||||
// ============================================
|
||||
#define LCD_SPI_HOST SPI2_HOST
|
||||
#define LCD_PIXEL_CLOCK_HZ (40 * 1000 * 1000) // 40MHz
|
||||
|
||||
#define LCD_PIN_MOSI 11 // SPI MOSI (DIN)
|
||||
#define LCD_PIN_MISO 13 // SPI MISO (nicht verwendet bei Display)
|
||||
#define LCD_PIN_SCLK 12 // SPI Clock
|
||||
#define LCD_PIN_CS 10 // Chip Select
|
||||
#define LCD_PIN_DC 8 // Data/Command
|
||||
#define LCD_PIN_RST 14 // Reset
|
||||
#define LCD_PIN_BCKL 9 // Backlight (PWM capable)
|
||||
|
||||
// Display specs
|
||||
#define LCD_WIDTH 240
|
||||
#define LCD_HEIGHT 320
|
||||
#define LCD_ROTATION 0 // 0, 1, 2, or 3 (0 = Portrait)
|
||||
|
||||
// Backlight PWM
|
||||
#define LCD_BCKL_DEFAULT 80 // 0-100%
|
||||
#define LCD_BCKL_MIN 10
|
||||
#define LCD_BCKL_MAX 100
|
||||
|
||||
// ============================================
|
||||
// TOUCH CONTROLLER PINS (CST816S)
|
||||
// I2C Interface
|
||||
// ============================================
|
||||
#define TOUCH_I2C_NUM I2C_NUM_0
|
||||
#define TOUCH_I2C_SCL 6
|
||||
#define TOUCH_I2C_SDA 5
|
||||
#define TOUCH_I2C_INT 7 // Touch interrupt (active low)
|
||||
#define TOUCH_I2C_RST -1 // Optional reset (not used)
|
||||
#define TOUCH_I2C_ADDR 0x15 // CST816S address
|
||||
|
||||
#define TOUCH_I2C_FREQ_HZ 400000 // 400kHz
|
||||
|
||||
// ============================================
|
||||
// SD CARD PINS (SPI - shares with Display)
|
||||
// ============================================
|
||||
#define SD_SPI_HOST LCD_SPI_HOST // Same SPI bus as display!
|
||||
#define SD_PIN_MOSI LCD_PIN_MOSI
|
||||
#define SD_PIN_MISO LCD_PIN_MISO
|
||||
#define SD_PIN_SCLK LCD_PIN_SCLK
|
||||
#define SD_PIN_CS 4 // Separate CS from display!
|
||||
|
||||
// ============================================
|
||||
// AUDIO PINS (I2S - MAX98357A)
|
||||
// ============================================
|
||||
#define I2S_NUM I2S_NUM_0
|
||||
#define I2S_SAMPLE_RATE 32000 // Hz (GameBoy: 32kHz)
|
||||
#define I2S_BITS_PER_SAMPLE 16
|
||||
|
||||
#define I2S_PIN_BCLK 35 // Bit Clock (moved from 41 - conflict fixed!)
|
||||
#define I2S_PIN_LRC 36 // Left/Right Clock (WS) (moved from 42 - conflict fixed!)
|
||||
#define I2S_PIN_DIN 37 // Data In (to MAX98357A) (moved from 40)
|
||||
// No MCLK needed for MAX98357A!
|
||||
|
||||
#define I2S_DMA_BUF_COUNT 8
|
||||
#define I2S_DMA_BUF_LEN 1024
|
||||
|
||||
// ============================================
|
||||
// NFC READER PINS (PN532)
|
||||
// I2C Interface (separate bus from touch!)
|
||||
// ============================================
|
||||
#define NFC_I2C_NUM I2C_NUM_1
|
||||
#define NFC_I2C_SCL 16 // I2C Clock
|
||||
#define NFC_I2C_SDA 15 // I2C Data
|
||||
#define NFC_I2C_INT -1 // Optional interrupt
|
||||
#define NFC_I2C_RST -1 // Optional reset
|
||||
|
||||
#define NFC_I2C_ADDR 0x24 // PN532 I2C address
|
||||
#define NFC_I2C_FREQ_HZ 100000 // 100kHz (PN532 spec)
|
||||
|
||||
// ============================================
|
||||
// GAMEBOY BUTTON PINS
|
||||
// All inputs with pull-up
|
||||
// ============================================
|
||||
#define BTN_UP 1
|
||||
#define BTN_DOWN 2
|
||||
#define BTN_LEFT 42
|
||||
#define BTN_RIGHT 41
|
||||
|
||||
#define BTN_A 21
|
||||
#define BTN_B 47
|
||||
#define BTN_START 48
|
||||
#define BTN_SELECT 45
|
||||
|
||||
// Button configuration
|
||||
#define BTN_ACTIVE_LEVEL 0 // Buttons pull to GND (active LOW)
|
||||
#define BTN_DEBOUNCE_MS 50 // Debounce time
|
||||
|
||||
// ============================================
|
||||
// POTENTIOMETER PINS (ADC)
|
||||
// ============================================
|
||||
#define POTI_VOLUME_PIN 3 // ADC1_CH2 - Volume control
|
||||
#define POTI_BRIGHT_PIN 46 // ADC2_CH5 - Brightness control
|
||||
|
||||
#define POTI_ADC_WIDTH ADC_WIDTH_BIT_12 // 12-bit (0-4095)
|
||||
#define POTI_ADC_ATTEN ADC_ATTEN_DB_11 // 0-3.3V range
|
||||
|
||||
// ============================================
|
||||
// LINK CABLE PINS (GPIO for 2-Player)
|
||||
// ============================================
|
||||
#define LINK_GPIO_SCLK 17 // Serial Clock (bidirectional)
|
||||
#define LINK_GPIO_SOUT 18 // Serial Out (data to other GB)
|
||||
#define LINK_GPIO_SIN 38 // Serial In (data from other GB)
|
||||
|
||||
// Link Cable Settings
|
||||
#define LINK_CLOCK_FREQ 8192 // Hz (GameBoy standard)
|
||||
#define LINK_BYTE_TIME_US 122 // Microseconds per byte
|
||||
#define LINK_BIT_TIME_US 15 // Microseconds per bit
|
||||
|
||||
// ============================================
|
||||
// STATUS LED (optional indicator)
|
||||
// ============================================
|
||||
#define LED_STATUS_PIN 39 // Status LED (Link active, etc.)
|
||||
#define LED_ACTIVE_LEVEL 1 // Active HIGH
|
||||
|
||||
// ============================================
|
||||
// POWER MANAGEMENT (optional)
|
||||
// ============================================
|
||||
#define BATTERY_ADC_PIN -1 // Not used yet
|
||||
#define POWER_ENABLE_PIN -1 // Not used yet
|
||||
|
||||
// ============================================
|
||||
// DEBUG/UART (Serial Console)
|
||||
// ============================================
|
||||
#define UART_NUM UART_NUM_0
|
||||
#define UART_TX_PIN 43
|
||||
#define UART_RX_PIN 44
|
||||
#define UART_BAUD_RATE 115200
|
||||
|
||||
// ============================================
|
||||
// GPIO SUMMARY (for reference/debugging)
|
||||
// ============================================
|
||||
/*
|
||||
WAVESHARE ESP32-S3-TOUCH-LCD-2 PIN ALLOCATION:
|
||||
|
||||
GPIO | Function | Direction | Notes
|
||||
------|-------------------|-----------|------------------
|
||||
1 | BTN_UP | Input | Pull-up
|
||||
2 | BTN_DOWN | Input | Pull-up
|
||||
3 | POTI_VOLUME | Input | ADC1_CH2
|
||||
4 | SD_CS | Output | SPI
|
||||
5 | TOUCH_SDA | I/O | I2C0
|
||||
6 | TOUCH_SCL | Output | I2C0
|
||||
7 | TOUCH_INT | Input | Interrupt
|
||||
8 | LCD_DC | Output | SPI
|
||||
9 | LCD_BCKL | Output | PWM
|
||||
10 | LCD_CS | Output | SPI
|
||||
11 | LCD_MOSI | Output | SPI
|
||||
12 | LCD_SCLK | Output | SPI
|
||||
13 | LCD_MISO | Input | SPI (not used)
|
||||
14 | LCD_RST | Output | Reset
|
||||
15 | NFC_SDA | I/O | I2C1
|
||||
16 | NFC_SCL | Output | I2C1
|
||||
17 | LINK_SCLK | I/O | Link Cable
|
||||
18 | LINK_SOUT | Output | Link Cable
|
||||
21 | BTN_A | Input | Pull-up
|
||||
35 | I2S_BCLK | Output | Audio Bit Clock
|
||||
36 | I2S_LRC | Output | Audio Word Select
|
||||
37 | I2S_DIN | Output | Audio Data
|
||||
38 | LINK_SIN | Input | Link Cable
|
||||
39 | LED_STATUS | Output | Status indicator
|
||||
41 | BTN_RIGHT | Input | Pull-up (conflict fixed!)
|
||||
42 | BTN_LEFT | Input | Pull-up (conflict fixed!)
|
||||
43 | UART_TX | Output | Debug
|
||||
44 | UART_RX | Input | Debug
|
||||
45 | BTN_SELECT | Input | Pull-up
|
||||
46 | POTI_BRIGHT | Input | ADC2_CH5
|
||||
47 | BTN_B | Input | Pull-up
|
||||
48 | BTN_START | Input | Pull-up
|
||||
|
||||
NOTES:
|
||||
- PIN CONFLICTS FIXED! I2S moved to GPIO 35/36/37
|
||||
- Buttons GPIO 41/42 are now free from conflicts!
|
||||
- SPI bus is shared: Display + SD Card
|
||||
→ Use separate CS pins!
|
||||
- I2C0: Touch controller
|
||||
- I2C1: NFC reader (separate bus!)
|
||||
- All buttons have internal pull-ups enabled
|
||||
*/
|
||||
|
||||
// ============================================
|
||||
// HARDWARE VALIDATION MACROS
|
||||
// ============================================
|
||||
#define VALIDATE_PIN(pin) ((pin) >= 0 && (pin) <= 48)
|
||||
|
||||
// Check for pin conflicts at compile time
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* @file hardware_config.h
|
||||
* @brief Hardware pin configuration for Waveshare ESP32-S3-Touch-LCD-2
|
||||
*
|
||||
* CORRECTED with proper pin assignments!
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// ============================================
|
||||
// DISPLAY PINS (ST7789) - VERIFIED WORKING
|
||||
// ============================================
|
||||
#define LCD_SPI_HOST SPI2_HOST
|
||||
#define LCD_PIXEL_CLOCK_HZ (80 * 1000 * 1000) // ST7789 supports up to 80MHz
|
||||
|
||||
#define LCD_PIN_MOSI 38
|
||||
#define LCD_PIN_MISO 40
|
||||
#define LCD_PIN_SCLK 39
|
||||
#define LCD_PIN_CS 45
|
||||
#define LCD_PIN_DC 42
|
||||
#define LCD_PIN_RST -1
|
||||
#define LCD_PIN_BCKL 1
|
||||
|
||||
#define LCD_WIDTH 320
|
||||
#define LCD_HEIGHT 240
|
||||
#define LCD_ROTATION 3
|
||||
|
||||
#define LCD_BCKL_DEFAULT 80
|
||||
#define LCD_BCKL_MIN 10
|
||||
#define LCD_BCKL_MAX 100
|
||||
|
||||
// ============================================
|
||||
// GAMEBOY DISPLAY SCALING OPTIONS
|
||||
// ============================================
|
||||
// Set to 1 for scaled display with black borders
|
||||
// Set to 0 for full screen stretch (320x240)
|
||||
#define GB_PIXEL_PERFECT_SCALING 1
|
||||
|
||||
// Scaling factor (float multiplier for GameBoy resolution)
|
||||
// Valid range: 1.0 to 2.0
|
||||
// Examples:
|
||||
// 1.4 = 160x144 -> 224x202 (smaller, faster performance)
|
||||
// 1.5 = 160x144 -> 240x216 (balanced)
|
||||
// 1.6 = 160x144 -> 256x230 (current, excellent balance)
|
||||
// 1.67 = 160x144 -> 267x240 (full height, max size)
|
||||
// 2.0 = 160x144 -> 320x288 (exceeds screen height, will be clipped)
|
||||
//
|
||||
// Performance tip: Smaller scaling = fewer pixels = higher FPS
|
||||
// Can be changed on-the-fly per game (future menu feature)
|
||||
#define GB_SCALE_FACTOR 1.5
|
||||
|
||||
#if GB_PIXEL_PERFECT_SCALING
|
||||
// Calculate scaled dimensions dynamically from GB_SCALE_FACTOR
|
||||
// GameBoy native resolution: 160x144
|
||||
#define GB_RENDER_WIDTH ((int)(160 * GB_SCALE_FACTOR))
|
||||
#define GB_RENDER_HEIGHT ((int)(144 * GB_SCALE_FACTOR))
|
||||
|
||||
// Center the GameBoy screen with black borders
|
||||
#define GB_OFFSET_X ((320 - GB_RENDER_WIDTH) / 2)
|
||||
#define GB_OFFSET_Y ((240 - GB_RENDER_HEIGHT) / 2)
|
||||
|
||||
// Frame buffer is full screen to hold black borders
|
||||
#define GB_SCREEN_WIDTH 320
|
||||
#define GB_SCREEN_HEIGHT 240
|
||||
#else
|
||||
// Full screen: 160x144 -> 320x240 (stretch mode)
|
||||
#define GB_RENDER_WIDTH 320
|
||||
#define GB_RENDER_HEIGHT 240
|
||||
#define GB_OFFSET_X 0
|
||||
#define GB_OFFSET_Y 0
|
||||
|
||||
#define GB_SCREEN_WIDTH 320
|
||||
#define GB_SCREEN_HEIGHT 240
|
||||
#endif
|
||||
|
||||
// ============================================
|
||||
// SD CARD PINS - VERIFIED WORKING
|
||||
// ============================================
|
||||
#define SD_SPI_HOST LCD_SPI_HOST
|
||||
#define SD_PIN_MOSI LCD_PIN_MOSI
|
||||
#define SD_PIN_MISO LCD_PIN_MISO
|
||||
#define SD_PIN_SCLK LCD_PIN_SCLK
|
||||
#define SD_PIN_CS 41
|
||||
|
||||
// ============================================
|
||||
// TOUCH CONTROLLER PINS (CST816S)
|
||||
// ============================================
|
||||
#define TOUCH_I2C_NUM I2C_NUM_0
|
||||
#define TOUCH_I2C_SCL 6
|
||||
#define TOUCH_I2C_SDA 5
|
||||
#define TOUCH_I2C_INT 7
|
||||
#define TOUCH_I2C_RST -1
|
||||
#define TOUCH_I2C_ADDR 0x15
|
||||
#define TOUCH_I2C_FREQ_HZ 400000
|
||||
|
||||
// ============================================
|
||||
// AUDIO PINS (I2S - MAX98357A)
|
||||
// ============================================
|
||||
#define I2S_NUM I2S_NUM_0
|
||||
#define I2S_SAMPLE_RATE 32768 // GameBoy native sample rate
|
||||
#define I2S_BITS_PER_SAMPLE 16
|
||||
#define I2S_PIN_BCLK 48 // Available on right header pin 6
|
||||
#define I2S_PIN_LRC 47 // Available on right header pin 5
|
||||
#define I2S_PIN_DIN 16 // Your original wiring (conflicts resolved by NFC move)
|
||||
#define I2S_DMA_BUF_COUNT 8
|
||||
#define I2S_DMA_BUF_LEN 1024
|
||||
|
||||
// ============================================
|
||||
// GAMEBOY BUTTON PINS - REMAPPED TO FREE GPIOS!
|
||||
// ============================================
|
||||
#define BTN_UP 8 // D-Pad Up
|
||||
#define BTN_DOWN 9 // D-Pad Down
|
||||
#define BTN_LEFT 10 // D-Pad Left
|
||||
#define BTN_RIGHT 11 // D-Pad Right
|
||||
|
||||
#define BTN_A 12 // Action Button A
|
||||
#define BTN_B 13 // Action Button B
|
||||
#define BTN_START 14 // Start Button
|
||||
#define BTN_SELECT 21 // Select Button
|
||||
|
||||
#define BTN_ACTIVE_LEVEL 0 // Active LOW (buttons pull to GND)
|
||||
#define BTN_DEBOUNCE_MS 50 // Debounce time
|
||||
|
||||
// ============================================
|
||||
// POWER SWITCH (Hardware Toggle Switch)
|
||||
// ============================================
|
||||
#define POWER_SWITCH_PIN 0 // GPIO0 - Hardware power switch
|
||||
#define POWER_SWITCH_ON 0 // Switch closed = GND = Device ON
|
||||
#define POWER_SWITCH_OFF 1 // Switch open = Pull-up = Device goes to sleep
|
||||
|
||||
// Power switch behavior:
|
||||
// - When switch is CLOSED (pin reads LOW): Device runs normally
|
||||
// - When switch is OPENED (pin reads HIGH): Device enters deep-sleep
|
||||
// - When switch is CLOSED again: ESP32 wakes up from deep-sleep
|
||||
//
|
||||
// Implementation:
|
||||
// - Configure GPIO0 with internal pull-up
|
||||
// - Monitor pin state in main loop
|
||||
// - On transition LOW->HIGH: Enter esp_deep_sleep_start()
|
||||
// - ESP32 will wake on GPIO LOW (RTC_GPIO wakeup)
|
||||
|
||||
#define POWER_SWITCH_CHECK_MS 100 // Check switch state every 100ms
|
||||
|
||||
// ============================================
|
||||
// POTENTIOMETER PINS (ADC)
|
||||
// ============================================
|
||||
#define POTI_VOLUME_PIN 3 // ADC1_CH2 - Volume control
|
||||
#define POTI_BRIGHT_PIN 4 // ADC1_CH3 - Brightness control
|
||||
#define POTI_ADC_WIDTH ADC_WIDTH_BIT_12
|
||||
#define POTI_ADC_ATTEN ADC_ATTEN_DB_11
|
||||
|
||||
// ============================================
|
||||
// NFC READER PINS (PN532) - Shared I2C Bus with Touch
|
||||
// ============================================
|
||||
#define NFC_I2C_NUM I2C_NUM_0 // SHARED with Touch Controller!
|
||||
#define NFC_I2C_SCL TOUCH_I2C_SCL // GPIO 6 (shared)
|
||||
#define NFC_I2C_SDA TOUCH_I2C_SDA // GPIO 5 (shared)
|
||||
#define NFC_I2C_INT -1 // Optional interrupt
|
||||
#define NFC_I2C_RST -1 // Optional reset
|
||||
#define NFC_I2C_ADDR 0x24 // Different I2C address than Touch (0x15)
|
||||
#define NFC_I2C_FREQ_HZ 100000
|
||||
|
||||
// ============================================
|
||||
// LINK CABLE PINS (GPIO for 2-Player) - Optional
|
||||
// ============================================
|
||||
#define LINK_GPIO_SCLK 15 // Free GPIO (NFC moved to I2C)
|
||||
#define LINK_GPIO_SOUT 2 // Free GPIO on left header
|
||||
#define LINK_GPIO_SIN 17 // Free GPIO
|
||||
#define LINK_CLOCK_FREQ 8192
|
||||
#define LINK_BYTE_TIME_US 122
|
||||
#define LINK_BIT_TIME_US 15
|
||||
|
||||
// ============================================
|
||||
// STATUS LED - Optional
|
||||
// ============================================
|
||||
#define LED_STATUS_PIN 18 // Changed from 19 (USB conflict!)
|
||||
#define LED_ACTIVE_LEVEL 1
|
||||
|
||||
// ============================================
|
||||
// DEBUG/UART
|
||||
// ============================================
|
||||
#define UART_NUM UART_NUM_0
|
||||
#define UART_TX_PIN 43
|
||||
#define UART_RX_PIN 44
|
||||
#define UART_BAUD_RATE 115200
|
||||
|
||||
// ============================================
|
||||
// PIN SUMMARY (for reference)
|
||||
// ============================================
|
||||
/*
|
||||
USED PINS - OPTIMIZED LAYOUT:
|
||||
- GPIO 0: POWER_SWITCH (Hardware toggle switch)
|
||||
- GPIO 1: LCD Backlight PWM
|
||||
- GPIO 3: Volume Potentiometer (ADC1_CH2)
|
||||
- GPIO 4: Brightness Potentiometer (ADC1_CH3)
|
||||
- GPIO 5: I2C SDA (Touch 0x15 + NFC 0x24 SHARED BUS)
|
||||
- GPIO 6: I2C SCL (Touch + NFC SHARED BUS)
|
||||
- GPIO 7: Touch Interrupt
|
||||
- GPIO 8: BTN_UP (native GPIO)
|
||||
- GPIO 9: BTN_DOWN (native GPIO)
|
||||
- GPIO 10: BTN_LEFT (native GPIO)
|
||||
- GPIO 11: BTN_RIGHT (native GPIO)
|
||||
- GPIO 12: BTN_A (native GPIO)
|
||||
- GPIO 13: BTN_B (native GPIO)
|
||||
- GPIO 14: BTN_START (native GPIO)
|
||||
- GPIO 2: Link Cable SOUT
|
||||
- GPIO 15: Link Cable SCLK (freed from NFC!)
|
||||
- GPIO 16: I2S DIN (now conflict-free, NFC moved to shared I2C!)
|
||||
- GPIO 17: Link Cable SIN
|
||||
- GPIO 18: Status LED (was 19, USB conflict fixed!)
|
||||
- GPIO 19: RESERVED (USB D-)
|
||||
- GPIO 20: RESERVED (USB D+)
|
||||
- GPIO 21: BTN_SELECT (native GPIO)
|
||||
- GPIO 26-37: RESERVED (Flash/PSRAM - DO NOT USE!)
|
||||
- GPIO 38: SPI MOSI (LCD + SD shared)
|
||||
- GPIO 39: SPI SCLK (LCD + SD shared)
|
||||
- GPIO 40: SPI MISO (LCD + SD shared)
|
||||
- GPIO 41: SD Card CS
|
||||
- GPIO 42: LCD DC
|
||||
- GPIO 43: UART TX
|
||||
- GPIO 44: UART RX
|
||||
- GPIO 45: LCD CS
|
||||
- GPIO 47: I2S LRC (right header pin 5)
|
||||
- GPIO 48: I2S BCLK (right header pin 6)
|
||||
- GPIO 16: I2S DIN (now conflict-free, NFC moved to shared I2C!)
|
||||
|
||||
POWER SWITCH WIRING:
|
||||
┌─────────┐
|
||||
│ ESP32 │
|
||||
│ GPIO 0 ├──────┬──────── Switch ──────┐
|
||||
│ │ │ │
|
||||
│ (Pull-up) [Switch] GND
|
||||
│ │ │
|
||||
└─────────┘ │
|
||||
└─ When closed: GPIO0 = LOW (ON)
|
||||
When open: GPIO0 = HIGH (SLEEP)
|
||||
*/
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* This example code is in the Public Domain (or CC0 licensed, at your option.)
|
||||
* Unless required by applicable law or agreed to in writing, this
|
||||
* software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
* CONDITIONS OF ANY KIND, either express or implied.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
// For pin mappings.
|
||||
#include "hardware.h"
|
||||
// For graphics.
|
||||
#include "pax_gfx.h"
|
||||
// For PNG images.
|
||||
#include "pax_codecs.h"
|
||||
// The screen driver.
|
||||
#include "ili9341.h"
|
||||
// For all system settings and alike.
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "esp_system.h"
|
||||
#include "nvs.h"
|
||||
#include "nvs_flash.h"
|
||||
// For WiFi connectivity.
|
||||
#include "wifi_connect.h"
|
||||
#include "wifi_connection.h"
|
||||
// For exiting to the launcher.
|
||||
#include "soc/rtc.h"
|
||||
#include "soc/rtc_cntl_reg.h"
|
||||
|
||||
// Updates the screen with the last drawing.
|
||||
void disp_flush();
|
||||
|
||||
// Exits the app, returning to the launcher.
|
||||
void exit_to_launcher();
|
||||
|
||||
void LoadState();
|
||||
void SaveState();
|
||||
void LoadSram();
|
||||
void SaveSram();
|
||||
+749
-147
@@ -1,185 +1,787 @@
|
||||
/**
|
||||
* @file main.c
|
||||
* @brief ESP32-S3 GNUBoy Emulator - Main Entry Point
|
||||
*
|
||||
* LEGO GameBoy Emulator Project
|
||||
* Hardware: Waveshare ESP32-S3-Touch-LCD-2
|
||||
*
|
||||
* Features:
|
||||
* - GameBoy/GameBoy Color emulation
|
||||
* - NFC ROM selection
|
||||
* - Potentiometer volume/brightness control
|
||||
* - Link Cable 2-player support
|
||||
* - SD Card ROM loading
|
||||
* @brief ESP32-S3 GameBoy - FIXED Audio Frequency Calculation!
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_psram.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_vfs_fat.h"
|
||||
#include "sdmmc_cmd.h"
|
||||
#include "driver/sdmmc_host.h"
|
||||
#include "driver/sdspi_host.h"
|
||||
#include "driver/i2s.h"
|
||||
|
||||
#include "hardware_config.h"
|
||||
#include "st7789.h"
|
||||
|
||||
// Component includes (will be created)
|
||||
// #include "st7789.h"
|
||||
// #include "nfc_manager.h"
|
||||
// #include "link_cable.h"
|
||||
// #include "potentiometer_manager.h"
|
||||
// #include "gnuboy.h"
|
||||
#define DISPLAY_WIDTH LCD_WIDTH
|
||||
#define DISPLAY_HEIGHT LCD_HEIGHT
|
||||
#undef LCD_WIDTH
|
||||
#undef LCD_HEIGHT
|
||||
|
||||
static const char *TAG = "MAIN";
|
||||
// ============================================
|
||||
// APU Constants
|
||||
// ============================================
|
||||
#define SAMPLE_RATE 32768
|
||||
#define SAMPLES_PER_FRAME 546 // 32768 Hz / 60 FPS = 546 samples/frame
|
||||
#define SAMPLES_PER_BUFFER 512
|
||||
|
||||
/**
|
||||
* @brief Callback for potentiometer changes
|
||||
*/
|
||||
static void poti_change_handler(uint8_t volume, uint8_t brightness)
|
||||
// GameBoy CPU frequency
|
||||
#define GB_CPU_FREQ 4194304.0f
|
||||
|
||||
// Samples per GB CPU cycle
|
||||
#define CYCLES_PER_SAMPLE (GB_CPU_FREQ / SAMPLE_RATE) // ~128
|
||||
|
||||
static const char *TAG = "GB";
|
||||
|
||||
// ============================================
|
||||
// APU Registers (directly mapped)
|
||||
// ============================================
|
||||
static uint8_t apu_regs[48] = {0};
|
||||
static uint8_t wave_ram[16] = {0};
|
||||
|
||||
// Master control
|
||||
static bool master_enable = false;
|
||||
static uint8_t master_vol_left = 7;
|
||||
static uint8_t master_vol_right = 7;
|
||||
static uint8_t panning = 0xFF;
|
||||
|
||||
// Channel 1 state
|
||||
static struct {
|
||||
bool active;
|
||||
bool dac_on; // DAC enable bit (NR12 bit 3-7 != 0)
|
||||
uint8_t duty;
|
||||
uint8_t volume;
|
||||
uint16_t freq_raw;
|
||||
float phase;
|
||||
} ch1 = {0};
|
||||
|
||||
// Channel 2 state
|
||||
static struct {
|
||||
bool active;
|
||||
bool dac_on; // DAC enable bit (NR22 bit 3-7 != 0)
|
||||
uint8_t duty;
|
||||
uint8_t volume;
|
||||
uint16_t freq_raw;
|
||||
float phase;
|
||||
} ch2 = {0};
|
||||
|
||||
// Channel 3 state
|
||||
static struct {
|
||||
bool active;
|
||||
bool dac_on;
|
||||
uint8_t volume_shift;
|
||||
uint16_t freq_raw;
|
||||
float phase;
|
||||
} ch3 = {0};
|
||||
|
||||
// Channel 4 state
|
||||
static struct {
|
||||
bool active;
|
||||
uint8_t volume;
|
||||
uint16_t lfsr;
|
||||
uint8_t divisor;
|
||||
uint8_t shift;
|
||||
bool width_mode;
|
||||
float timer;
|
||||
} ch4 = {.lfsr = 0x7FFF};
|
||||
|
||||
// Audio system
|
||||
static bool audio_enabled = false;
|
||||
static int16_t *audio_buffer = NULL;
|
||||
static SemaphoreHandle_t apu_mutex = NULL;
|
||||
|
||||
// Debug
|
||||
static int audio_write_count = 0;
|
||||
|
||||
// Duty waveforms (8 steps each) - BIPOLAR for proper square waves!
|
||||
static const int8_t duty_table[4][8] = {
|
||||
{-1, -1, -1, -1, -1, -1, -1, 1}, // 12.5% duty cycle
|
||||
{ 1, -1, -1, -1, -1, -1, -1, 1}, // 25% duty cycle
|
||||
{ 1, -1, -1, -1, -1, 1, 1, 1}, // 50% duty cycle
|
||||
{-1, 1, 1, 1, 1, 1, 1, -1}, // 75% duty cycle (inverted)
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// Peanut-GB Audio Callbacks
|
||||
// ============================================
|
||||
|
||||
uint8_t audio_read(const uint16_t addr);
|
||||
void audio_write(const uint16_t addr, const uint8_t val);
|
||||
|
||||
uint8_t audio_read(const uint16_t addr)
|
||||
{
|
||||
ESP_LOGI(TAG, "Potentiometer changed: Volume=%d%%, Brightness=%d%%",
|
||||
volume, brightness);
|
||||
|
||||
// TODO: Set audio volume when audio is implemented
|
||||
// audio_set_volume(volume);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialize NVS (Non-Volatile Storage)
|
||||
*/
|
||||
static void init_nvs(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Initializing NVS...");
|
||||
|
||||
esp_err_t ret = nvs_flash_init();
|
||||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_LOGW(TAG, "NVS partition was truncated, erasing...");
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
ret = nvs_flash_init();
|
||||
if (addr >= 0xFF30 && addr <= 0xFF3F) {
|
||||
return wave_ram[addr - 0xFF30];
|
||||
}
|
||||
ESP_ERROR_CHECK(ret);
|
||||
|
||||
ESP_LOGI(TAG, "✓ NVS initialized");
|
||||
if (addr == 0xFF26) {
|
||||
uint8_t status = master_enable ? 0x80 : 0x00;
|
||||
if (ch1.active) status |= 0x01;
|
||||
if (ch2.active) status |= 0x02;
|
||||
if (ch3.active) status |= 0x04;
|
||||
if (ch4.active) status |= 0x08;
|
||||
return status | 0x70;
|
||||
}
|
||||
if (addr >= 0xFF10 && addr <= 0xFF3F) {
|
||||
return apu_regs[addr - 0xFF10];
|
||||
}
|
||||
return 0xFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialize PSRAM
|
||||
*/
|
||||
static void init_psram(void)
|
||||
void audio_write(const uint16_t addr, const uint8_t val)
|
||||
{
|
||||
ESP_LOGI(TAG, "Checking PSRAM...");
|
||||
if (apu_mutex) xSemaphoreTake(apu_mutex, portMAX_DELAY);
|
||||
|
||||
if (esp_psram_is_initialized()) {
|
||||
size_t psram_size = esp_psram_get_size();
|
||||
ESP_LOGI(TAG, "✓ PSRAM initialized: %d MB", psram_size / (1024 * 1024));
|
||||
} else {
|
||||
ESP_LOGE(TAG, "✗ PSRAM not available!");
|
||||
ESP_LOGE(TAG, " Make sure PSRAM is enabled in sdkconfig!");
|
||||
audio_write_count++;
|
||||
|
||||
// Wave RAM
|
||||
if (addr >= 0xFF30 && addr <= 0xFF3F) {
|
||||
wave_ram[addr - 0xFF30] = val;
|
||||
if (apu_mutex) xSemaphoreGive(apu_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store raw register
|
||||
if (addr >= 0xFF10 && addr <= 0xFF3F) {
|
||||
apu_regs[addr - 0xFF10] = val;
|
||||
}
|
||||
|
||||
switch (addr) {
|
||||
// === NR52 - Master Control ===
|
||||
case 0xFF26:
|
||||
master_enable = (val & 0x80) != 0;
|
||||
if (!master_enable) {
|
||||
ch1.active = ch2.active = ch3.active = ch4.active = false;
|
||||
memset(apu_regs, 0, 0x17);
|
||||
}
|
||||
break;
|
||||
|
||||
// === Channel 1 - Square with Sweep ===
|
||||
case 0xFF11: // NR11 - Duty & Length
|
||||
ch1.duty = (val >> 6) & 3;
|
||||
break;
|
||||
|
||||
case 0xFF12: // NR12 - Volume Envelope
|
||||
ch1.volume = (val >> 4) & 0x0F;
|
||||
ch1.dac_on = (val & 0xF8) != 0; // DAC enable check
|
||||
if (!ch1.dac_on) ch1.active = false;
|
||||
break;
|
||||
|
||||
case 0xFF13: // NR13 - Freq Low
|
||||
ch1.freq_raw = (ch1.freq_raw & 0x700) | val;
|
||||
break;
|
||||
|
||||
case 0xFF14: // NR14 - Freq High + Trigger
|
||||
ch1.freq_raw = (ch1.freq_raw & 0xFF) | ((val & 0x07) << 8);
|
||||
if (val & 0x80) {
|
||||
ch1.active = ch1.dac_on; // Only activate if DAC is on
|
||||
ch1.phase = 0;
|
||||
ch1.volume = (apu_regs[0x02] >> 4) & 0x0F;
|
||||
}
|
||||
break;
|
||||
|
||||
// === Channel 2 - Square ===
|
||||
case 0xFF16: // NR21 - Duty & Length
|
||||
ch2.duty = (val >> 6) & 3;
|
||||
break;
|
||||
|
||||
case 0xFF17: // NR22 - Volume Envelope
|
||||
ch2.volume = (val >> 4) & 0x0F;
|
||||
ch2.dac_on = (val & 0xF8) != 0; // DAC enable check
|
||||
if (!ch2.dac_on) ch2.active = false;
|
||||
break;
|
||||
|
||||
case 0xFF18: // NR23 - Freq Low
|
||||
ch2.freq_raw = (ch2.freq_raw & 0x700) | val;
|
||||
break;
|
||||
|
||||
case 0xFF19: // NR24 - Freq High + Trigger
|
||||
ch2.freq_raw = (ch2.freq_raw & 0xFF) | ((val & 0x07) << 8);
|
||||
if (val & 0x80) {
|
||||
ch2.active = ch2.dac_on; // Only activate if DAC is on
|
||||
ch2.phase = 0;
|
||||
ch2.volume = (apu_regs[0x07] >> 4) & 0x0F;
|
||||
}
|
||||
break;
|
||||
|
||||
// === Channel 3 - Wave ===
|
||||
case 0xFF1A: // NR30 - DAC Enable
|
||||
ch3.dac_on = (val & 0x80) != 0;
|
||||
if (!ch3.dac_on) ch3.active = false;
|
||||
break;
|
||||
|
||||
case 0xFF1C: // NR32 - Volume
|
||||
ch3.volume_shift = (val >> 5) & 3;
|
||||
break;
|
||||
|
||||
case 0xFF1D: // NR33 - Freq Low
|
||||
ch3.freq_raw = (ch3.freq_raw & 0x700) | val;
|
||||
break;
|
||||
|
||||
case 0xFF1E: // NR34 - Freq High + Trigger
|
||||
ch3.freq_raw = (ch3.freq_raw & 0xFF) | ((val & 0x07) << 8);
|
||||
if (val & 0x80) {
|
||||
ch3.active = ch3.dac_on;
|
||||
ch3.phase = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
// === Channel 4 - Noise ===
|
||||
case 0xFF21: // NR42 - Volume Envelope
|
||||
ch4.volume = (val >> 4) & 0x0F;
|
||||
if ((val & 0xF8) == 0) ch4.active = false;
|
||||
break;
|
||||
|
||||
case 0xFF22: // NR43 - Polynomial Counter
|
||||
ch4.shift = (val >> 4) & 0x0F;
|
||||
ch4.width_mode = (val >> 3) & 1;
|
||||
ch4.divisor = val & 0x07;
|
||||
break;
|
||||
|
||||
case 0xFF23: // NR44 - Trigger
|
||||
if (val & 0x80) {
|
||||
ch4.active = true;
|
||||
ch4.lfsr = 0x7FFF;
|
||||
ch4.timer = 0;
|
||||
ch4.volume = (apu_regs[0x11] >> 4) & 0x0F;
|
||||
}
|
||||
break;
|
||||
|
||||
// === Master Volume & Panning ===
|
||||
case 0xFF24: // NR50
|
||||
master_vol_left = (val >> 4) & 7;
|
||||
master_vol_right = val & 7;
|
||||
break;
|
||||
|
||||
case 0xFF25: // NR51
|
||||
panning = val;
|
||||
break;
|
||||
}
|
||||
|
||||
if (apu_mutex) xSemaphoreGive(apu_mutex);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Audio Sample Generation
|
||||
// ============================================
|
||||
|
||||
static inline float get_frequency(uint16_t freq_raw)
|
||||
{
|
||||
// GameBoy frequency formula: f = 131072 / (2048 - freq_raw)
|
||||
if (freq_raw >= 2048) return 0;
|
||||
return 131072.0f / (2048.0f - freq_raw);
|
||||
}
|
||||
|
||||
static inline float get_wave_frequency(uint16_t freq_raw)
|
||||
{
|
||||
// Wave channel: f = 65536 / (2048 - freq_raw)
|
||||
if (freq_raw >= 2048) return 0;
|
||||
return 65536.0f / (2048.0f - freq_raw);
|
||||
}
|
||||
|
||||
static void generate_samples(int16_t *buffer, int num_samples)
|
||||
{
|
||||
if (apu_mutex) xSemaphoreTake(apu_mutex, portMAX_DELAY);
|
||||
|
||||
for (int i = 0; i < num_samples; i++) {
|
||||
int32_t left = 0;
|
||||
int32_t right = 0;
|
||||
|
||||
if (!master_enable) {
|
||||
buffer[i * 2] = 0;
|
||||
buffer[i * 2 + 1] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
// === Channel 1 - Square Wave ===
|
||||
if (ch1.active && ch1.dac_on && ch1.volume > 0 && ch1.freq_raw > 0) {
|
||||
float freq = get_frequency(ch1.freq_raw);
|
||||
float phase_inc = freq / SAMPLE_RATE;
|
||||
|
||||
ch1.phase += phase_inc;
|
||||
if (ch1.phase >= 1.0f) ch1.phase -= 1.0f;
|
||||
|
||||
int step = (int)(ch1.phase * 8) & 7;
|
||||
int sample = duty_table[ch1.duty][step] * ch1.volume;
|
||||
|
||||
if (panning & 0x10) left += sample;
|
||||
if (panning & 0x01) right += sample;
|
||||
}
|
||||
|
||||
// === Channel 2 - Square Wave ===
|
||||
if (ch2.active && ch2.dac_on && ch2.volume > 0 && ch2.freq_raw > 0) {
|
||||
float freq = get_frequency(ch2.freq_raw);
|
||||
float phase_inc = freq / SAMPLE_RATE;
|
||||
|
||||
ch2.phase += phase_inc;
|
||||
if (ch2.phase >= 1.0f) ch2.phase -= 1.0f;
|
||||
|
||||
int step = (int)(ch2.phase * 8) & 7;
|
||||
int sample = duty_table[ch2.duty][step] * ch2.volume;
|
||||
|
||||
if (panning & 0x20) left += sample;
|
||||
if (panning & 0x02) right += sample;
|
||||
}
|
||||
|
||||
// === Channel 3 - Wave ===
|
||||
if (ch3.active && ch3.dac_on && ch3.freq_raw > 0) {
|
||||
float freq = get_wave_frequency(ch3.freq_raw);
|
||||
float phase_inc = freq / SAMPLE_RATE;
|
||||
|
||||
ch3.phase += phase_inc;
|
||||
if (ch3.phase >= 1.0f) ch3.phase -= 1.0f;
|
||||
|
||||
int pos = (int)(ch3.phase * 32) & 31;
|
||||
int byte_idx = pos / 2;
|
||||
int sample_raw;
|
||||
if (pos & 1) {
|
||||
sample_raw = wave_ram[byte_idx] & 0x0F;
|
||||
} else {
|
||||
sample_raw = wave_ram[byte_idx] >> 4;
|
||||
}
|
||||
|
||||
// Volume shift: 0=mute, 1=100%, 2=50%, 3=25% - FIXED!
|
||||
int sample = 0;
|
||||
if (ch3.volume_shift > 0) {
|
||||
int shift = ch3.volume_shift - 1; // 1→0, 2→1, 3→2
|
||||
sample = (sample_raw >> shift) - 8; // Center around 0
|
||||
}
|
||||
|
||||
if (panning & 0x40) left += sample;
|
||||
if (panning & 0x04) right += sample;
|
||||
}
|
||||
|
||||
// === Channel 4 - Noise ===
|
||||
if (ch4.active && ch4.volume > 0) {
|
||||
// Noise frequency calculation
|
||||
int divisor = (ch4.divisor == 0) ? 8 : (ch4.divisor * 16);
|
||||
float noise_freq = 524288.0f / divisor / (1 << (ch4.shift + 1));
|
||||
float timer_inc = noise_freq / SAMPLE_RATE;
|
||||
|
||||
ch4.timer += timer_inc;
|
||||
while (ch4.timer >= 1.0f) {
|
||||
ch4.timer -= 1.0f;
|
||||
|
||||
// LFSR step
|
||||
int bit = (ch4.lfsr ^ (ch4.lfsr >> 1)) & 1;
|
||||
ch4.lfsr = (ch4.lfsr >> 1) | (bit << 14);
|
||||
if (ch4.width_mode) {
|
||||
ch4.lfsr &= ~(1 << 6);
|
||||
ch4.lfsr |= (bit << 6);
|
||||
}
|
||||
}
|
||||
|
||||
int sample = (ch4.lfsr & 1) ? 0 : ch4.volume;
|
||||
|
||||
if (panning & 0x80) left += sample;
|
||||
if (panning & 0x08) right += sample;
|
||||
}
|
||||
|
||||
// Apply master volume (0-7) - FIXED scaling to prevent clipping!
|
||||
// Each channel outputs -15 to +15 max (volume 0-15)
|
||||
// With 4 channels: max = 60, min = -60
|
||||
// Scale by 32 for good amplitude: ±60 * 32 * 8 = ±15360 (fits in 16-bit)
|
||||
left = left * (master_vol_left + 1) * 32;
|
||||
right = right * (master_vol_right + 1) * 32;
|
||||
|
||||
// Clamp to 16-bit range (safety)
|
||||
if (left > 32767) left = 32767;
|
||||
if (left < -32768) left = -32768;
|
||||
if (right > 32767) right = 32767;
|
||||
if (right < -32768) right = -32768;
|
||||
|
||||
buffer[i * 2] = (int16_t)left;
|
||||
buffer[i * 2 + 1] = (int16_t)right;
|
||||
}
|
||||
|
||||
if (apu_mutex) xSemaphoreGive(apu_mutex);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Peanut-GB Setup
|
||||
// ============================================
|
||||
|
||||
#define ENABLE_SOUND 1
|
||||
#define ENABLE_LCD 1
|
||||
|
||||
#include "peanut_gb.h"
|
||||
|
||||
// Undefine peanut_gb's LCD definitions (they're for GameBoy, not our display)
|
||||
#undef LCD_WIDTH
|
||||
#undef LCD_HEIGHT
|
||||
|
||||
#define LCD_WIDTH DISPLAY_WIDTH
|
||||
#define LCD_HEIGHT DISPLAY_HEIGHT
|
||||
|
||||
#define SD_MOUNT_POINT "/sd"
|
||||
#define DEFAULT_ROM "/sd/tetris.gb"
|
||||
|
||||
static struct gb_s gb;
|
||||
static uint8_t *rom_data = NULL;
|
||||
static size_t rom_size = 0;
|
||||
static uint16_t *line_buffer = NULL;
|
||||
static uint16_t *frame_buffer = NULL; // Full screen buffer in PSRAM
|
||||
static int current_line = 0;
|
||||
|
||||
// Double-buffering for parallel display/emulation
|
||||
static uint16_t *render_buffer = NULL; // Buffer being rendered to
|
||||
static uint16_t *display_buffer = NULL; // Buffer being displayed
|
||||
static SemaphoreHandle_t frame_ready_sem = NULL;
|
||||
static SemaphoreHandle_t frame_done_sem = NULL;
|
||||
|
||||
static const uint16_t gb_palette[4] = {
|
||||
0x9FE7, 0x6BE4, 0x3760, 0x0C20
|
||||
};
|
||||
|
||||
static uint8_t gb_rom_read(struct gb_s *gb, const uint_fast32_t addr)
|
||||
{
|
||||
return (addr < rom_size) ? rom_data[addr] : 0xFF;
|
||||
}
|
||||
|
||||
static uint8_t gb_cart_ram_read(struct gb_s *gb, const uint_fast32_t addr)
|
||||
{
|
||||
return 0xFF;
|
||||
}
|
||||
|
||||
static void gb_cart_ram_write(struct gb_s *gb, const uint_fast32_t addr, const uint8_t val)
|
||||
{
|
||||
}
|
||||
|
||||
static void gb_error(struct gb_s *gb, const enum gb_error_e err, const uint16_t addr)
|
||||
{
|
||||
ESP_LOGE(TAG, "GB Error %d at 0x%04X", err, addr);
|
||||
}
|
||||
|
||||
static void gb_lcd_draw_line(struct gb_s *gb, const uint8_t pixels[160], const uint_fast8_t line)
|
||||
{
|
||||
// Draw into RENDER buffer (double-buffering for parallel display)
|
||||
#if GB_PIXEL_PERFECT_SCALING
|
||||
// Dynamic scaling based on GB_SCALE_FACTOR
|
||||
// Vertical: Scale GameBoy line (0-143) to output Y coordinate
|
||||
int y_base = (line * GB_RENDER_HEIGHT) / 144;
|
||||
if (y_base >= GB_RENDER_HEIGHT) return;
|
||||
|
||||
// Horizontal scaling: 160 GameBoy pixels -> GB_RENDER_WIDTH output pixels
|
||||
// Dynamic pixel-width algorithm ensures every pixel is filled without gaps
|
||||
int x_dst = 0;
|
||||
for (int x = 0; x < 160; x++) {
|
||||
uint16_t c = gb_palette[pixels[x] & 0x03];
|
||||
uint16_t swapped = (c >> 8) | (c << 8); // RGB->BGR
|
||||
|
||||
// Calculate how wide this pixel should be at current scaling
|
||||
int next_x_dst = ((x + 1) * GB_RENDER_WIDTH) / 160;
|
||||
int pixel_width = next_x_dst - x_dst;
|
||||
|
||||
// Fill pixel_width positions with this color (no gaps!)
|
||||
for (int w = 0; w < pixel_width && x_dst + w < GB_RENDER_WIDTH; w++) {
|
||||
int dst = (y_base + GB_OFFSET_Y) * GB_SCREEN_WIDTH + (x_dst + w + GB_OFFSET_X);
|
||||
render_buffer[dst] = swapped;
|
||||
}
|
||||
|
||||
x_dst = next_x_dst;
|
||||
}
|
||||
|
||||
// Vertical scaling: duplicate lines as needed based on scaling factor
|
||||
int ny = ((line + 1) * GB_RENDER_HEIGHT) / 144;
|
||||
if (ny > y_base + 1 && ny < GB_RENDER_HEIGHT) {
|
||||
memcpy(&render_buffer[(y_base + 1 + GB_OFFSET_Y) * GB_SCREEN_WIDTH + GB_OFFSET_X],
|
||||
&render_buffer[(y_base + GB_OFFSET_Y) * GB_SCREEN_WIDTH + GB_OFFSET_X],
|
||||
GB_RENDER_WIDTH * 2);
|
||||
}
|
||||
#else
|
||||
// Full screen stretch: 160x144 -> 320x240
|
||||
int y = (line * 5) / 3;
|
||||
if (y >= GB_SCREEN_HEIGHT) return;
|
||||
|
||||
// Horizontal doubling: 160 -> 320, with BYTE SWAP for display
|
||||
for (int x = 0; x < 160; x++) {
|
||||
uint16_t c = gb_palette[pixels[x] & 0x03];
|
||||
uint16_t swapped = (c >> 8) | (c << 8); // RGB->BGR
|
||||
int dst = y * GB_SCREEN_WIDTH + x * 2;
|
||||
render_buffer[dst] = swapped;
|
||||
render_buffer[dst + 1] = swapped;
|
||||
}
|
||||
|
||||
// Vertical scaling: duplicate line if needed
|
||||
int ny = ((line + 1) * 5) / 3;
|
||||
if (ny > y + 1 && ny < GB_SCREEN_HEIGHT) {
|
||||
memcpy(&render_buffer[(y + 1) * GB_SCREEN_WIDTH],
|
||||
&render_buffer[y * GB_SCREEN_WIDTH],
|
||||
GB_SCREEN_WIDTH * 2); // 320 pixels * 2 bytes
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static esp_err_t init_sdcard(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Init SD...");
|
||||
esp_vfs_fat_sdmmc_mount_config_t cfg = {
|
||||
.format_if_mount_failed = false,
|
||||
.max_files = 5,
|
||||
.allocation_unit_size = 16 * 1024
|
||||
};
|
||||
sdmmc_card_t *card;
|
||||
sdmmc_host_t host = SDSPI_HOST_DEFAULT();
|
||||
host.max_freq_khz = 400;
|
||||
host.slot = SD_SPI_HOST;
|
||||
sdspi_device_config_t slot = SDSPI_DEVICE_CONFIG_DEFAULT();
|
||||
slot.gpio_cs = SD_PIN_CS;
|
||||
slot.host_id = host.slot;
|
||||
esp_err_t ret = esp_vfs_fat_sdspi_mount(SD_MOUNT_POINT, &host, &slot, &cfg, &card);
|
||||
if (ret == ESP_OK) ESP_LOGI(TAG, "✓ SD OK!");
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool load_rom(const char *path)
|
||||
{
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return false;
|
||||
fseek(f, 0, SEEK_END);
|
||||
rom_size = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
rom_data = malloc(rom_size);
|
||||
if (!rom_data) { fclose(f); return false; }
|
||||
fread(rom_data, 1, rom_size, f);
|
||||
fclose(f);
|
||||
ESP_LOGI(TAG, "✓ ROM: %d bytes", rom_size);
|
||||
return true;
|
||||
}
|
||||
|
||||
static esp_err_t init_audio(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Init Audio...");
|
||||
apu_mutex = xSemaphoreCreateMutex();
|
||||
|
||||
i2s_config_t cfg = {
|
||||
.mode = I2S_MODE_MASTER | I2S_MODE_TX,
|
||||
.sample_rate = SAMPLE_RATE,
|
||||
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
|
||||
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
|
||||
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
|
||||
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
|
||||
.dma_buf_count = 8,
|
||||
.dma_buf_len = SAMPLES_PER_BUFFER,
|
||||
.use_apll = false,
|
||||
.tx_desc_auto_clear = true,
|
||||
};
|
||||
|
||||
i2s_pin_config_t pins = {
|
||||
.bck_io_num = I2S_PIN_BCLK,
|
||||
.ws_io_num = I2S_PIN_LRC,
|
||||
.data_out_num = I2S_PIN_DIN,
|
||||
.data_in_num = I2S_PIN_NO_CHANGE
|
||||
};
|
||||
|
||||
esp_err_t ret = i2s_driver_install(I2S_NUM, &cfg, 0, NULL);
|
||||
if (ret != ESP_OK) return ret;
|
||||
|
||||
ret = i2s_set_pin(I2S_NUM, &pins);
|
||||
if (ret != ESP_OK) return ret;
|
||||
|
||||
i2s_zero_dma_buffer(I2S_NUM);
|
||||
|
||||
audio_buffer = heap_caps_malloc(SAMPLES_PER_BUFFER * 4, MALLOC_CAP_DMA);
|
||||
if (!audio_buffer) return ESP_ERR_NO_MEM;
|
||||
|
||||
audio_enabled = true;
|
||||
ESP_LOGI(TAG, "✓ Audio OK! BCLK=%d LRC=%d DIN=%d",
|
||||
I2S_PIN_BCLK, I2S_PIN_LRC, I2S_PIN_DIN);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void audio_task(void *arg)
|
||||
{
|
||||
ESP_LOGI(TAG, "🎵 Audio task started");
|
||||
int16_t *buffer = heap_caps_malloc(SAMPLES_PER_BUFFER * 4, MALLOC_CAP_DMA);
|
||||
|
||||
while (audio_enabled) {
|
||||
// Generate smaller buffers (512 samples) for lower latency
|
||||
generate_samples(buffer, SAMPLES_PER_BUFFER);
|
||||
size_t written;
|
||||
i2s_write(I2S_NUM, buffer, SAMPLES_PER_BUFFER * 4, &written, portMAX_DELAY);
|
||||
}
|
||||
free(buffer);
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
static void display_task(void *arg)
|
||||
{
|
||||
#if GB_PIXEL_PERFECT_SCALING
|
||||
// Clear screen to black once (for borders that don't change)
|
||||
st7789_fill_screen(0x0000);
|
||||
|
||||
// Allocate temp buffer for compacted GameBoy region (240x216 = 103KB)
|
||||
uint16_t *compact_buffer = heap_caps_malloc(GB_RENDER_WIDTH * GB_RENDER_HEIGHT * 2, MALLOC_CAP_DMA);
|
||||
#endif
|
||||
|
||||
while (1) {
|
||||
// Wait for frame to be ready
|
||||
xSemaphoreTake(frame_ready_sem, portMAX_DELAY);
|
||||
|
||||
#if GB_PIXEL_PERFECT_SCALING
|
||||
// Copy GameBoy region to compact buffer (remove gaps from black borders)
|
||||
for (int y = 0; y < GB_RENDER_HEIGHT; y++) {
|
||||
memcpy(&compact_buffer[y * GB_RENDER_WIDTH],
|
||||
&display_buffer[(y + GB_OFFSET_Y) * GB_SCREEN_WIDTH + GB_OFFSET_X],
|
||||
GB_RENDER_WIDTH * 2);
|
||||
}
|
||||
// Transfer only GameBoy content (240x216 = 33% less data than 320x240!)
|
||||
st7789_draw_buffer_preswapped(compact_buffer,
|
||||
GB_OFFSET_X, GB_OFFSET_Y,
|
||||
GB_RENDER_WIDTH, GB_RENDER_HEIGHT);
|
||||
#else
|
||||
// Full screen mode - draw entire buffer
|
||||
st7789_draw_buffer_preswapped(display_buffer, 0, 0, GB_SCREEN_WIDTH, GB_SCREEN_HEIGHT);
|
||||
#endif
|
||||
|
||||
// Signal frame display is done
|
||||
xSemaphoreGive(frame_done_sem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Print system information
|
||||
*/
|
||||
static void print_system_info(void)
|
||||
static void emulation_task(void *arg)
|
||||
{
|
||||
ESP_LOGI(TAG, "");
|
||||
ESP_LOGI(TAG, "╔════════════════════════════════════════════════════════╗");
|
||||
ESP_LOGI(TAG, "║ ║");
|
||||
ESP_LOGI(TAG, "║ ESP32-S3 GNUBoy LEGO GameBoy Emulator ║");
|
||||
ESP_LOGI(TAG, "║ ║");
|
||||
ESP_LOGI(TAG, "╚════════════════════════════════════════════════════════╝");
|
||||
ESP_LOGI(TAG, "");
|
||||
ESP_LOGI(TAG, "Hardware: Waveshare ESP32-S3-Touch-LCD-2");
|
||||
ESP_LOGI(TAG, "Display: ST7789 2.0\" 240x320");
|
||||
ESP_LOGI(TAG, "Flash: %d MB", spi_flash_get_chip_size() / (1024 * 1024));
|
||||
|
||||
if (esp_psram_is_initialized()) {
|
||||
ESP_LOGI(TAG, "PSRAM: %d MB", esp_psram_get_size() / (1024 * 1024));
|
||||
int frame = 0;
|
||||
TickType_t last = xTaskGetTickCount();
|
||||
int16_t *frame_audio = heap_caps_malloc(SAMPLES_PER_FRAME * 4, MALLOC_CAP_DMA);
|
||||
|
||||
while (1) {
|
||||
TickType_t frame_start = xTaskGetTickCount();
|
||||
|
||||
// Run emulation - renders into render_buffer
|
||||
gb_run_frame(&gb);
|
||||
|
||||
// Swap buffers
|
||||
uint16_t *temp = render_buffer;
|
||||
render_buffer = display_buffer;
|
||||
display_buffer = temp;
|
||||
|
||||
// Signal display task that new frame is ready
|
||||
xSemaphoreGive(frame_ready_sem);
|
||||
|
||||
// Wait for display to finish with previous frame
|
||||
xSemaphoreTake(frame_done_sem, portMAX_DELAY);
|
||||
|
||||
frame++;
|
||||
TickType_t frame_end = xTaskGetTickCount();
|
||||
int frame_time_ms = (frame_end - frame_start) * portTICK_PERIOD_MS;
|
||||
|
||||
if (frame % 60 == 0) { // Every second
|
||||
ESP_LOGI(TAG, "Frame %d | time=%dms (%.1f FPS) | writes=%d | sound=%s | ch1=%d ch2=%d ch3=%d ch4=%d",
|
||||
frame, frame_time_ms, 1000.0f / frame_time_ms,
|
||||
audio_write_count, master_enable ? "ON" : "OFF",
|
||||
ch1.active, ch2.active, ch3.active, ch4.active);
|
||||
}
|
||||
|
||||
// GameBoy runs at 59.7275 FPS = 16.7424ms per frame
|
||||
vTaskDelayUntil(&last, pdMS_TO_TICKS(17)); // 17ms ≈ 58.8 FPS
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "");
|
||||
ESP_LOGI(TAG, "Features:");
|
||||
ESP_LOGI(TAG, " ✓ GameBoy / GameBoy Color emulation");
|
||||
ESP_LOGI(TAG, " ✓ NFC ROM selection");
|
||||
ESP_LOGI(TAG, " ✓ Potentiometer controls");
|
||||
ESP_LOGI(TAG, " ✓ Link Cable 2-player");
|
||||
ESP_LOGI(TAG, " ✓ SD Card ROM loading");
|
||||
ESP_LOGI(TAG, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialize hardware components
|
||||
*/
|
||||
static void init_hardware(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Initializing hardware components...");
|
||||
|
||||
// TODO: Initialize display (ST7789)
|
||||
// st7789_init();
|
||||
ESP_LOGI(TAG, " [ ] Display (ST7789) - TODO");
|
||||
|
||||
// TODO: Initialize buttons
|
||||
// buttons_init();
|
||||
ESP_LOGI(TAG, " [ ] Buttons - TODO");
|
||||
|
||||
// TODO: Initialize audio (I2S)
|
||||
// audio_init();
|
||||
ESP_LOGI(TAG, " [ ] Audio (I2S) - TODO");
|
||||
|
||||
// TODO: Initialize SD Card
|
||||
// sd_card_init();
|
||||
ESP_LOGI(TAG, " [ ] SD Card - TODO");
|
||||
|
||||
// TODO: Initialize NFC
|
||||
// nfc_manager_init();
|
||||
ESP_LOGI(TAG, " [ ] NFC Reader - TODO");
|
||||
|
||||
// TODO: Initialize Link Cable
|
||||
// link_cable_init();
|
||||
ESP_LOGI(TAG, " [ ] Link Cable - TODO");
|
||||
|
||||
// TODO: Initialize Potentiometers
|
||||
// poti_manager_init();
|
||||
// poti_manager_start(poti_change_handler);
|
||||
ESP_LOGI(TAG, " [ ] Potentiometers - TODO");
|
||||
|
||||
ESP_LOGI(TAG, "✓ Hardware initialization complete");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Main application entry point
|
||||
*/
|
||||
void app_main(void)
|
||||
{
|
||||
// Print welcome banner
|
||||
print_system_info();
|
||||
ESP_LOGI(TAG, "");
|
||||
ESP_LOGI(TAG, "╔═══════════════════════════════════════╗");
|
||||
ESP_LOGI(TAG, "║ ESP32-S3 GameBoy - FIXED AUDIO! ║");
|
||||
ESP_LOGI(TAG, "╚═══════════════════════════════════════╝");
|
||||
|
||||
// Initialize core systems
|
||||
init_nvs();
|
||||
init_psram();
|
||||
nvs_flash_init();
|
||||
st7789_init();
|
||||
st7789_set_backlight(80);
|
||||
st7789_fill_screen(0x001F);
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
|
||||
// Check PSRAM availability
|
||||
size_t psram_total = heap_caps_get_total_size(MALLOC_CAP_SPIRAM);
|
||||
size_t psram_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
|
||||
ESP_LOGI(TAG, "PSRAM: %d KB total, %d KB free", psram_total / 1024, psram_free / 1024);
|
||||
|
||||
// Allocate TWO frame buffers for double-buffering
|
||||
// Size depends on scaling mode
|
||||
size_t buffer_size = GB_SCREEN_WIDTH * GB_SCREEN_HEIGHT * 2; // RGB565 = 2 bytes/pixel
|
||||
ESP_LOGI(TAG, "Buffer size: %d KB (%dx%d)", buffer_size / 1024, GB_SCREEN_WIDTH, GB_SCREEN_HEIGHT);
|
||||
|
||||
// Try PSRAM first, fallback to regular RAM if needed
|
||||
size_t min_psram_needed = buffer_size * 2 + 50000; // 2 buffers + margin
|
||||
if (psram_free > min_psram_needed) {
|
||||
render_buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM);
|
||||
display_buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM);
|
||||
ESP_LOGI(TAG, "Double buffers allocated in PSRAM");
|
||||
}
|
||||
|
||||
if (!render_buffer || !display_buffer) {
|
||||
ESP_LOGW(TAG, "PSRAM alloc failed, trying regular RAM...");
|
||||
if (render_buffer) free(render_buffer);
|
||||
if (display_buffer) free(display_buffer);
|
||||
render_buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_8BIT);
|
||||
display_buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_8BIT);
|
||||
ESP_LOGI(TAG, "Double buffers allocated in internal RAM");
|
||||
}
|
||||
|
||||
if (!render_buffer || !display_buffer) {
|
||||
ESP_LOGE(TAG, "No memory for double framebuffers!");
|
||||
while(1) vTaskDelay(1000);
|
||||
}
|
||||
|
||||
// Clear buffers to black (for letterboxing in pixel-perfect mode)
|
||||
memset(render_buffer, 0, buffer_size);
|
||||
memset(display_buffer, 0, buffer_size);
|
||||
|
||||
// Create semaphores for buffer synchronization
|
||||
frame_ready_sem = xSemaphoreCreateBinary();
|
||||
frame_done_sem = xSemaphoreCreateBinary();
|
||||
xSemaphoreGive(frame_done_sem); // Initially, display is "done"
|
||||
|
||||
// Initialize hardware peripherals
|
||||
init_hardware();
|
||||
if (init_sdcard() != ESP_OK) {
|
||||
st7789_fill_screen(0xF800);
|
||||
while(1) vTaskDelay(1000);
|
||||
}
|
||||
|
||||
st7789_fill_screen(0x07E0);
|
||||
vTaskDelay(pdMS_TO_TICKS(300));
|
||||
|
||||
if (!load_rom(DEFAULT_ROM)) {
|
||||
st7789_fill_screen(0xF800);
|
||||
ESP_LOGE(TAG, "ROM load failed!");
|
||||
while(1) vTaskDelay(1000);
|
||||
}
|
||||
|
||||
if (gb_init(&gb, &gb_rom_read, &gb_cart_ram_read, &gb_cart_ram_write, &gb_error, NULL) != GB_INIT_NO_ERROR) {
|
||||
st7789_fill_screen(0xF800);
|
||||
while(1) vTaskDelay(1000);
|
||||
}
|
||||
|
||||
gb_init_lcd(&gb, &gb_lcd_draw_line);
|
||||
|
||||
if (init_audio() == ESP_OK) {
|
||||
audio_enabled = true;
|
||||
// Run audio on Core 1, emulator on Core 0 for better performance
|
||||
xTaskCreatePinnedToCore(audio_task, "audio", 4096, NULL, 5, NULL, 1);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "");
|
||||
ESP_LOGI(TAG, "════════════════════════════════════════════════════════");
|
||||
ESP_LOGI(TAG, "System initialization complete!");
|
||||
ESP_LOGI(TAG, "════════════════════════════════════════════════════════");
|
||||
ESP_LOGI(TAG, "");
|
||||
|
||||
// TODO: Start emulator
|
||||
ESP_LOGI(TAG, "Starting GNUBoy emulator...");
|
||||
ESP_LOGI(TAG, " (GNUBoy core not yet integrated)");
|
||||
|
||||
// Main loop
|
||||
ESP_LOGI(TAG, "Entering main loop...");
|
||||
ESP_LOGI(TAG, "═══════════════════════════════════════");
|
||||
ESP_LOGI(TAG, "✓ TETRIS with FIXED AUDIO! 🎮🔊");
|
||||
ESP_LOGI(TAG, "═══════════════════════════════════════");
|
||||
|
||||
st7789_fill_screen(0x0000);
|
||||
|
||||
// Start display task on Core 0 (parallel to emulation!)
|
||||
xTaskCreatePinnedToCore(display_task, "display", 4096, NULL, 5, NULL, 0);
|
||||
|
||||
// Start emulation task on Core 1 (with audio for cache locality)
|
||||
xTaskCreatePinnedToCore(emulation_task, "emulation", 8192, NULL, 5, NULL, 1);
|
||||
|
||||
// Keep app_main running (don't exit)
|
||||
while (1) {
|
||||
// TODO: Run emulator main loop
|
||||
// gnuboy_run_frame();
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user