Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Driver for WS2812B LEDs #76

Open
wants to merge 44 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
06f4d0a
Initial commit
Ic-ks Jun 28, 2017
3b2c280
Replaced bit pattern converting algorithm
Ic-ks Jun 28, 2017
8014560
Added comments, variable name refactoring, formatation
Ic-ks Aug 14, 2017
943c4ef
Merge branch 'original-master'
Ic-ks Aug 14, 2017
1a6e170
Added tests:
Ic-ks Aug 17, 2017
cc4546a
Added additional tests
Ic-ks Aug 21, 2017
66c2f1b
Using a map to cache and convert the bit patterns
Ic-ks Aug 28, 2017
1f8a673
Fixed warnings: Reduced all method scopes to the required minimum
Ic-ks Aug 28, 2017
6c40fb5
Replaced Storage interface of TwelveBitIntToBitPatternMapper.java
Ic-ks Aug 29, 2017
d5a8e97
Name refactoring and completion of ColorToBitPatternConverterTest.java
Ic-ks Aug 30, 2017
8414469
Updated README.md and ws2812b-timings.svg
Ic-ks Aug 30, 2017
35aeb68
Merge branch 'original-master'
Ic-ks Aug 30, 2017
52d14f2
Merge branch 'original-master'
Ic-ks Aug 30, 2017
899b21d
Merge remote-tracking branch 'origin/master'
Ic-ks Aug 30, 2017
9e2bb56
Update README.md
Ic-ks Aug 30, 2017
a7e3786
Updated SVGs
Ic-ks Aug 30, 2017
4b24e66
Merge remote-tracking branch 'origin/master'
Ic-ks Aug 30, 2017
c14727d
Update README.md
Ic-ks Aug 30, 2017
e4b45d9
Update README.md
Ic-ks Aug 30, 2017
9839ced
Update README.md
Ic-ks Sep 4, 2017
64bb7ae
Update README.md
Ic-ks Sep 5, 2017
2d21d5f
Updated compileSdkVersion, minSdkVersion, targetSdkVersion and buildT…
Ic-ks Sep 9, 2017
0bd5ba6
Updated support-annotations library to 26.1.0
Ic-ks Sep 17, 2017
64eabee
Update README.md
Ic-ks Sep 18, 2017
8bb4a72
Merge branch 'original-master'
Ic-ks Oct 9, 2017
dae568a
Update README.md
Ic-ks Oct 9, 2017
d567e9a
Update README.md
Ic-ks Oct 9, 2017
ed0eb24
Fixed a unit test bug and unit test refactoring
Ic-ks Oct 11, 2017
1fd826b
Merge remote-tracking branch 'origin/master'
Ic-ks Oct 11, 2017
0b687fc
Update README.md
Ic-ks Oct 12, 2017
6efc128
Update README.md
Ic-ks Oct 12, 2017
22bae36
Update README.md
Ic-ks Oct 13, 2017
6bee650
Update README.md
Ic-ks Oct 13, 2017
13c9221
Update README.md
Ic-ks Oct 13, 2017
c395031
Update README.md
Ic-ks Oct 13, 2017
bc23345
Improve README and SVG images.
bert2 Oct 15, 2017
34ccd6e
Merge pull request #2 from bert2/master
Ic-ks Oct 15, 2017
94dbdb9
Update README.md
Ic-ks Oct 15, 2017
4da9d60
Auto stash before merge of "master" and "origin/master"
Ic-ks Oct 15, 2017
3d10410
Updated buildToolsVersion
Ic-ks Oct 15, 2017
c582583
Downgrade of compileSdkVersion, buildToolsVersion, minSdkVersion to 24
Ic-ks Oct 15, 2017
7ef19f5
Merge branch 'androi-26-update'
Ic-ks Oct 15, 2017
bb7b936
Added and changed uses-library's android:required field in AndroidMa…
Ic-ks Nov 20, 2017
37d29b8
Changed scope of the ColorChannelSequence class to public to allow a …
Ic-ks Nov 20, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Wed Jan 25 12:34:01 PST 2017
#Tue Aug 15 21:09:08 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ include ':rainbowhat'
include ':sensehat'
include ':voicehat'
include ':zxgesturesensor'
include ':ws2812b'

