CMSIS STM32 Урок 5. Работа с UART

Старый добрый UART, сколько лет он уже существует? Только сама природа это помнит, тех древних мудрецов, которые упаковывали сообщения в асинхронные пакеты и отправляли по гудящим столбам сквозь пространство и время... И вот этот интерфейс дожил до наших дней, и, кстати, никуда уходить не собирается. Внутри STM32 уже есть периферия для работы с портами UART во всевозможных режимах, на разных скоростях. Моя задача - показать, насколько просто и экономично можно программировать этот интерфейс. Вооружайтесь до зубов USB свистками UART, это будет увлекательное путешествие.

Предыдущий урок https://ipasoft.info/index.php/articles/cmsis-stm32-urok-4-ispolzovanie-sistemnogo-tajmera-systick 

Подпишитесь на канал, чтобы статьи выпускались чаще. Чем больше аудитория, тем сильнее мотивирован автор.

В микроконтроллере рассматриваемого цикла уроков STM32F103C6T6A присутствует два UART модуля: UART1 и UART2. Работа с любым из них начинается с тактирования периферии:

RCC->APB1ENR |= RCC_APB1ENR_USART2EN;	// Разрешить тактирование UART2
__NOP();
RCC->APB2ENR |= RCC_APB2ENR_USART1EN; // Разрешить тактирование UART1
__NOP();

Вы можете разрешить тактирование для одного и более модулей в вашем проекте перед началом настройки этих модулей. Затем, нужно разрешить тактирование портов ввода вывода (GPIO) если оно еще не разрешено.

Так на лапках PA2 может находиться UART2 TX, на PA3 UART2 RX.

RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;		// Включить тактирование GPIOA
__NOP();
// PA0, PA1, PA4, PA5, PA6, PA7 - аналоговые (CNF=00, MODE=00); PA2 - альтернатива UART TX (CNF=10, MODE=11); PA3 - вход UART RX (CNF=01 MODE=00)
GPIOA->CRL = 0x4B00;	// 0b 0000 0000 0000 0000 0100 1011 0000 0000

Пример настройки UART2 на PA2, PA3. Альтернативная функция (OUTPUT ALT) настраивается только на выходе Tx, а Rx так и остается входом Input Floating.

Аналогичным образом можно настроить и UART1 на PA9 / PA10.

RCC->APB2ENR |= RCC_APB2ENR_USART1EN; // Разрешить тактирование UART1
__NOP();
// PA8 - AF PWM (CNF=10, MODE=11), PA9 - AF TX (CNF=10, MODE=11), PA10-PA12 Floating inputs (CNF0=01 MODE0=00), PA13-PA14 Debug (CNF0=01 MODE0=00), PA15 - AF PWM (CNF=10, MODE=11)
GPIOA->CRH = 0xB44444BB; // 1011 0100 0100 0100 0100 0100 1011 1011

В этих примерах настраивается и другая периферия, не относящаяся к UART'у, но ей можно пренебречь или настроить по-своему. Конфигурацию скопом (регистр=значение) я делаю для жесткой оптимизации. Как раз с целью оптимизации мы иногда можем прибегнуть к замене HAL на «Bare metal». Так можно поместить в скромный микроконтроллер очень серьезный объем функционала.

Пример настройки контроля четности и размера данных

if (gwSettings.rs485DataSize) USART1->CR1 |= USART_CR1_M; // Размер данных 9 бит
if (gwSettings.rs485Parity) { // Если есть контроль четности
	USART1->CR1 |= USART_CR1_PCE; // включить его в конфиге
	if (gwSettings.rs485Parity == 1) USART1->CR1 |= USART_CR1_PS; // Odd
}

Скорость обмена по UART программируется через USART_BRR регистр (BaudRate register). В этот регистр необходимо записать значение, которое рассчитывается по формуле:

BRR = FREQ / BAUDRATE

