I2S example code

Post your cool example code here.
Post Reply
User avatar
Vassilis
Posts: 320
Joined: Thu May 21, 2015 6:42 am
Location: Thessaloniki, Greece
Contact:

I2S example code

Post by Vassilis » Fri Aug 28, 2015 5:52 pm

The sketch I wrote uses the hardware SPI interface of STM32 microcontroller. You can connect a I2S DAC like PT8211. The serial bus input data format of PT8211 is Japanese or called LSBJ (Least Significant Bit Justified) format.

The example sketch produces a 16-bit value Sine wave on the RIGHT Audio channel (PT8211 pin 8) and a SawTooth wave on the LEFT Audio channel (pin 6).

The STM32F103C8T6 is used but can be used on any STM32 with hardware SPI interface. The PT8211 is connected on SPI_1 port.
The example doesn't include the two's complement mathematical operation on binary numbers but this is something that can be done easily.

Connections
PT8211 <--> STM32F103
WS <--> PA3
BCK <--> PA5
DIN <--> PA7

Code: Select all

/**
    I2S example code

    Description:
    This I2S example creates a Sine waveform on the RIGHT Audio channel of PT8211
    and a Sawtooth waveform on the LEFT Audio channel.

    This is a very simple how-to-use an external I2S DAC example (DAC = Digital to Analog Converter).

    Created on 27 Aug 2015 by Vassilis Serasidis
    email:  avrsite@yahoo.gr

    Connections between PT8211 DAC and the STM32F103C8T6
    WS    <-->  PA3
    BCK   <-->  PA5 <-->  BOARD_SPI1_SCK_PIN
    DIN   <-->  PA7 <-->  BOARD_SPI1_MOSI_PIN
*/


#include <SPI.h>

#define WS   PA3
#define BCK  PA5
#define DATA PA7

uint16_t hData, lData;

// A full cycle, 16-bit, 2's complement Sine wave lookup table
uint16_t sine_table[256] = {

   0x0000, 0x0324, 0x0647, 0x096a, 0x0c8b, 0x0fab, 0x12c8, 0x15e2, 
   0x18f8, 0x1c0b, 0x1f19, 0x2223, 0x2528, 0x2826, 0x2b1f, 0x2e11,
   0x30fb, 0x33de, 0x36ba, 0x398c, 0x3c56, 0x3f17, 0x41ce, 0x447a, 
   0x471c, 0x49b4, 0x4c3f, 0x4ebf, 0x5133, 0x539b, 0x55f5, 0x5842,
   0x5a82, 0x5cb4, 0x5ed7, 0x60ec, 0x62f2, 0x64e8, 0x66cf, 0x68a6, 
   0x6a6d, 0x6c24, 0x6dca, 0x6f5f, 0x70e2, 0x7255, 0x73b5, 0x7504,
   0x7641, 0x776c, 0x7884, 0x798a, 0x7a7d, 0x7b5d, 0x7c29, 0x7ce3, 
   0x7d8a, 0x7e1d, 0x7e9d, 0x7f09, 0x7f62, 0x7fa7, 0x7fd8, 0x7ff6,
   0x7fff, 0x7ff6, 0x7fd8, 0x7fa7, 0x7f62, 0x7f09, 0x7e9d, 0x7e1d, 
   0x7d8a, 0x7ce3, 0x7c29, 0x7b5d, 0x7a7d, 0x798a, 0x7884, 0x776c,
   0x7641, 0x7504, 0x73b5, 0x7255, 0x70e2, 0x6f5f, 0x6dca, 0x6c24, 
   0x6a6d, 0x68a6, 0x66cf, 0x64e8, 0x62f2, 0x60ec, 0x5ed7, 0x5cb4,
   0x5a82, 0x5842, 0x55f5, 0x539b, 0x5133, 0x4ebf, 0x4c3f, 0x49b4, 
   0x471c, 0x447a, 0x41ce, 0x3f17, 0x3c56, 0x398c, 0x36ba, 0x33de,
   0x30fb, 0x2e11, 0x2b1f, 0x2826, 0x2528, 0x2223, 0x1f19, 0x1c0b, 
   0x18f8, 0x15e2, 0x12c8, 0x0fab, 0x0c8b, 0x096a, 0x0647, 0x0324,
   0x0000, 0xfcdc, 0xf9b9, 0xf696, 0xf375, 0xf055, 0xed38, 0xea1e, 
   0xe708, 0xe3f5, 0xe0e7, 0xdddd, 0xdad8, 0xd7da, 0xd4e1, 0xd1ef,
   0xcf05, 0xcc22, 0xc946, 0xc674, 0xc3aa, 0xc0e9, 0xbe32, 0xbb86, 
   0xb8e4, 0xb64c, 0xb3c1, 0xb141, 0xaecd, 0xac65, 0xaa0b, 0xa7be,
   0xa57e, 0xa34c, 0xa129, 0x9f14, 0x9d0e, 0x9b18, 0x9931, 0x975a, 
   0x9593, 0x93dc, 0x9236, 0x90a1, 0x8f1e, 0x8dab, 0x8c4b, 0x8afc,
   0x89bf, 0x8894, 0x877c, 0x8676, 0x8583, 0x84a3, 0x83d7, 0x831d, 
   0x8276, 0x81e3, 0x8163, 0x80f7, 0x809e, 0x8059, 0x8028, 0x800a,
   0x8000, 0x800a, 0x8028, 0x8059, 0x809e, 0x80f7, 0x8163, 0x81e3, 
   0x8276, 0x831d, 0x83d7, 0x84a3, 0x8583, 0x8676, 0x877c, 0x8894,
   0x89bf, 0x8afc, 0x8c4b, 0x8dab, 0x8f1e, 0x90a1, 0x9236, 0x93dc, 
   0x9593, 0x975a, 0x9931, 0x9b18, 0x9d0e, 0x9f14, 0xa129, 0xa34c,
   0xa57e, 0xa7be, 0xaa0b, 0xac65, 0xaecd, 0xb141, 0xb3c1, 0xb64c, 
   0xb8e4, 0xbb86, 0xbe32, 0xc0e9, 0xc3aa, 0xc674, 0xc946, 0xcc22,
   0xcf05, 0xd1ef, 0xd4e1, 0xd7da, 0xdad8, 0xdddd, 0xe0e7, 0xe3f5, 
   0xe708, 0xea1e, 0xed38, 0xf055, 0xf375, 0xf696, 0xf9b9, 0xfcdc,
};

