STM32 с нуля. Таймеры.

Таймеры в STM32, как в принципе и вся периферия, являются очень навороченными. От обилия разных функций, которые могут выполнять таймеры может даже закружиться голова. Хотя, казалось бы, таймер он на то и таймер, чтобы просто считать. Но на деле все гораздо круче )

Мало того, что таймеры обладают такими широкими возможностями, так их еще несколько у каждого контроллера. И даже не два и не три, а больше! В общем, нахваливать все это можно бесконечно. Давайте уже разбираться, что и как работает. Итак, микроконтроллер STM32F103CB имеет:

  • 3 таймера общего назначения (TIM2, TIM3, TIM4)
  • 1 более продвинутый таймер с расширенными возможностями (TIM1)
  • 2 WDT (WatchDog Timer)
  • 1 SysTick Timer

Собственно таймеры общего назначения и таймер TIM1 не сильно отличаются друг от друга, так что ограничимся рассмотрением какого-нибудь одного таймера. К слову я остановил свой выбор на TIM4. Без особой причины, просто так захотелось =). Таймеры имеют 4 независимых канала, которые могут использоваться для:

  • Захвата сигнала
  • Сравнения
  • Генерации ШИМ
  • Генерации одиночного импульса

Таймеры 16 битные (то есть могут считать до 65535), умеют работать с инкрементальными энкодерами и датчиками Холла, несколько таймеров можно синхронизировать между собой. Есть прерывания на разные события, а именно:

  • Переполнение
  • Захват сигнала
  • Сравнение
  • Событие-триггер

При наступлении любого из этих событий таймеры могут генерировать запрос к DMA (DMA – прямой доступ к памяти, уже скоро мы будем разбираться и с ним =) ). Теперь немного подробнее о каждом из режимов работы таймеров.

Режим захвата сигнала. Очень удобно при работе таймера в этом режиме измерять период следования импульсов. Смотрите сами: приходит импульс, таймер кладет свое текущее значение счетчика в регистр TIM_CCR. По-быстрому забираем это значение и прячем в какую-нибудь переменную. Сидим, ждем следующий импульс. Опа! Импульс пришел, таймер снова сует значение счетчика в TIM_CCR, и нам остается только вычесть из этого значения то, которое мы предварительно сохранили. Это, наверное, самое простое использование этого режима таймера, но очень полезное. Отлавливать можно как передний фронт импульса, так и задний, так что возможности довольно велики.

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

Режим генерации ШИМ. Ну тут все скрыто в названии ) В этом режиме таймер генерирует ШИМ! Наверно нет смысла что-то писать тут еще сейчас. Скоро будет примерчик как раз на ШИМ, там и поковыряем поподробнее.

Режим Dead-Time. Суть режима в том, что между сигналами на основном и комплементарном  выводах таймера появляется определенная задержка. В интернете есть  довольно много информации о том, где это можно и нужно применять.

Ну вот в принципе ооочень кратко об основных режимах работы таймера. Если будут вопросы про другие режимы, более специфические, пишите в Комментарии 😉 Вот кстати ссылочка на документацию — там все подробно описано Ссылочка

Надо бы потихоньку написать программку для работы с таймерами. Но сначала посмотрим, что есть в библиотеке Standard Peripheral Library. Итак, за таймеры несут ответственность файлы — stm32f10x_tim.h и stm32f10x_tim.c. Открываем первый и видим, что структура файла повторяет структуру файла для работы с GPIO, который мы рассматривали в предыдущей статье. Здесь описаны структуры и поля структур, которые нужны для конфигурирования таймеров. Правда здесь уже не одна, а несколько структур (режимов, а соответственно и настроек то у таймеров побольше, чем у портов ввода-вывода). Все поля структур снабжены комментариями, так что не должно тут возникать никаких проблем. Ну вот, например:

uint16_t TIM_OCMode;        // Specifies the TIM mode.

Здесь будем задавать режим работы таймера. А вот еще:

uint16_t TIM_Channel;      // Specifies the TIM channel.

Здесь выбираем канал таймера, ничего неожиданного ) В общем все довольно прозрачно, если что спрашивайте =) С первым файлом понятно. А в файле stm32f10x_tim.c – готовые функции для работы с таймерами. Тоже все в целом ясно. Мы уже использовали библиотеку для работы с GPIO, теперь вот работаем с таймерами, и очевидно, что для разной периферии все очень  похоже. Так что давайте создавать проект и писать программу.

Итак, запиливаем новый проект, добавляем все необходимые файлы:
Создание проекта для работы с таймерами
Пишем код:

/****************************timers.c*******************************/
#include "stm32f10x.h" 
#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_tim.h"
 
