S/SX AND G/GX SOFTWARE DEVELOPING TIPS

Last Updated: September 21, 1993

This document provides methods for writing assembly language software that will
work on all ROM versions of the HP48 (S, SX, G, or GX).  Many of these methods
may be used for system-RPL programming as well, but the main focus of the
document is for assembly language developers.  All of the ROM entry points
used in this document are supported by Hewlett-Packard.

It is assumed that you understand basic sys-RPL commands as well as the
Saturn assembly instruction set.

This document could easily mushroom into a 200 page manual describing
every detail of the problem at hand.  Rather than try to accomplish
this, I will present these methods that will work under certain
conditions.  You can choose the methods that work best for your
application, or use the ideas to taylor these methods to work even
better for you.

I have emailed this document to the following people, and all of
them have made corrections and additions.  As I still consider myself
a student in these programming areas, I greatly appreciate the support
and help that I received from these people:

Joe Ervin
Mohamed Fatri
Mika Heiskanen
Detlef Mueller

As a result of the help from these people, this document is written
mainly by them.  I started the idea, and I have compiled these routines
and ideas.  I didn't want help from too many people, because then I
would have to choose between 10 or more routines and ideas for each
concept.  To create this document as quickly as possible, and to make it as
effective as possible, I have tried to limit its input.  However, I do
hope to support this document, so if you have some ideas or
suggestions, then please send them to me.  I will post any updates to
this document.

Thanks,

Douglas R. Cannon
dougc@bert.cs.byu.edu
-----

Why do so many sys-RPL and ML applications written for the S/SX not work on
the G/GX machines?

One of the biggest reasons is that HP is no longer supporting the use of
system RAM pointers, as these pointers have been moved from their old positions
in the S/SX machines to new ones in the G/GX.  All pointers in the S/SX RAM
area from #70000h to approximately #70700h have moved into the new G/GX RAM
area starting at #80000h.  It is not as simple as adding #10000h to an
SX address to get the corresponding GX address.  Many addresses have
changed their order, or new things have been added to offset the addresses.

It is simple to modify an S/SX program to work on a G/GX by changing these
pointers to their new G/GX values, but then we start seeing programs that have
an S/SX version and a G/GX version.  This is poor programming practice,
especially since it is so easy to make ONE version that works on both machines.
It is my opinion that only HP supported entry points should be used in
all cases.  This guarantees a longer shelf-life of your software, and
you never have to worry if it will work on the next ROM revision.
Re-writing an unsupported routine, or using a sequence of slower,
supported routines is always better than using an unsupported routine.
Slower, bigger programs that work are much better than faster, smaller
programs that crash.

The methods described below may not be the absolute best methods, but they
work, and may help you develop your own ways of doing the same thing.

----------------------------------------
--- DISPLAYING GRAPHICS ----------------
----------------------------------------

First of all, the OLD method that was previously good would be something
like this:

