Continue to Site

Welcome to our site!

Electro Tech is an online community (with over 170,000 members) who enjoy talking about and building electronic circuits, projects and gadgets. To participate you need to register. Registration is free. Click here to register now.

  • Welcome to our site! Electro Tech is an online community (with over 170,000 members) who enjoy talking about and building electronic circuits, projects and gadgets. To participate you need to register. Registration is free. Click here to register now.

Drive 33 Servos With One PIC + USART

Status
Not open for further replies.

wschroeder

New Member
Yes, you can drive 33 servos with 256 steps of resolution with a single PIC18 (and PIC16 is possible too). Also, this program supports USART with standard baudrates up to 115K. The program presently uses RX only and you must send 33 bytes per frame. Also, there must be more than 20ms between the start of each servo update frame.

To achieve 33 outputs you must the use of the internal oscillator, which is 32MHz, 8MHz x 4PLL. There are several PIC18's that will work easily with this code, and some very inexpensive (PIC18F4510). The program is about 750 words compiled using mikroBasic 6.0, which is well under the 2K limitation of the free demo version of the compiler.

I will be adding the AutoBaud feature of the EUSART at a later time to this code. This is a great feature considering the possible variances of frequency using the internal oscillator.

I almost forgot to say what I consider a most important feature. You can use up to 5 different types of servos, such as those with different start delays. This makes this program extremely versatile for a complex servo project that requires servos from different manufacturers. At the beginning of the program you can set each port to drive a specified time frame, albeit with the assumption that 1500us is a standard for center.

Enjoy!


Code:
program SERVO33_USART
'***********************************************************************************
'***********************************************************************************
'              SERVO_33 SINGLE PIC SERVO DRIVER WITH USART TO 115Kbaud             *
'                        Warren Schroeder   December 6, 2007                       *
'            Program tested using PIC18F4525 @ 32MHz (Internal Osc + PLL)          *
'               Source Code is mikroBasic 6.0 (Demo Version will work)             *
'                                                                                  *
'    This code will manage 33 50Hz Servos @ 8-bit resolution.  Servo positions     *
'    can be updated with USART communication up to 115KBaud.  33 outputs are       *
'    available by using the internal oscillator feature of this PIC and the x4     *
'    PLL feature.  Only USART RX is required to use this driver program, but       *
'    TX is available to the user if needed.  Only a single PIC is needed!          *
'***********************************************************************************
'***********************************************************************************
'
'    These are the port pin assignments for the servo array:
'            servo[0] - PORTA.0
'            servo[1] - PORTA.1
'            servo[2] - PORTA.2
'            servo[3] - PORTA.3
'            servo[4] - PORTA.4
'            servo[5] - PORTA.5
'            servo[8] - PORTA.6
'            servo[7] - PORTA.7
'            servo[8] - PORTB.0
'            servo[9] - PORTB.1
'            servo[10]- PORTB.2
'            servo[11]- PORTB.3
'            servo[12]- PORTB.4
'            servo[13]- PORTB.5
'            servo[14]- PORTB.6
'            servo[15]- PORTB.7
'            servo[16]- PORTC.0
'            servo[17]- PORTC.1
'            servo[18]- PORTC.2
'            servo[19]- PORTC.3
'            servo[20]- PORTC.4
'            servo[21]- PORTC.5
'                       PORTC.6 = USART TX
'                       PORTC.7 = USART RX
'            servo[22]- PORTD.0
'            servo[23]- PORTD.1
'            servo[24]- PORTD.2
'            servo[25]- PORTD.3
'            servo[26]- PORTD.4
'            servo[27]- PORTD.5
'            servo[28]- PORTD.6
'            servo[29]- PORTD.7
'            servo[30]- PORTE.0
'            servo[31]- PORTE.1
'            servo[32]- PORTE.2
'                       PORTE.3 = MCLR or Input Only
'***********************************************************************************

symbol RXFlag = PIR1.RCIF
symbol T0Flag = INTCON.TMR0IF
symbol T1Flag = PIR1.TMR1IF
symbol T1Run  = T1CON.TMR1ON

'***********************************************************************************
' Each port can be customized for initial start delay and 1/256 timing
'***********************************************************************************
const
    T1_20      as word = 65536-40000+6     ' 20ms + timer1 stop compensation
    portAdelay as word = 600 * 8           ' in microseconds * 8
    portBdelay as word = 700 * 8
    portCdelay as word = 800 * 8
    portDdelay as word = 900 * 8
    portEdelay as word = 1000 * 8
    portAcycle as byte = (12000-portAdelay) >> 7  ' in microseconds * 8
    portBcycle as byte = (12000-portBdelay) >> 7
    portCcycle as byte = (12000-portCdelay) >> 7
    portDcycle as byte = (12000-portDdelay) >> 7
    portEcycle as byte = (12000-portEdelay) >> 7

