
官网 https://nerves-project.org/
文档 https://hexdocs.pm/nerves/getting-started.html
上一篇 Nerves环境配置 中我们在 Ubuntu 虚拟机上配置好了 Nerves 的开发环境。正所谓万事开头难,又所谓事事难开头。既然开了头,那就走两步。
如果是新安装 Erlang 和 Elixir 环境的话,首先我们需要安装 Elixir 和 Erlang 的包管理器:
elixir
mix local.hex
mix local.rebar
hex 是 Elixir 的包管理器,rebar3 是 Erlang 的包管理器。上面的命令既可以用来安装 hex 和 rebar3,也可以用来更新它们。
然后从 hex 源安装 nerves 包:
elixir
mix archive.install hex nerves_bootstrap
严格来说,这里只是安装 nerves 的 mix 任务,而不是 nerves 库,这和 Phoenix 是一样的。之后我们也可以通过下面的命令来更新 nerves。
elixir
mix local.nerves
Hello World
找一个合适的地方,在命令行运行下面的命令创建 nerves 工程:
elixir
mix nerves.new hello_nerves
进入 hello_nerves 目录,可以看到它也只是一个普通的带监督树的 Elixir 工程而已。在开始之前,我们需要先生成 SSH 公钥,如果已有公钥的话,可以跳过这一步。
bash
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
邮箱换成你自己的,然后一直回车保持默认选项就可以了,公钥会用于树莓派和主机之间的网络通信。如果这里你修改了公钥名称或者存放路径,可能会导致 nerves 找不到公钥,那时就需要去修改 config/target.exs 配置文件了,默认配置如下:
elixir
keys =
System.user_home!()
|> Path.join(".ssh/id_{rsa,ecdsa,ed25519}.pub")
|> Path.wildcard()
下一步下载依赖。
bash
cd hello_nerves
MIX_TARGET=rpi4 mix deps.get
Nerves 需要通过环境变量指定目标平台,我的开发板是树莓派4,所以是 rpi4,其他平台可以参考官方文档👈点击直达👈。
环境变量也可以使用 export MIX_TARGET=rpi4 来设置。如果这一步报错,提示你"需要依赖 nerves",那可能就是 Erlang 版本太低了,参考Nerves环境配置安装一个最新版的 Erlang 应该就可以了。
接下来构建镜像。
bash
MIX_TARGET=rpi4 mix firmware
如果是首次使用的话,需要将生成的镜像写到SD卡里面。取下树莓派上的SD卡,插入电脑里面,如果电脑没有SD卡接口,需要准备一个读卡器。将SD卡插入电脑后,用下面的命令写入固件。
bash
MIX_TARGET=rpi4 mix burn
如果使用的是虚拟机的话,插入USB后,会有弹窗提示,选择将USB设备连接到虚拟机。固件烧录时,会自动识别出SD卡。

烧录完成后,取出SD卡插回树莓派,连上电源和HDMI显示器,系统开机后,屏幕上会显示一些信息和 iex> 命令提示符,这和我们在电脑上运行 iex 时是一样的。将键盘连接到树莓派,就可以和 Elixir 交互式 shell 愉快的玩耍了。
树莓派4的 type c 接口既可以供电,也支持数据传输,将他和电脑主机相连,就得到了一个树莓派和主机之间的USB网络连接。树莓派4既可以连接WiFi,也可以连接网线,Nerves 默认还提供了基于USB的网络接口,这在开发调试阶段非常有用。
Nerves 在树莓派和主机之间使用 mDNS 做为内网主机发现服务,我们不需要知道树莓派的具体IP地址,而是可以通过 nerves.local 主机名(在config/target.exs中配置)来访问树莓派。比如我们可以在虚拟机的 shell 中通过 ssh 来连接到树莓派。
bash
ssh nerves.local
不出意外的话,我们会看到和连接到树莓派的屏幕上相同的内容。我们既可以在这里与树莓派交互,也可以将键盘连接到树莓派,选择一种你喜欢的方式就好。

我们在 iex shell 中输入 HelloNerves.hello() 回车,不出意外的话,会看到输出 :world,这正是 lib/hello_nerves.ex 的内容。
只要烧录过一次镜像,修改代码并运行 mix firmware 重新生成镜像之后,我们就不再需要取出SD卡 mix burn 了,而是可以直接 mix upload 上传新固件。
bash
MIX_TARGET=rpi4 mix upload
点亮 LED
Nerves 官方文档是通过 GPIO 点亮外接 LED 灯,作为 Hello World 还有有点超纲了,所以我们先从点亮板载 LED 开始。
原理
Linux 将一切设备都抽象成了文件,板载 LED 也不例外。树莓派4有两颗板载 LED:一颗红色电源指示灯;一颗绿色SD卡活动指示灯。分别对应 /sys/class/leds/ 目录下的 PWR 和 ACT 两个目录。它俩都是软链接,链接到的是 /sys/devices/platform/leds/leds/ 目录下的 PWR 和 ACT 目录。

具体控制 LED 的是 PWR 和 ACT 目录下的文件。比如brightness 文件用来控制 LED 灯的亮度,它里面是一个数字,0 表示熄灭,1(或255) 表示点亮。trigger 文件用来控制 LED 灯的行为,比如闪烁,或者根据某些条件亮灭。打开 ACT 目录下的 trigger 文件,可以看到下面的内容:
none rfkill-any rfkill-none timer oneshot heartbeat backlight default-on transient input panic pattern mmc1 [mmc0] rfkill0
它是一个空格分隔的 LED 行为列表,[] 表示当前选中的行为模式。
| 模式 | 说明 |
|---|---|
| none | 不触发任何模式,LED不会自动亮起或闪烁。 |
| rfkill-any | 当任何无线设备被禁用时,LED会亮起。 |
| rfkill-none | 当没有无线设备被禁用时,LED会亮起。 |
| timer | LED会按照设定的时间间隔闪烁。 |
| oneshot | LED会闪烁一次。 |
| heartbeat | LED会按照心跳模式闪烁。 |
| backlight | LED的亮度会根据背光设置变化。 |
| default-on | LED默认保持亮起。 |
| transient | LED会在特定事件发生时短暂亮起。 |
| input | LED会在接收到输入事件时亮起。 |
| panic | 在系统崩溃时,LED会闪烁。 |
| pattern | LED会按照预设的模式闪烁。 |
| mmc1 | 当SD卡槽1有活动时,LED会亮起。 |
| mmc0 | 表示当SD卡槽0有活动时,LED会亮起。 |
| rfkill0 | 当无线设备0被禁用时,LED会亮起。 |
如果我们希望通过 brightness 文件来控制 LED 灯,需要将模式设置为 none。
echo none > brightness
brightness 文件比较特殊,对它写入 none 会自动变成 [none] ... :
[none] rfkill-any rfkill-none timer oneshot heartbeat backlight default-on transient input panic pattern mmc1 mmc0 rfkill0
LED控制
以上是板载 LED 控制的原理,对于编程,我们可以使用 nerves_leds 库,它封装了板载 LED 的控制接口。首先我们 mix.exs 文件中添加依赖:
elixir
defp deps do
[
... ...
# Dependencies for all targets except :host
{:nerves_leds, "~> 0.8", targets: @all_targets},
... ...
]
end
然后获取依赖:
bash
MIX_TARGET=rpi4 mix deps.get
随后在 config.exs 文件中添加一行配置:
elixir
config :nerves_leds, names: [green: "ACT"]
这里的配置是可选的,有这个配置,后面我们可以通过 :green 来访问 LED,不配置的话也可以直接通过 "ACT" 来访问这个 LED。
接下来我们用一个 GenServer 来控制 LED,在 lib/hello_nerves/ 目录下创建 blinker.ex 文件,输入以下内容:
elixir
defmodule HelloNerves.Blinker do
use GenServer
require Logger
alias Nerves.Leds
def start_link(state \\ []) do
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
def init(state) do
enable()
{:ok, state}
end
def handle_cast(:enable, state) do
Logger.info("Enabling LED")
Leds.set(green: true)
{:noreply, state}
end
def handle_cast(:disable, state) do
Logger.info("Disabling LED")
Leds.set(green: false)
{:noreply, state}
end
def enable() do
GenServer.cast(__MODULE__, :enable)
end
def disable() do
GenServer.cast(__MODULE__, :disable)
end
end
这里我们通过 Leds.set 来控制 LED 灯的亮灭,true 表示点亮,false 表示熄灭。:green 就是前面我们配置的 LED 灯的名字。如果没配置,我们也可以直接使用 "ACT" 来访问它。最后我们在 lib/hello_nerves/application.ex 中将 HelloNerves.Blinker 添加到监督树。
elixir
defp target_children() do
[
# Children for all targets except host
# Starts a worker by calling: Target.Worker.start_link(arg)
# {Target.Worker, arg},
{HelloNerves.Blinker, name: HelloNerves.Blinker}
]
end
注意,Nerves 有两个监督树,我们要将它添加到设备端的监督树中。将树莓派链接到电脑,我们就可以重新编译并上传新固件了。
bash
MIX_TARGET=rpi4 mix firmware
MIX_TARGET=rpi4 mix upload
等待上传完成,树莓派会重新启动,运行 ssh nerves.local 连接到树莓派,然后我们就可以通过 HelloNerves.Blinker.enable() 和 HelloNerves.Blinker.disable() 来控制 LED 灯的亮灭了。
网络接口
既然 Nerves 也只是一个普通的 Elixir 工程而已,那么添加一个网络接口,通过网络远程遥控 LED 灯想必不是一件难事。编写 HTTP 接口需要用到 cowboy 库,首先还是添加依赖:
elixir
defp deps do
[
# Dependencies for all targets
{:plug_cowboy, "~> 2.0"},
... ...
]
end
然后获取依赖:
bash
MIX_TARGET=rpi4 mix deps.get
在 lib/hello_nerves/ 下添加 http.ex 文件,输入以下内容:
elixir
defmodule HelloNerves.Http do
use Plug.Router
plug(:match)
plug(:dispatch)
get("/", do: send_resp(conn, 200, "Feel free to use API endpoints!"))
get "/enable" do
HelloNerves.Blinker.enable()
send_resp(conn, 200, "LED enabled")
end
get "/disable" do
HelloNerves.Blinker.disable()
send_resp(conn, 200, "LED disabled")
end
match(_, do: send_resp(conn, 404, "Oops!"))
end
同样,它也需要添加到监督树中:
elixir
defp target_children() do
[
# Children for all targets except host
# Starts a worker by calling: Target.Worker.start_link(arg)
# {Target.Worker, arg},
{HelloNerves.Blinker, name: HelloNerves.Blinker},
{Plug.Cowboy, scheme: :http, plug: HelloNerves.Http, options: [port: 80]}
]
end
最后编译并上传固件:
bash
MIX_TARGET=rpi4 mix firmware
MIX_TARGET=rpi4 mix upload
上传完成后,打开浏览器,输入 nerves.local 回车,应该能看到 Feel free to use API endpoints! 的响应。

