Notice: Memcache::get(): Server localhost (tcp 11211) failed with: Connection refused (111) in /home/a146/www/we.easyelectronics.ru/engine/lib/external/DklabCache/Zend/Cache/Backend/Memcached.php on line 134
Фильтрация звука на STM32 с использованием КИХ фильтра / STM32 / Сообщество EasyElectronics.ru

Фильтрация звука на STM32 с использованием КИХ фильтра

Давным-давно, в начале перестройки начали появляться у народа забавные тайваньские штучки, к примеру, часы электронные с кучей мелодий, или вот брелок к ключам, который на свист забывшего, где лежат эти ключи человека, откликался своим ответным пронзительным писком. В брелоке находился пьезо-излучатель, он же и микрофон, пара маленьких батареек и платка с залитой клеем микросхемой.




Внутренности брелока (1987 год ):



Решил сделать подобное устройство на микроконтроллере, заодно попрактиковаться c цифровой обработкой сигнала. В интернете много хороших статей по цифровой обработки сигнала, я лишь в краце коснусь способа реализации в моём варианте. Программа в микроконтроллере сама рассчитывает требуемые коэффициенты фильтра, основываясь на длине фильтра, частоте дискретизации, начальной и конечной частоте пропускания фильтра. Получаемые значения коэффициентов имеют вещественное значения, и с многими цифрами после запятой. А так как в микроконтроллере нет блока для работы вещественными числами, и программная обработка таких чисел занимает гораздо больше времени, чем работа с целыми числами, то перенесём значения коэффициентов в целочисленные дроби. К примеру, вещественное число 0,12345 можно представить в виде дроби 505/4096, чей результат, не сильно отличается от исходного, но может значительно уменьшить время, затрачиваемое на арифметические операции. Можно конечно и все отнормированные значения коэффициентов фильтра умножить хоть на миллион, и потом делить на него же. Но если для ускорения работы программы придётся переходить на ассемблер, неправильные дроби предпочтительнее, так как позволят операции деления заменить сдвигами вправо. Хотя данный микроконтроллер затрачивает один такт на целочисленное умножение и от 2 до 12 тактов на деление, меньшие значения в знаменателе позволяют использовать меньше тактов для деления.

Для предварительной оценки работы цифрового фильтра написал программу на C#, которая производит расчёт коэффициентов фильтра, моделирует работу цифрового фильтра, позволяет визуально наблюдать результат обработки сигнала. На рисунках ниже представлены формы сигналов. К примеру, синим цветом, отображён входной сигнал, частота его меняется плавно от минимальной до максимальной из заданных частот. Красным отображён обработанный фильтром сигнал, зелёным отображены коэффициенты фильтра. Салатовым, форма весового окна Блекмена (если используется). Её используют для получения требуемой формы амплитудно-частотной характеристики фильтра в полосе пропускания и подавления всплесков сигнала за пределами её. В нижних текстовых окнах программы выводятся коэффициенты фильтра как вещественных, так и в целых числах. На рисунках видно, что если просуммировать все отсчёты входного сигнала, перемноженные с соответствующими отсчётами гармонического сигнала функции синуса определённой частоты (операция свёртки), то выходной сигнал будет содержать синусную составляющую от входного сигнала с такой же частотой.

Свёртка сигнала с синусоидой:


И если также поступить с гармоническим сигналом функции косинуса такой же частоты, то выходной сигнал будет содержать косинусную составляющую от входного сигнала с такой же частотой.

Свёртка сигнала с косинусоидой:


Имея два таких сигнала (синусную и косинусную составляющие, или реальную и мнимую часть сигнала), можно сказать. что осуществили дискретное преобразование Фурье для заданной частоты. Выстроив синусные и косинусные значения для всех интересующих нас частот, получим спектр амплитуды сигнала, и его фазу. Такая же аналогия наверно применима и для операции свёртки коэффициентов фильтра и входного сигнала.

Получились такие картинки:

Фильтр низких частот:


Фильтр высоких частот:


Полосовой пропускающий фильтр:


Полосовой пропускающий с окном Блекмена:


Полосовой заграждающий фильтр:


