Таймеры в 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. Суть режима в том, что между сигналами на основном и комплементарном выводах таймера появляется определенная задержка. В интернете есть довольно много информации о том, где это можно и нужно применять.
Ну вот, в принципе, очень кратко об основных режимах работы таймера. Если будут вопросы про другие режимы, более специфические, пишите в комментарии, буду рад помочь )
Время традиционной вставки: поскольку компания STMicroelectronics прекратила поддержку библиотеки SPL, которая использовалась в этом курсе, я создал новый, посвященный работе уже с новыми инструментами, так что буду рад видеть вас там - STM32CubeMx. Кроме того, вот глобальная рубрика по STM32, а также небольшая подборка на смежную тему из нового курса:
- STM32 и таймеры. STM32CubeMx. Настройка и использование.
- STM32 и watchdog. STM32CubeMx. Настройка модуля WWDG.
- STM32 и Timer Input Capture. Режим захвата сигнала.
Надо бы потихоньку написать программу какую-нибудь тестовую. Но сначала посмотрим, что есть в библиотеке 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.
Здесь выбираем канал таймера, ничего неожиданного 🙂 В общем все довольно прозрачно, если что спрашивайте!
Пример инициализации таймера в STM32CubeMx.
С первым файлом понятно. А в файле stm32f10x_tim.c – готовые функции для работы с таймерами. Тоже все в целом ясно. Мы уже использовали библиотеку для работы с GPIO, теперь вот работаем с таймерами, и очевидно, что для разной периферии все очень похоже. Так что давайте создавать проект и писать программу. Итак, создаем новый проект, добавляем в него все необходимые файлы:
Пишем код:
- необходимо отметить, что в поле TIM_Prescaler нужно записывать значение, на единицу меньшее, чем то, которое мы хотим получить.
/***************************************************************************************/ #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); } } /***************************************************************************************/
В этой программе мы смотрим, что было на выходе до момента генерации прерывания – если ноль, выставляем единицу на 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); } /***************************************************************************************/
Итак, продолжаем, на пути у нас очередные грабли! А именно ошибка:
- ..\..\..\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 и наблюдаем на выходе:
Видим, что все работает правильно! В следующей статье будем изучать режим генерации ШИМ, оставайтесь на связи 🙂
Симулятор выдал *** error 65: access violation at 0x4000200C : no 'read' permission, на следующем шаге выдает тоже самое, только на запись.
проц стоит 103RB, пробовал другие, тоже самое... Другие проекты (из статьи про GPIO например) симулирует нормально. Здесь же утыкается в эту вот строку TIMx->DIER |= TIM_IT; Это в файле stm32f10x_tim.c
Как бы эту неприятность побороть?
Известная проблемка, посмотри на сайте Кейла методы решения, должно помочь
Пасиб, нашел там решение!
Помогла команда MAP 0x4000200c, 0x4000200c READ WRITE
Может кому пригодится - ссылка на описание проблемы http://www.keil.com/support/docs/814.htm
Получается не дописанная строчка: #define TIM_CCER_CC4NP ((uint16_t)0x8000) косяк библиотеки?
Ну что-то вроде того
Ne pomojete, daet owibku:
timer.c(43): error: #20: identifier "TIM4_IRQn" is undefined
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() - обработчик прерывания, он вызывается, когда какое то событие вызывает прерывание. В данном случае событие переполнения таймера
Spasibo. A kak bit s owibkoi:
timer.c(43): error: #20: identifier «TIM4_IRQn» is undefined ?
a vse nawel kosyak, ne ta kategoriya MK bila razkomenchina v в stm32f10x.h.
Тема режима ШИМ не раскрыта. В коде у вас эмуляция работы таймера в режиме модулятора.
В частности фраза "Скоро будет примерчик как раз на ШИМ, там и поковыряем поподробнее." подразумевает настройку таймера в режим ШИМа.
«Скоро будет примерчик как раз на ШИМ, там и поковыряем поподробнее.» - значит, что про ШИМ будет в следующей после этой статье.
Вот следующая статья и вот ШИМ - https://microtechnics.ru/stm32-uchebnyj-kurs-tajmery-chast-2/.
Все именно так как написано
У меня при вводе в командную строку la portе&0×05, выдает ошибку : *** error 34: undefined identifier
С чем это может быть связано?
У меня на других компах тоже почему-то это не работает..
Хрен его знает то ли баг, толи кайль криво стоит.
Есть задача. На одной ножке хочу получить меандр 36МГц, на другой тоже 36МГц, той же полярности, но с задержкой (deadtime).
Вопросы: есть ли таймер, способный на это, если есть, то какой у него номер, есть ли такой таймер в 103 серии, 100 ногой линейке?
Ну и примеру кода, если это не сложно, был бы весьма благодарен.
Здравствуйте! Решил портировать код этого урока для 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)
Я извиняюсь, я это видел и исправил, только исправления в текстовый файл забыл внести. В Кейле компилировал версию, с прерыванием по таймеру 3. И в F0, таймер 3, вроде тактируется по этой шине. А где посмотреть, какой частотой тактируется шина, чтоб потом посчитать предделитель? Огромное спасибо за ответ!!
В мануале на контроллер должна быть схема большая со всеми частотами, делителями и шинами)
Перерыл весь мануал на F103? никак не пойму, откуда у Вас взялась частота тактирования шины таймеров 72мГц?
Со всем разобрался. Тактовая частота у меня 48мГц, предделитель я взял соответственно, 480. А ничего не было на выходе, из за этой строки:
RCC_APB1PeriphResetCmd(RCC_APB1Periph_TIM3, ENABLE);
надо было вот так:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3 , ENABLE);
Я просто на ту строку скопировал из файла stm32f0xx_rcc.h по запаре. Теперь всё исправил и всё работает, осциллограф рисует импкльсы на выходе PinB0. Спасибо Вам за помощь и внимание!!!!!
Здравствуйте! Читаю с упоением Ваш курс, все очень детально описываете и рассказываете, спасибо Вам за это. К сожалению у меня возникла проблема с повторением материала из этой статьи, с которой самому разобраться не получилось, а именно при отладке программы у меня интервалы отличаются от Ваших 0,5 мс и 2,5 мс. У меня они составляют 0,17мс и 0,84 мс соответственно. Не подскажите в чем проблема?
И еще не знаете ли Вы есть ли какая-нибудь библиотечка для создания функции задержки аналогичной функциям delay_ms(), delay_us() в CVAVR?
Оба интервала в три раза меньше, значит скорее всего частота тактирования таймера в 3 раза больше )
Для задержки лучше всего SysTick таймер использовать. Настроить его на прерывания каждые 1 мс и в прерывании уменьшать счетчик задержки, например.
после 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 с
сделал все как написано. среда 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)
Что не так?
Здравствуйте! Спасибо за статью, оказалась очень полезной. В плане небольшой оптимизации хочу добавить: В обработчике прерывание сброс флага прерывания происходит в каждом условии. Его можно вынести после условий.
По скольку таймер может генерировать различные прерывания необходимо в начале проверять чем оно сформировано (к этой статье особо и не отнесешь т.к. создано одно прерывание, оно же и обрабатывается).
Скажите, пожалуйста, что такое Режим Dead-Time. Я новичек, только учусь. Очень интересно. Или дайте рабочую ссылку раскрывающую этот вопрос.
Только сейчас понадобилось измерять длительность импульсов. Примерно я понимаю как это сделать:
период=время 2го импульса - время 1го импульса.
а как на аппаратном уровне настроить таймер?
и на будущее будет так же интересно про генерацию одиночного импульса. Можно про это небольшую статью или кусок кода?
Администраторы сайта исправьте доступ к этом статьям и их отображение. В мобильной версии есть кнопки назад, вперед для нахождения предыдущей, следующей статьи, Но сами статьи отображаются коряво и на компьютере и на планшете, - код примеров отображается местами поверх статьи. В десктопной же все наоборот, статьи отображаются правильно, но найти продолжение, через меню сайта очень не простая задача, так как кнопок вперед, назад уже нет.
Спасибо за замечание!
Постараюсь решить проблемы в ближайшее время.
Спасибо! Подробно всё. Только заглянул сюда - понял ошибку свою и сдал лабораторную, только - на 407-м.
Спасибо за хороший отзыв! Рад, что статьи полезны!
Подскажите такой момент. Не совсем понятно происхождение функции __enable_irq(), во всем проекте нашел только строчку "/* intrinsic void __enable_irq(); */" в файле cmsis_armcc.h и то она закоменчена. В Reffer manual её тоже нет.
Спасибо.
Не скажу, к сожалению, в каком файле определена, не помню... Но в CMSIS должна быть эта функция.
Здравствуйте. А можно повторить эту статью, только для HAL? Очень интересует режим захвата.
Добрый день!
Не успеваю, к сожалению, совсем пока статьями заняться... Запишу обязательно в список будущих статей.
Буду ждать. Вообще ваши статьи самые лучшие для понимания. Написано доходчиво, с картинками и примерами кода. Идеально для начинающего. Первые шаги с STM32 сделал благодаря этому сайту.
Большое спасибо!
Скажите, а, можно ли захватывать сигнал таймером с любого пина контроллера, настроенного на выход? Как скоммутировать какой - нибудь выход процессора с входом таймера для захвата?
Нигде нет инфы, везде подают сигнал извне
Нашел один пример, но, там просто, инициализируется выходной пин, который будет импульсы выводить и входной пин канала 1 таймера TIM3. a, вот, указания таймеру, чтобы "слушать" именно этот пин, там нет
Здравствуйте! Есть непонятный момент в работе TIM4 вместе с SysTick. Дело в том, что я настроил мигание одного светодиода с помощью SysTick, а второго с помощью TIM4 так, чтобы их периоды мигания были одинаковыми, но почему они мигают одновременно? Это же два прерывания, которые не могут вызываться одновременно. Я это понимаю так, что прерывание, вызванное TIM4 живет своей жизнью, но его постоянно прерывает SysTick для уменьшения буферной переменной. Скажите, пожалуйста, правильно ли я понимаю этот момент? Большое спасибо!
Добрый день!
А можете проект выслать, посмотреть, как организовано мигание?