2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

STM32G474のDACを本気で回す!オシロスコープに「ベクタースキャン」で絵を描画してみた

Posted at

はじめに

STM32マイコンで「Lチカ」や「UART通信」をするのはもう飽きた……。

そんなある日、X (旧Twitter) のタイムラインで 「オシロスコープに絵を描画している動画」 が流れてきました。緑色の蛍光線で描かれた幾何学模様がグリグリ動く、あのレトロフューチャーな見た目。「かっこいい……自分もこれを手元で再現したい!」と強く感化されました。

幸い、手元には STM32G474RE という、アナログ機能(DAC/OpAmp/Comparator)が異常に強化された「Mixed-Signal MCU」があります。

「この高性能なアナログ機能をフル活用すれば、もっと滑らかに描けるのでは?」

そこで今回は、オシロスコープをディスプレイ代わりにする 「ベクタースキャン(Vector Scan)」 表示に挑戦します。
マイコンのDACからX軸・Y軸の電圧を高速出力して電子ビームを操り、オシロスコープの管面に幾何学模様を描画してみました。

成果物

まずは結果をご覧ください。10種類以上の図形が次々と切り替わる、電子スライドショーの完成です。

なぜSTM32G474なのか?(技術的選定理由)

単にDACがついているマイコンなら他にもありますが、ベクタースキャン描画においてSTM32G474REは以下の2点で圧倒的に有利です。

1. 強力なDAC出力バッファ(Output Buffer)

DACの出力には内部抵抗が存在します。これがオシロスコープの入力容量やケーブルの浮遊容量(C成分)と結合するとローパスフィルタ(RC回路)が形成され、電圧を高速で変化させた際に波形がなまってしまいます。

STM32G4のDACには内蔵オペアンプによる出力バッファが搭載されており、これを Enable にすることで低インピーダンスで信号をドライブできます。これにより、高速描画時でもエッジの効いた「キレのある図形」を描くことが可能です。

2. Cortex-M4FのFPU(浮動小数点演算ユニット)の実力

今回、座標計算や補間処理には float 型を多用します。
通常、FPUを持たないマイコン(Cortex-M0+など)で浮動小数点演算を行うと、コンパイラはソフトウェアライブラリをリンクし、数十〜百サイクルかけてエミュレーション実行します。これでは描画のリアルタイム性は損なわれます。

しかし、STM32G4が搭載する Cortex-M4F コアは、シングル精度のFPU(Floating Point Unit)をハードウェアで内蔵しています。これにより、劇的なパフォーマンス向上が得られます。

① 命令レベルでの圧倒的な差

FPU有効時、浮動小数点の加算や乗算は vadd.f32vmul.f32 といった専用のハードウェア命令にコンパイルされ、わずか1〜3サイクル で完了します。
170MHzという高速なクロックに加え、サイクル数自体が1/10〜1/50に短縮されるため、座標変換のような行列演算も「一瞬」で終わります。

② 実装時の注意点: 「f」を忘れるな!

ここで一つ、重要なテクニックがあります。STM32G4のFPUは 「単精度(float)」専用 です。
C言語の仕様上、単に 3.14 と書くと 倍精度(double) として扱われてしまいます。

// 悪い例(激遅)
// xも0.8もdoubleに格上げされ、FPUが使われずソフトウェア処理になる
float x = val * 0.8; 

// 良い例(爆速)
// 全てfloatとして扱われ、FPU命令(vmul.f32)が走る
float x = val * 0.8f; 

今回のコードで 0.8f2047.5f のように、執拗に f サフィックスをつけているのはこのためです。この些細な違いが、オシロスコープ上の描画レートに直結します。

実装の原理と数式モデル

座標系からDAC電圧へのマッピング

図形データは扱いやすいように正規化座標系($-1.0 \le x, y \le +1.0$)で定義します。一方、STM32G4のDACは12bit分解能であり、出力値は $0$ ~ $4095$ の整数値です。

