Learning to program the PIC16LF1823

Thread Starter

cmartinez

Joined Jan 17, 2007
8,765
Well, as far as the handling of negative numbers go, I made things work quite quickly, thanks to the advice I've so kindly been given in this thread.

I don't consider myself what you'd call a "seasoned" programmer in this architecture yet. But my skills have been improving steadily, and I've been learning some pretty neat tricks that to me were unimaginable a year ago.

One thing I have not yet learned, though, is how to structure memory usage for optimizing the use of registers in assembly. For instance, I want to make sure that the registers being temporarily used in a routine are not destroyed in another routine being called from the current one. This is a problem because some of the processes that I run sometimes nest as many as five levels deep, before they sequentially return to the original calling routine. And it's become hard to manually keep track of things.

Is there some book or document out there that could teach me the best practices regarding this subject?
 

BobTPH

Joined Jun 5, 2013
11,518
For instance, I want to make sure that the registers being temporarily used in a routine are not destroyed in another routine being called from the current one.
Typically, one designates some registers that may be destroyed and others that may not.

If a routine needs any of the registers that may not be destroyed, it saves them on entry and restores them on exit.

If a caller needs registers designated as ones than can be destroyed to be preserved, it must save them before each call and restore them after.

Compilers do this for you automagically.
 

Thread Starter

cmartinez

Joined Jan 17, 2007
8,765
I've been performing some pretty neat tricks tweaking the TOSL and TOSH registers (Top of Stack) which determine the return address that a routine will be directed to when completed and the return command is executed.

I would, however, like to know how to read and store the location of a particular program address so that I can later load said address in the TOSx registers and keep on performing my tricks.

are this instructions legit?:

Code:
  movf PCLATH, w
  movwf FileH
  movf PCL, w
  movwf FileL

Continue_Program:
  more code ...
My main concern is how to compensate for the fact that every time an instruction is executed (in this case, the mere act of reading the program counter), said PCLATH, PCL pair changes.
 

joeyd999

Joined Jun 6, 2011
6,301
I've been performing some pretty neat tricks tweaking the TOSL and TOSH registers (Top of Stack) which determine the return address that a routine will be directed to when completed and the return command is executed.

I would, however, like to know how to read and store the location of a particular program address so that I can later load said address in the TOSx registers and keep on performing my tricks.

are this instructions legit?:

Code:
  movf PCLATH, w
  movwf FileH
  movf PCL, w
  movwf FileL

Continue_Program:
  more code ...
My main concern is how to compensate for the fact that every time an instruction is executed (in this case, the mere act of reading the program counter), said PCLATH, PCL pair changes.
Why read the PC? Your assembler already knows the address you are executing. It is referenced with the $ character:

I think this should work (untested):

movlw high $
movwf FileH
movlw low ($-2)
movwf FileL

Or use a label and reference the label.
 

Thread Starter

cmartinez

Joined Jan 17, 2007
8,765
Thanks, Joey. Neat tip.

I found another way better suited for my purpose. I wrote a routine that saves the TOS address, which is the next address after calling said routine, and then I take it from there.

Code:
;-----------------------------------------------------------------------------------------
; Auxiliary Save the return address currently stored in the Top of Stack (TOSx) registers
;                                   Exits with bank 0
;-----------------------------------------------------------------------------------------
Save_Return_Address:
      banksel TOSL      ;bank 63
      movf TOSL, w
      banksel RET_ADD_L ;bank 1
      movwf RET_ADD_L  
      banksel TOSH      ;bank 63
      movf TOSH, w
      banksel RET_ADD_H ;bank 1
      movwf RET_ADD_H
    return

;**** Main program

       call Load_ERROR_COUNTER               ;bank 0, loads ERROR_COUNTER with a predetermined value
       call Save_Return_Address
Set_File_Append_Mode:
         Send_GPRS_Cmd ATpFTPPUTAPPE, rATPpOK
In the code above, the Send_GPRS_Cmd macro will execute a series of routines that will evaluate for errors, and keep trying executing the same command, returning to Set_File_Append_Mode until the ERROR_COUNTER reaches zero, in which chase it then decides at which exit the call should "return" to depending on the type of error. If Send_GPRS_Cmd is successful, the program will simply continue as planned.
 

