Mark's software

home home fw home plus home vn aircraft black books cd dvd food history how to... links old news pictures pond projects quotes soft sounds weather
page updated: 2005.11.20

software - zagi mixer

zagi mixer

For flying something like a zagi, you need to mix 2 controls together. For up/down elevator (elevons move together) and left/right aileron (elevons move opposite). This can be done in the Tx if you have a computer version, or it can be done in the aircraft, in line with the 2x signals from the Rx to the 2x servos. I tend to use the computer Tx exclusively for powered aircraft, leaving the older/cheaper challenger Tx for the gliders. So all that need them have an onboard mixer.

I bought 2 kits for the Mk2 V tail mixer from here. These went into the original heavy zagi and siesta. No source code provided, but a swift bit of reverse engineering allowed me to emulate the behaviour. The home made mixer went into the light zagi.

Note: there is little point in doing this again, as commercially available units (kits and complete) are about the same cost, and usually use surface mount devices, saving weight. I mainly did it as an intellectual exercise, for fun.

It uses a 16f84a pic on a bit of veroboard, with just a 4M resonator and a decoupling capacitor. Measured weight 8g. I hard wired the servos and power lead direct to the vero board to eliminate the weight of the connectors.

Software developed and programmed into the device using the picstart plus development kit from microchip.

Source view and download.

;*******************************************************************
; mixer8.asm - came from mixer7.asm
; 2001/08/18
; measure the length of 2 input servo pulses
; generate 2 output mixed sum and difference pulses
;*******************************************************************
; rtcc not used - ideal resolution is 5us not a prescale option
; use instruction timing instead
; instructions take 1us or 2us for branch type 
; count loop takes 5us
; mid pulse = 1.52ms = 304 loops = 0x130h (256 + 48) 
; desired count for mid pulse is 0 so
; preload counter with 0 - 0x30h = 0xd0h (208)
; can't tell which will come 1st so need to check both
;*******************************************************************
;
         LIST    p=16F84A 		   ; PIC16F84A is the target processor
	#include "P16F84A.INC"        ; Include header file

; defines to find things easier
; ***** pic register equates *****
rtcc    equ     1                       ; counter register
pc      equ     2                       ; program counter
status  equ     3                       ; status register
carry   equ     0                       ; carry bit
zro     equ     2                       ; zero bit (note spelling)

; ***** port assignments *****
porta    equ     5                       ; port a equates
portb    equ     6                       ; port b equates 
ip0	equ	0			; input 0
ip1	equ	1			; input 1 
opsum   equ     2                       ; sum output to servo
opdif  	equ     3                       ; dif output to servo
lo0	equ	0ch			; use 1st spare register as lo byte counter
hi0	equ	0dh			; use 2nd spare register as hi byte counter
lo1	equ	0eh			; use 3rd spare register as lo byte counter
hi1	equ	0fh			; use 4th spare register as hi byte counter
sum	equ	10h			; for sum op pulse timing	
dif	equ	11h			; for dif op pulse timing
pulse	equ	12h			; counter for op pulse timing	
;
        org   0				; reset vector
        goto    init			; start at init
; init **************************************************************
init    movlw   b'00000011'             ; set a0-a1 inputs
	tris    porta                   ;     a2-a3 outputs
	clrf    porta                   ; clear porta out bits
	movlw   0h                    	; set port b as all outs
	tris    portb   
	movlw   b'00000010'             ; rtcc pre-scalar /8
	option                          ; 8us count loop = 2.048ms 

	movlw	b'01010101'		; alternate bits set
	movwf	portb			; put the count out to port b
	movlw	0h			; 0
	movwf	lo0			; init counters to 0
	movwf	hi0			; 
	movwf	lo1			; 
	movwf	hi1			; 

; ***********************************************************
; wait for either input0 or 1 to go from 0 to 1 (pulse start)
; ***********************************************************
; make sure both are low 1st
wait0_0	btfsc	porta, ip0		; wait for 0 on 0
	b	wait0_0			;
wait0_1	btfsc	porta, ip1		; wait for 0 on 1
	b	wait0_1			;
; ***********************************************************
; now check for either to go high - check when both done
; ***********************************************************
wait1	btfsc	porta, ip0		; wait for 1
	b	then1			; 0 came 1st
	btfsc	porta, ip1		;
	b	then0			; 1 came 1st
	b	wait1			; round again
;************************
; then1 0 pulse came 1st
; ***********************
then1	nop				;
;	bsf	porta, opsum		; echo the pulse
	call	time0			; measure the pulse
then11	btfss	porta, ip1		;
	b	then11			; wait for ip1 to go hi
	call	time1			; measure the other pulse (ip1)
	b	calc			; now do calc
