Contents


? OCTAL DEFINE BRAIN
> #1 25 +DO #1 117 +DO RND IF<0 57 ELSE 134 ENDIF PCHR +LOOP CRLF +LOOP END
? BRAIN
//\///\/\/\\//\\\/\\/\\/\\\//\\/\/\/\\///\//\\//\///\\\/\\\/\/\\\\/\\/\\\\////\
\\\\/\\//\\//\\\/\\\\\//\\//////\//\\\\/\\//\\\/\\//\\\//\\\\/\/\//\\\\/\\\/\\\
\/\\\/\\\/\\///\///\\///\\//\//////\//\////\/////\\////\\/\/\/\/\////\/\////\/\
//\////\///\\/\\//\\\\//////\/\\/\/\\/\\\\/\/\/\\\///\\/\\\\\\\\\\//\\////\/\\/
///\\\\\\///\/\/\\//\\\\\//////\/\\//\\/\\\\\\//\///\\\//\\//\//\/\\////\\//\/\
/\/\//\\\\/\/\/////\\\\\\/\\/\\//\/////\\\\/\\//\\//\\\\\\/\\\//\\\//\\////\\//
\/\//\/\//\\\///\//\/\//\//\/\\///\/\\///\/\\\\\\\\///\/\\\\/\\/\\//\\/\//\/\\\
\\///\\\\\//\///\/\\//\/\\\/\\/\///\/\/\/\/\\//\//\//\\//\\//\\///\/////\\\///\
\/\/\//\\\//////\\\//\\\\//\\\\////\\\/\\\\//\\\////\\/\/\/\/////\\\\\//\\//\\\
\\//\//\\\/\//\\\/\\//\///////\/\\\\/\\/\//\//\\/\\//\/\\////\/////\/\\\//\//\/
///\\/////\/\//\\\///\///\/\\/\\/\\\\\/\\/\////\\////\\\/\\\\\\\///////\\\/\\//
\\\\/\/\/\//\//\\//\\\/\/\\/\\\/\\\/\//\\//\///\/\/\\\/\\\\\\////\\////\\\/\/\/
\//\/\//\/\///\/\/\/\/\/\/\\\//\///\\\\\\/\\\\\\///\/\\\\\//\////\\////\\/\////
//\/\////\\/\\\////\\\///\\/\\/\///\///\\//\\\//\\//\///\\/\\/\/\//\//\///\\//\
///\\/\\//\/\\//\///\//\\/\/\/\/\\////\\\\/\///////\//\//\\/\\/\//\/\\/\////\/\
/\/\//\\\//\\/\\//\//\\//////\\///\//\/\\/\\////\\\\\\\\/\\//\//\/\\\\\\//\\\\/
\///\/\/\//\\///\\/\/\\/\\///\/\\////\\////\\/\/\/\///\\/\////\///\////\\///\\/
/////\\\\/////\//\/\/\\/\\\\\\\///\/\\///\\//\\/\\\/\\\///\\\////\//\//\///\/\/
\\\//\\//////\//\\/\//\\\\/\\/\\\///////\\/\\\//\\\\/////\/\////\\///\////\\\\/
/\\/\\\\///\\////\///////\//\//\\//\//\\\\/\\/\\\////\\\//\\///\\/\\/\///\//\\/
//\//\/\//\\/\///\/\\\//\\\\//\\//\/\/\\///\/\/\\\\\\\\/\\\\/\//\//\\\/\\\\/\\/

?


About HP-IPL/OS

HP-IPL/OS is an Interpreted Programming Language and Operating System for HP2100-series minicomputers. These robust machines were popular in the '70's and many survive today in the hands of collectors, and perhaps a few even performing their original tasks. As of 2010 newly manufactured versions of the machine in the form of PC cards and (expensive) rack-mount systems are available. For vintage computer collectors, these machines are fairly easy to repair (big fat boards with mostly standard TTL circuitry) with multiple companies offering replacement parts, and the various machines, interface boards and software are well-documented. Some of the original software is available and fairly easy to run under simulation, however the available disk operating systems (Time Share Basic E, F, Access and RTE 6) require fairly specific hardware setups that are often out of reach by the average hobbiest. Other available software that would run on a minimal system is often too tedious or too limited. The HP-IPL/OS project was started in 2001 by Bob Shannon to provide an alternative software system that could run on an 8KW machine with only a papertape reader emulator and a console, yet be extendable to support 21MX-class machines and whatever hardware is available.

I took up the task of hacking Bob's threaded interpreter and initial primitive words into a practical kernel, by 2002 we had something that worked enough to begin writing extensions. Bob provided most of the hardware drivers, I did lots of IPL coding. The system grew bit by bit until by 2004 we had enough functionality to do stuff like run HP BASIC from disk, type in a program, then save the binary image, program and all. That first primitive "TDOS" didn't care what kind of disk it used, instead it made calls to a disk driver so it could work on a 7900 or 7906 drive under simulation, or Bob's custom IDE disk controller, with no changes to the actual dos code. Later "XDOS" and "SFS" systems use the same mechanisms. Eventually I made my own IDE disk interface for my system, and configured a build with both 7906 and IDE drivers so that I could copy a simulated 7906 disk system to my IDE disk system and boot it with no changes. In 2008 I added a Viniculum VDRIVE2 module to my IDE interface, at first to save and restore disk images using a USB thumbdrive but it didn't take long to hack together a simple streaming "VDOS" to save and load files to and from the thumbdrive, reusing load/save components originally written in 2002. I'm contemplating how to make a simpler version of the VDRIVE2 adapter that could also function as a paper-tape reader emulator through the same "microchannel" card.

Hardware Requirements and Configuration

The minimum requirements for running HP-IPL/OS is a HP21xx CPU box with at least 8KW (16KB) memory, a serial console interface card connected to a terminal or PC, and a parallel interface card connected to a paper-tape reader emulator for loading the software. A paper-tape punch emulator helps but it is possible to dump changes to the console, save the resulting text, then use a PC utility to convert the dump file into a binary that can be reloaded.

A 21MX-type CPU with DCPC(DMA), DMS, and at least 64KW is required to use the existing alternate memory binary save/load tools, the magtape operating system, and/or the XDOS disk operating system. From 96KW-192KW is required to use the SFS package for applications that need to stream data to and from buffered disk files (XDOS saves, loads, renames and deletes files but can only process one file at a time, SFS adds up to four buffers with independent file pointers for more sophisticated applications). At one point 7970E magtape worked but can no longer be tested due to lack of working hardware. Works under simulation.

The IDE disk driver requires custom electronics that connects a IDE hard disk to a 16-bit bi-directional parallel interface card, the driver does not use DMA and contains no extended instructions. The 7900 and 7906 disk drivers use DMA channel 1 (loc 6) and as written require a CPU that supports the DIV and MPY instructions. The disk drivers require 32KW as written due to the ORG of the binary load code. Note that AFAIK the 7900 and 7906 drivers have not been tested on real hardware. Works under simulation.

The experimental VDOS system for storing files on a USB thumbdrive requires custom electronics that connects a Viniculum VDRIVE2 module to a 16-bit bi-directional parallel interface card, presently the electronics are implemented as part of the IDE interface. The drivers do not use DMA and do not require extended instructions. The base package requires at least 16KW to implement a system able to load IPL packages and save HP-IPL/OS builds. The full package requires a 21MX-type system with at least 64KW to load, run and save arbitrary binary files.

HP-IPL/OS presently has drivers for the following peripherals...

# - these boards are installed in my HP2113.
* - this board was in my machine when I got it, presently not connected.
** - IDE requires a "registered" interface, my older card required patching <IDE to always return 0.

Unused slots need a jumper or board of some kind inserted. Default slots for some devices conflict as not all of these are installed in my system but are present in other systems. Different I/O interface cards can be used for PTR, PTP and IDE depending on what sort of hardware is attached. The "+" boards use 12V logic signals and require level-shift circuits to interface to microcontrollers, the "MicroCircuit" board uses TTL-compatible 5V logic signals that can be directly connected to PIC pins etc.

For most builds CONFIG can be used to change the slot assignments for TBG, TTY, PTR, PTP, BACI, HPIB, LPT, IDE and 790x, and also be used to place the console on the BACI device rather than TTY. Builds containing !CFGE (cfge.ipl) can be configured by setting switches before running - put TTY slot in bits 0-5, BACI slot (opt) in bits 6-11, clear bit 12 for TTY or set for BACI console, bits 13-15 clear, then run from location 70 rather than the usual location 2.

The running of autostart words (words that begin with !) can be disabled if necessary by putting 177777 into the switch register then running from location 2. If reconfiguring the initial TTY slot using switch settings, autostarting can be disabled by setting bit 15, clearing bits 6-12, then running from location 70.

Device assignments may also be changed by altering the following memory locations...

270 - TBG slot
271 - BACI slot
272 - LPT slot
273 - 7900 slot (first board) or 7906 slot
274 - IDE slot
276 - HPIB slot
355 - TTY slot
356 - PTP slot

These can be changed using front-panel switches, or if the system is up, by using the PUT command.
For example to change the BACI slot to 14 enter: 271 14 PUT

Once the system has been configured it needs to be punched to a new ABS file (usually) using the SYSALL command. If PTP isn't available the SimH HP2100 simulator can be used to produce the reconfigured build, or the congen.ipl or version thereof used to dump the system to the console and capture to a file, then L2ABS (in log2abs.ipl or the hposutil.abs build) can be used under simulation to convert the dump file into an ABS file (or make a PC utility that does the same thing, both the dump format and the ABS file format are fairly simple). To use a new configuration without saving restart the system by entering: 2 RUN

Booting a HP-IPL/OS Build

To boot one of the provided builds as-is without using reconfiguration procedures, the TBG card needs to be in slot 10 (if the build includes tbg.ipl), and the TTY (terminal) card needs to be in slot 11. Some way to load the system is required, either a "passthrough" paper-tape reader emulator and transfer software or some other kind of paper-tape reader emulator with the desired ABS file loaded. A terminal or terminal emulator needs to be set to match the TTY card (for my system it's 2400 baud, 7 bits, even parity, 2 stop bits) and connected to the TTY card's serial port before running the system (I have only a single serial port on my PC so have to juggle).

The procedure for my HP2113 E-series machine is as follows...

Power up the machine
Ready the PTR file transfer with the desired ABS file so that it is waiting to send the file
Select the S register, set bits 6-11 to correspond to the PTR slot number, press Store
Press Preset IBL Preset Run - the transfer should begin
Wait until the machine halts and bits 0-5 light up indicating a successful load
If only a single serial port is available, close the PTR sender and start the terminal program
Select the P register, press Clear, set bit 1 to specify location 2, press Store Preset Run
The HP-IPL/OS sign on message and the ? prompt should appear on the terminal. Enter WORDS to make sure it works.

To run a build under simulation, obtain or compile a copy of the SimH HP2100 simulator and put it in a path directory (for Windows say C:\Windows, for Linux say /usr/local/bin). The easiest way (in my opinion) to use the simulator is to install the provided batch or shell scripts so that .sim scripts or .abs files can be double-clicked or right-clicked to run. Once the scripts are set up and working it makes it trivially easy to run a build or test a new build.

To run a build manually in the simulator, go to a command prompt where the build is, run hp2100, and do something like this...

sim>set clk dev=10
sim>set tty dev=11
sim>set ptr dev=12
sim>set ptp dev=13
sim>set cpu 21mx
sim>set cpu 256k
sim>set throttle 800K
sim>load hposdemo.abs
sim>run 2

HP-IPL/OS DEMO 1.55
?

HP-IPL/OS Concepts

HP-IPL/OS begins with a compact kernel that implements an indirectly-threaded interpreter and a base set of "words" (code routines with names stored in a "dictionary" in memory), from there "IPL" packages are loaded to build up the desired level of functionality. Many packages are provided to help create a variety of "builds" ranging from entirely memory-based to disk operating systems. Words are stored in the dictionary as a linked list, each word starts with four locations specifying the word's length, the first four characters of the word's name, and the address of the next word. The chain is terminated with a zero, marking the end of the dictionary. Word names must be carefully specified to avoid conflict but this primitive system is fast and efficient. Words are removed by replacing the length location with a zero (usually using the FORGET word from the extra.ipl package), which effectively removes all words that follow. In effect the dictionary functions like a sequential memory tape.

IPL packages are simply a collection of commands that are redirected into the otherwise interactive console. Some packages define temporary words that prompt for configuration options, such as the extra.ipl package that prompts for a key press to configure memory and block usage, and the internal.ipl package that prompts to optionally remove block-data support from the kernel for making compact 8KW builds that don't need CREATE or other things that store large arrays in memory. Most HP-IPL/OS builds for 16KW or more include the full kernel, the extra.ipl package (or the more compact extra2.ipl package) to provide command line tools like EXPLAIN, RENAME and FORGET and a few commonly-used string functions that aren't in the kernel, and the create.ipl package (or the smaller non-extended smallcre.ipl package) to provide the CREATE word assembler. Additional packages can be added to support specific hardware such as tbg.ipl for the Time Base Generator (TBG), baci.ipl for the BACI serial port, hpib.ipl for the HPIB instrument buss, and disk drivers for 7900 and 7906 drives and for an IDE disk drive attached using a custom-made interface. Some packages provide additional programming abilities such as double.ipl for 32-bit integer math and decimal string conversions, bigshift.ipl for 32-bit shifts and rotates, float.ipl and floatext.ipl for floating-point math and dms.ipl for accessing "alternate memory" (memory past the normal 32KW address space). Systems with 64KW or more can use packages that provide extra words that make it easier to manipulate binary code in alternate memory (altutil.ipl, fcam.ipl and the Show Alternate Memory dump utility in sham.ipl), and implement a Simple File System for storing files on a disk drive (xdos.ipl, sfs.ipl and associated utilities). However all of the packages (at least beyond CREATE) are optional and are not the only way these things can be done, just how it was done. The user is free to explore alternate methods by adapting the provided code or by writing entirely new packages.

HP-IPL/OS uses 5 stacks to store temporary data, the system stack (S), the return stack (R), the string stack (X), and two more stacks (Y and Z) for juggling numeric and string data. The stacks are limited in size - 112 decimal locations for S, 128 decimal locations for R, X, Y and Z. If a stack overflows or underflows the running program is stopped with an error message and the stack pointers are reset. Data can be stored in memory, zero-page locations 100 to 137 octal are reserved for user programs or for more storage, data blocks can be allocated in 1KW increments. Kernel words are provided for accessing block memory without having to specify absolute memory locations but it's perfectly OK to read or write any memory location desired (with the obvious implication that if incorrect the program will crash or the build corrupted, there is no protection as that would limit functionality). Variables are words that push a memory address for storing data, multiple-location variables can be declared to store arrays but the programmer must be careful not to write outside of the declared range, it is safer and more efficient to use block memory if many data locations are needed. Constants are words that push a specified number, constants that push memory locations can be used like variables, the main difference is a variable pushes an address of a location within the variable word, whereas a constant merely pushes a number.

Whether typed from the console or from an IPL package, items on the command line consist of word names, numbers to push to the system stack, or quoted strings to push to the string stack. The maximum command line length is 76 characters (column 78 if the interactive ? prompt is displayed). Comments are indicated by the ";" character and are not entered into the input buffer. The <PTR word redirects an IPL file attached to the paper-tape reader (PTR) into the console as if it were typed (the ? prompt is suppressed while loading), the IPL file should end with CONSOLE to restore normal interactive input. Alternatively <MS can be used to redirect input from whatever driver MS (mass storage) is assigned to, default is PTR but can be pointed to other code to stream to memory, disk or other devices (input and output can be redirected separately by writing subroutine addresses to certain memory locations - see summary.txt for details). The extra.ipl package defines a convenient LOAD word which is essentially <MS with a message if input is coming from a subroutine besides PTR.

Items on the command line are processed from left to right using RPN notation where operands appear before operators, precedence is indicated by order rather than parenthesis. For example to calculate 2*(5+17) and display the results in decimal format enter...

? DECIMAL 2 5 17 ADD MUL PNUM OCTAL
44

Numbers can be interpreted or displayed in DECIMAL OCTAL or BINARY format, there is no hex format. The above command line selects decimal mode, pushes 2, 5 and 17 to the system stack, runs ADD which pops and adds 5 and 17 and replaces with 22 decimal, runs MUL to multiply 2 and 22, replacing with 44, runs PNUM which pops and prints 44, then restores octal mode. Typically you want to return the system to OCTAL format after using other number systems to avoid mistakes as HP memory addresses and data is almost always specified in octal. The DMPS word can be used to display the current contents of the system stack, which should be empty unless there is data being passed. DUP OVER SWAP ROT and DROP perform basic stack manipulation - DUP pushes another copy of the top-most item, OVER pushes a copy of the item below the top-most item, SWAP exchanges the top two items, ROT exchanges the two items below the top item, and DROP erases the top item. Same as with other stack-based systems.

Memory (and variables) are accessed using GET and PUT, address GET pushes the data at address, address data PUT writes data to address - this is different from FORTH, the reason PUT was written like this was so a memory pointer can be maintained on the stack then written to using DUP [something that pushes data] PUT then INC or other words used to adjust the pointer. For example...

? 100 777 PUT
? 100 GET PNUM
000777

Most words expect parameters to be on the stack(s) but some words intended only for immediate mode expect the parameters to be after the word name. This is both for convenience and if needed permits a variable number of parameters without having to know how many items are on the stack. One word that requires the parameter after the command is DEFINE, which is used to define new words from other words. Here is a word that shows memory locations with the starting address and number of locations to show on the stack...

? OCTAL DEFINE SHM
> #1 SWAP +DO
> DUP INDEX DEC ADD GET PNUM
> +LOOP DROP END
? 100 2 SHM
000777 000000

The 777 is from the previous PUT example. +DO/+LOOP are similar to FOR/NEXT in BASIC (a >STEP word permits specifying increment and direction), INDEX pushes the current index, DEC decrements it before adding to the specified address. The END at the end tells DEFINE to stop defining, note that while defining a word the prompt changes to > rather than ?. If 0 is specified for the number of locations it prints 65,536 locations instead, with 32,768 of them being indirect references as memory locations with bit 15 set access the memory specified by the lower 15 bits. If error-checking is needed then it must be specifically coded for, for example...

? OCTAL DEFINE SHME
> OVER IF<0 "ADDRESS OUT OF RANGE" $PRINT DROP DROP ELSE
> OVER OVER DEC ADD IF<0 "LOCATIONS OUT OF RANGE" $PRINT DROP DROP ELSE
> DUP IFZ "NO LOCATIONS SPECIFIED" $PRINT DROP DROP ELSE
> #1 SWAP +DO DUP INDEX DEC ADD GET PNUM +LOOP DROP
> ENDIF
> ENDIF
> ENDIF END
? 177777 1 SHME
ADDRESS OUT OF RANGE
? 77777 2 SHME
LOCATIONS OUT OF RANGE
? 100 0 SHME
NO LOCATIONS SPECIFIED
? 100 1 SHME
000777
? DMPS
?

The DMPS at the end is to make sure I didn't mess up the drops (which could be more efficient but coded for more clarity instead). Conditional code execution is controlled using the IFZ, IFNZ, IF<0, ELSE and ENDIF words, the IFx words pop the stack and act accordingly (IF<0 is true if less-than-zero, or bit 15 set). If complex conditions are needed then they need to be computed first to reduce to a single item to test, for example to trigger if either the top item or the next item is non-zero and not remove the items, use OVER OVER OR IFZ ... ENDIF. Perhaps the hardest thing about programming in IPL (or any other stack-based system) is keeping track with what's on the stacks, if data is accidentally interpreted as an address then bad things can happen. If memory writes are involved, it is best to reload the system if a word crashes - unknown memory locations may have been corrupted causing unpredictable effects later so never save a build after something bugged out. In a way programming in IPL is similar to programming in machine code, have to be very careful to make sure the intended memory is being written to. Or else. In return for being careful, you get a system that can modify any location in memory (including the system itself) without limitations, and doesn't need to carry the bloat that would otherwise be needed to make safe. In fact it would be impractical to make a safe PUT since variables depend on being able to write to locations within that variable (which resides in the dictionary), it would require a large amount of code to distinguish between addresses that are legal variables from addresses that are random dictionary locations, and would preclude using PUT to otherwise alter the dictionary (such as changing data within words without having to redefine the word and everything after it, some packages depend on being able to do that to hide their internal words and variables). HP-IPL/OS assumes that what is specified is what it should do, without artificial limitations bloating the code and preventing potential uses.

If any word in the definition isn't already present in the dictionary the system stops defining and prints an error message. The WORDS word prints a list of currently defined words, since only the first 4 characters and the length are recorded, many words will have "xxxx" to indicate don't-care characters. The extra.ipl defines extra words for manipulating the dictionary - FORGET wordname removes a word and all words after it, RENAME wordname newname renames a word to a new name, EXPLAIN wordname lists the contents of a word if it can (in a format that can be re-entered to redefine), WHEREIS wordname shows where a word is in memory, ERASE removes the last package loaded using <PTR <MS LOAD etc.

Words containing other words are called "high-level" words, each word in the definition occupies one memory location, each literal number requires 2 locations, and each string requires 3 locations plus one location for every 2 characters in the string. Plus the 4 locations for the word header, one location for the "ENSEC" address and a location for the "NEXT" address to indicate the end of the word. Words can also be "primitive", or coded directly in machine code, in this format the location following the header is the address of the actual machine code to run, and the machine code exits by making an indirect jump to a zero-page location containing the NEXT address. It'd be quite tedious having to relocate and manually poke primitive words, instead use the CREATE word (in the create.ipl package, the non-extended version is in smallcre.ipl). CREATE works similarly to DEFINE except it creates primitive words from assembly source rather than definitions from words and constants. The machine code syntax for CREATE code is similar to ASMB/HPASM/etc except labels can be up to 6 characters long, and multi-location instructions must be specified on multiple lines using OCT to indicate the instruction parameters (which is handy since then labels can be assigned to them). As with any HP21xx mini assembler, memory references must be either to zero page or within the current 1KW page or you'll get page errors. There is no ORG since it's adding the word to the dictionary, so to avoid page errors make sure the current end-of-dictionary (WORDS or EOD PNUM) is well before the next page boundary, depending on how much assembly is used. If the IPL file starts with say a 200 octal word assembly program then counting overhead EOD should be at least 206 octal words before the next 1KW page. In that case if EOD PNUM says say 34200 or 35550 then there's room but if it says say 35575 then there isn't enough room (next boundary=36000 and 36000-35575=203). Of course it all depends on the amount of code being created and where the code appears in the package - sometimes it just comes down to trial and error - if a package doesn't go, FORGET the first word then load some other needed high-level package or word first to push it past the boundary.

Here is an example CREATE word that writes data to a specified I/O device, along with a variable to hold the slot address and a high-level wrapper for using, written as a package file...

;this package permits outputting to a device specified
;by the OUTA variable, usage: data OUTPUT
"Creating SOUT" $PRINT CRLF
OCTAL CREATE SOUT
* output word to I/O slot
* usage: [word] [slotadr] SOUT
JSB ZSPOP,I pop slot address
AND C77 mask to 6 bits
STA 1 put in B register
LDA PA get address of patch list
JSB PSUBA,I call patch sub to modify addresses
JSB ZSPOP,I pop the data to write
P1 OTA 0 \
P2 STC 0,C | usual output code
P3 SFS 0 | 0's will be replaced with
JMP *-1 | the specified slot address
P4 CLC 0 /
JMP END exit word
PA DEF *+1 address of patch list
DEF P1
DEF P2
DEF P3
DEF P4
OCT 0
C77 OCT 77
END
"Declaring OUTA and setting to 11" $PRINT CRLF
VARIABLE OUTA
OUTA 11 PUT
"Defining OUTPUT" $PRINT CRLF
DEFINE OUTPUT
OUTA GET SOUT END
"Done" $PRINT CRLF
CONSOLE

CREATE requires at least two data blocks for normal operation. CREATE includes many pre-defined symbols for internal HP-IPL/OS functions - JSB ZSPOP,I calls the code to pop the stack to the A register, JSB ZSPSH,I pushes A to the stack, JSB PSUBA,I calls the slot-patching code with the A register containing the address of the patch list (which must end with 0) and the B register containing the I/O slot address to merge into the I/O code. Usually real drivers do this only once using an "auto-start" word (a word that begins with the ! character) to avoid patching and slowing down each transfer. Usually the slot address for the device is placed in zero-page memory that isn't used for other things, locations in the range of 140-147 should be safe to use. Use CREATE /K wordname to preserve the existing symbol table from a previous CREATE so that the code can access locations in the previous word, and CREATE /L wordname to use all available data blocks for the symbol and fixup tables. This is an example of why sometimes it's really nice to be able to parse the command line, VARIABLE also accepts an optional number after the variable name to specify how many locations to reserve, which can come in handy if only a few more locations are needed to push EOD past a 1KW boundary. Some packages such as fed.ipl define temp words to calculate remaining room and define pad variables as needed to avoid page errors.

The above example can be copied to a file (say "output.ipl"), then loaded into a build containing CREATE (such as the hposutil.abs build) by attaching the file to PTR and entering LOAD (or <PTR). Under simulation it looks like...

HP-IPL/OS UTIL 1.54
? [control-e pressed to halt]
Simulation stopped, P: 02226 (JMP 2225)
sim>attach -e ptr output.ipl
sim>c
LOAD
Creating SOUT
045612 :
045612 :keep the changes
045612 :
045613 :
045614 :
045615 :
045616 :
045617 :
045620 :
045621 :
045622 :
045623 :
045624 :
045625 :
045626 :
045627 :
045630 :
045631 :
045632 :
045633 :
045634 :
045635 :
Fixing...
Declaring OUTA and setting to 11
Defining OUTPUT
Done
? 101 OUTPUT 102 OUTPUT CRLF
AB

To save a new build after loading a new package...

? [control-e pressed to halt]
Simulation stopped, P: 02226 (JMP 2225)
sim>attach ptp custom.abs
PTP: creating new file
sim>c
SYSALL
Writing...................
? [control-e pressed to halt]
sim>exit

Note that IPL files must have CR/LF (dos) line ends, if editing files under Linux use a conversion utility like unix2dos or flip -m, or use an editor like Scite that permits specifying and preserves line end format. HP-IPL/OS ignores line-feed characters directed into the console and processes only the CR which isn't present in unix-format text files. A similar note for terminal settings (especially if set for compatibility with BCS programs) - normally the terminal should only send CR when enter is pressed, no problems at the console if CRLF enter is used but it will mess up anything using $IN to input string lines. Add the nolf.ipl package if using HP-IPL/OS with CRLF terminal settings.


The HP-IPL/OS Kernel

The kernel implements the following functions (among things)...

Drivers for the console, papertape reader and papertape punch devices
Subroutines for obtaining input lines, using stacks, patching device addresses and other low-level stuff
A fixed set of zero-page memory locations other programs can use to access internal functions
An indirectly-threaded interpreter and subroutines for pushing literals and executing words
A core set of primitive machine-coded dictionary words for performing essential operations
Additional mixed and high-level words that make use of the core primitive words
Infrastructure for redirecting console or MS output and input to and from subroutines
Infrastructure for storing data in a user-specified number of 1KW data blocks
Infrastructure for making new words from existing words or raw machine code

The input line buffer can hold a maximum of 76 characters (not counting the CR), the only editing options are backspace (ascii 8 or ascii 127) to erase the last character, and escape (ascii 27) to cancel the input line and start a new line. Comments are indicated by an unquoted ";" character. Whitespace doesn't matter so long as at least one space separates the elements. Do not enter tabs, cursor keys, unicode text or any other non-plain-text characters other than CR BS or Esc. Almost all existing HP-IPL/OS words are in uppercase so usually HP-IPL/OS is run with the caps lock on, however lowercase or mixed case word names can be used as needed.

The kernel doesn't provide much in the way of operator convenience but provides all the essential functions upon which to build more complex systems. Typical builds almost always include extra.ipl (or one of its derivitives) to provide dictionary manipulation tools, memory dumping and saving and other often-used words that didn't make it into the kernel itself. All but the smallest of builds usually include the CREATE assembler for making words that need to be written in machine code, such as device drivers and accessing features of the later machines. Machine code can also be assembled to a fixed location in upper memory above HP-IPL/OS' dictionary and data blocks and called from programs or jumped to directly. The kernel also provides functions for creating primitive words from raw machine code, these functions are used by CREATE but can also be used to manually enter simple machine-coded words provided any branches or memory references are properly calculated to account for the position in the dictionary.

Buggy or no longer needed words need to be removed before entering more words or dictionary space will be needlessly consumed. The command that's normally used to remove one or more words (FORGET wordname) is part of the extra package - if working with just the kernel, words can be deleted manually by entering "wordname" $DEFADR 4 SUB 0 PUT but this can be risky, the system can be corrupted if "wordname" does not exist. A safer way to remove words using just the kernel is to enter this very simple $FORGET word that has no effect if a word doesn't exist...

DEFINE $FORGET
$DEFADR DUP IFZ DROP ELSE 4 SUB #0 PUT ENDIF END

Don't worry if the code makes no sense yet, all the words used in this definition will be explained in the following docs. The dictionary is a sequential chain of words where each word links to the next word until a zero is encountered, so to remove a word a zero is placed at the beginning of a word header (the 4 SUB part), effectively removing the word and all words that follow it. This simple word remover has no protections and will happily remove kernel words if you tell it to (which can be handy if you know what you're doing). Here's what it looks like entering the $FORGET definition (prompts and all) and using it to remove a TEST word defined for the occasion...

