STM32 ADC プログラミングの第一歩

STM32 ADC プログラミングの第一歩:アナログ信号をデジタル世界へ

はじめに:なぜADCが必要なのか?

私たちの身の回りには、温度、光、音、圧力など、さまざまなアナログ信号が存在します。これらは連続的に変化する物理量です。しかし、コンピューターやマイコン(マイクロコントローラー)のようなデジタル回路は、0と1の組み合わせであるデジタル信号しか扱うことができません。

例えば、センサーを使って周囲の温度を測定したいと考えたとします。温度センサーが出力するのは、温度に応じて変化する電圧などのアナログ信号です。このアナログ信号をマイコンで読み取り、処理するためには、デジタル信号に変換する必要があります。この変換を行うのが、ADC (Analog-to-Digital Converter) 、すなわちアナログ・デジタル変換器です。

STM32マイコンは、高性能で多機能なADCモジュールを内蔵しています。このADCを使うことで、外部のアナログセンサーから得られる信号を読み取り、デジタル値としてプログラムで処理できるようになります。これは、組み込みシステム開発において非常に重要な技術の一つです。

この記事では、STM32マイコンにおけるADCプログラミングの「第一歩」として、その基本的な仕組みから、STM32 CubeMXを使った設定方法、HALライブラリを使った簡単なコード例までを、初心者向けに詳細に解説します。約5000語のボリュームで、ADCの概念から具体的な実装までをじっくりと学ぶことができます。

第1章: ADCの超基本 – アナログ信号とデジタル信号の橋渡し

ADCプログラミングを始める前に、まずはADCがどのようにアナログ信号をデジタル信号に変換するのか、その基本的な仕組みを理解しましょう。

アナログ信号は、時間とともに連続的に変化する信号です。例えば、オーディオ信号や温度センサーの出力電圧などです。一方、デジタル信号は、決められた時間に飛び飛びの値(通常は0か1)をとる信号です。マイコンはデジタル信号を処理します。

ADCは、このアナログ信号をデジタル信号に変換するプロセスを担います。この変換プロセスは、主に以下の3つのステップからなります。

  1. サンプリング (Sampling):

    • アナログ信号は連続的であるため、デジタルで扱うためには、特定の瞬間の値だけを「抜き出す」必要があります。この抜き出す作業をサンプリングと呼びます。
    • サンプリングは、一定の時間間隔(サンプリング周期)で行われます。サンプリング周期の逆数がサンプリング周波数(サンプリングレート)です。サンプリングレートが高いほど、元の信号の情報をより細かく捉えることができますが、処理負荷は増加します。
    • STM32のADCでは、「サンプリング時間」として、各チャネルの入力をどれくらいの時間保持するかを設定できます。この時間が短いと高速なサンプリングが可能ですが、入力信号のソースインピーダンスが高い場合など、正確なサンプリングができないことがあります。適切なサンプリング時間の設定は重要です。
  2. 量子化 (Quantization):

    • サンプリングによって得られたアナログ信号の値は、まだ連続的な値です。これをデジタルの飛び飛びの値に対応させる作業を量子化と呼びます。
    • 量子化では、アナログ信号の入力レンジ(例えば0Vから3.3V)を、離散的な複数のレベルに分割します。分割されたレベルの数が、デジタル値の表現可能な値の範囲を決定します。
    • このレベルの細かさを表すのが「分解能 (Resolution)」です。分解能は通常、ビット数で表されます。例えば、8ビット分解能であれば $2^8 = 256$ レベル、12ビット分解能であれば $2^{12} = 4096$ レベルに分割されます。STM32のADCは、一般的に10ビット、12ビット、14ビット、16ビットなどの分解能を持っています。分解能が高いほど、より細かいアナログ信号の変化を捉えることができます。
  3. 符号化 (Encoding):

    • 量子化によって得られた各レベルに対応するデジタル値を、2進数のコード(ビット列)に変換する作業を符号化と呼びます。
    • 例えば、12ビット分解能のADCの場合、アナログ入力レンジを4096レベルに分割し、それぞれのレベルを0から4095までの12ビットの2進数で表現します。

これらのステップを経て、連続的に変化するアナログ信号は、一定の時間間隔でサンプリングされ、決められた分解能でデジタル値に変換されます。このデジタル値は、マイコンのプログラムで読み取って利用することができます。

重要なパラメータ:

  • 分解能 (Resolution): デジタル値が表現できる細かさ。ビット数で示され、高いほど微細な変化を捉えられる。
  • サンプリングレート (Sampling Rate): 1秒間に何回サンプリングを行うか。高いほど高速な信号変化に対応できるが、処理負荷が増える。
  • 入力レンジ (Input Range): ADCが正しく変換できるアナログ入力信号の電圧範囲。通常は0Vから基準電圧(Vref)までです。
  • 基準電圧 (Reference Voltage – Vref): 量子化の基準となる電圧です。入力レンジの上限(または下限と上限)を定義します。STM32では、電源電圧(VDDA)を基準電圧として使うことが多いですが、専用のVref+ピンや内部基準電圧を使うことも可能です。正確な変換には、安定した基準電圧が不可欠です。

ADCで得られたデジタル値 $D$ とアナログ入力電圧 $V_{in}$、基準電圧 $V_{ref}$、分解能 $N$ビットの間には、概ね以下の関係があります。

$D = \frac{V_{in}}{V_{ref}} \times (2^N – 1)$

これから、デジタル値 $D$ から元の入力電圧 $V_{in}$ を計算するには、以下の式を使います。

$V_{in} = \frac{D}{2^N – 1} \times V_{ref}$

例えば、12ビット分解能 ($N=12$)、基準電圧 $V_{ref}=3.3V$ のADCで、デジタル値 $D=2048$ を取得した場合、入力電圧は次のようになります。

