Top.Mail.Ru

Часть 15. STM32 и С++. Базовые таймеры TIM6 и TIM7.

Настало время заняться потихоньку таймерами. Начнём с самых простых - базовых таймеров TIM6 и TIM7.

Благодаря библиотеке прерываний в статье STM32 и C++. Мой вариант архитектуры, обработка прерываний автора Aveal, мы теперь можем управлять прерываниями как захотим. Надеюсь, что всё получится, и в будущем можно будет писать более гибкие библиотеки.

Ну и раз пошла такая пьянка, режь последний огурец. Функциональная схема таймеров TIM6 и TIM7:

Структурная схема таймера.

Что у нас умеют базовые таймеры? Да практически ничего. Они имеют:

  1. 16-битный прямой счётчик с перезагрузкой.
  2. 16-бит программируемый предделитель (можно "на лету") частоты на число от 1 до 65536.
  3. Схема синхронизации запуска ЦАП.
  4. Генерация прерываний/DMA по переполнению счётчика.

Получается, что мы его можем использовать как устройство, задающее временной интервал. 1 - 3 пункты понятны, 4-й пункт для нашей задачи пока не нужен. Вся периферия имеет и так доступ к DMA. Модуль DMA, подключаемый к таймеру, может копировать из памяти в память - такой режим позволяет копировать данные из внутреннего ОЗУ в подключенное внешнее и наоборот. Но это тема уже другой статьи, здесь мы рассматривать не будем. Как дойдём до внешней памяти, тогда и модернизируем библиотеку.

Инициализировать таймеры мы можем несколькими способами:

void    Init(uint16_t Prescaler, uint16_t Counter);     // Инициализация прямым заданием предделителя и счётчика
uint8_t InitSec(uint16_t Seconds);                      // Задаём в секундах от 1 до 6
uint8_t InitMS(uint16_t Millis);                        // Задаём в миллисекундах от 1 до 65535
uint8_t InitUS(uint16_t Micros);                        // Задаём в микросекундах от 10 до 65535

Наиболее простые функции инициализации:

InitSec() - задаёт время работы таймера в секундах. Максимум, что мы можем себе позволить, это от одной до 6-ти секунд. Больше нам не позволит размерность предделителя и счётчика. Правда, если не нужны точные границы секунд, а подойдут дробные, можно довести до 8.4 секунд с помощью Init().

InitMS() - задаёт время работы таймера в миллисекундах. Диапазон от 1 до 65535 миллисекунд, т. е. от 0.001 секунды до 6.5535 секунд.

InitUS() - задаёт время работы таймера в микросекундах. Диапазон от 10 до 65535 микросекунд, т. е. от 0.01 миллисекунды до 6.5535 миллисекунд. Ограничение в 10 микросекунд поставлено искусственно - при таком режиме хоть что-то работает. Одну микросекунду я тоже пробовал, работает, но МК почти 40% времени только и занимается обработкой прерываний от таймера. На более скоростных МК можно делать и наносекундные задержки, на более медленных - могут колом встать. Правда есть финт ушами - включить режим одного импульса. Задать наносекундную задержку, и после её окончания обработать прерывание. Так как прерывание возникнет только один раз, мы не получим перегрузку МК.

Init() - Наиболее сложная функция. Здесь приходится понимать, как задавать предделитель и счётчик, чтобы получить нужное время. Кроме всего прочего, нам нужно знать, как тактируются данные таймеры. Посмотрим даташит:

.Шины тактирования микроконтроллера.

Как видим из рисунка, эти два таймера тактируются от APB1, тактовая частота которой максимум 42 МГц., но посмотрим тактирование в CubeMX:

Тактирование таймера.

Сигнал PCLK1 максимально может дать 42 МГц, но APB1 Timer clock имеет перед входом тактирования умножитель на 2. Он включен всегда. И, таким образом, мы получаем тактирование наших таймеров частотой 84 МГц, из чего мы и будем исходить.

Допустим, мы хотим получить прерывание от таймера каждые 2 секунды. Чтобы получить целое число, нам нужно выставить предделитель PSC так, чтобы счётчик CNT тактировался целым числом, допустим частотой 1000 герц. Для этого нам нужно частоту сигнала CK_PSC разделить так, чтобы получить требуемую частоту:

