There are two facets to the microcode, the code required to get the
assembler to do what we wanted, and the microcode itself. The assembler
supports constant definition, macro expansion (including nested macros),
and line labeling. Toward the beginning of the file, there are definitions
for the controller input and output data, as well as the DO
,
BR0
, and BR1
macros. The DO
and BR macros assemble into one byte, for the high or low
rom depending on the setting of the assembler constant HIGH_ROM.
The macros themselves are relatively unimportant, but do merit some
discussion.
DO
takes one argument, the thing to assert on the controller
outputs. It then XORs (with a "!") the argument with the constant
bitmask DO_CMD, which allows active low signals by setting the bit in the
bitmask corresponding to that signal. The argument actually passed to DO
is usually more than one signal, and this works by taking defined controller
output bitmasks (such as ADR_INC or RAM_CS\) and ORing them together
(with a ".") before passing them. This is very strange, but it works well
and looks good.
BR0
and BR1
are branch on 0 and 1 respectively.
They take two arguments
separated by a ";". The first argument is the controller input number to
branch on, always a defined constant (such as USR_REW or IFF_TRU). The
second argument is the assembler label to branch to. The macro constructs
the necessary byte by shifting and adding its arguments.
Finally, we wrote a macro called BEEP, which causes the machine to beep.
This made it much easier to beep at will, and flex the powerful muscles
of our beep generator, as it generated beeps in all its glorious splendor.
The microcode itself consisted of six (6) main blocks:
- Initialization
- Initialization in SPAM mark I simply set the message count to 0.
- Event Loop
- The event loop mainly dispatched control to handlers for various
input events. In addition, on a USR_RNG, the main loop plays an
outgoing message, checks USR_RNG again to see if the caller has hung up,
and then increments MSG_CNT, sets CUR_MSG, and passes control to the code
for recording a message.
- Record
- Record takes data from the A to D converter, and stores it in
the ram for the current message. It works by first settting the
address controller to the beginning of the current message. Then we
encounter for the first time our ram loop. The loop first waits for a
rising edge on CLK_SLO, which we call 'synchronizing' with CLK_SLO.
- This ensures each loop iteration will start at the beginning of a CLK_SLO
cycle, and so we will sample at 8KHz. We then copy data from the A to D
converter into the RAM, and increment the address counters. The loop
continues as long as ADR_DON is low, which will occur after two (2)
seconds have passed. Record has an additional entry point which sets
CUR_MSG to 0 before recording, in order to record an outgoing message.
- Play Outgoing Message
- This is virtually identical to record, except that in the ram loop,
data is copied from the ram to the D to A converter.
- Play
- The guts of play are similar to play OGM, except that branches are
included in the loop to check rewind and fast forward. The size of the
loop becomes critical here, as the controller's clock is only eight (8)
times faster than CLK_SLO, which means only eight instructions can be
executed in each iteration. If FF or RW are pressed, USR_FFW or USR_REW
will go high, and the loop will be interrupted to handle the input. On
a fast forward, we check that there are still more messages to be played.
If not, we exit the play loop. If so, we start playing the next message.
On a rewind, we play the previous message, or restart the first message.
This was (surprise, surprise) not what was expected, and was fixed in SPAM
mark II. When done playing a message, execution branches to FASTFWD,
where the current message is incremented, or we return to the event loop.
- Erase
- Erase simply sets the message count to zero (0).
Full microcode for SPAM mark I is in Appendix B.
Next: SPAM Mark II