Top.Mail.Ru

STM32 с нуля. Timer. Настройка и использование таймеров.

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

Надо бы потихоньку написать программу какую-нибудь тестовую. Но сначала посмотрим, что есть в библиотеке 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 и наблюдаем на выходе:

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

Видим, что все работает правильно! В следующей статье будем изучать режим генерации ШИМ, оставайтесь на связи 🙂

Подписаться
Уведомить о
guest

43 комментариев
Старые
Новые
Межтекстовые Отзывы
Посмотреть все комментарии
DroN
DroN
10 лет назад

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

DroN
DroN
Ответ на комментарий  Aveal
10 лет назад

Пасиб, нашел там решение!
Помогла команда MAP 0x4000200c, 0x4000200c READ WRITE

Может кому пригодится - ссылка на описание проблемы http://www.keil.com/support/docs/814.htm

ATH
ATH
10 лет назад

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

gapner
gapner
10 лет назад

Ne pomojete, daet owibku:

timer.c(43): error: #20: identifier "TIM4_IRQn" is undefined

gapner
gapner
10 лет назад

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!

gapner
gapner
10 лет назад

Spasibo. A kak bit s owibkoi:
timer.c(43): error: #20: identifier «TIM4_IRQn» is undefined ?

gapner
gapner
10 лет назад

a vse nawel kosyak, ne ta kategoriya MK bila razkomenchina v в stm32f10x.h.

max0131
max0131
10 лет назад

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

max0131
max0131
10 лет назад

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

Евгений
Евгений
9 лет назад

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

Евгений
Евгений
9 лет назад

Хрен его знает то ли баг, толи кайль криво стоит.

zheka
zheka
9 лет назад

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

Maksim
Maksim
9 лет назад

Здравствуйте! Решил портировать код этого урока для 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 для этого примера, а то мне не очень понятно. Спасибо!!!

Maksim
Maksim
9 лет назад

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

Maksim
Maksim
9 лет назад

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

Maksim
Maksim
9 лет назад

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

Alexus
Alexus
9 лет назад

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

Паша
Паша
9 лет назад

после 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 с

HugoStiglits
HugoStiglits
8 лет назад

сделал все как написано. среда 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)
Что не так?

Vladimir
Vladimir
8 лет назад

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

Дмитрий
Дмитрий
8 лет назад

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

Паша
Паша
8 лет назад

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

Андрей
Андрей
6 лет назад

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

Потрясатель основ
Потрясатель основ
6 лет назад

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

Сергей
6 лет назад

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

К
К
5 лет назад

Здравствуйте. А можно повторить эту статью, только для HAL? Очень интересует режим захвата.

К
К
Ответ на комментарий  Aveal
5 лет назад

Буду ждать. Вообще ваши статьи самые лучшие для понимания. Написано доходчиво, с картинками и примерами кода. Идеально для начинающего. Первые шаги с STM32 сделал благодаря этому сайту.

sWin
sWin
5 лет назад

Скажите, а, можно ли захватывать сигнал таймером с любого пина контроллера, настроенного на выход? Как скоммутировать какой - нибудь выход процессора с входом таймера для захвата?
Нигде нет инфы, везде подают сигнал извне
Нашел один пример, но, там просто, инициализируется выходной пин, который будет импульсы выводить и входной пин канала 1 таймера TIM3. a, вот, указания таймеру, чтобы "слушать" именно этот пин, там нет

RRR
RRR
2 лет назад

Здравствуйте! Есть непонятный момент в работе TIM4 вместе с SysTick. Дело в том, что я настроил мигание одного светодиода с помощью SysTick, а второго с помощью TIM4 так, чтобы их периоды мигания были одинаковыми, но почему они мигают одновременно? Это же два прерывания, которые не могут вызываться одновременно. Я это понимаю так, что прерывание, вызванное TIM4 живет своей жизнью, но его постоянно прерывает SysTick для уменьшения буферной переменной. Скажите, пожалуйста, правильно ли я понимаю этот момент? Большое спасибо!

43
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x