Multi IO Expansion Card
Multi IO Expansion Card
During the development of the TU-58 IO routines for FIG Forth I came up with the idea of building a simple serial interface instead of the breadboard hacks used during development and when we are at it a boot ROM would be nice and for the PDP-11 LED blink challenge I also played with some GPIO using a W65C22S VIA. And thats how I developed a Multi IO Expansion Card for the expansion slot of the DCJ11 SBC.
This card in fact combines the second serial interface experiments using breadboards and the W65C22S interface on a breadboard shown in the previous chapter and adds a boot ROM. Using this Multi IO Expansion Card you can turn the DCJ11 SBC into a stand alone Forth system that boots from a TU-58 and with jumpers J1 and J2 properly set will directly start the TU-58 bootstrap loader and boot FIG Forth 1.3.3 via TU-58 with more than 8000 screens of storage to play around.
Second Serial Interface
The design of the serial interface on this Multi IO card is based on the second version of the serial interfaces built on a breadboard. The first serial interface which I used to develop the TU-58 IO routines for Forth used a DC319. But the second serial interface on a breadboard used a CDP6402 to achieve higher bit rates for the TU-58.
I designed this simple second serial interface after the successfull boot of stand alone Forth which is described in one of the next articles. Initially there was the idea to add a second serial interface together with a CPLD as the piece de resistance to enable all the features required to run at least RT-11. But things took another turn and with Forth now running in stand alone mode using the breadboard serial interface I thought it would be a nice addition to the DCJ11 SBC to have a second serial interface and some boot ROM using only simple logic and through hole parts to make it a small stand alone Forth system.
The primary goal of this interface is to connect the DCJ11 SBC to a TU-58 emulator so you can boot any program that is happy with a PDP-11 that has no line time clock, no interrupts, just 128kW of RAM and no decoding of missing bus addresses.
This card is designed to be plugged into the expansion slot of the DCJ11 SBC.
BootROM
The BootROM is implemented using two 8-bit 8kbyte ROM. The address ranges reserved for the BootROM are 165000..165776 and 173000.173776, the standard boot ROM address ranges for a PDP-11. There is no Page Control Register and you have only a total of two times 256 words of Boot ROM.
Adresses A0..12 of the ROM are connected to the latched DCJ11 addresses A1..A8 and A10..A12. This is done in a way to allow the write protection enable/disable algorithm of AT28C64 EEPROMs, in case you want to use those.
General Purpose Input Output
During the development of the second serial interface card with BootROM I decided to also add some GPIO. I decided to use the W65C22S Versatile Interface Adapter (VIA). Readers familiar with the 6502 processor and the family of support chips will certainly remember this device. WDC is still producing the W65C02 processor, the 16-bit version W65C816, the VIA W65C22S and the UART W65C51. Interfacing the DCJ11 with the 65xx family will be described in the next chapter.
Description of Schematic
The main part of the circuit is integrated into to GALs. Microchip still produces EEPLDs which are 100% compatible with the GALs used. So you can either use GALs from your stock, or buy the NOS from resellers or buy new Microchip EEPLDs from your distributer.
You need however a programmer to program the chips. I’m using the T48 from XGecu which is capable of programming legacy GALs and the new EEPLDs ATF16V8 and ATF22V10 from Microchip. Instead of the official interface programm XGpro from XGecu I use minipro.
The UART used is a CDP6402. They are no longer produced but are still available from private resellers or as NOS. I bought mine from various places and they all seem to be genuine Harris ICs. At least they all work.
The BootROM uses two AT28C64 EEPROMs or any equivalent chip. Two 28-pin DIP sockets are provided on the PCB. You can even use EPROMs or any other IC that uses the standard JEDEC pin-out for 8kbyte storage ICs.
Because of the complexity of the logic, or let’s say because the logic described using CUPL looks much logical than the same function written in GALASM or one of its derivate the source for the GAL is written using WinCUPL. I will provide all the files, i.e. the PLD, the DOC and the JED file. There are two features I like about WinCUPL, the first is the concept of a state machine and the second is the use of equations that are actually not allocated to a pin and use them as input for equations of real pins.
Address Latch
As the DCJ11 uses a multiplexed address/data bus we first need to latch the address for every cycle. Two 74HCT574 8-bit latches are used to latch the IO address. Note we only use A1..12. We do not latch A0 as there are no byte addressable registers. The W65C22S is connected in a way that all 8-bit registers are accessed by using only the lower byte of 16 consecutive words.
Also we do not need to latch address bits A13 and higher as the DCJ11 already indicates the access of external IO registers using the outputs BS0 and BS1 which are latched on the SBC and provided as LBS0 and LBS1 on the expansion slot.
Address Decoding
The higher order addresses A9 to A12 are decoded in U4, the GAL22V10. I creates the signals VIA, ROM, CON and SLU.
Serial Line Unit
The serial line unit, SLU, uses the GAL16V8 to interconnect the DCJ11 with the CPD6402 UART. The GAL decodes the address bits A1 and A2 and provides the status bits TBRE and DR via DAL7. The GAL also implements the BRK bit and gates the SO output of the UART to generate the BRK signal via software as it was the case for any standard DLV11 serial line unit. The status of the BRK bit can be read back as bit0 of the transmitter status register. Although the combination of GAL and CDP6402 implements many of the features of a DLV11 it is not 100% compatible as it only connects to the data bits 0..7 and the unused bits implemented in the receiver and transmitter status register, that normally should return zero, are not guaranteed to return zero.
SLU is asserted for the complete range of 176000..176776, which also covers the standard address 176500..176506 range for the first serial line unit on a PDP-11.
On-board Console
Before you can use this expansion card you must make sure that the onboard IO signal is disconnected from the IC that generates the signal, i.e. Pin 6 of the 74HCT139 (U14) or Pin 18 of the GAL16V8 (U18). In my case I just bend the Pin of the IC and reinsert it, here is a picture of the GAL with the pin outside the socket.
Now you can insert the expansion card and power on the DCJ11 SBC. The signal name CON of the GAL20V10 of the expansion card connects to the IO signal of the expansion slot which connects to the chip select of the DC319. The console is now only selected for the address range 177000..177776 which includes the standard address range 177560..177566 of the console, which is also used by ODT.
Versatile Interface Adapter
The W65C22S is selected by the address range 174000..174776. Note that address lines A1..A4 connect to the register select signals RS0..RS3 and hence you need to double the offset for the registers. I.e to access the data direction register of port A, which is register 3, you need to access the word at offset 6, e.g. 174006. Note that the VIA registers are mirrored for every 16 words. The same is true for the console and the second serial unit which are mirrored every 4 words.
Registers of the Second Serial Unit
As with all serial units emulating a DLV11 there are four registers, the second serial unit however only connects to DAL0..DAL7 and the status registers only connect to DAL0 and DAL7. Therefore in contrast to the real DLV11 other bits may read as non-zero. This is normally not an issue, as programs are advised only to check bit 7 of the RCSR and the XCSR for the status of RBUF and XBUF.
RCSR
The receive control and status register at register offset 0 has only one valid bit, the Data Ready bit. If set there is a character in the receive buffer waiting to be fetched. When fetched the DR bit is cleared. Note the the error bits D15..D12 are not implemented and hence there is no way you can detect transmission errors
D7 D6 D5 D4 D3 D2 D2 D0
+---+---+---+---+---+---+---+---+
| DR| x | x | x | x | x | x | x |
+---+---+---+---+---+---+---+---+
XCSR
The transmit control and status register at register offset 4 only implements the Transmit Buffer Register Empty and the Break bit. TRBE is set if the transmit buffer is empty and another character can be put into the TBUF register. The Break bit is read/write and when set will send a continuous SPACE on the transmit data output of the GAL. If asserted for more than the character duration this will be detected as a BRK signal be the receiver of the other end of the communication. Line
D7 D6 D5 D4 D3 D2 D2 D0
+---+---+---+---+---+---+---+---+
|TBRE x | x | x | x | x | x |BRK|
+---+---+---+---+---+---+---+---+
All other bits are undefined when read.
Sending a BREAK
The following code will send a BREAK signal. We will send two times the character 0xFF. As the transmitter is buffered we need to send two characters. However if the second character has been copied from the Transmit Buffer Register to the Transmit Register, the second character is still being sent. However some bits are already sent and hence the duration SPACE has been asserted via setting the BRK bit has already exceeded the duration of a character. If we now clear the BRK bit the transmit data line will reflect the second character starting approx with bit1 or bit2, as we send the character 0xFF all bits are set, i.e. a MARK, followed by a STOP bit which is again a MARK. Even we enable the transmit data line in the middle of the second character, nothing more than some MARKS are seen be the other end following SPACE with a period of at least 13 bits of transmitted data. Which exceeds the maximum valid SPACE period of 9 bits.
The reason we are sending this dummy 0xFF character twice is to make sure that the period of the BREAK signal is timed with the active baud rate.
BIS #1, XCSR ; Set the BRK bit send a SPACE
10$: TSTB XCSR ; TRBE ready
BPL 10$ ; no wait
MOV #177777, XBUF ;
20$: TSTB XCSR ; TRBE ready
BPL 20$ ; no wait
MOV #177777, XBUF ;
CLR XCSR ; end SPACE
30$: TSTB XCSR ; TRBE ready
BPL 30$ ; no wait
MOV #XCSR, R1
BIS #1, (R1) ; Set the BRK bit send a SPACE
MOV #177777, R2
10$: TSTB (R1) ; TRBE ready
BPL 10$ ; no wait
MOVB R2, 2(R1) ; Send 0377
CLRB R2
SWAB R2
BNE 10$
CLR (R1) ; end SPACE
30$: TSB (R1) ; TRBE ready
BPL 30$ ; no wait
The following shows the capture with a logic analyzer when the above code generates a break signal. Using a CDP6402 as the UART and gateing the transmit data of the UART with BRK using an external GAL gives the unique opportunity to capture all relevant signals.
- SO is the serial output of the CDP6402, ie. what effectively is sent
- TXD is the serail signal sent over the RS-232 to the host which is SO gated by BRK
- TBRE shows when the Transmit Buffer Register is Empty and accepts new data
- BRK is the inverted BRK bit of XCSR Bit 7.
The baud-rate is 115200baud and the generated break signal is 103µs, which is larger than the maximum allowable SPACE duration when sending the character 0x00, which would take the duration of sending 9 bits or 78µs, one start bit and the 8 data bits, followed by the mandatory stop bit which is always a MARK. You can compare that in the picture of the capture with the 0x00 sent later as the unit number.
TU-58 bootstrap
The following is my version of the TU-58 bootstrap code. This is the result of studying dozens of variations of the TU-58 bootstrap code and is just an example but by now means the only solution.
This will reset the communication with a TU-58 tape and send the boot command to retrieve the boot block. Note that the code boots from unit 0, this is hardcoded the way the command characters are prepared in R0. Most boot blocks expect R0 to contain the unit number. In case you want to modify
1 ;
2 ; Redesign of TU-58 bootstrap Peter Schranz 07-June-2025
3 ;
4 ; First we send two characters 03FF with the break bit set, this is
5 ; to time the break signal in a way to make it longer notable longer
6 ; than a single character. First we need to make sure that the UART
7 ; accepts a new character, i.e. TBRE must be set (this is bit 7 of
8 ; the XCSR). The transmitter is buffered, so when we
9 ; place the first character into the transmission buffer register
10 ; the character will be placed almost immediately into the transmission
11 ; register and the UART sets TBRE again. This takes approx 1.5 to 2.5
12 ; the time it takes for one bit to be sent, see CDP6402 datasheet for
13 ; example. Now we can place the second character into the transmission
14 ; buffer register. When the first character has been sent the second
15 ; character is copied from the transmission buffer register to the
16 ; transmission register and TBRE is set. The time elapsed now from
17 ; the moment we have set the BRK bit until now is equivalent to approx
18 ; 14 bits. Which is longer than the 10 bits (1 start, 8 data and
19 ; one stop bit) of sending a character and should be recognized as
20 ; BRK signal. Typically the remote end will detect this as a framing
21 ; error as there was no stop bit after the start and eight data bits.
22 ; Typically framing error is used as an equivalent to break. Note
23 ; that when we now clear the BRK bit the second character is still
24 ; being sent out the transmission register. But as we sent 0377
25 ; as the character during BRK the rest of the character and the stop
26 ; bit are sent as MARK and the remote end will not know that this
27 ; is just the end of the second 0377. We now will send the three
28 ; necessary bytes to request the boot block. As the first of this
29 ; three bytes is sent after the second 0377 has been sent completely
30 ; the remote end will detect these characters perfectly without errors.
31 ; After the BRK the TU-58 protocol expects at least one INIT before
32 ; the first command is accepted. This is the first character being sent.
33 ; Then second and third characters sent are the special boot command
34 ; which just consists of the BOOT flag and the UNIT number.
35 ;
36 ; ../src/macro11 ddboot5.mac -o ddboot5.obj -l ddboot5.lst
37 ; perl ../obj2bin-master/obj2bin.pl --raw --rt11 --outfile=ddboot5.bin ddboot5.obj
38 ;
39 ;
40 176504 XCSR = 176504
41 .asect
42 073000 .= 73000 ; BootROM Start
43 073000 012703 176504 BOOTDD: mov #XCSR, r3 ;
44 073004 010301 mov r3, r1
45 073006 005213 inc (r3) ; BRK
46 073010 012702 mov (pc)+, r2
47 073012 377 377 .byte 377,377 ; Send two 0377 characters
48 073014 105713 1$: tstb (r3)
49 073016 100376 bpl 1$
50 073020 110263 000002 movb r2, 2(r3)
51 073024 105002 clrb r2 ; mark that we have sent this character
52 073026 000302 swab r2 ; next character (aka second character)
53 073030 001371 bne 1$ ; after this r2 is zero and used as buffer address
54 073032 105713 3$: tstb (r3) ; all characters sent, now wait for TBRE to
55 073034 100376 bpl 3$ ; be set again.
56 073036 005013 clr (r3) ; ~BRK
57 073040 012700 000004 mov #4, r0 ; special bit shift trick using that BOOT is
58 ; just INIT shifted one bit to the left.
59 073044 005741 tst -(r1) ; RBUF, in case there is a INIT from the TU-58
60 073046 042700 000020 20$: bic #20, r0 ; INIT, BOOT, UNIT, the bic makes sure that
61 073052 105713 30$: tstb (r3) ; r0 is all zeroes after we sent the BOOT flags.
62 073054 100376 bpl 30$
63 073056 110063 000002 movb r0, 2(r3)
64 073062 006300 asl r0 ; after we sent UNIT r0 is zero
65 073064 001370 bne 20$ ;
66 073066 105761 177776 40$: tstb -2(r1) ; retrieve boot block as result of BOOT UNIT 0
67 073072 100375 bpl 40$ ;
68 073074 111122 movb (r1),(r2)+ ; store byte
69 073076 022702 001000 cmp #1000, r2
70 073102 101371 bhi 40$
71 073104 005007 clr pc ; and here we go
71
GAL16V8 Serial Unit Logic
The complete logic that makes a CDP6402 to be as compatible as possible with a DLV11 Serial Line Unit, except for Interrupts and undefined bits, fits in a simple GAL16V8. This GAL decodes the lower addresses A1 and A2 and generates the appropriate select signals for the CDP6402. It also implements bit 7 of the RCSR and XCSR and bit 0 of the XCSR, the break bit. The serial output of the CDP6402 is then fed into the GAL and gated using bit 0 of the XCSR to create an RS-232 BREAK signal as in the original DLV11 or the DC319. The GAL connects to DAL7 to read the status bits and connects to DAL0 twice, as input and as output, there should be a possibility to use a single output using the feedback, but I did not figure out how to do it and there were plenty of bits.
This GAL also implements an inverter as the INIT signal form the main board of the DCJ11 SBC is active high and the W65C22S VIA requires an active low reset signal.
Name CDP6402MINI;
Partno A;
Date 03/04/2025;
Revision 00;
Designer cbscpe;
Company Netzwerk & Design GmbH;
Assembly None;
Location None;
Device g16v8;
/*
Interface Logic for the CDP6402 Mini Adapter and the MULTI IO
Expansion Card
To compile with cupl batch file
.\cupl.bat Z:\CDP6402\CDP6402MINIBRK.PLD
2025-04-13 A small adapter with SIL pins that plug into the socket
for the DC319 with a CDP6402 and the necessary glue logic
to make ODT happy
2025-05-29 Add BRK bit, shuffle pins for Multi IO PCB
To program the GAL I use a T48 programmer from XGecu
and the open source program minipro which runs on Linux
and macOS.
minipro -p GAL16V8D -w CDP6402MINIBRK.jed
This GAL also includes the BRK flag necessary to make TU-58 happy.
The break flag is implemented as a RS Latch as this requires only
one OLMC. The break flag can even be read back although I'm not
aware of any program that requires this.
Note the complete different layout compared to CDP6402MINI.PLD
*/
PIN 1 = !WEL; /* Write Enable Lower Byte */
PIN 2 = !OE; /* Output Enable */
PIN 3 = !CE; /* Chip Enable */
PIN 4 = INIT; /* Initialization/Reset */
PIN 5 = TBRE; /* Transmit Buffer Register Empty */
PIN 6 = DR; /* Data Ready */
PIN 7 = D0; /* DAL0 input */
PIN 9 = A2; /* Address Line 2 */
PIN 8 = A1; /* Address Line 1 */
PIN 11 = SO; /* Serial Out */
PIN 12 = DAL7; /* DAL7 */
PIN 13 = !TBRL; /* Transmit Buffer Register Load */
PIN 14 = DAL0; /* DAL0 Output */
PIN 15 = !BRK; /* BRK Signal */
PIN 16 = !DRR; /* Data Receive Read */
PIN 17 = !RES; /* W65C22S Reset */
PIN 18 = TXD; /* Transmit Data after BRK */
DAL7 = !A2 & DR
# A2 & TBRE;
DAL7.oe = CE & OE & !A1;
TBRL = CE & WEL & A2 & A1;
DRR = CE & OE & !A2 & A1;
BRKS = CE & WEL & D0 & A2 & !A1;
BRKR = CE & WEL & !D0 & A2 & !A1
# INIT;
BRKA = !BRKS & !BRK;
BRK = !BRKR & !BRKA;
DAL0 = BRK;
DAL0.oe = CE & OE & A2 & !A1;
TXD = !BRK & SO;
RES = INIT;
GAL22V10 Adresse decoder and W65C22S PHI2 state machine
The full design of the GAL22V10 has already been described in the previous chapter that discusses the interface between the DCJ11 and the W65C22S. The only small difference is that the Multi IO card now makes use of the second chip select of the W65C22S which simplifies the equations for VIA which would make it possible to use any address decoder to generate this signal and use the state machine only to provide PHI2 and VIAACT. Which would allow to fit the W65C22S specific part into a GAL16V8.
VIA = LBS1 & !LBS0 & A12 & A11 & !A10 & !A9;