Note: This is a crosspost of a Cynthion update on Crowd Supply: https://www.crowdsupply.com/great-scott-gadgets/cynthion/updates/moondancer-a-facedancer-backend-for-cynthion
One of the core features promised in the Cynthion announcement is the ability to create your own Low-, Full- or High- speed USB devices using the Facedancer library – even if you don’t have experience with digital-hardware design, HDL or FPGA architecture. If you’ve been eagerly anticipating this feature, we’re pleased to introduce Moondancer, a new Facedancer backend for Cynthion.
What is Facedancer?
Facedancer is a host-side Python library for writing programs that remotely control the physical USB port(s) on Facedancer boards such as the original Facedancer21, GreatFET One, and Cynthion.
Using Facedancer to control a physical USB port gives you direct control over the data encoded in USB streams and allows you to do things like:
- Emulate a physical USB device such as a keyboard, mass storage device, or serial interface,
- Act as a programmable proxy between a device and its host with the ability to inspect and modify the data stream,
- Fuzz host-side USB device drivers by deliberately sending malformed data that can trigger faults in the device or host software.
Facedancer Example
Let’s say you need to automate the operation of a computer running some software that can only accept input via keyboard:
A USB keyboard connected to the Target Operating System.
By connecting a Facedancer board such as Cynthion to the computer (called the “target”) in place of the USB keyboard, you can now use a second computer (called the “host”) to run a small Python script to control USB traffic between the target computer and Cynthion:
A Facedancer emulation of a USB keyboard connected to the Target Operating System.
Whenever a new USB peripheral is plugged in, the target operating system will first send a standard set of USB enumeration requests to the peripheral asking it to identify itself to the operating system. In the diagram above, Cynthion is the peripheral receiving enumeration requests from the target. However, instead of replying directly, Cynthion will forward any enumeration requests it receives to the Facedancer host. The Facedancer host will then respond to the target with a set of USB descriptors corresponding to the peripheral you are emulating. Once the target operating system has received a set of known USB descriptors, it will load the appropriate device driver for controlling the USB peripheral. All subsequent USB transfers initiated by the device driver will also be received by Cynthion and forwarded to the Facedancer host. By using a Facedancer emulation that responds appropriately to the command set of the peripheral being emulated, Cynthion can respond to the target operating system as if it were any actual physical device.
In our example, we can use Facedancer’s USBKeyboardDevice object to provide the USB descriptors and transfer commands required for a keyboard that follows the USB human interface device class specification:
import asyncio
from facedancer.devices.keyboard import USBKeyboardDevice
device = USBKeyboardDevice()
async def type_on_keyboard():
# Type ls.
await device.type_letters('l', 's', '\n')
main(device, type_on_keyboard())
What is Moondancer?
Moondancer is a new backend for Facedancer that adds support for Cynthion.
Facedancer supports a variety of boards by providing different backends for each supported board. For example, GreatFET One uses a backend called “greatdancer” while RPi + Max3241 boards use the “raspdancer” backend. In keeping with Cynthion’s lunar origins, we decided to call the new backend “Moondancer”.
What makes Cynthion different from other Facedancer-compatible boards is that, instead of being based on a microcontroller, it is built around an FPGA connected to three USB 2.0 PHY chips under control of the open source LUNA USB gateware library. While this provides us with more direct access to USB signals and their behaviour it also represented a significant engineering challenge for our team. The most significant challenge was how to control the USB controllers. On previous Facedancer devices, the controllers have been under software control via device firmware running on the device CPU. However, being an FPGA-based platform, Cynthion does not have a CPU!
At first glance, we had two choices for controlling the USB 2.0 PHY chips:
- Implement the control logic as gateware.
- Integrate a microcontroller into the Cynthion hardware design.
In principle a Facedancer device merely acts as a forwarder between the USB controllers and the controlling host. This means a gateware implementation could be as simple as exposing the registers controlling LUNA’s “eptri” triple-fifo endpoint peripheral via a set of USB Vendor Class commands. On the other hand, integrating another microcontroller into Cynthion would increase the design complexity significantly and add substantially to the bill of materials cost. All things being equal, we may have ended up with a gateware implementation were it not for the recent emergence of high quality, libre-licensed RISC-V implementations. Hosting a microcontroller as a “soft-core” on an FPGA is not a new idea but RISC-V’s open Instruction Set Architecture (ISA) removes many barriers to implementation such as licensing, compilers and tools. Therefore, while a Facedancer device implementation in gateware would be a very cool hack indeed, we thought it would be even cooler to take an approach that would also let you use Cynthion as a tool for getting started with RISC-V, System-on-Chip (SoC) design, and Embedded Rust while exploring USB in embedded environments.
How does Moondancer work?
Moondancer consists of several distinct components:
- moondancer-soc: A custom RISC-V SoC that integrates a libre-licensed RISC-V CPU with the LUNA USB peripherals.
- lunasoc-pac and lunasoc-hal: Embedded Rust support crates for moondancer-soc peripherals.
- smolusb: A lightweight, low-level USB stack appropriate for LUNA USB device controllers.
- Moondancer firmware: The device-side implementation of the Facedancer command protocol.
- Moondancer backend: The host-side Facedancer backend for communication with the Moondancer firmware.
moondancer-soc
At the heart of Moondancer lies a stripped-down RISC-V SoC design described in the Amaranth Hardware Description Language (HDL):
- SpinalHDL VexRiscV CPU
- Full RV32IMAC instruction set support
- 60 MHz clock speed
- 64 kilobytes of SRAM
- 4 kilobytes L1 instruction cache
- 4 kilobytes L1 data cache
- 2x GPIO peripherals
- 6x LED peripherals
- 1x UART peripheral
- 1x Timer peripheral
- 3x LUNA USB eptri peripherals
While the feature set may be modest in comparison to most commercial micro-controllers, the full gateware source of every single component integrated within the design is libre-licensed with all four freedoms intact.
Moondancer SoC Architecture
After bringing up our “hardware” platform for the Moondancer firmware, we faced another set of challenges. In commercial SoC development, there are usually multiple teams tasked with creating the tooling, device drivers and development libraries for a new design. While we would still have to develop device drivers and libraries, we did not need to create yet another fork of GCC to implement our own custom toolchain with compiler, debugger, linker, and sundry utilities. Thanks to the efforts of many contributors, both commercial and from the broader community, the GNU toolchain has been shipping RiscV support for some time now, and Rust (via LLVM) can compile binaries for many RiscV variants right out of the box.
None of this would have been possible even a few years ago, and it is thanks to the efforts of a wide community that we were able to do it within the time and resources available to us:
lunasoc-pac
One of the fundamental building blocks in any Embedded Rust project is a Peripheral Access Crate (PAC) which provides safe register-level access to the processor’s peripherals. While there are already existing PACs and even HALs for RISC-V chips from companies such as Espressif and AllWinner there existed no equivalent for working with a custom-defined SoC implemented as gateware.
Fortunately, what most Rust PACs have in common is that their code is largely generated from an SVD description of the processor and its peripheral registers with the help of the svd2rust tool. Therefore, we extended the luna-soc library with the ability to export SVD files generated directly from the SoC design allowing anyone to easily generate a PAC for any luna-soc design.
lunasoc-hal
While it is entirely possible to develop an entire firmware using just a PAC crate, it would be nice to offer a friendlier programming interface and the possibility of code re-use across different processors. Normally, a chip will come with some form of vendor-provided HAL that provides higher-level abstractions for communicating with the peripherals and some compatibility with other products in the vendor’s product line. The Embedded Rust community took a slightly different approach to this problem with the embedded-hal project which provides a set of centrally defined traits to build an ecosystem of platform-agnostic drivers.
By adopting embedded-hal for our luna-soc design, we’ve made it possible for other luna-soc users to easily target their own custom designs even if the underlying peripheral implementations differ. It also means the Moondancer firmware can be more easily ported to any other platform with an embedded-hal implementation.
smolusb
Given that Facedancer requires direct access to the USB peripheral to perform emulation, and our SoC only has 64 kilobytes of RAM, we’ve developed ‘smolusb’, a new lightweight device-side USB stack that provides:
- a set of traits for implementing HAL USB device drivers
- data structures for defining device descriptors
- data structures for defining class and vendor requests
- device enumeration support
‘smolusb’ does not require Rust alloc support, uses a single statically allocated buffer for read operations, and supports zero-copy write operations. It supports high-level operations such as device enumeration but also provides several “escape hatches” that allow for direct control of the underlying peripheral for the purposes of device emulation and other Facedancer features.
Moondancer firmware and backend
Moondancer manages the communication between the Facedancer library and the remotely controlled USB peripheral and is split into two components:
- Moondancer firmware written in Rust and running in the SoC on Cynthion. The Moondancer firmware implements the Facedancer command set and controls Cynthion’s USB ports.
- Moondancer backend written in Python and running on the host. The Moondancer backend handles all communication between Facedancer and the Moondancer firmware.
To mediate communication between the Moondancer backend and the Moondancer firmware we’ve used a Rust implementation of the same libgreat RPC protocol used by GreatFET and other Great Scott Gadgets open-source projects. The power of libgreat is its ability to generate and expose simple explorable APIs via Python, allowing for flexible communications between computers and embedded devices, embedded drivers, and more without having to get into the murky details of driver development, transports or serialization protocols. We hope this design decision will also allow others to more easily develop and integrate their own custom firmware for embedded USB applications with host software!
On the host side, the Moondancer backend is responsible for translating calls from Facedancer into libgreat commands which are then received on Cynthion’s CONTROL USB port, deserialized by libgreat and forwarded to the Moondancer firmware which is responsible for operating the Cynthion’s TARGET USB port.
Finally, the Moondancer firmware implements the Moondancer API for directly controlling the USB peripheral via operations to manage port connections, reset the bus, set the device address, manage endpoints, and send/receive data packets.
Wrapping up
If you have access to Cynthion hardware and would like to try out Moondancer please feel free to check out the Cynthion repository. Also, if you’re interested in custom SoC development and Embedded Rust, you can check out the luna-soc repository. Most of the non-USB functionality has also been tested on other ECP5 devices so, with a little bit of luck, you might be able to get something going with your favorite development board.
Acknowledgements
We would like to express our sincere gratitude to two individuals without whom Moondancer would not have been possible.
In particular, our work builds on the research of Travis Goodspeed who developed the original Facedancer board and software, and Kate Temkin who extended the software and generalized it for other platforms.
You can learn more about the history of Facedancer and LUNA in our fifth Crowd Supply update: “The History of LUNA”.