joeyd999

Joined Jun 6, 2011
6,301
Thanks, Joey. Neat tip.

I found another way better suited for my purpose. I wrote a routine that saves the TOS address, which is the next address after calling said routine, and then I take it from there.

Code:
;-----------------------------------------------------------------------------------------
; Auxiliary Save the return address currently stored in the Top of Stack (TOSx) registers
;                                   Exits with bank 0
;-----------------------------------------------------------------------------------------
Save_Return_Address:
      banksel TOSL      ;bank 63
      movf TOSL, w
      banksel RET_ADD_L ;bank 1
      movwf RET_ADD_L
      banksel TOSH      ;bank 63
      movf TOSH, w
      banksel RET_ADD_H ;bank 1
      movwf RET_ADD_H
    return

;**** Main program

       call Load_ERROR_COUNTER               ;bank 0, loads ERROR_COUNTER with a predetermined value
       call Save_Return_Address
Set_File_Append_Mode:
         Send_GPRS_Cmd ATpFTPPUTAPPE, rATPpOK
In the code above, the Send_GPRS_Cmd macro will execute a series of routines that will evaluate for errors, and keep trying executing the same command, returning to Set_File_Append_Mode until the ERROR_COUNTER reaches zero, in which chase it then decides at which exit the call should "return" to depending on the type of error. If Send_GPRS_Cmd is successful, the program will simply continue as planned.
Seems overly complicated. You could just replace your "call Save_Return_Address" with my code above (or a macro), but if it works, I guess it's fine.

Just watch out for unintended consequences. Locating bugs in code which manually modifies the PC outside of small code sections can be difficult. Been there, done that.

Edit: the only time I manually access the TOS registers in my (usually substantial) code is in my CJUMP and SWITCH routines.
 
Last edited:

Thread Starter

cmartinez

Joined Jan 17, 2007
8,765
Dang it, Joey. you were right ... it's always best to use interrupts if there's a way to use them. I already had my firmware 99% finished when I realized that I had to monitor and act on two different events simultaneously ... the only way I could think to make that work was to stop polling and start using interrupts.

I've had to put many more hours into restructuring and rewriting a large slice of code because of that. And I'm not finished yet ... I hate learning things the hard way.
 

joeyd999

Joined Jun 6, 2011
6,301
Dang it, Joey. you were right ... it's always best to use interrupts if there's a way to use them. I already had my firmware 99% finished when I realized that I had to monitor and act on two different events simultaneously ... the only way I could think to make that work was to stop polling and start using interrupts.

I've had to put many more hours into restructuring and rewriting a large slice of code because of that. And I'm not finished yet ... I hate learning things the hard way.
Your next project won't take so long.
 

John P

Joined Oct 14, 2008
2,061
Dang it, Joey. you were right ... it's always best to use interrupts if there's a way to use them. I already had my firmware 99% finished when I realized that I had to monitor and act on two different events simultaneously ... the only way I could think to make that work was to stop polling and start using interrupts.

I've had to put many more hours into restructuring and rewriting a large slice of code because of that. And I'm not finished yet ... I hate learning things the hard way.
But interrupts don't do things simultaneously. One will execute, and then the other. And you pay a cost in terms of time, whenever you enter or leave an interrupt. Just how much you pay depends on how many registers have to be saved on entry, and restored on exit. It certainly helps that you're using one of the PIC processors which save a lot of stuff automatically, but there may be more that has to be done. Another penalty is that in PIC processors, there's only one vector, address 4, and when your program gets there it has to do some testing to figure out which interrupt it's servicing (unless your program enables just one interrupt source). So if time is critical, you have to be careful about how often you need to respond to interrupts, and what the effect of each one will be.
 

joeyd999

Joined Jun 6, 2011
6,301
Dang it, Joey. you were right ... it's always best to use interrupts if there's a way to use them. I already had my firmware 99% finished when I realized that I had to monitor and act on two different events simultaneously ... the only way I could think to make that work was to stop polling and start using interrupts.

I've had to put many more hours into restructuring and rewriting a large slice of code because of that. And I'm not finished yet ... I hate learning things the hard way.
After thinking about this, I'm not sure I ever said always use interrupts if possible. I wish you'd point me to that post.

