L'Hexapod: A timer driven PWM servo controller

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.

As I mentioned recently, the original servo controller firmware that I posted was flawed and wasn’t suitable to use as a base for the more complex servo control code that I want to add to the controller. The new firmware follows the design that I spoke of here and relies on the ATTiny’s 16-bit timer to generate our PWM signals. Since we’re using a timer we don’t need to waste processor time with hard-coded timing loops full of ’nop’s as we did in the original firmware and this gives us much more time to do useful work. Since the code is quite complex (and also not quite finished yet!) I’m going to present it in stages. First the initial shell of a timer driven PWM generator, next the calculations and data munging required to transform the control data from what we receive via the serial protocol to what we need for the PWM signal generation and finally the serial protocol. Once all of that is done I’ll begin to extend the firmware beyond the basic commands so that I can, perhaps, at last switch back to working with my prototype legs.

But first, a bit of a refresher. The aim of the code is to generate multiple PWM signals using an ATTiny2313. The signals should be suitable for controlling hobby servo motors like the Hitec HS-422 servo which means that the pulses should be adjustable from 900us to 2.1ms and repeated at a rate of 50Hz (20 times per second). The ATTiny2313 can generate PWM on its own, but for a hexapod robot I need at least 18 channels for the leg servos. The ATTiny2313 has 8 I/O pins on PORTB and 5 on PORTD that we can use but this is nowhere near enough to generate 18 signals with one pin per signal… Using 3-8 line demultiplexor chips (such as the CD74HCT238E it’s possible to generate 64 signals from 8 signal generation pins and 3 address control pins. 64 signals is more than I need, but… there’s a certain symmetry in it. To generate 64 signals we need to split the signals into 8 batches of 8. Each batch of signals should be generated 20 times per second so each batch of signals can take no longer than 2.5ms to generate. The second version of the simple firmware, presented here, uses 8 CD74HCT238E chips and lots of timing loops to generate 64 signals; but it only just manages it and, as the timing loops burn lots of processor time, we can’t make the servo controller that complex… Ideally I’d like to add commands to move servos incrementally over time to a final position, stop them wherever they are right now and report back their current location, move them in batches (so that we can say we want servo 1 in position 128, servo 2 in position 220 and servo 3 in position 1 and for them all to arrive in their final position at the same time.). These more complex commands take processor time to calculate the required servo data and with the simple controller firmware we don’t have that processor time available. Switching to a timer based approach gives us back the processor time that we are currently wasting in the timing loops and makes the PWM pulse train generation code the highest priority code; thus removing any chance that the serial I/O code and complex command processing could cause jitters in the PWM pulse train. And now, on with the code…

If you don’t currently know about AVR timers then you should go and read the excellent timer tutorial over at AVR Freaks. We’re going to use the 16-bit Timer1 in CTC mode to generate a series of timer interrupts which we will use to turn off the various PWM signals that we’re sending to the servos. There are actually two different jobs that the timer interrupt will do and we’ll use a jump table and the ijmp instruction to do an indirect jump based on the contents of the Z pointer. This means that initial setup and our interrupt handler looks like this:

; MCU  : ATTiny2313
; FClk : 8.0 MHz 

.nolist
.include "tn2313def.inc"
.list

.def count = r16
.def index = r17
.def bankIndex = r19
.def temp20 = r20
.def muxAddress = r23

.equ	POSITION_DATA_START = SRAM_START
.equ NUM_SERVOS = 64
.equ	PWM_DATA_START = POSITION_DATA_START + NUM_SERVOS
.equ	JUMP_TABLE_START = $0012

.ORG $0000
	rjmp Init ; Reset
	reti ; INT0 Interrupt
	reti ; INT1 Interrupt
	reti ; TC1Capt Interrupt
	rjmp TC1CmpA ; Timer 1 Compare A match
	reti ; TC1 Overflow Int
	reti ; TC0 Overflow Int
	reti ; UART0 RX Int
	reti ; UART0 TX Int
	reti ; Ana Comp Int
	reti ; PCINT
	reti ; TC1 CompB Int
	reti ; TC0 Compare Match A Int
	reti ; TC0 Compare Match B Int
	reti ; USISTART Int
	reti ; USIOverflow Int
	reti ; EERDY Int
	reti ; WDT Overflow Int

.ORG JUMP_TABLE_START
rjmp PWMSetup 
rjmp PWMPulseStop
 
TC1CmpA:	

	ijmp 	; Z is set up to point into our jump table (above) so we jump to Z then
			; jump to the correct state for our timer handling...

Next we set things up, first the stack, then we set up Timer1 to run in CTC mode with a resolution of clock/8. Note that when we set the 16-bit timer control register, OCR1A, we do so in high byte, low byte order. This is VERY important as the 16-bit registered are buffered so that they are updated atomically. This means that when you write the high byte it is copied into a buffer and when you write the low byte the contents of the low byte and the buffer are transferred to the register. Getting these updates in the wrong order creates ‘interesting’ effects in your timer code….

Init:

	; Set stack pointer - we use the stack for the return address of the interrupt handler...

	ldi temp20, RAMEND
	out SPL, temp20

	; Initialise Timer1 in CTC mode

	ldi temp20, HIGH(200) 	; CTC-value - something low to kick us off... Must be longer
	out OCR1AH, temp20	; than it takes for us to finish our initialisation...
	ldi temp20, LOW(200)	; Note that the 16-bit timer control registers MUST be written
	out OCR1AL, temp20	; high byte first then low byte as the low write triggers the
						; 16-bit atomic write.

	clr temp20			; Controlword A	0x0000
	out TCCR1A, temp20

	ldi temp20, (1<<WGM12) | (1<<CS11) 	; CTC mode (WGM12) with the timer running at clock/8
	out TCCR1B, temp20

	ldi temp20, 1<<OCIE1A	; Enable Timer1 interrupt
	out TIMSK, temp20

Now that we have the timer configured we can initialise our output ports for the PWM generation.** **

; Initialise our PWM output pins	

	ldi temp20, $FF				; set port B as output port
	out DDRB, temp20

	; Initialise our MUX address select output pins 		

	ldi temp20, $38			; Init MUX address line outputs; we use pins 3-5 on port D
	out DDRD, temp20

	clr muxAddress			; address channel 0 on the mux's
	out PORTD, muxAddress

We need to set the Z pointer to point into our jump table for timer interrupt dispatch.

    ; We have two different states for our timer interrupt handling, a setup phase and a 
    ; 'pulse switch off' phase. We use an indirect jump via the value of the Z pointer to deal 
    ; with this. Initially we set Z to point to the start of our jump table, which jumps to 
    ; the PWM setup routine, at the end of the setup routine we change Z to point to the next 
    ; entry in our jump table which is the PWM pulse switch off routine. Once all pulses have 
    ; been switched off we reset Z to the PWM setup routine and the next interrupt starts the 
    ; PWM generation for the next bank of servo pins.

	ldi ZL, LOW(JUMP_TABLE_START)       ; set up our first jump table entry for timer handling
	ldi ZH, HIGH(JUMP_TABLE_START)

For ease of debugging in the simulator I find it useful to explicitly clear down (or set) the SRAM area to a known value, 0x00 is usually good but sometimes 0xFF is useful too…


	; Clear down the SRAM area so that it is slightly easier to debug in the simulator
	; Note that this code can be removed without causing any harm...

	ldi XL, LOW(SRAM_START)		; clear down the area we'll be working in
	ldi XH, HIGH(SRAM_START)		; to make it easier to debug

	ldi count, SRAM_SIZE
	clr temp20					; usually it's easier if all of SRAM is 0x00 in the simulator
	;ldi temp20, 0xFF				; but sometimes 0xFF helps too...

InitFillMemoryLoop:

	st X+, temp20
	dec count
	brne InitFillMemoryLoop	

    ; End of SRAM memory fill loop

Now the initialisation work has been done we can turn on interrupts

; Initialisation is done...

	sei		; enable interrupts	
		
And then just loop. Once we develop the serial protocol processing code that will run in the main loop, but for now we just spin and wait for timer interrupts.

	; The serial handling goes in the loop...

loop: 			; wait here for interrupt

	rjmp	loop

So now we come to the meat of the code, the actual timer interrupt handling. As I said earlier, we’re using the Z pointer for an indirect jump into a jump table. The timer interrupt jumps to Z and Z jumps to either our PWM setup code or our PWM pulse switch off code. Here’s the setup code; note that this is very simplified as it’s just to show the timer handling and the list of timeouts being used to switch off PWM signals.

PWMSetup : 

	; Set the timer to 500ms which is much longer than the PWM setup phase takes to run. We 
	; will adjust this to the correct value when we complete the setup phase.

	ldi temp20,HIGH(500) 
	out OCR1AH,temp20
	ldi temp20, LOW(500)	; Note that the 16-bit timer control registers MUST be written
	out OCR1AL, temp20	; high byte first then low byte as the low write triggers the
 						; 16-bit atomic write.

	; set the port d bits to control the mux address selection...

	out PORTD, muxAddress		; mux address is 8 * bank index, 08, 10, 18, 20 etc

	; Start the PWM signal generation for this bank. All pins on port b are on...

	ldi temp20, $FF
	out PORTB, temp20		

	; We generate 8 PWM signals at a time and repeat each 8 signals every 20ms, since we can
	; support 64 servos in total this means we need to generate 8 batches of 8 signals and 
	; each batch can take at most 2.5ms to generate. The signals should be between 900us and
	; 2.1ms long. 

	; The data we use to generate the PWM signals is based on the data that the serial I/O
	; code stores at POSITION_DATA_START. The serial code stores a single byte per servo. The
	; servo control value can be 0-254 with 127 being in the centre with a pulse length of 
	; 1.5ms

	; The PWM generation code uses two byte servo control values that represent the actual
	; timeout values required to turn off the servo pin, we can have up to 8 off times, as
	; we merge duplicate off times together. The final off time (which could be number 9 if
	; we have 8 unique servo control values) is the time remaining between the last servo 
	; pin being turned off and the start of the next PWM generation cycle. I.e. it is the 
	; time remaining until the 2.5ms time period has passed.

	; Since this piece of code is just testing the basic timer principles we work with dummy
	; data here, but the real code would transfer the single byte servo control data for this
	; batch of servos from the serial i/o buffer at POSITION_DATA_START and then sort them
	; into ascending order, convert them into two byte timeouts and then set up the timer
	; appropriately.

	; Load some dummy data...

	ldi YL, LOW(PWM_DATA_START)
	ldi YH, HIGH(PWM_DATA_START)

	; servo 0

	ldi temp20, HIGH(1499)			; values are timeouts in us -1 to allow for the interrupt
  	st Y+, temp20					; handling time (i.e. it takes us 1us to get from the timer
	ldi temp20, LOW(1499)			; going off to the point where we switch off the pins)
  	st Y+, temp20
	ldi temp20, $FE				; turn off pin 1...
  	st Y+, temp20

	; servo 1

	ldi temp20, HIGH(5)				; 6us later (this is our smallest difference between control
  	st Y+, temp20					; values).
	ldi temp20, LOW(5)
  	st Y+, temp20
	ldi temp20, $FC				; turn off another servo
  	st Y+, temp20

etc for all 8 servos… In the real code we calculate these values from the input servo control data that the serial protocol manages for us. A final timeout value is then added to use up all of the 2.5ms time period that we have for PWM generation for this batch of 8 servos…

; final period timeout

	ldi temp20, HIGH(687)			; this timeout takes us to the end of the 2.5ms cycle for this batch
  	st Y+, temp20					; of servos.
	ldi temp20, LOW(687)
  	st Y+, temp20
	ldi temp20, $FF				; sentinel value
  	st Y+, temp20

Now that all the servo control data has been ‘calculated’ for this batch of servos we can adjust the timer so that it times out on the first calculated value so that we turn off the first servo…

ldi YL, LOW(PWM_DATA_START)
	ldi YH, HIGH(PWM_DATA_START)

	ld temp20, Y+
	out OCR1AH,temp20
	ld temp20,Y+			; Note that the 16-bit timer control registers MUST be written
	out OCR1AL, temp20	; high byte first then low byte as the low write triggers the
						; 16-bit atomic write.

Since we’ve adjusted the timer value up from a value that hadn’t been reached yet we have effectively set the timer from the point where this interrupt was generated to run for the time period specified; i.e. until the end of the first PWM signal. Now we can set up for when this code is run for the next batch of servos…

; Now we set things up for the next time that the PWMSetup code is called, we increment the
	; mux address by 8, and increment the bank address...

	ldi temp20, 8
	add muxAddress, temp20

	inc bankIndex			; select next bank of servos, wrap to 0 at 8...

	cpi bankIndex, 8        
	brne PWMSetupDone

	clr bankIndex
	clr muxAddress

And finally we can set up the Z pointer so that the next time the timer interrupt fires we will call the PWMPulseStop code instead of this setup code.

PWMSetupDone :

	; Finally set our Z pointer back into the correct place in our jump table so that
	; we're processing PWM pulse stops when the next interrupt goes off...
		
	ldi ZL, LOW(JUMP_TABLE_START + 1)			; set up Z for the next timer interrupt
	ldi ZH, HIGH(JUMP_TABLE_START + 1)			; we jump to the second entry in the jump table

	reti

And that’s the end of the first timer interrupt for this batch of servos. The code above has been run during the time when all the servo signals are ON since all of the signals must be at least 900us in length. When the next timer interrupt occurs we will be at the end of the shortest PWM signal and we’ll turn off the pin (or pins) associated with that signal, we then step along the control data and set the next timeout which will occur when the next PWM signal should stop. Once all of the signals have stopped we set one final timeout which will fire when the whole of the 2.5ms period has completed and the setup code above will be called for the next batch of servos.

; PWM pulse stop phase. This timer interrupt handler turns off some pins on PORTB and then sets the
; next stop time and returns, once all pins are off the final timeout uses up the remaining part
; of the 2.5ms slot and sets the Z pointer to call the PWMSetup code to start the next batch of servos

PWMPulseStop :

	ld temp20, Y+

	nop                             ; pad the handler so that there is a consistent 1us delay between
	nop                             ; the timer firing and the pins being switched off
	nop
	nop
	nop
	out PORTB, temp20				; Turn off servo pins...

	cpi temp20, 0x00				; If all the pins are now off then the next timeout is our last
	brne PWMPulseSetNextTime		; and we switch back to setup phase when it expires...

	ldi ZL, LOW(JUMP_TABLE_START)	; set up our first jump table entry for timer handling
	ldi ZH, HIGH(JUMP_TABLE_START)

PWMPulseSetNextTime:

	; load the next time...

	ld temp20, Y+
	out OCR1AH, temp20
	ld temp20,Y+			; Note that the 16-bit timer control registers MUST be written
	out OCR1AL, temp20	; high byte first then low byte as the low write triggers the
						; 16-bit atomic write.

	reti```
The five **nop**s are to adjust the interrupt handler so that it takes 1us to run. If you place a breakpoint in the AVR Studio simulator on the line after the **out PORTB** instructions and in the handler above and one after the **out PORTB instructions in the PWM setup handler you'll see that the time taken is exactly 1500us, i.e. the first timeout set +1us. Likewise the time taken between the **out PORTB** calls in the PWM setup handler is exactly 2500us.

The code presented here forms the basis of the timer driven PWM generation code. You can download the source from [here](https://lenholgate.com/lhexapod/ATtiny2313-8Mhz-TimerShell.asm) and play with setting breakpoints in the simulator to see that the PWM train is rock solid. You could also download the code to some hardware and see that it generates fixed PWM signals based on the values that you set in the dummy data setup phase; right now pin 1 is at the centre position of 1500us. Note that for subsequent servos the times are relative to the first servo timeout but that the first servo timeout is absolute, so if servo 1 is 1499 for a 1500us pulse and servo 2 is 5 you get a 1506us pulse for servo 2. The final value should always be 2500 - the total accumulated time for all servos (i.e. the length of the longest pulse) -1.
Next time I'll present the code that takes the single byte servo control values 0-254 and converts them into the data that the PWM setup code currently dummies out.