maniek86 logo

M8SBC-486: Initial hardware and chipset

[Go back to the main page of this project] [Previous part]

M8SBC-486 almost finished PCB

This content may be updated as the chipset is still in development. Last updated: 09/01/2026

Chipset

The production and delivery of the PCBs took about a week. While I was waiting, I decided to start designing the chipset. Since I've worked with VHDL before, I decided to use that language. 

It's worth mentioning that I had the Intel 486 datasheet (Oct.92 version) open throughout the development of this computer. In that datasheet, there is documentation and timing diagrams for all the types of bus transfer that the 486 can do. This is great, as almost no x86 chips today have documentation like this available. For simplicity, I decided to work only on the basic transfer cycles. I might take a moment to explain how 486 basic bus cycles work.

3-3 486 bus cycle

The 486 initiates a bus cycle by asserting the ADS pin (Address Data Strobe, active LOW). By doing so, the CPU also indicates that the address and control signals are valid. The actual read/write then occurs on the second clock cycle. 

In our design, where there are slower devices, we would want the CPU to wait, as a read/write in the second cycle might be too fast if we operate the CPU at higher frequencies. The solution is to use waitstates. So, to add to what I was saying before, the read/write only happens when the RDY (Data Ready, active LOW) is asserted. Therefore, holding RDY inactive extends the cycle. This can be seen in the above image. It is also worth mentioning that the 486 has BRDY (Burst Data Ready, active LOW), which is used for burst transfers. In short, burst transfers enable data to be read every clock cycle. As I don't use it, I tied it to VCC to keep it inactive. The FPGA's task is basically to interpret this and translate it into control signals for other onboard devices (SRAM, 8254, 8259, ISA, etc.).

The VHDL source is therefore divided into:


VHDL source

Address decoder

Its main purpose is to decode address lines into the correct CS (chip select) signal. Its code contains CS lines for: RAM, ROM, PIC, PIT, keyboard controller, port 0x61, ISA and CMOS. It also handles the assertion of the KEN (cache enable) and BS8/BS16 (8- or 16-bit device indication) signals. 

Current address map:

For the memory:

I also decided to add two jumpers to the board to enable or disable the cache. One is for the RAM and the other is for the ROM. There are also two inputs to the address decoder indicating the status of these.

For the IO:

BE0-3 decoder to A0/A1 (be_decoder.vhd)

Since the 486 is a 32-bit processor, it does not include A0 and A1, which are used for 8/16-bit devices. We can generate these address lines based on the four Byte Enable (BE0-3) pins. During a normal 32-bit transaction, they indicate which bytes of the 32-bit data bus the data is present on/accessed. They are also used for 8/16-bit transfers to indicate what is being transferred in split transactions.

The 486 datasheet provides an example of how to implement this, so it only took a few moments to implement it.

Generating A0, A1 in 486 datasheet

Clock dividers for the CPU, ISA and PIT (clock_section.vhd, clock_section_isa.vhd, clock_section_pit.vhd)

There are three files for each device that needs to be clocked: The CPU, the ISA CLK signal, and the PIT. There are two crystal oscillators on the board: One is 48 MHz and the other is 14.138 MHz. Both clocks go into the FPGA. The 14.318 MHz clock also goes to the ISA OSC pin. The 48 MHz clock is used to generate the CPU clock. By dividing it by 4.0, I get 12 MHz, which I have decided will be the CPU frequency for the initial tests. The second clock is divided by 12 for the PIT and by 2 for the ISA CLK signal. It's worth noting that the only purpose of the ISA CLK signal is to clock the CLK pin of the ISA. There is no CLK synchronization for the ISA from the main CPU bus inside the FPGA. Many datasheets state that the ISA can be used asynchronously by simply driving its dedicated read/write pins. Later I experimented with increasing the CPU clock and achieved good stability at 24 MHz (DX2 CPUs run at 48 MHz then!). It's difficult to go any further because the FPGA software indicates that the worst delays are about 25 ns, while the period time at 33 MHz is ~30.3 ns.

Simple CMOS RTC implementation & NVRAM (CMOS.vhd)

I decided to implement a minimal RTC for various existing software. It's nothing special - it acts similarly to the classic CMOS from the x86. The CMOS contains two registers: data and address ports. This implementation simulates that functionality. There are time and date counters in the file. It's clocked by a PIT frequency that is divided to give an exact 1 Hz. Later on, I also added to this file the implementation of 32-byte CMOS nonvolatile RAM, which holds the BIOS configuration. I realized that two of the five configuration lines that the ATmega128 uses to load a bitstream into the FPGA could be repurposed as general I/O later on. I used these two lines to transfer data between the FPGA and AVR, which allowed me to store the FPGA CMOS memory in the AVR EEPROM. Upon powering on, the ATmega128 restores the configuration. This was accomplished by creating a custom two wire protocol with clock and data lines to transfer data serially.

