Output I2S audio with PIO/DMA

Requires a MAX98357A amplifier and a 2W 8ohms speaker
This commit is contained in:
Vincent Mistler
2022-12-02 17:24:39 +00:00
parent a9f54dc37d
commit 29a04ad832
8 changed files with 285 additions and 31 deletions
+13 -13
View File
@@ -1,24 +1,24 @@
cmake_minimum_required(VERSION 3.13...3.23) cmake_minimum_required(VERSION 3.13...3.23)
# initialize the SDK based on PICO_SDK_PATH
include(pico_sdk_import.cmake) 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() pico_sdk_init()
add_executable(RP2040_GB src/main.c src/rom.c src/mk_ili9225.c) add_executable(RP2040_GB
target_include_directories(RP2040_GB PRIVATE inc) 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") pico_generate_pio_header(RP2040_GB ${CMAKE_CURRENT_LIST_DIR}/ext/i2s/audio_i2s.pio)
if(ENABLE_SOUND)
ADD_COMPILE_DEFINITIONS(ENABLE_SOUND=1) target_include_directories(RP2040_GB PRIVATE inc ext/minigb_apu ext/i2s)
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()
target_link_libraries(RP2040_GB target_link_libraries(RP2040_GB
pico_stdlib pico_stdio pico_bootrom pico_multicore pico_stdio pico_multicore pico_stdlib pico_stdio pico_bootrom pico_multicore pico_stdio pico_multicore
+13 -1
View File
@@ -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. 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. 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 # Raspberry Pi Pico Pins Assignment
* UP = GP2 * UP = GP2
* DOWN = GP3 * DOWN = GP3
@@ -16,13 +26,15 @@ Runs at more than 70 fps without audio emulation. With frame skip and interlacin
* BUTTON B = GP7 * BUTTON B = GP7
* SELECT = GP8 * SELECT = GP8
* START = GP9 * START = GP9
* BUZZER = GP15
* LCD CS = GP17 * LCD CS = GP17
* LCD CLK = GP18 * LCD CLK = GP18
* LCD SDI = GP19 * LCD SDI = GP19
* LCD RS = GP20 * LCD RS = GP20
* LCD RST = GP21 * LCD RST = GP21
* LCD LED = GP22 * LCD LED = GP22
* MAX98357A DIN = GP26
* MAX98357A BCLK = GP27
* MAX98357A LRC = GP28
# Installing # Installing
+63
View File
@@ -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));
}
%}
+100
View File
@@ -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;i<len;i++) {
pio_sm_put_blocking(i2s_config->pio, 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);
}
+52
View File
@@ -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 <stdlib.h>
#include <string.h>
#include <hardware/pio.h>
#include <hardware/clocks.h>
#include <hardware/dma.h>
#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);
+5 -7
View File
@@ -365,19 +365,17 @@ static void update_noise(int16_t *samples)
/** /**
* SDL2 style audio callback function. * 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. */ /* Appease unused variable warning. */
(void)userdata; (void)userdata;
memset(stream, 0, len); memset(stream, 0, len);
update_square(samples, 0); update_square(stream, 0);
update_square(samples, 1); update_square(stream, 1);
update_wave(samples); update_wave(stream);
update_noise(samples); update_noise(stream);
} }
static void chan_trigger(uint_fast8_t i) static void chan_trigger(uint_fast8_t i)
+3 -2
View File
@@ -9,19 +9,20 @@
#include <stdint.h> #include <stdint.h>
#define AUDIO_SAMPLE_RATE 32768 #define AUDIO_SAMPLE_RATE 44100
#define DMG_CLOCK_FREQ 4194304.0 #define DMG_CLOCK_FREQ 4194304.0
#define SCREEN_REFRESH_CYCLES 70224.0 #define SCREEN_REFRESH_CYCLES 70224.0
#define VERTICAL_SYNC (DMG_CLOCK_FREQ/SCREEN_REFRESH_CYCLES) #define VERTICAL_SYNC (DMG_CLOCK_FREQ/SCREEN_REFRESH_CYCLES)
#define AUDIO_SAMPLES ((unsigned)(AUDIO_SAMPLE_RATE / VERTICAL_SYNC)) #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 * Fill allocated buffer "data" with "len" number of 32-bit floating point
* samples (native endian order) in stereo interleaved format. * 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". * Read audio register at given address "addr".
+36 -8
View File
@@ -13,11 +13,11 @@
* PERFORMANCE OF THIS SOFTWARE. * PERFORMANCE OF THIS SOFTWARE.
*/ */
// Peanut-GB emulator settings
#define ENABLE_LCD 1 #define ENABLE_LCD 1
#define ENABLE_SOUND 1
#ifndef ENABLE_SOUND #define PEANUT_GB_HIGH_LCD_ACCURACY 1
# define ENABLE_SOUND 0 #define PEANUT_GB_USE_BIOS 0
#endif
/* Use DMA for all drawing to LCD. Benefits aren't fully realised at the moment /* Use DMA for all drawing to LCD. Benefits aren't fully realised at the moment
* due to busy loops waiting for DMA completion. */ * due to busy loops waiting for DMA completion. */
@@ -40,6 +40,7 @@
#include <string.h> #include <string.h>
/* RP2040 Headers */ /* RP2040 Headers */
#include <hardware/pio.h>
#include <hardware/clocks.h> #include <hardware/clocks.h>
#include <hardware/dma.h> #include <hardware/dma.h>
#include <hardware/spi.h> #include <hardware/spi.h>
@@ -55,8 +56,10 @@
/* Project headers */ /* Project headers */
#include "hedley.h" #include "hedley.h"
#include "minigb_apu.h"
#include "peanut_gb.h" #include "peanut_gb.h"
#include "mk_ili9225.h" #include "mk_ili9225.h"
#include "i2s.h"
/* GPIO Connections. */ /* GPIO Connections. */
#define GPIO_UP 2 #define GPIO_UP 2
@@ -75,6 +78,15 @@
#define GPIO_RST 21 #define GPIO_RST 21
#define GPIO_LED 22 #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. */ /* DMA channel for LCD communication. */
static uint dma_lcd; static uint dma_lcd;
/* Definition of ROM data variable. Must be declared like: /* 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); dma_channel_acknowledge_irq0(dma_lcd);
} }
#if ENABLE_LCD
void core1_lcd_draw_line(const uint_fast8_t line) void core1_lcd_draw_line(const uint_fast8_t line)
{ {
const uint16_t palette[3][4] = { const uint16_t palette[3][4] = {
@@ -317,6 +330,7 @@ void main_core1(void)
HEDLEY_UNREACHABLE(); HEDLEY_UNREACHABLE();
} }
#endif
#if ENABLE_LCD #if ENABLE_LCD
void lcd_draw_line(struct gb_s *gb, const uint8_t pixels[LCD_WIDTH], 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); spi_set_format(spi0, 16, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST);
/* Start Core1, which processes requests to the LCD. */ /* Start Core1, which processes requests to the LCD. */
#if ENABLE_LCD
putstdio("CORE1 "); putstdio("CORE1 ");
multicore_launch_core1(main_core1); multicore_launch_core1(main_core1);
#endif
/* Initialise GB context. */ /* Initialise GB context. */
memcpy(rom_bank0, rom, sizeof(rom_bank0)); memcpy(rom_bank0, rom, sizeof(rom_bank0));
@@ -469,7 +485,20 @@ int main(void)
//gb.direct.interlace = 1; //gb.direct.interlace = 1;
#endif #endif
#if ENABLE_SOUND #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(); audio_init();
putstdio("AUDIO "); putstdio("AUDIO ");
#endif #endif
@@ -479,9 +508,6 @@ int main(void)
while(1) while(1)
{ {
int input; int input;
#if ENABLE_SOUND
static uint16_t stream[1098];
#endif
gb.gb_frame = 0; gb.gb_frame = 0;
@@ -492,7 +518,9 @@ int main(void)
frames++; frames++;
#if ENABLE_SOUND #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 #endif
/* Update buttons state */ /* Update buttons state */