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

Делаем простой кольцевой буфер

Постоянно приходится передавать и принимать данные из/в UART, SPI, I2C и др. Всё просто и понятно когда посылки идут редко и есть время на их обработку, а что же делать если посылки могут идти пачками за один раз, а потом долгое время отсутствовать и мы при обработке первой не успеваем обрабатывать остальные и они теряются или портятся. Значит нужно использовать какой-то буфер, откуда потом понемногу брать и обрабатывать данные, а что делать если мы уже заполнили буфер и нам уже некуда писать? Писать дальше начиная с первого (на самом деле с нулевого) элемента буфера, дополнительно нужно учитывать место с которого можно читать данные и место с которого можно писать. Вот мы и получаем наш кольцевой буфер.

И так, мы уже определились, что нам нужно для нашего буфера, поэтому создадим структуру и определим её поля в соответствии с их предназначением:

typedef struct
{
    uint8_t buffer[SIZE];
    uint16_t idxIn;
    uint16_t idxOut;
} RING_buffer_t;

Теперь появляется вопрос, а каким же размером должен быть наш буфер, ведь для одной задачи может хватить 10, а для другой и 256 не хватит, мы же хотим сделать себе простой и удобный буфер на все случаи жизни? Значит нужно определять размерность как-то иначе, пожалуй выделим её с помощью malloc(size) а саму нашу структуру немного изменим.

typedef struct
{
    uint8_t *buffer;
    uint16_t idxIn;
    uint16_t idxOut;
    uint16_t size;
} RING_buffer_t;

Теперь buffer у нас стал указателем и добавилось новое поле size, пусть будет 16 разрядным, не думаю, что для простого обмена по UART может понадобиться буфер размером в 65000 байт, а если понадобиться, значит стоит задуматься, возможно что-то делаем не так.

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

  • Добавлять данные (побайтно);
  • Забирать данные.

Еще хорошо бы иметь возможность:

  • Узнать сколько у нас полезных даных;
  • Подсмотреть какой-то байт в буфере;
  • Очистить буфер;
  • Инициализировать, нужно ведь где-то обозначить размер массива *buffer

Описание функций

Пожалуй сразу сделаем прототипы наших функций:

void RING_Put(uint8_t symbol, RING_buffer_t* buf);
uint8_t RING_Pop(RING_buffer_t *buf);
uint16_t RING_GetCount(RING_buffer_t *buf);
int32_t RING_ShowSymbol(uint16_t symbolNumber ,RING_buffer_t *buf);
void RING_Clear(RING_buffer_t* buf);
void RING_Init(RING_buffer_t *buf, uint16_t size);

Добавить данные.

Для добавления данных нужно сделать несколько действий. Само добавление и изменение поля idxIn, что бы значть куда писать в следующий раз, попутно исключить ситуазию выхода за границу массива (buf->idxIn >= buf->size). Код функции будет следующим:

void RING_Put(uint8_t symbol, RING_buffer_t* buf)
{
    buf->buffer[buf->idxIn++] = symbol;
    if (buf->idxIn >= buf->size) buf->idxIn = 0;
}

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

Получить данные.

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

uint8_t RING_Pop(RING_buffer_t *buf)
{
    uint8_t retval = buf->buffer[buf->idxOut++];
    if (buf->idxOut >= buf->size) buf->idxOut = 0;
    return retval;
}

Узнаём сколько у нас полезных данных.

Здесь на первый взгляд всё просто. Нужно из конца (место откуда пишем) вычесть начало (место откуда читаем), оно так только о одном случае, если у нас указатель на запись стоит после указателя на чтение. Если это не так, то мы получим не правильный ответ, тут логика немного сложнее, но не сильно. Предлагаю взглянуть на код и разобраться самостоятельно.

uint16_t RING_GetCount(RING_buffer_t *buf)
{
    uint16_t retval = 0;
    if (buf->idxIn < buf->idxOut) retval = buf->size + buf->idxIn - buf->idxOut;
    else retval = buf->idxIn - buf->idxOut;
    return retval;
}

Подсматриваем байт.

Функция RING_Pop меняет указатель на место чтения, по сути удаляя прочитанный элемент, н оиногда требуется просто посмотреть значение без его удаления, тогда делаем так.

