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
Because of ignorant people who abuse the net, I can't put my email address here. You have to construct it yourself. For all I know, they have programs that can figure this out anyway. If you have any information about spam or net abuse, email abuse@abuse.net.
My email address is: doug underscore cannon at geocities dot com -----
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 -- ---------------------------------------- (Some may prefer 'Detecting on which machine you are' )
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. * Important note: (added 6/24/97) * There is a better way to do this. This next routine has * some in-line data that it is going to use. I need to get a * pointer to this RAM area that contains the data. The method * below is getting the program counter (A=PC) and then it * jumps to the code below the data. The instruction (GOTO ENEXT) * is a 4-nibble instruction, so the A[A] register will contain * an address pointing to 4 nibbles before the data. When I * add 4 to the A[A] register, it points directly to the data. * * The better way to do this is to use a GOSUB in conjunction * with C=RSTK. When you do a GOSUB, this pushes the return * address onto RSTK. In this case, we would never do a return, * but it just so happens that the address pushed onto RSTK * would be pointing directly at the data. This routine could * be modified to do something like this: * * A=C A (to preserve the C[A] value ) * GOSUB ENEXT * * NIBHEX ...... data goes here * * ENEXT C=RSTK * * At this point, C points to the first nibble of data and A contains * the offset. One day I may get around to modifying this doc * to reflect these changes. Until then, this comment will just have * to do. 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. * * See comments near ENEXT to find a better way to get the * address of in-line data. 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 ; "Get a copy of DOTS (425 bytes, not zipped).