SPI on a BeagleBone Black for LED domination

I have been using various types of LEDs for accent lighting for several years. I started with Arduino’s, ZigBee, and some I2C LED modules. This worked, but was a little flakey and difficult to update. Revision two used Raspberry PI’s. This made updating the code easier, but the Raspberry PI’s turned out to be unstable. The current hardware revision uses the BeagleBone Black and intelligent LPD8806 strips from Adafruit. Oh, somewhere in there I was also using 12V analog LED strips and PWM.

The current software revision uses an old build of Arch Linux for ARM, and Adafruit’s BBIO python library. This works, but slowly, I am working on updating to Debian 8. I’d like to switch back to a more official image with a newer kernel.

I’ve managed to work through the proof-of-concept bugs. I was unable to get the Adafruit BBIO library to work with the newer kernel. They may have fixed it by now. I’d like to port my code to Rust, so this is no longer a showstopper.

As a bonus, /dev/spidev* is writable by the spi group, of which the default Debian user is a member. That means we no longer have to run as root, which is a good thing.

Unlike my Debian 7 instructions, Debian 8 works with minimal changes to the default image from elinux.org.

I’ll provide examples in C and Rust. Let’s get to it.

Installing Debian

Copy the base image to a sacrificial microSD card

Grab the latest Debian Jessie console image from http://elinux.org/Beagleboard:BeagleBoneBlack_Debian#Jessie_Snapshot_console. I am using the standalone image. Alternatively you could use the “flasher” image to write the image to the internal flash. For development purposes, I am booting off a microSD card.

As of August 2016, this was:

$ wget https://rcn-ee.com/rootfs/bb.org/testing/2016-08-14/console/bone-debian-8.5-console-armhf-2016-08-14-2gb.img.xz
$ sha256sum bone-debian-8.5-console-armhf-2016-08-14-2gb.img.xz
31409aa2d598aa0bec212f775be7c390fd39527cb3b6de804de237a616249d1d  bone-debian-8.5-console-armhf-2016-08-14-2gb.img.xz

Copy the image to a micro SD card (don’t cut-and-paste this, know your environment and devices):

xzcat bone-debian-8.5-console-armhf-2016-08-14-2gb.img.xz | \
    sudo dd of=/dev/mmcblk0 ; sync

See the elinux.org page for instructions for other operating systems.

Boot

Insert the microSD card and boot your BeagleBone Black.

You may want to attach a serial console before proceeding. See my bus pirate notes for an example.

You may need to hold down the “S2” button to boot off the microSD. See Step #8: Boot your board off of the SD card.

Post-boot

Optional, but I like to update the OS before proceeding:

Debian GNU/Linux 8 beaglebone ttyS0

BeagleBoard.org Debian Image 2016-08-14

Support/FAQ: http://elinux.org/Beagleboard:BeagleBoneBlack_Debian

default username:password is [debian:temppwd]

beaglebone login: debian
Password:
Linux beaglebone 4.4.16-ti-r38 #1 SMP Wed Aug 3 17:12:46 UTC 2016 armv7l

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
debian@beaglebone:~$
Debian GNU/Linux 8 beaglebone ttyS0

BeagleBoard.org Debian Image 2016-08-14

Support/FAQ: http://elinux.org/Beagleboard:BeagleBoneBlack_Debian

default username:password is [debian:temppwd]

beaglebone login: debian
Password:
Linux beaglebone 4.4.16-ti-r38 #1 SMP Wed Aug 3 17:12:46 UTC 2016 armv7l

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
debian@beaglebone:~$ sudo apt update
   ... lots of output ...
   Fetched 10.2 MB in 14s (703 kB/s)
Reading package lists... Done
Building dependency tree
Reading state information... Done
5 packages can be upgraded. Run 'apt list --upgradable' to see them.
debian@beaglebone:~$ sudo apt upgrade
  ... lots more output ...
debian@beaglebone:~$ sudo reboot

Ok, now we have an updated Debian install. You should change the password on the Debian account now (passwd).

Device Tree Configuration

You need to configure the device tree for SPI. A good tutorial about the device tree can be found at Ken Shirrif’s blog.

You must set up the BB-SPIDEV0 cape during boot. If you use echo 'BB-SPIDEV0' > /sys/devices/platform/bone_capemgr/slots, as many tutorials online will tell you, it won’t work.

Add the cape configuration to /boot/uEnv.txt:

debian@beaglebone:~$ sudo -s
root@beaglebone:/home/debian# echo "cape_enable=bone_capemgr.enable_partno=BB-SPIDEV0" >> /boot/uEnv.txt
root@beaglebone:/home/debian# reboot

See https://www.mail-archive.com/beagleboard@googlegroups.com/msg38729.html.

Blinky Lights

At this point you should have a properly configured BeagleBone Black.

Currently, I am using the LPD8806 strips from Adafruit as my SPI device. There are plenty of other options, and the BeagleBone Black should be able to drive all of them. The newer “DotStar” strips from Adafruit look interesting, but I have not tried them yet.

Note that from this point on in the tutorial, if you are not using the LPD8806 strips, you will likely need to make adjustments. If your SPI device is not a strip of LEDs, this will still give you a starting place.

