Теперь рассмотрим 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.
Тема на форуме - перейти.
Эдуард, спасибо за материалы! Проверить пока нет возможности, есть такой вопрос. Нет ли у Вас данных об отличиях в объеме кода? То есть объем прошивки для данного примера и аналогичный объем для того же самого, но на HAL.
Я не могу сейчас сказать. Если только специально заморочиться.
Но сталкивался с такой штукой.
Кристалл с 16Кб. Флэш.
Инициализируется тактовый генератор, RTC, ADC, SPI
Инициализация на HAL заняла 66% Флеша и 45% ОЗУ.
И это без моего кода, который нужно ещё дописать.
Тогда я сделал всё на CMSIS. Код уменьшился во много раз. На сколько, сейчас не помню.
При работе над кодом и переводе его на C++ занятость памяти подскочила относительно голого CMSIS, но не критично.
Сейчас делаю проект где идут вперемешку готовые библиотеки и CMSIS, скорость работы выше, чем под HAL и занимает меньше.
Но все мои друзья смотрят на меня как на идиота и не собираются уходить из под HAL.
Кроме того, почему я так стал делать. Я выкладывал классы Print и GFX_TFT.
Скоро я покажу как на всём этом писать библиотеки для дисплеев. Если не использовать классы, пришлось бы к каждой библиотеке пристёгивать файлы занимающиеся выводом на экран и модернизировать их. Здесь же этого не потребуется.
Просто HAL - это готовый официальный инструмент. Вот взять меня даже - есть проекты на работе, которыми я занимаюсь. Времени просто нет уделять кучу времени настройке инструментов (то есть альтернативных неофициальных библиотек), которые еще надо отлаживать и доделывать... Плюс к тому же тогда надо переносить все имеющиеся проекты, это тоже время, много людей должны бросить текущие задачи и заниматься этим...
Я попробую сегодня проверить разницу по памяти, отпишусь на форуме тогда в Вашей теме.
Готово, сделал - https://microtechnics.ru/community/stm32/seriya-statej-pro-perehod-na-klassy-na-stm32/paged/2/#post-853
Я не понял, а что тут от С++? Начнём с простого: почему обработчик прерывания не виртуальная функция? Фрагмент из программы:
////////////////////////////////////////////////////////////////////////////////
//
// Любой класс, желающий обрабатывать прерывания, должен быть наследником
// класса IRQ, в котором ему необходимо реализовать метод IRQn_Handler(),
// а пОрты должны обеспечивать вызов этого метода.
// Заметь, что можно "повесить" несколько прерываний на один обработчик,
// но и при "отвязке" нужно "отвязывать" каждое прерывание от этого обработчика.
//
////////////////////////////////////////////////////////////////////////////////
...
class IRQ
{
...
protected:
IRQ( void );
virtual ~IRQ( void );
//----------------------------------------------------------------------
// Обработчик прерывания.
virtual void IRQ_Handler( void ) = 0;
IRQ* installInterruptHandler
(
IRQn_Type IRQn // See "stm32fxxx.h", enum IRQn_Type.
);
IRQ* uninstallInterruptHandler
(
IRQn_Type IRQn // See "stm32fxxx.h", enum IRQn_Type.
);
...
};
Методы приёма через кольцевой буфер... А для виртуального последовательного порта, работающего через USB или Ethernet тоже будем писать внутри свои кольцевые буферы? Где объект RingBuffer? А приём-передача через DMA при таком подходе автора так ваще мрак. ИМХО, автору за парту, и учиться, учиться, учиться.
Спасибо за рекомендацию про учёбу.
С прерыванием согласен. На коленке сделано.
Помогите сделать правильно.
Задачи через DMA работать пока не стояло.
Но если поможете, буду очень признателен.
С Вами можно как нибудь связаться?
Зачем? Стать соавтором этих малограмотных опусов?
Посмотрел статейки про GPIO. Это мрак. Полный мрак. Даже если не делать как у гуру на шаблонах, то вообще не понятен смысл того, что сделал автор.
Вот это
Led.digitalWrite(Low); // Устанавливаем выход в низкое состояние
на С++, ИМХО, должно выглядеть так:
Led = 0;
или
Led = false;
Я уже даже не говорю о том, что для одних устройств активным сигналом является "1", а для других- "0". И это должно указываться при создании объекта, например, OutputPin, чтобы логика управления периферийными микросхемами не была привязана к тому, чем выдаётся активный уровень, нулём или единицей. Хм, у автора даже нет деления входные и выходные пины, что закономерно, ведь если бы он понимал С++, то создал бы базовый тип ДвоичныйСигнал, наполнил бы его операциями, а потом уже привязал его к ногам МК.
Автор, глобальное замечание. Осознай простую вещь: у STM32 периферийные устройства на кристалле связаны с ногами МК через коммутатор, поэтому устройство не знает, и не должно знать, к каким ногам оно подключено.Отсюда вывод: настройка устройств и настройка выводов МК- это разные процессы, которые делаются независимо одно от другого. Да, есть исключения в виде аналоговых и высокоскоростных устройств, подключение которых к ногам невозможно изменить, но это не противоречит сказанному выше.
Для примера. Описание функций ног МК сведены в таблицу.
const PinDescriptor pin[] =
{
//------------------------------------------------------------------------------
// DO
//
{ "FAULT_LED", GPIOB, BIT_14, GPIO, OUTPUT, NO_PULL, PUSH_PULL, LOW_SPEED },
{ "STOP_LED", GPIOB, BIT_7, GPIO, OUTPUT, NO_PULL, PUSH_PULL, LOW_SPEED }, // BU
{ "RUN_LED", GPIOB, BIT_0, GPIO, OUTPUT, NO_PULL, PUSH_PULL, LOW_SPEED }, // GR
{ "DO0", GPIOD, BIT_7, GPIO, OUTPUT, NO_PULL, PUSH_PULL, LOW_SPEED },
{ "DO1", GPIOF, BIT_2, GPIO, OUTPUT, NO_PULL, PUSH_PULL, LOW_SPEED },
....
//
//------------------------------------------------------------------------------
// DI
//
{ "Button", GPIOC, BIT_13, GPIO, INPUT, NO_PULL, INPUT_PIN, LOW_SPEED },
{ "DI0", GPIOE, BIT_9, GPIO, INPUT, PULL_DOWN, INPUT_PIN, LOW_SPEED },
{ "DI1", GPIOE, BIT_11, GPIO, INPUT, PULL_DOWN, INPUT_PIN, LOW_SPEED },
...
//
//------------------------------------------------------------------------------
// Peripherias
//
// External EIA-485-1
{ "USART2_TX", GPIOD, BIT_5, AF07, ALTFUNC, PULL_UP, PUSH_PULL, HIGH_SPEED },
{ "USART2_RX", GPIOD, BIT_6, AF07, ALTFUNC, PULL_UP, PUSH_PULL, HIGH_SPEED },
...
// Logger (connected to ST-Link)
{ "USART3_TX", GPIOD, BIT_8, AF07, ALTFUNC, PULL_UP, PUSH_PULL, HIGH_SPEED },
{ "USART3_RX", GPIOD, BIT_9, AF07, ALTFUNC, PULL_UP, PUSH_PULL, HIGH_SPEED },
...
// SPI2 (W5500)
{ "WIZ_SCLK", GPIOD, BIT_3, AF05, ALTFUNC, NO_PULL, PUSH_PULL, HIGH_SPEED },
{ "WIZ_MISO", GPIOC, BIT_2, AF05, ALTFUNC, NO_PULL, PUSH_PULL, HIGH_SPEED },
{ "WIZ_MOSI", GPIOC, BIT_3, AF05, ALTFUNC, NO_PULL, PUSH_PULL, HIGH_SPEED },
{ "WIZ_RSTn", GPIOE, BIT_4, GPIO, OUTPUT, NO_PULL, PUSH_PULL, HIGH_SPEED },
{ "WIZ_CS", GPIOB, BIT_12, GPIO, OUTPUT, NO_PULL, PUSH_PULL, HIGH_SPEED },
// bxCAN1
{ "bxCAN1-RX", GPIOD, BIT_0, AF09, ALTFUNC, NO_PULL, PUSH_PULL, HIGH_SPEED },
{ "bxCAN1-TX", GPIOD, BIT_1, AF09, ALTFUNC, NO_PULL, PUSH_PULL, HIGH_SPEED },
//
//------------------------------------------------------------------------------
{ 0 } // end of map marker
};
А дальше одной строкой
// Настраиваем функции выводов МК.
configurePins( &pin[ 0 ], ( sizeof( pin ) / sizeof( PinDescriptor ) + 1 ) );
Мне не нужно шарахаться по 12000 строкам кода и проверять пересечения ног у периферии. Всё в одном месте. Более того, внутри configurePins() легко отслеживать коллизии, если это потребуется.
Автор, извини, но твои статьи похожи на анекдот про Вовочку, который узнал, что у слова "писька" есть синонимы и делиться своей радостью с родителями.
P. S. Я не программист. Занимаюсь разработкой электроники и автоматики, а программировать вынужден по причине отсутствия грамотных программеров.
1) А как ты смотришь, не написал ли ты одинаковый порт и номер пина в разных местах?
Я бы желал чтобы это было автоматическим способом.
2) Зачем так много выпендрёжа и такой негативной критики? Без этого нельзя? Человек многому научился, меня научил, если бы я сам начал программировать STM, я бы год потратил на изучение того, что сделал Эдуард. А тут на блюдечке в каёмочке преподнесено.
Нельзя ли было написать так: а у меня вот так вот сделано. Сравни.
Будь это изложено Эдуардом в форуме, именно так я и поступил бы, просто показав свой вариант. Я сам ни мало подчерпнул для себя из общения на форумах, изучая код других и показывая свой. Но в этих статьях человек пытается _учить_ людей, сам очень плохо владея вопросом. В итоге у новичков, не имеющих специального образования и опыта, сформируется неправильное понимание устройства МК и использования языка С++, в итоге они будут тратить год на освоение того, что делается по вечерам за пару недель.
Спасибо за критику.
Теперь я знаю что иду правильным путём.
Нужно только знания подтянуть.
А вам на будущее - гордыня ещё никого до добра не доводила.