diff --git a/examples/NES/NES.ino b/examples/NES/NES.ino new file mode 100644 index 0000000..7265f1d --- /dev/null +++ b/examples/NES/NES.ino @@ -0,0 +1,140 @@ +/* +* Base on https://github.com/moononournation/arduino-nofrendo , by moononournation +* - Adapt to LilyGo Twatch series +* - Added Dabble App Bluetooth control method for watch without handle +* - Dabble App download link : https://play.google.com/store/apps/details?id=io.dabbleapp&hl=en_US&gl=US +* Adapted to Twatch by Lewis 04/11/2020 +* */ + +#pragma mark - Depend arduino-nofrendo & DabbleESP32 library + +/* +cd ~/Arduino/libraries +git clone https://github.com/Xinyuan-LilyGO/arduino-nofrendo.git + +If you don’t use Dabble App, you don’t need it clone DabbleESP32 +git clone https://github.com/Xinyuan-LilyGO/DabbleESP32.git +*/ + +#include +#include +#include +#include +#include "config.h" + +extern "C" +{ +#include +#include +} + +#define FSROOT "/fs" + +TTGOClass *watch; +TFT_eSPI *tft; +int16_t bg_color; + +static int16_t frame_x, frame_y, frame_x_offset, frame_width, frame_height, frame_line_pixels; +extern uint16_t myPalette[]; + +void setup() +{ + Serial.begin(115200); + + watch = TTGOClass::getWatch(); + + watch->begin(); + watch->openBL(); + + tft = watch->tft; + + // turn off WiFi + esp_wifi_deinit(); + + // disable Core 0 WDT + TaskHandle_t idle_0 = xTaskGetIdleTaskHandleForCPU(0); + esp_task_wdt_delete(idle_0); + + bg_color = tft->color565(24, 28, 24); // DARK DARK GREY + tft->fillScreen(bg_color); + tft->setCursor(0, 0); + +#if defined(HW_CONTROLLER_GAMEPAD) || defined(LILYGO_WATCH_2019_WITH_TOUCH) || defined(LILYGO_WATCH_2019_NO_TOUCH) + tft->setRotation(3); +#endif + + SPIFFS.begin(false, FSROOT); + + FS filesystem = SPIFFS; + + // find first rom file (*.nes) + File root = filesystem.open("/"); + char *argv[1]; + if (!root) { + Serial.println("Filesystem mount failed!"); + tft->println("Filesystem mount failed!"); + } else { + bool foundRom = false; + + File file = root.openNextFile(); + while (file) { + if (file.isDirectory()) { + // skip + } else { + char *filename = (char *)file.name(); + int8_t len = strlen(filename); + if (strstr(strlwr(filename + (len - 4)), ".nes")) { + foundRom = true; + char fullFilename[256]; + sprintf(fullFilename, "%s%s", FSROOT, filename); + Serial.println(fullFilename); + argv[0] = fullFilename; + break; + } + } + + file = root.openNextFile(); + } + + if (!foundRom) { + Serial.println("Failed to find rom file, please copy rom file to data folder and upload with \"ESP32 Sketch Data Upload\""); + tft->println("Failed to find rom file, please copy rom file to data folder and upload with \"ESP32 Sketch Data Upload\""); + argv[0] = "/"; + } + + Serial.println("NoFrendo start!\n"); + nofrendo_main(1, argv); + Serial.println("NoFrendo end!\n"); + } +} + +void loop() +{ +} + + + +extern "C" void display_init() +{ + frame_x = 0; + frame_x_offset = (NES_SCREEN_WIDTH - tft->width()) / 2; + frame_width = tft->width(); + frame_height = NES_SCREEN_HEIGHT; + frame_line_pixels = frame_width; + frame_y = (tft->height() - NES_SCREEN_HEIGHT) / 2; +} + +extern "C" void display_write_frame(const uint8_t *data[]) +{ + tft->setAddrWindow(frame_x, frame_y, frame_width, frame_height); + tft->startWrite(); + for (int32_t i = 0; i < NES_SCREEN_HEIGHT; i++) { + tft->writeIndexedPixels((uint8_t *)(data[i] + frame_x_offset), myPalette, frame_line_pixels); + } + tft->endWrite(); +} + +extern "C" void display_clear() +{ + tft->fillScreen(bg_color); +} \ No newline at end of file diff --git a/examples/NES/config.h b/examples/NES/config.h new file mode 100644 index 0000000..8249102 --- /dev/null +++ b/examples/NES/config.h @@ -0,0 +1,29 @@ +// => Hardware select +// #define LILYGO_WATCH_2019_WITH_TOUCH // To use T-Watch2019 with touchscreen, please uncomment this line +// #define LILYGO_WATCH_2019_NO_TOUCH // To use T-Watch2019 Not touchscreen , please uncomment this line +// #define LILYGO_WATCH_2020_V1 //To use T-Watch2020, please uncomment this line +// #define LILYGO_WATCH_2020_V2 //To use T-Watch2020, please uncomment this line + + +// Controller select +// #define HW_CONTROLLER_DABBLE_APP //Need to install Dabble App on your phone,download link : https://play.google.com/store/apps/details?id=io.dabbleapp&hl=en_US&gl=US +#define HW_CONTROLLER_GAMEPAD //Need an external control handle, can only be used in 2019 Twatch + + + + +#ifdef HW_CONTROLLER_GAMEPAD +#define HW_CONTROLLER_GPIO +#define HW_CONTROLLER_GPIO_ANALOG_JOYSTICK +#define HW_CONTROLLER_GPIO_UP_DOWN 34 +#define HW_CONTROLLER_GPIO_LEFT_RIGHT 33 +#define HW_CONTROLLER_GPIO_SELECT 15 +#define HW_CONTROLLER_GPIO_START 36 +#define HW_CONTROLLER_GPIO_A 13 +#define HW_CONTROLLER_GPIO_B 25 +#define HW_CONTROLLER_GPIO_X 14 +#define HW_CONTROLLER_GPIO_Y 26 +#endif + +#include + diff --git a/examples/NES/controller.cpp b/examples/NES/controller.cpp new file mode 100644 index 0000000..4bf010e --- /dev/null +++ b/examples/NES/controller.cpp @@ -0,0 +1,196 @@ +#include +#include "config.h" +/* controller is GPIO */ +#if defined(HW_CONTROLLER_GPIO) + +extern "C" void controller_init() +{ +#if defined(HW_CONTROLLER_GPIO_ANALOG_JOYSTICK) + pinMode(HW_CONTROLLER_GPIO_UP_DOWN, INPUT); + pinMode(HW_CONTROLLER_GPIO_LEFT_RIGHT, INPUT); +#else /* !defined(HW_CONTROLLER_GPIO_ANALOG_JOYSTICK) */ + pinMode(HW_CONTROLLER_GPIO_UP, INPUT_PULLUP); + pinMode(HW_CONTROLLER_GPIO_DOWN, INPUT_PULLUP); + pinMode(HW_CONTROLLER_GPIO_LEFT, INPUT_PULLUP); + pinMode(HW_CONTROLLER_GPIO_RIGHT, INPUT_PULLUP); +#endif /* !defined(HW_CONTROLLER_GPIO_ANALOG_JOYSTICK) */ + pinMode(HW_CONTROLLER_GPIO_SELECT, INPUT_PULLUP); + pinMode(HW_CONTROLLER_GPIO_START, INPUT_PULLUP); + pinMode(HW_CONTROLLER_GPIO_A, INPUT_PULLUP); + pinMode(HW_CONTROLLER_GPIO_B, INPUT_PULLUP); + pinMode(HW_CONTROLLER_GPIO_X, INPUT_PULLUP); + pinMode(HW_CONTROLLER_GPIO_Y, INPUT_PULLUP); +} + +extern "C" uint32_t controller_read_input() +{ + uint32_t u, d, l, r, s, t, a, b, x, y; + +#if defined(HW_CONTROLLER_GPIO_ANALOG_JOYSTICK) + +#if defined(HW_CONTROLLER_GPIO_REVERSE_UP) + int joyY = 4095 - analogRead(HW_CONTROLLER_GPIO_UP_DOWN); +#else /* !defined(HW_CONTROLLER_GPIO_REVERSE_UD) */ + int joyY = analogRead(HW_CONTROLLER_GPIO_UP_DOWN); +#endif /* !defined(HW_CONTROLLER_GPIO_REVERSE_UD) */ + +#if defined(HW_CONTROLLER_GPIO_REVERSE_LF) + int joyX = 4095 - analogRead(HW_CONTROLLER_GPIO_LEFT_RIGHT); +#else /* !defined(HW_CONTROLLER_GPIO_REVERSE_LF) */ + int joyX = analogRead(HW_CONTROLLER_GPIO_LEFT_RIGHT); +#endif /* !defined(HW_CONTROLLER_GPIO_REVERSE_LF) */ + + if (joyY > 2048 + 1024) { + u = 1; + d = 0; + } else if (joyY < 1024) { + u = 0; + d = 1; + } else { + u = 1; + d = 1; + } + + if (joyX > 2048 + 1024) { + l = 1; + r = 0; + } else if (joyX < 1024) { + l = 0; + r = 1; + } else { + l = 1; + r = 1; + } + +#else /* !defined(HW_CONTROLLER_GPIO_ANALOG_JOYSTICK) */ + u = digitalRead(HW_CONTROLLER_GPIO_UP); + d = digitalRead(HW_CONTROLLER_GPIO_DOWN); + l = digitalRead(HW_CONTROLLER_GPIO_LEFT); + r = digitalRead(HW_CONTROLLER_GPIO_RIGHT); +#endif /* !defined(HW_CONTROLLER_GPIO_ANALOG_JOYSTICK) */ + + s = digitalRead(HW_CONTROLLER_GPIO_SELECT); + t = digitalRead(HW_CONTROLLER_GPIO_START); + a = digitalRead(HW_CONTROLLER_GPIO_A); + b = digitalRead(HW_CONTROLLER_GPIO_B); + x = digitalRead(HW_CONTROLLER_GPIO_X); + y = digitalRead(HW_CONTROLLER_GPIO_Y); + return 0xFFFFFFFF ^ ((!u << 0) | (!d << 1) | (!l << 2) | (!r << 3) | (!s << 4) | (!t << 5) | (!a << 6) | (!b << 7) | (!x << 8) | (!y << 9)); +} + +/* controller is I2C BBQ10Keyboard */ +#elif defined(HW_CONTROLLER_I2C_BBQ10KB) + +#include +#include +BBQ10Keyboard keyboard; +static uint32_t value = 0xFFFFFFFF; + +extern "C" void controller_init() +{ + Wire.begin(); + keyboard.begin(); + keyboard.setBacklight(0.2f); +} + +extern "C" uint32_t controller_read_input() +{ + + int keyCount = keyboard.keyCount(); + while (keyCount--) { + const BBQ10Keyboard::KeyEvent key = keyboard.keyEvent(); + String state = "pressed"; + if (key.state == BBQ10Keyboard::StateLongPress) + state = "held down"; + else if (key.state == BBQ10Keyboard::StateRelease) + state = "released"; + + // Serial.printf("key: '%c' (dec %d, hex %02x) %s\r\n", key.key, key.key, key.key, state.c_str()); + + uint32_t bit = 0; + if (key.key != 0) { + switch (key.key) { + case 'w': // up + bit = (1 << 0); + break; + case 'z': // down + bit = (1 << 1); + break; + case 'a': // left + bit = (1 << 2); + break; + case 'd': // right + bit = (1 << 3); + break; + case ' ': // select + bit = (1 << 4); + break; + case 10: // enter -> start + bit = (1 << 5); + break; + case 'k': // A + bit = (1 << 6); + break; + case 'l': // B + bit = (1 << 7); + break; + case 'o': // X + bit = (1 << 8); + break; + case 'p': // Y + bit = (1 << 9); + break; + } + if (key.state == BBQ10Keyboard::StatePress) { + value ^= bit; + } else if (key.state == BBQ10Keyboard::StateRelease) { + value |= bit; + } + } + } + + return value; +} +#elif defined(HW_CONTROLLER_DABBLE_APP) + +#include + +extern "C" void controller_init() +{ + Dabble.begin("Watch-NES"); +} + +extern "C" uint32_t controller_read_input() +{ + uint32_t u, d, l, r, s, t, a, b, x, y; + + Dabble.processInput(); + + d = GamePad.isUpPressed(); + u = GamePad.isDownPressed(); + r = GamePad.isLeftPressed(); + l = GamePad.isRightPressed(); + + s = !GamePad.isSelectPressed(); + t = !GamePad.isStartPressed(); + y = !GamePad.isTrianglePressed(); + b = !GamePad.isCrossPressed(); + x = !GamePad.isSquarePressed(); + a = !GamePad.isCirclePressed(); + + return 0xFFFFFFFF ^ ((!u << 0) | (!d << 1) | (!l << 2) | (!r << 3) | (!s << 4) | (!t << 5) | (!a << 6) | (!b << 7) | (!x << 8) | (!y << 9)); +} + +#else /* no controller defined */ + +extern "C" void controller_init() +{ + Serial.printf("GPIO controller disabled in menuconfig; no input enabled.\n"); +} + +extern "C" uint32_t controller_read_input() +{ + return 0xFFFFFFFF; +} + +#endif /* no controller defined */ diff --git a/examples/NES/osd.c b/examples/NES/osd.c new file mode 100644 index 0000000..ce06fca --- /dev/null +++ b/examples/NES/osd.c @@ -0,0 +1,367 @@ +/* start rewrite from: https://github.com/espressif/esp32-nesemu.git */ +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + + +TimerHandle_t timer; + +/* memory allocation */ +extern void *mem_alloc(int size, bool prefer_fast_memory) +{ + if (prefer_fast_memory) { + return heap_caps_malloc(size, MALLOC_CAP_8BIT); + } else { + return heap_caps_malloc_prefer(size, MALLOC_CAP_SPIRAM, MALLOC_CAP_DEFAULT); + } +} + +/* audio */ +#define DEFAULT_SAMPLERATE 22050 + +#if defined(HW_AUDIO) + +#define DEFAULT_FRAGSIZE 1024 +static void (*audio_callback)(void *buffer, int length) = NULL; +QueueHandle_t queue; +static int16_t *audio_frame; + +static int osd_init_sound(void) +{ + audio_frame = NOFRENDO_MALLOC(4 * DEFAULT_FRAGSIZE); + + i2s_config_t cfg = { +#if defined(HW_AUDIO_EXTDAC) + .mode = I2S_MODE_MASTER | I2S_MODE_TX, +#else /* !defined(HW_AUDIO_EXTDAC) */ + .mode = I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN, +#endif /* !defined(HW_AUDIO_EXTDAC) */ + .sample_rate = DEFAULT_SAMPLERATE, + .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, + .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, +#if defined(HW_AUDIO_EXTDAC) + .communication_format = I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB, +#else /* !defined(HW_AUDIO_EXTDAC) */ + .communication_format = I2S_COMM_FORMAT_PCM | I2S_COMM_FORMAT_I2S_MSB, +#endif /* !defined(HW_AUDIO_EXTDAC) */ + .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, + .dma_buf_count = 6, + .dma_buf_len = 512, + .use_apll = false, + }; + i2s_driver_install(I2S_NUM_0, &cfg, 2, &queue); +#if defined(HW_AUDIO_EXTDAC) + i2s_pin_config_t pins = { + .bck_io_num = HW_AUDIO_EXTDAC_BCLK, + .ws_io_num = HW_AUDIO_EXTDAC_WCLK, + .data_out_num = HW_AUDIO_EXTDAC_DOUT, + .data_in_num = I2S_PIN_NO_CHANGE, + }; + i2s_set_pin(I2S_NUM_0, &pins); +#else /* !defined(HW_AUDIO_EXTDAC) */ + i2s_set_pin(I2S_NUM_0, NULL); + i2s_set_dac_mode(I2S_DAC_CHANNEL_BOTH_EN); +#endif /* !defined(HW_AUDIO_EXTDAC) */ + i2s_zero_dma_buffer(I2S_NUM_0); + + audio_callback = NULL; + + return 0; +} + +static void osd_stopsound(void) +{ + audio_callback = NULL; +} + +static void do_audio_frame() +{ + int left = DEFAULT_SAMPLERATE / NES_REFRESH_RATE; + while (left) { + int n = DEFAULT_FRAGSIZE; + if (n > left) + n = left; + audio_callback(audio_frame, n); //get more data + + //16 bit mono -> 32-bit (16 bit r+l) + int16_t *mono_ptr = audio_frame + n; + int16_t *stereo_ptr = audio_frame + n + n; + int i = n; + while (i--) { +#if defined(HW_AUDIO_EXTDAC) + int16_t a = (*(--mono_ptr) >> 2); + *(--stereo_ptr) = a; + *(--stereo_ptr) = a; +#else /* !defined(HW_AUDIO_EXTDAC) */ + int16_t a = (*(--mono_ptr) >> 3); + *(--stereo_ptr) = 0x8000 + a; + *(--stereo_ptr) = 0x8000 - a; +#endif /* !defined(HW_AUDIO_EXTDAC) */ + } + + size_t i2s_bytes_write; + i2s_write(I2S_NUM_0, (const char *)audio_frame, 4 * n, &i2s_bytes_write, portMAX_DELAY); + left -= i2s_bytes_write / 4; + } +} + +void osd_setsound(void (*playfunc)(void *buffer, int length)) +{ + //Indicates we should call playfunc() to get more data. + audio_callback = playfunc; +} + +#else /* !defined(HW_AUDIO) */ + +static int osd_init_sound(void) +{ + return 0; +} + +static void osd_stopsound(void) +{ +} + +static void do_audio_frame() +{ +} + +void osd_setsound(void (*playfunc)(void *buffer, int length)) +{ +} + +#endif /* !defined(HW_AUDIO) */ + +/* video */ +extern void display_init(); +extern void display_write_frame(const uint8_t *data[]); +extern void display_clear(); + +//This runs on core 0. +QueueHandle_t vidQueue; +static void videoTask(void *arg) +{ + bitmap_t *bmp = NULL; + while (1) { + // xQueueReceive(vidQueue, &bmp, portMAX_DELAY); //skip one frame to drop to 30 + xQueueReceive(vidQueue, &bmp, portMAX_DELAY); + display_write_frame((const uint8_t **)bmp->line); + } +} + +/* get info */ +static char fb[1]; //dummy +bitmap_t *myBitmap; + +/* initialise video */ +static int init(int width, int height) +{ + return 0; +} + +static void shutdown(void) +{ +} + +/* set a video mode */ +static int set_mode(int width, int height) +{ + return 0; +} + +/* copy nes palette over to hardware */ +uint16 myPalette[256]; +static void set_palette(rgb_t *pal) +{ + uint16 c; + + int i; + + for (i = 0; i < 256; i++) { + c = (pal[i].b >> 3) + ((pal[i].g >> 2) << 5) + ((pal[i].r >> 3) << 11); + //myPalette[i]=(c>>8)|((c&0xff)<<8); + myPalette[i] = c; + } +} + +/* clear all frames to a particular color */ +static void clear(uint8 color) +{ + // SDL_FillRect(mySurface, 0, color); + display_clear(); +} + +/* acquire the directbuffer for writing */ +static bitmap_t *lock_write(void) +{ + // SDL_LockSurface(mySurface); + myBitmap = bmp_createhw((uint8 *)fb, NES_SCREEN_WIDTH, NES_SCREEN_HEIGHT, NES_SCREEN_WIDTH * 2); + return myBitmap; +} + +/* release the resource */ +static void free_write(int num_dirties, rect_t *dirty_rects) +{ + bmp_destroy(&myBitmap); +} + +static void custom_blit(bitmap_t *bmp, int num_dirties, rect_t *dirty_rects) +{ + xQueueSend(vidQueue, &bmp, 0); + do_audio_frame(); +} + +viddriver_t sdlDriver = { + "Simple DirectMedia Layer", /* name */ + init, /* init */ + shutdown, /* shutdown */ + set_mode, /* set_mode */ + set_palette, /* set_palette */ + clear, /* clear */ + lock_write, /* lock_write */ + free_write, /* free_write */ + custom_blit, /* custom_blit */ + false /* invalidate flag */ +}; + +void osd_getvideoinfo(vidinfo_t *info) +{ + info->default_width = NES_SCREEN_WIDTH; + info->default_height = NES_SCREEN_HEIGHT; + info->driver = &sdlDriver; +} + +void osd_getsoundinfo(sndinfo_t *info) +{ + info->sample_rate = DEFAULT_SAMPLERATE; + info->bps = 16; +} + +/* input */ +extern void controller_init(); +extern uint32_t controller_read_input(); + +static void osd_initinput() +{ + controller_init(); +} + +static void osd_freeinput(void) +{ +} + +void osd_getinput(void) +{ + const int ev[32] = { + event_joypad1_up, event_joypad1_down, event_joypad1_left, event_joypad1_right, + event_joypad1_select, event_joypad1_start, event_joypad1_a, event_joypad1_b, + event_state_save, event_state_load, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0 + }; + static int oldb = 0xffff; + uint32_t b = controller_read_input(); + uint32_t chg = b ^ oldb; + int x; + oldb = b; + event_t evh; + // nofrendo_log_printf("Input: %x\n", b); + for (x = 0; x < 16; x++) { + if (chg & 1) { + evh = event_get(ev[x]); + if (evh) + evh((b & 1) ? INP_STATE_BREAK : INP_STATE_MAKE); + } + chg >>= 1; + b >>= 1; + } +} + +void osd_getmouse(int *x, int *y, int *button) +{ +} + +/* init / shutdown */ +static int logprint(const char *string) +{ + return printf("%s", string); +} + +int osd_init() +{ + // nofrendo_log_chain_logfunc(logprint); + + if (osd_init_sound()) + return -1; + + display_init(); + vidQueue = xQueueCreate(1, sizeof(bitmap_t *)); + xTaskCreatePinnedToCore(&videoTask, "videoTask", 2048, NULL, 0, NULL, 0); + osd_initinput(); + return 0; +} + +void osd_shutdown() +{ + osd_stopsound(); + osd_freeinput(); +} + +char configfilename[] = "na"; +int osd_main(int argc, char *argv[]) +{ + config.filename = configfilename; + + return main_loop(argv[0], system_autodetect); +} + +//Seemingly, this will be called only once. Should call func with a freq of frequency, +int osd_installtimer(int frequency, void *func, int funcsize, void *counter, int countersize) +{ + nofrendo_log_printf("Timer install, configTICK_RATE_HZ=%d, freq=%d\n", configTICK_RATE_HZ, frequency); + timer = xTimerCreate("nes", configTICK_RATE_HZ / frequency, pdTRUE, NULL, func); + xTimerStart(timer, 0); + return 0; +} + +/* filename manipulation */ +void osd_fullname(char *fullname, const char *shortname) +{ + strncpy(fullname, shortname, PATH_MAX); +} + +/* This gives filenames for storage of saves */ +char *osd_newextension(char *string, char *ext) +{ + // dirty: assume both extensions is 3 characters + size_t l = strlen(string); + string[l - 3] = ext[1]; + string[l - 2] = ext[2]; + string[l - 1] = ext[3]; + + return string; +} + +/* This gives filenames for storage of PCX snapshots */ +int osd_makesnapname(char *filename, int len) +{ + return -1; +}