diff --git a/src/devices/Ws28xx/BitmapImageNeo4.cs b/src/devices/Ws28xx/BitmapImageNeo4.cs new file mode 100644 index 0000000000000000000000000000000000000000..9dae3197f65936aa69d9631e8b9fcfc85e1fc11f --- /dev/null +++ b/src/devices/Ws28xx/BitmapImageNeo4.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Drawing; +using Iot.Device.Graphics; + +namespace Iot.Device.Ws28xx +{ + internal class BitmapImageNeo4 : BitmapImage + { + private const int BytesPerComponent = 3; + private const int BytesPerPixel = BytesPerComponent * 4; + + // The Neo Pixels require a 50us delay (all zeros) after. Since Spi freq is not exactly + // as requested 100us is used here with good practical results. 100us @ 2.4Mbps and 8bit + // data means we have to add 30 bytes of zero padding. + private const int ResetDelayInBytes = 30; + + // This field defines the count within the lookup table. The length correlates to the possible values of a single byte. + private const int LookupCount = 256; + + private static readonly byte[] _lookup = new byte[LookupCount * BytesPerComponent]; + + static BitmapImageNeo4() + { + for (int i = 0; i < LookupCount; i++) + { + int data = 0; + for (int j = 7; j >= 0; j--) + { + data = (data << 3) | 0b100 | ((i >> j) << 1) & 2; + } + + _lookup[i * BytesPerComponent + 0] = unchecked((byte)(data >> 16)); + _lookup[i * BytesPerComponent + 1] = unchecked((byte)(data >> 8)); + _lookup[i * BytesPerComponent + 2] = unchecked((byte)(data >> 0)); + } + } + + public BitmapImageNeo4(int width, int height) + : base(new byte[width * height * BytesPerPixel + ResetDelayInBytes], width, height, width * BytesPerPixel) + { + } + + public override void SetPixel(int x, int y, Color c) + { + // Alpha is used as white. + var offset = y * Stride + x * BytesPerPixel; + Data[offset++] = _lookup[c.G * BytesPerComponent + 0]; + Data[offset++] = _lookup[c.G * BytesPerComponent + 1]; + Data[offset++] = _lookup[c.G * BytesPerComponent + 2]; + Data[offset++] = _lookup[c.R * BytesPerComponent + 0]; + Data[offset++] = _lookup[c.R * BytesPerComponent + 1]; + Data[offset++] = _lookup[c.R * BytesPerComponent + 2]; + Data[offset++] = _lookup[c.B * BytesPerComponent + 0]; + Data[offset++] = _lookup[c.B * BytesPerComponent + 1]; + Data[offset++] = _lookup[c.B * BytesPerComponent + 2]; + Data[offset++] = _lookup[c.A * BytesPerComponent + 0]; + Data[offset++] = _lookup[c.A * BytesPerComponent + 1]; + Data[offset++] = _lookup[c.A * BytesPerComponent + 2]; + } + } +} diff --git a/src/devices/Ws28xx/README.md b/src/devices/Ws28xx/README.md index 75fcddbc22b3f5678501b46d1db960ca00a1333b..a4dffcc4ac09d8b6738ff6c6cf79835fdbb97a19 100644 --- a/src/devices/Ws28xx/README.md +++ b/src/devices/Ws28xx/README.md @@ -1,19 +1,20 @@ -# Ws28xx LED drivers +# Ws28xx / SK6812 LED drivers -This binding allows you to update the RGB LEDs on Ws28xx and based strips and matrices. +This binding allows you to update the RGB LEDs on Ws28xx / SK6812 and based strips and matrices. -To see how to use the binding in code, see the [sample](samples/Program.cs). +To see how to use the binding in code, see the [sample](samples/Ws28xx_Samples/Program.cs). ## Documentation * WS2812B: [Datasheet](https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf) * WS2808: [Datasheet](https://datasheetspdf.com/pdf-file/806051/Worldsemi/WS2801/1) +* SK6812: [Datasheet](https://cdn-shop.adafruit.com/product-files/2757/p2757_SK6812RGBW_REV01.pdf) * [Neo pixels guide](https://learn.adafruit.com/adafruit-neopixel-uberguide) * [Neo pixels x8 stick](https://www.adafruit.com/product/1426) ## Board -### Neo pixels +### Neo pixels / SK6812 ![Raspberry Pi Breadboard diagram](rpi-neo-pixels_bb.png) @@ -44,7 +45,7 @@ SpiConnectionSettings settings = new(0, 0) using SpiDevice spi = SpiDevice.Create(settings); Ws28xx neo = new Ws2808(spi, count); -//Ws28xx neo = new Ws2812b(spi, Count); +// Ws28xx neo = new Ws2812b(spi, Count); while (true) { @@ -67,17 +68,27 @@ void Rainbow(Ws28xx neo, int count, int iterations = 1) } ``` +***Note:*** + +Using the SK6812 is almost the same, but the alpha channel of the color is used for the white LED. This means that the predefined color definitions (like ```System.Drawing.Color.Red```) will not work correctly as they have the alpha channel set to 255 (0xFF). That will turn the white LED always on. See the [sample](samples/SK6812_Samples/Programs.cs) for the main differences to the above code. +Because ```System.Drawing.Color``` is a readonly struct, it's not possible to change the any channel directly. In order to correctly set Red, use ```Color.FromArgb(0, 255, 0, 0)```. For setting the white LED, use ```Color.FromArgb(255, 0, 0, 0)```. It's also possible to use an existing definition and remove the white channel like this: + +```csharp +var color = Color.HotPink; +var newColor = Color.FromArgb(0, color.R, color.G, color.B); +``` + ## Binding Notes ### Raspberry Pi setup (/boot/config.txt) -* Make sure spi is enabled +* Make sure SPI is enabled ```text dtparam=spi=on ``` -* Make sure SPI don't change speed fix the core clock: +* To make sure SPI doesn't change speed fix the core clock: ```text core_freq=250 diff --git a/src/devices/Ws28xx/Sk6812.cs b/src/devices/Ws28xx/Sk6812.cs new file mode 100644 index 0000000000000000000000000000000000000000..1e9c848570d9271e07dad2f5a83021f359c74325 --- /dev/null +++ b/src/devices/Ws28xx/Sk6812.cs @@ -0,0 +1,36 @@ +// 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; + +namespace Iot.Device.Ws28xx +{ + /// + /// Represents the SK6812 Driver. + /// + /// + public class Sk6812 : Ws28xx + { + /// + /// Gets the default SPI clock frequency. + /// + /// + /// The default SPI clock frequency used for SK6812-LED-Strips. + /// + /// This frequency defines how fast the data will be put on the line. The SK6812-strip requires a speed of 1.25 kHz. + /// A single byte takes 1.2 µs for the transfer, the value for a single LED of 32 bit amounts to 38.4 µs. + /// + public static int DefaultSpiClockFrequency => 2_400_000; + + /// + /// Initializes a new instance of the class. + /// + /// The spi device. + /// The width. + /// The height. + public Sk6812(SpiDevice spiDevice, int width, int height = 1) + : base(spiDevice, new BitmapImageNeo4(width, height)) + { + } + } +} diff --git a/src/devices/Ws28xx/Ws28xx.sln b/src/devices/Ws28xx/Ws28xx.sln index db3d3439ba179de59182181a45fc5bab19d942f2..ce3935ac0d5eb70ae1f2e5d1aa3852a3b78e0ae5 100644 --- a/src/devices/Ws28xx/Ws28xx.sln +++ b/src/devices/Ws28xx/Ws28xx.sln @@ -1,13 +1,15 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26124.0 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31729.503 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{B2C3D1C3-4F59-4544-982C-E205F522DF27}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ws28xx.Samples", "samples\Ws28xx.Samples.csproj", "{D97F0DC4-777E-4F1A-BF18-A63DA22707ED}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ws28xx.Samples", "samples\Ws28xx_Samples\Ws28xx.Samples.csproj", "{D97F0DC4-777E-4F1A-BF18-A63DA22707ED}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ws28xx", "Ws28xx.csproj", "{D909238E-13C4-45C4-B044-CCBB08B51647}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ws28xx", "Ws28xx.csproj", "{D909238E-13C4-45C4-B044-CCBB08B51647}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sk6812.Samples", "samples\Sk6812_Samples\Sk6812.Samples.csproj", "{76D2B9B1-CBDE-445C-BE6D-B5C5C1754192}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -18,9 +20,6 @@ Global Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {D97F0DC4-777E-4F1A-BF18-A63DA22707ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D97F0DC4-777E-4F1A-BF18-A63DA22707ED}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -46,8 +45,27 @@ Global {D909238E-13C4-45C4-B044-CCBB08B51647}.Release|x64.Build.0 = Release|Any CPU {D909238E-13C4-45C4-B044-CCBB08B51647}.Release|x86.ActiveCfg = Release|Any CPU {D909238E-13C4-45C4-B044-CCBB08B51647}.Release|x86.Build.0 = Release|Any CPU + {76D2B9B1-CBDE-445C-BE6D-B5C5C1754192}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {76D2B9B1-CBDE-445C-BE6D-B5C5C1754192}.Debug|Any CPU.Build.0 = Debug|Any CPU + {76D2B9B1-CBDE-445C-BE6D-B5C5C1754192}.Debug|x64.ActiveCfg = Debug|Any CPU + {76D2B9B1-CBDE-445C-BE6D-B5C5C1754192}.Debug|x64.Build.0 = Debug|Any CPU + {76D2B9B1-CBDE-445C-BE6D-B5C5C1754192}.Debug|x86.ActiveCfg = Debug|Any CPU + {76D2B9B1-CBDE-445C-BE6D-B5C5C1754192}.Debug|x86.Build.0 = Debug|Any CPU + {76D2B9B1-CBDE-445C-BE6D-B5C5C1754192}.Release|Any CPU.ActiveCfg = Release|Any CPU + {76D2B9B1-CBDE-445C-BE6D-B5C5C1754192}.Release|Any CPU.Build.0 = Release|Any CPU + {76D2B9B1-CBDE-445C-BE6D-B5C5C1754192}.Release|x64.ActiveCfg = Release|Any CPU + {76D2B9B1-CBDE-445C-BE6D-B5C5C1754192}.Release|x64.Build.0 = Release|Any CPU + {76D2B9B1-CBDE-445C-BE6D-B5C5C1754192}.Release|x86.ActiveCfg = Release|Any CPU + {76D2B9B1-CBDE-445C-BE6D-B5C5C1754192}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {D97F0DC4-777E-4F1A-BF18-A63DA22707ED} = {B2C3D1C3-4F59-4544-982C-E205F522DF27} + {76D2B9B1-CBDE-445C-BE6D-B5C5C1754192} = {B2C3D1C3-4F59-4544-982C-E205F522DF27} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F452F127-C7D8-4D14-9484-B23E8F0C3468} EndGlobalSection -EndGlobal \ No newline at end of file +EndGlobal diff --git a/src/devices/Ws28xx/samples/Sk6812_Samples/Program.cs b/src/devices/Ws28xx/samples/Sk6812_Samples/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..2fb2c656a7fde424c39dbe75e7ce4367dee40d41 --- /dev/null +++ b/src/devices/Ws28xx/samples/Sk6812_Samples/Program.cs @@ -0,0 +1,152 @@ +// 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.Device.Spi; +using System.Drawing; +using System.Threading; +using Iot.Device.Graphics; +using Iot.Device.Ws28xx; + +// Configure the count of pixels +const int Count = 8; +Console.Clear(); + +SpiConnectionSettings settings = new(0, 0) +{ + ClockFrequency = Sk6812.DefaultSpiClockFrequency, + Mode = SpiMode.Mode0, + DataBitLength = 8 +}; +using SpiDevice spi = SpiDevice.Create(settings); + +Sk6812 neo = new Sk6812(spi, Count); + +Console.CancelKeyPress += (o, e) => +{ + BitmapImage img = neo.Image; + img.Clear(); + neo.Update(); + Console.Clear(); +}; + +while (true) +{ + Color4Wipe(neo, Color.White, Count); + Color4Wipe(neo, Color.Red, Count); + Color4Wipe(neo, Color.Green, Count); + Color4Wipe(neo, Color.Blue, Count); + + Theatre4Chase(neo, Color.White, Count); + Theatre4Chase(neo, Color.Red, Count); + Theatre4Chase(neo, Color.Green, Count); + Theatre4Chase(neo, Color.Blue, Count); + + Rainbow4(neo, Count); + Rainbow4Cycle(neo, Count); + TheaterChase4Rainbow(neo, Count); +} + +void Color4Wipe(Sk6812 neo, Color color, int count) +{ + BitmapImage img = neo.Image; + for (var i = 0; i < count; i++) + { + img.SetPixel(i, 0, color); + neo.Update(); + Thread.Sleep(100); + } +} + +void Theatre4Chase(Ws28xx neo, Color color, int count, int iterations = 10) +{ + BitmapImage img = neo.Image; + for (var i = 0; i < iterations; i++) + { + for (var j = 0; j < 3; j++) + { + for (var k = 0; k < count; k += 3) + { + img.SetPixel(j + k, 0, color); + } + + neo.Update(); + Thread.Sleep(100); + for (var k = 0; k < count; k += 3) + { + img.SetPixel(j + k, 0, Color.FromArgb(0, 0, 0, 0)); + } + } + } +} + +Color Wheel4(int position) +{ + if (position < 85) + { + return Color.FromArgb(0, position * 3, 255 - position * 3, 0); + } + else if (position < 170) + { + position -= 85; + return Color.FromArgb(0, 255 - position * 3, 0, position * 3); + } + else + { + position -= 170; + return Color.FromArgb(0, 0, position * 3, 255 - position * 3); + } +} + +void Rainbow4(Sk6812 neo, int count, int iterations = 1) +{ + BitmapImage img = neo.Image; + for (var i = 0; i < 255 * iterations; i++) + { + for (var j = 0; j < count; j++) + { + img.SetPixel(j, 0, Wheel4((i + j) & 255)); + } + + neo.Update(); + Thread.Sleep(50); + } +} + +void Rainbow4Cycle(Ws28xx neo, int count, int iterations = 1) +{ + BitmapImage img = neo.Image; + for (var i = 0; i < 255 * iterations; i++) + { + for (var j = 0; j < count; j++) + { + img.SetPixel(j, 0, Wheel4(((int)(j * 255 / count) + i) & 255)); + } + + neo.Update(); + Thread.Sleep(50); + } +} + +void TheaterChase4Rainbow(Ws28xx neo, int count) +{ + BitmapImage img = neo.Image; + for (var i = 0; i < 255; i++) + { + for (var j = 0; j < 3; j++) + { + for (var k = 0; k < count; k += 3) + { + img.SetPixel(k + j, 0, Wheel4((k + i) % 255)); + } + + neo.Update(); + System.Threading.Thread.Sleep(100); + + for (var k = 0; k < count; k += 3) + { + img.SetPixel(k + j, 0, Color.FromArgb(0, 0, 0, 0)); + } + } + } +} diff --git a/src/devices/Ws28xx/samples/Sk6812_Samples/Sk6812.Samples.csproj b/src/devices/Ws28xx/samples/Sk6812_Samples/Sk6812.Samples.csproj new file mode 100644 index 0000000000000000000000000000000000000000..310d97f99f3f09b77075ba250836dcbfd0ff0bfb --- /dev/null +++ b/src/devices/Ws28xx/samples/Sk6812_Samples/Sk6812.Samples.csproj @@ -0,0 +1,9 @@ + + + Exe + net5.0 + + + + + \ No newline at end of file diff --git a/src/devices/Ws28xx/samples/Program.cs b/src/devices/Ws28xx/samples/Ws28xx_Samples/Program.cs similarity index 100% rename from src/devices/Ws28xx/samples/Program.cs rename to src/devices/Ws28xx/samples/Ws28xx_Samples/Program.cs diff --git a/src/devices/Ws28xx/samples/Ws28xx.Samples.csproj b/src/devices/Ws28xx/samples/Ws28xx_Samples/Ws28xx.Samples.csproj similarity index 75% rename from src/devices/Ws28xx/samples/Ws28xx.Samples.csproj rename to src/devices/Ws28xx/samples/Ws28xx_Samples/Ws28xx.Samples.csproj index 629938ff422f6b063721a3f246365f75f46b4dbc..9f4efbb5f335be5e2aa95f72357bde4396b77068 100644 --- a/src/devices/Ws28xx/samples/Ws28xx.Samples.csproj +++ b/src/devices/Ws28xx/samples/Ws28xx_Samples/Ws28xx.Samples.csproj @@ -4,6 +4,6 @@ $(DefaultSampleTfms) - + \ No newline at end of file