STM32F411 I2S tone generator [SOLVED] - Page 1
Forum section: electronics / general

author: DELTA67
author_rank: Regular Contributor
timestamp: February 14, 2025, 12:41:50 pm
content: Hi all,I need an I2S tone generator to test an I2S DAC (UDA1334A) and an I2S Class-D amp (MAX9835), so I'm trying to write a bare metal program for the STM32F411 (Black Pill). Using the RM0383 I'm struggling with the configuration of the SPI2/I2S2 module.Here's a summary of my understanding, please tell me if I'm missing some thing:1- Configuring the PLLI2S:- configuration of the division factors M and R and the multiplication factor N in RCC_PLLI2SCFGR register before enabling the PLLI2S. Values are given in table page 594 for differnet sampling frequencies.- Select PLLI2S as I2S clock source in RCC_CR register (default).- Enable the PLLI2S clock in RCC_CR register and wait until PLLI2S is ready.2- Configuring the SPI2 module as I2S:- Enable the SPI2/I2S2 clock in RCC_APB1ENR register.- SPI2/I2S2 must be Disabled before configuring- Configure PLLI2SS Prescaler for the given sampling frequency in SPI2_I2SPR register (table page 594).- In SPI2_I2SCFGR configure I2S in Master Transmit mode, Philips format, 16bit data wide, clock steady state low- Finaly I2S capability is enabled by setting the MOD bit in the SPI_I2SCFGR register.3- Send data over I2S:Wait until transmit buffer is empty (TXE in SPI2_SR set) then write the 16bit data in SPI2_DR.NOTES:- Th I2S interface uses almost the same pins, flags and interrupts as the SPI.SD: Serial Data mapped on the MOSI pin (PB15 for I2S2).WS: Word Select mapped on the NSS pin (PB12).CK: Serial Clock mapped on the SCK pin (PB13).- each time a 16bit data is written to SPI_DR, the TXE flag is set and its interrupt, if allowed, is generated to load the new data to send.- The PLLI2S uses the same input clock as the main PLL (HSI 16MHz or HSE 25MHz in case of Black Pill).I've joined the full project. Here are some code snippets:Code:void i2s_init(){RCC->AHB1ENR |= 1<<1;// enable GPIOB clock// Set PB12, PB13, PB15 as Alternate Function modeGPIOB->MODER &= ~(3 << (12 * 2) | 3 << (13 * 2) | 3 << (15 * 2));GPIOB->MODER |= 2 << (12 * 2) | 2 << (13 * 2) | 2 << (15 * 2);// Configure PB12, PB13, PB15 as Alternate Function I2S2 (AF5)GPIOB->AFRH &= ~(0xF << (12 - 8) * 4 | 0xF << (13 - 8) * 4 | 0xF << (15 - 8) * 4);GPIOB->AFRH |= 5 << ((12 - 8) * 4) | 5 << ((13 - 8) * 4) | 5 << ((15 - 8) * 4);//For 25MHz crystal and sampling Freq = 8KHz:RCC->PLLI2SCFGR = 0; // clearRCC->PLLI2SCFGR |= 2 << 28 | 192 << 6 | 25 << 0; // see table page 594//RCC->CFGR &= ~(1<<23); // I2SSRC (Bit23) = 0 => PLLI2S used as I2S clock// Enable the PLLI2S clockRCC->CR |= 1 << 26; // Enable PLLI2Swhile (!(RCC->CR & (1 << 27))); // Wait until PLLI2S is readyRCC->APB1ENR |= (1 << 14); // Enable SPI2/I2S2 clockSPI2->I2SCFGR = 0; // Disable I2S before configuring// Config PLLI2SS Prescaler for FS=8KHz (see table page 594)SPI2->I2SPR = 1 << 8 | 187<< 0; // ODD (bit8) = 1, I2SDIV (Bits7:0) = 187// Configure I2S in Master Transmit modeSPI2->I2SCFGR = 1<<11; // I2SMOD (Bit11) = 1 => I2S modeSPI2->I2SCFGR |= 2<<8; // I2SCFG (Bits9:8) = 10 => Master - transmit//SPI2->I2SCFGR &= ~(3<<4); // I2SSTD (Bits5:4) = 00 => I2S Philips standard//SPI2->I2SCFGR &= ~(1<<3); // CKPOL (Bit3) = 0 => I2S clock steady state is low level//SPI2->I2SCFGR &= ~(3<<1); // DATLEN (Bits2:1) = 00 => 16-bit data length//SPI2->I2SCFGR &= ~(1<<0); // CHLEN (Bit0) = 0 => 16-bit wideSPI2->I2SCFGR |= 1<<10; // I2SE (Bit10) = 1 => Enable I2S}To send an arbitrary data over I2S:Code:for(;;){// Wait until transmit buffer is emptywhile (!(SPI2->SR & (1 << 1)));// TXE (Bit1) = 0 => Tx buffer not emptySPI2->DR = 0xCCCC; // arbitrary 16bit value}I'm supposed to get a nice square waves on PB12, PB13 and PB15 with SCK frequency:F = 16 x 8000 x 2 = 256KHzThe PC13 LED blinks but nothing on PB12, 13, 15 !!Alas the STM32F411 on the Black Pill have no MCO2 output to test if the PLLI2S is working or not.I need your help please.Thanks