正規化座標 $P_{norm}$ を DACのデジタル値 $D_{dac}$ に変換する式は以下のようになります。
$$D_{dac} = \left( \frac{P_{norm} \times S + 1.0}{2} \right) \times (2^{12} - 1)$$

ここで、$S$ はスケーリング係数(今回は画面端でのクリッピングを防ぐため$0.8$ )です。
この式を展開すると、プログラムに実装する定数が見えてきます。
$$D_{dac} \approx (P_{norm} \times 0.8 + 1.0) \times 2047.5$$

これにより、計算上の原点 $(0,0)$ は DACの中間値である $2047$ (約1.65V) にマップされ、オシロスコープの中心に表示される仕組みです。。

ベクトルによる線形補間(Lerp)

単に頂点の電圧だけを出力すると、オシロスコープ上では「点」しか表示されません。綺麗な「線」として表示させるため、点 から点 へ移動する間を埋める線形補間(Linear Interpolation) を行います。

点 $A(x_1, y_1)$ から点 $B(x_2, y_2)$ への直線のベクトル方程式は、パラメータ $t$ ($0 \le t \le 1$) を用いて以下のように表せます。
$$\vec{P}(t) = \vec{A} + t(\vec{B} - \vec{A})$$

これを離散的なステップ数 $N$ でループ処理する場合、第 $i$ ステップ目の座標 $(x_i, y_i)$ は次のように導出されます。
$$x_i = x_1 + \frac{i}{N}(x_2 - x_1)$$

プログラム上では、この $\Delta x = \frac{x_2 - x_1}{N}$ を事前に計算することで、ループ内を単純な加算処理にし、CPU負荷を最小限に抑えています。

ハードウェア構成

  • MCU: STM32 Nucleo-64 (NUCLEO-G474RE)

  • 表示機: X-Yモード機能付きのオシロスコープ
    ※今回はPC接続型のデジタルオシロ(OWON VDSシリーズ)を使用しています。
    昔ながらのアナログオシロはもちろん、最近のデジタルオシロでもX-Yモードがあれば再現可能です。デジタルオシロ特有の「サンプリングの点」が見える描画も、ドット絵のような味があって面白いです。

  • 配線:

Nucleo Pin 機能 オシロスコープ
PA4 DAC1_OUT1 CH1 (X軸)
PA5 DAC1_OUT2 CH2 (Y軸)
GND Ground 各CHのGND (必須)

※ GNDが浮いていると、誘導ノイズにより波形が崩壊します(経験済)。

CubeMXの設定

1. Clock Configuration

滑らかな線を描くには処理速度が命です。
HSI (内部高速発振) をソースにして、G474の最大クロックである 170MHz に設定します。
image.png

2. Analog (DAC1)

  • OUT1 / OUT2 Mode: Connected to external pin only
  • Configuration:
  • Output Buffer: Enable (前述の通り、ここが重要です)
    image.png

コードの実装

1. マクロ定義と座標データ

まずは数値扱いのためのマクロと、描画する図形データを定義します。 ここで定義する F2Q31 は、浮動小数点の -1.0 ~ +1.0 を、32bit整数の全範囲(Q1.31形式)にマッピングするものです。
今回は「ハート」「星」「稲妻」などの図形を用意しました。

// 浮動小数点(-1.0 ~ +1.0) を Q1.31固定小数点に変換
// ※ 整数演算での高速化や、CORDICユニットへの入力に使用します
#define F2Q31(x) ((int32_t)((x) * 2147483648.0f))

// 座標構造体
typedef struct { float x; float y; } Point;

// --- 図形データ定義 (一筆書きの順序で) ---

// 1. 四角形 (最後は始点に戻る)
const Point shape_square[] = {
    {-0.6f, -0.6f}, { 0.6f, -0.6f}, { 0.6f,  0.6f}, {-0.6f,  0.6f}, {-0.6f, -0.6f}
};

// 2. 三角形
const Point shape_triangle[] = {
    { 0.0f,  0.7f}, { 0.7f, -0.5f}, {-0.7f, -0.5f}, { 0.0f,  0.7f}
};

