Title: Altair Networking Environment
Date: January 06, 2019
Tags: altair programming
========================================
After getting the WiFi modem working, the next step was to write a more useful
client for using it. I wanted to remove the need to send commands to the modem
directly and allow for some processing of data coming back. I ended up with a
lot more.
The basic idea of this program was to abstract the modem away from the user. I
wanted a simple interface to connect to a host using a specific protocol and
allow for processing and formatting of output. The scope both shrank
considerably and grew massively.
What I ended up with is a very basic modem use-case. I only implemented a
telnet mode because the telnet client is built into the modem firmware. The
program only has to connect, disconnect, and pass input and output from terminal
to modem like the simple modem program used previously[0]. There is no need to
parse any server output or format anything. There are the remnants of my plans
to implement a gopher client which I want to add next. There is also a "raw"
mode which is functionally the same as the simple program from the previous
modem article and is basically the same thing as the terminal client but without
managing the connection.
A not too difficult program turned very complex because supporting multiple
clients and parsing commands has several possible implementations and user
interactivity. I alternated between specifically optimized implementations and
generalized implementations. The program is also highlighting the limitations
of hand assembling and using the switches for programming. But at the same
time, it was also very revealing about some features of assemblers, compilers,
and command interpreters or shells.
Supporting at least telnet and raw clients necessitated a framework supporting
multiple commands. In the days of BASIC environments, using floppy disks or
cassettes, you could load and execute multiple programs. The environment had to
write that program into memory then begin executing it and provide a way to
abort the program and return to the environment. I simulate this feature by
creating a "command table" listing available commands and the address to jump to
in order to begin their execution. This allows for dynamic loading of commands,
or programs, instead of hard coded compares and jumps. I can also add gopher
later without modifying the command processing code. For simplicity, I only
support single character commands.
If you consider more modern shell environments, you can pass parameters on the
command line when executing the command. The executed command will parse and
check that it received the parameters it requires. My implementation of a
parameter parser ended up somewhere in the middle of generalized and specific to
my needs here. It's written to be called like a library function by any client
(think: a proto-getopt). The subroutine does not know what the parameters are
expected to be or how many there are. It is up to the client to call the
subroutine as many times as needed to get the number of parameters it expects.
Since I am only working with networking clients, I only need a hostname to
connect to and a port. In that order. The port can be optional, which is easy
as the last parameter, since the client will support a specific protocol and can
default to that protocol's standard port.
My original implementation of the parser used hard coded memory locations to
copy each parameter into so the client could then read that location to get the
parameter. That, however, meant a lot of memory planning. I needed to find
space in memory to store strings of unknown length and in the general case, an
unknown number of them. I don't know an efficient way to do this. Also, since
the parameters were entered at the commandline already, I was duplicating
existing data which seemed silly. I needed a way to dynamically find the
parameter in the existing commandline string and just return that. Since the
commandline is a single string and each parameter needs to be a null terminated
string on its own, I still needed to traverse the string and terminate it.
Since parameters are going to be space separated, that leaves a convenient spot
for the null. The address of the start of the parameter is returned in the DE
register. I have the client push the address to the stack for storage to allow
subsequent calls to the parser until the end of the command line is found. The
client can then pull the addresses out of the stack and read the values as
whatever it expects. So ignoring the value of the string, return a 0 if we
found at least one character or a 1 if not to indicate the end of the command
line. The client can decide what to do based on the return value. I am copying
the UNIX and C-like implementation that a 0 indicates success instead of a 1.
Why? There is a command to jump if you have a 0 in the zero flag bit. There is
no command to jump if you get a 1. I could jump if not 0, but if I want to
introduce error codes later, I can jump on 0 to skip any compares that determine
what non-zero value was returned. Just like having the stack grow downwards,
returning a 0 on success was pretty much predestined by the hardware.
Instead of tossing the whole program up then walking through it, let's go
through the new pieces and the parts that taught a lesson. I'll also skip over
describing the stuff we've seen in the previous echo and modem programs.
## Command Entry and Execution ##
Command entry is string based like the string echo[1] program. Input is stored
to memory and when 'enter' is sent, we jump to process what we got.
# Process command #
; process command
; Assumes single char commands, looks for char in command table
; goes to address to execute command. A null terminates the table
; and means the command wasn't found.
343 LDA 072 ; command buffer
344 120Q 120
345 002Q 002
346 CPI 376 ; Check if empty string
347 000Q 000
350 JZ 312 ; goto command entry
351 213Q 213
352 000Q 000
353 MVI M 066 ; Terminate string
354 000Q 000
355 LXI H 041 ; EOL string
356 010Q 010
357 002Q 002
360 CALL 315 ; write string to terminal
361 300Q 300
362 000Q 000
363 LXI H 041 ; Set pointer to start of buffer
364 120Q 120
365 002Q 002
366 MOV A,M 176 ; Get first character
367 INX H 043 ; Go past command character
370 MOV B,A 107 ; Save command char to B
371 LXI H 041 ; load command table
372 103Q 103
373 002Q 002
374 CMP M 276 ; If table char == command char
375 JZ 312 ; goto found
376 015Q 015
377 001Q 001
400 MVI A 076 ; Check for end of table
401 000Q 000
402 CMP M 276 ; If table entry == null
403 JZ 312 ; goto command not found
404 023Q 023
405 001Q 001
406 MOV A,B 170 ; Copy command back from B
407 INX H 043 ; move to next table entry
410 INX H 043
411 INX H 043
412 JMP 303 ; goto next command char
413 374Q 374
414 000Q 000
; command found
415 INX H 043 ; Goto low address byte
416 MOV E,M 136 ; Store in E
417 INX H 043 ; Get high address byte
420 MOV D,M 126 ; Store in D
421 XCHG 353 ; Swap DE into HL
422 PCHL 351 ; Execute command at HL address
; command not found
423 LXI H 041 ; Set HL to error
424 013Q 013
425 002Q 002
426 CALL 315 ; Invalid command
427 300Q 300 ; write string to terminal
430 000Q 000
431 JMP 303 ; Get a new command
432 206Q 206 ; goto command init
433 000Q 000
; command table
1103 t 164Q 164 ; telnet
1104 073Q 073
1105 001Q 001
1106 ! 041Q 041 ; raw
1107 250Q 250
1110 001Q 001
1111 \0 000Q 000 ; null at end of table
Initially, command parsing was hard coded to check for each command character
and jumped accordingly but I quickly realized that adding new commands would be
difficult as it would shift all the code. I decided to put each command into a
table with it's subroutine address. With this, I can add any number of commands
and the same command parser would work. Turns out, this solution is similar to
what CP/M used for calling OS sub-routines (like modern day syscalls) and I'll
explore more about that in the future.
I took some shortcuts since command parsing isn't really what I was trying to
accomplish. I only allow single character commands. Makes it easy to compare
and jump and be on to executing fun things. However, I took the long cut of
allowing parameters to be passed with the command. So, targeting a telnet
client, the user can enter 't hostname port' with port being optional and
defaulting to the telnet port 23. Since telnet always needs a host to connect
to, might as well just enter it there. The same will be true for gopher and any
other network client I might write and add here so it's a useful feature.
All this code does is start at the beginning of the command buffer, read the
character and compare it to the command table. If there is no match, skip ahead
in the table past the sub-routine address to the next command character and
compare again. The table is null terminated to make it easy to know when we've
checked all known commands and can print an error.
If the command is found, read the address into DE, so HL can still be used to
point to the table, then swap DE into HL. Then set the program counter to the
address in HL which causes execution to continue at that addess.
If the command is not found, print an error and reset the command buffer and go
back for another one.
# Parse parameters #
; parse parameters
; expects pointer to args in HL
; returns pointer to terminated param in DE
; and return value in B
434 MVI B 006 ; Preload return with 1
435 001Q 001
436 MOV A,M 176 ; Read until first arg
437 INX H 043
440 CPI 376 ; If space
441 040Q 040
442 JZ 312 ; read next letter
443 036Q 036
444 001Q 001
445 DCX H 053 ; Go back to first non-space character
446 MOV D,H 124 ; Copy param pointer
447 MOV E,L 135 ; to DE
450 MOV A,M 176 ; Get character
451 CPI 376 ; If null
452 000Q 000
453 RZ 310 ; return
454 CPI 376 ; If space
455 040Q 040
456 JZ 312 ; goto done
457 067Q 067
460 001Q 001
461 MVI B 006 ; Found a char
462 000Q 000 ; Set return to 0
463 INX H 043 ; Increment args
464 JMP 303 ; Jump to get character
465 050Q 050
466 001Q 001
; done
467 MVI M 066 ; Terminate parameter string
470 000Q 000
471 INX H 043 ; Move past \0
472 RET 311
; telnet
473 LXI H 041 ; Load command buffer
474 120Q 120
475 002Q 002
476 INX H 043 ; move past command char
477 CALL 315 ; Call parse parameters
500 034Q 034
501 001Q 001
502 MOV A,B 170 ; Get return value
503 CPI 376 ; Check if return is 1
504 001Q 001
505 JZ 312 ; Yes, no hostname
506 207Q 207 ; goto telnet error
507 001Q 001
510 PUSH D 325 ; Save hostname address to stack
511 CALL 315 ; call parse params again
512 034Q 034
513 001Q 001
514 MOV A,B 170 ; Get return value
515 CPI 376 ; Check if return is 1
516 001Q 001
517 JNZ 302 ; No, goto connect
520 134Q 134
521 001Q 001
522 INX H 043 ; Skip over null
523 MVI M 066 ; Set port to 23
524 062Q 062
525 INX H 043
526 MVI M 066
527 063Q 063
530 INX H 043
531 MVI M 066 ; and terminate it
532 000Q 000
533 INX D 023 ; Increment param pointer in DE
; connect
534 PUSH D 325 ; Push port address to stack
..
Here is a snippet of the telnet program which uses parse parameters. More
shortcuts. Telnet knows it needs a hostname and that the port can be optional.
Think of it as telnet working with something like the ARGV variable we have in C
today. It starts with the command itself and the parameters follow. So telnet
starts at the beginning of the command buffer and skips ahead a character past
the command itself. The parse parameters sub-routine expects HL to be the
pointer to the current location in the command buffer so subsequent calls will
continue on to the next parameter until the end of the buffer. It returns the
start of the parameter value in DE and a 0 if a value was found or a 1 if not.
Parse parameters starts by setting the return value to 1 in case nothing is
found. Then we grab characters and loop over spaces until a non-space
character. The side effect here is that we don't need a space after the
command, but we need at least one space between parameters. Once we find a
non-space character, move the pointer back to set up for the next read loop.
That means we will read this character twice. I should try to optimize that.
That also puts the pointer in HL at the beginning of the parameter string so we
can save it to DE. Read the character, if we have a null, we're done. If not,
check again for a space, if not we have a parameter value so set the return
value to 0. Check the next characters until space or null. If space, write a
null over the space to terminate the parameter value string and increment the HL
pointer past it. Then return to the caller which might call back for another
parameter.
This process allows a program to call parse parameters until there is a return
value of 0. The program keeps track of the number of parameters it needs and if
it got enough of them. Telnet, in this example, pushes each parameter pointer
to the stack which allows for an arbitrary number of parameters to be found. We
know telnet only needs one parameter and can fill in a default for the second so
we only use a return of 0 to indicate the optional port parameter wasn't passed
and we need to supply a default. That is done by writing it to the command
buffer as if it had been entered by the user and since parse parameters saved
the address it thought might be a parameter value in DE we can use it by
incrementing and pushing it to the stack. If there are more parameters after a
port, they just get ignored.
I initially had parse parameters read from the command buffer and copy values
into new locations but it occurred to me that if the values were already in
memory, as entered by the user, why not just use that? Also managing memory
manually is hard. Where would I put those values? What if there are a lot of
them? We'll dig into more dynamic memory management some day, I hope.
## Memory management ##
Speaking of managing memory, enough iterations happened while writing this
program that I had to map out where things were so when they changed, I would
know what to update. When manually assembling, you also need to manually keep
tack of where everything will be in memory.
000 start up
025 delete
070 interrupt handler
200 reset
206 command init
213 command entry
265 write char to terminal
300 write string to terminal
314 write char to modem
327 write string to modem
343 process command
434 (001 034) parse parameters
467 (001 067) done
473 (001 073) telnet
534 (001 134) connect
607 (001 207) telnet error
650 (001 250) raw mode
675 (001 275) raw terminal handler
720 (001 320) raw modem handler
736 (001 336) abort connection
(754 end of program)
static data
1000 (002 000) hostname error
1010 EOL
1013 invalid command
1035 telnet command prefix
1043 telnet command suffix
1046 gopher command prefix
1053 gopher command suffix
1056 modem echo off
1064 modem echo on
1072 modem disconnect
1077 delete string
(1102 end of static data)
command table
1103 (002 103)
(1111 end of table)
dynamic data
1120 (002 120) command buffer
Each sub-routine's starting address is listed as well as any other jump points
into it. Also all the static data: strings and the command table, and dynamic
storage: in this case, just the command buffer. An assembler will allow you to
create data statements to store numbers and strings without having to worry
about their location.
I also got to learn first hand why the world switched from octal to hexadecimal.
I already knew this, but now I've lived it. As you can see, I have
parentheticals to show the separate high and low bytes in octal. This
illustrates the problem with octal. When you separate the bytes, the values
change. So an address of 434Q, when stored as 2 8 bit bytes becomes 001Q 034Q.
The same address in hexadecimal would be 0x011C which separates out to 0x01 and
0x1C. Much easier to deal with and why hexadecimal as adopted over octal.
This also highlights how bad life can be without an assembler. Every tweak and
change to code changes the memory values after it and all references need to be
updated. An assembler takes care of that for you and allows you to use labels
to reference specific addresses without needing to know the specific address.
## Conclusions ##
The lesson learned from all of this was not how to write an interface for using
the WiFi modem but rather how the development workflow needed to evolve very
quickly to allow more complex programming. This is not a particularly long
program (though the longest I have written for the Altair) but I think I have
already reached the limit of hand assembly as well as manual entry.
The solution to the second problem is the next step in the history of home
computing. As teletypes and terminals became more available, more efficient
program entry was possible. Monitor programs, which were stored in non-volatile
PROM chips allowed interaction with the system using a teletype or terminal and
also storage of boot loaders for other systems like BASIC.
I want to write a custom monitor that will allow me to leverage my modern
"terminal" (a computer with a terminal emulator) to write programs to the Altair
and read them back to store on the computer.
From there we can add assembly-like functionality to make programming easier and
more easily improve our command environment that's been started here.
--------------------------------------------------------------------------------
I'm not going to get into the telnet program here, this article is long enough
and there isn't much more of interest to it. It has some missing features and
it's just been too difficult to work on to continue as is. I already cheated
and wrote it in a text editor on my computer. I knew there would be too much
iteration to do on paper. I'll revisit and share it when we have a better
development process.
[0]
https://blog.kagu-tsuchi.com/articles/altair_on_wifi.html
[1]
https://blog.kagu-tsuchi.com/articles/8080_IO_string_echo.html