Contents

I2S Audio Codec Interface with STM32H743

Interfacing an audio codec with a microcontroller is a common task in DSP and audio-related projects.

This post will summarize the basics steps that have to be taken in order to make an audio codec work with an STM32 microcontroller.

Most of the principles exposed here can be applied to any audio codec which uses i2s protocol and control registers for hardware configuration.

 

Introduction

In this case, the audio codec will be the WM8960, form Cirrus Electronics.

As a brief explanation, this piece of hardware is an integrated circuit with multiple features which allow the user to build a high-quality interface between continuous and discrete audio signals, bith forward and backwards.

Basically, its main purpose is receiving (encoding) audio signals and converting it to a data stream while, simultaneously, converting from digital to analog (decoding). Both tasks are done by using i2s protocol.

This i2s interface can be configured to meet specific project requirements as the sampling frequency, channel/pinout routing, muting/unmuting, power modes etc. For this IC, this configuration is made via i2c registers, adressing its integrated control interface.

In some other audio codecs which, usually are more simple, this configuration is done by setting certain pins at specific logic levels (High/Low).

The main benefit for having a dedicated control interface inside the audio codec is that it gives a vastly greater configuration of the internal features.

/i2s-audio-codec-interface-with-stm32h7/WM8960_BlockDiagram.png
Figure 1. Block diagram of the WM8960.

As can be seen in the block diagram, there are multiple processing and routing blocks embedded inside the IC.

Although it is possible to change the internal configuration while the program is actually running, the essential step that need to be taken is configuring its features when initializing the device.

That said, this could be a basic flow diagram of its configuration proccess.

/i2s-audio-codec-interface-with-stm32h7/Audio_Codec_Setup.jpg
Figure 1. Block diagram of the WM8960.

With this diagram, it is easy to know what must be done first, configuring the microcontroller’s peripherals.

Peripheral configuration

In every microcontroller, the different peripherals are configured using registers that are explained in detail in the manufacturer’s datasheet.

Doing this manually can be a very slow proccess, specially when you are new to a certain platform like, for instance, the c2000 MCUs from Texas Instruments.

STMicroelectronics, on the other hand, has developed a framework to ease the programming of their MCUs.

This framework is based on a code generator and a hardware abstraction layer (HAL). In this project, both tools will be used during the peripheral configuration.

/i2s-audio-codec-interface-with-stm32h7/Peripheral_Configuration.png
Figure 1. Screenshot of STM32CubeMX initial code generator.

i2s and DMA

We want our MCU to continuously receive and send digital audio streams in combination with the audio codec. To do so, the DMA will need to be set as well.

Basically, these are the important configuration parameters in the i2s1 (transmitter):

Parameter settings:

  • Mode: Half-Duplex Master Transmit
  • Comunication Standard: MSB First (Left Justified)
  • Data and Frame Format: 16 bits on 16 Bits Frame
  • Selected audio frequency: 44KHz (This is optional)

DMA settings: Add a DMA request (SPI1_TX, for example)

  • Stream: DMA1 Stream 1
  • Direction: Memory To Peripheral

GPIO Settings: We have three signals in the i2s protocol:

  • I2S1_WS (Word select, best known as LeftRightClock)
  • I2S1_CK (Clock)
  • I2S1_SDO (Data output)

A physical pin of the MCU needs to be assigned to every signal individually. This is personal and the best option is always to make a decision based on the layout of the PCB. This can be changed anytime.

For the receiver, another independent i2s needs to be configured. In this case, it will be the i2s. The configuration is almost identical, the only different parameters are those:

Transmitter settings:

  • In parameter settings, mode must be Half-Duplex Master Receive
  • We need to add another independent DMA request, so the DMA stream must be different. For example, DMA1 Stream 2
Note
Both i2s must be triggered by the same clock source. If they don’t work synchronously, some sample skipping and and processing problems may occur in further stages of the project. In this microcontroller the option is locked so it is not possible to screw things up, but in other MCUs it could be a different case.

NVIC Settings

In order to make a callback function that sets the synchronism between the processing and reading states of the system, the NVIC settings must be configured. We need to add an interrupt routine for each DMA stream, one for each i2s channel.

i2c

It is not necesary to change much of the settings here, but it is important to check a few things:

Parameter settings:

  • Primary Adress Length: 7-bit
  • Dual Adress Aknowledged: Disabled

GPIO Setings: The i2c protocol uses two wires to communicate. They need to be assigned to physical pins.

  • I2C1_SCL (Clock)
  • I2C1_SDA (Data, bidirectional)

Define audio buffers for i2s data streams

At this time, we need to define some buffers that will store the audio samples before and after the processing.

Beginning of main.c:

102
103
104
105
106
107
108
109
110

static volatile int16_t adc_buf_i2s[BUFFER_SIZE]; //ARRAY FOR AUDIO INPUT
static volatile int16_t dac_buf_i2s[BUFFER_SIZE]; //ARRAY FOR AUDIO OUTPUT

