
Как известно на микроконтроллерах STM32 можно генерировать PWM сигналы. Это всегда применяют для регулирования яркости свечения светодиодов, управления температурой нагревателей, управления крутящим моментом на моторах. При этом легко регулировать частоту, заполнение и инвертировать фазу, меняя полярность. Однако как непрерывно регулировать фазу на первый взгляд даже не ясно. Фаза сигнала это то насколько он смещен вдоль оси Х. В карте регистров аппаратных таймеров STM32 просто нет регистра, который отвечает за фазу сигнала.
Вы, конечно, можете сделать полностью программный PWM, написать функцию вычисления PWM семпла.
uint8_t calc_pwm_sample_num(uint64_t time_us, uint32_t period_ms, float duty, int32_t phase_ms) { uint8_t val = 0; if(100.0f < duty) { duty = 100.0f; } float cur_time_ms = ((float)time_us) / 1000.0f; int32_t time_saw = (phase_ms + ((int32_t)cur_time_ms)) % period_ms; int32_t threshold = (int32_t)(((float)(period_ms)*duty) / 100.0f); if(threshold < time_saw) { val = 0; } else { val = 1; } return val; }
Однако не получится генерировать PWM на высокой частоте, да и стабильность программного PWM оставляет желать лучшего. Программный PWM нагружает процессор.
В чем проблема?
Проблема в том, что если вы запустите на STM32 два или более аппаратных PWM сигнала на одинаковой частоте, то скорее всего сигналы окажутся далеко не синфазными. Более того, предсказать образовавшуюся фазу PWM не удастся. Это зависит от множества факторов: частоты процессора, организации проекта и скорости исполнения кода и пр.
Почему нужно регулировать фазу PWM?
1--> Для тестировочного оборудования. Двумя PWM сигналами вы можете аппаратно имитировать сигнал квадратурного энкодера .

2--> Для формирования пары sin / cos сигнала в квадратурном смесителе (с фазовым сдвигом 90° относительно другого).

3--> Для управления асинхронными BLDC двигателями вам надо три PWM смещенных по фазе на 120 градусов.

4--> Для управления бензиновыми форсунками (или свечой зажигания). Частота у них всех одинаковая, а фаза срабатывания у всех разная.
5--> Для аппаратной генерации сигнала гетеродина (LO) в квадратурном смесителе в SDR обработке входного сигнала с ADC.
6--> Специально сделанное рассогласование фаз PWM сигналов (например в нагревателях или светильниках) помогает уменьшить электромагнитные помехи в цепях и, тем самым, пройти испытания на электромагнитную совместимость прибора.
Теория
Фаза - это то, на сколько два сигнала смещены друг относительно друга вдоль оси Х. То есть по времени. Может измеряться в секундах или в градусах.
Основная идея управления фазой в том, что фаза управляется регистром компаратора от другого таймера. Назовем его опорный таймер (или Master-таймер), фаза которого условно принимается за ноль. В момент воображаемого прерывания по превышению счетчика значения опорного компаратора следует обнулить счетчик таймера B. Master-таймер генерирует на своем выводе опорный ШИМ-сигнал.
Таким образом PWM на таймере B сместится относительно PWM на таймере A. В результате на улицу у вас будут выходить два PWM сигнала смещенных друг относительно друга по фазе.

В CLI (Command Line Interface) у вас может быть отдельная команда для управления фазой.

Она принимает номер PWM и абсолютное значение фазы в секундах (или градусах). Это значение может принимать числа от нуля (синфазные с опорным) до значения периода (360 deg).

Достоинства
++ Можно непрерывно регулировать фазу с погрешностью равной 1/(значение периода).
++ Нет прерываний
++ Нет остановки таймеров на время изменения фазы
Недостатки
--два аппаратных таймера должны работать на одной частоте.
--для управления фазой таймера B надо проникать в настройки таймера А. Происходит нарушение модульности.
--Сложность настройки связи между таймерами. Сложность поддержки кода.
--На один опорный PWM вы можете посадить только 4 ведомых PWM, так как всего только 4 компаратора (в реальности на STM32 только один).
--Если Вы решите обнулять ведомый таймер в обработчике прерывания по компаратору, то получите заметную просадку по производительности своего основного приложения. Всё таки аппаратные PWM работают на высоких частотах.
Практическая часть
Что ж, c теорией покончено. Попробуем теперь провернуть это на реальной учебно-тренировочной электронной плате JZ-F407VET6.

