PC-compatibility, it’s all relative

Update 21-12-2015: I have updated some of the information after testing on an AT with old Intel 8259A chips, and added some extra information on EISA and newer systems.

I would like to pick up where I left off last time, and that is with the auto-end-of-interrupt feature of the Intel 8259A PIC used in PC-compatibles. At the time I had a working proof-of-concept on my IBM PC/XT 5160, but not much more. The plot thickened when I wanted to make my routine generic for any PC-compatible machine. Namely, as we have already seen with 8088 MPH, PC-compatibility is a very relative notion. For example, EGA/VGA cards have very limited backward compatibility with CGA. And even among original IBM CGA cards, there are some notable differences.

The story of the 8259A PIC is another case where there are some subtle and some not-so-subtle differences between machines.

Classes of PCs

I think we should start by defining what types of PCs IBM has offered. So let’s start at the beginning, and have a quick look at some of the defining hardware specifications.

IBM 5150 PC (Personal Computer)

The first PC was quite a modest machine:

  • 8088 CPU at 4.77 MHz
  • Single 8259A Programmable Interrupt Controller
  • Single 8237 DMA controller
  • 8253 Programmable Interrupt Timer
  • PC keyboard interface
  • 5 wide 8-bit ISA expansion slots
  • CGA and/or MDA video
  • IBM Cassette BASIC ROM
  • Tape interface

IBM 5160 PC/XT (eXtended Technology)

The XT is a slight variation of the original PC, where the tape interface was dropped (but the Cassette BASIC ROM was kept, since the other versions of BASIC were not standalone, but extensions to this BASIC), and the ISA expansion slots were placed closer together, and increased to a total number of 8 slots. The XT became the standard, and most clones were modeled after the XT, with the main difference being the lack of the ROM BASIC. So:

  • 8088 CPU at 4.77 MHz
  • Single 8259A Programmable Interrupt Controller
  • Single 8237 DMA controller
  • 8253 Programmable Interrupt Timer
  • PC keyboard interface
  • 8 narrow 8-bit ISA expansion slots
  • CGA and/or MDA video
  • IBM Cassette BASIC ROM

In most cases, the PC and XT can be lumped together into the same class. The missing cassette interface only makes a difference if you actually wanted to use a cassette. But floppies and later harddrives became the storage of choice for PC, so cassette was never really used. Likewise, the slightly different form-factor of the ISA slots doesn’t make much difference either. The slot and card are the same, only the backplate is slightly different. The IBM 5155 Portable PC also uses the same motherboard as the PC/XT, and works exactly the same as well.

IBM 5170 PC/AT (Advanced Technology)

The AT was quite a departure from the original PC and XT. It bumped up the platform to 16-bit, had more interrupt and DMA channels, and also introduced a new keyboard interface, with bi-directional communication. This was again the blueprint for many clones, and later 32-bit PCs (386 and higher) largely maintained the same hardware capabilities (in fact, even your current PC will still be backward-compatible):

  • 80286 CPU at 6 MHz
  • Two 8259A PICs, in cascaded master/slave arrangement
  • Two 8237 DMA controllers, cascaded for 16-bit DMA transfers
  • 8253 Programmable Interrupt Timer
  • AT keyboard interface
  • 6 narrow 16-bit ISA expansion slots (backward compatible with 8-bit XT slots) and 2 narrow 8-bit ISA expansion slots
  • MC146818 real-time clock and timer
  • CGA, EGA or MDA video
  • IBM Cassette BASIC ROM

Aside from this, the AT also led to a standardization of power supply and case/motherboard form factors.

IBM also sold the 5162 XT/286, which, unlike the name suggests, had the same enhanced hardware capabilities as the AT, but housed in a PC/XT-style case.

Honourable mention: IBM PCjr

Although it never became a widespread standard, IBM made another variation on the PC-theme, namely the PCjr. It was not a fully PC-compatible machine, although it also runs a version of DOS, includes a version of BASIC, and its hardware is mostly compatible with that of the PC (8088 at 4.77 MHz and CGA-compatible video).

