Как сделать мигающий светодиод на STM32 без таймеров — просто, надёжно, без лишнего кода

Как сделать мигающий светодиод на STM32 без таймеров — просто, надёжно, без лишнего кода

Ты хочешь, чтобы светодиод на твоей STM32 мигал — не чаще, не реже, а ровно с нужной частотой. И ты не хочешь тратить таймеры. Не потому, что они плохие, а потому что ты уже используешь их для чего-то важного: измерения импульсов, управления мотором, обработки PWM. Или просто хочешь понять, как это работает «под капотом», без абстракций.

Вот как я делаю это на практике — без таймеров, без прерываний, без RTOS, без лишней магии. Просто цикл while(1) и чистый подсчёт тактов. И это работает. Надёжно. И не грузит процессор.

Этот подход пришёл из Arduino — там он спасает от блокировки кода функцией delay(). На STM32 он не просто полезен — он необходим, если ты хочешь, чтобы микроконтроллер оставался «живым»: отвечал на кнопки, обновлял дисплей, слушал UART. Если ты используешь HAL_Delay() или delay() — ты просто засыпаешь процессор. И всё, что должно работать параллельно, перестаёт работать.

Ты не хочешь, чтобы твой датчик температуры не обновлялся, потому что светодиод моргает. Ты хочешь, чтобы всё работало одновременно. И это возможно — если ты перестанешь ждать, а начнёшь спрашивать: «а сколько времени прошло?»

Как это работает — пошагово

Всё сводится к трём вещам:

  1. Запоминаешь момент, когда светодиод включился (или выключился).
  2. В каждом цикле проверяешь, сколько времени прошло с того момента.
  3. Если прошло достаточно — переключаешь состояние и запоминаешь новое время.

Нет таймеров. Нет прерываний. Нет очередей. Только переменные и сравнение.

Пример кода — прямо сейчас

Предположим, у тебя светодиод подключён к GPIOC, пин PC13 (это стандартный LED на большинстве STM32F1 и STM32F4 отладочных плат). У тебя тактовая частота — 72 МГц. Ты хочешь, чтобы LED мигал раз в секунду: 500 мс включён, 500 мс выключен.

Вот как это выглядит в коде:

#include "stm32f1xx_hal.h"

// Определяем пин светодиода
#define LED_PIN     GPIO_PIN_13
#define LED_PORT    GPIOC

// Время в тактах: 72 МГц = 72 000 000 тактов в секунду
// 500 мс = 36 000 000 тактов
#define BLINK_PERIOD 36000000UL

// Переменные для отслеживания времени
uint32_t lastToggleTime = 0;
bool ledState = false;

void SystemClock_Config(void);
void MX_GPIO_Init(void);

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();

    // Устанавливаем светодиод в начальное состояние
    HAL_GPIO_WritePin(LED_PORT, LED_PIN, ledState ? GPIO_PIN_SET : GPIO_PIN_RESET);

    // Запоминаем текущее время
    lastToggleTime = HAL_GetTick() * (SystemCoreClock / 1000);

    while (1)
    {
        // Получаем текущее время в тактах
        uint32_t currentTime = HAL_GetTick() * (SystemCoreClock / 1000);

        // Проверяем, прошло ли достаточно времени
        if ((currentTime - lastToggleTime) >= BLINK_PERIOD)
        {
            // Переключаем светодиод
            ledState = !ledState;
            HAL_GPIO_WritePin(LED_PORT, LED_PIN, ledState ? GPIO_PIN_SET : GPIO_PIN_RESET);

            // Обновляем время последнего переключения
            lastToggleTime = currentTime;
        }

        // Здесь можно делать что угодно — проверять кнопки, читать датчики, обновлять экран
        // Никаких задержек — всё работает параллельно
    }
}

Ты можешь спросить: «Почему HAL_GetTick() умножается на SystemCoreClock / 1000

