Amiga Machine Code Letter XI - The Mouse

Amiga Machine Code - Letter XI

In this post we will take a closer look at the Amiga mouse, and go through a little program that reads the mouse x and y counters and transform them to x and y coordinates.

For those of you that have followed along, we have now reached Letter XI of the Amiga Machine Code course. As always make sure to read the letter, since I won’t go through all the details here. 😉

The Hardware

The Amiga Mouse, also known as the Tank Mouse, is a mechanical contraption that uses the rotation of a ball to determine movement. The ball is pressed up against two rollers placed perpendicular to each other, and connected to encoder wheels, that turns when the mouse is moved.

On one side of the encoder wheel, is placed an infrared emitter and on the other side a pair of infrared detectors. The pair of detectors are slightly offset, and generate two pulse trains that can be used to determine the direction of rotation using quadrature encoding.

Wheel and sensors

The two pulse trains V and VQ are also illustrated in the Amiga Hardware Reference Manual in figure 8-2.

Fortunatly for us, we do not have to worry about these pulse trains, since the Amiga hardware takes care of analyzing the input and providing the result conveniently in an x and y counter.

The x and y counters are stored ind JOY0DAT, which is a 16 bit register at $DFF00A. The upper byte holds the y counter and the lower byte holds x counter. These counters have the special property that they will wrap around, when they overflow. A counter can thus only hold values from 0 to 255.

Reading the counters

The mouse produces about 200 count pulses per inch, which is about 79 count pulses per centimeter. Because the counters wrap, we should read the mouse input often to get a good grasp of direction and movement.

If we read the mouse counters at every vertical blanking, the most likely count difference will be below 127. Below that threshold we assert that no wrapping has accured.

To test this assertion, we calculate the maximum speed of the mouse, given that the count difference is 127 between vertical blanks. In Europe, where the Amiga runs in PAL mode, vertical blanking will be every 1/50th second.

$V_{mouse} = \frac{127 \ \textrm{counts}}{79 \ \frac{\text{counts}}{\text{cm}} \ * \ \frac{1}{50} \ \text{second}} \approx 80 \ \frac{cm}{second}$

The assertion that no wrapping occurs for count differences below 127, holds if the mouse is not moved faster than around 3 kilometres per hour. For most use cases, this assertion should hold, but it can fail, for fast pased games. The obvious solution will be to read the counters more often than once per vertical blank.

Letter XI outlines an algorithm to determine the direction and movement of the mouse. It’s a relative measure, that should be added to the last known absolute position of the mouse, and it looks like this:

// Pseudo code to determined signed counter_diff

counter_diff = counter_new - counter_old

if ( counter_diff > 127 )
   return counter_diff -= 256
else if ( counter_diff < -128 )
   return acounter_diff += 256
   return counter_diff

The movement is calculated as a numerical size, that denotes length, while the sign denotes direction. For the x-counter, left is negative and right is positive. For the y-counter, up is negative and down is positive.

Let’s illustrate this with a couple of examples from Letter XI. In the first example, we first read the value 100 from the x-counter, and on the next read we get the value 250. By using the algorithm we get

$(250 - 100) - 256 = -106$

The movement is 106 mouse counts to the left.


In the next example we read the x-counter value 250, and on the next read we get the value 10. Putting those numbers into the algorithm reveals

$(10 - 250) + 256 = 16$


The movement is 16 mouse counts to the right.

Notice, that we haven’t said anything about what mouse counts are in pixels. The mouse count to pixel transformation is usually a function of the prefered mouse sensitivty. E.g. the mouse speed can be set in the Workbench preferences.


The Mouse Subroutine

Letter XI includes the program mc1101 that demonstrates the use of the mouse subroutine. The idea is that you should use the mouse subroutine in your own projects.

The mc1101 program is broken up into two major subroutines; mouse and calcmouse.

The mouse subroutine requires that you place two labels of one byte each in the code, so that the x and y pixel coordinates can be returned. This could perhaps have been done a little more elegant by using the data registers…

