this year i am again participating to this nice adventure initiated by eli.li. let's do it better this year (^^). the main site with all the other participants listed can be found here:
Goal: Doing some daily Amiga assembler coding, mostly custom chips and less OS related. I will actually follow the tutorial videos from jel!
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 niber of bitoplanes ˜ 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.
This was the last day for the December Adventure. but, the series will be continued! Happy new year :)
(vid 16 t 26:25)