Required Reading

Before continuing, read through the documentation from Adafruit. Specifically here and here. Adafruit covers the wiring process quite well, so I will not repeat this information.

I am using SPI0, so your LPD8806 connects to P9_18 (data) and P9_22 (clock). See https://github.com/beagleboard/bb.org-overlays/blob/master/src/arm/BB-SPIDEV0-00A0.dts#L2.

Quick Test in C

Let’s do a quick, static test using the SPI example code from the Linux kernel.

You’ll need a compiler and friends:

debian@beaglebone:~$ sudo apt-get install build-essential
  ... lots of output, Y to install ...
  ... lots more output ...

Grab the example from kernel.org:

debian@beaglebone:~$ wget 'https://git.kernel.org/cgit/linux/kernel/git/stable/linux-stable.git/plain/Documentation/spi/spidev_test.c?id=refs/tags/v4.4.12' -O spidev_test.c

Now, let’s replace the default output with something the LPD8806 will understand.

In spidev_test.c replace (vi and nano are installed by default):

    uint8_t default_tx[] = {
            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
            0x40, 0x00, 0x00, 0x00, 0x00, 0x95,
            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
            0xF0, 0x0D,
    };

With:

    uint8_t default_tx[] = {
                    0x00, 0x00, 0x00,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0xFF, 0x80, 0x80,
                    0x80, 0xFF, 0x80,
                    0x80, 0x80, 0xFF,
                    0x00, 0x00, 0x00
    };

This is overkill, you could chop off some of those lines, but this will address 60+ LEDs. There’s no harm in sending too much data. And, yes, this could be replaced with a loop, but we’re going for a quick-and-dirty test.

Now, build and run:

debian@beaglebone:~$ cc spidev_test.c -o spidev_test
debian@beaglebone:~$ ./spidev_test

If everything worked, you should have a strip glowing with a green, red, and blue.

Again, see the Adafruit documentation for the protocol. In a nutshell, it’s …

  • Send 0x00 0x00 0x00 to reset the bus.
  • Send the RGB triplet for each LED. Note that it uses green-red-blue, instead of red-green-blue. Also, set bit 7 high (0x80 | value).
  • Send again 0x00 0x00 0x00 to reset the bus (might be overkill, but seems like this is/was necessary).

Now, with Rust

Install Rust

See https://www.rust-lang.org/en-US/downloads.html. Their rustup.sh script works. Running arbitrary scripts from the Internet is a bad idea, but we’ll throw caution to the wind.

debian@beaglebone:~$ sudo apt install curl file
debian@beaglebone:~$ curl -sSf https://static.rust-lang.org/rustup.sh | sh
  ... lots of output, lots of waiting ...
install: installing component 'cargo'

    Rust is ready to roll.

debian@beaglebone:~$

Create a new project

debian@beaglebone:~$ mkdir ~/projects
debian@beaglebone:~$ cd ~/projects/
debian@beaglebone:~/projects$ cargo new spitest --bin
debian@beaglebone:~/projects$ cd spitest/

Set up your project, there is a spidev library, so let’s add that to the dependencies. We’ll also pull in a random number library for, uh, randomness.

debian@beaglebone:~/projects/spitest$ vi Cargo.toml
[package]
name = "spitest"
version = "0.1.0"
authors = ["debian"]

[dependencies]
spidev = "0.2.1"
rand = ""

Here’s our main.rs. Note that I am a Rust noob, so this is probably a horrible example:

debian@beaglebone:~/projects/spitest$ vi src/main.rs
extern crate spidev;
extern crate rand;
use std::io::prelude::*;
use spidev::{Spidev, SpidevOptions, SPI_MODE_0};
use rand::Rng;
use std::time::Duration;
use std::thread;

fn rgb(spi: &mut Spidev, r: u8, g: u8, b: u8) {
    spi.write(&[0x00, 0x00, 0x00]).unwrap();

    for _ in 0..32 {
        spi.write(&[0x80 | g, 0x80 | r, 0x80 | b]).unwrap();

        // Optional delay for a "sweep" effect.
        thread::sleep(Duration::from_millis(10));
    }

    spi.write(&[0x00, 0x00, 0x00]).unwrap();
}

fn main() {
    let mut spidev = Spidev::open("/dev/spidev1.0").unwrap();

    // Note, you can probably run max_speed higher.
    let options = SpidevOptions::new()
                    .bits_per_word(8)
                    .max_speed_hz(200000)
                    .lsb_first(false)
                    .mode(SPI_MODE_0)
                    .build();

    spidev.configure(&options).unwrap();

    for _ in 0..60 {
        // Random Colors
        rgb(&mut spidev, rand::thread_rng().gen_range(0, 127),
            rand::thread_rng().gen_range(0, 127),
            rand::thread_rng().gen_range(0, 127));

        thread::sleep(Duration::from_millis(1000));
    }

    // Turn off LEDs
    rgb(&mut spidev, 0, 0, 0);
}

And, go!

debian@beaglebone:~/projects/spitest$ cargo run

You should see the strip sweep through random colors for about 1 minute, then turn off.