STM32 Duino Grid-Tie PLL

by Poldo in Circuits > Arduino

3694 Views, 5 Favorites, 0 Comments

STM32 Duino Grid-Tie PLL

IMG_1724.jpg

Ever since my journey started using microcontrollers back in 2019, one of my goals was to design a grid-tie inverter. After scouring the internet for all kinds of research papers, tutorials and YouTube videos on the subject, no where could I find a step by step how to phase lock the utilty grid sinewave to the microcontroller's pwm. There are plenty of theoretical papers with a lot of math on the subject but again no nuts & bolts discussion of a practical implementation using a microcontroller. That is why I decided to create an instructable on this topic. This is my first time to create one of these so bare with me and follow along.

For this project I am using an STM32F103C8 microcontroller. The 72Mhz clock speed along with the 16 bit counter resolution,12 bit A to D resolution and the complimentary PWM outputs with dead time make the STM32 a superior choice.

Supplies

STM32F103C8 Microcontroller

Step down transformer

8.2k resistor (x1) - This value is selectable based on the stepdown transformer output voltage

10k resistors (x2)

1k resistors (x2)

1nF capacitor (x1)

SPST pushbutton (x1)

Arduino IDE

The Basic PLL Concept

Basic PLL Structure.png

As I mentioned in the introduction, this will be more of a nuts & bolts approach so I will not go into a mathematical analysis. There are many papers written on this subject and I will include links as we go along in case you want to go for a deeper dive.

To get an idea of how we will implement the Phase Locked Loop (PLL) for our project let us start with the basic structure of a PLL shown above in Figure1. The PLL consists of four major parts;

1) Reference Signal - this is the signal you want to phase lock to. In our case it with be a 60Hz grid sample.

2) A Digitally Controlled Oscillator (DCO) - this is the oscillator that will be phase locked to the grid sample. In our case it will the the internal Timer1 of the STM32F103

3) A Frequency / Phase discriminator - this will output a voltage proportional to the difference in frequency and or phase of the DCO and the grid sample. We will be using a 1/2 Park Transformation to accomplish this and will be discussed in the next step.

4) A Proportional / Integral (PI) controller - this will take the output from the frequency / phase discriminator and compare it to a setpoint and provide feedback to the DCO in order to phase lock it to the reference signal.

For more info about this basic structure please see;

Phase Locked Loop (PLL) in a Software Defined Radio (SDR)

Implementing the PLL

PLL Implementation Block Diagram.png

To see how we go about implementing the PLL structure into our STM32F103 we first need to talk about what a Park Transformation is. In a Park transformation the two-axis orthogonal stationary reference frame quantities are transformed into rotating reference frame quantities. This transformation is expressed by the following equations:

where,

d, q are rotating reference frame quantities.

alpha(α), beta(β) are orthogonal stationary reference frame quantities.

theta(θ) is the rotation angle.

d = (α * cosθ) + (ß * sinθ)

q = (ß * cosθ) - (α * sinθ)

For more info please refer to;

Park, Inverse Park and Clarke, Inverse Clarke Transformations ...

and also;

Direct-quadrature-zero transformation - Wikipedia

So what does this mean and how can we use it? Well simply put, if alpha and beta are sinewaves of equal amplitude and 90º out of phase with each other, then d and q are DC quantities where d = equals the peak amplitude of alpha or beta and q = zero. Therefore, if the phase difference is other than 90º then q = a sinwave who's amplitude and DC offset is proportional to the phase difference. This means that if the phase angle is greater than 90º the q output will be an AC signal with a positive bias. Conversely, if the phase angle is less than 90º the q output will be a AC signal with a negative bias. Therefore, the q output acts a frequency / phase detector with zero = a phase difference 90º. This 90º difference is also referred to as the two signals being in quadrature. Since we only need the q output to implement the PLL we refer to the transformation as a 1/2 Park Transformation.

So now that we have our frequency / phase detector worked out lets look at Figure 2 to see how we go about the full implementation. Timer1 is setup to be the PWM running in center aligned mode with complimentary outputs with dead time. The frequency of Timer1 is determined by our grid frequency and the number of increments we will use to represent the 360º sinewave. For this project I default to a nominal grid frequency of 60Hz and 360 increments such that 1 increment equals 1º. The code allows the user to change these default settings as desired.

