Продолжаем последовательно настраивать все периферийные модули микроконтроллеров STM32 при помощи STM32CubeMx. Вот ссылка на все статьи курса - ссылка - а сегодня у нас на очереди инициализация модуля USART в STM32, а также прием и передача данных при помощи вышеупомянутого протокола UART. Начнем с небольшого экскурса в теоретические аспекты.
Теоретическая часть.
Обзор модуля UART.
Что такое UART в целом, и зачем он нужен думаю объяснять не надо 🙂 Так что перейдем сразу к реализации в STM32. Предлагаю сначала посмотреть, какие регистры за что отвечают, а потом уже набросать какой-нибудь проект для наглядности.
Небольшое лирическое отступление... Как вы уже догадываетесь, модуль USART в STM32, как и все остальное в этих контроллерах, имеет множество настроек и режимов. Тут тебе и обычный прием/передача и поддержка LIN (об этом протоколе как-нибудь поговорим отдельно). И если в AVR частенько приходилось реализовывать программные UART’ы, поскольку аппаратных просто не хватало, то, к примеру, в STM32F103CB их как минимум три штуки! А это уже немало. Итак, начинаем копаться в даташите.
Регистр USART_SR.
Статусный регистр. Тут содержатся флаги, отражающие текущее состояние дел, то есть текущее состояние модуля USART. Поглядим на некоторые из них поближе:
- LBD - Lin Break Detection flag. Выставляется при обнаружении брейка при использовании LIN (о том, что это такое обязательно расскажу в статье про LIN).
- TXE - Transmit data register empty. Регистр данных пуст, пора его заполнить.
- TC - Transmission complete. Передача завершена.
- RXNE - Read data register not empty. Приемный регистр не пуст, надо срочно прочитать данные.
Есть там еще 4 флага для классификации ошибок – ошибка кадра, наличие шума, ошибка переполнения и т. д.
Регистр USART_DR.
Регистр данных. Именно тут мы будем забирать ценнейшую информацию, которая к нам пришла извне, сюда же будем записывать свои данные.
Регистр USART_BRR.
Здесь настраивается скорость обмена, которую мы хотим получить.
Регистр USART_CR1.
Регистр контроля – контролирует весь процесс. Биты по порядку:
- UE - разрешение работы USART
- M - длина посылки: 0 – 8 бит данных, 1 – 9 бит данных
- Wake - будильник для USART’a – то есть метод его пробуждения
- PCE - контроль паритета – ВКЛ или ВЫКЛ
- PS - тип четности: 0 – четный, 1 – нечетный
- PEIE - разрешение прерывания при обнаружении ошибки четности
- TXEIE - прерывание от TXE
- RXNEIE - прерывание от RXNE
Ну и там еще парочка битов, разрешающих/запрещающих прием/передачу, а также бит для отправки брейка. В общем, это все есть в даташите, все наглядно, красиво и понятно. Поэтому подробно углубляться не буду, нет в этом смысла.
Регистр USART_CR2 - в этом регистре такие биты как включение/выключение режима работы по LIN, установка количества стоп-битов, настройки clock’а, установка параметров LIN брейка и некоторые другие.
Регистр USART_CR3 - а здесь биты для использования DMA и SmartCard.
Регистр USART_GTPR - биты, отвечающие за предделитель.
В общем, вот они, 7 регистров, которые контролируют весь модуль USART в STM32. У меня описаны довольно поверхностно, только основное, если что, спрашивайте в комментариях, буду рад помочь.
Плавно переходим к практической части в виде небольшого базового примера. Забегая вперед скажу, что в данном случае проект будет действительно несложный, ознакомительный, а вот еще парочка материалов для STM32 с использованием UART:
- Драйвер протокола LIN для микроконтроллеров на базе UART.
- Библиотека для работы с шиной 1-Wire на STM32.
- Modbus RTU Slave. Пример реализации на микроконтроллере STM32.
- Modbus RTU Master. Библиотека для микроконтроллеров STM32.
Практическая часть.
Инициализация в STM32CubeMx.
Сегодня же для тестирования реализуем следующее - будем передавать данные при помощи USART2, а принимать посредством USART1. Таким образом, сравнив отправляемые через один и принятые другим модулем данные, мы сможем убедиться, правильно ли функционирует приемопередатчик. Я буду использовать отладочную плату STM32F4Discovery, но это не особо важно, так как механизм взаимодействия с CubeMx по сути один и тот же для любого контроллера и семейства.
Для того, чтобы осуществить обмен данными, необходимо соединить выводы Rx/Tx USART1 с аналогичными выводами USART2. Только не забываем, что Rx одного идет на Tx второго и наоборот:
В итоге коммутируем так:
- PA2 (USART2_TX) - PA10 (USART1_RX)
- PA3 (USART2_RX) - PA9 (USART1_TX)
С электрико-физическими подключениями разобрались, переходим к работе непосредственно в STM32CubeMx. Создаем новый проект и активируем нужные нам модули USART1 и USART2, установив для них режим работы:
Здесь же доступны для изменения классические параметры UART:
- скорость передачи данных
- длина пакета
- четность
- количество стоп-битов
- направление обмена данными
- over sampling
Для нашего тестового проекта это не очень важно, важно лишь, чтобы настройки для USART1 и USART2 были одинаковыми. Поэтому аналогичным же образом конфигурируем и второй модуль. На вкладке "NVIC_Settings" также активируем прерывание:
На этом работу с STM32CubeMx заканчиваем, генерируем код и открываем проект. Вся инициализация портов и модулей USART уже включена в функцию main()
:
/* Initialize all configured peripherals */ MX_GPIO_Init(); MX_USART1_UART_Init(); MX_USART2_UART_Init();
Кроме того, CubeMx сгенерировал обработчики прерываний, в которых реализованы механизмы обмена данными и сброса всех нужных флагов - все это находится внутри функции HAL_UART_IRQHandler()
. Сами же обработчики в файле stm32f4xx_it.c:
/** * @brief This function handles USART2 global interrupt. */ void USART2_IRQHandler(void) { /* USER CODE BEGIN USART2_IRQn 0 */ /* USER CODE END USART2_IRQn 0 */ HAL_UART_IRQHandler(&huart2); /* USER CODE BEGIN USART2_IRQn 1 */ /* USER CODE END USART2_IRQn 1 */ } /** * @brief This function handles USART1 global interrupt. */ void USART1_IRQHandler(void) { /* USER CODE BEGIN USART1_IRQn 0 */ /* USER CODE END USART1_IRQn 0 */ HAL_UART_IRQHandler(&huart1); /* USER CODE BEGIN USART1_IRQn 1 */ /* USER CODE END USART1_IRQn 1 */ }
Прием и передача данных по UART.
Для приема и отправки данных в HAL реализованы следующие функции:
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout); HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout); HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
Из их названия, в принципе, уже понятно, какие механизмы будут использованы:
- прием и передача в обычном режиме
- прием и передача с использованием прерываний
- прием и передача посредством DMA
Поскольку в нашем проекте кроме работы с UART больше ничего не будет, особо оценить преимущества работы на прерываниях мы не сможем, но тем не менее осуществим обмен именно таким образом. Кроме того, объявим в файле main.c массивы (transmitBuffer[]
, receiveBuffer[]
) и их размер (BUFFER_SIZE
) для отправляемых и принимаемых данных и в итоге получаем следующее:
/* Private define ------------------------------------------------------------*/ /* USER CODE BEGIN PD */ #define BUFFER_SIZE 32 /* USER CODE END PD */ /* Private macro -------------------------------------------------------------*/ /* USER CODE BEGIN PM */ /* USER CODE END PM */ /* Private variables ---------------------------------------------------------*/ UART_HandleTypeDef huart1; UART_HandleTypeDef huart2; /* USER CODE BEGIN PV */ uint8_t transmitBuffer[BUFFER_SIZE]; uint8_t receiveBuffer[BUFFER_SIZE]; /* USER CODE END PV */ /* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_USART1_UART_Init(void); static void MX_USART2_UART_Init(void); /* USER CODE BEGIN PFP */ /* USER CODE END PFP */ /* Private user code ---------------------------------------------------------*/ /* USER CODE BEGIN 0 */ /* USER CODE END 0 */ /** * @brief The application entry point. * @retval int */ int main(void) { /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_USART1_UART_Init(); MX_USART2_UART_Init(); /* USER CODE BEGIN 2 */ for (unsigned char i = 0; i < BUFFER_SIZE; i++) { transmitBuffer[i] = i + 1; receiveBuffer[i] = 0; } HAL_UART_Receive_IT(&huart1, receiveBuffer, BUFFER_SIZE); HAL_UART_Transmit_IT(&huart2, transmitBuffer, BUFFER_SIZE); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ }
Собираем проект, зашиваем контроллер и проверяем работоспособность программы. Для этого запускаем отладку и смотрим, что у нас попадает в буфер receiveBuffer[]
:
Принятые данные в точности соответствуют отправленным👍 Для того, чтобы отследить момент окончания передачи, либо приема существуют callback-функции:
HAL_UART_TxCpltCallback()
HAL_UART_RxCpltCallback()
Их можно, к примеру, переопределить прямо в main.c:
/* USER CODE BEGIN 4 */ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { // USART1 завершил прием данных } } void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { // USART2 завершил отправку данных } } /* USER CODE END 4 */
В эти функции программа попадет автоматически при возникновении соответствующих событий, то есть окончания процесса отправки, либо приема - типичный механизм для библиотеки HAL.
И на этом на сегодня завершаем деятельность, знакомство с UART в STM32 осуществлено, спасибо за внимание, и до скорого 🤝