📺

Raspberry Pi - Iot.Device(.NET)で表示デバイスを制御する

2024/09/08に公開

前回の記事でIot.Deviceの利用について記載しました。

いろいろなデバイスが対応しており、今回は表示デバイスの制御について記載します。

https://twitter.com/tw_kotatu/status/1832550707339428033

💻開発環境

  • ターゲット
    • RaspberryPi 4B
      • Linux pi4home 5.15.32-v8+ #1538 SMP PREEMPT Thu Mar 31 19:40:39 BST 2022 aarch64 GNU/Linux
      • Visual Studio Code 1.93.0

📺表示デバイスを利用する

SSD1306(128x64、I2C接続)を利用します。

https://akizukidenshi.com/catalog/g/g112031/

接続図

プロジェクト作成とパッケージのインストール

SSHで接続し、dotnetコマンドで構築します。

$ dotnet new console -o Ssd1306Tutorial
$ dotnet add package Iot.Device.Bindings
$ dotnet add package Iot.Device.Bindings.SkiaSharpAdapter

生成したプロジェクトをワークスペースで開きます。

表示データの準備

  • ./images下のbitmapを連続して表示をします
  • bitmapの使用は以下です
項目
128 ピクセル
高さ 64 ピクセル
ビットの深さ 1

使用した画像は、フリイラくんの走る猫を加工しました。

https://furiirakun.com/creatures/走る猫?key=category

ファイル名 画像
frame_0.png
frame_1.png
frame_2.png
frame_3.png
frame_4.png
frame_5.png
frame_6.png
frame_7.png
frame_8.png

コード

  • はまったところも合わせて記載します

意図しない動作のコード

  • 公式レポジトリにサンプルをもとにコードを作成しました
Program.cs
using System.Device.I2c;
using Iot.Device.Graphics;
using Iot.Device.Graphics.SkiaSharpAdapter;
using Iot.Device.Ssd13xx;
using Iot.Device.Ssd13xx.Commands;
using Ssd1306Cmnds = Iot.Device.Ssd13xx.Commands.Ssd1306Commands;
using System.Threading;

namespace Ssd1306Tutorial
{
    class Program
    {
        static void Main(string[] args)
        {
            SkiaSharpAdapter.Register();

            // Set up the I2C bus
            I2cConnectionSettings connectionSettings = new(1, 0x3C);
            var i2cDevice = I2cDevice.Create(connectionSettings);

            var device = new Ssd1306(i2cDevice, 128, 64);

            device.ClearScreen();
            Console.WriteLine("Display Images");

            // 画像を表示
            for (var i = 0; i < 1000; i++)
            {
                foreach (var image_name in Directory.GetFiles("images", "*.bmp").OrderBy(f => f))
                {
                    using BitmapImage image = BitmapImage.CreateFromFile(image_name);
                    device.DrawBitmap(image);
                    Thread.Sleep(10);
                }
            }

            device.ClearScreen();
            device.Dispose();
        }
    }
}

上記を実施したところ、以下のような表示になりました

  • 画像の半分しか表示されていない
  • 画像に1ラインごとに黒い線が入っている

