Complementary PWM и Push-Pull режимы на таймере 1 контроллеров STM32

Это скорее не статья, а развернутый ответ на сомнения моего уважаемого коллеги Lifelover 'а, которые он выражал в этом обсуждении относительно возможности реализации некоторых режимов ШИМ на таймерах контроллеров серии STM32.



Итак (орфография оригинала сохранена):

Да ладно. По фазе оно сколочно гвоздями краями или серединками и ничего с этим не сделаешь.

Попробуем что-нибудь сделать.

Для начала посмотрим, что нам вообще требуется. Я нарисовал картинку попонятнее, чтобы было яснее видно, в чем проблема.



В комплементарном режиме второй сигнал представляет собой инверсию первого, укороченную с учетом продолжительности защитного интервала (deadtime).

В двухтактном режиме сигналы выровнены друг относительно друга и изменяют коэффициент заполнения синхронно.

Теперь к реализации. Для проверки идей я использовал плату STM32-vldiscovery, базирующуюся на контроллере STM32F100RBT6B — datasheet, reference manual.

Комплементарный режим получается на одном канале. Чтобы получить такой сигнал, достаточно включить комплементарный канал (CC1NE в регистре CCER) и задать защитный интервал (константа DEADTIME, записываемая в младшие биты BDTR):


    //CH1: PWM mode with complementary output & deadtime
    TIM1->CCMR1=TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE;
    TIM1->CCER=TIM_CCER_CC1E | TIM_CCER_CC1NE;
    TIM1->BDTR=TIM_BDTR_MOE | DEADTIME;
    TIM1->CCR1=PWM_VALUE;
    TIM1->ARR=TMR_T;
    TIM1->CR1=TIM_CR1_ARPE;
    TIM1->CR1|=TIM_CR1_CEN;
    TIM1->EGR=TIM_EGR_UG;


Результат наблюдаем на выводах PA8 (прямой канал) и PB13 (комплементарный канал):

D<50%:



D>50%:



Теперь самое интересное: двухтактный режим! Увы, на одном канале его, действительно, реализовать нельзя. Для этого нужно использовать два канала из четырех доступных в таймере 1. Таймер при этом должен работать в режиме, который Atmel Microchip, например, называет phase-correct PWM — сначала счет идет вверх, потом вниз.

Первый канал сравнения надо инициализировать в режиме PWM 2, а второй — в режиме PWM 1 (или наоборот). Это и даст интересующий сдвиг сигналов по фазе. Ну а дальше включаем синхронное обновление значений регистров (OC1PE/OC2PE), сами выходы (CC1E/CC2E), и так далее. Самое главное — не забыть про MOE, без этого ничего не будет работать.



Простите, отвлекся.

Код инициализации таймера:


    //CH1: PWM mode 2, CH2: PWM mode 1, preload enabled on all channels
    TIM1->CCMR1=TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_0 | TIM_CCMR1_OC1PE | TIM_CCMR1_OC2M_2 | TIM_CCMR1_OC2M_1 | TIM_CCMR1_OC2PE;
    TIM1->CCER=TIM_CCER_CC1E | TIM_CCER_CC2E;
    TIM1->BDTR=TIM_BDTR_MOE;
    TIM1->CCR1=TMR_T - PWM_VALUE;
    TIM1->CCR2=PWM_VALUE;
    TIM1->ARR=TMR_T;
    TIM1->CR1=TIM_CR1_ARPE | TIM_CR1_CMS_1 | TIM_CR1_CMS_0;
    TIM1->CR1|=TIM_CR1_CEN;
    TIM1->EGR=TIM_EGR_UG;


Результат виден на выводах PA8 (первый канал) и PA9 (второй канал).

D<25%:



D>25%:



Полный код тестового проекта:


#include "stm32f10x.h"
#include "stm32_ports.h"

#define TIM1_CH1N_PB        13
#define TIM1_CH1_PA         8
#define TIM1_CH2_PA         9

#define LED1_G_PC           9
#define LED2_B_PC           8

#define PWM_VALUE           20
#define TMR_T               200
#define DEADTIME            20

