Ads

Get STM32 tutorial using HAL at $10 for a limited time!

Thursday, June 2, 2016

ARM Cortex-M3 (STM32F103) Project - DSP Audio Effect

In this project, I made a simple real-time DSP (Digital Signal Processing) project using ARM Cortex-M3 (STM32F013C8). The signal processed in this project is audio signal that come from electronic devices through 3.5mm audio jack. The audio signal is processed using STM32F103C8 microcontroller.


There are 3 simple audio effect that implemented in this project. These effects are low pass filter, pitch up, and pitch down. The audio output from this microcontroller is sent to an active speaker. This is block diagram of this system:


The first part of this block diagram is an input circuit. The function of this circuit is to give a DC offset to the audio input signal. This is required because the audio input signal is AC signal that consist of negative voltage and the ADC can't read negative voltage. If you want to know the detail about the line output voltage of this audio signal, you can learn from here. The audio output from electronic devices usually a stereo output (left and right channel), but in this project, for simplicity, I will process only one of the channel (left or right). The output signal from this circuit is AC signal that oscillated between 0 and 3.3V that ready for ADC input on STM32F103.

The second part of this block diagram is a microcontroller module (STM23F103C8) that will process this audio signal in real-time. The ADC is required for sampling the input signal with the resolution of 10-bit. The timer 3 is used for generating interrupt for sampling and processing audio input signal at the rate of 35.15kHz. The timer 2 is used for generating audio output using PWM. The PWM frequency is 35.15kHz and the resolution is 10-bit. Button and LED is connected to the GPIO. The button is used for select the audio effect. The LED is used for give indication when any audio effect is on.

The third part of this block diagram is an output circuit. The function of this circuit is to smooth the audio signal from PWM output before sent to the speaker. This circuit is a passive band-pass filter using resistor and capacitor. Nyquist's Theorem states that for a sampling frequency of x Hz, the highest frequency that can be reproduced is x/2 Hz. So, if your sampling frequency is 35kHz the maximum frequency that can be reproduced is 17.5KHz. This circuit is implemented using standard resistor and capacitor value that available. So, the low and high cut off frequency on this filter is set to 16Hz and 16kHz.

You can calculate the cut-off frequency using this formula: Fc= 1/(2×π×R×C).
For the high-pass circuit: Fc= 1/(2×π×100×100u) = 15.915Hz 16Hz.
For the low-pass circuit: Fc= 1/(2×π×1k×10n) = 15.915kHz 16kHz.

This is the circuit implementation from the block diagram above:


This is the code for the microcontroller:
#include "stm32f10x.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_adc.h"
#include "stm32f10x_tim.h"
#include "delay.h"

#define LOW_PASS      0x1000
#define PITCH_UP      0x2000
#define PITCH_DOWN    0x4000

#define FILTER_BUF    9
#define PITCH_BUF     500

/* Low pass filter coefficient (frequency cutoff = 800Hz)
 * Matlab code:
 * N = 7; f_lp = 800; fs = 35156;
 * Wn = f_lp/(fs/2);
 * B = fir1(N, Wn, 'low');
 * freqz(B);
 */
const float filter_coeff[FILTER_BUF] = 
{
    0.0200, 0.0647, 0.1664, 0.2489, 0.2489, 0.1664, 0.0647, 0.0200
};
volatile uint16_t adcValue = 0;
volatile uint16_t effect = 0;

void ADC_Setup(void);
void PWM_Setup(void);
void GPIO_Setup(void);
uint16_t ADC_Read(void);
void PWM_Write(uint16_t val);
uint16_t low_pass(uint16_t input);
uint16_t pitch_up(uint16_t input);
uint16_t pitch_down(uint16_t input);

void TIM3_IRQHandler()
{
    // Checks whether the TIM# interrupt has occurred or not
    if (TIM_GetITStatus(TIM3, TIM_IT_Update))
    {
        // Read ADC value (10-bit PWM)
        adcValue = ADC_Read() >> 2;
 
        // Add audio effect
        if (effect & LOW_PASS)
        {
            adcValue = low_pass(adcValue);
        }
        if (effect & PITCH_UP)
        {
            adcValue = pitch_up(adcValue);
        }
        if (effect & PITCH_DOWN)
        {
            adcValue = pitch_down(adcValue);
        }

        // Write to PWM
        PWM_Write(adcValue);

        // Clears the TIM2 interrupt pending bit
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
    }
}

