Программирование STM32. Часть 17: Драйвер UART

В прошлой части мы познакомились с таким интересным блоком в STM32, как UART. В этой статье мы не будем разбираться с простыми примерами (но это пока), а стразу познакомимся с библиотекой, которая позволяет удобным способом взаимодействовать с любым UART-ом в микроконтроллерах STM32F103xx. Ссылка на проект в конце статьи.

Предыдущая статья здесь, все статьи цикла можно посмотреть тут: https://dimoon.ru/category/obuchalka/stm32f1.

Краткое описание

Данная библиотека была создана в результате профессиональной деятельности, направленной на разработку ПО для очередного проекта на микроконтроллере 🙂  Она состоит из 2-х заголовочных и 2-х си-файлов:

  • uart.h
  • uart.c
  • RingFIFO.h
  • RingFIFO.c

Основные возможности:

  1. Работает на микроконтроллерах stm32f103xx;
  2. Позволяет обмениваться данными через UART1, 2, 3, 4. Обмен данными через UART5 пока не реализован (не было необходимости, но это исправим 😉 );
  3. Возможность переинициализировать UART прямо во время выполнения кода;
  4. Возможность задействовать и настроить длину FIFO-буфера на приемник и передатчик;
  5. Незадействованные в данном проекте участки кода не будут компилироваться в проект (реализовано через #ifdef);
  6. Для работы использует только CMSIS;
  7. Не перегружена лишним кодом (по возможности) и требует не очень много ресурсов (разумная цена за удобство использования).

Рассмотрим содержимое uart.h. В самом начале там есть вот это:

#define UART1_ENABLE
#define UART2_ENABLE
#define UART3_ENABLE
//#define UART4_ENABLE

С помощью этих define-ов мы подключаем куски кода, которые отвечают за работу с соответствующим модулем UART. Тут следует обратить внимание на тот факт, что если в выбранном МК данный UART отсутствует, то необходимо закомментировать соответствующий define, иначе код не скомпилируется.

Далее идет следующее:

#ifdef UART1_ENABLE

#define UART1_USE_RING_BUFF
#define UART1_TXBUFF_LENGHT     16
#define UART1_RXBUFF_LENGHT     16

#endif

#ifdef UART2_ENABLE

#define UART2_USE_RING_BUFF
#define UART2_TXBUFF_LENGHT     16
#define UART2_RXBUFF_LENGHT     16

#endif


#ifdef UART3_ENABLE

#define UART3_USE_RING_BUFF
#define UART3_TXBUFF_LENGHT     16
#define UART3_RXBUFF_LENGHT     16

#endif

#ifdef UART4_ENABLE

#define UART4_USE_RING_BUFF
#define UART4_TXBUFF_LENGHT     16
#define UART4_RXBUFF_LENGHT     16

#endif

Тут куча define-ов, которые отвечают за тонкую настройку функциональности библиотеки.

Если мы хотим использовать кольцевой буфер, то нужно раскомментировать #define UARTn_USE_RING_BUFF, n — номер UART-a. Далее, можно настроить длину кольцевого буфера для приемника и передатчика через #define UARTn_TXBUFF_LENGHT и #define UARTn_RXBUFF_LENGHT, в данном примере длины буферов равны 16 байт.

Затем идет структура инициализации:

typedef struct
{
  uint32_t bus_freq;    //частота шины uart
  uint32_t baud;        //скорость передачи
  uint8_t data_bits;    //количество бит данных (8, 9)
  uint8_t stop_bits;    //количество стоп-бит (1 или 2)
  uint8_t parity;       //контроль четности (0 - нет, 1 - even, 2 - odd)
} UARTInitStructure_t;

и прототипы функций библиотеки:

//Инициализация UART
//
//id - номер порта
//init - структура инициализации UART
//
//Return: 0 - успех, -1 - ошибка инициализации
int16_t UART_Init(uint8_t id, const UARTInitStructure_t *init);

//Записать символ в буфер передатчика
//
//id - номер порта
//с - отправляемый символ
//
//Retrun: с - успех, -1 - ошибка, буфер переполнен
int16_t UART_PutC(uint8_t id, char c);

//Прочитать символ из буфера приемника
//
//id - номер порта
//
//Return: -1 - нет данных для чтения, 0..255 - прочитанный символ
int16_t UART_GetC(uint8_t id);

//Получить количество непрочитанных байт в буфере приемника
int16_t UART_BytesToRead(uint8_t id);

//Получить количество еще не отправленных байт в буфере передатчика
int16_t UART_BytesToWrite(uint8_t id);

//Очистить буфер приемника
void UART_ReadBuffClear(uint8_t id);

//Очистить буфер передатчика
void UART_WriteBuffClear(uint8_t id);

UART_Init() служит для инициализации, ее надо вызывать перед началом работы с UART-ом. UART_PutC() — отправить байт в UART, UART_GetC() — прочитать.

Далее идут функции работы с кольцевыми буферами.

UART_BytesToRead() — возвращает количество байт в буфере приемника, которые можно прочитать функцией UART_GetC().

UART_BytesToWrite() — этой функцией можно получить количество байт в буфере передатчика UART.

UART_ReadBuffClear() и UART_WriteBuffClear() — очистить буфер приемника и передатчика соответственно.

Вот и все функции ?

Внутреннее устройство

Полное описание библиотеки проводить не буду, остановлюсь только на функции инициализации UART. Она устроена примерно так:

int16_t UART_Init(uint8_t id, const UARTInitStructure_t *init)
{
  if(id == 1)
  {
    ....
    return 0;
  }

  if(id == 2)
  {
    ....
    return 0;
  }

  if(id == 3)
  {
    ....
    return 0;
  }

  ....
  return -1;
}

Для примера рассмотрим процесс инициализации UART1:

#ifdef UART1_ENABLE
  
  /*
  ПОРТЫ:
        DEFAULT  REMAP
  TX1     PA9     PB6
  RX1     PA10    PB7
  
  ШИНА:
  APB2
  */
  
  if(id == 1)
  {
    //Включаем тактирование модуля UART
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    
    //Настройка портов

    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
      
      //настройка TX (PA9)
    TxPinInit(&GPIOA->CRH, 
              GPIO_CRH_MODE9_Pos, 
              GPIO_CRH_CNF9_Pos);
      
      //настройка RX (PA10)
    RxPinInit(&GPIOA->CRH, 
              &GPIOA->BSRR, 
              GPIO_CRH_MODE10_Pos, 
              GPIO_CRH_CNF10_Pos, 
              10);
    
    //Сброс модуля
    RCC->APB2RSTR |= RCC_APB2RSTR_USART1RST;
    asm("nop");
    asm("nop");
    asm("nop");
    RCC->APB2RSTR &= ~RCC_APB2RSTR_USART1RST;
    asm("nop");
    asm("nop");
    asm("nop");

    //Инициализация основных регистров UART
    if(_uart_init(USART1, init) < 0)
      return -1;
    
    
#ifdef UART1_USE_RING_BUFF
    RingBuffInit(&tx_fifo1, tx_buff1, UART1_TXBUFF_LENGHT);
    RingBuffInit(&rx_fifo1, rx_buff1, UART1_RXBUFF_LENGHT);
    
    RXNEIEnable(USART1);
    
    NVIC_EnableIRQ(USART1_IRQn);
    
#endif
    
    //Запускаем UART
    _uart_en(USART1);
    
    
    
    return 0;
  }
  
#endif

Первой инструкцией в условии if(id == 1) { … } идет включение тактирования модуля UART1:

//Включаем тактирование модуля UART
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;

После этого преступаем к инициализации пинов, к которым подключены линии RX и TX:

//Настройка портов

RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
      
//настройка TX (PA9)
TxPinInit(&GPIOA->CRH, 
          GPIO_CRH_MODE9_Pos, 
          GPIO_CRH_CNF9_Pos);
      
//настройка RX (PA10)
RxPinInit(&GPIOA->CRH, 
          &GPIOA->BSRR, 
          GPIO_CRH_MODE10_Pos, 
          GPIO_CRH_CNF10_Pos, 
          10);

С помощью функции TxPinInit() мы инициализируем пин TX, а RxPinInit() пин RX. Данные функции получились не очень удобными, но у меня есть мысли, как это можно улучшить (хочу сделать нечто такое, как у DiHalt-а: http://easyelectronics.ru/udobnaya-rabota-s-gpio-na-stm32.html). По-умолчанию, используются пины без ремапа, если есть необходимость использовать ремап-пины, всю настройку нужно делать в этом блоке кода. Конечно, это не очень удобно, но пока как есть ?.

Далее следует инициализация UART-а:

//Сброс модуля
RCC->APB2RSR |= RCC_APB2RSTR_USART1RST;
asm("nop");
asm("nop");
asm("nop");
RCC->APB2RSTR &= ~RCC_APB2RSTR_USART1RST;
asm("nop");
asm("nop");
asm("nop");

//Инициализация основных регистров UART
if(_uart_init(USART1, init) < 0)
  return -1;

Первым делом выполняем сброс модуля UART через регисты RCC. Это сделано для «чистой» повторной инициализации модуля UART. Куча asm(«nop») на всякий случай, чтоб периферия успевала за стремительным выполнением программы в CPU. Мы же не используем разные HAL-ы, у нас же программа выполняется быстро? 😉

После этого вызываем функцию _uart_init(), и в случае неудачи экстренно выходим с кодом возврата -1.

Затем выполняем инициализацию кольцевого буфера, если он используется для данного UART-а:

#ifdef UART1_USE_RING_BUFF
  RingBuffInit(&tx_fifo1, tx_buff1, UART1_TXBUFF_LENGHT);
  RingBuffInit(&rx_fifo1, rx_buff1, UART1_RXBUFF_LENGHT);
    
  RXNEIEnable(USART1);
    
  NVIC_EnableIRQ(USART1_IRQn);
    
#endif

Ну и последний штрих: разрешаем работу UART и выходим с кодом возврата 0:

//Запускаем UART
_uart_en(USART1);
        
return 0;

На этом все, остальные UART-ы инициализируются схожим образом.

Пример работы с библиотекой

Изначально данная библиотека была написана для работы на микроконтроллере stm32f103c8, затем была доработана уже на stm32f103ve, и для написания статьи вновь был создан проект для stm32f103c8, при этом ни каких проблем это не вызвало. Скорее всего, проблем не будет при переносе этой библиотеки на любой микроконтроллер stm32f103.

Итак, перейдем к коду. Вот main.c:

#include <stdint.h>
#include "clock.h"
#include "uart.h"

static const UARTInitStructure_t UARTInitStr = 
{
  .bus_freq = 36000000,
  .baud = 19200,
  .data_bits = 8,
  .stop_bits = 1,
  .parity = 0,
};

void main()
{
  int16_t c;
  
  ClockInit();
  
  UART_Init(1, &UARTInitStr);
  
  UART_ReadBuffClear(1);
  UART_WriteBuffClear(1);
  
  for(;;)
  {
    c = UART_GetC(1);
    
    if(c != -1)
      UART_PutC(1, (char)c);
  }
}

Первым делом мы объявляем структуру, в которой содержится информация для инициализации UART-а, причем я объявил ее как const, чтоб она размещалась во flash-памяти и не отнимала место в ОЗУ микроконтроллера. Значение параметров структуры следующее:

  • bus_freq — частота шины, к которой подключен модуль UART, в данном случае 36МГц
  • baud — скорость передачи данных
  • data_bits — количество бит данных, 8 или 9
  • stop_bits — количество стоп-бит, 1 или 2
  • parity — контроль четности, 0 — нет, 1 — even, 2 — odd.

Тут есть небольшой нюанс. Если используется контроль четности (even или odd), и количество передаваемых бит равно 8, то data_bits надо выставить в 9 (8 бит данных + 1 бит четности, итого 9). Или это особенность STM32, или так принято у всех, я докапываться не стал, однако, данный момент описан в Referens Manual-е, хотя и найти его удалось только после того, когда знал, что искать ?

Переходим к main(). Первым делом инициализируем тактовый генератор с помощью ClockInit() на максимальную частоту работы. После этого инициализируем UART1:

UART_Init(1, &UARTInitStr);

В качестве аргументов передаем номер UART-а и структуру инициализации.

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

Заключение

На этом пока все, поздравляю всех с наступающим 0x07E4 годом! Желаю всем всего наилучшего, творческих успехов и удачи! До новых встреч! ???

Ссылки

Проект на GitHub: https://github.com/DiMoonElec/STM32UARTDriver

Метки: , , , , . Закладка Постоянная ссылка.

2 комментария: Программирование STM32. Часть 17: Драйвер UART

  1. Алексей пишет:

    Спасибо, Димон за твой труд. Всё работает!

  2. Алексей пишет:

    UART1 висит же на шине APB2, частота которой равна 72 МГц???

Добавить комментарий