Running the OPUS codec on the microcontroller

Initial data – we have an STM32 controller with very limited memory, and we want to record sound on it. Let’s say that there are a mountain and a small cart with examples of connecting the microphone we have chosen. As a result, we have a controller that can give us a WAV-like signal. I would like to record or transmit this WAV signal somewhere. There will be a lot of such data; there is a non-zero probability that we will not fit into the bandwidth of the channel being used or will fill the memory before we receive the necessary information. Compression comes to our aid!

Namely, an absolutely free, open and very productive OPUS codec. It is what is used in most streaming services, both independently and as an audio track processor. How beautiful it is – you can read it somewhere else. We will run it on a microcontroller with limited memory:

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

This codec has a wide range of settings. Each of them in a certain way affects the quality of the resulting sound, processing speed, etc., which is obvious. The first problem you may encounter is how to evaluate the sound quality after the compression/decompression process? You can subjectively listen to the result or look at spectrograms. The codec includes two other heavily modified codecs: SILK And CELT. Both work better in their own conditions – SILK, for example, copes better with spoken language, but is limited in frequency band. The OPUS encoder can operate in three modes: SILK, CELT And HYBRID. The names are descriptive enough to understand what they are.

What will we do in this project? Let’s connect the Opus library, process the audio data through it and generate an opus file.

So, let’s begin.

