【STM32H723ZG入門⑦】共通FW基盤編 | ディレクトリ構成とレイヤ構造の構築

スポンサーリンク

この記事でできること

  • 再利用可能な共通ファームウェア基盤を構築できる
  • 長期保守を前提としたディレクトリ構成を設計できる
  • app / drv / plat / utils / mw によるレイヤ分離アーキテクチャを定義できる
  • main.c を最小化し、アプリケーションを外部へ分離できる
  • third_partyライブラリを再現可能な形で管理できる
  • git clone 直後からビルド可能なテンプレートを用意できる

はじめに

作業量が多すぎて少し疲弊気味な jin@GoldearNetworksです。

STM32プロジェクトを新規に立ち上げるたびに、CubeMX生成コードの整理やディレクトリ構成の再検討、過去プロジェクトからのコード流用といった作業が発生します。一見すると小さな手間ですが、案件数が増えるにつれて構造のばらつきや責務の混在が発生し、保守性や再利用性が低下していきます。

そこで本記事では、STM32H723ZGを対象に、全プロジェクトで共通利用できるファームウェア基盤を構築します。目的は「今だけ動く構成」ではなく、長期保守可能かつ再利用可能な設計を最初から定義することです。

開発環境

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

  • 開発基板:STM32H723ZG
  • 開発環境:STM32CubeIDE
  • MCU設定:STM32CubeMX
  • OS:Windows11 24H2

なお、本記事の内容はSTM32H7シリーズに限らず、他のSTM32シリーズでも同様の考え方で適用可能です。

全体の流れ

本記事では、共通ファームウェア基盤を以下の手順で構築していきます。

  1. ディレクトリ構成の設計方針を決定する
  2. レイヤ分離アーキテクチャを定義する
  3. main.c を最小化し、アプリケーションの起動フローを整理する
  4. third_party 管理方針を決定し、再現可能ビルドを実現する
  5. テンプレートとしてビルド確認を行う

単なるフォルダ整理ではなく、責務分離と依存方向を明確にした構造設計 を行うことが本記事の主題です。

Step1. ディレクトリ構成の設計方針を決める

最初に決めるべきことは、フォルダ名ではなく「設計方針」です。本基盤では、以下の方針を採用します。

  • アプリケーションロジックとハードウェア依存部分を明確に分離する
  • MCU依存コードを最下層に閉じ込める
  • 再利用可能な構造を前提に設計する
  • 新規プロジェクトで迷わない階層構造にする

なお、STM32関連の解説記事では、動作確認を優先して main.c に全ての処理を集約している例をよく見かけます。しかし、テンプレートとして再利用する基盤では、初期段階から責務分離と依存方向を明確にしておかないと、すぐに構造が破綻します。本記事では、あえて main.c を最小化し、ロジックを外部へ分離する方針で構築を進めます。

Step2. レイヤ分離アーキテクチャの定義

本プロジェクトでは、ユーザーが管理するコードを<project>/User配下に集約します。CubeMX生成コード(Core / Drivers / Middlewares)とは物理的に分離し、責務の混在を防止します。User配下のレイヤは、以下の構造を採用します。

  • app:アプリケーションロジック
  • drv:デバイス/機能ドライバ
  • mw:ミドルウェアおよび移植層
  • plat:MCU/ボード依存層
  • utils:共通ユーティリティ
  • third_party:外部ライブラリ

依存方向ルール(絶対遵守)は以下の通りです。

  • 依存は一方向のみ:app → drv → mw → plat
  • utils は横断的に利用可能(ただし、上位レイヤは参照不可)
  • 逆方向依存の禁止
  • app から plat への直接アクセス禁止
  • app から mw 直接呼び出しの禁止
  • drv から app 参照の禁止
  • third_party の直接 include 禁止

ビルドとは直接関係しない成果物・補助コードは、以下のディレクトリに分離して格納します。

  • User/__docs:設計メモ、図、記事下書きなど
  • User/__scripts:セットアップ・生成・保守スクリプト
  • User/__tests:テストコード、試験用スタブ、検証用プログラム
  • User/__tools:ホスト側ツール、変換スクリプト、補助ユーティリティ

先頭に __ を付けることで、「アプリ本体のレイヤ(app/drv/mw/plat/utils/third_party)」と視覚的に区別しやすくし、ビルド対象からも切り離しやすくしています。

実際のディレクトリ構造

