mirror of
https://github.com/rust-vmm/vhost-device.git
synced 2026-01-03 15:58:37 +00:00
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:
parent
7efcb34b88
commit
d12bf9886d
19
Cargo.lock
generated
19
Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -9,6 +9,7 @@ members = [
|
||||
"vhost-device-scsi",
|
||||
"vhost-device-scmi",
|
||||
"vhost-device-sound",
|
||||
"vhost-device-spi",
|
||||
"vhost-device-template",
|
||||
"vhost-device-vsock",
|
||||
]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"coverage_score": 77.63,
|
||||
"coverage_score": 75.76,
|
||||
"exclude_path": "",
|
||||
"crate_features": ""
|
||||
}
|
||||
|
||||
15
vhost-device-spi/CHANGELOG.md
Normal file
15
vhost-device-spi/CHANGELOG.md
Normal file
@ -0,0 +1,15 @@
|
||||
# Changelog
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
|
||||
### Fixed
|
||||
|
||||
### Deprecated
|
||||
|
||||
## [0.1.0]
|
||||
|
||||
First release
|
||||
|
||||
35
vhost-device-spi/Cargo.toml
Normal file
35
vhost-device-spi/Cargo.toml
Normal 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"] }
|
||||
1
vhost-device-spi/LICENSE-APACHE
Symbolic link
1
vhost-device-spi/LICENSE-APACHE
Symbolic link
@ -0,0 +1 @@
|
||||
../LICENSE-APACHE
|
||||
1
vhost-device-spi/LICENSE-BSD-3-Clause
Symbolic link
1
vhost-device-spi/LICENSE-BSD-3-Clause
Symbolic link
@ -0,0 +1 @@
|
||||
../LICENSE-BSD-3-Clause
|
||||
80
vhost-device-spi/README.md
Normal file
80
vhost-device-spi/README.md
Normal 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)
|
||||
77
vhost-device-spi/src/linux_spi.rs
Normal file
77
vhost-device-spi/src/linux_spi.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
226
vhost-device-spi/src/main.rs
Normal file
226
vhost-device-spi/src/main.rs
Normal 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
1258
vhost-device-spi/src/spi.rs
Normal file
File diff suppressed because it is too large
Load Diff
1470
vhost-device-spi/src/vhu_spi.rs
Normal file
1470
vhost-device-spi/src/vhu_spi.rs
Normal file
File diff suppressed because it is too large
Load Diff
34
vhost-device-spi/src/virtio_spi.rs
Normal file
34
vhost-device-spi/src/virtio_spi.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user