STM32: Урок 6.2 — Таймеры общего назначения и продвинутые


Продолжаем тему таймеров в 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 мкс/°

То есть, для максимально прозрачного управления сервой нужно:

  1. Установить таймеру такой делитель частоты, чтобы отсчёт вёлся каждые 10 мкс.
  2. Задать период ШИМ в 20 мс, то есть 2000 отрезков времени по 10 мкс.
  3. Класть в регистр сравнения число, равное 50 (500 мкс / 10 мкс) + задаваемый угол.
  4. Регистр сравнения лучше обновлять строго в момент окончания периода во избежание дёргания сервы.

Не такая уж сложная задача:

#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 — Таймеры общего назначения и продвинутые”»

  1. Отличные статьи пишешь! Все по полочкам, долго искал в инете, почти нигде такого нет. Поменьше бы паузы между публикациями, и тогда вообще было бы замечательно!(:

    • Спасибо, рад, что статьи вам полезны (:

      Паузы — это да. Всё ещё не умею писать в режиме «поток сознания»: прежде чем написать хоть строчку, начинаю на автомате продумывать все варианты, даже ненужные — дурацкая программерская привычка, которая тут не к месту |:

    • Какую дальше тему планируешь разбирать?

  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:

      Bit 1 CC1P: Capture/Compare 1 output Polarity.
        ...
        CC1 channel configured as input:
        ...
        11: noninverted/both edges
        Circuit is sensitive to both TI1FP1 rising and
        falling edges (capture mode), TI1FP1 is not inverted.
  3. Приветствую. Только начал осваивать STM32, два дня сидел… не смог настроить таймер на необходимые функции, а время, блин, как всегда поджимает…
    Подскажите, а если можно, то примерчик накидайте, как настроить таймер следующим образом: тактируется со входа ETR, по событию на CH1 захватывает значение, сбрасывает счет таймера и пересылает значение (по DMA) в какую-то ячейку памяти?
    Пока только смог затактировать таймер внешним сигналом, и вызывать прерывание по захвату… в котором сбрасываю счет. И как-то не уверен, что все правильно настроил.
    Пользуюсь coocox-ом. Огромное спасибо!

    • DMA я сам ещё не трогал, так что тут помочь не могу.

    • Будет, но в другом формате. Курсов по периферии STM32 наплодилось — мама не горюй, а у меня, как назло, нехватка времени и творческий и трудовой тупик. Есть у меня идея писать про работу с SD-карточками, начиная с самого мяса (общения по SPI, усё руками) и заканчивая периферией SDIO, которая сама уже с извилинами (протокол, прерывания, DMA).

      В целом, сконцентрироваться хочу на девайсах, а не на STM32. В конце концов, эти МК — просто инструмент, который надо изучать в деле, а не только в теории.

      Сожалею, что заставляю ждать, но обстоятельства неумолимы.

    • То есть скоро можно будет увидеть новый урок? А то что затишье…

  4. Добрый день! У меня 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);…

  5. Повольте задать вопрос:

    из первого примера, такой коменнтарий

    /* Настраиваем захват сигнала:
    — канал: 1
    — счёт: по нарастанию
    — источник: напрямую со входа
    — делитель: отключен
    — фильтр: отключен */

    Я так понял, что пин B15 генерирует «сигнал» с периодом 146мс, а пин А6 принимает этот «сигнал».
    И что бы все работало нужно пины(B15 и А6) соеденить перемычкой?

  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

    • Так там же дальше написано в обработчике прерывания:

      void TIM3_IRQHandler(void)
      {
        if (TIM_GetITStatus(TIM3, TIM_IT_CC1) != RESET)
        {
          ...
      
          /* Запоминаем предыдущее измерение и считываем текущее */
          capture1 = capture2;
          capture2 = TIM_GetCapture1(TIM3);
      
          /* Для корректной обработки нужно минимум два измерения */
          if (!capture_is_first)
            capture_is_ready = 1;
      

      Короче говоря, значение периода сигнала, идущего на вход таймера — это разница между текущим и предыдущим значениями счётчика таймера. А что он вам насчитает — миллисекунды или что ещё, уже зависит от настройки предделителя таймера.

  7. Спасибо большое за статьи, с удовольствием буду читать! Извините за тупой неумный вопрос: а где можно почитать про API функции? Почему-то не могу найти на st.com соответствующего документа.

    • Вполне возможно, что его и нет. Я не искал, а вместо этого просто смотрел в заголовочные файлы — там достаточно подробные комментарии перед определениями функций и прочего.

  8. Всем привет. А кто-нибудь пробовал соединять последовательно таймеры в режиме Encoder. Чтобы повысить разрядность до 32 bit. Если соединять как написано здесь, то один работает нормально как энкодер, т.е. младший регистр и up, и down; а вот старший только up при каждом проходе через 0. Или возможно ли как-нибудь сделать, чтобы Slave Timer был двунаправленным. При Overflow — был up, а при Underflow — down?

  9. Объясните пожалуйста, предложенный режим захвата параметров ШИМ возможен только для каналов 1 и 2? если возможность измерять длительность импульсов ( пусть последовательно во времени) на разных ножках (соответственно разных каналов) но одним и тем же таймером (TIM4, например)? Ведь у него целых 4 канала.

    Цитата:«Также существует режим захвата ШИМ. На самом деле, это не отдельный режим, а просто особое сочетание настроек с таким эффектом. Таймер настраивается так, чтобы один канал ловил фронты и сбрасывал счётчик таймера, а второй ловил спады — тогда первый будет захватывать период ШИМ, а второй — заполнение. При этом оба канала подключаются к одному и тому же физическому входу.»

  10. Прошу помощи у тех кто съел зубы на STM32F103C8T6 — задача запустить счетный вход, таймера ТМх а второй ТМх — отчет 1 сек. нужно замерять импульсы от 3кГц до 10кГц большой точности не надо (шаг 50Гц) _|-|______|-|____ (форма сигнала). Готовые на зх таймерах (а у меня только 1 — 4) но 4й занят уже.
    Можно вход это прерывание,(не важно фронт или спад) кол-во тиков за это время
    / на число. Делаю контроль вращения вентилятора. 5000 об/мин (2 металических болта) = 10000 импульсов/мин.

  11. Добрый день. Я только начал знакомиться с ШИМ. Поставлена задача — реализовать 5 каналов для генерации частоты. Решил я для этого использовать 2 таймера — TIM1 и TIM3. Так вот, например, у таймера TIM3 есть 4 канала для генерации ШИМ. Подскажите, пожалуйста, как настроить конкретный канал? Просто я в вашем примере этого не нашел

  12. Или я неправильно понял концепцию таймеров для этой цели? Просто мне нужно генерировать 5 разных частот. Один таймер генерирует частоту на свои 4 канала одинаковую или есть настройка для каждого канала? Подскажите, пожалуйста

  13. Пример по захвату сигнала самый первый
    В упор не пойму, как Вы подключили PB15 к PA6. Не нашел строчки кода, которая ЯВНО указывает, что, PA6 должна быть приконнекчена к PB15

  14. Или, имеется в виду, физический проброс пина 15 на вход таймера проводком?
    Я просто думал, в контроллере есть какая — то внутренняя коммуникация портов, которая позволяет соединять входы и выходы программно

  15. Спасибо за ответ.

    почитал про TIM_Prescaler TIM_Period выставил параметры так:

    timer_base.TIM_Prescaler = 24 - 1;
    timer_base.TIM_Period = 1000 - 1;
    

    Здесь частота выходит 1КГц — с этой частотой будет опрашиваться порт и каждый опрос
    будет записываться в переменную таймера -если я правильно понял.

    период сигнала который генерируется 146мс == 6.849 Гц. частота таймера 1КГц т.е. в этот период(146мс) должно влезть 146 «опросов».

    Включаю debug ставлю brakepoint на строку::
    TIM_ClearITPendingBit(TIM3, TIM_IT_CC1);

    И получаю такие цифры. Стоит сразу отметить что systick имеет увиличение значения с каждым прирываем на 146мс. ::

    интерация  1  2    3   4   5
    systic:   73  219  365 511 657
    capture1:  0  0    34  628 526
    capture2:  0  34   628 526 681
    

    Посмотрел еще раз значение константы period установив brakepoint в строчке::
    const uint16_t period = uint16_time_diff(capture2, capture1);
    вот такие там значеня:: 2048 2 681 54 64889 (тоже пять итераций)

    Извените если не в тему, но мне бы очень хотелось заставить его считать период и разобраться что к чему.

  16. Функция

    uint16_t uint16_time_diff(uint16_t now, uint16_t before)
    {
      return (now >= before) ? (now - before) : (UINT16_MAX - before + now);
    }

    работает не правильно!
    При значении before=65535 и now=0 результат будет 0, а должен быть 1.
    Правильно будет так:

    uint16_t uint16_time_diff(uint16_t now, uint16_t before)
    {
      return (now >= before) ? (now - before) : ((UINT16_MAX +1) - before + now);
    }

  17. Решил подключить энкодер к stm, решил воспользоваться вашим примером, но возникли вопрос (с языком с++ напряжно.)Keil ругается на присвоение константы

    const Direction direction = captured_direction; 

    Что выполняет данная строчка? присваивает переменой direction одно из двух значений FORWARD или BACKWARD. Правильно ли я понял?

    • Правильно. Так странно, на первый взгляд, сделано по следующей причине: переменная captured_direction изменяется в прерывании (поэтому она объявлена как volatile) — это значит, что если мы станем использовать captured_direction в расчётах, может получиться такая картина:

      // *** В данный момент captured_direction = FORWARD
      if (captured_direction == BACKWARD)
      {
         // что-нибудь делаем
      }
      
      // *** Здесь произошло прерывание таймера
      // *** Сейчас уже captured_direction = BACKWARD,
      // *** потому что переменная была перезаписана в прерывании.
      // *** С этого момента любые расчёты, которые учитывают captured_direction,
      // *** будут учитывать новое значение BACKWARD.

      Вот поэтому сначала константа direction инициализируется значением captured_direction, а уже потом рассчёты предполагается вести с direction, которая в прерывании не меняется. Короче, мы работаем с копией данных, которые могут измениться в прерывании.

    • Немного поправил ваш пример, чтобы Keil не матерился.
      При такой записи, выдает:
      main.c(32): error: #268: declaration may not appear after executable statement in block

      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 ... */
          }
        }
      }

      Но если все записать так же, но указать константу перед выключением прерывания, то Keil радуется что все ОК!

      int main(void)
      {
        init_gpio();
        init_timer();
      
        while (1)
        {
          if (capture_is_ready)
          {
            const Direction direction = captured_direction;  // <---------
            NVIC_DisableIRQ(TIM3_IRQn);
            capture_is_ready = 0;
            NVIC_EnableIRQ(TIM3_IRQn);
      
            /* Обрабатываем direction ... */
          }
        }
      }

Добавить комментарий

Arduino

Что такое Arduino?
Зачем мне Arduino?
Начало работы с Arduino
Для начинающих ардуинщиков
Радиодетали (точка входа для начинающих ардуинщиков)
Первые шаги с Arduino

Разделы

  1. Преимуществ нет, за исключением читабельности: тип bool обычно имеет размер 1 байт, как и uint8_t. Думаю, компилятор в обоих случаях…

  2. Добрый день! Я недавно начал изучать программирование под STM32 и ваши уроки просто бесценны! Хотел узнать зачем использовать переменную типа…

3D-печать AI Arduino Bluetooth CraftDuino DIY Google IDE iRobot Kinect LEGO OpenCV Open Source Python Raspberry Pi RoboCraft ROS swarm ИК автоматизация андроид балансировать бионика версия видео военный датчик дрон интерфейс камера кибервесна манипулятор машинное обучение наше нейронная сеть подводный пылесос работа распознавание робот робототехника светодиод сервомашинка собака управление ходить шаг за шагом шаговый двигатель шилд юмор

OpenCV
Робототехника
Будущее за бионическими роботами?
Нейронная сеть - введение