Потому что HAL_GetTick() возвращает время в миллисекундах, а нам нужно время в тактах процессора. Если частота 72 МГц, то за 1 мс проходит 72 000 тактов. Значит, за 1 мс — 72 000, за 500 мс — 36 000 000. Это и есть BLINK_PERIOD.

Но если ты используешь другую частоту — например, 168 МГц — просто пересчитай: 168 000 000 / 2 = 84 000 000 тактов для 500 мс. И всё.

А если частота неизвестна? Или меняется?

Хороший вопрос. В реальном проекте ты не всегда знаешь, какая частота у процессора. Может, ты используешь PLL, может, переключаешь режимы энергосбережения.

Тогда есть два варианта:

Вариант 1 — используй HAL_GetTick() как есть (рекомендуется)

Вместо подсчёта тактов — считай в миллисекундах. HAL_GetTick() автоматически обновляется каждые 1 мс (если ты не трогал HAL_IncTick() в SysTick_Handler).

Перепиши код так:

#define BLINK_PERIOD_MS 500

uint32_t lastToggleTime = 0;
bool ledState = false;

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();

    HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET);
    lastToggleTime = HAL_GetTick();

    while (1)
    {
        uint32_t currentTime = HAL_GetTick();

        if ((currentTime - lastToggleTime) >= BLINK_PERIOD_MS)
        {
            ledState = !ledState;
            HAL_GPIO_WritePin(LED_PORT, LED_PIN, ledState ? GPIO_PIN_SET : GPIO_PIN_RESET);
            lastToggleTime = currentTime;
        }

        // ... остальной код
    }
}

Это проще, надёжнее и не зависит от частоты. И работает даже если ты изменишь тактовую частоту в рантайме — HAL_GetTick() продолжит считать правильно.

Единственное ограничение: HAL_GetTick() — это 32-битное число, которое переполняется примерно раз в 49 дней. Но для мигания светодиода это не проблема — разница между двумя значениями всегда считается правильно, даже при переполнении.

Вариант 2 — если тебе критично точное время и ты не доверяешь HAL_GetTick()

Например, ты делаешь синхронизацию с внешним устройством, и тебе нужно точно 500.0 мс, а не 500±2 мс. Тогда используй счетчик тактов — но только если ты уверен в частоте и не меняешь её в рантайме.

В этом случае используй __HAL_RCC_GET_FREQ(RCC_SYSCLK) для получения текущей частоты:

#include "stm32f1xx_hal_rcc.h"

uint32_t getSysClockHz(void)
{
    return __HAL_RCC_GET_FREQ(RCC_SYSCLK);
}

// В main():
uint32_t tickFreq = getSysClockHz(); // Получаем текущую частоту
uint32_t blinkPeriodTicks = (tickFreq / 1000) * 500; // 500 мс в тактах

Но помни: если ты потом переключишь PLL — эта величина станет неверной. Поэтому если ты хочешь гибкости — используй HAL_GetTick().

Сравнение подходов

Критерий Подсчёт тактов HAL_GetTick()
Точность Высокая (до 1 такта) Средняя (±1 мс)
Зависимость от частоты Да — если частота меняется — всё ломается Нет — работает с любой частотой
Сложность кода Выше — нужно считать такты, учитывать частоту Ниже — просто сравниваешь миллисекунды
Портативность Низкая — код привязан к конкретному MCU Высокая — работает на всех STM32
Нагрузка на CPU Минимальная — одна операция вычитания Минимальная — та же операция
Когда использовать Только если нужна микросекундная точность и частота фиксирована Во всех остальных случаях — 95% проектов

Практически в каждом проекте я выбираю второй вариант — HAL_GetTick(). Он проще, надёжнее и не требует от тебя постоянного контроля частоты. Если тебе нужно мигать раз в 100 мс, 2 секунды или 10 минут — всё работает одинаково.