Description: calculates mouse x and y pixel coordinate. Requires the labels
             mousex and mousey each allocated with one word.
Syntax:      calcmouse()
ML:          calcmouse()
Result:      Stores the mouse x and y pixel coordinate at the labels mousex
             and mousey.

The mouse subroutine call calcmouse twice to get the relative directional movements of the x and y counters. These movements are then mapped to pixel coordinates by the mouse subroutine.

Calcmouse is quite complex and have a conveluted calling syntax. However, this subroutine should only be called by the mouse subroutine, which in turn will be called by client code. Here’s a description of calcmouse.

Description: calculates mouse x or y coordinate, given old and new 
             x or y count values, subject to lower and upper coordinate bounds.
Syntax:      calcmouse(oldCountPtr, 
ML:          calcmouse(a1, a2, d0, d2, d3)
Arguments:   oldCountPtr = The old x or y count value 
             newCoordinatePtr = The new coordinate value
             newCount = x or y mouse count
             lowerBound = coordinate lower bound
             upperBound = coordinate upper bound 
Result:      x or y coordinate is written to address of newCoordinatePtr

Here’s the complete listing of mc1101 from Letter XI. The code can be a little hard to follow, so I’ve sprinkled it with comments that hopefully will make it a little easier to understand. 😉

main:                 ; just a label
bsr	mouse         ; branch to subroutine mouse

lea.l	mousex,a1     ; move mousex address into a1
lea.l	mousey,a2     ; move mousey address into a2

move.w	(a1),d1       ; move value at mousex address into d1
move.w	(a2),d2       ; move value at mousey address into d2

btst	#6,$bfe001    ; test left mouse button
bne	main          ; if not pressed goto main
rts                   ; return from subroutine - exit program

mouse:                    ; subroutine (mousex, mousey) = mouse()
movem.l	d0-d7/a0-a6,-(a7) ; save registers on stack
move.w	$dff00a,d0        ; move value in JOY0DAT to d0
andi.l	#255,d0           ; keep lower byte in d0 (mouse x counter) using immidiate AND
moveq	#0,d2             ; move 0 into d2 (lower bound on x)
move.l	#639,d3           ; move 639 into d3 (upper bound on x)
lea.l	oldx,a1           ; move oldx address into a1
lea.l	mousex,a2         ; move mousex address into a2
bsr.s	calcmouse         ; branch to subroutine calcmouse
move.w	$dff00a,d0        ; move value in JOY0DAT to d0
lsr.w	#8,d0             ; shift left 8 bits
andi.l	#255,d0           ; keep lower byte in d0 (mouse y counter) using immidiate AND 
moveq	#0,d2             ; move 0 into d2 (lower bound on y)
move.l	#511,d3           ; move 511 into d3 (upper bound on y)
lea.l	oldy,a1           ; move address of oldy into a1
lea.l	mousey,a2         ; move address of mousey into a2
bsr.s	calcmouse         ; branch to subroutine calcmouse
movem.l	(a7)+,d0-d7/a0-a6 ; load registers from stack
rts                       ; return from subroutine
calcmouse:                ; subroutine calcmouse(a1=oldCountPtr,a2=newCoordinatePtr,d0=newCount,d2=lowerBound,d3=upperBound)
moveq	#0,d1             ; move 0 into d1
move.w	(a1),d1           ; move value from address in a1 (oldCount) to d1
move.w	d0,(a1)           ; move d0 (newCount) into address pointed to by a1
move.l	d0,d5             ; move d0 (newCount) into d5
move.l	d1,d6             ; move d1 (oldCount) into d6
sub.w	d0,d1             ; subtract word d0 (newCount) from d1 (oldCount) and store result in d1 (countDiff)
cmp.w	#-128,d1          ; compare -128 with d1 (countDiff)
blt.s	mc_less           ; if d1 < -128 goto mc_less
cmp.w	#127,d1           ; compare 127 with d1 (countDiff)
bgt.s	mc_more           ; if d1 > 127 goto mc_more
cmp.w	#0,d1             ; compare 0 with d1 (countDiff)
blt.s	mc_chk2           ; if d1 < 0 goto mc_chk2
mc_chk1:                  ; label
cmp.w	d5,d6             ; compare d5 (newCount) with d6 (oldCount)
bge.s	mc_chk1ok         ; if d6 > d5 goto mc_chk1ok
neg.w	d1                ; negate d1 (countDiff)
mc_chk1ok:                ; label
bra.s	mc_storem         ; branch always to mc_storem
mc_chk2:                  ; label
cmp.w	d5,d6             ; compare d5 (newCount) with d6 (oldCount)
ble.s	mc_chk2ok         ; d6 < d5 goto mc_chk2ok
neg.w	d1                ; negate d1 (countDiff)
mc_chk2ok:                ; label
bra.s	mc_storem         ; branch always to mc_storem
mc_less:                  ; label
add.w	#256,d1           ; add 256 to d1 and store in d1 (countDiff)
bra.s	mc_storem         ; branch always to mc_storem
mc_more:                  ; label
sub.w	#256,d1           ; subtract 256 from d1 and store in d1 (countDiff)
mc_storem:                ; label
neg.w	d1                ; negate d1 (countDiff)
add.w	d1,(a2)           ; add d1 (countDiff) to the value pointed to by a2 (newCoordinatePtr)
move.w	(a2),d0           ; move value from address in a2 (newCoordinatePtr) to d0
cmp.w	d2,d0             ; compare d2 (lowerBound) with d0
blt.s	mc_toosmall       ; if d0 < d2 goto mc_toosmall
cmp.w	d3,d0             ; compare d3 (upperBound) with d0
bgt.s	mc_toolarge       ; if d0 > d3 goto mc_toolarge
rts                       ; return from subroutine
mc_toosmall:              ; label
move.w	d2,(a2)           ; move value in d2 (lowerBound) to address pointed to by a2 (newCoordinatePtr)
rts                       ; return from subroutine
mc_toolarge:              ; label
move.w	d3,(a2)           ; move value in d3 (upperBound) to address pointed to by a2 (newCoordinatePtr)
rts                       ; return from subroutine
dc.l	$0000             ; allocate space for oldx (mouse x counter)
dc.l	$0000             ; allocate soace for oldy (mouse y counter)
dc.w	$0000             ; allocate space for mousex (mouse x coordinate)
dc.w	$0000             ; allocate space for mousey (mouse y coordinate)

Mouse Debugging

To get more of a feel of how the mouse works, it’s worth investigating how the counters change and overflows when the mouse is moved around. We do not need to have an Amiga to do this, since the WinUAE emaulator can output the content of JOY0DAT.

According to a post on the English Amiga Board, it’s quite easy to turn on mouse debugging.

Before booting up the Amiga, make sure to enable debug logging in WinUAE under properties. I have choosen to select “Log window”, so that I can see the updates in a separate window.


Then boot into the Workbench and press shift+F12 to get into the debugger. In the debugger use the dj command together with a bitmask. Below I’ve added the help text and the bitmask options.

dj [<level bitmask>] Enable joystick/mouse input debugging. 

// 01 = host events
// 02 = joystick
// 04 = cia buttons
// 16 = potgo r/w
// 32 = vsync
// 128 = potgo write
// 256 = cia buttons write

Still in the debugger, write the first line. The other three lines are printet when you press enter.

dj 2
Input logging level 2
JOY0DAT=f84a 00fc0f94
JOY1DAT=0000 00fc0f94

The output is just a snapshot of JOY0DAT. Close the debugger to let the log window outputs live data 😃


The log window features a strange name “ Arabuusimiehet”. It looks like Toni Wilen might know what it is. 😎

Amiga Machine Code Course

Previous post: Amiga Machine Code Letter X - Trackdisk.

Next post: Amiga Machine Code Letter XI - The Printer

Mark Wrobel
Mark Wrobel
Team Lead, developer and mortgage expert