void setup() {
  Serial.begin(19200);
  delay(100);
  Serial.println("-= I2S Example =-");
  // Setup SPI 1
  SPI.begin(); //Initialize the SPI_1 port.
  SPI.setBitOrder(MSBFIRST); // Set the SPI_1 bit order
  SPI.setDataMode(SPI_MODE0); //Set the  SPI_2 data mode 0
  SPI.setClockDivider(SPI_CLOCK_DIV16); // Slow speed (72 / 16 = 4.5 MHz SPI_1 speed)
  pinMode(WS, OUTPUT); //Set the Word Select pin (WS) as output.
}

void loop() {
  uint16_t i;
  
  for (i=0;i<256;i++){
    lData = sine_table[i];
    hData = sine_table[i];
    hData >>= 8;
    
    digitalWrite(WS, LOW);  //Select RIGHT Audio channel
    SPI.transfer(hData);    // Data bits 15-8
    SPI.transfer(lData);    // Data bits 7-0

    digitalWrite(WS, HIGH); //Select LEFT Audio channel
    SPI.transfer(i);        //
    SPI.transfer(0);        //
  }
}
EDIT: Corrected some typos in the example description.
Attachments
SineWave.jpg
SineWave.jpg (42.65 KiB) Viewed 10520 times
SawTooth.jpg
SawTooth.jpg (41.95 KiB) Viewed 10520 times
Last edited by Vassilis on Sat Aug 29, 2015 10:57 am, edited 2 times in total.

madias
Posts: 813
Joined: Mon Apr 27, 2015 11:26 am
Location: Vienna, Austria

Re: I2S example code

Post by madias » Fri Aug 28, 2015 8:39 pm

You are great, Vassilis!
Thank you very much!
I soldered two another of my (guess about 50+ pcs) PT8211's:
Image
And the result: It's working!
Maybe if we cannot get the "real I2s" working, this could be a workaround. Just setting the SPI write routines on a timer, some phase accumulators for each voice combined with my USB-MIDI-branch (or normally serial MIDI - the standard lib works great) and ready is the basic synth :)
So with 2 SPI we can get 4 independent outputs (for analog filters)