author: pcprogrammer
author_rank: Super Contributor
timestamp: February 14, 2025, 01:05:37 pm
content: I have not looked to deep at your code, but my advice is to first write yourself a function to set an io pin. This will clean up the code big time.Second, although the hal.h idea is nice, in my opinion it is better to create peripheral dedicated files with defines for the settings, to again make things more readable.Third, there is no need for the assembly code to do the startup.You can find examples in my attached code. It is a bit messy and far from finished work, but it gives you a bit of an idea.

author: DELTA67
author_rank: Regular Contributor
timestamp: February 14, 2025, 01:25:52 pm
content: Thanks pcprogrammer for the advices. We can also use the standard peripheral library provided by ST.Yes, ARM CORTEX-M are C freindly, we can do the startup code in C easily.My real problem is with I2S module configuration.

author: pcprogrammer
author_rank: Super Contributor
timestamp: February 14, 2025, 01:53:25 pm
content: Quote from: DELTA67 on February 14, 2025, 01:25:52 pmThat is where I get the data from to create my own .h files.Quote from: DELTA67 on February 14, 2025, 01:25:52 pmI get that, but it is harder for someone else to look at your code and directly spot why things don't work. For every IO pin there is the need to check the code on correct parameters being shifted in, where as with a bit more structured code it is much simpler to exclude possible errors.I myself just started using the F411 with the project I provided in my previous post, and I have only done I2S on the F103 so far, which is different, at least when it comes to the clock.It should not be to difficult to get it going. Check the manual again on what should be set, and maybe look into using the ST-link to do debugging. It at times helps with finding the problem, but it is not the holy grail.

author: DELTA67
author_rank: Regular Contributor
timestamp: February 14, 2025, 02:07:07 pm
content: Quote from: pcprogrammer on February 14, 2025, 01:53:25 pmIn my code each bit used is explained in the comments with it's name, position and function.Example:Code:// Configure I2S in Master Transmit modeSPI2->I2SCFGR = 1<<11; // I2SMOD (Bit11) = 1 => I2S modeSPI2->I2SCFGR |= 2<<8; // I2SCFG (Bits9:8) = 10 => Master - transmitI'll do some debugging by printing the contents of some registers via UART.

author: DELTA67
author_rank: Regular Contributor
timestamp: February 14, 2025, 03:25:54 pm
content: First bug detected:SPI2 base address is 0x40003800 NOT 0x40013800Here are the contents of some registers after calling i2s_init():GPIOB->MODER: 0x8A000280GPIOB->AFRH: 0x50550000PB12, 13 and 15 are configured as expectedRCC->PLLI2SCFGR: 0x20003019PLLI2SR[30:28] = 2, PLLI2SN[8:0] = 192, PLLI2SM[5:0] = 25 as expectedRCC->CR: 0x0F038183PLLI2SRDY (Bit27) =1, PLLI2SON (Bit26) =1 as expectedRCC->CFGR: 0x0000100AI2SSRC (Bit23) = 0 as expectedSPI2->I2SCFGR: 0x0E00I2SMOD (Bit11) = 1, I2SCFG [Bits9:8] = 10, I2SSTD (Bits5:4) = 00, .... as expectedSPI2->I2SPR: 0x01BBODD (bit8) = 1, I2SDIV[7:0] = 187 as expected

