TWIN Operating Systems

Information on operating system versions, and on module files. Below there is also a subsection on the internal working of the operating systems. That section is still incomplete, and may contain errors.

The TWIN machine was sold under three brand names, using at least that many operating systems. From the launch around September 1977 to around 1982 there were several updates to each of these operating systems. Although it seems likely that most of these operating shared architecture, operating instructions and source code, I do not fully know where similarities end, nor what the differences are between versions of the same operating system.

Copies of SDOS (for the Signetics TWIN), UDOS (for the Millennium Universal One) and TEKDOS (for the Tektronix 8002) can be found on available disk images. So far I have looked at the disk images from the Stuttgart computer museum, the Tektronix and Millennium directories on, and the disks that came with my ECA system. The collection can be downloaded below. My HM system came with a interesting collection of disks, which is available for download.

To load DOS from a disk, a small boot EEPROM contains code that reads contents from tracks 1 to 4 into master memory starting from address 0000. The EEPROM is only 256 bytes long, and half of that is taken up by jump instructions for interrupt handler routines. Note that the contents of the EEPROM are in reversed order, probably because the address lines on the system bus are inverted. The file at bitsavers contains the EEPROM-contents in their stored order; file boot.bin below contains the contents as seen by the master cpu.

Based on information gleaned from documentation, dates of copyright notices and the identity-strings in modules on disk images I have compiled the following timelime. This is still very much work in progress!

One outlier is TOS version 1.0, which was found on an original Signetics disk with the ECA system. A clue to its origin can be found in the module for the Editor: the editor contains a command to return to the operating system. This command is either SDOS, UDOS, or SUSPEND (for SDOS, UDOS and TEKDOS respectively). For TOS this editor command is “SDOS”. The fact that TOS was found on an original Signetics-labeled disk most likely means that we are dealing with an internal test-version of SDOS. Some of the disks that came with the HM system contain another variant: EXOS. EXOS shares some of the unique features of TOS (such as a real-time clock and certain builtin commands), and must therefor be an evolved version of TOS. EXOS announces itself with the version string ** EXOS Nat.Lab. **. NatLab is the famed research and development lab of Philips, Signetics’ parent company. NatLab was involved with TWIN development, and developed a Pascal compiler for the TWIN. In fact, PASCAL is one of the built-in commands of EXOS. EXOS also includes a typesetting program and supports debugging of the Z80 microprocessor.

Not all release dates are known exactly. At first glance it looks like the major and minor versions numbers are synchronised between SDOS, UDOS and TEKDOS.

SDOS 1.0 No known documentation for this version. Its properties are only known in comparison to SDOS 2.0, based on the TWIN Technical Note SW002. The LDIR command does not show disk statistics.

SDOS 2.0 Described in TWIN Technical Note SW002. Command LDIR also prints disk statistics, the VERIFY command requires a drive number. All RAM is initialised on startup, to support the parity checks on the new dynamic RAM card.

SDOS 3.0 The SEARCH command has been removed. The SLAVE command is replaced by the ICE command (which takes different parameters). Adds CMPF and DFIL commands for file management. Adds MOVE, FILL, READ, WRITE, UPR commands to maniplate slave memory.

SDOS 3.1 The minimum version required for the new Real-time Hardware Analyzer card.

SDOS 4.0 Adds commands to compare two files, dump a file in hex format

I will updated this page when new information is discovered.

Module files

Modules contain executable code that can be loaded into master or slave memory. Almost all DOS commands are provided as modules. DOS’ module files are stored in hidden files: the first character of the filename is Ctrl-O (h'0F'). Note that LDIR can show hidden files on a disk when using “LDIR .” .

Modules consists of a 128-byte (one sector) header, followed by data bytes. Most often the data bytes are consecutively stored at the starting address, but the actual load sequence is dictated by load commands listed in the header. See the table below; offsets are hexadecimal, lengths are decimal.