A period of 60Hz = 16.666ms and we want to divide that by 360 increments (16.666ms / 360) = 46.3us. Therefore, our Timer1 frequency will be set to a nominal value of (1 / 46.3us) = 21.6kHz. To calculate the nominal overflow value for Timer1 we simply take the clock frequency and divide by the pwm frequency = (72Mhz / 21.6khz) = 3,333 and since we are running center aligned mode we need to divide by 2 again (3,333 / 2) = 1,666. We now need to set up an Interrupt Service Routine(ISR) which will execute when the Timer1 counter reaches the overflow value of 1,666 which will be every 46.3us. All calculations for PLL operation will happen in the ISR. Since there are 360 increments and a full sinewave is 360º the phase difference will be evaluated every 1º. The resolution or max phase jitter can be expressed by the overflow value +/-1 = (16.666ms +/-10us) or (+/-10us / 46.3us) = +/-0.216º. This means we will be able to adjust the phase angle in 1º steps and once adjusted the phase jitter will be a maximum +/-0.216º

There are three arrays that are pre-populated in the setup() section of the code;

sin_theta[] - This array contains the # of increments and = 360º of float sine values of -1 to 1.

cos_theta[] - This array contains the # of increments and = 360º of float cosine values of -1 to 1.

DCO_sinArry[] - This array contains the # of increments and = 360º of integer sine values +/- max value of the grid sample. This will make the grid sample sinewave (α) amplitude equal to the DCO sinewave amplitude (ß).

There are two software counters named;

sinInc - counts from 0-359º

sinInc_90 - starts +90º offset from sinInc and counts from 0-359º

The arrays and software counters are advanced by 1 during each ISR execution. Since the PLL will lock the grid sample and DCO in quadrature (90º apart) sinInc_90 is used to advance the DCO_sinArry to bring both the grid sample and DCO into perfect sync. DCO_sinArry[sinInc_90] is written to the PWM.

Feedback from the 1/2 Park frequency/phase discriminator to the overflow value of Timer1 is handled by the PI controller algorithm. It is a discrete and very basic inplementation. The PI controller is defined as;

Output = Kpe(t) + Ki∫e(t)dt

where;

Kp = proportional gain

e(t) = error (setpoint - process value)

The intregal portion Ki∫e(t)dt can be re-written as KiΣe(t) which is the sum of error over time multiplied by a constant Ki

Thus the Output = Kpe(t) + KiΣe(t) and in order to scale the output and get the sense direction right we use a map() function in the code.

To learn more about PI and PID controllers refer to;

PID Math Demystified - YouTube

Improving the Beginner's PID – Introduction ... - Brett Beauregard


To summerize, initially, on startup, the Timer1 overflow value is 1,666 which corresponds to the DCO running at 60.00Hz. Since the grid sample is going to be 60Hz +/-0.5Hz (a typical utilty grid frequency spec.) the error signal produced by the output of the frequency/phase discriminator will be well within the capture range of the system and the PI controller will act upon Timer 1 overflow value as to pull the DCO towards quadrature. Once the DCO and grid sample are phase locked, the PI controller will maintain an error signal near zero to ensure the PLL is stable.

The Schematic

STM32F103 Schematic.png

The T1 transformer can be any type of stepdown transformer that will step down the grid voltage to anything between 6VAC to 24VAC. My transformer is a 220VAC primary and an 18VAC secondary. The STM32F103 microcontroller is 3.3V device therefore, the R3 resistor is chosen as to keep the grid voltage sample at the microcontollers' A0 input between 0 and 3.3V under all grid condtions. The utility specification for grid voltage variation depends on your provider. Where I live it is 230VAC +/-10%. I allow for double that as safety margin. To calculate R3 compute the following parameters;

Transformer turns ratio N = Vpri / Vsec = 230 / 18 = 12.777

Pri Vmax = 230 * 120% = 276Vrms

Sec Vmax = Pri Vmax / N = 276 / 12.777 = 21.6Vrms

Sec Vpk = Sec Vmax * 1.414 = 21.6 * 1.414 = 30.54Vpk

VR5max = 3.3V (max desired voltage at the analog input)

IR5 = VR5 / R5 = 3.3 / 1000 = 3.3mA


R3 = (Sec Vpk - VR5max / IR5) = (30.54 - 3.3) / .0033 = 8,254Ω

The goal here is to get the largest dynamic range possible for the A to D without exceeding the 3.3V max under worst case conditions.