author: pcprogrammer
author_rank: Super Contributor
timestamp: February 14, 2025, 04:10:09 pm
content: Quote from: DELTA67 on February 14, 2025, 02:07:07 pmThat may well be, but one would still have to get the manual and check if that is correct. Sure, it is the same when it is defined in a .h file, that it still can be incorrect. You having the base address incorrect is prove of this.

author: DELTA67
author_rank: Regular Contributor
timestamp: February 14, 2025, 04:38:23 pm
content: WOW it's working. A nice square waves on PB12 (8KHz), PB13 (256KHz) and PB15 (64KHz).When debugging I've forgotten to send data to the I2S!!!!!So the bug was a wrong base address for SPI2.The next step is generating the tone using a sine LUT.How to generate a LUT for exactly 1 periode for a given frequency sampled at 8KHz?

author: pcprogrammer
author_rank: Super Contributor
timestamp: February 14, 2025, 04:46:59 pm
content: Look at my drum box project I posted in one of your older threads. There is a quarter sine table in there. No need for a full one, but if you want it, it just takes four times the memory.With a sample rate of 8kHz you need 8000 samples for a single period. So your sine step is 360/8000.See my spreadsheet file I used for calculation on the sine table for the drum box project.

author: DELTA67
author_rank: Regular Contributor
timestamp: February 14, 2025, 04:59:54 pm
content: Quote from: pcprogrammer on February 14, 2025, 04:46:59 pmFor a single periode or for 1second?

author: pcprogrammer
author_rank: Super Contributor
timestamp: February 14, 2025, 05:08:01 pm
content: Quote from: DELTA67 on February 14, 2025, 04:59:54 pmIt is the same. 8000 samples at a 8kHz sample rate means one second, and outputting a single period in that time means 1Hz of output frequency.