My general attitude is: use interrupts when appropriate, and use polling also when appropriate, and sometimes use a combination of both.

For instance, all my projects use an internal heartbeat (based off Timer 0), and it is always interrupt driven.

OTOH, if I need to capture and debounce switches, I poll the inputs at time periods roughly driven by the aforementioned heartbeat.

I could use interrupts for that, but it would be overly complicated and completely unnecessary. It would also consume rigidly-enforced instruction cycles that would be better off allocated to some other device. Critical timing is not required at the human-machine interface.

What you really need to develop is a reasonable philosophy that drives your programing decisions most of the time, and then be prepared to ignore it when necessary.
 

nsaspook

Joined Aug 27, 2009
16,328
But interrupts don't do things simultaneously. One will execute, and then the other. And you pay a cost in terms of time, whenever you enter or leave an interrupt. Just how much you pay depends on how many registers have to be saved on entry, and restored on exit. It certainly helps that you're using one of the PIC processors which save a lot of stuff automatically, but there may be more that has to be done. Another penalty is that in PIC processors, there's only one vector, address 4, and when your program gets there it has to do some testing to figure out which interrupt it's servicing (unless your program enables just one interrupt source). So if time is critical, you have to be careful about how often you need to respond to interrupts, and what the effect of each one will be.
+1
The cure for that is to use a PIC with vectored interrupts and shadow registers. Each poll is a task, if it's a task that does nothing, it's wasting (it might not matter) resources. Once you get the hang of interrupt heavy programming on 8-bit controllers with good hardware, blocking wait loop polling seems archaic.
1732640422127.png

Where there a sufficient resources (a unused CLC is great for this) I use interrupt/IO tracing to make sure interrupt driven tasks are balanced.
1732642126870.png
1732642181325.png
C:
/* 
 * File:   trace.h
 * Author: root
 *
 * Created on October 11, 2023, 5:33 PM
 * 
 * enable/disable various GPIO pins for program timing and debugging with logic analyzers
 * and o-scopes
 * 
 * adds small program delay and latencies when defined
 */

#ifndef TRACE_H
#define    TRACE_H

#ifdef    __cplusplus
extern "C" {
#endif

#define TRACE
// EXT_IO pin 4, MISC_IO pin 6, SPI DMA driver toggles to RB5
// EXT_IO PIN 5, interrupt toggles to RB6 PGC
// EXT_IO pin 6, composite I/O from CLC7 to RB7 PGD
// EXT_IO pin 2, MISC_IO pin 5, main program trace high to RD5
// EXT_IO pin 3, MISC_IO pin 7, RD7


#ifdef    __cplusplus
}
#endif

#endif    /* TRACE_H */
1732642055725.png
1732642091972.png
 

Thread Starter

cmartinez

Joined Jan 17, 2007
8,765
After thinking about this, I'm not sure I ever said always use interrupts if possible. I wish you'd point me to that post.

My general attitude is: use interrupts when appropriate, and use polling also when appropriate, and sometimes use a combination of both.

For instance, all my projects use an internal heartbeat (based off Timer 0), and it is always interrupt driven.

OTOH, if I need to capture and debounce switches, I poll the inputs at time periods roughly driven by the aforementioned heartbeat.

I could use interrupts for that, but it would be overly complicated and completely unnecessary. It would also consume rigidly-enforced instruction cycles that would be better off allocated to some other device. Critical timing is not required at the human-machine interface.

What you really need to develop is a reasonable philosophy that drives your programing decisions most of the time, and then be prepared to ignore it when necessary.
it's always best to use interrupts if there's a way to use them.

I should've said: "it's always best to use interrupts whenever practical" ... yeah, of course there will always be cases in which polling would be simpler and even more compact. And you're right about developing a "reasonable philosophy" for coding. I guess the only way to attain it is through experience, and by listening and paying attention to the advice of respectable sources. And this place has proved to be of invaluable help ... although I have an engineering degree and learned to code way before I even went to college, when it comes to assembly I'm a self-taught programmer.
 

nsaspook

Joined Aug 27, 2009
16,328
After thinking about this, I'm not sure I ever said always use interrupts if possible. I wish you'd point me to that post.