The biggest differences to a PC are:

  • Enhanced video chip with 16-colour modes
  • No dedicated video RAM, but system RAM shared with the video chip
  • SN76489 audio chip
  • No 8237 DMA controller on-board
  • ‘Sidecar’ interface instead of ISA slots for expansion
  • PCjr keyboard interface

Since it does have an 8259A PIC chip, the issues discussed here apply to PCjr as well. Also, the enhanced audio and video capabilities were cloned by Tandy (but not marketed as such, since PCjr was a commercial failure).

So, where are the problems?

Now that we have established that not all PC-compatible machines are quite equal in terms of hardware, let’s see how this affects us when programming the 8259A PIC.

The most obvious difference here is between the PC/XT and the AT. The AT uses two cascaded 8259A PICs. These PICs need to be initialized in a different way. The problem is that you can’t read back any of the settings from the PIC. So you can’t just save, restore or modify the current settings. You need to do a complete reprogramming of the chip, without being able to tell how it is configured beforehand.

Now, you may think that int 15h, ah=0Ch would be a nice way to check for this. It returns some feature-bytes, where there is a bit to indicate a second 8259A. But alas, this BIOS function was not present in the first revision of the AT BIOS. So you can not assume that if the BIOS doesn’t support this function, that the machine must be PC/XT class.

So I decided to check for the existence of a second PIC where the AT would normally have it, which is at I/O ports 0xA0 and 0xA1. The one thing you can read back and modify is the mask register. So, I used a simple trick to see if there was ‘memory’ at this port:

	// Check if we have two PICs
	in al, 0xA1
	mov bl, al	// Save PIC2 mask
	not al		// Flip bits to see if they 'stick'
	out 0xA1, al
	out DELAY_PORT, al	// delay
	in al, 0xA1
	xor al, bl	// If writing worked, we expect al to be 0xFF
	inc al		// Set zero flag on 0xFF
	mov al, bl
	out 0xA1, al	// Restore PIC2 mask
	jnz noCascade
	...		// We have two PICs
noCascade:
	...		// We have one PIC

Now we can assume that there is a second PIC present. Which means we should be dealing with an AT-class machine. I wanted to make my application do a clean exit back to DOS, and restore the original state of the PICs. Now, we don’t know exactly how the PICs are initialized. All we know is that we are either dealing with a PC/XT-class machine or an AT-class machine.

So, I have studied the BIOS code for the PC, XT and AT. The PC and XT set up their PIC in the same way, so that should do for the PC/XT-class with a single 8259A. And in the other case, I took the setup code for the two PICs from the AT BIOS. This means that in both cases, the PICs should be the left in the same state as after a reset, when my program shuts down. We can only hope that all clones work the same way.

Buffered and unbuffered modes

When studying the BIOS code, I noticed another difference between the PC/XT and the AT setup code. The PC/XT code initializes the 8259A in ‘buffered’ mode in ICW4 (it actually sets it up as ‘slave’ as well, but this bit probably does not do anything, since you set whether it runs in cascaded mode or not with ICW1, and it is configured as single). In that case, it uses the SP/EN pin to signal that its data output is enabled, so that external hardware can buffer the data.

The AT initializes its 8259A’s in non-buffered mode. This also means that the master/slave mode of each chip is triggered by the SP/EN pin (so it is now an input instead of an output), rather than by setting the mode in software via ICW4. And if we study the circuit of the AT (see page 1-76 here), we see that indeed the master PIC is wired to +5v and the slave PIC is wired to GND at the SP/EN pin.

There should be no harm in enabling buffered mode on the PICs in an AT though, and in theory you can set up the first PIC as standalone, and just configure it the same as you would on a PC/XT, ignoring the second PIC. But since we know we have to reset the PICs to the AT-specific configuration anyway, we might as well do a more ‘correct’ setup to AEOI-mode while we are at it, and stick to buffered mode for PC/XT and non-buffered mode for PC/AT.

Intel Inside?

Another issue is that 8259A chips are not necessarily made by Intel. Just like with early x86 CPUs, there were various ‘second source’ manufacturers of these chips, namely AMD, NEC, UMC and Siemens. You can find any one of these brands, even in original IBM machines. And like with the Motorola/Hitachi 6845 chip encountered on IBM CGA cards, it could be that these alternative suppliers may have slightly different behaviour.

