My first PIC trick

Discussion in 'Programmer's Corner' started by joeyd999, Jul 5, 2011.

  1. joeyd999

    Thread Starter AAC Fanatic!

    Jun 6, 2011
    2,677
    2,729
    I introduced myself a few days ago here:

    http://forum.allaboutcircuits.com/showpost.php?p=376421&postcount=225

    I'd like to share my first PIC trick with you, and it works on 18F series devices.

    The "Computed GOTO" is a valuable method of implementing structures like state machines and lookup tables (FWIW, I use TBLRD for lookup tables for the 18F parts as they are more efficient). As you all know, a computed goto has the form of:


    addwf pcl,f
    instruction 0
    instruction 1
    ...
    instruction n-1


    where the w register acts like an index to the next instruction to be executed. This capability has existed since the very first PIC silicon (PIC16C54).

    The Computed GOTO was relatively straight forward, until Microchip added the two byte instruction format, as in the 18F series parts. Therefore, it has become necessary to multiply w by 2 to get the proper index prior to the jump.

    In addition, as WREG is only 8 bits wide, a jump table could be at most 256 instructions long (16C) or 128 instructions long (18F) *and* all jump destinations must reside within a 256 byte page as defined by the PCLATH register, which must be set properly prior to executing the jump, resulting in such code as:


    movlw high jmptab
    movwf pclath
    bcf status,c
    rlncf index,w
    addwf pcl,f
    jmptab
    instruction 0
    instruction 1
    ...
    instrucion n-1​


    Then, after assembly, the list file must be inspected to ensure the jump table does not cross page boundaries. This is easy for a small number of tables, especially if you align the tables to page boundaries with ORG directives. But more numerous and longer tables become more difficult. And rearranging code using multiple ORG directives tends to waste program memory.

    One of the interesting, and little used, features of the 18F series silicon is that it provides access directly to the stack, through the top-of-stack registers TOSL, TOSH, and TOSU. These registers can be exploited to create a computed GOTO that is no longer restricted to page boundaries, page size, or WREG size.

    Here is a subroutine that will preform a computed GOTO anywhere in memory, with 256 possible jumps, and works anywhere in program memory, even across page boundaries:

    ;**************************************
    ;** CJUMP -- Preform a computed jump **
    ;**************************************
    ;** w = index (0 to 255) **
    ;**************************************

    cjump
    addwf tosl,f
    skpnc
    incf tosh,f

    addwf tosl,f
    skpnc
    incf tosh,f

    return​


    Upon entry, the top-of-stack registers have the return address following the calling instruction. The routine simply adds (twice) the value of WREG to that address, and the returns to the indexed instruction. Since the TOS registers fully define the return address, without reference to PCLATH or PCLATU, the table can cross or span multiple code pages. A minor modification can be made to add an additional 8 bit register that, when used in conjuction with WREG, could support a jump table up to 65536 instructions long.

    To use this routine, simply set WREG with an index from 0 to 255, and call CJUMP:



    movfw index
    call cjump
    instruction 0
    instruction 1
    ...
    instruction n-1


    The call itself takes 10 instruction cycles, as opposed to 6 in the traditional way (including loading PCLATH and doubling WREG). Code space is actually saved as each additional table only needs a single CALL (or RCALL) instruction with no additional support.

    BEWARE: The data sheets warn against modifying the TOS registers when using interrupts. I've discussed this concept with Microchip engineers, and they have given no good reason why the TOS may get corrupted if interrupts are used, *and* I have used this structure repeatedly in implementations with *lots* of high and low priority interrupts operating continuously and simultaneously. I have never found a single instance where the TOS gets corrupted under any circumstances.

    Also, I have no idea whatsoever how this routine would work within C compiled code (or a real-time os) where the compiler or OS manages the stack.

    Enjoy!
     
  2. ErnieM

    AAC Fanatic!

    Apr 24, 2011
    7,386
    1,605
    Interesting work.

    Have you ever tried to simulate an interrupt event during these 10 instructions? It looks to me like there would be a stack corruption if this occurs, though that would be simple enough to fix if it is a problem.

    I don't see any additional problems using this within a C framework as you are not using a C call and thus not using the TOS registers.
     
  3. joeyd999

    Thread Starter AAC Fanatic!

    Jun 6, 2011
    2,677
    2,729
    Even better than simulation: as a Microchip consultant, I have access to the Microchip engineers. I presented the code to them and asked them to tell me under what conditions stack corruption could occur.

    The answer: if WREG is changed by an ISR during execution of the call, the code will fly off into lala land. Well, duh! If you don't pay attention to context switching, then all bets are off!

    BTW: If corruption did occur, the solution would be to disable ints prior to the call (additional complexity!)...or just do it the old way.

    Actually, as an academic challenge to some of you PIC experts, it would be valuable to me if anyone can find even one situation in which stack corruption could be caused by an interrupt with this code. Any takers?

    Regarding C and/or RTOS, I don't write PIC code in C. Never have, never will. I absolutely refuse. When something breaks, I can't tell my customers it was the compiler's (or the compiler's libraries) fault. So, I don't know how the compilers use the stack. Same for RTOS.
     
  4. ErnieM

    AAC Fanatic!

    Apr 24, 2011
    7,386
    1,605
    Well I ain't the expert on assembler on anything in the 18 and up series. I don't write PIC code for those in assembler. Never have, never will. I absolutely refuse.

    So, I don't know how the compiler uses the stack either, that's why I use a compiler, that's it's job. AFAIK it makes it's own soft stack.

    If something don't work then I take the blame and find a fix, but so far the only problem like that I found was at the just my side of the door stage and it even shipped on time after I fixed it. I truly could not explain why a PIC12 was going into never never land occasionally, and without a debug port no way to check. The logic seemed fine, it sim-ed fine, but seemed a real problem so I shot gunned it by switching to another compiler. THAT version ran 10,000 times without fail. I know it was 10K as I had another PIC w/LCD display testing it.
     
  5. joeyd999

    Thread Starter AAC Fanatic!

    Jun 6, 2011
    2,677
    2,729
    Touché :D

    Actually, at this point in my life a compiler would work against me. I've built probably near 100 libraries over the past 20 years of tried and true routines that cover pretty much everything I do. Recoding in C would be a big step back for me!

    :eek: That would make me *very* nervous!
     
  6. MMcLaren

    Well-Known Member

    Feb 14, 2010
    759
    116
    Hi Joey. Nice to meet you.

    Routines utilizing the return address on the stack have been around a long time and described often on the Microchip Forum and PICLIST. An interrupt will push a new return address entry onto the stack and should not affect these routines.

    You should probably qualify your example by type of table entry. That is, you'll need table index increments of two (2) or four (4) depending on whether you have a table of "BRA" or "GOTO" instructions.

    BTW, a derivation of this method lends itself well to storing strings inline with your code;

    Code ( (Unknown Language)):
    1.  
    2.         call    PutStr          ;
    3.         db      "Hello World\n\r",0
    4.         call    PutStr          ;
    5.         db      "Menu\n\r",0
    6.  
    Code ( (Unknown Language)):
    1. ;******************************************************************
    2. ;
    3. ;  PutStr - print in-line string via Stack and TBLPTR
    4. ;
    5. ;  string must be terminated with a 0 byte and does not need
    6. ;  to be word aligned
    7. ;
    8. PutStr
    9.         movff   TOSL,TBLPTRL    ; copy return address into TBLPTR
    10.         movff   TOSH,TBLPTRH    ;
    11.         clrf    TBLPTRU         ; assume PIC with < 64-KB
    12. PutNext
    13.         tblrd   *+              ; get in-line string character
    14.         movf    TABLAT,W        ; last character (00)?
    15.         bz      PutExit         ; yes, exit, else
    16.         rcall   Put232          ; print character
    17.         bra     PutNext         ; and do another
    18. PutExit
    19.         btfsc   TBLPTRL,0       ; odd address?
    20.         tblrd   *+              ; yes, make it even (fix PC)
    21.         movf    TBLPTRH,W       ; setup new return address
    22.         movwf   TOSH            ;
    23.         movf    TBLPTRL,W       ;
    24.         movwf   TOSL            ;
    25.         return                  ; return to address after string
    26. ;
    27.  
    Also, since the new "enhanced" mid-range 16F devices provide access to the stack, we can use similar routines and methods with these devices. Notice the new ROM indirect access capability in these "enhanced" devices;

    Cheerful regards, Mike

    Code ( (Unknown Language)):
    1. PutStr
    2.         banksel TOSL            ; bank 31                         |B31
    3.         movf    TOSL,W          ; return (string) address lo      |B31
    4.         movwf   FSR1L           ;                                 |B31
    5.         movf    TOSH,W          ; return (string) address hi      |B31
    6.         movwf   FSR1H           ;                                 |B31
    7.         bsf     FSR1H,7         ; set FSR1.15 for rom access      |B31
    8. getchar
    9.         moviw   INDF1++         ; null terminator?                |B?
    10.         bz      putexit         ; yes, exit, else                 |B?
    11.         call    Put232          ; send char to LCD                |B3
    12.         bra     getchar         ; loop                            |B3
    13. putexit
    14.         banksel TOSL            ; bank 31                         |B31
    15.         movf    FSR1L,W         ; adjust return address           |B31
    16.         movwf   TOSL            ;                                 |B31
    17.         movf    FSR0H,W         ;                                 |B31
    18.         movwf   TOSH            ;                                 |B31
    19.         movlb   3               ; bank 3                          |B3
    20.         return                  ;                                 |B3
    21.  
     
    Last edited: Jul 10, 2011
  7. Barnaby Walters

    Member

    Mar 2, 2011
    103
    4
    Hello there,

    This is probably a silly suggestion that proves once and for all that even 10 reads of the datasheet is not enough, but… If ISRs are going to cause a problem, could you not just disable interrupts whilst you do the table call thing?

    Obviously this would have drawbacks, but would they be enough to stop you using this method?

    Thanks,
    Barnaby
     
  8. THE_RB

    AAC Fanatic!

    Feb 11, 2008
    5,435
    1,305
    Interesting stack method Joey! :)

    Another system (in C) would be a switch case statement full of gotos. It would have the same functionality but would be fully immune to interrupt issues.
     
  9. MMcLaren

    Well-Known Member

    Feb 14, 2010
    759
    116
    Much like the assembler case function described in August 2006 on the Microchip forum; PIC18F4331: Computed Goto: Post #17, which is also immune to interrupt issues, assuming correct context save & restore in the ISR.

    Regards...
     
  10. joeyd999

    Thread Starter AAC Fanatic!

    Jun 6, 2011
    2,677
    2,729
    I agree, except for the warning in the datasheets:


    This is a "generic" routine, intending one instruction word per index. Obviously, 2 word instructions are a special case.

    I have seen this example. I suppose it is fine for small single language applications, but supporting internationalization would be difficult (as is often a requirement in my case). String tables are superior in this regard. I also don't like mixing data in with my code...it makes maintenance difficult.
     
  11. ErnieM

    AAC Fanatic!

    Apr 24, 2011
    7,386
    1,605
    @joey: Glad you took my statement in good humor, after I posted it I squinted and worried you may take it not as I intended. Writing assembly in 18's is no simple trick!

    I appreciate your "full disclosure" in saying "Simon sez don't do this" as you go ahead and do it anyway, but it seems you did your full due diligence in proving this out. Thinking it out further I would expect a ISR event during this conversion would replace these registers as you left them as it would pop what it pushed leaving all as it was. (And of course we handle w, LDO!)

    Nice technique and thanks for sharing.

    I would hate to toss all those libraries too, but I depend on the MAL for most of my work now in PIC32 land doing USB and SD and GUI too. Nice when you get someone else to write your libraries for free (oh how I love Microchip!).
     
  12. MMcLaren

    Well-Known Member

    Feb 14, 2010
    759
    116
    An interrupt that occurs while you were incrementing or decrementing the stack pointer could be a problem, but that's not the case here. Besides, haven't you been arguing that it's not a problem? I'm agreeing with you (grin).

    You probably should have included that qualifier then since there's nothing in your initial post that suggests you're using a table of one word "BRA" instructions.

    Could you elaborate on why you think two word "GOTO" entries would be a special case given the limited address range of a "BRA" instruction, please?

    That's your personal preference, of course. Selecting between multi-language strings shouldn't be a problem but I understand your reluctance to change the way you may have been doing things for many many years. Many people, myself included, find inline strings more intuitive, much like using a high level language.

    I only posted the PutStr method because it was another example of exploiting the stack.

    I sincerely hope I have not offended you in any way. I think the qualifier is necessary to help avoid problems when someone with less experience tries your example using "GOTO" table entries.

    Cheerful regards, Mike
     
    Last edited: Jul 6, 2011
  13. joeyd999

    Thread Starter AAC Fanatic!

    Jun 6, 2011
    2,677
    2,729
    I know. I am just trying to make sure that everyone understands that the note exists, and *could* present an issue in some unknown way that I have yet to determine. Buyer beware.

    Perhaps I made the bad assumption that anybody who finds this method valuable should already recognize the limitations. I will be more careful in the future.

    Easy. I am big on good coding practices (as I see them). Unless you are writing an exceptionally large state machine, I can see no reason for a jump that exceeds 1023 instruction words, *in this context*. To wit, in 20 years, I never had the need (or desire) to compute a GOTO with a destination that far!

    I've done lots of C and C++ code, just not for PICs. Even when writing PC applications, I still use string tables. Again, maintenance is far easier, especially with large programs. For instance, I would normally call a print string function with a string identifier that represents the desired text (*not* a pointer to the actual string), regardless of language. The function itself would handle interpreting the identifier and select the proper internationalized string based upon a language selection variable. I do this in PIC code as well, just in assembly.

    I know. And I hijacked my own thread by commenting on it. :)

    Let's all stop apologizing right now. If I had thin skin, I wouldn' t have decided to be here. Me big boy. Me take punishment well. :D

    Noted for future reference.
     
  14. joeyd999

    Thread Starter AAC Fanatic!

    Jun 6, 2011
    2,677
    2,729
    Sorry for belaboring the point, but I have been thinking about this. Yet another deficiency of the "inline string" function is that it only works with static text. What about text substitution, or the C equivalent of printf("Hello, %s", name), especially in the context of internationalization? You would need at least two different print routines in your case: one for static text, and another for substitutable text.
     
  15. MMcLaren

    Well-Known Member

    Feb 14, 2010
    759
    116
    The PutStr function is designed for constant strings so I'm not sure I'd characterize that as a deficiency.

    Sorry, I'm not following you. Can you elaborate, please? I'd really like to see what you're doin'.

    Regards...
     
    Last edited: Jul 6, 2011
  16. joeyd999

    Thread Starter AAC Fanatic!

    Jun 6, 2011
    2,677
    2,729
    Let's pretend we are designing software for a friendly digital clock. You want the display to read:

    "Hello. It is [HH]:[MM] o'clock" where the bracketed info is determined by two integers.

    In C, you can write printf("Hello. It is %2d:%2d o'clock", int1, int2)
    after setting the values in int1, and int 2.

    With your PutStr routine, you would have to do:

    Code ( (Unknown Language)):
    1.  
    2.         call    PutStr          ;
    3.         db      "Hello. It is ",0
    4.  
    5.         ;; some code to convert hours to 2 digit string and print
    6.  
    7.         call    PutStr          ;
    8.         db      ":",0
    9.  
    10.         ;; some code to convert minutes to 2 digit string and print
    11.  
    12.         call    PutStr          ;
    13.         db      " o'clock",0
    14.  
    The "some code" part would have to include a conversion routine, plus another print method that prints character values from RAM, or inline during the conversion.

    I do this:

    Code ( (Unknown Language)):
    1.  
    2. lang    equ xxxH
    3.  
    4. ;program code:
    5.  
    6. mesg0   db  'Hello. It is ##:##\0'      ;english version
    7.     db  'Hola. Son las ##:##\0'     ;spanish version
    8.     db  'Bonjour. Il est ##h##\0'   ;french version
    9.  
    10.             ...
    11.  
    12. mesg1   ;and so on.
    13.  
    14. mestab  dw  mesg1, mesg2, ...       ;map each message group start address to a constant 0-n
    15.  
    16.             ...
    17.  
    18. ;now, print the time:
    19.  
    20.     movlw   0               ;select message 0
    21.     call    print               ;and print the message
    22.  
    The selected language is encoded in the "lang" register. The "print" routine selects the proper message based upon the message index in wreg and the language.

    But how does the time get in there???

    Easy. I use call backs. Prior to the actual printing of the message, the "generic" print routine calls an application specific routine and asks for the raw data (in the context of the message index which is provided as an argument). Upon return, the print routine automatically converts and formats the data, and places it in its proper location according to the # signs.

    This is a *very* simplified version of the kind of code I use, but I hope it gets the point across.

    The nice thing about it is very easily supports multiple languages, and very clearly and easily allows for variable data to be intermixed within a constant string, without lots of clutter to the main program.
     
  17. MMcLaren

    Well-Known Member

    Feb 14, 2010
    759
    116
    Hey Joey,

    Thank you for the explanation. That's a very interesting and intuitive method. I've used something similar to insert a RAM variable into a constant string.

    How involved does your "print" routine get since it prints both constant string characters as well as RAM variables?
     
    Last edited: Jul 6, 2011
  18. joeyd999

    Thread Starter AAC Fanatic!

    Jun 6, 2011
    2,677
    2,729
    The short answer: It's irrelevant. Once written it works now and forever. I write modular code, so I basically "include" a .asm and a .inc file, and never have to look at the underlying code, thus avoiding obscuring the application. The callbacks make it flexible for multiple applications. In addition, for apps with large message tables, the incremental cost (in code space) for each additional message and language is simply the number of bytes required to store the message and translations (plus some very small code to provide the data in the callbacks).

    The medium answer: it depends. I've got different versions depending on whether the strings are fixed or variable length, what kind of data and conversions are required. Does the app need ints? strings? floats? I've got code for all this, and the complexity varies depending on what I need.

    The long answer: I consider the print routine, among other things, as an adjunct to the device driver (yet another piece of modular code) that is actually outputting data to the display device. Writing to a terminal or RS-232 is different than writing to a 16x2 LCD display. I've got a neat version of the code, specifically for character LCD displays that supports not only multiple languages, but the ability to seamlessly alternate between multiple messages, flash certain lines or characters, move cursors around, and other nifty things. All without any "load" on the main application program (by this I mean the main code need not be aware that all this is going on).

    Bottom line...things can get *very* complex...but the complexity only makes things easier, not harder.
     
  19. THE_RB

    AAC Fanatic!

    Feb 11, 2008
    5,435
    1,305
    For sure, it's a positively ancient technique. Here's an asm one I used in 2001 on the LiniStepper for jumping to gotos that later select the stepper motor currents for the 36 possible microstep values for 2 motor phases;
    Code ( (Unknown Language)):
    1.  
    2. table_highpower         ;
    3.  
    4.     movf steptemp,w     ; add steptemp to the PCL
    5.     addwf PCL,f         ;
    6.                         ; here are the 36 possible values;
    7.     ;-------------------------
    8.     goto st00               ; * (hardware 6th steps)
    9.     goto st01               ;   (pwm tween steps)
    10.     goto st02               ;   (pwm tween steps)
    11.     goto st03               ; *
    12.     goto st04               ;
    13.     goto st05               ;
    14. (etc)
    15.  
    The same thing can be done on PIC 18F but just jumps 2 ROM addresses instead of 1 as on the 16F.
     
  20. joeyd999

    Thread Starter AAC Fanatic!

    Jun 6, 2011
    2,677
    2,729
    Hi, THE_RB,

    I don't mean to be rude, but what are you implying is an "ancient technique"? I agree that "Computed GOTOS", as you illustrated, have existed since the first Microchip silicon. My "trick" is to use the stack to avoid the 256 page boundary limitation of the traditional technique.

    PIC18F4331: Computed Goto: Post #17 demonstrates a similar technique as mine, but is limited to 64 possible jumps, as opposed to 256 with the given solution. I also proposed a solution allowing a possible 64K possible jumps (albiet, I can not think of a case where this would be advantageous!)
     
Loading...