БИХ-фильтр на 16-битном микроконтроллере (PIC24F)

Я пытаюсь реализовать простой БИХ-фильтр первого порядка на микроконтроллере (PIC24FJ32GA002), но пока безуспешно. Фильтр представляет собой фильтр слежения за постоянным током (фильтр нижних частот), целью которого является отслеживание постоянной составляющей сигнала частотой 1,5 Гц. Разностное уравнение было взято из примечаний по применению TI:

y (n) = K x (n) + y (n-1) (1-K)
с K = 1/2 ^ 8

Я сделал сценарий MATLAB, чтобы протестировать его, и он хорошо работает в моделировании. Используемый код:

K=1/2^8
b = K
a = [1 -(1-K)]

Fs=200; // sampling frequency
Ts=1/Fs;
Nx=5000; // number of samples
nT=Ts*(0:Nx-1);
fin=1.5; // signal frequency

randn('state',sum(100*clock));
noise=randn(1,Nx);
noise=noise-mean(noise);

xin=200+9*(cos(2*pi*fin*nT));
xin=xin+noise;

out = filter(b,a,xin);

Моделирование Частотная характеристика

Однако я не могу реализовать это на микроконтроллере PIC24F. я представляю коэффициенты в формате Q15 (1.15), сохраняя их в коротких переменных и используя длинную для умножения. Вот это код:

short xn;
short y;
short b0 = 128, a1 = 32640; // Q15
long aux1, aux2;

// (...)

while(1){

    xn = readADC(adc_ch);

    aux1 = ((long)b0*xn) << 1;
    aux2 = ((long)a1*y) << 1;
    y = ((aux1 + aux2) >> 16);

    delay_ms(5);
}

Длинный перевод используется для расширения сигнала, чтобы операция умножения выполнялась правильно. После каждого умножения я сдвигаю влево на один бит, чтобы удалить бит расширенного сигнала. При суммировании я сдвигаю вправо на 16 бит, чтобы получить y в формате Q15.

Я отлаживаю MCU с помощью Pickit2 и окна «Просмотр-> Часы» (MPLAB IDE 8.53) и тестирую фильтр с сигналом постоянного тока (я изменяю сигнал постоянного тока с помощью потенциометра для проверки различных значений). АЦП имеет разрешение 10 бит, а микроконтроллер питается от 3,3 В. Некоторые результаты:

1В --> xn = 312 (верно), yn = 226 (неверно)
1.5V --> xn = 470 (правильно), yn = 228 (совершенно неверно)

Что я делаю неправильно? Любые предложения о том, как реализовать этот фильтр IIR на 16-битном микроконтроллере?

Спасибо заранее :)

Ответы (6)

Я не слишком углублялся в дизайн вашего фильтра, но просто взглянув на исходный код, я обнаружил пару вещей. Например, эти строки:

aux1 = ((long)b0*xn) << 1;
aux2 = ((long)a1*y) << 1;
y = ((aux1 + aux2) >> 16);

Первая проблема, которую я вижу, это ((long)b0*xn). Я встречал компиляторы, которые неправильно компилировали бы это как ((long)(b0*xn)), что совершенно неверно. На всякий случай я бы написал это как (((long)b0)*((long)xn)). Безусловно, это параноидальное программирование, но...

Затем, когда вы делаете "<<1", это НЕ то же самое, что "*2". Для большинства вещей это близко, но не для DSP. Это связано с тем, как MCU/DSP обрабатывает условия переполнения, расширения знака и т. д. Но даже если он работает как *2, вы удаляете один бит разрешения, который вам не нужно удалять. Если вам действительно нужно сделать *2, тогда сделайте *2 и дайте компилятору выяснить, может ли он вместо этого заменить <<1.

>>16 также проблематичен. Внезапно я не знаю, произойдёт ли это логический или арифметический сдвиг. Вам нужен арифметический сдвиг. Арифметические сдвиги правильно обработают бит знака, тогда как логический сдвиг будет вставлять нули для новых битов. Кроме того, вы можете сэкономить биты разрешения, избавившись от <<1 и изменив >>16 на >>15. Ну и изменение всего этого на нормальное умножение и деление.

Итак, вот код, который я бы использовал:

aux1 = ((long)b0) * ((long)xn);
aux2 = ((long)a1) * ((long)y);
y = (short)((aux1+aux2) / 32768);

Я не утверждаю, что это решит вашу проблему. Это может или не может, но это улучшает ваш код.

Это не решило проблему, но помогло лучше понять код и «трюки» компилятора. Проблема была с коэффициентами. Это должно быть a1*xn и b0*y. Действительно хромая ошибка, eheh Спасибо за вашу помощь!
+1: технически стандарт C не определяет, является ли >> арифметическим или логическим сдвигом... или, по крайней мере, так мне сказали люди из Microchip несколько лет назад, когда я сообщил о проблемах с использованием >> для power-of -2-делится из-за отсутствия расширения знака.

Я предлагаю вам заменить «короткий» на «int16_t» и «длинный» на «int32_t». Сохраните «short», «int», «long» и т. д. для мест, где размер не имеет большого значения, например, для циклических индексов.

