Setting up the STMF4 to connect to the CODEC and make sound

For this next change we will need to add some source code which has been developed to support the circuit board and the specific devices on the board. First, in the IDE, you will need to create a folder in the “Drivers” folder. Name this new folder “BSP”. Now we will copy some code into “BSP.”  When you started the IDE it should have downloaded the board and chip support code. Look for the folder STM32Cube and under that Repository:STM32Cube_FW_F4_V1.24.1. Under that you will find Drivers:BSP. This stands for board specific package. Find the folder for STM32F4-Discovery and make a copy of it in your project by dropping the folder on your “BSP” IDE folder – select “Copy…”

You now need to do the same for BSP:Components. Drag and drop “Components” on “BSP”. Finally, there is a folder with code that supports the on-board microphone. It is Middleware:ST:STM32Audio. Drag and drop “STM32Audio” on the IDE folder “Middleware:ST”. When done, your project will look like this

Screen Shot 2019-09-19 at 3.22.58 PM

Once this is done, we should be able to Build the project and have no errors.

Making sound

To make sound we need to call functions to initialize the CODEC, start the CODEC and we need to periodically call functions to compute samples for the CODEC. The BSP functions in stm32f4_discovery_audio.c make the first two steps fairly simple. To initialize the CODEC call the functions:

BSP_AUDIO_OUT_Init(OUTPUT_DEVICE_HEADPHONE, 90, 48000);
BSP_AUDIO_OUT_Play((uint16_t *)codecBuffer, 128);

after  “/* USER CODE BEGIN 2 */.” We also need to a include statements on the top of main.c so that main.c can see the declarations of the functions and macros we want to use. Add:

#include "../Drivers/BSP/STM32F4-Discovery/stm32f4_discovery_audio.h"

after “/* USER CODE BEGIN Includes */.” Finally, we need to define the array “codecBuffer”. We can declare it as a global (as well as globals for the I2S serial device) after “/* USER CODE BEGIN PV */”

int16_t codecBuffer[64];    // 32 samples X 2 channels
extern I2S_HandleTypeDef       hAudioOutI2s;
extern I2S_HandleTypeDef       hAudioInI2s;

Finally, we need to create five callback functions which will be called whenever the CODEC needs more data. The first two handle interrupt signals from the I2S serial device on the STM32F4. These call the DMA processor whenever the I2S device has received or tranmitted data from/to the CODEC. The DMA processor then calls one of the next two functions when it has transmitted a certain amount of data. This is done every half block, so that we have time to fill one half block while the other is playing. At the moment we aren’t doing any synthesis, so we won’t hear anything coming from the CODEC headphone port

void I2S3_IRQHandler(void)
{
     HAL_DMA_IRQHandler(hAudioOutI2s.hdmatx);
}
void I2S2_IRQHandler(void)
{
    HAL_DMA_IRQHandler(hAudioInI2s.hdmarx);
}

void BSP_AUDIO_OUT_HalfTransfer_CallBack(void)
{
}

void BSP_AUDIO_OUT_TransferComplete_CallBack(void)
{
    BSP_AUDIO_OUT_ChangeBuffer((uint16_t *)codecBuffer, 64);
}

void BSP_AUDIO_OUT_Error_CallBack(void)
{
    /* Stop the program with an infinite loop */
    while (1) {}
}

Now we can create a very simple sound synthesis function – two sawtooth generators. The synthesis function is called from the audio out callbacks. After the first half is complete, one should fill the first half with new samples. After the full buffer is complete, one should fill the second half with new samples. These samples are converted from float to integer by multiplying them by the maximum 16 bit value of 32767.0.

void BSP_AUDIO_OUT_HalfTransfer_CallBack(void)
{
    int i;
     audioBlock(inBuffer, outBuffer, 16);
    for(i = 0; i < 32; i+=2)
    {
        codecBuffer[i+0] = (int16_t)((outBuffer[i]) * 32767.0f);
        codecBuffer[i+1] = (int16_t)((outBuffer[i+1]) * 32767.0f);
    }
}

void BSP_AUDIO_OUT_TransferComplete_CallBack(void)
{
    int i;
    BSP_AUDIO_OUT_ChangeBuffer((uint16_t *)codecBuffer, 64);
    audioBlock(inBuffer, outBuffer, 16);
    for(i = 0; i < 32; i+=2)
    {
        codecBuffer[i+32] = (int16_t)((outBuffer[i]) * 32767.0f);
        codecBuffer[i+33] = (int16_t)((outBuffer[i+1]) * 32767.0f);
    }
}

A few global declarations need to be added at the top of the main.c for the buffers and synthesis parameters.

// Sound globals
void audioBlock(float *input, float *output, int32_t samples);
float saw1, saw2;
float inBuffer[32], outBuffer[32];

The audio synthesis function audioBlock will calculate the 2 sawtooth waves, and keep the result in saw1 and saw2. A sawtooth is simply an increasing signal which gets reset to a minimum sample value when it exceeds a maximum sample value. In digital audio, we typically use the minimum and maximum values of -1.0 and 1.0. The amount of increase each sample is dependent on the frequency. The sawtooth needs to rise a value of 2.0 (from -1.0 to 1.0) every period. For a given frequency f, our increase is (f * 2.0)/sampleRate.

void audioBlock(float *input, float *output, int32_t samples)
{
    int i;
    float increment1, increment2;
    // 0.0000208333f is 1.0/48000.0
    increment1 = 350.0f * 2.0f * 0.0000208333f;
    increment2 = 440.0f * 2.0f * 0.0000208333f;
    for(i = 0; i < samples; i++)
    {
        saw1 += increment1;
       if(saw1 > 1.0f)
            saw1 = saw1 - 2.0f;
        saw2 += increment2;
       if(saw2 > 1.0f)
            saw2 = saw2 - 2.0f;
        output[i<<1] = saw1;
        output[(i<<1) + 1] = saw2;
    }
}

This framework is a good starting point for digital synthesis.

In the next lesson, I will provide a sample project with a MIDI host driver so that a class-compliant MIDI keyboard can be connected to the microUSB port (a microUSB>USBA OTG adapter will be required).

Leave a Reply

Your email address will not be published.