int main(void)
{ 
    // Initialize delay function
    DelayInit();
    
    // Initialize ADC, PWM, and GPIO
    ADC_Setup();
    PWM_Setup();
    GPIO_Setup();

    while (1)
    {
        // Read input switch (active low)
        effect = GPIO_ReadInputData(GPIOB);
        // Invert and mask input switch bits
        effect = ~effect & 0x7000;

        // If any audio effect is active, then turn on LED 
        if (effect)
        {
            // Turn on LED (active low)
            GPIO_ResetBits(GPIOC, GPIO_Pin_13);
        }
        else
        {
            GPIO_SetBits(GPIOC, GPIO_Pin_13);
        }
 
        DelayMs(50);
    }
}

void ADC_Setup()
{
    ADC_InitTypeDef ADC_InitStruct;
    GPIO_InitTypeDef GPIO_InitStruct;

    // Step 1: Initialize ADC1
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
    ADC_InitStruct.ADC_ContinuousConvMode = DISABLE;
    ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStruct.ADC_ExternalTrigConv = DISABLE;
    ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;
    ADC_InitStruct.ADC_NbrOfChannel = 1;
    ADC_InitStruct.ADC_ScanConvMode = DISABLE;
    ADC_Init(ADC1, &ADC_InitStruct);
    ADC_Cmd(ADC1, ENABLE);
    // ADC1 channel 1 (PA1)
    ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, 
        ADC_SampleTime_7Cycles5);

    // Step 2: Initialize GPIOA (PA1) for analog input
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_1;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStruct);
}

void PWM_Setup()
{
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
    NVIC_InitTypeDef NVIC_InitStruct;
    TIM_OCInitTypeDef TIM_OCInitStruct;
    GPIO_InitTypeDef GPIO_InitStruct;

    // Step 1: Initialize TIM2 for PWM
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    // Timer freq = timer_clock / ((TIM_Prescaler+1) * (TIM_Period+1))
    // Timer freq = 72MHz / ((1+1) * (1023+1) = 35.15kHz
    TIM_TimeBaseInitStruct.TIM_Prescaler = 1;
    TIM_TimeBaseInitStruct.TIM_Period = 1023;
    TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStruct);
    TIM_Cmd(TIM2, ENABLE);

    // Step 2: Initialize PWM
    TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OCInitStruct.TIM_Pulse = 0;
    TIM_OC1Init(TIM2, &TIM_OCInitStruct);
    TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);

    // Step 3: Initialize TIM3 for timer interrupt
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    // Timer freq = timer_clock / ((TIM_Prescaler+1) * (TIM_Period+1))
    // Timer freq = 72MHz / ((1+1) * (1023+1) = 35.15kHz
    TIM_TimeBaseInitStruct.TIM_Prescaler = 1;
    TIM_TimeBaseInitStruct.TIM_Period = 1023;
    TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);
    // Enable TIM3 interrupt
    TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
    TIM_Cmd(TIM3, ENABLE);

    // Step 4: Initialize NVIC for timer interrupt
    NVIC_InitStruct.NVIC_IRQChannel = TIM3_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0x00;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0x00;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);

    // Step 5: Initialize GPIOA (PA0) for PWM output
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOA, &GPIO_InitStruct);
}

void GPIO_Setup()
{
    GPIO_InitTypeDef GPIO_InitStruct;

    // Initialize GPIOC (PC13) for LED
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOC, &GPIO_InitStruct);

    // Initialize GPIOB (PB12, PB13, PB14, PB15) for switch
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13 | 
    GPIO_Pin_14 | GPIO_Pin_15;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOB, &GPIO_InitStruct);
}

uint16_t ADC_Read()
{
    // Start ADC conversion
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
    // Wait until ADC conversion finished
    while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC));

    return ADC_GetConversionValue(ADC1);
}

void PWM_Write(uint16_t val)
{
    // Write PWM value
    TIM2->CCR1 = val;
}

uint16_t low_pass(uint16_t input)
{
    int i;
    static float buffer[FILTER_BUF];
    uint16_t result;

    for (i = (FILTER_BUF-1); i > 0; i--)
    {
        buffer[i] = buffer[i-1];
    }
    buffer[0] = input;

    for (i = 0; i < FILTER_BUF; i++)
    {
        result += buffer[i] * filter_coeff[i];
    }

    return result;
}

uint16_t pitch_up(uint16_t input)
{
    static int index_wr = 0;
    static int index_rd = 1;
    static uint16_t buffer[PITCH_BUF];

    buffer[index_wr] = input;

    index_rd += 2;
    if (index_rd >= (PITCH_BUF-1))
    {
        index_rd = 0;
    }
    index_wr++;
    if (index_wr >= (PITCH_BUF-1))
    {
        index_wr = 0;
    }

    return buffer[index_rd];
}

