Skip to content

Commit

Permalink
vhost-device-spi: Add initial implementation
Browse files Browse the repository at this point in the history
This program is a vhost-user backend that emulates a VirtIO SPI bus.
This program takes the layout of the spi bus and its devices on the host
OS and then talks to them via the /dev/spidevX.Y interface when a request
comes from the guest OS for a SPI device.

The implementation corresponds with the specification:
https://github.com/oasis-tcs/virtio-spec/tree/virtio-1.4/device-types/spi

Signed-off-by: Haixu Cui <[email protected]>
  • Loading branch information
HaixuCui committed Mar 29, 2024
1 parent 04998a9 commit 1877bb6
Show file tree
Hide file tree
Showing 11 changed files with 2,360 additions and 0 deletions.
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ members = [
"vhost-device-sound",
"vhost-device-template",
"vhost-device-vsock",
"vhost-device-spi",
]
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Here is the list of device backends that we support:
- [SCSI](https://github.com/rust-vmm/vhost-device/blob/main/vhost-device-scsi/README.md)
- [Sound](https://github.com/rust-vmm/vhost-device/blob/main/vhost-device-sound/README.md)
- [VSOCK](https://github.com/rust-vmm/vhost-device/blob/main/vhost-device-vsock/README.md)
- [SPI](https://github.com/rust-vmm/vhost-device/blob/main/vhost-device-spi/README.md)

The vhost-device workspace also provides a
[template](https://github.com/rust-vmm/vhost-device/blob/main/vhost-device-template/README.md)
Expand Down
15 changes: 15 additions & 0 deletions vhost-device-spi/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Changelog
## [Unreleased]

### Added

### Changed

### Fixed

### Deprecated

## [0.1.0]

First release

33 changes: 33 additions & 0 deletions vhost-device-spi/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[package]
name = "vhost-device-spi"
version = "0.1.0"
authors = ["Haixu Cui <[email protected]>"]
description = "vhost spi backend device"
repository = "https://github.com/rust-vmm/vhost-device"
readme = "README.md"
keywords = ["spi", "vhost", "virt", "backend"]
license = "Apache-2.0 OR BSD-3-Clause"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[features]
xen = ["vm-memory/xen", "vhost/xen", "vhost-user-backend/xen"]

[dependencies]
clap = { version = "4.5", features = ["derive"] }
env_logger = "0.11"
libc = "0.2"
log = "0.4"
thiserror = "1.0"
vhost = { version = "0.11", features = ["vhost-user-backend"] }
vhost-user-backend = "0.14"
virtio-bindings = "0.2.2"
virtio-queue = "0.11"
vm-memory = "0.14.1"
vmm-sys-util = "0.12"

[dev-dependencies]
assert_matches = "1.5"
virtio-queue = { version = "0.11", features = ["test-utils"] }
vm-memory = { version = "0.14.1", features = ["backend-mmap", "backend-atomic"] }
1 change: 1 addition & 0 deletions vhost-device-spi/LICENSE-APACHE
1 change: 1 addition & 0 deletions vhost-device-spi/LICENSE-BSD-3-Clause
52 changes: 52 additions & 0 deletions vhost-device-spi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# vhost-device-spi - SPI emulation backend daemon

## Description
This program is a vhost-user backend that emulates a VirtIO SPI bus.
This program takes the layout of the spi bus and its devices on the host
OS and then talks to them via the /dev/spidevX.Y interface when a request
comes from the guest OS for a SPI device.

## Synopsis

**vhost-device-spi** [*OPTIONS*]

## Options

.. program:: vhost-device-spi

.. option:: -h, --help

Print help.

.. option:: -s, --socket-path=PATH

Location of vhost-user Unix domain sockets, this path will be suffixed with
0,1,2..socket_count-1.

.. option:: -c, --socket-count=INT

Number of guests (sockets) to attach to, default set to 1.

.. option:: -l, --device=SPI-DEVICES

Spi device full path at the host OS in the format:
/dev/spidevX.Y

Here,
X: is spi controller's bus number.
Y: is chip select index.

## Examples

The daemon should be started first:

::

host# vhost-device-spi --socket-path=vspi.sock --socket-count=1 --device "/dev/spidev0.0"

## License

This project is licensed under either of

- [Apache License](http://www.apache.org/licenses/LICENSE-2.0), Version 2.0
- [BSD-3-Clause License](https://opensource.org/licenses/BSD-3-Clause)
173 changes: 173 additions & 0 deletions vhost-device-spi/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// VIRTIO SPI Emulation via vhost-user
//
// Copyright (c) 2024 Qualcomm Innovation Center, Inc. All rights reserved.
// Haixu Cui <[email protected]>
//
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause

mod spi;
mod vhu_spi;

use log::error;
use std::process::exit;
use std::sync::{Arc, RwLock};
use std::thread::{spawn, JoinHandle};

use clap::Parser;
use thiserror::Error as ThisError;
use vhost_user_backend::VhostUserDaemon;
use vm_memory::{GuestMemoryAtomic, GuestMemoryMmap};

use spi::{PhysDevice, SpiController, SpiDevice};
use vhu_spi::VhostUserSpiBackend;

type Result<T> = std::result::Result<T, Error>;

#[derive(Debug, ThisError)]
/// Errors related to low level spi helpers
pub(crate) enum Error {
#[error("Invalid socket count: {0}")]
SocketCountInvalid(usize),
#[error("Low level Spi failure: {0:?}")]
SpiFailure(spi::Error),
#[error("Could not create backend: {0}")]
CouldNotCreateBackend(vhu_spi::Error),
#[error("Could not create daemon: {0}")]
CouldNotCreateDaemon(vhost_user_backend::Error),
#[error("Fatal error: {0}")]
ServeFailed(vhost_user_backend::Error),
}

#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct SpiArgs {
/// Location of vhost-user Unix domain socket. This is suffixed by 0,1,2..socket_count-1.
#[clap(short, long)]
socket_path: String,

/// Number of guests (sockets) to connect to.
#[clap(short = 'c', long, default_value_t = 1)]
socket_count: usize,

/// SPI device full path
#[clap(short = 'l', long)]
device: String,
}

#[derive(PartialEq, Debug)]
struct SpiConfiguration {
socket_path: String,
socket_count: usize,
device: String,
}

impl SpiConfiguration {
fn try_from(args: SpiArgs) -> Result<Self> {
if args.socket_count == 0 {
return Err(Error::SocketCountInvalid(0));
}

Ok(SpiConfiguration {
socket_path: args.socket_path.trim().to_string(),
socket_count: args.socket_count,
device: args.device.trim().to_string(),
})
}
}

fn start_backend<D: 'static + SpiDevice + Send + Sync>(args: SpiArgs) -> Result<()> {
let config = SpiConfiguration::try_from(args).unwrap();

let spi_dev = D::open(&config.device).map_err(Error::SpiFailure)?;

let spi_ctrl = Arc::new(SpiController::<D>::new(spi_dev).map_err(Error::SpiFailure)?);

let mut handles = Vec::new();

for i in 0..config.socket_count {
let socket = config.socket_path.to_owned() + &i.to_string();
let spi_ctrl = spi_ctrl.clone();

let handle: JoinHandle<Result<()>> = spawn(move || loop {
// A separate thread is spawned for each socket and can connect to a separate guest.
// These are run in an infinite loop to not require the daemon to be restarted once a
// guest exits.
//
// There isn't much value in complicating code here to return an error from the
// threads, and so the code uses unwrap() instead. The panic on a thread won't cause
// trouble to other threads/guests or the main() function and should be safe for the
// daemon.
let backend = Arc::new(RwLock::new(
VhostUserSpiBackend::new(spi_ctrl.clone()).map_err(Error::CouldNotCreateBackend)?,
));

let mut daemon = VhostUserDaemon::new(
String::from("vhost-device-spi-backend"),
backend.clone(),
GuestMemoryAtomic::new(GuestMemoryMmap::new()),
)
.map_err(Error::CouldNotCreateDaemon)?;

daemon.serve(&socket).map_err(Error::ServeFailed)?;
});

handles.push(handle);
}

for handle in handles {
handle.join().map_err(std::panic::resume_unwind).unwrap()?;
}

Ok(())
}

fn main() {
env_logger::init();

if let Err(e) = start_backend::<PhysDevice>(SpiArgs::parse()) {
error!("{e}");
exit(1);
}
}

#[cfg(test)]
pub(crate) mod tests {
use assert_matches::assert_matches;

use super::*;
use crate::spi::tests::DummyDevice;

impl SpiArgs {
fn from_args(path: &str, device: &str, count: usize) -> SpiArgs {
SpiArgs {
socket_path: path.to_string(),
socket_count: count,
device: device.to_string(),
}
}
}

#[test]
fn test_parse_failure() {
let socket_name = "vspi.sock";

// Zero socket count
let cmd_args = SpiArgs::from_args(socket_name, "spidev0.0", 0);
assert_matches!(
SpiConfiguration::try_from(cmd_args).unwrap_err(),
Error::SocketCountInvalid(0)
);
}

#[test]
fn test_fail_listener() {
// This will fail the listeners and thread will panic.
let socket_name = "~/path/not/present/spi";
let cmd_args = SpiArgs::from_args(socket_name, "spidev0.0", 1);

assert_matches!(
start_backend::<DummyDevice>(cmd_args).unwrap_err(),
Error::ServeFailed(_)
);
}
}
Loading

0 comments on commit 1877bb6

Please sign in to comment.