Алгоритмы для для расчёта фильтров, основаны на канонических формулах, которые умные математики давно придумали и они есть в соответствующих учебниках. Расчёт их выглядит так:

Коэффициенты фильтра низких частот:

for (i = 0; i <= nm; i++) { if (i == 0){hz[j] = 2.0 * Fc;} else
{hz[j + i] = (Math.Sin(2.0*Fc*pi*i) / (pi*i));hz[j - i] = hz[j + i];} }

Коэффициенты фильтра высоких частот:

for (i = 0; i <= nm; i++) { if (i == 0) {hz[j] = 1.0 - 2.0 * Fc;} else
{hz[j + i] = -(Math.Sin(2.0 * Fc * pi * i) / (pi * i)); hz[j - i] = hz[j + i];} }

Коэффициенты фильтра полосового пропускающего:

for (i = 0; i <= nm; i++) { if (i == 0) {hz[j] = 2.0 * (Fp2-Fp1)/Fd;} else {hz[j + i] = (Math.Sin(2.0 * Fp2 * pi * i / Fd) - Math.Sin(2.0 * Fp1 * pi * i / Fd)) / (pi * i); hz[j - i] = hz[j + i];} }

Коэффициенты фильтра полосового заградительного:

for (i = 0; i <= nm; i++) { if (i == 0) {hz[j] = 1- 2.0 * (Fp2-Fp1)/Fd;} else {hz[j + i] = (Math.Sin(2.0 * Fp1 * pi * i / Fd) - Math.Sin(2.0 * Fp2 * pi * i / Fd)) / (pi * i); hz[j - i] = hz[j + i];} }


где, Fc — частота среза фильтра высоких или низких частот, Fp1, Fp2 частота задержки и пропускания полосового фильтра, Fd — частота дискретизации, nm — половина длинны фильтра m, j=mn;

Источник формул:


И по просьбе, сделал устройство в автомобиль, которое выполняет по три функции: светомузыки, аварийного света, ночника (плавное псевдослучайное изменение цвета и яркости светодиодной ленты). Светодиодная лента планировалась быть наклеенной в багажнике автомобиля. Что ты меньше паять, решил использовать плату CAN модуля STARLINE (описан в другой статье), удалив с неё микросхему интерфейса и внешней памяти. Освободившиеся выводы, использованы для подключения внешнего источника звукового сигнала, кнопки, и транзисторов — ключей светодиодной ленты. В последнее время, в своих проектах, стараюсь использовать предлагаемую в STM32CubeMX возможность включить в проект USB CDC и FreeRTOS. Это упрощает написание и отладку программ, особенно если на микроконтроллер возлагается несколько задач. А используемый микроконтроллер вполне со всем справляется. Аудио-сигнал подается на вывод микроконтроллера PA5 через резистор с разделительным конденсатором, и для правильной работы АЦП стоит еще резисторный делитель, создающий постоянное напряжение смещения на входе АЦП в половину напряжения питания микроконтроллера. Выводы микроконтроллера PB6, PB7, PB8, через резисторы, подключены к затворам транзисторов, которые нагружены светодиодами ленты соответствующих цветов. Транзисторы надо использовать и низким напряжением открытия (Ugs = 2-2,5 Вольта), вариантов много, я использовал оказавшиеся под рукой F5020. В проекте, старался оставлять подробные комментарии, так что и схему приводить не стал (если кому понадобится — нарисую). Теперь к программе. При старте микроконтроллера, создаем три дополнительные задачи (потока), каждая отвечает за определенный режим работы устройства. Нажимая на кнопку, обрабатываем это событие в одной задаче, приостанавливаем или запускаем на выполнение нужную задачу режима работы. Есть еще задачи, активизирующиеся от приема символов от компьютера, что позволяет контролировать работу устройства при отладке. Перед запуском задач, после инициализации, микроконтроллер рассчитывает коэффициенты для фильтра низкой частоты, полосового фильтра средней частоты, и фильтра высокой частоты. Результат работы фильтров, используется для загрузки значений в таймер, который работая в режиме широтно-импульсной модуляции, управляет транзисторами светодиодной ленты. Подробно расписывать работу программы не буду, есть комментарии в проекте, а по работе с USB CDC и FreeRTOS полно информации на русском языке. Остановлюсь на некоторых моментах. Программу писал можно сказать в лоб и без затей. Можно еще много шлифовать и оптимизировать код, убирать лишнее и т.д. Но так как это просто светомузыка, которая полностью выполняет свои заложенные функции, оставил все как есть когда заработало ;).

