Top.Mail.Ru

Часть 9. STM32 и С++. Библиотека UART.

Теперь рассмотрим UART и USART - инициализация, передача/приём, так как вещь очень нужная как минимум при отладке кода. Ведь некоторые процессы, происходящие при выполнении, неудобно отслеживать отладчиком CubeIDE.

Предупреждение: Во всех модулях, которые я пишу, нет защиты от дурака. Я пошёл на это ради уменьшения кода, да и ленивый я. Поэтому необходимо заранее распланировать, ещё на этапе схемы, какая периферия будет задействована, и какие выводы для чего используются. Нужно учитывать, например, что у STM32F407xxx два модуля UART и четыре USART. А у STM32F411xxx три USART, но библиотека по-любому создаст все вектора прерываний, и обращение к ним будет обрабатываться, но выполнять ничего не будет, так как оставшейся периферии просто не существует, и поднимать флаги прерываний будет некому. Ещё распространённая ошибка, её можно сделать даже под HAL - назначить выводы какой-либо периферии, например, UART, а потом назначить выводы, используемые им, как обычные порты ввода-вывода. Здесь будет принцип - последним встал, тапки отобрал. То есть в случае, когда мы инициализируем UART, он забирает себе выводы для работы, но если потом инициализируем GPIO на ввод-вывод, то естественно GPIO перепрограммирует выводы под себя, и UART работать не будет.

Как обычно, у нас для инициализации периферии есть несколько шагов:

  1. Включить тактирование.
  2. Установить скорость передачи.
  3. Установить вектора прерываний.
  4. Если необходимо - разрешить прерывание и разрешить работу самой периферии.

Как было показано в части 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.

Тема на форуме - перейти.

Подписаться
Уведомить о
guest

12 комментариев
Старые
Новые
Межтекстовые Отзывы
Посмотреть все комментарии
ll47
6 месяцев назад

Эдуард, спасибо за материалы! Проверить пока нет возможности, есть такой вопрос. Нет ли у Вас данных об отличиях в объеме кода? То есть объем прошивки для данного примера и аналогичный объем для того же самого, но на HAL.

ll47
Ответ на комментарий  Эдуард
6 месяцев назад

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

ll47
Ответ на комментарий  Эдуард
6 месяцев назад

Я попробую сегодня проверить разницу по памяти, отпишусь на форуме тогда в Вашей теме.

ll47
Ответ на комментарий  Эдуард
6 месяцев назад
tonyk
tonyk
6 месяцев назад

Я не понял, а что тут от С++? Начнём с простого: почему обработчик прерывания не виртуальная функция? Фрагмент из программы:

////////////////////////////////////////////////////////////////////////////////
//
//     Любой класс, желающий обрабатывать прерывания, должен быть наследником
// класса 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 при таком подходе автора так ваще мрак. ИМХО, автору за парту, и учиться, учиться, учиться.

tonyk
Ответ на комментарий  Эдуард
6 месяцев назад

Зачем? Стать соавтором этих малограмотных опусов?

Посмотрел статейки про 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. Я не программист. Занимаюсь разработкой электроники и автоматики, а программировать вынужден по причине отсутствия грамотных программеров.

Алексей_Байдин
Ответ на комментарий  tonyk
6 месяцев назад

1) А как ты смотришь, не написал ли ты одинаковый порт и номер пина в разных местах?
Я бы желал чтобы это было автоматическим способом.
2) Зачем так много выпендрёжа и такой негативной критики? Без этого нельзя? Человек многому научился, меня научил, если бы я сам начал программировать STM, я бы год потратил на изучение того, что сделал Эдуард. А тут на блюдечке в каёмочке преподнесено.
Нельзя ли было написать так: а у меня вот так вот сделано. Сравни.

tonyk
Ответ на комментарий  Алексей_Байдин
6 месяцев назад

Будь это изложено Эдуардом в форуме, именно так я и поступил бы, просто показав свой вариант. Я сам ни мало подчерпнул для себя из общения на форумах, изучая код других и показывая свой. Но в этих статьях человек пытается _учить_ людей, сам очень плохо владея вопросом. В итоге у новичков, не имеющих специального образования и опыта, сформируется неправильное понимание устройства МК и использования языка С++, в итоге они будут тратить год на освоение того, что делается по вечерам за пару недель.

12
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x