【STM32H723ZG入門⑧】ADC基礎編 | タイマトリガ+DMA実装

スポンサーリンク

この記事でできること

  • STM32H723ZGのADCを使用したタイマトリガサンプリングを実装できる
  • DMA circular modeを使用した連続データ取得基盤を構築できる
  • CPU依存しないサンプリング処理を実現できる
  • 取得データをUARTで出力し、外部ツールで解析できる
  • Analog Discovery 3を使用したFFT解析による実測評価ができる
  • FFTアナライザやデータロガーの基礎取得基盤を構築できる

本記事で使用したプロジェクト一式は、以下のGitHubリポジトリで公開しています
git clone することで完全な動作環境をそのまま再現できます。

GitHub - jin820-dev/GN-fw_core_stm32: Layered firmware core framework (app/drv/plat/utils) for reusable STM32 and embedded projects.
Layered firmware core framework (app/drv/plat/utils) for reusable STM32 and embedded projects. - jin820-dev/GN-fw_core_s...

注意: 本記事は執筆時点のディレクトリ構成およびファイル構成を元に解説しています。
GitHubのリポジトリは継続的に改善されているため、ディレクトリ構成やファイル名が一部変更されている場合があります。

はじめに


本記事に全く関係ありませんが、AD3をケースに入れてみました。jin@GoldearNetworksです。

STM32のADCは高性能であり、様々な用途に使用できます。しかし、単純なポーリング方式で使用した場合、以下の課題があります。

  • サンプリング周期がCPUの実行状態に依存して変動
  • 割り込みや分岐処理の影響で取得間隔が不均一
  • FFT解析や周波数解析において誤差の原因
  • 高精度な信号解析用途には適さない

これらの問題を解決するためには、CPUに依存しないサンプリング基盤が必要です。STM32には、この問題を解決するためのハードウェア機能が用意されています。

  • タイマトリガにより、正確な周期でADC変換を開始
  • DMAを使用して、CPUを介さずにメモリへデータを転送
  • DMA circular modeにより、連続したデータ取得を実現
  • CPU依存せず完全に一定周期のサンプリング処理を実現

本記事では、タイマトリガとDMA circular modeを使用したADC取得基盤を構築し、その動作を実測によって確認します。取得したデータはUART経由で数値として出力し、PC側でログとして保存することで解析に使用します。単に動作させるだけでなく、なぜこの構成が必要なのか、どのように動作しているのかについても順を追って解説します。

開発環境

本記事では、以下の環境および条件で動作確認を行っています。

ハードウェア

ソフトウェア

  • 開発環境: STM32CubeIDE v2.0.0
  • MCU設定: STM32CubeMX v6.16.1
  • 測定ツール: WaveForms v3.24.4(Digilent)

ADC条件

  • ADC:ADC1
  • 分解能:12-bit
  • サンプリング方式:タイマトリガ
  • DMA:DMA Circular Mode
  • バッファサイズ:4096 samples
  • サンプリング周波数:約200kHz
  • 入力信号:正弦波(976.5625Hz)

全体の流れ

本記事で構築するADC取得基盤の動作の流れを以下に示します。

① タイマが一定周期で動作

② ADC変換が開始される

③ 変換結果がメモリへ保存される(DMA)

④ 一定量のデータが蓄積される

⑤ UART経由でPCへ出力する

まず、① タイマ(TIM6)を使用して、一定周期でイベントを発生させます。

このイベントをきっかけに、② ADC変換が自動的に開始されます。

ADC変換が完了すると、その結果は ③ DMAによってメモリへ保存されます。

DMA(Direct Memory Access)は、ADCの変換結果をCPUを使わずに自動的にメモリへ保存してくれる仕組みです。そのため、CPUの処理状況に関係なく、正確な周期でデータを取得することができます。DMAは DMA circular modeで動作しており、メモリバッファが一杯になると先頭に戻り、連続してデータを保存し続けます。

④ 一定量のデータが蓄積されたタイミングで、CPUはそのデータを ⑤ UART経由でPCへ出力します。

PC側では、このデータをログとして保存し、解析に使用します。

Step1. CubeMX設定

まず、STM32CubeMXを使用してADC、DMA、タイマの設定を行います。本記事では、タイマをトリガとしてADC変換を開始し、その結果をDMAでメモリへ転送する構成を使用します。