Монтаж незатейлив:


Получилось так:


Перед установкой в автомобиль:



По программе, кому нет надобности смотреть весь проект, приложу некоторые примеры кода:

Расчёт коэффициентов фильтра (для канала синего цвета, фильтр ВЧ):

nm = inbufflen / 2;     //номера значений для коэфф фильтра симметричны
j= nm;          //отсчет от середины фильтра
for (i = 0; i < nm; i++)
{
    if (i == 0)
    {
        hzB[j] = 1.0 - 2.0 * fcutH / fdiskret;
    }
    else
    {
      hzB[j + i] = - (sin(2.0 * pi * i * fcutH / fdiskret))  / (pi * i);
      hzB[j - i] = hzB[j + i];  
    }
}

Перевод их в целочисленные значения:

for (i = 0; i < inbufflen; i++)
{
 ch=0;               //числитель
 zn=1;               //знаменатель
 pv = hzB[i];        //временная переменная
 if(fabs(pv)>0.0001) //если значения в требуемых предела
 {
    sdl = 0;        //количество сдвигов вместо деления
    do          //цикл поиска значений числ и знам
    {
 zn = zn << 1;    //попробуем другой множитель
 sdl++;     //и знаменатель
 ch=pv*zn;      //вычислим числитель
 pr =  1.0 * ch / zn; //проверим результат
     pd = fabs(pv) - fabs(pr);  //найдем абсолютную разницу
}while(fabs(pd) > fabs(pv / 200)&&(zn<0x8000));//если устраивает
Bchislitel[i] = ch;     //занесем числитель коэффициента
Bznamenatel[i] = zn;    //занесем знаменатель коэффициента
   }
   else
   {
        Bchislitel[i] = 0;          //слишком малая величина
        Bznamenatel[i] = 1;     //на 0 не делим
   }
}

По прерыванию (какому указано ниже), читаем АЦП, читаем данные с АЦП:

//получить напряжение канала l    PA5
uint16_t    get_Lchannel(void)
{
        extern    int    lch;
        //    стартуем для проверки чтение данных с канала АЦП, левый канал
        ADC_ChannelConfTypeDef sConfig;
        //HAL_ADC_Stop(&hadc1);    
        sConfig.Rank = 1;
        sConfig.SamplingTime = ADC_SAMPLETIME_1CYCLE_5;
      sConfig.Channel = ADC_CHANNEL_5;    //время преобразования почти 10мкс
        HAL_ADC_ConfigChannel(&hadc1, &sConfig);
        HAL_ADC_Start(&hadc1);
        HAL_ADC_PollForConversion(&hadc1,1);
        newADCR1    = HAL_ADC_GetValue(&hadc1);
        lch    =    newADCR1;    //берем и отдаем сырые данные
        return    lch;
}

кладем значение в буфер:

void    inbufflwrite(int cdata)
{
    int ibr;
    int iamp;
    //освобождаем место для нового элемента
    //и сдвигаем все элементы на 1 в сторону больших номеров
    //начиная с конца
    for(ibr=(inbufflen-1);ibr>0;ibr--)
    {
        inbufflch[ibr]=inbufflch[ibr-1];    
    }
    //записываем новое значение
    //нормируем и корректируем относительно середины напряжения питания
    //+60   - коррекция нулевой точки неточного резисторного делителя
    inbufflch[0]= cdata -   2047 + 60;  //кладем в первый элемент
}

//Производим свёртку сигнала для каждого цвета (для примера синий):
void    Bfilter(void)
{
    int si;
    int stmp=0; 
//свертка (перемножение и накопление текущих данных с АЦП и коэф.имп.хар.фильтра)
        for (si = 0;si < inbufflen; si++)
        {
        //умножаем сигнал на числитель фильтра, делим на знаменатель
        //и накапливаем
        stmp += (inbufflch[si] * Bchislitel[si])/Bznamenatel[si];
        }
        //значение отфильтрованного сигнала
        OutB=stmp;
}