;************************
; then0 1 pulse came 1st
; ***********************
then0	nop
	call	time1			; measure the pulse
then01	btfss	porta, ip0		;
	b	then01			; wait for ip0 to go hi
	call	time0			; measure the other pulse (ip1)
	b	calc			; now do calc
; *************************************************
; calc - work out the timing for sum and dif pulses
; *************************************************
calc	nop				;	
;	movlw	0h			; put 0 in w
	comf	lo0			; complement lo0 leave result in lo0 - invert it
	movf	lo0, w			; temp use ip1 count
	rlf	lo0, w			; shift the sign bit into carry
	rrf	lo0, w			; div by 2 preserving sign
	movwf	sum			; stick it in sum
	movf	lo1, w			; temp use ip1 count
	rlf	lo1, w			; shift the sign bit into carry
	rrf	lo1, w			; div by 2 preserving sign
	addwf	sum			; add to sum and result in sum
	call	sumout			; do the sum pulse 
	movf	lo0, w			; temp use ip1 count
	rlf	lo0, w			; shift the sign bit into carry
	rrf	lo0, w			; div by 2 preserving sign
	movwf	dif			; stick it in dif
	movf	lo1, w			; temp use ip1 count
	rlf	lo1, w			; shift the sign bit into carry
	rrf	lo1, w			; div by 2 preserving sign
	subwf	dif			; add to sum and result in sum
	call	difout			; do the sum pulse 
	b	wait0_0			; round again
; ************
; sub routines
; ************
; ********************************
; time0 - time the 0 channel pulse
; ********************************
time0	movlw	0d0h			; frig counter for 0 at mid pulse
	movwf	lo0			;
cnt0	incf	lo0			; count up 	- 1us
	nop				; pad 5us loop	- 1us
end0	btfsc	porta, ip0		; done		- 1us
	b	cnt0			; not done yet	- 2us
	retlw	0h			; return
; *********************************************************** 
; ********************************
; time1 - time the 1 channel pulse
; ********************************
time1	movlw	0d0h			; frig counter for 0 at mid pulse
	movwf	lo1			;
cnt1	incf	lo1			; count up 	- 1us
	nop				; pad 5us loop	- 1us
end1	btfsc	porta, ip1		; done		- 1us
	b	cnt1			; not done yet	- 2us
	bcf	porta, opdif		; echo the pulse
	movf	lo1, w			; fetch the count
	movwf	portb			; count out to portb
	retlw	0h			; return
; ***********************************************************
; ******************************
; sumout - send the sum op pulse
; need a 5us loop
; need to go round 1x complete then time the 2nd
; ******************************
sumout	nop				;
	movlw	0b0h			; preload the counter
	movwf	pulse			;
	bsf	porta, opsum		; start the pulse
;precnt
cntsum1	decf	pulse			; count up 	- 1us
	nop				; pad 5us loop	- 1us
	btfss	status, zro		; done		- 1us
	b	cntsum1			; not done yet	- 2us
	movf	sum, w			; load the variable bit
	addlw	080h			; shift origin
	movwf	pulse			; load the count
cntsum2	decf	pulse			; count up 	- 1us
	nop				; pad 5us loop	- 1us
	btfss	status, zro		; done		- 1us
	b	cntsum2			; not done yet	- 2us
	bcf	porta, opsum		; stop the pulse
	retlw	0h			; return 
; ******************************
; difout - send the dif op pulse
; need a 5us loop
; need to go round 1x complete then time the 2nd
; ******************************
difout	nop				;
	movlw	0b0h			; preload the counter
	movwf	pulse			;
	bsf	porta, opdif		; start the pulse
;precnt
cntdif1	decf	pulse			; count up 	- 1us
	nop				; pad 5us loop	- 1us
	btfss	status, zro		; done		- 1us
	b	cntdif1			; not done yet	- 2us
	movf	dif, w			; load the variable bit
	addlw	080h			; shift origin
	movwf	pulse			; load the count
cntdif2	decf	pulse			; count up 	- 1us
	nop				; pad 5us loop	- 1us
	btfss	status, zro		; done		- 1us
	b	cntdif2			; not done yet	- 2us
	bcf	porta, opdif		; stop the pulse
	retlw	0h			; return 
END

Desription

There are a few equates at the top, to make the rest easier to read. These mostly point at the register locations within the hardware.