Есть несколько способов регулировать фазу:
0) У STM32 таймеров есть бит полярности. Это значит, что вы можете атомарно инвертировать PWM сигнал. Это равносильно смещению фазы на пол периода (на 180 градусов). Таким образом можно буквально одним битом генерировать BPSK модуляцию на обыкновенном аппаратном PWM. Преимущество в том, что не нужен второй таймер. Недостаток в том, что нет плавности изменения фазы.
1) Наивный способ - это просто подменить значение счетчика таймера PWM3 на новое значение относительно счетчика таймера TIM8 . Алгоритм прост. Берем текущее значение из регистра TIM8_CNT, добавляем к нему желаемый прирост разности фаз и результат тут же быстро прописываем в регистр TIM4_CNT. Код очень простой.
float timer_tick_get_s(uint8_t num) { float tick_s = -1; float bus_clock = (float)timer_bus_clock_get(num); if(0.0 < bus_clock) { uint32_t prescaler = timer_prescaler_get(num); tick_s = ((float)(prescaler + 1)) / bus_clock; } return tick_s; } static int32_t TimerPhaseUsToCompareValue(uint8_t num, int32_t phase_us) { int32_t phase_value = 0; float tick_s = timer_tick_get_s(num); float phase_s = USEC_2_SEC(phase_us); float phase_value_f = (phase_s / tick_s); phase_value = (int32_t) phase_value_f; return phase_value; } uint32_t timer_counter_get(uint8_t num) { uint32_t timer_cnt32 = 0; TIM_TypeDef* TIMx = timer_get_ptr(num); if(TIMx) { timer_cnt32 = TIMx->CNT; } return timer_cnt32; } bool timer_counter_set(uint8_t num, uint32_t value) { bool res = false; TimerInfo_t* Info = TimerGetInfo(num); if(Info) { Info->TIMx->CNT = value; res = true; } return res; } bool pwm_phase_set_counter_adjust(uint8_t num, int32_t phase_us) { bool res = false; PwmHandle_t *Node = PwmGetNode(num); if(Node) { int32_t compare_value = TimerPhaseUsToCompareValue(Node->PhaseComparator.timer, phase_us); int32_t counter_base = (int32_t) timer_counter_get(Node->PhaseComparator.timer); int32_t value = counter_base + compare_value; res = timer_counter_set(Node->TimChan.timer, (uint32_t) value); LOG_INFO(PWM, "SetPhaSW:%d us,compareValue:%d,CNTMaster:%u,CNTslave%u", phase_us, compare_value,counter_base,value); } return res; }
Таким образом можно на лету регулировать фазу. Для PWM2 на частоте 1 kHz погрешность установки фaзы составила 3us. Это 0.29 %. Эта погрешность обусловлена тем, что отработка функций тоже требует своего времени. В сумме на код самой установку фазы набегает 2.8us

3) Фазу можно регулировать и при помощи одного аппаратного таймера. Для этого надо настроить два канала и подать из на вход микросхемы бинарного сложения по модулю два. Например SN74AC86DR. Таким образом меняя значения компаратора на обоих таймерах вы сможете двигать фазу прямоугольных импульсов на выходе XOR-а. Минус лишь в том, что на PCB надо закладывать отдельную микросхему дискретной логики. Плюс же в том, что нужен только один аппаратный таймер.

4) У STM32 есть правильный способ регулирования фазы. Это механизм составного таймера (таймер с прицепом). Позволяет регулировать фазу без остановки таймера.

Согласно спецификации таймером 4 можно управлять при помощи таймера 8. Для этого надо сконфигурировать таймер 4 на режим ведомого устройства и указать, что надо брать события от IRT2 (от TIM3)

Вот справка из спецификации про то какие Master таймеры могут быть у каждого конкретного slave таймера. У таймеров 6, 7, 10, 11, 13, 14 вообще нет хозяина. На них получится регулировать фазу только вторым неточным способом.