Moreover, on newer systems, even XT-class clones, you will not be dealing with actual 8259A chips at all, but the logic will be integrated in multifunction chips. My Commodore PC20-III has a Faraday FE2010 chip, and my 286 has a Headland HT18/C. Both have only one chip that takes care of all the basic motherboard logic. And I have found the Headland to be somewhat picky in how you set up AEOI. With this large variety of chips out there, it may well be that there are other chips that have picky/broken/missing AEOI support. This feature was rarely used, so such problems may go completely unnoticed for the entire lifetime of a system.

Old and new 8259A

Another ‘gotcha’ can be read in the 8259A spec sheet. It says the following:

The AEOI mode can only be used in a master 8259A and not a slave. 8259As with a copyright date of 1985 or later will operate in the AEOI mode as a master or a slave.

That is rather nasty. XTs were made from 1983 to 1987, and ATs from 1984 to 1987, so either revision could be in these systems.

What are the consequences? Well, the second PIC in the AT should be running in slave mode. If it is a pre-1985 chip, then it will not work in AEOI mode. With the help of modem7, we could actually verify this on real hardware. His AT is an early model, and has pre-1985 chips, where we could not get the slave into AEOI-mode, even though we tried a few different approaches, trying to bend the rules somewhat.

po65127651652

So we shouldn’t try to use AEOI on the second PIC, if we want to be compatible with all AT systems. Note that in cascaded mode, an EOI needs to be sent both to the master and the slave that generated the IRQ. We can still save one EOI here, when the master is running in AEOI mode, so there are at least some gains still.

Luckily the first PIC is the most interesting one, since it handles the things we are normally interested in, like the timer, the keyboard and disk interrupts. Early sound cards would generally also stick to the first PIC (generally IRQ5 or IRQ7), for compatibility with PC/XT systems.

It could also be that the ‘buffered slave’ setup in ICW4 may not work reliably on certain clone chips in stand-alone mode, so to be safe, you should set it to ‘buffered master’ instead, when you want to enable AEOI. I encountered this issue on a 286 clone of mine. It is a late model 286 (BIOS date 7/7/91), with integrated Headland chipset. I found that AEOI only worked when I set ‘buffered master’, or when I set it to non-buffered mode (where it would be hardwired to master). I know it was only the AEOI that did not work, because the system worked fine if I still sent manual EOI commands to the PIC.

Using AEOI with buffered master mode worked on all 8259A chips I’ve tried, old and new Intels, and various clones.

Can we detect whether enabling AEOI actually worked? Well, yes. Namely, if the PIC does not get an EOI, it will not issue a new interrupt. So what can we do? We can enable AEOI, and set up a timer interrupt. Inside the handler, we increment a counter.  Our application will then wait a while (e.g. by polling the counter to detect when it wraps around), so that multiple timer interrupts will have fired. Then we check whether the counter has a value greater than 1. If so, then an EOI has been issued after each interrupt, so AEOI worked.

You can do this for both PICs, because the master PIC has the standard 8253 PIT connected to it, and the slave PIC has the MC146818 CMOS timer connected to it. Both timers can generate interrupts at a fixed interval, so for both cases you can set up an interrupt handler with a counter.

And what about the PS/2?

After the AT, IBM decided to set up a new standard, the PS/2, which was not entirely backward-compatible with the PC platform. The PS/2 is very PC-like though, in that they still use x86 processors, most of the hardware is very similar (in fact, the VGA standard was introduced on the PS/2 line and adopted by PC clones), it has an AT-compatible BIOS (as well as a new Advanced BIOS) and it runs DOS as well.

And indeed, in the PS/2 we also find the trusty two 8259A’s that we know from the AT. However, because it uses the new MCA-bus instead of the ISA-bus, there is a difference. On the ISA-bus, interrupts are edge-triggered. On the MCA-bus however, they are level-triggered. This means that PS/2 systems need yet another 8259A setup and restore routine. So you will need to detect whether you are running on a PS/2 system or not. You could use int 15h, ah=oCh for this (all PS/2 systems support it), and perhaps check for MCA support in the feature-bytes returned in the table.

What happened after that?