#define PP_MODE
//#define COMPL_MODE

void main(void)
{
    RCC->APB2ENR|=RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_TIM1EN;

    GPIOA->CRH=SET_CRH(TIM1_CH1_PA,M_OUT_50M,OUT_AF_PP) | SET_CRH(TIM1_CH2_PA,M_OUT_50M,OUT_AF_PP);
    GPIOB->CRH=SET_CRH(TIM1_CH1N_PB,M_OUT_50M,OUT_AF_PP);
    GPIOC->CRH=SET_CRH(LED1_G_PC,M_OUT_50M,OUT_GP_PP) | SET_CRH(LED2_B_PC,M_OUT_50M,OUT_GP_PP);

#ifdef PP_MODE
    //CH1: PWM mode 2, CH2: PWM mode 1, preload enabled on all channels
    TIM1->CCMR1=TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_0 | TIM_CCMR1_OC1PE | TIM_CCMR1_OC2M_2 | TIM_CCMR1_OC2M_1 | TIM_CCMR1_OC2PE;
    TIM1->CCER=TIM_CCER_CC1E | TIM_CCER_CC2E;
    TIM1->BDTR=TIM_BDTR_MOE;
    TIM1->CCR1=TMR_T - PWM_VALUE;
    TIM1->CCR2=PWM_VALUE;
    TIM1->ARR=TMR_T;
    TIM1->CR1=TIM_CR1_ARPE | TIM_CR1_CMS_1 | TIM_CR1_CMS_0;
    TIM1->CR1|=TIM_CR1_CEN;
    TIM1->EGR=TIM_EGR_UG;
#endif

#ifdef COMPL_MODE
    //CH1: PWM mode with complementary output & deadtime
    TIM1->CCMR1=TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE;
    TIM1->CCER=TIM_CCER_CC1E | TIM_CCER_CC1NE;
    TIM1->BDTR=TIM_BDTR_MOE | DEADTIME;
    TIM1->CCR1=PWM_VALUE;
    TIM1->ARR=TMR_T;
    TIM1->CR1=TIM_CR1_ARPE;
    TIM1->CR1|=TIM_CR1_CEN;
    TIM1->EGR=TIM_EGR_UG;
#endif

    while (1)
    {
    }
}
  • +5
  • 07 января 2018, 16:13
  • _YS_

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

RSS свернуть / развернуть
У меня есть ощущение, что Lifelover имел ввиду ровно то, что вы показали. Сколочено гвоздями или краями (комплементарный режим) или серединками (пуш-пулл) и ничего с этим не сделать.
+1
Еще одну статью, чтобы показать остальные режимы, кроме PWM1 и PWM2, я писать не буду. ;)
0
Именно так, произвольно фазу двигать нельзя. Про описанные режимы я прекрасно знаю, это также точно настраивается и для AVR. Не вижу где в этой статье ответ и на что.
0
Отписался в комментарии ниже.
0
Вы знаете, я еще раз подумал над вашими и Lifelover'а словами про края и серединки. Тут, конечно, надо рисовать, чтобы всем все стало понятно. Но если имеется в виду получение совсем произвольного сдвига между каналами (зачем?), то для этого можно сделать следующее (даже не трогая остальные аппаратные возможности):

— настроить прерывание по обнулению (при счете вниз) и переполнению (при счете вверх) таймера;
— в этом прерывании при достижении максимального значения таймера прибавлять значение сдвига к основному значению CCR канала, а при счете вниз и достижении нуля — вычитать.

Таким образом можно получить любой сдвиг.
0
Т.е. сделать программно… По сути таймер будет выполнять действия «включи по счёту M тактов» и «выключи по счёту N тактов». Для таймера это простые задачи. Хороший пример, который показывает, что когда задача хоть на миллиметр (а такие задачи встречаются гораздо чаще чем некоторые малюют) отходит от стандартного набора, и продвинутая (которая далеко не такая продвинутая, как некоторые малюют) периферия стм проигрывает и начинают выигрывать быстрые прерывания как в AVR и эффективные команды типа sbi/cbi как в AVR.
0
* и удобный, человеческий ассемблер, разумеется.
0
«Не поймите меня неправильно», разумеется. :D
0
Ради интереса, когда (и, если не секрет) для чего вам было необходимо получать произвольный сдвиг между каналами ШИМ? :)