include ':testingutils'
142 changes: 142 additions & 0 deletions ws2812b/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
WS2812B LED driver for Android Things
=====================================

This driver supports WS2812B LEDs (maybe WS1812S and SK6812 run too).

NOTE: these drivers are not production-ready. They are offered as sample
implementations of Android Things user space drivers for common peripherals
as part of the Developer Preview release. There is no guarantee
of correctness, completeness or robustness.

How to use the driver
---------------------

### Gradle dependency

To use the `WS2812B` driver, simply add the line below to your project's `build.gradle`,
where `<version>` matches the last version of the driver available on [jcenter][jcenter].

```
dependencies {
compile 'com.google.android.things.contrib:driver-ws2812b:<version>'
}
```

### Sample usage

```java
import com.google.android.things.contrib.driver.ws2812b.Ws2812b;

// Access the LED strip:

Ws2812b mWs2812b;

try {
mWs2812b = new Ws2812b(spiBusName);
} catch (IOException e) {
// couldn't configure the device...
}

// Light it up!

int[] colors = new int[] {Color.RED, Color.GREEN, Color.BLUE};
try {
mWs2812b.write(colors);
} catch (IOException e) {
// error setting LEDs
}

// Close the LED strip when finished:

try {
mWs2812b.close();
} catch (IOException e) {
// error closing LED strip
}
```

How it works
------------

### The WS2812B data format

The WS2812B LED controller needs 24 bits of data (8 bits per color channel) to set the color of one LED. Every further LED of a strip needs another 24 bit long block of data. The transmission of these bits is done by sending a chain of high and low voltage pulses over the data line of the LED controller.
Each separate bit is hereby defined by a high voltage pulse which is followed by a low voltage pulse. The recognition as 0- or 1-bit is defined by the timings of these pulses:

<p align="center">
<img align="center" src="https://rawgit.com/Ic-ks/contrib-drivers/master/ws2812b/ws2812b-timings.svg"/>
</p>

* The 0-bit is defined by a high voltage pulse with a duration of 400 ns which is followed by a low voltage pulse of 850 ns
* The 1-bit is defined by a high voltage pulse with a duration of 850 ns which is followed by a low voltage pulse of 400 ns
* Each pulse can have a deviation of +/- 150 ns

At the moment there is no way to send such short timed pulses having _different durations_ via the Android Things API.

### The Serial Peripheral Interface

