My first PIC trick

Thread Starter

joeyd999

Joined Jun 6, 2011
5,287
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!
 

ErnieM

Joined Apr 24, 2011
8,377
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.
 

Thread Starter

joeyd999

Joined Jun 6, 2011
5,287
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.
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.
 

ErnieM

Joined Apr 24, 2011
8,377
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.
 

Thread Starter

joeyd999

Joined Jun 6, 2011
5,287
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.
Touché :D

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.
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!

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.
:eek: That would make me *very* nervous!
 

MMcLaren

Joined Feb 14, 2010
861
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;

Rich (BB code):
        call    PutStr          ;
        db      "Hello World\n\r",0
        call    PutStr          ;
        db      "Menu\n\r",0
Rich (BB code):
;******************************************************************
;
;  PutStr - print in-line string via Stack and TBLPTR
;
;  string must be terminated with a 0 byte and does not need
;  to be word aligned
;
PutStr
        movff   TOSL,TBLPTRL    ; copy return address into TBLPTR
        movff   TOSH,TBLPTRH    ;
        clrf    TBLPTRU         ; assume PIC with < 64-KB
PutNext
        tblrd   *+              ; get in-line string character
        movf    TABLAT,W        ; last character (00)?
        bz      PutExit         ; yes, exit, else
        rcall   Put232          ; print character
        bra     PutNext         ; and do another
PutExit
        btfsc   TBLPTRL,0       ; odd address?
        tblrd   *+              ; yes, make it even (fix PC)
        movf    TBLPTRH,W       ; setup new return address
        movwf   TOSH            ; 
        movf    TBLPTRL,W       ;
        movwf   TOSL            ;
        return                  ; return to address after string
;
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

Rich (BB code):
PutStr
        banksel TOSL            ; bank 31                         |B31
        movf    TOSL,W          ; return (string) address lo      |B31
        movwf   FSR1L           ;                                 |B31
        movf    TOSH,W          ; return (string) address hi      |B31
        movwf   FSR1H           ;                                 |B31
        bsf     FSR1H,7         ; set FSR1.15 for rom access      |B31
getchar
        moviw   INDF1++         ; null terminator?                |B?
        bz      putexit         ; yes, exit, else                 |B?
        call    Put232          ; send char to LCD                |B3
        bra     getchar         ; loop                            |B3
putexit
        banksel TOSL            ; bank 31                         |B31
        movf    FSR1L,W         ; adjust return address           |B31
        movwf   TOSL            ;                                 |B31
        movf    FSR0H,W         ;                                 |B31
        movwf   TOSH            ;                                 |B31
        movlb   3               ; bank 3                          |B3
        return                  ;                                 |B3
 
Last edited:

Barnaby Walters

Joined Mar 2, 2011
102
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
 

THE_RB

Joined Feb 11, 2008
5,438
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.
 

Thread Starter

joeyd999

Joined Jun 6, 2011
5,287
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.
I agree, except for the warning in the datasheets:

The user must disable the global interrupt enable bits
while accessing the stack to prevent inadvertent stack
corruption.

You should probably qualify your example by type of table entry. That is, you'll use index values in WREG in increments of two (2) or four (4) depending on whether you have a table of "BRA" or "GOTO" instructions.
This is a "generic" routine, intending one instruction word per index. Obviously, 2 word instructions are a special case.

BTW, this method also lends itself well to storing strings inline with your code;
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.
 

ErnieM

Joined Apr 24, 2011
8,377
@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!).
 

MMcLaren

Joined Feb 14, 2010
861
MMcLaren said:
An interrupt will push a new return address entry onto the stack and should not affect these routines.
I agree, except for the warning in the datasheets:
The user must disable the global interrupt enable bits
while accessing the stack to prevent inadvertent stack
corruption.
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).

This is a "generic" routine, intending one instruction word per index. Obviously, 2 word instructions are a special case.
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?

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.
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:

Thread Starter

joeyd999

Joined Jun 6, 2011
5,287
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).
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.

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.
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.

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?
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!

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'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 only posted the PutStr method because it was another example of exploiting the stack.
I know. And I hijacked my own thread by commenting on it. :)

I sincerely hope I have not offended you in any way.
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

I think the qualifier is necessary to help avoid problems when someone with less experience tries your example using "GOTO" table entries.
Noted for future reference.
 

Thread Starter

joeyd999

Joined Jun 6, 2011
5,287
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.
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.
 

MMcLaren

Joined Feb 14, 2010
861
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.
The PutStr function is designed for constant strings so I'm not sure I'd characterize that as a deficiency.

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.
Sorry, I'm not following you. Can you elaborate, please? I'd really like to see what you're doin'.

Regards...
 
Last edited:

Thread Starter

joeyd999

Joined Jun 6, 2011
5,287
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'.
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:

Rich (BB code):
        call    PutStr          ;
        db      "Hello. It is ",0

        ;; some code to convert hours to 2 digit string and print

        call    PutStr          ;
        db      ":",0

        ;; some code to convert minutes to 2 digit string and print

        call    PutStr          ;
        db      " o'clock",0
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:

Rich (BB code):
lang	equ	xxxH

;program code:

mesg0	db	'Hello. It is ##:##\0'		;english version
	db	'Hola. Son las ##:##\0'		;spanish version
	db	'Bonjour. Il est ##h##\0'	;french version

			...

mesg1	;and so on. 

mestab	dw	mesg1, mesg2, ...		;map each message group start address to a constant 0-n

			...

;now, print the time:

	movlw	0				;select message 0
	call	print				;and print the message
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.
 

MMcLaren

Joined Feb 14, 2010
861
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:

Thread Starter

joeyd999

Joined Jun 6, 2011
5,287
How involved does your "print" routine get since it prints both constant string characters as well as RAM variables?
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.
 

THE_RB

Joined Feb 11, 2008
5,438
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...
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;
Rich (BB code):
table_highpower			;

	movf steptemp,w		; add steptemp to the PCL
	addwf PCL,f			; 
						; here are the 36 possible values;
	;-------------------------
	goto st00				; * (hardware 6th steps)
	goto st01				;   (pwm tween steps)
	goto st02				;   (pwm tween steps)
	goto st03				; *
	goto st04				; 
	goto st05				; 
(etc)
The same thing can be done on PIC 18F but just jumps 2 ROM addresses instead of 1 as on the 16F.
 

Thread Starter

joeyd999

Joined Jun 6, 2011
5,287
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;
Rich (BB code):
table_highpower			;

	movf steptemp,w		; add steptemp to the PCL
	addwf PCL,f			; 
						; here are the 36 possible values;
	;-------------------------
	goto st00				; * (hardware 6th steps)
	goto st01				;   (pwm tween steps)
	goto st02				;   (pwm tween steps)
	goto st03				; *
	goto st04				; 
	goto st05				; 
(etc)
The same thing can be done on PIC 18F but just jumps 2 ROM addresses instead of 1 as on the 16F.
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!)
 
Top