Skip to content

Added I2S (native) output support for RP2040 #163

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion AudioConfigRP2040.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,46 @@
#error This header should be included for RP2040, only
#endif


// AUDIO output modes
#define PWM_VIA_BARE_CHIP 1 // output using one of the gpio of the board
#define EXTERNAL_DAC_VIA_I2S 2 // output via external DAC connected to I2S (PT8211 or similar)

//******* BEGIN: These are the defines you may want to change. Best not to touch anything outside this range. ************/
#define RP2040_AUDIO_OUT_MODE PWM_VIA_BARE_CHIP
//******* END: These are the defines you may want to change. Best not to touch anything outside this range. ************/


#if (RP2040_AUDIO_OUT_MODE == PWM_VIA_BARE_CHIP)
#define AUDIO_CHANNEL_1_PIN 0
#if (AUDIO_CHANNELS > 1)
// Audio channel pins for stereo or HIFI must be on the same PWM slice (which is the case for the pairs (0,1), (2,3), (4,5), etc.
# define AUDIO_CHANNEL_2_PIN 1
#define AUDIO_CHANNEL_2_PIN 1
#endif

// The more audio bits you use, the slower the carrier frequency of the PWM signal. 11 bits yields ~ 60kHz on a 133Mhz CPU (which appears to be a reasonable compromise)
#define AUDIO_BITS 11
#endif

#if (RP2040_AUDIO_OUT_MODE == EXTERNAL_DAC_VIA_I2S)
// ****** BEGIN: These are define you may want to change. Best not to touch anything outside this range. ************/
#define BCLK_PIN 20
#define WS_PIN (pBCLK+1) // CANNOT BE CHANGED, HAS TO BE NEXT TO pBCLK
#define DOUT_PIN 22
#define LSBJ_FORMAT false // some DAC, like the PT8211, use a variant of I2S data format called LSBJ
// set this to true to use this kind of DAC or false for plain I2S.
#define AUDIO_BITS 16 // available values are 8, 16, 24 (LEFT ALIGN in 32 bits type!!) and 32 bits
// ****** END: These are define you may want to change. Best not to touch anything outside this range. ************/

#define BYPASS_MOZZI_OUTPUT_BUFFER true

// Configuration of the I2S port, especially DMA. Set in stone here as default of the library when this was written.
// Probably do not change if you are not sure of what you are doing
#define BUFFERS 8 // number of DMA buffers used
#define BUFFER_SIZE 256 // total size of the buffer, in samples
#endif


#define AUDIO_BITS_PER_CHANNEL AUDIO_BITS

#define AUDIO_BIAS ((uint16_t) 1<<(AUDIO_BITS-1))
Expand Down
81 changes: 77 additions & 4 deletions MozziGuts_impl_RP2040.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ void setupMozziADC(int8_t speed) {
);

uint dma_chan = dma_claim_unused_channel(true);
rp2040_adc_dma_chan = dma_chan;
static dma_channel_config cfg = dma_channel_get_default_config(dma_chan);

// Reading from constant address, writing to incrementing byte addresses
Expand All @@ -102,12 +103,14 @@ void setupMozziADC(int8_t speed) {

// we want notification, when a sample has arrived
dma_channel_set_irq0_enabled(dma_chan, true);
irq_set_exclusive_handler(DMA_IRQ_0, rp2040_adc_queue_handler);
irq_add_shared_handler(DMA_IRQ_0, rp2040_adc_queue_handler, PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY);
irq_set_enabled(DMA_IRQ_0, true);
dma_channel_start(dma_chan);
}

