2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

STM32F407でオーディオ再生

2
Last updated at Posted at 2026-05-26

背景

前職で

  • STM32F407/479
  • C言語
  • FatFs
  • SDカード/USBメモリ
  • 内蔵DAC
  • D級アンプ

を使って、音声案内をする組み込みシステムを作りました。

その時はリアルタイムOSを使っておらず、さらにDMAも使いこなせていなかったので、44.1KHzのタイマー割り込みで内蔵DACに音声データを直接書き込んでいました。

昨今、DMAを使わないと実現できない開発が増え、そういう中でSTM32F4のDMAの使い方がだんだんわかってきました。
他社マイコンのDMAに慣れすぎていて、STM32F4のDMAはとっつきにくかったのですが、CubeMXを使えば設定も簡単です。

そうしているうちに、

  • DAC
  • DMA
  • 転送トリガー用のタイマー
  • リアルタイムOSのタスク/キュー

を連動させれば、もっとシンプルにオーディオデータを送れるのでは?...と気づきました。

方針

制御ブロック図

ハードウェアは前職の時と同様で、外部に用意するのは

  • USBメモリ
  • D級アンプとスピーカー
  • 再生命令を出すもの

です。
再生命令はUSB CDCを利用し、ターミナルでコマンドを送ります。
ついでに、このターミナルでUSBメモリのファイルブラウズもできるので、USBメモリ内の曲を選んで再生させることができます。

image.png

音声データ

Windowsで使われるwavデータを使い、データの形式は

  • 44.1KHz
  • モノラル
  • 16bit整数

です。
音声案内であれば44.1KHzである必要はないですね。
人間の声だけであれば8KHzくらいで十分でしょう。

デバッグに音楽を流すので44.1KHzにしていますが、実際はスピーカーが貧弱なので、あまり意味はないですが...

私は敬虔なMacユーザーなので、音声データはAIFFを扱う方がいいと思ったのですが、マイコンがARMコアでリトルエンディアンなので、WAVの方が扱いやすいです。
AIFFだとバイトスワップが必要になるので、ここはARMコアで扱いやすい方がいいと考え、苦渋の決断です🤣

D級アンプとスピーカー

もうずいぶん昔ですが、秋月電子で手に入れたものです。
あっ、まだ売ってる。
100円スピーカーは結構重宝します。

マンション内での開発なので、ゲインは基板を改造して思いっきり下げています。
結構大きな音出せるんですよね...

リアルタイムOS

FreeRTOSを使います。
STM32CubeIDEでは簡単にFreeRTOSを組み込めるのと、資料もネット上や雑誌など豊富にありますからね。
今までは自作のスケジューラを使っていましたが、STM32F407は内蔵SRAMが128バイトあるので、フットプリントの小さい自作のスケジューラじゃなくてもメモリ容量は十分足ります。

2チャンネル同時再生

WAVデータをモノラルにした理由はここにあります。
音声案内を2つできるようにします。
そのためには、2つあるDACを独立して使う必要があり、音声データをモノラルにすれば、2つのDACに別々の音声データを送れます。

FreeRTOSのタスクは1つだけ

DAC(オーディオ)は2チャンネル同時再生をサポートしますが、データの資源となるUSBメモリは一つしかありません。
単純に考えればセマフォを使う手もありますが、私の方針としてセマフォは使わず、ロードした音声データの情報をキューに入れてもらって、1つのタスクで制御することにします。

なので、キューを待つタスク1つだけを作ります。

開発言語はC++

最近はこれ一択。
いろんなものをCからC++へ移植しているところです。
オーディオを2チャンネル扱うので、クラス化してインスタンスを2つ作るというのは自然な流れです。
コンパイラはC++17をサポートしたものを利用します。

DACの設定

DACを有効化

まずはDACを有効にすることと、転送のトリガーにタイマーを指定します。
タイマーは空いていればどれでもOKです。
今回はTimer 5とTimer 6を使います。

スクリーンショット 2026-05-26 11.01.45.png

DMAとの連携

DAC OUT 1と2の両方にDMAと連携させます。

転送のモードはCirclar、アドレスはペリフェラル(DAC/転送先)が固定、メモリ(転送元)はインクリメントします。

スクリーンショット 2026-05-26 11.05.15.png

タイマーの設定

今回は44.1KHzのオーディオデータを扱います。
44.1KHzのタイミングでトリガーを出力する設定はこちら。

スクリーンショット 2026-05-26 11.11.15.png

Trigger Output (TRGO) ParametersにUpdateEventを指定します。

タイマーのカウント値の計算はこちら。

\begin {align}
ARR &= \frac{1}{44.1KHz}\div\frac{1}{84MHz}-1\\
&= 1904.761\cdots - 1\\
&\fallingdotseq 1905-1\\
&=1904
\end {align}

