diff --git a/CMakeLists.txt b/CMakeLists.txt index e3f3236..b3b222d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,24 +1,24 @@ cmake_minimum_required(VERSION 3.13...3.23) -# initialize the SDK based on PICO_SDK_PATH include(pico_sdk_import.cmake) -project(RP2040_GB C CXX) +project(RP2040_GB C CXX ASM) +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) -# Initialize the Raspberry Pi Pico SDK pico_sdk_init() -add_executable(RP2040_GB src/main.c src/rom.c src/mk_ili9225.c) -target_include_directories(RP2040_GB PRIVATE inc) +add_executable(RP2040_GB + src/main.c + src/rom.c + src/mk_ili9225.c + ext/minigb_apu/minigb_apu.c + ext/i2s/i2s.c +) -SET(ENABLE_SOUND FALSE CACHE BOOL "Enable Sound emulation") -if(ENABLE_SOUND) - ADD_COMPILE_DEFINITIONS(ENABLE_SOUND=1) - target_sources(RP2040_GB PRIVATE ext/minigb_apu/minigb_apu.c) - target_include_directories(RP2040_GB PRIVATE ext/minigb_apu) -else() - ADD_COMPILE_DEFINITIONS(ENABLE_SOUND=0) -endif() +pico_generate_pio_header(RP2040_GB ${CMAKE_CURRENT_LIST_DIR}/ext/i2s/audio_i2s.pio) + +target_include_directories(RP2040_GB PRIVATE inc ext/minigb_apu ext/i2s) target_link_libraries(RP2040_GB pico_stdlib pico_stdio pico_bootrom pico_multicore pico_stdio pico_multicore diff --git a/README.md b/README.md index 1bc4867..0e9b813 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,16 @@ This fork includes all changes needed for the emulator to run on the [Pico-GB em RP2040-GB is a Game Boy (DMG) emulator [Peanut-GB](https://github.com/deltabeard/Peanut-GB) on the Raspberry Pi RP2040 microcontroller, using an ILI9225 screen. Runs at more than 70 fps without audio emulation. With frame skip and interlacing, can run at up to 120 fps. +# What you need +* Raspberry Pi Pico (1x) +* 2.2inch ILI9225 176×220 LCD Display Module (1x) +* MAX98357A amplifier (1x) +* 2W 8ohms speaker (1x) +* Micro Push Button Switch, Momentary Tactile Tact Touch, 6x6x6 mm, 4 pins (8x) +* Solderable Breadboard (1x) +* Dupont Wires Assorted Kit (Male to Female + Male to Male + Female to Female) +* Preformed Breadboard Jumper Wires + # Raspberry Pi Pico Pins Assignment * UP = GP2 * DOWN = GP3 @@ -16,13 +26,15 @@ Runs at more than 70 fps without audio emulation. With frame skip and interlacin * BUTTON B = GP7 * SELECT = GP8 * START = GP9 -* BUZZER = GP15 * LCD CS = GP17 * LCD CLK = GP18 * LCD SDI = GP19 * LCD RS = GP20 * LCD RST = GP21 * LCD LED = GP22 +* MAX98357A DIN = GP26 +* MAX98357A BCLK = GP27 +* MAX98357A LRC = GP28 # Installing diff --git a/ext/i2s/audio_i2s.pio b/ext/i2s/audio_i2s.pio new file mode 100644 index 0000000..1c306f4 --- /dev/null +++ b/ext/i2s/audio_i2s.pio @@ -0,0 +1,63 @@ +; +; Copyright (c) 2020 Raspberry Pi (Trading) Ltd. +; +; SPDX-License-Identifier: BSD-3-Clause +; + +; Transmit a mono or stereo I2S audio stream as stereo +; This is 16 bits per sample; can be altered by modifying the "set" params, +; or made programmable by replacing "set x" with "mov x, y" and using Y as a config register. +; +; Autopull must be enabled, with threshold set to 32. +; Since I2S is MSB-first, shift direction should be to left. +; Hence the format of the FIFO word is: +; +; | 31 : 16 | 15 : 0 | +; | sample ws=0 | sample ws=1 | +; +; Data is output at 1 bit per clock. Use clock divider to adjust frequency. +; Fractional divider will probably be needed to get correct bit clock period, +; but for common syslck freqs this should still give a constant word select period. +; +; One output pin is used for the data output. +; Two side-set pins are used. Bit 0 is clock, bit 1 is word select. + +; Send 16 bit words to the PIO for mono, 32 bit words for stereo + +.program audio_i2s +.side_set 2 + + ; /--- LRCLK + ; |/-- BCLK +bitloop1: ; || + out pins, 1 side 0b10 + jmp x-- bitloop1 side 0b11 + out pins, 1 side 0b00 + set x, 14 side 0b01 + +bitloop0: + out pins, 1 side 0b00 + jmp x-- bitloop0 side 0b01 + out pins, 1 side 0b10 +public entry_point: + set x, 14 side 0b11 + +% c-sdk { + +static inline void audio_i2s_program_init(PIO pio, uint sm, uint offset, uint data_pin, uint clock_pin_base) { + pio_sm_config sm_config = audio_i2s_program_get_default_config(offset); + + sm_config_set_out_pins(&sm_config, data_pin, 1); + sm_config_set_sideset_pins(&sm_config, clock_pin_base); + sm_config_set_out_shift(&sm_config, false, true, 32); + + pio_sm_init(pio, sm, offset, &sm_config); + + uint pin_mask = (1u << data_pin) | (3u << clock_pin_base); + pio_sm_set_pindirs_with_mask(pio, sm, pin_mask, pin_mask); + pio_sm_set_pins(pio, sm, 0); // clear pins + + pio_sm_exec(pio, sm, pio_encode_jmp(offset + audio_i2s_offset_entry_point)); +} + +%} \ No newline at end of file diff --git a/ext/i2s/i2s.c b/ext/i2s/i2s.c new file mode 100644 index 0000000..5bded89 --- /dev/null +++ b/ext/i2s/i2s.c @@ -0,0 +1,100 @@ +/** + * MIT License + * + * Copyright (c) 2022 Vincent Mistler + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include "i2s.h" + +i2s_config_t i2s_get_default_config(void) { + i2s_config_t i2s_config = { + .sample_freq = 44100, + .channel_count = 2, + .data_pin = 26, + .clock_pin_base = 27, + .pio = pio0, + .sm = 0, + .dma_channel = 0, + .dma_buf = NULL, + .dma_trans_count = 0, + }; + + return i2s_config; +} + +void i2s_init(i2s_config_t *i2s_config) { + uint8_t func=GPIO_FUNC_PIO0; // TODO: GPIO_FUNC_PIO0 for pio0 or GPIO_FUNC_PIO1 for pio1 + gpio_set_function(i2s_config->data_pin, GPIO_FUNC_PIO0); + gpio_set_function(i2s_config->clock_pin_base, GPIO_FUNC_PIO0); + gpio_set_function(i2s_config->clock_pin_base+1, GPIO_FUNC_PIO0); + + i2s_config->sm = pio_claim_unused_sm(i2s_config->pio, true); + + uint offset = pio_add_program(i2s_config->pio, &audio_i2s_program); + + audio_i2s_program_init(i2s_config->pio, i2s_config->sm , offset, i2s_config->data_pin , i2s_config->clock_pin_base); + + // Set PIO clock + uint32_t system_clock_frequency = clock_get_hz(clk_sys); + uint32_t divider = system_clock_frequency * 4 / i2s_config->sample_freq; // avoid arithmetic overflow + pio_sm_set_clkdiv_int_frac(i2s_config->pio, i2s_config->sm , divider >> 8u, divider & 0xffu); + + pio_sm_set_enabled(i2s_config->pio, i2s_config->sm, false); + + // Allocate memory for the DMA buffer + i2s_config->dma_buf=malloc(i2s_config->dma_trans_count*sizeof(uint32_t)); + + // Direct Memory Access setup + i2s_config->dma_channel = dma_claim_unused_channel(true); + + dma_channel_config dma_config = dma_channel_get_default_config(i2s_config->dma_channel); + channel_config_set_read_increment(&dma_config, true); + channel_config_set_write_increment(&dma_config, false); + channel_config_set_dreq(&dma_config, pio_get_dreq(i2s_config->pio, i2s_config->sm, true)); + channel_config_set_transfer_data_size(&dma_config, DMA_SIZE_32); + dma_channel_configure(i2s_config->dma_channel, + &dma_config, + &(i2s_config->pio->txf[i2s_config->sm]), // Destination pointer + i2s_config->dma_buf, // Source pointer + i2s_config->dma_trans_count, // Number of 32 bits words to transfer + false // Start immediately + ); + + pio_sm_set_enabled(i2s_config->pio, i2s_config->sm , true); +} + +void i2s_write(const i2s_config_t *i2s_config,const int16_t *samples,const size_t len) { + for(size_t i=0;ipio, i2s_config->sm, (uint32_t)samples[i]); + } +} + +// Write samples to DMA buffer and initiate DMA transfer (non blocking) +void i2s_dma_write(i2s_config_t *i2s_config,const int16_t *samples) { + // Wait the completion of the previous DMA transfer + dma_channel_wait_for_finish_blocking(i2s_config->dma_channel); + // Copy samples into the DMA buffer + memcpy(i2s_config->dma_buf,samples,i2s_config->dma_trans_count*sizeof(int32_t)); + // Initiate the DMA transfer + dma_channel_transfer_from_buffer_now(i2s_config->dma_channel, + i2s_config->dma_buf, + i2s_config->dma_trans_count); +} diff --git a/ext/i2s/i2s.h b/ext/i2s/i2s.h new file mode 100644 index 0000000..618ce11 --- /dev/null +++ b/ext/i2s/i2s.h @@ -0,0 +1,52 @@ +/** + * MIT License + * + * Copyright (c) 2022 Vincent Mistler + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include "audio_i2s.pio.h" + +typedef struct i2s_config +{ + uint32_t sample_freq; + uint16_t channel_count; + uint8_t data_pin; + uint8_t clock_pin_base; + PIO pio; + uint8_t sm; + uint8_t dma_channel; + uint16_t dma_trans_count; + uint16_t *dma_buf; + +} i2s_config_t; + + +i2s_config_t i2s_get_default_config(void); +void i2s_init(i2s_config_t *i2s_config); +void i2s_write(const i2s_config_t *i2s_config,const int16_t *samples,const size_t len); +void i2s_dma_write(i2s_config_t *i2s_config,const int16_t *samples); \ No newline at end of file diff --git a/ext/minigb_apu/minigb_apu.c b/ext/minigb_apu/minigb_apu.c index 74eb7d0..0167b53 100644 --- a/ext/minigb_apu/minigb_apu.c +++ b/ext/minigb_apu/minigb_apu.c @@ -365,19 +365,17 @@ static void update_noise(int16_t *samples) /** * SDL2 style audio callback function. */ -void audio_callback(void *userdata, uint8_t *stream, int len) +void audio_callback(void *userdata, int16_t *stream, size_t len) { - int16_t *samples = (int16_t *)stream; - /* Appease unused variable warning. */ (void)userdata; memset(stream, 0, len); - update_square(samples, 0); - update_square(samples, 1); - update_wave(samples); - update_noise(samples); + update_square(stream, 0); + update_square(stream, 1); + update_wave(stream); + update_noise(stream); } static void chan_trigger(uint_fast8_t i) diff --git a/ext/minigb_apu/minigb_apu.h b/ext/minigb_apu/minigb_apu.h index 1ab92d4..6847d54 100644 --- a/ext/minigb_apu/minigb_apu.h +++ b/ext/minigb_apu/minigb_apu.h @@ -9,19 +9,20 @@ #include -#define AUDIO_SAMPLE_RATE 32768 +#define AUDIO_SAMPLE_RATE 44100 #define DMG_CLOCK_FREQ 4194304.0 #define SCREEN_REFRESH_CYCLES 70224.0 #define VERTICAL_SYNC (DMG_CLOCK_FREQ/SCREEN_REFRESH_CYCLES) #define AUDIO_SAMPLES ((unsigned)(AUDIO_SAMPLE_RATE / VERTICAL_SYNC)) +#define AUDIO_BUFFER_SIZE_BYTES (AUDIO_SAMPLES*4) /** * Fill allocated buffer "data" with "len" number of 32-bit floating point * samples (native endian order) in stereo interleaved format. */ -void audio_callback(void *ptr, uint8_t *data, int len); +void audio_callback(void *ptr, int16_t *data, size_t len); /** * Read audio register at given address "addr". diff --git a/src/main.c b/src/main.c index dfe9198..917eb6d 100644 --- a/src/main.c +++ b/src/main.c @@ -13,11 +13,11 @@ * PERFORMANCE OF THIS SOFTWARE. */ +// Peanut-GB emulator settings #define ENABLE_LCD 1 - -#ifndef ENABLE_SOUND -# define ENABLE_SOUND 0 -#endif +#define ENABLE_SOUND 1 +#define PEANUT_GB_HIGH_LCD_ACCURACY 1 +#define PEANUT_GB_USE_BIOS 0 /* Use DMA for all drawing to LCD. Benefits aren't fully realised at the moment * due to busy loops waiting for DMA completion. */ @@ -40,6 +40,7 @@ #include /* RP2040 Headers */ +#include #include #include #include @@ -55,8 +56,10 @@ /* Project headers */ #include "hedley.h" +#include "minigb_apu.h" #include "peanut_gb.h" #include "mk_ili9225.h" +#include "i2s.h" /* GPIO Connections. */ #define GPIO_UP 2 @@ -75,6 +78,15 @@ #define GPIO_RST 21 #define GPIO_LED 22 +#if ENABLE_SOUND + // Global variables for audio task + // stream contains N=AUDIO_SAMPLES samples + // each sample is 32 bits + // (16 bits for the left channel + 16 bits for the right channel in stereo interleaved format) + // This is intended to be played at AUDIO_SAMPLE_RATE Hz + uint16_t *stream; +#endif + /* DMA channel for LCD communication. */ static uint dma_lcd; /* Definition of ROM data variable. Must be declared like: @@ -206,6 +218,7 @@ void core1_irq_dma_lcd_end_transfer(void) dma_channel_acknowledge_irq0(dma_lcd); } +#if ENABLE_LCD void core1_lcd_draw_line(const uint_fast8_t line) { const uint16_t palette[3][4] = { @@ -317,6 +330,7 @@ void main_core1(void) HEDLEY_UNREACHABLE(); } +#endif #if ENABLE_LCD void lcd_draw_line(struct gb_s *gb, const uint8_t pixels[LCD_WIDTH], @@ -422,8 +436,10 @@ int main(void) spi_set_format(spi0, 16, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST); /* Start Core1, which processes requests to the LCD. */ +#if ENABLE_LCD putstdio("CORE1 "); multicore_launch_core1(main_core1); +#endif /* Initialise GB context. */ memcpy(rom_bank0, rom, sizeof(rom_bank0)); @@ -469,7 +485,20 @@ int main(void) //gb.direct.interlace = 1; #endif #if ENABLE_SOUND + // Allocate memory for the stream buffer + stream=malloc(AUDIO_BUFFER_SIZE_BYTES); + assert(stream!=NULL); + memset(stream,0,AUDIO_BUFFER_SIZE_BYTES); // Zero out the stream buffer + + // Initialize I2S sound driver + i2s_config_t i2s_config = i2s_get_default_config(); + i2s_config.sample_freq=AUDIO_SAMPLE_RATE; + i2s_config.dma_trans_count =AUDIO_SAMPLES; + i2s_init(&i2s_config); + + // Initialize audio emulation audio_init(); + putstdio("AUDIO "); #endif @@ -479,9 +508,6 @@ int main(void) while(1) { int input; -#if ENABLE_SOUND - static uint16_t stream[1098]; -#endif gb.gb_frame = 0; @@ -492,7 +518,9 @@ int main(void) frames++; #if ENABLE_SOUND - audio_callback(NULL, stream, 1098); + audio_callback(NULL, stream, AUDIO_BUFFER_SIZE_BYTES); + // i2s_write(&i2s_config,stream,AUDIO_SAMPLES); + i2s_dma_write(&i2s_config, stream); #endif /* Update buttons state */