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_

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

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
Как это сделать в Cube?
0
  • avatar
  • Aneg
  • 29 октября 2018, 12:48
Вообще не представляю. Я им не пользуюсь, и вам не советую.
0
А можете посоветовать, что можно почитать про работу таймеров в STM32?
0
Вы только не смейтесь и не обижайтесь — я посоветую Reference Manual. Там правда очень толково все описано, особенно на новые серии. Я и сам не читаю ничего другого по STM32 — просто необходимости нет. Только недавно запускал FMC, сейчас сижу запускаю Ethernet MAC — все по официальной документации.
0
По таймерам стоит почитать вот это.
0
. Я им не пользуюсь, и вам не советую.
И напрасно. Куб штука удобная, другой вопрос, что то, что он генерит никуда не годится. Либо HAL, либо, еще хуже, портянки low level типа того, что в топике. Ни то, ни другое не пригодно для использования.
-1
другой вопрос, что то, что он генерит никуда не годится.

Это как раз основной вопрос. :)

Не надо никаких HAL'ов. Если человек может освоить документацию и имеет небольшие навыки программирования, то ему не составит труда писать с использованием регистров.

Например, для FMC я писал небольшой кодогенератор на Lua, который генерировал мне макросы для портов.
0
Если человек может освоить документацию и имеет небольшие навыки программирования, то ему не составит труда писать с использованием регистров.
То есть вместо одних вонючих портянок писать другие вонючие портянки. Выбор, как бы, понятен, но это вовсе не значит, что он правильный или удобный.
-1
Добро пожаловать в низкоуровневое программирование — некоторых прикладников оно шокирует, действительно. Особенно тех, которые не догадываются о том, как все работает под капотом привычных им конструкций на Питоне. :)
0
Я писал на низком уровне тогда, когда папки и мамки большинства местных обиталей еще только начали проявлять интерес к противоположному полу. А вот эти вот ваши вонючие портянки это не низкоуровневое программирование, а его профанация, попытка превратить непроизводительный труд в некую «илитную» «фишку». На самом деле это всего лишь бессмысленная трата ресурсов мозга программиста. Справедливости ради должен заметить, что С не дает достаточно инструментов, что бы сделать что-нибудь внятное с этими портянками, а изучение С++ для «илиты» выше их возможностей. Ну а те, кто таки его освоил, могут наслаждаться настоящим низкоуровневым программированием.
-2
Куда уж мне там до элиты, я скромный инженер, так что «непроизводительный труд» я не стесняюсь автоматизировать скриптованием. Но, конечно, для того, чтобы написать собственный кодогенератор «на случай», надо понимать, как устроена периферия. Это сложнее, чем копировать примеры HAL или тыкать мышкой в Cube. Потому те, кто пишут на HAL, регулярно приходят ко мне (это на работе, из других отделов), чтобы я им объяснил, почему у них что-то не работает.

Полагаю, вы один из тех немногих людей, которые пишут на Ada (если начинали в такие далекие времена, времена богов и героев). Между прочим да, по показателям надежности (главное для встроенных систем) — это лучший язык. По сравнению с ним что C, что C++ ничего не стоят.

Ну и кстати, расскажите же нам, как сделать то, что я описывал в статье, в Cube? Мне самому интересно, а заново ставить Cube лень.
0
Куда уж мне там до элиты, я скромный инженер
Что не мешает вам вести себя как та самая «илита». Вот характерный пример:
Ну и кстати, расскажите же нам, как сделать то, что я описывал в статье, в Cube?

Во-первых: кто такие «вы» и почему «вам» я что-то должен рассказывать? С чего вдруг, вы решили, что я «вписался» за куб, если выше открытым текстом написал, что то, что он генерит неюзабельно? Наконец, где я писал, что конкретно эту задачу можно сделать в кубе?
-2
«Вы» это я (Aneg) и _YS_.
0
Aneg вы поручили _YS_ говорить от вашего имени?
-1
«непроизводительный труд» я не стесняюсь автоматизировать скриптованием
Вы так привыкли к своим портянкам, что даже не поняли о чем речь. Более того, в вашем понимании, если не портянки, то HAL и никаких других вариантов вы не видите.
-1
Во-первых: кто такие «вы» и почему «вам» я что-то должен рассказывать?