uint16_t pitch_down(uint16_t input)
{
    static int index_wr = 0;
    static int index_rd = 1;
    static uint8_t half = 0;
    static uint16_t buffer[PITCH_BUF];

    buffer[index_wr] = input;
 
    half++;
    if (half == 2)
    {
        index_rd++;
        half = 0;
    }
    if (index_rd >= (PITCH_BUF-1))
    {
        index_rd = 0;
    }
    index_wr++;
    if (index_wr >= (PITCH_BUF-1))
    {
        index_wr = 0;
    }

    return buffer[index_rd];
}
First, we must initialize peripherals that will be used: ADC, PWM, Timer, and GPIO. The code in main loop is for read button and controlling LED indicator. The button is used for select audio effects. In the timer 3 ISR, we put code for read ADC value, process the value, and output the value to the PWM output.

This code below is used for implementing low-pass filter audio effect. The filter coefficient is obtained using MATLAB. The order of the filter is 7 and the cut off frequency is 800Hz.
/* Low pass filter coefficient (frequency cutoff = 800Hz)
 * Matlab code:
 * N = 7; f_lp = 800; fs = 35156;
 * Wn = f_lp/(fs/2);
 * B = fir1(N, Wn, 'low');
 * freqz(B);
 */
const float filter_coeff[FILTER_BUF] = 
{
    0.0200, 0.0647, 0.1664, 0.2489, 0.2489, 0.1664, 0.0647, 0.0200
};

uint16_t low_pass(uint16_t input)
{
    int i;
    static float buffer[FILTER_BUF];
    uint16_t result;

    for (i = (FILTER_BUF-1); i > 0; i--)
    {
        buffer[i] = buffer[i-1];
    }
    buffer[0] = input;

    for (i = 0; i < FILTER_BUF; i++)
    {
        result += buffer[i] * filter_coeff[i];
    }

    return result;
}
This code below is used for implementing pitch up and pitch down effect. This code is re-write from this project by Anders Skoog.
uint16_t pitch_up(uint16_t input)
{
    static int index_wr = 0;
    static int index_rd = 1;
    static uint16_t buffer[PITCH_BUF];

    buffer[index_wr] = input;

    index_rd += 2;
    if (index_rd >= (PITCH_BUF-1))
    {
        index_rd = 0;
    }
    index_wr++;
    if (index_wr >= (PITCH_BUF-1))
    {
        index_wr = 0;
    }

    return buffer[index_rd];
}

uint16_t pitch_down(uint16_t input)
{
    static int index_wr = 0;
    static int index_rd = 1;
    static uint8_t half = 0;
    static uint16_t buffer[PITCH_BUF];

    buffer[index_wr] = input;
 
    half++;
    if (half == 2)
    {
        index_rd++;
        half = 0;
    }
    if (index_rd >= (PITCH_BUF-1))
    {
        index_rd = 0;
    }
    index_wr++;
    if (index_wr >= (PITCH_BUF-1))
    {
        index_wr = 0;
    }

    return buffer[index_rd];
}
This is the result:


13 comments :

  1. Wow. I love it.
    Can you explain how to add a pot to get a variable filter instead of a fixed 800mhz?
    I think I need some array.
    Thank you

    ReplyDelete
  2. Great article. I Will try to make some experiments with this cheap card. Thanks. Giuseppe.

    ReplyDelete
  3. Nice article thank you.

    In some cases I do not quite understand your coding. For ex. in function low_pass() it looks to me that you are refering unintiliazed variables. First time when the function is called you are copying values from buffer[], but what are those values?

    You are also adding values to variable result but this local variable is not initiliazed when you use it first time.

    Even though there might be a compiler switch that is used to initiliaze all the variables to zero, this kind of coding is not portable. I prefer to initiliaze all the variables before using them. In this case I would write
    static float buffer[FILTER_BUF] = {0.0};
    uint16_t result = 0;
    which causes no extra run time instructions but makes the code much more easier to use in other environments.

    ReplyDelete
    Replies
    1. Not sure if you understand digital signal processing. The low pass filter uses current and past values of the signal for which a buffer, rather a queue of samples is used with fixed length. Old guys go out and new guys come in. And the output is a weighted average of the samples to do a low pass filter. The weights determine the filter cut off.

      Delete
  4. This comment has been removed by the author.

    ReplyDelete
  5. I TRIED BUT AM GETTING FULL NOISE NO AUDIO ...
    CAN YOU PLEASE HELP ME OUT...
    THANK YOU

    ReplyDelete
  6. Special THANK YOU .
    Please upload the HEX application.
    TNX.

    ReplyDelete

  7. Please also upload its libraries. Thank you

    ReplyDelete
  8. why the 10-bit resolution if the ADC supports 12?

    ReplyDelete