Как сделать мигающий светодиод на STM32 без таймеров — просто, надёжно, без лишнего кода
Ты хочешь, чтобы светодиод на твоей STM32 мигал — не чаще, не реже, а ровно с нужной частотой. И ты не хочешь тратить таймеры. Не потому, что они плохие, а потому что ты уже используешь их для чего-то важного: измерения импульсов, управления мотором, обработки PWM. Или просто хочешь понять, как это работает «под капотом», без абстракций.
Вот как я делаю это на практике — без таймеров, без прерываний, без RTOS, без лишней магии. Просто цикл while(1) и чистый подсчёт тактов. И это работает. Надёжно. И не грузит процессор.
Почему именно «blink without delay»?
Этот подход пришёл из Arduino — там он спасает от блокировки кода функцией delay(). На STM32 он не просто полезен — он необходим, если ты хочешь, чтобы микроконтроллер оставался «живым»: отвечал на кнопки, обновлял дисплей, слушал UART. Если ты используешь HAL_Delay() или delay() — ты просто засыпаешь процессор. И всё, что должно работать параллельно, перестаёт работать.
Ты не хочешь, чтобы твой датчик температуры не обновлялся, потому что светодиод моргает. Ты хочешь, чтобы всё работало одновременно. И это возможно — если ты перестанешь ждать, а начнёшь спрашивать: «а сколько времени прошло?»
Как это работает — пошагово
Всё сводится к трём вещам:
- Запоминаешь момент, когда светодиод включился (или выключился).
- В каждом цикле проверяешь, сколько времени прошло с того момента.
- Если прошло достаточно — переключаешь состояние и запоминаешь новое время.
Нет таймеров. Нет прерываний. Нет очередей. Только переменные и сравнение.
Пример кода — прямо сейчас
Предположим, у тебя светодиод подключён к 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. Но если ты хочешь понять, как это работает без ОС — используй этот подход как учебный пример.
Как лучше сделать — практические рекомендации
- Всегда используй
HAL_GetTick()для задач с интервалами больше 1 мс. Это стандарт для STM32, и он работает. - Если тебе нужно мигать быстрее — например, 100 Гц (10 мс), это тоже работает.
HAL_GetTick()обновляется каждые 1 мс, но ты можешь проверять разницу каждые 10 мс — и всё будет точно. - Не используй
delay()ни в каком виде. Он делает твой код «глухим». - Если у тебя несколько светодиодов — вынеси логику в отдельную функцию. Например,
blink_update(), и вызывай её в цикле. Тогда ты можешь управлять десятком LED с одним и тем же кодом. - Если ты хочешь, чтобы светодиод мигал с плавным затуханием — это уже не «blink without delay», а PWM. Тогда используй таймер. Но для простого мигания — не надо.
- Тестируй на реальной плате. Не доверяй симуляторам. Время в симуляторе может идти не так, как в железе.
Итог — что делать прямо сейчас
Если ты хочешь, чтобы светодиод мигал на STM32 без таймеров — сделай так:
- Используй
HAL_GetTick(). - Запоминай время последнего переключения.
- В цикле
while(1)проверяй: прошло ли нужное время. - Если прошло — переключай светодиод и обновляй время.
- Никогда не используй
HAL_Delay()в этом цикле.
Это работает. Это надёжно. Это не грузит процессор. И ты можешь добавить в этот же цикл чтение кнопки, обновление дисплея, обработку данных — всё будет работать одновременно.
Ты не должен знать, как работает таймер. Ты не должен настраивать регистры. Ты не должен бояться переполнения. Ты просто спрашиваешь: «а сколько времени прошло?» — и действуешь.
Это и есть суть «blink without delay». Не магия. Не сложный алгоритм. Просто дисциплина: не ждать — проверять.
Сделай так — и ты больше никогда не будешь использовать delay().
Информация в этой статье носит ознакомительный характер. При разработке промышленных или безопасностно-критических систем всегда проверяй поведение на реальном оборудовании и консультируйся с инженером по встроенным системам.
