Coding the passthrough
In this section, we will guide you through programming the microcontroller in order to implement the passthrough. Many of the concepts in this section lay the foundations for how to structure and code a real-time audio application on the microcontroller. In later sections we will build more complex processing functions, but the architecture of the code will remain the same.
In the previous section, you should have copied the blinking LED project before updating the IOC
file with CubeMX. From the SW4STM32 software, open the file "Src/main.c"
in the new project; we will be making all of our modifications here.
Macros
In programming a microcontroller, it is customary to define preprocessor macros to set the values of reusable constants and to concisely package simple tasks that do not require much logic and flow control and for which, therefore, a function call would be overkill. See here for more on macros and preprocessor directives when programming in C.
Macros are usually defined before the main
function; we will place our macros between the USER CODE BEGIN Includes
and USER CODE END Includes
comment tags.
The MUTE macro
As an example, we will begin by creating macros to change the logical level of the MUTE pin. As in the blinking LED example, we will be using HAL library calls in order to modify the state of the MUTE GPIO pin.
TASK 1: Complete the two macros below -MUTE
and UNMUTE
- in order to mute/unmute the output. Simply replace the XXX
in the definitions with eitherGPIO_PIN_SET
or GPIO_PIN_RESET
, according to whether you need a HIGH or LOW level.
Hint: you should check the datasheet of the DAC to determine whether you need a HIGH or LOW value to turn on the mute function of the DAC.
Note how the MUTE pin that we configured before automatically generates two constants called MUTE_GPIO_Port
and MUTE_Pin
, which is why we suggested giving meaningful names to pins configured with the CubeMX tool.
If you press "Ctrl" ("Command" on MacOS) + click on MUTE_GPIO_Port
or MUTE_Pin
to see its definition, you should see how the values are defined according to the pin we selected for MUTE. In our case, we chose pin PC0 which means that Pin 0 on the GPIO C port will be used. The convenience of the CubeMX software is that we do not need to manually write these definitions for the constants! The same can be said for LR_SEL.
The Channel Select macro
We will now define two more macros in order to assign the MEMS microphone to the left or right channel of the I2S bus, using the LR_SEL pin we defined previously. As before, you should place these macros between the USER CODE BEGIN Includes
and USER CODE END Includes
comments.
TASK 2: Define two macros - SET_MIC_RIGHT
and SET_MIC_LEFT
- in order to assign the microphone to the left or right channel. You will need to use similar commands as for the MUTE macros.
Hint: you should check the I2S protocol (and perhaps the datasheet of the microphone) to determine whether you need a HIGH or LOW value to set the microphone to the left/right channel.
Private variables (aka Constants)
In most applications we will need to set some numerical constants that define key parameters used in the application.
These definitions are also preprocessing macros and they are usually grouped together at the beginning of the code between the USER CODE BEGIN PV
and USER CODE END PV
comment tags.
We will now define a few constants which will be useful in coding our application. Before defining them in our code, let's clarify some of the terminology:
Sample: a sample is a single discrete-time value; for a stereo signal, a sample can belong either to the left or right channel.
Frame: a frame collects all synchronous samples from all channels. For a stereo signal, a frame will contain two samples, left and right.
Buffer length: a buffer is a collection of frames, stored in memory and ready for processing (or ready for a DMA transfer). The buffer's length is a key parameter that needs to be fine-tuned to the demands of our application, as we explained before.
Audio Parameters
Add the following lines to define the frame length (in terms of samples) and the buffer length (in terms of frames):
SAMPLES_PER_FRAME
is set to 2 as we have two input channels (left and right) as per the I2S protocol.
Since our application is a simple passthrough, which involves no processing, we can set the buffer length - FRAMES_PER_BUFFER
- to a low value, e.g. 32.
Data buffers
Again, as explained in Lecture 2.2.5b in the second DSP module, for real-time processing we normally need to use alternating buffers for input and output DMA transfers. The I2S peripheral of our microcontroller, however, conveniently sends two interrupt signals, one when the buffer is half-full and one when the buffer is full. Because of this feature, we can simply use an array that is twice the size of our target application's buffer and let the DMA transfer fill one half of the buffer while we simultaneously process the samples in the other half.
TASK 3: Using the constants defined before - SAMPLES_PER_FRAME
and FRAMES_PER_BUFFER
- define two more constants for the buffer size and for the size of the double buffer. Just replace the ellipsis in the macros below with the appropriate expressions.
Finally, we can create the input and output buffers as such:
Private function prototypes
In this section we will declare the function prototypes that implement the final application. The code should be placed between the USER CODE BEGIN PFP
and USER CODE END PFP
comment tags.
Main processing function
Ultimately, the application will work by obtaining a fresh data buffer filled by the input DMA transfer, processing the buffer and placing the result in a data buffer for the output DMA to ship out. We will therefore implement a main processing function with the following arguments:
a pointer to the input buffer to process
a pointer to the output buffer to fill with the processed samples
the number of samples to read/write.
The resulting function prototype is:
This will be the main processing function which will be invoked by the interrupts raised by the DMA transfer every time either the first or the second half of the buffer has been filled.
DMA callback functions
As previously mentioned, the STM32 board uses DMA to transfer data in and out of memory from the peripherals and issues interrupts when the DMA buffer is half full and when it's full.
The HAL family of instructions allows us to define callback functions triggered by these interrupts. Add the following function definitions for the callbacks, covering the four cases of two input and output DMAs times two interrupt signals:
Note that the Rx callbacks (that is, the callbacks triggered by the input DMAs), have an empty body and only the Tx callbacks (that is, the ones driven by the output process) perform the processing via our process
function.
This is a simple but effective way of synchronizing the input and the output peripherals when we know that the data throughput should be the same for both devices. Of course we can see that if the process
function takes too long, the buffer will not be ready in time for the next callback and there will be audio losses. In the next chapter, we will introduce a mechanism to monitor this.
You can read more about the HAL functions for DMA Input/Output for the I2S protocol in the comments of the file "Drivers/STM32F0XX_HAL_Driver/Src/stm32f0xx_hal_i2s.c"
from the SW4STM32 software:
The user application
Between the USER CODE BEGIN 4
and USER CODE END 4
comment tags, we will define the body of the process
function which, in this case, implements a simple passthrough.
TASK 4: Complete the main processing function which simply copies the input to the output buffer.
Initial Setup
Between the USER CODE BEGIN 2
and USER CODE END 2
comment tags, we need to initialize our STM32 board, namely we need to:
un-mute the DAC using the macro defined before.
set the microphone to either left or right channel using the macro defined here.
start the receive and transmit DMAs with
HAL_I2S_Receive_DMA
andHAL_I2S_Transmit_DMA
respectively.
This is accomplished by the following lines:
We can now try building and debugging the project (remember to press Resume after entering the Debug perspective). If all goes well, you should have a functioning passthrough and you should be able to hear in the headphones the sound captured by the microphone.
Going a bit further
If you still have time and you are curious to go a bit further, we propose to make a modification to theProcess
function. In the current implementation, since the input is mono and the output is stereo, you may have noticed that only one output channel carries the audio while the other is silent. Wouldn't it be nice if both had audio, thereby converting the mono input to a stereo output?
BONUS: Modify theProcess
function so that both output channels contain audio.
Note: remember to copy your project before making any significant modifications; that way you will always be able to go back to a stable solution!
Congrats on completing the passthrough! This project will serve as an extremely useful starting point for the following (more interesting) applications. The first one we will build is an alien voice effect. But first, let's talk about some key issues in real-time DSP programming.
Solutions
Are you sure you are ready to see the solution? ;)
Last updated