ADC設定

ADC1を有効化します。PA3を入力ピンとして、ADC1_INP15に設定します。

次に、ADCの詳細設定を行います。本記事で使用する主な設定は以下の通りです。

  • Clock Prescaler: Asynchronous clock mode divided by 1
  • Resolution: ADC 12-bit resolution
  • Scan Conversion Mode: Disabled
  • Continuous Conversion Mode: Disabled
  • Discontinuous Conversion Mode: Disabled
  • End Of Conversion Selection: End of single conversion
  • Overrun behaviour: Overrun data overwritten
  • Conversion Data Management Mode: DMA Circular Mode
  • Low Power Auto Wait: Disabled

ADC_Regular_ConversionMode

  • Enable Regular Conversions: Enable
  • Enable Regular Oversampling: Disable
  • Number Of Conversion: 1
  • External Trigger Conversion Source: Timer 6 Trigger Out event
  • External Trigger Conversion Edge: Trigger detection on the rising edge

External Trigger Conversion SourceにTimer 6 Trigger Out eventを設定することで、TIM6の更新イベントをトリガとしてADC変換が開始されます。

Continuous Conversion Modeは使用せず、タイマによって変換タイミングを制御します。

Rank設定

  • Rank: 1
  • Channel: Channel 15
  • Sampling Time: 64.5 Cycles
  • Offset Number: No offset

本記事ではRegular conversionを1チャネル構成とし、Rank1に対象チャネルを割り当てています。Sampling Timeは取得品質や入力インピーダンスの影響に関わるため、後の評価パートで挙動を確認します。

DMA設定

次に、ADCのDMA設定を行います。ADC設定画面のDMA SettingsタブからDMAを追加します。DMAの設定を以下のように変更します。

主な設定は以下の通りです。

  • Mode: Circular
  • Direction: Peripheral to Memory
  • Data Width(Peripheral): Half Word
  • Data Width(Memory): Half Word
  • Memory Increment: Enable

Circular modeを使用することで、バッファの末尾まで転送が完了すると先頭に戻り、連続したデータ取得が可能になります。

TIM6設定

次に、ADCのトリガとして使用するTIM6を設定します。TIM6を有効化します。

TIM6のParameter Settingsを以下のように設定します。

主な設定は以下の通りです。

  • Prescaler: サンプリング周波数に応じて設定
  • Counter Period: サンプリング周波数に応じて設定
  • Trigger Event Selection: Update Event

Trigger Event SelectionをUpdate Eventに設定することで、タイマ更新イベントがADCトリガとして使用されます。これにより、タイマ周期に同期した正確なサンプリングが可能になります。

以上で、ADC、DMA、タイマの設定は完了です。次のStepでは、生成されたコードを使用してADC取得処理を実装します。

Step2. コード実装

本記事のコードはGitHubで公開しています。ここでは全コードを掲載せず、動作の要点となる部分を抜粋して解説します。

GitHub - jin820-dev/GN-fw_core_stm32: Layered firmware core framework (app/drv/plat/utils) for reusable STM32 and embedded projects.
Layered firmware core framework (app/drv/plat/utils) for reusable STM32 and embedded projects. - jin820-dev/GN-fw_core_s...

参照するファイル

  • drv_adc_trig_dma.c/.h(TIM6トリガADC + DMA circular のドライバ)
  • app_adc_log.c/.h(前半データをスナップショットして数値出力)
  • app_run.c(main loop:1回だけCSV出力して停止)
  • app_boot.c(起動時にUART stdio初期化、ADCドライバ起動)

drv_adc_trig_dma:ADCをTIM6トリガ + DMA circularで連続取得する

DMAが書き込むバッファは静的に確保します。STM32H7ではキャッシュの影響を避けるため、バッファにアラインメントを指定しています。

#define DRV_ADC_TRIG_DMA_BUF_SIZE 4096u
static uint16_t s_buf[DRV_ADC_TRIG_DMA_BUF_SIZE] __attribute__((aligned(32)));

初期化ではADCのキャリブレーションを実行します。

