PIC18 .asm Switch Construct

Discussion in 'Embedded Systems and Microcontrollers' started by joeyd999, Dec 31, 2014.

  1. joeyd999

    Thread Starter AAC Fanatic!

    Jun 6, 2011
    2,693
    2,762
    I know you all are using C to program PIC18s these days, but I am still crunching along writing awesome code in .asm on a daily basis -- and loving every minute of it.

    I came up with a neat new trick today -- out of necessity -- and I wanted to share.

    Suppose you had a number of subroutines, and you wanted to call one (and only one!) of them based upon an index value. The 'C' equivalent would be:

    Code (Text):
    1.  
    2. switch (index)
    3. {
    4.     case 0: routine0(); break;
    5.     case 1: routine1(); break;
    6.     ...
    7.     case n: routinen(); break;
    8. }
    9.  
    In .asm, you'd normally do something like this:

    Code (Text):
    1.  
    2.     movlw    high jtab    ;ensure jump in page
    3.     movwf    pclath
    4.  
    5.     rlncf    index,w        ;double index
    6.     addwf    pcl,f        ;and make computed jump
    7.  
    8. jtab    bra    vect0        ;vector to call for specified routine
    9.     bra    vect1
    10.     ...
    11.     bra    vectn
    12.  
    13. vect0    call    routine0    ;and make the call
    14.     bra    jdone        ; "break" equivalent
    15. vect1    call    routine1
    16.     bra    jdone
    17.     ...
    18. vectn    call    routinen
    19.     bra    jdone
    20.  
    21. jdone                ;finished
    22.  
    There are a couple of things I don't like about this. First, the need to ensure pclath points to the proper page -- and the associated problem of ensuring that all destinations within the jump table are within that page -- gets annoying after a while. I previously solved this issue with another trick, a routine called CJUMP, detailed here. Using CJUMP, the code reduces to the following:

    Code (Text):
    1.  
    2.     movf    index,w        ;get index in wreg
    3.     call    cjump        ;and jump to indexed vector
    4.  
    5.     bra    vect0        ;vector to call for specified routine
    6.     bra    vect1
    7.     ...
    8.     bra    vectn
    9.  
    10. vect0    call    routine0    ;and make the call
    11.     bra    jdone        ; "break" equivalent
    12. vect1    call    routine1
    13.     bra    jdone
    14.     ...
    15. vectn    call    routinen
    16.     bra    jdone
    17.  
    18. jdone                ;finished
    19.  
    This is a bit better, as I no longer need to deal with PCLATH or worry about my jump table spanning page boundaries. But, I still have *two* tables. Can I reduce this to one simple table? Yes!

    First, I want it to be possible that my subroutines may be anywhere in the address space, so I must use either CALLs or GOTOs, not RCALLs or BRAs. So I changed my CJUMP routine as follows:

    Code (Text):
    1.  
    2. ;****************************************
    3. ;** CJUMP2 -- Preform a computed jump  **
    4. ;**           over 2 word instructions **
    5. ;****************************************
    6. ;**   w = index (0 to 63)              **
    7. ;****************************************
    8.  
    9. cjump2    rlncf    wreg,f
    10.  
    11. ;********************************************
    12. ;** CJUMP -- Preform a computed jump       **
    13. ;**          over single word instructions **
    14. ;********************************************
    15. ;**   w = index (0 to 127)                 **
    16. ;********************************************
    17.  
    18. cjump    rlncf    wreg,f
    19.  
    20.     addwf    tosl,f
    21.     skpnc
    22.     incf    tosh,f
    23.  
    24.     return
    25.  
    I've now got two options: a computed jump into either a table of single word instructions or a table of double word instructions (CJUMP2). Using this, I can try something like:

    Code (Text):
    1.  
    2.     movf    index,w        ;get index in wreg
    3.     call    cjump2        ;and jump to indexed vector
    4.  
    5.     call    routine0    ;and make the call
    6.     call    routine1
    7.     ...
    8.     call    routinen
    9.  
    10. jdone                ;finished
    11.  
    But this is not correct. If the index is 0, routine0, routine1, and all the subsequent routines will be executed. I only want routine0 called, followed by a branch to JDONE. What to do?

    Using the PIC18 PUSH instruction, I can preset the return stack to point to anywhere I like. If I set it to JDONE prior to indexing in the table, and use GOTOs instead of CALLs in the table, then I get what I desire:

    Code (Text):
    1.  
    2.     push            ;create a new stack entry
    3.     movlw    high jdone    ;copy return address to stack
    4.     movwf    tosh
    5.     movlw    low jdone
    6.     movwf    tosl
    7.  
    8.     movf    index,w        ;get index in wreg
    9.     call    cjump2        ;and jump to indexed vector
    10.  
    11.     goto    routine0    ;and make the call
    12.     goto    routine1
    13.     ...
    14.     goto    routinen
    15.  
    16. jdone                ;upon RETURN, each routine ends up here
    17.  
    Further, I can simplify with a macro:

    Code (Text):
    1.  
    2. switch    macro    idxreg,retaddr
    3.  
    4.     push            ;create a new stack entry
    5.     movlw    high retaddr    ;copy return address to stack
    6.     movwf    tosh
    7.     movlw    low retaddr
    8.     movwf    tosl
    9.  
    10.     movf    idxreg,w    ;get index in wreg
    11.     call    cjump2        ;and jump to indexed vector
    12.  
    13.     endm
    14.  
    15.  
    For final code that looks like this:

    Code (Text):
    1.  
    2.     switch    index,jdone    ;index to routine, exit at jdone
    3.  
    4.     goto    routine0    ;and make the call
    5.     goto    routine1
    6.     ...
    7.     goto    routinen
    8.  
    9. jdone                ;upon RETURN, each routine ends up here
    10.  
    Short, sweet, easy to read. I thought this was pretty neat. Comments welcome.

    PS -> Why does this new editor turn my tabs into spaces?!!!!!
     
    Last edited: Dec 31, 2014
  2. NorthGuy

    Active Member

    Jun 28, 2014
    605
    121
    Or you can read the table with TBLRD. You fit 16-bit jump addresses into consecutive instructions. You'll need one instruction per address as opposed to 2 instructions for GOTOs. Assuming the table is at address JUMPADDR in program memory, and ignoring "U" bytes (as you did):

    Code (Text):
    1.  
    2. movwf TBLPTRL
    3. rlncf TBLPTRL
    4. clrf TBLPTRH
    5. movlw JUMPADDR_H
    6. addwf TBLPTRH, f
    7. movlw JUMPADDR_L
    8. addwfc TBLPTRL, f
    9.  
    10. push
    11. tblrd*+
    12. movwf TOSL
    13. tblrd*+
    14. movwf TOSH
    15. return
    16.  
    Will run a little bit faster and will use only half of the memory compared to GOTOs. Easily expandable to higher indices too.
     
    Last edited: Dec 31, 2014
  3. joeyd999

    Thread Starter AAC Fanatic!

    Jun 6, 2011
    2,693
    2,762
    Um, yea. That's an alternative to the computed goto -- of which there are many alternatives. But that wasn't the point of my post.

    I have proposed what I think is an elegant solution to a C style switch statement (with successive index and breaks at the end of each case) that is concise and easy to read and maintain in .asm.
     
Loading...