C++でコーディング

まずはヘッダです。
オーディオ再生のためのC++クラスはLAudioLiteという名前にしました。
再生のために最低限必要な機能を定義しました。

USBメモリからのリードにかかる時間やリード間隔を考慮し、DMAのメモリサイズは4096ワード(8192バイト)にしました。
実際はこの半分のサイズを交互に読み出す仕組みです。

LAudioLite.hpp
#ifndef LAUDIOLITE_HPP
#define LAUDIOLITE_HPP

#define DEFAULT_SAMPLING_FREQ   44100
#define WAVE_SAMPLES 4096
#define HALF_LOAD_BYTES WAVE_SAMPLES
#define HALF_LOAD_WORDS (WAVE_SAMPLES >> 1)
#define HALF_POSITION (WAVE_SAMPLES >> 1)
#define AUDIO_OFFSET_LEVEL 0x8000

#define TASK_PRIORITY   26  //defaulTaskより上、最優先
#define LOAD_NEXT_DATA_QUEUE_LENGTH 64

#include "main.h"
#include "ff.h"

#ifdef __cplusplus

/*==============================================================================
    
==============================================================================*/
typedef struct LOADNEXTDATAINFO {
    void* audioInstance;
    short* aiffBufferPtr;
}LOADNEXTDATAINFO;

/*==============================================================================
    WAVファイルのヘッダ
==============================================================================*/
struct WAVHEADER {
    char            chunkID[4];     //RIFF
    unsigned long   chunkSize;      //Total Size - 8(except chunkID, chunkSize)
    char            fmt[4];         //WAVE

    char            fmtID[4];       //fmt
    unsigned long   fmtSize;        //linear PCM 16
    unsigned short  audioFormat;    //linear PCM  1 (0x0100)
    unsigned short  numberOfCh;     //1 = mono/2 = stereo
    unsigned long   samplingFreq;   //8KHz = 0x401f0000, 44.1KHz = 0x44ac0000
    unsigned long   averageOf1sec;  //1秒あたりのバイト数の平均
    unsigned short  blockSize;
    unsigned short  sampleBit;      //8bit = 0x0800, 16bit = 0x1000
    unsigned long   subChunkID;     //0x64617461 fix
    unsigned long   subChunkSize;   //波形データのバイト数
};
typedef struct WAVHEADER WAVHEADER;

/*==============================================================================

==============================================================================*/
class LAudio {
    public:
        LAudio(TIM_HandleTypeDef* htim, DMA_HandleTypeDef* hdma, int dacCh);
        void begin();

        void play(char* filename);
        void stop();

        void getQueueInfo(LOADNEXTDATAINFO* container, bool isHalf1);
    private:
        TIM_HandleTypeDef* _htim;
        DMA_HandleTypeDef* _hdma;
        int _dacCh;

        WAVHEADER _header;
        FIL _fp; //再生ファイルのオブジェクト
        short _dac_buffer[WAVE_SAMPLES];    //DMA用メモリ
        void _loadInitialWave(char* filename);
        static void _loadNextWave(void* pvParameters);
};

#endif  //__cplusplus
#endif  //LAUDIOLITE_HPP

コンストラクタ

htim(タイマ)、hdma(DMA)、dacCh(DACのCH)を引数として受け取り、インスタンス内に保持します。

void begin()

FreeRTOSのタスク生成など、クラスを使い始めるための準備をします。
組み込み系で利用されるもので、C++のコンストラクタで解決できない初期化をbegin()で実装するという習慣です。
Arduinoフレームワークでも使われていますね。

void play(char* filename)

filenameの音声ファイルを再生します。
filenameが有効かどうかは

  • playを呼ぶ時に判断
  • playの中で判断

システムに合わせて選べばいいと思います。
私はオーディオ関連の処理をシンプルにする目的で前者を採用しています。

void stop()

再生を停止します。

void getQueueInfo(LOADNEXTDATAINFO* container, bool isHalf1)

DMAの転送完了コールバックの中で、次にどのようなデータをDMAのメモリに読み出すか?
コールバックの種類が

  • 前半転送終了
  • 後半転送終了

の二つあります。
前者ならisHalf1がtrue, 後者ならfalseを指定します。
これでDMAのメモリの書き込む先のアドレスと、オーディオ再生のインスタンスのポインタが構造体に書き込まれます。
これをキューで送信します。

void _loadInitialWave(char* filename)

play()を呼ばれた時、最初のデータを読み込んでDMA用メモリに書き込むために用意します。
こちらでも特別なことはせず、USBメモリから読み込んでキューで送信します。

_loadNextWave(void* pvParameters)