drv_adc_trig_dma_status_t drv_adc_trig_dma_init(void)
{
    if (HAL_ADCEx_Calibration_Start(&hadc1, ADC_CALIB_OFFSET, ADC_SINGLE_ENDED) != HAL_OK) {
        return DRV_ADC_TRIG_DMA_ERR_HAL;
    }
    s_init = true;
    return DRV_ADC_TRIG_DMA_OK;
}

開始処理では、TIM6を起動し、その後にADC+DMAを開始します。

if (HAL_TIM_Base_Start(&htim6) != HAL_OK) return DRV_ADC_TRIG_DMA_ERR_HAL;

if (HAL_ADC_Start_DMA(&hadc1, (uint32_t*)s_buf, DRV_ADC_TRIG_DMA_BUF_SIZE) != HAL_OK) {
    (void)HAL_TIM_Base_Stop(&htim6);
    return DRV_ADC_TRIG_DMA_ERR_HAL;
}

DMAの転送完了は「半分」「全部」の2点で検出します。HALコールバック内では重い処理を行わず、フラグを立てるだけにします。

static volatile bool s_half_ready = false;
static volatile bool s_full_ready = false;

void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
    if (hadc->Instance == ADC1) {
        s_half_ready = true;
    }
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    if (hadc->Instance == ADC1) {
        s_full_ready = true;
    }
}

app層からはポーリングでイベントを確認できるようにしています。この関数は「発生したか?」を返し、同時にフラグをクリアします。

bool drv_adc_trig_dma_poll_half(void)
{
    if (s_half_ready) {
        s_half_ready = false;
        return true;
    }
    return false;
}

app_adc_log:前半2048点をスナップショットして数値出力する

本記事では、DMAバッファ前半(4096/2=2048点)が揃ったタイミングで1回だけデータを取り出します。DMAバッファは更新され続けるため、出力に使うデータは一度別バッファにコピーします。

#define CSV_N   (DRV_ADC_TRIG_DMA_BUF_SIZE/2)
static uint16_t s_csv_buf[CSV_N];

if (!drv_adc_trig_dma_poll_half()) {
    return false;
}

const uint16_t* buf = drv_adc_trig_dma_get_buffer();
for (uint32_t i = 0; i < CSV_N; i++) {
    s_csv_buf[i] = buf[i];
}

取得できているかを確認するため、min/max/sumなどの統計値も計算します。

typedef struct {
    uint16_t min;
    uint16_t max;
    uint32_t sum;
    uint32_t n;
} app_adc_stats_t;

データはUART経由で数値として出力します。出力は「1行1サンプル」で、PC側でログとして保存して解析に使用します。

static void dump_csv_u16(const uint16_t* p, uint32_t n)
{
    for (uint32_t i = 0; i < n; i++) {
        printf("%u\r\n", (unsigned)p[i]);
    }
}

app_boot:UART stdio初期化とADC取得開始

printf出力はUARTへリダイレクトして使用します。起動時にUART stdioを初期化し、その後ADCドライバを起動します。

void app_boot(void)
{
    plat_uart_stdio_init();

    if (drv_adc_trig_dma_init() == DRV_ADC_TRIG_DMA_OK) {
        drv_adc_trig_dma_start();
    }
}

app_run:1回だけ出力して停止する

メインループでは、CSV出力が完了したら停止します。デバッグや評価用途では、この形が最も扱いやすいです。

app_adc_stats_t st = {0};

while (1)
{
    if (app_adc_log_once_on_half_ready(&st)) {
        LOG_INFO("APP", "ADC CSV done. min=%u max=%u n=%lu",
                 (unsigned)st.min, (unsigned)st.max, (unsigned long)st.n);

        while (1) { __NOP(); }
    }
}

次のStepでは、実際に正弦波を入力し、UARTログが取得できることを確認します。

Step3. 動作確認

ここでは、実際に正弦波を入力し、UART経由でADCデータが取得できることを確認します。

接続構成

Analog Discovery 3のWavegen機能を使用して正弦波を生成し、STM32のADC入力ピンへ接続します。

接続構成を以下に示します。

  • AD3 Wavegen OUT → STM32 PA3 (ADC入力ピン)
  • AD3 Wavegen GND → STM32 GND
  • STM32H723 Nucleo (ST-Link) → PC(UARTログ取得)

正弦波の設定

本記事では、以下の条件で信号を入力します。

  • 波形:Sine
  • 周波数:976.5625 Hz
  • 振幅:1 V
  • オフセット:1.65 V