访问 nerves.local/enable 和 nerves.local/disable 可以点亮或熄灭 LED。


既然 HTTP 接口可以,那么 Phoenix 和 Live View 呢?自然也不在话下,但那已经不是本期的主题了。
Nerves工程 vs Elixir工程
Nerves 工程就是一个带监督树的 Elixir 工程,所以从结构和语法来说,它和 Elixir 工程是没有区别的,只是内容上和我们常看到的 Elixir 工程不太一样。
首先是 mix.exs 的依赖部分,有些依赖多了 :target 选项,而且整个依赖库也被清晰的分成了三大部分:
- 通用依赖:平台和主机都需要的依赖;比如
plug_cowboy。 - 平台依赖:除主机以外所有平台需要的依赖;比如
nerves_leds。 - 特定平台依赖:特定于目标平台的依赖;比如目标平台预编译镜像
nerves_system_xxx。
其次配置文件也分成了主机配置 host.exs 和目标平台配置 target.exs,由 config.exs 根据平台加载。
最后是 application.ex 中的监督树也分成了主机端 target_children 和目标平台端 target_children。
这些只是在 Elixir 工程上做的一些小小的变化,整体来说,它还是一个带有监督树的 Elixir 工程。
iex vs bash
与普通 Linux 系统不同,Nerves 系统启动后,提供的用户交互接口并不是 bash,而是 iex。Nerves 定制的 Linux 系统并没有将 bash 包含进去,因此在 Nerves 系统中,bash 确实是无法使用的,Nerves 官网也说了,如果你一定需要 bash,那么 Nerves 将不会是你的最佳选择。虽然 bash 用不了,但是对于熟悉 Erlang 和 Elixir 的观众来说,iex 也未尝不利。
首先是 Erlang 的标准库 c 提供了一些命令行接口函数,这里 "c" 是 "command" 的缩写。比如我们熟知的 cd、ls、pwd 等都是来自这个库。需要注意的是,这些都是 Erlang 函数,由于 Erlang 函数调用是可以省略括号的,因此对于无参函数,如 pwd,ls 我们可以像在 bash 中一样使用。但是对于有参数的情况就有所不同了,比如 cd "/bin",ls "/bin" 等,他们的参数是字符串,需要加上 ""。
其次是 Toolshed 库也提供了一些常用的命令,使用之前需要先 use Toolshed 导入这个库,但是 Nerves 启动 iex 之前已经帮我们导入好了。它为我们提供了 uname,ping,uptime,date,lsof 等常用命令,我们也可以通过 h Toolshed 来查看帮助文档。如果提示函数不存在,可以手动导入一下 Toolshed。
最后还有兜底的 cmd 函数,Erlang 的 os 库和 Toolshed 都提供了这个函数,后者调用的是 Elixir 的 System.cmd/3 。cmd 也是函数,我们可以将想要调用的命令作为参数传递给它,但是要加上 "",因为它的参数是一个字符串。比如 cmd "ls -l",cmd "echo 255 > brightness" 等。
命令总结
| 命令 | 说明 |
|---|---|
mix nerves.new |
生成 nerves 工程 |
mix firmware |
构建镜像 |
mix firmware.gen.script |
生成固件上传脚本 upload.sh |
mix burn |
烧录固件 |
mix upload |
上传固件 |