Надежность контрольной суммы CRC16

в 16:14, , рубрики: C, CRC, crc error, crc16, rs485, коллизии, Программирование

Не так давно по долгу службы столкнулся с довольно интересной проблемой.

У нас имеется устройство, которое осуществляет интенсивный обмен по внутренней шине RS485, число проходящих пакетов составляет порядка нескольких тысяч в секунду, каждый пакет имеет длину в 7 байт, два из которых предназначены для хранения контрольной суммы CRC16 в ее CMS варианте (полином = 0x8005, стартовое значение = 0xFFFF). Прием осуществляется в FIFO-буфер, который сдвигается вверх с вытеснением после приема каждого последующего байта. Индикатором получения реального пакета является факт совпадения его контрольной суммы со значением, переданным в самом пакете. Никаких заголовков или дополнительных параметров.

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

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

Для сравнения я выбрал несколько наиболее часто используемых 16-битных контрольных сумм с различными полиномами, стартовыми значениями и механизмом поступления битов. Выбранные мной суммы сведены в следующую таблицу:

Обозначение Polynomial Init RefIn RefOut XorOut
CMS 0x8005 0xFFFF false false 0x0000
CCITT 0x1021 0xFFFF false false 0x0000
AUG-CCITT 0x1021 0x1D0F false false 0x0000
BYPASS 0x8005 0x0000 false false 0x0000
CDMA2000 0xC867 0xFFFF false false 0x0000
DDS-110 0x8005 0x800D false false 0x0000
DECT-X 0x0589 0x0000 false false 0x0000
EN-13757 0x3D65 0x0000 false false 0xFFFF
Modbus 0x8005 0xFFFF true true 0x0000
T10-DIF 0x8BB7 0x0000 false false 0x0000
TELEDISK 0xA097 0x0000 false false 0x0000
XMODEM 0x1021 0x0000 false false 0x0000

В данном случае:

  • RefIn — порядок поступления битов из буфера данных: false — начиная со старшего значащего бита (MSB first), true – LSB first;
  • RefOut – признак инвертирования порядка битов на выходе: true – инвертировать.

При эмуляции повреждения пакетов я реализовал следующие модели:

  • Shuffle: заполнение случайного количества байт в пакете случайными значениями
  • Bit shift: сдвиг случайных байт в пакете влево
  • Roll packet: кольцевой сдвиг байт в пакете влево
  • Right shift: сдвиг пакета вправо на один байт, слева дописывается 0xFF (передача идет посредством UART)
  • Left shift: сдвиг пакета влево на один байт, справа дописывается 0xFF
  • Fill zeros: заполнение случайного количества байт в пакете байтами 0x00 (все нули)
  • Fill ones: заполнение случайного количества байт в пакете байтами 0xFF (все единицы)
  • Byte injection: вставка в пакет случайного байта в случайном месте, байты за вставленным сдвигаются в направлении хвоста
  • Single bit: повреждение единственного случайного бита

Затем программой были сгенерированы случайным образом 100.000.000 пакетов, над каждым из них была проведена указанные выше операции, после чего сравнивались контрольные суммы исходного и модернизированного пакета. Пакеты, которые не изменились при преобразовании, отбрасывались. Если контрольная сумма совпадала, то регистрировалась ошибка.

В итоге была получена следующая таблица с количеством ошибок:

Обозначение Shuffle Bit shift Roll packet Right shift Left shift Fill zeros Fill ones Byte injection Sum
CMS 5101 3874 2937 1439 1688 3970 4010 1080 24099
CCITT 2012 1127 3320 1494 1486 1063 1096 1130 12728
AUG-CCITT 2012 1127 3320 1494 1486 1063 1096 1130 12728
BYPASS 5101 3874 2937 1439 1688 3970 4010 1080 24099
CDMA2000 1368 1025 1946 1462 1678 1043 1028 1112 10662
DDS-110 5101 3874 2937 1439 1688 3970 4010 1080 24099
DECT-X 1432 1189 5915 1779 1580 1215 1209 1093 15412
EN-13757 1281 2209 3043 1520 1528 2193 2187 1039 15000
Modbus 5090 3888 3086 1282 1582 3947 3897 1073 23845
T10-DIF 1390 922 1424 1421 1630 994 938 1093 9812
TELEDISK 1394 1049 5398 1451 1512 1096 1066 1065 14031
XMODEM 2012 1127 3320 1494 1486 1063 1096 1130 12728

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

