Make things blink

Getting Started with Elixir and Nerves: Part 2

Part Two: Hook a Raspberry Pi to a breadboard

Note: this is part two of a two part series on Elixir, Nerves and the Raspberry Pi:

Please make sure you have read part one for a full parts list and an introduction to Elixir and Nerves.

What are we doing?

In this article we will:

  • Prototype a circuit on a breadboard to light up an LED w/ a 9v battery.
  • Take the Raspberry Pi we hooked up in part one and wire it to the breadboard
  • Make an LED on the breadboard blink

Electronics

I do not yet know how to draw a schematic, so I’m going to do my best try to explain the electronics in pictures and text. The main thing is to know what voltage your LED can handle, so we use Ohm’s law (I=V/R) to ensure you have a resistor that will lower the current enough so that you don’t blow your LED. To be honest, I couldn’t find anything in my kit on the LED’s acceptable current. So I just googled for an LED I thought was similar to what I already had. Many LEDs can take about 50mA of current and one of the resistors in the CanaKit was able to resist enough current to keep it alive.

If you don’t know anything about electronics, I recommend this short series of videos:

CodeNMore Beginner’s Electronics

Eventually, I may get around to some videos or articles of my own. In any case, if you have ever taken a flashlight apart, the electronics aren’t much more complicated than that.

Let’s start by seeing if we can prototype this separately from the Raspberry Pi using a 9 volt batttery:

The power rails flow current vertically, so plugging into any of the holes in the top half the board will connect to any of the other holes. The other holes on the board are connected to each other horizonally, so I only need to connect wires between the different parts of the circuit vertically.

Essentially, the circuit flows from the upper right, red positive line to the lower left, blue negative power rail. The circuit flow is as follows:

  1. Power flows from the positive battery terminal through the black wire to the positive power rail hole in the upper right (next to the red line and +)
  2. The power rail is connected vertically, so current flows downward to the hole where the resistor is wired.
  3. Current moves through the resistor, towards the middle of breadboard to the other hole the resistor is wired to.
  4. Current continues to the left inside the breadboard to where our first red wire is connected, just a few holes to the left.
  5. The wire carries the current down to the row with the blue LED.
  6. Current flows in the breadboard a few holes to where the LED is plugged in on its longer, anode, positive side. LEDs are a form of diode so current will only flow one direction so make sure the longer leg is plugged into this hole.
  7. Current flows into the LED and out the shorter (Cathode/negative) leg which is connected to the hole one row below the positive leg.
  8. The current moves to the right where our first blue wire is plugged in.
  9. The current flows down through the blue wire to negative rail next to the blue line.
  10. The current flows down along the negative rail tot he black wire.
  11. The black wire on the bottom left is hooked to the negative terminal of our battery, completing the circuit.

That’s basically it. As long as your LED is plugged in the right direction, you have the right resistor and your battery is charged, the LED should glow. And hopefully not explode.

Below are three pics of the circuit. The third one shows each of the steps above as current flows through a wire or the board to each compenent.

board with battery
board with batttery close up
breadboard showing current

Back to Code

The final step is controlling the external LED on our breadboard using Elixir Nerves code that we deploy to our Raspberry Pi. We need to whip up some code to do this. We’re going to add a small Elixir “application” file that will start up and begin blinking our LED.

First lets get rid of some auto-generated code that we do not need. Run the following in the terminal in the root of your nervest_heartbeat project folder:

rm -Rf lib/nerves_heartbeat

Next, we’ll add our own application which will start up and begin blinking our LEDs. Change (or create) the file lib/nerves_heartbeat.ex to contain this code:

