Legacy ISA Hardware Pass-Through via Custom QEMU/KVM Proxy
This documentation outlines the complete engineering pipeline required to bridge a legacy CIO-DAS08 ISA Data Acquisition Card into a virtualized Windows 98 SE / MS-DOS environment running on a modern Linux host machine with MBATX-CS620-H310C motherboard from DFI. It details the underlying hardware mapping architecture, deployment strategies, and validation protocols.
This documentation is specifically for MBATX-CS620-H310C motherboard running Ubuntu v18.04.5.
1. Hardware Architecture & Electrical Blueprint Map
Modern motherboards lack physical ISA slots. To bridge this gap, the target host machine utilizes a PCIe-to-ISA hardware bridge adapter (e.g., using an ITE or Winbond bridge controller).
The bridge maps the legacy 10-bit ISA I/O space into the host’s modern PCIe I/O Base Address Register (BAR) window.
DOWNBOUND PATH (Read / Write) UPBOUND PATH (Interrupt / IRQ)
=========================================== ===========================================
+-------------------------------------------+ +-------------------------------------------+
| WINDOWS 98 GUEST VM | | WINDOWS 98 GUEST VM |
| - Accesses ISA Ports: 0x390 - 0x39F | | - Virtual PIC intercepts Guest IRQ 5 |
| - Issues raw legacy 'inb'/'outb' macros | | - Interrupt Vector jumps to 'DASint' ISR |
+---------------------+---------------------+ +---------------------+---------------------+
| ^
| (Intercepted by | (qemu_set_irq pulses
| QEMU Memory Region) | virtual IRQ high/low)
v |
+-------------------------------------------+ +---------------------+---------------------+
| MIFE CUSTOM QEMU ISA PROXY | | MIFE CUSTOM QEMU ISA PROXY |
| - Tracks memory offset: addr (0 to 15) | | - Main Loop: Triggers Bottom Half (BH) |
| - Computes host port: 0x5390 + addr | | - Worker Thread: Blocks on read() loop |
| - Executes host assembly: inb / outb | | - Evaluates event state via host property |
+---------------------+---------------------+ +---------------------+---------------------+
| ^
| (Kernel space | (read() unblocks on
| execution via iopl) | host-irq-device descriptor)
v |
+-------------------------------------------+ +---------------------+---------------------+
| UBUNTU LINUX HOST KERNEL | | UBUNTU LINUX HOST KERNEL |
| - Directs outb/inb down PCIe BAR window | | - Catches hardware IRQ via UIO driver |
| | | - Exposes event signal to /dev/uio0 node |
+---------------------+---------------------+ +---------------------+---------------------+
| ^
v | (Motherboard physically
| | routes slot interrupt pin)
+---------------------+---------------------+ +---------------------+---------------------+
| PHYSICAL PCIe-TO-ISA BRIDGE | | PHYSICAL PCIe-TO-ISA BRIDGE |
| - Maps Host 0x5390 -> ISA 0x390 | | - Translates physical ISA IRQ slot line |
| - Directs cycle onto old motherboard bus | | straight into a Host PCIe MSI-X signal |
+---------------------+---------------------+ +---------------------+---------------------+
| ^
v | (Onboard 8253 Timer pulses
| | hardware IRQ line at 800Hz)
+---------------------+---------------------+ +---------------------+---------------------+
| CIO-DAS08 PHYSICAL ISA CARD | | CIO-DAS08 PHYSICAL ISA CARD |
| - Responds across 16-port register block | | - Pin state changes voltage level when |
| - Returns raw analog/digital data streams | | conversion process completes |
+-------------------------------------------+ +-------------------------------------------+
Physical-to-Virtual Address Mapping Layout
- Guest Base Address:
0x390(Configured via the card’s physical DIP switches). This is the default setting on the CIO-DAS08 ISA card used by MIFE v1. If different, this is configurable in the MIFE Custom QEMU ISA Proxy. - Host Translation Base:
0x5390(Assigned by the host PCIe bus architecture). This is configurable in the MIFE Custom QEMU ISA Proxy. Make sure this address has not been taken by other devices on the host. - Address Space Window: 16 Bytes (
0x390to0x39F). - Guest IRQ Address:
7 - Host IRQ Device:
/dev/uio0We patch a raw I/O listener directly into QEMU to intercept the Windows 98 guest’s legacy 0x390 port calls at the hypervisor level, execute them on the real physical host CPU at 0x5390, and pass the real data back.
2. Deployment
To enable low-latency memory mapping (mmap) of physical addresses under hardware-accelerated virtualization (KVM), standard Linux security mechanics must be explicitly configured. This document outlines how to establish this baseline persistently.
Prerequisites
Before beginning the installation, ensure the following conditions are met:
- Target OS: Ubuntu 18.04.05 LTS.
- Compiled Artifacts: mife-qemu-system-x86_64.zip
mife_uio.ko(The custom-compiled host kernel module).mife-qemu-system-x86_64(The custom-compiled QEMU binary).
Step 2.1 Kernel Driver Installation (mife_uio)
2.1.1: Copy and Install the kernal driver (mife_uio)
The custom kernel module mife_uio.ko acts as the direct hardware listener. It must be installed into the system driver library so it automatically loads when the computer boots up.
2.1.2 Manual Loading Verification
First, verify that the compiled module loads successfully into the running kernel:
sudo modprobe uio
sudo insmod mife_uio.ko irq_num=7
Verify the driver initialized by running:
lsmod | grep mife_uio
dmesg | tail -n 10
2.1.3 Permanent System Integration
To ensure the driver survives system updates and reboots, copy it into the official kernel driver tree:
-
Create a dedicated module directory and copy the driver:
sudo mkdir -p /lib/modules/$(uname -r)/kernel/drivers/uio/mife/ sudo cp mife_uio.ko /lib/modules/$(uname -r)/kernel/drivers/uio/mife/ -
Rebuild the system module dependency map:
sudo depmod -a -
Configure default boot parameters for the module (e.g., setting the target physical IRQ line to 7. Change it to what suits you.):
echo "options mife_uio irq_num=7" | sudo tee /etc/modprobe.d/mife_uio.conf -
Force the operating system to automatically load the driver at startup:
echo "mife_uio" | sudo tee -a /etc/modules
Step 2.2: Persistent Hardware Permissions Configuration (udev)
When the kernel loads the driver, it creates raw virtual device nodes (/dev/uio0 and /dev/kvm). By default, Linux locks these down exclusively for the root administrative user. We use udev rules to grant necessary access rights to the virtualization runtime layer automatically.
2.2.1 Create Unified MIFE Security Policy Rules
Open a new custom rules file using a text editor:
sudo nano /etc/udev/rules.d/99-mife-uio.rules
Paste the following rules configuration into the file exactly as shown:
SUBSYSTEM=="uio", KERNEL=="uio*", GROUP="kvm", MODE="0660"
Save and exit (Ctrl+O, then Ctrl+X).
2.2.2 Trigger and Apply Rules Instantly
Force the system’s hardware manager daemon to reload and enforce the rules immediately without rebooting the machine:
sudo udevadm control --reload-rules
sudo udevadm trigger
2.2.3 Verify Node Security State
Execute a permissions scan on the generated system targets to confirm successful propagation:
ls -l /dev/uio0
Expected Terminal Output:
crw-rw-rw- 1 root kvm 238, 0 Jul 8 11:00 /dev/uio0
Step 2.3: Hardware Virtualization Group Alignment
The execution shell must explicitly broadcast membership in the system’s hardware acceleration layers.
2.3.1 Assign User Groups
Run the following commands to permanently add current user to the kernel acceleration groups:
sudo usermod -aG kvm $USER
If the user group kvm doesn’t exist, created it:
sudo groupadd kvm
2.3.2 Live Environment Session Realignment
To activate these new group memberships inside an active terminal window without completely logging out and logging back in, execute:
newgrp kvm
Verify your active group configuration includes kvm by checking:
id
Step 2.4: Update Libvirt’s Device ACL (cgroups)
Libvirt strictly controls which devices a VM is allowed to access using Linux cgroups. You have to explicitly whitelist your UIO device.
- Edit the QEMU configuration file:
sudo nano /etc/libvirt/qemu.conf
- Search for the cgroup_device_acl array. Uncomment it and add your UIO device node (e.g., “/dev/uio0”) to the list. It should look something like this:
cgroup_device_acl = [
"/dev/null", "/dev/full", "/dev/zero",
"/dev/random", "/dev/urandom",
"/dev/ptmx", "/dev/kvm", "/dev/kqemu",
"/dev/rtc","/dev/hpet", "/dev/sev",
"/dev/uio0"
]
- Save the change. Then restart the libvirtd service:
sudo systemctl restart libvirtd
Step 2.5 Proxy Installation (mife-qemu-system-x86_64)
2.5.1: Copy and Register the proxy Binary on the Target Host
-
Transfer the
mife-qemu-system-x86_64binary to the host machine. -
Store the binary in /usr/bin:
sudo cp mife-qemu-system-x86_64 /usr/bin/mife-qemu-system-x86_64
sudo chmod +x /usr/bin/mife-qemu-system-x86_64
- Grant raw I/O privileges to the binary. Because standard users cannot execute raw hardware assembly inb/outb loops, grant the new binary raw I/O capabilities using Linux file traits:
sudo setcap cap_sys_rawio+ep /usr/local/bin/mife-qemu-system-x86_64
2.5.2: Updating Your Domain XML Configuration within Libvirt (Virtual Machine Manager)
In QEMU, property assignments are added directly to the device call string separated by commas. Boolean options can be set using on/off or true/false. QEMU accepts both base-10 integers and traditional hexadecimal prefix formats (0x) for the ports.
Open your virtual machine XML configuration profile, scroll all the way to the top of your XML editor in Virtual Machine Manager. Look at the very first <domain> tag.
It usually looks like this:
<domain type='kvm'>
You must inject the QEMU XML namespace attribute into this tag so Libvirt knows how to read custom command-line blocks. Change it to this:
<domain xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0' type='kvm'>
At the <devices> section, and look for the <emulator> tag near the top of that section. By default, it will look like this:
<devices>
<emulator>/usr/bin/qemu-system-x86_64</emulator>
Replace that path with the absolute path to your custom-compiled binary:
<devices>
<emulator>/usr/bin/mife-qemu-system-x86_64</emulator>
Scroll all the way to the very bottom of the XML file. Insert your custom command-line arguments right above the closing </domain> tag:
<qemu:commandline>
<qemu:arg value='-device'/>
<qemu:arg value='mife-host-isa-proxy,host-port=0x5390,guest-port=0x390,guest-irq=7,host-irq-device=/dev/uio0,debug=off,log_sample_rate=400'/>
<qemu:arg value='-rtc'/>
<qemu:arg value='base=localtime,clock=host,driftfix=slew'/>
</qemu:commandline>
</domain>
You can adjust the host-port/guest-port/guest-irq/host-irq-deviceas/debug/log_sample_rate as appropriate. It’s recommended to ONLY set debug to ON when confirming ports mapping. When turn on debug, it will have performance impact with data acquisition.
⚠️ Critical host/guest port assignment
Always keep guest-port and host-port aligned to a multiple of 16 (ending in 0 in hex, such as 0x300, 0x390, 0x5390) to ensure MIFE1 can work properly.
2.6 Troubleshooting Summary
| Symptoms | Identified Root Cause | Targeted Remediation |
|---|---|---|
no such file or directory on /dev/uio0 |
Host module is missing from the running kernel context. | Run sudo modprobe mife_uio to manually force-insert the driver module. |
Operation not permitted during KVM boot |
The QEMU binary was launched directly as standard user without required permission. | Ensure permission assigned in step 2.2. |
| VM Boots but data is not transferring | Misaligned IRQ settings or Port references across environments. | Ensure the values in the MIFE App match the VM’s guest-port= and guest-irq= configuration. |
3. Verification & Testing Protocol
To ensure full bi-directional pass-through behavior without loading external driver payloads, use the vintage native MS-DOS debug environment.
3.1: Host Side Live Log Monitoring
Before issuing commands inside the guest machine, establish a live trace interface on the Ubuntu Host to bypass systemd tracking blocks: (only do this step if debug option is ON in the VM config XML file for the proxy)
sudo tail -f /var/log/libvirt/qemu/<your VM name>.log
3.2: Executing MS-DOS Debug Sweeps
Boot the Windows 98 guest environment into an MS-DOS prompt window. Initialize the low-level utility tool:
C:\> debug
CRITICAL SYNTAX NOTE: Vintage versions of
debug.exeparse spaces strictly. Separate the target I/O port address and data byte payload using a comma (o port,data).
Execute an sequential sweeping check of the card’s assigned registers:
-i 390
-i 391
-i 392
-i 394
-i 396
-i 397
3.3: Interpreting the Diagnostics Data Profile
If the architecture pipeline is fully intact, the hardware registers will match the factory profile shown below:
-i 390 --> Returns 00 (ADC Low Byte: Expected idle condition state)
-i 391 --> Returns 00 (ADC High Byte: Expected idle condition state)
-i 392 --> Returns 77 (Status Flag: Binary 01110111, indicating steady digital inputs)
-i 394 --> Returns FD (Intel 8254 Counter 0: Live fluctuating value proving clock ticks)
-i 396 --> Returns 00 (Intel 8254 Counter 2: Stable, unassigned background counter clock)
-i 397 --> Returns FF (8254 Control Register: Write-only register; reading forces open-bus floating)
3.4: Verifying Complete Hardware Write Execution
To verify full outbound control pathing to the physical card, attempt to cycle the internal Multiplexer (MUX) configuration lines:
-o 392,00 <-- Sets MUX targeting Analog Channel 0
-i 392 <-- Read status line results back
-o 392,07 <-- Sets MUX targeting Analog Channel 7
-i 392 <-- Confirm changes register on the board hardware
Your host log output will capture every single execution step simultaneously, verifying that your software logic and hardware connection are fully operational.