use core::{
ptr::NonNull,
sync::atomic::{Ordering, fence},
};
use crate::mmio::{
UniqueMmioPointer, field,
fields::{ReadPure, ReadPureWrite, WriteOnly},
};
use super::queue::VirtQueue;
const VIRTIO_MMIO_MAGIC: u32 = crate::signature!('v', 'i', 'r', 't');
const VIRTIO_MMIO_VERSION: u32 = 2;
const STATUS_ACKNOWLEDGE: u32 = crate::bit!(0);
const STATUS_DRIVER: u32 = crate::bit!(1);
const STATUS_DRIVER_OK: u32 = crate::bit!(2);
const STATUS_FEATURES_OK: u32 = crate::bit!(3);
const STATUS_FAILED: u32 = crate::bit!(7);
const VIRTIO_F_VERSION_1_HIGH_WORD: u32 = crate::bit!(0);
#[repr(C)]
pub(super) struct VirtioMmioRegs {
magic_value: ReadPure<u32>,
version: ReadPure<u32>,
device_id: ReadPure<u32>,
vendor_id: ReadPure<u32>,
device_features: ReadPure<u32>,
device_features_sel: WriteOnly<u32>,
_reserved0: [u32; 2],
driver_features: WriteOnly<u32>,
driver_features_sel: WriteOnly<u32>,
_reserved1: [u32; 2],
queue_sel: WriteOnly<u32>,
queue_num_max: ReadPure<u32>,
queue_num: WriteOnly<u32>,
_reserved2: [u32; 2],
queue_ready: ReadPureWrite<u32>,
_reserved3: [u32; 2],
queue_notify: WriteOnly<u32>,
_reserved4: [u32; 3],
interrupt_status: ReadPure<u32>,
interrupt_ack: WriteOnly<u32>,
_reserved5: [u32; 2],
status: ReadPureWrite<u32>,
_reserved6: [u32; 3],
queue_desc_low: WriteOnly<u32>,
queue_desc_high: WriteOnly<u32>,
_reserved7: [u32; 2],
queue_driver_low: WriteOnly<u32>,
queue_driver_high: WriteOnly<u32>,
_reserved8: [u32; 2],
queue_device_low: WriteOnly<u32>,
queue_device_high: WriteOnly<u32>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(super) enum InitError {
UnsupportedDevice,
FeaturesRejected,
QueueUnavailable,
}
#[derive(Debug)]
pub(super) struct VirtioMmio {
regs: UniqueMmioPointer<'static, VirtioMmioRegs>,
}
unsafe impl Send for VirtioMmio {}
impl VirtioMmio {
pub(super) const unsafe fn new(base_address: usize) -> Self {
let ptr = NonNull::new(base_address as *mut VirtioMmioRegs).expect("Caller provided NULL address.");
Self { regs: unsafe { UniqueMmioPointer::new(ptr) } }
}
pub(super) fn begin_init(&mut self, expected_device_id: u32) -> Result<(), InitError> {
let mut regs = self.regs.reborrow();
if field!(regs, magic_value).read() != VIRTIO_MMIO_MAGIC
|| field!(regs, version).read() != VIRTIO_MMIO_VERSION
|| field!(regs, device_id).read() != expected_device_id
{
return Err(InitError::UnsupportedDevice);
}
field!(regs, status).write(0);
while field!(regs, status).read() != 0 {
core::hint::spin_loop();
}
field!(regs, status).write(STATUS_ACKNOWLEDGE);
field!(regs, status).write(STATUS_ACKNOWLEDGE | STATUS_DRIVER);
field!(regs, device_features_sel).write(1);
let device_features_high = field!(regs, device_features).read();
if device_features_high & VIRTIO_F_VERSION_1_HIGH_WORD == 0 {
field!(regs, status).write(STATUS_ACKNOWLEDGE | STATUS_DRIVER | STATUS_FAILED);
return Err(InitError::FeaturesRejected);
}
field!(regs, driver_features_sel).write(0);
field!(regs, driver_features).write(0);
field!(regs, driver_features_sel).write(1);
field!(regs, driver_features).write(VIRTIO_F_VERSION_1_HIGH_WORD);
field!(regs, status).write(STATUS_ACKNOWLEDGE | STATUS_DRIVER | STATUS_FEATURES_OK);
if field!(regs, status).read() & STATUS_FEATURES_OK == 0 {
field!(regs, status).write(STATUS_ACKNOWLEDGE | STATUS_DRIVER | STATUS_FAILED);
return Err(InitError::FeaturesRejected);
}
Ok(())
}
pub(super) fn configure_queue<const N: usize, const B: usize>(
&mut self,
index: u32,
queue: &VirtQueue<N, B>,
) -> Result<(), InitError> {
let mut regs = self.regs.reborrow();
field!(regs, queue_sel).write(index);
if field!(regs, queue_ready).read() != 0 {
return Err(InitError::QueueUnavailable);
}
let max = field!(regs, queue_num_max).read();
if max < N as u32 {
return Err(InitError::QueueUnavailable);
}
field!(regs, queue_num).write(N as u32);
let desc_pa = queue.desc_addr();
let avail_pa = queue.avail_addr();
let used_pa = queue.used_addr();
field!(regs, queue_desc_low).write(desc_pa as u32);
field!(regs, queue_desc_high).write((desc_pa >> 32) as u32);
field!(regs, queue_driver_low).write(avail_pa as u32);
field!(regs, queue_driver_high).write((avail_pa >> 32) as u32);
field!(regs, queue_device_low).write(used_pa as u32);
field!(regs, queue_device_high).write((used_pa >> 32) as u32);
field!(regs, queue_ready).write(1);
Ok(())
}
pub(super) fn set_driver_ok(&mut self) {
let mut regs = self.regs.reborrow();
field!(regs, status).write(STATUS_ACKNOWLEDGE | STATUS_DRIVER | STATUS_FEATURES_OK | STATUS_DRIVER_OK);
}
pub(super) fn set_failed(&mut self) {
let mut regs = self.regs.reborrow();
let current = field!(regs, status).read();
field!(regs, status).write(current | STATUS_FAILED);
}
pub(super) fn notify(&mut self, queue_index: u32) {
fence(Ordering::SeqCst);
let mut regs = self.regs.reborrow();
field!(regs, queue_notify).write(queue_index);
}
}
#[cfg(test)]
#[coverage(off)]
impl VirtioMmioRegs {
pub(super) fn new_fake(device_id: u32) -> Self {
let mut regs: Self = unsafe { core::mem::zeroed() };
let mut ptr = UniqueMmioPointer::from(&mut regs);
unsafe {
field!(ptr, magic_value).write_unsafe(ReadPure(VIRTIO_MMIO_MAGIC));
field!(ptr, version).write_unsafe(ReadPure(VIRTIO_MMIO_VERSION));
field!(ptr, device_id).write_unsafe(ReadPure(device_id));
field!(ptr, device_features).write_unsafe(ReadPure(VIRTIO_F_VERSION_1_HIGH_WORD));
field!(ptr, queue_num_max).write_unsafe(ReadPure(8));
}
regs
}
}
#[cfg(test)]
#[coverage(off)]
mod tests {
use super::*;
const TEST_DEVICE_ID: u32 = 3;
fn make_transport(regs: &mut VirtioMmioRegs) -> VirtioMmio {
let addr = core::ptr::from_mut(regs) as usize;
unsafe { VirtioMmio::new(addr) }
}
#[test]
fn test_begin_init() {
let mut regs = VirtioMmioRegs::new_fake(TEST_DEVICE_ID);
let mut t = make_transport(&mut regs);
assert_eq!(t.begin_init(TEST_DEVICE_ID), Ok(()));
assert_eq!(regs.status.0, STATUS_ACKNOWLEDGE | STATUS_DRIVER | STATUS_FEATURES_OK);
assert_eq!(regs.driver_features_sel.0, 1);
assert_eq!(regs.driver_features.0, VIRTIO_F_VERSION_1_HIGH_WORD);
}
#[test]
fn test_rejects_bad_magic() {
let mut regs = VirtioMmioRegs::new_fake(TEST_DEVICE_ID);
regs.magic_value = ReadPure(0xDEADBEEF);
let mut t = make_transport(&mut regs);
assert_eq!(t.begin_init(TEST_DEVICE_ID), Err(InitError::UnsupportedDevice));
}
#[test]
fn test_rejects_bad_version() {
let mut regs = VirtioMmioRegs::new_fake(TEST_DEVICE_ID);
regs.version = ReadPure(1);
let mut t = make_transport(&mut regs);
assert_eq!(t.begin_init(TEST_DEVICE_ID), Err(InitError::UnsupportedDevice));
}
#[test]
fn test_rejects_wrong_device_id() {
let mut regs = VirtioMmioRegs::new_fake(TEST_DEVICE_ID);
let mut t = make_transport(&mut regs);
assert_eq!(t.begin_init(TEST_DEVICE_ID + 1), Err(InitError::UnsupportedDevice));
}
#[test]
fn test_rejects_when_v1_unsupported() {
let mut regs = VirtioMmioRegs::new_fake(TEST_DEVICE_ID);
regs.device_features = ReadPure(0);
let mut t = make_transport(&mut regs);
assert_eq!(t.begin_init(TEST_DEVICE_ID), Err(InitError::FeaturesRejected));
assert_ne!(regs.status.0 & STATUS_FAILED, 0);
}
#[test]
fn test_configure_queue() {
let mut regs = VirtioMmioRegs::new_fake(TEST_DEVICE_ID);
let queue: VirtQueue<8, 16> = VirtQueue::new();
let mut t = make_transport(&mut regs);
assert_eq!(t.configure_queue(1, &queue), Ok(()));
assert_eq!(regs.queue_sel.0, 1);
assert_eq!(regs.queue_num.0, 8);
assert_eq!(regs.queue_ready.0, 1);
let desc = (regs.queue_desc_low.0 as u64) | ((regs.queue_desc_high.0 as u64) << 32);
let avail = (regs.queue_driver_low.0 as u64) | ((regs.queue_driver_high.0 as u64) << 32);
let used = (regs.queue_device_low.0 as u64) | ((regs.queue_device_high.0 as u64) << 32);
assert_eq!(desc, queue.desc_addr());
assert_eq!(avail, queue.avail_addr());
assert_eq!(used, queue.used_addr());
}
#[test]
fn test_configure_queue_rejects_too_small() {
let mut regs = VirtioMmioRegs::new_fake(TEST_DEVICE_ID);
let queue: VirtQueue<16, 16> = VirtQueue::new();
let mut t = make_transport(&mut regs);
assert_eq!(t.configure_queue(0, &queue), Err(InitError::QueueUnavailable));
}
#[test]
fn test_configure_queue_rejects_when_already_in_use() {
let mut regs = VirtioMmioRegs::new_fake(TEST_DEVICE_ID);
regs.queue_ready = ReadPureWrite(1);
let queue: VirtQueue<8, 16> = VirtQueue::new();
let mut t = make_transport(&mut regs);
assert_eq!(t.configure_queue(0, &queue), Err(InitError::QueueUnavailable));
}
#[test]
fn test_virtio_set_driver_ok() {
let mut regs = VirtioMmioRegs::new_fake(TEST_DEVICE_ID);
let mut t = make_transport(&mut regs);
t.set_driver_ok();
assert_eq!(regs.status.0, STATUS_ACKNOWLEDGE | STATUS_DRIVER | STATUS_FEATURES_OK | STATUS_DRIVER_OK);
}
#[test]
fn test_set_failed() {
let mut regs = VirtioMmioRegs::new_fake(TEST_DEVICE_ID);
regs.status = ReadPureWrite(STATUS_ACKNOWLEDGE | STATUS_DRIVER);
let mut t = make_transport(&mut regs);
t.set_failed();
assert_eq!(regs.status.0, STATUS_ACKNOWLEDGE | STATUS_DRIVER | STATUS_FAILED);
}
#[test]
fn test_notify_writes_queue_index() {
let mut regs = VirtioMmioRegs::new_fake(TEST_DEVICE_ID);
let mut t = make_transport(&mut regs);
t.notify(7);
assert_eq!(regs.queue_notify.0, 7);
}
}