'***********************************************************************************
' Variable Declarations
'***********************************************************************************
dim
    T0          as word     absolute $FD6  ' address of TMR0L
    T1          as word     absolute $FCE  ' address of TMR1L
    FSR_0       as word     absolute $FE9  ' address of FSR0L
    FSR_1       as word     absolute $FE1  ' address of FSR1L
    FSR_2       as word     absolute $FD9  ' address of FSR2L
    servo       as byte[33] absolute $15   ' access ram & past system vars
    rxbuffer    as byte[33]                ' usart rx array
    endofbuffer as word
    loops       as byte

'***********************************************************************************
' Interrupt Service Routine
'***********************************************************************************
sub procedure interrupt

'**************
'    PORTA
'**************
    LATA = Not(LATA)                       ' turn on all servos on PortA (8)
    loops = 0                              ' 256 counts.. rollover to original value
    TMR0H = Not(Hi(portAdelay))
    TMR0L = Not(portAdelay)                ' initial delay
    T0FLag = 0                             ' clear overflow flag
          ASM
    delayAloop:
             btfsc     PIR1, RCIF,0        ' check USART RX buffer
             movff     RCREG, POSTINC2     ' move RX byte into servobuffer
             btfss     INTCON, TMR0IF,0    ' check for overflow
             bra       delayAloop
    loopAtimeout:
             btfss     INTCON, TMR0IF      ' wait for 1/256 cycle to finsih
             bra       loopAtimeout
          End ASM
    TMR0H = Not(Hi(portAcycle))
    TMR0L = Not(portAcycle)                ' 1/256 cycle time
    T0Flag = 0
          ASM
    portAmask:                             ' VERY efficient PWM masking routine
             movlw     0
             decfsz    _servo+0, 1,0
             iorlw     1
             decfsz    _servo+1, 1,0
             iorlw     2
             decfsz    _servo+2, 1,0
             iorlw     4
             decfsz    _servo+3, 1,0
             iorlw     8
             decfsz    _servo+4, 1,0
             iorlw     16
             decfsz    _servo+5, 1,0
             iorlw     32
             decfsz    _servo+6, 1,0
             iorlw     64
             decfsz    _servo+7, 1,0
             iorlw     128
             andwf     LATA, 1,0
             btfsc     PIR1, RCIF,0        ' check USART RX buffer
             movff     RCREG, POSTINC2     ' move RX byte into servobuffer
             decfsz    _loops, 1,0         ' 256 loops yet?
             bra       loopAtimeout
          End ASM

'**************
'    PORTB
'**************
    LATB = Not(LATB)                       ' turn on all servos on PortB (8)
    loops = 0                              ' 256 counts.. rollover to original value
    TMR0H = Not(Hi(portBdelay))
    TMR0L = Not(portBdelay)                ' initial delay
    T0Flag = 0                             ' clear overflow flag
          ASM
    delayBloop:
             btfsc     PIR1, RCIF,0        ' check USART RX buffer
             movff     RCREG, POSTINC2     ' move RX byte into servobuffer
             btfss     INTCON, TMR0IF,0    ' check for overflow
             bra       delayBloop
    loopBtimeout:
             btfss     INTCON, TMR0IF,0    ' wait for 1/256 cycle to finsih
             bra       loopBtimeout
          End ASM
    TMR0H = Not(Hi(portBcycle))
    TMR0L = Not(portBcycle)                ' 1/256 cycle time
    T0Flag = 0
          ASM
    portBmask:                             ' VERY efficient PWM masking routine
             movlw     0
             decfsz    _servo+8, 1,0
             iorlw     1
             decfsz    _servo+9, 1,0
             iorlw     2
             decfsz    _servo+10, 1,0
             iorlw     4
             decfsz    _servo+11, 1,0
             iorlw     8
             decfsz    _servo+12, 1,0
             iorlw     16
             decfsz    _servo+13, 1,0
             iorlw     32
             decfsz    _servo+14, 1,0
             iorlw     64
             decfsz    _servo+15, 1,0
             iorlw     128
             andwf     LATB, 1,0
             btfsc     PIR1, RCIF,0        ' check USART RX buffer
             movff     RCREG, POSTINC2     ' move RX byte into servobuffer
             decfsz    _loops, 1,0         ' 256 loops yet?
             bra       loopBtimeout
          End ASM

