This is the recapitulation and continuation of my adventure in coding Amiga assembler demo the December adventure 2025.
the adventure evolves mostly around custom chips and demo scene effects and less around OS. I will actually follow the tutorial videos from jel (from agony?)! Thanks man you are a genius!
today I was mainly preparing the ground and installing the Amiga emulator, setting up workbench and tool. i will be using asmone as my development tool, thus i will develop as much as possible on the Amiga itself, no cross-compiling. i know it much harder and might be also dumb, but it get the feeling of those time back.
repeating a bit the number systems, binary and hexadecimal. in Amiga assembler binary numbers are preceded by the % sign and the hexadecimal numbers by the $ sign.
˜ decimal 10 ˜ binary %0110 ˜ hexydecimal $A
here i intentionally write the numbers in groups of nibbles (4 bits). also in memory the blocks are in bytes (8 bits). this also helps to translate easily for binary to hexadecimal, by splitting the binary number into nibbles (which can take values from 0 - 16) an translate each nibble into a hexadecimal position (0 to A, for 0 - 16)
˜ binary %0011 1101 1111 0011 ˜ hex $ 3 D F 3 --> $3DF3
by the way 2 nibbles (4 bits) are one byte (8 bits), two bytes are one word (16 bits), and two words are one long-word (32 bits).
interesting and probably useful fact: if all the bits except the last significant bit (LSB) of a binary number give a pair numbers when set, i.e. 2¹ = 2, 2²=4 ... but 2⁰=1. thus, if the number given is pair this implies that the last significant bit is not set (zero).
˜ --> %1110 = pair ˜ --> %0101 = unpair
similar reflections can be done by the most significant bit.
today freshening up on the hardware properties of the Amiga.
the Motorola 68k cpu address and data registers are 32 bites wide. the memory bus to chip and slow ram are 16 bit, which will require cpu 2 cycles to read out a 32 bit data /addresses. chip ram (usually 512KB) is accessible by the cpu and the co-processors (copper, blitter, deise, sprite, audio, disk), where the co-processors can access the chip ram directly, e.g. without passing through the cpu, which is called direct memory access (dma). this means that all image and sound data as well as other hast to reside in the chip meme to be accessible by the custom chips. there are 24 dma channels in total, which can bit toggled individually. then there are fast and slow ram which is accessible exclusively by the cpu as well as slow ram. fast rams sits on the cpu expansion port (thus fast access) where as slow ram sits on the general expansion port. finally there is also the kickstart rom holding the firmware which is also addressed by the cpu address space.
the memory can be seen as a long chain of boxes holding each one 1 byte (two nibbles, 8 bit) which are represented by two hexadecimal digits, e.g. $00-$FF. each one of those boxes has an address which is 32 bit wide. This means that any 32 bit address of the memory points to 8 bit (1 byte) of memory.
when using an assembler, e.g. asmone, the memory map in chip has 3 different zones: the zone where the asmone binary is sitting, the zone where the source code is sitting and the zone where the compiled code is sitting. The memory for the zones is allocated by the system requested by the assembler.
some related commands in asmone when in command mode (not editor)
˜ = --> return the memory map, e.g. starting address for the zones ˜ h$xx --> show the memory segment starting at $xxxxxxxx in ˜ hexadecimal notation and the corresponding ascii ˜ a --> assemble ˜ j --> run (jump to the binary un memory) ˜ d --> disassemble binary starting starting from ....
when writing assembler code in the editor the commands can not start at the beginning of the line but must be indented, i.e. with a tab. the beginning of the line is reserved for labels and assembler directives. here an example code:
˜ move.w #$2C,d0 ˜ rts
this code moved the value $2C into the d0 data register of the cpu. then return to subroutine (to asmone). when assembling the source code it will generate a binary in the memory. then when executing the code asmone will jump to the start address (put the current PC register onto the stack and copy start adders of binary onto PC register) will be set to the start of the compiled binary) and execute the code which in turn will handle the execution back to asmone via the rts command (copy back the saved PC from the stack). if there is no rts, the cpu continue execute command n the memory ending in nirvana, or guru meditation.
some useful editor commands:
˜ ctrl+d --> delete line ˜ ctrl+b --> paste line
continuing form yesterday.
one subtlety of the 68k cpu is that he can execute instructions only from pair addresses. that is why even if we move.b (byte) the compiled instruction will be the same size in memory as move.w.
˜ move.w #$AB,d0 --> XXXX00AB ˜ move.b #$AB,d0 --> YYYY00AB
XXXX and YYYY are the opcodes for move.w and move.b respectively (which I don't remember right now). When we will insert data in the memory we also need to take care to always have a pair amount of bytes. this can be achieved by adding NULLS or similar.
now looking at the move commands and addressing
in the direct addressing we move a value into a register ore memory place. therefore we ass the # before the value
˜ move.b #$AB,d0 ˜ move.b #%10001110,d0
you can move bytes (b), words (w) and long word (l). one subtlety when moving absolute values into a register is the the previous value is not removed/zeroed.
˜ ˜ So if d0 = $12345678 ˜ move.b #$AA,d0 ˜ will give d0 = $123456AA !!
in the absolute addressing the content of a memory cell will be copied into i.e. a register
˜ memory starting at $5000 contains $ABCDEEFF ˜ move.b $5000,d0 --> d0 = $xxxxxxAA ˜ move.w $5000,d0 --> d0 = $xxxxABCD ˜ move.l $5000,d0 --> d0 = $ABCDEEFF
note that $5000 is just the start address and in memory the bytes are as follows
˜ $5000 -> $AB ˜ $5001 -> $CD ˜ $5002 -> $EE ˜ $5003 -> $FF
the direct addressing is moving from a register. e.g. move.l d0,d1 the following addressing source,destination are possible
˜ move.x register,register --> direct,direct ˜ move.x number,register --> immediate, direct ˜ move.x asdress,register --> absolute, direct ˜ and all permutations exept immediate immediat, obvioulsy
where x can be b, l, w respectively.
moveq (move quick), is a shorter version for the command move.w and will be assembled into 2 bytes instead of 4 bytes! because the address bus of the 68k is only 16bits (word) wide, fetching the moveq command can be done in one bus cycle, compared to 2 cycles for the move.w. The limit is that the moveq can only move signed 8bit values from 128 and -127, e.g., $0F and $1F. moveq can only be applied into data resisters (dx)! also the moveq command into a register will zero all remaining bytes of the register, compared to the move.w which will leave the previous bytes of the high word of the register untouched. this is quite useful for zeroing the register and copy a value at the same time.
coming back to some practicalities of asmone, it is possible to do some calculation in the command line of asmone with the ? command, e.g. give you the hexadecimal and binary form a decimal but also the ASCII code of "b".
it's fool moon tonight! today still continuing on some basics for the amiga assembler in particular related to asmone. assembler pseudo instructions are related to the assembler directly. they will not be assembled into instruction for the cpu. some first pseudo instructions come up when defining constants and memory locations:
˜ ORG $50000
ORG will the define the origin or start point from which the data below / or also binary code, will reside in memory here $50000. usually however this is not good practice to define the memory point, as it should remain dynamically allocated. there might be something important at $50000. Better practice is to use labels (see later). the only time where one defines the memory segment is when we deliberately want to put something in the chip ram and not in the fast/slow ram, e.g. data needed by the co processors, such as images sound etc. one way to define data is as follows:
˜ dc.b $50,$90,$2E,$FF ˜ dc.w $5090,$2EFF ˜ dc.l $50902EFF
dc.x will declare constants (dc) in memory at the location given by a preceding ORG instruction. this data will the reside in memory when the binary is executed. the three examples above all write the same data into memory, once as sequence of bytes, once as words, etc. it is also possible to reserve space to put data later (like kind of variables)
˜ ds.w 5
will reserve 5 words on memory, or one could also say declare space (ds). one subtlety is that dc will reserve space at compilation, this is if you add 50 bytes of dc data, the binary will also increase by 50 bytes. on the other hand 50 bytes ds will not increase the binary file size. the ds storage will be reserved in memory at the execution of the binary. it is also possible to declare blocks of data
˜ dcb.b 40,$23
this will fill the memory with a block of 40 bytes containing $23. sometimes dcb.x can also be written blk.x (depends on the assembler, asmone understands both)
to address the data positions dynamically we can use labels, which are also pseudo instructions and basically placeholders for memory locations. the labels in comparison to all other instruction will be at the very beginning of the line and will be followed by a colon ":", e.g. "label:". the content of the memory at a label can be explored from the command line of asmone with
˜ hlabel
here label is the label name. same the address of to which the label is referencing can be fond by
˜ ?label
you can also disassemble starting form a label, ion particular if you label the start of you code with i.e. start
˜ dstart
moving with labels is also possible
˜ move.l label,d0
moves the content of the memory at label in d0
˜ move.l label+2,d0
will copy the memory 2 bytes further away in d0. the 2 bytes are added to the address not to the content. the address label+2 will be calculated by the assembler and replaced with the address during assembly. in general all the labels used in the source code will be replaced by addresses (relative addresses to the start to the binary) during compilation. the binary will not contain any readable label names.
today after a break from amiga assembler doing some go coding for a season greetings cards and visiting the 20th anniversary demo nights event, which was again very inspiring and great to meet with people in the local scene.
Today we will again continue in the basics before really diving into putting stuff on the screen.
Other assembler directives are constants, e.g. A, which will be replaced by its content at the time of assembly. constants are like labels written at the very beginning of the line.
˜ A = 4
you can use the via direct addressing using the #
˜ move.w #A,d0
will be replaced by
˜ move.w #4,d0
There are some similarities between labels and constants as they both can be moved into registers. Only the label content is defined by the assemble and the constant content is defined by us, but both can be handled in the same way. Thus if we could like to move the address of a label into a0 we need to add the # before
˜ move.l #start,a0
We previously saw the absolute addressing. for addresses that are word size ($0000 - $FFFF) we can also use the so called short absolute addressing
˜ move.l $4,a6 ˜ move.l $4.w,a6
are equivalent except that the short version will give a shorter binary opcode. further there are also the indirect addressing type
˜ move.l #data,a0 --> copy the adress of the datat block (32 bit --> .l) ˜ move.w (a0),d0 --> copy the content of the address stored in a0 to d0
here many additional things can be implemented. e.g indirect addressing with displacement, which is same as as above with an offset. the offset sill be added tho the address in a0 and then the content at this point will be copies. be aware that the offset is obviously in bytes!
˜ move.w 4(a0),d0 --> copy with an offest of 4 bytes
above the offset is written in decimals, but we can also write offset in hexadecimals
˜ move.w $4(a0),d0
the maximum offset is in 2 bytes ($FFFF). There is also the indexed indirect addressing
˜ move.w #4,d1 ˜ move.w (a0,d1.w),d0
similarly here d1 will be added to the address in a0 and the content from that point in memory will be moved to d0. the d1.w is because we only moved a word into d1 and we don't know that is in the high word of d1. so we are sure we use the index that is intended. An indexed indirect addressing with offset is also possible
˜ move.l $2(a0,d1.w),d0
in this case the offset can only be 1 byte ($00-$FF). here an overview of the addressing modes for the move command so far.
˜ move.l d0,d1 --> direct ˜ move.w #4,d0 --> immediate ˜ move.l $50000,a0 --> absolute ˜ move.l (a0), d0 --> indirect ˜ move.l $4(a0),d0 --> indirect with offset (max $FFFF) ˜ move.l (a0,d1),d0 --> indirect with index ˜ move.l $2(a0,d1.w),d0 --> indirect with index and offset (max $FF)
now let's see the signed numbers. in the signed numbers the MSB (most significant bit) defines if a number is negative or not. if the MSB is set the number is negative. for a byte it looks as follows
˜ positive: ˜ %00000000 -> %01111111 ˜ $00 -> $7F ˜ 0 -> 127 ˜ negative: ˜ %10000000 -> %11111111 ˜ $80 -> $FF ˜ -128 -> -1
The way to find the negative number is by a complement to 2, which is doing the complement to 1 (NOT) and add 1 to the results. so to find the number -24 we start with the positive number and complement to 2
˜ 24 -> $18 -> %00011000 (to binary) ˜ NOT %00011000 -> %11100111 -> $E7 (NOT is a complement to 1) ˜ ADD 1 to $E7 -> $E8 (is now the compelent to 2) ˜ $E8 -> %11101000 (to binary) ˜ $E8 -> -24 (signed number)
the same can be done with words and long words
˜ DEC word -> 25481 ˜ $6389 -> %0110 0011 1000 1001 ˜ NOT -> %1001 1100 0111 0110 ˜ ADD 1 -> %1001 1100 0111 0111 ˜ to HEX -> $9C77 ˜ in signed -> -25681
when we decide to treat the number as negative or positive we need to use the appropriate arithmetic command, e.g. MUL for signed and MULU for unsigned multiplications. also you well be able to see if an number is negative by looking in the flags of a the status register (SR) of the 68k. the SR consist of 2 bytes, the upper byte is the supervisor byte and the lover byte is the user byte or condition condition code register
˜ Status regisrter (SR) bites and flags ˜ 15 14 13 12 11 10 09 08 | 07 06 05 04 03 02 01 00 ˜ T - S - - I2 I1 I0 | - - - X N Z V C ˜ N: negative flag ˜ Z: zero flag ˜ C: carry flag
e.g. if the result of a subtraction is zero the Z flag will be set
e.g. if the result of a subtraction is negative among others the N flag will be set
e.g. if the result of an addition is to big, e.g. >$FF for a byte, the C flag will be set
The flags are very useful for conditional branching, i.e. jump to a certain part of the code depending if the preceding operation gave a negative number. the branch commands are bxx where the condition is in xx. among many others, often used are:
˜ bra --> unconditional brancing ˜ bne --> if not equal (z flag not set) ˜ beq --> if euqal (z flag set) ˜ bmi --> if negative (minus) (n flag set)
If you want to compare a value in a greater to a given number you could then subtract (sub) the number form the register. if they are equal the zero flag will be set and you can then branch. you can do the same with the compare command (cmp), which compared to sub, will not alternate the value in the register
˜ move.b #$5,d0 ˜ sub.b #$5,d0 ˜ beq
this will branch and d0 will be = $0. same
˜ move.b #$5,d0 ˜ cmp.b #$5,d0 ˜ beq
will also branch but d0 will remain uncharged.
so far we saw move command with different addressing, branching, comparing, declaring data, label etc. to complete this first exploration of assembler commands I will today look at boolean operations. First is the bit wise negation:
˜ move.b #%00001111,d0 ˜ not.b d0 ˜ d0 = %11110000
not is simply inverting each bit in the register its is given. works for byte, word and long word. next is the "and" command (don't miss it up with the "add" command. "and" doing a bit wise logical and. just for a fresh up here the and table
˜ (and) ˜ 0 0 -> 0 ˜ 0 1 -> 0 ˜ 1 0 -> 0 ˜ 1 1 -> 1
the resulting bit is only set when both inputs are also set. This can be and is also used for masking, which is setting zero parts of a bytes in memory
˜ move.w #$432C,d0 ˜ and.w #$00FF,d0 ˜ d0 = $002C
in binary notation
˜ $432C -> %0100 0011 0010 1100 ˜ $00FF -> %0000 0000 1111 1111 ˜ and ˜ $002C -> %0000 0000 0010 1100
notabene, in the code above the result will be in d0, e.g. the previous value of d0 is lost. masking can for example be used to get the reminder of a division by 2 4 8 16 32. this might look a bit abstract
˜ 10010111 div 2 = 01001011 remider 1
remember the lsb is equal to 1, the next is equal to 2, next 4 etc. thus dividing by two can be seen as shifting all bits to the right, i.e. what was 8 becomes 4, what was 4 becomes 2 etc. the last one falls off and is the remainder. thus if you would like top know the reminder, mask out bits 8-2
˜ 10010111 ˜ 00000001 (msak) ˜ 00000001 (reminder)
dividing by 2, would mean two shifts, two fall offs, this masking the last two bits will give you the remainder of such a division. Same stands for division by 4 etc. next there is the "or" operation. here th or table
˜ (or) ˜ 0 0 -> 0 ˜ 0 1 -> 1 ˜ 1 0 -> 1 ˜ 1 1 -> 1
"or" can be sued to set bits in byte, word, long word while keeping the other bytes unchanged, this is will show useful when activating dma channels later
˜ %10010010010 ˜ %00001001010 (or) ˜ %10011001110 (result)
in hex
˜ $432C ˜ $8000 (or)
all nibbles except the most significant ($4) one will remain unchanged. lets look at the most significant nibble in detail
˜ %0100 -> $4 ˜ %1000 -> $8 (or) ˜ $1100 -> $C (result)
here the code for testing in asm one and see the whole result of the or:
the "exclusive" or (eor) is similar to the "or" as it sets unset bits but is unsets set bits:
˜ (eor) ˜ 0 0 -> 0 ˜ 0 1 -> 1 ˜ 1 0 -> 1 ˜ 1 1 -> 0
finally two more commands shall be explored: the bit shifting and rolling commands lsl,lsr,ror,rol
˜ lsr #x,d0 ˜ lsl #x,d0
all bits of d0 will be shifted x times to the right or left respectively. which corresponds to a division or multiplication of 2^x.
˜ lsr #1,d0 -> d0/2 ˜ lsl #2,d0 -> d0*4
this is much faster than using a mul or div command, but works only for div or muls of 2^x obviously. when shifting with lsl and lsr, u bits will be added and the bit that falls off will go into the carry flag. in ror and rol the bits that are added are the once that fall off, thus it is a rotation of the bits, e.g. what was lat will be first:
˜ move.b %1101001,d0 ˜ lsl #1,d0 -> c=1 d0=1010010 ˜ lsr #1,d0 -> c=1 d0=0110100 ˜ rol #1,d0 -> c=1 d0=1010011 ˜ rsr #1,d0 -> c=1 d0=1110100
This was a bit a tour of the most commonly commands that i could think of. further commands can be addressed when they will pop up. however do not hesitate to have a look at the reference manual for the 68k for more detailed and probably better explanations. here some links (hoping they are still valid)
next we will start coding (^^)
Today we will code a little program that will access a data table with values and order these values
˜ data: dc.w 7,6,5,4,3,1,2,0
we will achieve this by always comparing two neighboring table entries and switch them into the descending order, the the next two values and switch if necessary. Once we are trough the table repeat from the beginning until no switch was necessary anymore. Probably this is the most lame ordering algorithm ever, bit good enough for a start.
˜ move.w data,d0 ˜ move.w data+2,d1 ˜ cmp.w d0,d1
here we copy the content of the table first word (not the address as we don't precede the label name with #) and the second word which start 2 bytes further away, into d0 and d1 respectively. then we compare d1 to d0 (even if we write d0,d1 !!) this is important to note.
˜ bhi next
branch to next pair if d1 is already higher than d0. else write back the values at inverted potions in the data table
˜ move.w d1,data ˜ move.w d0,data+2
here a first version which will just inverse the first two values if applicable.
That is all for today next we will need to change the program to go trough all the table elements by moving the address of the data location into an address register and use the advantage of incremental moving.
to run trough the table we have to put the address of data (where the numbers are stored) in an address register that we can increment after each move.
˜ move.l #data,a0 ˜ move.w (a0),d0 ˜ move.w 2(a0),d1
notabene here we precede the label with # indication that we will load the address of the labels into a0 not the content of the specific memory location. then we can copy the data into the registers by offset addressing. now we can advance in the table but we need to detect when the end of the table is reached else we will be in a infinite loop. for this we add a label after the data block called data end and we compare the a0 to this location - 2 bytes (as we want to know if we are at the last pair of words, a0 points to the First pair), then jump to the end is equal.
˜ add.l #2,a0 -> increase the pointer ˜ cmp.l #dataend-2,a0 -> are we already over the last word? ˜ beq end -> end if so ˜ bra loop -> contiune if not
here the full code so far
so far we have made one run trough the data table and ordered one number. now we need to restart and order the next number. to understand if we all numbers have been ordered we simply check if there has been an inversion or not. if we make a full run and all numbers are ordered, there was no inversion. then we end the program. here the logic
˜ start: ˜ set table beginning to address pointer ˜ reset swap marker ˜ loop; ˜ check pair ˜ if ordered branch to next ˜ else swap ˜ set swap marker ˜ copy back swapped values ˜ next: ˜ increase address pointer ˜ if end of table brance to end ˜ else branche to loop ˜ end: ˜ if swap marker set branche to start ˜ else we are finished
here the code
There is only one issue with the code above: what happens when we have twice ore more times the same number in the data set? the code will enter in an infinite loop when comparing the numbers as we branch only if we have a difference in the value.
˜ cmp.w d0,d1 ˜ bhi next -> brances only id d1 > d0
we should however branch if higher or same. this is branch to the next pair when d1>d0 or dame. this is done with bhs
˜ cmp.w d0,d1 ˜ bhs next -> brances only id d1 >= d0
the next issue arises when we are having negative numbers in the database as bhs is comparing unsigned numbers. actually there are two different classes of branch instructions, one class is for signed numbers and one for unsigned numbers. the instruction we are looking for is bge (branch if grater or equal)
˜ cmp.w d0,d1 ˜ bge next -> brances only id d1 >= d0 ˜ for signed numbers
then the corrected program is as follows
notabene the signed numbers are FFFx. here a list of branch instructions for unsigned and signed numbers
˜ unsigend: ˜ bhi higer ˜ bls lower or same ˜ bcc carry clear ˜ bhs higher or same ˜ bcs carry set ˜ bls lower or same ˜ signed: ˜ bge grater than or equal ˜ blt less than ˜ bgt greater than ˜ ble less than or equal ˜ inependent: ˜ bt true ˜ bf false ˜ bne not equal ˜ beq equal ˜ bvc overflow clear ˜ bvs overflow set ˜ bpl plus ˜ bmi minus
up to now we have been concerned with 68k assembly which can be implemented in any computer with such a cpu. next we will address amiga specific instructions to drive the customs chips.
lets start by the classic example to check if the mouse button is pressed. lets start to review loops with counts (such as if or while). We can use the bdf (maybe stands for decremented branch until false) instruction to branch and decrement a value in a data register until it is not false, i.e. not zero
˜ run: ˜ moveq #5,d7 ˜ loop: ˜ dbf d7,loop ˜ rts
the loop in the code above will not repeat 5 times as it might be expected but 6 times as the decrement happens after the test. this is in, the first loop the value of d7 is 5, then 4, 3, 2, 1, 0 and stop branch! therefore sometimes you will find codes that makes 5-1 loops, e.g. the number of loops-1 so that it can be easily seen read. you can test the number of loops with the following program:
˜ run: ˜ moveq #0,d0 ;counter ˜ moveq #5,d7 ;loop counter ˜ loop: ˜ addq #1,d0 ;increase by 1 ˜ dbf d7,loop ˜ rts
here the new command addq is similar to moveq as it uses a shorter opcode and is faster, but addq can only add values from 1-8. now lets make an infinite loop and exit from the loop when a mouse button is pressed. to understand if a button has bee pressed we will reed a data register, more precisely one of the 8bit CIA registers which take care of the communications with the keyboard, mouse, joystick etc. there are two such registers (CIA-A and CIA-B), the mouse buttons in CIA-A. although each register is 8bit the addresses are 256 bytes apart. for CIA-A the first address is
˜ $BFE001 -> 1st register address ˜ $BFE101 -> 2nd register address (256 bytes away) ˜ $BFE201 -> 3rd register address (")
the status of left amiga mouse button is stored in the 6th bit if of the CIA-A register at $BFE001. this bit is always at 1 (set) and when we press the left button the bit toggles to 0 (while pressed). the CIA registers are thus so called active low. these are by the way the same chips used by the commodore c64. to test if a bit is set or no we use the btst instruction which takes the number of the bit and the address to test the bit (remember each address block is 8 bit, 1 byte, just like the CIA)
˜ btst #6,$BFE001 ;test lmb press
then when we make a loop to test for the mouse button press we should branch back into the loop if the test results not zero (bne)
˜ start: ˜ btst #6,$BFE001 ˜ bne start ; 0 = pressed ˜ end: ˜ rts
the btst instruction is a bit special as it can test a different amount of bits depending on if you test a data register or address
˜ btst #x,dx -> can test bits 0-31 ˜ btst #x,$xxxxxxxx -> can test bist 0-7
Similarly to the 8 bit CIA registers the custom chips can also be addressed trough address registers which this time are 16 bit wide. the start at the address $DFFxx
˜ CIA-A -> $00BFExxx ˜ CIA-C -> $00BFDxxx ˜ Custom Cips -> $00DFFxxx
the address of the screen background color register is at
˜ backgrond color -> $DFF180
the background color is also called COLOR00. for changing t to read we simply move the rgb value $0rgb to this address, where r, g and b are the nibbles for the respective colors and can go form 0 to F, the most significant nibble is always 0
˜ move.w #$F00,$DFF180 ; 16 bit move of color red
we can now complete the program to change the background to red and wait for the left mouse button
˜ start: ˜ move.w #$F00,$DFF180 ; set background red ˜ btst #6,$BFE001 ˜ bne start ; 0 = pressed ˜ end: ˜ rts
when you run this program we see a flickering between red and blue at the beginning of the screen, which is doe to the fact the the amiga system tries to reset the background color and that we are not changing the color in sync with the screen refresh rate. an important detail when working with the memory registers for the custom chips which are 16 bit is to remember that the memory addressing in the amiga is in blocks of 8 bit. thous if we look at the memory location for the background color ($DFF180) it will be split as follows:
˜ $DFF180 $DFF181 ˜ |15 14 13 12 11 10 09 08 | 07 06 05 04 03 02 01 00 ˜ bit number
thus the color is actually divided in two addresses of 8 bit each. thus the next color (COLOR01) will be at $DFF182 and not at $DFF181! the amiga has 32 color registers
˜ COLOR00 -> $DFF180 ; background ˜ COLOR01 -> $DFF182 ; forground (i.e. text) ˜ COLOR02 -> $DFF184 ˜ . ˜ . ˜ COLOR31 -> $DFF1BE
although the amiga can address a color space of 4096 colors ($FFF -> 16 x 16 x 16 combinations possible) it can display only 32 colors at once. just to be clear, the code above is amiga specific and wont work on any other 68k machine, given to the specific way the custom chips are addressed. also the address of the color can be calculated as $DFF180 + 2 * the color number, i.e. COLOR01 = $DFF180 + 2 * 01 = $DFF182; COLOR03 = $DFF180 * 2 * 03 = $DFF186. there color register are write only, this is you can write a value to it but you can never read what value they contain. also you cant do arithmetic operations onto it like adding 1 to the current color.
before we continue lets see two more addressing modes:
˜ the post increment: ˜ move.b #$xx,(a0)+ -> increment a0 by 1 byte ˜ move.w #$xxxx,(a0)+ -> increnent a0 by 1 word ˜ move.l #$xxxxxxxx,(a0)+ -> increnent a0 by 1 long word
notabene a0 is incremented after the move operation (thus post increment). this can be used to run through data tables:
˜ start: ˜ move.l #data,a0 ˜ move.b #$ff,(a0)+ ˜ rts ˜ data: ˜ dc.b $00,$00.$00
here after the second move a0 will point to the second element in the data table. pre decrement can be used similarly
the post increment:
˜ move.b #$xx,-(a0) -> decrement a0 by 1 byte ˜ move.w #$xxxx,-(a0) -> decrenent a0 by 1 word ˜ move.l #$xxxxxxxx,-(a0) -> decrenent a0 by 1 long word
notabene the decrement happens before the move. thus we can fill in a table from the end towards the beginning.
˜ start: ˜ move.l #dataend,a0 ˜ move.b #$ff,-(a0) ˜ rts ˜ data: ˜ dc.b $00,$00.$00 ˜ dataend:
back to write colors onto the background. as we saw before it is not possible to read the current color from a color register or to add to it. so if we would like to increase the color value of a certain color register we need to hold the color value i a data register, copy it into the color register and the increase the data register and copy again etc. we can modify our previous code to increase the background color at each loop of the mouse button wait.
˜ run: ˜ moveq #0,d0 ; set all to zero ˜ mouse: ˜ move.w d0,$DFF180 ; set background color ˜ add.w #$1,d0 ; increase the color ˜ btst #6,$BFE001 : lmb pressed? ˜ bne mouse ; 0 = pressed ˜ end: ˜ rts
its raster time! the raster is very crucial to the programming in amiga. on old CRT monitors the image was build by rastering, this is scanning, an e-beam from left to right ant top to bottom. once the beam is at the bottom it jumps again to the top of the image. The time it needs to jump up is called vertical blank interval (VBI sometimes also called VBL). similarly the time needed for the beam to go from the end of one line to the beginning of the next line is called horizontal blank interval (HBI). one entire screen in PAL mode is mastered 50 times per second (50 Hz).
vertically the e-beam technically can run 625 lines which are rastered in two runs per frame, one for the par and one for the odd lines. this allowed to have high vertical resolution (625 lines) which however took more time to build one frame and reduced the frame rate to 25 Hz (two runs per frame at 50Hz -> full frame at 25 Hz). this mode is called interlaced and is rarely used. the non interlaced mode will only use 312 lines, which can be displayed at 50 Hz. notabene that technically the e-beam is still scanning once the pair and once the odd lines an this the image is zigzagging up and down at 25Hz. this is not noticeable as the lines are very close. vertically the resolution was either 320 or 640 pixels. independently of interlaced or non interlace mode.
the Amiga screen consist of a central zone called the playfield which is surrounded by a border zone. normally one will put the graphics inside the playfield, however the Amiga also permit to write into the border region by so called overscan. the position of the e-beam is indexed/codified/numbered for being accessible by the user. the first line of the playfield starts at $2C and end at $12C thus the playfield is 256 lines high. the lowest possible line (inside the border) is line $138 and the highest possible line is obviously $00, between $00 and $1B there is no rastering happening as this is the time necessary for the beam to reach the top of the screen again.
˜ $001B -> screen start ˜ $002C -> playfield start ˜ $012C -> playfield end ˜ $0138 -> screen end ˜ $0000 -> return to top
the vertical raster position can be read in a two register: VPOSR (index $DFF004, 16 bit large, red only) and VHPOSR (index $DFF006, 16 bit large, red only):
˜ VPOSR ($DFF004) ˜ 15 14 13 12 11 10 09 08 07 06 05 04 03 20 01 00 ˜ LOF - - - - - - - - - - - - - - V8 ˜ ˜ VHPOSR ($DFF006) ˜ 15 14 13 12 11 10 09 08 07 06 05 04 03 20 01 00 ˜ V7 V6 V5 V4 V3 V2 V1 V0 H8 H7 H6 H5 H4 H3 H2 H1
v0-v8 ate the 9 bits for the vertical position. 9 because we have more than 256 lines (8 bits). the vertical position is thus coded in two separate register which is rater unpractical. hx are the bits for the horizontal position which we will see later, as it is even more cumbersome. as the two registers are in a consecutive address we can read VPOSR and VHPOSR in one move.l
˜ move.l $DFF004,d0
will read both register to d0, which will now contain the vertical and horizontal position of the e-beam on the screen at the time of read. to get out only the vertical beam position we will now shift d0 8bits to the right and the vertical position will be in the lower part if d0. now we remove all other bits by masking d0 with an and.l #%00000000000000000000000111111111 which is equal to and.l #$000001FF or and.l #$1FF
˜ lsr.l #8,d0 ˜ and.l #$1FF,d0
now that we got the vertical position we could wait for a desired value and then change the background color from the desires line
˜ cmp.l #150,d0 -> line 150 in decimal
here the full code
of course on the amiga it is not usual to use the cpu to find the raster position and copy some color into a color register. the copper chip co-processor is specialized to take over that task and relief the cpu. nevertheless we will need to look for the vertical beam position to understand when we arrived at the bottom of the screen. this is important to know when to start recalculating the next screen. the usual way to do so is to have a subroutine to wait for the vertical blank interval which waits for raster line $138
˜ waitVBI: ˜ move.l $DFF004,d0 ˜ lsr.l #8,d0 ˜ and.w #$1FF,d0 ˜ cmp.w #$138,d0 ˜ bne waitVBI ˜ rts
note that we can mask only the word of d0 (and.w) if and only if we will also compare to the word in d0 (cmp.w). to be more explicit instead of d0 we could write d0.w but this would be redundant. to execute the subroutine we branch to it with the bsr (branch to subroutine) instruction
˜ bsr waitVBI
similarly we can make a subroutine waiting for a raster line
˜ waitraster: ; line nuber to wair for in d1.w ˜ move.l $DFF004,d0 ˜ lsr.l #8,d0 ˜ and.w #$1FF,d0 ˜ cmp.w d1,d0 ˜ bne waitraster ˜ rts
please find the example code so far here
next we will use the copper co-processor to change the background color and there we will need to wait for VBI as well.
its copper time! the reason why we had the bg color change instruction inside the mouse loop (which is here the main loop) is that the system is rewriting the standard screen at each frame. this is it is putting back the colors and the content of the screen every frame. thus we have to change it at least every frame. its is in fact the copy who is resetting the screen following a copperlist given by the system. the user can take advantage of the copper by passing it a custom copper list to display on the screen. we can check this by disabling the copper, this is disabling the DMA channel of the copper so that it cant access the chip memory, where the copper list resides, any longer. this register is called
˜ DMACON = $DFF096 -> for writing (16 bit) ˜ DMACONR = $DFF002 -> dor reading (16 bit)
here the buts of that register(s)
˜ 15 -> SET/CLR (set/clear the following bits) ˜ 14 -> BBUSY ˜ 13 -> BZERO ˜ 12 -> unused ˜ 11 -> unused ˜ 10 -> BLTPRI ˜ 09 -> DMAEN (enable all DMA channels) ˜ 08 -> BPELN (enable bitplane) ˜ 07 -> COPEN (enable copper) ˜ 06 -> BLTEN (enable blitter) ˜ 05 -> SPREN (enable sprites) ˜ 04 -> DSKEN (enable disk) ˜ 03 -> AUD1EN (enable audio channel 1) ˜ 02 -> AUD2EN (enable audio channel 2) ˜ 01 -> AUD3EN (enable audio channel 3) ˜ 00 -> AUD4EN (enable audio channel 4)
when writing into the DMACON we need to specify in bit 15 if the following bits are going to be set or cleared, i.e. setting (1) bit 15 and 8 will set bit 8, clearing bit 15 (0) but setting (1) bit 8 will clear bit 8. also all bits that are 0, except 15, will remain unaffected.
˜ move.w #%1000000100000000,$DFF096 -> switch on bitplanes ˜ move.w #%0000000100000000,$DFF096 -> switch off bitplanes
therefore if you want to set and unset some bytes it will need two instructions, on to unset and one to set. so to disable the copper we need to clear bit 7
˜ move.w #%0000000010000000,$DFF096 -> switch off copper
remember to think in nibbles (4 bytes -> numbers from 0-15 -> one hex digit 0-F) to convert from binary to hex this the binary number to disable the copper is
˜ binary: % 0000 0000 1000 0000 ˜ hex: $ 0 0 8 0
thus
˜ move.w #$0080,$DFF096 -> switch off copper
if we would like to disable all DMA channels
˜ move.w #%0111111111111111,$DFF096 -> switch off copper ˜ move.w #$7FFF,$DFF096 -> switch off copper
on the other hand to activate (reactivate) all DMA channels
˜ move.w #$FFFF,$DFF096 -> switch off copper
now if we deactivate all DMA at the beginning of our previous code and reactivate them just before we return to the OS (after the left moues button press) we only need to set the bg color once and the whole screen is of that color. now we could also wait for different raster positions and change the background color at different vertical raster positions. this is not the way it is done, usually we do a copper list and point to it. but as an exercise let do it.
when you run the code you will see that the color change is not at the beginning of th raster line and that it is flickering. this is do to the fact that the cpu is not in sync with the raster. but we will now see how to do the same thing with the copper. the will need a so called copperlist, which is sort of the instructions the copper will execute. the copper list is defined like a data block with a label. notabene, asm one does not have mnemonics for the copper instructions so we need to code them in machine language, i.e. hex code. the copper is basically coping data into a memory location (register) or waiting. each instruction is in two words
˜ copper: ˜ dc.w $100,$0200 ; copy $0200 (data) -> $100 (register) ˜ ; this is the bitplane control registe ˜ , which we will see later ˜ dc.w $6407,$FFFE ; wait for line $64 and column $07, $fffe is wait ˜ dc.w $180,$F00 ; copy red color to bg color ($DFF180) ˜ dc.w $6507,$FFFE ; wait for line $65 and column $07 ˜ dc.w $180,$000 ; copy black color to bg color ˜ dc.w $FFFF,$FFFE ; end the copper list by waiting for an impossible position
the last number of the move command ($FFFE) needs to be odd. for the copper all addresses are indexed with $DFF000, thus if we instruct to write to $180 he will write to $DFF180. the copper list has to reside in the chip memory, else the copper cant access it. this can be done by the asmone pseudo instruction
˜ section copperlist,data_C
to activate the copper we have to move the address of the copper list to $DFF080 and activate the copper
˜ move.l #copper,$DFF080 ; move copper list address ˜ move.w #0,$DFF088 ; activate the copper
the registers are as follows
˜ $DFF080 -> COP1LCH (high word of copper 1) ˜ $DFF082 -> COP1LCL (low word of copper 1) ˜ $DFF084 -> COP2LCH (high word of copper 2) ˜ $DFF086 -> COP2LCL (low word of copper 2)
The addresses on the amiga are 32 (long-word) bits wide, so as the registers are only 16 bits (word) wide two of them are needed to store the copper list address the High and the low parts. also there are two copper list address pointers, COP1XXX and COP2XXX. to write into the first copper list address we simply write a long word to to the high word pointer (COP1LCH) which will the fill both registries at once.
˜ $DFF088 -> COP1JMP (activate copper 1) ˜ $DFF08A -> COP2JMP (activate copper 2)
these two are so called strobe registers, which at the moment we write something into them they trigger the activation of the copper 1. when using this method there the color changer will always be synchronous to the raster. compared to before we do not deactivate the DMA and reactivate them before exit, we simply copy the address of our own copperlist to the copper pointer and activate it, bit before we leave we need to move back the address of the system copper, else we wont see the asmone screen. to do so we need to do some system programming to get the address of the current copper list of the system.
the copper is fetching its list/code during the vertical blank interrupt (VBL), this is when the e-beam runs form $00 to $1B. thus we can update the copper list any time in between ant it will be fetched at the beginning of the screen at each frame. if we check the instructions set of the copper more in detail, we see that there are basically two type of instructions
˜ move: ˜ dc.w $100,$0200
this instruction moves the value 200 to the address $DFF100. the copper knows that it is a move instruction basically because the first number (address) is pair. remember, instructions always start at pair addresses. same here
˜ move: ˜ dc.w $180,$F00
when the first number is odd and the second is pair, the copper knows that it is a wait command
˜ wait: ˜ dc.w $6407,$FFFE
here he want for line $64 and column $07. $FFFE is a mask for the lines (high nibble) and columns (low nibble) (we will see that probably later)
today a little intermezzo into system programming. all system calls and libraries are stored in in the kickstart rom (kind of the system kernel), to access them you need to know where they are located. the only fix point is address where the execBase is stored ($4) which is the root of all sys calls. from there we can access the open and close library functions which are at a fix offsets from the execBase address. these subroutines will help us to open other libraries, such as the dos library or graphics library. actually they will give us the the respective base address from which we can by offset access various calls and data such at the current system copper list.
the execBase address is stored in memory location $4. as the address can vary with different kickstart versions, the safe way is to look it p at $4 where it is always correct. form there we can call the open or close library routines with an offset to the execBase address. the offsets are fix and can be defined as constants at the beginning of the code. the open library routine needs the execBase in a6 and a pointer to the name of the routine to open in a1 (i.e. graphics.library) and it will return the address of the graphicsBase (gfxBase) to d0. this can also vary with kickstart version, therefore it is save to get the address by an open library system call. demos do hard code the addresses, which may function on some amiga versions, but this is not a good practice (although much shorter in code). the system copper is then again located at a fix offset from the gfxBase address and can be saved.
so it OS basically a cascade of calls and offsets:
˜ excecBase at $4 ˜ -> opelib subroutine stores at a gieven offset from execBase ˜ -> called to get the address of gfxBase ˜ -> Syscopper address stored at a given offset from gfxBase
as said this cascade ensures the compatibility of code for various kickstart versions which may have different locations of the library bases. here an example code. sometimes this routines are also calls s startup routines:
˜ ExecBase = 4 ˜ OpenLib = -552 ˜ CloseLib = -414 ˜ OldCopper = 38 ˜ ˜ ˜ openlibs: ˜ move.l ExecBase,a6 ; move the root address for system calls ˜ move.l #gfxname,a1 ; move the pointer (address) to the ˜ ; lib name we want to open to a1 ˜ ; this the # ! ˜ clr.l d0 ; the return will be in d0 ˜ jsr OpenLib(a6) ; jump to the system subroutine to open ˜ ; the library which is located at the ExecBase ˜ ; plus offset by OpenLib ˜ tst.l d0 ; test if we got something back ˜ beq end ; if still zero end the programm ˜ move.l d0,gfxbase ; else save the graphics base library base address ˜ clr.l d0 ˜ move.l gfxbase,a6 ; move the graphics base address to a6 ˜ move.l SysCopper(a6),d0 ; move the copper addres to d0 ˜ tst.l d0 ; is there soemtin inside? ˜ beq end ; end of zero, no address ˜ move.l d0,oldcopper ; save the address if the sys copper ˜ ˜ main: ; your code here ˜ ˜ clean: ; close linbs befor e leaving ˜ move.l ExecBase,a6 ˜ move.l gfxbase,a1 ; the routine CloseLib needs the base of the lib ˜ ; to close in a1 ˜ jsr CloseLib(a6) ; close the library ˜ ˜ end: ˜ move.l oldcopper, $DFF080 ; move nack the address of the system ˜ ; copper before going back ˜ rts ˜ ˜ gfxname: ˜ dc.b "graphics.library",0 ; Name of the gfx.library ˜ even ; be sure we end on an even address ˜ gfxbase: ˜ ds.l 1 ; save the gfx base here ˜ oldcopper: ˜ ds.l 1 ; save the system copper here
whit all that we can now rewrite the code generating a clean red line using the copper which is synced to the raster lines, using the system (kernel) routines to restore the system copper and exit clean to the system again.
As we saw the copper can wait for certain positions on the screen and copy values into memory locations or hardware registers. this in not limited to the background colors, the copper can write to all custom chips. Isn't this a fantastic concept?
It is a goo practice to save the state of the DMA by reading the DMACONR register ($DFF002)
˜ move.w $DDF002,olddma ˜ ˜ olddma: ˜ ds.l 1
which similarly to the old copper will be copied back ti DMACON ($DFF096) be fore exiting. but we cant just copy the content of the saved DMA blindly, we first need to set bit 15 (set/clr) to 1, so that we set values, and we also need to set bit 9 (DMAenable) to 1, so that we are sure to keep the DMA enabled. bit 15 is usually unset when we copy it from DMACONR. we do this with an or instruction (if set OK if not set) with %1000 0010 0000 0000 which is $8200 (think in nibbles!)
˜ or.w #$8200,olddma ; set bits 15 and 9 ˜ move.w #olddma,$DFF096 ; rostore oroginal DMA
now that we saved the original state of the DMA we can only set the channels we need, i.e. only the copper. thus we need to set bit 15, 9, and 7 and unset all the others: %1000 0010 1000 0000 -> $8280, after we deactivated all DMA first (remember we can either activate or deactivate but not bath at the same time)
˜ move.w #$7FFF,$DFF096 ˜ move.w #$8280,$DFF096
now e have a much cleaner entry and exit of the program. to improve the readability of the code it is good practice to define constants for the custom chip registers. it is also possible to include libraries, that among others also define these registers constants. to keep the code slim however it is better to just define the register names we actually need, but use the common naming conventions from the libraries defined by commodore.
˜ CUSTOM = $DFF000 ˜ DMACONR = $002 ˜ DMACON = $096 ˜ COP1LCH = $080 ˜ COPJMP1 = $088 ˜ COLOR00 = $180
notabene, we only define the offsets, so that we can use the constants also in the copper list, in which also takes the offsets from the custom chip base address $DFF000. now when we save the current DMA state we can write
˜ move.w CUSTOM+DMACONR,olddma
or better we move the CUSTOM address to a6 and use move with indirect addressing, which BTW. is also a shorter and faster instruction
˜ lea CUSTOM,a6 ˜ move.w DMACONR(a6)
same for the rest of the code. note that a6 will from now on be reserved for the custom chip base address, which in some cases might not be convenient an also a source of bugs! here the full code with an updated copper list displaying an RGB flag.
lets make a little copper cart today. for this leave all as is in the code for the rgb and change the copper list to the following
˜ dc.w $100,$0200 ˜ dc.w $1B07,$FFFE ˜ dc.w $180,$000 ˜ dc.w $6407,$FFFE ˜ dc.w $180,$555 ˜ ˜ dc.w $6507,$FFFE ˜ dc.w $180,$999 ˜ dc.w $6607,$FFFE ˜ dc.w $180,$CCC ˜ ˜ dc.w $6707,$FFFE ˜ dc.w $180,$FFF ˜ ˜ dc.w $6807,$FFFE ˜ dc.w $180,$FFF ˜ ˜ dc.w $6907,$FFFE ˜ dc.w $180,$CCC ˜ ˜ dc.w $6A07,$FFFE ˜ dc.w $180,$999 ˜ ˜ dc.w $6B07,$FFFE ˜ dc.w $180,$555 ˜ ˜ dc.w $6C07,$FFFE ˜ dc.w $180,$000 ˜ ˜ dc.w $FFFF,$FFFE
we now should know that the instructions do. we can now also move the copper bar down the screen by adding a label to the wait value we want to change and increase it and all the others with the cpu. more precisely for the copper bar above we need to increase the value of 9 wait instructions. thus we add the following at the beginning of the main loop
˜ lea copperbar,a0 ; save copperbar address ˜ move.w #9-1,d7 ; initiate the lkoop counter ˜ ; remeber: bxx loops once too much ˜ loop: ˜ add.b #$01,(a0) ; add one byte to the content of ˜ ; copperbar, which is the first wait ˜ ; this (a0) not a0 ˜ add.l #$08,a0 ; increase the address to point to ˜ ; next wait intruction ˜ dbf d7,loop ; branch and decrease d7 until zero
notabene it is important to add a byte not a word as we ant to change the very first byte of the wait instruction which is the vertical raster position: $6407,$FFFE -> $64 is the fist byte! we also need to wait for each vertical blank interval to update the copper, else it will be updated out pf sync during drawing the screen. our routine form before can just be called in the main loop!
˜ bsr waitVBI
Another important point with the copper wait in the for we saw, is that the highest vertical raster we can wait for is $FF (line 256). This however is not the bottom of the screen, which has 312 line (in the mode we use here). we saw before that the screen goes until $138. to go beyond $FF we have to tell the copper with a special wait command that the next wait command will be lines after $FF
˜ dc.w $FFDF,$FFFE -> next wait > $FF ˜ dc.w $1007,$FFFE -> wait for line $10F ˜ -> = $FF + $10
Actually the copper scroll we just programmed did not ho paste line 256.
today we will start to get into waiting on the horizontal positions of the e-beam. while on the vertical lines the copper can address each pixel in the horizontal it can only address the pair pixels. this is clear when you look at how the raster position is encoded in the VPOSR/VHPOSR register where for the horizontal line there is no H0, by choice of the amiga developers
˜ VPOSR ($DFF004) ˜ 15 14 13 12 11 10 09 08 07 06 05 04 03 20 01 00 ˜ LOF - - - - - - - - - - - - - - V8 ˜ ˜ VHPOSR ($DFF006) ˜ 15 14 13 12 11 10 09 08 07 06 05 04 03 20 01 00 ˜ V7 V6 V5 V4 V3 V2 V1 V0 H8 H7 H6 H5 H4 H3 H2 H1
this mans that seen from the raster the actual positions $00 and $01 are both perceived as position $00
˜ real -> VPOS ˜ $00,$01 -> $00 ˜ $02,$03 -> $01 ˜ ...
thus the position $6E on the screen is encoded as $37 an you can calculate it by $37 * 2 + 0 or + 1 = $6E or $6F. now for the copper there is an additional restriction ad the H1 is always set to 1. this is why the the wait instruction for the copper accepts only odd numbers for the horizontal positions. this actually address only every 4th pixel. and to make it even more complicated the real position is actually not $xx * 2, BUT ($xx + 1) * 2. this is when we write wait for $45 the real pixel position is ($45 + 1) * 2 = $46 * 2 = $8C = 140. Probably this is due to the time needed to fetch the next instruction on the copper list (color change etc.)
˜ horizontal: ˜ VPOS -> Real ˜ $xx -> ($xx+1)*2 ˜ $xx only odd
in the horizontal we also have a blank interval (HBI). the first horizontal position is $07, the HBI goes until $35, the screen goes until $E2 and then switches to $00 and runs until $06 before the beam returns. which is quite complicated. this is why when waiting for the next line we wait for position $07 (the positions $00-$06 are at the end of the line!)
˜ $07--->$35------->$E2 $00--->$06 ˜ HBI SCREEEN ???
Compared to the copper, Denise can address each pixel also in the horizontal position, which is important for the sprite positioning.
It is also important to notice that the copper although being very fast and convenient, needs the time of 8 pixels to process a wait command. this Mensa that if we would make consecutive moves into COLOR00 without a wait command, the colors would switch after the e-beam has drawn 8 pixels.
Today I am stating to have a look at the blitter, another fantastic piece of technology! we previously saw that the copper can access only every 4th vertical pixel, i.e. he has a granularity of 4. the cpu has a granularity of 2 due to the fact the VHPOS register misses the first bit (H0). the Denise chip on the other hand has a granularity of 1, she can access all the chips of the screen. this is important when we want to position the playfield, for example. the standard playfield with a size of 320x256 pixels goes from $2C to $12C in the vertical and $81 to $1C1 in the horizontal. these values are stored in two register
˜ DIWSTRT ($08E) -> upper left corner ˜ -> i.e $2C,$81 ˜ DIWSTOP ($90) -> lowe right corner ˜ -> i.e. $12C,$1C1
These two registers are write only. as always there are some peculiarities here too. DIWSTRT is simply two 8 bit numbers for vertical and horizontal positions
˜ DIWSTRT ($DFF08E) ˜ 15 14 13 12 11 10 09 08 07 06 05 04 03 20 01 00 ˜ V7 V6 V5 V4 V3 V2 V1 V0 H7 H6 H5 H4 H3 H2 H1 H0
notabene there is a H0 in comparison to VHPOS. DIWSTOP on the other hand is a bit complicated. as the screen extends beyond 256 lines the stop corner needs a 9th byte for the horizontal and vertical position.
˜ DIWSTOP ($DFF090) ˜ 15 14 13 12 11 10 09 08 07 06 05 04 03 20 01 00 ˜ V7 V6 V5 V4 V3 V2 V1 V0 H7 H6 H5 H4 H3 H2 H1 H0
this means that there are two hidden bytes:
˜ H8 -> is always 1 ˜ -> horizontal position always >= 256 ($FF) ˜ V8 -> compelentary to V7 ˜ -> if V7 = 1 V8 = 0 ˜ -> if V7 = 0 V8 = 1 ˜ -> vertical position always >= 128 ($80)
thus if we ant to write the standard playfield values we saw above, we add the following into the copper list:
˜ dc.w $08E,$2C81 -> DIWSTRT: $2C,$81 ˜ dc.w $090,$2CC1 -> DIWSTOP: $12C,$1C1
notabene in DIWSTOP $2C = $12C as $2C = %00101100, thus V7 = 0 and V8 = 1 this the 9but value is %100101100 = $12C, in the horizontal position the ninth bit is always set thus $C1 = %11000001 will be seen as %111000001 = $1C1. this will define the playfield into which Denise will write.
the playfield we set in DIWSTRT and DIWSTOP will contain the bitplanes that Denise will draw into. in every pixel of the screen Denise can set a color by giving it the color number. remember that we can have up to 32 colors defined in the registers COLOR00 to COLOR31. the number which is set in the pixel will point to this very register, i.e. if the pixel contains the number 0 it will be COLOR00 if it contains 1 it will be COLOR01. this is for if we have one bitplane. if we want ti use 4 colors, we will need two bitplanes. per pixel we can then address 4 colors
˜ BLP1 BPL2 COLOR ˜ 0 0 00 ˜ 1 0 01 ˜ 0 1 02 ˜ 1 1 03
so we combine bitplanes which can contain only one bit each, the by combining several bitplanes we can address more colors. side info: although this was a revolutionary it was one of the reasons why amiga was not able to perform 3D graphics with textures etc so well, here the chunky modes offered by the PC compatibles worked better, e.g. Doom etc.
if our standard playfield has 320 pixels per line and 256 lines, this would be 320 bits per line and 320/8 bytes per line -> 40 bytes per line. the for storing one p^bitplane of a playfield we need to reserve in memory 40*256 bytes. all that has again to be in chip memory
˜ dcb.b 320/8*256,$F0
as you know this command will reserve the defined amount of bytes and set them to $F0, which is in binary %11110000. when we use only one bitplane, this will set the first 4 pixels to COLOR1 and the following 4 pixels to COLOR0, the repeat trough all the playfield.
next we need to tell Denise where the bitplanes are located, i.e. give her the pointer to our bitplane. for doping so there are two registers, one for the high part of the address and one for the low part. remember the register are 16 bit and the addresses are 32 bits, thus 2 registers are needed, similar to the copper. for the first bitplane the registers are
˜ BPL1PTH = $0E0 ˜ BOL1PTL = $0E2
these register have to be set in the copperlist, as we need to remind Denise at each screen build where to look for the bitplane information!
˜ dc.w $xxxx,$0E0 ˜ dc.w $xxxx.$0E2
for simplicity we could set the address of our bitplane pointer to $50000
˜ ORG $50000 ˜ bpl1: ˜ dcb.b 320/4*256,$F0
the the copper command to set the pointer of the first bitplane would be
˜ dc.w $0E0,$0005 ˜ dc.w $0E2,$0000
Also we will need to activate the bitplanes in the DMA in addition to the copper (so far we only had the copper channel active), i.e. set also bit 8 in the DMA
˜ DMA ˜ 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00 ˜ 1 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 ˜ -> $8380 ($8280 is only copper on!)
in addition to that we need to define the numbers of bitplanes in the BPLCON0 register (this is the first copy command of the our copperlist so far)
˜ BPLCON0 = $100 ˜ 15 enable highres ˜ 14 BPU2 this 3 bits define ˜ 13 BPU1 the nuber of bitplanes ˜ 12 BPU0 to use (0-6) ˜ 11 enable HAM mode ˜ 10 enable DBPLF dual playfield ˜ 09 enable Color video out ˜ 08 Genlog audio selector ˜ 07 - ˜ 06 - ˜ 05 - ˜ 04 - ˜ 03 enable light pen ˜ 02 enable interlace ˜ 01 enable external sync ˜ 00 -
so far we st this register to $0200 which is
˜ $0200 ˜ %0000 0010 0000 0000 -> enable color output set
if we want to set 1 bitplane the BPU need to be 001 thus
˜ %0001 0010 0000 0000 -> $1200
then the first instruction in the copper for activation one bitplane is
˜ dc.w $0100,$1200
here a screen shot of the playfield filled with red stripes ($0F) drawn by pixel perfect by Denise (blitter)
Continuing with the bitplanes and playfield. the playfields are a fields of pixels that Denise can write with 1 pixel precision compare to what the copper can do. the amiga can have up to two playfields. one filed was typically used to display the status bar or similar and a second field was where the game play would happen. the color of the pixels is defined by bitplanes which can contain 0 or 1 per pixels up to five bitplanes can be combined to represent the 32 colors stored in the respective registers, i.e. if look at the first and pixel and their respective state in the 5 bitplanes we could have something like
˜ pixel pixel pixel ˜ BPL1 0 1 1 ˜ BPL2 1 1 0 ˜ BPL3 1 1 1 ˜ BPL4 1 1 0 ˜ BPL5 0 1 0 ˜ Color 14 31 5
notabene, the order is ascending, i.e. bpl5 will have the most significant bit and bpl1 the last significant bit. the bitplanes have to be defined in the chip memory as we saw before. also we have to pass the pointer to Denise (BPLxPTH,BPLxPTL) and also tell Denise how many bitplanes we intend to use (in BPLCON0). the content of the bitplanes can be defined at the moment of creation or the defined memory space can be written in to. we could for example define an empty ($00 = COLOR00, background) bitplane in chip memory and write into it before the main loop
˜ move.b #$FF,bpl1 ˜ main: ˜ ... ˜ ˜ bpl1: ˜ dcb.b 320/8*256,$00
this would set the first 8 bits of the playfield to COLOR01, while all others are COLOR00 (if BPLCON0 is set to 1 bitplane). it is also possible to set only one bit or some bits
˜ move.b #$80,bpl1 ; set the first pixel ˜ move.b #$AA,bpl1 ; set every odd pixel ˜ ; in the first 8 pixels ˜ ; %10101010 = $AA
if we want to write the byte at the end of line one we need to write to bpl1+39. similarly
˜ move.b #$80,bpl1+39 ; last byte line 1 ˜ move.b #$80,bpl1+40 ; first byte line 2 ˜ move.b #$80,bpl1+40*40 ; first byte line 40 ˜ move.b #$80,bpl1+40*255 ; first byte last line ˜ move.b #$80,bpl1+40+128+20 ; middel of the screen
to write on the full playfields we need also to set the two following register
˜ DDFSTRT = $092 ˜ DDFSTOP = $094
which we also have to write in the copperlist
˜ dc.w DDFSTRT,$38 ˜ dc.w DDFSTOP,$D0
we will explore later what these mean, and how to choose them.
if we want to use more than one bitplane we have to redo the same as we did for bitplane 1, this is reserve the chip memory, pass the pointers for the second bitplane to Denise, change BPLCON0 to two bitplanes and also define 4 colors
˜ copper: ˜ dc.w BPLCON0,$2200 ; two bitplanes ˜ ... ˜ dc.w BPL1PTH,$0005 ; remeber we defined bpl1 ˜ dc.w BPL1PTL,$0000 ; at org $50000 for now ˜ dc.w BPL2PTH,$0005 ; = $50000 + 320/8*256 ˜ dc.w BPL2PTL,$2800 ˜ palet: ˜ dc.w COLOR00,$FFF ˜ dc.w COLOR01,$F00 ˜ dc.w COLOR02,$0F0 ˜ dc.w COLOR03,$00F ˜ bpl1: ˜ dcb.b 320/8*256,$00 ˜ bpl2: ˜ dcb.b 320/8*256,$00
where
˜ COLOR02 = $184 ˜ COLOR03 = $186 ˜ BPL2PTH = $0E4 ˜ BPL2PTL = $0E6
now we can write 4 colors on the screen by moving the appropriate values in the two bitplanes, i.e.
˜ move.b #%00001111,bpl1 ˜ move.b #%00110011,bpl2
will give 2 consecutive pixels of each of the 4 colors at the beginning of line 1. notabene the colors are defined in the color registers, there you will define what color 02 is not in the bitplane! Here an example where we fill the bitplanes to get a stripes with a simple grading from violet to red.
we need to discuss how the bitplane size is defined to understand the DDFSTRT and DDFSTOP. previously we saw that DIWSTRT and DIWSTOP define the upper right and lover left corner
˜ DIWSTRT ($08E) -> upper left corner ˜ -> i.e $2C,$81 ˜ DIWSTOP ($90) -> lowe right corner ˜ -> i.e. $12C,$1C1
we also saw that DIWSTOP has a 9 bits to define the lower corner
˜ H8 -> is always 1 ˜ -> horizontal position always >= 256 ($FF) ˜ V8 -> compelentary to V7 ˜ -> if V7 = 1 V8 = 0 ˜ -> if V7 = 0 V8 = 1 ˜ -> vertical position always >= 128 ($80)
for the vertical position the complexity ends here. so if we would like oversize the playfield for its typical value we can change
˜ dc.w $08E,$2C81 -> DIWSTRT: $2C,$81 ˜ dc.w $090,$2CC1 -> DIWSTOP: $12C,$1C1
to
˜ dc.w $08E,$2081 -> DIWSTRT: $20,$81 ˜ dc.w $090,$2FC1 -> DIWSTOP: $12F,$1C1
the playfield is now 16 line higher. remember also to adapt the data for the playfield and make it 16 lines higher in this case! what actually happens is that Denise starts fetching the pixel data from the bitplanes defined in memory when the raster hits the line defined in DIWSTRT until the line defined in DIWSTOP. for the horizontal position it is more complicated, as usual. Denise is not starting to fetch the pixel data from the memory at the horizontal raster position defined in DIWSTRT until DIWSTOP, for this it uses the registers DDFSTRT and DDFSTOP! the positions defined in DIWSTRT and DIWSTOP serve as a display mask, this is, it defines if the pixels are displayed or not, independently of whether they have been fetched or not by Denise form the memory, i.e. sometimes the pixels are there but nit displayed. And to make it more complicated the positions in DDFXXXX are coded in horizontal raster positions compared to DIWXXX where we code the positions in pixel (i guess for saving the 9 bit trick for large sizes). remember pixel position is 2 * raster position. the typical playfield width is defined by
˜ dc.w DDFSTRT,$38 ˜ dc.w DDFSTOP,$D0
now the raster position $38 corresponds to the pixel position $70 which is before the typical playfield start of $81. this is due to the fact that Denise needs some time to fetch the data. thus we need to start fetching $11 (17) pixels positions earlier. also after the last fetching of pixels at raster position $D0 ($1A0 in pixels), Denise sends the remaining $10 (16), and then again 16bits to the screen. So the screen does not end at $D0 but $E0 ($1C0 in pixel position, or better $1C1 which is also in raster position $E0, remember each raster position addresses 2 pixels!). Finally this means that coding a start at raster position $38 will start putting the horizontal pixels at raster pixel $81, and stooping at raster position $D0 will finish drawing the pixels at horizontal pixel position $1C1.
˜ dc.w DDFSTRT,$38 -> start write at ˜ -> pixel $81 ˜ dc.w DDFSTOP,$D0 -> stop write at ˜ -> pixel $1C1
the screen thus looks like this
˜ raster pos: $38 $40 $48 $D0 $D8 $E0 ˜ pixel pos: $70 $80/$81 $90/$91 $1A0/$1A1 $1B0/1B1 $1C0/$1C1 ˜ F---17px---D&F---16px---F&D---16px---...---F&D---16px---D&F---16px---| ˜ DWIXXX: |-------------------------...-----------------------------| ˜ 320 pixels ˜ DDFXXX: |------------------------------------...----------------| ˜ 320 pixels ˜ ˜ D=diplay pixels; F=fetch pixels
thus the horizontal screen fetch size is from $38 to $D8. i.e. the width is $D8-$38=$A0 in raster an $A0*$2=$140 in pixel which in decimal is 320 pixels wide. here you also see that DDFXXXX has always to be in steps of $8 (16 pixels) as Denise fetches the pixels in batches of 16.
BTW:
˜ DDFSTRT -> display datafetch start ˜ DDFSTOP ->splay datafetch stop ˜ DIWSTRT -> display window start ˜ DIWSTOP -> display window stop
we can visualize the masking effect by DIWXXXX on DDFXXXX by changing the DIWXXXX in the copper list
˜ copper: ˜ dc.w BPLCON0,$2200 ; two bitplanes ˜ dc.w DIWSTRT,$2C81 ; display window ˜ dc.w DIWSTOP,$2CC1 ˜ dc.w DDFSTRT,$0038 ; Denise fetch start ˜ dc.w DDFSTOP,$00D0 ; Denise fetch stop ˜ dc.w BPL1PTH,$0005 ; remeber we defined bpl1 ˜ dc.w BPL1PTL,$0000 ; at org $50000 for now ˜ dc.w BPL2PTH,$0005 ; = $50000 + 320/8*256 ˜ dc.w BPL2PTL,$2800 ˜ ˜ dc.w $8070,$FFFE ; wait on line $70 ˜ dc.w DIWSTRT,$2CC1 ; narrow dispay width ˜ dc.w DIWSTOP,$2C81 ˜ dc.w $A070,$FFFE ; wait on line $f0 ˜ dc.w DIWSTRT,$2C81 ; reset dispay width ˜ dc.w DIWSTOP,$2CC1 ˜ ˜ palet: ˜ dc.w COLOR00,$000 ˜ dc.w COLOR01,$50A ˜ dc.w COLOR02,$A06 ˜ dc.w COLOR03,$F01 ˜ bpl1: ˜ dcb.b 320/8*256,$00 ˜ bpl2: ˜ dcb.b 320/8*256,$00
we saw how we can overscan from the 320x256 standard playfield in the vertical direction. now if we ant to overscan in horizontal direction we need to change DDFSTRT and DDFSTOP. Also we need to change the display window size DIWSTRT and DIWSTOP! Don't forget to also change the bitplane size in memory as well as the address of the bitplanes. if this s the standard 320x256 screen
˜ copper: ˜ ... ˜ dc.w DIWSTRT,$2C81 ; display window ˜ dc.w DIWSTOP,$2CC1 ˜ dc.w DDFSTRT,$0038 ; Denise fetch start ˜ dc.w DDFSTOP,$00D0 ; Denise fetch stop ˜ ... ˜ bpl1: ˜ dcb.b 320/8*256,$00 ˜ bpl2: ˜ dcb.b 320/8*256,$00
this would be an overscan of 16 pixels on each side for a 352x288 screen
˜ copper: ˜ ... ˜ dc.w DIWSTRT,$1C71 ; display window ˜ dc.w DIWSTOP,$3CD1 ; 352x288 ˜ dc.w DDFSTRT,$0030 ; fetch start $60 ˜ dc.w DDFSTOP,$00D8 ; fetch stop $180 ˜ ... ˜ bpl1: ˜ dcb.b 352/8*288,$0F ˜ bpl2: ˜ dcb.b 320/8*288,$0F
up to now we passed the bitplane pointers to the bitplane registers with a direct address which we defined by with the ORG directive. this is called hard coding and is bad practice. it would be better to pass the pointer via the label bpl1 etc. But hoe to split the 32bit address in a high and low word and pass the to the respective bitplane registers? we could use an assembler directive to shift the address as is used in some dome sources
˜ dc.w BPL1PTH,bpl1>>16 ; like lsr ˜ dc.w BPL1PTL,bpl1<<16 ; like lsl
this works for some strange reasons although it is not correct
˜ bpl1>>16
shifts the address 16 bits to the right now we have the previous high word in the low word and the high word part is filled with 0s for example
˜ test = $12345678 ˜ test>>16 -> $00001234
thus this wold work if we want to move the high word in to a 16 bit register as $00001234 is equivalent to $1234
˜ move.w #test>>16,d0 ; works!
but for the left shift however
˜ bpl1<<16
shifts the address 16 bits to the left now we have the previous low word in the high word and the low word part is filled with 0s
˜ test = $12345678 ˜ test<<16 -> $56780000
here moving a word into a 16 bit register wont work as $56780000 is not equivalent to $5678
˜ move.w #test>>16,d0 ; error!
however when we do the move with the copper it works. this is however not the proper way to copy the addresses. better is to calculate the high and low word starting from the label and copy them at run time into the copperlist. in the copper we put $0000 as a place holder
˜ lea copperbpl,a0 ˜ move.l #bpl1,d0 ; we need the pointer in a data reg ˜ ; as we want do arithmentics thus ˜ ; -> use move not lea ˜ move.w d0,6(a0) ; move.w will move the low word ˜ ; of d0 to BPL1PTL which is located ˜ ; a 6 bytes form copperbpl ˜ swap d0 ; swap low and high word of d0 ˜ move.w d0,2(a0) ; move.w will move the low word ˜ ; of d0 to BPL1PTH which is located ˜ ; a 2 bytes form copperbpl ˜ swap d0 ; back to oroginal address ˜ add.l #320/8*256,d0 ; increase d0 to the address of the next bpl ˜ ; this is also hardcoded better to move.l bpl2,d0 ˜ ; this also wont need any swapping ˜ add.l #8,a0 ; set the tartget addres to the location of the ˜ ; 2nd bitplane ˜ move.w d0,6(a0) ; same as before ˜ swap d0 ˜ move.w d0,2(a0) ˜ swap d0 ˜ ˜ copper: ˜ ... ˜ copperbpl: ˜ dc.w BPL1PTH,$0000 ; register adr is $00E0 ˜ dc.w BPL1PTL,$0000 ; register adr is $00E2 ˜ dc.w BPL2PTH,$0000 ; register adr is $00E4 ˜ dc.w BPL2PTL,$0000 ; register adr is $00E6 ˜ ... ˜ bpl1: ˜ dcb.b 320/8*256,$F0 ˜ bpl2: ˜ dcb.b 320/8*256,$F0
We can now introduce some constants to make life easier in case we change the screen size and number of bitplanes
˜ W = 320 ˜ H = 256 ˜ BPLS = 2
then we can replace all the hard coded values. when we cant to tell Denise how many bitplanes we use by coping the value in BPLCON0 we seed a trick
˜ dc.w BPLCON0,$2200 ; 2 bitplanes
will become
˜ dc.w BPLCON0,BPLS<<12!$0200
lets break this down: remember bits 12 13 14 of BPLCON0 define the number of bitplanes bit 9 enables color output the we want 2 bitplanes BLPS = 2
˜ %0000 0000 0000 0010 -> BPLS ˜ %0010 0000 0000 0000 -> BPLS<<12 ˜ %0000 0010 0000 0000 -> $0200 ˜ %0010 0010 0000 0000 -> BPLS<<12!$0200 = $2200 ; 2 bitplanes! ˜ ; ! = asmone OR operation
if we want 5 bitplanes
˜ %0000 0000 0000 0101 -> BPLS = 5 ˜ %0101 0000 0000 0000 -> BPLS<<12 ˜ %0101 0010 0000 0000 -> BPLS<<12!$0200 = $5200, 5 bitplanes incolor
we can now also make a loop to set all the bitplane pointers depending on the number of bitplanes we want. the same code now looks like
˜ lea copperbpl,a0 ˜ move.l #bpl1,d0 ˜ moveq #BPLS-1,d7 ; load bpl conter in d7 ˜ ; remember the -1 ! ˜ loopbpl: ˜ move.w d0,6(a0) ˜ swap d0 ˜ move.w d0,2(a0) ˜ swap d0 ˜ add.l #W/8*H,d0 ˜ add.l #8,a0 ˜ dbf d7,loopbpl ˜ copper: ˜ dc.w BPLCON0,BPLS<<12!$0200 ˜ ... ˜ copperbpl: ˜ dc.w BPL1PTH,$0000 ; register adr is $00E0 ˜ dc.w BPL1PTL,$0000 ; register adr is $00E2 ˜ dc.w BPL2PTH,$0000 ; register adr is $00E4 ˜ dc.w BPL2PTL,$0000 ; register adr is $00E6 ˜ ... ˜ bpl1: ˜ dcb.b W/8*H,$F0 ˜ bpl2: ˜ dcb.b W/8*H,$F0 ˜
BTW. with Asmone you can set a marker in the source code with shift-ctrl-(number) and the can jump to it with ctrl(number). this is quite useful, however setting a marker wont get you any visual reference where you set a marker or if a marker is set somewhere.
The code to fill the bitplanes can also be put into a routine and the do a bsr to it. The routine will take two inputs that we will have to copy before calling the routine, e.g. we need to put the address of the copper bitplanes into a0 and the starting address of the bitplanes in memory to d0. Also instead of reserving memory for each bitplane separately we can reserve the memory for the whole screen depending on the number of bitplanes we decided to have. The new pointer is called screen instead of bpl1 bpl2 etc
˜ lea copperbpl,a0 ; prepare variables ˜ move.l #screen,d0 ˜ bsr pokebpls ; branch to subroutine ˜ ... ˜ ** fill copper with bpls addresses ˜ ** a0: address of bitplane pointers in coperlist ˜ ** d0: address of bitplanes in memory ˜ pokebpls: ˜ moveq #BPLS-1,d7 ; load bpl conter in d7 ˜ ; remember the -1 ! ˜ loopbpl: ˜ move.w d0,6(a0) ˜ swap d0 ˜ move.w d0,2(a0) ˜ swap d0 ˜ add.l #W/8*H,d0 ˜ add.l #8,a0 ˜ dbf d7,loopbpl ˜ rts ˜ ... ˜ ˜ screen: ˜ dcb.b W/8*H*BPLS,$F0 ˜
Instead of filling the bitplanes "manually" it is now possible to include a memory region containing the bitplanes information of an image drawn with a program like deluxe paint. To do so we use the incbin directive of the assembler which is loading and placing the binary data of an image into the given spot in the code which we can label accordingly. the image as well as the color palette has to be exported in the correct bitplane format form the software and converted from the compressed IFF Amiga image format to a binary format using a program called iffconv
BTW in asm one the command v (view) displays the content of the current directory, "v/" the content one directory up and "v tutorials:" to display the content of the drive called tutorials. It will also change the current active directory to the specified path. this is hand when you you did not start asmone from within the directory of your source code, as then asmone will not find the include files (images/sound/etc)
we can now based on the current code base display an image. usually amiga uses the IFF file format, which is compressed and we can thus not simply use/include in our code. we need to convert it into a given uncompressed raw format for the bitplanes and the palette. this is done with the program "iffconv" (IFF converter). we can use this software to load the IFF image. mark the image and be sure you make the whole height/width of the image by holding the mouse button and scrolling the image with the arrows on the keyboard (common mistake). once marked select the mode to save the image in , e.g "RAW NORM" for now, as .raw file. the we should also save the pallet by selecting "COPPER" and saving with the file extension ".palette", or similar. now we can include the image at the label screen: and replace the dcb ...
˜ screen: ˜ incbin "filename.raw"
also we should set the screen size and the number of bitplanes to 5, i.e. BPLS = 5. now the routine (pokebpls) filling the bitplanes from above and the declaration of constant comes in quiet handy! also adapt the DIWSTOP command in the copper according to the actual image height!
˜ dc.w DIWSTOP,$2CC1 --> 256 pixel high ˜ dc.w DIWSTOP,$F4C1 --> 200 pixel high
same for the width of thew image/screen. finally we also need to load the palette at the appropriate position in the copper, i.e. label palette:, replacing the hard coded palette. don't remove the copper list end command dc.w $FFFF,$FFFE!
˜ pallete: ˜ inclide "filename.pallete"
take care it is include not incbin as the palette is a text file containing the dc.w directives. incbin will add the binary data directly in memory at compile time. alternatively in asmone you can also switch back to command mode (ESC) and use the i command, select a file from the browser, which ill add the content of the file at the current position of the cursor in the editor.
Today we will start to go into the modulo, which will help us to define the bitplanes a bit differently in memory. As we saw before the number of pixels displayed per line on the screen are defined by DDFSTRT and DDFSTOP, usually $38 and $D0 respectively. This is also called the effective width or sometimes also called visible width of the screen. to recapitulate, in pixels this would be ($D0-$38)x2+16 (remember two pixels per address are sent and another 16 pixels are sent after $D0, thus $98x2+16 = 152x2+16 = 304+16 =320 for a standard lowres screen. in bytes this will be 40 bytes. this means that every line in bitplane memory is 40 bytes large, a concatenation of lines of 40 bytes. if now we would like to have larger images, i.e. larger bitplanes, lets say 42 bytes large (remember only pair number pf bytes as Denise always sends 16 pixels, i.e. 2 bytes, per cycle) and only want to display 40 bytes of it, we will be in trouble as the image will appear messed up, as Denise will continue fetch the bytes one after another. this can be for an image that we would like to scroll for example. thus we need a way to tell Denise to skip the additional 16 pixels. this is usually done with modulo operations! Let's say our KingTut image is 336 pixels large and try to di
splay it we will get a messed up image, even if we changed the constants W and H plus DIWSTRT and DIWSTOP and correctly. to avoid this we have to set the modulo data registers. there are two register, one for pair and one for add bitplane numbers, which is due to the fact that the amiga could have two playfield.
˜ BPL1MOD = $108 ˜ BPL2MOD = $10A
as usual the registers are set in the copper list
˜ dc.w BPL1MOD,$2 ˜ dc.w BPL2MOS,$2
now our larger KingTut image should be displayed correctly again. in addition we can also now use the modulo to make the famous mirror effect, this is display the image backwards starting form a certain line, i.e. 200 ($F407). for this at the end of the copper we watt for line $F4 (before the last command!) and we change the modulo registers. instead of telling Denise to skip +2 bytes, will tell her to skip two lines backward, thus Denise will go trough the bitplanes backwards! in practice we Frost need to skip 2 bytes forward (as our image is 336 bytes large) and then skip two or more lines backwards --> 2-2*W/8.
˜ dc.w $F407,$FFFE ˜ dc.w BPL1MOD,2-2*W/8 ˜ dc.w BPL2MOD,2-2*W/8
if we skip more that two lines backwards each at each line end, the image will be a bit more crashed, giving a 3D mirror effect! but take care that we are not going further back than our screen memory biplanes defined in memory) else we will crash! after the wait command you can also add a new palette with reduced brightness to mimic the mirror effect better. additionally you could also copy the portion containing the copperbpl and rename the label to copperbpl2 after the wait command. now Denise will start coping the data form the beginning of the bitplanes again, this means the image would start again from line 0.
notabene: asmone is not reloading the includes and incbin at each assembling as it assumes they did not change if you did not change the filename. thus even if you changed the file content it will take the includes already loaded in memory. to avoid this you need to delete/zap the included flues form memory with the "zi" command!
it is important to understand how the modulo are calculated. first of all lets us always put some constants to help us
˜ W=336 ; width of the screen bitplane memory ˜ H=256 ; height of the screen bitplane memory ˜ BPLS=5 ; number of bitplanes ˜ LEFF=320 ; effective screen width for a lowres screen ˜ MODULO=W/8-LEFF/8 ; which i nthis case would be = 2 ˜ BPLSIZE = W/8*H ; Size of taken by one bitplane in memory if not interleaved ˜ LINESIZE = W/8 ; Lenth of image line
the effective width of the screen is very important for calculating the modulo and is defined as
˜ LEDD = ( DDFSTOP - DDFSTRT ) * 2 + 16 ; effective width in low res --> 320x256 ˜ = 320 ; for a stanard low res screen
Then the modulo is defined as
˜ MODULO = reral width of bitplanes - effective width of the screen ˜ = W/8 - LEFF/8
Additionally to being used to display images that are larger than the effective screen size, modulo are also used to display images saved in a different binary format. in raw format the bitplanes in the image are stored one by one in a consecutive order
˜ BPL1 (W/8*H bytes) ˜ BPL2 (W/8*H bytes) ˜ BPL3 (W/8*H bytes) ˜ BPL4 (W/8*H bytes) ˜ BPL5 (W/8*H bytes)
the other format is called interleaved and there the the bitplanes are also stored consecutively but line wise
˜ BLP1 <------------------ line 1 -------------------> ˜ BLP2 <------------------ line 1 -------------------> ˜ BLP3 <------------------ line 1 -------------------> ˜ BLP4 <------------------ line 1 -------------------> ˜ BLP5 <------------------ line 1 -------------------> ˜ BLP1 <------------------ line 2 -------------------> ˜ BLP2 <------------------ line 2 -------------------> ˜ ...
this mode is the default mode used in amiga. in the iffconv tool the interleaved modes called RAW BLIT. in this mode we need to modify the routine filling the bitplane (bplpoke) as now for jumping to the next bitplane (same line in the next bitplane) we don't need to jump W/8xH bytes as before but only W/8, one line. then for jumping to the next line we need to jump by BPLSxW/8, the number of biplanes time line width in bytes.
˜ Simple bitplanes ˜ next bitplane at: W/8*H ˜ next line at: W/8 ˜ ˜ Interleaved bitplanes ˜ next bitplane at: W/8 ˜ next line at: W/8*BPLS
the bplpoke routine will now change to
˜ ** a0: address of bitplane pointers in copperlist ˜ ** d0: address of bitplanes in memory ˜ pokebpls: ˜ moveq #BPLS-1,d7 ; load bpl content in d7 ˜ ; remember the -1 ! ˜ loopbpl: ˜ move.w d0,6(a0) ˜ swap d0 ˜ move.w d0,2(a0) ˜ swap d0 ˜ add.l #BPLSIZE,d0 ;<-- changed to constant here ˜ add.l #8,a0 ˜ dbf d7,loopbpl ˜ rts ˜ ... ˜
and the constants will be
˜ BPLSIZE = W/8 ; Size of taken by one bitplane in memory if interleaved ˜ LINESIZE = W/8*BPLS ; Lenth of image line
also more importantly what changes are the modulo values. Similarly to before where at each line Denise needed to jump over the portion of the image that is larger that the effective screen size (in our case 2 bytes) here Denise needs to jump the number of bitplanes times the line width of the full image minus the effective width in bytes W/8xBPLS-LEFF/8 which is LINESIZE - LEFF/8
˜ MODULO = W/8*BPLS - LEFF/8
the formula comes from the following reasoning: if you have let's say 5 bitplanes, once you reach the end of what you can display on line 1 of bitplane one (effective with of screen) Denise needs to jump over the remaining portion of the line which is not displayed, which is W/8-LEFF/8. what remains now to get to the second line is to jump the remaining 4 lines which contain the information of bitplanes 2-5, this is 4xW/8. in total thus Denise needs to jump W/8-LEFF/8+4xW/8 = 5xW/8-LEFF/8 and as BPLS = 5 Denise need to jump W/8xBPLS-LEFF/8. thus graphically explained, to jump from the end of displayed line 1 to beginning of line 2 Denise need to jump the following modulo
˜ <----------------bpls line width--------------> ˜ <---------------------W/8---------------------> ˜ <-----------LEFF/8-------------><-W/8-LEFF/8--> ˜ jump from here-->¦ ˜ |----------------line 1 BPL1---¦--------------| W/8-LEFF/8 ˜ |----------------line 1 BPL2------------------| W/8 ˜ |----------------line 1 BPL3------------------| W/8 ˜ |----------------line 1 BPL4------------------| W/8 ˜ |----------------line 1 BPL5------------------| W/8 ˜ |----------------line 2 BPL1------------------| ------------ ˜ ¦<--to here 5xW/8-LEFF/8 ˜ = MODULO to jump
now the source code starts to be a bit long and it is time to split the code in two files, one which is taking care of initializing all (which can be reused from project to project)-> base.s; and one containing the project specific code -> startup.s; where base will be included in the startup.s which for now will be the main code file we work on.
another important aspect of assembler coding is the stack, which is a special kind of memory when you can literally stack data on top of each other. there are even complete programming languages like forth that are work with the stack only. the processor and many system routines are using the stack continuously, i.e. to save the current program counter address when jumping to a subroutine in order to know there to return after the routine calls rts. so we have to be careful when using it as else we can easily crash the system. you can imagine the stack like a region of memory where you each time you add data the stack pointer (address of the current stack element) is decreased, this is goes backward. the stack pointer is stores in the register A7 which can also be addressed with sp
˜ memory data ˜ $1000 $00000000 ˜ $1004 $00000000 ˜ $1008 $00000000 ˜ $1012 $00000000 ˜ $1016 $00000000 ˜ $1020 $00000000 ˜ $1024 $00000000 ˜ $1028 $00000000 ˜ $1032 $ABABABAB <- stack pointer
now we add data (d0 = $FFFFFFFF) to the stack and decrease then stack pointer pointer
˜ move.l d0,-(sp)
note the pre-decrement addressing, e.g. the stack pointer address is first decreased according to the size of the data here a longword and only then the data is moved. sp now points at the last data that has been added, more precisely at the address of the first byte of data (here $1028). there after the memory region containing the stack will look like this
˜ memory data ˜ $1000 $00000000 ˜ $1004 $00000000 ˜ $1008 $00000000 ˜ $1012 $00000000 ˜ $1016 $00000000 ˜ $1020 $00000000 ˜ $1024 $00000000 ˜ $1028 $FFFFFFFF <- stack pointer ˜ $1032 $ABABABAB
if we want to fetch the data back we do
˜ move.l (sp)+,d0
note the post-increment, this is the data id fetched back to d0 an the the sp points to the next data on the stack. here again we move a longword as we put a longword! this is important not to mess up the stack pile! the stack will now look like this
˜ memory data ˜ $1000 $00000000 ˜ $1004 $00000000 ˜ $1008 $00000000 ˜ $1012 $00000000 ˜ $1016 $00000000 ˜ $1020 $00000000 ˜ $1024 $00000000 ˜ $1028 $FFFFFFFF ˜ $1032 $ABABABAB <- stack pointer
note that the data at the previous position of the stack is still there and was not deleted! where will we use the stack? actually we use it to save the content of the registers before we execute a routine, this is jump into a routine. as when executing the routine the registers are used and thereby changed and some other portion of our code may rely on the content of these registers (e.g. a6 containing the base address of the custom chips), we want to make sure to save and then restore these values before calling the routine and after exiting the routine respectively. this is best done by putting them onto the stack and the fetching them from there. we do not need to always save all registers, only the ones modified by the subroutines.
it is crucial to stress the following point again: we need to be sure to fetch back the exact amount of data we put on the stack else the stack pointer (sp) will point to the wrong location and when the system wants to fetch information from the stack he well get the wrong data, such as the wrong location in memory to jump back after a subroutine call, which would crash the system.
let's now see an example with the waitVBL routine
˜ waitVBL: ˜ move.l d0,-(sp) ; save corrent d0 ˜ ˜ waitVBLloop: ˜ ˜ move.l $DFF004,d0 ; use d0 as we want ˜ lsr.l #8,d0 ˜ and.w #$1FF,d0 ˜ cmp.w #$138,d0 ˜ bne waitVBLloop ˜ ˜ move.l (sp)+,d0 ; restore previous value of d0 ˜ rts ; befor returning
in this case we saved the values within the routine but we could also save them before we jump into the routine. as the waitVBL is a loop routine we need to add an additional label to loop to else we would be putting d0 to the stack continuously while waiting and the fetching it only one = system crash! in the example above, the routine just modified d0. if we have a routine that modifies more than one registers, e.g. A0 D0 D7, we can put them on the stack at once with the command movem
˜ movem.l a0/d0/d7,-(sp) ; store several registers on the stack ˜ movem.l (sp)+,a0/d0/d7 ; restore registers from stack
please notice that when restoring the register from the stack you do not need to invert the order of the registers in the command but you need to keep the order, the CPU will take care of putting them back in the right order. this is important as the stack is first in last out one could think that inverting the order is necessary, but this is not the case!
so far we exited the program by pressing the left mouse button. if this was done consequently, no issue should have arisen. however if by mischance we had presses the right mouse button the program would have crashed, or become unresponsive. this is because so far we did not disable the interrupts and only enable the interrupts we need. interrupts are used to point the cpu to another program in memory upon for example a mouse button press of something else. after the code has been executed the system jumps back and continues from where it left in the code. it can be see as a jsr triggered not by or own code directly (except we ask for an interrupt) but by the system or users action, like pressing a mouse button or getting a keystroke. for example the right mouse button in asmone triggers the menus, this is the cpu jumps to the position of the code in memory handling the menu of asmone. now when we start our code from within asmone, this interrupt trigger is still active bet the menu cant be executed properly anymore so the system hangs. thus we need to deactivate the interrupt for the right mouse button during the execution of our code and reactivate the interrupt after exiting, so that the menu works again. as for the DMA there is a register in which we can
disable and enable some type of interrupts. the interrupts are handled by 4 registers, in pair of two for reading and writing
˜ INTREQ = $09C ; write register for interrupt requests ˜ INTREQR = $01E ; read register ˜ INTENA = $09A ; write register for interrupt enable ˜ INTENAR = $01C ; read register
INTREQ is used to ask for interrupts, and INTENA is used to enable or disable interrupts.they all have the same structure but with different purposes
˜ bit name fuction ˜ ------------------------------------------------- ˜ 15 SET/CLR as in DMACON this bit will decide ˜ if the followinf bits will be set ˜ or cleared ˜ 14 INTEN interrupts allowed ˜ 13 EXTER CIA-B or expansion port interrupt ˜ 12 DSKSYN disk sync vallue known ˜ 11 RBF serial port buffer full ˜ 10 AUD3 audio emitted form channel 3 ˜ 9 AUD2 audio emitted form channel 2 ˜ 8 AUD1 audio emitted form channel 1 ˜ 7 AUD0 audio emitted form channel 0 ˜ 6 BLIT blitting done ˜ 5 VERTB start of vertical blank ˜ 4 COPER copper interrupts ˜ 3 PORTS CIA-A or ecpansion port interupt ˜ 2 SOFT software interrupts ˜ 1 DSKBLK transfer DMA -> disk done ˜ 0 TBE expansion port buffer empty
one interrupt often used in demo coding is VERTB (5) which is tested to see if we are at the end of the display and the screen ebeam is in the vertical blank zone. keyboard interrupts are handled by PORTS (CIA-A). to be on the safe side, we need to disable all the in those interrupts in the INTENA register, but before this we need to save the current state from INTENAR, which will be used to recover the state at the end. disabling all interrupts occurs by writing $7FFF into INTENA
˜ bit: 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00 ˜ value: 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 ˜ hex: 7 F F F
we will thus modify our base.s fir accordingly by adding
˜ INTENA = $09A ˜ INTENAR = $01C ˜ ... ˜ move.w INTENAR(a6),oldintena ; save currents INTENA ˜ move.w #$7FFF, INTENA(a6) ; disable all ˜ ... ˜ or.w #$C000,oldintena ; set bit 15 and 14 to 1 ˜ move.w olditena, INTENA(a6) ; restore INTENA ˜ ... ˜ oldintena: ˜ ds.w 1
attention similarly to as when we saved the DMACON, bit 15 (write 0 or 1) of the saved INTENA will be 0, this we need to set it in the saved oldintena before we move it back to the register. in addition we must also set bit 14 (allow interrupts). to set this two bits we need to OR oldintena with %1100000000000000 = $C000
just a little intermezzo into branching and jumping. all branch instruction bxx (bra, bne, etc) are relative jumps from the given potion. the assembler will calculate the portion to branch towards, by subtracting the current address from the address of the label we branch towards. one has however to notice that the current address is not the address where the ranch instruction sits but the address after it!
˜ run: ˜ bra end ˜ moveq #7,d1 ˜ end: ˜ rts
as the size of the bxx command is 4 bytes, of which 2 bytes after the opcode and 2 bytes are the address offset. the assembler will calculate the offset of the addresses end-(run+2). if we disassemble the above it looks as such
˜ address in memory binary code assemblres code ˜ 00C2BC78 60000004 bra.w $00C2BC7E ˜ 00C2BC7C 7207 moveq #$07,D1 ˜ 00C2BC7E 4E75 rts
the $6000 = bra.w and the $0004 is the offset from after the bra. this is the bra opcode begins at $00C2BC78 and the offset is stored at $00C2BC7A. these as rts is stored at $00C2BC7E, the offset value will be $00C2BC7A-00C2BC7E = $0004. as the offset addresses part of the bra opcode is 16bits (2 bytes) the maximal branch distance is -32768 to 32767, which 32 kbit branch distance. note that the offset is s signed number! this is valid for the regular bra command, which is actually a bra.w . but we can also use a bra.s (equivalent to bra.b) command which will be a 2 bytes command ($60xx, $xx is the offset) where the offset can only be encoded in 1 byte and this can jump only from -128 to 127. if anyways we intend to make small branches, we can optimize ore code in such a way. now you can do this manually and check each branch command for its distance or you can let the assembler optimize this. for this asmone offers the command "ao" (optimize and assemble) which you can use instead of "a". if we like, it is also possible to set our self the relative offset by hand. in that case we set the offset from the beginning of the bra instruction. in the example above this would be $0006, the assembler will the remove the needed bytes and set $0004 as offset. in th exam
ple above we could thus also write
˜ bra *+6
in comparison if we use the jmp (jump, is like jsr but there is no return expected thus also no return address will be stored on the stack) command, this is jump end, then the assembler will calculate the absolute address to jump to, this is the address where the end label is. this command needs 6 bytes, 2 bytes for the opcode and 4 bytes for the address, as the amiga has 32 bit addresses bus. thus the example above would look line
˜ address in memory binary code assemblres code ˜ 00C2BC78 4EF900C2BC80 bra.w $00C2BC80 ˜ 00C2BC7E 7207 moveq #$07,D1 ˜ 00C2BC80 4E75 rts
where 4EF9 is the opcode for jmp and $00C2BC80 the absolute address of end: to jump to. the big advantage is that all the addressable memory of the amiga is accessible!
lets write a routine to put a pixel on the screen! lets call it plot pixel. in d0 we will pass X coordinate and in d1 we will pass the X coordinate. first of all we need to find in which part of the byte our pixel is situated. remember, vertically the screen is split up in bytes (8 bits) that contain the pixels. each line of a standard low res screen contains 40 such bytes. the vertical position is thus easy to find, if we want line 100, then it will be 40x100 (interleaved mode bitplane 1) bytes offset from the start of the screen memory. in interleaved mode the next line is W/8*BPLS away! we can thus pass the offset into a0 by
˜ MULU #W/8*BPLS,d1 ; IL mode!
notabene we use an unsigned multiplication and the result will be stored in the second operand which is d1 in this case. also as we use IL mode the next line is W/8*BPLS away! now we could write a pixel in the first byte of line 100 by setting bite 7 for example
˜ add.l d1,a0 ; increase the pointer to point to line 100 ˜ bset #7,(a0)
note that we are writing only to bitplane #1. now we need to add the x coordinate. for this we need to know in which of the 40 bytes the pixel will be and which pixel of the byte it corresponds to. for the first part we can divide the x coordinate by 8, so we see how many bytes it is from the left border
˜ DIVU #8,d0
the results of the (unsigned) division will be in the low word of D0 and the rest of the division in the high word of D0
˜ D0 32 bit: ˜ |--byte--|--byte--|--byte--|--byte--| ˜ |------rest-------|-division result-|
the we can use d0.w to operate on the division result and the swap d0 to access again with d0.w the rest which we would have to invert:
˜ add.w d0,a0 ˜ swap d0
now lets see how the bytes are numerated
˜ bit number: 7 6 5 4 3 2 1 0 ˜ pixel number: 0 1 2 3 4 5 6 7
they are exactly inverted! we can invert the order with
˜ not.w d0 ˜ bset d0,(a0)
notabene we only invert the lower word!
why does this work? lets say d0 = 3 so we would like to set the 4th (the 1st is pixel 0, see above) pixel from the left witch is actually bit number 4, thus we need to change d0 from 3 to 4:
˜ d0.w = %00000011 = $3 ˜ not.w d0 d0.w = %11111100 = $FFFC
bset executed on a memory cell like (a0) will always be modulo 8 of the vale given (here the value is in d0), here $FFFC modulo 8 is equal to $4 the bit we want to set for the pixel $03
˜ bset d0,(a0) d0.w = %11111100 = $FFFC ˜ bset d0,(a0) d0.w = %00000100 = $4 ; is quivalent!
so the combination of not and bset is doing the magic of inversion. but be careful bset on a register, i.e. bset #$4,d2, will not take the modulo 8 bit the modulo 32!
to explain in in a different way doing a not.w can be seen equivalent to subtracting the current value of the register from the maximum possible value (here a word)
˜ not.w %00000011 = %11111111 - %00000011 = %11111100
as we saw passing this value to bset will perform a modulo 8 on it and set the resulting bit. now we can also take the modulo 8 on the difference equation above, which should be the equivalent:
˜ %11111100 mod 8 = %111111 mod 8 - %00000011 mod 8 ˜ = $FF mod 8 - $03 mod 8 ˜ = $07 - $03 ˜ = $04 (the bit to set for pixel $03)
thus doing a not.w d0 and a bset d0,(a0) is equal to perform a $07 - d0 (which actually inverts the order as we want) and set this bit. so why don't we do a subtraction operation instead? because it is computationally expensive I guess? finally the complete routine:
˜ ********************************* ˜ *** plot a plixel onto the screen ˜ ** a0: pointer to screen memory ˜ ** d0: x coordinate ˜ ** d1: y coordinate ˜ plotpixel: ˜ MULU #W/8,d1 ; Y part ˜ add.l d1,a0 ; move to the correct line ˜ DIVU #8,d0 ; X part ˜ add.w d0,a0 ; go to the correct byte ˜ swap d0 ; get the ringer of the div ˜ not.w d0 ; invert the order to ˜ bset d0,(a0) ; to set the correcte bit ˜ rts
MULU and DIVU operations are quite computationally expensive, thus to speed up the routine we would need to optimize it. one optimization can be done for the DIVU
˜ DIVU #8,d0 --> lsr #3,d0 ; shift 3 to the left = div 8
but what about the rest of the division? by definition the rest of a division by 8 is already in the first 3 bytes of the number and starting form the 4th bit all is dividable by 8. thus before lsr we save d0 to d2 and we can just and.w %00000111,d2 = and.w #7,d2 to get the rest! all in all
˜ DIVU #8,d0 ˜ add.w d0,a0 ˜ swap d0 ˜ not.w d0 ˜ bset d0,(a0)
is replaced by
˜ move.w d0,d2 ; save originla value ˜ lsr #3,d0 ; divide by 8 ˜ add.w d0,a0 ; go to the correct byte ˜ and.w #7,d2 ; get the reminder ˜ not.w d2 ; invert to get the ˜ bset d2,(a0) ; correct bit to set
here he optimized routine
˜ ********************************* ˜ *** plot a plixel onto the screen ˜ ** a0: pointer to screen memory ˜ ** d0: x coordinate ˜ ** d1: y coordinate ˜ plotpixel: ˜ MULU #W/8,d1 ; Y part ˜ add.l d1,a0 ; move to the correct line ˜ move.w d0,d2 ; save originla value ˜ lsr #3,d0 ; divide by 8 ˜ add.w d0,a0 ; go to the correct byte ˜ and.w #7,d2 ; get the reminder ˜ not.w d2 ; invert to get the ˜ bset d2,(a0) ; correct bit to set ˜ rts
now we have a semi optimized routine to display a pixel on bitplane one, but we might have several bitplanes and colors, thus we also need to consider this. for this we need to make a loop in function of the number of bitplanes. we will pass the color selection in register d2 (thus we need a bit to change the code to d2->d3). lets say we have 2 bitplanes and we want the color to be color03 the we set
˜ d2.b = $03 = %0011
this means that we need to set
˜ bitplane 1 = 1 ˜ bitplane 2 = 1
to do this we will make a a loop that goes through all the bitplanes given in BPLS (here 2) at each iteration we left shift d2 so that the bit than falls off will be in the carry flag. if the carry is set then we will bset the pixel in that bitplane else we skip.
˜ lsr #1,d2 : %0011 -> %0001 cf=1 -> bset d0,(a0)
then we increase the screen address to the best bitplane (one line further) and again right shift d2 etc. here the loop:
˜ move.w #BPLS-1,d7 ; take number wanted loops -1 ˜ setcolor: ˜ lsr.b #1,d2 ; -> bit to test into cf ˜ bcc nextbpl ; test cf, if 0 jump ˜ bset d3,(a0) ; else bset pixel ˜ nextbpl: ˜ add.l #BPLSIZE,a0 ; next bitplane ˜ dbf d7,setcolor ; loop
finally we need to put the variables that will be modified onto the stack and the restore them before exiting the routine. here the full routine:
˜ ********************************* ˜ *** plot a plixel onto the screen ˜ ** a0: pointer to screen memory ˜ ** d0: x coordinate ˜ ** d1: y coordinate ˜ ** d2: color ˜ plotpixel: ˜ movem.l d0-d3/d7/a0,-(sp) ; save on stack ˜ ˜ MULU #W/8,d1 ; Y part ˜ add.l d1,a0 ; move to the correct line ˜ move.w d0,d3 ; save originla value ˜ lsr.b #3,d0 ; divide by 8 ˜ add.w d0,a0 ; go to the correct byte ˜ and.w #7,d3 ; get the reminder ˜ not.w d3 ; invert to get the ˜ ; correct bit to set ˜ move.w #BPLS-1,d7 ; number wanted loops -1 ˜ setcolor: ˜ lsr.b #1,d2 ; -> bit to test into cf ˜ bcc nextbpl ; test cf, if 0 jump ˜ bset d3,(a0) ; else bset pixel ˜ nextbpl: ˜ add.l #BPLSIZE,a0 ; next bitplane ˜ dbf d7,setcolor ; loop ˜ ˜ movem.l (sp)+,d0-d3/d7/a0 ; recover from stack ˜ rts
the we can call the routine with
˜ moveq #0,d0 ; clear ˜ moveq #0,d1 ; clear ˜ lea screen,a0 ; pass the screen adr ˜ move.w #100,d0 ; x coord ˜ move.w #100,d1 ; y coord ˜ move.w #2,d2 ; color ˜ bsr plotpixel
lets spend some words on PC relative addressing when it comes to pass the address of a copper list or image to the address register. so far we used either one of these
˜ move.l #copperlist,a0 ˜ lea copperlist,a0
these two modes are direct addressing, this is, the effective address of the label is passed to a0. when the source code is assembled a loader is added to the binary which will take care of dynamically setting the address of the label (or something like this). Now for binaries with reduced size, like boot block intros or some demos, the loader is omitted and the label address is hard code and in most of the cases pointing to the wrong place. therefore it is advisable to use PC (program counter) relative addressing
˜ lea copperlist(pc),a0
this wont hard code the address of the label but instead the offset from the current PC to the label (#copperlist - PC), which should remain the same independently of where in memory the binary will be loaded. notabene, this addressing is limited to 16 bits, this is, the label should not be more than +/- 32KB away, which in most cases is OK (16 bit signed -> 15bit address space in both directions ~ 32KB). notabene that there is no PC relative addressing for the move.l variant
˜ move.l copperlist(pc),d0 ; move the CONTENT of the label ˜ move.l #copperlist(pc),a0 ; UNVALID
one of the big innovations in the C language was the introduction of structures and their instancing to manage data. this was also a bit the precursor for classes. in assembly language one can also define and use structures by means of the rs (reserve structure) directive. with this directive you basically define a scaffold of labels which are offset by a certain distance (b w l)
˜ rsreset ; resets the offset to 0 ˜ s_scanx: rs.l 1 ; offset 0 ˜ s_scany: rs.l 1 ; offset 8 (1 x l) ˜ s_text: rs.l 1 ; offset 16 (2 x l) ˜ s_size: rs.b 0 ; is at offset 24 (3 x l) ˜ ; which is also the size
RSRESET sets the start by resetting the offset to zero, the rs.b 0 just sets a label without an offset and is just a place holder, in this case s_size will have the offset 24 bytes (due to the 3 previous long word offsets) and will also be the sum of all offsets, i.e. the size the structure will take in memory when instanced. to instance the stricture we can simply write
˜ scoreline: ds.b size
this must be ds.b (bytes) as the size (= total memory offset) is given in bytes. now if we want to write into the structure we can do
˜ lea scoreline,a0 ; base address of the structure ˜ move.l #$AAAABBBB,s_scany(a0) ; move into the struct ˜ move.l s_scanx(a0),d0 ; read from the struct
notabene s_scanx has to be moved as longword as defined in the structure. any other move will also assemble without error but will result in a malfunction. now one could say, in what is that different fro writing
˜ move.l #$AAAABBBB,8(a0)
of even
˜ s_scany EQU 8 ˜ ... ˜ move.l #$AAAABBBB,s_scany(a0)
well you can add/remove elements in the structure definition and wont need to manually recalculate the size nor the offsets etc! Attached an example program
we saw before that we always wait for the vertical blank start before we start to update the data for the demo, e.g. setting the next image in the bitplanes or updating the copper. we can do this in the main loop or also automatically as done in most demo code. this is achieved by high jacking the vertical blank interrupt. each time the screen starts the vertical blank the 68k CPU receives a level 3 interrupt an then passes the execution of the code to the address stored in the vertical blank interrupt table at $6C. this is mostly a routine in the Kickstart memory. the system can the update its states before the next screen will be drawn. we can execute our own routine by writing the address of it in $6C. if course we need to make sure that our routine finishes before the vertical blank is over. before we write out routine into the vector table we need to save the current address so that we can the restore it before exiting our bin, similar to the interrupt enable and dma register. notabene INTENA and DMACON are 16 bit registers and the vector is a 32 bit address, thus reserve the space (dc.l) and move.l accordingly.
˜ ; before disabling all interrupts ˜ move.l $6C.w,oldirq3 ; save old irq (VBL) ˜ ... ˜ ; before enabling the interrupts again ˜ move.l oldirq3,$6C.w ; retore old irq3 ˜ ... ˜ ; fastmee data ˜ oldirq3: dc.l 1
now we can move the address or our IRQ3 routine into the vector table
˜ lea irq3,a0 ; replace irq3 with our own ˜ move.l a0,$6C.w ; interrupt routine ˜
notabene that we explicitly state $6C.w to indicate the it is a memory location in the lower memory map, for safety. immediately thereafter we need to enable the interrupts for the vertical blank again, else nothing will happen. This is bit $05 to write we also need to set bit $15 and $14, else no bit will be set: %1100 0000 0010 0000 -> $C020
˜ move.w #$C020,INTENA(a6) ; allow VBL interrupts!
now we can define our own interrupt routine. there are two important points when you write your own routine: 1. you need to acknowledge the interrupt by clearing the corresponding bit in the interrupt request register, so the CPU knows that the interrupt has been handled. for VBL it is again bit $05; 2. we don't exit by rts (return from subroutine) but by rte (return form exception)!
˜ lea CUSTOM,a6 ˜ move.w #$0020,INEREQ(a6) ; clear VBL interrupt ˜ ; to acknowledge irq
instead of blindly acknowledging bit $05 we can check which interrupt was given and acknowledge this one. for now we are only interested in level 3 interrupts which are bites $04-$06, which are COPPER VBL BLITTER respectively.
˜ irq3: ˜ movem.l a0-a6/d0-d7,-(sp) ; save on stack ˜ ˜ lea CUSTOM,a6 ; just to be sure ˜ move.w INTREQR(a6),d0 ; what did trigger the irq ˜ and.w #$0070,d0 ; mask VBI, COP, BLT ˜ ; level 3 interrupts ˜ ˜ move.w d0,INTREQ(a6) ; is necessary to tell the system that ˜ move.w d0,INTREQ(a6) ; the interrupt is beign handeld ˜ move.w d0,INTREQ(a6) ; need to be 3 time, dur to a bug ˜ ˜ ; do stuff ˜ ˜ movem.l (sp)+,a0-a6/d0-d7 ; retore state from stack ˜ rte ; retun with rtE not rtS!
from within or own IRQ3 routine we can now trigger our playlist to update the scenes of our demo. below a current scaffold with the base and startup code scaffolds.
a little intermezzo here. if we would like to play some mod sounds its is quite straight forward. first we need to include a mod play routine. one among many is the often used ptplay.s by Frank Wille. this routine play protracker saved mod files v2.3 (and maybe above?). find it here for your reference
I found it only works if you include it at the very end of the source code as else the portion below is not executed any longer for some reasons. although the mod file has to be in chip memory the ptplay routine can be in fast meme. include it as follows
˜ ********* DATA in CHIP MEM ************************ ˜ section ptplayer,data_C ˜ ... ˜ ... ˜ module: ˜ incbin "mysong.mod" ˜ ********* DATA in FAST MEM ************************ ˜ section ptplayer,data_F ˜ pt: ˜ include "ptplayer.s"
it is now crucial to enable the audio DMA channels, which means setting bit 0-4, this is the last nibble in the move command is $F. thus if you like to activate copper, bitplanes, and audio
˜ DMACON: ˜ %1000 0011 1000 1111 ˜ $8 3 8 F ˜ ˜ move.l #$838F,DMACON(a6)
also the play routine will need the interrupts fir CIAA (bit 13, external) and CIAB (bit 3, ports) enabled. this if we want VBL, CIAA and CIAB enabled
˜ INTENA ˜ %1110 0000 0010 1000 ˜ $C 0 2 8 ˜ ˜ move.l #$C028,INTENA(a6)
the source file of the ptplay.s routine has well done header explaining how to init and run the routine. based on that we can make two subroutines for play and for stop
˜ ptplay: ˜ sub.l a0,a0 ; a0 = autovector base = 0 ˜ moveq #0,d0 ; d0 = 0 for PAL ˜ jsr _mt_install_cia ; init cia timer ˜ ˜ lea module,a0 ; module address in a0 ˜ moveq #0,d0 ; start at pos d0 = 0 ˜ sub.l a1,a1 ; a0 = 0 for sample using sample adr ˜ jsr _mt_init ; init song ˜ ˜ move.b #1,_mt_enable ; set to 1 to play song ˜ rts ˜ ptstop: ˜ move.b #0,_mt_enable ; set to 0 to pause song ˜ jsr _mt_end ; end song ˜ jsr _mt_remove ; cleanup cia and set to ˜ ; original state ˜ rts ˜
the routines are quite self explanatory. now we can jump to ptplay AFTER we enabled the interrupts and the jump to ptstop just after exiting the idle loop. here the full code:
(v 21)