vhost-device-spi: Add initial implementation

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 <quic_haixcui@quicinc.com>
This commit is contained in:
Haixu Cui 2024-03-27 15:22:15 +08:00 committed by Stefano Garzarella
parent 7efcb34b88
commit d12bf9886d
14 changed files with 3219 additions and 1 deletions

19
Cargo.lock generated
View File

@ -1472,6 +1472,25 @@ dependencies = [
"vmm-sys-util",
]
[[package]]
name = "vhost-device-spi"
version = "0.1.0"
dependencies = [
"assert_matches",
"bitflags 2.4.1",
"clap",
"env_logger",
"libc",
"log",
"thiserror",
"vhost",
"vhost-user-backend",
"virtio-bindings",
"virtio-queue",
"vm-memory",
"vmm-sys-util",
]
[[package]]
name = "vhost-device-template"
version = "0.1.0"

View File

@ -9,6 +9,7 @@ members = [
"vhost-device-scsi",
"vhost-device-scmi",
"vhost-device-sound",
"vhost-device-spi",
"vhost-device-template",
"vhost-device-vsock",
]

View File

@ -21,6 +21,7 @@ Here is the list of device backends that we support:
- [SCMI](https://github.com/rust-vmm/vhost-device/blob/main/vhost-device-scmi/README.md)
- [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)
- [SPI](https://github.com/rust-vmm/vhost-device/blob/main/vhost-device-spi/README.md)
- [VSOCK](https://github.com/rust-vmm/vhost-device/blob/main/vhost-device-vsock/README.md)
The vhost-device workspace also provides a

View File

@ -1,5 +1,5 @@
{
"coverage_score": 77.63,
"coverage_score": 75.76,
"exclude_path": "",
"crate_features": ""
}

View File

@ -0,0 +1,15 @@
# Changelog
## [Unreleased]
### Added
### Changed
### Fixed
### Deprecated
## [0.1.0]
First release

View File

@ -0,0 +1,35 @@
[package]
name = "vhost-device-spi"
version = "0.1.0"
authors = ["Haixu Cui <quic_haixcui@quicinc.com>"]
description = "vhost spi backend device"
repository = "https://github.com/rust-vmm/vhost-device"
readme = "README.md"
keywords = ["spi", "vhost", "virt", "backend"]
categories = ["virtualization"]
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.15"
virtio-bindings = "0.2.2"
virtio-queue = "0.12"
vm-memory = "0.14.1"
vmm-sys-util = "0.12"
bitflags = "2.4.0"
[dev-dependencies]
assert_matches = "1.5"
virtio-queue = { version = "0.12", features = ["test-utils"] }
vm-memory = { version = "0.14.1", features = ["backend-mmap", "backend-atomic"] }

View File

@ -0,0 +1 @@
../LICENSE-APACHE

View File

@ -0,0 +1 @@
../LICENSE-BSD-3-Clause

View File

@ -0,0 +1,80 @@
# 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
```shell
vhost-device-spi [OPTIONS]
```
## Options
```text
-h, --help
Print help.
-s, --socket-path=PATH
Location of vhost-user Unix domain sockets, this path will be suffixed with
0,1,2..socket_count-1.
-c, --socket-count=INT
Number of guests (sockets) to attach to, default set to 1.
-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
### Dependencies
For testing the device the required dependencies are:
- Linux:
- Integrate *virtio-spi* driver:
- https://lwn.net/Articles/966715/
- Set `CONFIG_SPI_VIRTIO=y`
- QEMU:
- Integrate vhost-user-spi QEMU device:
- https://lore.kernel.org/all/20240712034246.2553812-1-quic_haixcui@quicinc.com/
### Test the device
First start the daemon on the host machine::
````suggestion
```console
vhost-device-spi --socket-path=vspi.sock --socket-count=1 --device "/dev/spidev0.0"
```
````
The QEMU invocation needs to create a chardev socket the device spi
use to communicate as well as share the guests memory over a memfd.
````suggestion
```console
qemu-system-aarch64 -m 1G \
-chardev socket,path=/home/root/vspi.sock0,id=vspi \
-device vhost-user-spi-pci,chardev=vspi,id=spi \
-object memory-backend-file,id=mem,size=1G,mem-path=/dev/shm,share=on \
-numa node,memdev=mem \
...
```
````
## 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)

View File

@ -0,0 +1,77 @@
// Linux SPI bindings
//
// Copyright (c) 2024 Qualcomm Innovation Center, Inc. All rights reserved.
// Haixu Cui <quic_haixcui@quicinc.com>
//
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
use bitflags::bitflags;
use vmm_sys_util::{ioctl_ioc_nr, ioctl_ior_nr, ioctl_iow_nr};
/// Describes a single SPI transfer
#[derive(Debug)]
#[repr(C)]
pub struct SpiIocTransfer {
/// Holds pointer to userspace buffer with transmit data, or null
pub tx_buf: u64,
/// Holds pointer to userspace buffer for receive data, or null.
pub rx_buf: u64,
/// Length of tx and rx buffers, in bytes.
pub len: u32,
/// Temporary override of the device's bitrate.
pub speed_hz: u32,
/// If nonzero, how long to delay after the last bit transfer
/// before optionally deselecting the device before the next transfer.
pub delay_usecs: u16,
/// Temporary override of the device's wordsize.
pub bits_per_word: u8,
/// True to deselect device before starting the next transfer.
pub cs_change: u8,
/// Number of bits used for writing.
pub tx_nbits: u8,
/// Number of bits used for reading.
pub rx_nbits: u8,
/// If nonzero, how long to wait between words within one
/// transfer. This property needs explicit support in the SPI controller,
/// otherwise it is silently ignored
pub word_delay_usecs: u8,
pub _padding: u8,
}
/// Linux SPI definitions
/// IOCTL commands, refer Linux's Documentation/spi/spidev.rst for further details.
const _IOC_SIZEBITS: u32 = 14;
const _IOC_SIZESHIFT: u32 = 16;
const SPI_IOC_MESSAGE_BASE: u32 = 0x40006b00;
ioctl_ior_nr!(SPI_IOC_RD_BITS_PER_WORD, 107, 3, u8);
ioctl_iow_nr!(SPI_IOC_WR_BITS_PER_WORD, 107, 3, u8);
ioctl_ior_nr!(SPI_IOC_RD_MAX_SPEED_HZ, 107, 4, u32);
ioctl_iow_nr!(SPI_IOC_WR_MAX_SPEED_HZ, 107, 4, u32);
ioctl_ior_nr!(SPI_IOC_RD_MODE32, 107, 5, u32);
ioctl_iow_nr!(SPI_IOC_WR_MODE32, 107, 5, u32);
// Corresponds to the SPI_IOC_MESSAGE macro in Linux
pub fn spi_ioc_message(n: u32) -> u64 {
let mut size: u32 = 0;
if n * 32 < (1 << _IOC_SIZEBITS) {
size = n * 32;
}
(SPI_IOC_MESSAGE_BASE | (size << _IOC_SIZESHIFT)) as u64
}
bitflags! {
pub struct LnxSpiMode: u32 {
const CPHA = 1 << 0;
const CPOL = 1 << 1;
const CS_HIGH = 1 << 2;
const LSB_FIRST = 1 << 3;
const LOOP = 1 << 5;
const TX_DUAL = 1 << 8;
const TX_QUAD = 1 << 9;
const TX_OCTAL = 1 << 13;
const RX_DUAL = 1 << 10;
const RX_QUAD = 1 << 11;
const RX_OCTAL = 1 << 14;
}
}

View File

@ -0,0 +1,226 @@
// VIRTIO SPI Emulation via vhost-user
//
// Copyright (c) 2024 Qualcomm Innovation Center, Inc. All rights reserved.
// Haixu Cui <quic_haixcui@quicinc.com>
//
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
mod linux_spi;
mod spi;
mod vhu_spi;
mod virtio_spi;
use std::{
any::Any,
collections::HashMap,
num::NonZeroUsize,
path::PathBuf,
process::exit,
sync::{Arc, RwLock},
thread,
};
use clap::Parser;
use log::error;
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
enum Error {
#[error("SPI device file doesn't exists or can't be accessed")]
AccessDeviceFailure(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),
#[error("Thread `{0}` panicked")]
ThreadPanic(String, Box<dyn Any + Send>),
}
#[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: PathBuf,
/// Number of guests (sockets) to connect to.
#[clap(short = 'c', long, default_value_t = NonZeroUsize::new(1).unwrap())]
socket_count: NonZeroUsize,
/// SPI device full path
#[clap(short = 'l', long)]
device: PathBuf,
}
#[derive(PartialEq, Debug)]
struct SpiConfiguration {
socket_path: PathBuf,
socket_count: usize,
device: PathBuf,
}
impl SpiConfiguration {
fn from(args: SpiArgs) -> Result<Self> {
Ok(SpiConfiguration {
socket_path: args.socket_path,
socket_count: args.socket_count.get(),
device: args.device,
})
}
}
impl SpiConfiguration {
pub fn generate_socket_paths(&self) -> Vec<PathBuf> {
let socket_file_name = self
.socket_path
.file_name()
.expect("socket_path has no filename.");
let socket_file_parent = self
.socket_path
.parent()
.expect("socket_path has no parent directory.");
let make_socket_path = |i: usize| -> PathBuf {
let mut file_name = socket_file_name.to_os_string();
file_name.push(std::ffi::OsStr::new(&i.to_string()));
socket_file_parent.join(&file_name)
};
(0..self.socket_count).map(make_socket_path).collect()
}
}
pub(crate) fn start_backend_server<D: 'static + SpiDevice + Send + Sync>(
socket: PathBuf,
device: PathBuf,
) -> Result<()> {
loop {
let spi_dev = D::open(&device).map_err(Error::AccessDeviceFailure)?;
let spi_ctrl =
Arc::new(SpiController::<D>::new(spi_dev).map_err(Error::AccessDeviceFailure)?);
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)?;
}
}
fn start_backend<D: 'static + SpiDevice + Send + Sync>(args: SpiArgs) -> Result<()> {
let config = SpiConfiguration::from(args)?;
let mut handles = HashMap::new();
let (senders, receiver) = std::sync::mpsc::channel();
for (thread_id, socket) in config.generate_socket_paths().into_iter().enumerate() {
let name = format!("vhu-vsock-spi-{:?}", thread_id);
let sender = senders.clone();
let device_ref = config.device.clone();
let handle = thread::Builder::new()
.name(name.clone())
.spawn(move || {
let result =
std::panic::catch_unwind(move || start_backend_server::<D>(socket, device_ref));
sender.send(thread_id).unwrap();
result.map_err(|e| Error::ThreadPanic(name, e))?
})
.unwrap();
handles.insert(thread_id, handle);
}
while !handles.is_empty() {
let thread_id = receiver.recv().unwrap();
handles
.remove(&thread_id)
.unwrap()
.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 std::path::Path;
use super::*;
use crate::spi::tests::DummyDevice;
impl SpiArgs {
fn from_args(path: &str, device: &str, count: usize) -> SpiArgs {
SpiArgs {
socket_path: PathBuf::from(path),
socket_count: NonZeroUsize::new(count)
.expect("Socket count must be a non-zero value"),
device: PathBuf::from(device),
}
}
}
#[test]
fn test_parse_successful() {
let socket_name = "vspi.sock";
let device_path = "/dev/spidev0.0";
let cmd_args = SpiArgs::from_args(socket_name, device_path, 3);
let config = SpiConfiguration::from(cmd_args).unwrap();
assert_eq!(
config.generate_socket_paths(),
vec![
Path::new("vspi.sock0").to_path_buf(),
Path::new("vspi.sock1").to_path_buf(),
Path::new("vspi.sock2").to_path_buf(),
]
);
}
#[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(_)
);
}
}

1258
vhost-device-spi/src/spi.rs Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
// Virtio SPI definitions
//
// Copyright (c) 2024 Qualcomm Innovation Center, Inc. All rights reserved.
// Haixu Cui <quic_haixcui@quicinc.com>
//
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
use bitflags::bitflags;
bitflags! {
pub struct ConfigNbits: u8 {
const DUAL = 0x1;
const QUAD = 0x2;
const OCTAL = 0x4;
}
pub struct ConfigMode: u32 {
const CPHA_0 = 0x1;
const CPHA_1 = 0x2;
const CPOL_0 = 0x4;
const CPOL_1 = 0x8;
const CS_HIGH = 0x10;
const LSB = 0x20;
const LOOP = 0x40;
}
pub struct ReqMode: u32 {
const CPHA = 1 << 0;
const CPOL = 1 << 1;
const CS_HIGH = 1 << 2;
const LSB_FIRST = 1 << 3;
const LOOP = 1 << 4;
}
}