ISA driver (isa_driver.vhd)

This one was more difficult to implement. The 8-bit ISA is easy to implement; however, this time, I also decided to implement the 16-bit ISA. The 16-bit ISA requires kind of a "handshake" for 16-bit transfers, as well as a more complicated state machine that checks for that capability before the transfer begins. Additionally, the ISA is a slow bus, so many wait states were required to make it work properly. Because of the dynamic bus sizing, this driver generates BS8 and BS16 on its own.

Later note: This driver will probably be rewritten because it causes issues. I decided to implement asynchronous transfers just to simplify the design, but they are probably problematic.

RAM driver (ram_driver.vhd)

I decided to use SRAM because it is easy to drive. We simply input an address and optionally, data, and then read or write with a strobe signal on an OE or WE pin. The RAM driver has its own file due to the organization of the memory on the board. A 32-bit 486 CPU works best with 32-bit memories, of course. With eight 8-bit, 512 K SRAM chips, I split them into two 2 MB banks. Each byte has an OE and a WE line. In total, we have OE0-OE3 and WE0-WE3. To address the correct bank, there are two CS lines.

Transceiver driver (transceiver_driver.vhd)

As I explained in the previous part, in order to interface a 32-bit 486 with 8- or 16-bit devices, we need a way to place the 8-bit bus data on the correct bytes of the 32-bit bus. This part listens for the BS8, BS16, and BE0-BE3 signals and controls the chip enables of the four transceivers to move the data correctly. I included a reference image from the datasheet below.

8 / 16-bit memory access principle from the 486 datasheet

WR/RD generator for onboard devices (wr_rd_generator.vhd)

Intel-style peripherals use RD/WR strobes to read or write data. To transfer data from/to the device, we simply put the correct address lines and then do a pulse on the RD or WR pin. The device will then make the data available or write it during that pulse. This part implements that functionality. Initial tests also used it for ISA (8-bit), but I moved it into a dedicated section when I needed to implement 16-bit. This part mainly generates IO reads/writes to the onboard peripherals and a MEM read signal to the ROM. The MEM write capability ultimately became unnecessary because we have the RAM and ISA drivers for memory writes. 

And a main file connecting all of these (m8sbc_main.vhd)

The main file contains all the definitions of the pins, internal signals, incoming signals, and outgoing signals. In that file, I also multiplex various signals because some output signals can be driven from multiple sources (one at a time, depending on the conditions) I also set the amount of wait states for the drivers here. Lastly, it's the perfect place for quick temporary fixes!


Issues

Here's a list of hardware/chipset issues that are still present or have been resolved.

Present issues:

Flaky compatibility with ISA cards & ISA BALE issue:
Some ISA cards simply don't work, and I'm not entirely sure why in some cases (I haven't investigated this extensively). For some ISA cards, especially the 16-bit ones, one of the main reasons is that the BALE (Bus Address Latch Enable) pin is tied to ground. Tying it to ground makes the latch always active and passes the address because the address doesn't change during a bus transaction. However, some cards require a pulse anyway. I later read (after manufacturing) that the extra address lines of the 16-bit ISA extension are valid when this signal is high. Even now, I am still confused about the full purpose of this pin. This problem can be solved on some cards by taping the BALE pin. It is difficult to solve this issue, even by cutting traces on the PCB because there is an internal GND layer.

L1 cache is broken on ROM:
Enabling the L1 cache on the ROM results in random RAM corruption. I haven't looked into this a lot yet.

Rare crashes with some configurations:
Honestly, I sometimes don't know. These appear randomly.

Fixed Issues:

The L1 cache is broken on RAM:
When enabling the L1 cache, especially on the RAM, it either fails the initial 64 KB RAM test or corrupts the memory.
The fix: Allowing the 486 to cache the current bus cycle (the KEN signal is active during the bus transfer) enables the 486 to convert the normal bus cycle into a cache fill bus cycle. This cache fill fetches 16 bytes of data in four transfers. I found out later that the datasheet mentions that during the first cache fill transfer, BE0-BE3 should be ignored. In the initial design, the OE pins were tied to the BE pins to request only the data wanted by the CPU. Tying these to 0 (enable) during a read made the L1 work! This means that, even with a single-byte read from memory all RAM chips will be active. That doesn't really matter because the 486 ignores the data on the rest of the lines.

[Next part]

Last updated: 09/01/2026


Homebrew Computers