Blinking LED using Elixir embedded image on Raspberry Pi

In the April Seattle Erlang/Elixir Meetup the awesome guys at Rose Point Navigation gave a demo of burning Elixir/Erlang application as part of a custom built image and then running it on Raspberry Pi. This was very interesting to me as I was also curious how to do that kind of thing. A custom built image allows you to keep your firmware size at very low and keep only necessary things so that we get better boot time etc.

In this post we will make the 'Hello World' of the embedded world, which is blinking a light. The hardware I am going to use will be Raspberry Pi (model A+) and we will blink the LED that is already part of Raspberry Pi.

We will do the following

  1. Create Elixir project for blinking LED
  2. Create image for Raspberry PI using Nerves
  3. Burning image on SD card
  4. Ooze over the blinking LED

Create Elixir blinking LED project

Lets create an Elixir project using mix and add the necessary dependencies.

$> mix new blinky

The application and deps in mix.exs should be as follows

...
  def application do
    [applications: [:logger],
     mod: {Blinky, []}]
  end

  defp deps, do: [
    { :exrm, "~> 0.15.0" }
  ]
...

Now lets write the code to blink the green LED on Raspberry Pi. Edit the lib/blinky.ex to look like this

defmodule Blinky do
  require Logger

  # Trigger file for LED0
  @led_trigger "/sys/class/leds/led0/trigger"

  # Brightness file for LED0
  @led_brightntess "/sys/class/leds/led0/brightness"

  def start(_type, _args) do
    # Setting the trigger to 'none' by default its 'mmc0'
    File.write(@led_trigger, "none")

    # Start blinking forever
    blink_forever
  end

  def blink_forever do
    # Turn on the green LED and sleep for 1000ms
    Logger.debug "Turning ON green"
    set_led(true)

    :timer.sleep 1000

    # Turn off the green LED and sleep for 1000ms
    Logger.debug "Turning OFF green"
    set_led(false)

    :timer.sleep 1000

    # Blink again
    blink_forever
  end

  # Setting the brightness to 1 in case of true and 0 if false
  def set_led(true), do: set_brightness("1")
  def set_led(false), do: set_brightness("0")

  def set_brightness(val) do
      File.write(@led_brightntess, val)
      |> inspect
      |> Logger.debug
  end
end

Lets see what we did there. Raspberry PI has some onboard LEDs like the power LED or the LEDs linked to ethernet.
The green LED on Raspberry Pi can be controller by the user by modifing few files. The files under /sys/class/leds/led0 control the green led on Raspberry Pi.

The files we care about are led0/trigger and led0/brightness. The trigger files defines that how the LED will be triggered, by default the green LED is trigged by any action on SD card hence its value is 'mmc0'. We set the trigger to none so that it is not linked to SD card.
The second step is to turn the LED on and off. The brightness file controls that. Though the range of value you can put inside it is 0..255 but as the brightness is not controllable so anything above 0 turns the green LED on.

In blinky.ex we first set the trigger to none and then turn on the green led, wait for 1s, turn off the green led, wait for 1s and then repeat the whole process again. Run mix do deps.get, compile to make sure everything compiles.

Create Image for Raspberry PI

Nerves-Project uses the Erlang/Elixir release and creates a bootable firmware image from that using Buildroot. The advantage of it is that its a shippable image which you can publish and anyone can burn that image onto the board/SD card and be done with it. This creates a bare bone image, stripping away all the useless things for embedded system such as video, UI etc. which in turn makes it bootup pretty quickly.

You can use the instructions at Nerves-Project Github to download the source code and compile it but it can take upto hour or more depending upon your machine. I am a sucker for Docker hence I have published an image that you can just pull and use, making the process a lot quicker.

Lets first pull in the docker image, run it and run few commands under it. If you are interested in seeing the Dockerfile for that image then you can see it here.

$> docker pull zabirauf/nerves-sdk-elixir
$> docker run -i -t -v /path/to/blinky:/opt/blinky zabirauf/nerves-sdk-elixir /bin/bash
root@bb9f59897a2a: cd /Downloads/nerves-sdk && source ./nerves-env.sh

We have to create a Makefile in our blinky project so that nerves can compile it and create an image.

/path/to/blinky: echo 'include $(NERVES_ROOT)/scripts/nerves-elixir.mk' > Makefile

So we just include the makefile that already comes with nerves.

Now in your docker terminal which is still running the docker container lets create the image

root@bb9f59897a2a: cd /opt/blinky
root@bb9f59897a2a: make

This will compile the project, create an erlang release from it and then use that release to create an image. After that is done your image would be under /path/to/blinky/_images. Lets see how big are the images run ls -alh under _images.

drwxr-xr-x   6 zohaibrauf  staff   204B Apr 25 03:31 .
drwxr-xr-x  18 zohaibrauf  staff   612B Apr 25 02:31 ..
-rw-r--r--@  1 zohaibrauf  staff   6.0K Apr 25 03:01 .DS_Store
-rw-r--r--   1 zohaibrauf  staff    15M Apr 25 03:31 blinky.fw
-rw-r--r--   1 zohaibrauf  staff   328M Apr 25 03:31 blinky.img
drwxr-xr-x  20 zohaibrauf  staff   680B Apr 23 23:05 rootfs

We can see that the blinky.fw is actually 15MB. But we will burn blinky.img to Raspberry Pi, you might wonder that why is that 328MB. If you use a hex viewer to open that, you will see that most of that is just 0 and actually everything i.e. Kernel, Erlang, Elixir and our application just adds upto 15MB.

Burning image on SD Card

I am using Mac so the instructions would be for Mac. If you are on Linux or Windows then you can get the instructions to burn .img on SD card by just searching for it.

First run diskutil list and see where is your SD card mounted (in my case its /dev/disk7). Then run the following

CAUTION: Wrong disk path can cause you to loose data for that disk. Make sure you are using the right SD card path.

$> diskutil unmountDisk /dev/disk7
$> sudo dd bs=1m if=/path/to/blinky/_images/blinky.img of=/dev/disk7

This will take some time and then once its done you can eject the SD card. If this does not work then use the alternative method mentioned at Raspberry Pi

Ooze over blinking LED

Insert the SD card into your Raspberry PI and power it up. Wait for few seconds and then you should see the blinking green LED.

You can get the complete project here.

What will be next

You can use the Elixir Ale to control the GPIO pins on Raspberry Pi. I will also explore that more in my next post and also look at some things open sourced by the people at Rose Point Navigation i.e. Cellulose, which will make discovery of these embedded devices easier and also provides easy controlling & updating firmware on the fly. You should check that out, though its in very early stages.