defmodule NervesHeartbeat do
  use Application
  
  alias Circuits.GPIO
  require Logger

  @heartbeat_pin 16

  def start(_type, _args) do
    Logger.info("Starting pin #{@heartbeat_pin} as output")
    {:ok, heartbeat_gpio} = GPIO.open(@heartbeat_pin, :output)
    spawn(fn -> beat(heartbeat_gpio) end)    
  end

  def beat(heartbeat_gpio) do
    Logger.debug("Turning pin #{@heartbeat_pin} ON")
    GPIO.write(heartbeat_gpio, 1)
    Process.sleep(50)

    Logger.debug("Turning pin #{@heartbeat_pin} OFF")
    GPIO.write(heartbeat_gpio, 0)
    Process.sleep(50)

    Logger.debug("Turning pin #{@heartbeat_pin} ON")
    GPIO.write(heartbeat_gpio, 1)
    Process.sleep(50)

    Logger.debug("Turning pin #{@heartbeat_pin} OFF")
    GPIO.write(heartbeat_gpio, 0)
    Process.sleep(1000)

    beat(heartbeat_gpio)    
  end
end

Let’s look at this line by line.
use Application tells Nerves to start this as an “Application.” In Elixir, an application is a module which can create supervisors to control other child processes.

If you don’t understand processes and supervisors in Nerves, just know that we’ve told Nerves to start this code first. The mix.exs file is already onfigured to automatically start this NervestHeartbeat module. This article goes into more depth about supervisors and applications: Elixir Supervior and Application

We then alias Circuits.GPIO so we can type less and just reference GPIO in our code. We require Logger so we can log out messages about what’s happening

We assign the constant @heartbeat_pin to 16, which is the pin we want to talk to. You can use a variety of the GPIO pins for different purposes. You’ll want to use this diagram to figure out the right pin(s) to use for your projects. Note that the phsyical pins, which are numbered 1-40, are not the pin numbers you will reference in code. In my case, I’m using “BCM 16” pin, which is physical pin 36 but I refer to it by #16 in elixir when making calls to the GPIO API.

Elixir will call our start function, and we’re ignoring the first two parameters by putting an underscore in from of them.

The GPIO.open function call in start will open our GPIO pin for output (for writing to it as opposed to reading from it.) This needs to be initialized once at the beginning so we can use the GPIO pin in the rest of the code. We take the result of that function and assign the variable heartbea_gpio to it. In our case, we have assigned it pin #16 using our constant defined in @heartbeat_pin.

Next we spawn a new process using an anonymous function. This function will call the beat function we’ll create below and pass it the heartbeat_gpio variable representing the pin we want to talk to.

The majority of the code is in the beat function. We use GPIO.write and pass it the heartbeat_pin and either a one or zero, to turn current on or off to that pin. The rest is just logging and using Process.sleep to pause between “heartbeats”.

That’s really all there is to it. You might do a mix compile to make sure there aren’t any errors.

Hooking the Board to the Pi

You’ll want to disconnect the battery from the the board if you haven’t already. You’ll also need a couple of extra wires to plug into the Pi. We need the positive rail connected to our BCM #16 pin that I mentioned earlier, and we’ll need the negative pin on any ground pin to complete the circuit. You can see the wiring setup below, but keep in mind that my Pi is flipped upside down compared to the Pinout diagram I linked to above. Thus Pin 16 is on the upper left, third hole from the top in this photo. The ground I’m using is third from the bottom, also on the left.

Deploying over USB

We’re ready to deploy our code. Make sure you plug your USB cable back into your computer. We could remove the SD card and and burn our firmware again using mix firmware.burn but this time we’re going to use the USB connection.

Make sure you can ping nerves.local and you might check your network connection again if you’re not getting a response. Remember, we should be able to see the Pi as a CDC device in our network settings. At one point, I had messed up the code so bad that I never saw the Pi in my network settings and I couldn’t deploy or SSH to it. When I hooked up the Pi to a monitor, the Elixir/Erlang VM was crashing and the Pi was rebooting over and over again. I had to burn the SD card again from a valid state. Hopefully that won’t be the case for you.

To deploy, we need a way to push our firmware to the Pi. A project included in nerves called nerves_firmware_ssh generates a simple bash script to do this for us. At the time of this writing, there isn’t a way to push the firmware directly to the Pi from a simple mix task, but I believe that is the eventual goal so check the documentation. For me, I had to run the following in my terminal, in the root of my nerves project:

