1use self::capabilities::*;
7use crate::QUEUE_MAX_SIZE;
8use crate::QueueResources;
9use crate::Resources;
10use crate::VirtioDevice;
11use crate::VirtioDoorbells;
12use crate::queue::QueueParams;
13use crate::spec::pci::*;
14use crate::spec::*;
15use chipset_device::ChipsetDevice;
16use chipset_device::io::IoResult;
17use chipset_device::mmio::MmioIntercept;
18use chipset_device::mmio::RegisterMmioIntercept;
19use chipset_device::pci::PciConfigSpace;
20use device_emulators::ReadWriteRequestType;
21use device_emulators::read_as_u32_chunks;
22use device_emulators::write_as_u32_chunks;
23use guestmem::DoorbellRegistration;
24use guestmem::MappedMemoryRegion;
25use guestmem::MemoryMapper;
26use inspect::InspectMut;
27use parking_lot::Mutex;
28use pci_core::PciInterruptPin;
29use pci_core::capabilities::PciCapability;
30use pci_core::capabilities::ReadOnlyCapability;
31use pci_core::capabilities::msix::MsixEmulator;
32use pci_core::cfg_space_emu::BarMemoryKind;
33use pci_core::cfg_space_emu::ConfigSpaceType0Emulator;
34use pci_core::cfg_space_emu::DeviceBars;
35use pci_core::cfg_space_emu::IntxInterrupt;
36use pci_core::msi::RegisterMsi;
37use pci_core::spec::hwid::ClassCode;
38use pci_core::spec::hwid::HardwareIds;
39use pci_core::spec::hwid::ProgrammingInterface;
40use pci_core::spec::hwid::Subclass;
41use std::io;
42use std::sync::Arc;
43use vmcore::device_state::ChangeDeviceState;
44use vmcore::interrupt::Interrupt;
45use vmcore::line_interrupt::LineInterrupt;
46use vmcore::save_restore::NoSavedState;
47use vmcore::save_restore::RestoreError;
48use vmcore::save_restore::SaveError;
49use vmcore::save_restore::SaveRestore;
50
51pub enum PciInterruptModel<'a> {
53 Msix(&'a mut dyn RegisterMsi),
54 IntX(PciInterruptPin, LineInterrupt),
55}
56
57enum InterruptKind {
58 Msix(MsixEmulator),
59 IntX(Arc<IntxInterrupt>),
60}
61
62#[derive(InspectMut)]
64pub struct VirtioPciDevice {
65 #[inspect(skip)]
66 device: Box<dyn VirtioDevice>,
67 #[inspect(skip)]
68 device_feature: [u32; 2],
69 #[inspect(hex)]
70 device_feature_select: u32,
71 #[inspect(skip)]
72 driver_feature: [u32; 2],
73 #[inspect(hex)]
74 driver_feature_select: u32,
75 msix_config_vector: u16,
76 queue_select: u32,
77 #[inspect(skip)]
78 events: Vec<pal_event::Event>,
79 #[inspect(skip)]
80 queues: Vec<QueueParams>,
81 #[inspect(skip)]
82 msix_vectors: Vec<u16>,
83 #[inspect(skip)]
84 interrupt_status: Arc<Mutex<u32>>,
85 #[inspect(hex)]
86 device_status: u32,
87 config_generation: u32,
88 config_space: ConfigSpaceType0Emulator,
89
90 #[inspect(skip)]
91 interrupt_kind: InterruptKind,
92 #[inspect(skip)]
93 doorbells: VirtioDoorbells,
94 #[inspect(skip)]
95 shared_memory_region: Option<Arc<dyn MappedMemoryRegion>>,
96 #[inspect(hex)]
97 shared_memory_size: u64,
98}
99
100impl VirtioPciDevice {
101 pub fn new(
102 device: Box<dyn VirtioDevice>,
103 mut interrupt_model: PciInterruptModel<'_>,
104 doorbell_registration: Option<Arc<dyn DoorbellRegistration>>,
105 mmio_registration: &mut dyn RegisterMmioIntercept,
106 shared_mem_mapper: Option<&dyn MemoryMapper>,
107 ) -> io::Result<Self> {
108 let traits = device.traits();
109 let queues = (0..traits.max_queues)
110 .map(|_| QueueParams {
111 size: QUEUE_MAX_SIZE,
112 ..Default::default()
113 })
114 .collect();
115 let events = (0..traits.max_queues)
116 .map(|_| pal_event::Event::new())
117 .collect();
118 let msix_vectors = vec![0; traits.max_queues.into()];
119
120 let hardware_ids = HardwareIds {
121 vendor_id: VIRTIO_VENDOR_ID,
122 device_id: VIRTIO_PCI_DEVICE_ID_BASE + traits.device_id,
123 revision_id: 1,
124 prog_if: ProgrammingInterface::NONE,
125 base_class: ClassCode::BASE_SYSTEM_PERIPHERAL,
126 sub_class: Subclass::BASE_SYSTEM_PERIPHERAL_OTHER,
127 type0_sub_vendor_id: VIRTIO_VENDOR_ID,
128 type0_sub_system_id: 0x40,
129 };
130
131 let mut caps: Vec<Box<dyn PciCapability>> = vec![
132 Box::new(ReadOnlyCapability::new(
133 "virtio-common",
134 VirtioCapability::new(VIRTIO_PCI_CAP_COMMON_CFG, 0, 0, 0, 56),
135 )),
136 Box::new(ReadOnlyCapability::new(
137 "virtio-notify",
138 VirtioNotifyCapability::new(0, 0, 56, 4),
139 )),
140 Box::new(ReadOnlyCapability::new(
141 "virtio-pci-isr",
142 VirtioCapability::new(VIRTIO_PCI_CAP_ISR_CFG, 0, 0, 60, 4),
143 )),
144 Box::new(ReadOnlyCapability::new(
145 "virtio-pci-device",
146 VirtioCapability::new(
147 VIRTIO_PCI_CAP_DEVICE_CFG,
148 0,
149 0,
150 64,
151 traits.device_register_length,
152 ),
153 )),
154 ];
155
156 let mut bars = DeviceBars::new().bar0(
157 0x40 + traits.device_register_length as u64,
158 BarMemoryKind::Intercept(
159 mmio_registration
160 .new_io_region("config", 0x40 + traits.device_register_length as u64),
161 ),
162 );
163
164 let msix: Option<MsixEmulator> = if let PciInterruptModel::Msix(register_msi) =
165 &mut interrupt_model
166 {
167 let (msix, msix_capability) = MsixEmulator::new(2, 64, *register_msi);
168 caps.insert(0, Box::new(msix_capability));
171 bars = bars.bar2(
172 msix.bar_len(),
173 BarMemoryKind::Intercept(mmio_registration.new_io_region("msix", msix.bar_len())),
174 );
175 Some(msix)
176 } else {
177 None
178 };
179
180 let shared_memory_size = traits.shared_memory.size;
181 let mut shared_memory_region = None;
182 if shared_memory_size > 0 {
183 let (control, region) = shared_mem_mapper
184 .expect("must provide mapper for shmem")
185 .new_region(
186 shared_memory_size.try_into().expect("region too big"),
187 "virtio-pci-shmem".into(),
188 )?;
189
190 caps.push(Box::new(ReadOnlyCapability::new(
191 "virtio-pci-shm",
192 VirtioCapability64::new(
193 VIRTIO_PCI_CAP_SHARED_MEMORY_CFG,
194 4, traits.shared_memory.id,
196 0,
197 shared_memory_size,
198 ),
199 )));
200
201 bars = bars.bar4(shared_memory_size, BarMemoryKind::SharedMem(control));
202 shared_memory_region = Some(region);
203 }
204
205 let mut config_space = ConfigSpaceType0Emulator::new(hardware_ids, caps, bars);
206 let interrupt_kind = match interrupt_model {
207 PciInterruptModel::Msix(_) => InterruptKind::Msix(msix.unwrap()),
208 PciInterruptModel::IntX(pin, line) => {
209 InterruptKind::IntX(config_space.set_interrupt_pin(pin, line))
210 }
211 };
212
213 Ok(VirtioPciDevice {
214 device,
215 device_feature: [
216 (traits.device_features & 0xffffffff) as u32
217 | VIRTIO_F_RING_EVENT_IDX
218 | VIRTIO_F_RING_INDIRECT_DESC,
219 (traits.device_features >> 32) as u32 | VIRTIO_F_VERSION_1,
220 ],
221 device_feature_select: 0,
222 driver_feature: [0; 2],
223 driver_feature_select: 0,
224 msix_config_vector: 0,
225 queue_select: 0,
226 events,
227 queues,
228 msix_vectors,
229 interrupt_status: Arc::new(Mutex::new(0)),
230 device_status: 0,
231 config_generation: 0,
232 interrupt_kind,
233 config_space,
234 doorbells: VirtioDoorbells::new(doorbell_registration),
235 shared_memory_region,
236 shared_memory_size,
237 })
238 }
239
240 fn update_config_generation(&mut self) {
241 self.config_generation = self.config_generation.wrapping_add(1);
242 if self.device_status & VIRTIO_DRIVER_OK != 0 {
243 *self.interrupt_status.lock() |= 2;
244 match &self.interrupt_kind {
245 InterruptKind::Msix(msix) => {
246 if let Some(interrupt) = msix.interrupt(self.msix_config_vector) {
247 interrupt.deliver();
248 }
249 }
250 InterruptKind::IntX(line) => line.set_level(true),
251 }
252 }
253 }
254
255 fn read_u32(&mut self, offset: u16) -> u32 {
256 assert!(offset & 3 == 0);
257 let queue_select = self.queue_select as usize;
258 match offset {
259 0 => self.device_feature_select,
261 4 => {
263 let feature_select = self.device_feature_select as usize;
264 if feature_select < self.device_feature.len() {
265 self.device_feature[feature_select]
266 } else {
267 0
268 }
269 }
270 8 => self.driver_feature_select,
272 12 => {
274 let feature_select = self.driver_feature_select as usize;
275 if feature_select < self.driver_feature.len() {
276 self.driver_feature[feature_select]
277 } else {
278 0
279 }
280 }
281 16 => (self.queues.len() as u32) << 16 | self.msix_config_vector as u32,
282 20 => self.queue_select << 24 | self.config_generation << 8 | self.device_status,
283 24 => {
284 let size = if queue_select < self.queues.len() {
285 self.queues[queue_select].size
286 } else {
287 0
288 };
289 let msix_vector = self.msix_vectors.get(queue_select).copied().unwrap_or(0);
290 (msix_vector as u32) << 16 | size as u32
291 }
292 28 => {
294 let enable = if queue_select < self.queues.len() {
295 if self.queues[queue_select].enable {
296 1
297 } else {
298 0
299 }
300 } else {
301 0
302 };
303 #[expect(clippy::if_same_then_else)] let notify_offset = if queue_select < self.queues.len() {
305 0 } else {
307 0
308 };
309 (notify_offset as u32) << 16 | enable as u32
310 }
311 32 => {
313 if queue_select < self.queues.len() {
314 self.queues[queue_select].desc_addr as u32
315 } else {
316 0
317 }
318 }
319 36 => {
321 if queue_select < self.queues.len() {
322 (self.queues[queue_select].desc_addr >> 32) as u32
323 } else {
324 0
325 }
326 }
327 40 => {
329 if queue_select < self.queues.len() {
330 self.queues[queue_select].avail_addr as u32
331 } else {
332 0
333 }
334 }
335 44 => {
337 if queue_select < self.queues.len() {
338 (self.queues[queue_select].avail_addr >> 32) as u32
339 } else {
340 0
341 }
342 }
343 48 => {
345 if queue_select < self.queues.len() {
346 self.queues[queue_select].used_addr as u32
347 } else {
348 0
349 }
350 }
351 52 => {
353 if queue_select < self.queues.len() {
354 (self.queues[queue_select].used_addr >> 32) as u32
355 } else {
356 0
357 }
358 }
359 56 => 0, 60 => {
362 let mut interrupt_status = self.interrupt_status.lock();
363 let status = *interrupt_status;
364 *interrupt_status = 0;
365 if let InterruptKind::IntX(line) = &self.interrupt_kind {
366 line.set_level(false)
367 }
368 status
369 }
370 offset if offset >= 64 => self.device.read_registers_u32(offset - 64),
371 _ => {
372 tracing::warn!(offset, "unknown bar read");
373 0xffffffff
374 }
375 }
376 }
377
378 fn write_u32(&mut self, address: u64, offset: u16, val: u32) {
379 assert!(offset & 3 == 0);
380 let queues_locked = self.device_status & VIRTIO_DRIVER_OK != 0;
381 let features_locked = queues_locked || self.device_status & VIRTIO_FEATURES_OK != 0;
382 let queue_select = self.queue_select as usize;
383 match offset {
384 0 => self.device_feature_select = val,
386 8 => self.driver_feature_select = val,
388 12 => {
390 let bank = self.driver_feature_select as usize;
391 if !features_locked && bank < self.driver_feature.len() {
392 self.driver_feature[bank] = val & self.device_feature[bank];
393 }
394 }
395 16 => self.msix_config_vector = val as u16,
396 20 => {
398 self.queue_select = val >> 16;
399 let val = val & 0xff;
400 if val == 0 {
401 let started = (self.device_status & VIRTIO_DRIVER_OK) != 0;
402 self.device_status = 0;
403 self.config_generation = 0;
404 if started {
405 self.doorbells.clear();
406 self.device.disable();
407 }
408 *self.interrupt_status.lock() = 0;
409 }
410
411 self.device_status |= val & (VIRTIO_ACKNOWLEDGE | VIRTIO_DRIVER | VIRTIO_FAILED);
412
413 if self.device_status & VIRTIO_FEATURES_OK == 0 && val & VIRTIO_FEATURES_OK != 0 {
414 self.device_status |= VIRTIO_FEATURES_OK;
415 self.update_config_generation();
416 }
417
418 if self.device_status & VIRTIO_DRIVER_OK == 0 && val & VIRTIO_DRIVER_OK != 0 {
419 let features =
420 ((self.driver_feature[1] as u64) << 32) | self.driver_feature[0] as u64;
421
422 let notification_address = (address & !0xfff) + 56;
423 for i in 0..self.events.len() {
424 self.doorbells.add(
425 notification_address,
426 Some(i as u64),
427 Some(2),
428 &self.events[i],
429 );
430 }
431 let queues = self
432 .queues
433 .iter()
434 .zip(self.msix_vectors.iter().copied())
435 .zip(self.events.iter().cloned())
436 .map(|((queue, vector), event)| {
437 let notify = match &self.interrupt_kind {
438 InterruptKind::Msix(msix) => {
439 if let Some(interrupt) = msix.interrupt(vector) {
440 interrupt
441 } else {
442 tracing::warn!(vector, "invalid MSIx vector specified");
443 Interrupt::null()
444 }
445 }
446 InterruptKind::IntX(line) => {
447 let interrupt_status = self.interrupt_status.clone();
448 let line = line.clone();
449 Interrupt::from_fn(move || {
450 *interrupt_status.lock() |= 1;
451 line.set_level(true);
452 })
453 }
454 };
455
456 QueueResources {
457 params: *queue,
458 notify,
459 event,
460 }
461 })
462 .collect();
463
464 self.device.enable(Resources {
465 features,
466 queues,
467 shared_memory_region: self.shared_memory_region.clone(),
468 shared_memory_size: self.shared_memory_size,
469 });
470
471 self.device_status |= VIRTIO_DRIVER_OK;
472 self.update_config_generation();
473 }
474 }
475 24 => {
477 let msix_vector = (val >> 16) as u16;
478 if !queues_locked && queue_select < self.queues.len() {
479 let val = val as u16;
480 let queue = &mut self.queues[queue_select];
481 if val > QUEUE_MAX_SIZE {
482 queue.size = QUEUE_MAX_SIZE;
483 } else {
484 queue.size = val;
485 }
486 self.msix_vectors[queue_select] = msix_vector;
487 }
488 }
489 28 => {
491 let val = val & 0xffff;
492 if !queues_locked && queue_select < self.queues.len() {
493 let queue = &mut self.queues[queue_select];
494 queue.enable = val != 0;
495 }
496 }
497 32 => {
499 if !queues_locked && queue_select < self.queues.len() {
500 let queue = &mut self.queues[queue_select];
501 queue.desc_addr = queue.desc_addr & 0xffffffff00000000 | val as u64;
502 }
503 }
504 36 => {
506 if !queues_locked && queue_select < self.queues.len() {
507 let queue = &mut self.queues[queue_select];
508 queue.desc_addr = (val as u64) << 32 | queue.desc_addr & 0xffffffff;
509 }
510 }
511 40 => {
513 if !queues_locked && queue_select < self.queues.len() {
514 let queue = &mut self.queues[queue_select];
515 queue.avail_addr = queue.avail_addr & 0xffffffff00000000 | val as u64;
516 }
517 }
518 44 => {
520 if !queues_locked && queue_select < self.queues.len() {
521 let queue = &mut self.queues[queue_select];
522 queue.avail_addr = (val as u64) << 32 | queue.avail_addr & 0xffffffff;
523 }
524 }
525 48 => {
527 if !queues_locked && (queue_select) < self.queues.len() {
528 let queue = &mut self.queues[queue_select];
529 queue.used_addr = queue.used_addr & 0xffffffff00000000 | val as u64;
530 }
531 }
532 52 => {
534 if !queues_locked && queue_select < self.queues.len() {
535 let queue = &mut self.queues[queue_select];
536 queue.used_addr = (val as u64) << 32 | queue.used_addr & 0xffffffff;
537 }
538 }
539 56 => {
541 if (val as usize) < self.events.len() {
542 self.events[val as usize].signal();
543 }
544 }
545 offset if offset >= 64 => self.device.write_registers_u32(offset - 64, val),
546 _ => {
547 tracing::warn!(offset, "unknown bar write at offset");
548 }
549 }
550 }
551}
552
553impl Drop for VirtioPciDevice {
554 fn drop(&mut self) {
555 self.device.disable();
557 }
558}
559
560impl VirtioPciDevice {
561 fn read_bar_u32(&mut self, bar: u8, offset: u16) -> u32 {
562 match bar {
563 0 => self.read_u32(offset),
564 2 => {
565 if let InterruptKind::Msix(msix) = &self.interrupt_kind {
566 msix.read_u32(offset)
567 } else {
568 !0
569 }
570 }
571 _ => !0,
572 }
573 }
574
575 fn write_bar_u32(&mut self, address: u64, bar: u8, offset: u16, value: u32) {
576 match bar {
577 0 => self.write_u32(address, offset, value),
578 2 => {
579 if let InterruptKind::Msix(msix) = &mut self.interrupt_kind {
580 msix.write_u32(offset, value)
581 }
582 }
583 _ => tracing::warn!(bar, offset, "Unknown write"),
584 }
585 }
586}
587
588impl ChangeDeviceState for VirtioPciDevice {
589 fn start(&mut self) {}
590
591 async fn stop(&mut self) {}
592
593 async fn reset(&mut self) {
594 }
596}
597
598impl ChipsetDevice for VirtioPciDevice {
599 fn supports_mmio(&mut self) -> Option<&mut dyn MmioIntercept> {
600 Some(self)
601 }
602
603 fn supports_pci(&mut self) -> Option<&mut dyn PciConfigSpace> {
604 Some(self)
605 }
606}
607
608impl SaveRestore for VirtioPciDevice {
609 type SavedState = NoSavedState; fn save(&mut self) -> Result<Self::SavedState, SaveError> {
612 Ok(NoSavedState)
613 }
614
615 fn restore(&mut self, NoSavedState: Self::SavedState) -> Result<(), RestoreError> {
616 Ok(())
617 }
618}
619
620impl MmioIntercept for VirtioPciDevice {
621 fn mmio_read(&mut self, address: u64, data: &mut [u8]) -> IoResult {
622 if let Some((bar, offset)) = self.config_space.find_bar(address) {
623 read_as_u32_chunks(offset, data, |offset| self.read_bar_u32(bar, offset))
624 }
625 IoResult::Ok
626 }
627
628 fn mmio_write(&mut self, address: u64, data: &[u8]) -> IoResult {
629 if let Some((bar, offset)) = self.config_space.find_bar(address) {
630 write_as_u32_chunks(offset, data, |offset, request_type| match request_type {
631 ReadWriteRequestType::Write(value) => {
632 self.write_bar_u32(address, bar, offset, value);
633 None
634 }
635 ReadWriteRequestType::Read => Some(self.read_bar_u32(bar, offset)),
636 })
637 }
638 IoResult::Ok
639 }
640}
641
642impl PciConfigSpace for VirtioPciDevice {
643 fn pci_cfg_read(&mut self, offset: u16, value: &mut u32) -> IoResult {
644 self.config_space.read_u32(offset, value)
645 }
646
647 fn pci_cfg_write(&mut self, offset: u16, value: u32) -> IoResult {
648 self.config_space.write_u32(offset, value)
649 }
650}
651
652pub(crate) mod capabilities {
653 use crate::spec::pci::VIRTIO_PCI_CAP_NOTIFY_CFG;
654 use pci_core::spec::caps::CapabilityId;
655
656 use zerocopy::Immutable;
657 use zerocopy::IntoBytes;
658 use zerocopy::KnownLayout;
659
660 #[repr(C)]
661 #[derive(Debug, IntoBytes, Immutable, KnownLayout)]
662 pub struct VirtioCapabilityCommon {
663 cap_id: u8,
664 cap_next: u8,
665 len: u8,
666 typ: u8,
667 bar: u8,
668 unique_id: u8,
669 padding: [u8; 2],
670 offset: u32,
671 length: u32,
672 }
673
674 impl VirtioCapabilityCommon {
675 pub fn new(len: u8, typ: u8, bar: u8, unique_id: u8, addr_off: u32, addr_len: u32) -> Self {
676 Self {
677 cap_id: CapabilityId::VENDOR_SPECIFIC.0,
678 cap_next: 0,
679 len,
680 typ,
681 bar,
682 unique_id,
683 padding: [0; 2],
684 offset: addr_off,
685 length: addr_len,
686 }
687 }
688 }
689
690 #[repr(C)]
691 #[derive(Debug, IntoBytes, Immutable, KnownLayout)]
692 pub struct VirtioCapability {
693 common: VirtioCapabilityCommon,
694 }
695
696 impl VirtioCapability {
697 pub fn new(typ: u8, bar: u8, unique_id: u8, addr_off: u32, addr_len: u32) -> Self {
698 Self {
699 common: VirtioCapabilityCommon::new(
700 size_of::<Self>() as u8,
701 typ,
702 bar,
703 unique_id,
704 addr_off,
705 addr_len,
706 ),
707 }
708 }
709 }
710
711 #[repr(C)]
712 #[derive(Debug, IntoBytes, Immutable, KnownLayout)]
713 pub struct VirtioCapability64 {
714 common: VirtioCapabilityCommon,
715 offset_hi: u32,
716 length_hi: u32,
717 }
718
719 impl VirtioCapability64 {
720 pub fn new(typ: u8, bar: u8, unique_id: u8, addr_off: u64, addr_len: u64) -> Self {
721 Self {
722 common: VirtioCapabilityCommon::new(
723 size_of::<Self>() as u8,
724 typ,
725 bar,
726 unique_id,
727 addr_off as u32,
728 addr_len as u32,
729 ),
730 offset_hi: (addr_off >> 32) as u32,
731 length_hi: (addr_len >> 32) as u32,
732 }
733 }
734 }
735
736 #[repr(C)]
737 #[derive(Debug, IntoBytes, Immutable, KnownLayout)]
738 pub struct VirtioNotifyCapability {
739 common: VirtioCapabilityCommon,
740 offset_multiplier: u32,
741 }
742
743 impl VirtioNotifyCapability {
744 pub fn new(offset_multiplier: u32, bar: u8, addr_off: u32, addr_len: u32) -> Self {
745 Self {
746 common: VirtioCapabilityCommon::new(
747 size_of::<Self>() as u8,
748 VIRTIO_PCI_CAP_NOTIFY_CFG,
749 bar,
750 0,
751 addr_off,
752 addr_len,
753 ),
754 offset_multiplier,
755 }
756 }
757 }
758
759 #[cfg(test)]
760 mod tests {
761 use super::*;
762 use pci_core::capabilities::PciCapability;
763 use pci_core::capabilities::ReadOnlyCapability;
764
765 #[test]
766 fn common_check() {
767 let common =
768 ReadOnlyCapability::new("common", VirtioCapability::new(0x13, 2, 0, 0x100, 0x200));
769 assert_eq!(common.read_u32(0), 0x13100009);
770 assert_eq!(common.read_u32(4), 2);
771 assert_eq!(common.read_u32(8), 0x100);
772 assert_eq!(common.read_u32(12), 0x200);
773 }
774
775 #[test]
776 fn notify_check() {
777 let notify = ReadOnlyCapability::new(
778 "notify",
779 VirtioNotifyCapability::new(0x123, 2, 0x100, 0x200),
780 );
781 assert_eq!(notify.read_u32(0), 0x2140009);
782 assert_eq!(notify.read_u32(4), 2);
783 assert_eq!(notify.read_u32(8), 0x100);
784 assert_eq!(notify.read_u32(12), 0x200);
785 }
786 }
787}