Учебный курс. Организация обмена по USART `у с использованием кольцевого буфера

Вступление

   Реализация обмена данными по USART`у, которую мы рассматривали в предыдущей статье имеет некоторые недостатки. Функция отправки символа перед тем как инициировать передачу выполняет опрос флага UDRE регистра UCSRA в цикле while. А это значит, что микроконтроллер тратит свое драгоценное время на пустую работу. Прием данных осуществляется в однобайтовый буфер, при этом предыдущие данные затираются, если микроконтроллер не успевает их считывать.  
   Для простых приложений такого подхода вполне достаточно, но для сложных гораздо эффективнее организовать передачу (прием) данных используя кольцевой (циклический) буфер. Чтобы ответить на вопрос почему, давайте сначала разберемся, что из себя представляет этот буфер.

Кольцевой буфер

   Простая реализация кольцевого буфера включает в себя массив и три дополнительные переменные – указатель головы буфера, указатель хвоста буфера и счетчик символов, содержащихся в массиве.

#define SIZE_BUF 8

//кольцевой (циклический) буфер
unsigned char cycleBuf[SIZE_BUF];
unsigned char tail = 0;      //"указатель" хвоста буфера 
unsigned char head = 0;   //"указатель" головы буфера
unsigned char count = 0;  //счетчик символов

   Данные добавляются в “хвост” буфера, при этом "указатель" хвоста и счетчик символов увеличиваются на единицу. Переменная tail всегда указывает на следующую свободную ячейку буфера и может изменяться в диапазоне от 0 до SIZE_BUF – 1.
   Считываются (извлекаются) данные из “головы”, при этом индекс головы увеличивается на единицу, а счетчик символов уменьшается. Переменная head указывает на текущий считываемый символ и тоже изменяется в диапазоне от 0 до SIZE_BUF – 1.
   Буфер называется кольцевым, потому что данные в него могут добавляться с любой позиции до тех пор пока в нем есть место. Получается, что данные записываются как бы по кольцу. Посмотрите на рисунки, думаю станет понятно.
кольцевой (циклический) буфер пуст
Когда в кольцевом буфере нет данных, указатель головы равен указателю хвоста, а счетчик символов равен нулю.
в кольцевом (циклическом) буфере данные
Когда в буфер помещены данные, tail указывает на следующую пустую ячейку буфера, head на первый символ, а счетчик показывает сколько ячеек в буфере занято. При считывании данных из буфера изменяются только указатели и счетчик, содержимое массива не меняется. Данные помещаемые в буфер записываются "поверх" старых.
в кольцевом (циклическом) буфере данные
Указатели хвоста и головы могут принимать любые значения от 0 до SIZE_BUF - 1.

Для работы с буфером требуется как минимум три функции – очистить буфер, положить символ, взять символ.
   
//"очищает" буфер
void FlushBuf(void)
{
  tail = 0;
  head = 0;
  count = 0;
}

   Как вы видите, массив cycleBuf[] не очищается. Функция обнуляет "указатели" хвоста, головы и счетчик символов, но это равносильно тому, что в буфере нет данных.

//положить символ в буфер
void PutChar(unsigned char sym)
{
  if (count < SIZE_BUF){   //если в буфере еще есть место
      cycleBuf[tail] = sym;    //помещаем в него символ
      count++;                    //инкрементируем счетчик символов
      tail++;                           //и индекс хвоста буфера
      if (txBufTail == SIZE_BUF) txBufTail = 0;
    }
}

//взять символ из буфера

unsigned char GetChar(void)
{
   unsigned char sym = 0;
   if (count > 0){                            //если буфер не пустой
      sym = cycleBuf[head];              //считываем символ из буфера
      count--;                                   //уменьшаем счетчик символов
      head++;                                  //инкрементируем индекс головы буфера
      if (head == SIZE_BUF) head = 0;
   }
   return sym;
}

Как можно использовать кольцевой буфер

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

Обмен по USART/UART с использованием кольцевого буфера