mix firmware.gen.script

You should see:

Writing upload.sh...

If you try this and don’t see anything in response, make sure your MIX_TARGET= is set to the Pi you’re using, either through export or putting the mix target before your mix command like so: MIX_TARGET=rpi0 mix firmware.gen.script. Without that environment variable, you can get silent failures. This one stumped me for about an hour! Most mix commands will tell you which type of firmware your building like so:

  Nerves environment
  MIX_TARGET:   rpi0
  MIX_ENV:      dev

if you do an ls in your terminal. You should now see an upload.sh file. This will push the firmware to our Pi. Let’s give that a shot.

mix firmware

This will create our .fw file just like what automatically got written to our SD card when we used mix firmware.burn previously. You should see:

Nerves environment
  MIX_TARGET:   rpi0
  MIX_ENV:      dev

|nerves_bootstrap| Building OTP Release...

==> You have strip_debug_info set to true.
    Please be aware that if you plan on performing hot upgrades later,
    this setting will prevent you from doing so without a rolling restart.
    You may ignore this warning if you have no plans to use hot upgrades.
Updating base firmware image with Erlang release...
Copying rootfs_overlay: /projects/nerves_heartbeat2/rootfs_overlay
Parallel mksquashfs: Using 8 processors
Creating 4.0 filesystem on /projects/nerves_heartbeat2/_build/_nerves-tmp/combined.squashfs, block size 131072.

Exportable Squashfs 4.0 filesystem, gzip compressed, data block size 131072
	compressed data, compressed metadata, compressed fragments, no xattrs
	duplicates are removed
Filesystem size 15956.73 Kbytes (15.58 Mbytes)
	50.77% of uncompressed filesystem size (31432.08 Kbytes)
Inode table size 19600 bytes (19.14 Kbytes)
	29.05% of uncompressed inode table size (67467 bytes)
Directory table size 21698 bytes (21.19 Kbytes)
	41.98% of uncompressed directory table size (51682 bytes)
Number of duplicate files found 11
Number of inodes 2061
Number of files 1654
Number of fragments 107
Number of symbolic links  142
Number of device nodes 0
Number of fifo nodes 0
Number of socket nodes 0
Number of directories 265
Number of ids (unique uids + gids) 4
Number of uids 3
	root (0)
	_appstore (33)
	craigbowes (504)
Number of gids 3
	wheel (0)
	_appstore (33)
	staff (20)
Building /projects/nerves_heartbeat2/_build/rpi0_dev/nerves/images/nerves_heartbeat.fw...

That last line shows you where the firmware is built.

Now let’s run our upload script which will deploy our heartbeat app. Fingers crossed! ./upload.sh

You should be greeted with:

Path: ./_build/rpi0_dev/nerves/images/nerves_heartbeat.fw
Product: nerves_heartbeat 0.1.0
UUID: 4f55791e-81fb-5146-c411-b19b1cc1b248
Platform: rpi0

Uploading to nerves.local...
Warning: Permanently added '[nerves.local]:8989,[172.31.18.193]:8989' (RSA) to the list of known hosts.
Running fwup...
fwup: Upgrading partition A
|====================================| 100% (22.58 / 22.58) MB
Success!
Elapsed time: 13.816 s
Rebooting...
Received disconnect from 172.31.18.193 port 8989:11: Terminated (shutdown) by supervisor
Disconnected from 172.31.18.193 port 8989

Wait a minute and your LED should start blinking!

If instead you see something like:

arp: nerves.local: Unknown host
Can't resolve nerves.local

…then it means your computer cannot connect to the Pi. Check all your USB connections obviously. You might also go into your network settings and “renew DHCP lease.” This has fixed the connection issue for me sveral times. Once your network settings show that your connected, try ./upload.sh again.

End of Line

This covers a lot of the basics of using Elixir, Nerves and Raspberry Pi for IoT style projects and devices. Hopefully this will clear the path for future projects with more complicated functionality.