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, теперь вот работаем с таймерами, и очевидно, что для разной периферии все очень  похоже. Так что давайте создавать проект и писать программу.

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

Необходимо отметить, что в поле TIM_Prescaler нужно записывать значение, на единицу меньшее, чем то, которое мы хотим получить.

/****************************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 - 1;
    //Тут значение, досчитав до которого таймер сгенерирует прерывание
    //Кстати это значение мы будем менять в самом прерывании
    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 мс. Компилируем и запускаем отладку =)

Небольшое, но очень важное отступление… Наш пример, конечно, будет работать и для теста он вполне сгодится, но все-таки в “боевых” программах нужно следить за оптимальностью кода как с точки зрения его объема, так и с точки зрения производительности и расхода памяти. В данном случае нет никакого смысла использовать структуру timer, а также вызывать функцию TIM_TimeBaseInit() каждый раз при смене периода. Правильнее менять всего лишь одно значение в одном регистре, а именно в регистре TIMx->ARR (где х – это номер таймера). В данном примере код трансформируется следующим образом:

/*******************************************************************/
void TIM4_IRQHandler()
{	
    //Если на выходе был 0..
    if (previousState == 0)
    {
	//Выставляем единицу на выходе
	previousState = 1;
	GPIO_SetBits(GPIOB, GPIO_Pin_0);
	//Период 50 тиков таймера, то есть 0.5 мс
	TIM4->ARR = 50;
    }
    else
    {
	//Выставляем ноль на выходе
	previousState = 0;
	GPIO_ResetBits(GPIOB, GPIO_Pin_0);
	//А период теперь будет 250 тиков – 2.5 мс
	TIM4->ARR = 250;
    }	
    TIM_ClearITPendingBit(TIM4, TIM_IT_Update);
} 
 
/****************************End of file****************************/

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

..\..\..\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 с нуля. Таймеры.: 36 комментариев
  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го импульса.
    а как на аппаратном уровне настроить таймер?
    и на будущее будет так же интересно про генерацию одиночного импульса. Можно про это небольшую статью или кусок кода?

  18. Администраторы сайта исправьте доступ к этом статьям и их отображение. В мобильной версии есть кнопки назад, вперед для нахождения предыдущей, следующей статьи, Но сами статьи отображаются коряво и на компьютере и на планшете, – код примеров отображается местами поверх статьи. В десктопной же все наоборот, статьи отображаются правильно, но найти продолжение, через меню сайта очень не простая задача, так как кнопок вперед, назад уже нет.

  19. Спасибо! Подробно всё. Только заглянул сюда – понял ошибку свою и сдал лабораторную, только – на 407-м.

  20. Подскажите такой момент. Не совсем понятно происхождение функции __enable_irq(), во всем проекте нашел только строчку “/* intrinsic void __enable_irq(); */” в файле cmsis_armcc.h и то она закоменчена. В Reffer manual её тоже нет.
    Спасибо.

    • Не скажу, к сожалению, в каком файле определена, не помню… Но в CMSIS должна быть эта функция.

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

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