План таков. Запустить два таймера PWM сигнала TIM8 и TIM4 на частоте 1kHz и заполнением 50%. Фазу PWM на TIM4 непрерывно двигать относительно PWM на первом компараторе TIM8.
Роль | Название сигнала | Таймер | Канал | GPIO | PinMux | Заполнение,% | Freq, Hz | Фаза, deg |
Master | PWM2 | TIM8 | CH1 | PС7 | 3 | 50 | 1000 | 0 |
Slave | PWM3 | TIM4 | CH2 | PB7 | 2 | 50 | 1000 | 90 |
Первый канал каждого таймера может выдавать наружу (то есть на ведомый таймер) события. Каждый раз, когда счетчик превысит значение первого компаратора сработает событие, которое может кто-нибудь принять. Надо в настройки мастер таймера прописать HAL-овскую константу TIM_TRGO_OC1 (Capture or a compare match 1 is used as trigger output (TRGO)). Slave таймер надо сконфигурировать в режим сброса (MODE_RESET) и указать источник INTERNAL_TRIGGER_3 (который ассоциирован с TIM8).
#include "timer_config.h" const TimerConfig_t TimerConfig[] = { { .num = TIMER_NUM_LO_BASE, .interrupt_on = false, .cnt_period_ns = 1000, .period_s = 0.001f, .name = "LocalOscillatorBase", .valid = true, .on_off = true, .dir = TIMER_CNT_DIR_UP, .role = TIMER_ROLE_MASTER, .master_out_trigger = TIMER_MASTER_OUT_TRG_OC1, }, { .num = TIMER_NUM_LO, .slave_input_trigger = TIMER_SLAVE_IN_TRIG_INTERNAL_TRIGGER_3, .slave_mode = TIMER_SLAVE_MODE_RESET, .role = TIMER_ROLE_SLAVE, .dir = TIMER_CNT_DIR_UP, .interrupt_on = false, .cnt_period_ns = 1000, .period_s = 0.001f, .name = "LocalOscillator", .valid = true, .on_off = true, .slave_trigger_polarity = TIMER_SLAVE_TRIGGER_POLARITY_INVERTED, .slave_trigger_prescaler = 1, .slave_trigger_filter = 1, }, }; TimerHandle_t TimerInstance[] = { { .num = TIMER_NUM_LO_BASE, .valid = true, }, { .num = TIMER_NUM_LO, .valid = true, }, }; COMPONENT_GET_CNT(Timer, timer)
Таким образом после инициализации TIM4 будет сбрасываться при срабатывании компаратора на первом канале таймера 8.
Достоинство этого метода в том, что нет нужды в прерываниях. Всё происходит полностью аппаратно. ARM процессор участвует только в момент записи нового числа в первый компаратор таймера 8, в функции установки фазы.
Диагностика фазы PWM
Вот я собрал прошивку и настроил несколько каналов PWM.

Далее логическим анализатором вижу сигнал, после настройки симфазности

Как видно, погрешность установки нулевой фазы вовсе отсутствует

Можно настроить противофазу

Режим энкодера. Всё, что захотите.

