Jump to content

Search the Community

Showing results for tags 'pic16'.

More search options

  • Search By Tags

    Type tags separated by commas.
  • Search By Author

Content Type


  • Top Level
    • Announcements
    • The Lounge
    • Questions and Answers
    • Forum Help
    • Project Gallery
    • Vendor Bug Reports


  • What every embedded programmer should know about ...
  • Programming Lore

Find results in...

Find results that contain...

Date Created

  • Start


Last Updated

  • Start


Filter by number of...


  • Start



About Me

Found 3 results

  1. Here I am on a clear cool evening, by the fire outside with my laptop. Tonight I will talk about a new peripheral, the timer/counter. Periodically I will be interrupted to put another log on my fire, but it should not slow me down too much. Timers and counters are almost the same peripheral with only the difference of what is causing the counter to count. If the counter is incrementing from a clock source, it becomes a timer because each count registers the passage of a precise unit of time. If the counter is incrementing from an unknown signal (perhaps not even a regular signal), it is simply a counter. Clearly, the difference between these is a matter of the use-case and not a matter of design. Through there are some technical details related to clocking any peripheral from an external "unknown" clock that is not synchronized with the clocks inside the microcontroller. We will happy ignore those details because the designers have done a good job of sorting them out. Let us take a peek at a very simple timer on the PIC16F18446. Turn to page 348 of your PIC16F18446 data sheet and take a look at figure 25-1. (shown below) This is the basic anatomy of a pretty vanilla timer. Of course most timers have many more features so this article is simply an introduction. On the left side of this image there are a number of clock sources entering a symbol that represents a multiplexer. A multiplexer is simply a device that can select one input and pass it to its output. The T0CS<2:0> signal below the multiplexer is shorthand for a 3-bit signal named T0CS. The slash 3 also indicates that that is a 3-bit signal. Each of the possible 3-bit codes is inside the multiplexer next to one of the inputs. This indicates the input you will select if you apply that code on the signal T0CS. Pretty simple. The inputs are from top to bottom (ignoring the reserved ones) SOSC (Secondary Oscillator), LFINTOSC (Low Frequency Internal Oscillator), HFINTOSC (High Frequency Internal Oscillator) Fosc/4 (The CPU Instruction Clock) an input pin (T0CKI) inverted or not-inverted. Let us cover each section of this peripheral in a little more detail. Of course you should go to the data sheet to read all the information. SOSC The secondary oscillator is a second crystal oscillator on 2 I/O pins. A crystal oscillator is a type of clock that uses a piece of quartz crystal to produce a very accurate frequency. The secondary oscillator is designed to operate at 32.768kHz which by some coincidence is 2^15 counts per second. This makes keeping accurate track of seconds very easy and very low power. You could configure the hardware to wake up the CPU every second and spend most of your time in a low power sleep mode. LFINTOSC and HFINTOSC There are two internal oscillator in the PIC16F18446. The LFINTOSC is approximately 31kHz and is intended for low power low speed operation but not very accurate timing. The HFINTOSC is adjustable from 1-32MHz and is better than 5% accurate so it is often sufficient for most applications. Because these two oscillators are directly available to the timer, the CPU can be operating at a completely different frequency allowing high resolution timing of some events, while running the CPU at a completely different frequency. FOSC/4 This option is the simplest option to select because most of the math you are doing for other peripherals is already at this frequency. If you are porting software for a previous PIC16 MCU, the timer may already be assumed to be at this frequency. Due to historical reasons, a PIC16 is often clocked at 4MHz. This makes the instruction clock 1MHz and each timer tick is 1us. Having a 1us tick makes many timing calculations trivial. If you were clocking at 32MHz, each tick would be 31ns which is much smaller but does not divide as nicely into base 10 values. T0CKI This option allows your system to measure time based upon an external clock. You might connect the timing wheel of an engine to this input pin and compute the RPM with a separate timer. Prescaler After the input multiplexer, there is an input pre-scaler. The goal of the pre-scaler is to reduce the input clock frequency to a slower frequency that may be more suitable for the application. The most prescallers are implemented as a chain of 'T' flip-flops. A T flip-flop simply changes its output (high to low or low to high) on each rising edge of an input signal. That makes a T Flip-Flop a divide by 2 circuit for a clock. If you have a chain of these and you use a multiplexer to decide which T flip flop to listen to, you get a very simple divider that can divide by some power of 2. i.e. 2, 4, 8, 16... with each frequency 1/2 of the previous one. Synchronizer The synchronizer ensures that input pulses that are NOT sourced by an oscillator related to FOSC are synchronized to FOSC. This synchronization ensures reliable pulses for counting or for any peripherals that are attached to the counter. However, synchronization requires the FOSC/4 clock source to be operating and that condition is not true in when the CPU is saving power in sleep. If you are building an alarm clock that must run on a tiny battery, you will want the timer to operate while the CPU is sleeping and to produce a wakeup interrupt at appropriate intervals. To do this, you disable synchronization. Once the CPU has been awakened, it is a good idea to activate synchronization or to avoid interacting with the counter while it is running. TMR0 Body The TMR0 body used to be a simple counter, but in more recent years it has gained 2 options. Either, the timer can be a 16-bit counter, or it can be an 8-bit counter with an 8-bit compare. The 8-bit compare allows the timer to be reset to zero on any 8-bit value. The 16-bit counter allows it to count for a longer period of time before an overflow. The output from the TMR0 body depends upon the module. In the 8-bit compare mode, the output will be set each time there is a compare match. In the 16-bit mode, the output will be set each time the counter rolls from 0xFFFF to 0x0000. Output The output from the core can be directed to other peripherals such as the CLC's, it can also be sent through a postscaler for further division and then create an interrupt or toggle an output on an I/O pin. The postscaler is different than the prescaler because it is not limited to powers of two. It is a counting divider and it can divide by any value between 1 and 16. We shall use that feature in the example. Using the Timer Timers can be used for a great number of things but one common thing is to produce a precise timing interval that does not depend upon your code. For instance, 2 lessons ago, we generated a PWM signal. The one way to do this was to set and clear the GPIO pin every so many instruction cycles. Unfortunately, as we added code to execute the PWM would get slower and slower. Additionally, it could get less reliable because the program could take different paths through the code. Using the PWM peripheral was the perfect solution, but another solution would be to use a timer. For instance, you could configure the timer to set the output after an interval. After that interval had elapsed, you could set a different interval to clear the output. By switching back and forth between the set interval and the clear interval, you would get a PWM output. Still more work than the PWM peripheral, but MUCH better than the pure software approach. For this exercise we will use the timer to force our main loop to execute at a fixed time interval. We will instrument this loop and show that even as we add work to the loop, it still executes at the same time interval. This type of structure is called an exec loop and it is often used in embedded programming because it ensures that all the timing operations can be simple software counting in multiples of the loop period. And here is the program. void main(void) { TRISAbits.TRISA2 = 0; // Configure the TRISA2 as an output (the LED) T0CON1bits.ASYNC = 0; // Make sure the timer is synchronized T0CON1bits.CKPS = 5; // Configure the prescaler to divide by 32 T0CON1bits.CS = 2; // use the FOSC/4 clock for the input // the TMR0 clock should now be 250kHz TMR0H = 250; // Set the counter to reset to 0 when it reaches 250 (1ms) TMR0L = 0; // Clear the counter T0CON0bits.T0OUTPS = 9; // Configure the postscaler to divide by 10 T0CON0bits.T0EN = 1; // turn the timer on // the timer output should be every 10ms while(1) { LATAbits.LATA2 = 0; // Turn on the LED... this allows us to measure CPU time __delay_ms(5); // do some work... could be anything. LATAbits.LATA2 = 1; // Turn off the LED... Any extra time will be while the LED is off. while(! PIR0bits.TMR0IF ); // burn off the unused CPU time. This time remaining could be used as a CPU load indicator. PIR0bits.TMR0IF = 0; // clear the overflow flag so we can detect the next interval. } } I chose to use a delay macro to represent doing useful work. In a "real" application, this area would be filled with all the various functions that need to be executed every 10 milliseconds. If you needed something run every 20 milliseconds you would execute that function every other time. In this way, many different rates can be easily accommodated so long as the total execution time does not exceed 10 milliseconds because that will stretch a executive cycle into the next interval and break the regular timing. Special Considerations One interesting effect in timers is they are often the first example of "concurrency issues" that many programmers encounter. Concurrency issues arise when two different systems access the same resource at the same time. Quite often you get unexpected results which can be seen as "random" behavior. In the code above I configured the timer in 8-bit mode and took advantage of the hardware compare feature so I never needed to look at the timer counter again. But let us imagine a slightly different scenario. Imagine that we needed to measure the lap time of a race car. When the car started the race we would start the timer. As the car crossed the start line, we would read the timer BUT WE WOULD NOT STOP IT. When the car finished the last lap, we could stop the timer and see the total time. IN this way we would have a record for every lap in the race. Simply by subtracting the time of completion for each lap, we would have each lap interval which would be valuable information for the race driver. Each time we read the timer without stopping it, we have an opportunity for a concurrency issue. For an 8-bit timer we can read the entire value with one instruction and there are no issues. However, the race is likely to last longer than we can count on 8-bits so we need a bigger timer. With a 16-bit timer we must perform 2 reads to get the entire value and now we encounter our problem. In the picture above I have shown two scenarios where TMR0 in 16-bit mode is counting 1 count per instruction cycle. This is done to demonstrate the problem. Slowing down the counting rate does not really solve the problem but it can affect the frequency of the issue. In this example the blue cell indicates the first read while the red cell indicates the second read to get all 16-bits. When the counter was 251, the reads are successful, however when the counter is 255, the actual value we will read will be 511 which is about 2x the actual value. If we reverse the read order we have the same problem. One solution is to read the high, then the low and finally, read the high a second time. With these three data points and some math, it is possible to reconstruct the exact value at the time of the first read. Another solution is in hardware. In the data sheet we see that there is some additional circuitry surrounding TMR0H. With this circuitry, the TMR0 will automatically read TMR0H from the counter into a holding register when TMR0L is read. So if you read TMR0L first and then TMR0H you will NEVER have the issue. Now consider the following line of C. timerValue = TMR0 It is not clear from just this line of code which byte of TMR0 is read first. If it is the low byte this line is finished and perfect. However, if it is the high byte, then we still have a problem. One way to be perfectly clear in the code is the following: timerValue = TMR0L; timerValue |= TMR0H << 8; This code is guaranteed to read the registers in the correct order and should be no less efficient. The left shift by 8 will probably not happen explicitly because the compiler is smart enough to simply read the value of TMR0H and store it in the high byte of timerValue. These concurrency issues can appear in many areas of computer programming. If your program is using interrupts then it is possible to see variables partially updated when an interrupt occurs causing the same concurrency issues. Some bigger computers use real-time operating systems to provide multi-tasking. Sharing variables between the tasks is another opportunity for concurrency issues. There are many solutions, for now just be aware that these exist and they will affect your future code. Timer 0 is probably the easiest timer on a PICmicrocontroller. It has always been very basic and its simplicity makes it the best timer to play with as you learn how they work. Once you feel you have mastered timer 0, spend some time with timer 1 and see what additional features it has. Once again, the project is in the attached files. Good Luck. exercise_6.zip
  2. “Code generation, like drinking alcohol, is good in moderation.” — Alex Lowe This episode we are going to try something different. The brute force approach had the advantage of being simple and easy to maintain. The hand-crafted decision tree had the advantage of being fast. This week we will look at an option that will hopefully combine the simplicity of the string list and the speed of the decision tree. This week we will use a code generator to automatically create the tokenizing state machine. I will leave it to you to decide if we use generation in moderation. Let me introduce RAGEL http://www.colm.net/open-source/ragel/. I discovered RAGEL a few years ago when I was looking for a quick and dirty way to build some string handling state machines. RAGEL will construct a complete state machine that will handle the parsing of any regular expression. It can do tokenizing and it can do parsing. Essentially, you define the rules for the tokens and the functions to call when each token is found. For instance, you can write a rule to handle any integer and when an integer is found it can call your doInteger() method. For our simple example of identifying 6 words, the RAGEL code will be a bit overkill but it will be MUCH faster than a brute force string search and in the same ball park as the hand crafted decision tree. Let us get started. First let us get the housekeeping out of the way. This part of the code you have seen before. It is identical to the first two examples I have already provided. There are two differences. First, this only LOOKS like C code. In fact, it is a RAGEL file (I saved it with a .rl extension) and you will see the differences in a moment. When I use a code synthesizer, I like to place the needed command line at the top of the file in comments. While comments are a smell, this sort of comment is pretty important. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // compile into C with ragel // ragel -C -L -G2 example3.rl -o example3.c // #include <string.h> #include <stdio.h> #include "serial_port.h" char * NMEA_getWord(void) { static char buffer[7]; memset(buffer,0,sizeof(buffer)); do { serial_read(buffer,1); } while(buffer[0] != '$'); for(int x=0;x<sizeof(buffer)-1;x++) { serial_read(&buffer[x], 1); if(buffer[x]==',') { buffer[x] = 0; break; } } return buffer; } enum wordTokens {NO_WORD = -1,GPGGA,GNGSA,GPGSV,GPBOD,GPDBT,GPDCN, GPRMC, GPBWC}; RAGEL is pretty nice in that they choose some special symbols to identify the RAGEL bits so the generator simply passes all input straight to the output until it finds the RAGEL identifiers and then it gets to work. This architecture allows you to simply insert RAGEL code directly into your C (or other languages) and add the state machines in place. The first identifiers we find are the declaration of a state machine (foo seemed traditional). You can define more than one machine so it is important to provide a hint to the generator about which one you want to define. After the machine definition, I specified the location to place all the state machine data tables. There are multiple ways RAGEL can produce a state machine. If the machine requires data, it will go at the write data block. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 %% machine foo; %% write data; enum wordTokens NMEA_findToken(char *word) { const char *p = word; const char *pe = word + strlen(word); int cs; enum wordTokens returnValue = NO_WORD; %%{ action gpgga { returnValue = GPGGA; fbreak; } action gngsa { returnValue = GNGSA; fbreak; } action gpgsv { returnValue = GPGSV; fbreak; } action gpbod { returnValue = GPBOD; fbreak; } action gpdbt { returnValue = GPDBT; fbreak; } action gpdcn { returnValue = GPDCN; fbreak; } action gpbwc { returnValue = GPBWC; fbreak; } action gprmc { returnValue = GPRMC; fbreak; } gpgga = ('GPGGA') @gpgga; gngsa = ('GNGSA') @gngsa; gpgsv = ('GPGSV') @gpgsv; gpbod = ('GPBOD') @gpbod; gpdbt = ('GPDBT') @gpdbt; gpdcn = ('GPDCN') @gpdcn; gpbwc = ('GPBWC') @gpbwc; gprmc = ('GPRMC') @gprmc; main := ( gpgga | gngsa | gpgsv | gpbod | gpdbt | gpdcn | gpbwc | gprmc )*; write init; write exec noend; }%% return returnValue; } Next is the C function definition starting at line 4 above. I am keeping the original NMEA_findToken function as before. No sense in changing what is working. At the beginning of the function is some RAGEL housekeeping defining the range of text to process. In this case the variable p represents the beginning of the test while pe represents the end of the text. The variable cs is a housekeeping variable and the token is the return value so initialize it to NO_WORD. The next bit is some RAGEL code. The %%{ defines a block of ragel much like /* defines the start of a comment block. The first bit of ragel is defining all of the actions that will be triggered when the strings are identified. Honestly, these actions could be anything and I held back simply to keep the function identical to the original. It would be easy to fully define the NMEA data formats and fully decode each NMEA sentence. These simply identify the return token and break out of the function. If we had not already sliced up the tokens we would want to keep store our position in the input strings so we could return to the same spot. It is also possible to feed the state machine one character a time like in an interrupt service routine. After the actions, line 21 defines the search rules and the action to execute when a rule is matched. These rules are simply regular expressions (HA! REGEX and SIMPLE in the same sentence). For this example, the expressions are simply the strings. But if your regular expressions were more complex, you could go crazy. Finally, the machine is defined as matching any of the rules. The initialization and the actual execute code are placed and the RAGEL is complete. Whew! Let us look at what happened when we compile it. One of my favorite programming tools is graphviz. Specifically DOT. It turns out that RAGEL can produce a dot file documenting the produced state machine. Lets try it out. bash> ragel -C -L -V example3.rl -o example3.dot bash> dot example3.dot -T png -O It would be nicer if all the numbers on the arrows were the characters rather than the ASCII codes but I suppose I am nitpicking. Now you see why I named my actions after the sentences. The return arrow clearly shows which action is being executed when the words are found. It also shows that the action triggers when the last letter is found rather than a trailing character. I suppose if you had the word gpgga2, then you would need to add some additional REGEX magic. The dotted arrow IN leading to state 17 refers to any other transition not listed. That indicates that any out-of-place letter simply goes back to 17 without triggering an ACTION. It is possible to define a “SYNTAX ERROR” action to cover this case but I did not care. For my needs, failing quietly is a good choice. This all looks pretty good so far. What does the C look like? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 /* #line 1 "example3.rl" */ // compile into C with ragel // ragel -C -L -G2 example3.rl -o example3.c // #include < string.h > #include < stdio.h > #include "serial_port.h" char * NMEA_getWord(void) { static char buffer[7]; memset(buffer, 0, sizeof(buffer)); do { serial_read(buffer, 1); } while (buffer[0] != '$'); for (int x = 0; x < sizeof(buffer) - 1; x++) { serial_read( & buffer[x], 1); if (buffer[x] == ',') { buffer[x] = 0; break; } } return buffer; } enum wordTokens { NO_WORD = -1, GPGGA, GNGSA, GPGSV, GPBOD, GPDBT, GPDCN, GPRMC, GPBWC }; /* #line 34 "example3.rl" */ /* #line 39 "example3.c" */ static const int foo_start = 17; static const int foo_first_final = 17; static const int foo_error = 0; static const int foo_en_main = 17; /* #line 35 "example3.rl" */ enum wordTokens NMEA_findToken(char * word) { const char * p = word; const char * pe = word + strlen(word); int cs; enum wordTokens returnValue = NO_WORD; /* #line 57 "example3.c" */ { cs = foo_start; } /* #line 62 "example3.c" */ { switch (cs) { tr5: /* #line 45 "example3.rl" */ { returnValue = GNGSA; { p++; cs = 17; goto _out; } } goto st17; tr12: /* #line 47 "example3.rl" */ { returnValue = GPBOD; { p++; cs = 17; goto _out; } } goto st17; tr13: /* #line 50 "example3.rl" */ { returnValue = GPBWC; { p++; cs = 17; goto _out; } } goto st17; tr16: /* #line 48 "example3.rl" */ { returnValue = GPDBT; { p++; cs = 17; goto _out; } } goto st17; tr17: /* #line 49 "example3.rl" */ { returnValue = GPDCN; { p++; cs = 17; goto _out; } } goto st17; tr20: /* #line 44 "example3.rl" */ { returnValue = GPGGA; { p++; cs = 17; goto _out; } } goto st17; tr21: /* #line 46 "example3.rl" */ { returnValue = GPGSV; { p++; cs = 17; goto _out; } } goto st17; tr23: /* #line 51 "example3.rl" */ { returnValue = GPRMC; { p++; cs = 17; goto _out; } } goto st17; st17: p += 1; case 17: /* #line 101 "example3.c" */ if (( * p) == 71) goto st1; goto st0; st0: cs = 0; goto _out; st1: p += 1; case 1: switch (( * p)) { case 78: goto st2; case 80: goto st5; } goto st0; st2: p += 1; case 2: if (( * p) == 71) goto st3; goto st0; st3: p += 1; case 3: if (( * p) == 83) goto st4; goto st0; st4: p += 1; case 4: if (( * p) == 65) goto tr5; goto st0; st5: p += 1; case 5: switch (( * p)) { case 66: goto st6; case 68: goto st9; case 71: goto st12; case 82: goto st15; } goto st0; st6: p += 1; case 6: switch (( * p)) { case 79: goto st7; case 87: goto st8; } goto st0; st7: p += 1; case 7: if (( * p) == 68) goto tr12; goto st0; st8: p += 1; case 8: if (( * p) == 67) goto tr13; goto st0; st9: p += 1; case 9: switch (( * p)) { case 66: goto st10; case 67: goto st11; } goto st0; st10: p += 1; case 10: if (( * p) == 84) goto tr16; goto st0; st11: p += 1; case 11: if (( * p) == 78) goto tr17; goto st0; st12: p += 1; case 12: switch (( * p)) { case 71: goto st13; case 83: goto st14; } goto st0; st13: p += 1; case 13: if (( * p) == 65) goto tr20; goto st0; st14: p += 1; case 14: if (( * p) == 86) goto tr21; goto st0; st15: p += 1; case 15: if (( * p) == 77) goto st16; goto st0; st16: p += 1; case 16: if (( * p) == 67) goto tr23; goto st0; } _out: {} } /* #line 66 "example3.rl" */ return returnValue; } int main(int argc, char ** argv) { if (serial_open() > 0) { for (int x = 0; x < 24; x++) { char * w = NMEA_getWord(); enum wordTokens t = NMEA_findToken(w); printf("word %s,", w); if (t >= 0) printf("token %d\n", t); else printf("no match\n"); } } serial_close(); return 0; } And this is why we use a code generator. The code does not look too terrible. i.e., I could debug it if I thought there were some bugs and it does follow the state chart in a perfectly readable way. BUT, I hope you are not one of those programmers who finds GOTO against their religion. (Though Edsger Dijkstra did allow an exception for low level code when he wrote EWD215 https://www.cs.utexas.edu/users/EWD/transcriptions/EWD02xx/EWD215.html ) So how does this perform? STRNCMP IF-ELSE RAGEL -G2 GNGSA 399 121 280 GPGSV 585 123 304 GLGSV 724 59 225 GPRMC 899 83 299 GPGGA 283 113 298 And for the code size MPLAB XC8 in Free mode on the PIC16F1939 shows 2552 bytes of program and 1024 bytes of data. Don’t forget that printf is included. But this is comparable to the other examples because I am only changing the one function. So our fancy code generator is usually faster than the brute force approach, definitely slower than the hand-crafted approach and is fairly easy to modify. I think I would use the string compare until I got a few more strings and then make the leap to RAGEL. Once I was committed to RAGEL, I think I would see how much of the string processing I could do with RAGEL just to speed the development cycles and be prepared for that One Last Feature from Marketing. Next week we will look at another code generator and a completely different way to manage this task. Good Luck. example3.X.zip
  3. The PICmcu is not known for being fast or supporting large memories but it does have one feature that can significantly simplify developing your applications. That feature is the lack of a hardware call stack. Sacrilege you say! But wait... The lack of this important feature has caused the compiler team to develop an incredibly useful alternative that I would argue is better in nearly every way than an actual stack. For those of you who are now wondering what I am talking about, let's take a quick diversion into stacks. A stack is simply a data structure that arranges a number of data elements so that the last thing you inserted is the first thing you get back. Think of a stack of plates, you add plates to the top of the stack and remove them in reverse order from the top of the stack. This is important for a few reasons. First, imagine your code was interrupt by a hardware interrupt. The current address of your code is pushed onto the stack, the interrupt runs and your address is popped from the stack so the interrupt can return where it interrupt you. This is a handy feature and for "free" you can handle any number of interruptions. In fact, a function can call itself and so long as there is sufficient room on the stack, everything will be sorted out on the return. Now, the PICmcu's have hardware stacks for the the function calls and returns. They simply don't have any hardware support for anything else. If the hardware stack is so useful for keeping track of return addresses, it would also be useful for all the parameters your function will need and the values your functions will return. This parameter stack is an important feature of most languages and most especially the C language. The AVR, ARM, 68000, IA86, Z80, VAX11, all have instruction level support for implementing a parameter stack for each function. I have written millions of lines of C code for the PIC16 so how does it do its job without this important part of the language and why do I think this missing features is such a strong strength of the CPU. The secret to the ability of XC8 to produce reasonably efficient C code for the PIC16 and PIC18 without a stack lies in the "compiled stack" feature. This feature analyses the call tree of your program and determines what the stack would look like at any point in the program. Functions that could be in scope at the same time (consider a multiply function in "main" and the interrupt) are duplicated so there are no parameters that need to be in two places at the same time. Any recursive functions are detected and the user alerted. Finally, the complete stack is converted to absolute addresses and mapped into the physical memory of the CPU. Then all the instructions are fixed up with those absolute addresses. This big-picture view of the program also allows the compiler to move parameters around to minimize banking (banking is extra instructions required to reach addresses further than 128 bytes away) and finally, the finished program is ready to run in your application. The amazing thing is the final memory report is the complete memory requirements of the program INCLUDING THE LOCAL VARIABLES. This is a shocker. In fact, when I was looking at the Arduino forums,. I would frequently encounter users who added one more line of code and suddenly their application stopped working. They were told to go buy the bigger CPU. Imagine if your compiler could tell you if the program would fit and that would include all the stack operations. This is a game changer and I would love to see the industry apply this technology across all CPU's. There is no reason why any CPU would not be able to operate with a compiled stack. In fact, most CPU's are capable of operating with both kinds of stacks at the same time. The biggest reason against this sort of operation is really in large project management. Consider, you develop a library for some special feature, perhaps GPS token parsing, Now you want to include this library in all of your applications. You MUST NOT recompile this code because it has passed all of your certification testing and is known good "validated" code. (remember, the compiler is permitted to produce a different binary on each run). If you cannot recompile the code, on some architectures you cannot change the addresses as there could be side effects (banking on a PIC16). If your code only relies upon a stack, then there is never any recompiling required and the linker task is dramatically simplified. Anyway, I must go back into the FreeRTOS world and continue my quest to find the thread with the overflowing stack. Life would be simpler with static analysis tools for the stack. Until next time.,
  • Create New...