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

 

 

m32f030k6t6 pinout 1

Общая структура и назначение

Код предназначен для микроконтроллера STM32F030K6T6 и демонстрирует, как восемь кнопок, подключённых к портам PB0, PB1, PB3–PB7 и PF0, управляют светодиодами, подключёнными к портам PA2, PA4, PA8, PA9, PA10, PA11, PA12, PA15. Это классическая задача для учебных проектов: нажал кнопку — зажёг светодиод, отпустил — погасил. Всё делается без прерываний, через опрос в цикле, что делает код простым и понятным, но не самым эффективным для реальных приложений с высокой нагрузкой.

Настройка системы: Частота и порты

Сначала настраивается системная частота. Используется внутренний генератор HSI на 8 МГц, что достаточно для простых задач. Это стандартный выбор для начинающих, так как не требует внешних кристаллов. Затем настраиваются порты GPIO:

  • Кнопки: Пины PB0, PB1, PB3–PB7 и PF0 конфигурируются как входы с отсутствием программной подтяжки (GPI O_NOPULL). Это означает, что предполагается наличие внешних подтяжных резисторов, обычно к питанию (VCC), чтобы в покое кнопка давала высокий уровень (HIGH), а при нажатии — низкий (LOW). Если внешних резисторов нет, пин может "плавать", что приведёт к ошибкам, поэтому важно проверить схему.
  • Светодиоды: Пины PA2, PA4, PA8–PA12, PA15 настраиваются как выходы с двухтактным управлением (push-pull). Светодиоды зажигаются, когда на пин подаётся высокий уровень (GPI O_PIN_SET), и гаснут при низком (GPI O_PIN_RESET). Предполагается, что анод светодиода подключён к пину, а катод — к земле через резистор.

Логика обработки кнопок: Опрос и дребезг

Основная работа происходит в бесконечном цикле while (1) в функции main. Здесь используется метод опроса (polling): микроконтроллер постоянно проверяет состояние каждой кнопки через функцию HAL_GPI O_ReadPin. Это проще, чем прерывания, но требует больше процессорного времени, особенно если кнопок много.

Чтобы справиться с дребезгом контактов (когда кнопка при нажатии или отпускании даёт кратковременные ложные сигналы), используется программный метод. Вот как это работает:

  • Для каждой кнопки хранится её предыдущее состояние (prev_state) и счётчик дребезга (debounce_count).
  • Если текущее состояние отличается от предыдущего, счётчик увеличивается. Если разница сохраняется несколько итераций (5, как задано в DEBOUNCE_THRESHOLD), считается, что состояние стабильно, и счётчик сбрасывается.
  • При стабильном изменении состояния: если кнопка нажата (LOW), светодиод включается, если отпущена (HIGH) — выключается.

Задержка Delay(1000) после опроса всех кнопок добавляет паузу примерно в 1 мс (при 8 МГц), чтобы снизить частоту опроса и дать время на стабилизацию сигнала. Это простое, но эффективное решение для подавления дребезга, хотя для точности лучше использовать таймер.

Важные моменты и рекомендации

  • Внешние подтяжки: Код предполагает, что кнопки имеют внешние подтяжные резисторы (обычно 4.7–10 кОм к VCC). Если их нет, добавьте программную подтяжку (GPI O_PULLUP) в настройке пинов, чтобы избежать неопределённого состояния.
  • Скорость реакции: Метод опроса может быть медленным для приложений, где важна мгновенная реакция, например, в играх. Для таких случаев лучше использовать прерывания (EXTI).
  • Энергоэффективность: Постоянный опрос в цикле потребляет больше энергии, чем обработка по прерыванию, что стоит учитывать для батарейных устройств.

Таблица соответствия кнопок и светодиодов:

Кнопка (Порт, Пин)Светодиод (Порт, Пин)
PB0, GPI O_PIN_0 PA8, GPI O_PIN_8
PB1, GPI O_PIN_1 PA9, GPI O_PIN_9
PB3, GPI O_PIN_3 PA10, GPI O_PIN_10
PB4, GPI O_PIN_4 PA11, GPI O_PIN_11
PB5, GPI O_PIN_5 PA12, GPI O_PIN_12
PB6, GPI O_PIN_6 PA15, GPI O_PIN_15
PB7, GPI O_PIN_7 PA2, GPI O_PIN_2
PF0, GPI O_PIN_0 PA4, GPI O_PIN_4

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