Частые ошибки — и как их избежать

  • Ошибка 1: Используешь if (currentTime >= lastToggleTime + BLINK_PERIOD) — это приводит к ошибкам при переполнении uint32_t. Исправление: Всегда пиши if (currentTime - lastToggleTime >= BLINK_PERIOD). Вычитание работает корректно даже при переполнении.
  • Ошибка 2: Не инициализируешь lastToggleTime. Если оно содержит мусор — первое сравнение может сработать сразу. Исправление: Всегда инициализируй переменную в начале main().
  • Ошибка 3: Забыл, что HAL_GetTick() возвращает время с момента вызова HAL_Init(). Если ты вызываешь его до инициализации — получишь ноль. Исправление: Инициализируй lastToggleTime только после HAL_Init().
  • Ошибка 4: Делаешь переключение в цикле без проверки времени — светодиод моргает как сумасшедший. Исправление: Проверяй время до переключения, а не после.
  • Ошибка 5: Используешь delay() где-то в другом месте кода — и всё ломается. Исправление: Убери все HAL_Delay() и delay(). Если тебе нужно подождать — используй тот же механизм: запомни время и жди, пока не пройдёт нужный интервал.

Когда что выбрать — сценарии

  • Ситуация 1: Ты делаешь простой проект — мигает светодиод, читает кнопку, отправляет данные по UART. Выбирай: HAL_GetTick(). Просто, надёжно, не надо думать о частотах.
  • Ситуация 2: Ты пишешь код для промышленного устройства, где нужно синхронизировать мигание с внешним сигналом с точностью до 10 мкс. Выбирай: Подсчёт тактов, но только если частота фиксирована и не меняется. Иначе — используй таймер.
  • Ситуация 3: Ты хочешь мигать с разными частотами — например, 1 Гц, 2 Гц, 5 Гц — в зависимости от режима работы. Выбирай: HAL_GetTick() + переменная currentBlinkPeriod. Меняй её в рантайме — всё продолжит работать.
  • Ситуация 4: Ты используешь RTOS (FreeRTOS, CMSIS-RTOS). Выбирай: Таймеры или таймеры RTOS. Но если ты хочешь понять, как это работает без ОС — используй этот подход как учебный пример.

Как лучше сделать — практические рекомендации

  1. Всегда используй HAL_GetTick() для задач с интервалами больше 1 мс. Это стандарт для STM32, и он работает.
  2. Если тебе нужно мигать быстрее — например, 100 Гц (10 мс), это тоже работает. HAL_GetTick() обновляется каждые 1 мс, но ты можешь проверять разницу каждые 10 мс — и всё будет точно.
  3. Не используй delay() ни в каком виде. Он делает твой код «глухим».
  4. Если у тебя несколько светодиодов — вынеси логику в отдельную функцию. Например, blink_update(), и вызывай её в цикле. Тогда ты можешь управлять десятком LED с одним и тем же кодом.
  5. Если ты хочешь, чтобы светодиод мигал с плавным затуханием — это уже не «blink without delay», а PWM. Тогда используй таймер. Но для простого мигания — не надо.
  6. Тестируй на реальной плате. Не доверяй симуляторам. Время в симуляторе может идти не так, как в железе.

Итог — что делать прямо сейчас

Если ты хочешь, чтобы светодиод мигал на STM32 без таймеров — сделай так:

  1. Используй HAL_GetTick().
  2. Запоминай время последнего переключения.
  3. В цикле while(1) проверяй: прошло ли нужное время.
  4. Если прошло — переключай светодиод и обновляй время.
  5. Никогда не используй HAL_Delay() в этом цикле.

Это работает. Это надёжно. Это не грузит процессор. И ты можешь добавить в этот же цикл чтение кнопки, обновление дисплея, обработку данных — всё будет работать одновременно.

Ты не должен знать, как работает таймер. Ты не должен настраивать регистры. Ты не должен бояться переполнения. Ты просто спрашиваешь: «а сколько времени прошло?» — и действуешь.

Это и есть суть «blink without delay». Не магия. Не сложный алгоритм. Просто дисциплина: не ждать — проверять.

Сделай так — и ты больше никогда не будешь использовать delay().

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

Radio-Blog.ru