84 000 000 / 1000 - 1 = 83 999.

Теперь, чтобы сделать задержку 2 секунды, нам в регистр предзагрузки нужно записать 2000 - 1 = 1999 (единица вычитается из-за того, что счётчик считает с нуля, а не с единицы).

Но тут нас встречает засада. PSC у нас 16-разрядный и максимум, что мы себе можем позволить, это 65535, что как-то меньше 84000. Но выход довольно прост: в PSC мы запишем 8399 и получим тактирование счётчика с частотой 10000 Герц, а в регистр предзагрузки пишем 19999 и получим необходимые нам две секунды, что и сделано в примере с таймером TIM7:

myTIM7.Init(8399, 19999);

Прерывания.

Теперь о прерываниях... Прерывание мы можем обработать двумя способами:

  • по флагу
  • своя функция

В первом случае библиотека имеет встроенный флаг, который поднимается при срабатывании прерывания. Циклически проверяя этот флаг, мы можем узнать было срабатывание или нет. Пример:

while(1)                                                  // В цикле способ обработки прерываний через флаги
{
  bool interruptOccured = myTIM6.getInterruptFlag();
  if (interruptOccured == true)                           // Если флаг от TIM6 поднят
  {
    // Здесь мы что нибудь делаем, так как у нас сработало прерывание
    myTIM6.clearInterruptFlag();                          // Сбрасываем флаг от TIM6, не забываем
  }
}

Мы читаем флаг и, в случае, если он выставлен в true, входим в условие и что-нибудь там делаем. И не забываем сбросить флаг. Иначе мы зациклимся в этом месте, даже если таймер будет уже выключен.

Второй способ, функция myTIM7.SetCallbackIRQ(myTIM_ISR7), а саму функцию описываем в main.c или каком-либо другом файле, например:

void myTIM_ISR7(void)
{
  myUART.print("TIM7 Function Interrupt - ");
  myUART.println(millis() / 1000);
}

Я в ней вывожу в UART строку и время прошедшее с момента старта программы.

Самое интересное, при таком способе не нужно мониторить флаг прерывания, но можно его там сбрасывать, если это зачем-то нужно. Кроме этого, эти два метода можно использовать одновременно. Допустим, в нашей функции нам нужно всегда выводить надпись как в моём примере, а по флагу заниматься чем-то другим. Можно не каждый раз по возникновению прерывания, а при каком-то условии. Получим два обработчика прерываний - один обязательный, один необязятельный.

При работе с прерываниями есть некоторые ограничения.

  1. В прерывании нельзя ждать другого прерывания, если текущее имеет равный или более высший приоритет, чем у ожидаемого.
  2. Обработка прерывания должна происходить быстрее, чем оно вызовется снова.

В своем обработчике прерывания я нарушил 1-й пункт. Но у меня приоритет таймера ниже, чем приоритет UART (это задано в библиотеке). И вызов прерывания от таймера происходит медленнее, чем передача по UART. Если бы я таймеру задал не 2 секунды, а 50 микросекунд, я бы нарушил и 2-й пункт. И у меня начались бы пропуски обработки. А если приоритеты были бы одинаковы, у меня вообще всё зависло бы.

Так что к этому нужно относится внимательнее. А для того, чтобы назначить приоритет прерыванию, есть функция SetIRQ_Priority(uint8_t Priority), в качестве аргумента передаётся число от 0 до 15. Так-то их 240, но силу имеют только первые четыре бита, поэтому ограничиваются 16-ю уровнями. Чем меньше число, тем выше приоритет. В библиотеке прерывание от таймера имеет уровень приоритета равный 5.

Примеры.

Таймер TIM7. Работа через свою функцию прерывания.

TIM_Base    myTIM7(TIM7);                                // Объявляем объект myTIM7

myTIM7.SetCallbackIRQ(myTIM_ISR7);                       // Назначаем обработчик прерывания
myTIM7.Init(8399, 19999);                                // Счётчик тактируется частотой 10КГц. Прерывание каждые две секунды
myTIM7.Start();                                          // Запускаем таймер