この周波数は、FFTの周波数分解能と一致する値になるよう設定しています。FFTの周波数分解能は以下で表されます。

Δf = Fs / N

今回の条件では、以下のようになります。

Fs = 200 kHz
N = 2048
Δf = 97.65625 Hz

976.5625 Hzは、この周波数分解能の整数倍に一致するため、FFT解析時にスペクトルが1つのビンに正確に現れます。これにより、スペクトルリーケージが発生せず、ADCの性能を正しく評価することができます。

※補足情報: スペクトルリーケージとは、本来1つの周波数成分である信号が、FFT解析時に複数の周波数に広がってしまう現象

UARTログの取得

STM32CubeIDEのコンソール、またはTera Termなどのシリアルターミナルを使用してUARTログを取得します。プログラムを実行すると、ADCで取得したデータが数値として出力されます。

以下は実際の出力例です。

492
504
513
512
515
520
515
524
531
528
530
539
546
544
544

このように、正弦波に対応した数値が取得できていることが確認できます。これらの値は、入力した正弦波に対応して周期的に変化しています。ADCは12-bit分解能のため、値は0〜4095の範囲で出力されます。今回の設定では、オフセットを1.65Vに設定しているため、値は中間付近を中心に変化しています。

取得結果の確認

数値の変化を見ることで、正弦波が正しく取得できていることが確認できます。

  • 値が周期的に増減している
  • 最大値と最小値が繰り返されている
  • 不連続な変化がない

ログの保存

シリアルターミナルのログ保存機能を使用して、出力されたデータをファイルとして保存します。保存したログは、後の解析で使用します。

私は Tera Term ログで保存しています。注意として画像は例です。正弦波を入力していないので、値が極端に小さいです

以上で、タイマトリガ + DMAによるADC取得が正常に動作していることが確認できました。

Step4. 内部解説

本記事では、タイマトリガとDMA circular modeを使用してADC取得を行いました。ここでは、なぜこの構成が必要なのかを内部動作から解説します。

なぜタイマトリガを使うのか?

ADCはソフトウェアから直接変換を開始することも可能です。

例えば、以下のようなコードです。

HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, timeout);
value = HAL_ADC_GetValue(&hadc1);

しかし、この方法には重要な問題があります。

  • CPUの実行時間に依存する
  • 割り込み処理の影響を受ける
  • サンプリング周期が一定にならない

例えば、理想的には以下のように一定間隔でサンプリングしたいとします。

|----|----|----|----|----|
↑    ↑    ↑    ↑    ↑    ↑
sample point

しかし、ソフトウェア制御では、実際には以下のようにばらつきが発生します。

|---|------|--|-----|----|
↑   ↑      ↑  ↑     ↑    ↑
sample point

このばらつきは、FFT解析において大きな問題となります。FFTは一定周期でサンプリングされたデータを前提としているため、サンプリング周期が不均一だと、スペクトルが正しく計算されません。

これを解決するために使用するのがタイマトリガです。タイマはハードウェアで動作しており、CPUの状態に関係なく正確な周期でイベントを発生させることができます。このタイマイベントをADCのトリガとして使用することで、完全に一定周期のサンプリングを実現できます。

TIM6 → ADC → DMA → Memory

これにより、FFT解析や信号測定に適した高精度な取得基盤を構築できます。

なぜDMA circular modeを使うのか?

ADCの変換結果は、CPUで直接読み取ることも可能です。しかし、この方法ではサンプリングのたびにCPUが処理を行う必要があります。サンプリング周波数が高くなると、CPUの負荷は急激に増加します。

例えば、200kSPSでサンプリングする場合、「1秒間に200,000回の処理が必要」となります。

これはCPUにとって非常に大きな負荷となります。DMAを使用すると、ADCの変換結果はCPUを介さずに自動的にメモリへ転送されます。

ADC → DMA → Memory

CPUはDMAの完了通知を受け取るだけでよく、サンプリングごとの処理は不要になります。さらに、DMA circular modeを使用することで、バッファの末尾まで到達すると自動的に先頭へ戻り、連続してデータを保存し続けます。

Memory Buffer
[0....................4095]
 ↑                     ↓
 └───── circular ──────┘