//POINTERS FOR PROCESSING PURPOSES  
static volatile int16_t *inBuf = &adc_buf_i2s[0];
static volatile int16_t *outBuf = &dac_buf_i2s[0];

uint8_t dataReadyFlag=0; //FLAG TO START PROCESSING

This processing flag will be set to 1 when the audio input buffer is half completed and full completed. The reason for this is to proccess the half of the buffer that has already been filled while the other half is being filled.

This technique is called Ping-Pong Buffering, and it is intended to ensure that the processing is sinchronized with the ADC and DAC conversions.

This function will be covered later.

Start Peripherals

Now, each peripheral needs to be initialized.

All those “Init” functions were written by the code generator, and they basically set the registers needed to match the configuration that has been decided before.

int main(void) function of main.c:

102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120

/* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_TIM3_Init();
  MX_I2S1_Init();
  MX_I2C1_Init();
  MX_I2S2_Init();

  /* USER CODE BEGIN 2 */

    HAL_TIM_Base_Start_IT(&htim3);  //THIS TIMER IS USED AS AN INTERRUPT FOR DEBUGGING PURPOSES

    HAL_I2S_Transmit_DMA(&hi2s1, (uint16_t*) dac_buf_i2s, BUFFER_SIZE); //START THE DMA AND STREAM DATA FROM THE OUTPUT BUFFER TO THE I2S PERIPHERAL
    HAL_I2S_Receive_DMA(&hi2s2, (uint16_t*) adc_buf_i2s, BUFFER_SIZE); //START THE DMA AND STREAM DATA FROM THE I2S PERIPHERAL TO THE INPUT BUFFER

    WM89060_Init(); //INITIALIZE THE AUDIO CODEC WITH ITS CORRESPONDING REGISTERS
  
  /* USER CODE END 2 */

Audio codec INIT

This function is responsible of writting all the registers needed in order to configure all the operation modes desired for the audio codec.

The register is written by calling the function WM8960_Write_Reg(reg, dat) where, “reg” is the register address that corresponds to an specific register and, “dat” is the actual value that is being written in that address.

All the addresses are referenced in the manufacturer’s datashet, as well as the possible configurations that we can write in the “dat” argument.

WM8960_init() function of main.c:

102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
uint8_t WM89060_Init(void)  {

  uint8_t res;
  
  //Reset Device
  res = WM8960_Write_Reg(0x0f, 0x0000);
  if(res != 0)
    return res;
  else
    printf("WM8960 reset completed !!\r\n");
  
  //Set Power Source
  res =  WM8960_Write_Reg(0x19, 1<<8 | 1<<7 | 1<<6);
  res += WM8960_Write_Reg(0x1A, 1<<8 | 1<<7 | 1<<6 | 1<<5 | 1<<4 | 1<<3);
  res += WM8960_Write_Reg(0x2F, 1<<3 | 1<<2);
  if(res != 0)  {
    printf("Source set fail !!\r\n");
    printf("Error code: %d\r\n",res);
    return res;
  }
  
  //Configure clock
  WM8960_Write_Reg(0x04, 0x0000); //MCLK->div1->SYSCLK->DAC/ADC sample Freq = 25MHz(MCLK)/2*256 = 48.8kHz
  
  //Configure audio interface settings
  WM8960_Write_Reg(0x07, 0x0002);  //I2S format 16 bits word length

  //Configure ADC/DAC De-Emphasis
  WM8960_Write_Reg(0x05, 0x0000); //No De-Emphasis
  
  //Input PGA 
  WM8960_Write_Reg(0x00, 0x003F | 0x0100);// Left channel PGA -> Change gain on zero cross and volume max (+30dB)
  WM8960_Write_Reg(0x01, 0x003F | 0x0100);// Right channel PGA -> Change gain on zero cross and volume max (+30dB)


  // ADCL/ADCR Signal Path
  WM8960_Write_Reg(0x20, 0x0000); //Left channel input PGA +0dB boost
  WM8960_Write_Reg(0x21, 0x0008 | 0x0100); //Right input PGA to Right input boost mixer + channel input PGA +0dB boost
  
  //Input Boost Mixer
  WM8960_Write_Reg(0x2B, 0x0000); //LINPUT3 & LINPUT2 to Boost Mixer gain = Muted
  WM8960_Write_Reg(0x2C, 0x0000); //RINPUT3 & RINPUT3 to Boost Mixer gain = Muted

  /*********ADC*********/

  //ADC Control 
  WM8960_Write_Reg(0x05, 0x000C); //ADC High Pass Filter + Digital Soft Mute 

  //ADC Digital Volume Control
  WM8960_Write_Reg(0x15, 0x00C3 | 0x0100); //Left ADC digital volume +0dB
  WM8960_Write_Reg(0x16, 0x00C3 | 0x0100); //Right ADC digital volume +0dB

  //Additional control
  WM8960_Write_Reg(0x17, 0x01C8); //Slow clock off + jack detect resp = slow +  ADC data routing Left=Left&Right=Right + DAC stereo + Low bias current (for 3v3) + Thermal shutdown on

  /*********ALC Control*********/
  //Noise Gate Control
  WM8960_Write_Reg(0x14, 0x00F9);
  
  //Configure HP_L and HP_R OUTPUTS
  WM8960_Write_Reg(0x02, 0x006F | 0x0100);  //LOUT1 Volume Set
  WM8960_Write_Reg(0x03, 0x006F | 0x0100);  //ROUT1 Volume Set
  
  //Configure DAC volume
  WM8960_Write_Reg(0x0a, 0x00FF | 0x0100);
  WM8960_Write_Reg(0x0b, 0x00FF | 0x0100);
  
  
  //Configure MIXER
  WM8960_Write_Reg(0x22, 1<<8 | 1<<7);
  WM8960_Write_Reg(0x25, 1<<8 | 1<<7);
  
  //Jack Detect
  WM8960_Write_Reg(0x18, 1<<6 | 0<<5);
  WM8960_Write_Reg(0x17, 0x01C3);
  WM8960_Write_Reg(0x30, 0x0009);//0x000D,0x0005
  
  return 0;
}