author: DELTA67
author_rank: Regular Contributor
timestamp: February 14, 2025, 06:02:39 pm
content: OK, I've generated a LUT for a 200Hz tone using an AI chatbot.Code:// 200Hz stero tone sampled at 8000Hzuint16_t sine_table = {0, 0, 5125, 5125, 10125, 10125, 14875, 14875, 19259, 19259, 23169, 23169, 26509,26509, 29195, 29195, 31163, 31163, 32363, 32363, 32767, 32767, 32363, 32363,31163, 31163, 29195, 29195, 26509, 26509, 23169, 23169, 19259, 19259, 14875, 14875,10125, 10125, 5125, 5125, 0, 0, -5125, -5125, -10125, -10125, -14875, -14875, -19259,-19259, -23169, -23169, -26509, -26509, -29195, -29195, -31163, -31163, -32363, -32363,-32767, -32767, -32363, -32363, -31163, -31163, -29195, -29195, -26509, -26509, -23169,-23169, -19259, -19259, -14875, -14875, -10125, -10125, -5125, -5125};I'll try it ASAP.Code:void send_data( ){short i=0, sample;for(;;){sample = sine_table[i++];// Wait until transmit buffer is emptywhile (!(SPI2->SR & (1 << 1)));// TXE (Bit1) = 1 => Tx buffer emptySPI2->DR = sample;if(i==80) i=0;}}

author: DELTA67
author_rank: Regular Contributor
timestamp: February 14, 2025, 06:49:31 pm
content: Tested with UDA1334A OK but there's a lot of distorsion. I'm using a headphone for test.//left channel onlyuint16_t sine_table = {0, 0, 5125, 0, 10125, 0, 14875, 0, 19259, 0, 23169, 0, 26509, 0, 29195, 0, 31163, 0, 32363, 0,32767, 0, 32363, 0, 31163, 0, 29195, 0, 26509, 0, 23169, 0, 19259, 0, 14875, 0, 10125, 0, 5125,0, 0, 0, -5125, 0, -10125, 0, -14875, 0, -19259, 0, -23169, 0, -26509, 0, -29195, 0, -31163, 0,-32363, 0, -32767, 0, -32363, 0, -31163, 0, -29195, 0, -26509, 0, -23169, 0, -19259, 0, -14875, 0,-10125, 0, -5125, 0};// right channel onlyuint16_t sine_table = {0, 0, 0, 5125, 0, 10125, 0, 14875, 0, 19259, 0, 23169, 0, 26509, 0, 29195, 0, 31163, 0, 32363, 0,32767, 0, 32363, 0, 31163, 0, 29195, 0, 26509, 0, 23169, 0, 19259, 0, 14875, 0, 10125, 0, 5125,0, 0, 0, -5125, 0, -10125, 0, -14875, 0, -19259, 0, -23169, 0, -26509, 0, -29195, 0, -31163, 0, -32363,0, -32767, 0, -32363, 0, -31163, 0, -29195, 0, -26509, 0, -23169, 0, -19259, 0, -14875, 0, -10125, 0, -5125};

author: pcprogrammer
author_rank: Super Contributor
timestamp: February 14, 2025, 07:21:35 pm
content: Check your data. Unsigned 16 bit variable and the values given are signed. The compiler will make them unsigned, but I'm not sure how, and this might cause distortion.Best to use a scope to look at the signal. Will tell you what is wrong much quicker.

author: shabaz
author_rank: Frequent Contributor
timestamp: February 14, 2025, 07:35:38 pm
content: You could use Python or MATLAB to generate a tone in a WAV file, listen to it on your PC, and then when you're happy, use Python to convert it to a C array etc. It's just a few dozen lines of code.For instance, here's my code to convert WAV to C array:https://github.com/shabaz123/sound_fx_mixer/blob/main/wav_convert/wav_convert.pyMy code is not general-purpose, it is only tested with 24kbps 16-bit mono WAV files, since that was all I was interested in, but by looking at it, you'll get the idea. I'm sure there must be other code out there to do the same thing better.Incidentally that repo contains code to generate I2S with a Pi Pico (uses the Pico C SDK). If you have a Pi Pico lying around, it may well be quicker to look for an I2S project using that, since there's likely to be a lot of such projects, if the choice of microcontroller is just a means to an end and if you don't care about using an ST micro chip for your I2S signals.

author: DELTA67
author_rank: Regular Contributor
timestamp: February 14, 2025, 07:46:21 pm
content: Quote from: pcprogrammer on February 14, 2025, 07:21:35 pmWell seen!I've changed it to short sine_table[].I think a part of distortion is due to samples MAX volume, perhaps we need to reduce the volume. Another part is due to using a headphone, the UDA1335A is intended to drive an amplifier.Joined a new version using DMA to transfer samples from the LUT to the I2S.

author: DELTA67
author_rank: Regular Contributor
timestamp: February 14, 2025, 07:52:08 pm
content: Quote from: shabaz on February 14, 2025, 07:35:38 pmThanks shabaz for the hint.I've already tried generating a WAV file using C. Here I need only 1 periode.

author: DELTA67
author_rank: Regular Contributor
timestamp: February 14, 2025, 08:39:58 pm
content: The STM32F411 has 5 SPI/I2S periherals. If we configure 2 of them exactly the same, can we share the SCK and WS signals between external DACs as shown in the joined picture?

author: pcprogrammer
author_rank: Super Contributor
timestamp: February 15, 2025, 06:59:03 am
content: Quote from: DELTA67 on February 14, 2025, 08:39:58 pmI guess it will work as long as you load both transmit registers at nearly the same time.Another option, at least with SPI, would be to set the second one as a slave and feed the clock signals into the peripheral. Would still need to be loaded at the same time though.

author: DELTA67
author_rank: Regular Contributor
timestamp: February 15, 2025, 09:05:37 am
content: Quote from: pcprogrammer on February 15, 2025, 06:59:03 amI'm guessing so.Alas I don't have a logic analyzer to test this configuration.Now, what about the STM32F103C8 (Blue Pill) which has not a hardware I2S. Can we create one using bit-banging or via SPI?

author: pcprogrammer
author_rank: Super Contributor
timestamp: February 15, 2025, 09:43:40 am
content: Quote from: DELTA67 on February 15, 2025, 09:05:37 amI have to many projectsI did I2S on a F303 not a F103. An issue with the F303 is that there is no separate PLL for the I2S, which limits the sampling rate that can be used. It does have I2S though.To do it on a F103 you have the same clock problem. It should be possible with the SPI peripheral to make the clock and the data signal, but you need to toggle between left and right in software (interrupt)Fully bit banging would be slow and result in a low sampling rate.

author: DELTA67
author_rank: Regular Contributor
timestamp: February 15, 2025, 10:07:30 am
content: Quote from: pcprogrammer on February 15, 2025, 09:43:40 amCan you share your code for the F103 using SPI and interrupt please?In bit banging mode, what is the MAX sampling frequency we can achieve @72MHz?