Администрация форума не несёт ответственности за достоверность информации и оставляет за собой право редактировать или в особых случаях даже удалять посты без предупреждения. Спасибо за понимание.

Программирование ATMEL в BASCOM.

Информация о пользователе

Привет, Гость! Войдите или зарегистрируйтесь.



STM32F030F4P6 - ШИМ

Сообщений 1 страница 7 из 7

1

Как и в МК других производителей в STM32 ШИМ генерируется таймерами, которых в STM32F030F4P6 содержится 5 штук не считая системного таймера, часов и сторожевых таймеров. TIM1 и TIM3 относятся к таймерам общего назначения и они многофункциональные - содержат по 4 канала захвата/сравнения и имеют множество других функций. Таймеры TIM14, TIM16 и TIM17 базовые и они попроще и имеют только 1 канал захвата/сравнения. Более подробно о таймерах и их возможностях можно узнать например из статей о таймерах общего назначения и базовых.

Цитаты из статей о возможностях таймеров

Базовые таймеры.

16-битный счётчик с автоперезагрузкой.
16-битный программируемый делитель частоты: с 1 по 65535.
Схема синхронизации для запуска АЦП/ЦАП.
Захват сигнала (input capture).
Сравнение вывода (output compare).
Генерация сигнала ШИМ (выровненного по границе или по центру).
Генерация одиночных импульсов.
Генерация прерывания и/или запроса DMA по переполнению счётчика.

Таймеры общего назначения.

16-битный счётчик с автоперезагрузкой.
16-битный программируемый делитель частоты: с 1 по 65535.
Схема синхронизации для запуска АЦП/ЦАП.
Захват сигнала (input capture).
Сравнение вывода (output compare).
Генерация сигнала ШИМ (выровненного по границе или по центру).
Генерация одиночных импульсов.
Генерация прерывания и/или запроса DMA по переполнению счётчика.
Схемы синхронизации для управления таймерами при помощи внешних сигналов и для соединения нескольких таймеров друг с другом.
Комплементарные выходы с программируемым dead-time.
Счётчик повторений.
Вход BRK для сброса выходов таймера или выставления их в известное состояние.
Поддерживают инкрементальные (квадратурные) энкодеры и датчики Холла.
Генерация прерывания или запроса DMA по следующим событиям:
* Обновление: переполнение счётчика.
* Событие-триггер: старт, остановка, инициализация счётчика или его обновление внутренним или внешним триггером.
* Захват сигнала.
* Сравнение (output compare).
* Включение BRK.

Вернемся к рассматриваемой теме - генерации ШИМ.
Схема тестового устройства.

http://s017.radikal.ru/i434/1607/b4/ef0e8ae536ce.gif

С выводом PB1 к которому подключен светодиод, соединены каналы TIM1, TIM3 и TIM14.

http://s020.radikal.ru/i704/1607/88/618e70aea161.png

Для генерации ШИМ будем использовать таймер TIM14 как наиболее простой из перечисленных.

Тестовый код.

Код:
#include "stm32f0xx_conf.h"

void GPIO_Conf(void) // Конфигурация портов ввода / вывода.
{
    GPIO_InitTypeDef s;

    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB, ENABLE); // Включаем тактирование порта PB.

    s.GPIO_Pin   = GPIO_Pin_1;       // Настройка вывода PB1.
    s.GPIO_Mode  = GPIO_Mode_AF;     // Вывод настроен на одну из альтернативных функций.
    s.GPIO_OType = GPIO_OType_PP;    // Двухтактный выход (т. е. не открытый сток).
    s.GPIO_PuPd  = GPIO_PuPd_NOPULL; // Подтягивающие резисторы отключены.
    s.GPIO_Speed = GPIO_Speed_50MHz;

    GPIO_Init(GPIOB, &s);            // Инициализация порта.

    GPIO_PinAFConfig(GPIOB, GPIO_PinSource1, GPIO_AF_0); // PB1 это выход таймера TIM 14.
}


