Перейти к содержанию

Запускаем кодек OPUS на микроконтроллере

Исходные данные — есть контроллер STM32 с очень ограниченной памятью, а мы хотим записывать на нем звук. Допустим, что примеров с подключением выбранного нами микрофона гора и маленькая тележка. В итоге имеем контроллер, который умеет выдавать нам WAV-подобный сигнал. Хотелось бы этот WAV-сигнал куда-то записать или передать. Таких данных будет очень много, есть ненулевая вероятность, что мы не влезем по полосе пропускания используемого канала или заполним память до того, как получим нужную информацию. На помощь нам спешит компрессия!

А именно абсолютно бесплатный, открытый и очень производительный кодек OPUS. Именно он используется в большинстве стриминговых сервисов — как самостоятельно, так и в качестве обработчика звуковых дорожек. Насколько он прекрасен — можно почитать будет в другом месте. Мы же будет заводить его на ограниченном по памяти микроконтроллере:

RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 48K
RAM2 (xrw) : ORIGIN = 0x10000000, LENGTH = 16K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 256K

Данный кодек имеет обширное количество настроек. Каждая их них определенным образом влияет на качество получаемого звука, скорость обработки и т.д., что очевидно. Первая проблема с которой можно столкнуться — как оценить качество звука после процесса компрессии/декомпрессии? Можно субъективно послушать результат или посмотреть спектрограммы. Кодек включает в себя два других сильно измененных кодека: SILK и CELT. Оба лучше работают в своих условиях — SILK, например, лучше справляется с разговорной речью, но имеет ограничение по полосе частот. OPUS-кодер может работать в трех режимах: SILK, CELT и HYBRID. Названия достаточно говорящие, чтобы понять, что они из себя представляют.

Что мы сделаем в данном проекте? Подключим библиотеку Opus, обработаем звуковые данные через него и сформируем опус-файл.

Итак, начнем.