Но это ладно. Я выше писал в предположении, что мы используем один таймер. Если разрешить себе использовать два таймера, то все сводится к конфигурации ШИМ-каналов от разных таймеров и задании разных начальных значений для этих двух таймеров. Дальше все будет происходить само по себе.

Надо сказать, что в STM32 это делается гораздо спокойнее, потому что таймеров там, как правило, больше, и потратить два не так жалко. :)

Вместо SBI/CBI в STM32 есть механизм bit-banding (отображение битов на индивидуальные адреса памяти). Для портов там вообще есть специальные регистры атомарной установки/снятия битов.

Не поймите меня правильно, я не продвигаю STM32 (или какую-то другую) архитектуру как абсолют. Если бы какая-то архитектура была абсолютом, то уже давно осталась бы она одна. Просто мы с вами разговариваем именно о STM32 и AVR, потому и примеры на STM32. С тем же успехом я мог бы приводить примеры на MSP430. Просто мы о них пока не говорим.

Кстати, у MSP430 мне как раз таки очень нравится ассемблер. Писать на нем — одно удовольствие! Вот это супер-архитектура для ассемблерного кода. Попробуйте, не пожалеете. Из них можно многое выжать.

А так, контроллер надо выбирать под задачу. В разных задачах разные архитектуры имеют преимущество. Скажем, в AVR мне нравится АЦП, а в STM32 — таймеры, особенно поддержка инкрементального энкодера.
+1
Пару раз мне хотелось иметь 3-4 фазы ШИМ (между которыми, соответственно, 120 и 90 градусов). Хотя да, не столь уж часто это нужно…

Вместо SBI/CBI в STM32 есть механизм bit-banding (отображение битов на индивидуальные адреса памяти).
Уродский механизм, именно после того как я прочитал о нём, моё отношение к стм сменилось на разочарование (впрочем, потом это подтвердилось раз 100). Неудобный (компилятор сам не делает) и костыльный (магические числа). Главное — неэффективынй. Установка бита через RMW — 5 команд (9 тактов ЕМНИП), через битбанд — 3 команды (5 тактов). Быстрый способ, нда… После AVR плеваться хочется.

Для портов там вообще есть специальные регистры атомарной установки/снятия битов.
В PIC32 на каждый регистр (SFR) есть по 3 регистра для установки, сброса и инверсии. Мне это решение нравится куда больше битбэнда.

Кстати, у MSP430 мне как раз таки очень нравится ассемблер. Писать на нем — одно удовольствие! Вот это супер-архитектура для ассемблерного кода. Попробуйте, не пожалеете. Из них можно многое выжать.
Попробую. Подробно пока не смотрел, но в целом отношение положительное. Мне нравится выбор разрядности (32 бита — очень тяжело для набора команд фиксированной длины, а 16 — гораздо лучше для кодирования команд и весьма неплохо для вычислений и для адресации).
0
В PIC32 на каждый регистр (SFR) есть по 3 регистра для установки, сброса и инверсии. Мне это решение нравится куда больше битбэнда.
У STM32 ровно так же 3 регистра для этих целей.
Главное — неэффективынй. Установка бита через RMW — 5 команд (9 тактов ЕМНИП), через битбанд — 3 команды (5 тактов).
Учитывая частоты это вообще не проблема.
Быстрый способ, нда… После AVR плеваться хочется.
Особенно учитывая, что у авр ничего похожего даже близко нет.
0
Большое спасибо за статью, в своей практике с ШИМ-ом сталкиваюсь нечасто, так что знания по различным режимам лишними не будут.
P.S. За MOE отдельный плюс :D
0
Нутк, без MOE (TIM_BDTR_MOE) действительно ничего не будет работать. :))
0
Самое главное — не забыть про MOE, без этого ничего не будет работать
Да уж, таймирование MOE — здеся уже стало мемом.
0
Не знал, что так можно настроить таймер. Спасибо.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.