User/
├─app
│  ├─Inc
│  └─Src
├─drv
│  ├─Inc
│  └─Src
├─mw
│  ├─Inc
│  └─Src
├─plat
│  ├─bsp
│  ├─Inc
│  └─Src
├─third_party
├─utils
│  ├─Inc
│  └─Src
├─_docs
├─_scripts
├─_tests
└─_tools

Step3. main.c最小化とアプリ起動フローの整理

本基盤では、CubeMX生成コードにアプリケーションロジックを書き込まない方針とします。そのため、main.c は最小限の初期化とアプリ起動のみを担当します。

  • HAL初期化
  • システムクロック設定
  • 周辺初期化(CubeMX生成)
  • アプリ初期化呼び出し(app_boot)
  • アプリメインループ呼び出し(app_run_forever)

アプリケーション側の起動フローは、以下の2関数に分離します。

  • app_boot():初期化処理
  • app_run_forever():メインループ処理

この分離により、main.c の肥大化を防ぎ、テンプレートとして再利用しやすい構造を維持できます。

Step4. third_party管理と再現可能ビルドの仕組み

外部ライブラリは User/third_party 配下に集約します。アプリケーション層から直接参照しない構造とします。

  • third_party の物理的分離
  • 直接 include の禁止
  • スクリプトによる取得・配置
  • リポジトリの肥大化防止
  • 再現可能ビルドの維持

特に外部ライブラリを直接リポジトリに取り込むのではなく、スクリプトによって同一構成を再生成できる状態を保持します。これにより、環境差や個人依存を排除し、長期保守可能なテンプレート構造を維持します。

Step5. 実際にテンプレートとして使ってみる(ビルド確認)

ここでは、作成した共通FW基盤をテンプレートとして扱い、STM32CubeIDEでインポートしてビルドできることを確認します。

  • リポジトリの取得(git clone)
  • 必要な third_party のセットアップ
  • STM32CubeIDE へのインポート
  • ビルド実行

ここでは Windows11 24H2 を例に作業を進めていきます。

リポジトリの取得

下記リポジトリを git clone および ZIPダウンロードして展開します。

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...

Windows で git を使う場合は TortoiseGit があると便利です。

TortoiseGit – Windows Shell Interface to Git

third_party のセットアップ

必要な外部ライブラリは、git clone したディレクトリに移動して、用意したスクリプトを実行することで取得・配置します。

> cd <repo_dir>
> powershell -ExecutionPolicy Bypass -File User/_scripts/setup_third_party_ssd1306.ps1

Checking git...
Cleaning old temp...
Cloning SSD1306 repo...
Cloning into 'E:\git\GN-fw_core_stm32\User\_tmp_ssd1306'...
remote: Enumerating objects: 61, done.
remote: Counting objects: 100% (61/61), done.
remote: Compressing objects: 100% (57/57), done.
remote: Total 61 (delta 8), reused 37 (delta 1), pack-reused 0 (from 0)
Receiving objects: 100% (61/61), 751.20 KiB | 25.90 MiB/s, done.
Resolving deltas: 100% (8/8), done.
Copying required files...
  + ssd1306.c  <=  E:\git\GN-fw_core_stm32\User\_tmp_ssd1306\ssd1306\ssd1306.c
  + ssd1306.h  <=  E:\git\GN-fw_core_stm32\User\_tmp_ssd1306\ssd1306\ssd1306.h
  + ssd1306_fonts.c  <=  E:\git\GN-fw_core_stm32\User\_tmp_ssd1306\ssd1306\ssd1306_fonts.c
  + ssd1306_fonts.h  <=  E:\git\GN-fw_core_stm32\User\_tmp_ssd1306\ssd1306\ssd1306_fonts.h
  + ssd1306_conf.h  <=  E:\git\GN-fw_core_stm32\User\_tmp_ssd1306\examples\oled-tester\firmware\i2c\ssd1306\ssd1306_conf.h
  + LICENSE  <=  E:\git\GN-fw_core_stm32\User\_tmp_ssd1306\LICENSE
  + README.md  <=  E:\git\GN-fw_core_stm32\User\_tmp_ssd1306\README.md
Patching ssd1306_conf.h for STM32H7...

=========================================
 SSD1306 setup complete!
 You can build immediately.
=========================================

STM32CubeIDE へインポート

.project をダブルクリックで STM32CubeIDE を起動します。以下のダイアログが出れば成功です。
また同名のプロジェクトファイルがあるとインポートできないので、すでにインポート済みのプロジェクトを閉じてください。(ファイル削除に注意)

インポート後のプロジェクトディレクトリは以下のようになっています。

ビルド実行