/*******************************************************************/
//При таком предделителе у меня получается один тик таймера на 10 мкс
#define TIMER_PRESCALER	720
 
/*******************************************************************/
//Переменная для хранения предыдущего состояния вывода PB0
uint16_t previousState;
GPIO_InitTypeDef port;
TIM_TimeBaseInitTypeDef timer;
 
/*******************************************************************/
void initAll()
{
    //Включаем тактирование порта GPIOB и таймера TIM4
    //Таймер 4 у нас висит на шине APB1
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
    //Тут настраиваем порт PB0 на выход
    //Подробнее об этом в статье про GPIO
    GPIO_StructInit(&port);
    port.GPIO_Mode = GPIO_Mode_Out_PP;
    port.GPIO_Pin = GPIO_Pin_0;
    port.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOB, &port);
    //А тут настройка таймера
    //Заполняем поля структуры дефолтными значениями
    TIM_TimeBaseStructInit(&timer);
    //Выставляем предделитель
    timer.TIM_Prescaler = TIMER_PRESCALER;
    //Тут значение, досчитав до которого таймер сгенерирует прерывание
    //Кстати это значение мы будем менять в самом прерывании
    timer.TIM_Period = 50;
    //Инициализируем TIM4 нашими значениями
    TIM_TimeBaseInit(TIM4, &timer);	
}
 
/*******************************************************************/
int main()
{
    __enable_irq();
    initAll();
    //Настраиваем таймер для генерации прерывания по обновлению (переполнению)
    TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE);
    //Запускаем таймер 
    TIM_Cmd(TIM4, ENABLE);
    //Разрешаем соответствующее прерывание
    NVIC_EnableIRQ(TIM4_IRQn);
    while(1)
    {
	//Бесконечно тупим) Вся полезная работа – в прерывании
	__NOP();
    }		
}
 
/*******************************************************************/
void TIM4_IRQHandler()
{	
    //Если на выходе был 0..
    if (previousState == 0)
    {
	//Выставляем единицу на выходе
	previousState = 1;
	GPIO_SetBits(GPIOB, GPIO_Pin_0);
	//Период 50 тиков таймера, то есть 0.5 мс
	timer.TIM_Period = 50;
	TIM_TimeBaseInit(TIM4, &timer);	
	//Очищаем бит прерывания
	TIM_ClearITPendingBit(TIM4, TIM_IT_Update);
    }
    else
    {
	//Выставляем ноль на выходе
	previousState = 0;
	GPIO_ResetBits(GPIOB, GPIO_Pin_0);
	//А период теперь будет 250 тиков – 2.5 мс
	timer.TIM_Period = 250;
	TIM_TimeBaseInit(TIM4, &timer);	
	TIM_ClearITPendingBit(TIM4, TIM_IT_Update);
    }	
}
 
/****************************End of file****************************/

В этой программе мы смотрим, что было на выходе до момента генерации прерывания – если ноль, выставляем единицу на 0.5 мс. Если была единица – ставим ноль на 2.5 мс. Компилируем и запускаем отладку =)

Немного отвлекусь, очередные грабли попались на пути) А именно ошибка:

..\..\..\SPL\src\stm32f10x_tim.c(2870): error:  #20: identifier «TIM_CCER_CC4NP» is undefined

Не так страшно как может показаться, идем в файл stm32f10x.h, находим строки

#define  TIM_CCER_CC3NE                    ((uint16_t)0x0400)
#define  TIM_CCER_CC3NP                    ((uint16_t)0x0800)
#define  TIM_CCER_CC4E                       ((uint16_t)0x1000)
#define  TIM_CCER_CC4P                       ((uint16_t)0x2000

И смело дописываем:

#define  TIM_CCER_CC4NP                      ((uint16_t)0x8000)

Вот теперь все собирается, можно отлаживать. Включаем логический анализатор. В командной строке пишем: la portb&0x01 и наблюдаем на выходе:

Работа таймера STM32

Что хотели, то и получили ) Другими словами все работает правильно. В следующей статье поковыряем режим генерации ШИМ, оставайтесь на связи 😉

Не пропустите хорошую статью про таймеры в целом — ссылка.

Понравилась статья? Поделись с друзьями!