My general attitude is: use interrupts when appropriate, and use polling also when appropriate, and sometimes use a combination of both.

For instance, all my projects use an internal heartbeat (based off Timer 0), and it is always interrupt driven.

OTOH, if I need to capture and debounce switches, I poll the inputs at time periods roughly driven by the aforementioned heartbeat.

I could use interrupts for that, but it would be overly complicated and completely unnecessary. It would also consume rigidly-enforced instruction cycles that would be better off allocated to some other device. Critical timing is not required at the human-machine interface.

What you really need to develop is a reasonable philosophy that drives your programing decisions most of the time, and then be prepared to ignore it when necessary.
I sometimes use a combination for switches. IOC for individual switch inputs and a timer based debounce. This abstracts the switch hardware to something easy to integrate into a FSM.
C:
/*
 * switch IOC handlers per PIN
 */
static void aswitch(void)
{
    B.a_trigger[D_SW_A] = true;
    a_debounce[D_SW_A] = 0;
}

static void lswitch(void)
{
    B.a_trigger[D_SW_L] = true;
    a_debounce[D_SW_L] = 0;
}

static void mswitch(void)
{
    B.a_trigger[D_SW_M] = true;
    a_debounce[D_SW_M] = 0;
}

void init_all_switch(void)
{
    for (uint8_t i = 0; i < D_SW_COUNT; i++) {
        B.a_type[i] = sw_contact_types[i];
        B.a_trigger[i] = false;
        B.a_switch[i] = false;
        a_debounce[i] = 0;
    }
    IOCAF5_SetInterruptHandler(aswitch);
    IOCAF2_SetInterruptHandler(lswitch);
    IOCAF1_SetInterruptHandler(mswitch);
}

/*
 * button checking/de-bounce routine for 500us timing ISR , runs in software timer interrupt ISR timer #4
 */
void button_press_check(void)
{
#ifdef TRACE
    IO_RD7_SetHigh();
#endif
    /*
     * check for button presses
     */
// debounce and switch state code
    }
#ifdef TRACE
    IO_RD7_SetLow();
#endif
[code]
 

joeyd999

Joined Jun 6, 2011
6,301
I sometimes use a combination for switches. IOC for individual switch inputs and a timer based debounce. This abstracts the switch hardware to something easy to integrate into a FSM.
C:
/*
* switch IOC handlers per PIN
*/
static void aswitch(void)
{
    B.a_trigger[D_SW_A] = true;
    a_debounce[D_SW_A] = 0;
}

static void lswitch(void)
{
    B.a_trigger[D_SW_L] = true;
    a_debounce[D_SW_L] = 0;
}

static void mswitch(void)
{
    B.a_trigger[D_SW_M] = true;
    a_debounce[D_SW_M] = 0;
}

void init_all_switch(void)
{
    for (uint8_t i = 0; i < D_SW_COUNT; i++) {
        B.a_type[i] = sw_contact_types[i];
        B.a_trigger[i] = false;
        B.a_switch[i] = false;
        a_debounce[i] = 0;
    }
    IOCAF5_SetInterruptHandler(aswitch);
    IOCAF2_SetInterruptHandler(lswitch);
    IOCAF1_SetInterruptHandler(mswitch);
}

/*
* button checking/de-bounce routine for 500us timing ISR , runs in software timer interrupt ISR timer #4
*/
void button_press_check(void)
{
#ifdef TRACE
    IO_RD7_SetHigh();
#endif
    /*
     * check for button presses
     */
// debounce and switch state code
    }
#ifdef TRACE
    IO_RD7_SetLow();
#endif
[code]
https://forum.allaboutcircuits.com/...owering-ones-expectations.190219/post-1781551
 

Thread Starter

cmartinez

Joined Jan 17, 2007
8,765
Ok, here's an update of my progress, so far. And I thought it would be polite to explain in a little more detail what problems I'm facing and the general purpose of my project, because I know that deep, deep down, you guys have been dying to know. So here it is.

The chip I'm using at this moment is the PIC16LF18446. It's a real beauty. It's got lots of pins, plenty of memory, and can be run at very low power. Which are exactly the kind of things I need.