Выделяем абсолютное среднее значение амплитуды сигнала за cntib периода

//усредняем значения за несколько периодов и получаем абсолютное значение
//получается, если на вход АЦП попадает сигнал от 0 до Vcc
//значения примерно от -1500 до +1500
//    при частоте на входе        уровень Bpow
//    нет сигнала                                        -9..10
//    50                                                        1200
//    100                                                        1000
//    300                                                        684
//    400                                                        430
//    500                                                        153
//    800                                                        33
//    1000                                                    45
//    2000                                                    12
//    3000                                                    11
//    необходимо будет добавить порог детектирования, примерно 512
void    DetB(void)
{
    int iii;
    EdetB+=abs(OutB);
    eib++;
    if(eib>=cntib)
    {
        Bpow=EdetB/cntib;
        eib=0;
        EdetB=0;
    }
}

Обработка сигнала для трех цветов, происходит по прерыванию таймера №2, который определяет частоту дискретизации (fdiskret=10044;), выглядит это так:

void TIM2_IRQHandler(void)
{
  /* USER CODE BEGIN TIM2_IRQn 0 */
    //время преобразования 10мкс
    int tmptim2;
    tmptim2=get_Lchannel(); //получим данные с АЦП
    inbufflwrite(tmptim2);      //положим в буфер   
    //запустим работу фильтров
    Bfilter();  //отфильтруем
    DetB();     //детектируем
    Gfilter();
    DetG(); 
    Rfilter();
    DetR();                                             //все эти процедуры занимают в сумме 70мкс.
  /* USER CODE END TIM2_IRQn 0 */
  HAL_TIM_IRQHandler(&htim2);
  /* USER CODE BEGIN TIM2_IRQn 1 */
 
  /* USER CODE END TIM2_IRQn 1 */
}

Значения частот среза фильтров определили ранее:

//        R                G                B
//        <    fcutL    <    fcutM < fcutH     
//частота среза фильтра нижней частоты
int    fcutL    =    250;
//частота среза фильтра средней частоты
int    fcutM    =    300;
//частота среза фильтра высокой частоты
int    fcutH    =    2000;


И так можно останавливать и запускать задачи в зависимости от режима работы:

switch(ModeBoard)
        {
            case    0:        //ночник
if(happyN==0)    //не в цикле, а 1 раз пошлем команду остановить мешающую задачу
                {
                    //если задача запущена, то приостановим ее
                    if(eTaskGetState(RunAlarmHandle)!=eSuspended)
                                {vTaskSuspend(RunAlarmHandle);}    //RunAlarm        приостанавливаем задачу
                    //если задача запущена, то приостановим ее
                    if(eTaskGetState(RunColorHandle)!=eSuspended)
                                {vTaskSuspend(RunColorHandle);}     //RunColor        приостанавливаем задачу
                    //если задача приостановлена, то запускаем на продолжение выполнения
                    if(eTaskGetState(RunNightHandle)!=eRunning)    
                                {vTaskResume(RunNightHandle);}      //RunNight        запускаем задачу
                    happyN=1;happyA=0;happyC=0;
                }
            break;
 


После инициализации микроконтроллера и переменных, создаем свои задачи, которые начинают работать после запуска планировщика, ну а потом ими можно управлять.

/* Create the thread(s) */
  /* definition and creation of defaultTask */
  osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
  defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);
    //vTaskPrioritySet( defaultTaskHandle, osPriorityNormal + 1 );
     
  /* USER CODE BEGIN RTOS_THREADS */
    //сделаем поток для задачи от прихода символа f по USB
    //сделаем в нем расчет и распечатку коэффициентов фильтра
  osThreadDef(Task1, fromMySymHandle, osPriorityNormal, 0, 128);
  Task1Handle = osThreadCreate(osThread(Task1), NULL);
     
    //сделаем поток для задачи от прихода символа d по USB
    //сделаем распечатку приемного буфера от АЦП
  osThreadDef(Task2, fromMyDataHandle, osPriorityNormal, 0, 128);
  Task2Handle = osThreadCreate(osThread(Task2), NULL);
     
    //сделаем поток для периодической задачи, светомузыка
    //
  osThreadDef(RunColor, RunColor, osPriorityNormal, 0, 128);
  RunColorHandle = osThreadCreate(osThread(RunColor), NULL);
    //сделаем поток для периодической задачи, аварийное мигание
    //
  osThreadDef(RunAlarm, RunAlarm, osPriorityNormal, 0, 128);
  RunAlarmHandle = osThreadCreate(osThread(RunAlarm), NULL);
    //сделаем поток для периодической задачи, режим ночника
    //
    osThreadDef(RunNight, RunNight, osPriorityNormal, 0, 128);
  RunNightHandle = osThreadCreate(osThread(RunNight), NULL);
     
    //сделаем поток для обработки сообщения от кнопки
  osThreadDef(Task6, MyKey, osPriorityNormal, 0, 128);
  Task6Handle = osThreadCreate(osThread(Task6), NULL);
    //vTaskPrioritySet( Task6Handle, osPriorityNormal + 1 );
  /* add threads,… */
  /* USER CODE END RTOS_THREADS */
  /* USER CODE BEGIN RTOS_QUEUES */
  /* add queues,… */
   
  /* USER CODE END RTOS_QUEUES */
  /* Start scheduler */
  osKernelStart();
 