$V_{in} = \frac{2048}{2^{12} – 1} \times 3.3V = \frac{2048}{4095} \times 3.3V \approx 0.500 \times 3.3V \approx 1.65V$

このように、取得したデジタル値から、元の物理量(電圧、そしてセンサーの特性に応じた温度や光量など)を計算することができます。

第2章: STM32のADCを理解する

STM32マイコンには、多くのモデルに高性能なADCモジュールが内蔵されています。STM32ファミリーによってADCモジュールの機能や性能は異なりますが、基本的な構成や使い方は共通しています。

STM32 ADCの主な特徴

  • 複数のチャネル: 複数のアナログ入力ピンに対応しており、それぞれ異なるアナログ信号を読み取ることができます。内部チャネル(温度センサー、内部基準電圧など)も利用可能です。
  • 高い分解能: 一般的に12ビット、上位モデルでは14ビットや16ビットの分解能を持つものもあります。
  • 高速変換: 高速なサンプリングレートに対応しています。
  • 柔軟な設定:
    • サンプリング時間: チャネルごとに設定可能です。
    • 変換モード:
      • 単一変換モード: 1回のトリガーで1回だけ変換を行います。
      • 連続変換モード: 1回のトリガーで変換を連続して行います。
    • スキャンモード: 複数のチャネルを順次変換します。単一変換または連続変換と組み合わせて使用します。
    • トリガーソース: ソフトウェアトリガー、タイマーの更新イベント、外部ピンからのトリガーなど、様々なトリガー源を選択できます。
    • データアライメント: 変換結果のデジタル値をレジスタの右端(右詰め、LSB側に詰める)または左端(左詰め、MSB側に詰める)に揃えることができます。通常は右詰め(LSBアライメント)を使用します。
  • データ転送方法:
    • ポーリング (Polling): プログラムが変換完了を待ち続けます。シンプルですが、CPUリソースを占有します。
    • 割り込み (Interrupt): 変換完了時に割り込みを発生させ、割り込みハンドラで結果を処理します。CPUリソースを効率的に使えます。
    • DMA (Direct Memory Access): 変換結果をメモリに直接転送します。CPUの介入なしに大量のデータを高速に転送できます。複数チャネルのスキャンモードや高速サンプリングで特に有効です。

ADCブロック構成(概念図)

STM32のADCモジュールは、おおよそ以下の要素で構成されています。

  1. アナログ入力ピン (ADCx_INy): 外部のアナログ信号を入力するピンです。使用する際は、GPIOをアナログ入力モードに設定する必要があります。
  2. 内部チャネル: マイコン内部の信号源(温度センサー、Vrefint、Vbatなど)への入力です。ピン設定は不要です。
  3. マルチプレクサ (Multiplexer – MUX): 複数の入力チャネルの中から、現在変換を行うチャネルを1つ選択するスイッチのようなものです。スキャンモードでは、設定されたチャネルを順次切り替えていきます。
  4. サンプル&ホールド回路 (Sample & Hold – S&H): マルチプレクサで選択されたアナログ入力信号を、サンプリング期間中に正確な値で保持するための回路です。これにより、変換中に信号が変動しても安定した値をADCコアに供給できます。サンプリング時間の長さは、この回路が信号を保持する時間に関係します。
  5. ADCコア (ADC Core): 保持されたアナログ電圧を、設定された分解能でデジタル値に変換する回路です。STM32では、一般的に逐次比較型ADC (SAR ADC) が使用されます。
  6. データレジスタ (ADC_DR): 変換されたデジタル結果が格納されるレジスタです。
  7. 制御レジスタ (ADC_CR1, ADC_CR2, etc.): ADCの動作モード、分解能、トリガーソース、割り込み設定などを構成するためのレジスタ群です。HALライブラリを使う場合、これらのレジスタを直接操作するのではなく、構造体や関数を通して設定します。
  8. ステータスレジスタ (ADC_SR): 変換完了などのADCの状態を示すフラグが格納されます。

STM32 ADC Block Diagram Concept
(STマイクロエレクトロニクスの資料から概念を引用。実際のブロック図はリファレンスマニュアルを参照してください)

GPIO設定(アナログモード)

外部アナログ入力ピンを使用する場合、そのピンをGPIOとして初期化する際に、モードを「アナログ入力」に設定する必要があります。アナログ入力モードでは、そのピンに関連するデジタル回路(プルアップ/プルダウン抵抗、シュミットトリガーなど)が無効になり、純粋なアナログ入力として機能するようになります。CubeMXを使う場合は、ピンの設定で簡単に選択できます。

第3章: ADCプログラミングの準備

STM32でADCプログラミングを始めるためには、いくつかの準備が必要です。

開発環境の準備

STM32開発で現在最も推奨されている統合開発環境(IDE)は、STマイクロエレクトロニクスが無償で提供するSTM32CubeIDEです。STM32CubeIDEは、コード生成ツールであるSTM32CubeMXの機能と、コーディング、ビルド、デバッグ機能を統合しています。

あるいは、STM32CubeMXでプロジェクト設定とコード生成を行い、生成されたコードをKeil MDK-ARMIAR Embedded Workbench for ARM、またはGCC (GNU Tools for ARM Embedded Processors) といった外部のIDEで開発することも可能です。初心者にはSTM32CubeIDEがおすすめです。

必要なライブラリ (HAL)

STマイクロエレクトロニクスは、STM32のペリフェラル(ADC、UART、SPIなど)を簡単に設定・操作するための抽象化レイヤーとして、HAL (Hardware Abstraction Layer) ライブラリを提供しています。CubeMXでプロジェクトを生成する際に、HALライブラリを組み込むことができます。本記事では、HALライブラリを使ったプログラミングを中心に解説します。HALを使うことで、特定のSTM32シリーズやモデルに依存しない、移植性の高いコードを書くことができます。

クロック設定の重要性

STM32のペリフェラルは、それぞれ動作に必要なクロックを受け取っています。ADCモジュールも同様に、正確な変換を行うためには適切なクロックが必要です。ADCクロックは、システムクロック(HCLK)やAPBクロックなどから分周して供給されます。

STM32CubeMXを使う場合、クロック設定ツールでADCに供給されるクロック源と分周比を設定できます。ADCモジュールの仕様で規定されている最大クロック周波数を超えないように設定することが重要です。サンプリングレートは、ADCクロック周波数と、チャネル設定で指定するサンプリング時間によって決まります。

(ADC変換時間) = (サンプリング時間) + (12ビット変換時間)
(サンプリングレート) = 1 / (ADC変換時間)

厳密には、ADCコアの変換時間は分解能によって異なります。12ビット分解能の場合、通常12.5 ADCクロックサイクルが必要です(これはSTM32ファミリーや特定のADCモジュールによって異なる場合があります。正確な値は使用するマイコンのリファレンスマニュアルを参照してください)。

例えば、ADCクロックが14MHz、サンプリング時間が12サイクル(STM32L4などの場合、サンプリング時間として設定できるサイクルの値)の場合、12ビット変換にかかる時間は $12 + 12.5 = 24.5$ ADCクロックサイクルとなります。

変換時間 = $24.5 \text{サイクル} / 14 \text{MHz} = 24.5 / 14,000,000 \text{秒} \approx 1.75 \mu\text{s}$
サンプリングレート = $1 / 1.75 \mu\text{s} \approx 571 \text{kHz}$

適切なサンプリング時間を設定しないと、入力信号が安定する前にサンプリングが行われ、不正確な値を取得してしまう可能性があります。特に、入力信号源のインピーダンスが高い場合や、マルチプレクサでチャネルを切り替えた直後は、S&H回路のキャパシタが充電されるまでに時間が必要なため、十分なサンプリング時間を確保することが重要です。

第4章: STM32CubeMXを使ったADC設定

STM32 CubeMXを使うと、ADCの設定をGUIで簡単に行い、その設定に基づいた初期化コードを自動生成できます。これがSTM32プログラミングの最も一般的なアプローチです。

ここでは、例としてSTM32F4シリーズのマイコン(他のシリーズでも基本的な流れは同じです)を想定し、PB0ピン(ADC1のIN8チャネルに対応)に接続したアナログセンサー(ボリュームなど)の値を読み取るプロジェクトを作成する手順を説明します。

  1. STM32CubeIDEまたはSTM32CubeMXを起動:
    • STM32CubeIDEの場合は、「Start New STM32 Project」を選択。
    • STM32CubeMXの場合は、「File」->「New Project」を選択。
  2. MCU/ボードの選択:
    • 使用するSTM32マイコンの型番(例: STM32F401RETx)または開発ボード(例: NUCLEO-F401RE)を選択します。Filter機能を使って絞り込むと探しやすくなります。
    • 「Start Project」をクリックします。
  3. ピン配置&ペリフェラル設定:
    • MCUのピン配置図が表示されます。
    • GPIO設定:
      • 今回はPB0をアナログ入力として使います。PB0ピンをクリックし、「ADC1_IN8」を選択します。(ピンにADC機能が割り当てられている場合、自動的に候補が表示されます)。ピンが緑色になり、ADC入力として設定されたことがわかります。
    • ADCモジュールの設定:
      • 左側の「Categories」ペインから「Analog」を展開し、「ADC1」を選択します。(使用するMCUによってはADC2, ADC3などもあります)。
      • 中央の「Mode and Configuration」ペインでADC1の設定を行います。
        • Parameter Settingsタブ:
          • Mode: Independent Mode を選択します。(複数のADCがある場合に同期モードなどを選択できますが、単一のADCを使う場合は通常これです)
          • Data Resolution: 12ビット、10ビット、8ビット、6ビットから選択します。ここでは12 bitsを選択しましょう。
          • Scan Conversion Mode: 単一チャネルの場合は Disabled、複数チャネルを順次変換する場合は Enabled にします。今回は単一チャネルなので Disabled です。
          • Continuous Conversion Mode: 1回のトリガーで連続して変換を行うか。テスト目的や常に監視したい場合はEnabledにすると便利です。今回はポーリングで1回ずつ変換するので Disabled としておきましょう。
          • DMA Continuous Requests: DMAを使う際に、連続変換中にリクエストを継続するか。DMAを使う場合に設定します。今回は使いません。
          • EOC Selection: 変換完了 (End Of Conversion) フラグの選択。通常はEnd of single conversionを選択しておきます。
          • Data Alignment: 変換結果をレジスタの右端に詰めるか左端に詰めるか。通常は Right alignment (右詰め) を選択します。
          • Discontinuous Conversion Mode: スキャンモード時に、設定されたチャネルグループをいくつかの小さいグループに分割して変換するか。今回は使いません。
          • Conversion Event: 変換をトリガーするイベント。ソフトウェアトリガー、タイマーイベント、外部ピンなどから選択します。ここでは Software trigger を選択します。
          • Low Power AutoWait: 低消費電力機能。必要に応じて設定します。
        • Rank Settingsタブ:
          • Rank: 変換順序とチャネルを設定します。スキャンモードがEnabledの場合は、ここで複数のチャネルをリストに追加し、変換順序を定義します。今回は単一チャネルなので、Rank 1にPB0 (ADC1_IN8) が表示されていることを確認します。
          • Channel: どの入力チャネルを使うか。PB0を選択したので、ADC1_IN8が自動的に設定されています。
          • Sampling Time: そのチャネルのサンプリング時間。入力信号のインピーダンスやADCクロック周波数に応じて適切な値を設定します。高いほど正確ですが、変換時間が長くなります。最初はデフォルト値(例: 3 Cycles)のまま試してみるか、センサーの出力インピーダンスを考慮して長め(例: 48 Cyclesまたは56 Cycles)に設定すると安定しやすいことがあります。ここでは例として56 Cyclesを選択してみましょう。
        • NVIC Settingsタブ (割り込みを使う場合):
          • ADC global interrupt のチェックボックスをオンにすると、変換完了割り込みが有効になります。今回はポーリングを使うのでチェックはオフのままです。
        • DMA Settingsタブ (DMAを使う場合):
          • 「Add」ボタンをクリックし、ADC1のリクエストを選択します。DMAチャンネル、方向、モード(Normal/Circular)などを設定します。今回は使いません。
    • RCC (クロック設定):
      • 左側の「Categories」ペインから「System Core」を展開し、「RCC」を選択します。
      • HSE (High Speed External)LSE (Low Speed External)を使う場合は設定します。今回は内部RC発振器 (HSI) を使う前提とします。
      • 「Clock Configuration」タブに移動します。
      • システムクロック源(HSIまたはHSE)、PLL (Phase-Locked Loop) の設定、各バス(AHB, APB1, APB2)の分周比などを設定します。
      • ADCクロックの設定: 右下のADC clock部分を確認します。ADCクロック源(PCLK2など)と分周比が表示されており、最終的なADCクロック周波数が計算されて表示されます。この周波数がADCモジュールの最大定格を超えていないことを確認します。必要に応じて、プリスケーラ(分周器)を設定して周波数を調整します。例えば、APB2クロックを4分周してADCクロックとする設定などが可能です。
  4. プロジェクト設定:
    • 上部メニューの「Project」->「Generate Code」または「Generate Report」の隣の歯車アイコンをクリックします。
    • Project Name: プロジェクト名を付けます (例: STM32_ADC_Test).
    • Project Location: プロジェクトを保存する場所を指定します。
    • Toolchain / IDE: 使用する開発環境を選択します (STM32CubeIDEを選択)。
    • Firmware Package: 使用するSTM32Cubeファームウェアパッケージのバージョンを確認します。
    • 「Advanced Settings」では、生成されるファイルの名前などを変更できますが、最初はデフォルトで構いません。
    • 設定が終わったら、「OK」をクリックします。
  5. コード生成:
    • 上部メニューの「Project」->「Generate Code」を選択するか、ショートカットアイコンをクリックします。
    • CubeMXが設定に基づいた初期化コード(HALライブラリを使用)を自動生成します。STM32CubeIDEを使用している場合は、生成されたプロジェクトが開かれます。

これで、ADCを使用するための基本的な初期化コードが生成されました。

第5章: 生成コードの解析とユーザーコードの追加 (HAL使用)

CubeMXで生成されたコードは、HALライブラリを使った各ペリフェラルの初期化処理を含んでいます。主要なファイルは main.c です。

生成コードの構造 (main.c)

main.c ファイルを開くと、以下のような構造になっています。

  • インクルードファイル (#include "main.h")
  • ペリフェラルハンドラ変数の定義 (ADC_HandleTypeDef hadc1;, UART_HandleTypeDef huart2; など)
  • 関数のプロトタイプ宣言 (SystemClock_Config(void);, MX_GPIO_Init(void);, MX_ADC1_Init(void); など)
  • main 関数:
    • HAL_Init(): HALライブラリおよび低レベルハードウェアの初期化。
    • SystemClock_Config(): クロック設定(CubeMXのClock Configurationで設定した内容に基づく)。
    • MX_GPIO_Init(): GPIOピンの初期化(アナログ入力、出力、代替機能など)。
    • MX_ADC1_Init(): ADC1モジュールの初期化。
    • MX_USART2_UART_Init(): (デバッグ用にUARTを使う場合など)UARTの初期化。
    • ユーザーコード領域 (User Code Begin x / User Code End x): ユーザーが独自の処理を記述する場所。CubeMXで再生成してもこの間のコードは保持されます。
      • /* USER CODE BEGIN 1 */ から /* USER CODE END 1 */: 変数定義など。
      • /* USER CODE BEGIN 2 */ から /* USER CODE END 2 */: main関数開始後の初期化処理など。
      • /* Infinite loop */ (while (1)) の中: /* USER CODE BEGIN WHILE */ から /* USER CODE END WHILE */ および /* USER CODE BEGIN 3 */ から /* USER CODE END 3 */: メインループ処理。
  • 初期化関数の実装 (SystemClock_Config, MX_GPIO_Init, MX_ADC1_Init, etc.)
  • 割り込みハンドラやコールバック関数の実装 (ユーザーが有効にした場合)

ADC初期化関数の解析 (MX_ADC1_Init)

MX_ADC1_Init 関数は、CubeMXで設定した内容に基づいてADC1モジュールを初期化します。以下はその一部抜粋と解説です。

“`c
/ ADC1 init function /
void MX_ADC1_Init(void)
{
ADC_ChannelConfTypeDef sConfig = {0}; // ADCチャネル設定用の構造体を定義し、0で初期化

/ USER CODE BEGIN ADC1_Init 0 /
/ USER CODE END ADC1_Init 0 /

// ハンドラ構造体の初期化
hadc1.Instance = ADC1; // 使用するADCモジュール (ADC1) を指定
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; // ADCクロック分周器(例: APB2クロックを4分周)
hadc1.Init.Resolution = ADC_RESOLUTION_12B; // 分解能 (12ビット)
hadc1.Init.ScanConvMode = DISABLE; // スキャンモード無効 (単一チャネル)
hadc1.Init.ContinuousConvMode = DISABLE; // 連続変換モード無効
hadc1.Init.DiscontinuousConvMode = DISABLE; // 非連続変換モード無効
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; // 外部トリガーエッジ無効 (ソフトウェアトリガー)
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T1_CC1; // 外部トリガーソース(ソフトウェアトリガーの場合、この値は関係しないことが多いが、設定によっては意味を持つことも)
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; // データアライメント (右詰め)
hadc1.Init.NbrOfConversion = 1; // 変換回数 (スキャンモード無効時は1)
hadc1.Init.DMAContinuousRequests = DISABLE; // DMA連続リクエスト無効
hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV; // 変換完了フラグの選択 (単一変換完了)
hadc1.Init.TwoSamplingDelay = ADC_TWOSAMPLINGDELAY_5CYCLES; // デュアルモードなどの場合の遅延(単一ADCでは関係しないことが多い)

// HALライブラリによるADCモジュールの初期化実行
if (HAL_ADC_Init(&hadc1) != HAL_OK)
{
Error_Handler(); // 初期化失敗時はエラー処理へ
}

/ USER CODE BEGIN ADC1_Init 2 /
/ USER CODE END ADC1_Init 2 /

// チャネル設定用の構造体にパラメータを設定
sConfig.Channel = ADC_CHANNEL_8; // 変換するチャネル番号 (PB0はADC1_IN8)
sConfig.Rank = 1; // スキャンモード無効時は1
sConfig.SamplingTime = ADC_SAMPLETIME_56CYCLES; // サンプリング時間 (56サイクル)

// HALライブラリによるチャネル設定実行
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler(); // 設定失敗時はエラー処理へ
}

/ USER CODE BEGIN ADC1_Init 3 /
/ USER CODE END ADC1_Init 3 /
}
“`

この関数では、まず ADC_HandleTypeDef 型の構造体 hadc1 に、CubeMXで設定したADCモジュールの全体的なパラメータを格納し、HAL_ADC_Init() 関数で初期化を実行します。次に、ADC_ChannelConfTypeDef 型の構造体 sConfig に、変換したい個々のチャネル(ここではADC1_IN8)のパラメータ(チャネル番号、変換順序、サンプリング時間)を格納し、HAL_ADC_ConfigChannel() 関数でそのチャネルの設定を行います。

ユーザーコードの記述(ADC変換と結果取得)

生成されたプロジェクトを開き、main.cwhile(1) ループ内に、ADC変換を行うためのユーザーコードを追加します。

1. ADC変換の開始

ソフトウェアトリガーでADC変換を開始するには、HAL_ADC_Start() 関数を使用します。

“`c
/ USER CODE BEGIN 2 /
// 初期化処理の後
// 必要であれば、ここでUART通信などの初期化を行う
/ USER CODE END 2 /

/ Infinite loop /
/ USER CODE BEGIN WHILE /
while (1)
{
/ USER CODE END WHILE /

/ USER CODE BEGIN 3 /
// 1. ADC変換開始
HAL_StatusTypeDef status = HAL_ADC_Start(&hadc1);
if (status != HAL_OK)
{
// 変換開始失敗時のエラー処理 (例: 点滅LEDなど)
}

// … 変換完了待ちと結果取得は次に続く …
}
/ USER CODE END 3 /
“`

HAL_ADC_Start() 関数は、引数にADCハンドラ構造体へのポインタをとります。戻り値は HAL_StatusTypeDef 型で、HAL_OK であれば成功です。

2. 変換完了待ち (Polling)

ポーリング方式で変換完了を待つには、HAL_ADC_PollForConversion() 関数を使用します。この関数は、ADC変換が完了するまで(または指定したタイムアウト時間が経過するまで)処理をブロックします。

“`c
// 1. ADC変換開始 (前述のコード)
HAL_StatusTypeDef status = HAL_ADC_Start(&hadc1);
if (status != HAL_OK) { / エラー処理 / }

// 2. 変換完了待ち (タイムアウト付き)
// タイムアウト時間 (ミリ秒)。ADC変換時間は短いので、例えば100msで十分でしょう。
status = HAL_ADC_PollForConversion(&hadc1, 100);
if (status != HAL_OK)
{
// タイムアウトまたはエラー発生時の処理
// エラーの種類を確認するには、hadc1.ErrorCodeを参照
// 例: 変換完了しない場合の処理
} else {
// 3. 変換結果の取得 (次に続く)
uint32_t adcValue = HAL_ADC_GetValue(&hadc1);

  // 4. 変換結果の後処理 (次に続く)
  // 例: デジタル値を電圧に変換したり、UARTで送信したりする

}

// 変換停止 (連続変換モードでない場合、通常は不要だが安全のため呼んでも良い)
HAL_ADC_Stop(&hadc1); // ポーリングの場合は省略可

// 次の変換まで待機するなど (必要に応じて)
HAL_Delay(100); // 例えば100msごとに変換する場合
“`

HAL_ADC_PollForConversion() 関数は、第1引数にADCハンドラ構造体へのポインタ、第2引数にタイムアウト時間(ミリ秒)をとります。

3. 変換結果の取得

変換が完了したら、HAL_ADC_GetValue() 関数で結果を取得できます。

“`c
// … 変換完了待ちの後 …
uint32_t adcValue = HAL_ADC_GetValue(&hadc1);

// adcValue には、設定した分解能に応じたデジタル値(0~4095 for 12bit)が格納されている
“`

HAL_ADC_GetValue() 関数は、引数にADCハンドラ構造体へのポインタをとり、取得したデジタル値を uint32_t 型で返します。

4. 変換結果の後処理(電圧計算など)

取得したデジタル値 adcValue は、0から $2^N – 1$ ($N$は分解能のビット数)の範囲の値です。これを実際の電圧値に変換するには、第1章で説明した式を使います。

$V_{in} = \frac{D}{2^N – 1} \times V_{ref}$

ここでは、12ビット分解能 ($N=12$)、基準電圧 $V_{ref}=3.3V$ と仮定します。

“`c
// … 変換結果の取得の後 …
uint32_t adcValue = HAL_ADC_GetValue(&hadc1);

// 変換結果から電圧を計算 (浮動小数点数で)
// ADCの最大値 (12ビットの場合 2^12 – 1 = 4095)
const uint32_t maxADCValue = (1 << hadc1.Init.Resolution) – 1; // または 4095U
// 基準電圧 (Vref+)
const float vrefVoltage = 3.3f; // または、VDDAピンの実際の電圧を測定して使用

float voltage = (float)adcValue / maxADCValue * vrefVoltage;

// voltage変数に、変換されたアナログ入力電圧値(V)が格納される
// 例: UARTで値を送信する、LCDに表示するなどして確認

// 例: デバッグ用にprintfで値を表示する場合 (UART設定が必要)
// printf(“ADC Value: %lu, Voltage: %.2f V\r\n”, adcValue, voltage);

``
*
printf`を使用するには、プロジェクト設定でSWV (Serial Wire Viewer) またはUARTを有効にし、リダイレクト設定を行う必要があります。CubeMXでUSARTを有効にし、HAL_UART_Transmit()を使うのが一般的です。*

5. 変換停止 (連続変換モード以外では必須ではないが推奨)

変換が終わったら、HAL_ADC_Stop() 関数でADCを停止します。連続変換モードでない場合は、次の HAL_ADC_Start() が呼ばれるまで次の変換は行われませんが、リソース解放のためにも明示的に停止することが推奨される場合があります。

“`c
// … 変換結果の後処理の後 …
HAL_ADC_Stop(&hadc1);

// 必要に応じて遅延
HAL_Delay(100); // 100ms待機
“`

第6章: 実践!単一チャネルのソフトウェアトリガー変換

それでは、実際にPB0ピンに接続されたアナログ入力(例:ボリュームや可変抵抗、ポテンショメータ)の値を読み取り、UART経由でPCに送信して確認する簡単なサンプルプロジェクトを作成しましょう。

使用するハードウェア:

  • STM32開発ボード (NUCLEO-F401REなどを想定)
  • アナログ入力信号源 (例: 10kΩ程度のボリュームまたは半固定抵抗)
  • ボリュームの端子: 一方をGND、もう一方を3.3V (またはVDD_3V3) に接続。中央のワイパー端子をSTM32のPB0ピンに接続。
  • USBケーブル (開発ボードとPCを接続し、電源供給、デバッグ、UART通信に使用)
  • PC上のターミナルソフト (Tera Termなど)

プロジェクト作成手順 (CubeMX/STM32CubeIDE):

  1. STM32CubeIDEを起動し、新しいプロジェクトを作成します。MCU SelectorでNUCLEO-F401REを選択します。
  2. Pinout & Configuration:
    • System Core -> SYS: DebugSerial Wireに設定します。
    • Analog -> ADC1:
      • Mode: Independent Mode
      • Data Resolution: 12 bits
      • Scan Conversion Mode: Disabled
      • Continuous Conversion Mode: Disabled
      • DMA Continuous Requests: Disabled
      • EOC Selection: End of single conversion
      • Data Alignment: Right alignment
      • Discontinuous Conversion Mode: Disabled
      • Conversion Event: Software trigger
      • External Trigger Conversion Source: T1_CC1 (Software triggerの場合はどれでも良いが、ここではデフォルトのまま)
      • Rank 1: ChannelIN8に設定します。Sampling Time56 Cyclesとします。
    • Connectivity -> USART2: (NUCLEOボードの仮想COMポート用)
      • Mode: Asynchronous
      • Hardware Flow Control: Disable
      • Baud Rate: 115200 Bits/s
      • Data: 8 Bits
      • Parity: None
      • Stop Bits: 1
      • NVIC Settings: USART2 global interrupt を有効にします。(printfをリダイレクトする場合に使う可能性がありますが、ポーリング送信であれば不要です)
    • PB0ピンが緑色になっていることを確認します。PA2(USART2_TX), PA3(USART2_RX)も緑色になっていることを確認します。
  3. Clock Configuration:
    • HCLKを最大速度(例: 84MHz for F401RE)に設定します。HSI(16MHz)をPLLで逓倍するのが簡単です。
    • ADC1クロックが最大定格(F4シリーズでは通常36MHz)を超えないように、APB2プリスケーラやADCプリスケーラを設定します。例えば、APB2を/1、ADCプリスケーラを/4とすると、APB2クロックが84MHzの場合、ADCクロックは21MHzとなり、定格内です。
  4. Project Manager:
    • プロジェクト名、保存場所、Toolchain/IDE (STM32CubeIDE) を設定します。
    • Generate Codeをクリックします。
  5. ユーザーコードの追加:

    • 生成されたプロジェクトのCore/Src/main.cを開きます。
    • /* USER CODE BEGIN 2 */ ブロックに、UART通信でprintfを使用するためのリダイレクトコードを追加します(オプションですが便利です)。

    c
    /* USER CODE BEGIN 2 */
    // printfをUART2にリダイレクトする設定 (オプション)
    // この機能を使用するには、syscalls.cやstdio.hのインクルードが必要になる場合があります。
    // また、_write関数をUART送信関数で実装する必要があります。
    // 簡単なデバッグ出力ならHAL_UART_Transmit()を直接使う方がシンプルかもしれません。
    // ここではHAL_UART_Transmit()を使う前提でprintfリダイレクトのコードは省略します。
    /* USER CODE END 2 */

    * /* USER CODE BEGIN WHILE */ ブロック内に、ADC変換と結果送信のコードを記述します。

    “`c
    / USER CODE BEGIN WHILE /
    uint32_t adcValue = 0; // ADC変換結果を格納する変数
    float voltage = 0.0f; // 計算した電圧値を格納する変数
    char txBuf[50]; // UART送信バッファ
    / USER CODE END WHILE /

    / Infinite loop /
    while (1)
    {
    / USER CODE BEGIN 3 /
    // ADC変換開始
    HAL_ADC_Start(&hadc1);

    // 変換完了待ち (ポーリング)
    // タイムアウト時間は適宜調整
    if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK)
    {
    // 変換結果取得
    adcValue = HAL_ADC_GetValue(&hadc1);

      // 変換停止 (連続変換モードでない場合は不要だが、明示的に)
      HAL_ADC_Stop(&hadc1);
    
      // デジタル値を電圧に変換 (3.3V基準、12ビット分解能)
      // F401REのVDDAは通常VDDと同じなので3.3Vを使用
      const float vrefVoltage = 3.3f;
      const uint32_t maxADCValue = (1 << hadc1.Init.Resolution) - 1; // または 4095U
    
      voltage = (float)adcValue / maxADCValue * vrefVoltage;
    
      // UARTで結果を送信
      // snprintfで文字列に変換 (stdio.hが必要)
      snprintf(txBuf, sizeof(txBuf), "ADC: %lu, Voltage: %.2f V\r\n", adcValue, voltage);
    
      // HAL_UART_Transmit() で送信 (文字列長+1)
      HAL_UART_Transmit(&huart2, (uint8_t*)txBuf, strlen(txBuf), 100); // タイムアウト100ms
    

    } else {
    // 変換失敗またはタイムアウト時の処理
    // 例えばエラーメッセージを送信
    // HAL_UART_Transmit(&huart2, (uint8_t*)”ADC Conversion Error\r\n”, 22, 100);
    }

    // 少し待ってから次の変換を行う
    HAL_Delay(200); // 200ms待機
    / USER CODE END 3 /
    }
    * `Core/Inc/main.h` に `stdio.h` と `string.h` をインクルードします。c
    / USER CODE BEGIN Includes /

    include // For snprintf

    include // For strlen

    / USER CODE END Includes /
    “`

  6. ビルドと書き込み:

    • プロジェクトを保存し、ビルドします(Project -> Build Project またはハンマーアイコン)。
    • エラーがなければ、開発ボードにプログラムを書き込みます(Run -> Debug またはRunアイコン)。デバッグモードで実行され、ブレークポイントを設定して値を確認することもできます。または、Run構成を作成してそのまま実行することもできます。
  7. 結果の確認:

    • PC上でターミナルソフト(Tera Termなど)を起動します。
    • 開発ボードの仮想COMポート(NUCLEOの場合はSTLink Virtual COM Port)を選択します。
    • 通信設定をボーレート115200 bps、データ長8ビット、パリティなし、ストップビット1ビットに設定します。
    • ターミナルを開くと、設定した間隔(この例では200msごと)でADC値と計算された電圧値が表示されるはずです。ボリュームを回すと、表示される値が変化することを確認してください。

このサンプルコードは、単一チャネルをソフトウェアトリガーでポーリング変換する最も基本的な方法です。ボリュームを回すことで、0から4095(または0Vから3.3V)の範囲で値が変化することが確認できます。

第7章: 発展:複数チャネルスキャン、連続変換、割り込み、DMA

上記の基本的なポーリングによる単一変換に慣れたら、より効率的で高度なADCの使い方に挑戦できます。

複数チャネルのスキャンモード

複数のアナログ入力ピンから同時に、または順次データを取得したい場合は、スキャンモードを有効にします。

  • CubeMXのADC設定で Scan Conversion ModeEnabled にします。
  • Rank Settings タブで、変換したいすべてのチャネル(例えばIN8、IN9、IN10…)をリストに追加し、Rank 番号を設定します(Rank 1, Rank 2, …)。これが変換される順序になります。
  • NbrOfConversion に、スキャンするチャネルの合計数を設定します。

コードでは、HAL_ADC_Start() を一度呼び出すだけで、設定されたすべてのチャネルが Rank の順に変換されます。ポーリングの場合、HAL_ADC_PollForConversion() は最後のチャネルの変換が完了するのを待ちます。変換結果は、通常、最後のチャネルの変換完了時にデータレジスタ (ADC_DR) にまとめて(または順次)格納されますが、スキャンモード+DMAを使用する場合は、メモリ上に変換順序通りに連続して転送されます。

連続変換モード

一度変換を開始したら、トリガーなしで連続して変換を繰り返したい場合は、連続変換モードを有効にします。

  • CubeMXのADC設定で Continuous Conversion ModeEnabled にします。

このモードでは、最初の HAL_ADC_Start() が呼ばれた後、ADCは設定されたチャネル(スキャンモード無効なら単一チャネル、有効なら設定された全チャネル)の変換を停止命令が出るまで繰り返し行います。変換完了ごとに新しい結果がデータレジスタに書き込まれます(古い値は上書きされるため、ポーリングで値を読み取る場合は注意が必要です)。連続変換モードは、割り込みやDMAと組み合わせて使用するのが一般的です。

割り込み (Interrupt)

変換完了を待つ間に他の処理を行いたい場合や、CPUの負荷を減らしたい場合は、変換完了割り込みを使用します。

  • CubeMXのADC設定の NVIC Settings タブで、ADC global interrupt を有効にします。
  • コード生成後、main.c に自動生成される(あるいは手動で追加する)HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) 関数内に、変換完了時に実行したい処理(例:変換結果の取得、次の変換開始)を記述します。
  • 変換開始には HAL_ADC_Start_IT(&hadc1) 関数を使用します。

割り込みを使うことで、変換完了を待つためのポーリングループが不要になり、CPUを他のタスクに解放できます。

DMA (Direct Memory Access)

複数チャネルのスキャンモードで多くのデータを連続して取得したい場合や、非常に高速なサンプリングを行いたい場合は、DMAを使うのが最も効率的です。DMAコントローラがADCとメモリの間で直接データを転送するため、CPUの介在が最小限で済みます。

  • CubeMXのADC設定の DMA Settings タブで、DMAリクエスト(例:ADC1)を追加します。
  • DMAモード(NormalまたはCircular)、データ幅(Word/Half Word/Byte)、インクリメントモード(Peripheral/Memory)などを設定します。通常、MemoryはIncrementモード、PeripheralはNon-incrementモード、データ幅はADC分解能に応じてHalf Word (16-bit) または Word (32-bit) を選択します。Circularモードは、連続的にデータをバッファに書き込みたい場合に便利です。
  • コードでは、HAL_ADC_Start_DMA(&hadc1, pData, Length) 関数を使用します。pData は転送先のメモリバッファのアドレス、Length は転送するデータ数です(スキャンモードの場合は、チャネル数×バッファリングしたい回数)。
  • DMA転送完了時のコールバック関数 HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)(DMAモードの場合も呼ばれます)や、ハーフ転送完了時のコールバック関数 HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) を利用して、バッファに格納されたデータを処理します。

DMAを使用すると、CPUが他のタスクを実行している間にADC変換結果を自動的にメモリに収集できるため、システム全体の効率が大幅に向上します。特にリアルタイム性の高いアプリケーションや、大量のセンサーデータを処理する場合に不可欠な手法です。

第8章: よくある問題と対策

ADCプログラミングにおいて、初心者がよく遭遇する問題とその対策をいくつか紹介します。

  • 取得したADC値が安定しない、ノイズが多い:
    • 対策:
      • サンプリング時間の確認: 入力ソースのインピーダンスに対してサンプリング時間が短すぎる可能性があります。CubeMXでより長いサンプリング時間(例: 48, 56, 112サイクルなど)を設定してみてください。
      • 電源ノイズ: ADCの基準電圧(VDDA, VREF+)やアナログ電源(VDDA)が不安定だと、変換結果にノイズが乗ります。開発ボードの電源品質を確認したり、外部基準電圧源を使用したり、電源ラインに適切なデカップリングコンデンサを入れるなどのハードウェア的な対策が必要です。
      • 入力信号ノイズ: アナログ入力ピンに入る信号自体にノイズが乗っている可能性があります。入力信号ラインを短くする、シールド線を使う、RCフィルタ(ローパスフィルタ)を挿入するなど、入力信号側でのノイズ対策を検討してください。
      • 多重化によるチャネル間の干渉: スキャンモードで複数のチャネルを使用する場合、チャネル切り替え時の残留電荷などにより、前のチャネルの値が次のチャネルの変換に影響を与えることがあります。これもサンプリング時間を長くすることで改善される場合があります。
  • GPIO設定ミス:
    • 対策: ADCを使用するピンが、CubeMXまたは手動設定で正しく「アナログ入力」モードになっていることを確認してください。デジタル入力/出力モードになっていると、アナログ信号を正しく読み取れません。
  • クロック設定ミス:
    • 対策: ADCモジュールに供給されるクロック周波数が、マイコンのデータシートに記載されている最大定格を超えていないことを確認してください。また、クロックが低すぎる場合、必要なサンプリングレートが得られない可能性があります。CubeMXのClock Configurationタブで設定を確認してください。
  • 基準電圧源の選択:
    • 対策: どの基準電圧源(VDDA, VREF+, 内部基準電圧Vrefint)を使用するか、およびその電圧値が正確であるかが、変換結果の精度に直結します。CubeMXの設定や、手動でVDDA/VREF+ピンに安定した電源を供給しているかを確認してください。内部基準電圧Vrefintは、VDDA変動の影響を受けにくく、相対的な測定に適していますが、絶対精度はマイコン個体差があります。必要に応じて、Vrefintの値を一度ADCで測定し、実際のVDDA値を計算して使用する手法も有効です。
  • 変換結果が期待する範囲にならない:
    • 対策:
      • 入力レンジの確認: アナログ入力信号の電圧が、ADCの入力レンジ(通常0V~Vref+)内に収まっているか確認してください。レンジ外の信号は正しく変換されません。必要に応じて信号を分圧するなどしてください。
      • データアライメント: 右詰め(Right alignment)と左詰め(Left alignment)の設定を確認してください。特に16ビット変数で結果を受け取る場合、どちらのアライメントかによって結果の解釈が変わります。通常は右詰めを使用します。
      • 計算式の確認: デジタル値から物理量(電圧など)への変換に使用している計算式が正しいか、基準電圧、分解能、最大デジタル値が正しく反映されているかを確認してください。

まとめ:ADCプログラミングの第一歩を踏み出す

この記事では、STM32マイコンにおけるADCプログラミングの「第一歩」として、ADCの基本的な概念から、STM32のADCモジュールの特徴、CubeMXを使った設定方法、HALライブラリを使ったポーリングによる単一チャネル変換の具体的なコード例までを詳細に解説しました。

アナログ信号をデジタル信号に変換するADCは、組み込みシステムで外部環境の情報を取得するための重要な機能です。STM32の高性能なADCを使いこなせるようになれば、様々なセンサーを接続して、より高度なアプリケーションを開発できるようになります。

今回紹介した内容は、ADCの最も基本的な使い方です。ここからさらに、複数チャネルのスキャンモード、連続変換モード、割り込み駆動、そしてDMA転送といった、より効率的で柔軟な使い方へとステップアップしていくことができます。

ADCプログラミングは、最初は少し難しく感じるかもしれませんが、この記事で説明した手順に従ってCubeMXで設定を行い、提供されたHALライブラリ関数を使ってコードを書いていくことで、比較的容易に実現できます。実際に手を動かし、コードを書いて、マイコン上で動作させてみることが、理解を深める最も良い方法です。

この記事が、あなたのSTM32 ADCプログラミングの第一歩を力強く後押しするものとなれば幸いです。次のステップとして、興味のあるセンサー(温度センサー、光センサーなど)をSTM32のADCに接続し、その値を読み取って、LEDの制御やLCDへの表示など、何か具体的なアプリケーションを作ってみることをお勧めします。そこから、ADCのさらなる可能性が広がっていくはずです。頑張ってください!

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール