Atmel ATtiny2313 Servo Controller v0.2 - source code
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, demultiplexer 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 demultiplexer 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 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
rjmp uart_rxcf
rcvdchar1: ; 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 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 *
;*******************************