За основу я взял предыдущий проект и существенно его переработал.
Добавил в функцию инициализации USART `а разрешение прерывания по завершению передачи. За это отвечает бит TXCIE регистра UCSRB.

Инициализация usart `a

void USART_Init(void)
{
  UBRRH = 0;
  UBRRL = 51; //скорость обмена 9600 бод

  //разр. прерыв при приеме и передачи, разр приема, разр передачи.
  UCSRB = (1<<RXCIE)|(1<<TXCIE)|(1<<RXEN)|(1<<TXEN);  

  //размер слова 8 разрядов
  UCSRC = (1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0);
}

  Объявил переменные для приемного и передающего кольцевого буфера и описал все функции для работы с ними. Рассмотрим только "передающую часть".

Передающий кольцевой буфер

unsigned char usartTxBuf[SIZE_BUF];
unsigned char txBufTail = 0;
unsigned char txBufHead = 0;
unsigned char txCount = 0;

Возможно не лишним будет добавить перед объявлением массива usartTxBuf[] и переменной txCount ключевое слово volatile, поскольку эти переменные используются и в основной программе и в прерываниях. Но проект работал без нареканий и я оставил так.

Функция очистки передающего буфера

void USART_FlushTxBuf(void)
{
  txBufTail = 0;
  txCount = 0;
  txBufHead = 0;
}
   В этой функции есть один нюанс! Переменная txCount специально обнуляется раньше txBufHead. Если  поменять их местами, могут возникнуть проблемы. Например, происходит передача данных, переменная txBufHead указывает на 4 элемент массива и в основном цикле программы мы запускаем эту функцию. Она обнуляет две переменные txBufTail и txBufHead и происходит прерывание по завершению передачи. Поскольку txCount еще не равна нулю, микроконтроллер будет извлекать данные из буфера. Но не из 4 ячейки массива, а из 0!
   По-хорошему это лечиться запрещением прерываний на время выполнения функции, например, с помощью ключевого слова __monitor

Функция загрузки символа в буфер

void USART_PutChar(unsigned char sym)
{
  if(((UCSRA & (1<<UDRE)) != 0) && (txCount == 0)) UDR = sym;
  else {
    if (txCount < SIZE_BUF){                    //если в буфере еще есть место
      usartTxBuf[txBufTail] = sym;             //помещаем в него символ
      txCount++;                                     //инкрементируем счетчик символов
      txBufTail++;                                    //и индекс хвоста буфера
      if (txBufTail == SIZE_BUF) txBufTail = 0;
    }
  }
}

   Если модуль USART не занят и передающий буфер пустой, символ записывается в регистр UDR. Это инициирует передачу данных и вызовет прерывание, когда модуль освободится.  В данной реализации символ будет помещаться в буфер, только если в нем есть свободное место.

Функция загрузки строки в буфер

void USART_SendStr(unsigned char * data)
{
  unsigned char sym;
  while(*data){
    sym = *data++;
    USART_PutChar(sym);
  }
}

  С подобной функцией мы уже имели дело, когда разбирались с lcd дисплеем. Функции передается указатель на строку и она посимвольно помещается в буфер с помощью функции USART_PutChar(sym). Цикл while выполняется пока не будет считан символ конца строки.

Обработчик прерывания по завершению передачи

#pragma vector=USART_TXC_vect
__interrupt void usart_txc_my(void)
{
  if (txCount > 0){                              //если буфер не пустой
    UDR = usartTxBuf[txBufHead];         //записываем в UDR символ из буфера
    txCount--;                                      //уменьшаем счетчик символов
    txBufHead++;                                 //инкрементируем индекс головы буфера
    if (txBufHead == SIZE_BUF) txBufHead = 0;
  }
}

Пока буфер содержит данные - обработчик прерывания будет считывать их из буфера и передавать по USART `у.

“Передающая часть” аналогична, только в прерывании данные принимаются, а для их чтения используется функция unsigned char USART_GetChar().
Полную версию программного модуля можно посмотреть по ссылкам ниже

usart.h
usart.c

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

Размышления

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

Файлы

У вас недостаточно прав для комментирования.