Настраиваем и генерируем ШИМ.
Генерируем ШИМ на микроконтроллере STM32F100 для полумостового преобразователя напряжения.

Широтно-импульсная модуляция (ШИМ, англ. pulse-width modulation (PWM)) — процесс управления мощностью, подводимой к нагрузке, путём изменения скважности импульсов, при постоянной частоте
В микроконтроллерах stm ШИМ можно генерировать несколькими способами. Один из них программный - подача нуля и единицы на логический вывод в нужное время, и аппаратный - используя один из таймеров контроллера.
В данной заметке рассмотрим второй способ.
Здесь как и везде для инициализации периферии я предпочитаю использовать STM32 Standard Peripheral Libraries, почему я так делаю - скорость при инициализации не нужна, а код получается максимально информативным (но это мои заморочки, можете делать по другому).
Для генерации будем использовать таймер 1 (TIM1
)
А теперь по пунктам, что нам нужно сделать.
- инициализировать все необходимые компоненты микроконтроллера:
- тактовый генератор
- ножки ввода вывода
- таймер
- запустить таймер
Установка настроек тактирования ядра и периферии микроконтроллера производится с помощью функций драйвера RCC (файлы stm32f10x_rcc.c
и stm32f10x_rcc.h
)
void RCC_Configuration(void)
{
// Внешний генератор HSE (8 МГц), частота системной шины 4 МГц
// Enable Prefetch Buffer
FLASH_PrefetchBufferCmd( FLASH_PrefetchBuffer_Enable );
// Цикл ожидания = 0
FLASH_SetLatency( FLASH_Latency_0 );
// Сброс настроек RCC
RCC_DeInit();
// Выключаем HSE и включаем HSI
RCC_HSEConfig( RCC_HSE_ON );
RCC_HSICmd( DISABLE );
RCC_PLLCmd( DISABLE );
// Конфигурация ADC = HSE / 2 = 4 MHz
RCC_HCLKConfig ( RCC_SYSCLK_Div1 );
RCC_PCLK2Config ( RCC_HCLK_Div1 );
RCC_SYSCLKConfig( RCC_SYSCLKSource_HSE );
}
Так как мы используем внешний кварц 8 МГц без PLL
необходимо настроить работу тактового генератора от него и выключить PLL
ку, за что отвечают строчки.
RCC_HSEConfig( RCC_HSE_ON );
RCC_HSICmd( DISABLE );
RCC_PLLCmd( DISABLE );
лучше понять систему тактирования позволит скриншот из утилиты STM32CubeMX:
Теперь необходимо разобраться какие нам нужны выводы для нашего ШИМ. И здесь весьма полезной окажется также самая программа.
Я выбрал два канала ШИМ - так как собирался использовать его в качестве управляющего сигнала для преобразователя, что то вроде этого (только здесь мостовая схема):
Схему объяснять не буду, так как она совершенно не связана с данной программой - это другое устройство, сходство лишь в двух каналах ШИМ.
Инициализируем наши ножки (все функции и структуры находятся в файле stm32f10x_gpio.h
) :
void GPIO_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
//--------- Включаем клоки ---------------------------------
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA // Порт А
|RCC_APB2Periph_AFIO, // Альтернативные функции
ENABLE);
//--------- Настройка всех выводов на вход -------
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_All;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;
// Порта А
GPIO_Init(GPIOA, &GPIO_InitStruct);
//--------- Настройка выводов для ШИМ ----------------------
// PA8 --> TIM1_CH1
// PA9 --> TIM1_CH2
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStruct);
}
Сначала, для уменьшения энергопотребления настраиваем все выводы на вход, а затем необходимые выводы настраиваем на альтернативную функцию.
Теперь самое интересное - настройка таймера (stm32f10x_tim.h
):
void TIM_Configuration(void)
{ // частота 8 МГц
// Структуры
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
// Разрешить тактирование TIM1
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
// ================== Таймер TIM1 =========================================
// Заполняем структуру
TIM_TimeBaseStructure.TIM_Period = 799;
TIM_TimeBaseStructure.TIM_Prescaler = 0;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_CenterAligned1;
// Делаем базовую настройку таймерам
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
// Заполняем конфигурацию каналов таймера
// 1 --> PA8
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 60;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;
TIM_OC1Init(TIM1, &TIM_OCInitStructure);
TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable);
// 2 --> PA9
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 50;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;
TIM_OC2Init(TIM1, &TIM_OCInitStructure);
TIM_OC2PreloadConfig(TIM1, TIM_OCPreload_Enable);
TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(TIM1, ENABLE);
// Разрешаем ШИМ от таймера TIM1
// Эта штука обязательна для этого таймера
TIM_CtrlPWMOutputs(TIM1,ENABLE);
TIM_Cmd(TIM1, DISABLE);
}
Что бы понять что здесь происходит необходимо приложить небольшие усилия. Сначала инициализируется счетчик таймера, а затем два канала сравнения.
Рассмотрим структуру TIM_TimeBaseStructure
:
TIM_TimeBaseStructure.TIM_Period // устанавливает период счета таймера - значение до которого идем счёт
TIM_TimeBaseStructure.TIM_Prescaler // предделитель счетчика, устанавливает то на сколько будет делиться тактовая частота
TIM_TimeBaseStructure.TIM_ClockDivision // делитель счетчика может быть 1, 2, 4
TIM_TimeBaseStructure.TIM_CounterMode // режим счета: вверх, вниз и др.
Как выбираются данные значение:
Допустим, мне необходимо получить частоту следования импульсов равную 100 кГц, то есть мне нужно тактовую частоту разделить на какое то число и получить необходимую частоту.
В данном случае частота 8 000 000 Гц, а необходимо 100 000 Гц, то есть наш делитель будет
8 000 000 / 100 000 = 800
Значит в период необходимо записать значение 800 - 1 (от 0 до 799 как раз 800 отсчётов).
TIM_Prescaler
и TIM_ClockDivision
устанавливаем в значение 1, так как больше делить нашу частоту нет необходимости.
Из интересного - это TIM_CounterMode
, - указывает как необходимо считать такты, для понимания необходимо заглянуть в даташит.
В моем случае используется TIM_CounterMode_CenterAligned1
, в данном случае это удобно - так как собирают использовать deadtime, так называемое мертвое время между включениями транзисторов, для исключения варианта когда один транзистор еще не закрылся, а второй уже открывается, так я ещё хочу и иметь возможность менять это время, но это тема другой дискуссии, здесь мы лишь настраиваем ШИМ. При чем частота при TIM_CounterMode_CenterAligned1
будет уже не 100, а 50 кГц.
Теперь стоит пояснить про поля структуры TIM_OCInitStructure
:
TIM_OCInitStructure.TIM_OCMode // настраивает способ переключения выхода при сравнении
TIM_OCInitStructure.TIM_OutputState // указывает нужно ли выводить сигнал ШИМ
TIM_OCInitStructure.TIM_Pulse // заполнение
TIM_OCInitStructure.TIM_OCPolarity // полярность
Вот так будет выглядеть ШИМ (извиняюсь, что не в масштабе), тут сразу видно как будет переключаться вывод при настройке TIM_OCMode
в значение TIM_OCMode_PWM1
или в значение TIM_OCMode_PWM2
.
В итоге у нас должен будет получиться ШИМ с заполнением:
- 100% * (800 - 60) / 800 = 90 %
- 100% * 50 / 800 = 6,25 %
Разница вычислений объясняется картинкой выше.
Строчка TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable);
указывает, что установка новых значений в регистр сравнения будет происходить только в момент обновления счетчика, что весьма важно при генерации сигнала для управления преобразователем.
Теперь в основной программе осталось вызвать по очереди все эти функции и запустить таймер командой:
TIM_Cmd(TIM1, ENABLE);
А для изменения процента заполнения можно использовать прямую запись в регистр сравнения, что значительно быстрее.
TIM1->CCR1 = 500; // Канал 1
TIM1->CCR2 = 100; // Канал 2