Here's the source code to the 64 channel ATtiny2313 servo controller. Note that you'll need to use up to 8 CD74HCT238E, or equivalent, demultiplexor chips and that you can adjust the number of servos that you can control in steps of 8 using as many or as few CD74HCT238E chips as you want. If you only want 8 channels then you can do this without any demultiplexor chips; see here for source code to the 8 channel version.
The controller uses the 'standard' three byte serial "SSC" protocol of 0xFF <servo> <position>. Where <servo> is a value between 0 and 63 (or as many servos as you decide to support) and <position> is a value between 0 and 254 where 127 gives a pulse length of 1500us. At present the timing gives us pulses of 579.25us for 0 and 2420.75us for 254. This is a greater range than we really need (at least for the Hitec servos that I'm using) 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... Anyway, I hope to move away from the current design shortly to one that gives a finer control over the pulse lengths.
Source code can be downloaded here.
; ******************************************* ; ** **; ** 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 taskuart_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 rjmp uart_rxcfrcvdchar1: ; If character is not sync byte then st Z+, rxchar ; increase Z and store the character cpi ZL, rxEnd ; Check if packet finished brne rcvdchar2 ; (i.e. we're at the end of our buffer) ldi ZL, rxStart rjmp panalysis ; If packet finished go to analyze itrcvdchar2: 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 delaydelay1: nop nop nop nop nop nop dec temp cpi temp, $00 brne delay1 ret ; * End of delay
;*******************************;* End Of PWM Control Routines *;*******************************

Leave a comment