Skip to content

Commit

Permalink
Add MCLK support for I2S, optimize clocks for jitter-free playback (#…
Browse files Browse the repository at this point in the history
…1555)

Fixes #1065
  • Loading branch information
palmerr23 authored Jul 7, 2023
1 parent 0f437e4 commit cc5d177
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 8 deletions.
17 changes: 17 additions & 0 deletions docs/i2s.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ bool setDATA(pin_size_t pin)
Sets the DOUT or DIN pin of the I2S device. Any pin may be used.
Call before ``I2S::begin()``

bool setMCLK(pin_size_t pin)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sets the MCLK pin of the I2S device and enables MCLK output. Any pin may be used.
Call before ``I2S::begin()``

bool setMCLKmult(int mult)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sets the sample rate to MCLK multiplier value. Only multiples of 64 are valid.
Call before ``I2S::begin()``

bool setBitsPerSample(int bits)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Specify how many bits per audio sample to read or write. Note that
Expand All @@ -58,6 +68,13 @@ Sets the word clock frequency, but does not start the I2S device if not
already running. May be called after ``I2S::begin()`` to change the
sample rate on-the-fly.

bool setSysClk(int samplerate)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Changes the PICO system clock to optimise for the desired samplerate.
The clock changes to 147.6 MHz for samplerates that are a multiple of 8 kHz, and 135.6 MHz for multiples of 11.025 kHz.
Note that using ``setSysClk()`` may affect the timing of other sysclk-dependent functions.
Should be called before any I2S functions and any other sysclk dependent initialisations.

bool setLSBJFormat()
~~~~~~~~~~~~~~~~~~~~
Enables LSB-J format for I2S output. In this mode the MSB comes out at the
Expand Down
84 changes: 84 additions & 0 deletions libraries/I2S/examples/SquareWaveMCLK/SquareWaveMCLK.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
This example demonstrates I2S output with the optional MCLK signal.
Note: System clock sppeds used here may not be compatible with all other libraries
requiring specific sys_clks, particularly Pico PIO USB.
Original 17 November 2016
by Sandeep Mistry
modified for RP2040 by Earle F. Philhower, III <[email protected]>
bool setBCLK(pin_size_t pin);
- This assigns two adjacent pins - the pin after this one (one greater)
is the WS (word select) signal, which toggles before the sample for
each channel is sent
bool setDATA(pin_size_t pin);
- Sets the DOUT pin, can be any valid GPIO pin
modified for MCLK by Richard Palmer June 2023
*/

#include <I2S.h>

// Create the I2S port using a PIO state machine
I2S i2s(OUTPUT);

// GPIO pin numbers
#define pDOUT 19
#define pBCLK 20
#define pWS (pBCLK+1)
#define pMCLK 22 // optional MCLK pin

const int frequency = 440; // frequency of square wave in Hz
const int sampleRate = 16000; // minimum for many i2s DACs
const int bitsPerSample = 16;
const int amplitude = 1 << (bitsPerSample - 2); // amplitude of square wave = 1/2 of maximum

#define MCLK_MUL 256 // depends on audio hardware. Suits common hardware.

const int halfWavelength = sampleRate / (2 * frequency); // half wavelength of square wave

int16_t sample = amplitude; // current sample value
int count = 0;

void setup() {
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, 1);

// set the system clock to a compatible value to the samplerate
i2s.setSysClk(sampleRate); // best to do this before starting anything clock-dependent

Serial.begin(115200);
while (!Serial) {
delay(10);
}
Serial.println("I2S with MCLK - square wave");

i2s.setBCLK(pBCLK);
i2s.setDATA(pDOUT);
i2s.setMCLK(pMCLK); // must be run before setFrequency() and i2s.begin()
i2s.setMCLKmult(MCLK_MUL); // also before i2s.begin()
i2s.setBitsPerSample(16);

// start I2S at the sample rate with 16-bits per sample
if (!i2s.begin(sampleRate)) {
Serial.println("Failed to initialize I2S!");
while (100); // do nothing
}

}

void loop() {
if (count % halfWavelength == 0) {
// invert the sample every half wavelength count multiple to generate square wave
sample = -1 * sample;
}

// write the same sample twice, once for left and once for the right channel
i2s.write(sample);
i2s.write(sample);

// increment the counter for the next sample
count++;
}
3 changes: 3 additions & 0 deletions libraries/I2S/keywords.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ end KEYWORD2

