Таймеры в 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 и наблюдаем на выходе:
Видим, что все работает правильно! В следующей статье будем изучать режим генерации ШИМ, оставайтесь на связи 🙂