STM32でのサウンドのデジタル化(ADC + DMA)および送信用のSpeexエンコーディング

画像 昨日のGeektimesでの私の記事の続きで、STM32マイクロコントローラでのサウンドのデジタル化とエンコードの実装について詳しく説明します。



記事では、STM32CubeMXでプロジェクトをセットアップし、DMAを使用してADCから2つのリングバッファーにデータを収集し、Speexライブラリを接続してデータをエンコードする方法を示します。 おそらく、この資料は多くの人にとって非常に明白に見えるかもしれませんが、少なくとも誰かにとっては役に立つと思います。



猫をお願いします。



Speexとは何ですか?

Speexは、音声信号を圧縮するための無料のコーデックであり、Voice over Internet(VoIP)アプリケーションで使用できます。 Speexコーデックで圧縮されたデータは、Oggオーディオデータストレージ形式で保存するか、UDP / RTPパケットを使用して直接送信できます。 ©Wiki


STM32F4-Discoveryの記事Speech RecognitionからSpeexについて学びました。読むことをお勧めします。ほとんどのコードはそこから取られています。



エレメンタルベース



画像 この記事では、STM32F103C8T6マイクロコントローラーに基づいた最も安価で最も一般的なデバッグボードを使用します。 プログラマーは別途購入する必要があります。 ディスカバリーボードのアプローチは変わりません。 デバッグのために、マイクモジュールをMax9812アンプに接続しました。



最初に示した記事の図を見ることができます。 そこで、Max9812の出力からADC信号を直接駆動します。 これを行うには、購入したモジュールのOUTレッグのコンデンサを短絡する必要があります(これを行うことはできませんが、その方法はわかりません)。 入力では、約1.6Vの一定成分の信号が得られます。 それを外し、プログラムでエンコード用の文字タイプに変換します。



STM32CubeMXでプロジェクトを設定する



STM32F103C8T6マイクロコントローラーで新しいプロジェクトを作成しましょう。 まず、外部水晶振動子が接続されていることを示します。 クロッククオーツは必要ありませんが、これはデバッグボードにもあります。 シリアルワイヤデバッグインターフェイスを有効にすることを忘れないでください。 次に、必要なADC入力をオンにします。IN8があります(前の記事の回路を参照)。 さて、便利なタイマーは、DMAがバッファからデータを取得することによるものです。



画像



その後、「 クロック構成」タブに移動して、クロック回路を構成します。 次のようになりました。



画像



マイクロコントローラーのメイン周辺の周波数を最大72 MHzに設定しました。 タイマーには72 MHzもあります。この値を覚えておいてください。 別の方法で行うこともできますが、タイマーは独自の方法でカウントする必要があります。



[ 構成 ]タブに移動します。 ここで、ADC、DMA、およびタイマーを構成する必要があります。



タイマー3のトリガーによってADCを構成します。DMAタブで、このために最初のDMA周辺機器からメモリチャネル(周辺機器からメモリへ)を選択します。 プログラムに他に何もない場合、優先順位は重要ではありません。 モードはCircularで、ハーフワードデータのサイズ(ハーフワード、2バイト)とメモリアドレスがインクリメントされます。



画像



次に、タイマーを設定します。 Speexは、狭い周波数帯域(狭帯域、8 kHz)、ワイド(広帯域、16 kHz)、ウルトラワイド(超広帯域、32 kHz)でのデータのエンコードをサポートしています。 コントローラーをロードすることはありません。最低限必要です。 コントローラーは、8 kHzの周波数でADCからデータを取得する必要があることがわかりました。 タイマーで72 MHzを取得します。 私達は考慮します:

= frac1720000008kHz= frac18000=0.125ms= frac8kHz= frac720000008000=9000







タイマーを8999の値(結局、ゼロから開始)とUpdate Eventタイマーイベントに設定します。 グローバル割り込みボックスをチェックします。



画像



プロジェクトの生成に進むことができます。 プロジェクト→Serringsに移動します。 プロジェクトを保存するパスと、スタックとヒープのサイズを指定します。 Speexエンコードの場合、約0x600と0x1600が必要です。 その後、環境用に生成して開き、このIARを取得します。



画像



画像 Speexライブラリを接続する



最初に行うことは、Speexライブラリを含むSTM32F10x_Speex_LibフォルダーをプロジェクトのDriversフォルダーにコピーすることです。 次に、プロジェクトにlibspeexグループを追加し、その中に次のファイルを追加します(スクリーンショットを参照)。



