Как модулировать частоту ШИМ в реальном времени с помощью Microchip dsPIC?

Я пытаюсь изменить выходную частоту ШИМ примерно раз в миллисекунду, используя dsPIC33FJ256GP710, и у меня смешанные результаты. Я сначала попробовал это:

 #include <p33fxxxx.h> 

 _FOSCSEL(FNOSC_PRIPLL); 
 _FOSC(FCKSM_CSDCMD & OSCIOFNC_OFF & POSCMD_XT); 
 _FWDT(FWDTEN_OFF); 

 static unsigned int PWM_TABLE[7][2] = 
 { 
     {132, 66}, {131, 66}, {130, 65}, {129, 65}, {128, 64}, {127, 64}, {126, 63} // Compare, 50% duty 
 }; 

 static int curFreq = 0; 

 int main(void) 
 { 
     int i; 

     PLLFBD = 0x009E;                // Set processor clock to 32 MHz (16 MIPS) 
     CLKDIV = 0x0048;  

     LATCbits.LATC1 = 0;             // Make RC1 an output for a debug pin 
     TRISCbits.TRISC1 = 0;     

     LATDbits.LATD6 = 0;             // Make RD6/OC7 an output (the PWM pin) 
     TRISDbits.TRISD6 = 0; 

     T2CONbits.TON = 0;              // Disable Timer 2                      
     OC7CONbits.OCM = 0b000;         // Turn PWM mode off 
     PR2 = PWM_TABLE[curFreq][0];    // Set PWM period 
     OC7RS = PWM_TABLE[curFreq][1];  // Set PWM duty cycle 
     OC7CONbits.OCM = 0b110;         // Turn PWM mode on 
     T2CONbits.TON = 1;              // Enable Timer 2 

     while (1) 
     {                 
         for (i = 0; i < 3200; i++) {}      // Delay roughly 1 ms         
         curFreq = (curFreq + 1) % 7;       // Bump to next frequency        
         PR2 = PWM_TABLE[curFreq][0];       // Set PWM period 
         OC7RS = PWM_TABLE[curFreq][1];     // Set PWM duty cycle 
         LATCbits.LATC1 = !LATCbits.LATC1;  // Toggle debug pin so we know what's happening         
     } 
 } 

В результате ШИМ пропадает примерно на 4 мс с повторяющимся интервалом, примерно совпадающим с моим переключателем отладочного контакта (другими словами, когда код возится с регистрами периода и рабочего цикла). Прилагаю фото следа прицела. Канал 1 — это ШИМ, а канал 2 — это вывод отладки, который переключается, когда код пытается отрегулировать частоту.

