Делаем простой и универсальный кольцевой буфер.
Делаем простой кольцевой буфер
Постоянно приходится передавать и принимать данные из/в UART
, SPI
, I2C
и др. Всё просто и понятно когда посылки идут редко и есть время на их обработку, а что же делать если посылки могут идти пачками за один раз, а потом долгое время отсутствовать и мы при обработке первой не успеваем обрабатывать остальные и они теряются или портятся. Значит нужно использовать какой-то буфер, откуда потом понемногу брать и обрабатывать данные, а что делать если мы уже заполнили буфер и нам уже некуда писать? Писать дальше начиная с первого (на самом деле с нулевого) элемента буфера, дополнительно нужно учитывать место с которого можно читать данные и место с которого можно писать. Вот мы и получаем наш кольцевой буфер.
И так, мы уже определились, что нам нужно для нашего буфера, поэтому создадим структуру и определим её поля в соответствии с их предназначением:
typedef struct
{
uint8_t buffer[SIZE];
uint16_t idxIn;
uint16_t idxOut;
} RING_buffer_t;
Теперь появляется вопрос, а каким же размером должен быть наш буфер, ведь для одной задачи может хватить 10, а для другой и 256 не хватит, мы же хотим сделать себе простой и удобный буфер на все случаи жизни? Значит нужно определять размерность как-то иначе, пожалуй выделим память статически и отдадим указатель на неё, а саму нашу структуру немного изменим.
typedef struct
{
uint8_t *buffer;
uint16_t idxIn;
uint16_t idxOut;
uint16_t size;
} RING_buffer_t;
Теперь buffer
у нас стал указателем и добавилось новое поле size
, пусть будет 16 разрядным, не думаю, что для простого обмена по UART
может понадобиться буфер размером в 65 кбайт, а если понадобиться, значит стоит задуматься, возможно что-то делаем не так.
Теперь осталось определиться с функционалом нашего буфера, а именно, что мы можем с ним делать:
- Добавлять данные (побайтно);
- Забирать данные.
Еще хорошо бы иметь возможность:
- Узнать сколько у нас полезных даных;
- Подсмотреть какой-то байт в буфере;
- Очистить буфер;
- Инициализировать.
Описание функций
Пожалуй сразу сделаем прототипы наших функций:
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);
RING_ErrorStatus_t RING_Init(RING_buffer_t *ring, uint8_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;
}
Инициализация буфера.
RING_ErrorStatus_t RING_Init(RING_buffer_t *ring, uint8_t *buf, uint16_t size)
{
ring->size = size;
ring->buffer = buf;
RING_Clear( ring );
return ( ring->buffer ? RING_SUCCESS : RING_ERROR ) ;
}
Как этим пользоваться
Расположеный ниже код носит только ознакомитеьный характер
Шаг первый.
Необходимо определить переменную типа 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;
}
UPD Со времени публикации версия на gitlab изменилась, будьте внимательны.