My project involves the monitoring of sensors and execution of actuators, and the transmission of the gathered data (which is recorded in a flash memory chip) through the chip's UART. My program worked fine on its first version, which executed its corresponding routines performing the aforementioned tasks at its own leisure.

That is, until my customer clarified the following requirements:
  • Sensor data should be recorded at exactly 30 second intervals
  • Said data should be transmitted on scheduled periods, or upon request
  • Data logging should never, ever stop.

The first two requirements were a piece of cake. I only needed to put in a couple of weeks or so of work to get them done. It's the third part that turned out to be much harder than I thought. This because data is transmitted through a GPRS module to an FTP server using the chip's UART. And my original code polled the reception of all info (in the form of character strings) requested from the module.

That was a mistake.

The only way I could think of to accomplish the third requirement was to restructure the code in such a way that all data arriving through the UART would be gathered using a routine in the chip's interrupt vector. And the interrupt vector was already being used to log the data a regular intervals using one of the chip's timers as a trigger source. So what I did was write a routine within the interrupt vector that would store all characters arriving through the UART (up to 500 at a time) at a predefined memory location, and then said data would be accessed and processed later on at the main thread.

What is critical is not to access and use the data at exact times, but to immediately store it in its corresponding place as it arrives, or otherwise the chip's buffer will be overrun and data would be lost. And in the meantime, it might just so happen that a sensor needs to be read and its data stored in a log for later transmission. The good news is that there's a one-second window in which said sensor can be accessed and read. The bad news is that there are only a few microseconds available to service the UART and retrieve and store the incoming data when that happens. The rest of the processes (actuators, indicators, etc...) are not time critical and are sequentially performed at the main thread.

That means that the UART's Rx event takes precedence on the sensor's readings every time.

My solution (which I believe I will finish implementing in the next couple of days) is as follows:
  1. Rewrite all routines (mathematical and logical) so that they use indirection (FSRs) instead of fixed working memory registers for their execution. (yes, I *was* such a noob in that respect)
  2. Split all memory being used into three parts, one for the main thread, another for the sensor's data processing and calculations, and another one for the interrupt routine. This so that no routine can alter the data being used by.
  3. Run the sensor's reading routines as an external thread, and *not* within the interrupt vector. This way UART reception will always take precedence when an interrupt is triggered.

To accomplish that, this will be the logic I'll be implementing:

When a sensor log interrupt is triggered the following steps take place inside the interrupt routine:
  • Backup the shadow core registers
  • Backup the 16 bytes of common RAM
  • Backup the main thread's return address
  • Change the return address to that of the Log_Current_Data routine
    • Log_Current_Data will immediately execute (and become the main thread) after exiting the interrupt vector via "retfie"
    • When a character arrives through the UART, Log_Current_Data will pause and the interrupt will be serviced and then return to the Log_Current_Data routine
    • Upon completion, Log_Current_Data will raise a flag, and will end with a "goto $" instruction, waiting for the next interrupt to be triggered
  • When the interrupt routine is again serviced (be it because either the UART or the Timer requested it) the aforementioned flag will signal the execution of the following events:
    • Restore the shadow core registers
    • Restore the 16 bytes of common RAM
    • Restore the original return address to that of the main thread
In simple words: The UART's data reception and storage will always be dealt with at the interrupt routine, but the sensor's readings and data logging will be executed as a thread outside of the interrupt vector. That, I believe, should work and meet the requirements. I just hope I've not overcomplicated things ...

As a side note, I've been working on this project for more than two years now, and I must say that my eyes were opened when I reviewed my original code, noticing a lot of redundancies and instructions that were not only clumsily written, but that were also bloated, inefficient, and in some instances even difficult to read. I can't believe I was ever such a greenhorn ...

I wonder how I'm going to feel about the code I've just written when perhaps I have to review it in a couple of years more.
 
Last edited:

joeyd999

Joined Jun 6, 2011
6,301
The bad news is that there are only a few microseconds available to service the UART and retrieve and store the incoming data when that happens.
This is why your receive interrupt routine should simply receive a character into a queue and promptly return to the main program for later processing, which your tl;dr solution seems to be doing.

This is (or should be) standard practice.
 
Top