Так или иначе, я начал думать о пролонгации таймера и поискал на нескольких форумах. Я придумал несколько идей, основанных на нескольких сообщениях, которые я прочитал. Лучшей идеей, казалось, было включить прерывание от Таймера 2, отключить внутри него режим ШИМ и изменить регистры периода и рабочего цикла только внутри прерывания от Таймера 2. Итак, я написал это:

 #include <p33fxxxx.h> 

 _FOSCSEL(FNOSC_PRIPLL); 
 _FOSC(FCKSM_CSDCMD & OSCIOFNC_OFF & POSCMD_XT); 
 _FWDT(FWDTEN_OFF); 

 static int curFreq = 0; 

 static unsigned int PWM_TABLE[7][2] = 
 { 
     {132, 66}, {131, 66}, {130, 65}, {129, 65}, {128, 64}, {127, 64}, {126, 63} // Compare, duty 
 }; 

 int main(void) 
 { 
     int i, ipl; 

     PLLFBD = 0x009E;                // Set processor clock to 32 MHz (16 MIPS) 
     CLKDIV = 0x0048;  

     LATCbits.LATC1 = 0;             // Make RC1 an output for a debug pin 
     TRISCbits.TRISC1 = 0;     

     LATDbits.LATD6 = 0;             // Make RD6/OC7 an output (the PWM pin) 
     TRISDbits.TRISD6 = 0; 

     OC7CONbits.OCM = 0b000;         // Turn PWM mode off     
     OC7RS = PWM_TABLE[curFreq][1];  // Set PWM duty cycle 
     PR2 = PWM_TABLE[curFreq][0];    // Set PWM period 
     OC7CONbits.OCM = 0b110;         // Turn PWM mode on 

     T2CONbits.TON = 0;              // Disable Timer 2 
     TMR2 = 0;                       // Clear Timer 2 register     
     IPC1bits.T2IP = 1;              // Set the Timer 2 interrupt priority level 
     IFS0bits.T2IF = 0;              // Clear the Timer 2 interrupt flag 
     IEC0bits.T2IE = 1;              // Enable the Timer 2 interrupt 
     T2CONbits.TON = 1;              // Enable Timer 2 

     while (1) 
     {                 
         for (i = 0; i < 1600; i++) {}      // Delay roughly 1 ms 
         SET_AND_SAVE_CPU_IPL(ipl, 2);      // Lock out the Timer 2 interrupt 
         curFreq = (curFreq + 1) % 7;       // Bump to next frequency         
         RESTORE_CPU_IPL(ipl);              // Allow the Timer 2 interrupt 
         LATCbits.LATC1 = !LATCbits.LATC1;  // Toggle debug pin so we know what's happening 
     } 
 } 

 void __attribute__((__interrupt__)) _T2Interrupt(void) 
 {     
     T2CONbits.TON = 0;              // Disable Timer 2 
     TMR2 = 0;                       // Clear Timer 2 register 
     OC7CONbits.OCM = 0b000;         // Turn PWM mode off 
     OC7RS = PWM_TABLE[curFreq][1];  // Set the new PWM duty cycle 
     PR2 = PWM_TABLE[curFreq][0];    // Set the new PWM period     
     OC7CONbits.OCM = 0b110;         // Turn PWM mode on 
     IFS0bits.T2IF = 0;              // Clear the Timer 2 interrupt flag 
     T2CONbits.TON = 1;              // Enable Timer 2 
 }

Это выглядит более стабильным, насколько я могу судить по моему древнему телескопу, но теперь форма волны больше не имеет правильной формы (рабочий цикл кажется необъяснимо непостоянным), и если я достаточно постараюсь, я смогу убедить себя, что я все еще увидеть миллисекунду выпадения ШИМ, когда мой прицел установлен на временную базу 5 или 10 миллисекунд.

Это, безусловно, лучше, чем было, и я могу продолжать возиться с ним в надежде исправить неправильную форму волны, создаваемую вторым битом кода, но мой вопрос:

Есть ли "правильный" способ сделать это? Или, по крайней мере, лучший путь, чем путь, по которому я иду?

Любая помощь будет тщательно, тщательно оценена.

Трассировка области действия http://www.freeimagehosting.net/uploads/c132216a28.jpg

Ответы (3)

Для всех, кому интересно, вот решение, к которому я пришел сегодня:

#include <p33fxxxx.h>

_FOSCSEL(FNOSC_PRIPLL);
_FOSC(FCKSM_CSDCMD & OSCIOFNC_OFF & POSCMD_XT);
_FWDT(FWDTEN_OFF);

static int curFreq = 0;
static int nextFreq = 0;

static unsigned int PWM_TABLE[7][2] =
{
    {132, 66}, {131, 66}, {130, 65}, {129, 65}, {128, 64}, {127, 64}, {126, 63} // Compare, duty
};