Ну и итоговая таблица качества контрольной суммы, уже без учета дублирующих алгоритмов:

Обозначение Number of collisions Place
CMS 24099 8
CCITT 12728 3
CDMA2000 10662 2
DECT-X 15412 6
EN-13757 15000 5
Modbus 23845 7
T10-DIF 9812 1
TELEDISK 14031 4

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

Исходный код программы приведен ниже.

Исходный код

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>

#define PACKET_LEN    (7)
#define NUM_OF_CYCLES (100000)

static unsigned char reverse_table[16] =
{
  0x0, 0x8, 0x4, 0xC, 0x2, 0xA, 0x6, 0xE,
  0x1, 0x9, 0x5, 0xD, 0x3, 0xB, 0x7, 0xF
};

uint8_t reverse_bits(uint8_t byte)
{
  // Reverse the top and bottom nibble then swap them.
  return (reverse_table[byte & 0b1111] << 4) | reverse_table[byte >> 4];
}

uint16_t reverse_word(uint16_t word)
{
  return ((reverse_bits(word & 0xFF) << 8) | reverse_bits(word >> 8));
}

uint16_t crc16_common(uint8_t *data, uint8_t len, uint16_t poly, uint16_t init,
                      uint16_t doXor, bool refIn, bool refOut)
{
  uint8_t y;
  uint16_t crc;

  crc = init;
	while (len--)
  {
    if (refIn)
      crc = ((uint16_t)reverse_bits(*data++) << 8) ^ crc;
    else
	    crc = ((uint16_t)*data++ << 8) ^ crc;
    for (y = 0; y < 8; y++)
    {
      if (crc & 0x8000)
        crc = (crc << 1) ^ poly;
      else
        crc = crc << 1;
    }
	}

  if (refOut)
    crc = reverse_word(crc);
  return (crc ^ doXor);
}

uint16_t crc16_ccitt(uint8_t *data, uint8_t len)
{
  return crc16_common(data, len, 0x1021, 0xFFFF, 0x0000, false, false);
}

uint16_t crc16_bypass(uint8_t *data, uint8_t len)
{
  return crc16_common(data, len, 0x8005, 0x0000, 0x0000, false, false);
}

uint16_t crc16_xmodem(uint8_t *data, uint8_t len)
{
  return crc16_common(data, len, 0x1021, 0x0000, 0x0000, false, false);
}

uint16_t crc16_teledisk(uint8_t *data, uint8_t len)
{
  return crc16_common(data, len, 0xA097, 0x0000, 0x0000, false, false);
}

uint16_t crc16_augccitt(uint8_t *data, uint8_t len)
{
  return crc16_common(data, len, 0x1021, 0x1d0f, 0x0000, false, false);
}

uint16_t crc16_cdma2000(uint8_t *data, uint8_t len)
{
  return crc16_common(data, len, 0xc867, 0xffff, 0x0000, false, false);
}

uint16_t crc16_dds110(uint8_t *data, uint8_t len)
{
  return crc16_common(data, len, 0x8005, 0x800d, 0x0000, false, false);
}

uint16_t crc16_dect(uint8_t *data, uint8_t len)
{
  return crc16_common(data, len, 0x0589, 0x0000, 0x0000, false, false);
}

uint16_t crc16_en13757(uint8_t *data, uint8_t len)
{
  return crc16_common(data, len, 0x3d65, 0x0000, 0xffff, false, false);
}

uint16_t crc16_t10dif(uint8_t *data, uint8_t len)
{
  return crc16_common(data, len, 0x8bb7, 0x0000, 0x0000, false, false);
}