void Tim14_Conf(void) // Конфигурация таймера.
{
    TIM_TimeBaseInitTypeDef Tim;
    TIM_OCInitTypeDef Pwm;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM14, ENABLE); // Включаем тактирование таймера 14.

    Tim.TIM_ClockDivision = TIM_CKD_DIV1; // Выключаем предварительный делитель частоты таймера.
    Tim.TIM_CounterMode   = TIM_CounterMode_Up; // Значение в счетном регистре увеличивается.
    Tim.TIM_Period        = 65535; // Значение до которого досчитав таймер обнулится (максимум 65535).
    Tim.TIM_Prescaler     = 0; // На сколько нужно делить частоту (максимум 65535).
    Tim.TIM_RepetitionCounter = 0;

    TIM_TimeBaseInit(TIM14, &Tim); // Инициализация таймера.

    // Настройка ШИМ.
    TIM_OCStructInit(&Pwm);  // Загрузка в структуру настроек таймера по умолчанию.

    Pwm.TIM_OCMode = TIM_OCMode_PWM1; // Режим ШИМ 1.
    Pwm.TIM_OutputState = TIM_OutputState_Enable;
    Pwm.TIM_Pulse = 1000; //32768;
    TIM_OC1Init(TIM14, &Pwm);

    TIM_Cmd(TIM14, ENABLE); // Запуск таймера.
}


int main(void)
{
    GPIO_Conf();   // Конфигурация портов ввода / вывода.
    Tim14_Conf();  // Конфигурация таймера.

    while(1)
    {

    }
}

Программа довольно простая. В функции main() вызываются функции настройки порта и таймера, после чего МК зацикливается до тех пор пока питание не выключат.
В функции GPIO_Conf() производится настройка вывода PB1 к которому подключен светодиод. Сперва включается тактирование порта GPIOB, которое по умолчанию выключено. Затем вывод порта настраивается на использование альтернативной функции. Какой именно зависит от этой строки.

Код:
GPIO_PinAFConfig(GPIOB, GPIO_PinSource1, GPIO_AF_0); // PB1 это выход таймера TIM 14.

Может показаться непонятно как здесь указывается что вывод PB1 должен быть соединен именно с выходом TIM14. Ответ на это можно найти в даташите на МК STM32F030F4P6 в котором есть таблица альтернативных функций для портов.
Для порта GPIOB.

http://s020.radikal.ru/i708/1607/15/1de756d10e0a.png

Из таблицы следует что нулевая альтернативная функция PB1 это выход таймера TIM14. А если бы нам потребовалось генерировать ШИМ третьим каналом таймера TIM1 и выводить его на PB1, то нужно было бы настроить вывод порта на вторую альтернативную функцию.

Функция Tim14_Conf() настраивает таймер TIM14. В ней в первую очередь включается тактирование таймера, а затем настраиваются основные параметры таймера, такие как предварительное деление частоты, значение после которого таймер обнулится и т. д.
Затем производится настройка первого канала таймера, так чтобы он генерировал ШИМ. Чтобы не заполнять все поля структуры, вызывается функция TIM_OCStructInit(), которая записывает в структуру настройки по умолчанию. После чего нужно изменить лишь те поля которые отличаются от тех что по умолчанию. Необходимо выбрать решим ШИМ, разрешить работу выхода канала и задать длительность импульса. Последнее необязательно, ведь длительность можно изменить позже.
После всех настроек разрешается работа таймера функцией TIM_Cmd().

Если прошить эту программу в МК, то при подаче питания светодиод будет слабо светится. Почему слабо? Потому что длительность импульса равна примерно 1/65 длительности периода ШИМ (в поле TIM_Period значение 65535, а в поле TIM_Pulse значение 1000). Т. к. частота поступающая на счетчик таймера равна 48 МГц и предделители отключены, а таймер обнуляется каждые 65536 импульсов, то частота ШИМ составит 48000000 / 65536 = 732.42 Гц.
Проверим это логическим анализатором.