// 3. 星型 (五芒星)
const Point shape_star[] = {
    { 0.00f,  0.80f}, { 0.47f, -0.64f}, {-0.76f,  0.25f},
    { 0.76f,  0.25f}, {-0.47f, -0.64f}, { 0.00f,  0.80f}
};
// 4. ダイヤ (ひし形)
const Point shape_diamond[] = {
    { 0.0f,  0.8f}, { 0.5f,  0.0f}, { 0.0f, -0.8f}, {-0.5f,  0.0f}, { 0.0f,  0.8f}
};

// 5. 砂時計 (Xのような形)
const Point shape_hourglass[] = {
    {-0.5f,  0.7f}, { 0.5f,  0.7f}, {-0.5f, -0.7f}, { 0.5f, -0.7f}, {-0.5f,  0.7f}
};

// 6. 矢印
const Point shape_arrow[] = {
    {-0.4f, -0.2f}, { 0.2f, -0.2f}, { 0.2f, -0.6f}, { 0.8f,  0.0f},
    { 0.2f,  0.6f}, { 0.2f,  0.2f}, {-0.4f,  0.2f}, {-0.4f, -0.2f}
};

// 7. 六角形
const Point shape_hexagon[] = {
    { 0.4f,  0.7f}, { 0.8f,  0.0f}, { 0.4f, -0.7f},
    {-0.4f, -0.7f}, {-0.8f,  0.0f}, {-0.4f,  0.7f}, { 0.4f,  0.7f}
};

// 8. イナズマ
const Point shape_bolt[] = {
    { 0.3f,  0.8f}, { 0.0f,  0.1f}, { 0.4f,  0.1f}, // 上のギザギザ
    {-0.2f, -0.8f}, { 0.1f, -0.1f}, {-0.3f, -0.1f}, // 下のギザギザ
    { 0.3f,  0.8f}  // 始点に戻る
};
// 9. 無限大 (∞の字) - 8の字結び
const Point shape_infinity[] = {
    { 0.0f,  0.0f}, {-0.4f,  0.4f}, {-0.8f,  0.0f}, {-0.4f, -0.4f},
    { 0.0f,  0.0f}, { 0.4f,  0.4f}, { 0.8f,  0.0f}, { 0.4f, -0.4f}, { 0.0f,  0.0f}
};

// 10. 渦巻き (四角いスパイラル)
const Point shape_spiral[] = {
    { 0.0f,  0.0f}, { 0.1f,  0.0f}, { 0.1f,  0.1f}, {-0.1f,  0.1f},
    {-0.1f, -0.1f}, { 0.2f, -0.1f}, { 0.2f,  0.2f}, {-0.2f,  0.2f},
    {-0.2f, -0.2f}, { 0.3f, -0.2f}, { 0.3f,  0.3f}, {-0.3f,  0.3f},
    {-0.3f, -0.3f}, { 0.4f, -0.3f}, { 0.4f,  0.4f}, {-0.4f,  0.4f},
    {-0.4f, -0.4f}, { 0.5f, -0.4f}, { 0.5f,  0.5f}, {-0.5f,  0.5f},
    {-0.5f, -0.5f}, { 0.6f, -0.5f}, { 0.6f,  0.6f}, {-0.6f,  0.6f}
};

// 11. ハート (ローポリゴン風)
// 曲線は難しいので、直線で構成したデジタルなハートです
const Point shape_heart[] = {
    { 0.0f, -0.8f}, // 下の尖った部分
    { 0.7f, -0.1f}, { 0.7f,  0.5f}, { 0.3f,  0.8f}, // 右半分
    { 0.0f,  0.4f}, // 中央の谷
    {-0.3f,  0.8f}, {-0.7f,  0.5f}, {-0.7f, -0.1f}, // 左半分
    { 0.0f, -0.8f}  // 始点に戻る
};
// --- 図形管理用 ---
typedef struct {
    int num_points;      // 頂点の数
    const Point* points; // 座標配列へのポインタ
} ShapeData;