MCA was the first ‘new’ bus architecture for the PC platform, where the engineers figured that level-triggered interrupts were nicer than edge-triggered ones. For later buses, such as EISA and PCI, engineers came to the same conclusion. When they were working on EISA, they had to solve a problem: how do we maintain backward compatibility with ISA?

They solved this by modifying the 8259A design somewhat. Instead of having a global setting for edge-triggered or level-triggered interrupts, this could be set on a per-interrupt basis. An Edge/Level Control Register (ELCR) is added for each PIC. The master PIC ELCR is at 0x4D0, and the slave PIC ELCR is at 0x4D1. Like with the interrupt mask register, each bit corresponds to one of the interrupt lines. When set to 0, that interrupt line is edge-triggered, when set to 1, the line is level-triggered. The global setting in the legacy register for the 8259A is ignored (these systems never use real 8259A chips, but always have the logic integrated into the chipset).

So basically, we do not have to care about edge/level triggering for newer systems. We don’t reprogram those registers when we enable the AEOI flag in the PIC, so they should retain the proper configuration. If you are interested in EISA, you can read more about it in this book.

So what do we do?

We basically have three types of configurations:

  1. PC/XT: Single 8259A, edge-triggered
  2. AT: Cascaded 8259A, edge-triggered
  3. PS/2: Cascaded 8259A, level-triggered (although there are ISA-based PS/2 systems, I believe all MCA-based PS/2 systems are AT-class or better)

We know how to detect which configuration we have (check if the mask of the second PIC can be written and read back, or use int 15h, ah=C0h to get system information, if that function is supported). At the very least, we know that standalone and master 8259A’s can all run in AEOI mode. So we can make three different routines to initialize the first 8259a to AEOI mode. And we can also make three different routines to initialize them back to their default mode on application exit.

To make this easier to manage, I made a simple helper function to set the different ICW values. There are 4 in total, but ICW3 and ICW4 can be optional in some cases, which complicates things somewhat. So I created a function to deal with that. Note that I write to an unused port to delay IO somewhat. For PC/XT machines this is not required. For AT’s, it is. IBM uses jmp $+2 delays in its code, which works well enough on a real AT, but on faster/newer systems (386/486), it is better to delay with a write to a port. I use port 0xEE, because that port is not used by anything:

void InitPIC(uint16_t address, uint8_t ICW1, uint8_t ICW2, uint8_t ICW3, uint8_t ICW4)
{
	_asm {
		cli

		mov dx, [address]
		inc dx
		in al, dx	// Save old mask
		mov bl, al
		dec dx

		mov al, [ICW1]
		out dx, al
		out DELAY_PORT, al	// delay
		inc dx
		mov al, [ICW2]
		out	dx, al
		out DELAY_PORT, al	// delay

		// Do we need to set ICW3?
		test [ICW1], ICW1_SINGLE
		jnz skipICW3

		mov al, [ICW3]
		out dx, al
		out DELAY_PORT, al	// delay
skipICW3:
		// Do we need to set ICW4?
		test [ICW1], ICW1_ICW4
		jz skipICW4

		mov al, [ICW4]
		out dx, al
		out DELAY_PORT, al	// delay
skipICW4:
		mov al, bl		// Restore old mask
		out dx, al

		sti
	}
}

With this helper-function, it becomes reasonably easy to initialize the PICs to auto-EOI mode and set them back to regular operation:

void SetAutoEOI(MachineType machineType)
{
	switch (machineType)
	{
		case MACHINE_PCXT:
			InitPIC(PIC1,
				ICW1_INIT|ICW1_SINGLE|ICW1_ICW4,
				0x08,
				0x00,
				ICW4_8086|ICW4_BUF_MASTER|ICW4_AEOI );
			break;
		case MACHINE_PCAT:
			InitPIC(PIC1,
				ICW1_INIT|ICW1_ICW4,
				0x08,
				0x04,
				ICW4_8086|ICW4_AEOI );
			break;
		case MACHINE_PS2:
			InitPIC(PIC1,
				ICW1_INIT|ICW1_LEVEL|ICW1_ICW4,
				0x08,
				0x04,
				ICW4_8086|ICW4_AEOI );
			break;
	}
}
void RestorePICState(MachineType machineType)
{
	switch (machineType)
	{
		case MACHINE_PCXT:
			InitPIC(PIC1,
				ICW1_INIT|ICW1_SINGLE|ICW1_ICW4,
				0x08,
				0x00,
				ICW4_8086|ICW4_BUF_SLAVE );
			break;
		case MACHINE_PCAT:
			InitPIC(PIC1,
				ICW1_INIT|ICW1_ICW4,
				0x08,
				0x04,
				ICW4_8086 );
			InitPIC(PIC2,
				ICW1_INIT|ICW1_ICW4,
				0x70,
				0x02,
				ICW4_8086 );
			break;
		case MACHINE_PS2:
			InitPIC(PIC1,
				ICW1_INIT|ICW1_LEVEL|ICW1_ICW4,
				0x08,
				0x04,
				ICW4_8086 );
			InitPIC(PIC2,
				ICW1_INIT|ICW1_LEVEL|ICW1_ICW4,
				0x70,
				0x02,
				ICW4_8086 );
			break;
	}
}

If you set up a detection routine with a timer and a counter in the handler, as I mentioned before, you could try a few variations of setting up AEOI, and check if it worked, to make it more robust for ‘wonky’ 8259A clones, and perhaps to detect problems and bail out with a warning to the user, rather than crashing their system because you assume it just works. Of course there is still the risk that the system doesn’t use the ‘default’ setup you’ve assumed (I have taken the above values from the original IBM BIOSes for PC and AT). Which means that it’s already too late when you detected that AEOI doesn’t work. Because you changed the PIC configuration, and you don’t know the initial state. So it may be best to warn the user beforehand that he may have to reset his system if things go wrong.

If you’re interested, you can download the source and binary of my simple test-program here. If you find any strange quirks on your 8259A chips, please let me know in the comments what chips you tested, and what strange things you saw.

And what did all this earn us?

Well, we can just save on these two instructions in our interrupt handler now:

// Send end-of-interrupt command to PIC
mov al, 0x20
out 0x20, al

Somewhat of a Pyrrhic victory you say? Well, indeed, it’s a lot of trouble for very few gains, but any gains are welcome, and once you get this working, you can just stop worrying about it and reap the benefits, modest as they may be. Especially with high-frequency timer handling, such as with playback of digital audio, it may just give you that extra ‘push over the cliff’, to speak with Nigel Tufnel. These interrupts go to 11!

Bonus material

When I was playing around with the 8259A stuff, Trixter pointed me to an article in this old magazine, which covers programming the 8259A in great detail (see page 173 and further). It does not really go into AEOI much, but it covers pretty much everything else, such as the priority schemes, and is a great read. Priority is the trade-off you’re making when enabling AEOI: each interrupt is immediately acknowledged, and can fire again as soon as the CPU is ready to receive interrupts. If you acknowledge the interrupts manually, you get control over which interrupts may fire when. For games and demos this is not such a big issue, because we generally don’t need to service that many interrupts anyway, and we can mask out anything we’re not interested in at any given time.

Amazing how in-depth and technical articles were in regular PC magazines back in the day, compared to how dumbed-down everything is today in mainstream media.

Advertisement
This entry was posted in Oldskool/retro programming, Software development and tagged , , , , , , , , , , , , , , , , . Bookmark the permalink.

11 Responses to PC-compatibility, it’s all relative

  1. Klimax says:

    Hello,
    last link to magazine from 1983 about PIC programming doesn’t work (they moved it). New URL: http://archive.pcjs.org/pubs/pc/magazines/pctj/PCTJ-1983-11/PCTJ-1983-11.pdf

  2. Pingback: Any real-keeping lately? | Scali's OpenBlog™

  3. Pingback: Putting the things together | Scali's OpenBlog™

  4. Pingback: DMA activation | Scali's OpenBlog™

  5. Pingback: More PC(jr) incompatibilities! | Scali's OpenBlog™

  6. Pingback: Just keeping it real at Revision 2019 | Scali's OpenBlog™

  7. Pingback: Am I a software architect? | Scali's OpenBlog™

  8. Pingback: When is a PC not a PC? The PC-98 | Scali's OpenBlog™

  9. Pingback: A great upgrade for the PCjr: the jr-IDE | Scali's OpenBlog™

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s