В первую очередь, нужно скачать стабильные исходные коды с сайта производителя (https://opus-codec.org/). На момент написания статьи самая свежая версия была 1.3.1

Предположим, вы уже создали проект. Распаковывем архив и добавляем к проекту файлы кодека.

В настройках проекта добавим путь OPUS_ROOT до папки с кодеком

В настройках препроцессора добавим следующие определения

VAR_ARRAYS — кодировщик будет использовать массивы переменной длины для хранения вычислений. Если версия компилятора не поддерживает С99, то стоит использовать макрос USE_ALLOCA

DISABLE_FLOAT_API и FIXED_POINT ускоряют вычисления на микроконтроллере и уменьшают используемую память.

OPUS_BUILD — тут все понятно

Вместо прописывания данных макросов непосредственно в настройках компиляции проекта, тоже самое можно сделать через файл config.h, который идет вместе с кодировщиком.

А сейчас неинтересная часть. Нужно исключить или удалить из проекта все файлы, которые не относятся к используемому контроллеру, процессору и настройкам. Это все файлы и папки, содержащие в названии _test, _demo, _float, _ne10, _multistream_, mips, doc, win32, arm64, tests, а также ассемблерные файлы (.S), где прописаны инструкции для процессора с поддержкой neon.

Правой кнопкой по файлу (Alt+Enter) -> Properties -> Execute resource from build

Если что-то пропустили, ничего страшного. В любом случае, компилятор подскажет, что что-то осталось или просто не будет компилировать если этот функционал не используется.

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

Src

Include

Папка silk слишком большая, поэтому покажу только исключенные файлы

Тоже самое с celt

Ах, да, не забываем добавить include paths

Готово. Далее, для работы с кодировщиком, нужно подключить в main.c следующие заголовочные файлы

#include "opus.h"
#include "opus_types.h"
#include "opus_private.h"

Проект подготовлен для работы. Сейчас мы немного обсудим структуру .opus файлов, чтобы мы могли формировать их. Да, кодировщик принимает на вход поток PCM (ИКМ) последовательности из которой формирует свой поток закодированных данных. Но эти данные будет невозможно воспроизвести на приемной стороне, если их правильно не упаковать. Например, TI в своем примере с opus вводят эдакий промежуточный вариант упаковки этих последовательностей, чтобы можно было декодировать обратно то, что мы записали. Но мы же хотим получить прям нормальные .opus файлы, которые можно открыть на пк с любого плеера! Такой вариант не годится. В примере от ST тоже использовался промежуточный контейнер. Давайте делать нормально без настолько некрасивых костылей.

Откроем любой opus файл через hex редактор и что же мы увидим

По всему видно, что opus файл состоит из Ogg контейнеров, внутри которых расположены данные кодека OPUS.

Что из себя представляет Ogg контейнер

Картинка с вики

Не буду расписывать каждое из полей — их описание есть на вики и на сайте разрабов Ogg. Остановлю внимание лишь на самом неочивидном для меня. Даже после прочтения документации (https://xiph.org/ogg/doc/rfc3533.txt)

Header Type

0x02 для самого первого

0x04 для самого последнего

Это все верно. А вот для тех, кто МЕЖДУ ними Header Type должен быть 0x00, если только это не разбитый на 2 «Ogg-страницы» пакет с данными, что бывает крайне редко.

Granule Position

Непереводимая и плохо описанная гадость-кровопийца. Представляет собой число, которое показывает сколько PCM сэмплов было закодировано за ВСЕ предыдущие страницы ПЛЮС данная страница.

Это можно узнать при работе энкодера и сборке файла.

Представим ситуацию — у вас микрофон записывает данные, программа засовывает их в кодировщик, собирает пакеты (страницы, Page) и отправляет на ПК.

Например только стартанули, вот вы засунули 600 первых сэмплов с микрофона в кодировщик, а он выплюнул N сэмплов закодированных. Потом еще вы засунули 700 сэмплов, а он выплюнул M закодированных. Так вот Granule Position для первого пакета (страницы) будет 600, а для второго пакета 600+700=1300. И так далее.

Bitstream serial number

О, а эта моя любимая.

Документация

This unique serialnumber is created randomly and does not have any connection to the content or encoder of the logical bitstream it represents.

Наверное, это очевидно. Но это просто совершенно случайное число, которое ОДИНАКОВОЕ для кажной страницы (пакета) для данного аудиострима. И это число придумываете/генерируете ВЫ, а не опус или кодировщик.

Page sequence number

Просто номер «Ogg-страницы». Начинается с 0.

Чек-сумма

Ничего сложного, но нужно быть внимательным

Хекс-редактор HxD позволяет ее посчитать для тестов, например.

Для подсчета КС, нужно вписать нули на место КС и выделить всю страницу (1 Ogg-пакет)

Переходим в Анализ->Контрольные суммы

Контрольная сумма считается по полиному 4C11DB7. Результатом будет следующее:

Как видно, контрольная сумма правильная, но развернута.

Page Segments

Число, которое описывает количество сегментов таблицы, которые будут далее

Segment Table

Первым идет массив длин сегментов таблицы. Длина данного массива описывается числом в Page Segments. После этого массива идут непосредственно сами данные.

Пример 1 — искусственно созданный мелкий файл

В данном примере у нас Page Segments равен 1. Значит длина массива, который находится после него будет равна 1. У нас массив длин из одного числа, который равен 0x31. Значит после данного массива идет 0x31 (49) штук данных.

Пример 2 — реальный файл

В данном примере у нас Page Segments равен 0x7F (247). Далее идет массив длиной 247 — выделен для наглядности. Каждая ячейка массива описывает длину одного сегмента данных. После него идут непосредственно данные. Их примерно, 14000 в данной странице (пакете).

Сам Opus и его параметры(OpusHead и OpusTags которые будут считаны декодером) располагаются в секциях Segment Table 2-х первых Ogg-контейнеров.

Описание данных полей есть в документе RFC 7845 на сайте Opus. Каждое после достаточно хорошо описано без двусмысленности и необходимости копать глубже, чем надо. Поэтому дублировать информацию не буду.

Вернемся к проекту.

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

f_in = open('example.wav', 'rb')
f_out = open('PCM_data.h', 'wb')

cntr = 0
while True:
    data = f_in.read(1)#[::-1]#reverse bytes
    cntr += 1
    if not data:
        break
    if cntr == 1:
        continue
    elif cntr == 4:
        cntr = 0
    else :
        f_out.write(data)

f_in.close()
f_out.close()

Все просто — имя входного и выходного файлов. В выходном файле дописать название массива и поставить скобку в конце.

Первое, что нужно сделать — распарсить формат(преамбулу) WAV-файла. Она достаточно простая и однозначная. Не буду её описывать по пунктам, т.к. есть прекрасные сайты с пояснениями (http://soundfile.sapp.org/doc/WaveFormat/). Стоит лишь обратить внимание, что некоторые из полей имеют little endian, а некоторые big endian.

Добавим данные wav-файла и файл для расчета crc. Весь код будет в main.c друг за другом.

#include "PCM_data.h"
#include "crc.h"

Объявим два дефайна — размер окна обработки в мс и размер обработчика в байтах.

#define OPUS_FRAME_SIZE_IN_MS 10
#define OPUS_SIZE 880

Создадим структуру для парсинга преамбулы.

typedef struct
{
    uint8_t ui8ChunkID[4];
    uint32_t ui32ChunkSize;
    uint8_t ui8Format[4];
    uint8_t ui8SubChunk1ID[4];
    uint32_t ui32SubChunk1Size;
    uint16_t ui16AudioFormat;
    uint16_t ui16NumChannels;
    uint32_t ui32SampleRate;
    uint32_t ui32ByteRate;
    uint16_t ui16BlockAlign;
    uint16_t ui16BitsPerSample;
    uint8_t ui8SubChunk2ID[4];
    uint32_t ui32SubChunk2Size;
}
tWaveHeader;

Теперь создадим структуру для создания Ogg-контейнеров(страниц).

typedef struct
{
	uint32_t capture_pattern;
	uint8_t version;
	uint8_t header_type;
	uint32_t granole_pos_l;
	uint32_t granole_pos_h;
	uint32_t bitstream_sn;
	uint32_t page_seq_num;
	uint32_t checksum;
	uint8_t segments_length;
	uint8_t page_segments;
}
tOggHeader;

Так же укажем, что есть массив данных от wav и объявим обе созданные структуры. Объявим энкодер OPUS’a

extern uint8_t sound_wav_u8[];

tWaveHeader sWaveHeader;
tOggHeader current_opus_header;

OpusEncoder *sOpusEnc;

Для уменьшения размера массива, отведенного под хранение результатов работы кодека, я упаковываю и отправляю по одному сегменту в одной странице. То есть в каждой из страниц будет по одному Page Segments.

  current_opus_header.capture_pattern = 0x5367674F;    //OggS
  current_opus_header.header_type = 0x00; 
  current_opus_header.version = 0x00;    //Всегда 0
  current_opus_header.bitstream_sn = 0x11111111;
  current_opus_header.granole_pos_l = 0;
  current_opus_header.granole_pos_h = 0;
  current_opus_header.page_seq_num = 2;
  current_opus_header.segments_length = 0x01;

Вот тут поясню (current_opus_header.page_seq_num = 2). Это значит, что я пропустил первые две страницы. В первых двух страницах содержится информация для декодера OPUS. У меня входные данные не меняются, поэтому и хедеры тоже всегда одинаковые и их дописываю сам через хекс-редактор. Если данные будут у вас разные, то нужно добавлять формировку OPUS-хедеров, что не должно быть сложно, т.к. он тоже упакован в контейнер Ogg.

Парсим данные wav

memcpy(&sWaveHeader, sound_wav_u8, sizeof(sWaveHeader));

Создаем энкодер

int32_t  i32error;
sOpusEnc = opus_encoder_create(sWaveHeader.ui32SampleRate, sWaveHeader.ui16NumChannels, OPUS_APPLICATION_AUDIO, &i32error);

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

Настраиваем энкодер в режиме работы только с celt. Гибридный метод при заданных настройках также будет использовать celt. Режим же SILK требует намного больше оперативной памяти для выполнения, чем у нас есть в контроллере.

opus_encoder_ctl(sOpusEnc, OPUS_SET_BITRATE((sWaveHeader.ui32SampleRate*2)));
opus_encoder_ctl(sOpusEnc, OPUS_SET_BANDWIDTH(OPUS_BANDWIDTH_SUPERWIDEBAND));
opus_encoder_ctl(sOpusEnc, OPUS_SET_VBR(1));
opus_encoder_ctl(sOpusEnc, OPUS_SET_VBR_CONSTRAINT(0));
opus_encoder_ctl(sOpusEnc, OPUS_SET_COMPLEXITY(0));
opus_encoder_ctl(sOpusEnc, OPUS_SET_INBAND_FEC(0));
opus_encoder_ctl(sOpusEnc, OPUS_SET_FORCE_CHANNELS(1));
opus_encoder_ctl(sOpusEnc, OPUS_SET_DTX(0));
opus_encoder_ctl(sOpusEnc, OPUS_SET_PACKET_LOSS_PERC(0));
opus_encoder_ctl(sOpusEnc, OPUS_SET_LSB_DEPTH(sWaveHeader.ui16BitsPerSample));
opus_encoder_ctl(sOpusEnc, OPUS_SET_EXPERT_FRAME_DURATION(OPUS_FRAMESIZE_10_MS));
opus_encoder_ctl(sOpusEnc, OPUS_SET_FORCE_MODE(MODE_CELT_ONLY));

OPUS_BANDWIDTH_SUPERWIDEBAND = 12 кГц ширина полосы

OPUS_SET_VBR(1) — переменный битрейт качества

OPUS_SET_BITRATE((sWaveHeader.ui32SampleRate * 2)) — выходной битрейт я выставил в два раза больше, чем входной.

Объявим массив для вывода данных из энкодера

uint8_t pui8data[150];

Так как на вход OPUS принимает 16-битные данные, нам нужно форматировать входные данные, поэтому объявим его и посчитаем размер.

opus_int16 *popi16fmtBuffer;
uint32_t ui32Sizeofpopi16fmtBuffer;

popi16fmtBuffer = (opus_int16 *)calloc((((sWaveHeader.ui32SampleRate * OPUS_FRAME_SIZE_IN_MS * sWaveHeader.ui16NumChannels)/1000) + 1), sizeof(opus_int16));
ui32Sizeofpopi16fmtBuffer = (sWaveHeader.ui32SampleRate * OPUS_FRAME_SIZE_IN_MS * sWaveHeader.ui16NumChannels * sizeof(opus_int16)) / 1000;

Смотрим, сколько байт отводится под одну запись

uint8_t  ui8ScaleFactor;
ui8ScaleFactor = (sWaveHeader.ui16BitsPerSample) >> 3;

В зависимости от количества байт под запись происходит формирование массива для передачи в энкодер.

uint8_t has_data = 1;
uint8_t pack = 0;
int32_t  i32len;
uint32_t ui32Loop;

while(has_data){
	  for(ui32Loop = 0 ; ui32Loop < OPUS_SIZE ; ui32Loop++)
	  {
		  if(ui8ScaleFactor == 1)
		  {
				 popi16fmtBuffer[ui32Loop] = (opus_int16)sound_wav_u8[OPUS_SIZE * pack + sizeof(sWaveHeader) + ui32Loop];
		  }
		  else if(ui8ScaleFactor == 2)
		  {
			  if(ui32Loop % 2 == 0)
				  popi16fmtBuffer[ui32Loop/2] = sound_wav_u8[OPUS_SIZE * pack + sizeof(sWaveHeader) + ui32Loop];
			  else
				  popi16fmtBuffer[ui32Loop/2] |= (sound_wav_u8[OPUS_SIZE * pack + sizeof(sWaveHeader) + ui32Loop] << 8);
		  }

	  }

//если данных для набора следующей пачки не хватает, то мы просто забиваем на них. Что-то лучше придумать можно, я пока использовал этот способ для тестов
	  if (OPUS_SIZE * pack+sizeof(sWaveHeader) > (sizeof(sound_wav_u8)/sizeof(sound_wav_u8[0]) - OPUS_SIZE )){
	  	has_data = 0;
	  }


	  i32len = opus_encode(sOpusEnc, popi16fmtBuffer, (ui32Sizeofpopi16fmtBuffer/2), pui8data, OPUS_SIZE);

	  current_opus_header.granole_pos_l += popi16fmtBuffer; //увеличиваем гранолу на размер обработанных ИКМ
	  current_opus_header.page_seq_num += 1;
	  current_opus_header.page_segments = (uint8_t)i32len; //длина сегмента - у нас она одна


	  pack++;

//работа с crc32
	  crc32_clear(); //очистка crc32

	  crc32_push((uint8_t)current_opus_header.capture_pattern);
	  crc32_push((uint8_t)(current_opus_header.capture_pattern >> 8));
	  crc32_push((uint8_t)(current_opus_header.capture_pattern >> 16));
	  crc32_push((uint8_t)(current_opus_header.capture_pattern >> 24));
	  crc32_push(current_opus_header.header_type);
	  crc32_push(current_opus_header.version);
	  crc32_push((uint8_t)current_opus_header.granole_pos_l);
	  crc32_push((uint8_t)(current_opus_header.granole_pos_l >> 8));
	  crc32_push((uint8_t)(current_opus_header.granole_pos_l >> 16));
	  crc32_push((uint8_t)(current_opus_header.granole_pos_l >> 24));
	  crc32_push((uint8_t)current_opus_header.granole_pos_h);
	  crc32_push((uint8_t)(current_opus_header.granole_pos_h >> 8));
	  crc32_push((uint8_t)(current_opus_header.granole_pos_h >> 16));
	  crc32_push((uint8_t)(current_opus_header.granole_pos_h >> 24));
	  crc32_push((uint8_t)current_opus_header.bitstream_sn);
	  crc32_push((uint8_t)(current_opus_header.bitstream_sn >> 8));
	  crc32_push((uint8_t)(current_opus_header.bitstream_sn >> 16));
	  crc32_push((uint8_t)(current_opus_header.bitstream_sn >> 24));
	  crc32_push((uint8_t)current_opus_header.page_seq_num);
	  crc32_push((uint8_t)(current_opus_header.page_seq_num >> 8));
	  crc32_push((uint8_t)(current_opus_header.page_seq_num >> 16));
	  crc32_push((uint8_t)(current_opus_header.page_seq_num >> 24));
	  crc32_push(0);
	  crc32_push(0);
	  crc32_push(0);
	  crc32_push(0);
	  crc32_push(current_opus_header.segments_length);
	  crc32_push(current_opus_header.page_segments);
	  for(ui32Loop = 0 ; ui32Loop < i32len ; ui32Loop++){
		  crc32_push((uint8_t)pui8data[ui32Loop]);
	  }
	  current_opus_header.checksum = crc32_get();


//вывод в кносоль через обычный printf. 
	  i32_print_to_hex(current_opus_header.capture_pattern);
	  printf("%02X ", (uint8_t)current_opus_header.version);
	  printf("%02X ", (uint8_t)current_opus_header.header_type);
	  i32_print_to_hex(current_opus_header.granole_pos_l);
	  i32_print_to_hex(current_opus_header.granole_pos_h);
	  i32_print_to_hex(current_opus_header.bitstream_sn);
	  i32_print_to_hex(current_opus_header.page_seq_num);
	  i32_print_to_hex(current_opus_header.checksum);
	  printf("%02X ", (uint8_t)current_opus_header.segments_length);
	  printf("%02X ", (uint8_t)current_opus_header.page_segments);
	  for(ui32Loop = 0 ; ui32Loop < i32len ; ui32Loop++){
		  printf("%02X ", (uint8_t)pui8data[ui32Loop]);
	  }
  }

Освобождаем память.

free(pui8data);
free(popi16fmtBuffer);

Вспомогательные функции

Функция для вывода 32 битных данных в консоль отладки.

void i32_print_to_hex(uint32_t data){
	printf("%02X %02X %02X %02X ", (uint8_t)data, (uint8_t)(data>>8), (uint8_t)(data>>16), (uint8_t)(data>>24));
}

Функция для расчета crc32

#include "crc.h"

const unsigned int crc32_table[] =
{
  0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9,
  0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005,
  0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61,
  0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd,
  0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9,
  0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75,
  0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011,
  0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd,
  0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039,
  0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5,
  0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81,
  0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d,
  0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49,
  0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95,
  0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1,
  0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d,
  0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae,
  0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072,
  0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16,
  0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca,
  0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde,
  0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02,
  0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066,
  0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba,
  0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e,
  0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692,
  0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6,
  0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a,
  0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e,
  0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2,
  0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686,
  0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a,
  0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637,
  0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb,
  0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f,
  0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53,
  0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47,
  0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b,
  0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff,
  0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623,
  0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7,
  0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b,
  0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f,
  0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3,
  0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7,
  0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b,
  0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f,
  0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3,
  0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640,
  0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c,
  0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8,
  0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24,
  0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30,
  0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec,
  0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088,
  0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654,
  0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0,
  0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c,
  0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18,
  0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4,
  0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0,
  0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c,
  0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668,
  0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4
};

uint32_t g_crc32_seq = 0x00;

void crc32_clear(void){ 
    g_crc32_seq = 0x00; 
}

void crc32_push(uint8_t val){
    g_crc32_seq = (g_crc32_seq << 8) ^ crc32_table[((g_crc32_seq >> 24) ^ val) & 0xFF];
}

uint32_t crc32_get(void) { 
    return g_crc32_seq ^ 0x00; 
}

Вот и все. Для проведения тестов мне этого было достаточно. Я измерял внутренним таймером, что по времени кодировка моего фрагмента данных в 400 мс занимает, примерно 112 мс, что пригодно для преобразования данных «на лету» даже с таким тестовым кодом. Частота была выставлена максимальная — 80 МГц. Прерываний дополнительных не включалось.

Видос от пользователя опуса

https://www.st.com/en/embedded-software/x-cube-opus.html

OPUS от STM

https://www.ti.com/lit/an/spma076/spma076.pdf?ts=1647533032617

OPUS от TI

Comment

programel