Нужно моргнуть светодиодом на STM32, но хочется обойтись без HAL_Delay и таймеров? Это частая задача, когда пишешь первый проект или отлаживание не позволяет использовать системные таймеры. Расскажу, как реализовать простое мигание с точной задержкой, используя только циклы и знание частоты ядра.
Что значит «без таймеров»
Под этим обычно понимают отказ от аппаратных таймеров (TIM1, TIM2 и т.д.) и от системного таймера SysTick, который использует HAL_Delay. Остаются только программные задержки — циклы, которые выполняют бесполезные операции заданное количество тактов.
Как работает программная задержка
Идея проста: мы знаем частоту ядра (например, 72 МГц для STM32F103), значит, каждая инструкция выполняется за определённое время. Если выполнить много инструкций подряд, получим нужную паузу. Главное — не дать компилятору оптимизировать пустой цикл.
volatile uint32_t i; for (i = 0; i < 500000; i++) { // ничего не делаем }
Ключевое слово volatile заставляет компилятор выполнять каждую итерацию, а не выбросить цикл как бесполезный.
Расчёт длительности задержки
Если ядро работает на 72 МГц, один такт занимает около 13,9 нс. Но каждая инструкция в цикле занимает не один такт — нужно учитывать конвейер и доступ к памяти. На практике одна итерация цикла for занимает примерно 3–5 тактов (зависит от оптимизаций и архитектуры).
Для грубой задержки в 500 мс на 72 МГц:
void delay_ms(uint32_t ms) { uint32_t i; while (ms--) { for (i = 0; i < 8000; i++) { __asm("nop"); } } }
Здесь 8000 итераций дают примерно 1 мс при 72 МГц. Точное значение подбирается экспериментально — замеряйте осциллографом или логическим анализатором.
Реализация мигания
Теперь соберём полноценный код для мигания светодиодом на выводе PC13 (как на многих отладочных платах):
#include "stm32f1xx.h" void delay_ms(uint32_t ms) { uint32_t i; while (ms--) { for (i = 0; i < 8000; i++) { __asm("nop"); } } } int main(void) { // Включаем тактирование порта C RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; // Настраиваем PC13 как выход push-pull, максимальная частота 2 МГц GPIOC->CRH &= ~(0xF << 20); // Очищаем биты CNF13[1:0] и MODE13[1:0] GPIOC->CRH |= (0x2 << 20); // MODE13 = 10 (Output mode, max speed 2 MHz) while (1) { GPIOC->ODR ^= GPIO_ODR_ODR13; // Инвертируем состояние PC13 delay_ms(500); // Пауза 500 мс } }
Этот код не использует HAL, только прямую работу с регистрами. Если вы работаете с HAL, замените настройку GPIO на соответствующий вызов, но суть задержки остаётся той же.
Подбор количества итераций под вашу частоту
Вот таблица примерных значений для разных частот ядра (задержка 1 мс):
| Частота ядра | Итерации цикла (примерно) | Примечание |
|---|---|---|
| 16 МГц (HSI) | ~2000 | Внутренний резистор без настройки |
| 48 МГц (STM32F0) | ~6000 | |
| 72 МГц (STM32F1) | ~8000 | |
| 80 МГц (STM32L4) | ~10000 | |
| 168 МГц (STM32F4) | ~21000 |
Эти значения — отправная точка. Реальная задержка зависит от оптимизаций компилятора, наличия кэша и других факторов. Всегда проверяйте осциллографом.
Как избежать оптимизации цикла
Компилятор может удалить пустой цикл, если не принять меры:
- Используйте
volatileдля переменной цикла. - Вставляйте
__asm("nop")внутрь цикла — это гарантирует выполнение каждой итерации. - Отключите оптимизацию для функции задержки с помощью прагмы (не рекомендуется для всего проекта).
#pragma GCC push_options #pragma GCC optimize ("O0") void delay_ms(uint32_t ms) { ... } #pragma GCC pop_options
Когда такой подход уместен
Программные задержки хороши для:
- Быстрого прототипирования, когда лень настраивать таймеры.
- Обучения — чтобы понять, как работает тактирование и исполнительный конвейер.
- Задач, где точность задержки не критична (мигание индикатором, опрос кнопки без ребунса).
Но помните: пока процессор крутится в цикле, он не может делать ничего другого. В реальных проектах лучше использовать таймеры и прерывания, чтобы освободить CPU.
Частые ошибки
Не используйте такие задержки в системах с низким энергопотреблением — процессор постояно активен, расходуя энергию. Также помните, что при включении прерываний реальная длительность задержки может увеличиться на время обработки прерывания.
Типичные промахи:
- Забыли включить тактирование порта — светодиод не мигает.
- Не указали
volatile— компилятор удалил цикл. - Неправильно рассчитали количество итераций — мигает слишком быстро или медленно.
- Использовали
HAL_Delayвнутри задержки — получаете непредсказуемое поведение.
Как проверить точность
Подключите осциллограф к выводу со светодиодом и измерьте период. Если он не совпадает с расчётным, подправьте количество итераций. Можно также использовать другой вывод для маркера — переключайте его в начале и конце задержки и смотрите длительность импульса.
Альтернатива: использование SysTick без HAL
Если всё-таки хочется более точную задержку без таймеров, можно настроить SysTick напрямую:
void SysTick_Init(void) { SysTick->LOAD = 72000 - 1; // 1 мс при 72 МГц SysTick->VAL = 0; SysTick->CTRL = SysTick_CTRL_ENABLE_Msk | SysTick_CTRL_CLKSOURCE_Msk; } void delay_ms(uint32_t ms) { while (ms--) { while (!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk)); } }
Это уже ближе к таймеру, но SysTick — часть ядра, а не периферийный таймер. Иногда такой компромисс приемлем.
Итог
Реализовать мигание светодиодом на STM32 без таймеров просто: используйте циклы с volatile и nop, подберите количество итераций под вашу частоту и обязательно проверяйте практикой. Для учебных целей и быстрой отладки этого достаточно. Для рабочих проектов переходите на таймеры и прерывания — это сэкономит ресурсы процессора и упростит код.