int32_t RING_ShowSymbol(uint16_t symbolNumber ,RING_buffer_t *buf)
{
    uint32_t pointer = buf->idxOut + symbolNumber;
    int32_t  retval = -1;
    if (symbolNumber < RING_GetCount(buf))
    {
        if (pointer > buf->size) pointer -= buf->size;
        retval = buf->buffer[ pointer ] ;
    }
    return retval;
}

Здесь symbolNumber - это смещение относительно начала буфера (точки для чтения). Если вы внимательно читали код, то заметили что эта функция в отличии от RING_Pop имеет знаковый 32-х битный тип данных (int32_t) это сделано для того, что бы мы в последствии могли определить, а не пытаемся ли мы подсмотреть ячейку которой не существует, если да, то функция вернёт -1, в случае правильного выбора номера элемента мы получим его значение.

Очистить буфер.

Нет, мы не будем забивать весь массив нулями или чем-либо ещё, это не нужно. Достаточно просто обнулить указатели на чтение и запись.

void RING_Clear(RING_buffer_t* buf)
{
    buf->idxIn = 0;
    buf->idxOut = 0;
}

Инициализация буфера.

Здесь мы устанавливаем значение поля size и выделяем память под массив с помощью malloc(), о чем писал в самом начале заметки. И обнуляем указатели чтения и записи.

void RING_Init(RING_buffer_t *buf, uint16_t size)
{
    buf->size = size;
    buf->buffer = (uint8_t*) malloc(size);
    RING_Clear(buf);
}

Как этим пользоваться

Расположеный ниже код носит только ознакомитеьный характер

Шаг первый.

Необходимо определить переменную типа RING_buffer_t и инициализировать размер буфера.

RING_buffer_t ring_Rx;

int main( void )
{
    RING_Init(&ring_Rx, 256); // Пусть будет 256 байт -  0..255!
}

Шаг второй.

Добавляем элементы в буфер, например в прерывании от UART.

void USART2_IRQHandler(void)
{
    USART_ClearITPendingBit(USART2, USART_IT_RXNE);
    RING_Put(USART_ReceiveData(USART2), &ring_Rx);
    data.flag_off = false;
}

Шаг третий.

Обрабатываем полученные данные, тут позволю чистателю самому придумать реализацию.

Где взять исходнии? Или нужно самому всё писать?

Конечно же лучше написать всё самостоятельно, так можно и получше разобраться и возможно найти косяки в моём коде. Особо ленивые могут заглянуть на gitlab или скачать мой пакет для keil, в котором есть этот модуль - devprodest.Lib.pack.

Ну и так, как я не считаю лень плохим качеством, по ссылкам можно получить бонус в виде вычисления CRC16 ccitt для кольцевого буфера. А для совсем уж ленивых вот оно:

uint16_t RING_CRC16ccitt(RING_buffer_t *buf, uint16_t lenght, uint16_t position)
{
    return RING_CRC16ccitt_Intermediate( buf, lenght, 0xFFFF, position);
}

uint16_t RING_CRC16ccitt_Intermediate(RING_buffer_t *buf, uint16_t lenght, uint16_t tmpCrc, uint16_t position)
{
    uint16_t crc = tmpCrc;
    uint16_t crctab;
    uint8_t byte;

    while (lenght--)
    {
        crctab = 0x0000;
        byte = (RING_ShowSymbol(lenght + position, buf))^( crc >> 8 );
        if( byte & 0x01 ) crctab ^= 0x1021;
        if( byte & 0x02 ) crctab ^= 0x2042;
        if( byte & 0x04 ) crctab ^= 0x4084;
        if( byte & 0x08 ) crctab ^= 0x8108;
        if( byte & 0x10 ) crctab ^= 0x1231;
        if( byte & 0x20 ) crctab ^= 0x2462;
        if( byte & 0x40 ) crctab ^= 0x48C4;
        if( byte & 0x80 ) crctab ^= 0x9188;

        crc = ( ( (crc & 0xFF)^(crctab >> 8) ) << 8 ) | ( crctab & 0xFF );
    }
    return crc;
}