Вычисление 8-битной CRC с помощью препроцессора C.

Я пишу код для крошечного 8-битного микроконтроллера с несколькими байтами ОЗУ. У него простая задача - передать 7 16-битных слов, а затем CRC этих слов. Значения слов выбираются во время компиляции. CRC, в частности, представляет собой «остаток от деления слова 0 на слово 6 как беззнаковое число, деленное на многочлен x ^ 8 + x² + x + 1 (начальное значение 0xFF)».

Можно ли вычислить CRC этих байтов во время компиляции с помощью препроцессора C?

#define CALC_CRC(a,b,c,d,e,f,g)    /* what goes here? */

#define W0    0x6301
#define W1    0x12AF
#define W2    0x7753
#define W3    0x0007
#define W4    0x0007
#define W5    0x5621
#define W6    0x5422
#define CRC   CALC_CRC(W0, W1, W2, W3, W4, W5, W6)

person Rocketmagnet    schedule 21.02.2012    source источник
comment
codegolf.stackexchange.com / questions / 3268 /   -  person Xophmeister    schedule 21.02.2012
comment
Если для вас гораздо важнее скорость, чем энергонезависимая память (флеш-память), тогда вы можете предварительно рассчитать все результаты и сохранить их в постоянной справочной таблице. Описанный вами полином CRC известен как CRC-8-CCITT. Я не знаю оптимального алгоритма для этого, я бы предложил поискать в Интернете.   -  person Lundin    schedule 22.02.2012


Ответы (3)


It is possible to design a macro which will perform a CRC calculation at compile time. Something like

 // Choosing names to be short and hopefully unique.
 #define cZX((n),b,v) (((n) & (1 << b)) ? v : 0)
 #define cZY((n),b, w,x,y,z) (cZX((n),b,w)^CzX((n),b+1,x)^CzX((n),b+2,y)^cZX((n),b+3,z))
 #define CRC(n) (cZY((n),0,cZ0,cZ1,cZ2,cZ3)^cZY((n),4,cZ4,cZ5,cZ6,cZ7))
should probably work, and will be very efficient if (n) can be evaluated as a compile-time constant; it will simply evaluate to a constant itself. On the other hand, if n is an expression, that expression will end up getting recomputed eight times. Even if n is a simple variable, the resulting code will likely be significantly larger than the fastest non-table-based way of writing it, and may be slower than the most compact way of writing it.

Кстати, одна вещь, которую я действительно хотел бы видеть в стандарте C и C ++, - это средство указания перегрузок, которые будут использоваться для функций, объявленных встроенными, только если определенные параметры могут быть оценены как константы времени компиляции. Семантика будет такой, что не будет `` гарантии '', что любая такая перегрузка будет использоваться в каждом случае, когда компилятор сможет определить значение, но будет гарантия, что (1) такая перегрузка не будет использоваться в любом случае, когда параметр "compile-time-const" должен быть оценен во время выполнения, и (2) любой параметр, который считается константой в одной такой перегрузке, будет считаться константой в любых функциях, вызываемых из него. Есть много случаев, когда функция может быть написана для оценки константы времени компиляции, если ее параметр является постоянным, но когда оценка времени выполнения была бы совершенно ужасной. Например:

#define bit_reverse_byte(n) ( (((n) & 128)>>7)|(((n) & 64)>>5)|(((n) & 32)>>3)|(((n) & 16)>>1)|
  (((n) & 8)<<1)|(((n) & 4)<<3)|(((n) & 2)<<5)|(((n) & 1)<<7) )
#define bit_reverse_word(n) (bit_reverse_byte((n) >> 8) | (bit_reverse_byte(n) << 8))

