diff --git a/src/Iot.Device.Bindings/CompatibilitySuppressions.xml b/src/Iot.Device.Bindings/CompatibilitySuppressions.xml
index 1c4c65ce8c..36073fb0ce 100644
--- a/src/Iot.Device.Bindings/CompatibilitySuppressions.xml
+++ b/src/Iot.Device.Bindings/CompatibilitySuppressions.xml
@@ -78,6 +78,13 @@
lib/net6.0/Iot.Device.Bindings.dll
true
+
+ CP0002
+ F:Iot.Device.Ssd13xx.Ssd13xx._i2cDevice
+ lib/net6.0/Iot.Device.Bindings.dll
+ lib/net6.0/Iot.Device.Bindings.dll
+ true
+
CP0002
M:Iot.Device.Axp192.Axp192.SetGPIO0(Iot.Device.Axp192.Gpio0Behavior,System.Byte)
@@ -225,6 +232,13 @@
lib/netstandard2.0/Iot.Device.Bindings.dll
true
+
+ CP0002
+ F:Iot.Device.Ssd13xx.Ssd13xx._i2cDevice
+ lib/netstandard2.0/Iot.Device.Bindings.dll
+ lib/netstandard2.0/Iot.Device.Bindings.dll
+ true
+
CP0002
M:Iot.Device.Axp192.Axp192.SetGPIO0(Iot.Device.Axp192.Gpio0Behavior,System.Byte)
diff --git a/src/devices/Ssd13xx/Commands/ISsd1309Command.cs b/src/devices/Ssd13xx/Commands/ISsd1309Command.cs
new file mode 100644
index 0000000000..f983a58ac0
--- /dev/null
+++ b/src/devices/Ssd13xx/Commands/ISsd1309Command.cs
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Iot.Device.Ssd13xx.Commands
+{
+ ///
+ /// Interface for all Ssd1309 commands
+ ///
+ public interface ISsd1309Command : ICommand
+ {
+ }
+}
diff --git a/src/devices/Ssd13xx/Ssd1306.cs b/src/devices/Ssd13xx/Ssd1306.cs
index 54dfc5252e..45180ec37c 100644
--- a/src/devices/Ssd13xx/Ssd1306.cs
+++ b/src/devices/Ssd13xx/Ssd1306.cs
@@ -60,7 +60,7 @@ private void SendCommand(ICommand command)
// Be aware there is a Continuation Bit in the Control byte and can be used
// to state (logic LOW) if there is only data bytes to follow.
// This binding separates commands and data by using SendCommand and SendData.
- _i2cDevice.Write(writeBuffer);
+ I2cDevice?.Write(writeBuffer);
}
// Display size 128x32 or 128x64
diff --git a/src/devices/Ssd13xx/Ssd1309.cs b/src/devices/Ssd13xx/Ssd1309.cs
new file mode 100644
index 0000000000..82dec53b30
--- /dev/null
+++ b/src/devices/Ssd13xx/Ssd1309.cs
@@ -0,0 +1,227 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Device.Gpio;
+using System.Device.Spi;
+using System.Linq;
+using System.Threading;
+using Iot.Device.Graphics;
+using Iot.Device.Ssd13xx.Commands;
+using Iot.Device.Ssd13xx.Commands.Ssd1306Commands;
+
+namespace Iot.Device.Ssd13xx
+{
+ ///
+ /// A single-chip CMOS OLED/PLED driver with controller for organic/polymer
+ /// light emitting diode dot-matrix graphic display system.
+ ///
+ public class Ssd1309 : Ssd13xx
+ {
+ private readonly GpioPin _csGpioPin;
+ private readonly GpioPin _dcGpioPin;
+ private readonly GpioPin _rstGpioPin;
+
+ private GpioController _gpioController;
+
+ ///
+ /// Initializes new instance of Ssd13069 device that will communicate using SPI
+ /// in a non-traditional "4-wire SPI" mode that uses DC and RST GPIO pins to switch
+ /// between Data/Command instructions and reset behavior.
+ /// A single-chip CMOS OLED/PLED driver with controller for organic/polymer
+ /// light emitting diode dot-matrix graphic display system.
+ ///
+ /// The SPI device used for communication.
+ /// Width of the display. Typically 128 pixels
+ /// Height of the display
+ /// Instance of the boards GpioController
+ /// GPIO pin for chip-select. Active state is LOW. Does not guarantee support for multiple SPI bus devices.
+ /// GPIO pin for Data/Command control
+ /// GPIO pin for Reset behavior
+ public Ssd1309(SpiDevice spiDevice, GpioController gpioController, int csGpioPin, int dcGpioPin, int rstGpioPin, int width, int height)
+ : base(spiDevice, width, height)
+ {
+ _gpioController = gpioController;
+
+ _csGpioPin = _gpioController.OpenPin(csGpioPin, PinMode.Output, PinValue.High);
+ _dcGpioPin = _gpioController.OpenPin(dcGpioPin, PinMode.Output, PinValue.Low);
+ _rstGpioPin = _gpioController.OpenPin(rstGpioPin, PinMode.Output, PinValue.High);
+
+ Reset();
+ Initialize();
+ }
+
+ ///
+ /// Sends command to the device
+ ///
+ /// Command being sent
+ public virtual void SendCommand(ISsd1309Command command) => SendCommand((ICommand)command);
+
+ ///
+ /// Sends command to the device
+ ///
+ /// Command being sent
+ public override void SendCommand(ISharedCommand command) => SendCommand(command);
+
+ ///
+ /// Send a command to the display controller.
+ ///
+ /// The command to send to the display controller.
+ private void SendCommand(ICommand command)
+ {
+ Span commandBytes = command?.GetBytes();
+
+ if (commandBytes is not { Length: >0 })
+ {
+ throw new ArgumentNullException(nameof(command), "Argument is either null or there were no bytes to send.");
+ }
+
+ // TODO: Is this needed at all if data/command is controlled via GPIO instead of a control byte?
+ Span writeBuffer = SliceGenericBuffer(commandBytes.Length);
+
+ commandBytes.CopyTo(writeBuffer.Slice(0));
+
+ if (SpiDevice != null)
+ {
+ // Begin consuming SPI bus data
+ // Because the timing is not perfect, this may have adverse side-effects when using multiple SPI bus devices
+ _csGpioPin.Write(PinValue.Low);
+
+ // Enable command mode
+ _dcGpioPin.Write(PinValue.Low);
+ SpiDevice.Write(writeBuffer);
+
+ // Stop consuming SPI bus data
+ _csGpioPin.Write(PinValue.High);
+ }
+ else
+ {
+ throw new InvalidOperationException("No SPI device available or it has been disposed.");
+ }
+ }
+
+ ///
+ /// Sends data to the device
+ ///
+ /// Data being sent
+ public override void SendData(Span data)
+ {
+ if (data.IsEmpty)
+ {
+ throw new ArgumentNullException(nameof(data));
+ }
+
+ if (SpiDevice != null)
+ {
+ // Begin consuming SPI bus data
+ // Because the timing is not perfect, this may have adverse side-effects when using multiple SPI bus devices
+ _csGpioPin.Write(PinValue.Low);
+
+ // Enable data mode
+ _dcGpioPin.Write(PinValue.High);
+ SpiDevice.Write(data);
+
+ // Stop consuming SPI bus data
+ _csGpioPin.Write(PinValue.High);
+ }
+ else
+ {
+ throw new InvalidOperationException("No SPI device available or it has been disposed.");
+ }
+ }
+
+ ///
+ /// Init 128x64
+ ///
+ protected virtual void Initialize()
+ {
+ SendCommand(new SetDisplayOff());
+ SendCommand(new SetDisplayClockDivideRatioOscillatorFrequency());
+ SendCommand(new SetMultiplexRatio());
+ SendCommand(new SetDisplayOffset());
+ SendCommand(new SetDisplayStartLine());
+ SendCommand(new SetMemoryAddressingMode(SetMemoryAddressingMode.AddressingMode.Horizontal));
+ SetStartAddress();
+ SendCommand(new SetSegmentReMap(true));
+ SendCommand(new SetComOutputScanDirection(false));
+ SendCommand(new SetComPinsHardwareConfiguration());
+ SendCommand(new SetContrastControlForBank0());
+ SendCommand(new SetPreChargePeriod());
+ SendCommand(new SetVcomhDeselectLevel());
+ SendCommand(new EntireDisplayOn(false));
+ SendCommand(new SetNormalDisplay());
+ SendCommand(new SetDisplayOn());
+ Thread.Sleep(200);
+ ClearScreen();
+ }
+
+ ///
+ /// Set the start address for the display
+ ///
+ protected override void SetStartAddress()
+ {
+ SendCommand(new SetColumnAddress());
+
+ if (ScreenHeight == 32)
+ {
+ SendCommand(new SetPageAddress(PageAddress.Page0, PageAddress.Page3));
+ }
+ else
+ {
+ SendCommand(new SetPageAddress());
+ }
+ }
+
+ ///
+ /// Reset device by cycling RST pin to LOW and resting at HIGH.
+ /// This will not re-initialize the device.
+ ///
+ protected virtual void Reset()
+ {
+ _rstGpioPin.Write(PinValue.Low);
+ Thread.Sleep(100);
+ _rstGpioPin.Write(PinValue.High);
+ Thread.Sleep(100);
+ }
+
+ ///
+ /// Sends the image to the display
+ ///
+ /// Image to send to display
+ 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;
+ int pages = image.Height / 8;
+ List 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());
+ }
+ }
+ }
+}
diff --git a/src/devices/Ssd13xx/Ssd1327.cs b/src/devices/Ssd13xx/Ssd1327.cs
index fab557e4ec..ae237b71b5 100644
--- a/src/devices/Ssd13xx/Ssd1327.cs
+++ b/src/devices/Ssd13xx/Ssd1327.cs
@@ -95,8 +95,7 @@ private void Initialize()
public void SendCommand(byte command)
{
Span writeBuffer = new byte[] { Command_Mode, command };
-
- _i2cDevice.Write(writeBuffer);
+ I2cDevice?.Write(writeBuffer);
}
///
diff --git a/src/devices/Ssd13xx/Ssd13xx.cs b/src/devices/Ssd13xx/Ssd13xx.cs
index 1105e923d6..1d961f5fcb 100644
--- a/src/devices/Ssd13xx/Ssd13xx.cs
+++ b/src/devices/Ssd13xx/Ssd13xx.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Device.I2c;
+using System.Device.Spi;
using System.Linq;
using Iot.Device.Graphics;
using Iot.Device.Ssd13xx.Commands;
@@ -23,10 +24,15 @@ public abstract class Ssd13xx : GraphicDisplay
///
/// Underlying I2C device
///
- protected I2cDevice _i2cDevice;
+ protected I2cDevice? I2cDevice { get; set; }
///
- /// Constructs instance of Ssd13xx
+ /// Underlying SPI device
+ ///
+ protected SpiDevice? SpiDevice { get; set; }
+
+ ///
+ /// Constructs instance of Ssd13xx using an I2C device
///
/// I2C device used to communicate with the device
/// Width of the display, in pixels
@@ -37,7 +43,22 @@ protected Ssd13xx(I2cDevice i2cDevice, int width, int height)
ScreenHeight = height;
ScreenWidth = width;
BrightnessThreshold = DefaultThreshold;
- _i2cDevice = i2cDevice ?? throw new ArgumentNullException(nameof(i2cDevice));
+ I2cDevice = i2cDevice ?? throw new ArgumentNullException(nameof(i2cDevice));
+ }
+
+ ///
+ /// Constructs instance of Ssd13xx using an SPI device
+ ///
+ /// SPI device used to communicate with the device
+ /// Width of the display, in pixels
+ /// Height of the display, in pixels
+ protected Ssd13xx(SpiDevice spiDevice, int width, int height)
+ {
+ _genericBuffer = new byte[DefaultBufferSize];
+ ScreenHeight = height;
+ ScreenWidth = width;
+ BrightnessThreshold = DefaultThreshold;
+ SpiDevice = spiDevice ?? throw new ArgumentNullException(nameof(spiDevice));
}
///
@@ -107,7 +128,19 @@ public virtual void SendData(Span data)
writeBuffer[0] = 0x40; // Control byte.
data.CopyTo(writeBuffer.Slice(1));
- _i2cDevice.Write(writeBuffer);
+
+ if (I2cDevice != null)
+ {
+ I2cDevice.Write(writeBuffer);
+ }
+ else if (SpiDevice != null)
+ {
+ SpiDevice.Write(writeBuffer);
+ }
+ else
+ {
+ throw new InvalidOperationException("No I2C/SPI device available or it has been disposed.");
+ }
}
///
@@ -115,8 +148,11 @@ protected override void Dispose(bool disposing)
{
if (disposing)
{
- _i2cDevice?.Dispose();
- _i2cDevice = null!;
+ I2cDevice?.Dispose();
+ I2cDevice = null!;
+
+ SpiDevice?.Dispose();
+ SpiDevice = null!;
}
}
@@ -206,5 +242,38 @@ public override void DrawBitmap(BitmapImage image)
SendData(buffer.Skip(i).Take(chunk_size).ToArray());
}
}
+
+ ///
+ /// Returns the display-ready span of bytes for a bitmap without sending the data to the display
+ ///
+ /// Image to render
+ public virtual byte[] PreRenderBitmap(BitmapImage image)
+ {
+ if (!CanConvertFromPixelFormat(image.PixelFormat))
+ {
+ throw new InvalidOperationException($"{image.PixelFormat} is not a supported pixel format");
+ }
+
+ int width = ScreenWidth;
+ int pages = image.Height / 8;
+ List 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);
+ }
+ }
+
+ return buffer.ToArray();
+ }
}
}
diff --git a/src/devices/Ssd13xx/Ssd13xx.csproj b/src/devices/Ssd13xx/Ssd13xx.csproj
index d52aaa67c5..1122fff0f3 100644
--- a/src/devices/Ssd13xx/Ssd13xx.csproj
+++ b/src/devices/Ssd13xx/Ssd13xx.csproj
@@ -8,6 +8,7 @@
+
diff --git a/src/devices/Ssd13xx/Ssd13xx.sln b/src/devices/Ssd13xx/Ssd13xx.sln
index 27d2007b0a..dc55226559 100644
--- a/src/devices/Ssd13xx/Ssd13xx.sln
+++ b/src/devices/Ssd13xx/Ssd13xx.sln
@@ -9,8 +9,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ssd13xx.Tests", "tests\Ssd1
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DA6C1243-517A-4827-A7B0-05DDCEFA88A5}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ssd13xx.Samples", "samples\Ssd13xx.Samples.csproj", "{26A1466F-AAD0-4DCF-AB48-061ED86EA645}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ssd13xx", "Ssd13xx.csproj", "{CDC920A8-123F-432D-834E-49BAB00191E7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arduino", "..\Arduino\Arduino.csproj", "{9531CD82-A4B7-4F7A-A220-E2E004A6F0E9}"
@@ -19,6 +17,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharpAdapter", "..\Skia
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "..\Common\Common.csproj", "{F067B019-474D-442E-B7A8-A87F59B46F41}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "i2c", "i2c", "{D1B22546-FB0C-498F-B637-449A3F9DC347}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "spi", "spi", "{C7302976-43EC-49DE-88F9-F2227C7F18B9}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ssd13xx.Samples", "samples\i2c\Ssd13xx.Samples\Ssd13xx.Samples.csproj", "{1BE0775E-A211-4D00-9016-C2825B065519}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ssd1309.Spi.Samples", "samples\spi\Ssd1309.Spi.Samples\Ssd1309.Spi.Samples.csproj", "{FF639AE0-9057-4FC5-AF8B-956368B20EBE}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -41,18 +47,6 @@ Global
{0C3BE886-C62F-41CA-BD18-1CECFE591589}.Release|x64.Build.0 = Release|Any CPU
{0C3BE886-C62F-41CA-BD18-1CECFE591589}.Release|x86.ActiveCfg = Release|Any CPU
{0C3BE886-C62F-41CA-BD18-1CECFE591589}.Release|x86.Build.0 = Release|Any CPU
- {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Debug|x64.ActiveCfg = Debug|Any CPU
- {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Debug|x64.Build.0 = Debug|Any CPU
- {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Debug|x86.ActiveCfg = Debug|Any CPU
- {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Debug|x86.Build.0 = Debug|Any CPU
- {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Release|Any CPU.Build.0 = Release|Any CPU
- {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Release|x64.ActiveCfg = Release|Any CPU
- {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Release|x64.Build.0 = Release|Any CPU
- {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Release|x86.ActiveCfg = Release|Any CPU
- {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Release|x86.Build.0 = Release|Any CPU
{CDC920A8-123F-432D-834E-49BAB00191E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CDC920A8-123F-432D-834E-49BAB00191E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CDC920A8-123F-432D-834E-49BAB00191E7}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -101,13 +95,40 @@ Global
{F067B019-474D-442E-B7A8-A87F59B46F41}.Release|x64.Build.0 = Release|Any CPU
{F067B019-474D-442E-B7A8-A87F59B46F41}.Release|x86.ActiveCfg = Release|Any CPU
{F067B019-474D-442E-B7A8-A87F59B46F41}.Release|x86.Build.0 = Release|Any CPU
+ {1BE0775E-A211-4D00-9016-C2825B065519}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1BE0775E-A211-4D00-9016-C2825B065519}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1BE0775E-A211-4D00-9016-C2825B065519}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1BE0775E-A211-4D00-9016-C2825B065519}.Debug|x64.Build.0 = Debug|Any CPU
+ {1BE0775E-A211-4D00-9016-C2825B065519}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1BE0775E-A211-4D00-9016-C2825B065519}.Debug|x86.Build.0 = Debug|Any CPU
+ {1BE0775E-A211-4D00-9016-C2825B065519}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1BE0775E-A211-4D00-9016-C2825B065519}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1BE0775E-A211-4D00-9016-C2825B065519}.Release|x64.ActiveCfg = Release|Any CPU
+ {1BE0775E-A211-4D00-9016-C2825B065519}.Release|x64.Build.0 = Release|Any CPU
+ {1BE0775E-A211-4D00-9016-C2825B065519}.Release|x86.ActiveCfg = Release|Any CPU
+ {1BE0775E-A211-4D00-9016-C2825B065519}.Release|x86.Build.0 = Release|Any CPU
+ {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Debug|x64.Build.0 = Debug|Any CPU
+ {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Debug|x86.Build.0 = Debug|Any CPU
+ {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Release|x64.ActiveCfg = Release|Any CPU
+ {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Release|x64.Build.0 = Release|Any CPU
+ {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Release|x86.ActiveCfg = Release|Any CPU
+ {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0C3BE886-C62F-41CA-BD18-1CECFE591589} = {59ED0E9C-95C8-469A-9EA8-0BC0624806A9}
- {26A1466F-AAD0-4DCF-AB48-061ED86EA645} = {DA6C1243-517A-4827-A7B0-05DDCEFA88A5}
+ {D1B22546-FB0C-498F-B637-449A3F9DC347} = {DA6C1243-517A-4827-A7B0-05DDCEFA88A5}
+ {C7302976-43EC-49DE-88F9-F2227C7F18B9} = {DA6C1243-517A-4827-A7B0-05DDCEFA88A5}
+ {1BE0775E-A211-4D00-9016-C2825B065519} = {D1B22546-FB0C-498F-B637-449A3F9DC347}
+ {FF639AE0-9057-4FC5-AF8B-956368B20EBE} = {C7302976-43EC-49DE-88F9-F2227C7F18B9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4AE79F3F-25E2-4C61-BE07-B2D2169C7000}
diff --git a/src/devices/Ssd13xx/samples/Program.cs b/src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/Program.cs
similarity index 100%
rename from src/devices/Ssd13xx/samples/Program.cs
rename to src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/Program.cs
diff --git a/src/devices/Ssd13xx/samples/Ssd13xx.Samples.csproj b/src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/Ssd13xx.Samples.csproj
similarity index 61%
rename from src/devices/Ssd13xx/samples/Ssd13xx.Samples.csproj
rename to src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/Ssd13xx.Samples.csproj
index 17d6950378..0814656b87 100644
--- a/src/devices/Ssd13xx/samples/Ssd13xx.Samples.csproj
+++ b/src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/Ssd13xx.Samples.csproj
@@ -5,9 +5,9 @@
-
-
-
+
+
+
diff --git a/src/devices/Ssd13xx/samples/images/dotnet-bot.bmp b/src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/images/dotnet-bot.bmp
similarity index 100%
rename from src/devices/Ssd13xx/samples/images/dotnet-bot.bmp
rename to src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/images/dotnet-bot.bmp
diff --git a/src/devices/Ssd13xx/samples/images/github-dotnet-iot-black.bmp b/src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/images/github-dotnet-iot-black.bmp
similarity index 100%
rename from src/devices/Ssd13xx/samples/images/github-dotnet-iot-black.bmp
rename to src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/images/github-dotnet-iot-black.bmp
diff --git a/src/devices/Ssd13xx/samples/images/github-dotnet-iot-white.bmp b/src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/images/github-dotnet-iot-white.bmp
similarity index 100%
rename from src/devices/Ssd13xx/samples/images/github-dotnet-iot-white.bmp
rename to src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/images/github-dotnet-iot-white.bmp
diff --git a/src/devices/Ssd13xx/samples/images/shapes.bmp b/src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/images/shapes.bmp
similarity index 100%
rename from src/devices/Ssd13xx/samples/images/shapes.bmp
rename to src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/images/shapes.bmp
diff --git a/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Program.cs b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Program.cs
new file mode 100644
index 0000000000..a55f3b1008
--- /dev/null
+++ b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Program.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Device.Spi;
+using System.Threading;
+using Iot.Device.Board;
+using Iot.Device.Graphics.SkiaSharpAdapter;
+using Iot.Device.Ssd13xx;
+using Ssd13xx.Samples.Simulations;
+
+SkiaSharpAdapter.Register();
+
+var spiSettings = new SpiConnectionSettings(0)
+{
+ ClockFrequency = 8_000_000
+};
+
+var board = new RaspberryPiBoard();
+var spiDevice = board.CreateSpiDevice(spiSettings);
+var gpioController = board.CreateGpioController();
+
+var display = new Ssd1309(spiDevice, gpioController, csGpioPin: 8, dcGpioPin: 25, rstGpioPin: 27, width: 128, height: 64);
+
+var simulation = new FallingSandSimulation(display, fps: 30, debug: true);
+await simulation.StartAsync(iterations: 5000, initialDelayMs: 500);
+
+display.ClearScreen();
+
+gpioController.Dispose();
+spiDevice.Dispose();
+display.Dispose();
+board.Dispose();
diff --git a/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Scripts/deployToPi.ps1 b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Scripts/deployToPi.ps1
new file mode 100644
index 0000000000..a7fd3001f0
--- /dev/null
+++ b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Scripts/deployToPi.ps1
@@ -0,0 +1,26 @@
+# Licensed to the .NET Foundation under one or more agreements.
+# The .NET Foundation licenses this file to you under the MIT license.
+
+# Uses SCP to copy local output files to a RaspberryPi on the same network
+# This assumes development on a Windows machine. For Linux, consider rsync instead.
+# Replace the variables below with your desired values:
+
+$PiUser = "pi"
+$PiHostnameOrIp = "pi"
+$TargetRuntime = "linux-arm64"
+$PublishDirectory = "C:\ssd1309Sample"
+$RemoteDirectory = "/home/${PiUser}/ssd1309sample"
+
+# Publish
+dotnet publish "../Ssd1309.Spi.Samples.csproj" --self-contained -c Debug -o $PublishDirectory -r $TargetRuntime
+
+# Deploy
+if ($?)
+{
+ Write-Host "Deploying files from ${PublishDirectory} to ${PiUser}@${PiHostnameOrIp}"
+ scp -r $PublishDirectory ${PiUser}@${PiHostnameOrIp}:${RemoteDirectory}
+}
+else
+{
+ Write-Host "Publish not successful. Skipping deployment."
+}
diff --git a/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Simulations/FallingSandSimulation.cs b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Simulations/FallingSandSimulation.cs
new file mode 100644
index 0000000000..e75c98249f
--- /dev/null
+++ b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Simulations/FallingSandSimulation.cs
@@ -0,0 +1,118 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Drawing;
+using Iot.Device.Graphics.SkiaSharpAdapter;
+using Iot.Device.Ssd13xx;
+using SkiaSharp;
+
+namespace Ssd13xx.Samples.Simulations
+{
+ ///
+ /// A demo simulation of falling sand for SSD1309 displays
+ ///
+ public class FallingSandSimulation : Ssd1309Simulation
+ {
+ private readonly Random _random;
+ private readonly int _grainsPerPour;
+
+ private int _grainsPoured;
+ private int _pourColumn;
+
+ private SKCanvas _canvas;
+
+ ///
+ /// A demo simulation of falling sand for SSD1309 displays
+ ///
+ /// Ssd1309 display device
+ /// Frames-per-second of the simulation
+ /// Number of grains to pour per column before moving to a random column
+ /// Toggle buffer debug logging on each frame
+ public FallingSandSimulation(Ssd1309 display, int fps = 1, int grainsPerPour = 20, bool debug = false)
+ : base(display, fps, debug: debug)
+ {
+ _grainsPerPour = grainsPerPour;
+ _random = new Random();
+ _pourColumn = _random.Next(_display.ScreenWidth);
+ var drawingApi = _renderState.GetDrawingApi();
+ _canvas = drawingApi.GetCanvas();
+ drawingApi.DrawText("Hello Sand!", "DejaVu Sans", 18, Color.White, new Point(15, 0));
+ }
+
+ ///
+ /// Generates the next frame of the falling sand simulation.
+ ///
+ protected override void Update()
+ {
+ if (_grainsPoured < _grainsPerPour)
+ {
+ // Pour a single grain of sand in the current pour position
+ SetPixel(_pourColumn, 0, SKColors.White);
+ _grainsPoured++;
+ }
+ else
+ {
+ // Move the pour position to a random column
+ _pourColumn = _random.Next(_display.ScreenWidth);
+ _grainsPoured = 0;
+ }
+
+ // Iterate starting at the bottom and working up
+ for (int y = _display.ScreenHeight - 1; y >= 0; y--)
+ {
+ for (int x = 0; x < _display.ScreenWidth; x++)
+ {
+ // If current pixel is sand, take action
+ if (GetPixelWithinBounds(x, y).Equals(SKColors.White))
+ {
+ // Move sand down if space is empty
+ if (GetPixelWithinBounds(x, y + 1).Equals(SKColors.Black))
+ {
+ SetPixel(x, y, SKColors.Black);
+ SetPixel(x, y + 1, SKColors.White);
+ }
+
+ // Move sand down-left if space is empty
+ else if (GetPixelWithinBounds(x - 1, y + 1).Equals(SKColors.Black))
+ {
+ SetPixel(x, y, SKColors.Black);
+ SetPixel(x - 1, y + 1, SKColors.White);
+ }
+
+ // Move sand down-right if space is empty
+ else if (GetPixelWithinBounds(x + 1, y + 1).Equals(SKColors.Black))
+ {
+ SetPixel(x, y, SKColors.Black);
+ SetPixel(x + 1, y + 1, SKColors.White);
+ }
+ }
+ }
+ }
+ }
+
+ private void SetPixel(int x, int y, SKColor color)
+ {
+ _canvas.DrawPoint(new SKPoint(x, y), color);
+ }
+
+ /// Null if the pixel is outside the bounds of the display
+ private SKColor? GetPixelWithinBounds(int x, int y)
+ {
+ if (x >= 0 && x < _display.ScreenWidth - 1)
+ {
+ if (y >= 0 && y < _display.ScreenHeight - 1)
+ {
+ return ConvertColor(_renderState.GetPixel(x, y));
+ }
+ }
+
+ return null;
+ }
+
+ private SKColor ConvertColor(Color c)
+ {
+ return new SKColor(c.R, c.G, c.B, c.A);
+ }
+ }
+}
diff --git a/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Simulations/Ssd1309Simulation.cs b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Simulations/Ssd1309Simulation.cs
new file mode 100644
index 0000000000..2a1316d4cc
--- /dev/null
+++ b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Simulations/Ssd1309Simulation.cs
@@ -0,0 +1,250 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Threading;
+using System;
+using System.Threading.Tasks;
+using System.Collections.Concurrent;
+using Iot.Device.Ssd13xx;
+using Iot.Device.Graphics;
+
+namespace Ssd13xx.Samples.Simulations
+{
+ ///
+ /// Base class for rendering simulations on SSD1309 displays
+ ///
+ public abstract class Ssd1309Simulation
+ {
+ ///
+ /// Optional value to enable debug logging
+ ///
+ protected readonly bool _debug;
+
+ ///
+ /// Instance of the Ssd1309 display to display the simulation
+ ///
+ protected readonly Ssd1309 _display;
+
+ ///
+ /// Number of display-ready frames to store in the buffer
+ ///
+ protected readonly int _frameBufferSize;
+
+ ///
+ /// Frames-per-second sent to the display
+ ///
+ protected readonly int _fps;
+
+ ///
+ /// Frame buffer for display-ready frames
+ ///
+ protected ConcurrentQueue _frameBuffer;
+
+ ///
+ /// Stores the current render state
+ ///
+ protected BitmapImage _renderState;
+
+ ///
+ /// Semaphore to limit the number of concurent threads that can access rendering resources
+ ///
+ protected SemaphoreSlim _bufferSemaphore;
+
+ ///
+ /// Semaphore to limit the number of concurrent threads that can send data to the display
+ ///
+ protected SemaphoreSlim _displaySemaphore;
+
+ ///
+ /// Start time of the simulation
+ ///
+ protected DateTime _startTime;
+
+ ///
+ /// Max frames of the simulation to run before stopping
+ ///
+ protected int _maxFrames = 0;
+
+ ///
+ /// Number of frames sent to the display
+ ///
+ protected int _framesSent = 0;
+
+ ///
+ /// Backing field. True if simulation is running
+ ///
+ protected bool _isRunning = false;
+
+ ///
+ /// True if simulation is running
+ ///
+ public bool IsRunning => IsRunning;
+
+ ///
+ /// Backing field. True if simulation has completed
+ ///
+ protected bool _isCompleted = false;
+
+ ///
+ /// True if simulation has completed
+ ///
+ public bool IsCompleted => _isCompleted;
+
+ ///
+ /// Base class for rendering simulations on SSD1309 displays
+ ///
+ /// Instance of a SSD1309 display
+ /// Frames-per-second to render the simulation
+ /// Number of device-ready frames to store in the buffer
+ /// Optional value to enable debug logging
+ public Ssd1309Simulation(Ssd1309 display, int fps = 30, int frameBufferSize = 30, bool debug = false)
+ {
+ _display = display;
+ _fps = fps;
+ _frameBufferSize = frameBufferSize;
+ _debug = debug;
+ _frameBuffer = new ConcurrentQueue();
+ _bufferSemaphore = new SemaphoreSlim(1);
+ _displaySemaphore = new SemaphoreSlim(1);
+ _renderState = _display.GetBackBufferCompatibleImage();
+ }
+
+ ///
+ /// Starts the simulation
+ ///
+ /// Number of frames to simulate before completing
+ /// Initial delay to buffer frames. If unspecified, delay will be a factor of FPS and buffer size.
+ public async virtual Task StartAsync(int iterations, int? initialDelayMs = null)
+ {
+ _startTime = DateTime.UtcNow;
+ _maxFrames += iterations;
+
+ _isRunning = true;
+
+ var displayInterval = 1000 / _fps;
+
+ // Assumes the buffer needs to be topped off when its 50% empty
+ var bufferInterval = displayInterval * (_frameBufferSize / 2);
+
+ if (initialDelayMs == null)
+ {
+ initialDelayMs = bufferInterval * 2;
+ }
+
+ // Sends the next frame in the simulation to the device after an initial delay
+ var displayTimer = new Timer(async (state) => await SendNextFrame(), null, initialDelayMs.Value, displayInterval);
+
+ // Populates buffer immediatley and upon every bufferInterval thereafter
+ var bufferTimer = new Timer(async (state) => await PopulateBufferAsync(), null, 0, bufferInterval);
+
+ Console.WriteLine("Running simulation. Press any key to stop...");
+
+ while (!Console.KeyAvailable)
+ {
+ if (_framesSent >= iterations)
+ {
+ Stop();
+ _isCompleted = true;
+
+ Console.WriteLine("Simulation completed");
+ break;
+ }
+
+ await Task.Delay(bufferInterval);
+ }
+
+ if (_isRunning)
+ {
+ Console.ReadKey();
+ Stop();
+ }
+ }
+
+ ///
+ /// Stops the simulation
+ ///
+ public virtual void Stop()
+ {
+ _isRunning = false;
+
+ Console.WriteLine("Simulation stopped");
+ }
+
+ ///
+ /// Timer delegate that dequeues the next available frame from the buffer and sends to the display
+ ///
+ protected async virtual Task SendNextFrame()
+ {
+ if (_isRunning)
+ {
+ // Ensure only one thread can send data to the display at a time. If busy, skip.
+ if (await _displaySemaphore.WaitAsync(0))
+ {
+ if (_frameBuffer.TryDequeue(out var nextFrame))
+ {
+ _display.SendData(nextFrame);
+ _framesSent++;
+ }
+ else
+ {
+ if (_debug)
+ {
+ // This indicates that the buffer cannot be populated as fast as the display is attemting to send frames.
+ // Performance improvements could be achieved by optimizing the Update method or the simulation pattern.
+ // One obvious improvement would be to use byte arrays for render state instead of drawing to a bitmap,
+ // but that comes with development tradeoffs for scenarios like drawing text and shapes.
+ Console.WriteLine($"Frame skipped, buffer is empty. Actual FPS: {GetActualFps()}");
+ }
+ }
+
+ _displaySemaphore.Release();
+ }
+ else
+ {
+ if (_debug)
+ {
+ // This indicates a bottleneck with the device itself. It may be possible to configure the display
+ // for better performance such as increasing clock speed. Otherwise consider reducing FPS.
+ Console.WriteLine($"Frame skipped, previous frame is still being sent. Actual FPS: {GetActualFps()}");
+ }
+ }
+ }
+ }
+
+ ///
+ /// Timer delegate that pre-renders frames and populates the buffer up to the max buffer size
+ ///
+ protected async virtual Task PopulateBufferAsync()
+ {
+ // Ensure only one thread can access rendering resources at a time. If busy, skip.
+ if (_isRunning && _renderState != null && await _bufferSemaphore.WaitAsync(0))
+ {
+ while (_frameBuffer.Count < _frameBufferSize)
+ {
+ // Stores to a buffer of display-ready bytes without sending them to the display.
+ // This compensates for frames that may take longer to render than others by rendering on a separate thread and
+ // allows for fewer skipped frames at higher frame rates.
+ Update();
+ _frameBuffer.Enqueue(_display.PreRenderBitmap(_renderState));
+ }
+
+ _bufferSemaphore.Release();
+ }
+ }
+
+ ///
+ /// Updates the render state for the next step of the simulation and returns a copy of the current state
+ ///
+ protected abstract void Update();
+
+ ///
+ /// Returns the actual FPS expressed as frames sent to the display in the elapsed time
+ ///
+ ///
+ protected virtual int GetActualFps()
+ {
+ var elapsed = DateTime.UtcNow - _startTime;
+ return (int)(_framesSent / elapsed.TotalSeconds);
+ }
+ }
+}
diff --git a/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Ssd1309.Spi.Samples.csproj b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Ssd1309.Spi.Samples.csproj
new file mode 100644
index 0000000000..f2521524fa
--- /dev/null
+++ b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Ssd1309.Spi.Samples.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ $(DefaultSampleTfms)
+ enable
+ enable
+ 10
+
+
+
+
+
+
+
+
+
diff --git a/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/TransparentSsd1309FallingSand.jpeg b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/TransparentSsd1309FallingSand.jpeg
new file mode 100644
index 0000000000..7404d1a973
Binary files /dev/null and b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/TransparentSsd1309FallingSand.jpeg differ