'**************
'    PORTC
'**************
    LATC = Not(LATC)                       ' turn on all servos on PORTC (6)
    loops = 0                              ' 256 counts.. rollover to original value
    TMR0H = Not(Hi(portCdelay))
    TMR0L = Not(portCdelay)                ' initial delay
    T0Flag = 0                             ' clear overflow flag
          ASM
    delayCloop:
             btfsc     PIR1, RCIF,0        ' check USART RX buffer
             movff     RCREG, POSTINC2     ' move RX byte into servobuffer
             btfss     INTCON, TMR0IF,0    ' check for overflow
             bra       delayCloop
    loopCtimeout:
             btfss     INTCON, TMR0IF,0    ' wait for 1/256 cycle to finsih
             bra       loopCtimeout
          End ASM
    TMR0H = Not(Hi(portCcycle))
    TMR0L = Not(portCcycle)                ' 1/256 cycle time
    T0Flag = 0
          ASM
    portCmask:                             ' VERY efficient PWM masking routine
             movlw     192                 ' mask out RX and TX
             decfsz    _servo+16, 1,0
             iorlw     1
             decfsz    _servo+17, 1,0
             iorlw     2
             decfsz    _servo+18, 1,0
             iorlw     4
             decfsz    _servo+19, 1,0
             iorlw     8
             decfsz    _servo+20, 1,0
             iorlw     16
             decfsz    _servo+21, 1,0
             iorlw     32
             andwf     LATC, 1,0
             btfsc     PIR1, RCIF,0        ' check USART RX buffer
             movff     RCREG, POSTINC2     ' move RX byte into servobuffer
             decfsz    _loops, 1,0         ' 256 loops yet?
             bra       loopCtimeout
          End ASM

'**************
'    PORTD
'**************
    LATD = Not(LATD)                       ' turn on all servos on PortD (8)
    loops = 0                              ' 256 counts.. rollover to original value
    TMR0H = Not(Hi(portDdelay))
    TMR0L = Not(portDdelay)                ' initial delay
    T0Flag = 0                             ' clear overflow flag
          ASM
    delayDloop:
             btfsc     PIR1, RCIF,0        ' check USART RX buffer
             movff     RCREG, POSTINC2     ' move RX byte into servobuffer
             btfss     INTCON, TMR0IF,0    ' check for overflow
             bra       delayDloop
    loopDtimeout:
             btfss     INTCON, TMR0IF,0    ' wait for 1/256 cycle to finsih
             bra       loopDtimeout
          End ASM
    TMR0H = Not(Hi(portDcycle))
    TMR0L = Not(portDcycle)                ' 1/256 cycle time
    T0Flag = 0
          ASM
    portDmask:                             ' VERY efficient PWM masking routine
             movlw     0
             decfsz    _servo+22, 1,0
             iorlw     1
             decfsz    _servo+23, 1,0
             iorlw     2
             decfsz    _servo+24, 1,0
             iorlw     4
             decfsz    _servo+25, 1,0
             iorlw     8
             decfsz    _servo+26, 1,0
             iorlw     16
             decfsz    _servo+27, 1,0
             iorlw     32
             decfsz    _servo+28, 1,0
             iorlw     64
             decfsz    _servo+29, 1,0
             iorlw     128
             andwf     LATD, 1,0
             btfsc     PIR1, RCIF,0        ' check USART RX buffer
             movff     RCREG, POSTINC2     ' move RX byte into servobuffer
             decfsz    _loops, 1,0         ' 256 loops yet?
             bra       loopDtimeout
          End ASM

'**************
'    PORTE
'**************
    LATE = Not(LATE)                       ' turn on all servos on PORTE (3)
    loops = 0                              ' 256 counts.. rollover to original value
    TMR0H = Not(Hi(portEdelay))
    TMR0L = Not(portEdelay)                ' initial delay
    T0Flag = 0                             ' clear overflow flag
          ASM
    delayEloop:
             btfsc     PIR1, RCIF,0        ' check USART RX buffer
             movff     RCREG, POSTINC2     ' move RX byte into servobuffer
             btfss     INTCON, TMR0IF,0    ' check for overflow
             bra       delayEloop
    loopEtimeout:
             btfss     INTCON, TMR0IF,0    ' wait for 1/256 cycle to finsih
             bra       loopEtimeout
          End ASM
    TMR0H = Not(Hi(portEcycle))
    TMR0L = Not(portEcycle)                ' 1/256 cycle time
    T0Flag = 0
          ASM
    portEmask:                             ' VERY efficient PWM masking routine
             movlw     0
             decfsz    _servo+30, 1,0
             iorlw     1
             decfsz    _servo+31, 1,0
             iorlw     2
             decfsz    _servo+32, 1,0
             iorlw     4
             andwf     LATE, 1,0
             btfsc     PIR1, RCIF,0        ' check USART RX buffer
             movff     RCREG, POSTINC2     ' move RX byte into servobuffer
             decfsz    _loops, 1,0         ' 256 loops yet?
             bra       loopEtimeout
          End ASM
          
    T1Run = 0                              ' stop timer1
    T1 = T1 + T1_20                        ' reload timer1
    T1Run = 1                              ' restart timer1
    T1Flag = 0                             ' clear interrupt flag

