/**
* PCM5102A_VoidLoop_R0_2.ino
* @version 1.2.0
* @date 08-28-26, 9/16/26, 9/17/26
*
* VoidLoopEngine is brought to you by VoidLoop. See www.voidloop.com for details.
* Two-channel DDS signal generator driving a PCM5102A I2S DAC from SAI1.
*
* Board: Teensy 4.1 only (Basecamp). Pins 36 and 37 carry the SAI1 transmit
* clocks on the 4.1; on the Teensy 4.0 the same pin numbers are SD card pads.
*
* Each channel (right and left) is set up independently, at compile time, as
* one of three sources:
* SOURCE_FIXED one sine frequency, with an optional starting phase
* SOURCE_SWEEP a stepped sine sweep from a start to a stop frequency,
* rising or falling
* SOURCE_NOISE a new random number every sample, no DDS
* A fixed sine or a noise source can also be amplitude modulated (AM) by a
* second, slower sine.
*
* Sine frequencies are limited to 60 kHz. Noise is not band-limited
* by the sketch. At 384 kSPS, the generated noise spans frequencies
* up to 192 kHz. The DAC's filtering and any external output filter
* shape the analog noise spectrum.
*
* A 32-bit phase accumulator represents one complete sine cycle.
* Each sample advances the phase by a fixed increment, and sin()
* calculates the sample value from that phase.
*
* Output frequency = (phase increment / 2^32) x sample rate.
*
* Using only the upper phase bits to index a lookup table introduces
* phase truncation, which can produce spurious tones. This sketch
* uses all 32 phase bits in the sine calculation, avoiding that
* source of error.
*
* Changing frequency changes the phase increment without resetting
* the accumulator, so the waveform's phase remains continuous.
*
* This demonstrates:
* - Register-level SAI1 (I2S) master transmit on the Teensy 4.1
* - Audio PLL (PLL4) configuration for an arbitrary sample rate
* - Direct digital synthesis with a phase accumulator, computed on the fly
* - Sweeps, white noise and amplitude modulation done entirely in software
*
* Hardware, purple PCM5102A breakout:
* - Teensy pin 36 -> BCK (SAI1 TX bit clock)
* - Teensy pin 37 -> LCK (SAI1 TX frame sync / LRCLK)
* - Teensy pin 7 -> DIN (SAI1 TX data)
* - GND -> SCK (grounding SCK selects the DAC's internal PLL,
* so no master clock wire is needed)
* - 5V -> VIN, GND -> GND
* - Solder jumpers on the back: 1(FLT)=L, 2(DEMP)=L, 3(XSMT)=H, 4(FMT)=L.
* XSMT=H unmutes the output; FMT=L selects I2S.
*
* Hardware, Adafruit PCM5102A module:
* - Teensy pin 36 -> BCK, pin 37 -> WSEL, pin 7 -> DIN
* - SCK (MCK pad) left unconnected; the board's default selects the
* internal PLL. There are no solder jumpers to set.
* - 5V -> VIN, GND -> GND
*
* The output is about 6 Vpp (2.1 Vrms) at full scale and centered on ground.
* It is a line-level output for loads of 1 kilohm or more: a scope, the line
* input of a powered speaker, or a headphone amplifier. It cannot drive
* headphones directly. Do not wire it straight to a Teensy ADC pin — it
* swings below ground.
*
* Sketch outline:
* - The "Channel setup" section below is the only part you need to edit.
* - setup() seeds the noise generators, loads the starting frequencies, then
* programs the audio PLL and SAI1 and enables the SAI1 interrupt.
* - The interrupt computes one left and one right sample per audio frame.
* - loop() steps any sweeps and prints a status line every two seconds.
*
* Things to try:
* - For a circle on an X/Y scope display, set both channels to SOURCE_FIXED
* at 1000 Hz with equal amplitudes and modulationEnabled = false.
* Set startPhaseDegrees to 0 on one channel and 90 on the other.
* Connect the outputs to the scope's X and Y inputs, select X/Y mode,
* and use equal volts-per-division settings on both axes.
* - Swap a sweep's start and stop frequencies and watch it fall instead.
* - Set modulationDepth to 1.0 and look at the envelope on a scope.
* - Halve a channel's amplitude and compare the spurs on a spectrum analyzer.
*
* Based on PCM5102 bring-up code by G. Kovacs and M. Flamer (2023), extended
* by Z. Magee and G. Kovacs (2026).
*
* set_audioClock() and the clock setup in config_i2s() are adapted from the
* PJRC Teensy Audio Library (output_i2s.cpp, utility/imxrt_hw.cpp):
*
* Copyright (c) 2014, Paul Stoffregen, paul@pjrc.com
*
* Development of this audio library was funded by PJRC.COM, LLC by sales
* of Teensy and Audio Adaptor boards. Please support PJRC's efforts to
* develop open source software by purchasing Teensy or other PJRC products.
*
* 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, development funding 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 <math.h>
#include <Entropy.h>
#if !defined(ARDUINO_TEENSY41)
#error "This sketch needs a Teensy 4.1: pins 36 and 37 are SAI1 clock pins only on that board."
#endif
enum SourceType { SOURCE_FIXED, SOURCE_SWEEP, SOURCE_NOISE };
// ChannelConfig groups the settings for one output channel.
// rightChannel and leftChannel below each supply a set of these values.
// Edit those values to choose the signals; this declaration defines
// the settings available to both channels.
struct ChannelConfig {
SourceType sourceType;
// Output level. 1.0 is full scale, about 6 Vpp. A negative value inverts
// the waveform.
double amplitude;
// SOURCE_FIXED only.
double frequencyHz;
double startPhaseDegrees;
// SOURCE_SWEEP only. The frequency starts at sweepStartFrequencyHz and
// moves toward sweepStopFrequencyHz by sweepStepFrequencyHz. Each frequency
// is held for at least sweepHoldMilliseconds. If start is above stop the
// sweep falls. After the stop frequency has played, the sweep begins again
// from start.
double sweepStartFrequencyHz;
double sweepStopFrequencyHz;
double sweepStepFrequencyHz;
uint32_t sweepHoldMilliseconds;
// Amplitude modulation, for SOURCE_FIXED and SOURCE_NOISE only.
// modulationDepth ranges from 0.0 (no modulation) to 1.0 (100 %).
// amplitude sets the maximum envelope level. The carrier amplitude is
// amplitude / (1.0 + modulationDepth), so increasing depth reduces the
// carrier level. At 100 % depth the envelope reaches zero but does not
// reverse the carrier's polarity.
bool modulationEnabled;
double modulationFrequencyHz;
double modulationDepth;
};
// ChannelState holds the working values that change as a channel runs.
// setup() initializes these values from that channel's configuration.
// The interrupt advances the waveform phase or noise sequence;
// loop() updates the sweep frequency and timing.
// Each channel has its own state. No user settings are entered here.
struct ChannelState {
volatile uint32_t phaseAccumulator; // carrier phase, 2^32 counts per turn
volatile uint32_t phaseIncrement; // carrier phase step per sample
volatile uint32_t modulationPhaseAccumulator; // phase of the modulating sine
volatile uint32_t modulationPhaseIncrement; // its phase step per sample
volatile uint32_t noiseState; // last random number drawn
volatile float amplitude; // output scale initialized from ChannelConfig
double sweepFrequencyHz; // frequency the sweep is playing now
uint32_t sweepHoldStartedMs; // millis() when that frequency began
};
// ---- Channel setup --------------------------------------------------------
// Edit rightChannel and leftChannel independently, then compile and upload.
// constexpr makes these settings fixed for that build.
//
// For each channel:
// - Choose sourceType and amplitude. An amplitude of 0.0 silences the channel.
// - SOURCE_FIXED: set frequencyHz and startPhaseDegrees.
// - SOURCE_SWEEP: set the start and stop frequencies, a positive frequency
// step, and the hold time. Start above stop gives a falling sweep.
// Set modulationEnabled to false.
// - SOURCE_NOISE: no frequency or starting phase is needed.
// - For AM on a fixed sine or noise, set modulationEnabled to true,
// then choose modulationFrequencyHz and modulationDepth.
//
// Leave unused fields in place; their values are ignored.
// Invalid combinations stop compilation with an explanatory message.
// Example: for an unmodulated 1 kHz sine, set sourceType to SOURCE_FIXED,
// frequencyHz to 1000.0, startPhaseDegrees to 0.0, amplitude to 0.5,
// and modulationEnabled to false. Leave the sweep settings unchanged.
constexpr ChannelConfig rightChannel = {
// SOURCE_FIXED, SOURCE_SWEEP or SOURCE_NOISE.
.sourceType = SOURCE_NOISE,
// Used by every source. 1.0 is full scale; negative inverts.
.amplitude = 1.0,
// Used by SOURCE_FIXED only.
.frequencyHz = 10000.0,
.startPhaseDegrees = 0.0,
// Used by SOURCE_SWEEP only. Start above stop gives a falling sweep.
.sweepStartFrequencyHz = 200.0,
.sweepStopFrequencyHz = 6000.0,
.sweepStepFrequencyHz = 10.0,
.sweepHoldMilliseconds = 50,
// Used by SOURCE_FIXED and SOURCE_NOISE. Not allowed with SOURCE_SWEEP.
// The frequency and depth are ignored while modulationEnabled is false.
.modulationEnabled = false,
.modulationFrequencyHz = 1000.0,
.modulationDepth = 1.0,
};
constexpr ChannelConfig leftChannel = {
// SOURCE_FIXED, SOURCE_SWEEP or SOURCE_NOISE.
.sourceType = SOURCE_SWEEP,
// Used by every source. 1.0 is full scale; negative inverts.
.amplitude = 0.5,
// Used by SOURCE_FIXED only.
.frequencyHz = 2000.1,
.startPhaseDegrees = 0.0,
// Used by SOURCE_SWEEP only. Start above stop gives a falling sweep.
.sweepStartFrequencyHz = 60000.0,
.sweepStopFrequencyHz = 20.0,
.sweepStepFrequencyHz = 100.0,
.sweepHoldMilliseconds = 50,
// Used by SOURCE_FIXED and SOURCE_NOISE. Not allowed with SOURCE_SWEEP.
// The frequency and depth are ignored while modulationEnabled is false.
.modulationEnabled = false,
.modulationFrequencyHz = 100.0,
.modulationDepth = 1.0,
};
// Samples per second on each channel. The SAI1 interrupt fires once per
// sample pair, so raising this raises interrupt load one-for-one.
static const int sampleRateHz = 384000;
// ---- Limits ---------------------------------------------------------------
// Highest frequency the generator is specified for.
constexpr double maximumFrequencyHz = 60000.0;
// AM puts a sideband on each side of the carrier, at carrier +/- modulation
// frequency. Holding the modulation to 20 % of the carrier keeps the
// sidebands close to it, and the upper sideband must also stay under
// maximumFrequencyHz. Noise has no carrier frequency, so this sketch holds
// noise modulation to 20 % of maximumFrequencyHz, 12 kHz. These limits apply
// to the sines only; they do not narrow the bandwidth of the noise itself.
constexpr double maximumModulationRatio = 0.20;
constexpr bool frequencyInRange(double frequencyHz) {
return frequencyHz >= 0.0 && frequencyHz <= maximumFrequencyHz;
}
#define CHECK_CHANNEL(config) \
static_assert(config.amplitude >= -1.0 && config.amplitude <= 1.0, \
#config ": amplitude must be within -1.0 .. 1.0"); \
static_assert(config.sourceType != SOURCE_FIXED \
|| frequencyInRange(config.frequencyHz), \
#config ": frequencyHz must be within 0 .. 60 kHz"); \
static_assert(config.sourceType != SOURCE_SWEEP \
|| (frequencyInRange(config.sweepStartFrequencyHz) \
&& frequencyInRange(config.sweepStopFrequencyHz)), \
#config ": sweep start and stop must be within 0 .. 60 kHz"); \
static_assert(config.sourceType != SOURCE_SWEEP \
|| (config.sweepStepFrequencyHz > 0.0 \
&& config.sweepHoldMilliseconds > 0), \
#config ": sweep step size and step time must be above zero"); \
static_assert(!(config.modulationEnabled && config.sourceType == SOURCE_SWEEP), \
#config ": modulation is not available on a sweep"); \
static_assert(!config.modulationEnabled \
|| (config.modulationDepth >= 0.0 && config.modulationDepth <= 1.0),\
#config ": modulationDepth must be within 0.0 .. 1.0"); \
static_assert(!(config.modulationEnabled && config.sourceType == SOURCE_FIXED) \
|| (config.modulationFrequencyHz >= 0.0 \
&& config.modulationFrequencyHz \
<= maximumModulationRatio * config.frequencyHz), \
#config ": modulationFrequencyHz must be at most 20 % of frequencyHz"); \
static_assert(!(config.modulationEnabled && config.sourceType == SOURCE_FIXED) \
|| config.frequencyHz + config.modulationFrequencyHz \
<= maximumFrequencyHz, \
#config ": frequencyHz + modulationFrequencyHz must stay under 60 kHz"); \
static_assert(!(config.modulationEnabled && config.sourceType == SOURCE_NOISE) \
|| (config.modulationFrequencyHz >= 0.0 \
&& config.modulationFrequencyHz \
<= maximumModulationRatio * maximumFrequencyHz), \
#config ": noise modulationFrequencyHz must be at most 12 kHz")
CHECK_CHANNEL(rightChannel);
CHECK_CHANNEL(leftChannel);
// ---- Generator state ------------------------------------------------------
static ChannelState rightState;
static ChannelState leftState;
static const double countsPerTurn = 4294967296.0; // 2^32
static const double radiansPerPhaseCount = 2.0 * M_PI / countsPerTurn;
// I2S data words are signed 32-bit two's complement; the int32_t cast in the
// interrupt produces exactly that.
static const double fullScale = 2147483647.0;
// Worst-case time spent in the interrupt, in CPU clock cycles, and the number
// of times the transmit FIFO ran empty. Both are printed by loop(). The
// interrupt has one sample period (2.6 us at 384 kHz) to finish. Two fixed
// sines with AM on both is the heaviest setup. If the interrupt is made
// slower than the sample period, loop() never gets to run and the status
// lines stop.
static volatile uint32_t longestInterruptCycles = 0;
static volatile uint32_t fifoErrorCount = 0;
static uint32_t incrementForFrequency(double frequencyHz) {
return (uint32_t)((frequencyHz / (double)sampleRateHz) * countsPerTurn + 0.5);
}
// Returns the next sample for one channel, from -1.0 to +1.0 at full
// amplitude. Because each ChannelConfig is a compile-time constant, the
// compiler keeps only the branches that channel uses.
static inline double nextSample(const ChannelConfig &config, ChannelState &state) {
double sample;
if (config.sourceType == SOURCE_NOISE) {
// xorshift32: three shift-and-XOR steps turn one 32-bit number into the
// next, visiting every non-zero value before repeating. Read as a signed
// number, the result is white noise spread evenly over full scale.
uint32_t randomBits = state.noiseState;
randomBits ^= randomBits << 13;
randomBits ^= randomBits >> 17;
randomBits ^= randomBits << 5;
state.noiseState = randomBits;
sample = (double)(int32_t)randomBits / 2147483648.0;
} else {
sample = sin(state.phaseAccumulator * radiansPerPhaseCount);
state.phaseAccumulator += state.phaseIncrement;
}
if (config.modulationEnabled) {
// envelope = 1 + depth x modulating sine, which swings between
// (1 - depth) and (1 + depth). Dividing by (1 + depth) keeps the peaks at
// the amplitude setting, so 100 % modulation cannot overflow the DAC.
// Carrier, modulating sine and amplitude are all signed floating-point
// numbers, so every product carries the correct sign. With depth limited
// to 1.0 the envelope never goes below zero.
double modulatingSine = sin(state.modulationPhaseAccumulator * radiansPerPhaseCount);
state.modulationPhaseAccumulator += state.modulationPhaseIncrement;
sample *= (1.0 + config.modulationDepth * modulatingSine)
/ (1.0 + config.modulationDepth);
}
return state.amplitude * sample;
}
// Runs every time the SAI1 TX FIFO drains to its watermark — once per audio
// frame in steady state. Pushes one left and one right sample.
static void sai1_isr(void) {
uint32_t startCycles = ARM_DWT_CYCCNT;
// An I2S frame carries the left word first, then the right word.
I2S1_TDR0 = (int32_t)(fullScale * nextSample(leftChannel, leftState));
I2S1_TDR0 = (int32_t)(fullScale * nextSample(rightChannel, rightState));
// FEF is set by the SAI when the FIFO ran empty; writing the bit back
// clears it.
if (I2S1_TCSR & I2S_TCSR_FEF) {
fifoErrorCount++;
I2S1_TCSR |= I2S_TCSR_FEF;
}
uint32_t cycles = ARM_DWT_CYCCNT - startCycles;
if (cycles > longestInterruptCycles) longestInterruptCycles = cycles;
}
// Loads one channel's starting values. seed must not be zero: xorshift32
// stays at zero forever once it gets there.
static void startChannel(const ChannelConfig &config, ChannelState &state, uint32_t seed) {
state.amplitude = config.amplitude;
state.noiseState = seed ? seed : 1;
state.phaseAccumulator = 0;
state.phaseIncrement = 0;
state.modulationPhaseAccumulator = 0;
state.modulationPhaseIncrement = 0;
if (config.sourceType == SOURCE_FIXED) {
state.phaseAccumulator = (uint32_t)(int64_t)(config.startPhaseDegrees / 360.0 * countsPerTurn);
state.phaseIncrement = incrementForFrequency(config.frequencyHz);
} else if (config.sourceType == SOURCE_SWEEP) {
state.sweepFrequencyHz = config.sweepStartFrequencyHz;
state.sweepHoldStartedMs = millis();
state.phaseIncrement = incrementForFrequency(state.sweepFrequencyHz);
}
if (config.modulationEnabled) {
state.modulationPhaseIncrement = incrementForFrequency(config.modulationFrequencyHz);
}
}
// Moves a sweeping channel to its next frequency once the current one has
// played for sweepHoldMilliseconds. The hold time restarts when the frequency
// actually changes, so a late call never shortens the next hold. The last
// frequency step is made smaller if needed so the stop frequency itself is
// always played.
static void updateSweep(const ChannelConfig &config, ChannelState &state) {
if (config.sourceType != SOURCE_SWEEP) return;
if (millis() - state.sweepHoldStartedMs < config.sweepHoldMilliseconds) return;
state.sweepHoldStartedMs = millis();
const bool rising = config.sweepStopFrequencyHz >= config.sweepStartFrequencyHz;
double frequencyHz = state.sweepFrequencyHz;
if (frequencyHz == config.sweepStopFrequencyHz) {
frequencyHz = config.sweepStartFrequencyHz;
} else if (rising) {
frequencyHz += config.sweepStepFrequencyHz;
if (frequencyHz > config.sweepStopFrequencyHz) frequencyHz = config.sweepStopFrequencyHz;
} else {
frequencyHz -= config.sweepStepFrequencyHz;
if (frequencyHz < config.sweepStopFrequencyHz) frequencyHz = config.sweepStopFrequencyHz;
}
state.sweepFrequencyHz = frequencyHz;
state.phaseIncrement = incrementForFrequency(frequencyHz);
}
static void printChannel(const char *name, const ChannelConfig &config, const ChannelState &state) {
Serial.printf(" %s: ", name);
if (config.sourceType == SOURCE_FIXED) {
Serial.printf("fixed %.3f Hz", config.frequencyHz);
} else if (config.sourceType == SOURCE_SWEEP) {
Serial.printf("sweep %.1f -> %.1f Hz, now %.1f Hz", config.sweepStartFrequencyHz,
config.sweepStopFrequencyHz, state.sweepFrequencyHz);
} else {
Serial.printf("noise");
}
Serial.printf(", amplitude %.3f", (double)state.amplitude);
if (config.modulationEnabled) {
Serial.printf(", AM %.1f Hz at %.0f %%", config.modulationFrequencyHz,
config.modulationDepth * 100.0);
}
Serial.println();
}
// Sets PLL4 (the audio PLL) to 24 MHz x (nfact + nmult/ndiv). A PLL that is
// already enabled is left untouched.
static void set_audioClock(int nfact, int32_t nmult, uint32_t ndiv) {
if (CCM_ANALOG_PLL_AUDIO & CCM_ANALOG_PLL_AUDIO_ENABLE) return;
CCM_ANALOG_PLL_AUDIO = CCM_ANALOG_PLL_AUDIO_BYPASS | CCM_ANALOG_PLL_AUDIO_ENABLE
| CCM_ANALOG_PLL_AUDIO_POST_DIV_SELECT(2)
| CCM_ANALOG_PLL_AUDIO_DIV_SELECT(nfact);
CCM_ANALOG_PLL_AUDIO_NUM = nmult & CCM_ANALOG_PLL_AUDIO_NUM_MASK;
CCM_ANALOG_PLL_AUDIO_DENOM = ndiv & CCM_ANALOG_PLL_AUDIO_DENOM_MASK;
CCM_ANALOG_PLL_AUDIO &= ~CCM_ANALOG_PLL_AUDIO_POWERDOWN;
while (!(CCM_ANALOG_PLL_AUDIO & CCM_ANALOG_PLL_AUDIO_LOCK)) {}
CCM_ANALOG_MISC2 &= ~(CCM_ANALOG_MISC2_DIV_MSB | CCM_ANALOG_MISC2_DIV_LSB);
CCM_ANALOG_PLL_AUDIO &= ~CCM_ANALOG_PLL_AUDIO_BYPASS;
}
// Clocks SAI1 from PLL4 and configures it as I2S master: two 32-bit words
// per frame, MSB first, data one bit clock after each frame-sync edge.
static void config_i2s(int fs) {
CCM_CCGR5 |= CCM_CCGR5_SAI1(CCM_CCGR_ON);
// PLL4 must run between 648 MHz (24 MHz x 27) and 1296 MHz (x 54); the
// n1/n2 dividers scale the target so the multiplier C lands in range.
int n1 = 4;
int n2 = 1 + (24000000 * 27) / (fs * 256 * n1);
double C = ((double)fs * 256 * n1 * n2) / 24000000;
int c0 = C;
int c2 = 10000;
int c1 = C * c2 - (c0 * c2);
set_audioClock(c0, c1, c2);
CCM_CSCMR1 = (CCM_CSCMR1 & ~(CCM_CSCMR1_SAI1_CLK_SEL_MASK))
| CCM_CSCMR1_SAI1_CLK_SEL(2); // 2 = PLL4
CCM_CS1CDR = (CCM_CS1CDR & ~(CCM_CS1CDR_SAI1_CLK_PRED_MASK | CCM_CS1CDR_SAI1_CLK_PODF_MASK))
| CCM_CS1CDR_SAI1_CLK_PRED(n1 - 1)
| CCM_CS1CDR_SAI1_CLK_PODF(n2 - 1);
// MCLK1, selected as the bit-clock source by MSEL below, is the SAI1
// clock root. The MCLK pad itself is never muxed out; the DAC recovers
// its clock from BCK.
IOMUXC_GPR_GPR1 = (IOMUXC_GPR_GPR1 & ~(IOMUXC_GPR_GPR1_SAI1_MCLK1_SEL_MASK))
| (IOMUXC_GPR_GPR1_SAI1_MCLK_DIR | IOMUXC_GPR_GPR1_SAI1_MCLK1_SEL(0));
// These SAI1 TX pins are free on Basecamp; on Ascent the same pins belong
// to the parallel sampler.
CORE_PIN36_CONFIG = 3; // pad B1_02 -> SAI1_TX_BCLK (BCK)
CORE_PIN37_CONFIG = 3; // pad B1_03 -> SAI1_TX_SYNC (LCK)
CORE_PIN7_CONFIG = 3; // pad B1_01 -> SAI1_TX_DATA0 (DIN)
// Transmitter as asynchronous master: it generates BCK and LRCLK itself.
// The FIFO watermark of 8 fires the refill interrupt with room to spare.
I2S1_TMR = 0;
I2S1_TCR1 = I2S_TCR1_RFW(8);
I2S1_TCR2 = I2S_TCR2_SYNC(0) | I2S_TCR2_BCP
| I2S_TCR2_BCD | I2S_TCR2_DIV(1) | I2S_TCR2_MSEL(1);
I2S1_TCR3 = I2S_TCR3_TCE;
I2S1_TCR4 = I2S_TCR4_FRSZ(2 - 1) | I2S_TCR4_SYWD(32 - 1) | I2S_TCR4_MF
| I2S_TCR4_FSD | I2S_TCR4_FSE | I2S_TCR4_FSP | I2S_TCR4_FCONT;
I2S1_TCR5 = I2S_TCR5_WNW(32 - 1) | I2S_TCR5_W0W(32 - 1) | I2S_TCR5_FBT(32 - 1);
// Prime the FIFO with silence so the transmitter has data the moment it
// starts.
for (int i = 0; i < 32; i++) {
I2S1_TDR0 = 0;
}
attachInterruptVector(IRQ_SAI1, sai1_isr);
NVIC_ENABLE_IRQ(IRQ_SAI1);
I2S1_TCSR = I2S_TCSR_TE | I2S_TCSR_BCE | I2S_TCSR_FRIE;
}
void setup() {
Serial.begin(115200);
// Seed each channel from the hardware random number generator. Each
// channel then generates pseudorandom samples without waiting for more
// hardware entropy, which arrives far too slowly for one number every
// 2.6 us.
Entropy.Initialize();
uint32_t rightSeed = Entropy.random();
uint32_t leftSeed = Entropy.random();
startChannel(rightChannel, rightState, rightSeed);
startChannel(leftChannel, leftState, leftSeed);
config_i2s(sampleRateHz);
}
void loop() {
updateSweep(rightChannel, rightState);
updateSweep(leftChannel, leftState);
// Status line, so a silent DAC is distinguishable from a board that never
// booted or never took the flash.
static uint32_t lastPrintMs = 0;
if (millis() - lastPrintMs >= 2000) {
lastPrintMs = millis();
double longestInterruptMicroseconds = longestInterruptCycles * 1.0e6 / F_CPU_ACTUAL;
Serial.printf("PCM5102A DDS at %d samples/s, up %lu s\n", sampleRateHz, millis() / 1000);
printChannel("right", rightChannel, rightState);
printChannel("left ", leftChannel, leftState);
Serial.printf(" longest interrupt %.2f us of %.2f us available, FIFO errors %lu\n",
longestInterruptMicroseconds, 1.0e6 / sampleRateHz,
(unsigned long)fifoErrorCount);
}
}