// Функция, выполняемая при возникновении прерывания
void myTIM_ISR7(void)
{
  myUART.print("TIM7 Function Interrupt - ");
  myUART.println(millis() / 1000);
}

Здесь всё просто и понятно из комментариев к коду. Можно убедиться, что таймер срабатывает каждые две секунды:

Пример работы прерываний таймера.

Это то, что выплёвывается в UART.

Таймер TIM6. Работа по флагу.

TIM_Base    myTIM6(TIM6);                                 // Объявляем объект myTIM6

myTIM6.InitSec(3);                                        // Инициализируем таймер на 3 секунды
myTIM6.SetOPM(true);                                      // Врубаем функцию OPM. Режим одного импульса
myTIM6.Start();                                           // Запускаем таймер

bool Togle = false;                                       // Флаг-"триггер"

while(1)                                                  // В цикле - способ обработки прерываний через флаги
{
  bool interruptOccured = myTIM6.getInterruptFlag();      // Считываем флаг
  if (interruptOccured == true)                           // Если флаг от TIM6 поднят
  {
    Led.digitalTogglePin();                               // Переключаем светодиод
    myTIM6.clearInterruptFlag();                          // Сбрасываем флаг от TIM6

    if(Togle)                                             // Если триггер включён
      myTIM6.InitSec(3);                                  // Переинициализируем таймер на 3 секунды
    else                                                  // Если триггер выключен
      myTIM6.InitSec(1);                                  // Переинициализируем таймер на 1 секунду

    Togle = !Togle;                                       // Переключаем триггер
    myTIM6.Start();                                       // Заново запускаем таймер
  }
}

Здесь всё гораздо интереснее. Функцией myTIM6.SetOPM(true) я загоняю таймер TIM6 в режим одного импульса. Т.е. таймер отрабатывает задержку, вызывает прерывание и останавливается. Прерывание поднимает флаг, и в цикле я его проверяю. Если прерывание произошло, переключаем светодиод, сбрасываем флаг прерывания. Также здесь введена переменная-триггер, она нужна, чтобы знать по какой ветке пускать обработку в зависимости от состояния этой переменной. При инициализации она равна false, что приведёт к инициализации таймера на одну секунду. После чего триггер перекидывается в противоположное значение, и таймер запускается заново. Теперь он отработает одну секунду и поднимет флаг.

Но теперь у нас триггер равен true, что приведёт к повторной инициализации таймера, но уже на 3 секунды. Так как у нас таймер отрабатывает то одну секунду, то три, время включенного и выключенного состояния светодиода будет разным. У меня светодиод подключен к VCC и на вывод МК, и время выключенного состояния будет составлять одну секунду, а время включенного состояния три секунды. У тех, у кого светодиод подключен к выводу МК и GND, будет всё в точности до наоборот.

Список доступных функций. С очень кратким примечанием:

TIM_Base(TIM_TypeDef* timerHandle);                     // Конструктор
virtual ~TIM_Base();
void    Init(uint16_t Prescaler, uint16_t Counter);     // Инициализация прямой задачей предделителя и счётчика
uint8_t InitSec(uint16_t Seconds);                      // Задаём в секундах от 1 до 6
uint8_t InitMS(uint16_t Millis);                        // Задаём в миллисекундах от 1 до 65535
uint8_t InitUS(uint16_t Micros);                        // Задаём в микросекундах от 10 до 65535
void    Start(void);                                    // Запускаем таймер
void    Stop(void);                                     // Останавливаем таймер
void    SetOPM(bool OPM);                               // Включаем режим одного импульса
void    SetIRQ_Priority(uint8_t Priority);              // Устанавливаем приоритет прерывания
void    SetCallbackIRQ(callback_IRQ ExternIT = NULL);   // Устанавливаем свой обработчик прерывания

Внутренне описание функций приводить не буду. Там слишком всё просто.

Примечание: Так как следующие библиотеки я буду писать с новой методикой обработки прерываний, старые буду переделывать под неё позже. Но совместимость библиотек существует.

Пример в проекте F407_IT_Shift. Ссылка на Яндекс диск, и WorkDevel. Спасибо Aveal за его библиотеку.

Подписаться
Уведомление о
guest
0 комментариев
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x