? DEFINE $FORGET
> $DEFADR DUP IFZ DROP ELSE 4 SUB #0 PUT ENDIF END
? DEFINE TEST
> "TEST" $PRINT END
? TEST
TEST
? "TEST" $FORGET
? TEST
TEST NOT FOUND
?

The following is a list of the more commonly-used kernel words with explanations and usage examples, refer to summary.txt for a complete list with brief explanations. The SimH HP2100 simulator was used to produce most of the examples, operation on real hardware is similar except the simulator commands need to be translated into actual hardware actions such as attaching files to the papertape reader (PTR) or papertape punch (PTP). The examples generally show what is actually displayed - the ? and > are prompts generated by the system, don't type them unless a > is actually used in the word, such as a CASE structure that's testing for greater-than. When [some parameter] is written in an explanation it means a literal number, don't type the brackets, just the number representing the parameter... i.e. [from] [to] DUMP means if you want to dump locations 100 to 107 use: 100 107 DUMP

WORDS  EOD

WORDS lists the words that are presently in the dictionary, along with the address of EOD (end of dictionary) and the amount of free memory. EOD pushes just the end of dictionary address, EOD PNUM prints it.

Here's what the kernel dictionary looks like after booting the hpiplos1.abs binary...

HP-IPL/OS  8K   V1.6
? WORDS
DO +DO INDEx +LOOx >STEx IFNZ IFZ IF<0 ENDIx ELSE UNTIx WHILx CASE = < > <=
>= <> DEFAxxx ENDCxxx EXECxxx WBOOx AND OR XOR ADD SUB INC DEC NOT 2CPL DUP
DROP OVER ROT SWAP GET PUT PNUM CRLF DECIxxx OCTAx BINAxx RADIx SP>S SB>S
XP>S XB>S YP>S YB>S ZP>S ZB>S END EOD DEFIxx DMPS S>SR SR>S PCHR PWRD CHRIx
S>X X>S S>Y Y>S S>Z Z>S MUL ASL ASR ROL ROR DIV RUN X>>Y X>>Z Y>>X Z>>X
$PRIxx $SWAx $CPY $DUP $DROx $LEN $ADR $XTExx $PUT $GET $CRExxx $STR $HEAx
$APPxxx $TAIx $IN $CAT $VAL <>COx >PTP <PTR MSPAxxx MSBOxx MSBIx MSWOxx
MSWIx MS$Oxx MS$Ix MSCRxx >MS <MS MS_Sxxx MS_Rxxxxxx CONSxxx #0 #1 @TL @TB1
@TB2 @ANVxx @LLP @ENSxx @CLH @LITxxxx @STRxxx @RTSxx @DIC @USR @BLK @END
@DIPxx RND TOKEx SDIC +IRQ -IRQ +AUTx -AUTx ADDCxxx INBLxxx OUTBxxxx GETIx
SETIx GETOx SETOx $DEFxxx HEADxxx WORDx ADDHxxxxx ADDHxxxxxx FIXLxxxx
ADDMxxxx VARIxxxx CONSxxxx ALLOxxxx ZEROxxxxx BPUT BGET
EOD=011534 FREE=002243
?

Only the first 4 characters of word names and the length are significant, the "xxx" characters mean "don't care". To HP-IPL/OS, TEST1 and TEST2 mean the same thing (TESTx). In the case of duplicates the last match is used, making it possible to add to or redefine existing words without having to remove them. Words which control the execution of blocks of code are disabled when at the command line level, these are placed at the beginning of the dictionary (DO to ENDCASE). Words that begin with @ push addresses of various subroutines and zero page variables. Words that begin with # are constants (0 and 1 are frequently used so using #0 or #1 saves memory). Words that begin with $ implement string functions. Words that begin with < or > usually (but not always) redirect the console from or to devices (also used for CASE conditions, and used in some packages to indicate general transfers). Words that contain a single > in the name usually transfer a single 16-bit value, words containing >> usually transfer strings. These are just (very) loose naming conventions, words can be named as needed so long as they're unique as far as HP-IPL/OS is concerned (unless purposely replacing an existing word), don't contain a quote character, don't consist entirely of numbers, and not be END since that's reserved for ending definitions. The exception is words that start with ! automatically run (autostart) when HP-IPL/OS starts up. All autostart words are run in the order they appear in the dictionary before the HP-IPL/OS version message is displayed.

PNUM  PCHR  PWRD  $PRINT  CRLF  CHRIN  $IN

Words for console input and output. PNUM pops and prints the number at the top of the S stack (the "stack") plus a space, PCHR pops and prints the ascii character of the number at the top of the stack, PWRD pops and prints a double-character, $PRINT pops and prints the string at the top of the X stack (the "string stack"), CRLF outputs a CR/LF sequence to start a new line, CHRIN obtains a single character and pushes it to the stack, $IN obtains a line of text until CR (Enter) is received and pushes the line to the string stack without the CR.

? 77 "ENTER:" $PRINT $IN $PRINT 40 PCHR PNUM
ENTER:SOMETHING
SOMETHING 000077
? "PRESS:" $PRINT CHRIN CRLF PNUM
PRESS:X
000130
?

$VAL  $STR

Numerical string conversions, $VAL pops the string stack pushes the value of the string to the stack, $STR pops the stack and pushes a string representation to the string stack (without a trailing space).

? "123" $VAL PNUM "NEXT" $PRINT CRLF 456 $STR $PRINT "NEXT" $PRINT
000123 NEXT
000456NEXT
?

DEFINE

DEFINE [wordname] defines a new word from literal numbers and strings and other words. When defining from the ? prompt the prompt changes to > instead, carefully enter each line (up to 76 characters per line). If any of the words on the line are not present in the dictionary it prints the first 4 characters of the word then NOT FOUND and the definition is cancelled without changing the dictionary. End the definition with END, anything after the end is interpreted as a command outside of the definition. DEFINE must only be used at the command-line level, never from within a definition.

? DEFINE TEST
> "TEST" $PRINT END TEST
TEST
?

Note that there is no built-in "easy" command way to remove words using just the kernel, normally this is done using FORGET from the extra.ipl package. The TEST word can be manually removed by entering "TEST" $DEFADR 4 SUB 0 PUT but be very careful, with this sequence if the quoted word does not exist the effect is unpredictable and could potentially corrupt the system.

VARIABLE  CONSTANT  PUT  GET

VARIABLE [wordname] defines a word that when executed pushes a memory address which can be used to store a value. VARIABLE [wordname] [count] defines a word containing multiple locations that can be used for an array. CONSTANT [wordname] [value] defines a word that pushes the value, if the value corresponds to a constant memory location then it can be used like a variable. VARIABLE and CONSTANT must only be used at the command-line level, never from within a definition. PUT and GET are used to store or retrieve values to and from memory. [address] [value] PUT stores value into addess (address and value are removed from the stack), [address] GET pops the address and replaces it with the value at that address.

