Demystifying The Use of Table Pointer in SPWM - Application in Sine Wave Inverter
It took quite a while for me to understand exactly how the table pointer works in this case – to retrieve data from the sine table and feed it to the PWM module and generate the required output 50Hz frequency (you can use any frequency but my requirement is 50Hz).
While many have seen my posted codes and explanations regarding sinusoidal pulse width modulation (SPWM), some people have requested me to explain how the table pointer in the interrupt works. Remembering my difficult experience with this, I decided to write up this article to help all of you who are struggling with this to help clear your doubts and make you understand exactly how it works so that one can implement SPWM with a sine table with any number of table values and for any desired output frequency.
Let’s take a look at the code line by line. And I’ll explain it all. Keep in mind that the target microcontroller is a PIC16F684 running off an oscillator frequency of 16MHz. So, if you’re attempting to “port” the code to any other microcontroller keep in mind that the registers will/may be different from those in the PIC16F684.
//----------------------------------------------------------------------------------------
//Programmer: TAHMID
//Target Microcontroller: PIC16F684
//Compiler: mikroC PRO for PIC (Can easily port to any other compiler)
//-----------------------------------------------------------------------------------------
unsigned char sin_table[32]={0,25,49,73,96,118,137,
159,177,193,208,220,231,239,245,249,250,249,245,
239,231,220,208,193,177,159,137,118,96,73,49,25};
unsigned int TBL_POINTER_NEW, TBL_POINTER_OLD, TBL_POINTER_SHIFT, SET_FREQ;
unsigned int TBL_temp;
unsigned char DUTY_CYCLE;
void interrupt(){
if (TMR2IF_bit == 1){
TBL_POINTER_NEW = TBL_POINTER_OLD + SET_FREQ;
if (TBL_POINTER_NEW < TBL_POINTER_OLD){
CCP1CON.P1M1 = ~CCP1CON.P1M1; //Reverse direction of full-bridge
}
TBL_POINTER_SHIFT = TBL_POINTER_NEW >> 11;
DUTY_CYCLE = TBL_POINTER_SHIFT;
CCPR1L = sin_table[DUTY_CYCLE];
TBL_POINTER_OLD = TBL_POINTER_NEW;
TMR2IF_bit = 0;
}
}
void main() {
SET_FREQ = 410;
TBL_POINTER_SHIFT = 0;
TBL_POINTER_NEW = 0;
TBL_POINTER_OLD = 0;
DUTY_CYCLE = 0;
ANSEL = 0; //Disable ADC
CMCON0 = 7; //Disable Comparator
PR2 = 249;
TRISC = 0x3F;
CCP1CON = 0x4C;
TMR2IF_bit = 0;
T2CON = 4; //TMR2 on, prescaler and postscaler 1:1
while (TMR2IF_bit == 0);
TMR2IF_bit = 0;
TRISC = 0;
TMR2IE_bit = 1;
GIE_bit = 1;
PEIE_bit = 1;
while(1);
}
//-------------------------------------------------------------------------------------
Here's the circuit diagram (the hardware portion of SPWM implementation):
Fig. 1 - Circuit Diagram (Click on image to enlarge it)
For the MOSFET drivers, you can use the common high-low side
drivers – IR2110. For a thorough tutorial on using the IR2110, check out:
Now, back to the software side of things. The registers responsible for the automatic updating of the duty
cycle are SET_FREQ, TBL_POINTER_SHIFT, TBL_POINTER_NEW, TBL_POINTER_OLD
and DUTY_CYCLE. So, I’ll get to this
a little later. Let me deal with the rest first.
Setting ANSEL to 0 selects the ADC-associated pins as digital and this is done since the ADC isn’t required for this program, and this frees up the pins multiplexed to the ADC so that they can be used as digital IO lines. Setting CMCON0 to 7 does the same thing, just for the comparator instead of for the ADC.
PORTC bits 0 to 5 are all made input at first. CCP1CON is set to 0x4C. This sets the ECCP mode to “PWM Mode” with P1A, P1B, P1C and P1D set active-high. Timer 2 is turned on and a polling check is done to check if Timer 2 has overflowed or not. When Timer 2 overflows, the interrupt flag is cleared and PORTC bits are selected as output pins. Timer 2 interrupt, global interrupt and peripheral interrupt are enabled.
Now the program just loops in the infinite endless while loop. So, the program now just does nothing – but execute the interrupt when it occurs. Since PR2=249, the time period is 62.5us. I assume you can do the math. So, an interrupt occurs every 62.5us.
Now on to the main part of this code – the interrupt. Every 62.5us the interrupt service routine is “entered” (if you think of it like that in terms of program “flow”) and executed.
In the main() function, I had assigned the value of 410 to SET_FREQ. SET_FREQ determines the output frequency of the sine wave, which in our case here is 50Hz. You’ll soon see how.
In the interrupt, the variable TBL_POINTER_NEW is updated – every 62.5us it is increased by 410. TBL_POINTER_NEW is a 16-bit variable and at the beginning of program execution has a value of 0. So, after the first interrupt it holds 410, after the second interrupt 820, after the third 1230 and so on. After 159 interrupts, TBL_POINTER_NEW holds 65190. So, at the next interrupt, TBL_POINTER_NEW overflows and holds 64.
The interrupt occurs every 62.5us. At each interrupt TBL_POINTER_NEW is increased by 410. At the 160th interrupt, TBL_POINTER_NEW overflows, just as it does the 320th interrupt, the 480th interrupt and so on.
Initially TBL_POINTER_NEW holds 0 and on subsequent interrupts, TBL_POINTER_NEW holds 410, 820, 1230, 1640, 2050, 2460, 2870, 3280, 3690, 4100 and so on.
TBL_POINTER_SHIFT right-shifts TBL_POINTER_NEW eleven times and stores that value. Right shifting 11 times is the same as dividing by 211 (211 = 2048) – just more efficient. When the microcontroller starts up, TBL_POINTER_SHIFT holds 0 and on subsequent interrupts, TBL_POINTER_SHIFT holds 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3 ….. and so on. Now, the trend is that TBL_POINTER_SHIFT holds the same value for 5 consecutive interrupts – go on, you can do the math. Why 5 times? We’ll see that a little later.
After TBL_POINTER_NEW is shifted 11 times, DUTY_CYCLE is assigned the resultant 5-bit value. This acts as – or rather is – the table pointer and the required value is retrieved off the sine table. This value is assigned to CCPR1L, thus setting the appropriate duty cycle.
Now let’s see why TBL_POINTER_SHIFT is needed to be incremented every 5 interrupts and how we’ve done that.
Our frequency is 50Hz – time period is 20ms. A half cycle takes 10ms. Recall that the sine table used is only for half a cycle. So, the 32 values must be called such that they make up the half cycle. The interrupt service routine is executed each 62.5us and so, retrieving all the values from the sine table and using them to update the duty cycle takes a total time of 62.5us * 32 = 2000us (assuming each value is retrieved/called once). That’s 2ms. But our half cycle is 10ms – 5 times larger than what we have now. So, we increase the time five folds. How? By calling each value from the table 5 times instead of only once. So, the time involved is now 32 * 62.5us * 5 = 10000us = 10ms.
This calculation we’ve just done is related to the frequency – 50Hz desired frequency and thus 10ms half cycle. I’ve shown this “backward” to explain why this has been chosen but remember that the microcontroller doesn’t know you want 50Hz frequency. The microcontroller doesn’t know that the desired time is 10ms.
So, now you know the answer to why I’ve called each sine table value 5 times: to ensure that the sine table that has 32 values occupies the entire 10ms. Now, how did I make it repeat 5 times? By setting SET_FREQ to 410. You might be wondering how this works. Well, it works this way.
SET_FREQ = 410. Each interrupt, TBL_POINTER_NEW is increased by 410. Every 5 interrupts, TBL_POINTER_NEW is increased by 2050 (compared to the initial value). 211 = 2048. Since TBL_POINTER_SHIFT divides TBL_POINTER_NEW by 2048, when TBL_POINTER_NEW increases 5*410 times, TBL_POINTER_SHIFT increases once. So, the number of repeats (5 in this case) is equal to the number of interrupts it takes for TBL_POINTER_SHIFT to increase by one, which is equal to the number of times it takes for SET_FREQ to equal or cross 211 (=2048). Since SET_FREQ = 410, the number of times to cross 2048 is 5 and that is the number of times each table value is called. So, the table pointer is incremented every 5 interrupts. Thus SET_FREQ determines the frequency. If we had set SET_FREQ to 200, the number of times to cross 2048 would be 11, as 200*10 = 2000 (less than 2048) but 200*11 = 2200 (more than 2048). Thus each sine table value would be called 11 times – the table pointer would be incremented every 11 interrupts. The output time period of the sine wave would then be 2 * 32 * 62.5us * 11 = 44000us = 44ms. The output frequency would thus be 22.7 Hz instead of the 50Hz we have now.
SET_FREQ can be calculated as {65536/ (32 * 5)} = 409.6 = 410 rounded off to the nearest integer. That’s [216/{(Number of sine table values) * (Number of times each value is to be called)}].
I’ve talked about SET_FREQ, TBL_POINTER_NEW, TBL_POINTER_SHIFT and right shifting eleven times. Now you may ask why eleven times. Why not 5 or 10 or 12 or any other value? The reason is that we have a sine table with 32 values. 32 = 25. Shifting a 16-bit variable eleven times to the right leaves us with a 5-bit value – one between 0 and 31. These 5 bits are the upper most (most significant) 5 bits. This 5-bit value is the sine table pointer. This works in conjunction with the number of sine table values and the value of SET_FREQ to set or determine the output sine wave frequency. If we had 64 sine table values, we would right-shift TBL_POINTER_NEW 10 times instead of 11 so that the shifting operation results in a 6-bit value. 26 = 64 and that is the number of sine table values we have – the table pointer would have a value between 0 and 63.
At the end of this tutorial, I have covered an example. You should follow the example and I’m sure things will be clear by then (I know that the amount of information presented thus far is overwhelming and difficult to grasp!).
In the interrupt, after the duty cycle is set, TBL_POINTER_OLD is assigned the value of TBL_POINTER_NEW. When TBL_POINTER_NEW is first assigned 410, TBL_POINTER_OLD holds 0. The 11-bit shifting is done. The duty cycle is set and then TBL_POINTER_OLD is set. So, the TBL_POINTER_OLD update trails TBL_POINTER_NEW update. So, every interrupt until TBL_POINTER_NEW overflows, TBL_POINTER_NEW is greater than TBL_POINTER_OLD.
When TBL_POINTER_NEW = 64370 and the ISR is to be executed, TBL_POINTER_OLD also holds 64370. TBL_POINTER_NEW is increased to 64780 and then the sine table value is called and TBL_POINTER_OLD is then updated to 64780. The next interrupt, TBL_POINTER_NEW is increased to 65190 and the “if condition” is again false since at that stage TBL_POINTER_OLD holds 64780 – TBL_POINTER_NEW > TBL_POINTER_OLD. Then at the end of the ISR, TBL_POINTER_OLD is assigned 65190. And now comes the interesting part. TBL_POINTER_NEW now holds 65190 and TBL_POINTER_OLD holds 65190. 410 is to added to TBL_POINTER_OLD and TBL_POINTER_OLD overflows to hold 64. Now, TBL_POINTER_NEW (which holds 64) is less than TBL_POINTER_OLD (which holds 65190) and the if condition is true and P1M1 bit of CCP1CON is inverted. This inverts the direction of the full-bridge drive. When P1A and P1D were being driven previously, P1B and P1C will be now be driven and vice versa. By inverting the direction of the full-bridge drive, we are carrying out the next half cycle with the same duty cycle values and sequence, but in the opposite direction – as is necessary (think AC waveform and opposite half cycle). This happens every (32*5 = 160) interrupts – when all the values of the table have each been called 5 times. Since TBL_POINTER_OLD then has a small value again, since it overflowed, TBL_POINTER_SHIFT will then be equal to 0, meaning that the table pointer has restarted from zero and the SPWM starts over again. So, TBL_POINTER_SHIFT, on subsequent interrupts has the values: 0,0,0,0,1,1,1,1,1,2,2,2,2,2,3,3,3,3,3,………..30,30,30,30,30,31,31,31,31,31,0,0,0,0,0,1,1,1,1,1,……. And so on.
Now that I’ve explained how it all works (I have a hunch you haven’t fully grasped everything yet), let’s see how you can make necessary modifications to suit your purpose (and make sure you grasp it all properly).
Now let’s consider a situation where we use 20kHz instead of 16kHz, have 64 sine table values and a sine wave frequency of approximately 30Hz.
We would set the number of shifts to 10 instead of 11 so that the shifting operation results in a six-bit value: 26 = 64, since we have 64 sine table values.
For 20kHz frequency, the time period is 50us. 50us * 64 = 3200us (= 3.2ms). Time period for 40Hz is 25ms – half a cycle is thus 12.5ms. As calculated above, the entire calling of the sine table, if each value is called once, takes a total time of 3.2ms. That’s much less than the required 12.5ms and so, each sine table value should be called more than once. Calling each value four times means a total time of 12.8ms, which is close to 12.5ms (the required calculated time). So, each value is to be called four times. The frequency is (1/{0.0128*2})Hz = 39.1Hz – slightly off our target 40Hz.
Earlier, I had mentioned:
SET_FREQ = [216/{(Number of sine table values) * (Number of times each value is to be called)}]
So, in this case, it’ll be SET_FREQ = [216/{64*4}] = 256.
Now to verify this.
SET_FREQ = 256. 256* 3 = 768 (less than 210). 256 * 4 = 1024 (equal to 210). So, each 4 interrupts, the value of the sine table pointer will increase by 1.
So now we have:
SET_FREQ = 256;
TBL_POINTER_SHIFT = TBL_POINTER_NEW >> 10;
And that’s it. It’s not too difficult once you grab it. The main part of the interrupt is 7 lines only! And it's taken me 6 pages to explain it! And I hope I’ve helped you understand it clearly so now you can edit my code to your requirement or even write a new code from scratch! Let me know your comments and feedback! Happy coding!
Comments