Current section

Files

Jump to
nerves_livebook priv samples basics spi_rgb_leds.livemd
Raw

priv/samples/basics/spi_rgb_leds.livemd

# SPI RGB LEDs
## Table of Contents
1. [Prerequisites](#prerequisites)
1. [Introduction](#introduction)
1. [Wiring and discovering your SPI bus](#wiring-and-discovering-your-SPI-bus)
1. [Formatting our SPI payload](#formatting-our-SPI-serial-data-structure)
1. [Blinking a single LED](#blinking-a-single-led)
1. [Managing all the LEDs on a strip](#managing-all-the-leds-on-a-strip)
1. [Simple Larson scanner](#simple-larson-scanner)
1. [Simple rainbow](#simple-rainbow)
1. [What next](#what-next)
1. [Acronyms](#acronyms)
## Prerequisites
* A Raspberry Pi, BeagleBone, or other board with a SPI bus
* SK9822 or APA102 RGB LED strip
* (Recommended) A 5 volt 3 amp external power supply
* (Recommended) A general understanding of the [SPI bus](https://learn.sparkfun.com/tutorials/serial-peripheral-interface-SPI/all)
## Introduction
In this exercise, we will be lighting up an RGB LED strip using the `circuits.SPI` library.
If you are unfamiliar with the SPI bus, please take a moment to review the [circuits.SPI](https://hexdocs.pm/circuits_SPI/readme.html)
online documentation and the [Open Source Hardware Association's resolution](https://www.oshwa.org/a-resolution-to-redefine-SPI-signal-names/).
It should help you build a general understanding of naming convention, and how data is transferred between a controller and peripheral.
## Wiring and discovering your SPI bus
First and foremost, we need to wire up the board and LED strip.
Using your boards documentation find the pins for `SCLK`, `GND`, and `COPI`.
<pre>
Example SPI Diagram:
___________________ ____________________
| SCLK |---->| SCLK |
| SPI COPI |---->| COPI SPI |
| Controller CIPO |<----| CIPO Peripheral |
| CS |---->| CS |
------------------- --------------------
</pre>
###### Single controller to single peripheral: basic SPI bus example
<details closed>
<summary>Example Raspberry Pi Wiring Diagram</summary>
<br>
Using the Raspberry Pi's SPI0.
<pre>
Raspberry Pi SK9822
__________________ __________________
| Ground GND |<----| GND |
| GPIO 11 SCLK |---->| CI |
| GPIO 10 COPI |---->| DI |
------------------ | |
External Power 5v ------>| 5v |
------------------
</pre>
<h5>If using an external power supply, ensure it shares a common Ground with your board.</h5>
</details>
<details closed>
<summary>Example Beaglebone Wiring Diagram</summary>
<br>
Using the BeagleBone's SPI0.
<pre>
BeagleBone GateWay SK9822
__________________ __________________
| Ground GND |<----| GND |
| GPIO 22 SCLK |---->| CI |
| GPIO 21 COPI |---->| DI |
------------------ | |
External Power 5v ------>| 5v |
------------------
</pre>
<h5>If using an external power supply, ensure it shares a common Ground with your board.</h5>
</details>
You should be able to see your enabled SPI buses with the following:
```elixir
Circuits.SPI.bus_names()
```
Lets match on your bus.
```elixir
{:ok, ref} = Circuits.SPI.open("spidev0.0")
```
As well as alias `Circuits.SPI`.
```elixir
alias Circuits.SPI
```
[return to top](#table-of-contents)
## Formatting our SPI Serial Data Structure
Now that we have our LED strip wired up to our device, let's send some data!
The SK9822 and APA102 leverage the same series data structure.
A thorough write up on a unified protocol is available [here](https://cpldcpu.wordpress.com/2016/12/13/SK9822-a-clone-of-the-apa102/)
and this is the same logic leveraged by the popular Arduino [FastLED library](https://github.com/FastLED/FastLED).
We will need to send a few frames from the controller to the peripheral.
###### Start Frame of 32 `zero` Bits.
| 0000 0000 | 0000 0000 | 0000 0000 | 0000 0000 |
| :-------: | :-------: | :-------: | :-------: |
| 0x00 | 0x00 | 0x00 | 0x00 |
| 8 Bits | 8 Bits | 8 Bits | 8 Bits |
Elixir example: `<<0::32>>`.
###### LED Frame 32 Bits.
| Gray Scale | Brigtness | Blue | Green | Red |
|:----------:|:---------:|:---------:|:---------:|:---------:|
| 111 | 11111 | 1111 1111 | 1111 1111 | 1111 1111 |
| 3 Bits | 5 Bits | 8 Bits | 8 Bits | 8 Bits |
Elixir example: `<<3::3, 1::5, 0xFF, 0xFF, 0xFF>>`.
###### End Frame
| 0 |
| :---: |
| 0x0 |
| 1 Byte |
Times (n/2), where n is the number of LEDs on the strip rounded to the next byte.
Elixir example: `<<0::8>>`.
[return to top](#table-of-contents)
## Blinking a single LED
First, we will turn on your first LED.
Start frame is static, and can be reused.
We will only have one LED frame, to blink the first LED on the strip.
However, our end frame depends on the number of LEDs we have,
so lets calculate the number of bytes we will end with.
The `Kernel.<>/2` operator concatenates our binaries.
```elixir
led_count = 1
start_frame = << 0::32 >>
# The end frame is 8 '0' bits * (leds - 1) / 16 (rounded)
# These are clock edges to ensure the LED's respond immediately and is borrowed from the FASTLed library.
end_frame = :binary.copy(<<0>>, round((led_count - 1) / 16))
# Creating a List of frames with each LED on white
led_frame = <<7::3, 1::5, 0xff, 0xff, 0xff>>
# Transfer the frames over SPI
SPI.transfer(ref, start_frame <> led_frame <> end_frame)
```
Your first LED should bet lit up white now.
Lets blink it a few times, alternating the `led_frame` and an `off_frame`.
```elixir
blink_times = 10
delay = 1000
off_frame = <<7::3, 1::5, 0x00, 0x00, 0x00>>
Enum.map(0..blink_times, fn x ->
case rem(x, 2) do
0 -> SPI.transfer(ref, start_frame <> led_frame <> end_frame)
_ -> SPI.transfer(ref, start_frame <> off_frame <> end_frame)
end
Process.sleep(delay)
end)
```
[return to top](#table-of-contents)
## Managing all the LEDs on a strip
We can create lists of frames and send them all at one.
Update the `led_count` to reflect your strip.
```elixir
# Set the number of LED's in your strip
led_count = 144
# The end frame is 8 '0' bits * (leds - 1) / 16 (rounded up)
# These are clock edges to ensure the LED's respond immediately and is borrowed from the FASTLed library.
end_frame = :binary.copy(<<0>>, round((led_count - 1) / 16))
# Creating a List of frames with each LED on white
led_frames = Enum.map(1..led_count, fn _ -> led_frame end)
off_frames = Enum.map(1..led_count, fn _ -> off_frame end)
# Transfer the frames over SPI
SPI.transfer(ref, start_frame <> Enum.join(led_frames) <> end_frame)
# Wait for a prespecified duration of time.
Process.sleep(delay)
# Turn them off
SPI.transfer(ref, start_frame <> Enum.join(off_frames) <> end_frame)
```
[return to top](#table-of-contents)
## Simple larson scanner
A Larson scanner, named after Glen A. Larson, is a back and forth scanning red light.
We are going to create a map the length of our LED strip for addressing.
```elixir
delay = 1
scans = 2
# Create an addressible Map of all your strips LED's.
map = Enum.reduce(0..led_count, %{}, fn x, acc -> Map.put(acc, x, off_frame) end)
# For each scan
Enum.map(1..scans, fn _ ->
# Write from left to right
Enum.map(0..(led_count - 1), fn x ->
map = Map.replace(map, x, <<7::3, 15::5, 0x00, 0x00, 0x0f>>)
bytes =
map
|> Enum.sort
|> Enum.map(fn {_, v} -> v end)
|> Enum.join
SPI.transfer(ref, start_frame <> bytes <> end_frame)
Process.sleep(delay)
end)
# Write from right to left
Enum.map((led_count - 1)..0, fn x ->
map = Map.replace(map, x, <<7::3, 15::5, 0x00, 0x00, 0x0f>>)
bytes =
map
|> Enum.sort
|> Enum.map(fn {_, v} -> v end)
|> Enum.join
SPI.transfer(ref, start_frame <> bytes <> end_frame)
Process.sleep(delay)
end)
end)
# Turn them off
SPI.transfer(ref, start_frame <> Enum.join(off_frames) <> end_frame)
```
[return to top](#table-of-contents)
## Simple rainbow
```elixir
scans = 1
delay = 1
# A quick and dirty guard for lower and upper limits.
min_max = fn
(_min, max, value) when value > max -> max
(min, _max, value) when value < min -> min
(_min, _max, value) -> value
end
# A conversion function from HSV to RGB.
from_hsv = fn(h, s, v) ->
h = min_max.(0, 360, h)
s = min_max.(0, 100, s)
v = min_max.(0, 100, v)
h = h / 60
i = floor(h)
f = h - i
sat_dec = s / 100
p = v * (1 - sat_dec)
q = v * (1 - sat_dec * f)
t = v * (1 - sat_dec * (1 - f))
p_rgb = floor(p * 255 / 100)
v_rgb = floor(v * 255 / 100)
t_rgb = floor(t * 255 / 100)
q_rgb = floor(q * 255 / 100)
case i do
0 -> << 7::3, 1::5, p_rgb, t_rgb, v_rgb >>
1 -> << 7::3, 1::5, p_rgb, v_rgb, q_rgb >>
2 -> << 7::3, 1::5, t_rgb, v_rgb, p_rgb >>
3 -> << 7::3, 1::5, v_rgb, q_rgb, p_rgb >>
4 -> << 7::3, 1::5, v_rgb, p_rgb, t_rgb >>
_ -> << 7::3, 1::5, q_rgb, p_rgb, v_rgb >>
end
end
# Create a list of lists containing
color_map =
Enum.map(0..360, fn h ->
Enum.map(0..(led_count - 1), fn
led when h - led < 0 ->
360 + h - led
led ->
h - led
end)
end)
Enum.map(1..scans, fn _ ->
for rainbow <- color_map do
map =
rainbow
|> Enum.with_index()
|> Enum.reduce(%{}, fn {x, i}, acc -> Map.put(acc, i, from_hsv.(x, 100, 50)) end)
bytes =
map
|> Enum.sort
|> Enum.map(fn {_, v} -> v end)
|> Enum.join
SPI.transfer(ref, start_frame <> bytes <> end_frame)
Process.sleep(delay)
end
end)
# Turn them off
SPI.transfer(ref, start_frame <> Enum.join(off_frames) <> end_frame)
```
[return to top](#table-of-contents)
## What next
Want to keep going with these exercises? Here is a list of suggestions!
1. Add a faded trail to the Larson scanner.
1. Make Frame and Color modules with guards for the led_frame values.
1. Managing state and rendering using GenServer.
[return to top](#table-of-contents)
## Acronyms
| Acronym | Definition |
| ------: | ---------- |
| LED | Light Emitting Diode |
| RGB | Red, Green, Blue. |
| SPI | Serial Peripheral Interface |
| SDO | Serial Data Out. An output signal on a device where data is sent out to another SPI device |
| SDI | Serial Data In. An input signal on a device where data is received from another SPI device |
| CS | Chip Select. Activated by the controller to initiate communication with a given peripheral |
| COPI | controller out / peripheral in). For devices that can be either a controller or a peripheral; the signal on which the device sends output when acting as the controller, and receives input when acting as the peripheral |
| CIPO | controller in / peripheral out). For devices that can be either a controller or a peripheral; the signal on which the device receives input when acting as the controller, and sends output when acting as the peripheral |
| SDIO | Serial Data In/Out. A bi-directional serial signal |
[return to top](#table-of-contents)