http://s019.radikal.ru/i643/1607/6f/a7ca6b3c7082.png

Длительность импульса 20 мкс, период ШИМ 1.36 мс и частота 732.22 Гц. Результат близок к расчетному. Небольшое отличие видимо из-за того что частота кварцевого резонатора не ровно 8 МГц, а немного меньше.

Но не всегда нужен 16-ти битный ШИМ. Обычно нужна частота побольше. Уменьшим значение поля TIM_Period, скажем до 4000.

http://s020.radikal.ru/i703/1607/71/4fba39928af7.png

Длительность импульса осталась прежней, а период и частота изменились. Светодиод начал светить намного ярче.

Теперь добавим в цикл код, изменяющий длительность импульса во время работы программы. Функция main() будет выглядеть таким образом.

Код:
int main(void)
{
    uint16_t x=0;
    volatile uint16_t i;

    GPIO_Conf();   // Конфигурация портов ввода / вывода.
    Tim14_Conf();  // Конфигурация таймера.

    while(1)
    {
        TIM_SetCompare1(TIM14, x); // Изменяем длительность импульса.
        x=x+100;

        for (i=0; i<30000; i++); // Небольшая задержка.
    }
}

Запустив этот код, можно увидеть как светодиод плавно увеличивает яркость до максимума, а потом гаснет и все начинается сначала.

Файлы http://pure-basic.narod.ru/forum_files/ … 14_PWM.zip

+3

2

Вопрос к Петру.
Сейчас работаю в такой сфере, где в основном STM и шим, шим и еще раз шим.
Параметры шим в основном такие: 50 Гц, длительность импульса от 1 до 2 мс.
Как многие догадаются - это в основном сервоприводы.
На AVR это реализуется даже без написания кода. Просто берём 16-ти разрядный таймер в режиме коррекции фазы и выставляем его регистры в нужные значения.
А как это делается в STM? Только без HAL :D

Отредактировано sva-don (2025-04-23 21:32:56)

0

3

sva-don написал(а):

А как это делается в STM? Только без HAL

Вот как-то как в первом сообщении. HAL там нет. Это библиотека SPL.
На уровень регистров я особо не лез - нет смысла, т. к. конфигурация не требует много места в прошивке, но при этом не нужно от и до изучать даташит выясняя особенность каждой модели МК. Достаточно прочитать по диагонали чтобы понимать что есть в МК и что с ним можно сделать.
Код конфигурации таймеров из одного из проектов с 24 сервами.