int main(void)
{
    int i, ipl;

    PLLFBD = 0x009E;                // Set processor clock to 32 MHz (16 MIPS)
    CLKDIV = 0x0048; 

    LATCbits.LATC1 = 0;             // Make RC1 an output for a debug pin
    TRISCbits.TRISC1 = 0;

    OC7CONbits.OCM = 0b000;         // Turn PWM mode off    
    OC7RS = PWM_TABLE[curFreq][1];  // Set PWM duty cycle
    PR2 = PWM_TABLE[curFreq][0];    // Set PWM period
    OC7CONbits.OCM = 0b110;         // Turn PWM mode on

    T2CONbits.TON = 0;              // Disable Timer 2
    TMR2 = 0;                       // Clear Timer 2 register    
    IPC1bits.T2IP = 1;              // Set the Timer 2 interrupt priority level
    IFS0bits.T2IF = 0;              // Clear the Timer 2 interrupt flag
    IEC0bits.T2IE = 1;              // Enable the Timer 2 interrupt
    T2CONbits.TON = 1;              // Enable Timer 2

    while (1)
    {                
        for (i = 0; i < 1600; i++) {}      // Delay roughly 1 ms
        SET_AND_SAVE_CPU_IPL(ipl, 2);      // Lock out the Timer 2 interrupt
        curFreq = (curFreq + 1) % 7;       // Bump to next frequency
        nextFreq = 1;                      // Signal frequency change to ISR
        RESTORE_CPU_IPL(ipl);              // Allow the Timer 2 interrupt        
    }
}

void __attribute__((__interrupt__)) _T2Interrupt(void)
{   
    IFS0bits.T2IF = 0;                  // Clear the Timer 2 interrupt flag     

    if (nextFreq)
    {        
        nextFreq = 0;                   // Clear the frequency hop flag
        OC7RS = PWM_TABLE[curFreq][1];  // Set the new PWM duty cycle
        PR2 = PWM_TABLE[curFreq][0];    // Set the new PWM period         
    }
}

С помощью области видимости и отладочной булавки я подтвердил свое подозрение: исходный код страдал от состояния гонки. Основной цикл не удосужился синхронизировать изменения в PR2 с фактическим состоянием счетчика TMR2, и поэтому время от времени устанавливал PR2 в значение МЕНЬШЕ (или, возможно, равное) текущему значению TMR2. Это, в свою очередь, заставит TMR2 считать до тех пор, пока не перевернется, а затем продолжить счет, пока не достигнет PR2 и не сгенерирует нарастающий фронт. В то время как TMR2 считал до 65535, чтобы перевернуться, выход ШИМ не генерировался. При 16 MIPS время переключения для 16-разрядного таймера, такого как TMR2, составляет примерно 4 мс, что объясняет пропадание ШИМ на 4 мс. Итак, код делал именно то, для чего я его написал :)

Во втором фрагменте код корректно синхронизирует изменения в PR2 и регистре рабочего цикла с событием опрокидывания TMR2, поэтому 4-мс пропадание исчезло. Я упомянул «странную» форму волны, связанную с этим примером: это было связано с тем, что вывод RD6/OC7 был сконфигурирован как выход и имел низкое значение, установленное в регистре LATD. Второй фрагмент фактически отключает режим ШИМ внутри ISR Таймера 2: это позволяет функциям GPIO вступить во владение и отключает RD6/OC7 на несколько микросекунд перед повторным включением ШИМ и генерацией нарастающего фронта, что приводит к «икоте».

У второго фрагмента также есть проблема, заключающаяся в том, что он переконфигурирует PR2 и регистр рабочего цикла при каждом переключении Таймера 2, независимо от того, дал ли основной цикл команду на изменение частоты или нет. Из наблюдений мне кажется, что таймер переворачивается и генерирует нарастающий фронт на выводе ШИМ, а ТОГДА таймер 2 ISR получает управление через несколько наносекунд (из-за задержки вектора и т. Д.). Отключение ШИМ и перенастройка регистров каждый раз не дает вам правильную частоту и рабочий цикл в долгосрочной перспективе, потому что аппаратное обеспечение уже сгенерировало нарастающий фронт и начало считать до следующего значения сравнения.

Это означает, что в исправленном фрагменте, который я опубликовал сегодня, работа, проделанная в ISR Timer 2, должна быть сведена к минимуму! Поскольку я запускаю ШИМ на такой высокой частоте, и из-за небольшой задержки между передним фронтом, генерируемым оборудованием ШИМ, и вызовом таймера 2 ISR, к тому времени, когда я попадаю в ISR, TMR2 уже успел досчитать до справедливого числа. Мой код должен установить PR2 и регистр рабочего цикла немедленно и напрямую (т.е. никаких вызовов функций, и даже поиск в таблице подталкивает его), в противном случае он рискует пропустить сравнение и вызвать ошибку пролонгации 4 мс, которая была моей оригинальной проблема.

