Shop OBEX P1 Docs P2 Docs Learn Events
P2 Taqoz V2.8: I2S Driver for MAX98357A Amplifier — Parallax Forums

P2 Taqoz V2.8: I2S Driver for MAX98357A Amplifier

Christof Eb.Christof Eb. Posts: 1,213
edited 2023-10-28 08:47 in Forth

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

Sign In or Register to comment.