? OCTAL
? CONSTANT MYMEM 110
? VARIABLE V1
? MYMEM PNUM
000110
? V1 PNUM
011727
? MYMEM 222 PUT
? V1 333 PUT
? MYMEM GET PNUM V1 GET PNUM
000222 000333
?

Use extreme care when using PUT, there is no protection whatsoever (nor should there be since that would limit its usefulness). Perhaps a safer way to implement variables should have been implemented, but that would slow down and increase the size of the system. If you feel you cannot be trusted with the power of PUT then perhaps define a VP (variable put) word to use instead...

OCTAL DEFINE VP
OVER CASE ;case branch on address value
 < 100 #1 ;error if less than 100
> 147 ;if more than 147 check to make sure a variable...
OVER DUP DEC GET SUB IFNZ #1 ;error if (adr-1)<>adr
ELSE OVER DEC DEC GET 124321 SUB IFNZ #1 ;error if not var
ELSE #0 ENDIF ENDIF ;appears to be a var, no error
DEFAULT #0 ;no error if from 100-147
ENDCASE IFNZ " OUT OF RANGE " $PRINT WBOOT ;warmboot
ELSE PUT ENDIF ;if in allowed memory do the PUT
END

? OCTAL DEFINE VP
> OVER CASE < 100 #1 > 147 OVER DUP DEC GET SUB IFNZ #1 ELSE
> OVER DEC DEC GET 124321 SUB IFNZ #1 ELSE #0 ENDIF ENDIF
> DEFAULT #0 ENDCASE IFNZ " OUT OF RANGE " $PRINT WBOOT
> ELSE PUT ENDIF END
? MYMEM 377 VP MYMEM GET PNUM
000377
? V1 1777 VP V1 GET PNUM
001777
? 147 555 VP 147 GET PNUM
000555
? 17000 111 VP 17000 GET PNUM
OUT OF RANGE
?

This optional safety-net only works for simple variables and the "user" zero-page locations. It won't work for multi-location array variables or for system variables (the @ words). The words used in this example will be explained below.

BINARY  DECIMAL  OCTAL  RADIX

These control the number system used for numbers when entered at the command-line level and when printing numbers and using numerical string conversions. BINARY selects binary representation, DECIMAL selects decimal representation, OCTAL selects the usual octal representation. When in decimal mode values > 32767 are displayed or converted as negative numbers without leading zeros, binary representation is always converted to 16 characters and octal representation is always converted to 6 digits. RADIX pushes the current number system to the stack which is occasionally used to restore the current number system upon exit. When using other number systems it is recommended that the system be returned to OCTAL to avoid mistakes when specifying memory addresses and values which almost always assume octal.

? OCTAL 377 DECIMAL PNUM OCTAL
255
? RADIX PNUM
000010
? RADIX DECIMAL PNUM OCTAL
8
?

Note - when defining words containing literal octal values OCTAL must be used outside of the definition. OCTAL etc do not affect the interpretation of literal numbers when used inside of word definitions, only how numbers are printed and converted. Often word definitions begin with OCTAL DEFINE wordname to ensure that the proper number system is selected.

DUP  OVER  SWAP  ROT  DROP  DMPS

These words are used to juggle words on the stack. DUP pushes a copy of the value at the top of the stack, OVER pushes a copy of the value beneath the top value (pushes over the top value), SWAP exchanges the two top-most stack values, ROT exchanges the two values beneath the top of the stack, and DROP pops and discards the value at the top of the stack. DMPS is a tool that displays the stack without affecting its contents.

? 1 2 3 DMPS

000001
000002
000003

? SWAP DMPS

000001
000003
000002

? ROT DMPS

000003
000001
000002

? OVER DMPS

000003
000001
000002
000001

? DROP DROP DROP DUP DMPS

000003
000003

? DROP DROP

S>X  X>S  S>Y  Y>S  S>Z  Z>S

These transfer values between stacks, i.e. S>X pops a value from the S (main) stack and pushes it to the X stack, Z>S pops a value from the Z stack and pushes it to the main stack, etc. Very useful when values need to be popped and saved to get to deeper values, then restored. Also useful for holding exit flags, pointers, etc to avoid cluttering up the main stack.

? 101 S>Z 102 S>Z 100 DMPS

000100

? Z>S Z>S DMPS

000100
000102
000101

? DROP DROP DROP

$DUP  $SWAP  $DROP  X>>Y  Y>>X  X>>Z  Z>>X  $CPY  $CAT

These juggle strings. $DUP makes a copy of the top-most string on the string (X) stack, $SWAP exchanges the two top-most strings, $DROP removes the top-most string, X>>Y moves a string to the Y stack, Y>>X moves a string from the Y stack back to the string stack, X>>Z and Z>>X transfer strings to and from the Z stack. $CPY makes a copy of the top-most string and pushes it to the Y stack (same as $DUP X>>Y). $CAT combines the two top-most strings into a single string.

Generally the X stack should be reserved for strings since that's where all string functions take their parameter(s) from. The Y and Z stacks can be used for strings and numbers but care must be taken not to get them mixed up and use Y>>X etc when Y holds a numeric value, usually I use Y for strings and Z for numeric values to avoid confusing them.

? "HELLO " "WORLD" X>>Y $PRINT Y>>X $PRINT
HELLO WORLD
? "WORLD" "HELLO " $SWAP $CAT $PRINT
HELLO WORLD
?

$APPEND  $HEAD  $TAIL  $LEN

These permit adding characters to and removing characters from strings. $APPEND pops an ascii value from the number (S) stack and appends the character to the string on the string (X) stack (which must exist, use "" to push an empty string). $HEAD removes the first character of a non-empty string and pushes its ascii value to the number stack. $TAIL removes the last character of a non-empty string and pushes its ascii value to the number stack. $LEN pushes the length of a string without affecting the string.

? "ABCDE" $HEAD PNUM $TAIL PNUM $PRINT
000101 000105 BCD
? "" 101 $APPEND 102 $APPEND 103 $APPEND $PRINT
ABC
? "12345" $LEN PNUM $PRINT
000005 12345
?

$CREATE  $GET  $PUT  $ADR

[number of chars] [ascii value] $CREATE creates a string containing repeating characters. [position] $GET pushes the ascii value of a character within a string (numbering starts at 0). [position] [ascii value] $PUT replaces a character within a string. $ADR pushes the address of the first word of a string on the string stack.

? OCTAL "AB" 5 40 $CREATE $CAT "YZ" $CAT $DUP $PRINT
AB YZ
? 2 103 $PUT $DUP $PRINT
ABC YZ
? 10 $GET PCHR
Z
? $ADR GET PWRD
AB
? $DROP

IFZ  IFNZ  IF<0  ELSE  ENDIF

These words implement flow control, the IFxx words pop the stack then if zero (IFZ), not zero (IFNZ) or has bit 15 set (IF<0) then the code following the IF word is executed until the corresponding ELSE or ENDIF is encountered. If the test is not true then execution continues after the corresponding ELSE. These can be nested to any level.

? DEFINE IFTEST
> DUP IFZ "ZERO" DROP ELSE
> IF<0 "LESS THAN ZERO" ELSE
> "NOT ZERO"
> ENDIF
> ENDIF
> $PRINT END
? 0 IFTEST
ZERO
? 1 IFTEST
NOT ZERO
? 177777 IFTEST
LESS THAN ZERO
?

To do traditional comparisons the IFxx word is preceeded by an expression that results in a single value to test, for example "if V1 < V2 then ... endif" is coded as V1 GET V2 GET SUB IF<0 ... ENDIF (V1 and V2 are variables). It's ok for there to be no code between IF<0 and ELSE for computing <= and >= situations.

CASE  =  <  >  <=  >=  <>  DEFAULT  ENDCASE

These implement another kind of flow control and may be more suitable for more complex conditions. CASE pops a value from the stack and directs control to one of the branches defined by = < > <= >= <> or DEFAULT. Each of the condition codes must be followed by a literal number, a variable or a constant, followed by the code for the branch. Only the code for the first true condition runs, even if more than one condition is true. Code following DEFAULT (if present) runs if none of the conditions are true. Code in an execution branch skips to past the ENDCASE if another condition tag or DEFAULT is encountered. CASE structures can be nested to any depth.

Using CASE, "if V1 < V2 then ... endif" translates to V1 GET CASE < V2 ... ENDCASE, a bit more reasonable than having to translate conditions into a subtraction formulaes, however the second parameter has to be a literal, variable or constant, not another stack value.

? VARIABLE A
? VARIABLE B
? DEFINE ?AB
> A GET CASE
> > B "A > B" $PRINT
> < B "A < B" $PRINT
> DEFAULT "A = B" $PRINT
> ENDCASE END
? A 1 PUT
? B 2 PUT
? ?AB
A < B
? A 10 PUT ?AB
A > B
? B 10 PUT ?AB
A = B
? A 177777 PUT B 0 PUT ?AB
A < B
?

DO  WHILE  UNTIL

DO pushes the next location to the return stack to permit repeating a block of code. WHILE pops the main stack and if the value is non-zero, continues interpreting from the location on the return stack, repeating code following the corresponding DO, otherwise removes the loop address from the return stack to continue. UNTIL is similar but loops until a non-zero value is on the stack. DO loops can be nested a reasonable number of times depending on the number of addresses currently on the the return stack, which is also used when a word runs another word (the return stack can hold a maximum of 128 addresses, if exceeded an "R stack overflow" error occurs).

? DEFINE DOTEST
> DO
> "ENTER SOMETHING:" $PRINT $IN
> $LEN IFZ $DROP #1 ELSE $PRINT CRLF #0 ENDIF
> UNTIL END
? DOTEST
ENTER SOMETHING:THIS
THIS
ENTER SOMETHING:THAT
THAT
ENTER SOMETHING:

? DEFINE DOWTEST
> "ENTER SOMETHING:" $PRINT $IN
> $LEN IFZ $DROP ELSE
> DO
> $TAIL PCHR $LEN
> WHILE $DROP
> ENDIF END
? DOWTEST
ENTER SOMETHING:SOMETHING
GNIHTEMOS
?

+DO  +LOOP  INDEX  >STEP

+DO and +LOOP iterate a block of code a certain number of times while providing an index if the current iteration value is needed. [start] [end] +DO pushes the next location to the return stack, pops the end index value from the main stack and pushes it to the return stack, and pops the start index value from the main stack and pushes it to the return stack. +LOOP (by default) increments the value at the top of the return stack, compares it to the end value, and if not equal loops back to rerun the code after the corresponding +DO. INDEX pushes the current index value. +DO +LOOP loops can be nested but not as deep because each level consumes 3 locations on the return stack. >STEP pops the main stack and stores it in an internal location used by +LOOP to determine what to add to the index to permit looping backwards, however the result must eventually equal the specified end value or the the code will loop forever, +LOOP always resets the step value back to 1 after terminating a loop.

? DEFINE SIMPLE+LOOP
> 2 7 +DO INDEX PNUM +LOOP END
? SIMPLE+LOOP
000002 000003 000004 000005 000006 000007
? DEFINE FANCY+LOOP
> 7 2 +DO INDEX PNUM -1 >STEP +LOOP END
? FANCY+LOOP
000007 000006 000005 000004 000003 000002
?

Be careful when using >STEP as it affects every other word containing +DO +LOOP. If calling any sort of string function or other complex code from within a loop using an alternate step value, use #1 >STEP right after the +DO so all other code will see the normal increment, then use >STEP for an alternate step value right before the +LOOP that uses it. Sometimes it's easier to code a backwards loop as a value on the stack with a regular DO loop (depending what else needs to be accessed from the stack). Another way to reverse a loop without actually looping backwards is to invert the index value, something like #0 7 +DO 7 INDEX SUB [some code using value on stack counting from 7 to 0] DROP +LOOP is safer. >STEP is for cases where it really makes it easier, such as when numerous things need to use INDEX.

AND  OR  XOR  ADD  SUB  MUL  DIV

These words pop two values, perform an operation and push the results. AND, OR and XOR perform bit-wise logic. ADD, SUB, MUL, DIV perform 16-bit mathematical operations, the values are popped in an order that makes sense when reading from left to right (i.e. 3 2 SUB pushes 1 and 4 2 DIV pushes 2). There is no built-in provision for detecting overflow, if required overflow can be detected by examining bit 15 of the operands and the result.

? DECIMAL
? 7 11 22 ADD MUL 4 SUB PNUM
227
? 16 2 DIV PNUM 15 2 DIV PNUM
8 7
? OCTAL DEFINE OVADD
> OVER OVER S>Z S>Z ADD DUP 100000 AND
> Z>S Z>S OVER OVER S>Z S>Z AND 100000 AND IFNZ DROP " OVF " $PRINT ELSE
> Z>S Z>S OVER OVER S>Z S>Z XOR 100000 AND IFZ DROP ELSE IFZ " OVL "
> $PRINT ENDIF ENDIF ENDIF Z>S DROP Z>S DROP END
? 2 2 OVADD PNUM DMPS
000004
? 100000 100001 OVADD PNUM DMPS
OVF 000001
? 100000 77777 OVADD PNUM DMPS
177777
? 177777 2 OVADD PNUM DMPS
OVL 000001
?

INC  DEC  NOT  2CPL  ASL  ASR  ROL  ROR

These words replace the top word of the stack with an altered value. INC and DEC increment and decrement (tos=tos+1 and tos=tos-1). NOT inverts the bits, all 0's become 1's and all 1's become 0's. 2CPL does a two's complement operation (same as NOT INC), when doing signed math this is the same as multiplying by -1. ASL and ASR perform arithmetic shift left and right, the vacated bit is replaced with a 0 bit and the bit that is shifted out is discarded. ROL and ROR perform rotate left and rotate right, no bits are lost, just moved.

? DECIMAL 222 INC DUP PNUM
223
? DEC DUP PNUM
222
? BINARY DUP PNUM
0000000011011110
? NOT DUP PNUM
1111111100100001
? ASL DUP PNUM
1111111001000010
? ASR DUP PNUM
0111111100100001
? ROR DUP PNUM
1011111110010000
? ROL DUP PNUM
0111111100100001
? DECIMAL DUP PNUM
32545
? 2CPL PNUM
-32545
? OCTAL

@END  @BLK  EOD  ALLOCATE

These determine how the system memory is used. @END is a psuedo-variable that defines the last location in memory used by HP-IPL/OS (although there isn't much enforcement of this except when using the block access words), normally this should be set to the address of a 1KW boundary - 1. In the stock kernel @END is set to 17777, if using the kernel on a real 8KW machine this is into the bootloader so make sure the 64-word bootloader program is write-protected, or take care not to get into that memory (I read that it is write-protected). The @BLK psuedo-variable determines the end of the usable dictionary space and the start of block data memory, normally it is set to an even 1KW boundary. In the stock kernel @BLK is set to 14000. EOD pushes the end of the currently defined dictionary, the FREE= display is calculated as one less than the start of block memory minus EOD (there has to be room for the zero that terminates the dictionary). ALLOCATE pops the stack and allocates that many 1KW data blocks, setting @BLK to the beginning of block memory. If not already on a 1KW boundary then @BLK is set to the next lowest boundary, if @END is set to the end of physical memory then to protect the boot area while maximizing memory set @END after using ALLOCATE, this shorts the last block a bit but should be fine. For that matter on an 8KW machine it's probably best to use internal.ipl to totally eliminate the block support words, since no current apps that'll work in that little memory use block storage. There is no fundamental reason why blocks must begin on a 1KW boundary, but some applications (such as XDOS) assume that it does.

@END and ALLOCATE is how the kernel is configured to support larger memory sizes...
To configure for 16KW, 2 data blocks and 2KW high memory enter: @END 33777 PUT 2 ALLOCATE
To configure for 32KW, 4 data blocks and 4KW high memory enter: @END 67777 PUT 4 ALLOCATE
To configure for 8KW with no blocks and no high memory enter: @END 17677 PUT @BLK 17700 PUT

BPUT  BGET  ZEROBLOCK

BPUT and BGET provide a controlled way to access block memory. [value] [offset] [block] BPUT writes value to an offset within a block, [offset] [block] BGET pushes the value at an offset within a block. Block numbers and offsets begin at 0. [block] ZEROBLOCK zeroes the specified block but note that if using MS (mass storage) vectors they must be reset after using ZEROBLOCK.

? 123 456 0 BPUT
? 456 0 BGET PNUM
000123
? 0 ZEROBLOCK 456 0 BGET PNUM
000000
? 444 0 1 BPUT
? 444 0 2 BPUT
ERROR
? 444 2222 1 BPUT
ERROR
? MSPAPER ;restore MS after using ZEROBLOCK

<PTR  >PTP  <>CON  CONSOLE

These are used to redirect the console from and to papertape. <PTR causes console input to come from the papertape reader, while redirected the ? and > prompts are suppressed. The redirected stream must end with <>CON or CONSOLE to restore normal console operation, CONSOLE is the same as <>CON except it also resets the MS vectors back to PTR/PTP. Redirected IPL packages *must* end with a line containing CONSOLE, <>CON, or some other word that undoes the redirection, otherwise the load will not be terminated (to recover halt and execute from location 2). Packages that need to restore the console temporarily to get configuration information use <>CON so that the load can continue if loading from something besides papertape. >PTP causes all text output (except for the ? prompts) to go to the papertape punch, until <>CON or CONSOLE is used.

HP-IPL/OS  8K   V1.6
?
Simulation stopped, P: 02225 (SFS 11)
sim>attach -e ptr ../ipl/internal.ipl
sim>c

? <PTR
Loading configurator
Press the appropriate key...
1) Stock 8K config with 2 blocks
2) Remove blocks and block support
3) Remove blocks and constants/variables
> 2
Loading $DEFADR
Loading HEADER$
Loading WORDS
Loading ADDHEADER
Loading ADDHEADER$
Loading FIXLINKS
Loading ADDMLVAR
Loading VARIABLE
Loading CONSTANT
Done
?
Simulation stopped, P: 02226 (JMP 2225)
sim>attach -e ptr ../ipl/8kextra2.ipl
sim>c

? <PTR
Protecting boot block...
Loading FORGET
Loading PDEF
Loading EXPLAIN
Loading $EQUAL
Loading $SLICE
Loading $TRIM
Loading RENAME
Loading WHEREIS
Loading DUMP
Loading ABSOUT
Loading PTZERO
Loading SYSGEN
Done
? WORDS
DO +DO INDEx +LOOx >STEx IFNZ IFZ IF<0 ENDIx ELSE UNTIx WHILx CASE = < > <=
>= <> DEFAxxx ENDCxxx EXECxxx WBOOx AND OR XOR ADD SUB INC DEC NOT 2CPL DUP
DROP OVER ROT SWAP GET PUT PNUM CRLF DECIxxx OCTAx BINAxx RADIx SP>S SB>S
XP>S XB>S YP>S YB>S ZP>S ZB>S END EOD DEFIxx DMPS S>SR SR>S PCHR PWRD CHRIx
S>X X>S S>Y Y>S S>Z Z>S MUL ASL ASR ROL ROR DIV RUN X>>Y X>>Z Y>>X Z>>X
$PRIxx $SWAx $CPY $DUP $DROx $LEN $ADR $XTExx $PUT $GET $CRExxx $STR $HEAx
$APPxxx $TAIx $IN $CAT $VAL <>COx >PTP <PTR MSPAxxx MSBOxx MSBIx MSWOxx
MSWIx MS$Oxx MS$Ix MSCRxx >MS <MS MS_Sxxx MS_Rxxxxxx CONSxxx #0 #1 @TL @TB1
@TB2 @ANVxx @LLP @ENSxx @CLH @LITxxxx @STRxxx @RTSxx @DIC @USR @BLK @END
@DIPxx RND TOKEx SDIC +IRQ -IRQ +AUTx -AUTx ADDCxxx $DEFxxx HEADxxx WORDx
ADDHxxxxx ADDHxxxxxx FIXLxxxx ADDMxxxx VARIxxxx CONSxxxx FORGxx PDEF
EXPLxxx $EQUxx $SLIxx $TRIx RENAxx WHERxxx DUMP ABSOxx PTZExx SYSGxx
EOD=012310 FREE=005367
?

Mass Storage and Redirection

HP-IPL/OS implements a simple but versatile system that lets anything that uses the MS (mass storage) words for input and output to be redirected to any device capable of streaming data. In a nutshell, location 350 (ZMINP or just MS input) is a pointer to a subroutine for inputting data, and location 351 (ZMOUT or MS output) is a pointer to a subroutine for outputting data. By default these locations are set to the PTR and PTP drivers but other drivers can provide words that redirect MS input and/or MS output to their own subroutines. What it means is I can reuse things written long ago for papertape on new devices like a thumbdrive with no code changes, all that's needed is a stream driver. MS input and output streams are always bytes, appropriate for byte-based things like papertape but for 16-bit things like memory and disk blocks, the drivers must pack and unpack the bytes and maintain a flag so it can restart a stream with the correct byte. If streaming to a disk then the driver (or firmware) needs to detect when the buffer is full, write it to a disk block, then start collecting data for the next block. MS streams don't have an inherent end-of-file indication other than the formatting of the data itself (for example IPL files end with CONSOLE and ABS binaries end with zeroes). MS drivers can provide an EOF indication if they wish, a reasonable approach is if a file is read past the end, return a zero and set an error code to be returned by the driver's status function.

MSPAPER  <MS  >MS

MSPAPER sets MS input and output to the PTR and PTP devices. <MS redirects data from the MS input device into the console, when MS input is PTR then <MS has the same effect as <PTR. >MS redirects console output to the MS output device, when MS output is PTP then >MS has the same effect as >PTR. The <MS word is often used to load IPL files from disk, where MS input is set to get data from an input file stream driver or from alternate memory containing the IPL code to load.

MSBIN  MSBOUT  MSWIN  MSWOUT

MSBIN gets a byte from MS input and pushes it to the stack. MSBOUT pops a byte from the stack and writes it to MS output. MSWIN gets two bytes from MS input and pushes a 16-bit value with the first byte in bits 8-15 (the high byte) and the second byte in bits 0-7 (the low byte). MSWOUT pops a 16-bit value and writes the high byte then the low byte to MS output.

MS$IN  MS$OUT  MSCRLF

MS$IN gets a string from MS input until CR is received and pushes the string (without the CR) to the string stack, the character after the CR is skipped and assumed to be a LF. "string" MS$OUT writes a string to MS output. MSCRLF writes a CRLF to MS output.

INBLOCK  OUTBLOCK  GETIP  SETIP  GETOP  SETOP

These are MS drivers that connect to data blocks allocated by ALLOCATE. [block] INBLOCK sets MS input to the start of the specified block. [block] OUTBLOCK sets MS output to the start of the specified block. GETIP pushes the current input byte pointer (where the next byte will be read from), [byte offset] SETIP sets the input byte pointer (where the next byte will be written to). GETOP and SETOP do the same for the output byte pointer.

? 0 OUTBLOCK
? "HELLO BLOCK" MS$OUT MSCRLF
? "LINE TWO" MS$OUT MSCRLF
? 0 INBLOCK
? MS$IN $PRINT
HELLO BLOCK
? MS$IN $PRINT
LINE TWO
? 4 SETIP MSBIN PCHR
O
? GETIP PNUM
000005
? 1 SETOP "A" MS$OUT 0 SETIP MS$IN $PRINT
HALLO BLOCK
? MSPAPER

MS_SAVE  MS_RESTORE

MS_SAVE saves the current MS input and output vectors to internal memory. MS_RESTORE restores the previous MS input and output vectors. These can be used to temporarily save the current settings then restore the settings afterwards, but there is only one pair of temp variables so they cannot be nested, use sparingly. Primary uses are so words used at the console that use MS for their own purposes can save and restore the MS settings, and to make up for a side-effect of ZEROBLOCK, which leaves MS output set to a memory block (but if ZEROBLOCK saved/restored itself it would make the word about 7 locations larger and every location counts, if it made use of MS_SAVE/RESTORE then it would disrupt other code that saves/restores upon entry/exit, best to leave it alone - sometimes the compact way is a bit odd).

$DEFADR

The $DEFADR word pops a string containing a word name, then pushes the word's entry address, also called the word address (WA). If the word isn't in the dictionary it pushes a 0. For high-level IPL words WA contains the address of the "enter secondary" code which starts the threaded interpreter on another branch, ending when the address for "return from secondary" is encountered. For low-level machine-coded words, WA contains the actual address of the code to run, which exits by making an indirect jump to the "next" code. The word's header is the 4 locations before WA - length of the word's name, the first 2 characters, the next 2 characters, then a link to the next header. If the length is 0 then the end of the dictionary has been reached.

This is a rather low-level function but can be very useful for certain things. Since it indicates where a word is, it provides a means to remove words (the only way using only the kernel), make self-deleting words (handy for packages that need to run a temporary configurator or check EOD to make sure machine code won't cross a 1KW boundary), patch constants or even change words within words, alter the link so that an application can hide its variables and subroutines, and execute other words regardless of where (or if) they appear in the dictionary.

? DEFINE TEMP
> "TEMP" $PRINT END
? "TEMP" $DEFADR PNUM
011540
? "TEMP" $DEFADR 4 SUB 0 PUT ;REMOVES TEMP AND ALL AFTER
? "TEMP" $DEFADR PNUM
000000
? DEFINE APP
> DEC ;WILL REPLACE WITH WA OF MAIN WORD
> END
? VARIABLE V1
? VARIABLE V2
? DEFINE SUB
> "IN THE SUB" $PRINT END
? DEFINE MAIN
> SUB ;BLA BLA
> END
? "APP" $DEFADR INC "MAIN" $DEFADR PUT ;REPLACE DEC WITH MAIN
? "APP" $DEFADR DEC "MAIN" $DEFADR DEC GET PUT ;WRAP APP AROUND MAIN
? APP
IN THE SUB
? EOD PNUM "APP" $DEFADR DEC GET PNUM ;MAKE SURE APP LINK=EOD
011615 011615
? WORDS
...edited...
ADDMxxxx VARIxxxx CONSxxxx ALLOxxxx ZEROxxxxx BPUT BGET APP
EOD=011615 FREE=002162
?

RUN  EXECUTE  WBOOT

RUN pops the stack and directly jumps to that address, as with low-level words, code can return to HP-IPL/OS by executing 124321 or an indirect jump to "next". 2 RUN restarts HP-IPL/OS. EXECUTE pops the stack and sets the threaded interpreter to that address, can be used to run words by the address returned by $DEFADR. WBOOT warm-boots the system, stacks are reset and the prompt is displayed, but autostart words are not run making it a handy way to exit a word back to the prompt.

? 120 102000 PUT
? 121 124321 PUT
? DEFINE RUNTEST
> 120 RUN
> "WORKED" $PRINT END
? RUNTEST

HALT instruction 102000, P: 00121 (JMP 321,I)
sim> c
WORKED
? 120 RUN

HALT instruction 102000, P: 00121 (JMP 321,I)
sim> c
? DEFINE TEMP
> "TEMP" $PRINT END
? "TEMP" $DEFADR EXECUTE
TEMP
? DEFINE WTEST
> IFZ WBOOT ENDIF
> PNUM END
? 1 1 1 DMPS

000001
000001
000001

? 1 WTEST
000001
? 0 WTEST

? DMPS
? 2 RUN

HP-IPL/OS 8K V1.5
?

#0  #1

These are constants that push 0 or 1, taking up one less location than using literal 0 or 1.

Making a HLT word

The stock kernel doesn't provide a ready-made way to pause the system so that a single serial port can be shared between the console and the PTR emulator. The kernel does however provide a way to create a machine-coded word manually... if building a system on real hardware with only a single serial port for both the terminal and a pass-through PTR emulator, type this in...

? OCTAL
? "HLT" ADDHEADER$ @DIPTR GET INC ADDCODE
? 102007 ADDCODE 124321 ADDCODE FIXLINKS

The ADDHEADER$ word pops a string containing a word name and starts a word header, leaving @DIPTR set to the new word's WA location. The ADDCODE word pops the stack, writes it to the address specified by @DIPTR, then increments @DIPTR. For a machine-coded word WA = address of machine code to run, usually the next address so @DIPTR GET INC ADDCODE puts WA+1 in the WA location. Next ADDCODE is used to add 102007, the halt instruction, then 124321, an indirect jump to the "next" vector. Finally FIXLINKS is used to pop info pushed by ADDHEADER$ to set the word name length of the HLT word, adjust the next word link to point to the end of dictionary and put a zero there. Very low level stuff but handy when a bit of machine code is needed - if doing anything that branches @DIPTR GET PNUM can be used to display the current code pointer to pack the jump and memory reference instructions correctly. For true hackers only, but HLT is useful - to load an IPL package with a single serial port enter HLT <PTR then close the terminal emulator, swap the serial cable to the PTR emulator, launch the PTR file transfer program, press the Run switch, when the lights stop blinking and sure the load is complete, close the transfer program, swap the serial cable back to the console, launch the terminal emulator and press Enter.


The "Extra" words

While the kernel supports complex programming (all of HP-IPL/OS derives from it), it doesn't provide many tools for examining and manipulating the dictionary (WORDS is about it). The extra.ipl package provides a set of commonly-used words, mostly command-line tools but also additional words used by other packages. The reason for putting these words in a separate package is for versatility, systems built for different purposes don't need all of the words in extra.ipl (some of which I never use but others do so can't remove them).

Presently there are at least four versions of the extra words...

extra.ipl - the original version with all the extras and an interactive memory configurator
extra2.ipl - a more compact version that omits some words and has no configurator
8kextra.ipl - a very trimmed down version for 8KW that includes just a few command-line tools
8kextra2.ipl - another version for 8KW block-less builds that puts back the programming words

My favorite is extra2.ipl, it provides all the extras that other packages use, plus the original extra.ipl cannot be loaded if only a single serial port is available (can't use a menu when the only serial cable is connected to the PTR emulator). For really trimmed down builds the 8kextra2.ipl package can be used even for larger memory builds, it has the usual programming extras and a functional set of "compacted" tools that don't confirm or print error messages.

Note - when using an alternate extra package that doesn't prompt for configuration, after loading use
@END [last addr] PUT [#blocks] ALLOCATE to configure memory as needed.
Examples from extra2.ipl (these allocate 2 blocks)...

; For 16KW, no himem:  @END 37677 PUT 2 ALLOCATE
; For 16KW, 1KW himem: @END 35777 PUT 2 ALLOCATE
; For 16KW, 2KW himem: @END 33777 PUT 2 ALLOCATE
; For 32KW, 4KW himem: @END 67777 PUT 2 ALLOCATE

The memory indication in the sign on message is automatically altered when using extra.ipl's configurator, to alter the message after manual memory adjustment change location 2010...

? 2010 "32" $ADR GET PUT $DROP
? 2 RUN

HP-IPL/OS 32K V1.6

Change the "32" to the amount of memory configuring for (the $DROP just removes the string, not needed if 2 RUN done to verify). Use VERSION from the version.ipl package to change the entire string or otherwise place packed text in locations 2003-2014.

Configuring a 32KW system with 4 blocks and 4KW high mem using the original extra.ipl package...

HP-IPL/OS  8K   V1.6
?
Simulation stopped, P: 02225 (SFS 11)
sim>attach -e ptr ../ipl/extra.ipl
sim>c

? <PTR
Loading Configurator
Select configuration...
1) keep existing setup
2) 16K w/ 2 blocks
3) 16K w/ 2 blocks and 2K hi-mem
4) 32K w/ 4 blocks and 4K hi-mem
> 4
Configured to 32K w/himem
Loading $EQUAL
Loading $SLICE
Loading $TRIM
Loading ABSOUT
Loading PTZERO
Loading MARKCON
Loading SETCON
Loading GLOBAL
LOADING DUMP
LOADING PDEF
LOADING EXPLAIN
Loading FORGET
Loading ERASE
Loading FETCH
Loading STASH
Loading HIDEDUPS
Loading RENAME
Loading DELETE
Loading UNDELETE
Loading WHEREIS
Loading LOAD
Done
? WORDS
DO +DO INDEx +LOOx >STEx IFNZ IFZ IF<0 ENDIx ELSE UNTIx WHILx CASE = < > <=
>= <> DEFAxxx ENDCxxx EXECxxx WBOOx AND OR XOR ADD SUB INC DEC NOT 2CPL DUP
DROP OVER ROT SWAP GET PUT PNUM CRLF DECIxxx OCTAx BINAxx RADIx SP>S SB>S
XP>S XB>S YP>S YB>S ZP>S ZB>S END EOD DEFIxx DMPS S>SR SR>S PCHR PWRD CHRIx
S>X X>S S>Y Y>S S>Z Z>S MUL ASL ASR ROL ROR DIV RUN X>>Y X>>Z Y>>X Z>>X
$PRIxx $SWAx $CPY $DUP $DROx $LEN $ADR $XTExx $PUT $GET $CRExxx $STR $HEAx
$APPxxx $TAIx $IN $CAT $VAL <>COx >PTP <PTR MSPAxxx MSBOxx MSBIx MSWOxx
MSWIx MS$Oxx MS$Ix MSCRxx >MS <MS MS_Sxxx MS_Rxxxxxx CONSxxx #0 #1 @TL @TB1
@TB2 @ANVxx @LLP @ENSxx @CLH @LITxxxx @STRxxx @RTSxx @DIC @USR @BLK @END
@DIPxx RND TOKEx SDIC +IRQ -IRQ +AUTx -AUTx ADDCxxx INBLxxx OUTBxxxx GETIx
SETIx GETOx SETOx $DEFxxx HEADxxx WORDx ADDHxxxxx ADDHxxxxxx FIXLxxxx
ADDMxxxx VARIxxxx CONSxxxx ALLOxxxx ZEROxxxxx BPUT BGET $EQUxx $SLIxx $TRIx
ABSOxx PTZExx MARKxxx SETCxx GLOBxx DUMP PDEF EXPLxxx FORGxx ERASx FETCx
STASx HIDExxxx RENAxx DELExx UNDExxxx WHERxxx LOAD
EOD=014627 FREE=043150
?

Configuring a 32KW system with 4 blocks and 4KW high memory using the extra2.ipl package...

HP-IPL/OS  8K   V1.6
?
Simulation stopped, P: 02225 (SFS 11)
sim>attach -e ptr ../ipl/extra2.ipl
sim>c

? <PTR
Loading $EQUAL
Loading $SLICE
Loading $TRIM
Loading ABSOUT
Loading PTZERO
LOADING DUMP
LOADING PDEF
LOADING EXPLAIN
Loading FORGET
Loading ERASE
Loading FETCH
Loading STASH
Loading RENAME
Loading WHEREIS
Loading LOAD
Done
? @END 67777 PUT 4 ALLOCATE
? 2010 "32" $ADR GET PUT $DROP
? 2 RUN

HP-IPL/OS 32K V1.6
? WORDS
DO +DO INDEx +LOOx >STEx IFNZ IFZ IF<0 ENDIx ELSE UNTIx WHILx CASE = < > <=
>= <> DEFAxxx ENDCxxx EXECxxx WBOOx AND OR XOR ADD SUB INC DEC NOT 2CPL DUP
DROP OVER ROT SWAP GET PUT PNUM CRLF DECIxxx OCTAx BINAxx RADIx SP>S SB>S
XP>S XB>S YP>S YB>S ZP>S ZB>S END EOD DEFIxx DMPS S>SR SR>S PCHR PWRD CHRIx
S>X X>S S>Y Y>S S>Z Z>S MUL ASL ASR ROL ROR DIV RUN X>>Y X>>Z Y>>X Z>>X
$PRIxx $SWAx $CPY $DUP $DROx $LEN $ADR $XTExx $PUT $GET $CRExxx $STR $HEAx
$APPxxx $TAIx $IN $CAT $VAL <>COx >PTP <PTR MSPAxxx MSBOxx MSBIx MSWOxx
MSWIx MS$Oxx MS$Ix MSCRxx >MS <MS MS_Sxxx MS_Rxxxxxx CONSxxx #0 #1 @TL @TB1
@TB2 @ANVxx @LLP @ENSxx @CLH @LITxxxx @STRxxx @RTSxx @DIC @USR @BLK @END
@DIPxx RND TOKEx SDIC +IRQ -IRQ +AUTx -AUTx ADDCxxx INBLxxx OUTBxxxx GETIx
SETIx GETOx SETOx $DEFxxx HEADxxx WORDx ADDHxxxxx ADDHxxxxxx FIXLxxxx
ADDMxxxx VARIxxxx CONSxxxx ALLOxxxx ZEROxxxxx BPUT BGET $EQUxx $SLIxx $TRIx
ABSOxx PTZExx DUMP PDEF EXPLxxx FORGxx ERASx FETCx STASx RENAxx WHERxxx
LOAD
EOD=013547 FREE=044230
?

