A Robotics Project Page
Google
 
Web    www.PhilBot.com
My other sites
  • OurCoolHouse
  • Phil's Resume
  • Web Portfolio

  • [Home] [Projects] [Hardware] [Software] [Books] [Links] [Downloads]


    Variable-Speed Continuous-Rotation Servo Hack

    People have been hacking the classic Radio Controlled Servo to turn it into a wheel drive for quite a while.  The basic concept is that you remove any physical stops that prevent the servo horn from continuously rotating, and then you somehow disable the feedback pot.  Methods for disabling the feedback pot include replacing it with a fixed voltage divider, or  simply cutting the linkage between the pot wiper and the final gear drive.   Either way, you end up with a servo that can be attached to a robot wheel, and is capable of forward and backwards motion.

    The solution is elegant and effective, with a wide range of speed, size and torque servos to choose from.  However, as always, there are a couple of drawbacks.

    In most cases the resulting servo needs to be calibrated.  This means tweaking the pot, or voltage divider to be able to direct the servo to stop rotating.  Depending on how the servo was modified this may need to be re-done from time to time.  Another glitch is that although it might appear that it's also possible to vary the drive speed of the servo, there is really only a very narrow range of control, and this range is somewhat unpredictable.  It's usually only possible to a few speed variations between Stopped and Full Speed.

    If a robot needs a wide variety of speeds, it's typical to either remove all of the drive electronics from the servo and only use the gearing, or to abandon the servo all together.  If the servo is just used for the motor and gearing, then a separate H-Bridge driver is used, and the robot controller must generate the correct PWM (Pulse Width Modulation) signals to run the motor bridge.

    My first project was to see if I could come up with an alternative to these approaches.  I wanted to see if I could hack the Servo internals such that the robot would still think that it's controlling a servo, but that the servo itself would respond with a more linear range of speeds.

    I did some basic research and found a [great page] by Lee Buse on the [Seattle Robotics Society] website that illustrated the internals of a typical servo. This page was showing another way to achieve a similar "variable speed" result, but it still used the basic feedback-pot elimination method.  The real benefit to me was that it identified the pin functions on the motor driver IC that the Futaba Servo uses.  I decided that I would replace all the circuitry except that driver IC with my own tiny embedded processor.  That processor would read the pulsed control signal coming from the robot and signal the motor driver IC to vary the speed of the motor.  I had already played around with the cool [PIC12F683] [Microchip] processor, and since it was pretty small (just an 8-pin DIP), and I already had some spares floating around, I decided to use it for my project.

    I decided that I has some basic goals that I needed to meet with my solution:

    After checking on the PIC12F683 one more time, I felt confidant that I could write the code, so I started on the Servo conversion.  Naturally, this began with stripping off all the parts that I didn't need.  I decided to leave the main supply capacitor, the motor drive IC and a few surface-mount decoupling caps.  Here's a picture of the servo board after I was done wielding my soldering iron.   Click any image for an enlargement.

    Topside of Servo board showing the few remaining components Underside of Servo board showing the few remaining surface mount components

    Now I had a motor controller board with the high current drivers intact.  It turns out that there are two pins on the driver IC.  One causes the motor to spin in the forward direction, the other causes it to spin in the opposite direction. So now I needed to add the micro-controller to drive either of these pins with a PWM signal. 

    OK, the 12F683 is an 8 pin dip microcontroller with 6 multi-function I/O lines.  If your application can work with a clock less than 8MHz, then you don't need any external oscillator or crystal, as the 12F683 has it's own internal 8MHz clock.   I decided to use one line as a  digital input, and four as outputs.  I only really needed 2 outputs but I wasn't sure how much current I needed to run the Motor Driver IC, so I decided to double them up to be "better safe than sorry".  I ended up only using the PIC processor and a diode in series with the VCC line to drop the voltage to an acceptable value.  Here's a picture of the CRUDE prototype.

    Since I was going to be using PIC assembly language, I wanted the timing to be as straight forward as possible, so I started figuring out the ideal timing frequency. I needed to be able to measure the classic servo control signal, which is a variable width pulse that repeats about once every 20mSec.  The width of the pulse goes from 1mSec (for full reverse) to 2mSec (for full forward).  A pulse width of 1.5mSec is taken to be the Stop signal. 

    One easy way that I set up accurate timing in the 12F683 is to use Timer 2 which can be set to repeatedly count down from a preset value to zero.  Each time it hits zero it sets an interrupt bit and starts again.  Even if I don't use the interrupt feature, I can check and reset this bit and use it as a moderate speed counter.  So the goal in this instance is to determine the ideal Timer 2 interrupt rate, and then just use this to run a software counter (or timer).

    I'm need to measure the Servo Control Pulse, and I want 10 forward and 10 reverse speeds, so I need a timing resolution of about 50 uSec (1mSec Full Span / 20 steps).  If I measure the width of the control pulse with 50 uSec timer clock, my active control range would be 20 - 40 counts, with a count of 30 being the neutral position.  So:

    I then need to generate a PWM signal on either the forward or reverse pin on the Motor Driver IC based on this count.  The original servo circuit had a motor PWM frequency that matched control pulse frequency, which was about 50 Hz (a 20 mSec repeat rate).  Since I knew this rate wasn't an absolute requirement, I chose a slightly different rate that made my program easier to write.  Since I knew that I was going to have a 50 uSec Timer 2 running in my PIC, I divided this by 256 (a wonderful 8 bit number) and saw that it gave me a period of 12.8 mSec (or 78 Hz).  This faster rate would do just fine for the motor PWM..

    To see how I used these different timers and ratios, let's look at the forward speed range for a minute. 

    On my front-end, I would be seeing a Servo Control Pulse width ranging from 1.5 to 2.0 mSec.  With a counter running at 50 uSec, this corresponds to a count range of 30 - 40 for the forward speed range.  My code would subtract 30 from this to get a range of 0 - 10.  A count of zero means stop, so I'm only interested in the range of 1-10.  Since I've decided that my Motor Drive PWM is going to be 256 Timer 2 clocks, I can approximate each speed range as an additional 25 clocks of pulse width (I really need 25.6 clocks... but who's counting :)

    My procedure is determine the speed range (1-10) and multiply it by 25 to get the Motor Pulse width (in Timer 2 Clocks).

    So my program is really two independent state machines.

    1. State Machine 1 is looking at the positive and negative transitions of the Servo Control Pulse, and calculating the corresponding Motor Drive Pulse Width.
    2. State Machine 2 is generating the positive and negative transitions for the Motor PWM outputs based on a 256 clock long cycle.

    The full program is called PBot_ServoHack1.asm and is available on my [download] page.  Here are some highlights:

    The first thing I always do is define the various constants that I want to use in the program.  This way if I need to change one, it's easy to find.

    ;***** VARIABLE DEFINITIONS
    Temp			EQU	0x20  	;
    Flags			EQU	0x21	; variable used for context saving
    ICntLSB      		EQU 	0x22  	; Timer for measuring Servo High/Low Period
    ICntMSB			EQU 	0x23  	; Timer for measuring Servo High/Low Period
    Length			EQU 	0x24  	; Pulse Length (Ticks)
    PulseTimer		EQU	0x25	; Pulse countdown timer
    CycleTimer		EQU	0x26	; Full Cycle Timer
    
    ;***** GPIO Allocations
    IO_SERVO	  	EQU	 3	; GPIO Bit 3, Servo Control Pulse Input.
    
    IO_FWD_ON		EQU	0x03	; GPIO Bits 0,1 Forward Drive Out
    IO_FWD_OFF		EQU	0xFC
    IO_REV_ON		EQU	0x30	; GPIO Bits 4,5 Reverse Drive Out
    IO_REV_OFF		EQU	0xCF
    
    
    ; ***** FLAG BIT DEFINITIONS
    F_Forward	  	EQU	0	; Set = Forward.
    F_Reverse	  	EQU	1	; Set = Forward.
    
    ;****  TIMER Constants
    TICK			EQU	0x64	; 50uS (8MHz Clock, 20KHz Sample rate)
    REV_THRESH		EQU	0x1D	; < 1.5 mSec
    FWD_THRESH		EQU	0x1E	; > 1.5 mSec
    MAX_RANGE		EQU	0x0A	; 10 steps per axis (plus 1)
    MAX_RANGEP1		EQU	0x0B	; Max Range Plus 1
    
    
    The next part of the program just sets up the various registers and puts everything in motion.  Loading the TICK value into Timer 2 preload register will cause this timer to roll over every 50 uSeconds (0.000050 Seconds). 
    main
    
    	bcf 	STATUS,RP0 	; Set file register bank to 0
    	clrf 	GPIO 		; Init GPIO
    	movlw 	0x07 		; Set all Comparitors off (GPIO On)
    	movwf 	CMCON0
    
    	bsf 	STATUS,RP0 	; Set file register bank to 1
    	clrf 	ANSEL 		; Disable Analog Chanels
    
    	movlw 	0x70 		; Set Clock Frequency to 8MHz
    	movwf 	OSCCON
    
    	clrf 	TRISIO 		; Set GPIO Directions
    	bsf 	TRISIO,IO_SERVO ; Setup servo as input
    
    	movlw 	TICK 		; Load Timer 2 up with period
    	movwf 	PR2 		;
    	bsf 	PIE1,TMR2IE 	; Turn On Timer 2 Interrupt
    
    	bcf 	STATUS,RP0 	; Set file register bank to 0
    
    	clrf 	Flags 		; Clear out all variables
    	clrf 	ICntLSB 	;
    	clrf 	ICntMSB 	;
    	clrf 	Length 		;
    	clrf 	PulseTimer 	;
    	clrf 	CycleTimer 	;
    
    	bsf T2CON,TMR2ON 	; Turn On Timer 2
    
    

    After setting up the registers, the program enters it's main processing loop.  Here it's operation depends heavily on a routine called CheckForTic. CheckForTic is used to watch the Timer2 Interrupt bit. If the bit is set, it means that another 50 uSec has elapsed so the ICntLSB is incremented, and the interrupt is reset.

    The program loop basically follows the low to high and high to low transitions of the Servo Control input.  When the input goes high, the code resets the ICntLSB counter, and when it goes low again, the code checks the value of the counter to see how long the control pulse was.  

    Start
    	; Wait for Input to go low before continuing
    	btfsc 	GPIO,IO_SERVO 	; Test Servo Input
    	goto 	Start 		; Loop if not clear
    	call 	ResetTimer2 	; Clear out all timer variables
    
    WaitingForHigh
    	call 	CheckForTic 	; Accumulate Ticks
    	btfss 	GPIO,IO_SERVO 	; Test Servo Input
    	goto 	WaitingForHigh 	; Loop if not Set
    PositiveEdge
    	call 	ResetTimer2 	; Resynch timer2 & related variables
    WaitingForLow
    	call 	CheckForTic 	; Get in synch with Timer 2
    	btfsc 	GPIO,IO_SERVO 	; Test Servo Input
    	goto 	WaitingForLow 	; Loop if not Clear
    
    NegativeEdge
    
    
    Next the program determines if the pulse count indicates forward, reverse, or stop. 
    Based on this determination the F_Reverse or F_Forward is set (or neither for a stop) and the SetPulseLength function is called to calculate the Motor Drive Pulse width. 
    GetDirection
    
    	; First check to see if we are in reverse
    	movlw 	REV_THRESH 	; Set the Reverse threshold
    	subwf 	ICntLSB,F 	; Subtract Thresh from Pulse time
    	btfss 	ICntLSB,7 	; Is the result negative?
    	goto 	NotRev 		; No, Try the next threshold
    
    InReverse
    	; Calculate 1 to X timer count. Current value = -1 to -X
    	comf 	ICntLSB,F 	; Negate Count (Complement and add 1)
    	incf 	ICntLSB,F 	; Val is now 1 to REV_THRESH .
    
    	; Signal drive Reverse
    	call 	SetPulseLength 	; Convert count to pulse length
    	bcf 	Flags,F_Forward
    	bsf 	Flags,F_Reverse
    	goto 	DirDone
    
    NotRev
    	; Now check to see if we are in Neutral
    	decf 	ICntLSB,F 	; Decrement Pulse time for Neutral gap
    	btfss 	ICntLSB,7 	; Is the result negative?
    	goto 	InForward 	; No, Must be forward
    
    InNeutral
    	; Signal Neutral
    	bcf 	Flags,F_Reverse
    	bcf 	Flags,F_Forward
    	clrf 	Length
    	goto 	DirDone
    
    InForward
    	; Signal drive Forward
    	incf 	ICntLSB,F 	; Start range at 1 not o
    	call 	SetPulseLength 	; Convert count to pulse length
    	bcf 	Flags,F_Reverse
    	bsf 	Flags,F_Forward
    	goto 	DirDone
    
    DirDone
    	call 	ClearTimer2 	; Clear out all timer variables
    
    PulseComplete
    	goto WaitingForHigh 	; Repeat loop looking for next pulse
    ; ---------------------------------------------------------
    SetPulseLength
    	; Assumes iCntLSB has a tick count of 1 to some number
    	; Clip the number to 10 and then set the Pulse length
    	movf 	ICntLSB,W 	; Make temp. copy of Count
    	movwf 	Temp 		;
    	movlw 	MAX_RANGE 	; Compare with MAX_RANGE
    	subwf 	Temp,F 		;
    	btfss 	Temp,7 		; If Negative, do nothing
    	movwf 	ICntLSB 	; otherwise, Set count to Max Range
    
    	; Calculate pulse lengh by multiplying count by 25 (16+8+1)
    	movf 	ICntLSB,W 	; Make temp copy of Count
    	movwf 	Temp 		; W starts out with Count
    	rlf 	Temp,F 		; Multiply Count by 8 (shift 3 bits)
    	rlf 	Temp,F
    	rlf 	Temp,F
    	addwf 	Temp,F 		; Accumulate in W
    	rlf 	Temp,F 		; Multiply Count by 16 (one more shift)
    	addwf 	Temp,W 		; Accumulate in W
    	movwf 	Length 		; Save in pulse length
    	return
    
    

    The final part of the program is actually the CheckForTic function which not only increments the ICntLSB but it also runs the Motor Drive PWM output.  Every time the CycleTimer wraps back to zero, we set the appropriate Motor Drive output bit high, and then wait for the pre-calculated number of clock ticks to go by before we drop it back to low. 

    ; ---------------------------------------------------------
    CheckForTic
    	btfss 	PIR1,TMR2IF 	; Check Timer2 interrupt bit
    	goto 	CFT_Done 	; Exit if not set
    
    IsTick
    	bcf 	PIR1,TMR2IF 	; Clear Timer Interrupt
    	incfsz 	ICntLSB,F 	; Inc 2 byte Input Counter
    	goto 	PulseCheck
    	incf 	ICntMSB,F
    	btfss 	ICntMSB,3 	; Have we reached 2048? (100ms)
    	goto 	PulseCheck 	; Nope, OK, run pulse code
    
    	; Servo Timeout
    	bcf 	Flags,F_Reverse ; Set output to Neutral
    	bcf 	Flags,F_Forward
    	clrf 	Length
    
    PulseCheck
    	;Look for end of cycle
    	decfsz 	CycleTimer,F 	; Has Cycle timer wrapped
    	goto 	PulseEndCheck
    
    ; Start of New Cycle, Start next pulse
    NewCycle
    	movf 	Length,W 	; Put pulse length in PulseTimer
    	movwf 	PulseTimer
    
    CheckForward
    	btfss 	Flags,F_Forward ; Going Forward?
    	goto 	CheckReverse
    SetForward
    	movlw 	IO_REV_OFF 	; Set Reverse Bits off
    	andwf 	GPIO,F 		;
    	movlw 	IO_FWD_ON 	; Set Forward Bits on
    	iorwf 	GPIO,F 		;
    	goto 	CFT_Done
    
    CheckReverse
    	btfss 	Flags,F_Reverse ; Skip if going Forward
    	goto 	SetNeutral
    SetReverse
    	movlw 	IO_FWD_OFF 	; Set Forward Bits off
    	andwf 	GPIO,F 		;
    	movlw 	IO_REV_ON 	; Set Reverse Bits on
    	iorwf 	GPIO,F 		;
    	goto 	CFT_Done
    
    SetNeutral
    	movlw 	IO_FWD_OFF 	; Set Forward Bits off
    	andwf 	GPIO,F 		;
    	movlw 	IO_REV_OFF 	; Set Reverse Bits off
    	andwf 	GPIO,F 		;
    	goto 	CFT_Done
    
    PulseEndCheck
    	; Look to see if it's time to end the pulse
    	movf 	PulseTimer,F 	; Get pulse time and check for zero
    	btfsc 	STATUS,Z 	; Is pulse still running?
    	goto 	CFT_Done 	; No, goto end
    	decfsz 	PulseTimer,F 	; Count down Pulse
    	goto 	CFT_Done
    
    	; Pulse has ended so clear all drive bits
    	movlw 	IO_FWD_OFF 	; Set Forward Bits off
    	andwf 	GPIO,F 		;
    	movlw 	IO_REV_OFF 	; Set Reverse Bits off
    	andwf 	GPIO,F 		;
    
    CFT_Done
    	return
    
    
    The final result seems to work pretty well.  I haven't had a chance to test it under load yet, so I don't know what the torque is like, but you can clearly hear the 10 different speed steps in each direction.  I'll post test results once I have two units modified.

     

    Web content is copyright © PhilBot.com 2005, Deep Creek Lake, MD.
    Contact: Phil Malone 301.387.2331, webmaster@PhilBot.com