/*
main.c
*/
#include "main.h"
#include <string.h>   #define SYSCLK_HZ 8000000UL // Тактовая частота системы: 8 МГц #define BAUDRATE 115200UL // Скорость UART: 115200 бод   // Пин для управления направлением RS485 #define RS485_CONTROL_PIN GPIO_PIN_1 #define RS485_CONTROL_PORT GPIOF   // Буферы #define RX_BUFFER_SIZE 64 char rx_buffer[RX_BUFFER_SIZE]; // Буфер для приёма данных char send_buffer[RX_BUFFER_SIZE] = "madmentat.ru"; // Буфер для отправки по таймеру uint8_t rx_index = 0; char* text_1 = send_buffer; // text_1 указывает на send_buffer   // Прототипы функций void SystemClock_Config(void); void UART_Init(void); void Timer_Init(void); void UART_SendString(char *str); static void MX_GPIO_Init(void); void RS485_Init(void); void RS485_SetTransmitMode(void); void RS485_SetReceiveMode(void);   // Инициализация RS485 void RS485_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOF_CLK_ENABLE(); // Включаем тактирование порта F GPIO_InitStruct.Pin = RS485_CONTROL_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(RS485_CONTROL_PORT, &GPIO_InitStruct); RS485_SetReceiveMode(); // По умолчанию — режим приёма }   // Установка режима передачи для RS485 void RS485_SetTransmitMode(void) { HAL_GPIO_WritePin(RS485_CONTROL_PORT, RS485_CONTROL_PIN, GPIO_PIN_SET); }   // Установка режима приёма для RS485 void RS485_SetReceiveMode(void) { HAL_GPIO_WritePin(RS485_CONTROL_PORT, RS485_CONTROL_PIN, GPIO_PIN_RESET); }   // Инициализация UART void UART_Init(void) { __HAL_RCC_USART1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE();   // Настраиваем PA2 (TX) и PA3 (RX) для альтернативной функции USART1 GPIOA->MODER &= ~(GPIO_MODER_MODER2_Msk | GPIO_MODER_MODER3_Msk); GPIOA->MODER |= (GPIO_MODER_MODER2_1 | GPIO_MODER_MODER3_1); // Режим AF GPIOA->AFR[0] &= ~((0xF << GPIO_AFRL_AFSEL2_Pos) | (0xF << GPIO_AFRL_AFSEL3_Pos)); GPIOA->AFR[0] |= ((1 << GPIO_AFRL_AFSEL2_Pos) | (1 << GPIO_AFRL_AFSEL3_Pos)); // AF1 для USART1   // Настройка UART: 8 бит данных, 1 стоп-бит, без контроля чётности USART1->CR1 = 0; USART1->CR2 = 0; USART1->BRR = SYSCLK_HZ / BAUDRATE; // Делитель для скорости USART1->CR1 |= (USART_CR1_UE | USART_CR1_TE | USART_CR1_RE | USART_CR1_RXNEIE); // Включаем UART, TX, RX и прерывание по приёму   // Включаем прерывание в NVIC NVIC_EnableIRQ(USART1_IRQn); }   // Отправка строки через UART void UART_SendString(char *str) { RS485_SetTransmitMode(); while (*str) { while (!(USART1->ISR & USART_ISR_TXE)) {} // Ждём, пока регистр передатчика освободится USART1->TDR = *str++; } while (!(USART1->ISR & USART_ISR_TC)) {} // Ждём завершения передачи RS485_SetReceiveMode(); }   // Инициализация таймера TIM3 void Timer_Init(void) { __HAL_RCC_TIM3_CLK_ENABLE(); TIM3->PSC = (SYSCLK_HZ / 1000) - 1; // Предделитель для частоты 1 кГц TIM3->ARR = 1000 - 1; // Период 1 секунда TIM3->DIER |= TIM_DIER_UIE; // Разрешаем прерывание по обновлению NVIC_EnableIRQ(TIM3_IRQn); TIM3->CR1 |= TIM_CR1_CEN; // Включаем таймер }   // Обработчик прерывания таймера TIM3 void TIM3_IRQHandler(void) { if (TIM3->SR & TIM_SR_UIF) { TIM3->SR &= ~TIM_SR_UIF; // Сбрасываем флаг прерывания UART_SendString(text_1); // Отправляем текущее сообщение UART_SendString("\r\n"); } }   // Обработчик прерывания UART (приём данных) void USART1_IRQHandler(void) { if (USART1->ISR & USART_ISR_RXNE) { // Если данные приняты char received = USART1->RDR; // Читаем принятый байт (char, а не char*) if (received == '\n') { // Если получен символ конца строки rx_buffer[rx_index] = '\0'; // Завершаем строку в буфере strcpy(send_buffer, rx_buffer); // Копируем принятое сообщение в send_buffer text_1 = send_buffer; // Указываем text_1 на send_buffer RS485_SetTransmitMode(); // Переключаем в режим передачи UART_SendString("ECHO: "); // Отправляем метку UART_SendString(rx_buffer); // Отправляем принятые данные UART_SendString("\r\n"); // Добавляем перевод строки RS485_SetReceiveMode(); // Возвращаем в режим приёма rx_index = 0; // Сбрасываем индекс буфера } else { if (rx_index < RX_BUFFER_SIZE - 1) { // Проверяем, не переполнен ли буфер rx_buffer[rx_index++] = received; // Добавляем байт в буфер } else { rx_index = 0; // Если буфер переполнен, сбрасываем } } } }   // Основная функция int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); // Инициализация GPIO (если нужно для других пинов) UART_Init(); RS485_Init(); Timer_Init();   while (1) { // Основной цикл пуст, работа выполняется в прерываниях } }   // Настройка системной частоты void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};   RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI; // Используем HSI RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE; // PLL не используется HAL_RCC_OscConfig(&RCC_OscInitStruct);   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; HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0); }   // Инициализация GPIO (если нужно для других пинов) static void MX_GPIO_Init(void) { // Здесь можно добавить настройку дополнительных пинов, если потребуется }   // Обработчик ошибок void Error_Handler(void) { __disable_irq(); while (1) {} }   #ifdef USE_FULL_ASSERT void assert_failed(uint8_t *file, uint32_t line) { } #endif