First of all, you need to download stable source codes from the manufacturer’s website (https://opus-codec.org/). At the time of writing, the latest version was 1.3.1

Let’s assume you’ve already created a project. Let’s unpack the archive and add codec files to the project.

In the project settings, add the OPUS_ROOT path to the folder with the codec

In the preprocessor settings we will add the following definitions

VAR_ARRAYS – The encoder will use variable length arrays to store calculations. If the compiler version does not support C99, then you should use the USE_ALLOCA macro

DISABLE_FLOAT_API And FIXED_POINT speed up calculations on the microcontroller and reduce memory usage.

OPUS_BUILD – everything is clear here

Instead of writing these macros directly in the project compilation settings, the same can be done through the config.h file, which comes with the encoder.

Now comes the uninteresting part. It is necessary to exclude or delete from the project all files that do not relate to the controller, processor and settings used. These are all files and folders containing in the name _test, _demo, _float, _ne10, _multistream_, mips, doc, win32, arm64, tests, as well as assembler files (.S), which contain instructions for a processor with support neon.

Right click on the file (Alt+Enter) -> Properties -> Execute resource from build

If you missed something, it’s okay. In any case, the compiler will tell you that something is left or simply will not compile if this functionality is not used.

What does my opus library folder look like in my project now?

Src

Include

Folder silk too big, so I will show only excluded files

Same thing with celt

Oh yes, don’t forget to add include paths

Ready. Next, to work with the encoder, you need to connect to main.c following header files

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

The project is ready for work. Now we will discuss a little about the structure of .opus files so that we can form them. Yes, the encoder takes as input a PCM stream of sequence from which it forms its own stream of encoded data. But this data will be impossible to reproduce on the receiving side if it is not packaged correctly. For example, TI, in their example with opus, introduces a kind of intermediate packaging option for these sequences so that we can decode back what we wrote down. But we want to get straight normal .opus files that can be opened on a PC from any player! This option is not suitable. The example from ST also used an intermediate container. Let’s do it normally without such ugly crutches.

Let’s open any opus file through a hex editor and what will we see?

Apparently, the opus file consists of Ogg containers containing codec data OPUS.

What is it like? Ogg container

Picture from wiki

Picture from wiki

I won’t describe it each from fields – their description is on the wiki and on the Ogg developer website. I will focus only on what is not obvious to me. Even after reading the documentation (https://xiph.org/ogg/doc/rfc3533.txt)

Header Type

0x02 for the very first

0x04 for the most recent

This is all true. But for those who BETWEEN them the Header Type should be 0x00unless it is a data package split into 2 “Ogg pages”, which is extremely rare.

Granule Position

Untranslatable and poorly described blood-sucking abomination. Represents a number that shows how many PCM samples were encoded during ALL previous pages PLUS this page.

This can be found out when operating the encoder and assembling the file.

Let’s imagine a situation – your microphone records data, the program puts it into the encoder, collects packets (pages, Page) and sends it to the PC.

For example, we just started, so you put the first 600 samples from the microphone into the encoder, and it spat out N encoded samples. Then you put in 700 samples, and he spat out M encoded ones. So the Granule Position for the first package (page) will be 600, and for the second package 600+700=1300. And so on.

Bitstream serial number

Oh, and this one is my favorite.

Documentation

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

This is probably obvious. But this is just a completely random number, which is the SAME for each page (package) for a given audio stream. And you come up with/generate this number YOUnot an opus or encoder.

Page sequence number

Just the Ogg page number. Starts from 0.

Check amount

Nothing complicated, but you need to be careful

The HxD hex editor allows you to calculate it for tests, for example.

To calculate the CS, you need to enter zeros in place of the CS and select the entire page (1 Ogg package)

Go to Analysis->Checksums

The checksum is calculated using a polynomial 4C11DB7. The result will be the following:

As you can see, the checksum is correct, but expanded.

Page Segments

A number that describes the number of table segments that will be next

Segment Table

The first is an array of table segment lengths. The length of this array is described by a number in Page Segments. After this array comes the data itself.

Example 1 – artificially created small file

In this example we have Page Segments is equal to 1. This means that the length of the array that is located after it will be equal to 1. We have an array of lengths of one number, which is equal to 0x31. This means that after this array there are 0x31 (49) pieces of data.

Example 2 – real file

In this example we have Page Segments equals 0x7F (247). Next comes an array of length 247 – highlighted for clarity. Each array cell describes the length of one data segment. After it comes the data itself. There are approximately 14,000 of them in this page (package).

Opus itself and its parameters(OpusHead And OpusTags which will be read by the decoder) are located in sections Segment Table 2 first Ogg containers

A description of these fields is in the document RFC 7845 Online Opus. Each one after is fairly well described without ambiguity or the need to dig deeper than necessary. Therefore, I will not duplicate information.

Let’s return to the project

Data for the encoder can be taken directly from the microphone. This project will compress information from a wav file. The Wav file was formatted as an array, which is connected via a .h file. To generate the output file I used the following Python script:

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()

It’s simple – the name of the input and output files. In the output file, add the name of the array and put a parenthesis at the end

The first thing you need to do is parse the format (preamble) of the WAV file. It is quite simple and unambiguous. I won’t describe it point by point, because… there are excellent websites with explanations (http://soundfile.sapp.org/doc/WaveFormat/). Just note that some of the fields have little endian, and some have big endian.

Let’s add the wav file data and the file for calculating crc. All the code will be in main.c one after another.

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

Let’s declare two definitions – the size of the processing window in ms and the size of the handler in bytes.

#define OPUS_FRAME_SIZE_IN_MS 10
#define OPUS_SIZE 880

Let’s create a structure for parsing the preamble.

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;

Now let’s create a structure for creating Ogg containers (pages).

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;

We will also indicate that there is a data array from wav and declare both created structures. Let’s declare the OPUS encoder

extern uint8_t sound_wav_u8[];

tWaveHeader sWaveHeader;
tOggHeader current_opus_header;

OpusEncoder *sOpusEnc;

To reduce the size of the array allocated for storing the results of the codec, I pack and send one segment per page. That is, each page will have one 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;

I’ll explain here (current_opus_header.page_seq_num = 2). This means I skipped the first two pages. The first two pages contain information for the OPUS decoder. My input data does not change, so the headers are also always the same and I add them myself using a hex editor. If your data is different, then you need to add the formation of OPUS headers, which should not be difficult, because it is also packaged in an Ogg container.

Parsing wav data

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

Creating an encoder

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

OPUS_APPLICATION_AUDIO tells the encoder that we expect to use wide-spectrum data. OPUS_APPLICATION_VOIPfor example, aims for a higher quality voice.

We configure the encoder in operating mode only with celt. The hybrid method, with the given settings, will also use celt. Mode SILK requires a lot more RAM to execute than we have in the controller.

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 kHz bandwidth

OPUS_SET_VBR(1) – variable quality bitrate

OPUS_SET_BITRATE((sWaveHeader.ui32SampleRate * 2)) – I set the output bitrate to twice the input bitrate.

Let’s declare an array to output data from the encoder

uint8_t pui8data[150];

Since OPUS accepts 16-bit data as input, we need to format the input data, so let’s declare it and calculate the size.

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;

Let’s see how many bytes are allocated for one record

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

Depending on the number of bytes for recording, an array is formed for transmission to the encoder (a piece of code with comments)

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]);
      }
  }

Freeing up memory.

free(pui8data);
free(popi16fmtBuffer);

Secondary functions

Function for outputting 32-bit data to the debug console.

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));
}

Function for calculating 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; 
}

That’s all. This was enough for me to carry out tests. I measured with an internal timer that the encoding of my 400 ms piece of data takes approximately 112 ms, which is suitable for converting data on the fly even with this test code. The frequency was set to the maximum – 80 MHz. No additional interrupts were enabled.

Video from opus user

https://www.youtube.com/watch?v=zLaQrhB1_zg

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

OPUS from STM

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

OPUS by TI

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *