Who? I am Michael Albaugh (formerly WB6SYQ, but I let that lapse and it was apparently reused for another radio amateur). I am a volunteer docent at the Computer History Museum. Among the associated benefits is the opportunity to work with the restoration crew for a pair of IBM 1401 computers, and to demonstrate those computers to the public. Paul Laughton (AC6B) is also a docent and 1401 demonstrator, and suggested a special demo to be given to the Palo Alto Amateur Radio Association when they visited.
What? The demo involved the IBM 1401 computer sending a greeting in Morse Code, by manipulating the RFI produced by its operation. It sounded like this.
You could also watch the video versions of the Demo and Intro
When? March 21, 2015.
Where? Mountain View California.
Why? For fun, and to demonstrate why things like TEMPEST exist. Paul knew that I was an ex-ham and had been messing around trying to implement an RF-based music program such as had been popular on the 1401 during its heyday. This seemed like a logical offshoot.
How? The 1401 has a memory cycle time of about 11 microseconds, so runs at about 90kHz. The seventh harmonic of that is about 630kHz, which is near the bottom of the AM broadcast band. A MLC (Move Left Characters) instruction uses 2 memory cycles per character moved, and produces a fairly steady pattern. A loop repeating such a move will produce a glitch corresponding to the much shorter bursts of memory access for instructions and counters to implement the loop. Those glitches will produce an audible tone. Similarly, a very long move, even in a loop, will produce a low frequency, possibly inaudible, at least in a noisy machine room.
Following is the code
that produced that demo. The structure of this code
is influenced by my desire to re-use cards from the
object decks of preceding versions, since I was assembling
each version then hand-punching the object code resulting from the
assembly. Because of that, I occasionally did "patches", where
I changed the source code such that only a small number of characters
in a given object card would change. Using the DUPlicate function of
the keypunch, I was able to change only those characters. Of course
the new data had to be the same length as the old. I'm using this
as a general purpose excuse for anything weird and otherwise inexplicable.
Above is pretty much boilerplate, and a bit anachronistic. The two
lines of numbers are something I include as a visual aid when typing
assembly code, as both supported assemblers (SPS and
AUTOCODER) have formatting
requirements1. In real life,
the column numbers would be printed on the coding sheets and I could
look at the column indicator of the keypunch.
JOB MORSE HELLO VIA RF FROM 1401
* 111111111122222222223333333333444444444455555555556666666666777
* 89012345678901234567890123456789012345678901234567890123456789012
* $Revision: 1.7 $
*
* ASSEMBLE WITH COMMAND
* AUTOCODER -L MHELLO.LIS -O MHELLO.CD -BV MHELLO.AC
CTL 6111
First off, we define some useful symbols. The Index Registers
of the 1401 are not actually registers, but dedicated storage locations
tucked at the top of the first 100 characters of memory. The
AUTOCODER assembler recognizes the sybols X1, X2, and X3
as indicating indexing in particular contexts, but if you refer to these
locations in another context, the symbols are not defined. You could
call them anything, but sanity tends to prevail.
You can probably figure out what ORG is. DS
is "Define Symbol", which defines a symbol to have the value of the
current location, and reserves some memory. EQU is
similar, but typically for constants, reserving no memory, and can be a
(limited form) expression.
* INDEX REGISTERS
ORG 87
X1 DS 3
ORG 92
X2 DS 3
ORG 97
X3 DS 3
The next two lines refer to the implicit use of some memory2
Comments start two spaces after the last character that could be a legal
part of an instruction or pseudo-op. Whole-line comments start with an
asterisk in column one, but you figured that out.
Note the '&' above. If I were using a Scientific print-chain,
that would render as a '+'. Similarly, the '@' characters below would be
apostrophes.3
* MISC DATA IN PUNCH BUFF. NOT PUNCHING
ORG 100
MSG EQU 99 COMPENSATE FOR PRE-INCREMENT
MSGNX EQU MSG&1 FOR SPACE LOOKAHEAD
Each characters is represented by a DC (Define Constant)
of a string enclosed
in '@' symbols, followed by a comment with its corresponding character, for
those innocent of Morse. The resulting string of '*', '-', and ' ' is read by
the code at CLOOP, which generates the appropriate sound (or silence).
* MESSAGE LIVES AT 100. MUST FIT BEFORE CODE AT 400
Note that the line above contains a typo. It was spotted by a reviewer
after I had run the code and made the recording, so I have left it
as is until I can fix it.
* CURRENTLY HELLO DE 1401 QSL?
DC @**** @ H
DC @* @ E
DC @*-** @ L
DC @*-** @ L
DC @--- @ O
DC @ @
DC @-** @ D
DC @* @ E
DC @ @
DC @** @ I
DC @-*** @ B
DC @-- @ M
DC @*---- @ 1
DC @****- @ 4
DC @----- @ 0
DC @*---- @ 1
DC @ @
DC @--*- @ Q
DC @*** @ S
DC @*-** @ L
DC @**--* @ ?
As the below says, Paul requested that the message start with two 'V'
characters, to make it easier to "Sync Up" with the sending rate.
Since the message loops, the end is as good a place as the beginning, and
less extra punching. The code that reads the message loops back to the
beginning if it sees any character other than '*', '-', or space.
Here's the basic principle. The thing about the existing music programs
is that they don't do rests. You can change the pitch, but you can't get
them to shut up. That's not good for Morse Code, so I tried a variety
of methods to make the desired sound (and lack thereof). You can see
that experiment in mmtest.html
DC @ @ TWO WORD SPACES BEFORE LOOPING
DC @***- ***- @ VV ADDED PER PAUL
DC @X@
The choice of a very long move for silence limits me to 1401s with
at least 12000 characters of memory.4
* BASIC TIMING IS BY LONG MOVES, OF MEMORY BASED AT MBASE
MBASE EQU 1000 MOVE BASE
TADDR EQU MBASE&54 54 CHAR MOVE FOR TONE
SADDR EQU MBASE&10435 10435 CHAR MOVE FOR SILENCE
*
You could chose to clear all of memory before loading code, or
not. WM stands for Word Mark. SBR
(Store B Register) is a sort
of Swiss Army Knife Instruction. In this case, it is used to
initialize the X1 index register to the effective address in
the second operand. Zero is a boring form of that.
ORG 400
* THE MESSAGE IS BAKED INTO THIS PROGRAM AT MSG
* ENCODED AS * FOR DIT, - FOR DAH, SPACE BETWEEN CHARACTERS.
* TWO SPACES FOR WORD-SPACE.
The major loop. MA (Modify Address) is a special form of
addition that operates on the strange form of addresses on systems with
more than 4K.5
BCE is Branch Character Equal. You can probably
figure out the rest, although I should point out that the first operand of
an instruction is the Source, and the second the Destination. Usually.
* SETUP ASSUMES CLEAR-STORAGE BOOTSTRAP, SETS WM
* FOR OVERLAPPING AREAS TO BE MOVED TO SELF FOR
* TONE/SILENCE
SETUP SW MBASE
SBR X1,0
There are no Immediates, so XINC has to be a constant in
memory.6
CLOOP dispatches to one of the three routines below. We decided on
5 WPM because it is no longer appropriate to assume that all hams
can copy Morse faster, or even at all.7
*
* CLOOP: LOOP ACROSS ALL CHARS OF MESSAGE, UNTIL WE
* SEE A CHARACTER WHICH IS NOT *, -, OR SPACE.
* RESET POINTER (IN X1) AT END, TO LOOP MESSAGE.
* START BY INCREMENTING POINTER. DEFINITION OF
* MSG IS TWEAKED TO MAKE THIS WORK, AND AVOID WRAP
CLOOP MA XINC,X1
BCE DIT,MSG&X1,*
BCE DAH,MSG&X1,-
BCE SPC,MSG&X1,
* IF NOT AN EXPECTED CHARACTER, RESET POINTER TO 1ST CHAR
SBR X1, 0
B CLOOP NO PAUSE BEFORE RESTART
Note that loop counts are done by adding until a counter overflows.
The BAV (Branch on Arithmetic Overflow) test is the
shortest/fastest
way to check for loop termination.8 We want to
minimize time spent
doing anything but the Move instruction, to keep the output as clean
as possible.
* ROUTINES FOR '*', '-', ' '
* DIT IS ONE ELEMENT OF TONE, GENERATED BY MID-LENGTH
* MOVE (MLC) PER STAN PADDOCK DESCRIPTION OF MUSIC CODE
*
* WITH A TARGET SPEED OF 5 WPM, A DIT IS 240 MILLISECONDS
* IF OUR BASE TONE IS ABOUT 800HZ, A CYCLE IS ABOUT 1.25
* MILLISECONDS, SO A DIT IS A BIT LESS THAN 200 CYCLES.
* A DAH IS 3 TIMES AS LONG. BOTH JUST SET ELTIM AND MERGE.
That slight click does exist, and could be really annoying if I wanted
to run on a small machine, using more moves of shorter length.
DIT MLC DITCT,ELTIM OVERFLOW AFTER ADDING 1 ELT WORTH
B TONE
DAH MLC DAHCT,ELTIM OVERFLOW AFTER ADDING 3 ELT WORTH
*
* TONE IS GENERATED BY A MOVE OF XXX CHARACTERS. EACH
* CHARACTER MOVED TAKES TWO MEMORY CYCLES, 23 MICROSECONDS
* SO A MOVE OF 108-ISH CHARACTERS WILL DO. SOME OVERHEAD
* FOR THE INSTRUCTIONS THEMSELVES WILL LOWER THE TONE, OR
* WE CAN TRIM THE MOVE.
TONE MLC TADDR,TADDR
A ELINC,ELTIM
BAV ESPC ON OVERFLOW, DO 1 ELEMENT SPACING
B TONE
*
* VARIOUS LENGTHS OF SILENCE BELOW, THEN BACK TO CLOOP
* 240 MILLISECONDS IS 240000 MICROSECONDS, OR ABOUT 20870
* CYCLES. SO A MOVE OF 10435 CHARACTERS IS ABOUT RIGHT
* THERE MAY BE A SLIGHT CLICK AT MOVE BOUNDARIES,
All over but the variables. Well, only ELTIM is an uninitialized variable.
(the #3 says to just reserve three characters) The rest are initialized
variables that are used as constants.
*
* WE GET TO SPC IF MSG CHARACTER IS SPACE. A SINGLE SPACE
* IS AN INTER-CHARACTER SPACE. SO WE NEED 3 ELEMENTS OF
* SILENCE, IF IT IS FOLLOWED BY A SPACE, WE NEED SEVEN
* TOTAL FOR A WORD SPACE.
* WE HAVE ALREADY DONE ONE VIA ESPC AFTER PREVIOUS ELEMENT,
* SO DO TWO OR SIX MORE,
SPC B WSPC,MSGNX&X1, 6 MORE IF NEXT CHAR IS SPACE
B CSPC 2 MORE FOR CHARACTER SPACE
WSPC MLC SADDR,SADDR WORD SPACE
MLC SADDR,SADDR
MLC SADDR,SADDR
MLC SADDR,SADDR
CSPC MLC SADDR,SADDR CHARACTER SPACE
ESPC MLC SADDR,SADDR ELEMENT SPACE
B CLOOP
ELTIM DCW #3
XINC DCW @001@
DITCT DCW @800@ OVERFLOW AFTER 200 (800HZ)
DAHCT DCW @400@ OVERFLOW AFTER 600 (800HZ)
ELINC DCW @1@ 1 for 5WPM, 2 FOR 10WPM
END SETUP
OBJECT DECK
Code can be loaded into the 1401 by resetting and pressing the
LOAD button. This causes a card to be read into
locations 1-80, wordmarks to be cleared from those locations,
a wordmark to be set at location 1, and control to be transfered to location 1.
You can see a clear division about midway through the card images
below. Essentially, the left hand side contains data and instructions
to be loaded, while the right had side contains the instructions to move
the left side into the appropriate address and set any needed wordmarks.
This is not strictly true of the first two cards, which do a bit of
housekeeping like clearing memory.
You may also notice that the last four columns contain a sequence number,
useful when you drop the deck, although unless you are doing stuff like
overlays, getting the first two and the last one card in the right order
is often sufficient.
FOOTNOTES
[1]
By convention, columns 1-5 are for
PGLIN, that is, the page and line number on the coding sheet.
In real life, I would have punched these, but I got lazy since they
are optional and I was really working with a text file, so there was
not real danger of dropping the source code and being unable to run
the deck through a sorter.
The actual source code occupies columns 6-72, while columns 73-80 are
often used (in an older convention) for similar sequencing purposes.
[2]
The 1401 uses three areas of memory for basic I/O. 1-80 are the
Card Read buffer, 101-180 are the Card Punch buffer, and 201-332 are the
Print Buffer. Those were the only I/O devices in the original design.
Later models added Tape, Disk, and a sort of generic I/O option. The
follow-on 1440 used that option for all I/O, but the same areas were typically
used by convention to ease portability. In this case, the Punch Buffer
is available for general use since we are not punching cards.
[3]
An accountant who dropped a full box of cards on his foot might
say "&#@$%" while an engineer might say "+='$(".
I originally thought I'd have this program read a card, translate it to
Morse Code, and send that. Then I looked at the size of the needed table,
the difficulty of doing a table-lookup on a character, and the fact that
I'd be punching all this by hand, and decided to "Bake the message in"
[4]
out of the available choices of 1400, 4000,
8000, 12000, or 16000. Since both the 1401s I have access to have 16K, I'll
live with that. This program also relies on having the Advanced
Programming option, which includes subroutines and index registers.
Yes, lots of stuff was optional, and 1401s were typically built to order.
One of the CHM machines has the Sterling Currency option, proving
that computing and LSD go way back.
[5]
Addresses on the 1401 are 3 character long decimal numbers. The normal
addition operation modifies the Zone Bits on the most significant
digit on the case of overflow, changing that digit to a letter or symbol.
For example, the highest address on the base (1400 character) model was
T99. When models larger than 4K were produced (requiring an external
cabinet for the other 1-3 4K stacks), the zone bits over the least significant
digit were used, but this was no longer a side effect of regular addition,
so the MA instruction was added. Indexing used the zone bits of the middle
digit.
[6]
The assembler provides Literals, which are anonymous initialized
variables (that should always be used as constants, but you know programmers)
whose address is filled in to the instruction using them. e.g.
,008015,022026,030040/019,001L020100 ,047054,061068,072072)08108110220001
,008047/047046 /000H025B022100 4/061046,054061,068072,00104010400002
**** * *-** *-** --- -** * ** -*** L037136)100100,040040,04004010400003
-- *---- ****- ----- *---- --*- *** L037173)137137,040040,04004010400004
*-** **--* ***- ***- X L027200)174174,040040,04004010400005
,|00H089000#557089B4530Z9*B4640Z9- L034433,404411,418426,04004010400006
B4940Z9 H089000B411M560554B471M563554 L037470,442449,453460,46404010400007
M|54|54A564554B541ZB471B5061|0 B534 L035505,478485,490494,50204010400008
MD3ND3NMD3ND3NMD3ND3NMD3ND3NMD3ND3N L035540,513520,527534,04004010400009
MD3ND3NB411 0018004001 L024564,548552,555558,56156410400010
/400080 0011
could have been written
CLOOP MA XINC,X1
but I like to name all but the most trivial constants. Besides, if I used
a literal, it could be Pooled with other uses so I couldn't patch it
without borking other uses.
CLOOP MA @001@,X1