これにより、以下のような利点があります。

  • CPU負荷を最小限に抑える
  • 連続したデータ取得を実現する
  • 高いサンプリング周波数に対応できる

本記事で構築した構成は、FFT解析や信号測定などの用途に適した、実用的なADC取得基盤となっています。

ハマりポイント・注意点

ADC + DMA + タイマトリガ構成では、いくつか注意すべきポイントがあります。本記事で実装する際に重要だった点をまとめます。

① サンプリング周期はTIM6の設定で決まる

本記事では、TIM6のTrigger Out eventを使用してADC変換を開始しています。つまり、ADCのサンプリング周期はTIM6のタイマ周期によって決まります。TIM6の周期は、以下の2つの設定で決まります。

  • Prescaler
  • Counter Period

TIM6はAPB1 Timer Clockで動作しており、本記事の設定では192MHzで動作しています。このクロックはCubeMXのClock Configuration画面で確認できます。

TIM6のサンプリング周波数は、以下の式で計算できます。

Sampling Frequency = 192 MHz / ((Prescaler + 1) × (Counter Period + 1))

PrescalerまたはCounter Periodを変更することで、サンプリング周波数を調整できます。

本記事では、以下のようなTIM6設定により約200kHzでサンプリングしています。

Prescaler = 0
Period = 959

Fs = 192 MHz / 960
= 200 kHz

またADCのSampling Time設定は変換精度に影響しますが、サンプリング周期には影響しません。ADCはTIM6のトリガを受けたタイミングで変換を開始するため、サンプリング周期はTIM6によって完全に制御されます。

② Continuous Conversion Modeは使用しない

ADCにはContinuous Conversion Modeがありますが、本記事では使用していません。

Continuous Conversion Modeを使用すると、ADCは変換完了後すぐに次の変換を開始します。この場合、サンプリング周期はADCの変換時間に依存し、正確な周期制御ができません。タイマトリガを使用することで、正確な周期でサンプリングを行うことができます。

③ DMA circular modeを使用する

DMAにはNormal modeとCircular modeがあります。

Normal modeでは、バッファの末尾まで転送が完了するとDMAは停止します。連続してデータを取得する場合は、Circular modeを使用する必要があります。

Circular modeでは、バッファの末尾まで到達すると先頭に戻り、自動的に転送を継続します。これにより、連続したデータ取得が可能になります。

④ Overrun設定

Overrunは、新しいデータが到着した際に古いデータをどう扱うかを決める設定です。本記事では、以下の設定で使用し、古いデータを上書きします。DMA circular modeと組み合わせる場合、この設定が適しています。

Overrun behaviour:Overrun data overwritten

⑤ FFT解析では周波数設定が重要

FFT解析を行う場合、入力信号の周波数はFFTの周波数分解能と一致させる必要があります。

一致していない場合、スペクトルリーケージが発生し、正しい評価ができません。本記事ではFFTの周波数分解能に一致する周波数を使用しています。FFT解析については、次回の記事で詳しく解説します。

まとめ

本記事では、STM32H723ZGの内蔵ADCを使用し、タイマトリガとDMAを組み合わせたADC取得基盤を構築しました。

本記事で実装した内容:

  • TIM6のTrigger Out eventを使用した一定周期サンプリング
  • DMA circular modeによる連続データ取得
  • UART経由でのADCデータ出力
  • シリアルターミナルを使用したログ保存
  • 約200kHzでの安定したサンプリング動作の確認

これにより、CPU負荷に依存しない、高精度で安定したADC取得基盤を構築できました。本記事で構築した構成は、信号測定やデータ解析などの用途にそのまま使用できます。

次回の記事では、取得したデータをFFT解析し、以下の内容を解説します。

  • FFTによる周波数スペクトル解析
  • スペクトルリーケージの確認
  • ノイズフロアの測定
  • STM32H723ZG内蔵ADCの性能評価

これにより、STM32H723ZGのADC性能を実測ベースで評価していきます。

前回

【STM32H723ZG入門⑦】共通FW基盤編 | ディレクトリ構成とレイヤ構造の構築
この記事でできること 再利用可能な共通ファームウェア基盤を構築できる 長期保守を前提としたディレクトリ構成を設計できる app / drv / plat / utils / mw によるレイヤ分離アーキテクチャを定義できる main.c を...

次回: (準備中)

コメント