Homebrew 68000-computer met een PCI-bus
Specificaties
- CPU: Motorola 68000 op 10 MHz
- RAM: 1 MB SRAM
- ROM: 4 KB
- I/O: MC68681P als UART, timerbron en I/O
- PCI-slots: Twee 32-bit 5V PCI-slots, interrupt-ondersteuning, geen bus mastering (geen DMA)
- PCI-bridge: Xilinx Spartan II FPGA (XC2S100)
- Glue logic: XC95144XL CPLD
---
De PCI-bus en het voorafgaande experiment
Tijdens de late ontwikkeling van de M8SBC-486 kreeg ik suggesties om VLB (Vesa Local Bus) en PCI toe te voegen. Op dat moment was ik meer gefocust op de functionaliteit van het basisbord dan op een nieuwe revisie met nieuwe interfaces, dus liet ik het onderwerp vallen. Hoewel VLB eenvoudig is — het is in feite gewoon een 486-bus op een connector — is PCI dat niet. PCI is een bus die een bridge vereist om CPU-cycli te vertalen naar eigen cycli. Daarnaast staat het geheugen-DMA via bus mastering toe. Dit is zowel op elektrisch als op softwareniveau complex.
Rond het midden van 2026 raakte ik echter verveeld met de M8SBC-486 en wilde ik iets complexers doen. De voortgang van de M8SBC-486 vertraagde vanwege hardwarebeperkingen. Mijn interesse in PCI werd gewekt door de vele mogelijkheden; er zijn honderden kaarten die een systeem op unieke manieren kunnen uitbreiden.
Vanwege de complexiteit van PCI kon ik dit niet in twee dagen leren door wat artikelen online te lezen. Ik heb het boek "PCI System Architecture" van Mindshare Inc. gelezen voor een heldere uitleg. Omdat ik het liefst leer door praktijkwerk, wilde ik iets fysieks bouwen dat koppelt met de bus. Ik herinnerde me dat ik oude, propriëtaire PCI-kaarten had met Xilinx Spartan 2 FPGA's (XC2S100) erop, die ik als schroot van een docent had gekregen.
Het belangrijkste kenmerk van deze kaarten is dat de FPGA direct is verbonden met de PCI-bus. Omdat er geen schema's of pin-outs beschikbaar waren, heb ik ze gereverse-engineered. Ik heb componenten verwijderd met een heteluchtpistool en kwalitatieve scans van de PCB gemaakt. Aangezien de PCB uit twee lagen bestond, was het mogelijk de sporen handmatig te volgen en een schema in KiCad te reconstrueren. Dit proces nam ongeveer 3 tot 4 dagen in beslag.
Met deze gereconstrueerde schema's had ik in feite een basis PCI-ontwikkelingsbord. De Xilinx Spartan 2 FPGA's zijn 5V-tolerant, wat betekent dat ik de kaart in 5V PCI-omgevingen kan gebruiken.
Nadat ik genoeg theorie had gelezen om een basis "target only" PCI-kaart te maken (een kaart die geen DMA gebruikt), begon ik VHDL-code te schrijven in Xilinx ISE. Met behulp van timing-diagrammen uit het boek implementeerde ik het PCI-protocol (bus-signalering) en de PCI Configuration Space. De moeilijkste stap was de signalering: het correct detecteren van de configuratie-leesopdracht en het reageren met de juiste data.
Uiteindelijk bereikte ik het punt waarop de kaart het volgende deed:
- Implementatie van configuratie-ruimte reads en writes.
- Implementatie van basis configuratie-registers (zodat de kaart gedetecteerd kon worden).
- Implementatie van een intern 64-byte RAM en een aanvraag voor een BAR (Base Address Register), waardoor de kaart kan worden toegewezen aan de geheugen- of I/O-ruimte van het systeem.
- Implementatie van reads en writes (naar het interne 64-byte RAM), inclusief burst-transacties.
- Vastleggen van de eerste geschreven byte en het doorsturen hiervan naar output-pinnen, waardoor deze byte als output-register fungeerde.
Daarnaast schreef ik een eenvoudige Linux-testdriver om de kaart te binden, het interne geheugen te testen en een LED te laten knipperen. Dit was mijn eerste praktische ervaring met de PCI-bus.
---
Hoe de PCI-bus werkt
Inleiding
Oudere computerbussen, zoals ISA, waren direct verbonden met de CPU-bus. Dit leidde tot problemen zoals apparaatconflicten, de noodzaak voor handmatige configuratie van resources, een hoog aantal pinnen en beperkte snelheid (ISA kon officieel maximaal 8,33 MHz draaien). Intel ontwikkelde de PCI-bus om deze problemen op te lossen.
De belangrijkste kenmerken van PCI zijn:
- Processoronafhankelijkheid: Componenten voor de PCI-bus zijn PCI-specifiek, niet CPU-specifiek.
- Compact en efficiënt ontwerp: De basisbus is 32-bit. Data- en adrespinnen zijn gemultiplext (AD-signalen), waardoor de connector klein blijft (124 pinnen).
- Snelheid: Een standaard 32-bit PCI-bus draait op 33 MHz en kan theoretisch 132 MB/s transfereren. Dit kan worden verhoogd naar 264 MB/s (64-bit) of 528 MB/s (66 MHz).
- Burst reads en writes: Om de snelheid te maximaliseren kan de "initiator" (het apparaat dat de bus benadert) meer data overdragen zonder telkens een nieuw adres te presenteren. De "target" (het apparaast dat gelezen of beschreven wordt) moet intern het adres bijhouden.
Signalering
De PCI-bus maakt gebruik van reflected-wave switching. Deze techniek gebruikt signaalreflecties in het voordeel, waardoor afsluitweerstanden (termination resistors) overbodig zijn en de kosten van het moederbord dalen.
De bus draait standaard op 33 MHz, maar kan worden vertraagd of zelfs volledig worden gestopt voor energiebesparing. Alles is gesynchroniseerd met de stijgende flank van de klok. Er zijn 5V en 3.3V signaleringsomgevingen, waarbij "universele" PCI-kaarten in beide types slots kunnen werken.
PCI is een lokale bus die onafhankelijk van de CPU kan werken via bus mastering (DMA). Een apparaat kan toegang tot de bus aanvragen bij een arbiter; zodra deze toegang krijgt, wordt het apparaat de "initiator" en kan het het systeemgeheugen of andere PCI-apparaten benaderen.
Om het aantal pinnen te beperken, worden de adres- en datapinnen (AD) en de command- en byte-enable pinnen (C/BE#) gemultiplext. Elke transactie vereist een "handshake".
Voorbeeld van een PCI burst write transactie
De gebruikte signalen zijn:
- CLK: De klok (synchronisatie op stijgende flank).
- FRAME#: (Active low) Geeft aan dat een transactie begint.
- AD: Gemultiplexte adres- en databus.
- C/BE#: Gemultiplexte command- en byte-enable bus.
- IRDY#: (Active low) Initiator Ready; geeft aan dat de initiator klaar is om data te verzenden.
- TRDY#: (Active low) Target Ready; geeft aan dat de target klaar is om data te accepteren.
- DEVSEL#: (Active low) Device Select; geeft aan dat het apparaat het adres heeft gedecodeerd en dat dit binnen zijn bereik valt.
Verloop van de transactie:
- De initiator plaatst het adres op de AD-bus en de opdracht op de C/BE#-bus, terwijl FRAME# laag wordt gemaakt.
- Alle PCI-apparaten controleren het adres. Het juiste apparaat trekt DEVSEL# laag. De initiator trekt IRDY# laag.
- Wanneer zowel IRDY# als TRDY# laag zijn, vindt de dataoverdracht plaats. Omdat FRAME# nog laag is, weet de target dat er meer data volgt.
- De tweede data-item wordt overgedragen. De initiator maakt FRAME# nu hoog, wat betekent dat het volgende item het laatste zal zijn.
- Het derde en laatste data-item wordt overgedragen. Daarna worden alle controlesignalen weer hoog gemaakt en is de bus vrij.
Als geen enkel apparaat DEVSEL# laag maakt, ontstaat er een "Master Abort". Als een target geen burst-cycli ondersteunt, kan het de transactie afbreken via het STOP#-signaal. Bij lees-transacties is er altijd één verplichte wachtstatus (wait state) na de adresfase om te voorkomen dat twee apparaten tegelijk de AD-bus aansturen.
PCI Software-architectuur
Vanuit softwareperspectief is PCI verdeeld in drie ruimtes:
- Configuration space: Elke kaart heeft een verborgen configuratieruimte van 256 bytes. Hierin staan de Base Address Registers (BARs), die het systeem vertellen hoeveel geheugen of I/O-ruimte de kaart nodig heeft.
- Memory space: Gebruikt voor grote datablokken (bijv. videogeheugen).
- I/O space: Een legacy-concept uit de x86-wereld, gebruikt voor controleregisters.
Endianness
De PCI-bus is little-endian. De Motorola 68000 is echter big-endian. Dit betekent dat de volgorde van bytes in het geheugen verschilt.
- Little-endian (PCI):
0xAABBCCDDwordt opgeslagen alsDD CC BB AA. - Big-endian (68000):
0xAABBCCDDwordt opgeslagen alsAA BB CC DD.
Om dit op te lossen zijn er twee technieken:
- Value preserving: Bytes worden in hardware omgewisseld op de databus. Dit is handig voor 32-bit hardware-registers.
- Location preserving: De bus wordt zo gekruist dat byte 0 naar adres 0 gaat, etc. Dit is ideaal voor sequentiële buffers, zoals C-strings of VGA-pixels.
Interrupts en functies
Elke PCI-slot heeft vier interrupt-pinnen: INTA#, INTB#, INTC# en INTD#. Dit is omdat één PCI-apparaat zich kan voordoen als maximaal acht verschillende apparaten (PCI-functies), waarbij elke functie zijn eigen configuratieruimte heeft. Deze pinnen zijn active-low en worden vaak gedeeld tussen meerdere apparaten.
---
Het idee
Het idee voor deze computer ontstond per toeval. Ik kreeg een partij elektronica-schroot van mijn vriend Shadowdestroyer777, waaronder een industrieel bord met een Motorola 68000 CPU en twee kapotte moederborden uit het Socket 7-tijdperk. Na het desolderen van de PCI-slots en de CPU dacht ik: "Wat als ik een 68000 homebrew bouw met PCI op een prototype-bord?"
Ik besloot de volgende onderdelen te gebruiken:
- MC68000P10: Motorola 68000 CPU (max 10 MHz).
- XC95144XL CPLD: Voor de glue logic.
- 2x HM628512 SRAM: 512 KB per stuk (totaal 1 MB).
- 2x Xicor X2816C EEPROMs: 2 KB per stuk voor de software.
- MC68681P DUART: Voor UART en timing.
- 74245 en 74244 buffers: Voor de IDE-schijfinterface.
- 7405 inverter: Voor de reset.
- Twee PCI-slots (5V) met een Xilinx Spartan 2 FPGA (XC2S100) als host bridge.
- ATMega128: Uitsluitend voor het laden van de FPGA-bitstream.
Ik gebruikte een 10 MHz kristaloscillator als hoofklok voor zowel de CPU als de PCI-bus om het FPGA-ontwerp te vereenvoudigen. (Uiteindelijk bleek de oscillator defect en verving ik deze door een 20 MHz kristal met een 74163-teller om de frequentie door twee te delen).
Hardware-uitdagingen
- SMD-componenten: Voor onderdelen zoals de CPLD en FPGA (QFP-pakketten) heb ik zelf adapters gemaakt. In één geval heb ik zelfs een chip met een haakse slijper uit een oud ISA-prototypebord verwijderd om hem te kunnen hergebruiken.
- PCI-slots: Standaard PCI-slots hebben een verspringing van 1,27 mm tussen de pinnen. Ik heb de pinnen handmatig verbogen naar 2,54 mm zodat ze in het prototype-bord pasten.
- Ruimtegebrek: Alles moest op een bord van 18x12 cm passen, wat resulteerde in een zeer compact ontwerp waarbij sommige componenten op de achterzijde zijn geplaatst.
- Bedrading: Ik heb geëmailleerd koperdraad gebruikt voor alle verbindingen.
- Voeding: Het systeem draait op 5V, maar de CPLD heeft 3.3V nodig en de FPGA 2.5V. Dit heb ik opgelost met LM1117 spanningsregelaars.
---
Solderen en eerste tests
Ik begon met de voeding en JTAG-connectoren om de CPLD en FPGA te testen; beide werden direct herkend. Vervolgens soldeerde ik de CPU, RAM en ROM.
Geheugenkaart (Memory Map)
0x000000 - 0x0FFFFF: RAM (1 MB)0x100000 - 0x100FFF: ROM (4 KB)0x200000 - 0x200FFF: DUART0x300000 - 0x300FFF: IDE0x800000 - 0xFFFFFF: PCI (8 MB)
Omdat de 68000 de startup-vector aan het begin van het geheugen zoekt, implementeerde ik een "ROM overlay" truc in de CPLD. Na een reset is het RAM onbereikbaar en neemt de ROM deze plek in. Zodra er toegang is tot het werkelijke adres van de ROM, schakelt de CPLD over naar het RAM.
VHDL snippet van de ROM overlay:
PROCESS(CLK, RESET_n, ADDR)
VARIABLE ADDR_msb : STD_LOGIC_VECTOR(23 DOWNTO 20);
BEGIN
ADDR_msb := ADDR(23 DOWNTO 20);
IF RESET_n = '0' THEN
ROM_BYPASS <= '1';
ELSE
IF RISING_EDGE(CLK) THEN
IF ADDR_msb /= X"0" AND AS_n = '0' THEN
ROM_BYPASS <= '0';
END IF;
END IF;
END IF;
END PROCESS;
Na het oplossen van een foutieve bedrading van de R/W-lijn van de DUART, zag ik de eerste karakters op het terminalscherm en liet ik een LED knipperen via een assembly-programma.
---
De C toolchain
Voor de PCI-implementatie is assembly te complex, dus wilde ik een C-toolchain gebruiken. De Motorola 68000 wordt officieel ondersteund door GCC (gcc-m68k-linux-gnu).
Omdat de standaard Linux-bibliotheken uitgaan van een MMU en nieuwere processoren (zoals de 68020), gebruikt libgcc.a instructies die ongeldig zijn voor de originele 68000. Ik heb daarom de standaardbibliotheken uitgeschakeld en een eigen versie van de benodigde routines gemaakt. Met behulp van AI heb ik een Makefile, linker script en bootstrap assembly gegenereerd om tijd te besparen.
---
Interrupts
In tegenstelling tot mijn vorige projecten, wilde ik dit keer interrupts implementeren. De 68000 heeft drie interrupt-pinnen waarmee hij 6 maskeerbare en 1 non-maskeerbare interrupt kan herkennen.
In de CPLD schreef ik logica om externe interrupt-lijnen te decoderen naar de juiste 3-bit combinaties op de IPL-pinnen. De CPLD asserted vervolgens het VPA-signaal (Valid Peripheral Address), waardoor de CPU autovectoring gebruikt.
VHDL snippet van de interrupt controller:
PROCESS(UART_IRQ_n, IDE_INTRQ, PCI_INT_S1_n, PCI_INT_S2_n, PCI_INT_EN)
BEGIN
IF UART_IRQ_n = '0' THEN
IPL <= "011"; -- Level 4
ELSIF IDE_INTRQ = '1' THEN
IPL <= "100"; -- Level 3
ELSIF (PCI_INT_S1_n = '0' AND PCI_INT_EN = '1') THEN
IPL <= "101"; -- Level 2
ELSIF (PCI_INT_S2_n = '0' AND PCI_INT_EN = '1') THEN
IPL <= "110"; -- Level 1
ELSE
IPL <= "111"; -- Level 0: none
END IF;
END PROCESS;
Door de IF/ELSIF structuur fungeert dit als een hardware-prioriteitsencoder: de DUART heeft de hoogste prioriteit, gevolgd door IDE en tot slot de PCI-slots.
---
Bootloader
Om het constant flashen van ROM's te vermijden, schreef ik een seriële bootloader.
- Programma's worden in chunks van 96 bytes verzonden.
- De bootloader berekent een checksum om de overdracht te verifiëren.
- De data wordt geschreven naar het RAM op adres
0x000400. - De snelheid is ingesteld op 38400 baud; 20 KB wordt in ongeveer 6 tot 8 seconden geüpload.
---
Implementatie van de PCI-bus
FPGA-CPU Interface
De Motorola 68000-bus is asynchroon. Ik wilde mijn FPGA-ontwerp synchroniseren op de stijgende flank van de klok. De bus-staten (S0 t/m S7) zorgen ervoor dat de FPGA een adres pas bij S4 kan detecteren. Voor een zero-waitstate read of write moet de FPGA het DTACK-signaal (data acknowledge) zeer snel teruggeven.
PCI-bus signalen en hardware
Om FPGA-pinnen te besparen, heb ik de IDSEL-pinnen van de slots via 75-ohm weerstanden verbonden met de AD-bus. Dit is een techniek die ook in echte PCI-moederborden wordt gebruikt.
Ik heb ook een ATMega128 toegevoegd om de configuratie-bitstream in de FPGA te laden via SPI. Dit gebeurt in enkele honderden milliseconden, wat veel sneller is dan bit-banging via de DUART.
Eerste PCI-tests
Ik heb de volgende geheugenkaart ontworpen voor de bridge:
- De eerste 64 KB van de PCI-ruimte in de CPU-geheugenmap wordt vertaald naar PCI I/O-cycli.
- Adressen boven de 64 KB worden 1-op-1 doorgegeven als PCI-geheugentoegang.
- Configuratie-toegang gebeurt via twee registers:
CONFIGADDRESS(0xFFFF00) enCONFIGDATA(0xFFFF04).
Voor de big-endian/little-endian conversie heb ik de value preserving methode gekozen; de databussen zijn intern in de FPGA gekruist.
Bij het testen van diverse kaarten merkte ik dat sommige kaarten niet werden herkend of zelfs de voeding uitschakelden door overstroom. Dit bleek te komen door een ontbrekende pull-up weerstand op de GNT#-pin. Na correctie werkten de kaarten naar behoren.
Geheugen- en I/O-toegang
Ik implementeerde memory- en I/O-toegang (maximaal 16 bits). Voor I/O-adressering moest ik AD0 en AD1 decoderen met behulp van de A1-lijn van de 68000 in combinatie met LDS# en UDS#.
Voor deze ruimtes gebruikte ik address preserving translation. Dit betekent dat de software handmatig bytes moet omwisselen voor 16- of 32-bit registers, maar dat 8-bit sequentiële data (zoals strings) correct in het geheugen landen.
Als eerste test gebruikte ik een Moschip NM9835 PCI-seriële kaart. Omdat deze kaart $\pm 12\text{V}$ nodig heeft voor de RS232-driver, heb ik een kleine step-up module toegevoegd aan het bord.
PCI-Interrupts
Ik heb de PCI-interrupts fysiek verbonden met de systeem-interrupts van de CPU. De PCI INTx#-pinnen zijn open drain en worden gedeeld. Ik heb alle vier de lijnen (INTA-D) van één slot samenbundeld naar één systeem-interrupt. Via het CONFIG_MISC register kan de CPU deze interrupts in software maskeren.
---
Geavanceerde ondersteuning: Grafische kaarten en Netwerken
Grafische kaarten (VGA)
Het initialiseren van PCI-videokaarten is lastig omdat ze normaal een x86 Video BIOS (VBIOS) gebruiken. Omdat ik geen x86-emulator heb, initialiseerde ik een Cirrus Logic GD5440 volledig handmatig.
Ik ontdekte dat de "ontgrendelingssequentie" om de kaart in legacy VGA-modus te zetten slechts uit een paar I/O-writes bestaat. Omdat legacy VGA geheugenadressen onder de 1 MB vereist (0xA0000-0xBFFFF), voegde ik een "legacy memory translation" bit toe aan mijn CONFIG_MISC register. Dit vertaalt CPU-toegang in het bereik 0x810000-0x8FFFFF naar 0x010000-0x0FFFFF op de PCI-bus.
Ik kreeg de kaart werkend in:
- Tekstmodus (mode 03h): 80x25, 16 kleuren. "Hello World!" werd succesvol geprint.
- Grafische modus (mode 13h): 320x200, 256 kleuren. De framebuffer werd in ongeveer 300 ms gevuld ($\approx 200\text{ KB/s}$).
Ook een S3 Trio64V2/DX en een obscure IGS IGA 1680_A kaart kregen beeld, hoewel bij die laatste de grafische modus niet volledig werkte.
Netwerken
Als laatste test gebruikte ik een RTL8029 netwerkkaart. Deze is speciaal omdat hij geen DMA vereist voor basisoperaties (hij is NE2000 compatibel).
Ik gebruikte een AI-agent om de driver en een eenvoudige netwerkstack te schrijven. Dit resulteerde in een werkend systeem met:
- Een RTL8029-driver.
- Een eenvoudige TCP/IP-stack.
- Een Telnet-besturingsserver.
- Een eenvoudige HTTP-server.
---
Samenvatting
Het hoofddoel van dit project was om de low-level mechanica van de PCI-bus te leren vanuit het perspectief van de bridge. Ondanks het ontbreken van DMA-ondersteuning werkt het PCI-subsystem perfect. Dankzij de standaardisatie van PCI kunnen er nu talloze diverse kaarten worden aangesloten op een zelfbouwcomputer.
Alle projectbestanden zijn open-source beschikbaar. Dit is mogelijk een eerste homebrew-computer met een volledig functionele PCI-bus die volledig op een prototype-bord is gebouwd.
---
Veelgestelde vragen
Welke kaarten vereisen DMA (Bus Mastering) en werken dus niet op deze computer? Omdat ik geen bus mastering heb geïmplementeerd, moet de CPU elke byte handmatig overzetten. Kaarten die strikt DMA vereisen, werken niet. Denk hierbij aan high-end 3D-kaarten, de meeste PCI-geluidskaarten (voor audiobuffers), USB-controllers, TV-capture kaarten en veel moderne netwerkkaarten.
Welke andere kaarten zouden wel kunnen werken? Alles wat kan werken via standaard memory mapped I/O of legacy port I/O (PIO) zonder de bus over te nemen. PCI IDE-controllerkaarten zijn hier een goed voorbeeld van, omdat ze vaak kunnen terugvallen op PIO-modus.
Hoe zit het met PCI-bridges? Deze zijn momenteel niet ondersteund om het ontwerp simpel te houden. Ze verschijnen wel in de PCI-scan. Om apparaten achter een bridge te bereiken, zou de FPGA geüpdatet moeten worden om Type 1 configuration cycles te genereren en de software zou de secundaire bussen moeten kunnen enumereren.
Groetjes,