Настраиваем и генерируем ШИМ.

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

Cover Image

Широтно-импульсная модуляция (ШИМ, англ. 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 = 80

Значит в период необходимо записать значение 80 - 1 (от 0 до 79 как раз 80 отсчётов).

TIM_Prescaler и TIM_ClockDivision устанавливаем в значение 1, так как больше делить нашу частоту нет необходимости.

Из интересного - это TIM_CounterMode, - указывает как необходимо считать такты, для понимания необходимо заглянуть в даташит.

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 = 50; // Канал 1
TIM1->CCR2 = 10; // Канал 2