R1,R2,R4 and R5 form a voltage divider and provides a DC bias voltage of 3.3V / 2 = 1.65V. In software this will be subtracted out so that we can have a true AC +/- grid sample around zero.

C1 is added to reduce noise on the analog input. C2 provides some additional filtering of the 3.3VDC bus.

The S1 pushbutton when pressed sends one cycle (360º) of up to 4 variables to the serial monitor or serial plotter for data capture and display purposes. It provides a means to look at the PLL in action without disturbing the loop itself.

A8 & B13 are the Channel 1 complimentary pair with dead time insterted

A9 & B14 are the Channel 2 complimentary pair with dead time insterted

B1 provides a 60Hz square wave in sync with the grid.

The Breadboard

PLL3_bb.png

The breadboarding is pretty straight forward and nothing special to be concerned about. I like to use a twisted pair to connect the secondary of the transformer T1 and the input of the voltage divider.

Oscilloscope Display

Oscope.jpg

This is probing the grid sample on T1 secondary CH1 and Base squarewave on output PB4 CH2

Data Display

Serial Plot.png

The display of one cycle on the serial plotter after a button press. It dispalys the Grid sample, DCO sinewave, PB4 and the phase error. Feel free to change variables to probe other things.

The Code

/*
 * Timer1 is setup as a center aligned PWM with complimentary outputs and 560ns dead time. It will generate an ISR when the overflow value is reached. All PLL code will run in this interrupt
 * The PLL uses a 1/2 park transform to act as a frequency / phase detector. When V_grid and the reference DCO(Digital Controlled Oscillator) are 90º out of phase 
 * the q output of the park transform will be zero. q is the phase error which is fed to the input of a PI controller
 * The output of the PI controler is mapped over to be the input overflow value for timer1. This closes the loop locking the grid signal and DCO output in quadrature
 * The base squarewave on PB4 and the pwm outputs on PA8,PB13 & PA9,PB14 are in sync with the grid thanks to software counter sinInc_90 which runs +90º relative to counter sinInc
 * The pwm signal is provided on PA8,PB13 & PA9,PB14 and can either be a pure 120Hz Halfsine or it can follow the grid voltage input on PA0
 * Data capture and display is available and will provide one full cycle of data on three variables after a button press as not to disturb the PLL.
 * Calculated constants are based on the user selected constants. Table one shows the effects of different increments on the PLL
 * The default increments is 360 for a good balance between Phase adjust and phase jitter while maintaing a good pwm frequency of 21.6kHz
 */
#include <STM32ADC.h>
STM32ADC myADC(ADC1);
// NOTE: JC Button works just fine ignore the compiler warning
#include <JC_Button.h>// https://github.com/JChristensen/JC_Button
Button PUSH_BUTTON(PB1, 150);
/* Table1
 * Increments | pwm_ovf value | PWM Frequency |   ISR    | Phase Adj Increment  | Phase Jitter
 *    256     |     2343      |   15.365kHz   | 65.08us  |      +/-1.4º         |   +/-0.15º
 *    360     |     1666      |   21.609kHz   | 46.28us  |      +/-1º           |   +/-0.22º
 *    400     |     1500      |   24.000KHZ   | 41.67us  |      +/-0.9º         |   +/-0.24º
 *    512     |     1171      |   30.743kHz   | 32.5us   |      +/-0.7º         |   +/-0.3º
 */
// User selected constants
const int clk_freq = 72E06;// Microcontroller nominal clock frequency
const int incrmnts = 360;// Number of increments for a 360º sine-wave (must be evenly divisable by 2)
const float grid_freq = 60.0;// Nominal grid frequency


/* The phase may need to be compensated for transformer and other circuit effects. This is accomplised by adding or subtracting from the +90º offset
   and will help bring the reference DCO closer in phase with the grid.
   Table1 shows the minimum phase adjust increment based on the total number of full sinewave increments
*/
const float phase_adjust = 2.0;// Phase adjust increment


/*Calculated constants*/
const float grid_freq_upper = grid_freq + .5;// Grid frequency upper bound for pll lock
const float grid_freq_lower = grid_freq - .5;// Grid frequency lower bound for pll lock
const int phase_correction = incrmnts / 4;// corresponds to a 90º phase shift
const int half_incrmnts = incrmnts / 2;
const float grid_per = 1/60.;
const float incrmnt_sec = grid_per/incrmnts;
const int isr_freq = 1/incrmnt_sec;
const int pwm_ovf_edge = clk_freq / isr_freq;// Nominal overflow value for PWM edge aligned mode
const int pwm_ovf_center = (clk_freq / isr_freq)/2;// Nominal overflow value for PWM center aligned mode
const int pwm_ovf_upper = (grid_freq_upper / grid_freq)*pwm_ovf_center;
const int pwm_ovf_lower = (grid_freq_lower / grid_freq)*pwm_ovf_center;