STM32 с нуля. Таймеры.: 30 комментариев
  1. Симулятор выдал *** error 65: access violation at 0x4000200C : no ‘read’ permission, на следующем шаге выдает тоже самое, только на запись.
    проц стоит 103RB, пробовал другие, тоже самое… Другие проекты (из статьи про GPIO например) симулирует нормально. Здесь же утыкается в эту вот строку TIMx->DIER |= TIM_IT; Это в файле stm32f10x_tim.c
    Как бы эту неприятность побороть?

  2. Получается не дописанная строчка: #define TIM_CCER_CC4NP ((uint16_t)0x8000) косяк библиотеки?

  3. i ya ne sovsem ponimau gde mi vizivaem funkziu TIM4_IRQHandler()

    esli mi vse vremya v petle:
    while(1)
    {
    //Бесконечно тупим) Вся полезная работа – в прерывании
    __NOP();
    }

    i wto delaet funkziya __NOP() ?
    spasibo zaranee i sory for translit!

    • NOP() — просто функция заглушка, она ничего не делает.
      TIM4_IRQHandler() — обработчик прерывания, он вызывается, когда какое то событие вызывает прерывание. В данном случае событие переполнения таймера

  4. Тема режима ШИМ не раскрыта. В коде у вас эмуляция работы таймера в режиме модулятора.

  5. В частности фраза «Скоро будет примерчик как раз на ШИМ, там и поковыряем поподробнее.» подразумевает настройку таймера в режим ШИМа.

  6. У меня при вводе в командную строку la portе&0×05, выдает ошибку : *** error 34: undefined identifier
    С чем это может быть связано?

  7. Есть задача. На одной ножке хочу получить меандр 36МГц, на другой тоже 36МГц, той же полярности, но с задержкой (deadtime).
    Вопросы: есть ли таймер, способный на это, если есть, то какой у него номер, есть ли такой таймер в 103 серии, 100 ногой линейке?
    Ну и примеру кода, если это не сложно, был бы весьма благодарен.

  8. Здравствуйте! Решил портировать код этого урока для STM32F0Discovery. Вот то, что получилось:

    /****************************TIM.c*********************************/
    //Подключаем все нужные файлы
    #include «stm32f0xx.h»
    #include «stm32f0xx_rcc.h»
    #include «stm32f0xx_gpio.h»
    #include «stm32f0xx_tim.h»

    /*******************************************************************/
    //При таком предделителе у меня получается один тик таймера на 10 мкс
    #define TIMER_PRESCALER 720

    /*******************************************************************/
    //Переменная для хранения предыдущего состояния вывода PB0
    uint16_t previousState;
    GPIO_InitTypeDef port;
    TIM_TimeBaseInitTypeDef timer;

    /*******************************************************************/
    void initAll()
    {
    //Включаем тактирование порта GPIOB и таймера TIM4
    //Таймер 4 у нас висит на шине APB1
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB, ENABLE);
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_TIM3, ENABLE);
    //Тут настраиваем порт PB0 на выход
    //Подробнее об этом в статье про GPIO
    port.GPIO_Mode = GPIO_Mode_OUT;
    port.GPIO_Pin = (GPIO_Pin_0);
    port.GPIO_Speed = GPIO_Speed_Level_1;
    port.GPIO_OType = GPIO_OType_PP;
    GPIO_Init(GPIOB, &port);
    //А тут настройка таймера
    //Заполняем поля структуры дефолтными значениями
    TIM_TimeBaseStructInit(&timer);
    //Выставляем предделитель
    timer.TIM_Prescaler = TIMER_PRESCALER;
    //Тут значение, досчитав до которого таймер сгенерирует прерывание
    //Кстати это значение мы будем менять в самом прерывании
    timer.TIM_Period = 50;
    //Инициализируем TIM3 нашими значениями
    TIM_TimeBaseInit(TIM3, &timer);
    }

    int main()
    {
    __enable_irq();
    initAll();
    //Настраиваем таймер для генерации прерывания по обновлению (переполнению)
    TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
    //Запускаем таймер
    TIM_Cmd(TIM3, ENABLE);
    //Разрешаем соответствующее прерывание
    NVIC_EnableIRQ(TIM3_IRQn);
    while(1)
    {
    //Бесконечно тупим) Вся полезная работа – в прерывании
    __NOP();
    }
    }

    /*******************************************************************/
    void TIM4_IRQHandler()
    {
    //Если на выходе был 0..
    if (previousState == 0)
    {
    //Выставляем единицу на выходе
    previousState = 1;
    GPIO_SetBits(GPIOB, GPIO_Pin_0);
    //Период 50 тиков таймера, то есть 0.5 мс
    timer.TIM_Period = 50;
    TIM_TimeBaseInit(TIM3, &timer);
    //Очищаем бит прерывания
    TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
    }
    else
    {
    //Выставляем ноль на выходе
    previousState = 0;
    GPIO_ResetBits(GPIOB, GPIO_Pin_0);
    //А период теперь будет 250 тиков – 2.5 мс
    timer.TIM_Period = 250;
    TIM_TimeBaseInit(TIM3, &timer);
    TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
    }
    }

    //****************************End of file****************************/

    всё нормально скомпилилось, без ошибок, но сигнала на выходе я так и не увидел, становлюсь осциллографом на PinB0, но там ничего нет, не пойму почему. Не могли бы Вы подсказать мне, где я чего недосмотрел. И ещё, можно по подробнее рассказать, как правильно высчитать TIMER_PRESCALER для этого примера, а то мне не очень понятно. Спасибо!!!

    • Предделитель рассчитывается исходя из того, какую необходимо получить частоту работы таймера. Если таймер тактируется от 72 МГц, например, то при делителе 7200 получим частоту таймера 72 000 000 / 7200 = 10 000 Гц.
      По поводу F0 — надо посмотреть на той ли шине там вообще висит таймер. А кстати, в твоем примере таймер 3 используется, а прерывание таймера 4)

  9. Я извиняюсь, я это видел и исправил, только исправления в текстовый файл забыл внести. В Кейле компилировал версию, с прерыванием по таймеру 3. И в F0, таймер 3, вроде тактируется по этой шине. А где посмотреть, какой частотой тактируется шина, чтоб потом посчитать предделитель? Огромное спасибо за ответ!!

    • В мануале на контроллер должна быть схема большая со всеми частотами, делителями и шинами)

  10. Перерыл весь мануал на F103? никак не пойму, откуда у Вас взялась частота тактирования шины таймеров 72мГц?

  11. Со всем разобрался. Тактовая частота у меня 48мГц, предделитель я взял соответственно, 480. А ничего не было на выходе, из за этой строки:
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_TIM3, ENABLE);
    надо было вот так:
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3 , ENABLE);
    Я просто на ту строку скопировал из файла stm32f0xx_rcc.h по запаре. Теперь всё исправил и всё работает, осциллограф рисует импкльсы на выходе PinB0. Спасибо Вам за помощь и внимание!!!!!

  12. Здравствуйте! Читаю с упоением Ваш курс, все очень детально описываете и рассказываете, спасибо Вам за это. К сожалению у меня возникла проблема с повторением материала из этой статьи, с которой самому разобраться не получилось, а именно при отладке программы у меня интервалы отличаются от Ваших 0,5 мс и 2,5 мс. У меня они составляют 0,17мс и 0,84 мс соответственно. Не подскажите в чем проблема?
    И еще не знаете ли Вы есть ли какая-нибудь библиотечка для создания функции задержки аналогичной функциям delay_ms(), delay_us() в CVAVR?

    • Оба интервала в три раза меньше, значит скорее всего частота тактирования таймера в 3 раза больше )
      Для задержки лучше всего SysTick таймер использовать. Настроить его на прерывания каждые 1 мс и в прерывании уменьшать счетчик задержки, например.

  13. после Include
    static __IO uint32_t TimingDelay;
    void Delay_ms(__IO uint32_t nTime);
    в начале ф-ции main()
    SysTick_Config(72000); // F_CPU 72 000 000 * 0.001=72000

    //и уже в конце программы добавляем функцию временной задержки
    void Delay_ms(__IO uint32_t nTime) { TimingDelay = nTime; while(TimingDelay != 0); }
    void TimingDelay_Decrement(void) { if (TimingDelay != 0x00) { TimingDelay—; } }
    void SysTick_Handler(void) { TimingDelay_Decrement(); }

    В программе:
    Delay_ms(100); //Временная задержка на 0,1 с

  14. сделал все как написано. среда keil 5.
    В процессе отладки в симуляторе, бесконечно
    работает цикл
    /* Wait till HSE is ready and if Time out is reached exit */
    do
    {
    HSEStatus = RCC->CR & RCC_CR_HSERDY;
    StartUpCounter++;
    }
    из функции
    static void SetSysClockTo72(void)
    Что не так?

  15. Здравствуйте! Спасибо за статью, оказалась очень полезной. В плане небольшой оптимизации хочу добавить: В обработчике прерывание сброс флага прерывания происходит в каждом условии. Его можно вынести после условий.
    По скольку таймер может генерировать различные прерывания необходимо в начале проверять чем оно сформировано (к этой статье особо и не отнесешь т.к. создано одно прерывание, оно же и обрабатывается).

  16. Скажите, пожалуйста, что такое Режим Dead-Time. Я новичек, только учусь. Очень интересно. Или дайте рабочую ссылку раскрывающую этот вопрос.

  17. Только сейчас понадобилось измерять длительность импульсов. Примерно я понимаю как это сделать:
    период=время 2го импульса — время 1го импульса.
    а как на аппаратном уровне настроить таймер?
    и на будущее будет так же интересно про генерацию одиночного импульса. Можно про это небольшую статью или кусок кода?

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *