はい、承知いたしました。STM32のHAL/LLライブラリ別にDACの制御方法をまとめた、約5000語の詳細な記事を作成します。
【完全版】STM32 DACマスターガイド:HAL vs LLライブラリ別 制御方法の徹底解説
1. はじめに
STマイクロエレクトロニクスが提供するSTM32シリーズは、その豊富なラインナップ、高性能、優れた電力効率、そして強力な開発エコシステムにより、ホビーから産業用途まで、世界中の組み込み開発者から絶大な支持を得ています。この強力なマイクロコントローラ(マイコン)が内蔵する数多くのペリフェラルの中でも、DAC(Digital-to-Analog Converter)は、デジタル世界とアナログ世界を繋ぐ重要な架け橋です。
DACとは何か?
DACは、その名の通り、マイコン内部で処理される「0」と「1」のデジタル値を、連続的なアナログ電圧信号に変換する機能ブロックです。これにより、マイコンはスピーカーを鳴らして音を出したり、モーターの速度を滑らかに制御したり、センサーに与える基準電圧を精密に調整したりと、物理世界に対してより繊細な働きかけが可能になります。
STM32のDACは、単純な電圧出力に留まらず、DMA(Direct Memory Access)と連携した高速な波形生成や、内蔵のノイズ・三角波ジェネレータなど、多彩な機能を備えています。これらの機能を使いこなすことで、オーディオプレーヤー、ファンクションジェネレータ、高精度な制御システムなど、高度なアプリケーションを少ない外付け部品で実現できます。
HALライブラリとLLライブラリ:2つのアプローチ
STM32の開発において、私たちがペリフェラルを制御するために利用するのが、ST社が提供する公式のソフトウェアライブラリです。これには大きく分けて2つの種類があります。
-
HAL (Hardware Abstraction Layer) ライブラリ:
- ハードウェアの詳細を抽象化し、高い移植性を持つAPI(Application Programming Interface)を提供します。
HAL_DAC_Start()
のように、機能が直感的に分かる関数名になっており、初心者でも比較的容易にペリフェラルを扱うことができます。- 複雑な設定(例えばDMAとの連携)も、数行のコードで実現できるように設計されています。
- 一方で、抽象化のレイヤーを挟むため、生成されるコードサイズが大きくなったり、実行速度がわずかに低下したりする可能性があります。
-
LL (Low-Layer) ライブラリ:
- ハードウェアのレジスタ操作に非常に近い、低レベルなAPIを提供します。
LL_DAC_Enable()
のように、レジスタのビット操作を直接行うような感覚でコーディングします。- コードサイズと実行速度の点で最高のパフォーマンスを引き出すことができ、ハードウェアの機能を最大限に活用したい上級者や、リソースが非常に限られたマイコンでの開発に向いています。
- その代償として、マイコンのデータシートやリファレンスマニュアルを深く理解する必要があり、学習コストは高くなります。
この記事の目的
この記事では、STM32のDACをテーマに、HALライブラリとLLライブラリの両方を使った制御方法を、具体的なコード例を交えながら徹底的に解説します。
- DACの基本機能の紹介
- 開発環境(STM32CubeIDE)のセットアップ
- HALライブラリによる基本的な電圧出力からDMAを使った波形生成まで
- LLライブラリによる同様の機能の実装とHALとの比較
- 両ライブラリの長所・短所を整理し、どのような場合にどちらを選ぶべきかの指針
- オーディオ出力やファンクションジェネレータといった実践的な応用例
この記事を最後まで読めば、あなたはプロジェクトの要件に応じてHALとLLを自在に使い分け、STM32のDAC機能を最大限に引き出すスキルを身につけることができるでしょう。
2. STM32 DACの基本機能
制御方法を学ぶ前に、まずSTM32が搭載するDACがどのような能力を持っているのかを理解しましょう。ここでは主要な機能を紹介します。なお、機能はSTM32のシリーズや型番によって異なる場合があるため、詳細は対象デバイスのリファレンスマニュアルをご確認ください。
-
分解能 (Resolution)
DACがどれだけ細かく電圧を表現できるかを示す指標です。STM32のDACは主に12ビットまたは8ビットの分解能を持ちます。- 12ビット: 2の12乗、つまり4096段階で電圧を表現できます。基準電圧(Vref)が3.3Vの場合、約0.8mV (3.3V / 4096) 刻みで電圧を調整でき、高精度な制御が可能です。
- 8ビット: 2の8乗、つまり256段階で電圧を表現できます。12ビットに比べて精度は劣りますが、データ量が少なく済むため、より高速な変換が求められる場合などに有利です。
-
出力バッファ (Output Buffer)
DACの内部回路は、大きな電流を供給する能力(駆動能力)が低い場合があります。出力バッファを有効にすると、オペアンプを介して出力されるため、外部の負荷(例えばLEDや小規模なスピーカー)を直接駆動する能力が向上します。バッファを無効にすると、より高速な応答が得られますが、駆動能力は低下するため、後段にインピーダンスの高い回路を接続する必要があります。 -
データアライメント (Data Alignment)
DACに設定するデジタル値を、32ビットのデータホールディングレジスタ(DHR)内のどこに配置するかを指定します。- 12ビット右寄せ (Right Alignment): 12ビットの値をレジスタの下位12ビットに格納します。
0x00000FFF
のような値になります。直感的で扱いやすいです。 - 12ビット左寄せ (Left Alignment): 12ビットの値をレジスタの上位ビットに詰めて格納します。
0x0FFF0000
のような形になります。8ビットモードとの互換性や、特定の演算処理で有利な場合があります。 - 8ビット右寄せ (Right Alignment): 8ビットの値をレジスタの下位8ビットに格納します。
- 12ビット右寄せ (Right Alignment): 12ビットの値をレジスタの下位12ビットに格納します。
-
トリガーソース (Trigger Source)
DACがデジタル値をアナログ電圧に変換するタイミング(きっかけ)を指定します。- ソフトウェアトリガー: プログラムから明示的に命令を送ることで変換を開始します。最もシンプルで基本的な方法です。
- タイマートリガー: TIM2, TIM6, TIM7などの内蔵タイマーのイベント(更新イベントなど)をトリガーにできます。これにより、正確な周期で自動的に電圧を更新でき、波形生成に不可欠です。
- 外部ピントリガー: 特定のGPIOピンへの入力信号(立ち上がり/立ち下がりエッジ)をトリガーにできます。外部イベントに同期した電圧出力が可能です。
-
DMA連携 (Direct Memory Access)
DMAは、CPUを介さずにメモリとペリフェラル間で直接データを転送する機能です。DACでDMAを使うと、メモリ上に用意した波形データ(サイン波のテーブルなど)を、CPUに負荷をかけることなく、タイマートリガーに同期して次々とDACに送り込むことができます。これにより、滑らかで高周波な波形生成が実現できます。 -
波形生成機能 (Waveform Generation)
一部のSTM32シリーズでは、DAC自体が簡単な波形を生成する機能を持っています。- ノイズ波形: 線形帰還シフトレジスタ(LFSR)を用いて擬似ランダムノイズを生成します。
- 三角波形: 設定した振幅で自動的に増減する三角波を生成します。
これらの機能を使えば、DMAを使わずに簡単なテスト信号などを手軽に出力できます。
-
デュアルDACモード (Dual DAC Mode)
2つのDACチャネルを持つSTM32デバイスでは、両方のチャネルを同期させて動作させることができます。これにより、ステレオオーディオ出力や、IQ信号の生成など、より高度なアプリケーションに対応できます。
これらの機能を組み合わせることで、STM32のDACは非常に柔軟で強力なツールとなります。
3. 開発環境の準備
この記事では、ST公式の統合開発環境であるSTM32CubeIDEを使用します。これには、GUIでペリフェラルの初期設定ができるSTM32CubeMXが統合されており、非常に便利です。
- ハードウェア: NUCLEO-F446RE ボードを使用することを想定します。他のSTM32ボードでも、DAC機能があれば同様の手順で進められます。
- ソフトウェア: 最新版のSTM32CubeIDEをインストールしておきます。
プロジェクト作成手順の概要:
- STM32CubeIDEを起動し、「File」>「New」>「STM32 Project」を選択します。
- 「Target Selector」ウィンドウで、使用するボード名(例:
NUCLEO-F446RE
)を入力して選択し、「Next」をクリックします。 - プロジェクト名(例:
DAC_HAL_LL_Tutorial
)を入力します。 - 「Targeted Language」は「C」、「Targeted Binary Type」は「Executable」を選択し、「Finish」をクリックします。
- 「Initialize all peripherals with their default Mode?」と聞かれたら「Yes」と答えます。
- STM32CubeMXの画面(.iocファイル)が開きます。
これでプロジェクトの雛形が完成しました。次に、この画面でDACの設定を行っていきます。
4. HALライブラリによるDAC制御
まずは、直感的で扱いやすいHALライブラリを使ってDACを制御する方法を学びます。
4.1. CubeMXでの設定
-
ピン配置とペリフェラル有効化:
- 左側のペイン(Pinout & Configurationタブ)で、「Analog」カテゴリを展開し、「DAC」をクリックします。
- 「DAC Configuration」パネルが表示されます。「OUT1 Mode」のチェックボックスをオンにします。これにより、DACのチャネル1が有効になります。
- 右側のマイコンのピン配置図を見ると、
PA4
ピンがDAC_OUT1
として緑色にハイライトされます。これがDACの出力ピンです。
-
パラメータ設定:
- 「Configuration」パネルで、詳細な設定を行います。
Output Buffer
:Enabled
(有効)にしておくと、出力が安定しやすくなります。Trigger
: まずはSoftware Trigger
を選択します。- その他の設定はデフォルトのままで問題ありません。
-
コード生成:
- 設定が完了したら、
Ctrl + S
で保存します。コード生成を促すダイアログが表示されたら「Yes」をクリックします。 - これにより、
main.c
などに必要な初期化コードが自動的に生成されます。
- 設定が完了したら、
4.2. 基本の電圧出力(ソフトウェアトリガー)
最も基本的な、任意の電圧を出力するプログラムを作成します。main.c
ファイルを開き、/* USER CODE BEGIN 3 */
と書かれた while(1)
ループ内にコードを記述します。
目的: 0Vから約3.3Vまで、電圧を滑らかに上昇させ、また0Vに戻るのを繰り返す。
“`c
/ main.c /
/ USER CODE BEGIN 2 /
// DACを起動
HAL_DAC_Start(&hdac, DAC_CHANNEL_1);
/ USER CODE END 2 /
/ Infinite loop /
/ USER CODE BEGIN WHILE /
while (1)
{
/ USER CODE END WHILE /
/ USER CODE BEGIN 3 /
// 電圧を0から4095まで上昇させるループ
for (uint32_t i = 0; i < 4096; i++)
{
// DACチャネル1に12ビット右寄せで値を設定
HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, i);
HAL_Delay(1); // 1ms待機
}
// 電圧を4095から0まで下降させるループ
for (uint32_t i = 4095; i > 0; i–)
{
HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, i);
HAL_Delay(1); // 1ms待機
}
}
/ USER CODE END 3 /
“`
コード解説:
-
HAL_DAC_Start(&hdac, DAC_CHANNEL_1);
while(1)
ループの前に一度だけ呼び出し、DACのチャネル1を有効化します。この関数を呼び出すことで、DACは変換の準備が整った状態になります。
-
HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, i);
- これが実際に電圧を設定する関数です。
&hdac
: CubeMXが生成したDACのハンドル構造体へのポインタ。DAC_CHANNEL_1
: 操作対象のチャネルを指定。DAC_ALIGN_12B_R
: データを12ビット右寄せ形式で設定することを示します。i
: 設定するデジタル値(0~4095)。
このコードを書き込んでビルドし、マイコンに書き込みます。PA4
ピンの電圧をオシロスコープやテスターで観測すると、約8秒周期(4096ms * 2)で電圧が0VからVREF(約3.3V)まで滑らかに変化する様子が確認できます。
電圧とデジタル値の関係:
出力電圧 Vout
は、基準電圧 VREF+
と設定したデジタル値 DAC_value
を用いて、以下の式で計算されます。
Vout = VREF+ * (DAC_value / 4095)
4.3. DMAを使った波形生成(タイマー連携)
次に、CPUに負荷をかけずに正確な周期で波形(ここではサイン波)を生成する方法を学びます。DMAとタイマーの連携は、DACを使いこなす上で最も重要なテクニックの一つです。
ステップ1: CubeMXでの設定変更
-
タイマーの設定:
- 「Pinout & Configuration」タブで「Timers」カテゴリを展開し、「TIM6」を選択します。
- 「Activated」のチェックボックスをオンにします。
- 「Configuration」パネルの「Parameter Settings」タブで、「Trigger Event Selection (TRGO)」を
Update Event
に設定します。これにより、タイマーがカウントアップしてオーバーフローするたびに、トリガー信号が発生します。
-
DACの設定:
- 「Analog」>「DAC」を選択します。
- 「Configuration」パネルの
Trigger
をTimer 6 Trigger Out event
に変更します。 - 次に「DMA Settings」タブに切り替えます。
- 「Add」ボタンをクリックして
DAC1
を選択します。 - DMAの設定が表示されます。
Mode
をCircular
に設定します。これにより、DMAはバッファの終端に達すると自動的に先頭に戻り、波形を繰り返し再生します。 Data Width
のMemory
側はHalf Word
(16ビット) を選択します(12ビットデータを格納するため)。
-
コード生成:
Ctrl + S
で保存し、コードを再生成します。
ステップ2: プログラムの作成
main.c
にサイン波のデータテーブルを作成し、DMAを開始するコードを記述します。
“`c
/ main.c /
include “math.h” // sin関数を使うために追加
/ USER CODE BEGIN PFP /
// プライベートなプロトタイプ宣言
define SINE_WAVE_SAMPLES 128 // サイン波1周期のサンプル数
define PI 3.14159265
uint16_t sine_wave_data[SINE_WAVE_SAMPLES]; // 波形データを格納する配列
/ USER CODE END PFP /
/ USER CODE BEGIN 2 /
// サイン波テーブルの生成
for(int i = 0; i < SINE_WAVE_SAMPLES; i++)
{
// 0-4095の範囲のサイン波を生成
// (sin(x) + 1) で -1~1 -> 0~2 の範囲に
// * 2047.5 で 0~4095 の範囲に変換
sine_wave_data[i] = (uint16_t)((sin(i * 2 * PI / SINE_WAVE_SAMPLES) + 1.0) * 2047.5);
}
// TIM6を起動
HAL_TIM_Base_Start(&htim6);
// DACチャネル1をDMAモードで起動
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)sine_wave_data, SINE_WAVE_SAMPLES, DAC_ALIGN_12B_R);
/ USER CODE END 2 /
/ Infinite loop /
/ USER CODE BEGIN WHILE /
while (1)
{
/ USER CODE END WHILE /
// メインループは空でOK。DMAがバックグラウンドで全てやってくれる。
/ USER CODE BEGIN 3 /
}
/ USER CODE END 3 /
“`
コード解説:
-
sine_wave_data
配列:- サイン波1周期分のデジタル値を格納するバッファです。
SINE_WAVE_SAMPLES
でサンプル数を定義しています。 for
ループ内で、sin()
関数を使って計算し、0~4095の範囲にスケーリングした値を格納しています。
- サイン波1周期分のデジタル値を格納するバッファです。
-
HAL_TIM_Base_Start(&htim6);
- トリガーソースであるタイマー6を開始させます。これにより、設定された周期でTRGO信号がDACに向けて送出され始めます。
-
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)sine_wave_data, SINE_WAVE_SAMPLES, DAC_ALIGN_12B_R);
- DACをDMAモードで起動する、非常に強力な関数です。
- 引数には、DACハンドル、チャネル、波形データ配列のポインタ、データ数、データアライメントを渡します。
- この関数を呼び出すと、TIM6からのトリガーを受け取るたびに、DMAが
sine_wave_data
配列から値を1つ取り出してDACのデータレジスタに転送する、という動作が自動的に開始されます。 - DMAのモードを
Circular
に設定したため、128個のデータを送り終えると、自動的に配列の先頭に戻って転送を繰り返します。
このコードを実行すると、PA4
ピンからはCPUに全く負荷をかけることなく、綺麗なサイン波が出力され続けます。while(1)
ループ内が空であることに注目してください。CPUは他の処理(例えば、UIの操作や通信処理など)を自由に行うことができます。
出力周波数の計算:
出力されるサイン波の周波数 F_out
は、タイマーの更新周波数 F_tim
とサンプル数 N_samples
によって決まります。
F_out = F_tim / N_samples
タイマーの更新周波数は、CubeMXのClock Configurationで設定されたタイマーの入力クロック PCLK
と、タイマーのプリスケーラ PSC
、オートリロードレジスタ ARR
の値から計算されます。
F_tim = PCLK / ((PSC + 1) * (ARR + 1))
4.4. HALライブラリの利点と注意点
-
利点:
- 高い生産性:
HAL_DAC_Start_DMA
のように、複雑な設定を一行で実行できるため、開発時間を大幅に短縮できます。 - 直感性: 関数名が機能を表しており、コードが読みやすく、メンテナンスが容易です。
- 移植性: STM32の異なるシリーズ間でコードを再利用しやすいように設計されています。
- 高い生産性:
-
注意点:
- オーバーヘッド: 抽象化レイヤーを介するため、LLライブラリに比べてコードサイズが大きくなり、実行速度もわずかに遅くなる可能性があります。
- ブラックボックス化: 関数の内部で何が行われているかが隠蔽されているため、細かなタイミング制御や予期せぬ問題が発生した際のデバッグが難しくなることがあります。
5. LLライブラリによるDAC制御
次に、同じ機能をLL(Low-Layer)ライブラリで実装してみましょう。ハードウェアをより直接的に制御する感覚を掴むことができます。
5.1. LLライブラリの導入
まず、プロジェクト全体で使用するライブラリをHALからLLに変更します。
- STM32CubeIDEで
.ioc
ファイルを開きます。 - 「Project Manager」タブに移動します。
- 「Advanced Settings」セクションで、「Driver Selector」という項目があります。
- ここで、
DAC
やDMA
、TIM
などのペリフェラルについて、「HAL」から「LL」にドロップダウンリストを変更します。 Ctrl + S
で保存し、コードを再生成します。「Do you want to generate Code?」に「Yes」と答えます。これにより、HALの初期化コードの代わりにLLの初期化コードが生成され、LL関数のヘッダファイルがインクルードされるようになります。
5.2. 基本の電圧出力(ソフトウェアトリガー)
HALで実装した、電圧を滑らかに上下させるプログラムをLLで書き換えます。main.c
の while(1)
ループを以下のように変更します。
“`c
/ main.c /
/ USER CODE BEGIN 2 /
// DACチャネル1を有効化
LL_DAC_Enable(&hdac, LL_DAC_CHANNEL_1);
/ USER CODE END 2 /
/ Infinite loop /
/ USER CODE BEGIN WHILE /
while (1)
{
/ USER CODE END WHILE /
/ USER CODE BEGIN 3 /
// 電圧を0から4095まで上昇させるループ
for (uint32_t i = 0; i < 4096; i++)
{
// DACチャネル1の12ビット右寄せデータホールディングレジスタに値を書き込む
LL_DAC_ConvertData12RightAligned(DAC1, LL_DAC_CHANNEL_1, i);
// ソフトウェアトリガーを発生させる
LL_DAC_TrigSWConversion(DAC1, LL_DAC_CHANNEL_1);
HAL_Delay(1); // 待機関数はHALのものを流用可能
}
// 電圧を4095から0まで下降させるループ
for (uint32_t i = 4095; i > 0; i–)
{
LL_DAC_ConvertData12RightAligned(DAC1, LL_DAC_CHANNEL_1, i);
LL_DAC_TrigSWConversion(DAC1, LL_DAC_CHANNEL_1);
HAL_Delay(1);
}
}
/ USER CODE END 3 /
“`
コード解説:
HALの HAL_DAC_SetValue()
一つが、LLでは2つの関数に分かれていることに注目してください。
-
LL_DAC_Enable(&hdac, LL_DAC_CHANNEL_1);
- これはHALの
HAL_DAC_Start()
に相当し、DACチャネルを有効化します。リファレンスマニュアルのDAC_CRレジスタのEN1ビットをセットする操作に対応します。
- これはHALの
-
LL_DAC_ConvertData12RightAligned(DAC1, LL_DAC_CHANNEL_1, i);
- 指定したチャネルのデータホールディングレジスタ(DHR12R1)に値を書き込みます。この時点ではまだ電圧は出力されません。
-
LL_DAC_TrigSWConversion(DAC1, LL_DAC_CHANNEL_1);
- ソフトウェアトリガレジスタ(DAC_SWTRIGR)のビットをセットし、変換を実際に開始させます。DHRに書き込まれた値がDACの変換回路に送られ、アナログ電圧として出力されます。
このように、LLライブラリは「値をレジスタにセットする」操作と、「トリガーをかける」操作が明確に分離されています。これはハードウェアの実際の動作シーケンスを忠実に反映しています。
5.3. DMAを使った波形生成(タイマー連携)
HALの時と同様に、DMAとタイマー連携によるサイン波生成をLLで実装します。CubeMXでのペリフェラル有効化やピン設定はHALの時と同じですが、生成される初期化コードがLLベースになります。
LLライブラリでは、HAL_DAC_Start_DMA
のような便利な包括的関数は存在しません。DMAの転送元アドレス、転送先アドレス、データ数などを、一つ一つLL関数を使って設定していく必要があります。
“`c
/ main.c /
include “math.h”
/ USER CODE BEGIN PFP /
define SINE_WAVE_SAMPLES 128
define PI 3.14159265
uint16_t sine_wave_data[SINE_WAVE_SAMPLES];
/ USER CODE END PFP /
/ USER CODE BEGIN 2 /
// サイン波テーブルの生成 (HALの例と同じ)
for(int i = 0; i < SINE_WAVE_SAMPLES; i++)
{
sine_wave_data[i] = (uint16_t)((sin(i * 2 * PI / SINE_WAVE_SAMPLES) + 1.0) * 2047.5);
}
// ==== LLによるDMA設定 ====
// DMAストリームを無効化 (設定変更のため)
LL_DMA_DisableStream(DMA1, LL_DMA_STREAM_5);
// 転送データ数を設定
LL_DMA_SetDataLength(DMA1, LL_DMA_STREAM_5, SINE_WAVE_SAMPLES);
// メモリアドレス(転送元)を設定
LL_DMA_SetMemoryAddress(DMA1, LL_DMA_STREAM_5, (uint32_t)sine_wave_data);
// ペリフェラルアドレス(転送先)を設定
// DAC1のチャネル1、12ビット右寄せデータホールディングレジスタのアドレスを取得
uint32_t dac_dhr12r1_address = LL_DAC_GetDHRAddress(DAC1, LL_DAC_CHANNEL_1, LL_DAC_ALIGN_12B_R);
LL_DMA_SetPeriphAddress(DMA1, LL_DMA_STREAM_5, dac_dhr12r1_address);
// ==== LLによるDACとタイマーの起動 ====
// DMAストリームを有効化
LL_DMA_EnableStream(DMA1, LL_DMA_STREAM_5);
// DACからのDMAリクエストを有効化
LL_DAC_EnableDMAReq(DAC1, LL_DAC_CHANNEL_1);
// DACチャネル1を有効化
LL_DAC_Enable(DAC1, LL_DAC_CHANNEL_1);
// DACチャネル1のトリガーを有効化 (TIM6からのトリガーを待つため)
LL_DAC_EnableTrigger(DAC1, LL_DAC_CHANNEL_1);
// TIM6を有効化
LL_TIM_EnableCounter(TIM6);
/ USER CODE END 2 /
/ Infinite loop /
/ USER CODE BEGIN WHILE /
while (1)
{
/ USER CODE END WHILE /
// こちらもメインループは空でOK
/ USER CODE BEGIN 3 /
}
/ USER CODE END 3 /
“`
コード解説:
HALに比べて手順が格段に増えているのが分かります。
-
LLによるDMA設定:
LL_DMA_DisableStream
: 設定を変更する前には、まず対象のDMAストリームを一旦無効にするのが安全です。LL_DMA_SetDataLength
: 転送するデータの総数を設定します。LL_DMA_SetMemoryAddress
: 転送元となるメモリ(sine_wave_data
配列)のアドレスを設定します。LL_DMA_SetPeriphAddress
: 転送先となるペリフェラルのレジスタアドレスを設定します。LL_DAC_GetDHRAddress
というヘルパー関数を使うと、正しいレジスタアドレスを簡単に取得できます。
-
LLによるDACとタイマーの起動:
LL_DMA_EnableStream
: DMAの転送を開始できる状態にします。LL_DAC_EnableDMAReq
: DAC側で、トリガー発生時にDMAへ転送リクエストを送る機能を有効にします。LL_DAC_Enable
: DACチャネル自体を有効化します。LL_DAC_EnableTrigger
: DACチャネルが外部(今回はTIM6)からのトリガーを受け付けるように設定します。LL_TIM_EnableCounter
: タイマーのカウントを開始させます。
HALでは HAL_DAC_Start_DMA
の一行で済んでいた処理が、LLではこれだけのステップに分解されます。しかし、これは裏を返せば、DMAの各パラメータ(アドレス、データ長など)をプログラム実行中に動的に、かつ個別に変更できる柔軟性を持っていることを意味します。例えば、再生する波形を途中で切り替えたい場合、DMAストリームを一旦無効にし、メモリアドレスとデータ長を再設定して再度有効にする、といった細やかな制御が可能です。
5.4. LLライブラリの利点と注意点
-
利点:
- パフォーマンス: 生成されるコードがインライン関数化されることが多く、関数呼び出しのオーバーヘッドがほぼないため、非常に高速に動作します。コードサイズも最小限に抑えられます。
- 透明性と制御性: 全ての操作がレジスタ操作に1対1で対応しているため、ハードウェアが何をしているかが明確です。これにより、厳密なタイミング制御や高度な最適化が可能になります。
- 学習効果: LLライブラリを使う過程で、リファレンスマニュアルを読むことが必須となるため、STM32のアーキテクチャに対する深い理解が得られます。
-
注意点:
- 学習コスト: リファレンスマニュアルとデータシートの知識が不可欠であり、初心者には敷居が高いです。
- コードの冗長性: HALに比べて記述量が多くなり、コードが複雑になりがちです。
- 移植性の低下: レジスタレベルでの操作が多いため、異なるSTM32シリーズへの移植には、より多くの修正が必要になる場合があります。
6. HAL vs LL: どちらを選ぶべきか?
ここまで見てきたように、HALとLLにはそれぞれ明確な長所と短所があります。どちらのライブラリを選択するかは、プロジェクトの性質、開発者のスキルレベル、そして何を最も重視するかによって決まります。
比較項目 | HAL (Hardware Abstraction Layer) | LL (Low-Layer) |
---|---|---|
抽象度 | 高い (ハードウェアを隠蔽) | 低い (レジスタ操作に近い) |
学習コスト | 低い | 高い (マニュアル必須) |
開発速度 | 速い (生産性が高い) | 遅い (記述量が多い) |
コードサイズ | 大きい | 小さい |
実行速度 | 良好 | 最速 |
移植性 | 高い | 低い |
柔軟性・制御性 | 標準的 | 非常に高い |
主なターゲット | アプリケーション開発者、初心者、プロトタイピング | ファームウェア/ドライバ開発者、上級者、性能/サイズ最適化 |
ユースケース別のおすすめ:
-
こんな時はHALがおすすめ:
- プロトタイピング: とにかく早くアイデアを形にして動かしてみたい。
- 初心者: まずはSTM32のペリフェラルの動かし方を学びたい。
- アプリケーション重視: ペリフェラルの細かな動作より、アプリケーション全体のロジック構築に集中したい。
- 移植の可能性がある: 開発の途中で、より高性能な、あるいは低コストなSTM32シリーズへの変更が考えられる。
-
こんな時はLLがおすすめ:
- リソース制約が厳しい: メモリやFlash容量が非常に限られたマイコンを使用している。
- 最高のパフォーマンスが必要: 高速な信号処理や、μsオーダーの厳密なタイミング制御が求められる。
- バッテリー駆動機器: 少しでも消費電力を抑えるため、不要な処理を徹底的に排除したい。
- 再利用可能なドライバ開発: 特定の機能に特化した、高効率なドライバをライブラリとして作成したい。
ハイブリッドアプローチという選択肢:
CubeMXではペリフェラルごとにHALかLLかを選択できるため、両者を混在させる「ハイブリッドアプローチ」も非常に有効です。
例えば、「USBやファイルシステムなど複雑な部分はHALに任せ、パフォーマンスがクリティカルなDACとDMAの部分だけをLLで記述する」といった使い方が可能です。これにより、開発効率とパフォーマンスの最適なバランスを取ることができます。
7. 実践的な応用例とTips
7.1. オーディオ出力
DMAを使った波形生成は、オーディオ再生に直接応用できます。SDカードなどからWAVファイルのデータを読み込み、そのデータをDMAバッファに格納して再生します。
- サンプリングレートの調整: WAVファイルのサンプリングレート(例: 44.1kHz)に合わせて、タイマーの更新周波数を正確に設定する必要があります。
F_tim = サンプリングレート
となるように、PCLK
,PSC
,ARR
の値を計算して設定します。 - ダブルバッファリング: DMAには、転送完了割り込みや転送半完了割り込みを発生させる機能があります。これを利用し、DMAがバッファの前半を再生している間に、CPUが後半のバッファに次のデータを読み込む(またはその逆)という「ダブルバッファリング」を実装すると、音途切れのない連続再生が可能です。
- ローパスフィルタ: DACの出力は階段状の波形(サンプリング・ホールド出力)になるため、高周波成分(ノイズ)を含んでいます。出力ピンとGNDの間に簡単なRCローパスフィルタ(抵抗とコンデンサを1つずつ)を入れるだけで、波形が滑らかになり、音質が大幅に改善します。
7.2. ファンクションジェネレータ
サイン波だけでなく、矩形波、三角波、のこぎり波などのテーブルをメモリ上に複数用意し、ユーザーの選択に応じてDMAの転送元アドレスを切り替えることで、簡単なファンクションジェネレータを実装できます。
- 周波数・振幅の変更:
- 周波数: タイマーのARRレジスタの値を変更することで、再生速度、つまり周波数を動的に変更できます。
- 振幅: DMAで転送する波形データ自体を、CPUで計算し直すことで振幅を変更できます。例えば、
new_value = original_value * (amplitude / max_amplitude)
のような計算を行います。
7.3. デバッグのヒント
- オシロスコープは必須: DACを扱う上で、出力波形を直接観測できるオシロスコープは最も強力なデバッグツールです。期待通りの波形が出ているか、ノイズは乗っていないかなどを視覚的に確認できます。
- Live Expressions: STM32CubeIDEのデバッガには「Live Expressions」という機能があります。ここにDACやDMAのレジスタ名(例:
DAC1->DHR12R1
,DMA1_Stream5->NDTR
)を登録しておくと、プログラムを実行しながらリアルタイムでレジスタの値の変化を監視でき、問題の切り分けに非常に役立ちます。 - GPIOでタイミングを確認: 処理の特定のポイント(例: DMAの転送完了割り込みハンドラの中)でGPIOピンをトグル(High/Lowを反転)させるコードを挿入し、そのピンをオシロスコープで観測することで、処理が意図したタイミングで実行されているかを確認できます。
8. まとめ
本記事では、STM32の強力なペリフェラルであるDACについて、その基本機能から、HALライブラリとLLライブラリを用いた具体的な制御方法までを、詳細に解説しました。
- HALライブラリは、高い生産性と移植性を提供し、迅速な開発を可能にする強力な味方です。
- LLライブラリは、ハードウェアの性能を極限まで引き出し、コードサイズと速度を最適化するための鋭い武器です。
両者の特性を理解し、プロジェクトの要求に応じて適切に選択、あるいは混在させることが、STM32をマスターする上での鍵となります。
まずはHALで素早く動くものを作り、DACの動作原理を体感してみてください。そして、より高いパフォーマンスや、より深いハードウェアの理解が求められる場面で、LLライブラリへの挑戦を始めてみてはいかがでしょうか。この記事が、あなたのSTM32開発におけるデジタル-アナログ変換の旅の一助となれば幸いです。
更なる探求のためには、ぜひST公式サイトから入手できる、お使いのSTM32デバイスのリファレンスマニュアル(RM)とデータシート(DS)に目を通してみてください。そこには、この記事で触れられなかった高度な機能や、レジスタレベルでの詳細な動作が記されており、あなたの知識をさらに深めてくれるはずです。