1. Nerves框架与嵌入式Elixir开发概述
Nerves是一个基于Elixir语言的嵌入式开发框架,它将Erlang虚拟机的强大并发能力与嵌入式系统的实时性要求完美结合。作为一名长期从事嵌入式开发的工程师,我第一次接触Nerves时就被它独特的开发模式所吸引——你可以在开发板上运行完整的Elixir/OTP环境,这在传统嵌入式开发中简直是天方夜谭。
与传统嵌入式开发相比,Nerves带来了几个革命性改变:
- 完整的Erlang虚拟机运行环境
- OTP监督树保障系统稳定性
- 热代码升级能力
- 函数式编程的可靠性优势
在硬件支持方面,Nerves官方支持包括Raspberry Pi系列、BeagleBone、NVIDIA Jetson等常见开发板。我推荐初学者从Raspberry Pi 3/4开始,因为它们价格亲民、资料丰富,而且板载LED方便我们进行基础实验。
2. 开发环境搭建与项目初始化
2.1 工具链安装
在开始之前,我们需要准备以下环境:
- Elixir 1.9+ (需匹配Erlang/OTP 21+)
- 特定平台的Nerves工具链
- 固件烧录工具fwup
在Ubuntu系统下,可以通过以下命令快速安装:
bash复制# 安装Erlang和Elixir
wget https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb
sudo dpkg -i erlang-solutions_2.0_all.deb
sudo apt-get update
sudo apt-get install esl-erlang elixir
# 安装Nerves依赖
sudo apt-get install build-essential automake autoconf git squashfs-tools ssh-askpass
mix archive.install hex nerves_bootstrap
2.2 创建第一个Nerves项目
使用mix命令创建项目骨架:
bash复制mix nerves.new hello_led
cd hello_led
关键文件结构说明:
config/:包含不同环境的配置lib/:主要业务逻辑代码rel/:发布相关配置rootfs_overlay/:自定义根文件系统内容
设置目标平台(以Raspberry Pi 3为例):
bash复制export MIX_TARGET=rpi3
mix deps.get
3. LED控制实现详解
3.1 硬件层配置
Raspberry Pi的板载LED通过GPIO控制,Nerves通过nerves_leds库提供了抽象接口。首先在mix.exs中添加依赖:
elixir复制defp deps do
[
{:nerves_leds, "~> 0.8", targets: @all_targets}
]
end
然后在config/config.exs中配置LED映射:
elixir复制config :nerves_leds, names: [green: "led0"] # Raspberry Pi的板载LED标识
3.2 OTP进程实现
我们使用GenServer来实现LED的状态管理,创建lib/hello_led/led_controller.ex:
elixir复制defmodule HelloLed.LedController do
use GenServer
require Logger
alias Nerves.Leds
# 客户端API
def start_link(_opts) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def turn_on, do: GenServer.cast(__MODULE__, :on)
def turn_off, do: GenServer.cast(__MODULE__, :off)
def blink(interval), do: GenServer.cast(__MODULE__, {:blink, interval})
# 服务器回调
def init(_) do
Leds.set(green: false) # 初始状态关闭
{:ok, %{blinking: false}}
end
def handle_cast(:on, state) do
Leds.set(green: true)
{:noreply, %{state | blinking: false}}
end
def handle_cast(:off, state) do
Leds.set(green: false)
{:noreply, %{state | blinking: false}}
end
def handle_cast({:blink, interval}, %{blinking: false} = state) do
Process.send_after(self(), :toggle, interval)
Leds.set(green: true)
{:noreply, %{state | blinking: true, interval: interval}}
end
def handle_info(:toggle, %{blinking: true, interval: interval} = state) do
Leds.toggle(:green)
Process.send_after(self(), :toggle, interval)
{:noreply, state}
end
end
3.3 监督树集成
在lib/hello_led/application.ex中添加子进程规范:
elixir复制def children(_target) do
[
{HelloLed.LedController, []}
]
end
4. 网络控制接口实现
4.1 静态IP配置
修改config/target.exs配置网络:
elixir复制config :vintage_net,
regulatory_domain: "US",
config: [
{"eth0",
%{
type: VintageNetEthernet,
ipv4: %{
method: :static,
address: "192.168.1.100",
prefix_length: 24,
gateway: "192.168.1.1",
name_servers: ["8.8.8.8"]
}
}}
]
4.2 Web控制接口
添加Plug.Cowboy依赖后,创建lib/hello_led/web_controller.ex:
elixir复制defmodule HelloLed.WebController do
use Plug.Router
plug(:match)
plug(:dispatch)
get "/" do
send_resp(conn, 200, """
LED Control API:
GET /on - Turn LED on
GET /off - Turn LED off
GET /blink/:ms - Blink with interval (ms)
""")
end
get "/on" do
HelloLed.LedController.turn_on()
send_resp(conn, 200, "LED ON")
end
get "/off" do
HelloLed.LedController.turn_off()
send_resp(conn, 200, "LED OFF")
end
get "/blink/:interval" do
case Integer.parse(interval) do
{ms, _} when ms > 50 ->
HelloLed.LedController.blink(ms)
send_resp(conn, 200, "Blinking every #{ms}ms")
_ ->
send_resp(conn, 400, "Interval must be >50ms")
end
end
end
在application.ex中添加:
elixir复制{Plug.Cowboy, scheme: :http, plug: HelloLed.WebController, options: [port: 80]}
5. 固件构建与部署
5.1 本地SD卡烧录
bash复制mix firmware
mix firmware.burn
5.2 网络部署(高级)
生成上传脚本并部署:
bash复制mix firmware.gen.script
./upload.sh 192.168.1.100
6. 调试技巧与常见问题
6.1 IEx控制台访问
通过SSH连接到设备控制台:
bash复制ssh 192.168.1.100
6.2 典型问题排查
-
LED不响应:
- 检查GPIO编号是否正确
- 确认nerves_leds版本与硬件匹配
- 查看dmesg输出确认GPIO权限
-
网络连接失败:
elixir复制# 在IEx中检查网络状态 VintageNet.info -
固件烧录失败:
- 确保SD卡已正确格式化
- 检查MIX_TARGET环境变量设置
- 尝试
mix firmware.burn --force
7. 进阶扩展思路
-
添加Phoenix Web界面:
elixir复制defp deps do [ {:phoenix, "~> 1.5", targets: @all_targets} ] end -
实现OTA远程升级:
elixir复制config :nerves_firmware_ssh, authorized_keys: [ File.read!(Path.join(System.user_home!(), ".ssh/id_rsa.pub")) ] -
添加传感器集成:
elixir复制# 使用Circuits库读取传感器 {:circuits_gpio, "~> 0.4", targets: @all_targets}
在实际项目中,我发现Nerves最大的优势在于其可靠性。曾经有一个商业项目需要7x24小时运行,传统C语言实现平均每两周就会因内存泄漏崩溃一次,而改用Nerves后连续运行了6个月没有任何问题。这种稳定性正是Erlang虚拟机带来的核心价值。
