Продолжаем тему таймеров в STM32. В прошлый раз мы рассмотрели базовые таймеры, которые довольно-таки просты. Но сегодня мы поиграемся с более крутой игрушкой — таймерами общего назначения, которые на голову выше предыдущих.
Умеют они всё то же, что и базовые таймеры, но у них есть дополнительные возможности:
- До 4-х каналов для:
- Захвата сигнала (input capture).
- Сравнения вывода (output compare).
- Генерации сигнала ШИМ (выровненного по границе или по центру).
- Генерации одиночных импульсов.
- Схемы синхронизации для управления таймерами при помощи внешних сигналов и для соединения нескольких таймеров друг с другом.
- Комплементарные выходы с программируемым dead-time.
- Счётчик повторений.
- Вход BRK для сброса выходов таймера или выставления их в известное состояние.
- Поддерживают инкрементальные (квадратурные) энкодеры и датчики Холла.
- Генерация прерывания или запроса DMA по следующим событиям:
- Обновление: переполнение счётчика.
- Событие-триггер: старт, остановка, инициализация счётчика или его обновление внутренним или внешним триггером.
- Захват сигнала.
- Сравнение (output compare).
- Включение BRK.
Неплохо, да? Но в этой бочке мёда таки есть маленькая ложечка дёгтя: эти возможности малость неравномерно распределены между таймерами, поэтому придётся поглядывать в разделы (их три) General-purpose timers (…) в руководства по STM32F100xx (ещё ссылка), прежде чем пытаться воспользоваться какой-либо из них.
Вот как вы думаете, если у таймеров общего назначения так много функций, чем тогда продвинутые (advanced-control) таймеры отличаются от них? o_O
Правильный ответ — почти ничем, это по факту просто таймеры общего назначения, которые не имеют никаких ограничений: в них напихано по 4 канала (с комплементарными) и есть все возможности сразу, без какого-то ни было разброса. Так что остальная часть статьи будет относиться ко всем таймерам выше базовых, а продвинутые таймеры я отдельно упоминать не буду.
В даташите на STM32F100xx (ещё ссылка) есть сводная таблица возможностей таймеров, в которую тоже удобно поглядывать для справки:
Кстати, обращайте внимание на сноски. Например, там написано, что у МК семейства Low density Value line нет таймера TIM4.
Захват сигнала
В этом режиме с выбранного канала захватываются импульсы, на каждый из них текущее значение счётчика таймера кладётся в регистр TIM_CCRx, где x — номер канала. Таким образом, период следования импульсов равен разнице между текущим значением TIM_CCRx и предыдущим. Ну а для того, чтобы получить период в каких-то внятных единицах измерения, нужно настроить предделитель через функции базового таймера.
При этом можно настроить генерацию прерывания и запроса DMA на приход очередного импульса, и если в это время предыдущее значение TIM_CCRx не было считано, будет сгенерировано так называемое прерывание over-capture, т.е. сигнал о том, что предыдущее значение потерялось.
Ловить можно фронты, спады или и то, и другое вместе. Есть настройка так называемого фильтра — числа выборок, после которого переход уровня будет считаться состоявшимся (полезно для устранения дребезга). Значение фильтра может принимать значения от 0 (фильтр выключен) до 15 (0xF). Также настраивается делитель входной частоты — 2, 4 или 8: будет ловиться каждый 2й, 4й или 8й импульс соответственно.
Примера ради подёргаем вывод PB15 и замерим таймером TIM3 период, подключив PB15 к его каналу 1 (PA6):
#include <stm32f10x.h> #include <stm32f10x_gpio.h> #include <stm32f10x_rcc.h> #include <stm32f10x_tim.h> #include <misc.h> void init_gpio(void); void init_timer(void); uint16_t uint16_time_diff(uint16_t now, uint16_t before); volatile uint16_t systick_ms = 0; volatile uint16_t capture1 = 0, capture2 = 0; volatile uint8_t capture_is_first = 1, capture_is_ready = 0; int main(void) { init_gpio(); init_timer(); SysTick_Config(SystemCoreClock / 1000); while (1) { /* Каждые 73 миллисекунды меняем уровень на ножке на противоположный, так что период импульсов будет равен 146 мс. */ static uint32_t toggle_ms = 0; if (uint16_time_diff(systick_ms, toggle_ms) >= 73) { toggle_ms = systick_ms; GPIO_Write(GPIOB, GPIO_ReadOutputData(GPIOB) ^ GPIO_Pin_15); } if (capture_is_ready) { NVIC_DisableIRQ(TIM3_IRQn); capture_is_ready = 0; /* Обрабатываем захваченный период, который должен быть равен 146 */ const uint16_t period = uint16_time_diff(capture2, capture1); // ... NVIC_EnableIRQ(TIM3_IRQn); } } } void init_gpio(void) { GPIO_InitTypeDef gpio_cfg; GPIO_StructInit(&gpio_cfg); /* Вывод тестового сигнала на PB15 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); gpio_cfg.GPIO_Mode = GPIO_Mode_Out_PP; gpio_cfg.GPIO_Pin = GPIO_Pin_15; GPIO_Init(GPIOB, &gpio_cfg); /* Таймер TIM3, канал 1 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); gpio_cfg.GPIO_Mode = GPIO_Mode_IN_FLOATING; gpio_cfg.GPIO_Pin = GPIO_Pin_6; GPIO_Init(GPIOA, &gpio_cfg); } void init_timer(void) { /* Подаём такты на TIM3 */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); /* Настраиваем предделитель так, чтобы таймер считал миллисекунды. На бо́льших частотах следите, чтобы предделитель не превысил максимальное значение uint16_t - 0xFFFF (65535) */ TIM_TimeBaseInitTypeDef timer_base; TIM_TimeBaseStructInit(&timer_base); timer_base.TIM_Prescaler = 24000 - 1; TIM_TimeBaseInit(TIM3, &timer_base); /* Настраиваем захват сигнала: - канал: 1 - счёт: по нарастанию - источник: напрямую со входа - делитель: отключен - фильтр: отключен */ TIM_ICInitTypeDef timer_ic; timer_ic.TIM_Channel = TIM_Channel_1; timer_ic.TIM_ICPolarity = TIM_ICPolarity_Rising; timer_ic.TIM_ICSelection = TIM_ICSelection_DirectTI; timer_ic.TIM_ICPrescaler = TIM_ICPSC_DIV1; timer_ic.TIM_ICFilter = 0; TIM_ICInit(TIM3, &timer_ic); /* Разрешаем таймеру генерировать прерывание по захвату */ TIM_ITConfig(TIM3, TIM_IT_CC1, ENABLE); /* Включаем таймер */ TIM_Cmd(TIM3, ENABLE); /* Разрешаем прерывания таймера TIM3 */ NVIC_EnableIRQ(TIM3_IRQn); } void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_CC1) != RESET) { /* Даём знать, что обработали прерывание */ TIM_ClearITPendingBit(TIM3, TIM_IT_CC1); /* Запоминаем предыдущее измерение и считываем текущее */ capture1 = capture2; capture2 = TIM_GetCapture1(TIM3); /* Для корректной обработки нужно минимум два измерения */ if (!capture_is_first) capture_is_ready = 1; capture_is_first = 0; /* Тут как-нибудь обрабатываем событие over-capture, если провороним */ if (TIM_GetFlagStatus(TIM3, TIM_FLAG_CC1OF) != RESET) { TIM_ClearFlag(TIM3, TIM_FLAG_CC1OF); // ... } } } /* Тут просто считаем миллисекунды */ void SysTick_Handler(void) { ++systick_ms; } /* Вычисляет разность во времени с учётом переполнения счётчика таймера */ uint16_t uint16_time_diff(uint16_t now, uint16_t before) { return (now >= before) ? (now - before) : (UINT16_MAX - before + now); }
Также существует режим захвата ШИМ. На самом деле, это не отдельный режим, а просто особое сочетание настроек с таким эффектом. Таймер настраивается так, чтобы один канал ловил фронты и сбрасывал счётчик таймера, а второй ловил спады — тогда первый будет захватывать период ШИМ, а второй — заполнение. При этом оба канала подключаются к одному и тому же физическому входу. Суть работы этого «режима» показана в даташите следующим образом:
Изменим предыдущий пример, используя захват ШИМ (прокомментированы только изменения):
#include <stm32f10x.h> #include <stm32f10x_gpio.h> #include <stm32f10x_rcc.h> #include <stm32f10x_tim.h> #include <misc.h> void init_gpio(void); void init_timer(void); uint16_t uint16_time_diff(uint16_t now, uint16_t before); volatile uint16_t systick_ms = 0; volatile uint16_t period_capture = 0, duty_cycle_capture = 0; volatile uint8_t capture_is_first = 1, capture_is_ready = 0; int main(void) { init_gpio(); init_timer(); SysTick_Config(SystemCoreClock / 1000); while (1) { static uint32_t toggle_ms = 0; if (uint16_time_diff(systick_ms, toggle_ms) >= 73) { toggle_ms = systick_ms; GPIO_Write(GPIOB, GPIO_ReadOutputData(GPIOB) ^ GPIO_Pin_15); } if (capture_is_ready) { /* Опять же, копируем результаты, пока нет прерываний, но не задерживаем прерывания надолго. */ NVIC_DisableIRQ(TIM3_IRQn); capture_is_ready = 0; const uint16_t period = period_capture, duty_cycle = duty_cycle_capture; NVIC_EnableIRQ(TIM3_IRQn); /* Обрабатываем параметры ШИМ (переменные period и duty_cycle). Ничего рассчитывать на сей раз не придётся (: ... */ } } } void init_gpio(void) { GPIO_InitTypeDef gpio_cfg; GPIO_StructInit(&gpio_cfg); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); gpio_cfg.GPIO_Mode = GPIO_Mode_Out_PP; gpio_cfg.GPIO_Pin = GPIO_Pin_15; GPIO_Init(GPIOB, &gpio_cfg); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); gpio_cfg.GPIO_Mode = GPIO_Mode_IN_FLOATING; gpio_cfg.GPIO_Pin = GPIO_Pin_6; GPIO_Init(GPIOA, &gpio_cfg); } void init_timer(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); TIM_TimeBaseInitTypeDef timer_base; TIM_TimeBaseStructInit(&timer_base); timer_base.TIM_Prescaler = 24000 - 1; TIM_TimeBaseInit(TIM3, &timer_base); TIM_ICInitTypeDef timer_ic; timer_ic.TIM_Channel = TIM_Channel_1; timer_ic.TIM_ICPolarity = TIM_ICPolarity_Rising; timer_ic.TIM_ICSelection = TIM_ICSelection_DirectTI; timer_ic.TIM_ICPrescaler = TIM_ICPSC_DIV1; timer_ic.TIM_ICFilter = 0; /* Эта функция настроит канал 1 для захвата периода, а канал 2 - для захвата заполнения. */ TIM_PWMIConfig(TIM3, &timer_ic); /* Выбираем источник для триггера: вход 1 (PA6) */ TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1); /* По событию от триггера счётчик будет сбрасываться. */ TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset); /* Включаем события от триггера */ TIM_SelectMasterSlaveMode(TIM3, TIM_MasterSlaveMode_Enable); TIM_ITConfig(TIM3, TIM_IT_CC1, ENABLE); TIM_Cmd(TIM3, ENABLE); NVIC_EnableIRQ(TIM3_IRQn); } void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_CC1) != RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_CC1); NVIC_DisableIRQ(TIM3_IRQn); /* А вот и параметры ШИМ, захваченные с каналов 1 и 2 */ period_capture = TIM_GetCapture1(TIM3); duty_cycle_capture = TIM_GetCapture2(TIM3); NVIC_EnableIRQ(TIM3_IRQn); if (!capture_is_first) capture_is_ready = 1; capture_is_first = 0; if (TIM_GetFlagStatus(TIM3, TIM_FLAG_CC1OF) != RESET) { TIM_ClearFlag(TIM3, TIM_FLAG_CC1OF); // ... } } } void SysTick_Handler(void) { ++systick_ms; } uint16_t uint16_time_diff(uint16_t now, uint16_t before) { return (now >= before) ? (now - before) : (UINT16_MAX - before + now); }
Режим чтения энкодера
Работу с энкодером я уже как-то описывал, и тогда я считывал и декодировал данные с энкодера программно, здесь же таймер сделает работу за нас (не всю, конечно же). Боковые выводы энкодера надо подключить к двум каналам таймера, а средний вывод — к GND. Таймер в этом режиме сам обрабатывает поступающие с энкодера импульсы, а также увеличивает/уменьшает свой счётчик на 4 при каждом щелчке энкодера, и запоминает направление вращения.
Так как мне захотелось ещё и прерывание заиметь, я сделал период равным 4 и разрешил счёт в обе стороны, так что теперь прерывание будет возникать при каждом щелчке энкодера. Использовал я каналы 1 и 2 таймера TIM3 (PA6 и PA7):
#include <stm32f10x.h> #include <stm32f10x_gpio.h> #include <stm32f10x_rcc.h> #include <stm32f10x_tim.h> #include <misc.h> typedef enum { FORWARD, BACKWARD } Direction; volatile uint8_t capture_is_first = 1, capture_is_ready = 0; volatile Direction captured_direction = FORWARD; void init_gpio(void); void init_timer(void); int main(void) { init_gpio(); init_timer(); while (1) { if (capture_is_ready) { NVIC_DisableIRQ(TIM3_IRQn); capture_is_ready = 0; const Direction direction = captured_direction; NVIC_EnableIRQ(TIM3_IRQn); /* Обрабатываем direction ... */ } } } void init_gpio(void) { GPIO_InitTypeDef gpio_cfg; GPIO_StructInit(&gpio_cfg); /* Каналы 1 и 2 таймера TIM3 - на вход, подтянуть к питанию */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); gpio_cfg.GPIO_Mode = GPIO_Mode_IPU; gpio_cfg.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_Init(GPIOA, &gpio_cfg); } void init_timer(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); /* Разрешаем счёт в обе стороны, период ставим 4 */ TIM_TimeBaseInitTypeDef timer_base; TIM_TimeBaseStructInit(&timer_base); timer_base.TIM_Period = 4; timer_base.TIM_CounterMode = TIM_CounterMode_Down | TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &timer_base); /* Считать будем все переходы лог. уровня с обоих каналов */ TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_BothEdge, TIM_ICPolarity_BothEdge); TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); TIM_Cmd(TIM3, ENABLE); NVIC_EnableIRQ(TIM3_IRQn); } void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); if (!capture_is_first) capture_is_ready = 1; capture_is_first = 0; /* В бите TIM_CR1_DIR регистра TIM3_CR1 хранится направление вращения энкодера, запоминаем его. */ captured_direction = (TIM3->CR1 & TIM_CR1_DIR ? FORWARD : BACKWARD); } }
Сравнение вывода (output compare)
В этом режиме выбранный канал таймера будет подключен к соответствующему выводу и будет изменять его (вывода) состояние каждый раз, когда счётчик таймера досчитает до значения регистра TIM_CCRx. Состояние вывода, в зависимости от настройки, будет меняться на ноль, на единицу или на противоположное текущему. У многих таймеров у каналов есть комплементарные выводы, которые по умолчанию являются инверсными: на такой выход подаётся тот же сигнал, что и на обычный, но с противоположным уровнем.
Смотрим в сводную таблицу по таймерам в даташите и видим, что комплементарных выводов у TIM3 нет, но вот у единственного канала таймера TIM16 есть такой вывод — этот таймер я и использую для примера. Вообще, комплементарные выводы есть и у нескольких других таймеров, но вот TIM15 — особенный: у него есть два канала, но комплементарный вывод имеет только 1й канал. Будьте бдительны!
В таблице пинаутов находим выводы канала 1 таймера TIM16 — PB8 (основной) и PB6 (комплементарный). Для иллюстрации работы таймера подключим эти выводы к светодиодам на плате STM32VLDiscovery — PC8 и PC9, которые в коде мы отключим от греха подальше. Таким образом, выводы канала таймера будут напрямую мигать светодиодами:
#include <stm32f10x.h> #include <stm32f10x_gpio.h> #include <stm32f10x_rcc.h> #include <stm32f10x_tim.h> #include <misc.h> void init_gpio(void); void init_timer(void); int main(void) { init_gpio(); init_timer(); do __NOP(); while (1); } void init_gpio(void) { GPIO_InitTypeDef gpio; GPIO_StructInit(&gpio); /* TIM16, канал 1 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Pin = GPIO_Pin_8 | // TIM16_CH1 - основной вывод GPIO_Pin_6; // TIM16_CH1N - комплементарный GPIO_Init(GPIOB, &gpio); } void init_timer(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM16, ENABLE); /* Делитель выставляем так, чтобы счётчик тикал каждую миллисекунду, и выставляем период в 2 секунды. */ TIM_TimeBaseInitTypeDef base_timer; TIM_TimeBaseStructInit(&base_timer); base_timer.TIM_Prescaler = 24000 - 1; base_timer.TIM_Period = 1000; TIM_TimeBaseInit(TIM16, &base_timer); /* Теперь настраиваем 1й канал таймера */ TIM_OCInitTypeDef timer_oc; TIM_OCStructInit(&timer_oc); timer_oc.TIM_Pulse = 500; timer_oc.TIM_OCMode = TIM_OCMode_Toggle; /* Включаем основной и комплементарный выводы */ timer_oc.TIM_OutputState = TIM_OutputState_Enable; timer_oc.TIM_OutputNState = TIM_OutputNState_Enable; /* Активируем каналы */ TIM_OC1Init(TIM16, &timer_oc); /* Включать AOE нужно для всех таймеров, имеющих break input, т.к. по умолчанию вывод на все каналы выключен. */ TIM_BDTRInitTypeDef timer_bdtr; TIM_BDTRStructInit(&timer_bdtr); timer_bdtr.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable; TIM_BDTRConfig(TIM16, &timer_bdtr); TIM_Cmd(TIM16, ENABLE); }
В это примере я выбрал режим переключения вывода в противоположное состояние (TIM_OCMode_Toggle), а остальные настройки оставил по умолчанию. Кстати, не забывайте вызывать функции типа TIM_OCStructInit() для инициализации соответствующих структур, даже если заполняете все поля структур вручную: copy&paste-ориентированное программирование никто не отменял, но при нём легко забыть заполнить какое-нибудь поле и ловить потом баги.
Для обоих выводов канала можно настроить (поля TIM_OCPolarity и TIM_OCNPolarity структуры) так называемую «полярность» — состояние вывода в промежуток времени от начала отсчёта и до TIM_Pulse. По умолчанию для выводов выставляются значения TIM_OCPolarity_High и TIM_OCNPolarity_High, но комплементарный вывод является инверсным — поэтому, если на нём нужен обычный (не инверсный) сигнал, нужно ему выставить TIM_OCNPolarity_Low.
В режиме сравнения генерируется то же самое прерывание, что и в режиме захвата сигнала — TIM_IT_CCx, но здесь оно было не нужно, поэтому я его не разрешал.
Генерация ШИМ
Вот это куда более интересная и практичная штука. Принципы ШИМ уже неоднократно были описаны — как на нашем сайте, так и у Di Halt’a (уж там разжёвано всё до мелочей), а я сосредоточусь на особенностях реализации в STM32.
Настройка этого режима не слишком отличается от настройки output compare: вместо режима TIM_OCMode_Toggle нужно выбрать один из режимов ШИМ, тогда TIM_Period будет трактоваться как период ШИМ, а поле TIM_Pulse — как заполнение (duty cycle). Режимов ШИМ имеется два — выровненный по границе и по центру (edge-aligned и center-aligned). У микроконтроллеров AVR они называются Fast PWM и Phase Correct PWM, соответственно.
Отличной иллюстрацией крутизны таймеров STM32 для генерации ШИМ будет типичная прикладная задача — управление сервомашинкой: Как известно, сервы управляются импульсами переменной ширины, которые шлются с частотой примерно 50 Гц (каждые 20 мс). У сервы, которая оказалась под рукой (Robbe 4.3 g), ширина управляющего импульса от 500 мкс (0°) до 2250 мкс (175°), судя по замерам — то есть, по 10 мкс на каждый градус поворота:
ΔT = T₂ — T₁ = 2250 — 500 = 1750 мкс
∠A = 175°
ΔT/A = 10 мкс/°
То есть, для максимально прозрачного управления сервой нужно:
- Установить таймеру такой делитель частоты, чтобы отсчёт вёлся каждые 10 мкс.
- Задать период ШИМ в 20 мс, то есть 2000 отрезков времени по 10 мкс.
- Класть в регистр сравнения число, равное 50 (500 мкс / 10 мкс) + задаваемый угол.
- Регистр сравнения лучше обновлять строго в момент окончания периода во избежание дёргания сервы.
Не такая уж сложная задача:
#include <stm32f10x.h> #include <stm32f10x_gpio.h> #include <stm32f10x_rcc.h> #include <stm32f10x_tim.h> #include <misc.h> enum { SERVO_SHORTEST_PULSE = 50, SERVO_MAX_ANGLE = 175 }; void init_gpio(void); void init_timer(void); int main(void) { init_gpio(); init_timer(); /* Даём одну секунду на полный поворот сервы */ SysTick_Config(SystemCoreClock / SERVO_MAX_ANGLE); do __NOP(); while (1); } void init_gpio(void) { GPIO_InitTypeDef gpio; GPIO_StructInit(&gpio); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Pin = GPIO_Pin_8; GPIO_Init(GPIOB, &gpio); } void init_timer(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM16, ENABLE); /* Частота счёта - 10 мкс, период - 20 мс */ TIM_TimeBaseInitTypeDef base_timer; TIM_TimeBaseStructInit(&base_timer); base_timer.TIM_Prescaler = SystemCoreClock / 100000 - 1; base_timer.TIM_Period = 2000; TIM_TimeBaseInit(TIM16, &base_timer); /* Конфигурируем канал: - начальное заполнение: 50 тиков (500 мкс) - режим: edge-aligned PWM */ TIM_OCInitTypeDef timer_oc; TIM_OCStructInit(&timer_oc); timer_oc.TIM_Pulse = SERVO_SHORTEST_PULSE; timer_oc.TIM_OCMode = TIM_OCMode_PWM1; timer_oc.TIM_OutputState = TIM_OutputState_Enable; TIM_OC1Init(TIM16, &timer_oc); TIM_BDTRInitTypeDef bdtr; TIM_BDTRStructInit(&bdtr); bdtr.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable; TIM_BDTRConfig(TIM16, &bdtr); /* Включаем прерывание переполнения счётчика */ TIM_ITConfig(TIM16, TIM_IT_Update, ENABLE); TIM_Cmd(TIM16, ENABLE); NVIC_EnableIRQ(TIM1_UP_TIM16_IRQn); } volatile uint8_t servo_angle = 0; void TIM1_UP_TIM16_IRQHandler(void) { /* Если счётчик переполнился, можно смело закидывать в регистр сравнения новое значение. */ if (TIM_GetITStatus(TIM16, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM16, TIM_IT_Update); TIM_SetCompare1(TIM16, SERVO_SHORTEST_PULSE + servo_angle); } } void SysTick_Handler(void) { /* Крутим серву от 0° до 180° и обратно */ static int direction = 1; int new_angle = (int)servo_angle + direction; if (new_angle >= SERVO_MAX_ANGLE) { direction = -direction; new_angle = SERVO_MAX_ANGLE; } else if (new_angle < 0) { direction = -direction; new_angle = 0; } servo_angle = new_angle; }
Dead-time
Если кто не знает, это задержка фронтов сигналов на основном и комплементарном выводах канала таймера. Эта функция есть у некоторых таймеров (смотрите руководство), и нужна она бывает для исключения сквозных токов при управлении силовыми ключами [полу]мостовых схем. Даже не спрашивайте меня, что это такое - это вне моей компетенции.
Настраивается этот самый dead-time в поле TIM_DeadTime структуры TIM_BDTRInitTypeDef и имеет диапазон значений с 0 по 255 (0xFF). Но смысл этого числа не так уж прямолинеен:
Ага, вот так оно и рассчитывается. Здесь Tdts - это длительность такта генератора dead-time (DTG), зависящая от Tdts - текущей частоты тактирования таймера. Обычно таймеры тактируются системной частотой, и TIM_Prescaler на это никак не влияет, а влияет поле TIM_ClockDivision структуры TIM_TimeBaseInitTypeDef - делитель частоты таймера.
Для примера положим, что таймер затактирован без деления частоты (делитель равен 1, TIM_CKD_DIV1), системная частота F равна 24 МГц, а значение DTG = 150. Тогда:
Tdts = 1/F = 1/24 мкс
DTG = 150 = 100101102 ⇒ DTG[7:5] = 1002
Tdtg = 2⋅Tdts = 1/12 мкс
DT = (64 + DTG[5:0])Tdtg = (64 + 6)/12 = 5.8(3) мкс
"Just like that" ☯ ChosunNinja
Я тут для примера набросал код с dead-time попроще для расчёта: DTG=96 ⇒ DT=96.
#include <stm32f10x.h> #include <stm32f10x_gpio.h> #include <stm32f10x_rcc.h> #include <stm32f10x_tim.h> #include <misc.h> void init_gpio(void); void init_timer(void); int main(void) { init_gpio(); init_timer(); do __NOP(); while (1); } void init_gpio(void) { GPIO_InitTypeDef gpio; GPIO_StructInit(&gpio); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_6; GPIO_Init(GPIOB, &gpio); } void init_timer(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM16, ENABLE); /* Частота счёта - 1 мкс, период - 20 мкс */ TIM_TimeBaseInitTypeDef base_timer; TIM_TimeBaseStructInit(&base_timer); base_timer.TIM_Prescaler = 24 - 1; base_timer.TIM_Period = 20; TIM_TimeBaseInit(TIM16, &base_timer); /* Заполнение ШИМ 10 мкс */ TIM_OCInitTypeDef timer_oc; TIM_OCStructInit(&timer_oc); timer_oc.TIM_Pulse = 10; timer_oc.TIM_OCMode = TIM_OCMode_PWM1; timer_oc.TIM_OutputState = TIM_OutputState_Enable; timer_oc.TIM_OutputNState = TIM_OutputNState_Enable; TIM_OC1Init(TIM16, &timer_oc); /* Выставляем dead-time в 4 мкс при 24 МГц */ TIM_BDTRInitTypeDef bdtr; TIM_BDTRStructInit(&bdtr); bdtr.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable; bdtr.TIM_DeadTime = 24 * 4; TIM_BDTRConfig(TIM16, &bdtr); TIM_Cmd(TIM16, ENABLE); }
Для того, чтобы узреть этот самый dead-time на одноканальном осциллографе, нужно подключить PB8 и PB6 через резисторы 1 кОм к его щупу:
Т.к. на эти выводы идут взаимно инверсные сигналы, на экране будут прекрасно видны места, где во время dead-time уровень на обоих входах одинаков из-за задержки фронтов:
Ну, и напоследок - имейте ввиду, что если длительность dead-time превышает длительность импульса на выводе, то соответствующий импульс не будет сгенерирован вообще.
Счётчик повторений
Этот счётчик имеется у нескольких таймеров (TIM15, TIM16 и TIM17) и выполняет он очень простую функцию: генерировать событие (прерывание или запрос DMA) update не на каждое переполнение счётчика, а на каждые N переполнений. То есть, вы задаёте счётчик повторений, таймер его копирует в скрытый регистр и при каждом переполнении уменьшает значение копии на 1. Когда значение достигает нуля, генерируется событие update, таймер снова копирует счётчик повторений и т.д. На самом деле, перечисленные таймеры и так задействуют этот счётчик, просто по умолчанию его значение равно нулю, и событие генерируется на каждое переполнение.
Счётчик может принимать значения от 0 до 255 (0xFF). Описывать тут особо нечего, потому что для использования этой функции достаточно при инициализации таймера написать что-то вроде:
base_timer.TIM_RepetitionCounter = 7;
и всё. В этом случае событие update будет генерироваться каждые 8 переполнений (7 повторений).
Вход BRK
Если вам вдруг понадобится резко перевести выводы каналов таймера в заранее определённое состояние (например, выключить), то эта функция - то, что нужно. Включить её проще пареной репы - нужно сконфигурировать пин TIMx_BKIN на вход, и при инициализации BDTR включить вход BRK:
/* Для примера возьмём таймер TIM16 */ ... gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; gpio.GPIO_Pin = GPIO_Pin_5; GPIO_Init(GPIOB, &gpio); ... bdtr.TIM_Break = TIM_Break_Enable; TIM_BDTRConfig(TIM16, &bdtr); ...
По умолчанию для активации функции break нужно на вход BRK подать логический ноль, но это можно настроить в поле TIM_BreakPolarity. Как только break активирован, все выводы каналов переходят в состояние, которое задаётся при их инициализации полями TIM_OCIdleState и TIM_OCNIdleState в структуре TIM_OCInitTypeDef (по умолчанию на выводах будет низкий уровень). Dead-time при этом учитывается.
Синхронизация таймеров
Таймеры можно соединять друг с другом для синхронизации или образования цепочек таймеров. В первом случае появляется возможность синхронного старта нескольких таймеров по внешнему триггеру, а во втором - возможность одному таймеру управлять счётчиком другого - запускать, разрешать, сбрасывать.
Второй случай (цепочка таймеров) больше подойдёт для иллюстрации, ибо интереснее он. Сделаем-ка для примера 32-битный таймер из двух обычных 16-битных. Для примера я возьму таймеры TIM2 и TIM3. Задача состоит в том, таймер TIM3 тактировал таймер TIM2 по переполнению своего счётчика: то есть, счётчик таймера TIM2 будет увеличиваться при переполнении счётчика TIM3 - получаем 32-битный счётчик, "состоящий" из TIM2_CNT (старшие биты) и TIM3_CNT (младшие).
Для этого нужно настроить выходной триггер таймера TIM3 на переполнение (update), а входной триггер таймера TIM2 - на вход с триггера TIM3. Смотрим в таблицу соединения триггеров для таймеров TIM2-TIM4 (таких таблиц несколько - для разных групп таймеров):
Здесь мы видим, что TIM3 соединён с входом ITR2 таймера TIM2. И тут выясняется, что в Reference manual рассматриваемый случай описан в разделе "Using one timer as prescaler for another", но там допущена ошибка: вместо ITR2 там указан ITR1. Я джва года час искал ошибку в коде!
Ну что ж, переходим от слов к делу:
#include <stm32f10x.h> #include <stm32f10x_gpio.h> #include <stm32f10x_rcc.h> #include <stm32f10x_tim.h> #include <misc.h> void init_timer(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); /* Выбираем вход триггера от TIM3 (ITR2) */ TIM_SelectInputTrigger(TIM2, TIM_TS_ITR2); /* Включаем тактирование от внешнего источника. Теперь TIM2 тактируется по ITR2. */ TIM_SelectMasterSlaveMode(TIM2, TIM_MasterSlaveMode_Enable); TIM_SelectSlaveMode(TIM2, TIM_SlaveMode_External1); /* Выходной триггер будет срабатывать по переполнению */ TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update); /* Обнуляем оба счётчика ( или один 32-битный - кому как :) */ TIM_SetCounter(TIM2, 0); TIM_SetCounter(TIM3, 0); /* Поехали! */ TIM_Cmd(TIM2, ENABLE); TIM_Cmd(TIM3, ENABLE); } int main(void) { init_timer(); while (1) { // ... // /* Где-то здесь нам вдруг захотелось прочитать значение нашего импровизированного 32-битного счётчика: для этого "сшиваем" два 16-битных счётчика вместе. Т.к. TIM2 тактируется от TIM3, его счётчик будет старшей половиной 32-битного значения. */ const uint16_t high = TIM_GetCounter(TIM2), low = TIM_GetCounter(TIM3); const uint32_t counter = (uint32_t)(((uint32_t)high << 16) | low); /* Теперь у нас в counter - значение 32-битного счётчика. PROFIT! */ // ... // } }
Фиксация параметров
Есть возможность зафиксировать параметры таймера до следующего сброса (reset) МК. За это отвечает поле TIM_LOCKLevel структуры TIM_BDTRInitTypeDef. Фиксация возможна лишь единожды - изменить параметры фиксации нельзя, пока не произойдёт сброс. Функция имеет три уровня, которым соответствуют следующие наборы фиксируемых параметров:
- TIM_LOCKLevel_1
Фиксируются настройки dead-time, break enable, break polarity, AOE и состояний выводов таймера в режиме break. - TIM_LOCKLevel_2
TIM_LOCKLevel_1 + фиксируются output polarity для каналов таймера, а также настройки таймера в режиме break (вывод вкл/выкл). - TIM_LOCKLevel_3
TIM_LOCKLevel_2 + фиксируются настройки сравнения вывода и ШИМ.
39 комментариев на «“STM32: Урок 6.2 — Таймеры общего назначения и продвинутые”»
Отличные статьи пишешь! Все по полочкам, долго искал в инете, почти нигде такого нет. Поменьше бы паузы между публикациями, и тогда вообще было бы замечательно!(:
Спасибо, рад, что статьи вам полезны (:
Паузы — это да. Всё ещё не умею писать в режиме «поток сознания»: прежде чем написать хоть строчку, начинаю на автомате продумывать все варианты, даже ненужные — дурацкая программерская привычка, которая тут не к месту |:
Какую дальше тему планируешь разбирать?
АЦП, а затем — ЦАП.
Подскажите, а захват (input capture) по обоим фронтам есть в МК серии F1xx? Я нашел в реф мане, что в F4xx это есть, но никак не могу найти это для сотой серии.
Есть он и у STM32F100xx, просто в поле TIM_ICPolarity структуры TIM_ICInitTypeDef нужно внести значение TIM_ICPolarity_BothEdge.
А что вы смотрели для STM32F100xx? Я в каждой статье ссылаюсь наSTM32F100xx Reference manual (RM0041), там всё расписано.
Именно реф ман я и смотрел. Для f2 и f4 нашел сразу, а для f1 не вижу что-то. А можете страницу в реф мане указать?
Например, в RM0041 (Doc ID 16188 rev 4) на странице 371:
Приветствую. Только начал осваивать STM32, два дня сидел… не смог настроить таймер на необходимые функции, а время, блин, как всегда поджимает…
Подскажите, а если можно, то примерчик накидайте, как настроить таймер следующим образом: тактируется со входа ETR, по событию на CH1 захватывает значение, сбрасывает счет таймера и пересылает значение (по DMA) в какую-то ячейку памяти?
Пока только смог затактировать таймер внешним сигналом, и вызывать прерывание по захвату… в котором сбрасываю счет. И как-то не уверен, что все правильно настроил.
Пользуюсь coocox-ом. Огромное спасибо!
DMA я сам ещё не трогал, так что тут помочь не могу.
Продолжения серии не будет? 🙁
Будет, но в другом формате. Курсов по периферии STM32 наплодилось — мама не горюй, а у меня, как назло, нехватка времени и творческий и трудовой тупик. Есть у меня идея писать про работу с SD-карточками, начиная с самого мяса (общения по SPI, усё руками) и заканчивая периферией SDIO, которая сама уже с извилинами (протокол, прерывания, DMA).
В целом, сконцентрироваться хочу на девайсах, а не на STM32. В конце концов, эти МК — просто инструмент, который надо изучать в деле, а не только в теории.
Сожалею, что заставляю ждать, но обстоятельства неумолимы.
То есть скоро можно будет увидеть новый урок? А то что затишье…
Это будет скорее поучительное исследование. И не скоро, а недели через полторы-две. Про SD-карты, файловые системы и снова о модели драйверов, как впосте по библиотеку для HD44780 .
откуда мы знаем что канал 1 это порт А — 6пин.
И как сделать тоже для кнопки stm32f4discovery? Хочу попробовать с дребезгом разобраться
В даташите, вестимо.STM32: Урок 3 — Документация , второй абзац, ключевое слово — распиновка.
AVRщик?
А нету кода по работе с ШИМ и TIM1? Не могу запустить. Точнее запустил, но работает как-то через раз. На TIM2 все идет без проблем.
Вопрос отпал, настроил.
Добрый день! У меня stm32l-discovery (STM32L152RBT6), пытаюсь определить частоту импульсов на входе PA01… использую для этого TIM2. TIM2 тактируется (TIM_GetCounter выдает изменяющиеся значения) даже возникает прерывание если в TIM_ICInit указываю TIM_Channel_2, но когда пытаюсь в прерывании считать TIM_GetCapture1 — там упорно 0. При попытке сконфигурировать TIM_PWMIConfig с последующим TIM_SelectInputTrigger TIM_SelectSlaveMode… прерывание пропадает… Такое впечатление что PA01 не подключено к входу счетчика, и не совсем ясно от чего срабатывает в такой ситуации прерывание. Не могли бы вы подсказать как найти в чем дело?
Сегодня мои телепатические способности дремлют, поэтому шлите код в личку — посмотрю.
для L152 не хватало GPIO_PinAFConfig(GPIOA, GPIO_PinSource1, GPIO_AF_TIM2);…
Повольте задать вопрос:
из первого примера, такой коменнтарий
/* Настраиваем захват сигнала:
— канал: 1
— счёт: по нарастанию
— источник: напрямую со входа
— делитель: отключен
— фильтр: отключен */
Я так понял, что пин B15 генерирует «сигнал» с периодом 146мс, а пин А6 принимает этот «сигнал».
И что бы все работало нужно пины(B15 и А6) соеденить перемычкой?
Мое предположение подтвердилось, порты замыкаем на себя.
У меня другой вопрос:
Почему значение переменных постоянно скачут до заполнения 16бит; capture1 и capture2 только в первую итерацию верные(с brakepoint на строчке const uint16_t period = uint16_time_diff(capture2, capture1); т.е. 73 и 219 соответственно, а уже в следующею итерацию они имеют вид 219 и 9934 соответственно… их значения каждый раз разные, закономерности не могу увидеть.
Почему так происходит. здесь речь я ввиду о первом примере.
для debug использую coocox CoIDE 1.6.0 и GNU tools ARM 4.6.2012q4
Так там же дальше написано в обработчике прерывания:
Короче говоря, значение периода сигнала, идущего на вход таймера — это разница между текущим и предыдущим значениями счётчика таймера. А что он вам насчитает — миллисекунды или что ещё, уже зависит от настройки предделителя таймера.
Спасибо большое за статьи, с удовольствием буду читать! Извините за
тупойнеумный вопрос: а где можно почитать про API функции? Почему-то не могу найти на st.com соответствующего документа.Вполне возможно, что его и нет. Я не искал, а вместо этого просто смотрел в заголовочные файлы — там достаточно подробные комментарии перед определениями функций и прочего.
Всем привет. А кто-нибудь пробовал соединять последовательно таймеры в режиме Encoder. Чтобы повысить разрядность до 32 bit. Если соединять как написано здесь, то один работает нормально как энкодер, т.е. младший регистр и up, и down; а вот старший только up при каждом проходе через 0. Или возможно ли как-нибудь сделать, чтобы Slave Timer был двунаправленным. При Overflow — был up, а при Underflow — down?
Объясните пожалуйста, предложенный режим захвата параметров ШИМ возможен только для каналов 1 и 2? если возможность измерять длительность импульсов ( пусть последовательно во времени) на разных ножках (соответственно разных каналов) но одним и тем же таймером (TIM4, например)? Ведь у него целых 4 канала.
Цитата:«Также существует режим захвата ШИМ. На самом деле, это не отдельный режим, а просто особое сочетание настроек с таким эффектом. Таймер настраивается так, чтобы один канал ловил фронты и сбрасывал счётчик таймера, а второй ловил спады — тогда первый будет захватывать период ШИМ, а второй — заполнение. При этом оба канала подключаются к одному и тому же физическому входу.»
Прошу помощи у тех кто съел зубы на STM32F103C8T6 — задача запустить счетный вход, таймера ТМх а второй ТМх — отчет 1 сек. нужно замерять импульсы от 3кГц до 10кГц большой точности не надо (шаг 50Гц) _|-|______|-|____ (форма сигнала). Готовые на зх таймерах (а у меня только 1 — 4) но 4й занят уже.
Можно вход это прерывание,(не важно фронт или спад) кол-во тиков за это время
/ на число. Делаю контроль вращения вентилятора. 5000 об/мин (2 металических болта) = 10000 импульсов/мин.
Добрый день. Я только начал знакомиться с ШИМ. Поставлена задача — реализовать 5 каналов для генерации частоты. Решил я для этого использовать 2 таймера — TIM1 и TIM3. Так вот, например, у таймера TIM3 есть 4 канала для генерации ШИМ. Подскажите, пожалуйста, как настроить конкретный канал? Просто я в вашем примере этого не нашел
Или я неправильно понял концепцию таймеров для этой цели? Просто мне нужно генерировать 5 разных частот. Один таймер генерирует частоту на свои 4 канала одинаковую или есть настройка для каждого канала? Подскажите, пожалуйста
Пример по захвату сигнала самый первый
В упор не пойму, как Вы подключили PB15 к PA6. Не нашел строчки кода, которая ЯВНО указывает, что, PA6 должна быть приконнекчена к PB15
Или, имеется в виду, физический проброс пина 15 на вход таймера проводком?
Я просто думал, в контроллере есть какая — то внутренняя коммуникация портов, которая позволяет соединять входы и выходы программно
Спасибо за ответ.
почитал про TIM_Prescaler TIM_Period выставил параметры так:
Здесь частота выходит 1КГц — с этой частотой будет опрашиваться порт и каждый опрос
будет записываться в переменную таймера -если я правильно понял.
период сигнала который генерируется 146мс == 6.849 Гц. частота таймера 1КГц т.е. в этот период(146мс) должно влезть 146 «опросов».
Включаю debug ставлю brakepoint на строку::
TIM_ClearITPendingBit(TIM3, TIM_IT_CC1);
И получаю такие цифры. Стоит сразу отметить что systick имеет увиличение значения с каждым прирываем на 146мс. ::
Посмотрел еще раз значение константы period установив brakepoint в строчке::
const uint16_t period = uint16_time_diff(capture2, capture1);
вот такие там значеня:: 2048 2 681 54 64889 (тоже пять итераций)
Извените если не в тему, но мне бы очень хотелось заставить его считать период и разобраться что к чему.
Функция
работает не правильно!
При значении before=65535 и now=0 результат будет 0, а должен быть 1.
Правильно будет так:
Решил подключить энкодер к stm, решил воспользоваться вашим примером, но возникли вопрос (с языком с++ напряжно.)Keil ругается на присвоение константы
Что выполняет данная строчка? присваивает переменой direction одно из двух значений FORWARD или BACKWARD. Правильно ли я понял?
Правильно. Так странно, на первый взгляд, сделано по следующей причине: переменная captured_direction изменяется в прерывании (поэтому она объявлена как volatile) — это значит, что если мы станем использовать captured_direction в расчётах, может получиться такая картина:
Вот поэтому сначала константа direction инициализируется значением captured_direction, а уже потом рассчёты предполагается вести с direction, которая в прерывании не меняется. Короче, мы работаем с копией данных, которые могут измениться в прерывании.
Немного поправил ваш пример, чтобы Keil не матерился.
При такой записи, выдает:
main.c(32): error: #268: declaration may not appear after executable statement in block
Но если все записать так же, но указать константу перед выключением прерывания, то Keil радуется что все ОК!