Things really start at init where the 1st 2 pins of port a are set as the inputs, the last 2 as outputs. These are the lines that will connect to the servo signals. All 8 bits of port b are set as outputs. Port b is not actually used here, and it wouild be better to set them as inputs. This is left over from earlier development when I was using it to drive 8 leds to show me what was going on.
The real time clock prescaler is set to divide by 8. With a 4MHz crystal and 1MHz instruction beat rate, the 8bit timer clock ticks 1x every 8us, and take 8*256us = 2.048ms to repeat itself eg. go from zero to zero again. This was initially used to measure time intervals, but later decided that 5us was the ideal time resolution, so instruction based timing is done instead.
The port b gets set to alternate 1 and 0 pattern, which is again just a bit of dinosaur code, only there to let me know that it is actually doing something.
The various counter registers get initialised to zero, mainly just to be tidy.

The main loop begins at wait0_0.
What is going to happen, is that both input signals from the Rx will be low most of the time, and they will take turns to go high briefly, with the length of the high pulse indicating the information in that channel. We need to measure this, but 1st we need to know where we are in that cycle, so the 1st thing is to make sure that both of the signals are low (logic 0).
wait0_0
loops until there is a 0 on the 1st input ip0, and wait0_1 loops until there is a 0 on the 2nd input ip1.
Next 1 of the input signals is going to go high (logic 1), but we don't know which, so wait1 loops around checking both until 1 of them is found high. Note: if neither does, it will sit here forever ie. no signals in means no signals out.
Suppose the 1st input ip0 goes high 1st.
Flow jumps to then1 (so named because the 2nd input ip1 is expected next), where the sub routine time0 is called to measure the pulse time. Flow proceeds to then11, which loops until the 2nd input ip1 goes high, then calls time1 to measure it.
Flow on to calc where the new output pulse requirements are worked out.
If the 2nd pulse occurs 1st, very similar, but opposite code is used to do time1 and time0 in that order before flowing to calc just the same.

The lengths for the 2 outputs are derived from the values measured at the inputs in calc.
The values for each channel is represented by a signed 8 bit number eg. a value of 0 repesents a centre position, 1,2,3 etc. represent slightly longer pulses. Shorter pulses are represented by negative numbers -1,-2,-3 etc. with these in turn being represented by 255,254,253 etc.
Pulses generally vary between about 1ms and 2ms ie. a range of 1ms.
A 1ms divided by 256 possible states gives 3.9us resolution.
To cope with extremes a 5us time resolution is used covering a range of 1.28ms.

1st the value for 1st input ip0 is inverted. It just so happens that I have the aileron channel on my Tx inverted for all aircraft, and it needed to work in the opposite sense for the light zagi, so it gets done here. It would be possible to invert either or both channels conditionally on the state of other input(s), but that would mean more hardware, so I decided to just hard code it in here.
Note: given 2 inputs a and b the outputs will be (a+b)/2 and (a-b)/2. By reversing the inputs you can get (b+a)/2 and (b-a)/2. There is no way of getting (-a-b)/2 without software change.

The value from the 1st input ip0 is loaded into the working register (accumulator) from lo0. The sign bit is left shifted into the carry bit. We need this. A right shift divides the value by 2. The least significant bit is lost, but the most significant is shifted in from the carry bit, retaining the sign, 0 for positive, 1 for negative. The result is saved to sum.
Note: The divide by 2 is necessary to avoid extreme pulses being too big. 2 inputs at max could result in a servo deflection of 90 degrees instead of the normal max of 45 degrees.
The 2nd input value (from ip1) is similarly loaded from lo1, divided by 2 then added to the value already in sum.
We now have the required pulse length for the sum output, so the sub routine sumout is called.

Next the required difference pulse length is calculated in a similar way, using dif to store the answer by subraction.
Then the sub routine difout is called.

Now we've done 2 output pulses, we're done, so loop back to the beginning at wait0_0.

time0 is the sub routine that measures the length of the input pulse at ip0. The answer is in lo0. An experimentally determined value is preloaded into lo0 such that the answer will be zero for a centre or mid range pulse. Then a 5us loop is entered. This is the resolution of the timer. Each time round the loop, the value in lo0 is incremented by 1, and the ip0 is checked to see if it has gone low yet. Each of those instructions take 2 cycles ie. 2us. A 1us nop (no instruction) pads out the loop to the required 5us. When ip0 has gone low, the sub routine returns, leaving the answer in lo0.
time1 is very similar, using ip1, lo1. before it returns there is another bit of dinosaur/development code that copies the answer in lo1 out to portb. This was so that I could work out the correct preload value.

sumout is the sub routine that outputs the sum pulse. The pulse is timed by the value in the pulse register. An experimentally determined value is preloaded. The output outsum is set high to start the pulse. Despite what it says in the comment (cut and pasted), the value in pulse is decremented in a 5us loop until zero is reached. This times the "fixed" padding at the start of all pusles. Then the variable value is loaded from sum into pulse. This is decremented in a similar 5us loop. Finally the output outsum is set low to end the pulse, and the sub routine returns.
difout is very similar, using pulse, outdif.