end sub

'***********************************************************************************
' Initialize Ports, Timers, and Other Registers
'***********************************************************************************
sub procedure general_setup
   LATA = 0
   LATB = 0
   LATC = 0
   LATD = 0
   LATE = 0
   TRISA = 0
   TRISB = 0
   TRISC = 0
   TRISD = 0
   TRISE = 0
   ADCON1 = 15                             ' disable ADC's
   CMCON = 7                               ' disable Comparators
   endofbuffer = @rxbuffer + 33
   FSR0ptr = @servo
   While FSR0ptr < (@servo + 33)
      POSTINC0 = 128                         ' initialize all servo values to 0 position
   Wend
end sub

sub procedure timers_setup
   INTCON = 192                            ' enable GIE & PEIE
   T1CON = 32                              ' prescaler=4, timer off
   T1 = 0                                  ' clear Timer1
   PIE1.TMR1IE = 1                         ' enable Timer1 Interrupt
   PIR1.TMR1IF = 0                         ' clear Timer1 Interrupt Flag
   T1CON.TMR1ON = 1                        ' start Timer1
   T0CON = 8                               ' no prescaler
   T0CON.TMR0ON = 1                        ' start Timer0
end sub

'***********************************************************************************
' Main Program
'***********************************************************************************
main:

   While OSCCON.OSTS = 0                   ' wait for oscillator startup flag
   Wend
   OSCCON = $70                            ' internal primary oscillator @ 8MHz
   OSCTUNE.6 = 1                           ' enable x4 PLL for internal oscillator
   general_setup
   timers_setup
   USART_Init(115200)                      ' any baudrate up to 115K is allowed

   FSR_2 = @rxbuffer
   While true
      While FSR_2 < endofbuffer            ' FSR2 is RX servobuffer
         While RXFlag = 0                  ' wait for new RX byte
         Wend
         While TMR1H = 255                 ' avoid trasfer if close to interrupt
         Wend                              ' max possible delay is 256*.5us= 128us
         If RXFlag = 1 Then
            POSTINC2 = RCREG               ' save RX byte in array
         End If
      Wend                                 ' when 30 RX bytes move on
      FSR_2 = @rxbuffer                    ' reset RX buffer
      FSR_1 = @rxbuffer                    ' setup tranfer of RX array to
      FSR_0 = @servo                       ' servo work array
      While FSR_1 < endofbuffer
         POSTINC0 = inc(POSTINC1)          ' transfer RX buffer to work values.. add 1
      Wend
   Wend

end.
 
Very nice Warren. An elegant solution.

Would you care to review or critique a 'method' I came up with yesterday? The whole ISR 'engine' is something like 35 instructions. Here's the feature list;

Code:
;******************************************************************
;                                                                 *
;   K8LH Hi-Performance 33 Channel Soft Servo Controller Method   *
;   ===========================================================   *
;       (C) Copyright 2007, Micro Applications Consultants        *
;                                                                 *
;  <>  up to 115.2 kbaud serial input, 256 byte circular buffer   *
;  <>  single or multi channel serial update command structure    *
;  <>  33 pwm "Servo" and or digital "on/off" channel outputs     *
;  <>  Servo pulse widths of 1 to 2559 usecs in 1 usec steps      *
;  <>  a channel width value of 0 is a digital '0' output         *
;  <>  a channel width value of 2560 is a digital '1' output      *
;  <>  only 1 cycle period jitter (125 ns, interrupt latency)     *
;  <>  zero pulse width jitter (no pulse timing variations)       *
;                                                                 *
;  4.0 msec Timer 2 interrupts provide five 8-bit output group    *
;  intervals per 20 msec servo period (32 MHz clock).  Not all    *
;  output bits are used in all output groups.                     *
;                                                                 *
;  ch   group 0   out   ch   group 1   out   ch   group 2   out   *
;  ------------------   ------------------   ------------------   *
;  00  Servo[0,0] RA0   08  Servo[1,0] RB0   16  Servo[2,0] RC0   *
;  01  Servo[0,1] RA1   09  Servo[1,1] RB1   17  Servo[2,1] RC1   *
;  02  Servo[0,2] RA2   10  Servo[1,2] RB2   18  Servo[2,2] RC2   *
;  03  Servo[0,3] RA3   11  Servo[1,3] RB3   19  Servo[2,3] RC3   *
;  04  Servo[0,4] RA4   12  Servo[1,4] RB4   20  Servo[2,4] RC4   *
;  05  Servo[0,5] RA5   13  Servo[1,5] RB5   21  Servo[2,5] RC5   *
;  06  Servo[0,6] RA6   14  Servo[1,6] RB6   --  Servo[2,6] ---   *
;  07  Servo[0,7] RA7   15  Servo[1,7] RB7   --  Servo[2,7] ---   *
;                                                                 *
;  ch   group 3   out   ch   group 4   out                        *
;  ------------------   ------------------                        *
;  22  Servo[3,0] RD0   30  Servo[4,0] RE0                        *
;  23  Servo[3,1] RD1   31  Servo[4,1] RE1                        *
;  24  Servo[3,2] RD2   32  Servo[4,2] RE2                        *
;  25  Servo[3,3] RD3   --  Servo[4,3] ---                        *
;  26  Servo[3,4] RD4   --  Servo[4,4] ---                        *
;  27  Servo[3,5] RD5   --  Servo[4,5] ---                        *
;  28  Servo[3,6] RD6   --  Servo[4,6] ---                        *
;  29  Servo[3,7] RD7   --  Servo[4,7] ---                        *
;                                                                 *
;                                                                 *
;  ISR 41 words, 20569 cycles, 2572 usecs, 64.3% mcu 'overhead'   *
;                                                                 *
 
