Теперь рассмотрим UART и USART - инициализация, передача/приём, так как вещь очень нужная как минимум при отладке кода. Ведь некоторые процессы, происходящие при выполнении, неудобно отслеживать отладчиком CubeIDE.
Предупреждение: Во всех модулях, которые я пишу, нет защиты от дурака. Я пошёл на это ради уменьшения кода, да и ленивый я. Поэтому необходимо заранее распланировать, ещё на этапе схемы, какая периферия будет задействована, и какие выводы для чего используются. Нужно учитывать, например, что у STM32F407xxx два модуля UART и четыре USART. А у STM32F411xxx три USART, но библиотека по-любому создаст все вектора прерываний, и обращение к ним будет обрабатываться, но выполнять ничего не будет, так как оставшейся периферии просто не существует, и поднимать флаги прерываний будет некому. Ещё распространённая ошибка, её можно сделать даже под HAL - назначить выводы какой-либо периферии, например, UART, а потом назначить выводы, используемые им, как обычные порты ввода-вывода. Здесь будет принцип - последним встал, тапки отобрал. То есть в случае, когда мы инициализируем UART, он забирает себе выводы для работы, но если потом инициализируем GPIO на ввод-вывод, то естественно GPIO перепрограммирует выводы под себя, и UART работать не будет.
Как обычно, у нас для инициализации периферии есть несколько шагов:
- Включить тактирование.
- Установить скорость передачи.
- Установить вектора прерываний.
- Если необходимо - разрешить прерывание и разрешить работу самой периферии.
Как было показано в части 6, создаём файлы библиотеки на классах, но, так как библиотека UART зависима от ядра, создаём её не в каталоге Library, а в каталоге Device в разделе для своего МК. Сейчас наиболее готовая библиотека находится в STM32F4xx. Позже доделаю для остальных МК и разложу в соответствующие каталоги, а можете и сами это попробовать сделать.
Инициализация будет классическая:
- Скорость передачи - выбирается при инициализации.
- Количество передаваемых/принимаемых бит - 8.
- Чётность - не проверяется.
- Стоп бит - 1.
- Сигналы управления состоянием шины - не используются.
static RingBuff_t tx_fifo; static uint8_t tx_buff[UART_TXBUFF_LENGHT]; static RingBuff_t rx_fifo; static uint8_t rx_buff[UART_RXBUFF_LENGHT]; uart::uart(USART_TypeDef *Port, bool Alternate) { _USART_Port = Port; _Alternate = Alternate; if(Port == UART4) _IRQn = UART4_IRQn; else if(Port == UART5) _IRQn = UART5_IRQn; else if(Port == USART1) _IRQn = USART1_IRQn; else if(Port == USART2) _IRQn = USART2_IRQn; else if(Port == USART3) _IRQn = USART3_IRQn; else if(Port == USART6) _IRQn = USART6_IRQn; }
Выделяем массивы для буферов передачи и приёма. В конструкторе инициализируем переменные, отвечающие за порт и альтернативный порт, и указываем обработчик прерывания.
Далее идёт функция void uart::init(uint32_t BaudRate)
, которой передаётся требуемая скорость. Первым делом мы порт GPIO переключаем на альтернативную функцию GPIO_AF8_UART4
, которая проинициализирует необходимые выводы:
// Блок инициализации портов GPIO для работы с UART // UART4 if(_USART_Port == UART4) { RCC->APB1ENR |= RCC_APB1ENR_UART4EN; // Включаем тактирование UART _SetSpeed(Speed_WeryHigh); _SetPull(Pull_Up); // При инициализации подтяжка вверх if(!_Alternate) // Если используются не альтернативные выходы, _SetPinAlternate(GPIOA, GPIO_PIN_0 | GPIO_PIN_1, GPIO_AF8_UART4); // инициализируем "родные" выводы для UART4 - A0, A1 else _SetPinAlternate(GPIOC, GPIO_PIN_10 | GPIO_PIN_11, GPIO_AF8_UART4); // Или альтернативные C10, C11 _SetSpeed(Speed_Low); }
Далее устанавливаем регистры, заведующие скоростью передачи:
// Рассчитываем делитель для получения скорости передачи _BaudRate = BaudRate; // В зависимости от порта выбираем делитель if((_USART_Port == USART2) || (_USART_Port == USART3) || (_USART_Port == UART4) || (_USART_Port == UART5)) Prescaler = APBPrescTable[(RCC->CFGR & RCC_CFGR_PPRE1) >> RCC_CFGR_PPRE1_Pos]; else Prescaler = APBPrescTable[(RCC->CFGR & RCC_CFGR_PPRE2) >> RCC_CFGR_PPRE2_Pos]; FreqUART = SystemCoreClock >> Prescaler; _USART_Port->BRR = UART_BRR_SAMPLING16(FreqUART, _BaudRate);
Так как разные порты тактируются от разных источников, делители у нас будут разные. Этим заведует часть кода, построенная на операторе if
, где переменная Prescaler
инициализируется из таблицы делителей APBPrescTable
, рассмотренной ранее и находящейся в файле system_stm32f4xx.c. Нужное значение частоты тактирования будет находиться в переменной FreqUART
. Далее в регистр BRR будет записан нужный коэффициент деления, чтобы получить необходимую нам скорость передачи. Она вычисляется скриптом UART_BRR_SAMPLING16
, "стыренным" из HAL. В коде находятся все скрипты для всех типов МК:
#define UART_DIV_SAMPLING16(_PCLK_, _BAUD_) ((uint32_t)((((uint64_t)(_PCLK_))*25U)/(4U*((uint64_t)(_BAUD_))))) #define UART_DIVMANT_SAMPLING16(_PCLK_, _BAUD_) (UART_DIV_SAMPLING16((_PCLK_), (_BAUD_))/100U) #define UART_DIVFRAQ_SAMPLING16(_PCLK_, _BAUD_) ((((UART_DIV_SAMPLING16((_PCLK_), (_BAUD_)) - (UART_DIVMANT_SAMPLING16((_PCLK_), (_BAUD_)) * 100U)) * 16U) + 50U) / 100U) /* UART BRR = mantissa + overflow + fraction = (UART DIVMANT << 4) + (UART DIVFRAQ & 0xF0) + (UART DIVFRAQ & 0x0FU) */ #define UART_BRR_SAMPLING16(_PCLK_, _BAUD_) ((UART_DIVMANT_SAMPLING16((_PCLK_), (_BAUD_)) << 4U) + (UART_DIVFRAQ_SAMPLING16((_PCLK_), (_BAUD_)) & 0xF0U) + (UART_DIVFRAQ_SAMPLING16((_PCLK_), (_BAUD_)) & 0x0FU))
Так как есть UART с дополнительными регистрами, в которых можно указать не только основной делитель, но и дробную его часть, и, так как код из этой статьи большей своей частью подходит для других МК, я решил оставить его здесь, чтобы не искать его снова при адаптации данной библиотеки для других МК.
У нас почти всё готово, можем разрешать работу UART:
// Разрешаем работу UART _USART_Port->CR1 |= (USART_CR1_RE | USART_CR1_TE | USART_CR1_UE); // Включаем передатчик и приёмник
И, как всегда, вишенка на торте. Благодаря товарищу http://dimoon.ru мы можем использовать его библиотеку RingFIFO
для наших целей. Эта библиотека просто принимает от нас данные, которые мы хотим передать, и записывает в буфер, который мы выделили в начале кода. Получается, что мы не работаем с UART напрямую, а просто скидываем передаваемые данные в FIFO, а уже по прерыванию они уходят в UART.
TxBuff = tx_buff; // Инициализируем указатели на буфер FIFO RxBuff = rx_buff; RingBuffInit(&tx_fifo, TxBuff, _BuffLength); // Инициализируем переменные, управляющие буфером FIFO RingBuffInit(&rx_fifo, RxBuff, _BuffLength); RXNEIEnable(); // Разрешаем приём данных NVIC_EnableIRQ(_IRQn); // Разрешаем прерывание от UART
Инициализация закончена, теперь мы можем передавать/принимать данные. И для передачи используем функцию:
size_t uart::write(uint8_t c) { return ring_put(_BuffLength, &tx_fifo, c); }
Как я и говорил ранее, она не обращается напрямую к UART, а кидает данные на стек FIFO. Теперь уже не вишенка, а целая роза на торте. Так как мы пользуемся классом Print (class uart : public Print)
, нам доступны все его методы для передачи информации:
// Функции из ардуины virtual size_t write(uint8_t); inline size_t write(unsigned long n) { return write((uint8_t)n); } inline size_t write(long n) { return write((uint8_t)n); } inline size_t write(unsigned int n) { return write((uint8_t)n); } inline size_t write(int n) { return write((uint8_t)n); } using Print::write; // используется для write(str) и write(buf, size) как Print
Для приёма данных используется одна функция read()
:
int16_t uart::read() { return ring_get(&rx_fifo); }
Она просто берёт байт со стека FIFO и отдаёт его нам, а если стек пуст - возвращает "-1". Ну и сами функции для работы с FIFO:
int16_t uart::ring_put(uint16_t BuffLen, RingBuff_t *fifo, uint8_t c) // Функция для закидывания байта в стек FIFO { int16_t ret; while((BuffLen - RingBuffNumOfItems(fifo)) < 5){;} // Если буфер близок к переполнению, притормаживаем NVIC_DisableIRQ(_IRQn); // Отключаем прерывание, чтобы не помешало заполнять буфер RingBuffPut(fifo, c); // Закидываем на стек FIFO ret = c; // Зачем? Не помню TXEIEnable(); // Запускаем передачу NVIC_EnableIRQ(_IRQn); // Разрешаем прерывание return ret; } int16_t uart::ring_get(RingBuff_t *fifo) // Функция забирает байт со стека { int16_t ret; NVIC_DisableIRQ(_IRQn); // Запрещаем прерывание, чтобы не порушить стек FIFO ret = RingBuffGet(fifo); // Забираем байт NVIC_EnableIRQ(_IRQn); // Разрешаем прерывание return ret; // Возвращаем байт }
Наиболее интересна функция ring_put()
. В самом начале она проверяет, не приблизился ли буфер к концу, если приблизился, она ожидает, когда тот начнёт освобождаться. Так как буфер приёмника и передатчика всего 64 байта, при передаче 128 байт без этой проверки мы получим на выходе мусор, так как собьются указатели FIFO. До момента переполнения буфера функция является неблокирующей, как только FIFO приближается к пределу, она будет ждать освобождения памяти и становится блокирующей. Если данное поведение критично, можно выделить под буфер дополнительную память функцией SetBuffSize(uint16_t Size)
. Данная функция запрашивает у системы память размером Size
и возвращает 1 - при удаче и 0 - при отсутствии свободной памяти. Свободной памяти может не быть в случае, если прошивка забивает переменными всю доступную память. Так что нужно проверять, что возвращает данная функция.
Далее мы отключаем прерывания, чтобы они не мешали работе функции RingBuffPut()
, так как не вовремя пришедшее прерывание от UART может сбить указатели FIFO и запороть наши данные. Бояться потери данных от UART не стоит. Так как даже в случае приёма или передачи байта данные обрабатываются намного быстрее, чем работает UART. Если в данный момент UART примет байт, флаг прерывания поднимется всё равно, но отработает он только после разрешения прерываний. Затем мы кидаем наш байт в FIFO и разрешаем передачу и прерывания.
Функция ring_get()
просто читает байт из регистра UART и кидает его в FIFO. Здесь защититься от переполнения буфера FIFO не удастся, поэтому нужно строить приём или постоянным чтением байта и помещением его уже в свой буфер, или увеличением буфера, или, если известно сколько байт должно прийти, мониторить их приём с помощью функции BytesToRead()
, которая возвращает количество байт, находящихся в буфере FIFO. На некоторых МК UART имеет флаг, который поднимается в случае отсутствия входных данных определённое время. Можно построить механизм окончания приёма на этой фиче.
Теперь вопрос, а как же тогда идёт передача и приём, если мы общаемся только с буфером FIFO? Всё просто. Это делается в прерывании:
/* * Прерывания */ // UART4 extern "C" void UART4_IRQHandler(void) { static uint16_t tmp; // Прерывание от приёмника. if((UART4->SR & USART_SR_RXNE) && (UART4->CR1 & USART_CR1_RXNEIE)) // Если прерывание приема разрешено, и что-то получили { tmp = UART4->DR; // Читаем данные RingBuffPut(&rx_fifo, tmp); // и кидаем их в FIFO } // Прерывание от передатчика. if((UART4->SR & USART_SR_TXE) && (UART4->CR1 & USART_CR1_TXEIE)) // Если прерывание разрешено и произошло { if(RingBuffNumOfItems(&tx_fifo) > 0) // проверяем - есть что передавать? { tmp = RingBuffGet(&tx_fifo); // Берём из FIFO байт и UART4->DR = (uint8_t)(tmp & 0xFF); // передаём его } else // если передавать нечего { UART4->CR1 &= ~USART_CR1_TXEIE; // отключаем прерывание от передатчика. Позже его включит функция ring_put() } } }
В обработчике две разных группы, обрабатывающие приём и передачу байт. Первой идёт обработка прерывания от приёмника, следующей - обработка от передатчика. Как это происходит, расписано в комментариях.
Ну и примеры работы с UART в проекте WorkDevel\Developer\Tests MK/F407\F407VExx_UART. Объяснять что-либо особого смысла нет, там всё просто. Работа библиотеки проверена на STM32F407, UART4 и USART1. На остальных не проверял, если не будет работать, пишите.
Проект на Яндекс диске и одним архивом F407VExx_UART. Следите также за веткой на форуме, там можно выкладывать замечания, пожелания. Также на форуме я буду выкладывать информацию о модернизации библиотек.
Особые благодарности автору RingFIFO
за библиотеку и идею http://dimoon.ru.
Тема на форуме - перейти.