Код:
static void init_servo(void) // Настройка сервоприводов.
{
    RCC_AHB1PeriphClockCmd(Servo_hw.RCC_AHB1Periph, ENABLE);
    RCC_APB1PeriphClockCmd(Servo_hw.RCC_APB1Periph, ENABLE);
    RCC_APB2PeriphClockCmd(Servo_hw.RCC_APB2Periph, ENABLE);

    // Настройка GPIO.
    {
        GPIO_InitTypeDef gpio;


        gpio.GPIO_Mode = GPIO_Mode_AF;     // Вывод работает как альтернативная функция.
        gpio.GPIO_OType = GPIO_OType_PP;   // Двухтактный выход.
        gpio.GPIO_PuPd = GPIO_PuPd_NOPULL; // Подтягивающие резисторы отключены.
        gpio.GPIO_Speed = GPIO_Speed_2MHz;

        gpio.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_15;
        GPIO_Init(GPIOA, &gpio);

        gpio.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_3 | GPIO_Pin_10 | GPIO_Pin_11 | GPIO_Pin_15;
        GPIO_Init(GPIOB, &gpio);

        gpio.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
        GPIO_Init(GPIOC, &gpio);

        gpio.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
        GPIO_Init(GPIOD, &gpio);

        gpio.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_11 | GPIO_Pin_13 | GPIO_Pin_14;
        GPIO_Init(GPIOE, &gpio);

        gpio.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7 | GPIO_Pin_8;
        GPIO_Init(GPIOF, &gpio);
    }

    // Настройка альтернативных функций портов.
    GPIO_PinAFConfig(GPIOE, GPIO_PinSource9,  GPIO_AF_TIM1);
    GPIO_PinAFConfig(GPIOE, GPIO_PinSource11, GPIO_AF_TIM1);
    GPIO_PinAFConfig(GPIOE, GPIO_PinSource13, GPIO_AF_TIM1);
    GPIO_PinAFConfig(GPIOE, GPIO_PinSource14, GPIO_AF_TIM1);

    GPIO_PinAFConfig(GPIOA, GPIO_PinSource15, GPIO_AF_TIM2);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource3,  GPIO_AF_TIM2);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource10, GPIO_AF_TIM2);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource11, GPIO_AF_TIM2);

    GPIO_PinAFConfig(GPIOC, GPIO_PinSource6, GPIO_AF_TIM3);
    GPIO_PinAFConfig(GPIOC, GPIO_PinSource7, GPIO_AF_TIM3);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource0, GPIO_AF_TIM3);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource1, GPIO_AF_TIM3);

    GPIO_PinAFConfig(GPIOD, GPIO_PinSource12, GPIO_AF_TIM4);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource13, GPIO_AF_TIM4);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource14, GPIO_AF_TIM4);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource15, GPIO_AF_TIM4);

    GPIO_PinAFConfig(GPIOA, GPIO_PinSource0, GPIO_AF_TIM5);
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource1, GPIO_AF_TIM5);
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource2, GPIO_AF_TIM5);
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource3, GPIO_AF_TIM5);

    GPIO_PinAFConfig(GPIOF, GPIO_PinSource6, GPIO_AF_TIM10);
    GPIO_PinAFConfig(GPIOF, GPIO_PinSource7, GPIO_AF_TIM11);
    GPIO_PinAFConfig(GPIOF, GPIO_PinSource8, GPIO_AF_TIM13);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource15, GPIO_AF_TIM12);

    // Настройка таймеров.
    {
       TIM_TimeBaseInitTypeDef Tim;
       TIM_TimeBaseStructInit(&Tim);

       Tim.TIM_Period = 1000000 / 50; // На выходах частота 50 Гц.

       //  Частота APB2 для таймеров равна частоте ядра.
       Tim.TIM_Prescaler = (SystemCoreClock / 1000000) - 1; // После предделителя частота 1 МГц.
       TIM_TimeBaseInit(TIM1, &Tim);
       TIM_TimeBaseInit(TIM10, &Tim);
       TIM_TimeBaseInit(TIM11, &Tim);

       //  Частота APB1 для таймеров равна частоте ядра поделенной на 2.
       Tim.TIM_Prescaler = (SystemCoreClock / 2 / 1000000) - 1; // После предделителя частота 1 МГц.
       TIM_TimeBaseInit(TIM2, &Tim);
       TIM_TimeBaseInit(TIM3, &Tim);
       TIM_TimeBaseInit(TIM4, &Tim);
       TIM_TimeBaseInit(TIM5, &Tim);
       TIM_TimeBaseInit(TIM12, &Tim);
       TIM_TimeBaseInit(TIM13, &Tim);
    }

    // Настройка каналов таймеров.
    {
       TIM_OCInitTypeDef Ch;
       TIM_OCStructInit(&Ch);

       Ch.TIM_OCMode = TIM_OCMode_PWM1;
       Ch.TIM_OutputState = TIM_OutputState_Enable;
       Ch.TIM_OCPolarity = TIM_OCPolarity_High;

       TIM_OC1Init(TIM1, &Ch);
       TIM_OC2Init(TIM1, &Ch);
       TIM_OC3Init(TIM1, &Ch);
       TIM_OC4Init(TIM1, &Ch);

       TIM_OC1Init(TIM2, &Ch);
       TIM_OC2Init(TIM2, &Ch);
       TIM_OC3Init(TIM2, &Ch);
       TIM_OC4Init(TIM2, &Ch);

       TIM_OC1Init(TIM3, &Ch);
       TIM_OC2Init(TIM3, &Ch);
       TIM_OC3Init(TIM3, &Ch);
       TIM_OC4Init(TIM3, &Ch);

       TIM_OC1Init(TIM4, &Ch);
       TIM_OC2Init(TIM4, &Ch);
       TIM_OC3Init(TIM4, &Ch);
       TIM_OC4Init(TIM4, &Ch);

       TIM_OC1Init(TIM5, &Ch);
       TIM_OC2Init(TIM5, &Ch);
       TIM_OC3Init(TIM5, &Ch);
       TIM_OC4Init(TIM5, &Ch);

       TIM_OC1Init(TIM10, &Ch);
       TIM_OC1Init(TIM11, &Ch);
       TIM_OC1Init(TIM13, &Ch);
       TIM_OC2Init(TIM12, &Ch);
    }

    TIM_Cmd(TIM1, ENABLE); // Запуск таймеров.
    TIM_Cmd(TIM2, ENABLE);
    TIM_Cmd(TIM3, ENABLE);
    TIM_Cmd(TIM4, ENABLE);
    TIM_Cmd(TIM5, ENABLE);
    TIM_Cmd(TIM10, ENABLE);
    TIM_Cmd(TIM11, ENABLE);
    TIM_Cmd(TIM12, ENABLE);
    TIM_Cmd(TIM13, ENABLE);
}