Last edited:
Mike,

Your other code certainly demonstrates your mastery of the time-slice. So I have little doubt that the code behind those specs will fall short of your capability.

From your description I am not clear on something. Does each servo have a 2 byte value, or are you updating with just a single byte?
 
The Servo[] array is type INT (word) and each element holds a 16 bit number in the range of 0 through 2560, inclusive...

The "output group update" code uses byte size data to update the output port at 1 usec intervals from interval 0 through interval 2559...
 
Last edited:
Nope, I'm not using vertical counters.

Basically I update one 'group' output port every 4.0 msec interrupt cycle at 1 usec intervals from a 2560 byte RAM array. Since the array element index is directly proportional to the 1 usec timing interval, it's super fast and eazy to place 8 servo "off" bits into the array using each servo pulse width value as the array index.

I've attached the assembly language ISR "engine" (only 35 instructions) and a pseudo BASIC code example of the algorithm. Please tell me if it makes sense?

Mike


Code:
;
;  pseudo BASIC algorithm example    (C) 2007, K8LH
;
;  Dim n As Byte                ' index
;  Dim group As Byte            ' port group, 0..4
;  Dim Servo(5,8) As Word       ' five 8 bit port groups
;                               ' element values of 0..2560
;  Dim WBuff(2561) As Byte      ' work buffer
;
;  Interrupt example            ' 4.0 msec interrupts
;    PIR1.TMR2IF = 0            ' clear Timer 2 interrupt flag
;
;    Mask = %00000001           ' servo channel bit mask
;    For n = 0 To 7             ' add 8 servo "off" bits to buffer
;      WBuff(Servo(group,n)) = WBuff(Servo(group,n)) Or Mask
;      Mask = Mask << 1         ' shift bit for next channel
;    Next
;
;    INDF1 = @LATA + group      ' indirect access to group port
;    group = (group + 1) Mod 5  ' bump group for next interrupt
;
;    WBuff(0) = WBuff(0) Xor (Not INDF1)   ' prep 1st XOR value
;
;    For n = 0 to 2559          ' exactly 1 usec iterrations
;      INDF1 = INDF1 ^ WBuff(n) ' 1 usec output update step
;      WBuff(n) = 0             ' zero buffer after using it
;      RxCheck                  ' cache incoming Rx character
;    Next                       '
;  End Interrupt
;
Code:
;--< variables >---------------------------------------------------

Group   equ     0x000           ; u08, values of 0..4
GetPtr  equ     0x001           ; u16, circular buf 'get' pointer

WBuff   equ     0x100           ; u08, WBuff[0..2559], 100..AFF
OnBits  equ     0xB00           ; u08, WBuff[2560] 1 byte
Servo   equ     0xB80           ; u16, Servo[5,8], 80 bytes...BCF
RBuff   equ     0xC00           ; Rx buffer, C00..CFF

WSize   equ     WBuff+d'2560'   ; work buffer end address

;                                                                 *
;  ISR 41 words, 20569 cycles, 2572 usecs, 64.3% mcu 'overhead'   *
;                                                                 *
        org     0x0000

