L'Hexapod: Atmel ATtiny2313 Servo Controller v0.2 - source code

Previously published

This article was previously published on lhexapod.com as part of my journey of discovery into robotics and embedded assembly programming. A full index of these articles can be found here.

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 *
;*******************************