STM32 ОБРАБОТКА КНОПОК В ЦЫКЛЕ

STM32F030K6 — Тест GPIO: 8 кнопок → 6 светодиодов (опрос в цикле, debounce, toggle)
Цель. Базовый тест платы: проверяем входы/выходы без прерываний и RTOS. Восемь кнопок (PB0, PB1, PB3–PB7, PF0) управляют шестью светодиодами (PA8, PA9, PA10, PA11, PA12, PA15). Логика — toggle с жёстким антидребезгом (по времени), удобна для проверки «проводом».
Поведение кнопок
- PB0..PB6 — переключают (toggle) свой светодиод.
- PB7 — инвертирует все светодиоды.
- PF0 — тоже инвертирует все светодиоды (можно поменять на «погасить всё», если нужно).
Соответствие пинов
| Кнопка (порт/пин) | Светодиод (порт/пин) | Комментарий |
|---|---|---|
| PB0 | PA8 | индивидуальный toggle |
| PB1 | PA9 | индивидуальный toggle |
| PB3 | PA10 | индивидуальный toggle |
| PB4 | PA11 | индивидуальный toggle |
| PB5 | PA12 | индивидуальный toggle |
| PB6 | PA15 | индивидуальный toggle |
| PB7 | PA8..PA12, PA15 | инвертирует все |
| PF0 | PA8..PA12, PA15 | инвертирует все |
Электрика (предполагаемая)
- Кнопки — активный ноль: в покое HIGH, при замыкании — LOW. Для простых тестов можно включить внутренние подтяжки к VCC.
- Светодиоды: анод → пин PAx через резистор 220–1кΩ, катод → GND.
CubeMX / STM32CubeIDE: как быстро собрать «чистый» проект
1) Новый проект
- MCU: STM32F030K6Tx. Toolchain/IDE: STM32CubeIDE.
- RCC/Clock: SYSCLK = HSI 8 МГц, без PLL.
- GPIO:
- PA8,9,10,11,12,15 — Output Push-Pull, Low speed, No pull.
- PB0,1,3,4,5,6,7 и PF0 — Input. Для тестов удобно сразу поставить Pull-up.
- NVIC: EXTI не настраиваем (тут опрос в цикле).
- Middleware: ничего (никаких FreeRTOS, CMSIS-RTOS2, CMSIS-DSP/NN).
- Project Manager → Code Generator:
- «Generate peripheral initialization as a pair of '.c/.h' files per peripheral» — по желанию.
- «Keep User Code when re-generating» — включить.
- Project Manager → Advanced/Linker Settings:
- Minimum Heap Size =
0(если без malloc) или0x80. - Minimum Stack Size =
0x200(512 B) для HAL + опроса.
- Minimum Heap Size =
- Сгенерировать проект.
2) Если случайно «надергало» лишних пакетов (симптомы: arm_math.h, cmsis_os2.h и пр.)
- В дереве проекта
Drivers/CMSIS/NNиDrivers/CMSIS/RTOS2— удалить или Exclude from Build. - В .ioc → Software Packs убедиться, что RTOS/DSP/NN не выбраны.
3) RAM overflow: «section ._user_heap_stack will not fit in region RAM»
- Это 4 КБ SRAM у F030. Уменьшаем Heap/Stack, как выше.
- Проверьте, что не подключены тяжелые middleware и что printf без float (используется
--specs=nano.specs).
Код (main.c) — опрос, toggle, антидребезг
Жёсткий антидребезг по времени (по HAL_GetTick()). Порог по умолчанию — 25 мс. Для «крокодила с проводом» можно поднять до 40–60 мс. Внутренние подтяжки включаются директивой USE_INTERNAL_PULLUP.
/* HAL demo: 8 кнопок (active-low) → Toggle LED logic + strong debounce (STM32F030K6T6) */ #include "main.h" /* --- Прототипы --- */ void SystemClock_Config(void); static void MX_GPIO_Init(void); /* --- Настройки --- */ #define DEBOUNCE_MS 25 /* устойчивость низкого уровня для фиксации клика */ #define USE_INTERNAL_PULLUP 1 /* 1 = включить PULLUP для PBx/PF0 (удобно для «провода») */ #define LEDS_MASK (GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_15) /* --- Карта кнопок (PB0..PB7, PF0) --- */ typedef struct { GPIO_TypeDef *port; uint16_t pin; } Btn; static const Btn btns[8] = { {GPIOB, GPIO_PIN_0}, // 0 {GPIOB, GPIO_PIN_1}, // 1 {GPIOB, GPIO_PIN_3}, // 2 {GPIOB, GPIO_PIN_4}, // 3 {GPIOB, GPIO_PIN_5}, // 4 {GPIOB, GPIO_PIN_6}, // 5 {GPIOB, GPIO_PIN_7}, // 6 (ALL TOGGLE) {GPIOF, GPIO_PIN_0} // 7 (ALL TOGGLE по просьбе) }; /* Соответствие PB0..PB6 → LED (PAx) */ static const uint16_t led_of_btn_idx[7] = { GPIO_PIN_8, GPIO_PIN_9, GPIO_PIN_10, GPIO_PIN_11, GPIO_PIN_12, GPIO_PIN_15, 0 /* PB7 служебная */ }; /* --- Вспомогательные --- */ static inline uint8_t raw_is_low(uint8_t idx) { return (HAL_GPIO_ReadPin(btns[idx].port, btns[idx].pin) == GPIO_PIN_RESET); } /* Жёсткий антидребезг: фиксируем устойчивое состояние по таймеру. Возвращает 1 РОВНО ОДИН РАЗ на устойчивый переход HIGH→LOW (клик). */ static uint8_t pressed_falling(uint8_t idx) { static uint8_t last_raw[8] = {1,1,1,1,1,1,1,1}; // считаем, что при старте «не нажато» (HIGH) static uint8_t stable_state[8] = {1,1,1,1,1,1,1,1}; // устойчивое состояние static uint32_t last_change_ms[8] = {0}; // время последней смены «сыра» static uint8_t armed[8] = {0}; // готовность выдать фронт uint32_t now = HAL_GetTick(); uint8_t raw = raw_is_low(idx) ? 0 : 1; // 0=LOW(нажато), 1=HIGH(отпущено) if (raw != last_raw[idx]) { last_raw[idx] = raw; last_change_ms[idx] = now; // замечаем момент изменения } /* Если «сырой» уровень держится неизменным достаточно долго — обновляем устойчивое состояние */ if ((now - last_change_ms[idx]) >= DEBOUNCE_MS && raw != stable_state[idx]) { stable_state[idx] = raw; if (stable_state[idx] == 0) { /* Зафиксирован устойчивый LOW — это наш клик (HIGH→LOW) */ armed[idx] = 1; } } if (armed[idx]) { // выдаём импульс один раз armed[idx] = 0; return 1; } return 0; } static inline void leds_write(uint16_t mask_on) { HAL_GPIO_WritePin(GPIOA, LEDS_MASK, GPIO_PIN_RESET); if (mask_on) HAL_GPIO_WritePin(GPIOA, mask_on, GPIO_PIN_SET); } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); uint16_t leds = 0; while (1) { /* PB0..PB6 — toggle своих LED */ if (pressed_falling(0)) leds ^= led_of_btn_idx[0]; if (pressed_falling(1)) leds ^= led_of_btn_idx[1]; if (pressed_falling(2)) leds ^= led_of_btn_idx[2]; if (pressed_falling(3)) leds ^= led_of_btn_idx[3]; if (pressed_falling(4)) leds ^= led_of_btn_idx[4]; if (pressed_falling(5)) leds ^= led_of_btn_idx[5]; /* PB7 — инвертировать все */ if (pressed_falling(6)) leds ^= LEDS_MASK; /* PF0 — тоже инвертировать все (по твоей задумке) */ if (pressed_falling(7)) leds ^= LEDS_MASK; leds_write(leds); HAL_Delay(1); // период опроса 1 мс } } /* --- Тактирование: HSI 8 МГц --- */ void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) Error_Handler(); RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK) Error_Handler(); } /* --- GPIO --- */ static void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOF_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); /* Светодиоды: PA8,9,10,11,12,15 */ HAL_GPIO_WritePin(GPIOA, LEDS_MASK, GPIO_PIN_RESET); GPIO_InitStruct.Pin = LEDS_MASK; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); ф /* Кнопки PB0,1,3,4,5,6,7 — входы */ GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; #if USE_INTERNAL_PULLUP GPIO_InitStruct.Pull = GPIO_PULLUP; // удобно для теста «проводом» #else GPIO_InitStruct.Pull = GPIO_NOPULL; // если стоят внешние подтяжки #endif HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); /* Кнопка PF0 — вход */ GPIO_InitStruct.Pin = GPIO_PIN_0; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; #if USE_INTERNAL_PULLUP GPIO_InitStruct.Pull = GPIO_PULLUP; #else GPIO_InitStruct.Pull = GPIO_NOPULL; #endif HAL_GPIO_Init(GPIOF, &GPIO_InitStruct); } void Error_Handler(void) { __disable_irq(); while (1) { } }
Пояснения по коду
- DEBOUNCE_MS — время устойчивости уровня LOW для регистрации клика.
- USE_INTERNAL_PULLUP — быстрое включение внутренних подтяжек для всех входов.
- Опрос 1 мс + фильтр по времени даёт стабильную работу даже при «тыкалке проводом».
Диагностика PF0 (если «как будто не работает»)
- На время проверки включить Pull-up на PF0 (в шаблоне это уже делается при
USE_INTERNAL_PULLUP=1). - Убедиться, что PF0 не задействован под HSE (OSC_IN) железом: навесной резонатор/конденсаторы могут тянуть линию.
- Проверить фактический вывод корпуса (не перепутать с PF1/PA0).
- Если стоит «индикаторный диод на входе», он может сажать линию — лучше ставить буфер/транзистор.
Чек-лист «чтобы собралось с первого раза»
- Без RTOS/DSP/NN. Только RCC + GPIO.
- Heap ≤
0x80(или 0), Stack ≈0x200. - HSI 8 МГц, без PLL.
- Входы — Pull-up (для теста), выходы — push-pull.
- Если IDE спрашивает про «иерархический проект» — можно жать Yes, это безвредно.
