- 2024年9月3日
 
1. はじめに
ユニフェイス 竹尾です。
入社して早くも半年が過ぎました。
ユニフェイスではハードと通信するためのシステムを開発する機会も多いです。
プログラミングを学習する上で、何らかのハードを制御してみたいと思い、何を動かすか考えていたところ、子供の頃から好きだったNゲージ(12V電源で走行する線路幅が9mm(Nine)の鉄道模型)を自動制御してみたら面白いのではと思いRaspberry PiとC#で制御してみることにしました。
※本記事で紹介する方法や情報をお試しいただく際は、ご自身の責任において行ってください。本ブログの内容により発生したいかなる損害やトラブルについて、弊社は一切の責任を負いかねますので、予めご了承ください。
2. 想定読者
- これからRaspberry Piで遊びたいと考えている方
 - プログラミングが好きな方
 - 何か制御したいと思っている方
 - Nゲージが好きな鉄道ファン
 
3. 必要なものを準備
通常、Nゲージを走行させるには前進・後退・速度調整が可能な専用のコントローラを用いて操作します。
今回、Raspberry PiでNゲージを制御する際は専用コントローラーと同等の操作が可能にしたいところです。
それを実現するためにまずは必要なものを揃えていきましょう。
電子部品に関しては秋月電子通商から調達できます。
- 鉄道模型(一番大切です)
 

- Raspberry Pi(Raspberry Pi4 ModelB 4GB)
 

- TB6612(モータードライバ)
- Nゲージは12Vの電源で動くのに対し、Raspberry PiのGPIO出力ピンは3.3Vです。
GPIOの出力ピンをそのままNゲージの電源に用いるとRaspberry Piの破損に繋がるので、それを防ぐためにモータードライバという部品を使用します。2個のモーターを制御できますが、今回は1個使用します。電源電圧13.5V、出力電流1.2AとNゲージの制御にちょうど良いスペックのTB6612を使用します。 
 - Nゲージは12Vの電源で動くのに対し、Raspberry PiのGPIO出力ピンは3.3Vです。
 

- ブレッドボード用2.1mm標準DCジャックDIP化キット
- ACアダプターの端子を接続するために使用します。
 
 - ACアダプターの端子を接続するために使用します。
 

- スイッチングACアダプター 12V2A KSA-24W-120200HU
- Nゲージを制御するための市販のパワーユニットは定格2A程度なので、このくらいのACアダプタで十分と考えます。
 
 - Nゲージを制御するための市販のパワーユニットは定格2A程度なので、このくらいのACアダプタで十分と考えます。
 

- DCフィーダーN,Nゲージ用レール
- DCフィーダーNはNゲージ用のレールに電力を供給するためのコネクタです。モータードライバと接続するにはそのままでは使用できないので、加工が必要です。(後述します)Nゲージのレールは車両が一周できる分あればOKです。
 
 

- ブレッドボード
- はんだ付けすることなく電子部品を差し込むだけで回路を作れるので何かと重宝します。用意しておきましょう。
 
 
- ジャンパーワイヤ
- ブレッドボードに差し込んだ電子部品同士を接続するために必要です。
 
 
4. DCフィーダーNの加工
DCフィーダーNのコントローラ側の端子は、専用コントローラーに接続するための形状となっており、そのままではモータドライバに接続はできません。
そこで、TJC8コネクターとコネクター用ハウジング 1Pを使用してモータドライバに接続できるよう加工します。
- 加工前のDCフィーダーN
 

- 加工後のDCフィーダーN
 

5. モータドライバとRaspberry Piの接続
モータドライバとRaspberry Piを接続しましょう。GPIO 側と端子側の接続先を表に示します。
この時、Raspberry Piの電源は必ずOFFの状態で作業しましょう。
誤ってショートを起こすとRaspberry Piを破損させることに繋がりかねません。

- GPIO 側
 
| TB6612 | GPIO | 
|---|---|
| PWMA | GPIO12 | 
| AIN2 | GPIO21 | 
| AIN1 | GPIO20 | 
| VCC | 3.3V | 
| STBY | VCCに接続した3.3V端子もしくはショートする | 
| GND | GND | 
| BIN1 | 接続しない | 
| BIN2 | 接続しない | 
| PWMB | 接続しない | 
- 端子(電源、レール)側
 
