My ultimate goal for Matrix Voice w/ ESP32 is to be able to develop in Rust.
Basic Toolchain
Previously we used their recommend dev environment of PlatformIO. It’s also possible to do development without using PlatformIO. Matrix-io provides matrixio_hal_esp32 C/C++ repository for programming of Matrix Voice with ESP32. The Github repo and this hackster post contain details.
From the initial tests we know Matrix Voice depends on release v1.9.0
of platformio’s espressif toolchain. Looking at platformio releases this corresponds to ESP-IDF v3.2.2.
The guide to install v3.2.2 (there’s also stable as well as latest/HEAD) is detailed and boils down to:
- ESP32 toolchain/pre-requisites (for Mac, Linux, Windows)
- Install ESP-IDF and set
IDF_PATH
- Install required python packages
On Mac/Linux:
# ESP32 toolchain/pre-requisites
sudo easy_install pip
mkdir -p ~/esp
cd ~/esp
curl -O https://dl.espressif.com/dl/xtensa-esp32-elf-osx-1.22.0-80-g6c4433a-5.2.0.tar.gz
tar -xzf xtensa-esp32-elf-osx-1.22.0-80-g6c4433a-5.2.0.tar.gz
# Add to PATH
export PATH=$HOME/esp/xtensa-esp32-elf/bin:$PATH
# Install ESP-IDF v3.2.2
git clone -b v3.2.2 --recursive https://github.com/espressif/esp-idf.git
# Set IDF_PATH
export IDF_PATH=$HOME/esp/esp-idf
# Install required python packages
python -m pip install --user -r $IDF_PATH/requirements.txt
Build and run matrixio_hal_esp32 example:
git clone https://github.com/matrix-io/matrixio_hal_esp32.git
cd matrixio_hal_esp32/examples/everloop_demo
# Mac Only: Install bison
brew install bison
export PATH=/usr/local/opt/bison/bin:$PATH
make menuconfig
make -j4
export RPI_HOST=pi@raspberrypi.local
make deploy
The deploy script currently requires a small hack to work correctly (see this issue).
Rust-ified
There’s an excellent write-up of the current state of ESP32 development in Rust:
- LLVM support for Xtensa/ESP32 (not yet) in mainline
- Rust support also not in mainline (yet)
- Need
no_std
-i.e. excluding the standard library
Building and installing custom versions of LLVM/Rust along with configuring cross-compiling is a bit onerous, but the author put together a docker image:
git clone https://github.com/ctron/rust-esp-container.git
cd rust-esp-container
git submodule update --init --recursive
cd ../matrix-io
docker run -it -v $(pwd):/home/matrix-io quay.io/ctron/rust-esp /bin/bash
matrix_hal_esp32_sys
The components/hal/
directory of matrix_hal_esp32 contains C++ to access Matrix Voice-specific functionality. Let’s pass it through bindgen to generate a Rust FFI wrapper matrix_hal_esp32_sys
.
With a basic Makefile
:
PROJECT_NAME := esp-app
EXTRA_COMPONENT_DIRS += $(PROJECT_PATH)/matrixio_hal_esp32/components
include $(IDF_PATH)/make/project.mk
include $(PROJECT_PATH)/matrixio_hal_esp32/make/deploy.mk
This bindgen-project
script based off the original.
use std::{env, path::PathBuf};
fn main() {
link_nng();
bindgen_generate();
}
fn link_nng() {
let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
println!(
"cargo:rustc-link-search=native={}", root.join("build/hal/").display()
);
// Link to matrixio_hal_esp32 generated `hal` library
println!("cargo:rustc-link-lib=static=hal");
}
The main thing being linking to build/hal/libhal.a
generated by make
:
# Generate sdkconfig
make menuconfig
# Populate `build/`
make -j4
# From: https://github.com/ctron/rust-esp-container/blob/master/Dockerfile
export LIBCLANG_PATH=/home/esp32-toolchain/llvm/llvm_install/
rustup toolchain link xtensa /home/esp32-toolchain/rustc/rust_build/
cargo install cargo-xbuild bindgen
./bindgen-project
# From `xbuild-project` script
cargo +xtensa xbuild --target "${XARGO_TARGET:-xtensa-esp32-none-elf}" --release
Make sure to build --release
of you’ll get the mysterious error: Error: CFI is not supported for this target
. Debug configuration isn’t yet supported, but should be shortly (see issues #1 and #2).
“Hello World”
esp-idf contains several esp_log_*
funtions, but they don’t work here. There’s also several ESP_EARLY_LOGx
macros which ultimately call ets_printf()
enabling you to write “hello world”:
#![no_std]
#![no_main]
#[no_mangle]
pub fn app_main() {
unsafe {
use matrix_hal_esp32_sys::*;
// `b`-prefix creates byte string, `\0` null-terminates it
let text = b"Hello World\n\0";
// `ets_printf()` takes a null-terminated `* const u8`
ets_printf(text.as_ptr() as *const _);
}
}
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#![no_std]
precludes using the standard library (see embedded book)#![no_main]
lets us build a binary withoutmain()
- we instead haveapp_main()
#[no_mangle]
ensuresapp_main()
symbol isn’t mangled in way unreconizable to native linkers (see nomicon and embedded)#[panic_handler]
handlespanic!()
inno_std
applications (see nomicon and reference)
# From `image-project` script
"${IDF_PATH}/components/esptool_py/esptool/esptool.py" \
--chip esp32 \
elf2image \
-o build/esp-app.bin \
../../target/xtensa-esp32-none-elf/release/everloop
install.sh
comes from esp32-platformio, and is basically identical to flash-project
in docker using esp_tool
to push the build to the device.
Previously we used screen to monitor console output from the ESP32, but tail
is better since we can still write images to the serial port white capturing output:
# On Raspberry Pi:
tail -f /dev/ttyS0
# OR
screen /dev/ttyS0 115200
Everloop
Matrix-io provides an example everloop_demo
.
The C++:
#include <stdio.h>
#include <cmath>
#include "esp_system.h"
#include "everloop.h"
#include "everloop_image.h"
#include "voice_memory_map.h"
#include "wishbone_bus.h"
namespace hal = matrix_hal;
int cpp_loop() {
hal::WishboneBus wb;
wb.Init();
hal::Everloop everloop;
hal::EverloopImage image1d;
everloop.Setup(&wb);
unsigned counter = 0;
int blue = 0;
while (1) {
// Pulsing blue
blue = static_cast<int>(std::sin(counter / 64.0) * 10.0) + 10;
for (hal::LedValue& led : image1d.leds) {
led.red = 0;
led.green = 0;
led.blue = blue;
led.white = 0;
}
// Set the LEDs
everloop.Write(&image1d);
++counter;
}
return 0;
}
extern "C" {
// Entry point
void app_main(void) { cpp_loop(); }
}
If you’ve seen any of the Rust/Lumberyard stuff I’ve been experimenting with, or you’ve tried on your own codebases, you’re familiar with bindgen’s limitations regarding C++ and especially STL.
//----------------------------------------------------
// everloop_image.h
const int kMatrixCreatorNLeds = 18;
// An array of 18 LED values
class EverloopImage {
public:
EverloopImage(int nleds = kMatrixCreatorNLeds) { leds.resize(nleds); }
std::vector<LedValue> leds;
};
//----------------------------------------------------
// everloop.cpp
bool Everloop::Write(const EverloopImage* led_image) {
if (!wishbone_) return false;
// Create array of 18*4 bytes
std::valarray<unsigned char> write_data(led_image->leds.size() * 4);
// Fill RGB-White values
uint32_t led_offset = 0;
for (const LedValue& led : led_image->leds) {
write_data[led_offset + 0] = led.red;
write_data[led_offset + 1] = led.green;
write_data[led_offset + 2] = led.blue;
write_data[led_offset + 3] = led.white;
led_offset += 4;
}
// Write array of values to wishbone bus
wishbone_->SpiWrite(kEverloopBaseAddress, &write_data[0], write_data.size());
return true;
}
We can write a pure Rust version of most of this and skip wrangling with what bindgen generates from C++.
[package]
name = "everloop"
version = "0.1.0"
edition = "2018"
[dependencies]
matrix_hal_esp32_sys = {path = "../../matrix_hal_esp32_sys"}
# `no_std` access to math functions like `sin()`
libm = "0.2"
# C types for FFI
cty = {version = "0.2"}
Owing to the no_std
requirement: libm gives us access to math routines like sin()
, and cty provides std::os::raw
types.
In Rust:
#![no_std]
#![no_main]
// Entry point
#[no_mangle]
pub fn app_main() {
unsafe {
everloop();
}
}
unsafe fn everloop() {
use matrix_hal_esp32_sys::*;
let mut wb = matrix_hal::WishboneBus::default();
wb.Init();
// Don't bother with Everloop helper class, it just makes a byte array
// let mut everloop = matrix_hal::Everloop::new();
// everloop._base.Setup(&mut wb);
let mut counter = 0;
loop {
const NUMBER_LEDS: usize = matrix_hal::kMatrixCreatorNLeds as usize;
let mut image1d = [0u8; NUMBER_LEDS * 4];
let blue = (libm::sinf(counter as f32 / 64.0) * 10.0 + 10.0) as u8;
ets_printf(b"counter=%d blue=%d\n\0".as_ptr() as *const _, counter, blue as cty::c_uint);
for i in 0..NUMBER_LEDS {
image1d[i * 4 + 2] = blue;
}
wb.SpiWrite(matrix_hal::kEverloopBaseAddress as u16, image1d.as_ptr(), image1d.len() as i32);
counter += 1;
}
}
// Same panic handler as above
#[panic_handler]
//...
Tips
Remote Development has become one of my favorite plugins for VS Code. It lets you run a VS Code session locally that interacts with a remote/virtual environment. Remote- SSH makes it easier to work with another device like a Raspberry Pi- and is decidedly better than X11 forwarding. Remote- Containers lets you do the same with running containers and is especially handy when you want to mess with files not in a volume mounted from the host.
Next
- Potentially replace matrix_hal_esp32_sys with pure Rust implementation of matrix_hal_esp32 via esp-idf-sys or esp-sys
- Replace direct use of
ets_printf
with logger implementation - Better understand what’s going on with
.cargo/config
andcargo-xbuild
and move as much as possible to Rustbuild.rs
build script