Something different: Has somebody got SPI3 working on any device? I see in the board.h file, that there are some troubles
* Note:
* SPI3 is unusable due to pin 43 (PB4) and NRST tie-together :(, but
* leave the definitions so as not to clutter things up. This is only
* OK since RET6 Ed. is specifically advertised as a beta board. */
Is this an actual entry (I guess no, because everything we have done, we have signed with our names)
Fact is, even with the new SPI library example, SPI3 wont work for me.

Some updates to real I2S:
I've merged again some of the mubase code https://github.com/mubase/STM32F4-Ardui ... m32f4codec
but I wont get even a simple master clock out of my MCU....maybe there are too much register differences between F4 and F1...
Last edited by madias on Fri Aug 28, 2015 9:26 pm, edited 1 time in total.

madias
Posts: 813
Joined: Mon Apr 27, 2015 11:26 am
Location: Vienna, Austria

Re: I2S example code

Post by madias » Fri Aug 28, 2015 9:10 pm

So, it's time for some fun:
With this sketch the demo is modified for playing a Waldorf-Format wavetable: 64 tables, each table with 128 entries.
It plays the sound "zero" with different speeds.
Have fun!

For experts:
I make my wavetables with the program "audio-term" https://www.youtube.com/watch?v=cPSjMORs39o
With the output of the program (standard Waldorf Blofeld wavetable format) I wrote my own processing sketch to translate and exclude the audio data from the sysex file (this drove me nuts: raw signed 21bit audio format and much more pitfalls, but I've done it ;) )
Attachments
I2s-PT8211-pseudo-i2s_WT.zip
(22.35 KiB) Downloaded 261 times

User avatar
martinayotte
Posts: 1229
Joined: Mon Apr 27, 2015 1:45 pm

Re: I2S example code

Post by martinayotte » Sat Aug 29, 2015 1:51 am

Ok, guys !
I got me in your bandwagon, I've just ordered some PT8211 on eBay ! :)
There are so cheap !

victor_pv
Posts: 1681
Joined: Mon Apr 27, 2015 12:12 pm

Re: I2S example code

Post by victor_pv » Sat Aug 29, 2015 5:04 am

madias wrote:You are great, Vassilis!
Thank you very much!
I soldered two another of my (guess about 50+ pcs) PT8211's:
Image
And the result: It's working!
Maybe if we cannot get the "real I2s" working, this could be a workaround. Just setting the SPI write routines on a timer, some phase accumulators for each voice combined with my USB-MIDI-branch (or normally serial MIDI - the standard lib works great) and ready is the basic synth :)
So with 2 SPI we can get 4 independent outputs (for analog filters)

Something different: Has somebody got SPI3 working on any device? I see in the board.h file, that there are some troubles
* Note:
* SPI3 is unusable due to pin 43 (PB4) and NRST tie-together :(, but
* leave the definitions so as not to clutter things up. This is only
* OK since RET6 Ed. is specifically advertised as a beta board. */
Is this an actual entry (I guess no, because everything we have done, we have signed with our names)
Fact is, even with the new SPI library example, SPI3 wont work for me.

Some updates to real I2S:
I've merged again some of the mubase code https://github.com/mubase/STM32F4-Ardui ... m32f4codec
but I wont get even a simple master clock out of my MCU....maybe there are too much register differences between F4 and F1...
It seems like that reference is for Leaflabs RET6 board, so I dont see a reason SPI3 should not work in any other board where pb4 is not tied to NRST.
That said, leaflabs may have never tested SPI3, so there is a small chance they didn't catch perhaps a typo in one of the registers definitions, so I would check the files with the SPI3 definitions and make sure everything fits with the datasheet.
About the SPI routines in a timer I would do this:
-Use a timer channel that maps to the same DMA channel as the SPI port to be used.
-Set the SPI port DMA, but do not enable the SPI TX DMA request, instead enable the DMA request for the that timer channel.
-Set the timer to preload, OC etc so it flips at the desired frequency.
-Enable the pin output for that channel compare, so the pin flips with the OC.
Result, on every time the OC flips from high to low, and back to high, a DMA request is issued and the DMA controller loads a new value in the SPI TX register, at the same time the channel pin takes the low or high value, to signal left or right to the DAC, all synchronized at whatever frequency you set your timer.
The SPI port can be set to 16bit mode, and the DMA transfer set to 16 bit transfer, so 1 DMA request loads a 16 bit value in the SPI port to be sent.

madias
Posts: 813
Joined: Mon Apr 27, 2015 11:26 am
Location: Vienna, Austria

Re: I2S example code

Post by madias » Sat Aug 29, 2015 7:44 am

Thanks for your inputs, victor!
SPI3: Is now working, see thread: viewtopic.php?f=3&t=521
Following things I've implemented:
16Bit data transfer (remember that SPI.setDataSize must be the last SPI config line ;) )
setting up the whole sound engine on timer3 (same timer as DMA 1 SPI1_TX)
The DMA stuff is totally new to me, I've to think and learn about it.... but must be easily to be implemented in the new code below

Code: Select all

/**
    I2S example code (modified by Matthias Diro: 16bit transfer, setting up on timer3)

    Description:
    This I2S example creates a Sine waveform on the RIGHT Audio channel of PT8211
    and a Sawtooth waveform on the LEFT Audio channel.

    This is a very simple how-to-use an external I2S DAC example (DAC = Digital to Analog Converter).

    Created on 27 Aug 2015 by Vassilis Serasidis
    email:  avrsite@yahoo.gr

    Connections between PT8211 DAC and the STM32F103C8T6
    WS    <-->  PA3
    BCK   <-->  PA5 <-->  BOARD_SPI1_SCK_PIN
    DIN   <-->  PA7 <-->  BOARD_SPI1_MOSI_PIN
*/


#include <SPI.h>

#define WS   PA3
#define BCK  PA5
#define DATA PA7

// A full cycle, 16-bit, 2's complement Sine wave lookup table
uint16_t sine_table[256] = {

  0x0000, 0x0324, 0x0647, 0x096a, 0x0c8b, 0x0fab, 0x12c8, 0x15e2,
  0x18f8, 0x1c0b, 0x1f19, 0x2223, 0x2528, 0x2826, 0x2b1f, 0x2e11,
  0x30fb, 0x33de, 0x36ba, 0x398c, 0x3c56, 0x3f17, 0x41ce, 0x447a,
  0x471c, 0x49b4, 0x4c3f, 0x4ebf, 0x5133, 0x539b, 0x55f5, 0x5842,
  0x5a82, 0x5cb4, 0x5ed7, 0x60ec, 0x62f2, 0x64e8, 0x66cf, 0x68a6,
  0x6a6d, 0x6c24, 0x6dca, 0x6f5f, 0x70e2, 0x7255, 0x73b5, 0x7504,
  0x7641, 0x776c, 0x7884, 0x798a, 0x7a7d, 0x7b5d, 0x7c29, 0x7ce3,
  0x7d8a, 0x7e1d, 0x7e9d, 0x7f09, 0x7f62, 0x7fa7, 0x7fd8, 0x7ff6,
  0x7fff, 0x7ff6, 0x7fd8, 0x7fa7, 0x7f62, 0x7f09, 0x7e9d, 0x7e1d,
  0x7d8a, 0x7ce3, 0x7c29, 0x7b5d, 0x7a7d, 0x798a, 0x7884, 0x776c,
  0x7641, 0x7504, 0x73b5, 0x7255, 0x70e2, 0x6f5f, 0x6dca, 0x6c24,
  0x6a6d, 0x68a6, 0x66cf, 0x64e8, 0x62f2, 0x60ec, 0x5ed7, 0x5cb4,
  0x5a82, 0x5842, 0x55f5, 0x539b, 0x5133, 0x4ebf, 0x4c3f, 0x49b4,
  0x471c, 0x447a, 0x41ce, 0x3f17, 0x3c56, 0x398c, 0x36ba, 0x33de,
  0x30fb, 0x2e11, 0x2b1f, 0x2826, 0x2528, 0x2223, 0x1f19, 0x1c0b,
  0x18f8, 0x15e2, 0x12c8, 0x0fab, 0x0c8b, 0x096a, 0x0647, 0x0324,
  0x0000, 0xfcdc, 0xf9b9, 0xf696, 0xf375, 0xf055, 0xed38, 0xea1e,
  0xe708, 0xe3f5, 0xe0e7, 0xdddd, 0xdad8, 0xd7da, 0xd4e1, 0xd1ef,
  0xcf05, 0xcc22, 0xc946, 0xc674, 0xc3aa, 0xc0e9, 0xbe32, 0xbb86,
  0xb8e4, 0xb64c, 0xb3c1, 0xb141, 0xaecd, 0xac65, 0xaa0b, 0xa7be,
  0xa57e, 0xa34c, 0xa129, 0x9f14, 0x9d0e, 0x9b18, 0x9931, 0x975a,
  0x9593, 0x93dc, 0x9236, 0x90a1, 0x8f1e, 0x8dab, 0x8c4b, 0x8afc,
  0x89bf, 0x8894, 0x877c, 0x8676, 0x8583, 0x84a3, 0x83d7, 0x831d,
  0x8276, 0x81e3, 0x8163, 0x80f7, 0x809e, 0x8059, 0x8028, 0x800a,
  0x8000, 0x800a, 0x8028, 0x8059, 0x809e, 0x80f7, 0x8163, 0x81e3,
  0x8276, 0x831d, 0x83d7, 0x84a3, 0x8583, 0x8676, 0x877c, 0x8894,
  0x89bf, 0x8afc, 0x8c4b, 0x8dab, 0x8f1e, 0x90a1, 0x9236, 0x93dc,
  0x9593, 0x975a, 0x9931, 0x9b18, 0x9d0e, 0x9f14, 0xa129, 0xa34c,
  0xa57e, 0xa7be, 0xaa0b, 0xac65, 0xaecd, 0xb141, 0xb3c1, 0xb64c,
  0xb8e4, 0xbb86, 0xbe32, 0xc0e9, 0xc3aa, 0xc674, 0xc946, 0xcc22,
  0xcf05, 0xd1ef, 0xd4e1, 0xd7da, 0xdad8, 0xdddd, 0xe0e7, 0xe3f5,
  0xe708, 0xea1e, 0xed38, 0xf055, 0xf375, 0xf696, 0xf9b9, 0xfcdc,
};
HardwareTimer timer(3);
void setup() {

  SPI.begin(); //Initialize the SPI_1 port.
  SPI.setBitOrder(MSBFIRST); // Set the SPI_1 bit order
  SPI.setDataMode(SPI_MODE0); //Set the  SPI_2 data mode 0
  SPI.setClockDivider(SPI_CLOCK_DIV4); //  speed (72 / 4 = 18 MHz SPI_1 speed) PT8211 up to 20MHZ clock
  pinMode(WS, OUTPUT); //Set the Word Select pin (WS) as output.
  SPI.setDataSize (SPI_CR1_DFF); // setting SPI data size to 16Bit

  // timer3 setup
  timer.pause(); // Pause the timer while we're configuring it
  timer.setPeriod(1); // // Set up period in microseconds
  timer.setChannel1Mode(TIMER_OUTPUT_COMPARE); // Set up an interrupt on channel 1
  timer.setCompare(TIMER_CH1, 1);  // Interrupt 1 count after each update
  timer.attachCompare1Interrupt(timer3_irq);
  timer.refresh();  // Refresh the timer's count, prescale, and overflow
  timer.resume(); // Start the timer counting
}
byte counter=0;
void loop() {

}

void timer3_irq(void) {
   digitalWrite(WS, LOW);  //Select RIGHT Audio channel
    SPI.write(sine_table[counter]);
    digitalWrite(WS, HIGH); //Select LEFT Audio channel
    uint16_t ch2=counter<<8;
    SPI.write(ch2);
    counter++;
}
Last edited by madias on Sat Aug 29, 2015 11:26 am, edited 1 time in total.

madias
Posts: 813
Joined: Mon Apr 27, 2015 11:26 am
Location: Vienna, Austria

Re: I2S example code

Post by madias » Sat Aug 29, 2015 10:48 am

Meanwhile a little <edit> Mario melody with correct pitch (I transfered it from my PIC32, so there is huge overhead in the code left)
Image
As you can see 4.25us are used from 21us, so the audio routine takes about 20%, maybe this can be better with DMA...
Attachments
I2s-PT8211-timer_16bit_synth.zip
(3.24 KiB) Downloaded 140 times

victor_pv
Posts: 1681
Joined: Mon Apr 27, 2015 12:12 pm

Re: I2S example code

Post by victor_pv » Sat Aug 29, 2015 12:57 pm

madias wrote:Meanwhile a little <edit> Mario melody with correct pitch (I transfered it from my PIC32, so there is huge overhead in the code left)
Image
As you can see 4.25us are used from 21us, so the audio routine takes about 20%, maybe this can be better with DMA...
Edit. That is the left/right pin output right?
Then yes, the output routine is taking 20% of your time. If it was all set with the timer and DMA would take basically 0% once fired up, as the DMA and timer run by themselves, the only CPU time needed would be to update the output table.
For DMA setup, have a look at the libmaple documentation, and the SPI DMA transfer functions we added. The setup would be the same, except instead of enabling the SPITX DMA request, you enable the timer DMA request, for a timer channel in the same DMA channel.
The DMA controller does not care which device in that channel fires the DMA, it just starts 1 transfer every time it gets a request.
If I remember right the timer can be set in up/down count mode, and to toggle the output pin on each time it reaches 0, or the upper limit. So that way it is left free running toggling the pin at the frequency you want, and would trip a DMA request each time, so you get one for right, next one for left, and so on.
You will need to manipulate the timer registers directly as libmaple does not include functions for a lot of the advanced stuff, but the registers are all defined.
On your RAM buffer you will have to interleave the left and right channels.
Finally the DMA transfer can be set to generate an interrupt at half way, and when it finish all the bytes.
You can use both to treat the buffer as 2 half buffers, so when the half irq is called, you know you can refill the first half of the buffer, and when the end of transfer once trips, you can refill the top half.
Ahh, and the DMA can be set in circular mode if you want the same buffer to keep playing continuously. So you can play the same melofy over and over, but even better you can have the DMA play the same buffer over and over as you update the buffer with new sections of a melofy you can read from an SD card or network, or generate real time.

madias
Posts: 813
Joined: Mon Apr 27, 2015 11:26 am
Location: Vienna, Austria

Re: I2S example code

Post by madias » Sat Aug 29, 2015 10:07 pm

Thanks again Victor. As I said, the DMA stuff is unknown territory for me, so I have to learn the basics of DMA first.
Found a good tutorial for SPI here: http://polplaiconesa.com/tutorials/SPI+DMA.html
The leaflabs wiki is nice written, but without any examples (ok, one usart example). http://wiki.leaflabs.com/index.php?title=DMA
Offtopic: (or not so offtopic...)
Meanwhile I shredded all my 10pcs printed analog-filter boards (from dirty cheap pcb's) ---> too much errors/mistakes (and a complete unrepairable analog switch section) This draws me back at least for an half year for my master project. Ok, I call it "rising learning curve". All I know is, that I will never use the silly program "fritzing" anymore. I'm now on Kicad and it looks like as a really alternative for eagle. (I cannot use eagle free license, because I need 100x100 and not only 80x100..)
But there are good news to myself: At the beginning I planned to use a STM32 MCU only for controlling my synth (TFT, encoder, knobs, USB-MIDI) and as audio-unit-MCU a PIC32MX250. Drawback: two different territories and upload methods in the final product. Meanwhile I'll take a bigger STM32 MCU (RET,VET) as a single version for everything. (2x SPI/I2s=4 ind. 16-bit voice outputs, 1x SPI for the rest: TFT, ext. control DAC's like LT1665,...) Drawback is the lack of a FPU unit in the STMF1 series. I've done benchmark tests and every test says: "AVOID float for every costs!". Ok, I have learned to use fix point arithmetic's....maybe in one or two years I'll on STM32F7.

madias
Posts: 813
Joined: Mon Apr 27, 2015 11:26 am
Location: Vienna, Austria

Re: I2S example code

Post by madias » Sun Aug 30, 2015 12:04 am

ok, forgot some infos for victor:
The picture shows a dummy pin, I set it up high on output-routine start and low on the end. So I got the 20% usage. This also helps me finding the right tuning frequency: 47,62 kHz in my example (instead of 48khz..).

Post Reply