Load module layout
00..02   3   "MOD"
03       1   01=overlay area 1, 02=overlay area 2, 03=slave memory
04..05   2   Load address (slave's perspective)
06..07   2   End address
08..09   2   Start address (first address of execution)
0a..1f  22   Identity (e.g. module name, version, date), 0d terminated text.
20..7f  var  Load commands (variable length)

From offset 20 onwards there are one or more load commands. Load commands start with a command byte, followed by one or two bytes, depending on the command. In the explanation below the command byte is separated into its eight bits (0 or 1, a letter indicating a special function, or ‘x’ for don’t care).

Set Base: bbxx-x00x
bb is the bank. The two bytes following this command byte indicate the base address from the master’s perspective. This base address is used for the subsequent Read Data command.

Read Data: abcd-e01z
00za bcde is the number of sectors to read, minus 1. The byte following this command byte is the length for each of these read operations. Typically this length is h'80' (128) for one full sector, but when the number of sectors is one (the last sector to be read), the length will often be less than 128.

Load End: xxxx-x10x
Indicates the end of the list of load commands. Some of the bits may have a function, but this I have not figured out yet.

DOS reserves two memory areas for modules: overlay area 1 (address 1500-187f) and overlay area 2 (address 1880-1bff). These areas are small, just under 900 bytes each. Some DOS commands actually consist of two modules. The LDIR command for example, in module file “Ctrl-O L”, is loaded into overlay area 1. LDIR then verifies that overlay area 2 is not in use, and loads module file “Ctrl-O O” into overlay area 2.

The design of modules seems overly complex. Note also that code that is too large for overlay areas 1 and 2 combined has to be loaded into slave memory, and then has to be executed by the slave processor.

I wrote a Perl-script to parse the header contents of modules.

For example, the module for the MACRO command:

Module file   :  [@8]
Is module?    :  probably
Identity      :  [** MACRO VER 2.1 **]
Overlay       :  slave memory
Address range :  0040 - 1a13
Start address :  0e05
Load commands :
  - bank/base  0/4040
  - 1 partial blocks of 64 bytes into 4040 ~ 407f
  - bank/base  0/4080
  - 32 full blocks into 4080 ~ 507f
  - bank/base  0/5080
  - 19 full blocks into 5080 ~ 59ff
  - bank/base  0/5a00
  - 1 partial blocks of 20 bytes into 5a00 ~ 5a13
  - end.

For comparison, the header of the MACRO module looks like this (with the nine load commands highlighted).

00000000: 4d4f 4403 0040 1a13 0e05 2a2a 204d 4143  MOD..@....** MAC
00000010: 524f 2056 4552 2032 2e31 202a 2a0d 3031  RO VER 2.1 **.
00000020: 0040 4002 4000 4080 fa80 0050 8092 8000  .@@.@.@....P....
00000030: 5a00 0214 040d 2020 2020 2020 204c 0044  Z....
00000040: 492c 3320 203e 4d53 000d 2020 2020 2020  
00000050: 2042 0041 2020 2020 2044 4953 004c 2020  
00000060: 2020 2020 2020 0020 2020 2020 4449 5350  
00000070: 0041 5920 4d45 5353 4147 000d 2020 2020 

Note that unused bytes of the header (padding of the Identity string, and bytes from the Load End command onwards) contain random data.

DOS Internals

This section is going to be long, and is currently only partially complete and possibly incorrect in places. I expect to publish updates regularly.

The operating systems for the TWIN (SDOS, UDOS, TEKDOS, and TOS) share a common ancestry. I use the term ‘DOS’ as the generic word for all. To understand their working I started reverse-engineering on the earliest, and therefore hopefully most simple version: TOS v1. TOS already turned out to be quite complex internally.

Application programs and modules make use of DOS’ service functions. This is done through a Supervisor Call (SVC). The calling program supplies a Service Request Block (SRB), 8 to 12 bytes containing all relevant information for the request. More details on the SVC interface can be found in one of the Operator Manuals.


DOS is multitasking. It can handle four jobs, although only one of those can be running at any time (no multiprocessing). Jobs can be suspended from the console and restarted. While one job is waiting for slow I/O operations to finish another job can continue processing.

Job 0 is for DOS internally, when handling service calls or running a procedure (“batch file”).
Job 1 is for modules running in overlay area 1.
Job 2 is for modules running in overlay area 2.
Job 3 is for programs running on the slave CPU.

DOS maintains a job control block, which is a set of tables of length four, indexed by the job number. Most table entries are byte-values, those that contain addresses are word-values (two bytes).

2050 JobStatus (byte):  Job current state
	0  job idle (no job)
	1  job loaded
	2  job ready to start
	3  job executing
	4  job in i/o wait
	5  job i/o complete
	6  job suspended
	7  job being aborted
	8  job self paused
2054 JobPrevStatus (byte):  Job previous state, values same meaning as current status
2058 JobChanAssigned (byte):  Job assigned channels (bit mask, 0/1 = (un/)assigned)
205c JobChanActive (byte):  Job channels with an in-progress SVC (bit mask, 0/1 = (not/)active)
2060 JobWaitingForCh (byte):  Job waiting for channel (bit mask)
2064 JobChanIOComplete (byte):  Job channel has completed I/O task (bit mask)
2068 JobWait (byte): (JJO), number of job that this job is waiting for (zero if none)
			Uses ExtraReturntab when waiting is done.
206c JobIdx (byte):  JID??
2070 JobBank (byte):  Job bank
2074 ModuleStartOffset (byte):  See information on modules.
2078 ModuleStartAddr (word):  See information on modules.
2080 SRBreturntab (word):  SRB return addresses
2088 ExtraReturntab (word):  ?? addresses
2890 JobStatusCh0 ~ JobStatusCh7 (byte): per channel the status of that channel
20b0 JobDeviceCh0 ~ JobDeviceCh7 (byte): per channel the device used by that channel
20d0 (end of job control block)


DOS exposes very few function to application programs. The interface consists of a few routines and subroutines at address 2000 and further.

2000 API_SupervisorCall: the main entry into DOS service functions. The calling program supplies the address of the Service Request Block (SRB) in registers R2R3. The SRB contains the return address. Called as a routine, using a branch instruction.

2003 API_IntSaveState: saves all registers, for later restore. Called as a subroutine.

2006 API_IntRestrState: restore previously saved registers. Called as a subroutine.

2009 API_ConvertASCtoNum: converts a string representing a hexadecimal number into a binary number. Called as a subroutine.

200C API_FileReadyIOComplete: indicates that file operations are complete and the file can be closed.

200F API_Dispatcher: the job scheduler. Not normally called; programs and modules suspend or abort using Supervisor calls, which will run the dispatcher automatically.

2012 API_QueueError: report a module ID and error number. DOS will display the error number on the console when no other job is scheduled by the dispatcher.

2015 API_LookupDeviceName: parse a device name (such as CONI or LPT1).

2018 API_ParseCommand: parse and run a DOS command.

201B API_SkipToCmdBuff1Param: used to parse the commandline parameters.


Jobs perform input/output operations not directly on devices but on channels. Each job can use at most 8 channels. Before a channel can be used it must be assigned using a Supervisor call to a device name (such as CONO for console output, or LPT1 for the system printer), or to a filename. All further I/O operations (reading, writing, deleting the file, renaming the file) are performed with reference to the channel number. When a channel is no longer needed it must be closed using a Supervisor call.

Data on channels is kept in the job control block, using JobStatusCh0 to JobStatusCh7 and JobDeviceCh0 to JobDeviceCh7. Job status is a bit mask. Bit 80 indicates that the channel is assigned. Bit 20 indicates that it is writeable. Other bits to be determined. The device indicates an offset into the device table.

When assigning a channel to a file that does not exist, that file will be created. There is often a sequence of assigning a file, testing whether a new file was created, and deleting that new file if so.


DOS can handle at most 16 physical devices. CONO, CONI, and the floppy disk are examples of sharable devices which can be assigned to more than one channel. The LPT1, HSPT, and TTYR are non-sharable devices and can only be assigned to one channel at any given time. Which devices are installed by default varies between DOSes. Device drivers can be “loaded” by patching DOS.

DOS maintains a Device Control Block: a list of tables, each 16 entries long. Indices into the tables are shown below. Note that the assignment of devices to indices may differ slightly from DOS to DOS. The ERRQ device is a pseudo-device. Writing to it will report errors, similarly to a call to API_QueueError. The MEM device is a pseudo-device for reading and writing to memory locations. It is not used in TOS, but was introduced in a later version.

dvLPT1		EQU	2
devERRQ		EQU	4	; check: error queue or prom programmer?
devPRM		EQU	4
dvUSR1		EQU	7
dvUSR2		EQU	8
dvUSR3		EQU	9
dvUSR4		EQU	10
dvUSR5		EQU	11
dvUSR6		EQU	12
dvUSR7		EQU	13
devMEM		EQU	14
dvFDSK		EQU	15

Most tables in the device control block contain byte-values, those that contain addresses are word-values (two bytes). The jump table JmpDevhandlerBXA and the table with device names are three and four bytes respectively.

210f DevGetIdx (byte): read pointer into the ring buffer 
211f DevPutIdx (byte): write pointer into the ring buffer
2346 DevBusy (byte): busy-state of the device
	0  device ready
	1  device busy
	2  device down
2356 DevStatus (byte): device state
	0  drive state unknown
	1  drive dsb set
	2  drive down
2366 DevBufIndex (byte): I/O buffer index
2376 DevBufCount (byte): I/O buffer count
2386 DevBufEchoCnt (byte): I/O buffer echo count
2396 DevFCB (byte): active index into the file control block
23a6 DevFnCode (byte): Supervisor function being handled (active)
23b6 DevBank (byte): bank switch
23c6 _DevIOAddr (word): I/O buffer address
23e6 DevRingbufAdr (word): starting address of the device's ring buffer
2406 DevID (byte): device ID. Is index+1, but -1 (0xff) indicates disk file.
2416 DevAvail (byte): device Availability. Bit mask:
       01  shareable
       80  down
2426 DevType: device type. Bit mask:
       01  can read
       02  can write
       10  sys abort
       40  binary (otherwise ascii)
2436 JmpDevhandlerBXA: Jumps to Device Handlers, 3 bytes each, unconditional branch instructions for use with the 2650 BXA instruction.
2466 Device Names, strings of 4 bytes each, not 0D terminated. Special devices start with 0x0f (just like special filenames do)
24a6 (end of the device control block)

Each device can have a small ring buffer. Ring buffers are used to stash a short amount of data. They can be at most 63 bytes long, but are often much shorter. The ring buffer for data received from the console, for example, is only two bytes deep. Ring buffers have a pointer for reading and a pointer for storing data. Both wrap around, to create a ring-like structure. When the pointers point to the same position the buffer is either completely full or completely empyty. The first byte of the ring buffer contains the buffer length in the lower six bits. Bits 80 and 40 are indicators: 80 indicates a buffer full condition, 40 indicates that the buffer is empty.


It is unclear how many open files DOS can handle. The internal file control block consists of tables that are 22 items long, but only the first 8 positions appear to be used. And some of the tables in the file control block are actually just 8 entries long. This is confusing.

2150 FileState (byte, length 22): state of the file
	0  free
	1  assigned
	2  open
	3  busy
	4  ready
	5  end of file/data
	6  abort
2166 FJCB Job
217c FileBank Bank
2192 FileChan Channel number
21a8 FileFuncCode SVC function code
21be FileDevStatus Physical device status
21d4 FileIOStatus I/O complete status: assume same values as SVCs
21ea FileIOCount I/O buffer byte count
2200 FileDskRWBuf I/O buffer addr
222c FileSRB SRB addr
2258 FileSVCRetAdr SVC ret addr
2284 FileRetAdr chan I/O complete ret addr
22b0 FileDskCmd command (same as Command for direct disk IO?)
22c6 FileDrive
22ce FileSector
22d6 FileTrack
22de FileIOBuf
22ee FileUnknown0 (byte)
22f6 FileCntToDo (indexed by the same value/register)
22fe FileUnknown1 (word)
230e FileUnknown2 (word)
231e FileUnknown3 (byte)
2326 FileUnknown5 (byte)
232e FileUnknown4 (byte)

Supervisor call functions

The Supervisor can handle 32 functions, but using modifiers the total number of functions is larger.

Function codes
  7 6 5 4 3 2 1 0
  | | | | | | | +-- c0
  | | | | | | +---- c1
  | | | | | +------ c2
  | | | | +-------- c3
  | | | |
  | | | +---------- c4
  | | +------------ 0/1 = Char/Block ; special assign
  | +-------------- 0/1 = Ascii Line/Binary
  +---------------- 0/1 = Proceed/Wait

srbFnRead	EQU	H'01'
srbFnWrite	EQU	H'02'
srbFnClose	EQU	H'03'
srbFnRewind	EQU	H'04'
srbFnDelete	EQU	H'05'
srbFnRename	EQU	H'06'
srbFnDirectIO	EQU	H'07'   for the Floppy Disk device
srbFnAssign	EQU	H'10'
srbFnTime	EQU	H'11'
srbFnOvlAddr	EQU	H'12'
srbFnParam	EQU	H'13'
srbFnDevType	EQU	H'14'
srbFnDevStatus	EQU	H'15'
srbFnLastChar	EQU	H'16'
srbFnLoadOvl	EQU	H'17'
srbFnExecOvl	EQU	H'18'
srbFnSuspend	EQU	H'19'
srbFnExit	EQU	H'1A'   to exit without closing open channels
srbFnPause	EQU	H'1B'
srbFnSlvParam	EQU	H'1C'
srbFnClkTime	EQU	H'1D'	only TOSv1
srbFnSlvMem	EQU	H'1D'	not in TOSv1
srbFnClkDate	EQU	H'1E'	only TOSv1
srbFnAbort	EQU	H'1F'   to exit cleanly

srbFnBlock	EQU	H'20'
srbFnSpecial	EQU	H'20'
srbFnBinary	EQU	H'40'
srbFnWait	EQU	H'80'

A normal read or write operation will handle a string terminated by Carriage Return. The actual number of characters read or written depends on the length of the string, which may be less than the requested number or size of the buffer. A binary read or write will act on the exact number of bytes requested.

Each call to the Supervisor requires a Service Request Block. The size of the block may differ.

Ofs   Dir Meaning
00     <  Function code (as above)
01     <  Channel number
02     >  Status code
03     >< Data
04     >< Count
05     <  Length
06 07  <  Buffer address
08 09  <  Return address
0a 0b  <  Address of direct I/O command bytes

When an SVC is invoked by the slave, only the first 8 bytes are used. When an SVC is invoked by a module or within DOS itself, the return address needs to be specified in the next two bytes. When the requested operation is finished execution will resume at this address. Only for direct I/O operations on the floppy disk controller (function code 7) the address of the four controller instructions needs to be specified.

The direction indicates whether the data is supplied by the caller for use by the Supervisor, or whether the Supervisor will supply the data when returning control to the caller. Some fields are bi-directional. See the Operator Manuals for details on SVC calls.

In practice you will see that modules use a single byte SRB for Exit, Pause or Abort functions. Sometimes a trick is employed whereby the address of the jump-to-Supervisor instruction is used as the SRB address. An unconditional branch instruction has 2650 opcode 1F, matching the code for the Abort function:

      lodi,r2  <Exit
      lodi,r3  >Exit
Exit  bcta,un  API_Supervisor

Because the continuation address is specified in the SRB, code using a sequence of Supervisor calls is quite difficult to read and understand.

I/O ports

DOS reserves a number of extended I/O ports for controlling the TWIN system itself. Most of the port addresses are hard-coded into the hardware modules. The GPIO General Purpose I/O module is the exception: it can be assigned to any free I/O port.

* Extended IO Ports
ioPort_HWAStatus	EQU	H'C0'
ioPort_SelectSlave1	EQU	H'E0'
ioPort_SelectSlave2	EQU	H'E1'
ioPort_SelectSlave3	EQU	H'E2'
ioPort_SelectSlave4	EQU	H'E3'
ioPort_ConsoleData	EQU	H'E8'
ioPort_ConsoleControl	EQU	H'E9'
ioPort_DiskLPTControl	EQU	H'EA'
ioPort_DiskLPTData	EQU	H'EB'
ioPort_IntervTimerControl EQU	H'EC'
ioPort_MasterMemProtect	EQU	H'ED'
ioPort_CommonMemBank	EQU	H'EE'
ioPort_SVC6		EQU	H'F2'
ioPort_SVC5		EQU	H'F3'
ioPort_SVC4		EQU	H'F4'
ioPort_SVC3		EQU	H'F5'
ioPort_SVC2		EQU	H'F6'
ioPort_SVC1		EQU	H'F7'
ioPort_DebugControl	EQU	H'F8'
ioPort_DebugCommand	EQU	H'F9'
ioPort_JumpAddrLo	EQU	H'FA'
ioPort_JumpAddrHi	EQU	H'FB'
ioPort_BPAddr1Lo	EQU	H'FC'
ioPort_PCLastLo		EQU	H'FC'
ioPort_BPAddr1Hi	EQU	H'FD'
ioPort_PCLastHi		EQU	H'FD'
ioPort_BPAddr2Lo	EQU	H'FE'
ioPort_PCNextLo		EQU	H'FE'
ioPort_BPAddr2Hi	EQU	H'FF'
ioPort_PCNextHi		EQU	H'FF'

Some ports can only be read while others can only be written to. For some ports the meaning is different for reading and writing. Ports FC/FD for example will write the address for Breapoint 1, but retrieve the Last Program Counter address when read. For several ports DOS keeps an internal copy of their current value, because that value can only be written and not read. For example, 2024 DebugCommand will always mirror the value of ioPort_DebugCommand.

The meaning of bits for each port is explained below. The strings in italics explain the meaning of the bits; character x indicates that that bit is not used.

C0 Harware analyzer control
01 access mode
02 trace mode
04 arm stop on address compare plus count
08 fetch don't care
10 M/IO don't care
20 M/IO polarity
40 R/W don't care
80 R/W polarity

E0-E3 Slave command byte 
03 memory protect base
	00 = base 0
	01 = base 16K
	10 = base 32K
	11 = base 48K
0c memory protect block size
	00 = no protect, 16K
	01 = not used [Slave block diagram says: 16 K protect, bit 14 override]
	10 = 32K (base 0 or base 32K only), bit 15 override
	11 = 16K
10 interrupt enable
60 user mode, ICE mode
	00 = mode 0, normal
	01 = mode 1, user I/O, system (slave) memory
	10 = special instr. only
	11 = mode 2, full user mode, user IO and memory
80 active/inactive

E9 Console Status (READ)
01 data available
02 transmit buffer empty
04 data overrun
08 framing error
10 parity error

E9 Console Control (WRITE)
01 paper tape reader on (one byte at a time)
02 parity select
04 enable TTY interrupts

EA Floppy/LPT status (READ)
01 printer busy =0
02 printer fault (power off, out of paper, disconnected) =0
80 flag =1 (controller has data or can accept data)

EA Floppy/LPT control (WRITE)
01 Strobe =0
06 C1 C0
10 enable disk interrupt
20 enable LPT interrupt

EB Disk status byte (READ after Start I/O, Reset or Printer strobe)
02 Unit check =1
04 Device not ready =1
08 Write protect =1
10 Error recovery =1
20 Illegal operation =1

F8 DebugControl (inverted: active=0)
01 bkpt 1 at read =0
02 bkpt 1 at write =0
04 bkpt 2 at read =0
08 bkpt 2 at write =0
10 tracing active, single cycle =0
20 debugging active / sequencer enable =0
C0 unused

Debug Command F9 (inverted: active=0)
01 forced reset =0 [FE]
02 forced interrupt (not used on 2650 slave) =0 [FD]
04 forced jump =0 [FB]
08 interrupt mask (tracing and breakpoints interrupts) =0 [F7]

xx+5 GPIO RS232 Status 2 (READ)
10 Ring indicator =1
20 Carrier detect =1
40 Data Set Ready DSR =1
80 Clear to Send CTS =1

xx+6 GPIO TTY/RS232 Status (READ)
01 data available
02 transmit buffer empty
04 data overrun
08 framing error
10 parity error

xx+6 GPIO TTY/RS232 Control (WRITE)
01 paper tape reader on (one byte at a time)
02 parity select (1=even, 0=odd)
04 enable TTY interrupts
08 local mode, RS232 only =1
10 originate mode, RS232 only 1=answer, 0=orig
20 send restraint, RS232 only =1
40 data terminal ready DTR, RS232 only =1
80 request to send RTS, RS232 only =1