setBCLK KEYWORD2
setDATA KEYWORD2
setMCLK KEYWORD2
setBitsPerSample KEYWORD2
setFrequency KEYWORD2
setBuffers KEYWORD2
setLSBJFormat KEYWORD2
swapClocks KEYWORD2
setMCLKmult KEYWORD2
setSysClk KEYWORD2

read8 KEYWORD2
read16 KEYWORD2
Expand Down
72 changes: 68 additions & 4 deletions libraries/I2S/src/I2S.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <Arduino.h>
#include "I2S.h"
#include "pio_i2s.pio.h"
#include <pico/stdlib.h>


I2S::I2S(PinMode direction) {
Expand All @@ -30,6 +31,8 @@ I2S::I2S(PinMode direction) {
_isOutput = direction == OUTPUT;
_pinBCLK = 26;
_pinDOUT = 28;
_pinMCLK = 25;
_MCLKenabled = false;
#ifdef PIN_I2S_BCLK
_pinBCLK = PIN_I2S_BCLK;
#endif
Expand All @@ -53,6 +56,7 @@ I2S::I2S(PinMode direction) {
_silenceSample = 0;
_isLSBJ = false;
_swapClocks = false;
_multMCLK = 256;
}

I2S::~I2S() {
Expand All @@ -67,6 +71,14 @@ bool I2S::setBCLK(pin_size_t pin) {
return true;
}


bool I2S::setMCLK(pin_size_t pin) {
if (_running || (pin > 28)) {
return false;
}
_pinMCLK = pin;
return true;
}
bool I2S::setDATA(pin_size_t pin) {
if (_running || (pin > 29)) {
return false;
Expand Down Expand Up @@ -96,12 +108,41 @@ bool I2S::setBuffers(size_t buffers, size_t bufferWords, int32_t silenceSample)
bool I2S::setFrequency(int newFreq) {
_freq = newFreq;
if (_running) {
float bitClk = _freq * _bps * 2.0 /* channels */ * 2.0 /* edges per clock */;
pio_sm_set_clkdiv(_pio, _sm, (float)clock_get_hz(clk_sys) / bitClk);
if (_MCLKenabled) {
int bitClk = _freq * _bps * 2.0 /* channels */ * 2.0 /* edges per clock */;
pio_sm_set_clkdiv_int_frac(_pio, _sm, clock_get_hz(clk_sys) / bitClk, 0);
} else {
float bitClk = _freq * _bps * 2.0 /* channels */ * 2.0 /* edges per clock */;
pio_sm_set_clkdiv(_pio, _sm, (float)clock_get_hz(clk_sys) / bitClk);
}
}
return true;
}

bool I2S::setSysClk(int samplerate) { // optimise sys_clk for desired samplerate
if (samplerate % 11025 == 0) {
set_sys_clock_khz(I2SSYSCLK_44_1, false); // 147.6 unsuccessful - no I2S no USB
return true;
}
if (samplerate % 8000 == 0) {
set_sys_clock_khz(I2SSYSCLK_8, false);
return true;
}
return false;
}

bool I2S::setMCLKmult(int mult) {
if (_running || !_isOutput) {
return false;
}
if ((mult % 64) == 0) {
_MCLKenabled = true;
_multMCLK = mult;
return true;
}
return false;
}

bool I2S::setLSBJFormat() {
if (_running || !_isOutput) {
return false;
Expand Down Expand Up @@ -136,6 +177,16 @@ void I2S::onReceive(void(*fn)(void)) {
}
}

void I2S::MCLKbegin() {
int off = 0;
_i2sMCLK = new PIOProgram(&pio_i2s_mclk_program);
_i2sMCLK->prepare(&_pioMCLK, &_smMCLK, &off); // not sure how to use the same PIO
pio_i2s_MCLK_program_init(_pioMCLK, _smMCLK, off, _pinMCLK);
int mClk = _multMCLK * _freq * 2.0 /* edges per clock */;
pio_sm_set_clkdiv_int_frac(_pioMCLK, _smMCLK, clock_get_hz(clk_sys) / mClk, 0);
pio_sm_set_enabled(_pioMCLK, _smMCLK, true);
}

bool I2S::begin() {
_running = true;
_hasPeeked = false;
Expand All @@ -162,6 +213,9 @@ bool I2S::begin() {
pio_i2s_in_program_init(_pio, _sm, off, _pinDOUT, _pinBCLK, _bps, _swapClocks);
}
setFrequency(_freq);
if (_MCLKenabled) {
MCLKbegin();
}
if (_bps == 8) {
uint8_t a = _silenceSample & 0xff;
_silenceSample = (a << 24) | (a << 16) | (a << 8) | a;
Expand Down Expand Up @@ -189,6 +243,11 @@ bool I2S::begin() {

void I2S::end() {
if (_running) {
if (_MCLKenabled) {
pio_sm_set_enabled(_pioMCLK, _smMCLK, false);
delete _i2sMCLK;
_i2sMCLK = nullptr;
}
pio_sm_set_enabled(_pio, _sm, false);
_running = false;
delete _arb;
Expand All @@ -204,8 +263,13 @@ int I2S::available() {
} else {
auto avail = _arb->available();
avail *= 4; // 4 samples per 32-bits
if (_bps < 24 && !_isOutput) {
avail += _isHolding / 8;
if (_bps < 24) {
if (_isOutput) {
// 16- and 8-bit can have holding bytes available
avail += (32 - _isHolding) / 8;
} else {
avail += _isHolding / 8;
}
}
return avail;
}
Expand Down
15 changes: 13 additions & 2 deletions libraries/I2S/src/I2S.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ class I2S : public Stream {

bool setBCLK(pin_size_t pin);
bool setDATA(pin_size_t pin);
bool setMCLK(pin_size_t pin);
bool setBitsPerSample(int bps);
bool setBuffers(size_t buffers, size_t bufferWords, int32_t silenceSample = 0);
bool setFrequency(int newFreq);
bool setLSBJFormat();
bool swapClocks();
bool setMCLKmult(int mult);
bool setSysClk(int samplerate);

bool begin(long sampleRate) {
setFrequency(sampleRate);
Expand Down Expand Up @@ -110,14 +113,17 @@ class I2S : public Stream {
private:
pin_size_t _pinBCLK;
pin_size_t _pinDOUT;
pin_size_t _pinMCLK;
int _bps;
int _freq;
int _multMCLK;
size_t _buffers;
size_t _bufferWords;
int32_t _silenceSample;
bool _isLSBJ;
bool _isOutput;
bool _swapClocks;
bool _MCLKenabled;

bool _running;

Expand All @@ -132,9 +138,14 @@ class I2S : public Stream {
int _isHolding = 0;

void (*_cb)();
void MCLKbegin();

AudioBufferManager *_arb;
PIOProgram *_i2s;
PIO _pio;
int _sm;
PIOProgram *_i2sMCLK;
PIO _pio, _pioMCLK;
int _sm, _smMCLK;

const int I2SSYSCLK_44_1 = 135600; // 44.1, 88.2 kHz sample rates
const int I2SSYSCLK_8 = 147600; // 8k, 16, 32, 48, 96, 192 kHz
};
19 changes: 18 additions & 1 deletion libraries/I2S/src/pio_i2s.pio
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,16 @@
; License along with this library; if not, write to the Free Software
; Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA


.program pio_i2s_mclk
; PIO SM clock is MCLK x 2
; MCLK/BCLK sync is maintained by ensuring IN/OUT PIO SM clock dividers
; are x128, 384, 256, 512 or 768 multiples of MCLK SM divider as required.

loop_mclk:
set pins, 1 ; toggle mclk
set pins, 0
; Loop back to beginning...

.program pio_i2s_out
.side_set 2 ; 0 = bclk, 1=wclk

Expand Down Expand Up @@ -157,6 +166,14 @@ right:

% c-sdk {

static inline void pio_i2s_MCLK_program_init(PIO pio, uint sm, uint offset, uint MCLK_pin) {
pio_gpio_init(pio, MCLK_pin);
pio_sm_set_consecutive_pindirs(pio, sm, MCLK_pin, 1, true);
pio_sm_config sm_config = pio_i2s_mclk_program_get_default_config(offset);
sm_config_set_set_pins(&sm_config, MCLK_pin, 1);
pio_sm_init(pio, sm, offset, &sm_config);
}

static inline void pio_i2s_out_program_init(PIO pio, uint sm, uint offset, uint data_pin, uint clock_pin_base, uint bits, bool swap) {
pio_gpio_init(pio, data_pin);
pio_gpio_init(pio, clock_pin_base);
Expand Down
Loading

0 comments on commit cc5d177

Please sign in to comment.