void rp2040_adc_queue_handler() {
dma_hw->ints0 = 1u << rp2040_adc_dma_chan; // clear interrupt flag
if (!dma_channel_get_irq0_status(rp2040_adc_dma_chan)) return; // shared handler may get called on unrelated events
dma_channel_acknowledge_irq0(rp2040_adc_dma_chan); // clear interrupt flag
//adc_run(false); // adc not running continuous
//adc_fifo_drain(); // no need to drain fifo, the dma transfer did that
dma_channel_set_trans_count(rp2040_adc_dma_chan, 1, true); // set up for another read
Expand All @@ -119,16 +122,17 @@ void rp2040_adc_queue_handler() {
////// BEGIN audio output code //////
#define LOOP_YIELD tight_loop_contents(); // apparently needed, among other things, to service the alarm pool

#include <hardware/pwm.h>

#if (RP2040_AUDIO_OUT_MODE == PWM_VIA_BARE_CHIP) || (EXTERNAL_AUDIO_OUTPUT == true)
#include <hardware/pwm.h>
#if (EXTERNAL_AUDIO_OUTPUT != true) // otherwise, the last stage - audioOutput() - will be provided by the user
inline void audioOutput(const AudioOutput f) {
pwm_set_gpio_level(AUDIO_CHANNEL_1_PIN, f.l()+AUDIO_BIAS);
#if (AUDIO_CHANNELS > 1)
pwm_set_gpio_level(AUDIO_CHANNEL_2_PIN, f.r()+AUDIO_BIAS);
#endif
}
#endif
#endif // #if (EXTERNAL_AUDIO_OUTPUT != true)

#include <pico/time.h>
/** Implementation notes:
Expand All @@ -150,8 +154,56 @@ void audioOutputCallback(uint) {
// NOTE: hardware_alarm_set_target returns true, if the target was already missed. In that case, keep pushing samples, until we have caught up.
} while (hardware_alarm_set_target(audio_update_alarm_num, next_audio_update));
}

#elif (RP2040_AUDIO_OUT_MODE == EXTERNAL_DAC_VIA_I2S)
#include <I2S.h>
I2S i2s(OUTPUT);


inline bool canBufferAudioOutput() {
return (i2s.availableForWrite());
}

inline void audioOutput(const AudioOutput f) {

#if (AUDIO_BITS == 8)
#if (AUDIO_CHANNELS > 1)
i2s.write8(f.l(), f.r());
#else
i2s.write8(f.l(), 0);
#endif

#elif (AUDIO_BITS == 16)
#if (AUDIO_CHANNELS > 1)
i2s.write16(f.l(), f.r());
#else
i2s.write16(f.l(), 0);
#endif

#elif (AUDIO_BITS == 24)
#if (AUDIO_CHANNELS > 1)
i2s.write24(f.l(), f.r());
#else
i2s.write24(f.l(), 0);
#endif

#elif (AUDIO_BITS == 32)
#if (AUDIO_CHANNELS > 1)
i2s.write32(f.l(), f.r());
#else
i2s.write32(f.l(), 0);
#endif
#else
#error The number of AUDIO_BITS set in AudioConfigRP2040.h is incorrect
#endif


}
#endif


static void startAudio() {
#if (RP2040_AUDIO_OUT_MODE == PWM_VIA_BARE_CHIP) || (EXTERNAL_AUDIO_OUTPUT == true) // EXTERNAL AUDIO needs the timers set here
#if (EXTERNAL_AUDIO_OUTPUT != true)
// calling analogWrite for the first time will try to init the pwm frequency and range on all pins. We don't want that happening after we've set up our own,
// so we start off with a dummy call to analogWrite:
Expand Down Expand Up @@ -185,9 +237,30 @@ static void startAudio() {
next_audio_update = make_timeout_time_us(micros_per_update);
// See audioOutputCallback(), above. In _theory_ some interrupt stuff might delay us, here, causing us to miss the first beat (and everything that follows)
} while (hardware_alarm_set_target(audio_update_alarm_num, next_audio_update));

#elif (RP2040_AUDIO_OUT_MODE == EXTERNAL_DAC_VIA_I2S)
i2s.setBCLK(BCLK_PIN);
i2s.setDATA(DOUT_PIN);
i2s.setBitsPerSample(AUDIO_BITS);

#if (AUDIO_BITS > 16)
i2s.setBuffers(BUFFERS, (size_t) (BUFFER_SIZE/BUFFERS), 0);
#else
i2s.setBuffers(BUFFERS, (size_t) (BUFFER_SIZE/BUFFERS/2), 0);
#endif
#if (LSBJ_FORMAT == true)
i2s.setLSBJFormat();
#endif
i2s.begin(AUDIO_RATE);
#endif
}

void stopMozzi() {
#if (RP2040_AUDIO_OUT_MODE == PWM_VIA_BARE_CHIP) || (EXTERNAL_AUDIO_OUTPUT == true)
hardware_alarm_set_callback(audio_update_alarm_num, NULL);
#elif (RP2040_AUDIO_OUT_MODE == EXTERNAL_DAC_VIA_I2S)
i2s.end();
#endif

}
////// END audio output code //////
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,16 @@ on the RP2040 SDK API. Tested on a Pi Pico.

- This is a recent addition, implementation details may still change (currently just PWM driven by a timer; this may be worth changing to a DMA driven output)
- Wavetables and samples are not kept in progmem on this platform. While apparently speed (of the external flash) is not much of an issue, the data always seems to be copied into RAM, anyway.
- Audio output is to pin 0, by default, with 11 bits default output resolution
- One hardware alarm and one DMA channel are claimed (number not hardcoded)
- HIFI_MODE not yet implemented (although that should not be too hard to do)
- Currently, two audio output modes exist (configurable in AudioConfigRP2040.h) in addition to using an user-defined `audioOutput` function, with the default being PWM_VIA_BARE_CHIP:
- PWM_VIA_BARE_CHIP: PWM audio output on pin 0, by default, with 11 bits default output resolution
- One non-exclusive timer interrupt (DMA_IRQ_0) and one DMA channel are claimed (number not hardcoded).
- HIFI_MODE not yet implemented (although that should not be too hard to do).
- EXTERNAL_DAC_VIA_I2S: I2S output to be connected to an external DAC
- 16 bits resolution by default (configurable in AudioConfigRP2040.h), 8, 16, 24 (left aligned) and 32 resolution are available.
- Both plain I2S and LSBJ_FORMAT (for the PT8211 for instance) are available (configurable in AudioConfigRP2040.h), default is LSBJ.
- Outputs pins can be configured in AudioConfigRP2040.h. Default is BCK: 20, WS: 21, DATA: 22.
- One non-exclusive timer interrupts (DMA_IRQ_0) and two DMA channels are claimed (numbers not hardcoded).
- At the time of writing, LSBJ is only available with github arduino-pico core.
- Note that AUDIO_INPUT and mozziAnalogRead() return values in the RP2040's full ADC resolution of 0-4095 rather than AVR's 0-1023.
- twi_nonblock is not ported
- Code uses only one CPU core
Expand Down