The [Serial Peripheral Interface (SPI)](https://developer.android.com/things/sdk/pio/spi.html) can send bits as voltage pulses. Those pulses all have the same specified duration.

* The pulse duration is indirectly defined by the frequency of the SPI
* A transmitted 1-bit results in a short high voltage pulse at the SPI MOSI (Master Out Slave In) pinout
* A transmitted 0-bit results in a short low voltage pulse at the MOSI pinout

### Approximating the WS2812B data format using SPI

In order to control WS2812B LEDs via the SPI, we must find two assemblies of bits (hereinafter bit pattern) and a frequency such that each of these bit patterns results in a sequence of voltage pulses which are recognized as 0 or 1 bit by the receiving WS2812B controller. With these two bit patterns we are able to convert every bit of an arbitrary array of color data. If then the converted data is sent to the WS2812B controller by SPI, the controller will recognize the orignal color and light up the LEDs accordingly. A possible solution for this approach are the two 3 bit sized patterns below:

<p align="center">
<img align="center" src="https://rawgit.com/Ic-ks/contrib-drivers/master/ws2812b/ws2812b-bit-pattern.svg"/>
</p>

The deviation from the WS2812B specified pulse duration is -16 ns and +17 ns respectively, which is within the allowed range of +/- 150 ns. It is possible to create a more accurate bit pattern with more than 3 bits, but increasing the size of the bit pattern also reduces the number of controllable LEDs as the fixed-size SPI buffer fills up more quickly. The appropriate frequency is defined by the duration of 1 bit (417 ns):

<p align="center">
<img align="center" src="http://latex.codecogs.com/gif.latex?f%3D%5Cfrac%7B1%20%7D%7B417%20%5Ccdot%2010%5E%7B-9%7D%7DHz"/>
</p>

### Handling pauses between SPI words

SPI will automatically insert low voltage pauses between transmitted words. This short break marks the end of a transmitted word and has the same duration as a single bit. If left unhandled, those pauses would interfere with our approximated data format and lead to incorrect colors. By considering a word size of 8 bits (maximum size), the pause can be understood as an automatically inserted 0-bit between the 8th and the 9th bit. Fortunately, all possible bit sequences yield a bit pattern where every 9th bit is a 0-bit. Hence an easy solution is to discard the last bit like shown in the table below:

| Source bit sequence | Resulting bit pattern | Without trailing 0-bit |
| ------------------- |:----------------------:|:------------------------:|
| 000 | 100 100 10**0** | 100 100 10 |
| 001 | 100 100 11**0** | 100 100 11 |
| 010 | 100 110 10**0** | 100 110 10 |
| 011 | 100 110 11**0** | 100 110 11 |
| 100 | 110 100 10**0** | 110 100 10 |
| 110 | 110 110 10**0** | 110 110 10 |
| 111 | 110 110 11**0** | 110 110 11 |

With this in mind we can represent three source bits using one destination byte. So any possible 24 bit color needs to be converted to a 8 byte sized sequence of bit patterns (24 / 3 = 8).

To prevent excessive memory usage this driver stores and maps only 12 bit numbers to their corresponding 4 byte sized bit patterns ([TwelveBitIntToBitPatternMapper.java](/ws2812b/src/main/java/com/google/android/things/contrib/driver/ws2812b/TwelveBitIntToBitPatternMapper.java)). The full 8 byte sized bit pattern for 24 bits of color data is then constructed by two 4 byte sized bit patterns ([ColorToBitPatternConverter.java](/ws2812b/src/main/java/com/google/android/things/contrib/driver/ws2812b/ColorToBitPatternConverter.java)). Last but not least, most WS2812B controllers expect GRB as order of the incoming color. So a reordering from RGB to the expected order must be done before the bit pattern conversion ([ColorChannelSequence.java](/ws2812b/src/main/java/com/google/android/things/contrib/driver/ws2812b/ColorChannelSequence.java)).

References
----------
* https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/
* https://cpldcpu.com/2014/01/14/light_ws2812-library-v2-0-part-i-understanding-the-ws2812/
* https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf

License
-------

Copyright 2016 Google Inc.

Licensed to the Apache Software Foundation (ASF) under one or more contributor
license agreements. See the NOTICE file distributed with this work for
additional information regarding copyright ownership. The ASF licenses this
file to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License.

[jcenter]: https://bintray.com/google/androidthings/contrib-driver-ws2812b/_latestVersion
39 changes: 39 additions & 0 deletions ws2812b/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

apply plugin: 'com.android.library'

android {
compileSdkVersion 24
buildToolsVersion '24.0.3'

defaultConfig {
minSdkVersion 24
targetSdkVersion 24
versionCode 1
versionName "1.0"
}
}

dependencies {
provided 'com.google.android.things:androidthings:0.5-devpreview'
compile 'com.android.support:support-annotations:24.2.0'

testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.10.19'
testCompile 'org.powermock:powermock-module-junit4:1.6.6'
testCompile 'org.powermock:powermock-api-mockito:1.6.6'
}
2 changes: 2 additions & 0 deletions ws2812b/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
TYPE="RGB LED strip"
ARTIFACT_VERSION=0.1
1 change: 1 addition & 0 deletions ws2812b/publish-settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rootProject.buildFileName = '../publish.gradle'
22 changes: 22 additions & 0 deletions ws2812b/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2016 Google Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.things.contrib.driver.ws2812b">
<application>
<uses-library android:required="false" android:name="com.google.android.things"/>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.google.android.things.contrib.driver.ws2812b;


import android.support.annotation.ColorInt;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;

import java.lang.annotation.Retention;
import java.util.Arrays;

import static java.lang.annotation.RetentionPolicy.SOURCE;

@SuppressWarnings("WeakerAccess")
public class ColorChannelSequence {
public static final int RGB = 1;
public static final int RBG = 2;
public static final int GRB = 3;
public static final int GBR = 4;
public static final int BRG = 5;
public static final int BGR = 6;
@Sequence
private static final int[] ALL_SEQUENCES = {RGB, RBG, GRB, GBR, BRG, BGR};

@Retention(SOURCE)
@IntDef({RGB, RBG, GRB, GBR, BRG, BGR})
@SuppressWarnings("WeakerAccess")
public @interface Sequence {}

interface Sequencer
{
int rearrangeColorChannels(@ColorInt int color);
}

@NonNull
static Sequencer createSequencer(@Sequence int colorChannelSequence)
{
switch (colorChannelSequence) {
case BGR:
return new Sequencer() {
@Override
public int rearrangeColorChannels(int color) {
return ((color & 0xff0000) >> 16) | (color & 0xff00) | ((color & 0xff) << 16);
}
};
case BRG:
return new Sequencer() {
@Override
public int rearrangeColorChannels(int color) {
return ((color & 0xff0000) >> 8) | ((color & 0xff00) >> 8) | ((color & 0xff) << 16);
}
};
case GBR:
return new Sequencer() {
@Override
public int rearrangeColorChannels(int color) {
return ((color & 0xff0000) >> 16) | ((color & 0xff00) << 8) | ((color & 0xff) << 8);
}
};
case GRB:
return new Sequencer() {
@Override
public int rearrangeColorChannels(int color) {
return ((color & 0xff0000) >> 8) | ((color & 0xff00) << 8) | (color & 0xff);
}
};
case RBG:
return new Sequencer() {
@Override
public int rearrangeColorChannels(int color) {
return (color & 0xff0000) | ((color & 0xff00) >> 8) | ((color & 0xff) << 8);
}
};
case RGB:
return new Sequencer() {
@Override
public int rearrangeColorChannels(int color) {
return color;
}
};
default:
throw new IllegalArgumentException("Invalid color channel sequence: " + colorChannelSequence + ". Supported color channel sequences are: " + Arrays.toString(ColorChannelSequence.ALL_SEQUENCES));
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.google.android.things.contrib.driver.ws2812b;


import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Size;
import android.support.annotation.VisibleForTesting;

class ColorToBitPatternConverter {
private static final int MAX_NUMBER_OF_SUPPORTED_LEDS = 512;
private static final int FIRST_TWELVE_BIT_BIT_MASK = 0x00FFF000;
private static final int SECOND_TWELVE_BIT_BIT_MASK = 0x00000FFF;

private final TwelveBitIntToBitPatternMapper mTwelveBitIntToBitPatternMapper;
private ColorChannelSequence.Sequencer colorChannelSequencer;

ColorToBitPatternConverter(@ColorChannelSequence.Sequence int colorChannelSequence) {
this(colorChannelSequence, new TwelveBitIntToBitPatternMapper());
}

@VisibleForTesting
ColorToBitPatternConverter(@ColorChannelSequence.Sequence int colorChannelSequence, @NonNull TwelveBitIntToBitPatternMapper twelveBitIntToBitPatternMapper) {
colorChannelSequencer = ColorChannelSequence.createSequencer(colorChannelSequence);
mTwelveBitIntToBitPatternMapper = twelveBitIntToBitPatternMapper;
}

/**
* Converts the passed color array to a correlating byte array of bit patterns. These resulting
* bit patterns are readable by a WS2812B LED strip if they are sent by a SPI device with the
* right frequency.
*
* @param colors An array of color integers {@link ColorInt}
* @return Returns a byte array of correlating bit patterns
*/
byte[] convertToBitPattern(@ColorInt @NonNull @Size(max = MAX_NUMBER_OF_SUPPORTED_LEDS) int[] colors) {
if (colors.length > MAX_NUMBER_OF_SUPPORTED_LEDS) {
throw new IllegalArgumentException("Only " + MAX_NUMBER_OF_SUPPORTED_LEDS + " LEDs are supported. A Greater Number (" + colors.length + ") will result in SPI errors!");
}

byte[] bitPatterns = new byte[colors.length * 8];

int i = 0;
for (int color : colors) {
color = colorChannelSequencer.rearrangeColorChannels(color);
int firstValue = (color & FIRST_TWELVE_BIT_BIT_MASK) >> 12;
int secondValue = color & SECOND_TWELVE_BIT_BIT_MASK;

System.arraycopy(mTwelveBitIntToBitPatternMapper.getBitPattern(firstValue), 0, bitPatterns, i, 4);
i += 4;
System.arraycopy(mTwelveBitIntToBitPatternMapper.getBitPattern(secondValue), 0, bitPatterns, i, 4);
i += 4;
}
return bitPatterns;
}
}
Loading