| TB6612 | 各種接続先 | 
|---|---|
| AO1 | D.C.フィーダーNの白色 | 
| AO2 | D.C.フィーダーNの茶色 | 
| BO2 | 接続しない | 
| BO1 | 接続しない | 
| VM | AC電源の+ | 
| PGND | AC電源の- | 
こちらがモータードライバTB6612とRaspberry PiのGPIOピンを接続した様子です。
※GPIOピンの位置を分かりやすくするために、拡張ボードを使用しています。

6. Raspberry Piのセットアップ
Raspberry Piを使用するにあたり、まずはOSをインストールする必要があります。
OS のインストール手順については、こちらの記事をお手本にさせていただきました。
ラズベリーパイのOSインストールと初期設定
7. Nゲージを制御するためのコンソールアプリを開発
TrainControllerという名称のコンソールアプリを開発しましょう。
アプリ開発に必要なライブラリ等を列挙します。
- .NET SDK
- .NETアプリケーションを開発、ビルド、テスト、およびデプロイするためのツールとライブラリです。
 
 - samba
- Raspberry PiをWindowsネットワーク上のファイルサーバーとして設定するために使用します。
 
 - Visual Studio
- Microsoftが提供する統合開発環境(IDE)です。プログラミング、デバッグ、テスト、デプロイなど、アプリケーション開発に必要な全ての機能を備えています。
 
 - その他の必要なライブラリやツール
- System.Device.Gpio
- NETアプリケーションからRaspberry PiなどのシングルボードコンピュータのGPIO(General Purpose Input/Output)ピンにアクセスし、操作するためのライブラリです。このライブラリを使用すると、センサーやアクチュエーターなどのハードウェアとのやり取りが可能になります。
 
 - System.Device.Pwm
- .NETアプリケーションでPWM(Pulse Width Modulation)信号を生成し、制御するためのライブラリです。PWMは、モーターの速度制御やLEDの明るさ調整などでよく使用される技術で、このライブラリを使うことで、プログラムからPWM信号を生成してデバイスを制御できます。
 - 今回はNゲージの速度調整を行うためにPWMを使用します。
 
 - System.Device.Pwm.Drivers
System.Device.Pwmライブラリの一部で、特定のPWMハードウェアドライバーをサポートするための追加ライブラリです。これにより、Raspberry Piや他のシングルボードコンピュータに接続された特定のPWMデバイスをより簡単に制御することができます。
 
 - System.Device.Gpio
 
.NET アプリケーションの構築
・プロジェクトの構成を下記に示します。
.
├── TrainControl
│   ├── MotorController.cs
│   ├── Program.cs
│   ├── TrainControl.csproj
│   ├── bin
│   │   └── Debug
│   │       └── net8.0
│   │           └── linux-arm64
│   └── obj
│       └── Debug
│           └── net8.0
│               └── linux-arm64
│                   ├── TrainControl.GlobalUsings.g.cs
│                   ├── ref
│                   └── refint
└── TrainControl.sln
- プログラムのソースは下記の通りです。
- MotorControllerクラスは、モーターの前進・後退、PWMのデューティー比を制御します。(デューティー比は、PWM信号において「オン」の時間が全体の周期(オンとオフを合わせた時間)の中でどれくらいの割合を占めるかを示す値です。デューティー比が高くなればモーターの回転数が上昇します。)
 - デューティー比の上限は実際の鉄道の走行スピードと合わせたいのと、スピードの出しすぎによる脱線防止のため、40%に設定します。(for文で徐々にスピードアップするロジックにしています)
 
 
 TrainControl/Program.cs
