STM32 СВЯЗКА UART - RS485
Собственно, микроконтроллеер мы тут будем использовать семейства STM32, в частности STM32F030K6T6. У нас на производстве он обкатан на платах, управляющих термоусадочным упаковочным оборудованием. Вещь надежная, хорошая, дешманская. Вот, собственно та самая плата, а если быть точнее, то аккуратно слизанный с нее гербер с некоторыми добаботками (покажу, но не поделюсь):
Изолированный трансивер RS485 ISO3082DW у нас используется для обмена данными с HMI панелью DWIN по протоколу MODBUS, но вообще, конечно, такое устроство связи можно использовать как душе угодно (Бога нет, но вы держитеесь). Подключение осуществляется через ноги PA2 - TX, PA3 - RX и еще один пин PF1 - RE, DE. Здесь RE и DE объеденины, эти пины отвечают за режим направления работы приемо-передачи.
Что это вообще такое и зачем тебе?
- Отправляет данные: Каждую секунду твой STM32 будет кричать в линию RS485: "madmenat.ru\r\n". Типа "привет, я живой!".
- Принимает данные: Если кто-то что-то пришлёт, он поймает это в буфер и, как только увидит конец строки (\n), ответит: "ECHO: " плюс то, что прислали. Как попугай, только умнее.
- Управляет RS485: Сам переключается между "говорю" и "слушаю", чтобы не было путаницы на линии. Всё автоматом, как в лучших домах!
И всё это на прерываниях — микроконтроллер не пыхтит зря, а работает как ленивый гений: только когда надо.
Как это настроено? Всё по полочкам
- Частота: 8 МГц, внутренний генератор (HSI). Не быстро, не медленно — в самый раз для старта.
- Пины для UART: PA2 — это выход (TX), PA3 — вход (RX). Они подключены к USART1. Всё по стандарту, ничего выдумывать не надо.
- Пин для RS485: PF1 решает, передаём мы или принимаем. HIGH — говорим, LOW — слушаем. Как выключатель: щёлк, и готово!
Как это работает? Шаг за шагом, для балбесов
- Отправка:
- Таймер TIM3 каждую секунду орёт: "Пора!". Тогда STM32 ставит PF1 в HIGH (режим "говорю"), шлёт "madmenat.ru\r\n" и сразу возвращает PF1 в LOW (режим "слушаю"). Всё само, как по таймеру будильника!
- Приём:
- Если что-то приходит на линию (и PF1 в LOW), данные летят в PA3 (RX).
- Каждый байт вызывает прерывание UART, и он падает в буфер. Как только прилетает \n, STM32 шлёт обратно "ECHO: " и всё, что накопил. Например, прислали "hi\r\n" — в ответ "ECHO: hi\r\n". Просто эхо, только круче!
- Прерывания — это магия:
- Микроконтроллер не сидит и не ждёт, как дурак, а занимается своими делами. Как только что-то приходит — бах, прерывание, и всё само обрабатывается. Это как уведомления на телефоне — не надо тыкать каждую секунду!
Что важно знать, чтобы не сесть в лужу
- Трансивер RS485: Без него ничего не выйдет! Используй что-то типа ISO3082DW или SN75176BDR, и подключи его пин DE к PF1. Это как педали для машины - без них никуда.
- Подтяжки: В шаблоне их нет, но если в реальном проекте кнопки или входы начнут глючить, добавь резисторы. Это как ремень безопасности — лучше с ним.
- Коллизии: Если два устройства начнут орать одновременно, будет каша. Шаблон это не лечит, но ты можешь добавить проверку: "тихо на линии? тогда говорю!".
Что ты увидишь в терминале? Простой пример
- Каждую секунду: "madmenat.ru\r\n".
- Отправил "test\r\n" — получил "ECHO: test\r\n".
- Это как крикнуть в лесу, а в ответ тебе то же самое, но с приколом!
Почему этот шаблон — твой лучший друг?
- Просто: Скопируй код, зашей в STM32, и оно сразу заработает. Даже думать не надо!
- Прерывания: Научишься работать с ними, а это как супергеройская сила для любых проектов с данными.
- RS485: Разберёшься, как рулить направлением передачи/приёма — пригодится в куче полудуплексных штук.
- Гибкость: Хочешь поменять текст, буфер или добавить команды? Легко — всё как конструктор LEGO.
Итог: твой билет в мир RS485
Этот шаблон — как велосипед с дополнительными колёсиками: простой, но мощный. Бери его, пробуй, добавляй свои фишки, и скоро ты будешь делать крутые системы обмена данными. А если что-то не пашет — проверь трансивер и пины. Это как убедиться, что шнур в розетке, когда телик не включается!
#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]; uint8_t rx_index = 0; // Прототипы функций 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("madmenat.ru\r\n"); } } // Обработчик прерывания UART (приём данных) void USART1_IRQHandler(void) { if (USART1->ISR & USART_ISR_RXNE) { // Если данные приняты char received = USART1->RDR; // Читаем принятый байт if (received == '\n') { // Если получен символ конца строки RS485_SetTransmitMode(); // Переключаем в режим передачи UART_SendString("ECHO: "); // Отправляем метку rx_buffer[rx_index] = '\0'; // Завершаем строку в буфере 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