uint16_t crc16_cms(uint8_t *data, uint8_t len)
{
  return crc16_common(data, len, 0x8005, 0xFFFF, 0x0000, false, false);
}

uint16_t crc16_modbus(uint8_t *data, uint8_t len)
{
  return crc16_common(data, len, 0x8005, 0xFFFF, 0x0000, true, true);
}

bool compare_buf(uint8_t *buf1, uint8_t *buf2)
{
  uint8_t x;

  for (x = 0; x < PACKET_LEN; x++)
  {
    if (buf1[x] != buf2[x])
      return true;
  }

  return false;
}

bool method_shuffle(uint8_t *buf)
{
  uint8_t i, j;
  uint16_t rnd;
  uint8_t copy[PACKET_LEN];

  memcpy(copy, buf, PACKET_LEN);

  for (i = 0; i < PACKET_LEN; i++)
  {
    for (j = 0; j < 10; j++)
    {
      rnd = (uint16_t)rand();
      if (rnd % 7 == 0)
        buf[i] ^= (1 << (rnd % 8));
    }
  }

  return compare_buf(buf, copy);
}

bool method_bitshift(uint8_t *buf)
{
  uint8_t x, i, j;
  uint8_t copy[PACKET_LEN];

  memcpy(copy, buf, PACKET_LEN);

  x = (uint8_t)(rand() % PACKET_LEN) + 1;
  for (j = 0; j < x; j++)
  {
    i = (uint8_t)(rand() % PACKET_LEN);
    if (buf[i] == 0)
      buf[i] = 0x01;
    else
      buf[i] <<= 1;
  }

  return compare_buf(buf, copy);
}

bool method_packetroll(uint8_t *buf)
{
  uint8_t x, i, j;
  uint8_t temp;
  uint8_t copy[PACKET_LEN];

  memcpy(copy, buf, PACKET_LEN);

  x = (uint8_t)(rand() % (PACKET_LEN - 1)) + 1;
  for (j = 0; j < x; j++)
  {
    temp = buf[0];
    for (i = 0; i < PACKET_LEN - 1; i++)
      buf[i] = buf[i + 1];
    buf[PACKET_LEN - 1] = temp;
  }

  return compare_buf(buf, copy);
}

bool method_shiftright(uint8_t *buf)
{
  uint8_t i;
  uint8_t copy[PACKET_LEN];

  memcpy(copy, buf, PACKET_LEN);

  for (i = 0; i < PACKET_LEN - 1; i++)
      buf[i + 1] = buf[i];
  buf[0] = 0xff;

  return compare_buf(buf, copy);
}

bool method_shiftleft(uint8_t *buf)
{
  uint8_t i;
  uint8_t copy[PACKET_LEN];

  memcpy(copy, buf, PACKET_LEN);

  for (i = 0; i < PACKET_LEN - 1; i++)
      buf[i] = buf[i + 1];
  buf[PACKET_LEN - 1] = 0xff;

  return compare_buf(buf, copy);
}

bool method_zero(uint8_t *buf)
{
  uint8_t x, i, j;
  uint8_t copy[PACKET_LEN];

  memcpy(copy, buf, PACKET_LEN);

  x = (uint8_t)(rand() % PACKET_LEN) + 1;
  for (j = 0; j < x; j++)
  {
    i = (uint8_t)(rand() % PACKET_LEN);
    if (buf[i] != 0x00)
      buf[i] = 0x00;
    else
      buf[i] = 0xFF;
  }

  return compare_buf(buf, copy);
}

bool method_one(uint8_t *buf)
{
  uint8_t x, i, j;
  uint8_t copy[PACKET_LEN];

  memcpy(copy, buf, PACKET_LEN);

  x = (uint8_t)(rand() % PACKET_LEN) + 1;
  for (j = 0; j < x; j++)
  {
    i = (uint8_t)(rand() % PACKET_LEN);
    if (buf[i] != 0xFF)
      buf[i] = 0xFF;
    else
      buf[i] = 0x00;
  }

  return compare_buf(buf, copy);
}