WM8960_Write_Reg() function of main.c:

102
103
104
105
106
107
108
109
110
111
112
113
114
uint8_t WM8960_Write_Reg(uint8_t reg, uint16_t dat)  {

  uint8_t res,I2C_Data[2];

  I2C_Data[0] = (reg<<1)|((uint8_t)((dat>>8)&0x0001));  //RegAddr
  I2C_Data[1] = (uint8_t)(dat&0x00FF);                  //RegValue

  res = HAL_I2C_Master_Transmit(&hi2c1,(WM8960_ADDRESS<<1),I2C_Data,2,10);
  if(res == HAL_OK)
    WM8960_REG_VAL[reg] = dat;

  return res;
}

WM8960_ReadReg() function of main.c:

102
103
104
105
106

uint16_t WM8960_Read_Reg(uint8_t reg) {

  return WM8960_REG_VAL[reg];
}

Buffer completion callbacks

Each of the two functions are intended to do two different tasks. The first one is changing the pointers loctaion to the position of the buffer that needs to be processed later. The second one is setting the DataReadyFlag to 1 so that the processing can start after the callback is finished.

HAL_I2SEx_TxRxHalfCpltCallback(I2S_HandleTypeDef *hi2s) function in main.c:

102
103
104
105
106
107
108
void HAL_I2SEx_TxRxHalfCpltCallback(I2S_HandleTypeDef *hi2s)
{
inBuf = &adcBuf_i2s[0];
outBuf = &adcBuf_i2s[0];

DataReadyFlag = 1;
}

HAL_I2SEx_TxRxCpltCallback(I2S_HandleTypeDef *hi2s) function in main.c:

102
103
104
105
106
107
108
void HAL_I2SEx_TxRxCpltCallback(I2S_HandleTypeDef *hi2s)
{
  inBuf = &adcBuf_i2s[BUFFER_SIZE/2];
  outBuf = &adcBuf_i2s[BUFFER_SIZE/2];

  DataReadyFlag = 1;
}

Infinite loop

This is the infinite loop. All the processing must be done here, by calling the Process_Data() function, where there will be one independent function for each filter that has to be executed.

while(1) function in main.c:

102
103
104
105
106
107
108
    while (1)
    {
    	if (dataReadyFlag==1)
    	{
    		processData();
    	}
    }

This function is intended to proccess half of the buffer each time is called. This is done to work with the Ping-Pong Buffering technique that was introduced earlier.

Once the proccessing is done, the DataReadyFlag is set to 0. This flag will remain in this state until it occurs a callback whenever the new half of the buffer has been filled, meaning that, again, it is time to proccess.

Process_Data() function in main.c:

102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
void processData(void)
{

    for (int16_t i = 0; i < BUFFER_SIZE/2; i++) //PROCESSING ONLY ONE HALF OF THE BUFFER
    {

      //ALL THE DSP ALGORITHMS FOR THE LEFT CHANNEL SHOULD BE CALLED HERE

      //AFTER THE LEFT SAMPLE IS PROCESSED, IT IS COPIED TO THE OUTPUT BUFFER AND BOTH POINTERS ADVANCE ONE POSITION
      	*outBuf_i2s = *inBuf_i2s;
        outBuf_i2s++;
        inBuf_i2s++;

      //ALL THE DSP ALGORITHMS FOR THE RIGHT CHANNEL SHOULD BE CALLED HERE

      //AFTER THE RIGHT SAMPLE IS PROCESSED, IT IS COPIED TO THE OUTPUT BUFFER AND BOTH POINTERS ADVANCE ONE POSITION
      	*outBuf_i2s = *inBuf_i2s;
        outBuf_i2s++;
        inBuf_i2s++;
    }

    dataReadyFlag = 0;

}