FREQ – частота работы шины. Для UART1 это частота APB2 (самая быстрая), для UART2 это частота APB1 (медленная).

BAUDRATE – требуемая скорость обмена.

Рассмотрим пример. UART2 находится на шине APB1, у меня она тактируется частотой 32 МГц, а требуемая скорость обмена по UART для этого примера, 9600 бод.

32000000 / 9600 = 3333,33

Таким образом в регистр BRR мы записываем целое значение 3333 и получим фактическую скорость обмена 32000000 / 3333 = 9600,96 и она на 1% отличается от требуемой, но это нормально, т. к. расхождение скорости обмена допустимо до 2% (иногда до 1%).

Рассмотрим пример. UART1 находится на шине APB2. Она тактируется в моем случае частотой 64 МГц. А требуемая скорость обмена по UART, 115200 бод. BRR = 64000000 / 115200 = 555,55 — целая часть пойдет, а дробная отбросится.

64000000 / 555 = 115315,315 - это 115200 + 1%

В регистр возможно записать только 16-битное значение. То есть значение BRR не может быть больше 65535, а значит и минимальная скорость ограничена FREQ / 65535.

Теперь настроим полностью UART1 на 9600 бод, с прерываниями на событие Rx.

// USART1
RCC->APB2ENR |= RCC_APB2ENR_USART1EN; // Разрешить тактирование UART1
__NOP();
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;	// Включить тактирование GPIOA
__NOP();
// PA8 - AF PWM (CNF=10, MODE=11), PA9 - AF TX (CNF=10, MODE=11), PA10-PA12 Floating inputs (CNF0=01 MODE0=00), PA13-PA14 Debug (CNF0=01 MODE0=00), PA15 - AF PWM (CNF=10, MODE=11)
GPIOA->CRH = 0xB44444BB; // 1011 0100 0100 0100 0100 0100 1011 1011
USART1->CR1 = USART_CR1_UE;	// Разрешить работу UART
USART1->BRR = 64000000U / 9600U; // Настройка скорости USART_BRR = Fck / BAUD
USART1->CR1 |= USART_CR1_TE | USART_CR1_RE | USART_CR1_RXNEIE; // Разрешить прием, передачу и прерывания по событию Rx
NVIC_SetPriority(USART1_IRQn, 0);
NVIC_EnableIRQ(USART1_IRQn);

Таким образом, по факту приема одного байта по UART1 должно произойти прерывание, а вектор прерывания приведет в сервисную рутину USART2_IRQHandler, которая уже определена внутри startup файла (автоматически добавленного при создании проекта). Чтобы было куда «проваливаться», создадим функцию обработчика прерывания от USART1 внутри main.h

// Прерывание Rx от UART1
void USART1_IRQHandler(void) {	
}

Обработчик будет вызываться каждый раз, когда вы получите байт по UART'у и чтобы сформировать буфер принимаемой датаграммы, вы будете принятые данные добавлять в массив байт за байтом.

В этом уроке я покажу алгоритм приема информации в буфер с использованием счетчика времени бездействия шины. Принцип работы очень простой: принимаем данные в буфер, после каждого принятого байта возводим счетчик. Как только данные перестали приходить, счетчик опустошается (отсчитывает в обратную сторону каждую миллисекунду). Опустошение счетчика означает, что шина простаивает и датаграмма принята, можно ее обрабатывать.

#define UART_IDLE_TIMEOUT		10
#define UART_MODBUS_MAX_BUF_LEN	200	// Макс размер буфера UART1
uint8_t uart1ito = 0; // Счетчик таймаута бездействия для UART1
uint8_t uart1Buf[UART_MODBUS_MAX_BUF_LEN];
uint16_t uart1BufPos = 0; // Текущая позиция в буфере (и кол-во принятых байтов)
// Прерывание Rx от UART1 (Modbus)
void USART1_IRQHandler(void) {
	USART1->SR &= ~USART_SR_RXNE; // Пока не сбросишь, прерывание будет возникать
	uart1Buf[uart1BufPos++] = USART1->DR; 
	uart1ito = UART_IDLE_TIMEOUT;
	if (uart1BufPos > UART_MODBUS_MAX_BUF_LEN) uart1BufPos = 0;
}

