Számítógéppel generált álomvilág
Virtuális valóság egy 286-os processzor számára
Mi a valóság? Hogy határoznád meg? Mivel? Ha arról beszélsz, amit érzel, amit szagolsz, ízlelsz, látsz... a valóság csupán az agyad által megfejtett elektromos jelhalmaz.
— Morpheus, Mátrix
Ha a processzor a számítógép agya, akkor neki is lehet része egyfajta virtuális valóságban? Szimulált memória, szoftveresen definiált perifériák, mesterségesen generált megszakítások.
Életem első számítógépe egy 286-os volt, 1 MB RAM-mal és ha jól emlékszem 50 MB HDD-vel. Ezért arra az elhatározásra jutottam, hogy beszerzek egy 286-os processzort és megpróbálom a számítógép többi részét köré szimulálni. Vagy legalábbis rábírni arra, hogy elinduljon és futtasson meg valami egyszerű assembly kódot.
Két évvel ezelőtt meg is rendeltem kettő darab (annyi volt egy csomagban) Harris 80C286-12 processzort. Homályosak már az emlékeim, de a C a nevében azt hiszem lényeges, mert ezek azok a fajták, amik nem annyira kényesek az órajel pontosságára (a 12 a végén azt jelenti, hogy 12 Mhz-en szeret futni), akár kézzel is lehet léptetni őket.
Először nem jöttek a sikerélmények, úgyhogy a fiókba került a projekt. Aztán idén újra elővettem és megpróbáltam kideríteni, hogy hol mehettek félre a dolgok.
Összeszerelés
A processzor egy PLCC-68-as foglalatba megy bele. A foglalat lábai nem alkalmasak arra, hogy jumper kábeleket dugdossunk rá, így a foglalat egy adapter NYÁK-ra került, amin vannak jumper kábel kompatibilis kiállások. A chip és a foglalat csatlakozó kiosztása is szerepel az adatlapon, de az adapter NYÁK még csavar egy kicsit a dolgokon, így készítettem hozzá egy kis konvertáló táblázatot.
A táblázat abban is segített, hogy a különböző ki- és bemenetek azonosítva legyenek, amik később a Raspberry Pi-ra való csatlakozáskor lesznek hasznosak. Mint az látszik, nem kevesebb, mint 57 pin-re van szükség, amihez a Pi kevés lesz. Az MCP23S17 típusú IO bővítő sietett itt a segítségünkre, amivel ugyan nem tudnánk kipörgetni a processzor által támogatott hajmeresztő 12 Mhz-es sebességet, de szerencsére nem is ez a cél.
A chip 16 darab IO pin-t tartalmaz, így négy chip-re lesz szükségünk. Bár pin-enként lehet állítani, hogy ki- vagy bemenetek legyenek, azért próbáltam logikusan csoportosítani őket. A bővítőnek A és B oldala van, oldalanként 8 pinnel, a végeredmény a következő lett:
┌───┬──┬───┐
┤ └──┘ ├
┤ ├
┤ FLAG ├ ERROR
┤ ├ BUSY
┤ ADDR:100 ├ INTR
READY ┤ ├ NMI
RESET ┤B A├ PEREQ
CLK ┤ ├ HOLD
└──────────┘
┌───┬──┬───┐
HLDA ┤ └──┘ ├ A23
COD/INTA ┤ ├ A22
M/IO ┤ MISC ├ A21
LOCK ┤ ├ A20
BHE ┤ ADDR:011 ├ A19
S1 ┤ ├ A18
S0 ┤B A├ A17
PEACK ┤ ├ A16
└──────────┘
┌───┬──┬───┐
A8 ┤ └──┘ ├ A7
A9 ┤ ├ A6
A10 ┤ ADDR ├ A5
A11 ┤ ├ A4
A12 ┤ ADDR:010 ├ A3
A13 ┤ ├ A2
A14 ┤B A├ A1
A15 ┤ ├ A0
└──────────┘
┌───┬──┬───┐
D8 ┤ └──┘ ├ D7
D9 ┤ ├ D6
D10 ┤ DATA ├ D5
D11 ┤ ├ D4
D12 ┤ ADDR:001 ├ D3
D13 ┤ ├ D2
D14 ┤B A├ D1
D15 ┤ ├ D0
└──────────┘
A Pi SPI-n tud kommunikálni a bővítőkkel. Több megoldás is létezik erre, én azt választottam, hogy mindegyik chip egyszerre aktív és hardver cím alapján döntik el, hogy kinek szólnak a Pi felől érkező üzenetek.
A lila színű kábellel bekötött RESET pin-t esetünkben nem szükséges a Pi-ról vezérelni, de az egyik hibakeresés során ezt is kipróbáltam és így maradt. Nincs más hátra, mint ipari mennyiségű jumper kábellel összekötni az egészet és át is térhetünk a programozásra.
IO bővítés
Az MCP23S17 tudásának viszonylag kis részére van csak szükségünk. Az IO pin-ek irányát kell csak beállítanunk és írni/olvasni. A konfigurálás regiszterek értékeinek változtatásával történik. Először is engedélyeznünk kell a hardver címek használatát. Alapból mindenkinek 000 a címe, így ha erre a címre küldünk egy regiszter változtatást (az IOCON regiszterben lévő HAEN bit), akkor mind a négy chip-en egyszerre kapcsolódnak be a hardver címek.
Néhány órányi (napnyi) fejtörés következett, amire végre kiderült, hogy ez még nem biztos, hogy elég a megfelelő működéshez. Kell egy üzenetet küldenünk a beállított hardver címre is, hogy engedélyezzük a hardver címeket (elég furcsa, tudom). Tehát ha mondjuk 101-es hardver címet állítottunk be, akkor az eredeti, 000-ra küldött regiszter módosítási üzenetet el kell küldenünk újra a 101-nek is.
Most, hogy a hardver címekkel megvagyunk az egyes chip-ek IODIRA és IODIRB regiszterét kell a megfelelő irányba állítani. A csoportosítás miatt az egész oldalt egyszerre állíthatjuk olvasásra (11111111) vagy írásra (00000000). További részletek a chip adatlapján.
Eredetileg egy Pi Zero-val kezdtem a munkát, de a végül egy Pi Pico mellett kötöttem ki, amin MicroPython fut. A chip-ek kezelésére az alábbi kis osztályka született:
class MCP23S17:
IODIRA = 0x00
IODIRB = 0x01
IOCON = 0x0B
GPIOA = 0x12
GPIOB = 0x13
def __init__(self, address, spi, cs):
self.__address = address
self.__spi = spi
self.__cs = cs
def init(self):
self.__writeRegister(0b01000000, self.IOCON, 0b00001000)
self.writeRegister(self.IOCON, 0b00001000)
def writeRegister(self, reg, value):
self.__writeRegister(self.__address, reg, value)
def readRegister(self, reg):
tx = bytearray([self.__address | 1, reg, 0])
rx = bytearray(3)
self.__cs.value(0)
self.__spi.write_readinto(tx, rx)
self.__cs.value(1)
return rx[2]
def __writeRegister(self, address, reg, value):
self.__cs.value(0)
self.__spi.write(bytes([address, reg, value]))
self.__cs.value(1)
Az init-ben szépen látszik, hogy kétszer állítjuk be az IOCON regiszter értékét. Az osztályt a következő módon tudjuk használni a processzorral való kommunikációhoz:
spi = SPI(0, baudrate=1000000, sck=Pin(2), mosi=Pin(3), miso=Pin(4))
cs = Pin(5, mode=Pin.OUT, value=1)
rst = Pin(6, mode=Pin.OUT, value=0)
chip_data = MCP23S17(0b01000010, spi, cs)
chip_addr = MCP23S17(0b01000100, spi, cs)
chip_misc = MCP23S17(0b01000110, spi, cs)
chip_flag = MCP23S17(0b01001000, spi, cs)
rst.value(1)
chip_data.init()
chip_addr.init()
chip_misc.init()
chip_flag.init()
chip_data.writeRegister(MCP23S17.IODIRA, 0xff)
chip_data.writeRegister(MCP23S17.IODIRB, 0xff)
chip_addr.writeRegister(MCP23S17.IODIRA, 0xff)
chip_addr.writeRegister(MCP23S17.IODIRB, 0xff)
chip_misc.writeRegister(MCP23S17.IODIRA, 0xff)
chip_misc.writeRegister(MCP23S17.IODIRB, 0xff)
chip_flag.writeRegister(MCP23S17.IODIRA, 0x00)
chip_flag.writeRegister(MCP23S17.IODIRB, 0x00)
Első körben képes voltam lehagyni innen az init hívásokat és csodálkoztam, hogy miért nem működik. A pin-ek nagy részét olvasásra állítjuk, csak a flag-eket kell írásra.
Az alapállapot
Mielőtt bármibe belekezdhetnénk, RESET-elnünk kell a processzort. Ehhez legalább 16 órajelciklusnyi ideig kell a RESET flag-et bekapcsolva tartanunk, és a ki- és bekapcsolásnak szinkronban kell lennie az órajel flag-gel is. Először is csináltam pár konstanst a flag-eknek, hogy egyszerűbb legyen az élet:
# chip_flag GPIOA
FLAG_ERROR = 0x20
FLAG_BUSY = 0x10
FLAG_INTR = 0x08
FLAG_NMI = 0x04
FLAG_PEREQ = 0x02
FLAG_HOLD = 0x01
# chip_flag GPIOB
FLAG_CLK = 0x80
FLAG_RESET = 0x40
FLAG_READY = 0x20
# chip_misc GPIOB
FLAG_PEACK = 0x80
FLAG_S0 = 0x40
FLAG_S1 = 0x20
FLAG_BHE = 0x10
FLAG_LOCK = 0x08
FLAG_M_IO = 0x04
FLAG_COD_INTA = 0x02
FLAG_HLDA = 0x01
Ezt érdemes összenézni a fentebbi MCP23S17 pin kiosztással. Az egyes oldalak 8 pin-jét 8 bitnyi / 1 byte-nyi adatként kezeljük. Például a 'misc' chip GPIOB oldala által kiadott byte-ban a HLDA flag a legkisebb helyi értékű szám, a PEACK pedig a legnagyobb helyi értékű.
PEACK
↓
10100111
↑
HLDA
A flag-ek segítségével jöhet is a RESET:
for i in range(17):
chip_flag.writeRegister(MCP23S17.GPIOB, FLAG_CLK | FLAG_RESET)
time.sleep(0.001)
chip_flag.writeRegister(MCP23S17.GPIOB, FLAG_RESET)
time.sleep(0.001)
A sleep-eket teljesen hasra ütés szerűen választottam, nem kell igazodnunk semmilyen ütemhez. A RESET során a processzornak egy meghatározott állapotba kell kerülnie. Ezt az alábbi kis kóddal tudjuk ellenőrizni:
data = chip_addr.readRegister(MCP23S17.GPIOA)
print('A7-0: ' + str(bin(data)))
data = chip_addr.readRegister(MCP23S17.GPIOB)
print('A15-8: ' + str(bin(data)))
data = chip_misc.readRegister(MCP23S17.GPIOA)
print('A23-16: ' + str(bin(data)))
data = chip_misc.readRegister(MCP23S17.GPIOB)
print('PEACK, S0, S1, BHE, LOCK, M/IO, COD/INTA, HLDA: ' + str(bin(data)))
Az értékek, amit látnunk kell így néznek ki:
A7-0: 0b11111111
A15-8: 0b11111111
A23-16: 0b11111111
PEACK, S0, S1, BHE, LOCK, M/IO, COD/INTA, HLDA: 0b11111000
Furcsa módon engem a következő fogadott:
A7-0: 0b11111111
A15-8: 0b11111000
A23-16: 0b11111111
PEACK, S0, S1, BHE, LOCK, M/IO, COD/INTA, HLDA: 0b11111000
Nehéz volt nem észrevenni, hogy a második és a negyedik sor megegyezik. Ellenőriztem az összes csatlakozást, szétszedtem az egészet, LED-ek segítségével debug-oltam, hogy jó helyre mennek-e az értékek, amiket írok, kicseréltem a chip-et, amihez az A15-8 pin-ek voltak rendelve, kicseréltem a processzort a tartalékra, ezerszer is átolvastam a kódot, de semmi sem segített.
Végül az MCP23S17-nél említett hardver címes mókázás lett a megoldás. Lényeg, hogy ha minden rendben ment, elengedhetjük a RESET flag-et és elkezdődhet a boot-olás.
chip_flag.writeRegister(MCP23S17.GPIOB, FLAG_CLK | FLAG_RESET)
time.sleep(0.001)
chip_flag.writeRegister(MCP23S17.GPIOB, 0)
time.sleep(0.001)
Az inicializálás
Ezek után a processzornak 50 órajelcikluson belül el kell kezdenie olvasni a 0xFFFFF0 címről az első végrehajtandó utasítást. A COD/INTA, M/IO, S0 és S1 flag-ek határozzák meg, hogy mit szeretne csinálni a processzor.
COD/INTA |
M/IO |
S0 |
S1 |
Művelet |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | Interrupt acknowledge |
| 0 | 1 | 0 | 0 | halt / shutdown |
| 0 | 1 | 0 | 1 | Memory data read |
| 0 | 1 | 1 | 0 | Memory data write |
| 1 | 0 | 0 | 1 | I/O read |
| 1 | 0 | 1 | 0 | I/O write |
| 1 | 1 | 0 | 1 | Memory instruction read |
A kevésbé érdekeseket kihagytam a táblázatból, az adatlapon megtekinthetőek. De a maradékból is csak erre a négyre lesz szükségünk a kis tesztünkhöz:
- halt / shutdown
- memory data write
- memory data read
- memory instruction read
Szóval elkezdjük küldeni az órajeleket és várunk, hogy elérkezzünk az első 'Memory instruction read'-ig:
cycle = 1
while True:
print(f'#{cycle}')
chip_flag.writeRegister(MCP23S17.GPIOB, FLAG_CLK)
time.sleep(0.001)
chip_flag.writeRegister(MCP23S17.GPIOB, 0)
time.sleep(0.001)
data = chip_misc.readRegister(MCP23S17.GPIOB)
PEACK = data & FLAG_PEACK
S0 = data & FLAG_S0
S1 = data & FLAG_S1
BHE = data & FLAG_BHE
LOCK = data & FLAG_LOCK
M_IO = data & FLAG_M_IO
COD_INTA = data & FLAG_COD_INTA
HLDA = data & FLAG_HLDA
if not COD_INTA and M_IO and not S1 and not S0:
print('halt / shutdown')
sys.exit(0)
elif not COD_INTA and M_IO and not S1 and S0:
print('Memory data read')
elif not COD_INTA and M_IO and S1 and not S0:
print('Memory data write')
elif COD_INTA and M_IO and not S1 and S0:
print('Memory instruction read')
time.sleep(0.01)
cycle += 1
Ha sikeresen megérkezünk, akkor elkezdhetünk mondjuk NOP (0x90) utasításokat küldeni. Az adat buszt írásra állítjuk, ráküldjük a NOP utasítást, küldünk egy órajelet és visszaállítjuk az adat buszt olvasásra.
chip_data.writeRegister(MCP23S17.IODIRA, 0x00)
chip_data.writeRegister(MCP23S17.IODIRB, 0x00)
chip_data.writeRegister(MCP23S17.GPIOA, 0x90)
chip_data.writeRegister(MCP23S17.GPIOB, 0x90)
chip_flag.writeRegister(MCP23S17.GPIOB, FLAG_CLK)
time.sleep(0.001)
chip_flag.writeRegister(MCP23S17.GPIOB, 0)
time.sleep(0.001)
chip_data.writeRegister(MCP23S17.IODIRA, 0xFF)
chip_data.writeRegister(MCP23S17.IODIRB, 0xFF)
Bonyolult matematikai műveletek
Ez mind szép és jó, de nézzünk valami érdekesebbet, amihez olvasni és írni is kell a memóriát. Egy egyszerű kis program, ami a memóriából olvas be két számot, összeadja és visszaírja a memóriába az eredményt.
Mivel a memóriának eléggé a végén kezdtünk (0xFFFFF0), így nincs túl sok helyünk, először el kell ugranunk máshova.
reset.asm
[cpu 286]
org 0xfff0
jmp 0x0000:0x0500
Aztán jöhet az összeadás:
add.asm
[cpu 286]
org 0x0500
xor ax, ax
mov ds, ax
mov ax, [num1]
add ax, [num2]
mov [result], ax
hlt
; Data
num1 dw 0x1234
num2 dw 0x000a
result dw 0x0000
A nasm program segítségével generálhatunk belőle binárist is:
$ nasm reset.asm
$ nasm add.asm
Majd egy rövid kis Python script segítségével Python-formára hozhatjuk, hogy belerakhassuk a virtuális memóriánkba:
hex_dump.py
import sys
with open(sys.argv[1], "rb") as f:
data = f.read()
hex_values = ", ".join(f"0x{byte:02x}" for byte in data)
print(f"[{hex_values}]")
$ python hex_dump.py reset
[0xea, 0x00, 0x05, 0x00, 0x00]
$ python hex_dump.py add
[0x31, 0xc0, 0x8e, 0xd8, 0xa1, 0x0f, 0x05, 0x03, 0x06, 0x11, 0x05, 0xa3, 0x13, 0x05, 0xf4, 0x34, 0x12, 0x0a, 0x00, 0x00, 0x00]
A memória szimulálására a következő osztálykát raktam össze:
class Memory:
def __init__(self):
self.__data = {}
def load(self, base, data):
for i, b in enumerate(data):
self.__data[base + i] = b
def __getitem__(self, address):
return self.__data.get(address, 0x00)
def __setitem__(self, address, value):
self.__data[address] = value & 0xFF
Csak egy egyszerű dict egy kis segédfüggvénnyel, aminek segítségével tetszőleges címre tölthetünk be adatot. Amit aztán meg is teszünk a nasm által generált kóddal:
MEMORY = Memory()
MEMORY.load(0x000500, [
0x31, 0xc0,
0x8e, 0xd8,
0xa1, 0x0f, 0x05,
0x03, 0x06, 0x11, 0x05,
0xa3, 0x13, 0x05,
0xf4,
0x34, 0x12,
0x0a, 0x00,
0x00, 0x00
])
MEMORY.load(0xfffff0, [
0xea, 0x00, 0x05, 0x00, 0x00
])
Már csak az elágazásokat kell kitöltenünk, de előtte még szót kell ejteni a BHE flag-ről és az A0 értékről.
BHE |
A0 |
Művelet |
|---|---|---|
| 0 | 0 | Word transfer |
| 0 | 1 | Byte transfer on upper half of data bus (D15 - D8) |
| 1 | 0 | Byte transfer on lower half of data bus (D7 - D0) |
Tehát egy adat buszhoz kapcsolódó művelet során tudjuk a teljes adatbuszt, a nagyobb helyi értékű felét vagy a kisebb helyi értékű felét írni/olvasni.
A 'Memory data read' esetünkben nagyon hasonlít a 'Memory instruction read'-hez, úgyhogy össze is vonhatjuk a kettőt. Az újdonság a fenti flag-ek kezelése és a kamu memória használata lesz.
address = (a3 << 16) + (a2 << 8) + a1
if not COD_INTA and M_IO and not S1 and S0:
print('Memory data read 0x{:06X}'.format(address))
else:
print('Memory instruction read 0x{:06X}'.format(address))
if not BHE and not A0:
print('Word transfer 0x{:02X}{:02X}'.format(MEMORY[address + 1], MEMORY[address]))
chip_data.writeRegister(MCP23S17.IODIRA, 0x00)
chip_data.writeRegister(MCP23S17.IODIRB, 0x00)
chip_data.writeRegister(MCP23S17.GPIOA, MEMORY[address])
chip_data.writeRegister(MCP23S17.GPIOB, MEMORY[address + 1])
elif not BHE and A0:
print('Byte transfer on upper half of data bus 0x{:02X}'.format(MEMORY[address]))
chip_data.writeRegister(MCP23S17.IODIRB, 0x00)
chip_data.writeRegister(MCP23S17.GPIOB, MEMORY[address])
elif BHE and not A0:
print('Byte transfer on lower half of data bus 0x{:02X}'.format(MEMORY[address]))
chip_data.writeRegister(MCP23S17.IODIRA, 0x00)
chip_data.writeRegister(MCP23S17.GPIOA, MEMORY[address])
chip_flag.writeRegister(MCP23S17.GPIOB, FLAG_CLK)
time.sleep(0.001)
chip_flag.writeRegister(MCP23S17.GPIOB, 0)
time.sleep(0.001)
chip_data.writeRegister(MCP23S17.IODIRA, 0xFF)
chip_data.writeRegister(MCP23S17.IODIRB, 0xFF)
Nem sokkal bonyolultabb, mint az eredeti NOP-os megoldásunk, de azért került bele egy extra pofon, amibe bele lehet futni. Milyen sorrendben tegyük a byte-okat az adat buszra? A GPIOA regiszter az adat busz legkisebb helyi értékű byte-ja, a GPIOB pedig a legnagyobb. Így például a kezdeti JMP utasításunk (0xea00) 0x00ea-ként fog utazni (little-endian).
Érdemes kicsit visszagörgetni és megnézni, hogy már a nasm is csinált ilyen cseréket, például az összeadáshoz használt 0x1234 számunk a memóriában 0x3412-ként van tárolva.
A 'Memory data write' nagyon egyszerű, csak a kamu memóriát kell használnunk:
address = (a3 << 16) + (a2 << 8) + a1
print('Memory data write 0x{:06X}'.format(address))
if not BHE and not A0:
print('Word transfer 0x{:02X}{:02X}'.format(d2, d1))
MEMORY[address] = d1
MEMORY[address + 1] = d2
elif not BHE and A0:
print('Byte transfer on upper half of data bus 0x{:02X}'.format(d2))
MEMORY[address] = d2
elif BHE and not A0:
print('Byte transfer on lower half of data bus 0x{:02X}'.format(d1))
MEMORY[address] = d1
Itt is megfigyelhető a little-endian sorrend, bár a futtatások során nem láttam olyat, hogy egyszerre két byte-ot próbált volna írni a memóriába.
A 'halt / shutdown' során pedig csak kiíratjuk a memóriából az összeadás eredményét és kilépünk:
print('Result: 0x{:04X}'.format((MEMORY[0x000514] << 8) + MEMORY[0x000513]))
sys.exit(0)
A végeredmény
A futtatásnak végül egy ilyesmi kimenetet kell kiadnia, ahol látszik, ahogy beolvassuk a kezdeti JMP utasítást, elugrunk az új címre, továbbolvassuk onnan az instrukciókat, beolvassuk a memóriából a két összeadandó számot majd visszaírjuk a memóriába az eredményt:
RESET
A7-0: 0b11111111
A15-8: 0b11111111
A23-16: 0b11111111
PEACK, S0, S1, BHE, LOCK, M/IO, COD/INTA, HLDA: 0b11111000
START
#40
Memory instruction read 0xFFFFF0
Word transfer 0x00EA
#43
Memory instruction read 0xFFFFF2
Word transfer 0x0005
#46
Memory instruction read 0xFFFFF4
Word transfer 0x0000
#49
Memory instruction read 0xFFFFF6
Word transfer 0x0000
#52
Memory instruction read 0xFFFFF8
Word transfer 0x0000
#67
Memory instruction read 0x000500
Word transfer 0xC031
#70
Memory instruction read 0x000502
Word transfer 0xD88E
#73
Memory instruction read 0x000504
Word transfer 0x0FA1
#76
Memory instruction read 0x000506
Word transfer 0x0305
#79
Memory instruction read 0x000508
Word transfer 0x1106
#82
Memory instruction read 0x00050A
Word transfer 0xA305
#85
Memory instruction read 0x00050C
Word transfer 0x0513
#88
Memory data read 0x00050F
Byte transfer on upper half of data bus 0x34
#91
Memory data read 0x000510
Byte transfer on lower half of data bus 0x12
#94
Memory instruction read 0x00050E
Word transfer 0x34F4
#99
Memory data read 0x000511
Byte transfer on upper half of data bus 0x0A
#102
Memory data read 0x000512
Byte transfer on lower half of data bus 0x00
#115
Memory data write 0x000513
Byte transfer on upper half of data bus 0x0A
#116
Memory data write 0x000513
Byte transfer on upper half of data bus 0x3E
#119
Memory data write 0x000514
Byte transfer on lower half of data bus 0x12
#120
Memory data write 0x000514
Byte transfer on lower half of data bus 0x12
#123
halt
Result: 0x123E
Hatalmas öröm volt először látni a helyes végeredményt a futás végén. Azt hiszem ezzel el is jutottam egy olyan mérföldkőhöz, aminél érdemes megállni és megpihenni.
Természetesen csak a felszínt kapargattuk meg, rengeteg minden tanulnivaló maradt még. Érdemes belelapozgatni egy kicsit a processzor adatlapjába, esetleg elgondolkodni azon, hogy hogyan valósulnak meg a különböző perifériák, mint a billentyűzet vagy egy szöveges kijelző.
Ami viszont biztos, hogy a processzor számára egyáltalán nem virtuális ez a valóság. Mindegy neki, hogy honnan kapja az elektromos jelhalmazt, amíg az kompatibilis a saját belső valóságával.