Vect_IntHi
;
;  prep 2560 byte work buffer for current 8 bit output group
;
;  each channel pulse width value of 0-2559 is used as a buffer
;  array index (or time interval index?) with the channel "off"
;  or "toggle" time represented as a single '1' bit within that
;  buffer location.  note the special case where a digital "on"
;  channel pulse width value is 2560.  in this case the '1' bit
;  is placed one byte beyond the work buffer which prevents the
;  bit from being processed (the output isn't toggled "off").
;
;  example buffer setup for group '0' (LATA)
;                                                   76543210
;  chan 00 RA0 Servo[0,0] = 1500 --> WBuff[1500] = '00000001'
;  chan 01 RA1 Servo[0,1] = 2200 --> WBuff[2200] = '00000010'
;  chan 02 RA2 Servo[0,2] = 1750 --> WBuff[1750] = '00000100'
;  chan 03 RA3 Servo[0,3] = 1500 --> WBuff[1500] = '00001001'
;  chan 04 RA4 Servo[0,4] = 1550 --> WBuff[1550] = '00010000'
;  chan 05 RA5 Servo[0,5] = 1500 --> WBuff[1500] = '00101001'
;  chan 06 RA6 Servo[0,6] = 2559 --> WBuff[2559] = '01000000'
;  chan 07 RA7 Servo[0,7] = 0    --> WBuff[0]    = '10000000'
;
;  Mask = %00000001             ' servo channel bit mask
;  For n = 0 to 7               ' add servo "off" bits to buffer
;    WBuff(Servo(Group,n)) = WBuff(Servo(Group,n)) Or Mask
;    Mask = Mask << 1           ' shift mask for next channel
;  Next
;
;  20 words, 83 cycles, 10.375 usecs (isochronous)
;
        lfsr    0,Servo         ; FSR0 = @Servo[0,0]
        swapf   Group,W         ; W = Group*16 (0,16,32,48,64)
        addwf   FSR0L,F         ; FSR0 = @Servo[Group,0]
        movlw   b'00000001'     ; servo channel bit mask
lpa     movff   POSTINC0,FSR1H  ; servo value as WBuff index
        incf    FSR1H,F         ; add the 100h buffer offset
        movff   POSTINC0,FSR1L  ; FSR1 = @WBuff[pulse_width]
        iorwf   INDF1,F         ; set the servo "off time" bit
        rlcf    WREG,W          ; all 8 servos done?
        bnz     lpa             ; no, branch (do next), else

        lfsr    1,LATA          ; FSR1 = @LATA
        movf    Group,W         ; add Group number, 0..4
        addwf   FSR1L,F         ; FSR1 = @LATx 'group' latch

        btfsc   Group,2         ; increment Group
        setf    Group           ;
        incf    Group,F         ; Group = 0..4
;
;  2560 step 2560 usec 8 channel group output update routine
;
;  update the output group port from the 2560 byte work buffer
;  at 1 usec intervals.  outputs are toggled from 'on' to 'off'
;  at the correct interval as we encounter each '1' bit in the
;  work buffer array.  each byte in the work buffer is cleared
;  during the process in preperation for the next output group
;  cycle.
;
;  example output group update sequence excerpt
;
;  WBuff[1499] = '00000000'  -->  LATA '11111111'
;  WBuff[1500] = '00001001'  -->  LATA '11110110' RA0,RA3 off
;  WBuff[1501] = '00000000'  -->  LATA '11110110'
;  WBuff[1502] = '00000010'  -->  LATA '11110100' RA1 now off
;
;  21 words, 20486 cycles, 2560.75 usecs (isochronous)
;
        comf    INDF1,W         ; invert output latch bits to
        xorwf   WBuff,F         ; prep initial port XOR value
        lfsr    0,WBuff         ; FSR0 = @WBuff[0]
lpb     movf    INDF0,W         ; WREG = WBuff[time_interval]
        xorwf   INDF1,F         ; 1 usec output update 'step'
        clrf    POSTINC0        ; clear the buffer location
        btfsc   PIR1,RCIF       ; UART Rx char? no, skip, else
        movff   RCREG,POSTINC2  ; add char to circular buffer
        movlw   high(RBuff)     ; crude but safe %256
        movwf   FSR2H           ; keep it circular (C00..CFF)
        movf    INDF0,W         ; WREG = WBuff[time_interval]
        xorwf   INDF1,F         ; 1 usec output update 'step'
        clrf    POSTINC0        ; clear the buffer location
        nop                     ; burn off 1 cycle
        movf    FSR0H,W         ; time_interval index hi byte
        xorlw   high(WSize)     ; all 2560 output steps?
        bnz     lpb             ; no, branch, else
        bcf     PIR1,TMR2IF     ; clear timer 2 interrupt flag
        retfie  FAST            ; exit interrupt service routine
;
 
Last edited:
Mike,
Very nice and it makes perfect sense. I had thought about using a buffer and writing it straight to the port but hadn't thought of doing the xor like that. It also looks like you've used all the available resources. It actually looks as though the hardware was almost designed for that code.

I assume this will only work with the newer chips that can use the pll with the internal oscillator to get the 32Meg clock or an older one with an external crystal.

Mike.
 
Pommie said:
Mike,
I assume this will only work with the newer chips that can use the pll with the internal oscillator to get the 32Meg clock or an older one with an external crystal.

Mike.
I suspect you could make this basic 'method' work with different clocks and different step/interval/buffer combinations. Perhaps 1024 1 usec steps or 512 2 usec steps, both with a 988..2011 usec pulse width range. Or how about a 1024 byte buffer instead of my 2560 byte buffer but with 2.5 usec steps to cover that same 2560 usec pulse width range? This would provide 400 steps between that important 1.0 and 2.0 msec range and the 2.5 usec 'step' size works out to 10 cycles per step at 16 MHz and that might just be enough to implement the basic method on a 16F' device (how much RAM available on 16F' devices?) for 8, 16, or 24 output channels.