// 全図形のリスト
const ShapeData my_shapes[] = {
    { sizeof(shape_square)/sizeof(Point),    shape_square },
    { sizeof(shape_triangle)/sizeof(Point),  shape_triangle },
    { sizeof(shape_star)/sizeof(Point),      shape_star },
    { sizeof(shape_diamond)/sizeof(Point),   shape_diamond },
    { sizeof(shape_hourglass)/sizeof(Point), shape_hourglass },
    { sizeof(shape_arrow)/sizeof(Point),     shape_arrow },
    { sizeof(shape_hexagon)/sizeof(Point),   shape_hexagon },
    { sizeof(shape_bolt)/sizeof(Point),      shape_bolt },
    { sizeof(shape_infinity)/sizeof(Point),  shape_infinity },
    { sizeof(shape_spiral)/sizeof(Point),    shape_spiral },
	{ sizeof(shape_heart)/sizeof(Point),     shape_heart },
};
    // ここに丸やバツなどを追加していく

const int NUM_SHAPES = sizeof(my_shapes) / sizeof(ShapeData);

2. 描画ドライバ

数式モデルに基づいたDAC出力関数と、補間関数です。

/* USER CODE BEGIN 0 */
// 座標(-1.0~1.0) を DAC値(0~4095) に変換して出力
void DAC_Out(float x, float y)
{
    // マッピング式: D = (P * 0.8 + 1.0) * 2047.5
    uint32_t dac_x = (uint32_t)((x * 0.8f + 1.0f) * 2047.5f);
    uint32_t dac_y = (uint32_t)((y * 0.8f + 1.0f) * 2047.5f);

    // クリッピングガード
    if(dac_x > 4095) dac_x = 4095;
    if(dac_y > 4095) dac_y = 4095;

    HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, dac_x);
    HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_2, DAC_ALIGN_12B_R, dac_y);
}

// 線分を描画する(線形補間)
void DrawLine(float x1, float y1, float x2, float y2)
{
    const int steps = 25; // 分割数(ここを増やすと線が濃くなる)
    float dx = (x2 - x1) / steps;
    float dy = (y2 - y1) / steps;

    for (int i = 0; i <= steps; i++) {
        DAC_Out(x1 + dx * i, y1 + dy * i);
    }
}
/* USER CODE END 0 */

3. メインループ

2秒ごとに図形を切り替えるスライドショー形式にします。

/* USER CODE BEGIN WHILE */
while (1)
{
    // --- 図形の切り替え処理 ---
    if (HAL_GetTick() - last_switch_time > 2000)
    {
        current_shape_idx++;
        if(current_shape_idx >= NUM_SHAPES) current_shape_idx = 0;
        last_switch_time = HAL_GetTick();
    }

    // --- 描画処理 ---
    const ShapeData* now = &my_shapes[current_shape_idx];
    
    // 一筆書きで全頂点を結ぶ
    for(int i = 0; i < now->num_points - 1; i++) {
        DrawLine(now->points[i].x, now->points[i].y,
                 now->points[i+1].x, now->points[i+1].y);
    }
}
/* USER CODE END WHILE */

結果

170MHzの処理能力とDACバッファのおかげで、チラつきの全くない、非常にシャープな図形が表示されました。

特にイナズマの鋭角な部分や、図形の交差部分も綺麗に表現できています。デジタル制御ですが、オシロスコープの蛍光表示特有の「滲み」が良いレトロフューチャー感を出しています。

まとめ・今後の展望

STM32G474のDACを使って、理論通りにオシロスコープにお絵かきをすることができました。
「数式通りの波形を、物理的な電圧として出力し、電子ビームを曲げる」というプロセスは、ディスプレイに printf するのとは違った原始的な喜びがあります。

今回は頂点同士を直線で結ぶ「ポリゴン描画」のみでしたが、G474には三角関数を高速計算する CORDIC ユニットも搭載されています。次はこれを使って、3D回転や滑らかな曲線の描画にも挑戦してみたいと思います。

2
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?