Using GDISP (#70565h) you can find the address of the PICT GROB and use it
to display your graphics.

       D0=(5)  =GDISP          * (#70565h)
       A=DAT0  A               * read the address of PICT
       LC(5)   20              * offset past prolog, dimensions, etc.
       C=C+A   A               * C now contains the bitmap address of PICT

Now I have the address of PICT where I can store graphics to display
them, or whatever.  All I have to do to get this to work on a G/GX is
change the GDISP address to the new one which is #806E4h.  HOWEVER, using
this method is not wise, since the code will only work either on the S/SX,
or on the G/GX, but not both.  Also, HP is no longer supporting GDISP,
so it could change in the future.

Here is a better method... it just requires a little setup using sys-RPL.

Before executing your ML code, you need to push the ABUFF address to the
stack.  You can also use HARDBUFF (currently displayed GROB), HARDBUFF2 
(menu labels GROB), or GBUFF (PICT GROB). The best thing about ABUFF
is that it is the text GROB, so you never even touch PICT, which means
the user doesn't have to worry about losing their PICT when they run
your software.

First, call RECLAIMDISP (#130ACh) to be sure you are looking at the text
GROB, and also clear it.  (see RPLMAN.DOC page 95 for more details).
If you don't want to clear the text GROB, then use TOADISP (#1314Dh).
Then, call TURNMENUOFF (#4E2CFh) to enlarge the text GROB to the full
131 by 64 size.  Then call ABUFF (#12655h) which will push a pointer to
the text GROB onto the stack.  Then you can call your ML routine, and
continue.  Once your ML routine has finished, you should call TURNMENUON
(#4E347h) immediately followed by RECLAIMDISP (#130ACh) to re-size the
text GROB to 131 x 56.

Here is how this would look in sys-RPL:

::
  RECLAIMDISP
  TURNMENUOFF
  ABUFF
  ID MAIN
  TURNMENUON
  RECLAIMDISP
;

This program assumes that your ML code is in a variable called 'MAIN'.

Now your ML code needs to be a little different, but it's practically
the same:

       A=DAT1  A               * read address in stack level 1 (ABUFF)
       LC(5)   20              * skip past prolog, dimentions, etc.
       C=C+A   A               * C now contains the bitmap address of text GROB

The only difference, is that you read the GROB pointer from the stack
instead of from the RAM pointer GDISP.  All you need to be careful with
is that the pointer is there on the stack for you to get!  (If someone
runs your MAIN code without running the sys-RPL calling routine first, you
will probably lose RAM!)

In the above example, it is assumed that D1 has not changed at all yet.
D1 contains the address of the top element of the stack, so it should
be pointing to the ABUFF pointer.  You can either drop the ABUFF pointer
from the stack at this point, or better, add a DROP in your sys-RPL calling
routine to drop it off the stack after the ML portion has finished running.

The above example is probably easier to understand, but here is a more
practical version of essentially the same thing (more practical,
because it also saves the RPL vars, and you will probably be doing
that anyway):

       GOSBVL  =PopASavptr     * put address in stack level 1 into A,
                               * drop ABUFF from stack, and save RPL
                               * vars (like GOSBVL =SAVPTR).
       LC(5)   20              * skip past prolog, dimentions, etc.
       C=C+A   A               * C now contains the bitmap address of text GROB

Of course, this method assumes that you use the same sys-RPL calling
portion as the previous example.

These are generally good methods to use even if you could care less about
the G/GX.  At this point I don't know if HP is going to make all of
the RAM pointers unsupported, or if they will support version dependent
entries.  Since there is a workaround, it might be a good idea to consider
them all unsupported. 

----------------------------------------
--- DELAY LOOPS ------------------------
----------------------------------------

As you surely know, the G/GX machine runs at a faster CPU speed than the S/SX.
If you would like to create delay loops that delay the same amount of time
on either machine, then you will need to base these delays on the clock.

One simple method that I have found is to use TIMER2 (#00138h) which is a
supported entry.  I won't give full details of this timer, but I will
summarize it's behavior.

TIMER2 is an 8 nibble value of clock ticks (1 tick == 1/8192 seconds).  TIMER2
counts backwards, but its starting value can be different depending on the
folowing conditions:

  - If the user has enabled the ticking clock in the status display (system
    flag -40 is set) then TIMER2 will count in intervals of 1 second.  Thus,
    it will count backwards from #00001FFFh to #00000000h.
    
  - If the clock is not enabled, and an alarm is due in less than 1 hour,
    then TIMER2 will contain the number of ticks left until that alarm comes
    due.  TIMER2 will continue to count backwards.
    
  - If the clock is not enabled, and there is no alarm due in less than 1
    hour, then TIMER2 will count in intervals of 1 hour.  Thus it will count
    backwards from #01C20000h to #00000000h.

There is, however a small problem with using TIMER2.  Before reading
its value, you have to synchronize the CPU with the TIMER, otherwise
you risk reading garbage.  By disassembling the ROM TICKS command, you
can see how HP does this.  Obviously it was important enough to them
to safeguard against reading garbage.

I've included the information about TIMER2 to let you know that it is
one possibility, and can be a simple method of reading the clock using
few registers.  However, there are two methods that are much better,
the only drawback being that they use a lot of registers.

This next routing uses the supported entry GetTimChk (#12EEh).
GetTimChk will simply get the 13 nibble ticks value and return it in
C[W].  If something with the system time was corrupt, then it performs
a warmstart.  Before using this routine, it is necessary that interrupts
are disabled via the ST=0 15 command.  GetTimChk must not be interrupted
while it's reading the timer, or the value will not be valid.  If
GetTimChk is interrupted, you stand a good chance of having a warmstart.
An interrupt that happens while the stored "next event" time is being
read could cause a checksum miscompare, which GetTimChk considers a
fatal error (and branches to the warm-start code).

The following assembler slice will wait n ticks (1 tick == 1/8192 s),
n is passed in A[W].  Uses A, C, P, R0 and R1.  GetTimChk alters A, B, C,
D, P, D1 and CARRY, and uses 3 RSTK levels:

DELAY   R0=A                   * put n into R0
        GOSBVL =GetTimChk      * (#012EEh)  This routine returns the 13
                               * nibble system time into C[W].
        R1=C                   * current time into R1

DelayLP GOSBVL =GetTimChk      * time
        A=R1                   * start time
        P=     12
        C=C-A  WP              * elapsed time
        A=R0                   * n
        ?A>=C  WP              * delay some more?
        GOYES  DelayLP
 
        P=     0


The next routine is pratically identical, only it uses the supported
entry, GetTime++ (#130Eh).  GetTime++ performs the same function as
GetTimChk, only it calls the disable and enable interrupt routines.
Therefore, you can use this routine when you don't want interrupts
disabled.  In fact, if they are disabled, then GetTime++ will have
enabled them again before it exits.

The following assembler slice will wait n ticks (1 tick == 1/8192 s),
n is passed in A[W].  Uses A, C, P, R0 and R1.  GetTime++ alters A, B, C,
D, P, D1 and CARRY, calls the enable and disable interrupt routines,
and uses 4 RSTK levels:

DELAY   R0=A                   * put n into R0
        GOSBVL =GetTime++      * (#0130Eh)  This routine returns the 13
                               * nibble system time into C[W].
        R1=C                   * current time into R1

DelayLP GOSBVL =GetTime++      * time
        A=R1                   * start time
        P=     12
        C=C-A  WP              * elapsed time
        A=R0                   * n
        ?A>=C  WP              * delay some more?
        GOYES  DelayLP
 
        P=     0

----------------------------------------
--- DETECTING WHICH MACHINE YOU'RE ON --
----------------------------------------

In many cases, you can write software to work on both the S/SX and the G/GX
without ever detecting which machine you're on.  However, there are some valid
reasons to want to know which machine you are currently running on.  Sound
effects are one good example.  Most good sound effects are not possible if you
try to use a clock-based delay loop, so you use definite delay loops.  All
definite delay loops will run faster on a G/GX than an S/SX, so the sounds
will have a higher pitch.  If you write two separate instances of sound code,
one for the S/SX, and one for the G/GX, then you can check which machine you're
on and run the proper sound code.  This would result in a sound effect that
appears identical when run on both machines.

If HP publishes version-dependent entries, then I hope they will also
provide a better method for detecting which machine you're on from ML.
Until that happens, this method works well.

In sys-RPL, you should call VERSTRING (#30794h) to push the ROM version string
to the stack.  Then you can check this value from ML to see which ROM version
you are running on.  On my SX, ROM ver E, this returns "HPHP48-E" and on my
GX, ROM ver L, this returns "HPHP48-L".  All ROM versions <= "J" are an S/SX
machine, and all ROM versions > "J" are a G/GX machine.

For example:

::

VERSTRING

CODE

sGX     EQU    0               * ST flag clear == S/SX, set == G/GX

        GOSBVL =PopASavptr     * (#3251Ch) Pop VERSTRING into A[A].
        LC(5)  10+2*7          * point past prolog, length, and to 8th char
        A=A+C  A
        D1=A                   * D1 is pointing to the version letter
        A=DAT1 B               * read version letter into A
        LCASC  'J'             * ASCII for "J", last S/SX version
        ST=0   sGX             * Assume S/SX
        ?A<=C  B               * Assumption correct?
        GOYES  CONT            * Yes? continue
        ST=1   sGX             * No, set G/GX

CONT    * the rest of your program here...

END     GOVLNG =GETPTRLOOP     * restore RPL pointers, return to RPL
       
ENDCODE

;


Some have mentioned that the easiest way to detect which machine you're
on is to check the nibble at (=INHARDROM?)+14.  It's the most significant
nibble of the RAM start address (7 on an S/SX, and 8 on a G/GX).  Using
this method is clearly better, since you don't need sys-RPL to push
anything to the stack.  However, although INHARDROM? is a supported
entry, there is no guarantee that the nibble at (=INHARDROM?)+14 will
remain the same.  HP could possibly leave this entry frozen by only moving
the code, not the pointer at the position (=INHARDROM?).  Because of this,
it is better to use the method described above. 

----------------------------------------
--- CHECKING THE SOUND FLAG ------------
----------------------------------------

You may need to check many of the System flags.  Checking the sound
flag is probably the most common, so I will give an example here.
Checking the other flags would be similar.

Since the entry SystemFlags (#706C5h in SX, #80843h in GX) may not be
supported, and it's different in the two machines, this is not a good
approach.  Instead, a sys-RPL/ML combination may be used.

In sys-RPL, execute the command:

56 TestSysFlag

This will return TRUE if system flag #56 is set.  (See RPLMAN.DOC page
123 for details.  See page 76 for details on the FALSE and TRUE
flags).

In ML, you would do this:

        GOSBVL =popflag        * Pops flag from stack, sets carry if TRUE
        GOC    SNDON
SNDOFF	...
SNDON	...

The SNDOFF and SNDON routines can be as simple as setting a status
flag.  All you are doing is determining whether system flag 56 is clear
or set.  Then your sound routines will know whether or not they should
play the sounds.

----------------------------------------
--- SOURCE CODE EXAMPLE ----------------
----------------------------------------

I took the liberty of writing a little piece of code to show all the
above examples at work together.  This program doesn't do anything
much, just a little graphics, a little sound, and a little delay.

If you have the chance to try this out on an SX and a GX, then try
and see if you can tell a difference.  Try it with sound on with both
machines, sound off with both machines, or sound on with one, sound
off with the other...  Getting it to work this way took very little effort.

This source code can be compiled with Detlef Mueller's <-RPL-> 5.0 
Thanks Detlef!

I used Jean-Yves Avenard's StringWriter 4.1 for the GX to enter this
source code.  Thanks Jean-Yves!

%%HP: T(3)A(D)F(.);
"
::

  TOADISP              (be sure we're looking at text GROB)
  VERSTRING            (put the VERSTRING on the stack)
  56 TestSysFlag       (put FALSE or TRUE on the stack)
  ABUFF                (put a pointer to the text GROB on the stack)

CODE

sGX    EQU     0            * clr == SX; set == GX
sSND   EQU     1            * clr == sound; set == no sound

       ST=0    15
       INTOFF
       GOSBVL  =PopASavptr  * get ABUFF into A[A], save RPL pointers
       LC(5)   20           * point past prolog, etc.
       C=C+A   A
       R0=C.F  A            * store ABUFF bitmap addr here permanently

       GOSBVL  =GETPTR
       ST=0    sSND         * assume SND on
       GOSBVL  =popflag     * pop TRUE or FALSE, set carry if TRUE
       GONC    VER
       ST=1    sSND         * sound is off

VER    GOSBVL  =PopASavptr  * pop VERSTRING addr into A, savptr again
       LC(5)   10+2*7       * point to version letter
       A=A+C   A
       D1=A
       A=DAT1  B            * read the letter into A[B]
       LCASC   'J'          * last version letter for S/SX
       ST=0    sGX          * assume G/GX
       ?A<=C   B
       GOYES   START        * it's a G/GX, go ahead and start
       ST=1    sGX          * it's an S/SX

START  GOSUB   ERASE        * Erase the status line
       LC(5)   34+1         * one line down, 1 nibble right
       GOSUB   EMPTY        * draw an empty dot
       C=C+CON A,4          * four nibbles right
       GOSUB   EMPTY        * draw an empty dot
       C=C+CON A,4          * four nibbles right
       GOSUB   EMPTY        * draw an empty dot
       C=C+CON A,4          * four nibbles right
       GOSUB   EMPTY        * draw an empty dot
       
       C=0     W
       LC(4)   8192         * 8192 ticks, or 1 second
       GOSUB   DELAY        * delay 1 second
       LC(5)   34+1         * one line down, 1 nibble right
       GOSUB   FULL         * draw a filled dot
       GOSUB   S1           * make sound #1

       C=0     W
       LC(4)   8192
       GOSUB   DELAY        * delay 1 second
       LC(5)   34+5         * 1 line down, 5 nibbles right
       GOSUB   FULL         * draw a filled dot
       GOSUB   S1           * make sound #1

       C=0     W
       LC(4)   8192
       GOSUB   DELAY        * delay 1 second
       LC(5)   34+9         * 1 line down, 9 nibbles right
       GOSUB   FULL         * draw a filled dot
       GOSUB   S1           * make sound #1

       C=0     W
       LC(4)   8192
       GOSUB   DELAY        * delay 1 second
       LC(5)   34+13        * 1 line down, 13 nibbles right
       GOSUB   FULL         * draw a filled dot
       GOSUB   S2           * make sound #2

       C=0     W
       LC(4)   8192
       GOSUB   DELAY        * delay for 1 second


END    INTON
       ST=1    15
       GOVLNG  =GETPTRLOOP  * GETPTR, return to RPL


* The following assembler routine will wait n ticks, n is
* passed in C[W].  Uses A, C, P, R1, and R2.  GetTimChk alters
* A, B, C, D, P, D1, and CARRY, and uses 3 RSTK levels.

DELAY  R1=C                 * put n into R1
       GOSBVL  =GetTimChk   * get 13 nibble system time into C[W]
       R2=C                 * current time into R2

DelayLP GOSBVL =GetTimChk   * time
       A=R2                 * start time
       P=      12
       C=C-A   WP           * elapsed time
       A=R1                 * n
       ?A>=C   WP           * delay some more?
       GOYES   DelayLP
       P=      0
       RTN


* The following routine will erase the status line.
* 34 nibbles on each line, 14 lines total, 476 (#1DCh) nibbles total.
* It assumes that a pointer to the screen bitmap is in R0.

ERASE  A=R0.F  A
       D0=A                 * put screen pointer into D0
       LC(5)   34-1         * do loop 34 times
       A=0     W
ERloop DAT0=A  14           * write 14 nibbles of white
       D0=D0+  14           * increment screen pointer
       C=C-1   A            * decrement counter
       GONC    ERloop       * do again until counter == FFFFF
       RTN


* The next routine will draw an empty dot on the screen.
* It assumes that a pointer to the screen bitmap is in R0.
* C[A] must contain the nibble offset of where to draw the dot.

EMPTY  A=PC                 * get PC, so we can find data
       GOTO    ENEXT

       NIBHEX  0F0C03       * data... bitmap of the dot
       NIBHEX  204204
       NIBHEX  108108
       NIBHEX  108108
       NIBHEX  204204
       NIBHEX  C030F0

ENEXT  A=A+CON A,4          * A now points to 1st nibble of data
       D1=A
       A=R0.F  A
       A=A+C   A
       D0=A                 * D0 points to correct screen position

       LA(2)   12-1         * 12 lines in graphic
       B=A     B
Eline  A=DAT1  3            * read 3 nibbles (one line of graphic)
       DAT0=A  3            * write to screen
       D1=D1+  3
       D0=D0+  16
       D0=D0+  16
       D0=D0+  2
       B=B-1   B            * decrement counter
       GONC    Eline
       RTN


* The next routine will draw a filled dot on the screen.
* It assumes that a pointer to the screen bitmap is in R0.
* C[A] must contain the nibble offset of where to draw the dot.

FULL   A=PC                 * comments here are the same as
       GOTO    Fnext        * those for EMPTY

       NIBHEX  0F0CF3
       NIBHEX  EF7EF7
       NIBHEX  FFFFFF
       NIBHEX  FFFFFF
       NIBHEX  EF7EF7
       NIBHEX  CF30F0

Fnext  A=A+CON A,4
       D1=A
       A=R0.F  A
       A=A+C   A
       D0=A

       LA(2)   12-1
       B=A     B
Fline  A=DAT1  3
       DAT0=A  3
       D1=D1+  3
       D0=D0+  16
       D0=D0+  16
       D0=D0+  2
       B=B-1   B
       GONC    Fline
       RTN


* This next routine plays sound #1
*
* Notice that the sound code for the S/SX and the
* sound code for the G/GX is practically identical.
* The only difference is the pitch.  I found that if you
* multiply the S/SX pitch by 1.55, then you get an almost
* exact value for the same sound on the G/GX (using this
* sound method).  Notice also that the length of the sounds
* are the same.  Since the G/GX pitch takes longer to execute,
* this is where the length is lengthened.  Both sounds take
* the same amount of time to execute, within a few mili-seconds.

S1     ?ST=0   sSND         * is SND ON?
       GOYES   S1DOIT       * do sound
       C=0     W
       LC(3)   1819         * Sound #1 is about 1819 ticks
       GOTO    DELAY        * If sound is off, delay, and RTN
S1DOIT ?ST=0   sGX          * is this an SX?
       GOYES   SXS1

* Sound code for the G/GX       
       
       LA(2)   100          * length
S1Glp  LCHEX   800
       OUT=C
       LC(2)   155          * pitch #1
S1GD1  C=C-1   B
       GONC    S1GD1
       C=0     A
       OUT=C
       LC(2)   155          * pitch #2
S1GD2  C=C-1   B
       GONC    S1GD2
       A=A-1   B
       GONC    S1Glp
       RTN

* Sound code for the S/SX

SXS1   LA(2)   100          * length
S1Slp  LCHEX   800
       OUT=C
       LC(2)   100          * pitch #1
S1SD1  C=C-1   B
       GONC    S1SD1
       C=0     A
       OUT=C
       LC(2)   100          * pitch #2
S1SD2  C=C-1   B
       GONC    S1SD2
       A=A-1   B
       GONC    S1Slp
       RTN


* This next routine plays sound #2

S2     ?ST=0   sSND         * is SND ON?
       GOYES   S2DOIT
       C=0     W
       LC(4)   8184         * Sound #2 is about 8184 ticks
       GOTO    DELAY        * if sound is off, delay, then RTN
S2DOIT ?ST=0   sGX          * is this an SX?
       GOYES   SXS2

* Sound code for the G/GX
       
       LA(3)   1000         * length
S2Glp  LCHEX   800
       OUT=C
       LC(2)   70           * pitch #1
S2GD1  C=C-1   B
       GONC    S2GD1
       C=0     A
       OUT=C
       LC(2)   67           * pitch #2
S2GD2  C=C-1   B
       GONC    S2GD2
       A=A-1   X
       GONC    S2Glp
       RTN

* Sound code for the S/SX

SXS2   LA(3)   1000         * length
S2Slp  LCHEX   800
       OUT=C
       LC(2)   45           * pitch #1
S2SD1  C=C-1   B
       GONC    S2SD1
       C=0     A
       OUT=C
       LC(2)   43           * pitch #2
S2SD2  C=C-1   B
       GONC    S2SD2
       A=A-1   X
       GONC    S2Slp
       RTN

ENDCODE


;

"

----------------------------------------
--- UUENCDOED VERSION OF DOTS ----------
----------------------------------------

begin 644 DOTS
M2%!(4#0X+4R=+=`4$Y0'H[ADA#=591+,+:`Q`$B/@(_/43)#%`#`@J$/B"]]S
M!DB!+Z!A58`5^!PE,X0!`*PQ$;032DB0K@58<-PP-`(`)P\8^#*'#ACX,N<-&
M&/@R1PWZ,@,`<GXP-`(`)Q)'&/HR`P!R8C!T`@!G$(<6^C(#`')&,+0"`*<."
MQQ3Z,@,`<BHP]`(`YPQG&OHR`P!R#H"`@/780U$0D/CN$A"@^.X2$"'"&Q(19
MF;XN`(&A'Q`#0R$`H`]1V&'-7O80&$MV`O#`,`(D0`$8@`$8@`(D0`P##QCXH
M,#&!H1_`&@,(*+&@CE$K42AQ$O9A'Q9J7>@0&$MV`O#`/_[G?_________[G&
M?_P##QCX,#&!H1_`&@,(*+&@CE$K42AQ$O9A'Q9J7>@0:.&@+R,;9]V.!C((?
M*$$V`H`(,;&IYL7?@A`3FVI>_&I<VQ`(*$$V`H`(,4&FYL7?@A`39&I>_&I<\
MVQ!H\:`O,_@?9N9H,(.`(N@S`H`(,6&DYL7?@A`30VI>_#I<VQ`(*((^(P"(:
4$!,M:E[\+0@QL:+FQ:_#M0VQ$@.QK
``
end
