; ******************************************* ; ** ** ; ** 64 Channel Serial Servo Controller ** ; ** For ATtiny2313 ** ; ** ** ; ** Copyright (c) May 2009 ** ; ** Len Holgate ** ; ** ** ; ** Based on original work ** ; ** by George Vastianos ** ; ** ** ; ** Note that this controller assumes ** ; ** that we have CD74HCT238E or equivalent** ; ** demultiplexor chips connected to the ** ; ** outputs of PortB and that the required** ; ** address lines for these MUXs are run ** ; ** from pins 3-5 of PortD. ** ; ******************************************* ; ******************* ; * Microcontroller * ; * characteristics * ; ******************* ; MCU = ATtiny2313 ; Fclk = 4.0 MHz .nolist .include "tn2313def.inc" .list .cseg .org $0000 ; Reset handler rjmp start .org URXC0addr ; UART RX Complete handler rjmp uart_rxc .org $000d ; Main program start .equ posnBase = $80 ; servo position data starts here .equ numServos = $40 ; number of servos that we support ;****************************** ;* Interrupt Service Routines * ;****************************** .def sregb = r16 .def stemp = r17 .def stemp2 = r18 uart_rxc: in sregb, SREG ; Store status register rjmp rcvdchar ; Start the task uart_rxcf: out SREG, sregb ; Restore status register ldi stemp, $90 ; Enable UART Receiver & RX Complete Interrupt out UCR, stemp reti ; Return to main program ;************************** ;* UART Reception Routine * ;************************** .def rxchar = r19 .equ rxStart = $60 ; The start of our serial rx buffer .equ rxServoNum = $60 ; The buffer space that holds the servo number .equ rxServoPosn = $61 ; The buffer space that holds the servo posn .equ rxEnd = $62 ; The end of our rx buffer... rcvdchar: ; Store the received character in rxchar, udr cpi rxchar, $ff ; Check if character is sync byte brne rcvdchar1 ldi ZL, rxStart ; If character is sync byte then clr ZH ; set Z register in the begin of packet area (in int. SRAM) rjmp uart_rxcf rcvdchar1: ; If character is not sync byte then st Z+, rxchar ; increase Z and store in int. SRAM the character cpi ZL, rxEnd ; Check if packet finished (i.e. we're at the end of our buffer) brne rcvdchar2 ldi ZL, rxStart rjmp panalysis ; If packet finished go to analyze it rcvdchar2: rjmp uart_rxcf ;******************************** ;* Data Packet Analysis Routine * ;******************************** panalysis: lds stemp, rxServoNum ; Check that our servo address is within range; ldi stemp2, numServos sub stemp2, stemp ; Any value bigger than numServos will mean that the next brcs panalysis1 ; ignore the packet lds stemp, rxServoNum ; It's a valid servo index ldi YL, posnBase ; Update the servo position data clr YH add YL, stemp ; use our servo number as an index into the control data lds stemp, rxServoPosn ; and update our servo's control value... st Y, stemp ; into the table... panalysis1: rjmp uart_rxcf ; Analysis finished ;************************************* ;* End Of Interrupt Service Routines * ;************************************* ;**************** ;* Main Program * ;**************** start: ;************** ;* Initiation * ;************** .def temp = r20 init: ldi temp, RAMEND out SPL, temp ldi temp, $19 ; Set UART on 9600 bps (for 115200 bps use $01) out UBRR, temp ldi temp, $90 ; Enable UART Receiver & RX Complete Interrupt out UCR, temp clr temp out WDTCR, temp ; Watchdog Timer disable out ACSR, temp ; Analog Comparator disable sts $0060, temp ; Init pck byte 01 sts $0061, temp ; Init pck byte 02 ; ldi temp, $00 ; Init servo positions to 'full left' ldi temp, $7F ; Init servo positions to 'middle' ; ldi temp, $FE ; Init servo positions to 'full right' ldi YL, posnBase ; Set up Y pointer to start of servo position data store clr YH setupchannels: st Y+, temp ; Initialise the channel with the starting servo position cpi YL, posnBase + numServos ; Check for end of loop, all servo channels initialised brne setupchannels ldi temp, $ff ; Init all PWM outputs; all 8 pins of port B are used out ddrb, temp clr temp ; Reset all PWM outputs out portb, temp ldi temp, $38 ; Init MUX address line outputs; we use pins 3-5 on port D out ddrd, temp clr temp ; Reset port d outputs to 0, initial address is $00 out portd, temp ldi ZL, rxStart ; UART routines store data here. clr ZH ; In Z register... sei ; Global interrupt enable mainloop: ;************************ ;* PWM Control Routines * ;************************ .def sActive = r21 ; Represents all of the pins on port B that are currently ; active. When we start the PWM generation loop this is set ; to FF and as each servo's pulse period comes to an end we ; turn off the bit that represents that servo's pin. .def s0Pos = r0 ; servo position of servo on pin 0 .def s1Pos = r1 ; servo position of servo on pin 1 .def s2Pos = r2 ; servo position of servo on pin 2 .def s3Pos = r3 ; servo position of servo on pin 3 .def s4Pos = r4 ; servo position of servo on pin 4 .def s5Pos = r5 ; servo position of servo on pin 5 .def s6Pos = r6 ; servo position of servo on pin 6 .def s7Pos = r7 ; servo position of servo on pin 7 pwmmark: ; There isn't enough SRAM in the ATtiny2313 to allow us to use a mapping ; table to map physical PWM output pins to the logical 0-63 indexes that we ; use for controlling them. This means that the layout below may need to be ; adjusted to make your rows of PWM connectors line up nicely and in order on ; your final board layout... Simply move the indexes used around so that the ; connections from your MUX chips result in a sensible arrangement of PWM ; connectors on your board and the logical control indexes make sense. ; We COULD use a mapping table in the program memory space and access it via ; lpm but this takes 3 cycles per load and since we have no space in SRAM for ; the values we'd need to load each time through the loop which seems wrong... ; The format shown below numbers the logical indexes sequentially across ; the MUX chips in order; that is the first chip (connected to B0) has outputs ; for servos 0-7, the second (connected to B1) for servos 1-15, etc. This makes ; it easy to use only some of available physical channels by simply not ; connecting a MUX to the later pins of the ATtiny... ; each time through the loop we generate all 64 PWM channels. we start by ; generating a pulse on each of the ATtiny's port B pins with the address select ; pins set to select pin 0 of the attached MUX chips. This produces a PWM ; signal on pin 0 of each of the MUX chips. Next we adjust the address select ; pins to select pin 1 on the MUX chips and generate a new pulse for the next ; set of servos. We continue until we've generated a pulse on each of the ; MUX pins. The loop below takes less than 20ms to execute so we produce our ; pulses at 50hz. .def count = r22 clr temp ; address channel 0 on the mux's out portd, temp lds s0Pos, posnBase + 0 ; MUX0 lds s1Pos, posnBase + 8 ; MUX1 lds s2Pos, posnBase + 16 lds s3Pos, posnBase + 24 lds s4Pos, posnBase + 32 lds s5Pos, posnBase + 40 lds s6Pos, posnBase + 48 lds s7Pos, posnBase + 56 ; MUX7 rcall pwm ldi temp, $08 ; address channel 1 on the mux's out portd, temp lds s0Pos, posnBase + 1 ; MUX0 lds s1Pos, posnBase + 9 ; MUX1 lds s2Pos, posnBase + 17 lds s3Pos, posnBase + 25 lds s4Pos, posnBase + 33 lds s5Pos, posnBase + 41 lds s6Pos, posnBase + 49 lds s7Pos, posnBase + 57 ; MUX7 rcall pwm ldi temp, $10 ; address channel 2 on the mux's out portd, temp lds s0Pos, posnBase + 2 ; MUX0 lds s1Pos, posnBase + 10 ; MUX1 lds s2Pos, posnBase + 18 lds s3Pos, posnBase + 26 lds s4Pos, posnBase + 34 lds s5Pos, posnBase + 42 lds s6Pos, posnBase + 50 lds s7Pos, posnBase + 58 ; MUX7 rcall pwm ldi temp, $18 ; address channel 3 on the mux's out portd, temp lds s0Pos, posnBase + 3 ; MUX0 lds s1Pos, posnBase + 11 ; MUX1 lds s2Pos, posnBase + 19 lds s3Pos, posnBase + 27 lds s4Pos, posnBase + 35 lds s5Pos, posnBase + 43 lds s6Pos, posnBase + 51 lds s7Pos, posnBase + 59 ; MUX7 rcall pwm ldi temp, $20 ; address channel 4 on the mux's out portd, temp lds s0Pos, posnBase + 4 ; MUX0 lds s1Pos, posnBase + 12 ; MUX1 lds s2Pos, posnBase + 20 lds s3Pos, posnBase + 28 lds s4Pos, posnBase + 36 lds s5Pos, posnBase + 44 lds s6Pos, posnBase + 52 lds s7Pos, posnBase + 60 ; MUX7 rcall pwm ldi temp, $28 ; address channel 5 on the mux's out portd, temp lds s0Pos, posnBase + 5 ; MUX0 lds s1Pos, posnBase + 13 ; MUX1 lds s2Pos, posnBase + 21 lds s3Pos, posnBase + 29 lds s4Pos, posnBase + 37 lds s5Pos, posnBase + 45 lds s6Pos, posnBase + 53 lds s7Pos, posnBase + 61 ; MUX7 rcall pwm ldi temp, $30 ; address channel 6 on the mux's out portd, temp lds s0Pos, posnBase + 6 ; MUX0 lds s1Pos, posnBase + 14 ; MUX1 lds s2Pos, posnBase + 22 lds s3Pos, posnBase + 30 lds s4Pos, posnBase + 38 lds s5Pos, posnBase + 46 lds s6Pos, posnBase + 54 lds s7Pos, posnBase + 62 ; MUX7 rcall pwm ldi temp, $38 ; address channel 7 on the mux's out portd, temp lds s0Pos, posnBase + 7 ; MUX0 lds s1Pos, posnBase + 15 ; MUX1 lds s2Pos, posnBase + 23 lds s3Pos, posnBase + 31 lds s4Pos, posnBase + 39 lds s5Pos, posnBase + 47 lds s6Pos, posnBase + 55 lds s7Pos, posnBase + 63 ; MUX7 rcall pwm rjmp pwmmark ;******************** ;* PWM Mark Routine * ;******************** ; PWM routine for one bank of 8 servos ; Note that the delays and looping are set to be correct for the 'middle position' ; of 7F. This gives a 1500us pulse, the 0 and FE positions are a bit less useful... pwm: ldi sActive, $FF ; Turn on all 8 servos on this channel of MUXs out portb, sActive ; Set output pins of the Servo rcall delay ; Initial, constant delay ldi count, $00 ; Start variable length pulse delay pwm0: cp count, s0Pos ; Reset output pin of Servo 0 if position = count brne pwm1 cbr sActive, $01 pwm1: cp count, s1Pos ; Reset output pin of Servo 1 if position = count brne pwm2 cbr sActive, $02 pwm2: cp count, s2Pos ; Reset output pin of Servo 2 if position = count brne pwm3 cbr sActive, $04 pwm3: cp count, s3Pos ; Reset output pin of Servo 3 if position = count brne pwm4 cbr sActive, $08 pwm4: cp count, s4Pos ; Reset output pin of Servo 4 if position = count brne pwm5 cbr sActive, $10 pwm5: cp count, s5Pos ; Reset output pin of Servo 5 if position = count brne pwm6 cbr sActive, $20 pwm6: cp count, s6Pos ; Reset output pin of Servo 6 if position = count brne pwm7 cbr sActive, $40 pwm7: cp count, s7Pos ; Reset output pin of Servo 7 if position = count brne pwm8 cbr sActive, $80 pwm8: ; Now we update our output pins to turn off those that have finished generating ; their pulse. ; This gives us a 1500us pulse for a position value of 0x7F, a 579.25us pulse for ; 0x00 and, a 2420.75us pulse for 0xFE. This is a greater range than we really ; need but the issue here is the number of instructions required to check each ; pin each time through the loop. A faster clock speed would allow us to ; perform the real work faster and add a delay into this loop to allow us to ; fine tune the range a little better... Perhaps... out portb, sActive ; turn off those pins that have finished... inc count cpi count, $ff ; Check if delay completed brne pwm0 ret ; Stop pulse generation ; Note that the PWM generation function always takes the same amount of time ; to execute as the loop always takes the same number of instructions and always ; runs to completion; ie it always does 255 iterations. At present this takes ; 2423.75us... ;***************** ; Delay Routine ; The idea is that between setting the pin and unsetting the pin with a ; time value of 7F we get a 1500 uSec delay. This is the 'middle' position ; of the hitec servos that we're using... delay: nop nop nop nop ldi temp, $E4 ; * Start of delay delay1: nop nop nop nop nop nop dec temp cpi temp, $00 brne delay1 ret ; * End of delay ;******************************* ;* End Of PWM Control Routines * ;*******************************