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
11 лет назад

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

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

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

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

ATH
ATH
11 лет назад

Получается не дописанная строчка: #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 лет назад

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

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

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

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

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

zheka
zheka
10 лет назад

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

Maksim
Maksim
10 лет назад

Здравствуйте! Решил портировать код этого урока для 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
10 лет назад

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

Maksim
Maksim
10 лет назад

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

Maksim
Maksim
10 лет назад

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

Alexus
Alexus
10 лет назад

Здравствуйте! Читаю с упоением Ваш курс, все очень детально описываете и рассказываете, спасибо Вам за это. К сожалению у меня возникла проблема с повторением материала из этой статьи, с которой самому разобраться не получилось, а именно при отладке программы у меня интервалы отличаются от Ваших 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
9 лет назад

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

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

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

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

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

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

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

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

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

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

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

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

К
К
6 лет назад

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

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

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

sWin
sWin
5 лет назад

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

RRR
RRR
3 лет назад

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

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