The beauty of the concept, in my opinion, is that you only need to place eight '1' bits in that buffer no matter how big it is.

Have fun.

Mike
 
Last edited:
Mike,

That is a unique, and may I say, very optimized approach to the project. As with all your ideas I like it very much. :) :)

Warren
 
Mike,

I don't think it is possible with a 16F series. The biggest chips only have 368 bytes of ram. I guess I should really make the switch to the 18 series.

Mike.
 
Mike's great piece of code encouraged me to at least try to optimize my original code in mikroBasic. Still I could not meet quite the ISR size of Mike's code for my ISR methods.

I want to add here that I consider Mike's code a real masterpiece of ingenuity and smart coding.

Here is an ASM version of my original ISR code:

Code:
servo		equ		0x000
sbuff		equ		0x028
cyccounter	equ		0x042
portcounter 	equ     	0x043
initdelay	equ		0x044           ; preloaded in setup
cycdelay  	equ		0x045           ; preloaded in setup
portcount   	equ		0x046           ; preloaded in setup
		
;******************************************************************************
;***** 33 Servos @ 8-bit Resolution
;***** Timer0 maintains initial delay (8-bit mode, prescaler=32, 4us)
;***** Timer1 maintains 20mS Interrupt Cycle (no prescaler, 20000us)
;***** This ISR example assumes 600us intial delay + 7us per step

HighInterrupt:

		lfsr	0,servo
		lfsr	1,LATA
		movff   portcount,portcounter  	; load portcounter with # of ports
servoport_on
		comf	INDF1,0			; turn on whole port	
init_delay
       		bsf	T0CON,3,0         	; prescaler=32
        	movff	initdelay,TMR0L	  	; preload timer0 with init delay
        	bcf	INTCON,TMR0IF	
       		bra	timeout				
servo_update
		movlw	.0			; servo masking to port
		decfsz	INDF0+0,f,0
		iorlw	.1
		decfsz	INDF0+1,f,0
		iorlw	.2
		decfsz	INDF0+2,f,0
		iorlw	.4
		decfsz	INDF0+3,f,0
		iorlw	.8
		decfsz	INDF0+4,f,0
		iorlw	.16
		decfsz	INDF0+5,f,0
		iorlw	.32
		decfsz	INDF0+6,f,0
		iorlw	.64
		decfsz	INDF0+7,f,0
		iorlw	.128
		andwf	INDF1,f,0		; any zeros clear pin 
timeout						; and remain cleared
		btfss   INTCON,TMR0IF,0 	; timer overflow?
		bra	reload_timer
		btfsc	PIR1,RCIF,0         	; test for rx byte
		movff	RCREG,POSTINC2
		bra    	timeout
reload_timer
		bcf	T0CON,3,0           	; no prescaler
		movf	cycdelay,w,0
		addwf	TMR0L,f,0		; reload timer0
		bcf	INTCON,TMR0IF,0				
		decfsz	cyccounter,f,0      	; 256 yet?
		bra	servo_update
next_port
	   	movlw	.8					
	    	addwf	FSR0L,f,0		; index to next 8 servo values
	    	incf	FSR1L,f,0		; index next port	
		decfsz	portcounter,f,0     	; finished?
		bra	servoport_on		; do it again
		retfie	FAST
 
Last edited:
Very nice Warren. Much cleaner. I know it doesn't seem very intuitive using 40 array elements when you're only using 33 of them but it sure makes the code simpler, doesn't it?
 
Pommie said:
Mike,

I don't think it is possible with a 16F series. The biggest chips only have 368 bytes of ram. I guess I should really make the switch to the 18 series.