В любом случае, я думаю , что это точное описание вещей, и я запускаю код в своем «настоящем» приложении с обнадеживающими результатами. Если что-то еще изменится, я опубликую здесь, и, конечно, любые исправления к вышеизложенному будут очень признательны.

Спасибо за помощь, pingswept.

Мне кажется вполне правдоподобный анализ. Молодец, сэр!
Еще несколько быстрых замечаний по этому поводу: во-первых, нет необходимости сохранять и тестировать флаг, указывающий, что целевая частота изменилась в пределах ISR; PR2 и регистр рабочего цикла могут быть безоговорочно изменены. В некоторых случаях это может сэкономить несколько циклов. Во-вторых, вы получите более точную начальную частоту и коэффициент заполнения, если линия, которая включает ШИМ, будет расположена как можно ближе к линии, которая включает таймер 2. Чем дальше они друг от друга, тем больше времени проходит, когда вывод ШИМ находится в таймер начинает отсчет к сравниваемым значениям.

Я бы попробовал замедлить все в 10 раз, чтобы вы могли более подробно увидеть, когда именно ШИМ умирает. Я также попытался бы настроить значения в вашей таблице периода и рабочего цикла. Может быть, вы неправильно настраиваете ШИМ на 4 из 7 циклов? Я заметил, что есть 4 значения периода ШИМ, которые превышают 128 — может быть, это вызывает проблемы?

Если бы это не помогло, я бы поискал более крупную модель. Как часто повторяется промежуток в 4 мс?

Очень хорошо заданный вопрос, кстати.

Спасибо за ваш ответ! Очень полезные идеи. Я возьму это завтра и отчитаюсь, если мне удастся решить проблему. Регистры периода и рабочего цикла на dsPIC33 являются 16-битными регистрами, поэтому я не уверен, что 128 — это проблема, но, тем не менее, это отличный улов. Я еще раз проверю, что значения в таблице в порядке. Я подозреваю, что код в первом фрагменте периодически устанавливал регистр периода в значение НИЖЕ регистра TMR2, и поэтому он не генерировал передний фронт снова, пока TMR2 не переполнился. ~ 65000 циклов на частоте 16 МГц - это примерно 4 мс. Посмотрим :)
Было бы интересно проверить, точно ли нарастающий фронт в конце «4 мс» соответствует нарастающему фронту другого сигнала. Из показанного выше телескопа видно, что расстояние между ними составляет около 100 мс, что составляет много тактов.
Кстати, просто из любопытства, почему написано "Microchip Tecnology Inc." [так в оригинале] на вашем скриншоте?
У тебя зоркий глаз! Согласен, интересна задержка между нарастающим фронтом отладочного штифта и перезапуском ШИМ. Интересно, связано ли это с моей теорией о том, что цикл while может колебать PR2 в любой момент счета TMR2, и, таким образом, следующий нарастающий фронт может появиться в случайном интервале. Завтра я обязательно сосредоточусь на этом. Что касается водяного знака Microchip, я разместил свой вопрос на форумах Microchip (которые до сих пор были менее чем полезными) и переработал изображение здесь. Они поставили водяной знак и, по-видимому, неправильно написали собственное имя :) Кстати, я очень ценю ваше сотрудничество.

Я не уверен насчет ШИМ в dsPIC, но в документации на PIC16F1508 говорится, что когда TMR2 достигает цикла перезагрузки, он перезагружает регистр TMR2, а затем загружает регистры PWMxDC в фактические регистры рабочего цикла в аппаратном обеспечении ШИМ. т.е. запись рабочего цикла ШИМ автоматически синхронизируется с перезагрузкой TMR2. Поэтому, когда вы пишете рабочий цикл, он не имеет эффекта до следующего рабочего цикла. Это означает, что ваше изменение рабочего цикла может быть отложено на один цикл перезагрузки TMR2, но это единственное изменение, которое вы должны увидеть. Посмотрите в своей документации.