variable I2S master clock (and sampling) frequency

Status
Not open for further replies.

kpc

Well-known member
If people need another sampling frequency than the usual 44.117 kHz. I wrote a little function, that allows for other frequencies.
Eg. 8, 11.025, 12, 16, 22.05, 24, 32, 44.1, 48 kHz or 96 kHz, or anything in between.
It does not have to be used in conjunction with the Audio stuff. The MCLK pin can just be used standalone as another clock generator.
It should work for all CPU frequencies > 16 MHz.
Although outside of spec, generating the master clock for 96 kHz is no problem for my device.

As an example, With a CPU clock of 96 MHz
Desired: 12345 Hz
Reality: 12345.02 Hz
Delta: 0.02 Hz

Since the Audiostream objects assume a fixed frequency of 44117, to set the frequency of e.g. a sine generator, compensate by the frequency ration between the 44117 and the sample clock, as shown in the example.

Edit: Only tested it on two of my Teensy 3.1 devices. Do not know about Teensy 3.0 or even LC.

Code:
/* This code is free to use */
/* Kire Pudsje, Feb. 2015 */
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>

AudioControlSGTL5000 sgtl5000;
AudioSynthWaveformSine sine;
AudioOutputI2S i2sout;
AudioConnection pc(sine, i2sout);

// function prototype
bool setI2SmasterClock(uint32_t freq, uint32_t mult = 256);
float getI2SmasterClock(uint32_t mult = 256);

#define FSAMPLE 12345

void setup() {
  Serial.begin(9600);
  AudioMemory(12);
  setI2SmasterClock(FSAMPLE);
  sgtl5000.enable();
  // AudioStream objects assume a given sample rate of 44117 Hz, correct for this.
  sine.frequency(1e3 * AUDIO_SAMPLE_RATE_EXACT / FSAMPLE);
  sine.amplitude(1);
}

void loop() {
  Serial.print("Desired: ");
  Serial.print(FSAMPLE);
  Serial.print(" Hz, Reality: ");
  Serial.print(getI2SmasterClock());
  Serial.print(" Hz, Delta: ");
  Serial.print(getI2SmasterClock() - FSAMPLE);
  Serial.println(" Hz");
}

/* Generate I2S master clock

   The MCLK output frequencies should be in the range from F_pll/4096 to 12.5 MHz.
   For convenience the frequency has been split into a sample frequency and 
   a sample clock multiplication factor.
   For direct control of the master clock frequency, set the multiplier to 1.
   For almost all combinations of sample clocks (8, 11.025, 12, 16, 22.05, 24,
   32, 44.1 and 48 kHz) and cpu clock frequencies it will find a ratio, that
   generates the exact frequency. For the other few, the output frequency
   lies within 0.5 Hz distance from the desired frequency.

   freq: sample frequency in Hz
   mult: sample clock multiplication factor, (default 256)
   return value: true if the frequency is within the valid range
*/