В разделе «Использование системного таймера SysTick» я показал как можно отсчитывать интервалы в 1 миллисекунду. Этим приемом мы сейчас и воспользуемся.

Внутри функции main() в бесконечном цикле

while(1) { // Бесконечный главный цикл
	if (t1ms) { // Прошла 1 миллисекунда
		t1ms = false;
		if (uart1ito) {
			// Вышел таймаут бездействия UART1, значит датаграмма принята
			if (--uart1ito == 0) { 
				// Обработать датаграмму
				ProcessUARTBuffer(uart1Buf, &uart1BufPos);
				uart1BufPos = 0;
			}
		}
	}
}

Этот алгоритм очень простой и неприхотливый. Он отлично подходит для обработки Modbus запросов. Здесь не используется кольцевой буфер, т. к. мы работаем в формате запрос-ответ. Мы не запрещаем прерывания Rx пока происходит обработка и передача, т. к. формат «запрос — ответ» подразумевает, что после запроса нам дадут достаточно времени для формирования ответа.

В режиме жесткой оптимизации и в формате «запрос-ответ» мы можем использовать один буфер для приема и для отправки. Это позволяет меньше расходовать память и упрощать вызов функции передачи ответа.

// Отправить буфер по Modbus. Сам буфер формируем в массиве uart1Buf
// количество байтов для передачи в  uart1BufPos
void USART1_SendBuffer(void) {
	for (uint8_t i = 0; i < uart1BufPos; i++) {
		while ((USART1->SR & USART_SR_TXE) == 0) {} // Ждать чтобы TXE стал 1
		USART1->DR = uart1Buf[i];
	}
}

Этот способ применим, если вы работаете по полнодуплексным интерфейсам UART напрямую или RS-232. Однако с интерфейсом RS-485 нужно сделать чуть больше.

// Отправить буфер по Modbus
void USART1_SendBuffer(void) {
	uint8_t temp;
	USART1_DE_GPIO_Port->BSRR = USART1_DE_Pin; // Включить драйвер
	for (uint8_t i = 0; i < uart1BufPos; i++) {
		while ((USART1->SR & USART_SR_TXE) == 0) {} // Ждать чтобы TXE стал 1
		temp = USART1->SR; // Последовательность чтения регистров
		// SR, DR сбросит флаг TC (который означает что передача завершена)
		temp = USART1->DR;
		USART1->DR = uart1Buf[i];
	}
	// Ждать окончания передачи из сдвигового регистра
	while((USART1->SR & USART_SR_TC) == 0) {} 
	USART1_DE_GPIO_Port->BSRR = USART1_DE_Pin << 16; // Выключить драйвер
}

У приемопередатчика RS-485 есть управляющий вход DE, который определяет режим работы: «прием» или «передача». В полудуплексе мы не можем одновременно передавать и принимать. Это напоминает принцип рации с кнопкой PTT (Push to talk), нажали кнопку — говорим, отпустили — слушаем. Такой пин в моем примере находится на GPIO Линии PB15. И где-то в коде (например в main.h) я могу задефайнить это:

#define GPIO_PIN_15			0x8000  // 15-ый бит
#define USART1_DE_Pin 		GPIO_PIN_15
#define USART1_DE_GPIO_Port 	GPIOB

Для работы в формате «Поток запросов» вам, конечно же, потребуется кольцевой буфер приема и кольцевой буфер отправки. Но это уже техника за пределами урока по CMSIS.

ОБСУДИТЬ СТАТЬЮ

Следующая статья https://ipasoft.info/index.php/articles/cmsis-stm32-urok-6-rabota-s-shinoj-i2c-v-rezhime-master 

Tags: ,