FreeRTOSのキュー待ちタスクです。

  • 音声データを書き込むDMAメモリのポインタ
  • オーディオ再生のインスタンス

を使ってDMAメモリに音声データを書き込みます。

image.png

音声データの処理

wavのデータフォーマット

16bitの整数型データを対象にします。
-32768〜32767の値を持っています。
STM32F407のDACは12bitで0〜4096までの値なので、そのままDACに転送というわけにはいきません。
前職ではこんなふうにしていました。

int dacData = wavData;

dacData += 0x8000;//0〜65535に変換
dacData >>= 4;    //12bitに変換
DAC->DHR12R1;     //DACレジスタにライト

ところが、DACのレジスタには左詰めという仕様が存在することを知りました!
なので、こう書けばOKということがわかりました。

int dacData = wavData;

dacData += 0x8000;//0〜65535に変換
DAC->DHR12L1;     //DACレジスタに左詰めでライト

ちょうど下位4ビットがなくなってくれます。

あとは、単純にデータを持ち上げるだけなら、このように書くのがカッコイイかも。

int dacData = wavData;

dacData ^= 0x8000;//0〜65535に変換, 0x8000持ち上げるのと同じ効果
DAC->DHR12L1;     //DACレジスタに左詰めでライト

実際はDACのレジスタに転送せず、DAC用メモリへ書き込みます。
DMA転送開始時に

HAL_DAC_Start_DMA(&hdac, _dacCh, (uint32_t*)_dac_buffer, WAVE_SAMPLES, DAC_ALIGN_12B_L);

と指定すれば左詰めで転送されます。

cppソースコード

このページの最後に転載します。
最低限の機能なので、とても短くすることができました。

改良すべき点

今回掲載したものは最低限必要な機能です。
実際の音声再生としては、下記の改良が必要です。

  • 再生開始時にポップノイズが発生する
  • DMAの停止処理のタイミングが不適切
  • 再生終了のタイミングがわからない
  • 音量調整があるといい

再生開始時にポップノイズが発生する

オーディオアンプは入力段にDCカップリング用のコンデンサがあります。
オーディオデータをいきなりDACに転送するとポップノイズが発生します。
DCカップリングコンデンサが機能して微分回路になってます。
そのため、急峻な変化がD級アンプに伝わります。

これは停止時や、再生中に違う音声へ切り替えた際にも発生する可能性があります。

そこで、私はフェードイン/フェードアウトする処理を入れて、ポップノイズが出ないようにしました。

音声データの最初と最後は無音の場合が多く、短い期間にフェードイン/フェードアウトさせれば実害なないでしょう。
この辺は多少ハードウェアの知識と、サンプリングに関する知識が必要ですよ。

DMAの停止処理のタイミングが不適切

掲載しているソースでは、最後にUSBメモリから読み込んだデータが再生されません。
FatFsで最後のデータを読み込んだということはわかりますが、その時点でDMAを停止させると、そのデータが再生される前にDMAが止まります。

ポップノイズの件と同じく、最後のデータは無音の場合が多く、実質問題はなくて気づかれないでしょう。
エンジニア的にモヤモヤする場合は工夫が必要ですね。
私は

  • 最後のデータ読み出した
  • 再生を止めるべきか

という二段階で判断しています。
さらに、最後はフェードアウトでDACの値を0にしています。
ここはエンジニア的なこだわり😁

無音をハーフレベルの0x8000とするのもこだわりとしてはいいと思いますし、正負の値の場合の0と合いますから、考え方としては良いでしょう。
しかし、独自で作成するWAVデータとの整合が取れないことも多いです。
なので、内蔵DACを使う、そしてフェードイン/アウトをすることも考えると、無音は0とするのは割と理にかなっていると思います。

再生終了のタイミングがわからない

これはなくてもいいんでしょうが、連続再生などをする時には必要です。
適切なタイミングにコールバックを実行するようにすればいいでしょう。

私は適切にDMAを止めるタイミングでコールバックが登録されていれば実行するようにしました。

デモ動画

こちらにデモ動画を投稿しました。
今回掲載しているクラスはLAudioLiteというものですが、私のクラスはLAudioという改良された、さらに機能が追加されているものです。

オーディオをやられている方はすでにこのくらいの機能はお持ちでしょう...

この記事が「お手軽にオーディオをやってみたい」という方の参考になれば幸いです。

cppソースコード

#include "LAudioLite.hpp"
#include "FreeRTOS.h"
#include "queue.h"
#include "timers.h"

QueueHandle_t xLoadNextDataQueue;
void* audio1;
void* audio2;

extern DAC_HandleTypeDef hdac;

LAudio::LAudio(TIM_HandleTypeDef* htim, DMA_HandleTypeDef* hdma, int dacCh) :
    _htim(htim),
    _hdma(hdma),
    _dacCh(dacCh)
{
    if(_dacCh == DAC_CHANNEL_1) {
        audio1 = this;
    } else {
        audio2 = this;
    }
}