Простой рендеринг нециклической однобайтовой функции обратного преобразования битов на C на PIC будет примерно 17-19 однотактных инструкций; бит-реверс слова будет равен 34, или около 10 плюс функция обратного байта (которая будет выполняться дважды). Оптимальный ассемблерный код будет примерно 15 одноцикловых инструкций для байтового обратного или 17 для обратного слова. Вычисление bit_reverse_byte(b) для некоторой байтовой переменной b потребовало бы множества десятков инструкций, составляющих в сумме много десятков циклов. Вычисление bit_reverse_word(w) for some 16-bit wordw`, вероятно, потребовало бы сотен инструкций, требующих для выполнения сотен или тысяч циклов. Было бы действительно хорошо, если бы можно было пометить функцию для расширения встроенной, используя что-то вроде приведенной выше формулировки в сценарии, где она будет расширяться в общей сложности до четырех инструкций (в основном, просто загружая результат), но использовать вызов функции в сценариях, где встроенная расширение было бы отвратительным.

person supercat    schedule 24.02.2012
comment
+1 умница. Однако я думаю, что было бы легче понять код (возможно, написанный на обычном C или Python), который работает на моем настольном компьютере и распечатывает предварительно рассчитанную таблицу в исходном файле .h, который позже # включается в код C, который будет работать на микроконтроллере. Что-то вроде с использованием Python для создания C или pycrc - person David Cary; 01.05.2013

Простейший алгоритм контрольной суммы - это так называемая продольная проверка четности, которая разбивает данные на «слова» с фиксированным числом битов n, а затем вычисляет исключающее или всех этих слов. Результат добавляется к сообщению как дополнительное слово.

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

(источник: вики)

В вашем примере:

#define CALC_LRC(a,b,c,d,e,f) ((a)^(b)^(c)^(d)^(e)^(f))
person vulkanino    schedule 21.02.2012
comment
Это не циклическая проверка с избыточностью, это просто партийная проверка без полинома. Вероятность невозможности обнаружения одиночных битовых ошибок составляет 50%. - person Lundin; 21.02.2012
comment
Согласен, но, как я уже сказал, он самый простой. С этой контрольной суммой любая ошибка передачи, которая переворачивает один бит сообщения или нечетное количество битов, будет обнаруживаться как неправильная контрольная сумма. Однако ошибка, затрагивающая два бита, не будет обнаружена, если эти биты находятся в одной и той же позиции в двух разных словах. Если затронутые биты независимо выбраны случайным образом, вероятность того, что двухбитовая ошибка не будет обнаружена, равна 1 / n. - person vulkanino; 21.02.2012
comment
Я должен был упомянуть, что это не просто контрольная сумма, мне нужна конкретная. - person Rocketmagnet; 21.02.2012

Отказ от ответственности: на самом деле это не прямой ответ, а скорее серия вопросов и предложений, которые слишком длинные для комментария.

Первый вопрос: контролируете ли вы оба конца протокола, например Можете ли вы выбрать алгоритм контрольной суммы с помощью себя или коллег, контролирующих код на другом конце?

Если ДА на вопрос №1:

Вам необходимо оценить, зачем вам нужна контрольная сумма, какая контрольная сумма подходит и последствия получения поврежденного сообщения с действительной контрольной суммой (что влияет как на то, что и почему).

Какая у вас среда передачи, протокол, битрейт и т. Д.? Вы ожидаете / наблюдаете битовые ошибки? Так, например, с SPI или I2C от одного чипа к другому на одной плате, если у вас есть битовые ошибки, вероятно, это ошибка инженеров HW или вам нужно снизить тактовую частоту, или и то, и другое. Контрольная сумма не повредит, но в этом нет необходимости. С другой стороны, с инфракрасным сигналом в шумной среде у вас будет гораздо большая вероятность ошибки.

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

Если эти 6 байтов запускают ракету, задают положение роботизированного скальпеля или вызывают перевод денег, вам лучше быть чертовски уверенным, что у вас правильная контрольная сумма, и, возможно, даже захочется изучить криптографический хэш (который может потребовать больше RAM чем у вас).

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

Итак, какая контрольная сумма вам нужна?

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

LRC, как предлагает ответ вулканино, является простейшим алгоритмом.

В Википедии есть неплохая информация о том, как / зачем выбирать полином, если вам действительно нужен CRC: http://en.wikipedia.org/wiki/Cyclic_redundancy_check

Если НЕТ на вопрос №1:

Какой алгоритм / полином CRC требует другой конец? Это то, с чем вы застряли, но, рассказав нам, вы можете получить лучший / более полный ответ.

Мысли о реализации:

Большинство алгоритмов довольно легкие с точки зрения ОЗУ / регистров, требуя всего пару дополнительных байтов. Как правило, функция приводит к созданию более качественного, чистого, читаемого и удобного для отладчика кода.

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

Использование макроса также имеет некоторые странные последствия, о которых вы, возможно, еще не задумывались:
Вы знаете, что препроцессор может производить вычисления только в том случае, если все байты в сообщении зафиксированы во время компиляции, верно? Если у вас есть переменная, компилятор должен сгенерировать код. Без функции этот код будет встроен каждый раз, когда он будет использоваться (да, это может означать много использования ПЗУ). Если все байты являются переменными, этот код может быть хуже, чем просто написание функции на C. Или с хорошим компилятором, это может быть лучше. Трудно сказать наверняка. С другой стороны, если различное количество байтов является переменным в зависимости от отправляемого сообщения, вы можете получить несколько версий кода, каждая из которых оптимизирована для этого конкретного использования.

person Brian McFarland    schedule 21.02.2012
comment
НЕТ на вопрос 1. Я добавил многочлен к своему вопросу. - person Rocketmagnet; 22.02.2012
comment
Я знаю, что все байты должны быть известны во время компиляции. Вот почему в моем примере кода все они определены. - person Rocketmagnet; 22.02.2012
comment
@Rocketmagnet: сначала я бы посмотрел, сможете ли вы придумать рабочий алгоритм с таблицей поиска в качестве ярлыка для побитовых операций. Рассчитайте справочную таблицу с помощью программы для ПК и сохраните ее в переменной препроцессора (то есть в макросе). Затем разверните «внешний» цикл, который выполняет поиск по каждому слову примерно так: #define CALC_CRC(a,b, c) LUT[ c ^ LUT[ b ^ LUT[ a ^ FF ] ] ] - person Brian McFarland; 22.02.2012
comment
SPI или I2C ... / - / ... если у вас есть битовые ошибки, вероятно, это ошибка инженеров HW. Есть очень много способов ошибиться с медью. Так что, наоборот, если у вас есть битовые ошибки на SPI или I2C, скорее всего, это ошибка инженеров программного обеспечения! По моему опыту, проблемы с неправильной конфигурацией или плохо определенные / стандартизированные параметры связи составляют около 99% всех ошибок SPI / I2C. - person Lundin; 22.02.2012
comment
@Lundin: Я имел в виду, что если вы наблюдали битовую ошибку SPI / I2C на проводе из-за искаженного сигнала, то вероятно пора изменить схему, компоновку, корпус или экранирование. Реальная ошибка передачи из-за шума будет редкостью. Таким образом, на хорошо сделанном HW простой XOR или отсутствие контрольной суммы, вероятно, подходят для таких маленьких сообщений. Конечно, CRC-8 всегда лучше, но иногда и MD5 тоже. В любой области инженерии важно найти минимальное, но адекватное решение. Как я уже упоминал, важно также оценить влияние плохого сообщения. - person Brian McFarland; 22.02.2012
comment
Между прочим, многие компиляторы C для встроенных систем не используют стеки для автоматических переменных или передачи параметров. Вместо этого они статически распределяют переменные, так что подпрограммы, переменные которых активны одновременно, будут хранить свои автопеременные и параметры по разным адресам, в то время как тем, чьи переменные не являются активными одновременно, могут быть предоставлены перекрывающиеся адреса. - person supercat; 24.02.2012
comment
@supercat, Хороший момент. Я предполагаю, что это особенно важно для арки со стеком HW для счетчика программ. Исправлено соответствующим образом. - person Brian McFarland; 24.02.2012