Build を実行し、エラーなくコンパイル・リンクが完了することを確認します。include path 等のビルド設定は .cproject に保存されているため、clone 後に追加設定は不要です。

Finished building target: GN-fw_core.elf
 
arm-none-eabi-size  GN-fw_core.elf 
arm-none-eabi-objdump -h -S GN-fw_core.elf  > "GN-fw_core.list"
   text	   data	    bss	    dec	    hex	filename
  53624	    128	   5120	  58872	   e5f8	GN-fw_core.elf
Finished building: default.size.stdout
 
Finished building: GN-fw_core.list
 
10:58:08 Build Finished. 0 errors, 1 warnings. (took 3s.19ms)

なお、一部の自己診断用コードを無効化しているため、未使用関数に関する warningが出力される場合があります。本記事の動作確認には影響ありません。将来的には、未使用コードの整理またはコンパイル条件の分離によりwarning 0を目指します。

この確認により、本リポジトリが「git clone → セットアップ → CubeIDE import → build」まで再現可能であることが分かります。

内部解説(設計思想と依存関係の考え方)

本基盤の目的は、単なるフォルダ整理ではありません。責務を分離し、依存方向を固定することで、構造の破綻を防ぐことにあります。

なぜ依存方向を固定するのか

ソフトウェアが複雑化する主な原因は、依存関係の逆流です。上位レイヤが下位レイヤを参照することは自然ですが、下位レイヤが上位レイヤを参照し始めると、循環依存が発生します。

依存方向を一方向に限定することで、以下の効果があります。

  • 循環依存の防止
  • テスト容易性の向上
  • MCU変更時の影響範囲限定
  • 機能単位での再利用性向上

依存方向は Step2 で定義した通り、app → drv → mw → plat の一方向とします。

main.c を最小化する理由

main.c はエントリポイントであり、構造の中心ではありません。アプリケーションロジックを main.c に直接記述すると、構造の分離が崩れ、テンプレートとしての再利用性が低下します。

本基盤では main.c を初期化専用に限定し、アプリケーションの責務は app 層へ委譲します。

third_party を隔離する理由

外部ライブラリを直接アプリ層から参照すると、将来的な差し替えやテストが困難になります。third_party を物理的に分離し、ラッパを介して利用する構造とすることで、依存の暴走を防止します。

ディレクトリは結果であり、設計が先

ディレクトリ構成は設計思想の結果です。依存方向と責務を先に定義することで、自然に構造が決まります。本記事で示した構造は一例ですが、「依存方向を固定する」という原則は普遍です。

ハマりポイント・設計時の注意点

本基盤を構築する中で、実際にハマりやすいポイントを整理します。

include path の設定漏れ

レイヤを分離すると、CubeIDE の include path 設定が増加します。.cproject に保存されるとはいえ、相対パスで設定していないと環境依存が発生します。

  • 相対パス設定の徹底
  • .cproject のリポジトリ管理
  • クリーン環境でのビルド確認

依存方向の破綻

実装が進むと、app から plat を直接参照したくなる場面が発生します。一時的に楽でも、構造の破綻につながります。

  • 直接HAL呼び出しの禁止
  • ミドルウェア直接呼び出しの禁止
  • 循環依存の回避

third_party の直接利用

外部ライブラリを app 層から直接 include すると、差し替えが困難になります。

  • ラッパ経由での利用
  • third_party の物理的隔離
  • 将来差し替え前提の設計

未使用コードによる warning

自己診断機能などを一時的に無効化すると、未使用関数の warning が出る場合があります。

  • 動作への影響なし
  • 将来的な整理対象

まとめ

本記事では、STM32H723ZG向けの共通ファームウェア基盤を構築し、ディレクトリ構成とレイヤ分離アーキテクチャを定義しました。重要なのは、フォルダ構成そのものではなく、依存方向と責務を固定する設計思想です。

  • 依存は一方向のみ
  • 責務の明確化
  • main.c の最小化
  • third_party の隔離
  • 再現可能ビルドの維持

これらのルールを遵守することで、新規プロジェクト立ち上げ時の初期構築コストを大幅に削減できます。構造を最初に決めることが、長期保守可能なプロジェクトへの第一歩です。

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...

前回

【STM32H723ZG入門⑥】SDカード読み書き編(FatFS/ログ)
この記事でできること STM32H723ZGでSDカードを読み書きできる FatFsをCubeMXで統合する方法が分かる FatFsを直接使わない「drv抽象化設計」が理解できる CSV形式でログ保存するロガーを実装できる そのまま実務で流...

次回:(準備中)

コメント