Управление сервоприводами.

Код:
// Задание угла поворота сервопривода.
static void set_servo_angle(uint8_t Num, uint16_t Angle) // Угол в 0.1 градусов.
{
    if ((Num>=0 && Num<COUNT_Servo) && (Angle>=0 && Angle<=1800))
    {
        // Диапазон времени импульса.
        uint16_t Range = Servo_hw.Servo[Num].MaxPulse - Servo_hw.Servo[Num].MinPulse;
         // Вычисление длительности импульса.
        uint16_t x = Servo_hw.Servo[Num].MinPulse;
        if (Angle>0)
        {
            x += (uint16_t)((float)Range/(1800.0f / (float)Angle));
        }

        switch (Servo_hw.Servo[Num].Channel)
        {
        case 1:
            TIM_SetCompare1(Servo_hw.Servo[Num].Tim, x);
            break;
        case 2:
            TIM_SetCompare2(Servo_hw.Servo[Num].Tim, x);
            break;
        case 3:
            TIM_SetCompare3(Servo_hw.Servo[Num].Tim, x);
            break;
        case 4:
            TIM_SetCompare4(Servo_hw.Servo[Num].Tim, x);
            break;
        }
    }
}

+1

4

Отлично, только вот мои коллеги и я в том числе пишем на WIRING. Можно как ото прокомментировать перенос этого  (в принципе не сложного) кода на него?
Понимаю всю наглость моего поста. :blush:

0

5

Я подумал. Попробую сам всё переписать. Спасибо Пётр.

0

6

sva-don написал(а):

Отлично, только вот мои коллеги и я в том числе пишем на WIRING.

В ардуине?

sva-don написал(а):

Можно как ото прокомментировать перенос этого (в принципе не сложного) кода на него?

Сначала нужно прикрутить SPL к ардуине и заставить нормально работать.
Вот честно не понимаю, зачем использовать ардуину? Больше недостатков чем преимуществ. Насколько мне известно ST-Link не поддерживается ардуиной, а это очень большой недостаток!

0

7

Ок, спасибо. Я попробую.

0