Saturday, June 12, 2010

Simplified State Machine - Hexapod (Part 4)

The full source code can be found here.

Now that I'm looking at control 3 servos in the one routine I want the routine to be as simple as possible so its easier for my brain and so the timing is more stable. In this version I have combined the servoMinimumDelay state and the servoPositionDelay state. I didn't so this intially because I wanted to be able to use one byte for the position. If I added the minimum delay to the position I would, in some cases, need two bytes. E.G. Maximum position = 0xEE and minimum position = 0x13, add those two numbers together and you get 0x100 or 256.

After thinking about it a bit more I realised the solution was quite simple. I load the timer0 high bit with 0xFF. Then add the minimum delay to the position. If the carry flag is set I decrement the timer0 high bit. The low bit is loaded with 0xFF - position.

servoPositionDelay
    movlw 0xFF
    movwf TMR0H

    ;add position to minimum to get total
    movf SERVO1POS,w
    addwf SERVO1MIN,w
    ;if the carry is set, decrement the high byte for timer 0
    btfsc STATUS, C
        decf TMR0H, f

    sublw 0xFF
    movwf TMR0L
    goto ENDISR

I did notice a bit of jitter that I'll have to track down but its not a concern at this point. The MPLab simulator is very useful for checking the timing delays so shouldn't be too difficult

Servo Position With State Machine (Hexapod Part3)


The full source code can be found here
The interesting part is the Interrupt Service Routine (ISR). In the previous example I wasn't using a prescalar but in this one I am. I'm using a 1:8 prescalar which means the timer is increased at an eighth the rate it used to be. this allows the full movement range of the servo to be expressed in one byte.

The ISR implements a 4 state state machine. The states are
  • initialDelay - this state turns on the servo and pauses for 0.25ms. This is a absolute shorted pulse that will ever be needed
  • servoMinimumDelay - this state pauses for the minimum position of the servo. The SERVO1MIN variable is set to 13 for the Futaba servo.  In my last post I discovered that the position I want to be the 0 position requires a pulse of 0.4ms. We have already delayed by 0.25ms in the previous state so we now need to delay by 0.15 ms to complete a 0.4ms delay. Delaying for 0x13 (19) timer ticks, with the prescalar set to 1:8 means we will delay for 19 * (0.01us * 8) = 0.152ms
  • servoPositionDelay - the delay based on the required position of the servo. The position is subtracted from 0xFF because the timer triggers when it overflows (reaches 0xFF+1 or 256) . I could miss this line if I made position 0 the max position but having the subtraction means the minimum position is also the minimum delay. The only gotcha in this state is that if we are in position 0 we want to jump to the next state without waiting for the timer to expire
  • operationalDelay - this state turns the pulse to the servo off and then sets the timer to delay for 18ms.
You'll also see the  the line decf    SERVO1POS, f which will make the servo move one position per ISR call so it takes a few seconds to move across its full range. The position then underflows which puts it back to it maximum position.

So far I haven't taken into account the time it takes to execute the ISR code. When the ISR is longer I'll probably have to modify the timers by a step or two to avoid jitter on the pulse but we'll see how it goes


...
...
ISR
    ;clear the interrupt
    bcf     INTCON, TMR0IF
    bcf     T0CON, TMR0ON    ;disable timer
  
    ;which state?
    movf    CONTROL_STATE, w
    addwf    PCL, f
    nop
    nop
    ;delay for 0.25ms as the minimum position of any servo
    goto     initialDelay  
    ;delay for the minimum for a selected servo
    goto     servoMinimumDelay
    ;delay for the position of the servo
    goto     servoPositionDelay
    ;delay for 18ms so we can do other stuff  
    goto    operationalDelay   

initialDelay
    movlw    0xFF
    movwf    TMR0H
    movlw    0xE1            ;256-(250us/8) = 256-31 - 225 = 0xE1
    movwf    TMR0L

    bsf        PORTB, RB1        ;turn on the servo control bit

    goto    ENDISR

servoMinimumDelay
    movlw    0xFF
    movwf    TMR0H            ;load high timer
  
    movf    SERVO1MIN , w    ;load min position
    sublw    0xFF            ;prepare low byte for timer

    movwf    TMR0L            ;load min delay into timer

    goto     ENDISR
  
servoPositionDelay
    movf    SERVO1POS,w
    bz        operationalDelay    ;if we're at pos=0 don't delay
    movf    SERVO1POS,w
    sublw    0xFF
    movwf    TMR0L
    goto     ENDISR  

