Here I document some useful tricks I discovered while learning to program the G7000 / Odyssey² with asl. The code examples shown here are not directly taken from a real program, although I have used them all while experimenting with the machine. So there are no demo programs, just these code snippets.
Yes, the 8048 doesn't have any commands for subtraction. This is not a big problem, just basic knowledge about computer arithmetics. But it took me some time to figure it out after I saw it in the BIOS. The trick is to calculate -R2 + R1 instead of R1 - R2. For -R2 I have to use 2's complement: invert all bits and add 1. So the full code to calculate R1 - R2 is:
mov a,r2 ; second operand
cpl a ; invert bits
inc a ; add 1: a contains -R2
add a,r1
One feature of the 8048 is quite useful for numbers which need to be
displayed: The 8048 can use BCD arithmetics. All you need to do is interpret
numbers in hex as decimal: so 010h represents 10. This way it is much
easier to display the numbers, just take the upper and lower 4 bits and display
them separately, you no longer have to divide by 10. To calculate in BCD first
add the numbers as usual. The 8048 has an auxiliary carry flag that shows
overflows from the lower to the higher nibble. This information is now used by
the DA A opcode to correct any non-BCD range nibbles. The A
register contains the correct result in BCD afterwards, the carry flag is set
if necessary. So this is code to calculate 18 + 89:
mov a,#018h
add a,#089h ; now in a: 0a1h
da a ; now in a: 007h, carry set
This trick works on nearly all microprocessors, not only on the 8048. The
carry flag is only changed by some commands, mainly the addition commands. So
you can insert some code statements between the carry changing command and the
jc/jnc. For example:
add a,#010h ; set carry, when a>=0f0h
mov r0,#020h ; carry not changed
mov a,@r0 ; carry not changed
jc somewhere
This trick only works with the carry flag. There is no zero flag,
jz jumps when A is 0.
If you run out of internal RAM and have to put some data into external RAM you should try to put related data into the same addresses. That way you only need one pointer register:
iram_x equ 020h ; x position
eram_y equ 020h ; y position
mov r0,#iram_x ; pointer to position data
mov a,@r0 ; x position in internal ram
mov r3,a
movx a,@r0 ; y position in external ram
mov r4,a
It is possible to only display the bottom parts of characters. To achieve
this you have to fill in the char bytes per hand instead of calling
printchar. Bytes 0 and 1 are simply just the y and x position.
For bytes 2 and 3 you can call calcchar23. For every line you
want to cut off from the character you have to increase the char-map
pointer in r5 by 1. You may have to manipulate bit 0 in r6 also as this is
the 9th bit of the char pointer. This depends on the exact y position on
the screen and can be omitted if the char does not move around as in this
example, which displays a vertical bar by cutting off the horizontal part of
a T:
mov r4,#GROUNDLEVEL - 6 ; y position
mov a,r4
movx @r0,a
inc r0
mov a,r3
movx @r0,a
inc r0
mov r5,#014h ; T
mov r6,#col_chr_yellow
call calcchar23 ; calculate byte 2/3
mov a,r5 ; get char-pointer
inc a ; advance one line...
movx @r0,a ; ...to cut off first line of T
inc r0
mov a,r6 ; get byte 3
movx @r0,a ; store it
inc r0
This trick is used in many of the commercial games, e.g. the enemies in Killer Bees are constructed by combining the lower parts of 8 and the feet of the running mans.
Advancing the char-map pointer does work for quads, too. The number of lines that are drawn are controlled by the last sub-quad. The other 3 sub-quads draw exactly the same number of lines, the normal character ends are ignored. So you can cut off the bottom of the first 3 sub-quads, just set the last sub-quad to a space with the number of lines to display. The first 3 sub-quads are now cut off at the bottom when used without offset. With offset you can move freely inside the whole char bitmap, if one character is finished the next one is drawn with a one pixel empty line between them.
This trick does not work in o2em version 1.01.
If you clear port 1 bit 7 dark colors are made brighter. This means that
the colors for background and dark grid are now identical to the colors
for chars, quads, sprites and bright grid. Clearing P1.7 is not easy, all
BIOS routines which touch P1 set it. Among these routines are
vdcenable, extramenable, getjoystick
and the interrupt routines. So to keep P1.7 cleared you have to replace a
lot of BIOS routines with your own versions.
One feature of asl is the possibility to divide programs into sections.
They start with section name and end with
endsection name. All labels inside a section are local to the
section, so you can reuse common names like loop etc:
section code1
mov r2,#3
loop ; do something here
djnz r2,loop
endsection code1
section code2
mov r2,#4
loop ; do something different here
djnz r2,loop
endsection code2
More info about this can be found in the documentation of asl. This reuse of local labels is something that I have found convenient. One could argue that having labels with the same name is a source of confusion, but I like them.
One of the problems any programmer on the G7000 / Odyssey² has to fight is the page alignment of ROM data and code which accesses it. One way to provoke error messages when assembling works by using subtracting the high byte of the current ROM position multiplied by 256 from the start of the table to get the low-byte. But this only keeps the start of the table in the same page. So at the end of the table I need to compare the high-bytes of the first and last table entry. For that I use conditional assembly to generate an error message if they are not equal.
addscore section addscore
add a,#scoretable - hi($)*0100h
movp a,@a
; ... lines snipped
ret
endsection addscore
scoretable
db 5,15,10
if (hi(scoretable) <> hi($))
error "scoretable crosses page border"
endif
A more versatile way to achieve the same result is the usage of an assert macro. It takes an expression included into "" as argument. At assemble time this argument is evaluated. If it is not true an error message is generated.
assert macro expr
if (~~val(expr))
error expr
endif
endm
Here is the example from above, this time using the assert macro.
scoretable
db 5,15,10
assert "hi(scoretable) == hi($)"