Nerves Framework

repository·main·Indexed 25 days ago

https://github.com/nerves-project/nerves

A framework for building small, self-contained, and reliable embedded software images using Elixir and the Erlang virtual machine on top of the Linux kernel. It provides core components like Erlinit, Nerves.Bootstrap, and Nerves.Runtime, with support for various hardware platforms including Raspberry Pi, BeagleBone, and x86_64. The ecosystem includes VintageNet for networking and Circuits for hardware access (GPIO, I2C, SPI, UART), utilizing C/C++ cross-toolchains for consistent builds.

Tokens
40.1K
Snippets
89
Records
196
Agent score
82%

What's inside Nerves

  1. Overview of Nerves Framework and Core Projects

    main

    Nerves is a framework for crafting and deploying bulletproof embedded software in Elixir. The ecosystem is distributed across multiple repositories to maintain a focused scope. The core tooling and documentation are located in the main nerves-project/nerves repository.

    Key core components include:

    • Erlinit: A replacement for /sbin/init that launches an Erlang/OTP Release.
    • Nerves.Bootstrap: The new project generator and low-level hooks into Mix.
    • Nerves.Runtime: General runtime utilities for Nerves devices.
    • NervesPack: Initialization setup for Nerves devices.
    • NervesSystemBR: A Buildroot-based build platform for Nerves Systems.
    • RingLogger: A ring buffer backend for Elixir Logger with IO streaming.
  2. Overview of Nerves

    main

    Nerves is a framework for crafting and deploying bulletproof embedded software using Elixir and the Erlang virtual machine. It leverages the hardware support of Linux to run microprocessor-based embedded systems while providing a high-level development experience.

    Key characteristics:

    • Small and Self-Contained: Nerves only includes the libraries you explicitly use, allowing for minimal software images.
    • Erlang-Centric: It starts the Erlang runtime as one of the first OS processes, letting Elixir/Erlang manage the system.
    • Linux-Based: It uses the Linux kernel for hardware support but is not a traditional Linux distribution. It provides access to packages via Buildroot if Linux-level tools are required.
    • Ecosystem Integration: You can use standard Elixir libraries such as Phoenix/LiveView (for web UIs), Nx (for machine learning), Livebook (for interactive notebooks), and Scenic (for on-screen UIs).
  3. Use Elixir Circuits to interface with hardware

    main

    To communicate with hardware devices (like GPIO pins) on a Nerves target, use the Elixir Circuits library. It provides the necessary interfaces to control components like LEDs and buttons directly from Elixir code.

    Key resources for getting started:

    • Circuits GPIO: Use this for direct hardware control in your own firmware. Documentation: Circuits.GPIO Docs.
    • Quickstart Guide: A dedicated guide for beginners: Circuits Quickstart.
    • Example Projects: Refer to nerves_examples for real-world implementations:
      • blinky: Blinking an onboard LED.
      • hello_gpio: Controlling an external LED and reading a manual switch.
  4. Customize GCC flags using TARGET_GCC_FLAGS

    main

    The TARGET_GCC_FLAGS environment variable allows you to pass additional options to gcc invocations. Nerves tooling prepends the contents of TARGET_GCC_FLAGS to the CFLAGS and CXXFLAGS used when compiling NIFs and ports.

    This is useful for enabling specific CPU features (like ARM NEON support) that might not be enabled by default in the cross-compiler toolchain. While most users do not need to set this, it is a powerful way to optimize NIFs for specific hardware.

    Note: If you are creating a custom Nerves system, omitting TARGET_GCC_FLAGS is usually fine, but NIFs and ports will be built with generic compiler options.

  5. Handling hardware initialization in Nerves

    main

    Unlike traditional Linux distributions, Nerves does not use init scripts (like /etc/init.d/ or systemd). There is no shell running at boot to execute setup scripts.

    If your hardware requires specific initialization steps at startup (e.g., loading firmware blobs, toggling GPIOs, or configuring peripherals), you must handle these within your Elixir application. The recommended approach is to create an OTP application that performs the necessary setup when it starts. You can use libraries like Circuits.GPIO or call into small C programs to perform these tasks.

  6. Understand the relationship between Targets and Systems

    main

    In Nerves, there is a distinction between a Target and a System:

    • Target: A short, arbitrary tag name used in your project (e.g., rpi4) to select configuration via the MIX_TARGET environment variable.
    • System: A library (usually hosted on hex.pm) that provides the actual low-level components required for a device, such as the bootloader, Linux kernel, and C libraries (e.g., nerves_system_rpi4).

    Nerves uses the Mix target feature to map a Target tag to a specific System library within your mix.exs file. By convention, target tags are often named after the system they use.

  7. Understand Nerves target metadata (TARGET_*) variables

    main

    Nerves systems optionally define TARGET_* environment variables to describe the target hardware. These are primarily used to guide the compilation of LLVM-based tools and are defined within the Nerves system's mix.exs.

    Common variables include:

    • TARGET_ARCH: The target CPU architecture (e.g., arm, aarch64, x86_64).
    • TARGET_CPU: The specific target CPU (e.g., cortex_a7).
    • TARGET_OS: The target OS (always linux for Nerves).
    • TARGET_ABI: The target ABI (e.g., gnueabihf, musl).

    To see valid combinations of these targets for LLVM/Zig, you can install zig and run:

    zig targets | less
  8. Best practices for compiling native code in Nerves

    main

    To avoid common errors when integrating native code, follow these three critical rules:

    1. Always compile under _build: Never compile directly in the source directory. Compiling in the source directory often leads to host-architecture executables being accidentally included in the target build, resulting in the ERROR: Unexpected executable format error.
    2. Do not have a priv directory in your source tree: Elixir's default behavior of copying files from a source priv directory to the build output can cause confusion with native code. Instead, use a Makefile or mix.exs to explicitly copy static assets to the output priv directory.
    3. Prefer ports over NIFs: If you have a choice, use Erlang ports to interface with external code. Ports run in a separate OS process, so if they crash, Linux cleans up the mess. If a NIF (Native Implemented Function) crashes, the entire BEAM crashes, causing Nerves to reboot the device.
  9. Configure firmware partition layouts with fwup.conf

    main

    Nerves uses the fwup tool to create firmware images, burn them to SD cards, and perform OTA updates. The fwup.conf file defines the partition layout and tasks for creating or upgrading firmware.

    Standard MBR Layout (ARM/U-Boot/SD Card)

    Most ARM boards use a dual A/B scheme for safe OTA updates:

    • U-Boot env: A small key/value store for firmware metadata (active slot, validation status).
    • Boot Slots (A & B): FAT32 partitions holding the kernel, device tree, and boot script.
    • Rootfs Slots (A & B): Read-only squashfs partitions.
    • App data: A writable partition (e.g., F2FS).

    Boards with U-Boot SPL

    Some SoCs (Allwinner, Rockchip, TI, NXP) require a first-stage loader (SPL) to be raw-written at a specific offset before any partitions. For example, Allwinner's SPL typically goes at block 16 (8 KiB offset). Always verify the exact offset in the U-Boot board-specific documentation.

    +----------+--------+----------+----------+-------------+-----------+
    | U-Boot   | Boot A | Boot B   | Rootfs A | Rootfs B    | App data  |
    | env      | (FAT)  | (FAT)    | (squash) | (squash)    | (F2FS)    |
    +----------+--------+----------+----------+-------------+-----------+
  10. Capture logs across reboots with RamoopsLogger

    main

    The RamoopsLogger is a logger backend that uses the Linux pstore driver to write logs to a special memory region that survives reboots. This is critical for debugging unexpected crashes or reboots where disk-backed logs might be lost due to caching or filesystem unmounting.

    Note: :ramoops_logger is not added to Nerves projects by default; you must register it manually in your application configuration.

  11. Use RingLogger to view past log messages

    main

    Nerves projects often include RingLogger, an in-memory logger backend that prevents Flash wear by storing logs in RAM. Because it is in-memory, logs are lost on reboot and old messages are eventually discarded.

    • Use RingLogger.next to print newly received log messages.
    • Use RingLogger.reset to start reading from the oldest available message in the ring.
  12. How Linux boots on embedded devices

    main

    Understanding the Linux boot sequence is essential for debugging Nerves porting issues. A standard Linux system consists of three parts:

    1. Bootloader (e.g., U-Boot, Barebox, GRUB): Initializes hardware (CPU, RAM, storage), loads the kernel into memory, and starts it.
    2. Kernel: Manages processes, memory, filesystems, and hardware via device drivers. It relies on a device tree (DTB) to understand the specific SoC and peripheral configuration.
    3. Root filesystem (rootfs): Contains binaries, libraries, and your application. The kernel mounts this and executes the first userspace process (PID 1). On Nerves, PID 1 is erlinit.

    Boot Sequence: Power on $\rightarrow$ Bootloader $\rightarrow$ Kernel $\rightarrow$ mounts rootfs $\rightarrow$ runs PID 1