namespace TrainControl
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Enterキーを押してアプリケーションを起動します。");
            Console.ReadLine();
            using (MotorController motorController = new MotorController())
            {
                Console.WriteLine("走行開始");
                // モーターを前進
                await motorController.GoForward(5000); // 5秒間前進
                // モーターを後退
                await motorController.GoBackward(5000); // 5秒間後退
                Console.WriteLine("走行終了 Ctrl+Cキーを押すとプログラムを終了します。");
            }
            Console.ReadLine();
        }
    }
}
TrainControl/MotorController.cs
using System.Device.Gpio;
using System.Device.Pwm;
using System.Device.Pwm.Drivers;
namespace TrainControl
{
    public class MotorController : IDisposable
    {
        private const int motorRight = 20; // GPIO 20
        private const int motorLeft = 21;  // GPIO 21
        private const int pwmChannel = 12; // GPIO 12
        private const int pwmFrequency = 100;
        private const int stopDelayMs = 1000;
        private GpioController gpioController;
        private PwmChannel pwmController;
        private bool disposed = false;
        public MotorController()
        {
            gpioController = new GpioController(PinNumberingScheme.Logical);
            try
            {
                pwmController = new SoftwarePwmChannel(pwmChannel, pwmFrequency, 0);
                gpioController.OpenPin(motorRight, PinMode.Output);
                gpioController.OpenPin(motorLeft, PinMode.Output);
                pwmController.Start();
            }
            catch (Exception ex)
            {
                Console.WriteLine($"GPIOピンの初期化でエラーが発生: {ex.Message}");
                throw;
            }
        }
        public async Task GoForward(int durationMs)
        {
            await StopAsync();
            Console.WriteLine("前進");
            pwmController.DutyCycle = 0;
            gpioController.Write(motorRight, PinValue.High);
            gpioController.Write(motorLeft, PinValue.Low);
            await Accelerate();
            await Task.Delay(durationMs);
            await Decelerate();
            await StopAsync();
        }
        public async Task GoBackward(int durationMs)
        {
            await StopAsync();
            Console.WriteLine("後退");
            gpioController.Write(motorRight, PinValue.Low);
            gpioController.Write(motorLeft, PinValue.High);
            await Accelerate();
            await Task.Delay(durationMs);
            await Decelerate();
            await StopAsync();
        }
        public async Task StopAsync()
        {
            try
            {
                Console.WriteLine("停止");
                pwmController.DutyCycle = 0;
                gpioController.Write(motorRight, PinValue.High);
                gpioController.Write(motorLeft, PinValue.High);
                await Task.Delay(stopDelayMs);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"停止処理中にエラーが発生: {ex.Message}");
            }
        }
        private async Task Accelerate()
        {
            for (int i = 0; i <= 40; i++)
            {
                pwmController.DutyCycle = (double)i / 100;
                await Task.Delay(100);
            }
        }
        private async Task Decelerate()
        {
            for (int i = 40; i >= 0; i--)
            {
                pwmController.DutyCycle = (double)i / 100;
                await Task.Delay(100);
            }
        }
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if(disposing)
                {
                    pwmController?.Stop();
                    gpioController?.ClosePin(motorRight);
                    gpioController?.ClosePin(motorLeft);
                    gpioController?.Dispose();
                    pwmController?.Dispose();
                }
                disposed = true;
            }
        }
    }
}
コンソールアプリの発行
アプリケーションが完成したらRaspberry Piに発行します。(Raspberry Piに.NETをインストールしておく必要があります。インストール方法はこちらを参考にしてください。ARM シングルボード コンピューターに .NET アプリを配備する) Visual Stadioのソリューションエクスプローラーにあるプロジェクトを右クリックして、「発行」を選択します。

正しいパスになっていることを確認して、「発行」をクリックします。

アプリが配置されていることを確認します。

8. コンソールアプリを起動してNゲージを走行させる
Raspberry Piのコンソールを立ち上げて、アプリを起動します。
アプリを配置したディレクトリに移動します。
cd TrainControl
下記コマンドで起動します。
dotnet ./TrainControl.dll

無事、Nゲージが走り出せば成功です!
自動で動くNゲージを見ると、本物の電車を眺めているような感覚になりますね。



9. 今後の予定
次回はセンサを用いて、決められた位置で走行開始、停止できるようにし、より本物の鉄道らしい制御に挑戦したいと思います。
10. 最後に
ユニフェイスは主に製造業向けのシステム開発を行っており、ハードとの通信も得意な企業です。
今回の個人開発を通じて、ハードとやり取りをする方法のキャッチアップができたのではないかと考えます。
今後も楽しみながら学習を続けたいです。