Subscribe via RSS
23Sep/190

Unleashing An Old 8-bit ISA IO Card

This monster had been sitting in my box-o'-stuff for a while... actually, it seems next month is its three year anniversary in the box... jeesh. This was part of a dismantled 386 PC, used at the Siding Spring Observatory where I assume they needed a lot of IO to control telescopes? Anyway, I've been cleaning/clearing out a lot of junk lately and decided to finally check this thing out. As per that post above, I'd guessed it might be similar to the ACCESS - IOD-64, but I wasn't too sure.

DSC01830

DSC01831 DSC01832 DSC01833

DSC01834 DSC01835 DSC01836

As you can see it's a full length card with four 50-pin IDC headers along the top. There are very few markings on the card, making it near-impossible to work out who the manufacturer was. The bottom left tag area indicates D6004039 and the top-right IC has a serial number indicating 5215001. The card is 8-bit.

Base IO Header Status

Before getting started with anything like this, record everything you have in its initial state. Take photos of all the switches, all the jumpers, all the traces (handy to point fingers at who scratched what), all the pins, all the headers... one always ends up changing too-many-variables and needs a reference point to return to! Even if that point is incorrect (say 0x220 is a furphy?), it's a base to begin testing from again.

In this situation, I also wanted to know what the four ports were outputting, voltage-wise. Turns out the basic idea is as follows:

01 03 05 07 09 11 13 15 17 19 21 23 25
~1.4v ~1.4v ~1.4v ~1.4v ~1.4v ~1.4v ~1.4v ~1.4v ~1.4v ~1.4v ~1.4v ~1.4v ~1.4v
+12v +12v 0v 0v 0v 0v 0v 0v 0v 0v 0v 0v 0v
02 04 06 08 10 12 14 16 18 20 22 24 26
27 29 31 33 35 37 39 41 43 45 47 49
~1.4v ~1.4v ~1.4v ~1.4v ~1.4v ~1.4v ~1.4v ~1.4v ~1.4v ~1.4v ~1.4v +5v
0v 0v 0v 0v 0v 0v 0v 0v 0v 0v 0v 0v
28 30 32 34 36 38 40 42 44 46 48 50

So, the two tables above should be butted together at the 20s... they're just too long to display in one hit. The table, as a whole, describes the first IO 50-pin IDC plug on the board. This is the one closest to the end plate. Note that the port is keyed, that a SCSI cable fits perfectly and that pin 1 lines up with the red wire.

DSC01837

From the image above, pin 1 is in the bottom-right and pin 50 top-left. We can determine that the even pins (top row) 6 to 50 are all ground/common since you can see them bridged via a horizontal trace. Pins 2 and 4 are providing 12v and pin 49 gives you 5v. I may have accidentally grounded all of these supply lines whilst initially testing, but it doesn't seem to have hurt the card! It did nearly melt my test wire though.

One quick thing we could determine here was the "V/G" jumper aligned with each header. My guess that it was either 'voltage' or 'ground' and would probably control the 'rail' of second pins from 6 to 48. A quick swap of the jumper on the first header proved that assumption totally wrong. Instead, when set to V, as they all were at the start, pins 2 and 4 were 12v. When on G, pins 2 and 4 then became GND, just like the entire rest of the row. This jumper does not affect the 5v on pin 49.

I never did work out what the EDP jumper setting did!

Programming Choices

I was going to install Visual Basic 6.0 (mount it with Daemon Tools) and build a pretty windows form, but I quickly realised that there's no direct hardware access to IO ports? I might dig into that again. Instead I downloaded QBasic 4.5 (it's also on the Win98 CD!) and slapped it in a folder on C:\. Note that if you want to play with QBasic on Windows 7++ then use dosbox.