bool setI2SmasterClock(uint32_t freq, uint32_t mult)
{
        // determine fractional ratio for: p / q = freq * mult / f_pll
        // f_pll = 16 MHz * ((MCG_C6 & 0x1f) + 24) / ((MCG_C5 & 0x1f) + 1)
	uint32_t p = freq * mult * ((MCG_C5 & 0x1f) + 1);
        uint32_t q = 16000000ul * ((MCG_C6 & 0x1f) + 24);
	uint32_t fract = 0, divide = 1; // I2Sx_MDR register values
	uint32_t fract1, fract2 = 1;	// values of iterations n-1 and n-2.
	uint32_t divide1, divide2 = 0;	// idem.
        uint8_t src = 3;		// clock source selection: 3 = pll

        // Master clock should be slower than reference clock
        if (p > q)
                return false;
        else if (p == q)
                fract = divide = 1;
        else {
	        // Approximate p/q by a continued fraction expansion.
  	        // (typically takes only 4 to 8 iterations)
  	        while (p) {
  	    		// find next element of continued fraction
	  		uint32_t a = q / p;
		  	uint32_t oldp = p;
  			p = q - a * p;
	  		q = oldp;

    		  	// update fract/divide by Wallis method
  			fract1 = fract;
	  		divide1 = divide;
		  	fract = fract1 * a + fract2;
  			divide = divide1 * a + divide2;
	  		fract2 = fract1;
		  	divide2 = divide1;

  			// Exit loop if fraction gets too big for the registers
	  		if (fract > 256 || divide > 4096) {
		  		// Revert back to last approximation that still fitted
  				fract = fract2;
	  			divide = divide2;
		  		break;
  			}
	  	}
  	        // Check if frequency too low or too high
          	if (!fract)
                  	return false;
        }

	// Actually set registers and enable MCLK.
        SIM_SCGC6 |= SIM_SCGC6_I2S;
	I2S0_MDR = I2S_MDR_FRACT((fract - 1)) | I2S_MDR_DIVIDE((divide - 1));
        I2S0_MCR = I2S_MCR_MICS(src) | I2S_MCR_MOE;

	// 'Connect' MCLK output to an actual output pin
        CORE_PIN11_CONFIG = PORT_PCR_MUX(6);
        //CORE_PIN28_CONFIG = PORT_PCR_MUX(4); // Pin on bottom

        return true;
}

float getI2SmasterClock(uint32_t mult)
{
        float fpll = 16.0e6f * ((MCG_C6 & 0x1f) + 24) / ((MCG_C5 & 0x1f) + 1);
        uint32_t fract = ((I2S0_MDR >> 12) & 0xff) + 1;
        uint32_t divide = (I2S0_MDR & 0xfff) + 1;
        return fpll * fract / (divide * mult);
}
 
Last edited:
200 kHz sampling with sgtl5000

I did some more testing and I was able to achieve a sample clock of 200 kHz with the sgtl5000. The CPU was at 96 MHz.
I generated a sawtooth signal, send it to the i2s output. Connected this output to an input. Again send this data to the other i2s output. Until 200 kHz, the output was as expected.
Code:
AudioSynthWaveform saw;
AudioOutputI2S i2sout;
AudioInputI2S i2sin;
AudioConnection pc1(i2sin, 0, i2sout, 0);
AudioConnection pc2(saw, 0, i2sout, 1);
 
I did some more testing and I was able to achieve a sample clock of 200 kHz with the sgtl5000. The CPU was at 96 MHz.
I generated a sawtooth signal, send it to the i2s output. Connected this output to an input. Again send this data to the other i2s output. Until 200 kHz, the output was as expected.
Code:
AudioSynthWaveform saw;
AudioOutputI2S i2sout;
AudioInputI2S i2sin;
AudioConnection pc1(i2sin, 0, i2sout, 0);
AudioConnection pc2(saw, 0, i2sout, 1);

can you share the three clock constants (multiplier and dividers) for 200 kHz?
 
For the MCLK generation, I only use two values.
As said, these values are for a cpu clock of 96 MHz.
fract = 8 => I2S_MDR_FRACT = 7
divide = 15 => I2S_MDR_DIVIDE = 14
The register values are one less.
96 MHz/256 * 8/15 = 200 kHz

Just run my example, replace FSAMPLE by 200000, and insert a few println's
You could even push it a bit futher. I managed to get 300 kHz, but this was not reliable.
 
For the MCLK generation, I only use two values.
As said, these values are for a cpu clock of 96 MHz.
fract = 8 => I2S_MDR_FRACT = 7
divide = 15 => I2S_MDR_DIVIDE = 14
The register values are one less.
96 MHz/256 * 8/15 = 200 kHz

Just run my example, replace FSAMPLE by 200000, and insert a few println's
You could even push it a bit futher. I managed to get 300 kHz, but this was not reliable.

what about the CHIP_PLL_CTRL register of the SGTL5000? It must be adapted to the new MCLK. (51.2 MHz new vs 11.294 MHz in the 44.1 kHz case)