[ プリプロセッサ ]タブのプロジェクトプロパティで、 HAVE_CONFIG_Hおよび次のディレクトリを追加します。



$ PROJ_DIR $ / .. / Drivers / STM32F10x_Speex_Lib / include

$ PROJ_DIR $ / .. / Drivers / STM32F10x_Speex_Lib / libspeex

$ PROJ_DIR $ / .. /ドライバー/ STM32F10x_Speex_Lib / STM32

$ PROJ_DIR $ / .. / Drivers / STM32F10x_Speex_Lib / STM32 / include

$ PROJ_DIR $ / .. / Drivers / STM32F10x_Speex_Lib / STM32 / libspeex

$ PROJ_DIR $ / .. / Drivers / STM32F10x_Speex_Lib / STM32 / libspeex / iar



コンパイルしてみましょう。すべて警告とエラーなしで問題ないはずです。



プログラミング



ここでの主なことは、特別に割り当てられたUSER CODE BEGIN-ENDブロックにコードを記述し、キューバプロジェクトに変更を加えて再生成する必要がある場合、すべてのコードが保存されることです。 別のspeexx.cファイルでライブラリを操作します。 これが彼のコードとspeexx.hヘッダーファイルのコードです。



speexx.h
#ifdef HAVE_CONFIG_H #include "config.h" #endif #include <speex/speex.h> #include "stm32f1xx_hal.h" #define FRAME_SIZE 160 //*0.125 = 20 ( 8) #define ENCODED_FRAME_SIZE 20 //  8  #define MAX_REC_FRAMES 90 //   ,  = MAX_REC_FRAMES*0,02 extern __IO uint16_t IN_Buffer[2][FRAME_SIZE]; extern __IO uint8_t Start_Encoding; void Speex_Init(void); void EncodingVoice(void);
      
      







speexx.c
 #include "speexx.h" //SPEEX variables __IO uint16_t IN_Buffer[2][FRAME_SIZE]; __IO uint8_t Start_Encoding = 0; uint8_t Index_Encoding = 0; uint32_t Encoded_Frames = 0; uint8_t REC_DATA[2][MAX_REC_FRAMES*ENCODED_FRAME_SIZE]; //    uint8_t* Rec_Data_ptr = &REC_DATA[0][0]; //    uint8_t* Trm_Data_ptr; //    int quality = 4, complexity=1, vbr=0, enh=1;/* SPEEX PARAMETERS, MUST REMAINED UNCHANGED */ SpeexBits bits; /* Holds bits so they can be read and written by the Speex routines */ void *enc_state, *dec_state;/* Holds the states of the encoder & the decoder */ void Speex_Init(void) { /* Speex encoding initializations */ speex_bits_init(&bits); enc_state = speex_encoder_init(&speex_nb_mode); speex_encoder_ctl(enc_state, SPEEX_SET_VBR, &vbr); speex_encoder_ctl(enc_state, SPEEX_SET_QUALITY,&quality); speex_encoder_ctl(enc_state, SPEEX_SET_COMPLEXITY, &complexity); } void EncodingVoice(void) { uint8_t i; //====================     ====================== if(Start_Encoding > 0) { Index_Encoding = Start_Encoding - 1; for (i=0;i<FRAME_SIZE;i++) IN_Buffer[Index_Encoding][i]^=0x8000; /* Flush all the bits in the struct so we can encode a new frame */ speex_bits_reset(&bits); /* Encode the frame */ speex_encode_int(enc_state, (spx_int16_t*)IN_Buffer[Index_Encoding], &bits); /* Copy the bits to an array of char that can be decoded */ speex_bits_write(&bits, (char *)Rec_Data_ptr, ENCODED_FRAME_SIZE); Rec_Data_ptr += ENCODED_FRAME_SIZE; Encoded_Frames += 1; Start_Encoding = 0; } if (Encoded_Frames == MAX_REC_FRAMES) { __no_operation(); //   ,    &REC_DATA[0][0] } if (Encoded_Frames == MAX_REC_FRAMES*2) { Rec_Data_ptr = &REC_DATA[0][0]; Encoded_Frames = 0; __no_operation(); //   ,    &REC_DATA[1][0] } }
      
      







また、stm32f1xx_it.cファイルでタイマーとDMA割り込みハンドラーを見つけ、Start_Encodingエンコードデータフラグを切り替えてTIM3_IRQnタイマーフラグをリセットすることで、これらを補足する必要があります。