«Мы» — это уважаемая аудитория сообщества, которая, уверен, как и я, не откажется получить крупицу бесценного знания от умудренного опытом старца, годящегося их родителям в деды или, на худой конец, в отцы.

Я не решил, что вы прозелит Cube. Просто я предположил, что годы плодотворной практики расширили ваш кругозор в числе прочего и до блестящего знания возможностей Cube, которым я не обладаю; а я всегда стремлюсь поучиться у знаюшего человека, потому и задал вопрос. Если в Cube это невозможно — то и такую новость я приму со смирением.

Более того, в вашем понимании, если не портянки, то HAL и никаких других вариантов вы не видите.

Вариантов распихивать битовые маски по регистрам существует множество. Можно и до шаблонов дойти. Это смотрится круто…

Выше я писал только о том, что ко мне приходят люди, запутавшиеся в HAL. Люди, запутавшиеся в шаблонах, ко мне не приходили, вот и все.
+1
«Мы» — это уважаемая аудитория сообщества
Уважаемая аудитория сообщества в курсе, что поручила вам вести переговоры от ее имени?
Можно и до шаблонов дойти.
Можно. И для этого даже не нужно выходить за пределы сообщества. Покопайтесь в блоге neiver , например.
-2
Куб штука удобная, другой вопрос, что то, что он генерит никуда не годится.
А зачем он тогда?
0
А зачем он тогда?
На нем довольно удобно смотреть разные варианты распиновки с точки зрения разводки. Сильно экономит время.
0
Чтобы совершенствоваться!!!
И в один прекрасный день его научат генерировать отличный код.
0
Учитывая куда ST занесло в плане написания либ, ждать прийдётся очень долго.
+1
Учитывая куда ST занесло в плане написания либ, ждать прийдётся очень долго.
На самом деле это только вопрос желания и свободного времени, никаких особых чудес или сложностей там нет.
0
И в один прекрасный день его научат генерировать отличный код.
Его самого — врядли. В принципе, задача решаемая, но слишком геморная. Куда проще написать свою генерилку прекрасного кода из кубовского файла проекта. Там примитивный текстовый файл в стиле =и вся необходимая информация, что бы сгенерировать инициализацию, там есть.
0
Сорри, форум порезал угловые скобки, осталось только = :)
Формат файла такой:
<key>=<value>
0
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);

У меня почему-то эту часть не хочет компилировать.
0
  • avatar
  • Aneg
  • 29 октября 2018, 17:19
А-а-а. Ну так тут используются мои самописные макросы SET_CRH/SET_CRL. Забыл об этом написать в статье, т.к. давно их использую.

Эти макросы не делают ничего волшебного — просто скрывают битовые сдвиги.

Посмотрите в докуменации, как устроен регистр GPIOx->CRH, я наизусть не помню, где там какие битовые поля.
0
Если все будет совсем плохо, то по приходе домой я выложу свой заголовочник stm32_ports.h
0
Ждем-с stm32_ports.h
0
#ifndef STM32_PORTS_H_INCLUDED
#define STM32_PORTS_H_INCLUDED

#define M_IN        0x00
#define M_OUT_2M    0x02
#define M_OUT_10M   0x01
#define M_OUT_50M   0x03

#define IN_ANALOG   0x00
#define IN_FLOATING 0x01
#define IN_PULLED   0x02

#define OUT_GP_PP   0x00
#define OUT_GP_OD   0x01
#define OUT_AF_PP   0x02
#define OUT_AF_OD   0x03

#define SET_CRL(pin_no,mode,conf)           (((mode) | ((conf) << 2)) << (4*(pin_no)))
#define SET_CRH(pin_no,mode,conf)           (((mode) | ((conf) << 2)) << (4*((pin_no)-8)))
#define PIN_MASK(pin_no)                    (1<<(pin_no))

#define PIN_SETMASK(pin_no)                 (1<<(pin_no))
#define PIN_RSTMASK(pin_no)                 (1<<((pin_no)+16))

#endif /* STM32_PORTS_H_INCLUDED */
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.