OK, it is fixed at 256 and not using PLL of SGTL500
 
Last edited:
I am not using the PLL. The sgtl5000 is directly clocked by MCLK.
The example code I gave uses just the library as is. I made no modiications to control_sgtl500 or output_i2s.
I was amazed myself, I could get this far without tweaking any sgtl5000 register.
 
KPC: Can this be applied to sample rates when not using the audio card?

<edit> read linked post - No I'm not using the audio card so I can't use this.
 
Last edited:
This function can not. The same principle however might be applied for the on-chip ADC I think. I might have a look at this at some later time.

Edit: If someone can tell me which registers are involved, I can write a fractional expansion routine for it.
OK found it, it is just PDB0_MOD.
No need for a fractional expansion, just a division.
Also This is probably the reason why the audio shield does not use 44100 but 44117 Hz.
 
Last edited:
@defragster: no need for a function. just use the following (albeit untested from my side, at the moment no teensy here)
PDB0_MOD = (F_BUS / fsample) - 1;

Edit: you should apply the same corrections as the first post, since the Audio library does not know about the frequency.
sine.frequency(1e3 * AUDIO_SAMPLE_RATE_EXACT / fsample);
I would not advice you to do it, but you could also modify AUDIO_SAMPLE_RATE_EXACT.
 
Last edited:
I just realized that AUDIO_SAMPLE_RATE_EXACT is only valid for an 48 MHz bus clock.
@Paul, as a suggestion, maybe make this dependant with ifdefs on the bus speed, so that AUDIO_SAMPLE_RATE_EXACT gives the exact frequency for all bus speeds?
 
kpc: Taking you literally I put "PDB0_MOD = (F_BUS / 44100) - 1;" in setup() and that seems to stop execution
 
Sorry, I do not have access to teensy at the moment. you might want to try overruling the value set in Audio/utility/pdb.h and see if that works. To be able to adjust it on the fly, maybe to adjust the timer, it first needs to be stopped, then updated and restarted again.
 
kpc: No problem - I thought if it was that easy I'd try. I put it at start and end of setup() and in a simple sketch with no audio use and all 3 cases are DOA. I may look at it - if you do I'd like to know and see if it might help me. I don't need anything over 4khz I suppose - so samples would just be farther part in time - but still need the same number - but each FFT bin would represent a smaller range.
 
@defragster: The SC_LDOK needed to be set before the new clock is applied.
When transitioning from a high to a low sample frequency, this seems to be continuous. The other way, seems to keep the ADC on hold for about 1 ms. Probably first needs to finish the dma cycle or something like that?
Code:
    PDB0_MOD = (F_BUS / fsample) - 1;
    PDB0_SC |= PDB_SC_LDOK;

Edit: solved the ADC being on hold, needed also to set the LDMOD register.
Code:
PDB0_MOD = (F_BUS / fsample) - 1;
PDB0_SC |= PDB_SC_LDOK| PDB_SC_LDMOD(1)
 
Last edited:
When using both the audio board and the DAC, both sample streams need to be kept synchronized. The master clock can achieve much higher frequency resolution than the PDB clock. To keep them aligned, first set the DAC frequency, then based on the truncation of the PDB0_MOD register, the actual sampling frequency is calculated, which is then used to set the master clock
Code:
setI2SmasterClock(256 * F_BUS / ((uint32_t) (F_BUS / fsample)), 1);
 
Thanks Kire,
I have been looking for ways to investigate the lower-frequency end of the spectrum. I managed to get this to compile, output etc.

Is it true that reducing the sampling rate this way will result in increased precision at the lower end (more bins/kHz)?

Is 8 kHz a lower limit? I appear to have 4 kHz working but it might not be producing any benefit...

Best
Graham
 