QBasic supports the INP() Function which simply (and unrestricted-ly) reads a byte from an IO address. I had always thought this was only for serial/parallel ports, but it then occurred to me that any port in the system is really just a direct mapping (hopefully with some electrical isolation) of the data lines into the ISA bus. Therefore, if you give the IO address of anything else on the bus, you'll read data from those locations! Here's a list of standard ports for x86 which can, in this scenario, give you an idea of areas to avoid.

Hardware Configuration

Trying to work this out was a blast.... no, really... it took quite a lot of trial and error. If you look at the images above, you'll see the IO Address setting is set to 0-1-0-0-0-0-0-0, and if you look below, you'll even note a sharpie'd address written on the side of the first IDC port! 0x220 base IO address!

DSC01846

Crap, that'll conflict directly with any SoundBlaster... if that's what you happened to have installed. Using my new-found knowledge above, I wrote a really simple QBASIC script to get the data from a range of addresses near 0x220.

FOR PORT# = 220 TO 230
  PRINT INP(PORT#)
NEXT PORT#

The output was upsetting... just multiple outputs of the number 255. I wondered what the chances were that the dipswitches had been tampered with? Who knows if that inscription of 0x220 was, at all, correct? At the same time, if my pins were floating, then would the card report a 1 or a 0? Knowing where the supply voltage pins where, I danced around them and grounded a few of (what I expected were) the IO lines.

DSC01855

I still didn't get any results around 0x220, so I started searching... but, of course, where do I start searching? A DOS terminal screen gives me about 8 good rows of text space in text-mode, so I set a larger offset and started panning through the addresses. Using the IO Port Mapping for x86, I knew there were a few locations to skip and so I just kept digging.

This got pretty annoying after a while, so I went graphical... it really helped the next step!

DECLARE FUNCTION PortIsFree! (portNum#)
DIM DATACACHE(0 TO &H900) AS INTEGER
DIM OnOffPoints(0 TO 128) AS INTEGER
DIM OnPoint AS INTEGER
DIM OffPoint AS INTEGER
DIM COLWIDTH AS INTEGER
DIM ROWHEIGHT AS INTEGER
DIM COLUMNS AS INTEGER
DIM MAXADDRESSES AS INTEGER
COLWIDTH = 8
ROWHEIGHT = 16
COLUMNS = 75
MAXADDRESSES = &H5A
SCREEN 12
RESTORE OffPointData
FOR PY = 0 TO 5
  FOR PX = 0 TO 3
    READ A
    PSET (PX, PY), A
  NEXT PX
NEXT PY
RESTORE OnPointData
FOR PY = 0 TO 5
  FOR PX = 4 TO 7
    READ A
    PSET (PX, PY), A
  NEXT PX
NEXT PY
RESTORE OffCrapData
FOR PY = 0 TO 5
  FOR PX = 8 TO 11
    READ A
    PSET (PX, PY), A
  NEXT PX
NEXT PY
RESTORE OnCrapData
FOR PY = 0 TO 5
  FOR PX = 12 TO 15
    READ A
    PSET (PX, PY), A
  NEXT PX
NEXT PY

OnPoint = 0
OffPoint = 33
otherOnPOint = 64
OtherOffPoint = 80
GET (0, 0)-(3, 5), OnOffPoints(OnPoint)
GET (4, 0)-(7, 5), OnOffPoints(OffPoint)
GET (8, 0)-(11, 5), OnOffPoints(otherOnPOint)
GET (12, 0)-(15, 5), OnOffPoints(OtherOffPoint)
PORTSTART# = &H220
SELPORT# = &H220
PREVSELPORT# = &H220
FIRSTLOOP = 1
DATAOUT = 0
CLS
DO
	DIM row AS INTEGER
	DIM col AS INTEGER
	col = 1
	row = 1
	FOR PORT# = PORTSTART# TO (PORTSTART# + MAXADDRESSES)
		IF (PORT# > 0) THEN
			IF (FIRSTLOOP = 1 OR SELPORT# <> PREVSELPORT#) THEN
				LOCATE row, col
				IF (SELPORT# = PORT#) THEN
					COLOR 12
				ELSE
					COLOR 15
				END IF
				PRINT HEX$(PORT#);
			END IF
			freeport = PortIsFree(PORT#)
			IF (freeport = 1 OR 1 = 1) THEN
				DATAIN# = INP(PORT#)
				CACHED = DATACACHE(PORT#)
				IF DATAIN# <> CACHED OR FIRSTLOOP = 1 THEN
					DATACACHE(PORT#) = DATAIN#
					dotrow = (row * ROWHEIGHT)
					offset = (col - 1) * COLWIDTH
					FOR bit = 8 TO 1 STEP -1
						IF (DATAIN# AND (2 ^ (bit - 1))) THEN
							IF (freeport = 1) THEN
								PUT (offset, dotrow), OnOffPoints(OnPoint), PSET
							ELSE
								PUT (offset, dotrow), OnOffPoints(otherOnPOint), PSET
							END IF
						ELSE
							IF freeport = 1 THEN
								PUT (offset, dotrow), OnOffPoints(OffPoint), PSET
							ELSE
								PUT (offset, dotrow), OnOffPoints(OtherOffPoint), PSET
							END IF

						END IF
						offset = offset + 4
					NEXT bit
					LOCATE row + 2, col
					PRINT "    "
					COLOR 15
					LOCATE row + 2, col
					PRINT DATAIN#
				END IF
			END IF
		END IF
		LOCATE 1, 1
		col = col + 6
		IF (col > COLUMNS) THEN
			col = 1
			row = row + 4
		END IF
	NEXT PORT#
	PREVSELPORT# = SELPORT#
	FIRSTLOOP = 0
	LET K$ = INKEY$
	LOCATE 29, 20
	IF K$ = "q" OR K$ = "Q" THEN
		EXIT DO
	ELSEIF K$ = "/" THEN
		SELPORT# = SELPORT# + 20
	ELSEIF K$ = CHR$(0) + CHR$(77) THEN
		SELPORT# = SELPORT# + 1
	ELSEIF K$ = CHR$(0) + CHR$(75) THEN
		SELPORT# = SELPORT# - 1
	ELSEIF K$ = CHR$(0) + CHR$(72) THEN
		CLS
		FIRSTLOOP = 1
		PORTSTART# = PORTSTART# - MAXADDRESSES
		SELPORT# = PORTSTART#
	ELSEIF K$ = CHR$(0) + CHR$(80) THEN
		CLS
		FIRSTLOOP = 1
		PORTSTART# = PORTSTART# + MAXADDRESSES
		SELPORT# = PORTSTART#
	ELSEIF K$ >= "0" AND K$ < = "9" THEN
		IF (DATASTEP = 0) THEN
			DATAOUT = VAL(K$)
		ELSE
			DATAOUT = VAL(K$) + (DATAOUT * 10)
		END IF
		DATASTEP = DATASTEP + 1
		IF DATASTEP > 2 THEN
			DATASTEP = 0
		END IF
		LOCATE 29, 1
		PRINT "         ";
		LOCATE 29, 1
		PRINT DATAOUT;
	ELSEIF K$ = "s" THEN
		OUT SELPORT#, DATAOUT
	ELSEIF K$ <> "" THEN
		PRINT K$;
	END IF
LOOP
END

OffPointData:
DATA 00,10,10,00,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,00,10,10,00
OnPointData:
DATA 00,12,12,00,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,00,12,12,00
OffCrapData:
DATA 00,9,9,00,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,00,9,9,00
OnCrapData:
DATA 00,1,1,00,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,00,1,1,00

FUNCTION PortIsFree (portNum#)
	IF (portNum# >= &H2F8 AND portNum# < = &H2FF) THEN
		PortIsFree = 0
	ELSEIF (portNum# >= &H170 AND portNum# < = &H177) THEN
		PortIsFree = 0
	ELSEIF (portNum# >= &H1F0 AND portNum# < = &H1F7) THEN
		PortIsFree = 0
	ELSEIF (portNum# >= &H278 AND portNum# < = &H27A) THEN
		PortIsFree = 0
	ELSEIF (portNum# >= &H3B0 AND portNum# < = &H3DF) THEN
		PortIsFree = 0
	ELSEIF (portNum# >= &H3F0 AND portNum# < = &H3F7) THEN
		PortIsFree = 0
	ELSEIF (portNum# >= &H3F8 AND portNum# < = &H3FF) THEN
		PortIsFree = 0
	ELSE
		PortIsFree = 1
	END IF
END FUNCTION

DSC01862

Ahh... christmas... this now made it very easy for the next step... determination of the IO address switch. If you look at the photo of the monitor, you'll see from 680 that the first and last pins are grounded. This continues for 3 bytes, then skips a lot and continues again. That was the pattern I created on the cables to make it easier to find the card when I was adjusting the switches below.

Decoding the switch for IO address

Here comes the binary! I checked out the manual for that card I mentioned at the very start and it had a very convoluted way of adjusting the port offset. I chose to have more fun with the qbasic app above which simply displays the IO map of the system. Changing the cards' dipswitches whilst the machine was still on instantly affected the bytes displayed on-screen! The pattern of grounded wires, set on the first and last IO ports on the card, made it easy to distinguish... and, well, as dangerous as it sounds, it worked perfectly! The only slightly concerning thing was the address sharing... some of the areas had a duplicated area 0x400 bytes higher in the port table.

SW1 SW2 SW3 SW4 SW5 SW6 SW7 SW8 Output
0x3F0,0x7F0
X 0x1F0
X 0x2F0
X X 0x0F0
X 0x370,0x770
X X 0x170
X X 0x270
X X X 0x470
X 0x3B0,0x7B0
X X 0x5B0
X X 0x2B0
X X X 0x4B0
X X 0x330,0x730
X X X 0x130
X X X 0x230
X X X X 0x430
X 0x3D0,0x7D0
X X 0x1D0
X X 0x2D0,0x6D0
X X X 0x4D0
X X 0x350
X X X 0x550
X X X 0x250,0x650
X X X X 0x450
X X 0x390
X X X 0x190,0x590
X X X 0x290,0x690
X X X X 0x490,0x890
X X X 0x310,0x710
X X X X 0x110,0x510
X X X X 0x210,0x610
X X X X X 0x410
X 0x3E0
X X 0x5E0
X X 0x2E0,0x6E0
X X X 0x4E0
X X 0x760
X X X 0x560
X X X 0x660
X X X X 0x460
X X 0x3A0
X X X 0x5A0
X X X 0x2A0,0x6A0
X X X X 0x4A0
X X X 0x720
X X X X 0x520
X X X X 0x620
X X X X X 0x420
X X 0x7C0
X X X 0x1C0
X X X 0x6C0
X X X X 0x4C0
X X X 0x740
X X X X 0x540
X X X X 0x640
X X X X X 0x440
X X X 0x380
X X X X 0x580
X X X X 0x680
X X X X X 0x480
X X X X 0x300
X X X X X 0x500
X X X X X 0x600
X X X X X X 0x400
X 0x7F0
X X 0x5F0
X X 0x6F0
X X X 0x4F0
X X 0x770
X X X 0x570
X X X 0x670
X X X X 0x470
X X 0x7B0
X X X 0x5B0
X X X 0x6B0
X X X X 0x4B0
X X X 0x730
X X X X 0x530
X X X X 0x630
X X X X X 0x430
X X 0x7D0
X X X 0x5D0
X X X 0x6D0
X X X X 0x4D0
X X X 0x750
X X X X 0x550
X X X X 0x650
X X X X X 0x450
X X X 0x790
X X X X 0x590
X X X X 0x690
X X X X X 0x490
X X X X 0x710
X X X X X 0x510
X X X X X 0x610
X X X X X X 0x410
X X 0x7E0
X X X 0x5E0
X X X 0x6E0
X X X X 0x4E0
X X X 0x760
X X X X 0x560
X X X X 0x660
X X X X X 0x460
X X X 0x7A0
X X X X 0x5A0
X X X X 0x6A0
X X X X X 0x4A0
X X X X 0x720
X X X X X 0x520
X X X X X 0x620
X X X X X X 0x420
X X X 0x7C0
X X X X 0x5C0
X X X X 0x6C0
X X X X X 0x4C0
X X X X 0x740
X X X X X 0x540
X X X X X 0x640
X X X X X X 0x440
X X X X 0x780
X X X X X 0x580
X X X X X 0x680
X X X X X X 0x480
X X X X X 0x700,0x300
X X X X X X 0x500
X X X X X X 0x600
X X X X X X X 0x400
X 0x7F0,0x3F0
X X 0x5F0
X X 0x6F0
X X X 0x4F0
X X 0x770
X X X 0x570
X X X 0x670
X X X X 0x470
X X 0x7B0
X X X 0x5B0
X X X 0x6B0
X X X X 0x4B0
X X X 0x730
X X X X 0x530
X X X X 0x630
X X X X X 0x430
X X 0x0x7D0,0x3D0
X X X 0x5D0
X X X 0x6D0
X X X X 0x4D0
X X X 0x350
X X X X 0x550
X X X X 0x650
X X X X X 0x450
X X X 0x390
X X X X 0x590
X X X X 0x690
X X X X X 0x490
X X X X 0x310
X X X X X 0x510
X X X X X 0x610
X X X X X X 0x410
X X 0x3E0
X X X 0x5E0
X X X 0x6E0
X X X X 0x4E0
X X X 0x360
X X X X 0x560
X X X X 0x660
X X X X X 0x460
X X X 0x3A0
X X X X 0x5A0
X X X X 0x6A0
X X X X X 0x4A0
X X X X 0x320
X X X X X 0x520
X X X X X 0x620
X X X X X X 0x420
X X X 0x7C0
X X X X 0x5C0
X X X X 0x6C0
X X X X X 0x4C0
X X X X 0x740,0x340
X X X X X 0x540
X X X X X 0x640
X X X X X X 0x440
X X X X 0x380,0x780
X X X X X 0x580
X X X X X 0x680
X X X X X X 0x480
X X X X X 0x300,0x700
X X X X X X 0x500
X X X X X X 0x600
X X X X X X X 0x400
X X 0x3F0,0x7F0
X X X 0x5F0
X X X 0x6F0
X X X X 0x4F0
X X X 0x770,0x370
X X X X 0x570
X X X X 0x670
X X X X X 0x470
X X X 0x3B0,0x7B0
X X X X 0x5B0
X X X X 0x6B0
X X X X X 0x4B0
X X X X 0x330,0x730
X X X X X 0x530
X X X X X 0x630
X X X X X X 0x430
X X X 0x3D0,0x7D0
X X X X 0x5D0
X X X X 0x6D0
X X X X X 0x4D0
X X X X 0x350,0x750
X X X X X 0x550
X X X X X 0x650
X X X X X X 0x450
X X X X 0x790,0x390
X X X X X 0x590
X X X X X 0x690
X X X X X X 0x490
X X X X X 0x710,0x310
X X X X X X 0x510
X X X X X X 0x610
X X X X X X X 0x410
X X X 0x3E0,0x7E0
X X X X 0x5E0
X X X X 0x6E0
X X X X X 0x4E0
X X X X 0x760,0x360
X X X X X 0x560
X X X X X 0x660
X X X X X X 0x460
X X X X 0x7A0,0x3A0
X X X X X 0x5A0
X X X X X 0x6A0
X X X X X X 0x4A0
X X X X X 0x720,0x320
X X X X X X 0x520
X X X X X X 0x620
X X X X X X X 0x420
X X X X 0x7C0,0x3C0
X X X X X 0x5C0
X X X X X 0x6C0
X X X X X X 0x4C0
X X X X X 0x740
X X X X X X 0x540
X X X X X X 0x640
X X X X X X X 0x440
X X X X X 0x780
X X X X X X 0x580
X X X X X X 0x680
X X X X X X X 0x480
X X X X X X 0x700,0x300
X X X X X X X 0x500
X X X X X X X 0x600
X X X X X X X X 0x400,0x800

So from above... the magical calculation is... dunno... but the weird sharing of XXX with XXX+400h was confusing and slightly dangerous when both ranges could conflict with other hardware in the system!

Port Mapping

So now we know how to set the base address, how do we then actually work with the data? The card needs 16 consecutive IO Ports, so please make sure you find a space in the address table that has the capacity. You don't want it interfering with other hardware in your system. Actually, the code above gives you a really good idea as to what areas in use and which are free. Also make note of the 'ghosting' that seems to occur with the lower address ranges. The table below expects you've set the above address to 0x220, but this means the data will also be visible on 620! This could easily interfere with an AWE Soundcard.

Port Usage
0x220 CN1 pins 33,35,37,39,41,43,45,47
0x221 CN1 pins 17,19,21,23,25,27,29,31
0x222 CN1 pins 01,03,05,07,09,11,13,15
0x223 Control Register for CN1
0x224 CN2 pins 33,35,37,39,41,43,45,47
0x225 CN2 pins 17,19,21,23,25,27,29,31
0x226 CN2 pins 01,03,05,07,09,11,13,15
0x227 Control Register for CN2
0x228 CN3 pins 33,35,37,39,41,43,45,47
0x229 CN3 pins 17,19,21,23,25,27,29,31
0x22A CN3 pins 01,03,05,07,09,11,13,15
0x22B Control Register for CN3
0x22C CN4 pins 33,35,37,39,41,43,45,47
0x22D CN4 pins 17,19,21,23,25,27,29,31
0x22E CN4 pins 01,03,05,07,09,11,13,15
0x22F Control Register for CN4

Writing to the ports

With the QBASIC code above, you can use the Up and Down arrows to paginate. You can then use the left and right to select a specific address. Use the forward-slash key to skip 10 addresses at a time. Once on an address, you can write a value to the selected port by typing that number in and pressing the letter S. From here I worked out the following:

  • You can only write out values to the bytes if you send a value to the control register first.
  • If you write a number to one of the three blocks of pins, then it'll set all associated bits HIGH.
  • Sending a 0 therefore sets them all LOW, 255 will set them all HIGH.
  • The final control register allows you to set all three bytes to 0 or 255 quickly. But I haven't totally decoded what the required values are.

I'm still trying to ascertain how it works. The EDP jumper seems to determine if one is allowed to send bytes to the ports, but I haven't correctly determined exactly how.
As is, the card could easily be used to control/read binary bits in its default setting.

Let's control a model railway!

This card would've been perfect when I was younger. I remember back at Lyneham Hockey Courts in Canberra, there was an engineer who'd built a smallish model railway at one of the conventions and it was totally computer controlled. There was this hulking XT/AT machine next to the layout with ribbon cables running everywhere. A card with this much IO would've been fantastic for sensors, point control and even throttles. I might do that now, just for fun, using some of the off-the-shelf Arduino modules in my box-o'-junk. Sure, it's cheating, but I'll still be using the full power of the card.

An L298 can provide the throttle, but it needs a variable resistance fed into this. We can use a Resistor Ladder to provide this, as we have so many pins available.

I might just put it back in its box for the next rainy day. If anyone has further documentation on this card, then please leave a comment!

Filed under: Retro Leave a comment
Comments (0) Trackbacks (2)

No comments yet.


Leave a comment


*