割り込みハンドラー
 void DMA1_Channel1_IRQHandler(void) { /* USER CODE BEGIN DMA1_Channel1_IRQn 0 */ if (DMA1->ISR & DMA_FLAG_HT1) { Start_Encoding = 1; } //   DMA ,     if (DMA1->ISR & DMA_FLAG_TC1) { Start_Encoding = 2; } //  DMA ,      /* USER CODE END DMA1_Channel1_IRQn 0 */ HAL_DMA_IRQHandler(&hdma_adc1); /* USER CODE BEGIN DMA1_Channel1_IRQn 1 */ /* USER CODE END DMA1_Channel1_IRQn 1 */ } /** * @brief This function handles TIM3 global interrupt. */ void TIM3_IRQHandler(void) { /* USER CODE BEGIN TIM3_IRQn 0 */ HAL_NVIC_ClearPendingIRQ(TIM3_IRQn); /* USER CODE END TIM3_IRQn 0 */ HAL_TIM_IRQHandler(&htim3); /* USER CODE BEGIN TIM3_IRQn 1 */ /* USER CODE END TIM3_IRQn 1 */ }
      
      





したがって、メインプログラム全体は、タイマーとDMAの開始、Speexとそのコーディングの初期化(もちろん、標準のHAL初期化に加えて)に削減されます。



  Speex_Init(); if(HAL_TIM_Base_Start_IT(&htim3) != HAL_OK) Error_Handler(); if(HAL_ADC_Start_DMA(&hadc1,(uint32_t*)&IN_Buffer[0],FRAME_SIZE*2) != HAL_OK) Error_Handler(); while (1) { EncodingVoice(); }
      
      





次に、コードを少し見ていきます。 Speex_Init関数では、Speexエンコーダーのみが初期化され、デコーダーは個別に初期化する必要があります。



そこで、タイマートリガーでトリガーするようにADCを構成しました。 割り込みのタイマートリガーを0.125ms(8 kHz)ごとにリセットします。



 HAL_NVIC_ClearPendingIRQ(TIM3_IRQn);
      
      





DMAを中断することにより、次のことができます。



 if (DMA1->ISR & DMA_FLAG_HT1) { Start_Encoding = 1; } if (DMA1->ISR & DMA_FLAG_TC1) { Start_Encoding = 2; }
      
      





DMAが作業の半分を完了すると(DMAの半分が転送完了)、DMAが転送を完了すると(DMAの前半分がいっぱいになると)DMA_FLAG_TC1(転送完了フラグ)フラグがそれぞれ上がります(後半がいっぱいになる)。



ここで、私は知らなかったその時点で失われた興味深い機能に出会いました。 デバッガーでは、シャットダウン中にDMAの実行が継続されます。 したがって、バッファは常に満杯に見え、両方のフラグが上げられた状態になります。 この方法でDMAをデバッグすることはできません;それは停止しません。



 #define FRAME_SIZE 160 //*0.125 = 20 ( 8) #define ENCODED_FRAME_SIZE 20 //   #define MAX_REC_FRAMES 90 //   ,  = MAX_REC_FRAMES*0,02
      
      





ADCサンプリングは、ダブルバッファーIN_Buffer [2] [FRAME_SIZE]に送られ、各半分のサイズは160サンプルです。 出力では、アドレスRec_Data_ptrのREC_DATA [2] [MAX_REC_FRAMES * ENCODED_FRAME_SIZE]配列に送信されるENCODED_FRAME_SIZEバイトのデータを既に取得しています。 アドレスはENCODED_FRAME_SIZEずつ増加します。



各エンコード後、Encoded_Framesカウンターがインクリメントされ、MAX_REC_FRAMESになると、出力バッファーの前半が完全にいっぱいになり、データを取得できます。 残りの半分が満たされるまで、これを行う時間があります。 それぞれREC_DATA [0]およびREC_DATA [1]からデータを取得します。



フレームレーマー、品質設定などをいじってみることができますが、私はしませんでした。



 int quality = 4, complexity=1, vbr=0, enh=1;/* SPEEX PARAMETERS, MUST REMAINED UNCHANGED */
      
      





転送されたサウンドファイルの例は、最初の記事のリポジトリにあります。



素材



1. Githubで作成されたプロジェクトのリポジトリ

2. Speexコーデックマニュアル

3. Silicon Labsによるアプリケーションノート



All Articles