Last edited:
Slowing the clock using kpc's code is probably completely cool, when I tried this code to speed up the clock the MCLK became (much more) jittery to the point that it was flagged by the EE I must obey as unusable - tried on a few Teensys and a few MK20DX256VLH7s paired with TLV320AIC3254 on a prototype we are playing with. With Teensy as master the jitter on BCLK is pretty bad when you use the above to set sampling rate = 96000.

I spent hours on it, tried to prove that it was his layout on our prototype; when I tried it on 3 Teensys (*without even having Audio Adapter present) and could not make the jitter go away I conceded that MCLK is just not perfect from MK20DX256VLH7 - the oscilloscope on my desk (RIGOL DS 5202CA) would not reveal the jitter but the oscilloscope on his desk (Tektronix TDS 2022) made it as obvious as anything really easily - I made him swap with me for a couple of days while I investigated ways to mitigate the problem.


As an aside, I have always meant to mention (*into this thread) that the Audio quality *should* be better if you configure the SGTL5000 for whatever sampling rate properly. This may not be as important for the SGTL5000 as opposed a TLV320AIC3254/PCM3070 where not choosing the correct decimation and interpolation filters (and a few other details, for given sampling frequency) can turn out to be shockingly noticeable.

I posted a technique (into 'Audio Library' thread) which shows how to configure the SGTL5000 to be the I2S bus master @96K samples and using similar technique I was able to satisfy the EE that the resultant BCLK and LRCLK were jitter free on the TLV320AIC3254 - haven't gone back to check SGTL5000 since, it is buried in something on my desk waiting for me to be freed from other tasks atm.

kpc saw the post I am talking about and was kind enough to PM me a note, something along the lines that my choices for PLL settings in SGTL5000 were not as good/precise as they could have been - I reviewed the resultant waveforms and quality of Audio across the interface and decided not to try to improve it.


MCLK, as derived by the code the original code in the Audio Library, has a tiny bit of jitter in it - keeping it at the library's intended rate and using PLL in the codecs I have tried appears to produce jitter free BCLK at speeds up to 96K (am willing to assume will make 192K fine too); the resultant BCLK jitter was unacceptable (to EE) when I used any technique at all to speed MCLK from MK20DX256VLH7 up.


Disclaimer: Been proved wrong before. (don't mind either, eventually I will learn better - gonna have to be at least a bit convincing and work just like the 'correcter' says when I try exactly their instructions...)
 
Teensy/Audio deaf after power off

I have been looking for ways to investigate the lower-frequency end of the spectrum. I managed to get this to compile, output etc.

Is it true that reducing the sampling rate this way will result in increased precision at the lower end (more bins/kHz)?

Is 8 kHz a lower limit? I appear to have 4 kHz working but it might not be producing any benefit...

I have a strange phenomenon, perhaps someone using Kire's code can help...
I use Kire's setI2SmasterClock() in an application derived from the FFT example, with FSAMPLE=4096.
If I programme teensy with my/Kire's code, it works fine...
until I power-cycle Teensy, after which it appears to 'hear' nothing from the audio board's MIC input.
To get it to work, I compile and upload the basic FFT example which doesn't use Kire's code. Then it starts 'hearing' again.
Then I can build and deploy my code as often as I like, until I power-cycle Teensy again, when it goes deaf.

I've tested this many times and I'm pretty sure that the problem is as I describe it. I've looked hard for a difference between the FFT example and my code, particularly the setup() part, and the call to setI2SmasterClock(FSAMPLE); is overwhelmingly the most likely candidate as I can see it.

Partly answering my own question... I set FSAMPLE=8192 and now my code continues hearing (no longer goes deaf) through a power cycle.

Still, I am interested to hear if anyone has a view on why this might occur? My interest is focussed on low frequencies so anything I can do to improve low-frequency performance is of value to me.

Thanks
Graham
 
Are you still subscribed to this thread kpc?

I hope you are well. I also hope I can be forgiven for being basically completely off topic here.

Drop us a line mate. I am not the only one who misses you around here.
 
Status
Not open for further replies.
Back
Top