STM32 – Частина 5: Таймери — основи та приклади використання

28.05.2025 4 хвилин Автор: animator404

Таймери — один з найважливіших апаратних модулів мікроконтролерів STM32. У цій статті розглянуто фундаментальні принципи роботи таймерів STM32, їх конфігурацію та практичні сценарії використання. Матеріал буде корисний як новачкам, так і досвідченим розробникам вбудованих систем. Ви дізнаєтесь, як за допомогою HAL (Hardware Abstraction Layer) налаштувати таймери у STM32CubeIDE, створювати переривання таймерів, реалізувати точні затримки без використання HAL_Delay() та генерувати ШІМ-сигнали (PWM) для керування світлодіодами, моторами або іншими пристроями.

Таймери​

Таймери — без перебільшення є однією з ключових складових мікроконтролерів (звісно, після самого ядра). В їхній основі лежить звичайний лічильник, який відлічує до заданого значення, після чого обнуляється (наприклад, до нуля) і починає відлік заново. Здається, нічого складного — адже ми могли б реалізувати таку саму логіку у звичайному циклі for, просто збільшуючи змінну.

Так, це можливо, але в такому випадку ядро МК не зможе займатись жодними іншими (більш пріоритетними) задачами, адже буде зайняте цим примітивним відліком. Саме тому використання таймерів дозволяє розвантажити ядро і передати йому важливішу роботу.

Проблема​

Щоб краще відчути цінність таймерів давайте розглянемо простий приклад.

int main()
{
  HAL_Init();
  GPIO_PA4_Init();  //Init LED1

  while (1)
  {
    toggleLed1();
    HAL_Delay(1000);
  }
}

Програма, наведена вище — це проста “блималка”, яка раз на секунду вмикає або вимикає світлодіод. На перший погляд, у цьому коді немає нічого поганого — і це справді так. Проблеми з’являються тоді, коли виникає потреба виконувати інші періодичні завдання з різними інтервалами, відмінними від однієї секунди.

Наприклад, потрібно кожні пів секунди зчитувати дані з датчика температури, а кожні півтори секунди — вмикати або вимикати другий світлодіод на іншому виході. Звісно, можна було б реалізувати програму й таким чином, щоб усе це працювало:

int main()
{
  HAL_Init();
  GPIO_PA4_Init();  //Init LED1
  GPIO_PA11_Init(); //Init LED2

  while (1)
  {
    readTemp();
    HAL_Delay(500);

    readTemp();
    HAL_Delay(500);

    toggleLed1();
    HAL_Delay(500);

    toggleLed2();
  }
}

Оновлений приклад теж працює, але тепер має складнішу логіку. З коду вище не одразу можна зрозуміти чому в циклі while функція readTemp() викликається саме 3 рази чи чому функція toggleLed1() викликається саме після 2-го виклику readTemp().

До того ж приклад вище був ідеально підібраний так, щоб інтервали всіх 3-ох операцій були кратні 500 мілісекундам і тому відносно легко “уживаються” в одному while циклі. Але уявіть не ідеальний приклад: операція зчитування температури має виконуватися раз на 400 мілісекунд, а не 500. В такому разі код буде виглядати так:

int main()
{
  HAL_Init();
  GPIO_PA4_Init();  //Init LED1
  GPIO_PA11_Init(); //Init LED2

  while (1)
  {
    readTemp();
    HAL_Delay(400);
    readTemp();
    HAL_Delay(400);
    readTemp();

    HAL_Delay(200);
    toggleLed1();

    HAL_Delay(200);
    readTemp();

    HAL_Delay(300);
    toggleLed2();

    HAL_Delay(100);
  }
}

Щоб здійснювати зчитування температури кожні 400 мілісекунд, нам довелось вручну проходити код у циклі while та виставляти інші значення затримок не лише перед викликом readTemp(), а й перед усіма іншими функціями. І якщо, скажімо, потрібно змінити інтервал на 300 мс, доведеться знову все перераховувати і виставляти нові значення затримок у відповідних місцях.