The more compact extra2.ipl package provides about 0.5KW more room in the dictionary by leaving out some of the old console words I rarely if ever use...

MARKCON SETCON GLOBAL - a way to selectively hide/restore words (don't need)
HIDEDUPS [wordname] - renames duplicates to a space to hide (dups don't bother me)
DELETE [wordname] - renames a word to space to hide (don't need)
UNDELETE - prompts to rename words named to a space (don't need)


FORGET

FORGET [wordname] checks to see if a word exists, if it does it asks to remove the word and all words that follow it. If Y is pressed it does it. The 8KW versions don't confirm. If a driver is removed (in particular a console driver like !NOLF), immediately do 2 RUN to restart the system to make sure the driver's code is unhooked and no longer executing before adding more code to the dictionary.

? DEFINE TEST
> "TEST" $PRINT END
? TEST
TEST
? FORGET TEST
Forget TEST and everything after it? Y
Done
? TEST
TEST NOT FOUND
?

ERASE

ERASE is like an automatic forget for entire IPL packages. Whenever <PTR, <MS, LOAD or any other MS-based load is used, a variable named @LLP (last load point) is set to the end of the dictionary before the load. ERASE checks @LLP and if pointing to a valid word, prompts to erase from that word. This is handy for removing a failed load when an error occurs (word not found, page error, dictionary overflow, etc), or loading a package for temporary use. As with FORGET, if ERASE is used to remove a package containing a driver, best to follow by 2 RUN to reset HP-IPL/OS and make sure the the driver is no longer active.

HP-IPL/OS 32K   V1.6
?
Simulation stopped, P: 02226 (JMP 2225)
sim>attach -e ptr ../ipl/screen.ipl
sim>c

? LOAD
$DST NOT FOUND
? ERASE
Erase from ESC (Y/N)? Y
Done
?

By default, all supplied builds (except for the kernel and 8K build) are set so that if no loads are performed the default ERASE action is to remove any new code typed into the build. When making a new build, this default can be set by entering @LLP EOD PUT before saving the new build. The default erase point can be set to any word by doing @LLP "wordname" $DEFADR 4 SUB PUT (be careful typing these) or set to be after a specific word by doing @LLP "wordname" $DEFADR DEC GET PUT. To force a default erase point after a coldboot/restart, define an autostart word (a word beginning with !) that sets @LLP as desired.

EXPLAIN  PDEF

EXPLAIN [wordname] lists the contents of high-level words in a format that can be re-entered back into the dictionary. Not only is this useful for examining words, but can be used to dump words elsewhere (usually blocks) to permit rearranging the dictionary. This only works for high-level words, machine-coded words can't be explained and have to be reassembled to relocate. EXPLAIN works only from the console (like all words that take parameters after the command). PDEF pops an address from the stack that represents WA (from $DEFADR) and lists the definition, it's what EXPLAIN calls to do the actual explaining.

? EXPLAIN EXPLAIN
OCTAL DEFINE EXPLxxx ;ENSEC=012524
TOKEx IFNZ "Need name" $PRIxx ELSE SDIC IFZ "Not found"
$PRIxx ELSE PDEF ENDIx ENDIx END

? "EXPLAIN" $DEFADR PDEF
OCTAL DEFINE EXPLxxx ;ENSEC=012524
TOKEx IFNZ "Need name" $PRIxx ELSE SDIC IFZ "Not found"
$PRIxx ELSE PDEF ENDIx ENDIx END

If only high-level words exist after a word that needs to be removed (no variables, constants or machine-coded words), and they'll fit into the allocated blocks in source form, then a word can be removed while keeping words after it...

? DEFINE W2
> END
? DEFINE W3
> "W3" $PRINT END
? DEFINE W4
> "W4" $PRINT END
? ;REMOVE W2 WHILE KEEPING W3 AND W4
? 0 OUTBLOCK >MS ;REDIR MS TO BLOCK 0 THEN REDIR CONSOLE TO MS
? EXPLAIN W3
? EXPLAIN W4
? "CONSOLE" $PRINT CRLF
? CONSOLE ;RESTORE NORMAL CONSOLE OUTPUT
? FORGET W2
Forget W2 and everything after it? Y
Done
? 0 INBLOCK <MS ;RELOAD W3 AND W4
? W3
W3
? W4
W4
? W2
W2 NOT FOUND
?

RENAME

RENAME [wordname] [newname] changes the name of a word. This does not affect the operation of other words already in the dictionary that use the word, as words are stored by address, not by name.

? DEFINE DEC
> "DAC" $PRINT END
? DAC
DAC NOT FOUND
? RENAME DEC DAC
? DAC
DAC
?

STASH  FETCH

[block] "wordname" STASH copies a word using PDEF to the specified block, ending with CONSOLE. [block] FETCH reloads the word saved by STASH. If only a few words need to be saved when removing middle words from the dictionary, and there are enough blocks, this is easier than the manual method shown in the EXPLAIN example.

? 0 "DAC" STASH
? FORGET DAC
Forget DAC and everything after it? Y
Done
? 0 FETCH
? DAC
DAC
?

Note - CREATE uses blocks 0 and 1 so if using this technique (or the manual method) with machine-coded packages block 2 or higher must be used to save words.

It wouldn't be difficult to make a multi-word stasher using TOKEN and SDIC kernel words, TOKEN gets the next parameter and sets @TL @TB1 and @TB2 to the length and first 4 characters and pushes a 0, if no more parameters  then it pushes a 1. SDIC searches the dictionary for a word matching @TL @TB1 @TB2 and if found pushes the word address twice, otherwise pushes a single 0. A multi-stasher would have to take parms both before and after the command name so the block plus a variable number of word names can be specified. Something like...

DEFINE MSTASH ;usage: block MSTASH word1 word2 etc
MS_SAVE OUTBLOCK >MS
DO
TOKEN IFNZ #1 ELSE
SDIC IFZ CONSOLE "Not found" $PRINT WBOOT ELSE
 PDEF #0
ENDIF
ENDIF
UNTIL
"CONSOLE" $PRINT CRLF CONSOLE MS_RESTORE
END

FETCH can be used to fetch the combined package.

WHEREIS

WHEREIS [wordname] displays the word address and the memory occupied by a word and its header.

? WHEREIS DAC
Word address = 013553
Occupies 013547 to 013562
?

DUMP

[start] [end] DUMP displays memory from start to end in octal and text. DUMP always switches to octal mode regardless of the radix when entering, not restored.

? 13547 13562 DUMP

13547:000003 042101 041440 013563 002775 003572 000003 042101 ..DAC .s...z..DA
13557:041400 000003 006314 003002 000000 000000 000000 000000 C...............

?

$EQUAL  $SLICE  $TRIM

$EQUAL pops a string and compares it to the next string, which is left on the string stack. If the strings are equal it pushes a 1, if not it pushes a 0. [from] [to] $SLICE pushes a substring of a string on the stack, the original string is left on the string stack. $TRIM pops a string, removes leading and trailing spaces and pushes the trimmed string.

? "STRING" "STRING" $EQUAL PNUM $PRINT
000001 STRING
? "STRING" "STRING2" $EQUAL PNUM $PRINT
000000 STRING
? "STRING" 2 5 $SLICE $PRINT CRLF $PRINT
RING
STRING
? " TRIN " $TRIM "S" $PRINT $PRINT "G" $PRINT
STRING
?

ABSOUT  PTZERO

[from] [to] ABSOUT outputs memory to MS output in ABS format. PTZERO writes 20 zeroes to MS output to use for a leader and trailer, some PTR emulators or software require leading zeroes, trailing zeroes are required to terminate an ABS load. These can be used to save a system to papertape or whatever MS is set to.

? 0 MSTASH RENAME WHEREIS LOAD MSTASH
? FORGET STASH
Forget STASx and everything after it? Y
Done
? 0 FETCH
? WORDS
DO +DO INDEx +LOOx >STEx IFNZ IFZ IF<0 ENDIx ELSE UNTIx WHILx CASE = < > <=
>= <> DEFAxxx ENDCxxx EXECxxx WBOOx AND OR XOR ADD SUB INC DEC NOT 2CPL DUP
DROP OVER ROT SWAP GET PUT PNUM CRLF DECIxxx OCTAx BINAxx RADIx SP>S SB>S
XP>S XB>S YP>S YB>S ZP>S ZB>S END EOD DEFIxx DMPS S>SR SR>S PCHR PWRD CHRIx
S>X X>S S>Y Y>S S>Z Z>S MUL ASL ASR ROL ROR DIV RUN X>>Y X>>Z Y>>X Z>>X
$PRIxx $SWAx $CPY $DUP $DROx $LEN $ADR $XTExx $PUT $GET $CRExxx $STR $HEAx
$APPxxx $TAIx $IN $CAT $VAL <>COx >PTP <PTR MSPAxxx MSBOxx MSBIx MSWOxx
MSWIx MS$Oxx MS$Ix MSCRxx >MS <MS MS_Sxxx MS_Rxxxxxx CONSxxx #0 #1 @TL @TB1
@TB2 @ANVxx @LLP @ENSxx @CLH @LITxxxx @STRxxx @RTSxx @DIC @USR @BLK @END
@DIPxx RND TOKEx SDIC +IRQ -IRQ +AUTx -AUTx ADDCxxx INBLxxx OUTBxxxx GETIx
SETIx GETOx SETOx $DEFxxx HEADxxx WORDx ADDHxxxxx ADDHxxxxxx FIXLxxxx
ADDMxxxx VARIxxxx CONSxxxx ALLOxxxx ZEROxxxxx BPUT BGET $EQUxx $SLIxx $TRIx
ABSOxx PTZExx DUMP PDEF EXPLxxx FORGxx ERASx FETCx RENAxx WHERxxx LOAD
MSTAxx
EOD=013533 FREE=044244
?
Simulation stopped, P: 02225 (SFS 11)
sim>attach ptp ../../mybuild.abs
PTP: creating new file
sim>c

? PTZERO 2 EOD ABSOUT PTZERO
?
Simulation stopped, P: 02225 (SFS 11)
sim>detach ptp


The CREATE Word Assembler

IPL code can be used to program lots of things, but it cannot directly access hardware or make use of additional features of the later HP2100-series machines such as DMS (dynamic memory system), floating point or other extended instructions. For these things machine-code is needed. The indirectly threaded interpreter is fairly fast but is still interpreted - for each word in a definition it has to get the address representing the word, get the contents of that address representing the address of some machine code to run, run that code which has to exit by jumping to the next code, which then starts the process over. For high level words, the first address is for "enter secondary" which pushes where it was to the return stack so it can start interpreting the words in the sub-word, then exit with an address for "return from secondary" that pops the return stack to continue interpreting from where the word was called from. Yikes! all that happens for almost every word encountered during execution, it's a wonder it achieves reasonable speed at all. IPL does OK but sometimes recoding a function in machine code can result in greatly improved execution speed.

CREATE comes in two flavors - the full version in the create.ipl package presently uses 6371 octal locations (about 3.25KW) and supports most extended and 21MX instructions, the small version in the smallcre.ipl package presently uses 5156 octal locations (about 2.6KW) and supports only the original HP2116 instruction set. Both versions require at least 2 data blocks (2KW) for storing symbols and fixup addresses.

The full version also creates a PRESET word which (sort of) simulates pressing the Preset button, sometimes needed to make loaded binaries work correctly. This word disables interrupts, tells HP-IPL/OS that it disabled interrupts, does a brute force CLC slot STF slot for slots 6 to 57, and clears the overflow flag. Quite possibly overkill. If a PRESET is needed when using the small CREATE, or if your needs are simpler and you would rather save memory, the following version of PRESET "should" work for loading "most" software...

CREATE PRESET
CLF 0
CLA
STA 461
CLC 0
CLO
END

The 21MX manual says CLC 0 clears control for all devices, but does not say it sets all flags as happens when actually pressing the Preset button. Most things shouldn't care but who knows. HP-IPL/OS does its own CLC 0 on startup so this isn't needed to load a HP-IPL/OS build. Of course to load a binary you'll need some loader code up in high memory, something that reads ABS data for loading binaries from papertape, or if using a 21MX machine, the main/alt swapper/runner program put into memory at 77000 by the ALTSAVE word in the DMS package, then just have to load a binary into alt mem and jump to 77000 to run the binary from location 2. Jump to 77000 manually or from the binary being run to swap HP-IPL/OS back in, leaving the binary and whatever was in its data buffers in alt mem. Anyway...

CREATE is composed of many words and variables, to avoid cluttering the dictionary and prevent running the internal words, the package wraps the initial CREATE word around all the internal components and patches the initial word to run the main word. Here's what the create.ipl package looks like when loading...

?
Simulation stopped, P: 02225 (SFS 11)
sim>attach -e ptr ../ipl/create.ipl
sim>c

? LOAD
Loading CREATE and support
Loading A=B
Loading A>=B
Loading PUTSYMBOL
Loading FINDSYMBOL
Loading PUTFIX
Loading GETFIX
Loading CONVERT_TO_ADDRESS
Loading IHASH
Loading 2D1
Loading MATCHSIMPLE
Loading MATCHIO
Loading MATCHEXT
Declaring ISLOT ITYPE
Loading TEST_ASG
Loading S_A S_B S_ASG S_1 S_2 S_3 S_4
Loading DROP_B
Loading ICOMPOUND
Loading PARSE_FRAGMENT
Loading MATCHCOMPOUND
Loading $ML2CODE
Loading MAIN
Folding into CREATE
Loading PRESET
022006 :
...etc, addresses depend on where loading to
022040 :
Fixing...
Done
?

Using CREATE

Usage (here [] means optional): CREATE wordname [/K] [/L]

The /K option (as in CREATE wordname /K) tells CREATE to keep the previous symbol table, useful when other words need to access memory or code within a previous word. The /L option (full version only) tells CREATE to use all data blocks for symbols and addresses.

Here's a simple usage from the command prompt...

? CREATE HLT
020633 : HLT 7
020634 : END

? HLT "CONTINUING" $PRINT

HALT instruction 102007, P: 20634 (JMP 321,I)
sim>c
CONTINUING
?

Typical IPL package format for CREATE code...

;comments
"Loading SOMEWORD" $PRINT CRLF
CREATE SOMEWORD
* comments
symbol EQU value comments
label code          comments
code comments
...etc
END
;comments
"Loading ANOTHERWORD" $PRINT CRLF
CREATE ANOTHERWORD /K
code etc
END
;high level words...
DEFINE APPWORD
ipl code
END
"Done" $PRINT
CONSOLE

Instructions must be entered in uppercase. Numbers are always octal (it switches to OCTAL mode upon start) except when using DEC to specify a decimal value in a memory location. Labels can be up to 6 characters long and must begin in the first column (in an IPL file or entered text, not counting the prompt). Code must begin in the second column, or if preceded by a label there must be at least one space between the label and the instruction. Comments must be placed well away from the code to avoid a "Parse error", at least column 11 or greater and at least 9 positions past the beginning of the instruction. When the END directive is encountered, a JMP ZNXT,I (ZNXT=321) is assembled to jump to HP-IPL/OS' "next" code and assembly stops. END also places an END label at the exit instruction so the easy way to exit a word to skip data at the end is to use JMP END, although a couple microseconds can be saved by using JMP ZNXT,I instead. If there is no data to skip then no extra instructions are needed. If any references to labels were used, once the code has been entered "Fixing..." is displayed while it sifts through all the references replacing them with addresses.

The fixing process can take a long time so if loading on real hardware via a transfer program (especially when using a single port where the console cannot be seen) make sure plenty of time is given before assuming the load is finished and closing the transfer program. This delay also means that when pasting CREATE code into the console using character/line delays (i.e. HyperTerminal), only one word can be pasted at a time.

The instruction format is similar to ASMB etc except the only supported directives are EQU and END. DEF, OCT, DEC and ASC,n (n=#locations in decimal, syntax is different from ASMB) can be used to reserve and specify memory locations, these must begin in column 2 or greater. If an odd number of characters is used in an ASC psuedoinstruction, the low byte of the last word will contain a space (some HP assemblers may encode a 0 byte instead). If B is appended to numbers (used by ASMB to specify octal) it is ignored. There obviously is no ORG, the code is assembled to where EOD happens to be plus the header and WA location (if you need code in a specific location, use a cross-assembler and load it into high-memory somewhere where it won't conflict - if the machine code ends in JMP 321,I it will still behave like a word when executed). The "*" character represents the current location, as in DEF *+1 etc but only simple +/- offsets are supported and the offset number must be specified in octal (invalid octal numbers evaluate to 0, no error indicated).

Multi-location instructions must be coded on multiple lines using OCT to specify the additional locations and contents, for example a DIV instruction would be coded DIV on one line followed by DEF address pointing to the value to divide by. Examine the instruction reference manual carefully, some instructions like CBT and MBT require a 3rd OCT 0 line that's not apparent unless the text for the instruction is read, not shown in the charts.

Avoiding page errors

As with all HP21xx machine code, memory references must be either to zero page or the current 1KW page, so generally CREATE code should not cross a 1KW boundary (22000, 34000, 36000, or any octal address with an even number in the 4th digit from the right). If there are any references to locations beyond the current page or zero page then "Page error" is displayed during the fix process along with the errant references and the word is not created. The easiest way to avoid page errors is to keep track of the end of dictionary (EOD, displayed by WORDS or EOD PNUM) to make sure there's enough room for the code before reaching a 1KW boundary. If not, load a high-level word or package first, or if memory usage isn't a concern DEFINE ~PAD [#locations needed]. Another technique is to write mostly relocatable code where all references are very close or done indirectly through a table at the beginning of the code - but this wastes memory.

When building a system, page errors are not much of an issue if EOD is checked and a bit of thought is put into the loading process - builds are usually composed of many packages, some mostly machine-coded, some mostly or entirely plain IPL, and some packages that aren't dependent on the machine-coded packages (or at least packages not already loaded successfully). When a page error occurs, use ERASE to remove the partial load, then load one or more non-dependent high-level packages so that when machine-coded packages are really needed EOD is sitting at or just after a 1KW boundary. It's not that difficult and in trade for sometimes having to think a bit you get more dictionary space for doing more stuff. If you can't be bothered with figuring out a working load order, then at least check EOD before loading a package containing CREATE code and if it looks like it's not going to fit within the current page, make a dummy variable to push EOD into the next page.

Where page errors really are an issue is when creating IPL applications (as opposed to building a base system) where the thing runs from a menu or a library file (run and forget) and there's no telling where EOD will be when loaded. In this case, memory usage usually isn't an issue since once the application is run it'll be removed, but load failure is very bad. The solution is code the IPL package so that it checks EOD itself and if not enough room, adds a pad variable just big enough to get past the boundary. Here's one of the page-crossing fixes from fed.ipl...

;----------------------------------------------------------
; LNLEN requires ~30 octal words, do the page-crossing thing...
2000 EOD 1777 AND SUB 34 SUB ;push difference, if < 0 then...
VARIABLE ~PAD 15
;enough to push to start of next 1KW page under all conditions
DEFINE TEMP
IF<0
"Declaring ~PAD to cross page boundary" $PRINT CRLF
"TEMP" $DEFADR 4 SUB #0 PUT ;delete just TEMP
ELSE
"~PAD" $DEFADR 4 SUB #0 PUT ;EOD ok, delete ~PAD and TEMP
ENDIF END
TEMP
"Creating LNLEN..." $PRINT CRLF
CREATE LNLEN ;2/18/08
* ENTRY: MEMADR ON STACK
* EXIT: LENGTH OF TEXT LINE ON STACK
JSB ZSPOP,I GET MEMADR
ADA C38 ADD 38 DEC (LAST WORD)
LDB C39 GET 39 DEC (MAX LEN)
STB LNLEN SAVE IN LNLEN TO RETURN
LOOP LDB 0,I GET WORD POINTED TO BY A
CPB SPACES DOUBLE SPACES?
JMP DECPTR YES - DECREMENT LEN AND PTR
JMP RETVAL NO - RETURN VALUE
DECPTR ADA CM1 SUBTRACT 1 FROM POINTER
LDB LNLEN GET LINE LENGTH
ADB CM1 SUBTRACT 1 FROM LENGTH
STB LNLEN SAVE BACK
SZB SKIP IF 0 (DONE)
JMP LOOP
RETVAL LDA LNLEN GET LEN
JSB ZSPSH,I PUSH TO STACK
JMP ZNXT,I EXIT WORD
LNLEN OCT 0 LINE LEN
C38 DEC 38
C39 DEC 39
CM1 DEC -1
SPACES OCT 20040
END
;----------------------------------------------------------

Basically the technique is to count the octal number of memory locations needed, then do...

2000 EOD 1777 AND SUB [#locations + 4] SUB
VARIABLE ~PAD [#locations - 13 octal]
DEFINE TEMP
IF<0
"Declaring ~PAD to cross page boundary" $PRINT CRLF
"TEMP" $DEFADR 4 SUB #0 PUT ;delete just TEMP
ELSE
"~PAD" $DEFADR 4 SUB #0 PUT ;EOD ok, delete ~PAD and TEMP
ENDIF END
TEMP

How it works - at command line level it subtracts the 1KW portion of EOD (EOD 1777 AND) from 2000 octal, then subtracts the required number of locations plus the header from that, leaving the results on the stack. Next it makes a ~PAD variable with enough locations specified so when added to the variable overhead pushes EOD far enough ahead so the machine code will be in the next page once CREATE adds the word header. Next it defines a TEMP word that checks to see if the test SUB left on the stack is negative, if it is it displays a message and uses the $DEFADR manual forget technique to delete just the TEMP word, leaving ~PAD in place. If the SUB test is >=0 then it forgets ~PAD too, not needed.

Predefined symbols

CREATE predefines many symbols that can be used to access internal HP-IPL/OS variables and code routines. Some are pretty easy to use like JSB ZSPOP,I to pop the stack to the A register and JSB ZSPSH,I to push A to the stack. Some are a bit trickier but useful like JSB PSUBA,I with A set to the address of a patch list (must end with 0) and B set to a slot number to patch into the locations - that one is the basis for almost everything that self-patches using an autostart word.

Symbol   Loc   Description
------ --- -----------
ZNXT 321 indirect NEXT vector
ZSPSH 323 indirect S push
ZSPOP 324 indirect S pop
ZRPSH 325 indirect R push
ZRPOP 326 indirect R pop
ZXPSH 327 indirect X push
ZXPOP 330 indirect X pop
ZYPSH 331 indirect Y push
ZYPOP 332 indirect Y pop
ZZPSH 333 indirect Z push
ZZPOP 334 indirect Z pop
ZCOUT 337 indirect char-out vector
ZCHIN 341 indirect char-in vector
ZCRLF 340 indirect crlf-out vector
ZPTWD 342 indirect word-out vector
ZPBFL 343 indirect string-out vector - addr in B, #words in A
ZIN 346 indirect standard-in vector
ZOUT 347 indirect standard-out vector
ZMINP 350 indirect MS in vector
ZMOUT 351 indirect MS out vector
ISAVE 475 indirect state-save/-irq vector
IREST 476 indirect state-restore/+irq vector
TMRLO 473 location of timer low (tbg)
TMRHI 474 location of timer high (tbg)
TMP1 311 temp locations
TMP2 312
TMP3 313
TMP4 314
SP 301 S stack pointer
RP 302 R stack pointer
XP 303 etc
YP 304
ZP 305
BM2A 463 address of sign-on string
ASENA 465 auto-start enable if <>0
TXTWP 466 text-was-printed flag
PSUBA 470 patch sub address - addr in A, slot# in B
CHINS 471 actual chrin sub address
CHOUS 472 actual chrout sub address
CON1 433 contains -1
IENAV 462 address of int.enable sub
ABFLG 260 alternate boot flag
ABVEC 261 alt boot vector
WDENA 262 watchdog enable
WDTMR 263 watchdog timer
WDTOV 264 watchdog timeout value

These don't cover all of the zero-page locations (such as 461 which contains the global interupt enable flag), and some are probably never used, but these are the locations that have been predefined since CREATE was created so not messing with it. If making a custom version, each symbol deleted from the create.ipl source saves about 9 locations.


Last modified 6/9/2010