operationalDelay            ;delay of 18ms that lets other things happen
    bcf        PORTB, RB1        ;turn off the servo control bit
    ;wait 18
    movlw     0xF7            ;F7 is the high byte of the timer offset
    movwf    TMR0H            ;write to the high byte first because the actual write occurs when the low byte is written
    movlw    0x2E            ;36S is the low byte of the timer offset
    movwf     TMR0L            ;The high and low bytes are now written

    movlw    0x00            ;reset state machine (0x00 because it will be incremented at the end
    movwf    CONTROL_STATE    ;
  
    decf    SERVO1POS, f    ;move the servo one step (for testing only)

ENDISR
    incf CONTROL_STATE, f   
    incf CONTROL_STATE, f   
    incf CONTROL_STATE, f   
    incf CONTROL_STATE, f    ;move to the next state
    bsf     T0CON, TMR0ON    ;enable timer, we're good to go
    RETFIE

Wednesday, June 2, 2010

PIC18, timers and servos (Hexapod part 2)

I've obtained a PICDEM 2 Plus demo board. It provides some standard hardware interfaced with a PIC18F542 and an in circuit programmer and debugger. The hardware includes an lcd screen, an I2C thermometer, a variable resistor wired to an analog-to-digital converter, a serial port and some buttons. There is an on board 4Mhz crystal for clocking the processor and a voltage regulator for a clean power supply.

This board saves me trying to understand the hardware and the software at the same time which is great. I'll write a basic version of the software I want and then build some hardware for it to run on.

The first task I set myself was to move a servo to a hard coded position and then maintain it. This didn't prove too difficult. I then found the limits of the servos movements using a multimeter to measure current. When the servo is at rest it consumes about 9ma. If the servo is not moving and it is consuming more than 9ma then it is up against the stop and I have sent it a pulse that is either too long or too short for its range. Once I found the limits I found the pulses required to move the servo through 180°.

A pulse of 1.5ms is the defacto standard for the center position. 1ms is 0° and 2ms is 90°. My servo, Futaba FP-S28, can move through about 190-200°. Using the method described above I found the absolute limits were 0.25ms to 2.4ms however the min and max for 180° movement were 0.4ms to 2.3ms.

The below code uses Timer0 to send a pulse of the desired length and then wait 18ms before sending the same pulse again. the servo signal wire is connected to RB1.


    list p=18f452
    INCLUDE "P18F452.inc"

;code protect disabled
    CONFIG     CP0=OFF
;Oscillator switch enabled, RC oscillator with OSC2 as I/O pin.
    CONFIG     OSCS=ON, OSC=HS
;Brown-OutReset enabled, BOR Voltage is 2.5v
    CONFIG     BOR=ON, BORV=25
;Watch Dog Timer disabled
    CONFIG     WDT=OFF


    ORG 0x00
        GOTO MAIN

       ORG 0x08 ;INTERRUPT VECTOR
           GOTO ISR ;INTERRUPT SERVICE ROUTINE
;================================================

ISR
    ;clear the interrupt
    bcf     INTCON, TMR0IF

    ;is the output already on?
    btfsc    PORTB, RB1
    ;no, it is not   
    goto    turnServoOff
    ;yes it is       
turnServoOn
    ;turn on the servo control bit                   
    bsf        PORTB, RB1           
    ;wait 0.4ms
    ;F7 is the high byte of the timer offset
    movlw     0xF7
    ;write to the high byte first because the actual write occurs when the low byte is written           
    movwf    TMR0H       
    ;00 is the low byte of the timer offset
    movlw    0x00         
    ;The high and low bytes are now written
    movwf     TMR0L          
    ;we're done, get back to what we were doing before the interrupt
    goto ENDISR

turnServoOff
    ;turn on the servo control bit
    bcf        PORTB, RB1           
    ;wait 18ms
    ;B9 is the high byte of the timer offset
    movlw     0xB9       
    ;write to the high byte first because the actual write occurs when the low byte is written
    movwf    TMR0H       
    ;B1 is the low byte of the timer offset
    movlw    0xB1       
    ;The high and low bytes are now written
    movwf     TMR0L           

ENDISR
    RETFIE
;================================================

MAIN
    ;init port b
    ;zero everything (trun it all off)
    clrf PORTB           
    ;clear bit 1 to make RB1 output
    bcf    TRISB , RB1       

    ;setup tmr0
    ;disable the timer until we have set it up
    bcf        T0CON, TMR0ON   
    ;turn the prescalar off
    bsf     T0CON, PSA       
    ;internal clock source (use the program counter clock rather than an external pin)
    bcf     T0CON, T0CS
    ;set to 16 bit mode
    bcf     T0CON, T08BIT   

    ;B9 is the high byte of the timer offset
    movlw     0xB9
    ;write to the high byte first because the actual write occurs when the low byte is written
    movwf    TMR0H           
    ;B1 is the low byte of the timer offset
    movlw    0xB1            
    ;The high and low bytes are now written
    movwf     TMR0L          

    ;enable tmr0 interrupt
    bsf     INTCON, TMR0IE   
    ;enable all interrupts
    bsf     INTCON, GIE   
    ;enable timer, we're good to go
    bsf     T0CON, TMR0ON   
    ;do nothing until interrupted
    goto $                   


;use TMR0 with no prescalar
;18ms is 18000 timer ticks so 65536-18000=47537 which is B9B1
;so B9B1 is the value to preload tmr0 with

;0.5ms is 50 timer ticks so 65536-500=65036 which is FE0C
;1ms is 1000 timer ticks so 65536-1000=64536 which is FC18
;2ms is 2000 timer ticks so 65536-2000=63536 which is F830
;2.5ms is 2500 timer ticks so 65536-2500=63036 which is F63C

    END


Changing the values in the turnServoOn routine will cause the servo to move to another position. The limit positions are laid out in the table at the end of the code.

Next step is to manage more than 1 servo, probably 3 since thats how many I have to play with.