void LAudio::begin() {
    if(xLoadNextDataQueue == NULL) {    //セマフォを使わず、キュー・タスクを一つだけ生成して、このタスクだけが資源を利用する
        xLoadNextDataQueue = xQueueCreate(LOAD_NEXT_DATA_QUEUE_LENGTH, sizeof(LOADNEXTDATAINFO));
        xTaskCreate(_loadNextWave, "_loadNextWave", 256, NULL, TASK_PRIORITY, NULL);
    }
    HAL_TIM_Base_Start(_htim);
}

void LAudio::_loadInitialWave(char* filename) {
    UINT bytesOfRead;
    
    /*--------------------------------------------------------------------------
        リードするファイルを指定、オープン
    --------------------------------------------------------------------------*/
    f_open(&_fp, filename, FA_READ);
    f_read(&_fp, &_header, sizeof(WAVHEADER), &bytesOfRead);

    /*--------------------------------------------------------------------------
        再生データ読込み
    --------------------------------------------------------------------------*/
    LOADNEXTDATAINFO item;

    getQueueInfo(&item, true);  //前半
    xQueueSend(xLoadNextDataQueue, &item, 0);

    getQueueInfo(&item, false); //後半
    xQueueSend(xLoadNextDataQueue, &item, 0);
}
void LAudio::play(char* filename) {
    HAL_DMA_Abort(_hdma);
    _loadInitialWave(filename);
    HAL_DAC_Start_DMA(&hdac, _dacCh, (uint32_t*)_dac_buffer, WAVE_SAMPLES, DAC_ALIGN_12B_L);
}

void LAudio::stop()
{
    HAL_DMA_Abort(_hdma);
}

void LAudio::getQueueInfo(LOADNEXTDATAINFO* container, bool isHalf1) {
    container->audioInstance = this;

    if(isHalf1) {
        container->aiffBufferPtr = _dac_buffer;
    } else {
        container->aiffBufferPtr = &_dac_buffer[HALF_POSITION];
    }
}

void LAudio::_loadNextWave(void* pvParameters) {
    LOADNEXTDATAINFO item;
    
    for(;;) {        
        if(xQueueReceive(xLoadNextDataQueue, &item, portMAX_DELAY) == pdPASS) {
            LAudio* instance = static_cast<LAudio*>(item.audioInstance);
            UINT bytesOfRead;

            f_read(&instance->_fp, (char *)item.aiffBufferPtr, HALF_LOAD_BYTES, &bytesOfRead); //前半にロード

            int samples = (int)bytesOfRead >> 1;

            for(int i = 0; i < samples; i++) {
                item.aiffBufferPtr[i] ^= AUDIO_OFFSET_LEVEL;    //持ち上げてマイナスの値をなくす
            }

            if(bytesOfRead < HALF_LOAD_BYTES) {
                HAL_DMA_Abort(instance->_hdma); //ここでDMAを止めると、最後のデータは再生されずに終わってしまうので、工夫してください
            }
        }
    }
}

/*==============================================================================
    STのDAC/DMAのコールバック関数
==============================================================================*/
void HAL_DAC_ConvHalfCpltCallbackCh1(DAC_HandleTypeDef *hdac) {
    LAudio* instance = static_cast<LAudio*>(audio1);
    LOADNEXTDATAINFO item;

    instance->getQueueInfo(&item, true);
    xQueueSendFromISR(xLoadNextDataQueue, &item, NULL); //最高の優先順位を与えているので、コールバックを抜ければ確実にキューを実行してくれる
}
void HAL_DAC_ConvCpltCallbackCh1(DAC_HandleTypeDef *hdac) {
    LAudio* instance = static_cast<LAudio*>(audio1);
    LOADNEXTDATAINFO item;

    instance->getQueueInfo(&item, false);
    xQueueSendFromISR(xLoadNextDataQueue, &item, NULL);
}
void HAL_DACEx_ConvHalfCpltCallbackCh2(DAC_HandleTypeDef *hdac) {
    LAudio* instance = static_cast<LAudio*>(audio2);
    LOADNEXTDATAINFO item;

    instance->getQueueInfo(&item, true);
    xQueueSendFromISR(xLoadNextDataQueue, &item, NULL);
}
void HAL_DACEx_ConvCpltCallbackCh2(DAC_HandleTypeDef *hdac) {
    LAudio* instance = static_cast<LAudio*>(audio2);
    LOADNEXTDATAINFO item;

    instance->getQueueInfo(&item, false);
    xQueueSendFromISR(xLoadNextDataQueue, &item, NULL);
}


2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?