Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

SPI RGB LEDs

priv/samples/basics/spi_rgb_leds.livemd

SPI RGB LEDs

Table of Contents

  1. Prerequisites
  2. Introduction
  3. Wiring and discovering your SPI bus
  4. Formatting our SPI payload
  5. Blinking a single LED
  6. Managing all the LEDs on a strip
  7. Simple Larson scanner
  8. Simple rainbow
  9. What next
  10. 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

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 online documentation and the Open Source Hardware Association’s resolution.. 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.

Example SPI Diagram:
 ___________________       ____________________ 
|              SCLK |---->| SCLK               |
|    SPI       COPI |---->| COPI       SPI     |
| Controller   CIPO |<----| CIPO    Peripheral |
|              CS   |---->| CS                 |
 -------------------       -------------------- 
Single controller to single peripheral: basic SPI bus example

Example Raspberry Pi Wiring Diagram


Using the Raspberry Pi’s SPI0.

    Raspberry Pi                SK9822
 __________________       __________________ 
|  Ground      GND |<----| GND              |
|  GPIO 11    SCLK |---->|  CI              |
|  GPIO 10    COPI |---->|  DI              |
 ------------------      |                  |
External Power 5v ------>|  5v              |
                          ------------------ 
If using an external power supply, ensure it shares a common Ground with your board.

Example Beaglebone Wiring Diagram


Using the BeagleBone’s SPI0.

 BeagleBone GateWay             SK9822
 __________________       __________________ 
|  Ground      GND |<----|  GND             |
|  GPIO 22    SCLK |---->|   CI             |
|  GPIO 21    COPI |---->|   DI             |
 ------------------      |                  |
External Power 5v ------>|   5v             |
                          ------------------ 
If using an external power supply, ensure it shares a common Ground with your board.

You should be able to see your enabled SPI buses with the following:

Circuits.SPI.bus_names()

Lets match on your bus.

{:ok, ref} = Circuits.SPI.open("spidev0.0")

As well as alias Circuits.SPI.

alias Circuits.SPI

return to top

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 and this is the same logic leveraged by the popular Arduino FastLED library.

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

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.

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.

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

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.

# 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

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.

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

Simple rainbow

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

What next

Want to keep going with these exercises? Here is a list of suggestions!

  1. Add a faded trail to the Larson scanner.
  2. Make Frame and Color modules with guards for the led_frame values.
  3. Managing state and rendering using GenServer.

return to top

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