Перед вами сырые значения регистров таймера 4 и таймера 8, при которых работает управление фазы. Этого более чем достаточно, чтобы понять суть применённых настроек.
53:12-->timer_diag_raw_reg 4 3199.137,+31698,256,I,[Timer],Base:0x40000800,Cnt:20 +-----+--------------------+-------+------+------------+------------+-------------------------------------------+ | N | Name |offset | size | Addr | ValHex | ValBin | +-----+--------------------+-------+------+------------+------------+-------------------------------------------+ | 1 | TIMx_CR1 | 0x000 | 0 | 0x40000800 | 0x00000081 | 0b0000_0000|0000_0000|0000_0000|1000_0001 | | 2 | TIMx_CR2 | 0x004 | 4 | 0x40000804 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 3 | TIMx_SMCR | 0x008 | 4 | 0x40000808 | 0x00000034 | 0b0000_0000|0000_0000|0000_0000|0011_0100 | | 4 | TIMx_DIER | 0x00c | 4 | 0x4000080c | 0x00000005 | 0b0000_0000|0000_0000|0000_0000|0000_0101 | | 5 | TIMx_SR | 0x010 | 4 | 0x40000810 | 0x0000005f | 0b0000_0000|0000_0000|0000_0000|0101_1111 | | 6 | TIMx_EGR | 0x014 | 4 | 0x40000814 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 7 | TIMx_CCMR1 | 0x018 | 4 | 0x40000818 | 0x00006800 | 0b0000_0000|0000_0000|0110_1000|0000_0000 | | 8 | TIMx_CCMR2 | 0x01c | 4 | 0x4000081c | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 9 | TIMx_CCER | 0x020 | 4 | 0x40000820 | 0x00000010 | 0b0000_0000|0000_0000|0000_0000|0001_0000 | | 10 | TIMx_CNT | 0x024 | 4 | 0x40000824 | 0x00000139 | 0b0000_0000|0000_0000|0000_0001|0011_1001 | | 11 | TIMx_PSC | 0x028 | 4 | 0x40000828 | 0x00000052 | 0b0000_0000|0000_0000|0000_0000|0101_0010 | | 12 | TIMx_ARR | 0x02c | 4 | 0x4000082c | 0x000003f3 | 0b0000_0000|0000_0000|0000_0011|1111_0011 | | 13 | TIMx_RCR | 0x030 | 4 | 0x40000830 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 14 | TIMx_CCR1 | 0x034 | 4 | 0x40000834 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 15 | TIMx_CCR2 | 0x038 | 4 | 0x40000838 | 0x000001f9 | 0b0000_0000|0000_0000|0000_0001|1111_1001 | | 16 | TIMx_CCR3 | 0x03c | 4 | 0x4000083c | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 17 | TIMx_CCR4 | 0x040 | 4 | 0x40000840 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 18 | TIMx_BDTR | 0x044 | 4 | 0x40000844 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 19 | TIMx_DCR | 0x048 | 4 | 0x40000848 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 20 | TIMx_DMAR | 0x04c | 4 | 0x4000084c | 0x00000081 | 0b0000_0000|0000_0000|0000_0000|1000_0001 | +-----+--------------------+-------+------+------------+------------+-------------------------------------------+ 53:19-->timer_diag_raw_reg 8 3204.547,+5410,257,I,[Timer],Base:0x40010400,Cnt:20 +-----+--------------------+-------+------+------------+------------+-------------------------------------------+ | N | Name |offset | size | Addr | ValHex | ValBin | +-----+--------------------+-------+------+------------+------------+-------------------------------------------+ | 1 | TIMx_CR1 | 0x000 | 0 | 0x40010400 | 0x00000081 | 0b0000_0000|0000_0000|0000_0000|1000_0001 | | 2 | TIMx_CR2 | 0x004 | 4 | 0x40010404 | 0x00000030 | 0b0000_0000|0000_0000|0000_0000|0011_0000 | | 3 | TIMx_SMCR | 0x008 | 4 | 0x40010408 | 0x00000080 | 0b0000_0000|0000_0000|0000_0000|1000_0000 | | 4 | TIMx_DIER | 0x00c | 4 | 0x4001040c | 0x00000005 | 0b0000_0000|0000_0000|0000_0000|0000_0101 | | 5 | TIMx_SR | 0x010 | 4 | 0x40010410 | 0x0000001f | 0b0000_0000|0000_0000|0000_0000|0001_1111 | | 6 | TIMx_EGR | 0x014 | 4 | 0x40010414 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 7 | TIMx_CCMR1 | 0x018 | 4 | 0x40010418 | 0x00006800 | 0b0000_0000|0000_0000|0110_1000|0000_0000 | | 8 | TIMx_CCMR2 | 0x01c | 4 | 0x4001041c | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 9 | TIMx_CCER | 0x020 | 4 | 0x40010420 | 0x00000010 | 0b0000_0000|0000_0000|0000_0000|0001_0000 | | 10 | TIMx_CNT | 0x024 | 4 | 0x40010424 | 0x00000265 | 0b0000_0000|0000_0000|0000_0010|0110_0101 | | 11 | TIMx_PSC | 0x028 | 4 | 0x40010428 | 0x000000a6 | 0b0000_0000|0000_0000|0000_0000|1010_0110 | | 12 | TIMx_ARR | 0x02c | 4 | 0x4001042c | 0x000003ec | 0b0000_0000|0000_0000|0000_0011|1110_1100 | | 13 | TIMx_RCR | 0x030 | 4 | 0x40010430 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 14 | TIMx_CCR1 | 0x034 | 4 | 0x40010434 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 15 | TIMx_CCR2 | 0x038 | 4 | 0x40010438 | 0x000001f6 | 0b0000_0000|0000_0000|0000_0001|1111_0110 | | 16 | TIMx_CCR3 | 0x03c | 4 | 0x4001043c | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 17 | TIMx_CCR4 | 0x040 | 4 | 0x40010440 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 18 | TIMx_BDTR | 0x044 | 4 | 0x40010444 | 0x00008000 | 0b0000_0000|0000_0000|1000_0000|0000_0000 | | 19 | TIMx_DCR | 0x048 | 4 | 0x40010448 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 20 | TIMx_DMAR | 0x04c | 4 | 0x4001044c | 0x00000081 | 0b0000_0000|0000_0000|0000_0000|1000_0001 | +-----+--------------------+-------+------+------------+------------+-------------------------------------------+ 53:24-->
Сравнение способов установки фазы PWM сигнала
Способ управления фазой PWM | Достоинство | Недостаток |
программный PWM | гибкость настройки | процессор участвует |
Инверсия полярности | процессор не участвует | нет непрерывной настройки |
подмена счетчика | есть погрешность установок | процессор участвует |
master-slave | +нет прерываний | -надо два таймера |
Результат
Непрерывное регулирование фазы PWM с минимальной возможной погрешностью более чем возможная задача. Готовый бинарь demo прошивки JZ-F407VET6 можно скачать тут.
Или можете собрать PWM-phase-demo из исходников
Учебные тестировочные сборки для PCB JZ-F407VET6 в репозитории trunk
https://github.com/aabzel/trunk/tree/main
Вот конкретная сборка PWM-phase-demo проекта
https://github.com/aabzel/trunk/tree/main/source/projects/jz_f407vet6_pwm_phase_demo_gcc_m
Все конфигурации для платы JZ-F407VET6 лежат тут
https://github.com/aabzel/trunk/tree/main/source/boards/jz_f407vet6
Все конфигурации для микроконтроллера stm32f407ve тут
https://github.com/aabzel/trunk/tree/main/source/microcontroller/stm32f407ve
Логика шатания фазы описана отдельным модулем
https://github.com/aabzel/trunk/tree/main/source/applications/pwm_phase_demo
Драйвер PWM лежит тут
https://github.com/aabzel/trunk/tree/main/source/mcal/mcal_common/pwm
https://github.com/aabzel/trunk/tree/main/source/mcal/mcal_stm32f4/pwm
Драйвер Timer лежит в source/mcal
https://github.com/aabzel/trunk/tree/main/source/mcal/mcal_common/timer
https://github.com/aabzel/trunk/tree/main/source/mcal/mcal_stm32f4/timer
Остальное можно найти утилитой grep по репозиторию.
Словарь
Сокращение | Расшифровка |
PWM | pulse-width modulation |
BPSK | Binary Phase Shift Keying |
MCAL | Microcontroller Abstraction Layer |
LO | Local Oscillator |
STM32 | STM 32 bit |
Ссылки
Название | URL |
Demo прошивка регулирования фазы | https://github.com/aabzel/Artifacts/tree/main/jz_f407vet6_pwm_phase_demo_gcc_m |
How to Generate 3-Phase PWM Using Synchronized STM32 Timers | https://controllerstech.com/stm32-timers-6-timer-synchronization-generate-3-phase-pwm/ |
Обзор учебно-тренировочной платы JZ-F407VET6 (или электронная парта) | |
Каскадный Таймер на STM32 (или Таймер с Прицепом) @danil_12345 | |
Аналитика по таймерам STM32 | |
STM32 PWM Phase Shift (Timer Synchronized) + Example Code | https://deepbluembedded.com/stm32-pwm-phase-shift-timer-synchronized-example-code/ |
Вопросы
1--Как в прошивке микроконтроллера прочитать мгновенное значение на выходе аппаратного PWM ведь но согласно PinMux не подключен к GPIO ?
2--Можно ли на STM32 в случае схемы Master-Slave сделать регулирование фазы на Slave PWM сигнале без использования прерываний микроконтроллера и остановки счета, чтобы не затормаживать основную прошивку? Да. Надо использовать схему Master-slave. Генерировать по первому компаратору мастером события сброса для slave таймера.