Тогда алгоритм будет работать так же, если вы компилируете на своем настольном компьютере. На рабочем столе будет намного проще отлаживать. Когда он будет удовлетворительно работать на рабочем столе, тогда переместите его на микропроцессор. Чтобы упростить задачу, разместите хитрую подпрограмму в отдельном файле. Вы, вероятно, захотите написать простую функцию main() для рабочего стола, которая выполняет процедуру и завершает работу с нулем (в случае успеха) или ненулевым значением (в случае неудачи).

Затем интегрируйте сборку и запуск теста в систему сборки вашего проекта. Любая система разработки, которую стоит рассмотреть, позволит вам сделать это, но вам, возможно, придется прочитать руководство. Мне нравится GNU Make .

Более того, вам не нужно определять эти определения типов самостоятельно, если в вашей системе есть <stdint.h> : pubs.opengroup.org/onlinepubs/007904975/basedefs/stdint.h.html .

Дэвид Кесснер упомянул, что >> 16 ненадежен, и это правда. Я был укушен этим раньше, поэтому теперь у меня всегда есть статическое утверждение в моем коде, которое проверяет, что >> работает так, как я ожидаю.

Вместо этого он предложил фактический разрыв. Но вот как сделать правильную арифметику >> 16, если выяснится, что у вас нет.

union
{
     int16_t i16s[2];
    uint16_t i16u[2];

     int32_t i32s;
    uint32_t i32u;
}union32;

union32 x;
x.i32s = -809041920;

// now to divide by 65536

x.i16u[0] = x.i16u[1];        // The shift happens in one go.

                              // Now we do the sign extension
if (x.i16u[0] & 0x8000)       // Was this a negative number?
    x.i16u[1] = 0xFFFF;       // Then make sure the final result is still negative
else
    x.i16u[1] = 0x0000;       // Otherwise make sure the final result is still positive

// Now x.i32s = -12345

Убедитесь, что ваш компилятор генерирует хороший код для (x.i16u[0] & 0x8000). Компиляторы Microchip не всегда используют инструкции PIC BTFSC в подобных случаях. Особенно 8-битные компиляторы. Иногда мне приходится писать это на ассемблере, когда мне действительно нужна производительность.

Выполнение сдвига таким образом намного быстрее на PIC, которые могут выполнять сдвиги только по одному биту за раз.

+1 за использование определений типов intNN_t и uintNN_t, а также подход к объединению, оба из которых я использую в своих системах.
@JasonS - Спасибо. Объединенный подход — это то, что отличает разработчиков встраиваемых систем от разработчиков ПК. Парни из ПК, кажется, понятия не имеют, как работают целые числа.

Использование dsPIC вместо PIC24 может значительно упростить задачу (они совместимы по выводам). Компилятор dsPIC поставляется с библиотекой DSP, которая включает IIR-фильтры, а аппаратное обеспечение напрямую поддерживает арифметику с фиксированной точкой.

Я должен использовать этот MCU (PIC24F), потому что он должен использоваться в портативном приложении с низким энергопотреблением, а dsPIC не соответствует этому требованию. Фильтр простой, он 1-го порядка. Я предполагаю, что моя проблема заключается в преобразовании формата double->fixed point. Однако, читая код, я думаю, что делаю правильно, но это не работает... Интересно, почему.
«используется в портативном приложении с низким энергопотреблением, а dsPIC не соответствует этому требованию» <- такое общее утверждение вызывает тревогу в моей голове. Энергопотребление зависит от приложения, а аппаратная поддержка часто означает снижение общего энергопотребления. Любое приложение с низким энергопотреблением будет удерживать процессор в спящем режиме большую часть времени, поэтому сокращение времени «включения» — это путь к снижению энергопотребления...
это фиксированная точка или плавающая точка? Плавающее было бы плюсом, но вам не нужен DSP для реализации биквадратного фильтра. Вам гораздо больше нужно программное обеспечение для проектирования фильтров, чтобы найти параметры.

Похоже, вы никогда не цените себя, прежде чем использовать его. Вы используете y в вычислении для aux2, что правильно, потому что это отфильтрованный предыдущий образец, но вы понятия не имеете, что он запустится, как в этой ситуации.

Я бы посоветовал вам взять образец ADC, прежде чем вы войдете в цикл while (1). Присвойте это значение y, задержите 5 мс, затем войдите в свой цикл. Это позволит вашему фильтру работать нормально.

Я бы также порекомендовал вам пойти дальше и сменить шорты на более длинные переменные. Если у вас мало места, у вас будет гораздо более простой код, если вам не нужно вводить приведение типов в каждом цикле.

Многие ранние микроконтроллеры даже не имели инструкции умножения, не говоря уже о делении. БИХ-фильтр с магическим значением K = 1/2^8 был специально разработан для таких микроконтроллеров.

// filter implements
// y(n)=K*x(n)+y(n-1)*(1-K)
// with K = 1/2^8.
// based on ideas from
// http://techref.massmind.org/techref/microchip/math/filter.htm
// WARNING: untested code.
// Uses no multiplies or divides.
// Eventually y converges to a kind of "average" of the x values.
short average; // global variable
// (...)
while(1){
    short xn = readADC(adc_ch);
    // hope that we don't overflow the subtract --
    // -- perhaps use saturating arithmetic here?
    short aux1 = xn - average;
    short aux2 = aux1 / 256;
    // compiler should use an arithmetic shift,
    // or perhaps an unaligned byte read, not an actual divide
    average += aux2;
    delay_ms(5);
}