Mike.
That's not a lot of RAM so you might have to strip down the capabilities a bit. I suspect you could still do 256 or 512 steps and 8 to 24 outputs but I wouldn't know for sure until I tried to code it.

Here's my "method" in a lo-rez 254 step 8 channel BASIC example for those who couldn't quite work through the assembly language example. The 'blue' section of code is time critical and so is a good candidate for replacement with assembler code (or much more carefully written BASIC instructions compared to what I have shown).

Do you think this could be coded on a 16F' with 4 usec update loop timing? Note that receive buffering isn't shown but is implied.

Code:
;
;  ' 8 pwm 'servo' or digital 'on/off' channel output example
;  ' 254 servo steps, 1..254, 992 to 2008 usecs, 4 usec steps
;  ' step value 0 is a digital 'off' output
;  ' step value 255 is a digital 'on' output
;
;  Dim n As Byte                '
;  Dim Servo(8) As Byte         ' 8 channel values of 0..255
;  Dim WBuff(256) As Byte       ' work (toggle/interval) buffer
;
;  Interrupt example            ' once exactly every 20 msecs
;    PIR1.TMR2IF = 0            ' clear Timer 2 interrupt flag
;
;    Mask = %00000001           ' servo channel bit mask
;    For n = 0 To 7             ' add servo "off" bits to buffer
;      WBuff(Servo(n)) = WBuff(Servo(n)) Or Mask
;      Mask = Mask << 1         ' shift bit for next channel
;    Next
;
;    WBuff(0) = WBuff(0) Xor (Not PORTB)  ' prep 1st XOR value
;
;    [COLOR=Blue]PORTB = PORTB Xor WBuff(0) ' step 0[/COLOR]
;
;    [COLOR=Blue]DelayUs(992)               ' delay 992 usecs[/COLOR]
;
;    [COLOR=Blue]For n = 1 to 254           ' steps 992 thru 2008 usecs[/COLOR]
;      [COLOR=Blue]PORTB = PORTB Xor WBuff(n) ' 4 usec output update 'step'[/COLOR]
;      [COLOR=Blue]WBuff(n) = 0             ' zero buffer after using it[/COLOR]
;    [COLOR=Blue]Next [/COLOR]                      '
;
;  End Interrupt
 
Last edited:
Mike said:
Very nice Warren. Much cleaner. I know it doesn't seem very intuitive using 40 array elements when you're only using 33 of them but it sure makes the code simpler, doesn't it?

I see that you don't miss too many details... :) A few extra RAM slots sure does make a difference in code dimensions... as you've well demonstrated.

I also thought about the PIC16 with the slower clock. Your code has possibilities, at least in theory at this point. 256 bytes is available, but the broken up addressing would be a interesting hurdle. Maybe some FSR magic could be done.
 
wschroeder said:
I see that you don't miss too many details... :) A few extra RAM slots sure does make a difference in code dimensions... as you've well demonstrated.

I also thought about the PIC16 with the slower clock. Your code has possibilities, at least in theory at this point. 256 bytes is available, but the broken up addressing would be a interesting hurdle. Maybe some FSR magic could be done.
Well, a 4 usec update interval is some 20 instruction cycles if you're using a 20 MHz clock so that gives us some room to work. The available RAM limits us to one 8-bit port group per update cycle so we'd need at least three output 'groups' using my method just to get to 16 outputs.

Perhaps we need another new 'method' for 16F' devices (another design challenge, grin)?
 
Mike said:
Perhaps we need another new 'method' for 16F' devices (another design challenge, grin)?

I like a challenge.:D I think I can manage a 2uS update for a 16 series chip. It requires some tiresome cycle counting, but I think it is possible. When I get more time I'll have a go at coding it.

Mike.
 
I like a challenge too (grin).

A 2 usec update interval (10 instruction cycles?) seems aggressive but I'm sure if anyone could do it, you guys could. Remember, we need to include UART receive buffering which is implied but not shown in my previous algorithm example.

Happy Holidays, Mike
 
Last edited:
Managed to spend some time on this and I have a working 2uS servo loop and it's just 22 instructions long. Hopefully, I'll be able to post some working code in the next day or two.

Mike.

Added, unrolling the loop doesn't work as there isn't 512 ram locations.
Happy Holidays.
 
Last edited:
So how do we get 512 steps from a 256 byte array?

How about if we do 4 bit output groups? Build the buffer with the first 256 steps in the right nybbles and the second 256 steps in the left nybbles. It means using more 2000+ msec output group sequences but we can probably squeeze quite a few of those sequences into the overall 20 msec servo period.

What do you think?
 
Status
Not open for further replies.

Latest threads

New Articles From Microcontroller Tips

Back
Top