feat(protocan-transport): транспортный уровень ProtoCAN и каталог GUI
Перенесён из репозитория protocan-transport, который подключался сабмодулем в CAN_to_RS485. Кадрирование AA 55 с CRC16 поверх любого байтового потока (RS485, RS232, USB CDC), разбор 29-битного идентификатора, общее адресное пространство регистров и каталог с подпиской на поток значений для SETGUI. Состояние живёт в структурах вызывающего, поэтому в одной прошивке поднимается сколько угодно независимых каналов. Порт STM32F4 (USART + DMA) в комплекте. Хостовые тесты test_transport и test_gui проходят.
This commit is contained in:
195
c/protocan-transport/src/gui_catalog.c
Normal file
195
c/protocan-transport/src/gui_catalog.c
Normal file
@@ -0,0 +1,195 @@
|
||||
#include "gui_catalog.h"
|
||||
|
||||
static size_t name_len(const char *name)
|
||||
{
|
||||
size_t n = 0U;
|
||||
if (name == NULL) {
|
||||
return 0U;
|
||||
}
|
||||
while ((name[n] != '\0') && (n <= GUI_NAME_SIZE)) {
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static uint8_t object_registers(uint8_t type)
|
||||
{
|
||||
return ((type == GUI_OBJ_U32) || (type == GUI_OBJ_I32)) ? 2U : 1U;
|
||||
}
|
||||
|
||||
bool gui_catalog_validate(const gui_catalog_t *catalog, const pcan_gas_map_t *map)
|
||||
{
|
||||
if ((catalog == NULL) || ((catalog->count != 0U) && (catalog->items == NULL))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint16_t i = 0U; i < catalog->count; i++) {
|
||||
const gui_object_t *item = &catalog->items[i];
|
||||
if (name_len(item->name) > GUI_NAME_SIZE) {
|
||||
return false;
|
||||
}
|
||||
if (item->type > GUI_OBJ_BITS) {
|
||||
return false;
|
||||
}
|
||||
if (map != NULL) {
|
||||
uint8_t words = object_registers(item->type);
|
||||
for (uint8_t w = 0U; w < words; w++) {
|
||||
uint32_t addr = (uint32_t)item->address + w;
|
||||
if (addr > 0xFFFFUL) {
|
||||
return false;
|
||||
}
|
||||
if (pcan_gas_find(map, (uint16_t)addr) == NULL) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t gui_catalog_encode(const gui_catalog_t *catalog, uint16_t start_index,
|
||||
uint16_t max_count, uint8_t *out, size_t out_size)
|
||||
{
|
||||
if ((catalog == NULL) || (out == NULL) || (start_index >= catalog->count)) {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
uint16_t available = (uint16_t)(catalog->count - start_index);
|
||||
uint16_t count = ((max_count == 0U) || (max_count > available)) ? available : max_count;
|
||||
if (count > GUI_ENTRIES_PER_FRAME) {
|
||||
count = (uint16_t)GUI_ENTRIES_PER_FRAME;
|
||||
}
|
||||
size_t total = (size_t)GUI_CATALOG_HEADER + ((size_t)count * GUI_ENTRY_SIZE);
|
||||
if (out_size < total) {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
gui_put_u16(&out[0], catalog->count);
|
||||
gui_put_u16(&out[2], start_index);
|
||||
gui_put_u16(&out[4], count);
|
||||
|
||||
for (uint16_t i = 0U; i < count; i++) {
|
||||
const gui_object_t *item = &catalog->items[start_index + i];
|
||||
uint8_t *dst = &out[GUI_CATALOG_HEADER + ((size_t)i * GUI_ENTRY_SIZE)];
|
||||
|
||||
gui_put_u16(&dst[0], item->address);
|
||||
dst[2] = item->type;
|
||||
dst[3] = item->flags;
|
||||
dst[4] = (uint8_t)item->scale_pow10;
|
||||
dst[5] = item->unit;
|
||||
dst[6] = 0U;
|
||||
dst[7] = 0U;
|
||||
|
||||
size_t n = name_len(item->name);
|
||||
if (n > GUI_NAME_SIZE) {
|
||||
n = GUI_NAME_SIZE;
|
||||
}
|
||||
for (size_t c = 0U; c < n; c++) {
|
||||
dst[8U + c] = (uint8_t)item->name[c];
|
||||
}
|
||||
for (size_t c = n; c < GUI_NAME_SIZE; c++) {
|
||||
dst[8U + c] = 0U;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
void gui_watch_init(gui_watch_t *watch)
|
||||
{
|
||||
watch->period_ms = 0U;
|
||||
watch->count = 0U;
|
||||
watch->next_ms = 0U;
|
||||
watch->sent = 0U;
|
||||
watch->skipped = 0U;
|
||||
}
|
||||
|
||||
uint16_t gui_watch_apply(gui_watch_t *watch, const pcan_gas_map_t *map,
|
||||
const uint8_t *payload, uint16_t size)
|
||||
{
|
||||
if (size < 4U) {
|
||||
return 0xFFFFU;
|
||||
}
|
||||
uint16_t period = gui_get_u16(&payload[0]);
|
||||
uint16_t requested = gui_get_u16(&payload[2]);
|
||||
if (size != (uint16_t)(4U + (requested * 2U))) {
|
||||
return 0xFFFFU;
|
||||
}
|
||||
if (requested > GUI_WATCH_MAX) {
|
||||
return 0xFFFFU;
|
||||
}
|
||||
|
||||
uint16_t accepted = 0U;
|
||||
for (uint16_t i = 0U; i < requested; i++) {
|
||||
uint16_t address = gui_get_u16(&payload[4U + (i * 2U)]);
|
||||
/* Адрес вне карты молча пропускаем: иначе одна опечатка в GUI
|
||||
отменяла бы всю подписку. */
|
||||
if ((map != NULL) && (pcan_gas_find(map, address) == NULL)) {
|
||||
continue;
|
||||
}
|
||||
watch->address[accepted++] = address;
|
||||
}
|
||||
|
||||
watch->count = accepted;
|
||||
watch->period_ms = (accepted == 0U) ? 0U : period;
|
||||
watch->next_ms = 0U;
|
||||
return accepted;
|
||||
}
|
||||
|
||||
size_t gui_watch_encode_ack(const gui_watch_t *watch, uint8_t *out, size_t out_size)
|
||||
{
|
||||
if (out_size < 4U) {
|
||||
return 0U;
|
||||
}
|
||||
gui_put_u16(&out[0], watch->period_ms);
|
||||
gui_put_u16(&out[2], watch->count);
|
||||
return 4U;
|
||||
}
|
||||
|
||||
bool gui_watch_due(const gui_watch_t *watch, uint32_t now_ms)
|
||||
{
|
||||
if ((watch->period_ms == 0U) || (watch->count == 0U)) {
|
||||
return false;
|
||||
}
|
||||
/* Разность беззнаковая: переполнение счётчика миллисекунд обрабатывается
|
||||
само собой, сравнение с абсолютным временем - нет. */
|
||||
return (uint32_t)(now_ms - watch->next_ms) < 0x80000000UL;
|
||||
}
|
||||
|
||||
size_t gui_watch_encode_data(const gui_watch_t *watch, const pcan_gas_map_t *map,
|
||||
uint32_t timestamp_ms, uint8_t *out, size_t out_size)
|
||||
{
|
||||
if ((watch->count == 0U) || (out == NULL)) {
|
||||
return 0U;
|
||||
}
|
||||
size_t total = 6U + ((size_t)watch->count * 2U);
|
||||
if (out_size < total) {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
gui_put_u32(&out[0], timestamp_ms);
|
||||
gui_put_u16(&out[4], watch->count);
|
||||
for (uint16_t i = 0U; i < watch->count; i++) {
|
||||
uint16_t value = 0U;
|
||||
/* Регистр мог исчезнуть между подпиской и опросом - тогда 0,
|
||||
а не мусор из стека. */
|
||||
(void)pcan_gas_read(map, watch->address[i], &value);
|
||||
gui_put_u16(&out[6U + ((size_t)i * 2U)], value);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
void gui_watch_advance(gui_watch_t *watch, uint32_t now_ms)
|
||||
{
|
||||
if (watch->period_ms == 0U) {
|
||||
return;
|
||||
}
|
||||
uint32_t next = watch->next_ms + watch->period_ms;
|
||||
/* Если отстали больше чем на такт (линия была занята), не пытаемся
|
||||
нагнать пачкой пакетов - идём от текущего момента. */
|
||||
if ((uint32_t)(now_ms - next) >= 0x80000000UL) {
|
||||
watch->next_ms = next;
|
||||
} else {
|
||||
watch->next_ms = now_ms + watch->period_ms;
|
||||
}
|
||||
watch->sent++;
|
||||
}
|
||||
230
c/protocan-transport/src/gui_frame.c
Normal file
230
c/protocan-transport/src/gui_frame.c
Normal file
@@ -0,0 +1,230 @@
|
||||
#include "gui_frame.h"
|
||||
|
||||
uint32_t gui_crc32(const uint8_t *data, size_t len)
|
||||
{
|
||||
uint32_t crc = 0xFFFFFFFFUL;
|
||||
for (size_t i = 0U; i < len; i++) {
|
||||
crc ^= data[i];
|
||||
for (uint8_t bit = 0U; bit < 8U; bit++) {
|
||||
crc = (crc & 1UL) ? ((crc >> 1) ^ 0xEDB88320UL) : (crc >> 1);
|
||||
}
|
||||
}
|
||||
return crc ^ 0xFFFFFFFFUL;
|
||||
}
|
||||
|
||||
size_t gui_frame_encode(uint8_t type, uint16_t sequence,
|
||||
const uint8_t *payload, uint16_t size,
|
||||
uint8_t *out, size_t out_size)
|
||||
{
|
||||
if ((out == NULL) || (size > GUI_MAX_PAYLOAD) ||
|
||||
((size > 0U) && (payload == NULL))) {
|
||||
return 0U;
|
||||
}
|
||||
size_t total = (size_t)GUI_HEADER_SIZE + size + GUI_CRC_SIZE;
|
||||
if (out_size < total) {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
out[0] = GUI_SOF0;
|
||||
out[1] = GUI_SOF1;
|
||||
/* CRC считается от version до конца payload; SOF в него не входит. */
|
||||
out[2] = GUI_PROTOCOL_VERSION;
|
||||
out[3] = type;
|
||||
out[4] = (uint8_t)(sequence >> 8); /* заголовок big-endian */
|
||||
out[5] = (uint8_t)(sequence & 0xFFU);
|
||||
out[6] = (uint8_t)(size >> 8);
|
||||
out[7] = (uint8_t)(size & 0xFFU);
|
||||
for (uint16_t i = 0U; i < size; i++) {
|
||||
out[GUI_HEADER_SIZE + i] = payload[i];
|
||||
}
|
||||
|
||||
uint32_t crc = gui_crc32(&out[2], (size_t)6U + size);
|
||||
gui_put_u32(&out[GUI_HEADER_SIZE + size], crc); /* CRC little-endian */
|
||||
return total;
|
||||
}
|
||||
|
||||
void gui_parser_init(gui_parser_t *p)
|
||||
{
|
||||
p->state = GUI_ST_SOF0;
|
||||
p->idx = 0U;
|
||||
p->size = 0U;
|
||||
p->stats.frames = 0U;
|
||||
p->stats.crc_errors = 0U;
|
||||
p->stats.version_errors = 0U;
|
||||
p->stats.length_errors = 0U;
|
||||
p->stats.stray_bytes = 0U;
|
||||
}
|
||||
|
||||
bool gui_parser_push(gui_parser_t *p, uint8_t byte, gui_frame_t *out)
|
||||
{
|
||||
switch (p->state) {
|
||||
case GUI_ST_SOF0:
|
||||
if (byte == GUI_SOF0) {
|
||||
p->state = GUI_ST_SOF1;
|
||||
} else {
|
||||
p->stats.stray_bytes++;
|
||||
}
|
||||
break;
|
||||
|
||||
case GUI_ST_SOF1:
|
||||
if (byte == GUI_SOF1) {
|
||||
p->idx = 0U;
|
||||
p->state = GUI_ST_HEADER;
|
||||
} else if (byte == GUI_SOF0) {
|
||||
/* A5 A5 5A - тоже валидное начало, остаёмся здесь. */
|
||||
p->stats.stray_bytes++;
|
||||
} else {
|
||||
p->stats.stray_bytes += 2U;
|
||||
p->state = GUI_ST_SOF0;
|
||||
}
|
||||
break;
|
||||
|
||||
case GUI_ST_HEADER:
|
||||
/* Версию проверяем сразу на первом байте заголовка. Если ждать все
|
||||
шесть, ложная сигнатура успеет проглотить начало настоящего кадра
|
||||
и он будет потерян целиком. */
|
||||
if ((p->idx == 0U) && (byte != GUI_PROTOCOL_VERSION)) {
|
||||
p->stats.version_errors++;
|
||||
p->state = (byte == GUI_SOF0) ? GUI_ST_SOF1 : GUI_ST_SOF0;
|
||||
break;
|
||||
}
|
||||
p->header[p->idx++] = byte;
|
||||
if (p->idx < 6U) {
|
||||
break;
|
||||
}
|
||||
p->size = (uint16_t)(((uint16_t)p->header[4] << 8) | p->header[5]);
|
||||
if (p->size > GUI_RX_PAYLOAD_MAX) {
|
||||
/* Либо мусор, либо кадр, который прибор обслужить не может.
|
||||
И то и другое - повод вернуться к поиску сигнатуры. */
|
||||
p->stats.length_errors++;
|
||||
p->state = GUI_ST_SOF0;
|
||||
break;
|
||||
}
|
||||
p->idx = 0U;
|
||||
p->state = (p->size > 0U) ? GUI_ST_PAYLOAD : GUI_ST_CRC;
|
||||
break;
|
||||
|
||||
case GUI_ST_PAYLOAD:
|
||||
p->payload[p->idx++] = byte;
|
||||
if (p->idx >= p->size) {
|
||||
p->idx = 0U;
|
||||
p->state = GUI_ST_CRC;
|
||||
}
|
||||
break;
|
||||
|
||||
case GUI_ST_CRC: {
|
||||
p->crc_bytes[p->idx++] = byte;
|
||||
if (p->idx < GUI_CRC_SIZE) {
|
||||
break;
|
||||
}
|
||||
p->state = GUI_ST_SOF0;
|
||||
|
||||
uint32_t received = ((uint32_t)p->crc_bytes[0])
|
||||
| ((uint32_t)p->crc_bytes[1] << 8)
|
||||
| ((uint32_t)p->crc_bytes[2] << 16)
|
||||
| ((uint32_t)p->crc_bytes[3] << 24);
|
||||
|
||||
/* CRC защищает header[0..5] и payload; считаем по частям,
|
||||
чтобы не собирать кадр целиком во втором буфере. */
|
||||
uint32_t crc = 0xFFFFFFFFUL;
|
||||
for (uint8_t i = 0U; i < 6U; i++) {
|
||||
crc ^= p->header[i];
|
||||
for (uint8_t bit = 0U; bit < 8U; bit++) {
|
||||
crc = (crc & 1UL) ? ((crc >> 1) ^ 0xEDB88320UL) : (crc >> 1);
|
||||
}
|
||||
}
|
||||
for (uint16_t i = 0U; i < p->size; i++) {
|
||||
crc ^= p->payload[i];
|
||||
for (uint8_t bit = 0U; bit < 8U; bit++) {
|
||||
crc = (crc & 1UL) ? ((crc >> 1) ^ 0xEDB88320UL) : (crc >> 1);
|
||||
}
|
||||
}
|
||||
crc ^= 0xFFFFFFFFUL;
|
||||
|
||||
if (crc != received) {
|
||||
p->stats.crc_errors++;
|
||||
return false;
|
||||
}
|
||||
out->type = p->header[1];
|
||||
out->sequence = (uint16_t)(((uint16_t)p->header[2] << 8) | p->header[3]);
|
||||
out->size = p->size;
|
||||
out->payload = p->payload;
|
||||
p->stats.frames++;
|
||||
return true;
|
||||
}
|
||||
|
||||
default:
|
||||
p->state = GUI_ST_SOF0;
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t gui_parser_feed(gui_parser_t *p, const uint8_t *data, size_t len,
|
||||
gui_frame_cb_t cb, void *user)
|
||||
{
|
||||
gui_frame_t frame;
|
||||
size_t count = 0U;
|
||||
|
||||
for (size_t i = 0U; i < len; i++) {
|
||||
if (gui_parser_push(p, data[i], &frame)) {
|
||||
count++;
|
||||
if (cb != NULL) {
|
||||
cb(&frame, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool gui_link_init(gui_link_t *link, const pcan_io_t *io,
|
||||
gui_frame_cb_t on_frame, void *user)
|
||||
{
|
||||
if ((link == NULL) || (io == NULL) || (io->write == NULL)) {
|
||||
return false;
|
||||
}
|
||||
link->io = *io;
|
||||
link->on_frame = on_frame;
|
||||
link->user = user;
|
||||
link->tx_frames = 0U;
|
||||
link->tx_dropped = 0U;
|
||||
gui_parser_init(&link->parser);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool gui_link_can_send(const gui_link_t *link, uint16_t size)
|
||||
{
|
||||
if (link->io.tx_space == NULL) {
|
||||
return true;
|
||||
}
|
||||
return link->io.tx_space(link->io.ctx) >= ((size_t)GUI_HEADER_SIZE + size + GUI_CRC_SIZE);
|
||||
}
|
||||
|
||||
bool gui_link_send(gui_link_t *link, uint8_t type, uint16_t sequence,
|
||||
const uint8_t *payload, uint16_t size)
|
||||
{
|
||||
size_t n = gui_frame_encode(type, sequence, payload, size,
|
||||
link->tx_buf, sizeof link->tx_buf);
|
||||
if (n == 0U) {
|
||||
link->tx_dropped++;
|
||||
return false;
|
||||
}
|
||||
if (link->io.write(link->io.ctx, link->tx_buf, n) != n) {
|
||||
link->tx_dropped++;
|
||||
return false;
|
||||
}
|
||||
link->tx_frames++;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool gui_link_nack(gui_link_t *link, uint16_t sequence, uint16_t result)
|
||||
{
|
||||
uint8_t payload[2];
|
||||
gui_put_u16(payload, result);
|
||||
return gui_link_send(link, GUI_MSG_NACK, sequence, payload, sizeof payload);
|
||||
}
|
||||
|
||||
size_t gui_link_feed(gui_link_t *link, const uint8_t *data, size_t len)
|
||||
{
|
||||
return gui_parser_feed(&link->parser, data, len, link->on_frame, link->user);
|
||||
}
|
||||
66
c/protocan-transport/src/pcan_crc.c
Normal file
66
c/protocan-transport/src/pcan_crc.c
Normal file
@@ -0,0 +1,66 @@
|
||||
#include "pcan_crc.h"
|
||||
|
||||
#if PCAN_CRC_TABLE
|
||||
|
||||
static const uint16_t s_table[256] = {
|
||||
0x0000U, 0x1021U, 0x2042U, 0x3063U, 0x4084U, 0x50A5U, 0x60C6U, 0x70E7U,
|
||||
0x8108U, 0x9129U, 0xA14AU, 0xB16BU, 0xC18CU, 0xD1ADU, 0xE1CEU, 0xF1EFU,
|
||||
0x1231U, 0x0210U, 0x3273U, 0x2252U, 0x52B5U, 0x4294U, 0x72F7U, 0x62D6U,
|
||||
0x9339U, 0x8318U, 0xB37BU, 0xA35AU, 0xD3BDU, 0xC39CU, 0xF3FFU, 0xE3DEU,
|
||||
0x2462U, 0x3443U, 0x0420U, 0x1401U, 0x64E6U, 0x74C7U, 0x44A4U, 0x5485U,
|
||||
0xA56AU, 0xB54BU, 0x8528U, 0x9509U, 0xE5EEU, 0xF5CFU, 0xC5ACU, 0xD58DU,
|
||||
0x3653U, 0x2672U, 0x1611U, 0x0630U, 0x76D7U, 0x66F6U, 0x5695U, 0x46B4U,
|
||||
0xB75BU, 0xA77AU, 0x9719U, 0x8738U, 0xF7DFU, 0xE7FEU, 0xD79DU, 0xC7BCU,
|
||||
0x48C4U, 0x58E5U, 0x6886U, 0x78A7U, 0x0840U, 0x1861U, 0x2802U, 0x3823U,
|
||||
0xC9CCU, 0xD9EDU, 0xE98EU, 0xF9AFU, 0x8948U, 0x9969U, 0xA90AU, 0xB92BU,
|
||||
0x5AF5U, 0x4AD4U, 0x7AB7U, 0x6A96U, 0x1A71U, 0x0A50U, 0x3A33U, 0x2A12U,
|
||||
0xDBFDU, 0xCBDCU, 0xFBBFU, 0xEB9EU, 0x9B79U, 0x8B58U, 0xBB3BU, 0xAB1AU,
|
||||
0x6CA6U, 0x7C87U, 0x4CE4U, 0x5CC5U, 0x2C22U, 0x3C03U, 0x0C60U, 0x1C41U,
|
||||
0xEDAEU, 0xFD8FU, 0xCDECU, 0xDDCDU, 0xAD2AU, 0xBD0BU, 0x8D68U, 0x9D49U,
|
||||
0x7E97U, 0x6EB6U, 0x5ED5U, 0x4EF4U, 0x3E13U, 0x2E32U, 0x1E51U, 0x0E70U,
|
||||
0xFF9FU, 0xEFBEU, 0xDFDDU, 0xCFFCU, 0xBF1BU, 0xAF3AU, 0x9F59U, 0x8F78U,
|
||||
0x9188U, 0x81A9U, 0xB1CAU, 0xA1EBU, 0xD10CU, 0xC12DU, 0xF14EU, 0xE16FU,
|
||||
0x1080U, 0x00A1U, 0x30C2U, 0x20E3U, 0x5004U, 0x4025U, 0x7046U, 0x6067U,
|
||||
0x83B9U, 0x9398U, 0xA3FBU, 0xB3DAU, 0xC33DU, 0xD31CU, 0xE37FU, 0xF35EU,
|
||||
0x02B1U, 0x1290U, 0x22F3U, 0x32D2U, 0x4235U, 0x5214U, 0x6277U, 0x7256U,
|
||||
0xB5EAU, 0xA5CBU, 0x95A8U, 0x8589U, 0xF56EU, 0xE54FU, 0xD52CU, 0xC50DU,
|
||||
0x34E2U, 0x24C3U, 0x14A0U, 0x0481U, 0x7466U, 0x6447U, 0x5424U, 0x4405U,
|
||||
0xA7DBU, 0xB7FAU, 0x8799U, 0x97B8U, 0xE75FU, 0xF77EU, 0xC71DU, 0xD73CU,
|
||||
0x26D3U, 0x36F2U, 0x0691U, 0x16B0U, 0x6657U, 0x7676U, 0x4615U, 0x5634U,
|
||||
0xD94CU, 0xC96DU, 0xF90EU, 0xE92FU, 0x99C8U, 0x89E9U, 0xB98AU, 0xA9ABU,
|
||||
0x5844U, 0x4865U, 0x7806U, 0x6827U, 0x18C0U, 0x08E1U, 0x3882U, 0x28A3U,
|
||||
0xCB7DU, 0xDB5CU, 0xEB3FU, 0xFB1EU, 0x8BF9U, 0x9BD8U, 0xABBBU, 0xBB9AU,
|
||||
0x4A75U, 0x5A54U, 0x6A37U, 0x7A16U, 0x0AF1U, 0x1AD0U, 0x2AB3U, 0x3A92U,
|
||||
0xFD2EU, 0xED0FU, 0xDD6CU, 0xCD4DU, 0xBDAAU, 0xAD8BU, 0x9DE8U, 0x8DC9U,
|
||||
0x7C26U, 0x6C07U, 0x5C64U, 0x4C45U, 0x3CA2U, 0x2C83U, 0x1CE0U, 0x0CC1U,
|
||||
0xEF1FU, 0xFF3EU, 0xCF5DU, 0xDF7CU, 0xAF9BU, 0xBFBAU, 0x8FD9U, 0x9FF8U,
|
||||
0x6E17U, 0x7E36U, 0x4E55U, 0x5E74U, 0x2E93U, 0x3EB2U, 0x0ED1U, 0x1EF0U
|
||||
};
|
||||
|
||||
uint16_t pcan_crc16_update(uint16_t crc, uint8_t byte)
|
||||
{
|
||||
return (uint16_t)((crc << 8) ^ s_table[(uint8_t)((crc >> 8) ^ byte)]);
|
||||
}
|
||||
|
||||
#else /* побитовый расчёт: 0 байт таблиц, ~8 итераций на байт */
|
||||
|
||||
uint16_t pcan_crc16_update(uint16_t crc, uint8_t byte)
|
||||
{
|
||||
crc ^= (uint16_t)((uint16_t)byte << 8);
|
||||
for (uint8_t bit = 0U; bit < 8U; bit++) {
|
||||
crc = (crc & 0x8000U) ? (uint16_t)((crc << 1) ^ 0x1021U)
|
||||
: (uint16_t)(crc << 1);
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
uint16_t pcan_crc16(const uint8_t *data, size_t len)
|
||||
{
|
||||
uint16_t crc = PCAN_CRC_INIT;
|
||||
for (size_t i = 0U; i < len; i++) {
|
||||
crc = pcan_crc16_update(crc, data[i]);
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
141
c/protocan-transport/src/pcan_frame.c
Normal file
141
c/protocan-transport/src/pcan_frame.c
Normal file
@@ -0,0 +1,141 @@
|
||||
#include "pcan_frame.h"
|
||||
#include "pcan_crc.h"
|
||||
|
||||
size_t pcan_frame_encode(const pcan_frame_t *frame, uint8_t *out, size_t out_size)
|
||||
{
|
||||
if ((frame == NULL) || (out == NULL) || (out_size < PCAN_FRAME_MAX)) {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
uint8_t dlc = (frame->dlc > PCAN_DATA_MAX) ? (uint8_t)PCAN_DATA_MAX : frame->dlc;
|
||||
size_t n = 0U;
|
||||
|
||||
out[n++] = PCAN_SOF0;
|
||||
out[n++] = PCAN_SOF1;
|
||||
|
||||
size_t body = n;
|
||||
out[n++] = (uint8_t)(PCAN_LEN_MIN + dlc);
|
||||
out[n++] = frame->seq;
|
||||
out[n++] = frame->flags;
|
||||
out[n++] = (uint8_t)(frame->id & 0xFFU);
|
||||
out[n++] = (uint8_t)((frame->id >> 8) & 0xFFU);
|
||||
out[n++] = (uint8_t)((frame->id >> 16) & 0xFFU);
|
||||
out[n++] = (uint8_t)((frame->id >> 24) & 0x1FU);
|
||||
for (uint8_t i = 0U; i < dlc; i++) {
|
||||
out[n++] = frame->data[i];
|
||||
}
|
||||
|
||||
uint16_t crc = pcan_crc16(&out[body], n - body);
|
||||
out[n++] = (uint8_t)(crc & 0xFFU);
|
||||
out[n++] = (uint8_t)(crc >> 8);
|
||||
return n;
|
||||
}
|
||||
|
||||
void pcan_parser_init(pcan_parser_t *p)
|
||||
{
|
||||
p->state = PCAN_ST_SOF0;
|
||||
p->len = 0U;
|
||||
p->idx = 0U;
|
||||
p->crc_rx = 0U;
|
||||
p->stats.frames = 0U;
|
||||
p->stats.crc_errors = 0U;
|
||||
p->stats.bad_len = 0U;
|
||||
p->stats.stray_bytes = 0U;
|
||||
}
|
||||
|
||||
bool pcan_parser_push(pcan_parser_t *p, uint8_t byte, pcan_frame_t *out)
|
||||
{
|
||||
switch (p->state) {
|
||||
case PCAN_ST_SOF0:
|
||||
if (byte == PCAN_SOF0) {
|
||||
p->state = PCAN_ST_SOF1;
|
||||
} else {
|
||||
p->stats.stray_bytes++;
|
||||
}
|
||||
break;
|
||||
|
||||
case PCAN_ST_SOF1:
|
||||
if (byte == PCAN_SOF1) {
|
||||
p->state = PCAN_ST_LEN;
|
||||
} else if (byte == PCAN_SOF0) {
|
||||
/* 0xAA 0xAA 0x55 - тоже валидное начало, остаёмся здесь. */
|
||||
p->stats.stray_bytes++;
|
||||
} else {
|
||||
p->stats.stray_bytes += 2U;
|
||||
p->state = PCAN_ST_SOF0;
|
||||
}
|
||||
break;
|
||||
|
||||
case PCAN_ST_LEN:
|
||||
if ((byte < PCAN_LEN_MIN) || (byte > PCAN_LEN_MAX)) {
|
||||
p->stats.bad_len++;
|
||||
/* Байт мог оказаться началом настоящего кадра. */
|
||||
p->state = (byte == PCAN_SOF0) ? PCAN_ST_SOF1 : PCAN_ST_SOF0;
|
||||
break;
|
||||
}
|
||||
p->len = byte;
|
||||
p->body[0] = byte;
|
||||
p->idx = 1U;
|
||||
p->state = PCAN_ST_BODY;
|
||||
break;
|
||||
|
||||
case PCAN_ST_BODY:
|
||||
p->body[p->idx++] = byte;
|
||||
if (p->idx >= (uint8_t)(p->len + 1U)) {
|
||||
p->state = PCAN_ST_CRC_L;
|
||||
}
|
||||
break;
|
||||
|
||||
case PCAN_ST_CRC_L:
|
||||
p->crc_rx = byte;
|
||||
p->state = PCAN_ST_CRC_H;
|
||||
break;
|
||||
|
||||
case PCAN_ST_CRC_H: {
|
||||
p->crc_rx |= (uint16_t)((uint16_t)byte << 8);
|
||||
p->state = PCAN_ST_SOF0;
|
||||
if (p->crc_rx != pcan_crc16(p->body, (size_t)p->len + 1U)) {
|
||||
p->stats.crc_errors++;
|
||||
return false;
|
||||
}
|
||||
out->seq = p->body[1];
|
||||
out->flags = p->body[2];
|
||||
out->id = ((uint32_t)p->body[3])
|
||||
| ((uint32_t)p->body[4] << 8)
|
||||
| ((uint32_t)p->body[5] << 16)
|
||||
| ((uint32_t)p->body[6] << 24);
|
||||
out->id &= 0x1FFFFFFFUL;
|
||||
out->dlc = (uint8_t)(p->len - PCAN_LEN_MIN);
|
||||
for (uint8_t i = 0U; i < out->dlc; i++) {
|
||||
out->data[i] = p->body[7U + i];
|
||||
}
|
||||
for (uint8_t i = out->dlc; i < PCAN_DATA_MAX; i++) {
|
||||
out->data[i] = 0U;
|
||||
}
|
||||
p->stats.frames++;
|
||||
return true;
|
||||
}
|
||||
|
||||
default:
|
||||
p->state = PCAN_ST_SOF0;
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t pcan_parser_feed(pcan_parser_t *p, const uint8_t *data, size_t len,
|
||||
pcan_frame_cb_t cb, void *user)
|
||||
{
|
||||
pcan_frame_t frame;
|
||||
size_t count = 0U;
|
||||
|
||||
for (size_t i = 0U; i < len; i++) {
|
||||
if (pcan_parser_push(p, data[i], &frame)) {
|
||||
count++;
|
||||
if (cb != NULL) {
|
||||
cb(&frame, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
200
c/protocan-transport/src/pcan_gas.c
Normal file
200
c/protocan-transport/src/pcan_gas.c
Normal file
@@ -0,0 +1,200 @@
|
||||
#include "pcan_gas.h"
|
||||
#include "pcan_id.h"
|
||||
|
||||
bool pcan_gas_map_validate(const pcan_gas_map_t *map)
|
||||
{
|
||||
if ((map == NULL) || ((map->count != 0U) && (map->regions == NULL))) {
|
||||
return false;
|
||||
}
|
||||
if (map->count > PCAN_GAS_MAX_REGIONS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t prev_end = 0U;
|
||||
for (uint16_t i = 0U; i < map->count; i++) {
|
||||
const pcan_gas_region_t *r = &map->regions[i];
|
||||
if (r->count == 0U) {
|
||||
return false;
|
||||
}
|
||||
/* Регион не должен уходить за 0xFFFF. */
|
||||
if (((uint32_t)r->base + r->count) > 0x10000UL) {
|
||||
return false;
|
||||
}
|
||||
if ((r->storage == NULL) && (r->read == NULL) && (r->write == NULL)) {
|
||||
return false;
|
||||
}
|
||||
if ((i != 0U) && ((uint32_t)r->base < prev_end)) {
|
||||
return false; /* не отсортировано либо перекрытие */
|
||||
}
|
||||
prev_end = (uint32_t)r->base + r->count;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const pcan_gas_region_t *pcan_gas_find(const pcan_gas_map_t *map, uint16_t addr)
|
||||
{
|
||||
if ((map == NULL) || (map->regions == NULL)) {
|
||||
return NULL;
|
||||
}
|
||||
/* Регионы отсортированы, поэтому двоичный поиск. */
|
||||
uint16_t lo = 0U;
|
||||
uint16_t hi = map->count;
|
||||
while (lo < hi) {
|
||||
uint16_t mid = (uint16_t)(lo + ((hi - lo) / 2U));
|
||||
const pcan_gas_region_t *r = &map->regions[mid];
|
||||
if (addr < r->base) {
|
||||
hi = mid;
|
||||
} else if ((uint32_t)addr >= ((uint32_t)r->base + r->count)) {
|
||||
lo = (uint16_t)(mid + 1U);
|
||||
} else {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pcan_gas_status_t pcan_gas_read(const pcan_gas_map_t *map, uint16_t addr,
|
||||
uint16_t *value)
|
||||
{
|
||||
const pcan_gas_region_t *r = pcan_gas_find(map, addr);
|
||||
if (r == NULL) {
|
||||
return PCAN_GAS_NO_REG;
|
||||
}
|
||||
if ((r->flags & PCAN_GAS_WRONLY) != 0U) {
|
||||
return PCAN_GAS_WRITE_ONLY;
|
||||
}
|
||||
|
||||
uint16_t offset = (uint16_t)(addr - r->base);
|
||||
if (r->read != NULL) {
|
||||
return r->read(r, offset, value);
|
||||
}
|
||||
if (r->storage != NULL) {
|
||||
*value = r->storage[offset];
|
||||
return PCAN_GAS_OK;
|
||||
}
|
||||
return PCAN_GAS_WRITE_ONLY;
|
||||
}
|
||||
|
||||
pcan_gas_status_t pcan_gas_write(const pcan_gas_map_t *map, uint16_t addr,
|
||||
uint16_t value)
|
||||
{
|
||||
const pcan_gas_region_t *r = pcan_gas_find(map, addr);
|
||||
if (r == NULL) {
|
||||
return PCAN_GAS_NO_REG;
|
||||
}
|
||||
if ((r->flags & PCAN_GAS_RDONLY) != 0U) {
|
||||
return PCAN_GAS_READ_ONLY;
|
||||
}
|
||||
|
||||
uint16_t offset = (uint16_t)(addr - r->base);
|
||||
if (r->write != NULL) {
|
||||
return r->write(r, offset, value);
|
||||
}
|
||||
if (r->storage != NULL) {
|
||||
r->storage[offset] = value;
|
||||
return PCAN_GAS_OK;
|
||||
}
|
||||
return PCAN_GAS_READ_ONLY;
|
||||
}
|
||||
|
||||
uint16_t pcan_gas_read_block(const pcan_gas_map_t *map, uint16_t addr,
|
||||
uint16_t *out, uint16_t max)
|
||||
{
|
||||
uint16_t n = 0U;
|
||||
while (n < max) {
|
||||
/* Останавливаемся на 0xFFFF, чтобы адрес не завернулся в 0. */
|
||||
if (pcan_gas_read(map, (uint16_t)(addr + n), &out[n]) != PCAN_GAS_OK) {
|
||||
break;
|
||||
}
|
||||
n++;
|
||||
if (((uint32_t)addr + n) > 0xFFFFUL) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
uint16_t pcan_gas_write_block(const pcan_gas_map_t *map, uint16_t addr,
|
||||
const uint16_t *in, uint16_t count)
|
||||
{
|
||||
uint16_t n = 0U;
|
||||
while (n < count) {
|
||||
if (pcan_gas_write(map, (uint16_t)(addr + n), in[n]) != PCAN_GAS_OK) {
|
||||
break;
|
||||
}
|
||||
n++;
|
||||
if (((uint32_t)addr + n) > 0xFFFFUL) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
uint16_t pcan_gas_frame_to_regs(const pcan_frame_t *frame, uint16_t *out,
|
||||
uint16_t max)
|
||||
{
|
||||
uint16_t n = 0U;
|
||||
for (uint8_t i = 0U; (uint8_t)(i + 1U) < frame->dlc; i = (uint8_t)(i + 2U)) {
|
||||
if (n >= max) {
|
||||
break;
|
||||
}
|
||||
out[n++] = (uint16_t)((uint16_t)frame->data[i] |
|
||||
((uint16_t)frame->data[i + 1U] << 8));
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void pcan_gas_regs_to_frame(pcan_frame_t *frame, uint32_t base_id,
|
||||
uint16_t addr, const uint16_t *regs, uint16_t count)
|
||||
{
|
||||
if (count > PCAN_GAS_REGS_PER_FRAME) {
|
||||
count = PCAN_GAS_REGS_PER_FRAME;
|
||||
}
|
||||
|
||||
pcan_id_t id;
|
||||
pcan_id_unpack(base_id, &id);
|
||||
id.msg_type = PCAN_MSG_GAS;
|
||||
id.msg_body = addr;
|
||||
|
||||
frame->id = pcan_id_pack(&id);
|
||||
frame->flags = PCAN_FLAG_IDE;
|
||||
frame->seq = 0U;
|
||||
frame->dlc = (uint8_t)(count * 2U);
|
||||
for (uint16_t i = 0U; i < count; i++) {
|
||||
frame->data[i * 2U] = (uint8_t)(regs[i] & 0xFFU);
|
||||
frame->data[(i * 2U) + 1U] = (uint8_t)(regs[i] >> 8);
|
||||
}
|
||||
for (uint8_t i = frame->dlc; i < PCAN_DATA_MAX; i++) {
|
||||
frame->data[i] = 0U;
|
||||
}
|
||||
}
|
||||
|
||||
bool pcan_gas_handle(const pcan_gas_map_t *map, const pcan_frame_t *req,
|
||||
pcan_frame_t *rsp)
|
||||
{
|
||||
pcan_id_t id;
|
||||
pcan_id_unpack(req->id, &id);
|
||||
if (id.msg_type != (uint8_t)PCAN_MSG_GAS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (req->dlc == 0U) {
|
||||
uint16_t regs[PCAN_GAS_REGS_PER_FRAME];
|
||||
uint16_t n = pcan_gas_read_block(map, id.msg_body, regs,
|
||||
PCAN_GAS_REGS_PER_FRAME);
|
||||
if (n == 0U) {
|
||||
return false; /* адреса нет в карте - отвечать нечем */
|
||||
}
|
||||
pcan_gas_regs_to_frame(rsp, req->id, id.msg_body, regs, n);
|
||||
pcan_id_unpack(rsp->id, &id);
|
||||
id.route = PCAN_ROUTE_FROM_DEVICE;
|
||||
rsp->id = pcan_id_pack(&id);
|
||||
rsp->flags = (uint8_t)(req->flags & PCAN_FLAG_IDE);
|
||||
return true;
|
||||
}
|
||||
|
||||
uint16_t regs[PCAN_GAS_REGS_PER_FRAME];
|
||||
uint16_t n = pcan_gas_frame_to_regs(req, regs, PCAN_GAS_REGS_PER_FRAME);
|
||||
(void)pcan_gas_write_block(map, id.msg_body, regs, n);
|
||||
return false;
|
||||
}
|
||||
22
c/protocan-transport/src/pcan_id.c
Normal file
22
c/protocan-transport/src/pcan_id.c
Normal file
@@ -0,0 +1,22 @@
|
||||
#include "pcan_id.h"
|
||||
|
||||
uint32_t pcan_id_pack(const pcan_id_t *id)
|
||||
{
|
||||
return ((uint32_t)(id->priority & 0x1U) << 28)
|
||||
| ((uint32_t)(id->route & 0x1U) << 27)
|
||||
| ((uint32_t)(id->device_type & 0x7U) << 24)
|
||||
| ((uint32_t)(id->device_id & 0xFU) << 20)
|
||||
| ((uint32_t)(id->msg_type & 0xFU) << 16)
|
||||
| ((uint32_t)id->msg_body);
|
||||
}
|
||||
|
||||
void pcan_id_unpack(uint32_t raw, pcan_id_t *id)
|
||||
{
|
||||
raw &= PCAN_ID_MASK;
|
||||
id->msg_body = (uint16_t)(raw & 0xFFFFU);
|
||||
id->msg_type = (uint8_t)((raw >> 16) & 0xFU);
|
||||
id->device_id = (uint8_t)((raw >> 20) & 0xFU);
|
||||
id->device_type = (uint8_t)((raw >> 24) & 0x7U);
|
||||
id->route = (uint8_t)((raw >> 27) & 0x1U);
|
||||
id->priority = (uint8_t)((raw >> 28) & 0x1U);
|
||||
}
|
||||
59
c/protocan-transport/src/pcan_link.c
Normal file
59
c/protocan-transport/src/pcan_link.c
Normal file
@@ -0,0 +1,59 @@
|
||||
#include "pcan_link.h"
|
||||
|
||||
bool pcan_link_init(pcan_link_t *link, const pcan_io_t *io,
|
||||
pcan_frame_cb_t on_frame, void *user)
|
||||
{
|
||||
if ((link == NULL) || (io == NULL) || (io->write == NULL)) {
|
||||
return false;
|
||||
}
|
||||
link->io = *io;
|
||||
link->on_frame = on_frame;
|
||||
link->user = user;
|
||||
link->tx_seq = 0U;
|
||||
link->tx.tx_frames = 0U;
|
||||
link->tx.tx_dropped = 0U;
|
||||
pcan_parser_init(&link->parser);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool pcan_link_can_send(const pcan_link_t *link)
|
||||
{
|
||||
if (link->io.tx_space == NULL) {
|
||||
return true;
|
||||
}
|
||||
return link->io.tx_space(link->io.ctx) >= PCAN_FRAME_MAX;
|
||||
}
|
||||
|
||||
bool pcan_link_send_raw(pcan_link_t *link, const pcan_frame_t *frame)
|
||||
{
|
||||
uint8_t raw[PCAN_FRAME_MAX];
|
||||
|
||||
size_t n = pcan_frame_encode(frame, raw, sizeof raw);
|
||||
if (n == 0U) {
|
||||
link->tx.tx_dropped++;
|
||||
return false;
|
||||
}
|
||||
if (link->io.write(link->io.ctx, raw, n) != n) {
|
||||
link->tx.tx_dropped++;
|
||||
return false;
|
||||
}
|
||||
link->tx.tx_frames++;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool pcan_link_send(pcan_link_t *link, pcan_frame_t *frame)
|
||||
{
|
||||
frame->seq = link->tx_seq;
|
||||
if (!pcan_link_send_raw(link, frame)) {
|
||||
return false;
|
||||
}
|
||||
/* SEQ двигаем только на успешно отданном кадре: иначе приёмник
|
||||
засчитает потерю там, где кадра просто не было. */
|
||||
link->tx_seq++;
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t pcan_link_feed(pcan_link_t *link, const uint8_t *data, size_t len)
|
||||
{
|
||||
return pcan_parser_feed(&link->parser, data, len, link->on_frame, link->user);
|
||||
}
|
||||
88
c/protocan-transport/src/pcan_ring.c
Normal file
88
c/protocan-transport/src/pcan_ring.c
Normal file
@@ -0,0 +1,88 @@
|
||||
#include "pcan_ring.h"
|
||||
|
||||
bool pcan_ring_init(pcan_ring_t *rb, uint8_t *storage, uint16_t size)
|
||||
{
|
||||
if ((rb == NULL) || (storage == NULL) || (size < 2U) ||
|
||||
((size & (uint16_t)(size - 1U)) != 0U)) {
|
||||
return false;
|
||||
}
|
||||
rb->buf = storage;
|
||||
rb->size = size;
|
||||
rb->head = 0U;
|
||||
rb->tail = 0U;
|
||||
rb->dropped = 0U;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint16_t pcan_ring_count(const pcan_ring_t *rb)
|
||||
{
|
||||
return (uint16_t)((rb->head - rb->tail) & (uint16_t)(rb->size - 1U));
|
||||
}
|
||||
|
||||
uint16_t pcan_ring_free(const pcan_ring_t *rb)
|
||||
{
|
||||
return (uint16_t)(rb->size - 1U - pcan_ring_count(rb));
|
||||
}
|
||||
|
||||
bool pcan_ring_put(pcan_ring_t *rb, uint8_t byte)
|
||||
{
|
||||
uint16_t next = (uint16_t)((rb->head + 1U) & (uint16_t)(rb->size - 1U));
|
||||
if (next == rb->tail) {
|
||||
rb->dropped++;
|
||||
return false;
|
||||
}
|
||||
rb->buf[rb->head] = byte;
|
||||
PCAN_BARRIER();
|
||||
rb->head = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool pcan_ring_get(pcan_ring_t *rb, uint8_t *byte)
|
||||
{
|
||||
if (rb->head == rb->tail) {
|
||||
return false;
|
||||
}
|
||||
*byte = rb->buf[rb->tail];
|
||||
PCAN_BARRIER();
|
||||
rb->tail = (uint16_t)((rb->tail + 1U) & (uint16_t)(rb->size - 1U));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool pcan_ring_write(pcan_ring_t *rb, const uint8_t *data, uint16_t len)
|
||||
{
|
||||
if (pcan_ring_free(rb) < len) {
|
||||
rb->dropped += len;
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t mask = (uint16_t)(rb->size - 1U);
|
||||
uint16_t head = rb->head;
|
||||
for (uint16_t i = 0U; i < len; i++) {
|
||||
rb->buf[head] = data[i];
|
||||
head = (uint16_t)((head + 1U) & mask);
|
||||
}
|
||||
/* Публикуем весь блок одной записью: читатель никогда не увидит
|
||||
кадр наполовину. */
|
||||
PCAN_BARRIER();
|
||||
rb->head = head;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint16_t pcan_ring_linear(const pcan_ring_t *rb, const uint8_t **ptr)
|
||||
{
|
||||
uint16_t head = rb->head;
|
||||
uint16_t tail = rb->tail;
|
||||
|
||||
if (head == tail) {
|
||||
*ptr = NULL;
|
||||
return 0U;
|
||||
}
|
||||
*ptr = &rb->buf[tail];
|
||||
return (head > tail) ? (uint16_t)(head - tail)
|
||||
: (uint16_t)(rb->size - tail);
|
||||
}
|
||||
|
||||
void pcan_ring_consume(pcan_ring_t *rb, uint16_t len)
|
||||
{
|
||||
rb->tail = (uint16_t)((rb->tail + len) & (uint16_t)(rb->size - 1U));
|
||||
}
|
||||
Reference in New Issue
Block a user