// Global variables
static int DCO_sinArry[incrmnts];//array containg the DCO sine wave
static float cos_theta[incrmnts];//0 +/- 1.0 cosine wave
static float sin_theta[incrmnts];//0 +/- 1.0 sine wave
int pwm_ovf = pwm_ovf_center;// Timer1 overflow value 
int V_grid;// Grid sample
volatile int sinInc;// Software counter 1
volatile int sinInc_90 = phase_correction + phase_adjust;// Software counter 2
int DCO_sine;
int pwmVal;
volatile float mod_factor = 1.0;// Controls the amplitude of the pwm output
float cos_angle;
float sin_angle;


// Data capture and display arrays
int data_arry1[incrmnts];
int data_arry2[incrmnts];
int data_arry3[incrmnts];
int data_arry4[incrmnts];
//***************************


// PLL PI Tuning variables
//**************************
int pll_out;
int phase_error;
int q;
float PI_p, PI_i;
float Kp = 10, Ki = .05;
//**************************


void setup() {
  Serial.begin(115200);
  //calibrate ADC.
  myADC.calibrate(); 
  //myADC.setSampleRate(ADC_SMPR_7_5); // approx 3us per sample
  pinMode(PA0, INPUT_ANALOG);// Grid voltage
  pinMode(PB1, INPUT_PULLUP);//BUTTON
  pinMode(PB3, OUTPUT);// TEST1
  pinMode(PB4, OUTPUT);// 60Hz sq-wave scope probe signal
  pinMode(PB5, OUTPUT);// TEST2
  
  delay(250);
  // Set up  Timer1 for Complimentary PWM on Channel 1 and Channel 2
  Timer1.setPrescaleFactor(1);//1 to 65,536 Freq = (72MHz / Prescale_val=1)
  Timer1.setOverflow(pwm_ovf);
  bitSet(TIMER1_BASE->CCER,2); //Sets CC1NE bit enable complimentary Channel l Output
  bitSet(TIMER1_BASE->CCER,6); //Sets CC2NE bit enable complimentary Channel 2 Output
  //Inserts Dead Time approx 14ns per increment 0 to 128
  //(DTG Bits 7:0)
  //DGT Bit 2&5 = 00100100 = 560ns
  bitSet(TIMER1_BASE->BDTR,2);
  bitSet(TIMER1_BASE->BDTR,5);
    /* Reference Manual p338 - Set CMS bit6 for Center-aligned mode 2. The counter counts up and down alternatively. Output compare
   interrupt flags of channels configured in output (CCxS=00 in TIMx_CCMRx register) are set
   only when the counter is counting up*/
  bitSet(TIMER1_BASE->CR1,6);


  // When CH1 & CH2 are set to outputs then use CH3 or CH4 to get the interrupt
  Timer1.attachInterrupt(TIMER_CH3, TIM1_ISR);
  
  // Initially Shut OFF pwm
  pwmWrite(PA8,0);
  pwmWrite(PA9,0);


  delay(250);
  
  for(int i = 0; i < incrmnts; i++)
  { 
    // Create sine array to near the nominal peak value of the grid sample
    int val = sin(i*M_PI/(half_incrmnts)) * 1700;
    DCO_sinArry[i] = val;
    // Create full sine and cosine arrays -1 to +1
    float val2 = sin(i * M_PI/half_incrmnts);
    sin_theta[i] = val2;
    float val3 = cos(i * M_PI/half_incrmnts);
    cos_theta[i] = val3;
  }
  
  // Wait 2 sec before turing on the PWM GPIO's
  // This will prevent output transients since PA8 and PA9 have already been written to zero during Timer1 setup
  delay(1000);
  pinMode(PA8, PWM);//Timer1 Channel 1(T1C1) output assigned to PA8 and setting it to PWM Output (Q1)
  pinMode(PB13, PWM);//Timer1 Channel 1(T1C1N) complimentary output assigned to PB13 and setting it to PWM Output(Q3)
  pinMode(PA9, PWM);//Timer1 Channel 2(T1C2) output assigned to PA9 and setting it to PWM Output(Q2)
  pinMode(PB14, PWM);//Timer1 Channel 2(T1C2N) complimentary output assigned to PB14 and setting it to PWM Output(Q4)


} //end setup


