The current version of the library also provides an abstract base class for Communications Register Unit (CRU) devices but doesn’t provide any other I/O.
Comming Real Soon Now are support for the TMS9901 parallel port, the TMS9902 serial port, a serial TTY (which defaults to std::cin and std::cout), and a parallel printer (which may be a serial device behind the scenes, and indeed defaults to std::cerr). |
No support for disk or tape drives is planned; but the author has a design in mind for “file system” and “file set” (collection of open files) pseudo-devices which occupy 32 bits of CRU address space each. |
Also planned for the future,
and occupying 32 bits of CRU address space, is a real-time clock that
provides C’s |
Users may provide additional I/O functionality (the default in the current version of the library is no output and reading all binary 1s); and they may also supply behavior for the five user-defined instructions (the default is to change the IDLE instruction into a proper halt) and trapping of unrecognized instructions (the default is to halt).
The library is usable on any C++ implementation that provides
Appendix A provides a brief overview of the TMS9900 architecture and instruction set; Appendix B describes library-supplied locking alternatives for those who wish to use the library for multi-threaded emulators that support interrupts.
All identifiers are declared in the tms9900 namespace directly or in other namespaces inside tms9900.
This open-source library is distributed under the Boost Software License (which isn’t viral like the GPL and others are believed to be). The distribution includes the files:
If you know that the emulator will run on a big-endian architecture, you can get the most efficient execution by compiling tms9900.cpp with the macro, TMS9900_EMULATOR_BIG_ENDIAN, defined. Similiarly, if you know that the emulator will run on a little-endian architecture, you can get the least inefficient execution by compiling tms9900.cpp with the macro, TMS9900_EMULATOR_LITTLE_ENDIAN, defined. If neither macro is defined, the library will still work correctly; but it will use a type-punning hack to determine the endianness at run time every time a 16-bit word is read from or written to the TMS9900 memory.
Values passed to, and returned from, library functions will always have the correct endianness for the emulator’s target platform; so aside from defining the macros above, the only interesting problem is the way 16-bit words are stored in memory when the emulator is running on a little-endian box. If you access bytes through ubyte*s, you’ll always see more significant bytes of 16-bit words at lower-numbered, even-numbered addresses regardless of the endianness of the target box; but if you use the library-supplied word_iterator to access 16-bit words in the TMS9900 memory, you’ll see the correct endianness for the emulator’s target platform.
The optional user-supplied functions run in the same thread, and so TMS9900 instruction
execution blocks until the functions return; thus it’s OK to touch the
registers or memory from within those functions. For example, the user might
want to do a DMA
transfer within some I/O instruction. This is OK. What would not be OK would be
to spawn another thread to do the DMA. (The author is thinking about how to
implement
An interrupt request would almost certainly come from a different thread, so we need a lock for requesting and servicing interrupts. The library provides three implementations described in Appendix B.
The closest it comes is the IDLE instruction which puts
This framework is capable of mimicking that behavior;
but since most users would probably want their emulator to halt some day,
the library-supplied default behavior is for IDLE, and any of the several
undefined opcodes as well, to just halt. The library also supplies a
Users who change the default behavior
by supplying their own
#define TMS9900_HPP_INCLUDED namespace tms9900 { typedef /* 8-bit 2’s-comp */ sbyte; typedef /* 8-bit unsigned */ ubyte; typedef /* 16-bit 2’s-comp */ sword; typedef /* 16-bit unsigned */ uword; uword& workspace_pointer(); uword& program_counter(); uword& status_register(); ubyte* memory_begin(); ubyte* memory_end(); class word_iterator; word_iterator memory_word_begin(); word_iterator memory_word_end(); void reset(); void request_interrupt(unsigned level); void start(); void load(); void force_halt(); bool exit_idle(); void reboot(); class cru_device { private: cru_device(const cru_device&); cru_device& operator=(const cru_device&); protected: cru_device(unsigned base_addr, unsigned size); public: virtual ~cru_device(); unsigned begin() const; unsigned end() const; unsigned size() const; virtual bool tb(uword bit_nbr) = 0; virtual void sb(uword bit_nbr, bool value) = 0; virtual uword stcr(uword relative_addr, unsigned bit_count) = 0; virtual void ldcr(uword relative_addr, unsigned bit_count, uword value) = 0; }; namespace user { enum high_addr_bits { IDLE = 2, RSET = 3, CKON = 5, CKOF = 6, LREX = 7 }; void user_defined_operation(high_addr_bits); void invalid_opcode(uword offending_instruction); } } // namespace tms9900
typedef /* 8-bit 2’s-comp */ sbyte; typedef /* 8-bit unsigned */ ubyte; typedef /* 16-bit 2’s-comp */ sword; typedef /* 16-bit unsigned */ uword;In order to model the TMS9900 memory both easily and correctly, the emulator requires integer types of exactly 8 and 16 bits; and the signed versions must have two’s-complement representations. The author doesn’t believe that that’s too Draconian a limitation since it’s exactly what seems to have won out in the architecture marketplace.
uword& workspace_pointer(); uword& program_counter(); uword& status_register();These three functions return non-const references to the CPU registers. The registers are stored internally with the correct endianness for the emulator’s target platform, so no byte swapping is necessary.
These functions are not thread-safe. If you call any of them while a TMS9900 program is running in a separate thread, you’ll get a data race.
ubyte* memory_begin(); ubyte* memory_end(); class word_iterator; word_iterator memory_word_begin(); word_iterator memory_word_end();These functions return non-const iterators to the beginning, and one past the end, of the TMS9900 memory. They can be passed to Standard-Library algorithms for initializing or otherwise directly accessing the emulated memory.
A word_iterator is a random-access iterator. Objects of its reference type adjust the endianness on a little-endian box when dereferenced.
A word_iterator is explicitly constructible from a ubyte*; and a ubyte* may be assigned to it. Such a ubyte* is required to represent an even-numbered address; and a debug build will assert if it doesn’t. A release build will quietly mask off the LSB.
The emulated memory is not thread-safe. If you try to access it while a TMS9900 program is running in a separate thread, you’ll get a data race.
void request_interrupt(unsigned level); inline void reset() { request_interrupt(0U); }
void start(); void load();The
The
The emulated memory and CPU registers are not thread-safe. If you you call one of these functions in one thread and then try to access the internals in another, you’ll get a data race.
void force_halt();which may be called from another thread, will cause instruction execution to halt if the emulator is stuck in an endless IDLE loop because there’s no interrupt of sufficiently high priority pending.
class cru_device { protected: cru_device(unsigned base_addr, unsigned size); public: virtual ~cru_device(); unsigned begin() const; unsigned end() const; unsigned size() const; virtual bool tb(uword bit_nbr) = 0; virtual void sb(uword bit_nbr, bool value) = 0; virtual uword stcr(uword relative_addr, unsigned bit_count) = 0; virtual void ldcr(uword relative_addr, unsigned bit_count, uword value) = 0; };This abstract base class is provided to support the Communications Register Unit (CRU).
There is no default constructor, and the copy constructor and copy-assignment operator are private and undefined, thus instances of derived classes are expected to be singletons.
The protected constructor takes two arguments: the CRU address that corresponds to the device’s bit 0, and the number of CRU bits that the device uses. The constructor will install *this at the appropriate place in the CRU address space; and the destructor will correctly remove the device.
Three non-virtual member functions are provided for determining what CRU bits the device occupies. They’re intended as helpers for the CRU instructions themselves; but calling them in user code can do no harm.
The four pure-virtual member functions are the ones that do the deed. In all cases, the bit_nbr and relative_addr arguments are relative to the device’s base CRU address, and so will always have values in the half-open interval, [0, size()).
The TB instruction calls tb() and uses the returned value to set or clear the “equal” bit in the status register.
The SBO and SBZ instructions call sb() passing true (SBO) or false (SBZ) as the second argument.
As expected, the STCR instruction calls Instruction execution blocks until the four pure-virtual functions return;
so accessing the CPU registers or the TMS9900 memory from within these functions
cannot, by itself, yield a data race.
It’s not an error to execute a CRU instruction for a CRU address
at which no device is installed. The instructions will set and clear
status register bits as appropriate but will perform no other operation. In such
cases, the TB and STCR instructions will behave as if
Users may provide their own definitions, although default definitions
are included in separate source files distributed with the library.
Instruction execution blocks until this function returns; so accessing
the CPU registers or the TMS9900 memory from within this function cannot,
by itself, yield a data race.
The RSET instruction clears the interrupt mask after calling
this function, so the function can’t change that behavior; but it
can test the status register for the value of the interrupt mask
when the function was called.
The file, tms9900_default_userdef.cpp, provides a default
implementation for users who don’t need to recognize these instructions.
If the argument is user::IDLE, it does
a proper halt; otherwise, it performs no operation.
If the user provides a definition of this function,
in order to correctly implement the IDLE instruction, the
function should sit in a loop waiting for
The library also provides
Instruction execution blocks until this function returns; so accessing
the CPU registers or the TMS9900 memory from within this function cannot,
by itself, yield a data race.
The file, tms9900_default_badop.cpp, provides a default implementation
that just quietly halts instruction execution.
The three actual CPU registers are:
Some general-purpose registers have special properties or uses:
There is also a The way the CRU works is to set the TMS9900’s address pins
Single-bit CRU instructions add a signed offset to the base address in R12;
multiple-bit CRU instructions increment A3–A14 for each bit.
The source addressing modes are the same as above.
With the possible exception of LDCR and STCR, all are
word-oriented instructions, so the source register is incremented by two
when using register indirect with auto-increment (addressing mode 3).
XOP is a mechanism for calling one of up to 16
particular subroutines passing a memory address in R11. These subroutines,
presumably part of the operating system, would probably do things like read and
write characters and strings to/from the TTY or provide other operating-system
services.
The new WP and PC for XOP-called routines are stored in
pairs of words, WP first, beginning at absolute address 4016;
and the XOP’s “destination” (the value 0 to 15,
not the contents of a register) For LDCR, which does output, and STCR, which does input,
the instruction’s “destination” field holds a bit count. If the value is 0, the bit
count is 16. If the bit count is 1 through 8, this is a
byte-oriented instruction; otherwise, it’s a
word-oriented instruction. The base CRU address
is in bits The addressing modes are the same as above.
All are word-oriented instructions, so the register is incremented by two
when using register indirect with auto-increment (addressing mode 3).
BL and BLWP are the two ways of calling user-defined subroutines.
BL just saves PC in R11.
Return BLWP is used when you want a “context switch,” that is,
a whole new set of general-purpose registers. It gets the new WP and
PC from the two words beginning at the computed operand address, WP first,
and then saves the old WP, PC and ST in the new
registers 13, 14 and 15, respectively. As with XOP,
return with the RTWP instruction.
The jump instructions can branch within a range of +127 to –128 words
(not bytes) from the current PC (which points to the word following
the instruction while the instruction is executing). Instructions whose names
contain “high” and “low” test ST0 (unsigned greater than);
instructions whose names contain “greater” and “less” test
ST1 (signed greater than).
SBO, SBZ and TB
are the single-bit CRU instructions.
The CRU address is the base address in bits 3–14 of R12 plus the
signed displacement in the instruction. TB’s input is into
ST2, the “equal” status bit.
SLA is the only shift instruction that can affect ST4 (overflow).
All can affect If the bit count field of the instruction is 0, the bit count is the four LSBs of R0.
If the four LSBs of R0 are also 0, the bit count is 16. (The joy of shifting a
RTWP returns from an interrupt or a subroutine called by
BLWP or XOP.
It restores WP, PC and ST from registers 13, 14 and 15,
respectively.
IDLE, RSET, CKON, CKOF and LREX
all put bits 8 to 10 of the instruction (the three LSBs of the opcode) onto
the chip’s three high address pins and pulse the CRUCLK pin.
Presumably, there exists some hardware that will behave appropriately in response.
(Note that, despite pulsing CRUCLK, this is not actually a
CRU operation because RSET has the additional behavior of clearing the interrupt mask.
IDLE has the additional behavior of repeatedly pulsing CRUCLK
while waiting for an interrupt, which is as close as we get
to a halt instruction. This lets us optimize the
operating system’s idle
tms9900_locks.hpp declares The library doesn’t provide any kind of try-lock because it’s not clear
what to do when the lock fails.
tms9900_lock_win32_spin.cpp provides a
spinlock that uses Windows’
tms9900_lock_win32_cs.cpp provides a lock that uses a Windows
CRITICAL_SECTION for use where spinlocks won’t do.
User-Supplied Functions:
The following are declared in the user namespace
inside the tms9900 namespace.
User-defined instructions:
enum high_addr_bits { IDLE = 2, RSET = 3, CKON = 5, CKOF = 6, LREX = 7 };
void user_defined_operation(high_addr_bits);
This function is called when any of the IDLE, RSET,
CKON, CKOF and LREX instructions is executed.
The argument is the
Invalid opcodes:
void invalid_opcode(uword offending_instruction);
This function is called when an unrecognized instruction appears in the
instruction stream. The argument is the 16-bit instruction word. The
argument’s endianness is correct for the emulator’s target
platform, so no byte swapping is needed.
Testing:
A unit test is comming Real Soon Now.
Appendix A, The TMS9900 Registers and Instruction Set:
This is just a brief overview. For more detailed information, see the
TMS 9900
Microprocessor Data Manual.
Registers
The TMS9900 has 16 general-purpose “registers” which, in a fit of
premature optimization
that leaves one absolutely speechless, are implemented in memory so that
different parts of a program can use different sets of registers. In principle,
this allows really fast context switches; in practice, it yields really slow registers.
(Wikipedia’s TMS9900 article
claims that this was a rational decision because, at the time,
RAM was as fast as, or faster than, the CPU. That’s not
the way I remember it; but maybe I remember incorrectly.)
Status register bits (a subset of the status bits on the TI-990 minicomputer):
WP Workspace Pointer
The address of the current general-purpose register 0
PC Program Counter
The address of the next instruction
ST Status Register
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
L>
A>
=
C
O
P
X
Interrupt Mask
(Note that a
Bit(s) Use
0
“Logical” (unsigned) greater than
1
“Arithmetic” (signed) greater than
2
Equal
3
Carry
4
Overflow
5
Odd parity (byte-oriented instructions only)
6
An XOP instruction just got executed
7–11
Don’t care when setting, always read as 0
12–15
The lowest-priority (highest-numbered) interrupt
that may interrupt the processor
Reg. Use
0
Cannot be used as index register
Bit count for shift instructions
11
Return address after BL instruction
Address passed by XOP instruction
12
CRU address
13
Previous
WP
after “context switch”
(interrupt, BLWP, XOP)
14
PC
15
ST
Memory Organization
Memory is byte-addressible. Sixteen-bit words are stored in
Addresses
Reserved for
0000 - 003F
Interrupt vectors
0040 - 007F
XOP instruction vectors
FFFC - FFFF
LOAD vector
Interrupts
There are 16 vectored interrupts with the new WP and PC
values stored, WP first, beginning at absolute address 0.
Interrupt n is effectively a
Communications Register Unit (CRU)
The TMS9900 implements I/O as 4096 individually addressible bits;
and all multiple-bit I/O is serial (unless a peripheral does a
DMA).
The Dual Operand Instructions with Multiple Addressing Modes for Source and Destination
Instruction Format
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Opcode
Destination
Source
Immediate Address?
Immediate Address?
Instructions Opcode Mnemonic Operation Notes
1010
A
Add
1011
AB
Add byte
1000
C
Compare
1001
CB
Compare byte
0110
S
Subtract
0111
SB
Subtract byte
1110
SOC
Set ones corresponding
bitwise OR
1111
SOCB
Set ones corresponding byte
0100
SZC
Set zeros corresponding
bitwise AND with
complement of source
0101
SZCB
Set zeros corresponding byte
1100
MOV
Move
1101
MOVB
Move byte
6-Bit Multi-Mode Addresses
2-Bit
Mode4-Bit
RegisterEffect
Notes
0
any
register
1
register indirect
2
0
absolute
The word following the instruction contains the base address.
If both operands use absolute or indexed addressing,
the source address comes first.
1–15
indexed
3
any
register indirect with post-increment
Increments by one for byte instructions, by two for word instructions.
Instruction Format
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
001
Opcode
Destination
Source
Immediate Address?
Instructions Opcode Mnemonic Operation Notes
000
COC
Compare ones corresponding
For each 1 bit in the source, set ST bit 2 if the
corresponding bit in the destination is
001
CZC
Compare zeros corresponding
010
XOR
Bitwise exclusive OR
110
MPY
Multiply
The 32-bit product or dividend is in Rdest and
big-endian. If Rdest is 15,
not R0. For DIV, quotient->Rdest, remainder->Rdest+1.
111
DIV
Divide
011
XOP
Extended operation
100
LDCR
Load CRU
Write
101
STCR
Store CRU
Read
Single Operand Instructions with Multiple Addressing Modes
Instruction Format
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0000 01
Opcode
Operand
Immediate Address?
Immediate Address or Operand for X Instruction?
Instructions Opcode Mnemonic Operation Notes
0001
B
Branch
Load the operand address into PC
1010
BL
Branch and link
Save PC in R11, then load the operand address into PC
0000
BLWP
Branch and load workspace pointer
Call subroutine with “context switch” (new WP)
0011
CLR
Clear
0 -> operand
1100
SETO
Set to ones
FFFF16 -> operand
0101
INV
Invert
One’s complement
0100
NEG
Negate
Two’s complement
1101
ABS
Absolute value
1011
SWPB
Swap bytes
0110
INC
Increment
0111
INCT
Increment by two
1000
DEC
Decrement
1001
DECT
Decrement by two
0010
X
Execute
Execute the instruction at the operand address.
If the instruction requires an immediate address or other immediate operand,
it will be taken from the word following this X instruction,
not the word following the executed instruction.
Instruction Format
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0001
Opcode
Signed Displacement
Instructions Opcode Mnemonic Operation
0011
JEQ
Jump equal
0101
JGT
Jump greater than
1011
JH
Jump high
0100
JHE
Jump high or equal
1010
JL
Jump low
0010
JLE
Jump low or equal
0001
JLT
Jump less than
0000
JMP
Jump unconditionally
0111
JNC
Jump no carry
0110
JNE
Jump not equal
1001
JNO
Jump no overflow
1000
JOC
Jump on carry
1100
JOP
Jump odd parity
1101
SBO
Set bit to one
1110
SBZ
Set bit to zero
1111
TB
Test bit
Shift Instructions
Instruction Format
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0000 1
Opcode
Bit Count
Register
Instructions Opcode Mnemonic Operation Notes
010
SLA
Shift left arithmetic
LSB <- 0
000
SRA
Shift right arithmetic
old MSB -> MSB
011
SRC
Shift right circular
old LSB -> MSB
001
SRL
Shift right logical
0 -> MSB
Other Instructions
Instruction Format
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0000 001
Opcode
?
Register?
Immediate Operand?
Bit 11 is always don’t-care.
Bits 12–15 are don’t-care if the register field isn’t used.
Instructions
Opcode
Mnemonic
Operation
Uses
Register
FieldUses
Immediate
Operand
0001
AI
Add immediate
Yes
0010
ANDI
AND immediate
0100
CI
Compare immediate
0000
LI
Load immediate
0011
ORI
OR immediate
0111
LWPI
Load workspace pointer immediate
No
Yes
1000
LIMI
Load interrupt mask immediate
0110
STST
Store status
Yes
No
0101
STWP
Store workspace pointer
1100
RTWP
Return workspace pointer
No
1010
IDLE
Idle
1011
RSET
Reset
1110
CKOF
[externally defined]
1101
CKON
1111
LREX
I’ve heard that “CKON”, “CKOF” and
“LREX” were mnemonic for things that happened on the
loop 0300 LIMI 15
000F
0340 IDLE
10FC JMP loop
Undefined Instructions
The TMS9900 microcomputer had a subset of the TI-990 minicomputer instruction set.
On the TMS9900:
Appendix B, Notes on Interrupt Concurrency:
Requesting and servicing of interrupts will likely happen in different threads,
so we’ll need some kind of mutex to control access to any data structures
that those two operations have in common. The library provides three kinds of locks;
and users may choose one of those or write their own.
POSIX
For use in a POSIX environment,
Microsoft Windows
Two locks are provided for users of Wintel boxes.
All corrections and suggestions will be welcome, all flames will be amusing.
Mail to was at pobox dot com.