bool method_injection(uint8_t *buf)
{
  uint8_t x, i;
  uint8_t copy[PACKET_LEN];

  memcpy(copy, buf, PACKET_LEN);

  x = (uint8_t)(rand() % PACKET_LEN);
  for (i = PACKET_LEN - 1; i > x; i--)
  {
    buf[i] = buf[i - 1];
  }
  buf[x] = (uint8_t)rand();

  return compare_buf(buf, copy);
}

bool method_single(uint8_t *buf)
{
  uint8_t x;

  x = (uint8_t)(rand() % (PACKET_LEN * 8));
  buf[(uint8_t)(x / 8)] ^= (1 << (x % 8));

  return true;
}

typedef struct
{
  uint16_t crc1;
  uint16_t crc2;
  uint32_t errors;
  uint16_t (*fn)(uint8_t *data, uint8_t len);
  char name[32];
} tCRC;

typedef struct
{
  bool (*fn)(uint8_t *buf);
  char name[32];
} tMethod;

static tMethod methods[] =
{
  {method_shuffle, "Shuffle"},
  {method_bitshift, "Bit shift"},
  {method_packetroll, "Roll packet"},
  {method_shiftright, "Right shift"},
  {method_shiftleft, "Left shift"},
  {method_zero, "Fill zeros"},
  {method_one, "Fill ones"},
  {method_injection, "Byte injection"},
  {method_single, "Single bit"}
};

static tCRC crcs[] =
{
  {0, 0, 0, crc16_cms, "CMS"},
  {0, 0, 0, crc16_ccitt, "CCITT"},
  {0, 0, 0, crc16_augccitt, "AUG-CCITT"},
  {0, 0, 0, crc16_bypass, "BYPASS"},
  {0, 0, 0, crc16_cdma2000, "CDMA2000"},
  {0, 0, 0, crc16_dds110, "DDS-110"},
  {0, 0, 0, crc16_dect, "DECT-X"},
  {0, 0, 0, crc16_en13757, "EN-13757"},
  {0, 0, 0, crc16_modbus, "Modbus"},
  {0, 0, 0, crc16_t10dif, "T10-DIF"},
  {0, 0, 0, crc16_teledisk, "TELEDISK"},
  {0, 0, 0, crc16_xmodem, "XMODEM"}
};

int main(int argc, char * argv[])
{
  uint32_t num_of_cycle;
  uint32_t num_of_sums;
  uint8_t packet[PACKET_LEN];
  uint8_t i;
  uint8_t m;
  //uint8_t buf[8] = {0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17};

  srand(time(NULL));

  printf("------------------------- CRC16 comparison -------------------------n");

  num_of_sums = sizeof(crcs) / sizeof(tCRC);
  for (m = 0; m < sizeof(methods) / sizeof(tMethod); m++)
  {
    printf("r%s:n", methods[m].name);

    for (i = 0; i < num_of_sums; i++)
    {
      crcs[i].errors = 0;
    }
    for (num_of_cycle = 0; num_of_cycle < NUM_OF_CYCLES; num_of_cycle++)
    {
      for (i = 0; i < PACKET_LEN; i++)
        packet[i] = (uint8_t)rand();

      for (i = 0; i < num_of_sums; i++)
        crcs[i].crc1 = crcs[i].fn(packet, PACKET_LEN);

      if (!methods[m].fn(packet))
        continue;

      for (i = 0; i < num_of_sums; i++)
      {
        crcs[i].crc2 = crcs[i].fn(packet, PACKET_LEN);
        if (crcs[i].crc1 == crcs[i].crc2)
          crcs[i].errors++;
      }
      if (num_of_cycle % 1000 == 0)
        printf("r%.2f%%", (float)num_of_cycle / NUM_OF_CYCLES * 100);
    }

    for (i = 0; i < num_of_sums; i++)
      printf("r  %20s: %10dn", crcs[i].name, crcs[i].errors);
  }

  return 0;
}

В итоге в следующей версии изделия для внутренней шины была выбрана контрольная сумма CCITT, в большей степени потому, что ее реализация была доступна в аппаратной части используемого микроконтроллера.

Автор: Polaris99

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js