А якщо виникне необхідність перемикати світлодіод Led1 не раз на секунду, а кожні 700 мс? А якщо ці інтервали потрібно буде ще й змінювати динамічно — прямо під час роботи пристрою? Наприклад, при натисканні кнопки блимання Led1 має пришвидшитися на 100 мс у порівнянні з режимом, коли кнопка не натиснута. Сподіваюсь, логіка зрозуміла. Програмно реалізувати це все можливо, але доведеться докласти чимало зусиль, постійно узгоджуючи затримки між трьома окремими функціями.

Було б набагато зручніше, якби існував простий інструмент, який дозволяє просто задати інтервал виконання певної функції — без урахування інших частин коду. Саме таку роль і виконують таймери.

P.S.: альтернативним підходом до вирішення цієї задачі могла б стати RTOS із вбудованими механізмами періодичних задач, але це вже зовсім інша історія.

Застосування​

Таймери це окремі апаратні блоки в МК, які працюють окремо від ядра і часто навіть на іншій частоті. Окрім простої функції лічильника вони також можуть:

  • генерувати переривання.

  • генерувати сигнали на пінах (GPIO.

  • керувати моторами (крокові, щіткові, безщіткові).

  • автоматично перезапускати МК у разі несправності (сторожовий* таймер).

  • аналіз вхідних сигналів (тривалість їх пульсів, періодів.

  • і багато інших, які тільки можна уявити.

* Сторожові таймери хоч і називаються таймерами, але все ж відрізняються призначенням від розглянутих в цій статті. Тому розглядати тут їх не будемо

Тобто функціонал таймерів насправді набагато ширший за просто функцію лічильника. Давайте розглянемо кілька прикладів простих застосувань таймерів і почнемо саме з функції лічильника.

Приклад №1 – Лічильник​

Почнемо з самого простого прикладу, який демонструє як запустити таймер в режимі звичайного лічильника. Але спочатку потрібно познайомитись з параметрами, з якими можна запустити таймер:

  • частота таймера (задається дільником – prescaler)

  • число до якого рахувати – period

  • режим лічильника (рахувати вверх чи вниз, тобто наприклад від 0 до 1000 чи від 1000 до 0)

Частота таймера визначає, скільки разів за секунду його лічильник здатен зробити інкремент. Наприклад, якщо вона становить 8 МГц, це означає, що за одну секунду таймер потенційно може нарахувати до 8 мільйонів кроків. Джерелом цієї частоти зазвичай є шина APB (Advanced Peripheral Bus), яка в більшості випадків синхронізується з тактовою частотою ядра мікроконтролера. Тобто, якщо МК працює на 16 МГц і не застосовано дільник, то таймер теж працює на частоті 16 МГц, і теоретично лічильник може дійти до 16 мільйонів за одну секунду.

Проте на практиці такого значення ми не досягнемо, і ось чому. У таймерів є параметр “період”, який визначає, до якого значення лічильник повинен дорахувати перед тим, як скинутись у нуль. Саме цей період і обмежує максимально можливе число, яке ми можемо задати. Наприклад, у 16-бітного таймера це обмеження становить 65 535, а у 32-бітного — понад 4 мільярди. І це абсолютно нормально, адже в більшості випадків такої великої точності й не потрібно.

Отже, розрядність таймера — це просто технічне обмеження, яке визначає його максимальний лічильний діапазон. Коли таймер досягає встановленого в полі “період” значення, він переповнюється і починає рахунок заново.

На прикладі STM32G030F6P6 (8KB/32KB/TSSOP-20) поглянемо на ініціалізацію і запуск таймера:

void example1()
{
    HAL_TIM_Base_Stop(&htim3);
    HAL_TIM_Base_DeInit(&htim3);
    __HAL_RCC_TIM3_CLK_ENABLE();

    htim3.Instance = TIM3;
    htim3.Init.Prescaler = 16000 - 1;   //downclock timer to 1000Hz
    htim3.Init.Period = 6000 - 1;       //count to 6000 (6 sec)
    htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
    HAL_TIM_Base_Init(&htim3);
    HAL_TIM_Base_Start(&htim3);
}

У випадку STM32G030F6P6 частота ядра за замовчуваннями 16 MHz (хоча може працювати і на 64 MHz). Виставивши дільник в 16 000 ми тим самим змусили таймер працювати на частоті 1000 Hz (16MHz / 16k), тобто робити 1000 інкрементів за секунду. А виставивши період в 6 000 змусили рахувати до 6 секунд (6000/1000).

Давайте запустимо цей приклад та поглянемо на значення лічильника таймера в дебаг режимі:

На відео видно як лічильник рахує до 6000 і щоразу починає заново. От тільки користі від такого прикладу не дуже багато — лічильник просто рахує так як ми задали і все. Було б добре якби ми ще могли виконувати потрібну нам дію щоразу коли таймер дорахує до заданого числа. Цього можна досягти запустивши таймер в режимі переривання, як в наступному прикладі.

Приклад №2 – Переривання​

Цей приклад схожий на попередній лише з тією різницею, що коли лічильник досягне заданого періоду таймер згенерує переривання, в яке ми помістимо потрібний нам код – наприклад блимання світлодіоду.

void example2()
{
    HAL_TIM_Base_Stop(&htim3);
    HAL_TIM_Base_DeInit(&htim3);
    __HAL_RCC_TIM3_CLK_ENABLE();

    htim3.Instance = TIM3;
    htim3.Init.Prescaler = 16000 - 1;
    htim3.Init.Period = 2000 - 1;

    HAL_TIM_Base_Init(&htim3);
    HAL_NVIC_SetPriority(TIM3_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(TIM3_IRQn);
    HAL_TIM_Base_Start_IT(&htim3);
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM3)
  {
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_4);
  }
}

В прикладі ми використали той самий дільник, але тепер рахуємо до 2000, тобто до 2 секунд. Також увімкнули переривання для нашого таймеру (TIM3) з найвищим пріоритетом, тобто 0. І що не менш важливо — запустили таймер в режимі переривання через HAL_TIM_Base_Start_IT. А нижче описали код, який буде викликатися на кожне переривання: блимання світлодіоду на піні PA4.

Такий приклад уже більш корисний ніж просто лічильник. Але його можна покращити якщо і саме блимання світлодіоду звалити на плечі таймера. Для цього розглянемо наступний приклад.

Приклад №3 – Режим порівняння​

У попередньому прикладі ми вже вивільнили ресурси ядра МК, делегувавши відлік часу таймеру. І лише коли таймер генерує переривання, то ядру доводиться перервати свою роботу, щоб перемкнути світлодіод. Але можна ще краще — можна і роботу зі світлодіодом делегувати таймеру. Оскільки таймер — це апаратний модуль, він може мати фізичне з’єднання з GPIO, тобто може керувати і LED.

Для цього скористаємось режимом таймера, який називається режимом вихідного порівняння (Output Compare mode). Цей режим має багато підрежимів роботи, які хоч і не сильно між собою відрізняються, але кожен має своє застосування. Наприклад, один з таких підрежимів — це TIM_OCMODE_TOGGLE — перемикання. Тобто ми збираємось використати таймер в режимі порівняння з підрежимом перемикання. Тільки не заплутайтесь.

Але і тут не все так просто. Будь-який таймер не можна підключити до будь-якого GPIO, щоб ним керувати. Лише певні канали певних таймерів можуть бути підключені до певних пінів. Щоб дізнатися який таймер можна підімкнути до якого піна треба звернутися до даташита STM32G030F6P6 (сторінка 35) або до STM32CubeIDE, які нам підкажуть, що світлодіод на піні PA4 може бути підключений до 1-го каналу (CH1) таймера 14 (TIM14).

STM32CubeIDE на допомогу

Приклад виглядає наступним чином:

void example3()
{
   HAL_TIM_Base_Stop(&htim14);
   HAL_TIM_Base_DeInit(&htim14);
   __HAL_RCC_TIM14_CLK_ENABLE();

   htim14.Instance = TIM14;
   htim14.Init.Prescaler = 16000 - 1;       //downclock timer to 1000Hz
   htim14.Init.Period = 5000 - 1;           //count to 5 seconds
   HAL_TIM_OC_Init(&htim14);

   TIM_OC_InitTypeDef htimOcConfig = {0};
   htimOcConfig.OCMode = TIM_OCMODE_TOGGLE; //GPIO Toggle mode
   htimOcConfig.Pulse = 2500;               //do toggle in 2.5s - middle between 5s
   HAL_TIM_OC_ConfigChannel(&htim14, &htimOcConfig, TIM_CHANNEL_1);
   HAL_TIM_OC_Start(&htim14, TIM_CHANNEL_1);
}

void GPIO_PA4_TIM_OC_Init()
{
 __HAL_RCC_GPIOA_CLK_ENABLE();
 GPIO_InitTypeDef GPIO_InitStruct = {0};
 GPIO_InitStruct.Pin = GPIO_PIN_4;
 GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;     //Init GPIO PA4 as Alternate Function
 GPIO_InitStruct.Alternate = GPIO_AF4_TIM14; //Init GPIO PA4 as Alternate Function 4
 HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

Також потрібно ініціалізувати рін PA4 в режимі альтернативної функції AF4 згідно даташиту (чи STM32CubeIDE). Запустимо приклад:

Як видно з прикладу світлодіод перемикається не на 5-ти секундах як можна було подумати з параметру період, а посередині – на 2.5 секундах. Це все із-за нового параметру пульс, який ми задали.

 Приклад №4 – ШІМ​

Останній приклад дуже схожий на попередній і теж вважається режимом порівняння. Тут ми змінюємо яскравість світлодіоду виставляючи в змінну пульс потрібний робочий цикл (duty cycle). Починаємо з 50% (500/1000) і потім збільшуємо на 5% кожні 100 мілісекунд.

Що важливо, частота роботи таймера в режимі ШІМ має бути значно вищою ніж в попередніх прикладах, щоб блимання світлодіоду стало непомітне для людини. Тому дільником виставляємо частоту в 1 MHz. Період ШІМу можна регулювати частотою таймера (тобто дільником) і періодом.

void example4()
{
    HAL_TIM_Base_Stop(&htim14);
    HAL_TIM_Base_DeInit(&htim14);
    __HAL_RCC_TIM14_CLK_ENABLE();

    htim14.Instance = TIM14;
    htim14.Init.Prescaler = 16 - 1; //downclock timer to 1 MHz
    htim14.Init.Period = 1000 - 1;   //count to 1000

    TIM_OC_InitTypeDef htimOcConfig = {0};
    htimOcConfig.OCMode = TIM_OCMODE_PWM1;
    htimOcConfig.Pulse = 500;        //startup duty cycle - 50% (500/1000)

    HAL_TIM_PWM_ConfigChannel(&htim14, &htimOcConfig, TIM_CHANNEL_1);
    HAL_TIM_PWM_Init(&htim14);
    HAL_TIM_PWM_Start(&htim14, TIM_CHANNEL_1);

    while (1)
    {
        htimOcConfig.Pulse = htimOcConfig.Pulse + 50;
        if (htimOcConfig.Pulse >= 1000)
        {
            htimOcConfig.Pulse = 0;
        }
        HAL_TIM_PWM_ConfigChannel(&htim14, &htimOcConfig, TIM_CHANNEL_1);
        HAL_Delay(100);
    }
}

void GPIO_PA4_TIM_OC_Init()
{
  __HAL_RCC_GPIOA_CLK_ENABLE();
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  GPIO_InitStruct.Pin = GPIO_PIN_4;
  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Alternate = GPIO_AF4_TIM14;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

Після налаштування таймера і піна в циклі while змінюємо пульс від 0 до 1000, тим самим регулюємо робочий цикл від 0% до 100%.

Це GIF Зображення

Ось так за допомогою таймера і ШІМ плавно змініюємо яскравість світлодіоду.

Висновок

На перший погляд, таймери у мікроконтролерах можуть здатися чимось простим і не надто корисним — звичайними лічильниками. Проте на практиці вони здатні виконувати завдання не менш ефективно, ніж саме ядро. Окрім описаних варіантів використання, існує ще безліч режимів роботи таймерів та способів їх застосування. На початковому етапі розробки проєктів можна обходитись і без них, але з часом, у міру ускладнення функціоналу пристроїв, їх опанування стає необхідним.

Посилання на першоджерело: https://solderkid-blog.netlify.app/stm32/neopixel

Підписатися
Сповістити про
0 Коментарі
Найстаріші
Найновіше Найбільше голосів
Знайшли помилку?
Якщо ви знайшли помилку, зробіть скріншот і надішліть його боту.