P2 Taqoz V2.8: I2S Driver for MAX98357A Amplifier
Perhaps this might be useful for somebody?
I started experimenting with I2S output to a MAX98357A Amplifier module using @FredBlais implementation. https://github.com/speccy88/TAQOZ/blob/master/src/forth/audio/WAV-I2S.FTH Which was very helpful to understand I2S. After getting that to work, I thought, that there must be simpler ways.
The picture shows, what is needed for an output at 16kHz sample rate. $0F is output as data in yellow. The tricky thing is, that the LRCLK (blue) changes, before the last bit of the data is still sent.
While @FredBlais shifted the data bits to achieve the tricky phase shift between data and LRCLK, here we simply add the phase into the NCO-counter for LRCLK. Also we directly output all 32bits of the stereo signal.
{ WAV-I2S_H.fth output a wav file at a MAX98357A module needs autou.f for fast local variables inspired and simplified from: https://github.com/speccy88/TAQOZ/blob/master/src/forth/audio/WAV-I2S.FTH https://forums.parallax.com/discussion/167868/taqoz-tachyon-forth-for-the-p2-boot-rom/p23 WAVE FILE PLAYBACK FOR PROPELLER 2 WITH ADAFRUIT MAX98357A I2S CLASS D MONO AMPLIFIER CLOCK SETUP AT 200MHZ 16 KHZ SAMPLE RATE, 16 BITS RESOLUTION BCLK FREQUENCY : 16KHZ * 16 (BITS) * 2 (LEFT/RIGHT) = 512KHZ BCLK AND LRCLK GENERATED WITH 2 SMARTPINS IN NCO FREQUENCY MODE WYPIN for BCLK SET AT 2**32/2 = $8000_0000 WXPIN FOR BCLK : 200E6/512E3/2 = 195 WYPIN for BCLK SET $8000_0000 / 32 WXPIN FOR LRCLK : 195 PHASE of LRCLK is adjusted at startup to 1 bit. DIN SETUP IN SYNCHRONOUS SERIAL MODE IF YOU CHANGE DIN AND BCLK PINS, YOU NEED TO CHANGE DIN MODE CONFIG DIN CLOCK PIN IS SETUP AT PIN + 2 LINKS : https://learn.adafruit.com/adafruit-max98357-i2s-class-d-mono-amp/pinouts https://cdn-learn.adafruit.com/downloads/pdf/adafruit-max98357-i2s-class-d-mono-amp.pdf https://cdn-shop.adafruit.com/product-files/3006/MAX98357A-MAX98357B.pdf http://soundfile.sapp.org/doc/WaveFormat/ https://forums.parallax.com/discussion/171603/propeller-2-what-does-00110-nco-frequency-mode-do Roy Eltham: When running, every X[15:0] system clocks Y[31:0] is added to Z[31:0]. The output pin will be set to Z[31]. While setting up, when you do the WXPIN instruction, X[31:16] is copied to Z[31:16]. This just offsets the initial Z value so that you can phase adjust the frequency (aka start already partially into the cycle). X[31:16] is not used after that. With NCO Frequency mode you do not control the high and low time (it's always 50% duty). You are just controlling the frequency. If X[15:0] is set to 1, then Y[31:0] will be added to Z[31:0] every system clock tick. So in order to set the frequency you desire you need to calculate how many system clocks one cycle of your given frequency takes. This can be done by using "qfrac desiredFrequency, systemClockFrequency" and then "getqx valueForY". If you set X[15:0] to 100, then Y[31:0 will be added to Z[31:0] every 100 system clock ticks. Thus your output frequency would be divided by 100 (e.g. 100kHz would become 1kHz). } \ IFDEF *LRCLK forget *LRCLK } --- ASSIGN PINS 14 := *LRCLK 12 := *BCLK 10 := *DIN 57 := dLedPin --- SMARTPINS CONSTANTS \ %AAAA_BBBB_FFF_MMMMMMMMMMMMM_TT_SSSSS_0 %1010 24 << ( B-input: x010 = relative +2 pin's read state 1= inverted ) %1_11100_0 OR := #SYNC_TX %1_00110_0 := #NCO_FREQ %011111 := #CONT-32BIT : DISABLE_I2S *LRCLK FLOAT *BCLK FLOAT *DIN FLOAT ; : ENABLE_I2S *LRCLK HIGH *BCLK HIGH *DIN HIGH ; --- SMARTPINS SETUP : !PINS DISABLE_I2S *LRCLK PIN #NCO_FREQ WRPIN 195 $8000 1 32 */ 16 << or WXPIN \ phase in upper bits $8000 32 / 16 << WYPIN *BCLK PIN #NCO_FREQ WRPIN ( 78 ) 195 WXPIN $8000_0000 WYPIN *DIN PIN #SYNC_TX WRPIN #CONT-32BIT WXPIN ; \ Am Ausgang: MSB von Left kommt zuerst, (der syncTx mode schickt LSB first) \ ein letztes LSB bit wird noch übertragen, nachdem LRCLK gewechselt hat. \ ausgegeben wird von der Platine default (L+R)/2 : seekDATA ( -- addr ) \ seek for the data header in WAV 0 begin 1+ dup sd@ $61746164 = until \ "data" reverse \ sdPointer ! ; : i2sOut32 {: addr# samples# -- } samples# for I 441 160 */ \ for 44.1 kHz record frequency 4* \ 4 for stereo 2 for mono addr# + SDW@ \ drop $F \ for tests REV DUP 16 >> or \ bit reverse and duplicate for L+R *DIN PIN WYPIN WAITPIN key ?next ; forgetLocals pre playI2s !PINS \ " mario.wav" " bal001.wav" FOPEN$ key drop *DIN PIN 0 WYPIN ENABLE_I2S seekDATA dup 4 + SD@ 2/ 2 - ( subchunksize ) i2sOut32 --- EXIT ON KEY PRESS OR EOF crlf @PIN . fclose DISABLE_I2S ; \ playI2s
As I do not like to keep track of excessive stack juggling, I use fast value type locals here, though in this simplified case only the start address is needed to be kept. So you can easily reconvert to stack usage.
As shown the code maps a 44.1kHz stereo WAV file to the 16kHz output jumping over samples.
The timing constants are set up for a clock frequency of 200MHz.
I am a little bit surprised, that the reading of the SD card, which must refill the buffer for new sectors, is fast enough here. I won't trust this and use a circular buffer.
(Mandatory for this is the use of the version _BOOT_P2.BIX in Taqoz.zip in https://sourceforge.net/projects/tachyon-forth/files/TAQOZ/binaries/ . )
Have fun!
Christof