void TIM1_ISR(){
    gpio_write_bit(GPIOB,3,1);// ISR execution time = 22.2us
    V_grid = analogRead(PA0)-2036;// Compensate for resistor divider error and calibrate to zero by turning off grid voltage and display only v_grid
    
    //Data Capture
    data_arry1[sinInc_90] = gpio_read_bit(GPIOB,4)* 12;// Add an additional variable to display
    data_arry2[sinInc_90] = V_grid * .1;// Normalize for scaling with phase_error
    data_arry3[sinInc] = DCO_sine * .1;// Normalize for scaling with phase_error 
    data_arry4[sinInc_90] = phase_error;


    if(++sinInc >= incrmnts)
    {
      sinInc = 0;//
    } 
    if(++sinInc_90 >= incrmnts)
    {
      sinInc_90 = 0;//
    } 
    
    // PLL
    // Timer1 interupts every 46.28us this corresponds to 60Hz grid taking 360 samples per cycle(46.28us * 360 incrmnts)
    // Variable "sinInc" is advanced by one count for each interrupt up to 359 these counts represent a φ of 1º per cnt
    // Using 1/2 of a park transformation as a frequency/phase detector where as q = (ß*cosφ)-(α * sinφ) where ß= internal oscillator DCO_sinArry[] and α = V_grid
    // cosφ and sinφ are generated in setup() and each are stored in an array.
    // The PI controller keeps Timer1 in sync with the grid by adjusting the timer1 overflow value every increment (1º) and has a max phase jitter of +/- 0.22º
    // maintaing a 90º difference between α and ß resulting in a q = phase error of 0 and a more positve error when grid freq is higher and visa-versa


    // Frequency / Phase Discriminator (1/2 Park Transformation)
    sin_angle = sin_theta[sinInc];
    cos_angle = cos_theta[sinInc];
    DCO_sine = DCO_sinArry[sinInc];
    q = (DCO_sine * cos_angle) - (V_grid * sin_angle);//
    phase_error = -q;
    // PI Controller
    PI_p = Kp * phase_error;
    PI_i = PI_i + (Ki * phase_error);
    pll_out =PI_p + PI_i;
    pll_out = map(pll_out, -1000,1000, pwm_ovf_lower, pwm_ovf_upper);
    pwm_ovf = pll_out;
    
    Timer1.setOverflow(pwm_ovf);


    // Timer1 PWM value is either pure reference sinewave or grid sinewave
    // The mod_factor can control the amplitude of the pwm
    //pwmVal = mod_factor * abs(V_grid);
    pwmVal = mod_factor * abs(DCO_sinArry[sinInc_90]);


    
    // Generate a base square-wave and pwm in sync with the grid      
    if(sinInc_90 < half_incrmnts)
    {
      pwmWrite(PA8, pwmVal);
      pwmWrite(PA9, 0);
      gpio_write_bit(GPIOB,4,1);
    }
    else
    {
      pwmWrite(PA9, pwmVal);
      pwmWrite(PA8, 0);      
      gpio_write_bit(GPIOB,4,0);
    }  
  gpio_write_bit(GPIOB,3,0); 
}


void loop(){
    PUSH_BUTTON.read();
  if (PUSH_BUTTON.wasReleased())
    {
      data_capture_dsply();
    }


} //end loop


void data_capture_dsply(){


      for(int i=0; i < incrmnts; i++)
      {
        Serial.print("PB4:");// Add an additional variable to display and uncomment this line and the next two lines
        Serial.print(data_arry1[i]);
        Serial.print(",");        
        Serial.print("V_grid:");
        Serial.print(data_arry2[i]);
        Serial.print(",");
        Serial.print("DCO_Sine:");
        Serial.print(data_arry3[i]);
        Serial.print(",");        
        Serial.print("Phase_Error:");
        Serial.println(data_arry4[i]);
      }


}

Summary

I hope this project was helpful for some of you out there. One area that use improvement is providing some sort of AGC for the DCO amplitude to track the amplitude of the grid sample this will help minimize the phase error accross all grid conditions. I am continuing my goal of designing a HV grid tie inverter and I hope to share that as a project here in the future. Thank you for taking the time to read about my project.

Cheers!