За планировщиком, код можно не писать свой, вы туда не попадете :).

Не забудьте добавить памяти для «кучи» в файле конфигурации freertosconfig.h:

#define configMINIMAL_STACK_SIZE                 ((uint16_t)256)    //увеличили с 128
#define configTOTAL_HEAP_SIZE                    ((size_t)8000)     //увеличили с 4000

и возможно, раскомментировать, дополнительные директивы freertos, которые будем использовать (eTaskGetState).

При использовании созданного проекта в CubeMX, для KEIL 4.5, компилятор выдает ошибку, нельзя использовать ассемблер в тексте программы на c. Я добавил такой файл на ассемблере —

TabletInlineAsm.s:



подключил его к проекту, а в файле stm32f1xx_hal_pwr.c сделал изменения:

/* Private functions ---------------------------------------------------------*/
extern    void    PWR_OverloadWfeASM(void);
__NOINLINE
static void PWR_OverloadWfe(void)
{
    PWR_OverloadWfeASM();    //вылечим таким образом
  //__asm volatile( "wfe" );
  //__asm volatile( "nop" );
}


Вот вроде бы по минимуму все…

Видео работы: yadi.sk/i/AgHOSDR2kdzpP

Если есть вопросы, задавайте, ссылка на проект: yadi.sk/d/ly7OCeWPkf4rY

Комментарии (9)

RSS свернуть / развернуть
Как так, статья про фильтрацию звука, а видео без звуков.
Даешь ролик со звуком с фильтром и без!!!
0
А не проще для фильтрации использовать библиотеку CMSIS?
0
а подробнее про эту библиотеку можно?
0
+1
за вторую ссылку спасибо. А для Cortex-m4 такое имеется? У этого ядра тоже dsp есть
0
CMSIS написана на Си, за использование DSP инструкций отвечает компилятор. И он действительно их использует. Проверено на КИХ-фильтрах использующих float формат данных и коэффициентов.
0
вполне может быть, но желательно делится опытом, может студентам и школьникам полезно знать будет основы, в примитивном вольном изложении :)
0
И по просьбе, сделал устройство в автомобиль, которое выполняет по три функции: светомузыки, аварийного света, ночника (плавное псевдослучайное изменение цвета и яркости светодиодной ленты). Светодиодная лента планировалась быть наклеенной в багажнике автомобиля

Не совсем понял зачем светомузыка и ночник в багажнике авто. Почему-то вспомнился анекдот:
Плохая примета — ехать ночью… в лес… в багажнике...
 
Вот для этого применения — светомузыка в багажнике в самый раз, «забота о клиенте» :)

З.Ы. Спасибо за статьи
+1
  • avatar
  • e_mc2
  • 18 февраля 2016, 21:31
а мне припомнился этот брелок
откуда-то его достал, но так и не понял что это
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.