原因の調査

  • ライブラリをみたところ、128x64に対応しておらず、x32の固定になっていました

  • 以下にコメントをいれて修正箇所を抜粋します

  • レジスタの設定

    Ssd1306.cs
        public class Ssd1306 : Ssd13xx
        {
     
            // Display size 128x32 or 128x64
            private void Initialize()
            {
                SendCommand(new SetDisplayOff());
                SendCommand(new Ssd1306Cmnds.SetDisplayClockDivideRatioOscillatorFrequency(0x00, 0x08));
    -           SendCommand(new SetMultiplexRatio(0x1F));   
    +           // マルチプレックス比(表示の高さに相当する値)
    +           // 128x64 の場合、SET_MUX_RATIO の値は 0x3F(63)を指定
    +           SendCommand(new SetMultiplexRatio(0x3F));   
                SendCommand(new Ssd1306Cmnds.SetDisplayOffset(0x00));
                SendCommand(new Ssd1306Cmnds.SetDisplayStartLine(0x00));
                SendCommand(new Ssd1306Cmnds.SetChargePump(true));
                SendCommand(new Ssd1306Cmnds.SetMemoryAddressingMode(Ssd1306Cmnds.SetMemoryAddressingMode.AddressingMode.Horizontal));
                SendCommand(new Ssd1306Cmnds.SetSegmentReMap(true));
                SendCommand(new Ssd1306Cmnds.SetComOutputScanDirection(false));
    -           SendCommand(new Ssd1306Cmnds.SetComPinsHardwareConfiguration(false, false));
    +           // COMピンのハードウェア設定
    +           // 128x64 の場合、SET_COM_PIN_CFG は 0x12(代替 COMピン設定と通常の出力スキャン方向を使用)
    +           SendCommand(new Ssd1306Cmnds.SetComPinsHardwareConfiguration(true, false));
                SendCommand(new SetContrastControlForBank0(0x8F));
                SendCommand(new Ssd1306Cmnds.SetPreChargePeriod(0x01, 0x0F));
                SendCommand(new Ssd1306Cmnds.SetVcomhDeselectLevel(Ssd1306Cmnds.SetVcomhDeselectLevel.DeselectLevel.Vcc1_00));
                SendCommand(new Ssd1306Cmnds.EntireDisplayOn(false));
                SendCommand(new Ssd1306Cmnds.SetNormalDisplay());
                SendCommand(new SetDisplayOn());
                SendCommand(new Ssd1306Cmnds.SetColumnAddress());
    -           SendCommand(new Ssd1306Cmnds.SetPageAddress(Ssd1306Cmnds.PageAddress.Page1, Ssd1306Cmnds.PageAddress.Page3));
    +           // ページ数
    +           // 128x64 の場合、height / 8 = 64 / 8 = 8ページ 
    +           SendCommand(new Ssd1306Cmnds.SetPageAddress(Ssd1306Cmnds.PageAddress.Page1, Ssd1306Cmnds.PageAddress.Page7));
                ClearScreen();
            }
    
  • 描画処理

    Ssd13xx.cs
          public override void DrawBitmap(BitmapImage image)
          {
              if (!CanConvertFromPixelFormat(image.PixelFormat))
              {
                  throw new InvalidOperationException($"{image.PixelFormat} is not a supported pixel format");
              }
    
              SetStartAddress();
    
              int width = ScreenWidth;
    -         Int16 pages = 4;
    +         // 128x64 の場合、8ページ分の描画が必要
    +         Int16 pages = 8;
              List<byte> buffer = new();
    

整理したら、どこかでPull Requestを投げたい✊。

意図した動作のコード

  • 公式レポジトリにサンプルをもとにコードを作成しました

  • Program側

    Program.cs
    using System.Device.I2c;
    using Iot.Device.Graphics;
    using Iot.Device.Graphics.SkiaSharpAdapter;
    using System.Threading;
    using CustomDevices;
    
    
    namespace Ssd1306Tutorial
    {
        class Program
        {
            static void Main(string[] args)
            {
                SkiaSharpAdapter.Register();
    
                // Set up the I2C bus
                I2cConnectionSettings connectionSettings = new(1, 0x3C);
                var i2cDevice = I2cDevice.Create(connectionSettings);
    
                // MyCustomSsd1306を使用して、ディスプレイを初期化
                var device = new MyCustomSsd1306(i2cDevice, 128, 64);
                device.CustomInitialize();
    
                device.ClearScreen();
                Console.WriteLine("Display Images");
    
                // 画像を表示
                for (var i = 0; i < 1000; i++)
                {
                    foreach (var image_name in Directory.GetFiles("images", "*.bmp").OrderBy(f => f))
                    {
                        using BitmapImage image = BitmapImage.CreateFromFile(image_name);
                        device.DrawBitmap(image);
                        Thread.Sleep(10);
                    }
                }
    
                device.ClearScreen();
                device.Dispose();
            }
        }
    }
    
  • 128x64ドットの対応Ssd1306クラス

    MyCustomSsd1306.cs
    using System.Device.I2c;
    using Iot.Device.Graphics;
    using Iot.Device.Graphics.SkiaSharpAdapter;
    using Iot.Device.Ssd13xx;
    using Iot.Device.Ssd13xx.Commands;
    using Ssd1306Cmnds = Iot.Device.Ssd13xx.Commands.Ssd1306Commands;
    using System.Threading;
    
    
    namespace CustomDevices
    {
        public class MyCustomSsd1306 : Ssd1306
        {
            public MyCustomSsd1306(I2cDevice i2cDevice, int width, int height)
                : base(i2cDevice, width, height)
            {
            }
    
            // カスタムの初期化方法を定義したい場合
            public void CustomInitialize()
            {
                SendCommand(new SetMultiplexRatio(0x3F)); // 128x64用
                SendCommand(new Ssd1306Cmnds.SetComPinsHardwareConfiguration(true, false)); // 128x64用
                SendCommand(new Ssd1306Cmnds.SetPageAddress(Ssd1306Cmnds.PageAddress.Page0, Ssd1306Cmnds.PageAddress.Page7)); // 全ページを設定
            }
    
            protected override void SetStartAddress()
            {
                SendCommand(new Ssd1306Cmnds.SetColumnAddress(0, 127)); // 全カラムを設定
                SendCommand(new Ssd1306Cmnds.SetPageAddress(Ssd1306Cmnds.PageAddress.Page0,
                    Ssd1306Cmnds.PageAddress.Page7));
            }
    
            public override void DrawBitmap(BitmapImage image)
            {
                if (!CanConvertFromPixelFormat(image.PixelFormat))
                {
                    throw new InvalidOperationException($"{image.PixelFormat} is not a supported pixel format");
                }
    
                SetStartAddress();
    
                int width = ScreenWidth;
                Int16 pages = 8;
                List<byte> buffer = new();
    
                for (int page = 0; page < pages; page++)
                {
                    for (int x = 0; x < width; x++)
                    {
                        int bits = 0;
                        for (byte bit = 0; bit < 8; bit++)
                        {
                            bits = bits << 1;
                            bits |= image[x, page * 8 + 7 - bit].GetBrightness() > BrightnessThreshold ? 1 : 0;
                        }
    
                        buffer.Add((byte)bits);
                    }
                }
    
                int chunk_size = 16;
                for (int i = 0; i < buffer.Count; i += chunk_size)
                {
                    SendData(buffer.Skip(i).Take(chunk_size).ToArray());
                }
            }
        }
    }
    

記事冒頭のアニメーションが表示されました🎉

さいごに

最初はデバイス側の故障かと思ったので、原因をつかむのに時間がかかりました。

PythonのSsd1306モジュールと比較したりして、解決はできました。

Iot.Deviceと言いながら、ネットワークにつながっていないので、

今後は、簡単なアプリケーションでも作ってみようと思います。

GitHubで編集を提案
株式会社ジード テックブログ

Discussion