Continue to Site

Welcome to our site!

Electro Tech is an online community (with over 170,000 members) who enjoy talking about and building electronic circuits, projects and gadgets. To participate you need to register. Registration is free. Click here to register now.

  • Welcome to our site! Electro Tech is an online community (with over 170,000 members) who enjoy talking about and building electronic circuits, projects and gadgets. To participate you need to register. Registration is free. Click here to register now.

PI controller using PIC18F

Status
Not open for further replies.

elec123

New Member
Hi,

I am currently trying to implement a PI controller code to adjust the duty cycle accordingly and maintain a 100V output of a boost converter. I am using a PIC18F4520 and have been reading the application note as guidance...

**broken link removed**

I have done lots of background research on PI control and am now trying to write the code in c, Microchip provide some source code in .asm format but I have only ever used c so its very hard to understand what is going on!

I'm having some difficulties in understanding how the integral term (a_error0:a_error1) is obtained in the code, can anyone help on this?

i understand that the user is meant to define the values of timer_hi and timer_lo, and that when timer0 overflows an interrupt is triggered....but do not know how the interrupt works, and how the a_error0:a_error1 term is calculated?

I have never used interrupts before, so some clear guidance would be very useful!

If anyone has any experience of using PID on a PIC18F chip your help on this would be greatly appreciated....
 
The PID code in AN937 doesn't actually include any code that calculates
error0:error1.

The idea is that you have a routine that measures some variable, and subtracts the desired value, leaving an error in error0:error1

Then the PID code from AN937 is called at regular intervals, possibly with a timer interupt.

The PID code from AN937 then:-

Adds the error value is into the 3 byte accumulator of
a_Error0:a_Error1:a_Error2

Applies limits to stop the accumultor getting too large if the error persists.

Multiplies by ki, the integral gain

Adds that result to the proportional and derivative terms.

(and of course it works out the proportional and derivative terms)

The important thing to understand here is that the frequency with which the PID code is called is one of the most important factors, as it controls the rate at which a_Error0:a_Error1:a_Error2 increases or decreases for any particular error.

I hope this helps.
 
Thanks for your repy diver, just want to confirm a few things with you....

From my understanding, the interrupt routine will contain the code that measures a variable (the output from my boost converter) and subtracts it from a set value (100V), leaving an error in error0:error1, this will be called at time intervals defined in the program code.

The Main PID section of code will include:

Adds the error value is into the 3 byte accumulator of
a_Error0:a_Error1:a_Error2

Applies limits to stop the accumultor getting too large if the error persists.

Multiplies by ki, the integral gain

Adds that result to the proportional and derivative terms.

(and of course it works out the proportional and derivative terms)

Is this the right structure?

You state that:

The important thing to understand here is that the frequency with which the PID code is called is one of the most important factors

Is their a general rule to what frequency should be used in relation to the rate at which the output of the plant (my boost converter) is sampling?

Thanks again for the help!
 
It is the interupt code, the part that is called regularly, that should have the part that adds the error value into the accumulator.

It doesn't matter how often you measure the voltage and subtract the set value, as long as it as least as often as the part that is called regularly.

The part that is called regularly should be called at least 10 times as often as the response time of the system.
 
So if Timer1 interrupt is enabled it will be generated when timer1 overflows.

I am confused as to what code actually needs to be written in the interrupt?

doesn't the PIC calculate the accumulater error when timer1 overflows itself and then the user can use this result in the main?

If you could provide an example of what needs to be written in the interrupt, it would be greatly appreciated....
 
elec123 said:
So if Timer1 interrupt is enabled it will be generated when timer1 overflows.

That is correct.

elec123 said:
I am confused as to what code actually needs to be written in the interrupt?

doesn't the PIC calculate the accumulater error when timer1 overflows itself and then the user can use this result in the main?
The code in AN937 includes the interupt service routine and the PID code. Therefor your main code has to calculate the error, and the result should always be available.

I don't know how the interupt vectoring works on that PIC.

elec123 said:
If you could provide an example of what needs to be written in the interrupt, it would be greatly appreciated....

I think that is the code in AN937. I can't help you with C programming as I use assembler.
 
Thanks for your replies diver, I have now sorted out my interrupt section.

The only trouble i'm having now with my code is when i try to write:

Code:
prop0:prop2 = kp * error0:error1;	// Calculate proportional term
integ0:integ2 = ki * a_error:a_error1; // Calculate integral term
pid_out0:pid_out2 = prop0:prop2 + integ0:integ2; 	// Sum to produce final pid result

i get compiler errors....

I wanted to pick your brain on this as you said you use assembler and i'm trying to use the .asm soure code.

The author of the source code defines these system constants:

Code:
	EXTERN	FXM1616U,FXD2416U,_24_BitAdd,_24_bit_sub	
	EXTERN	AARGB0,AARGB1,AARGB2,AARGB3		
	EXTERN	BARGB0,BARGB1,BARGB2,BARGB3
	
	GLOBAL	error0, error1, pidStat1

and then uses these to calculate the proportional/integral/derivative terms i.e.
Code:
Proportional:
	clrf	BARGB0						
	movff	kp,BARGB1	
	movff	error0,AARGB0							
	movff	error1,AARGB1
	call	FXM1616U			;proportional gain * error	

	movff	AARGB1,prop0		;AARGB2 --> prop0
	movff	AARGB2,prop1		;AARGB3 --> prop1
	movff	AARGB3,prop2		;AARGB4 --> prop2
	return				;return to mainline code

Could you tell me what the FXM1616U term is?

Do i have to split each 24 bit result first into three 8 bit sections before i use the formula, something like this...?

pid_out0= prop0+ integ0;
pid_out1= prop1+ integ1;
pid_out2= prop2+ integ2;

final pid result = pid_out0+pid_out1_pid_out2

Your help on this would be greatly appreciated, as i'm so close to completing my code!!

Thanks
 
FXM1616U is the subroutine that multiplies two 16 bit numbers.

In this case the subroutine is used to multiply an 8 bit number by a 16 bit number.

The result is a 24 bit number, that has to be represented by 3 bytes because it is only an 8 bit processor.

This is just the same as we represent large numbers (more than 9) by using more digits.

You wrote:-

pid_out0= prop0+ integ0;
pid_out1= prop1+ integ1;
pid_out2= prop2+ integ2;

final pid result = pid_out0+pid_out1+pid_out2

That is correct, but you also have to consider the carry bits, and pid_out0 is 256 times as significant as pid_out_1 etc

With assembly language programming you have to consider each byte when adding. In C you can declare A, B and C as 2 byte variables and just write A = B + C and the compiler takes care of it all.

In assembly, you need to have A0, A1, B0, B1, C0 and C1 as bytes

The assembly code is:-

movf B1, w
addwf C1, w
movwf A1

movf B0, w
addwfc C0, w ;This adds in the carry bit from the lower byte
movwf A0

I hope this helps.
 
thanks diver, you've really helped me understand this!

I realised what the FXM1616U was when i built my code successfully and simulated the code and saw the subroutine being called.

In my code i have inserted a line: error = 3 to see if my code was working properly and view the results, but when i use the "Watch" facility in MPLAB i see that the variables (prop, integ, pid_out0, pid_out1, pid_out2, and pid_out) all stay at 0x00 and hence my duty cycle remains at the set value.

I think i must be doing something wrong in my "PImain" section,

I know you use assembler and not c, but i have commented my code, so hopefully you could spot an error that i have made (probably something stupid!)

Code:
#include	<p18f4520.h> 
#include 	<stdio.h>

#define	pid_100		 //using 0 - 100% scale

void main (void);
void PImain (void);
void Initialize(void);
void a2d_conversion(void);
void PI_Interrupt(void);

#pragma interrupt PI_Interrupt
void PI_Interrupt (void)
{

PIR1bits.TMR1IF = 0;	// Clear Timer1 interrupt flag bit

TMR1H=0xF8;		// Reload T1 registers with sampling time
TMR1L=0x2F;

}

// High priority interrupt vector

#pragma code InterruptVectorHigh = 0x08
void
InterruptVectorHigh (void)
{
  _asm
    goto PI_Interrupt //jump to interrupt routine
  _endasm
}

#pragma code

/*************************
 * Define Global Variables
 *************************/


	unsigned int pid_out; 	// unsigned int (4 bytes) 0 - 4294967925
	unsigned int prop;
	unsigned int integ;
	unsigned short int error; 	// unsigned short int (2 bytes) 0 - 65535
	unsigned short int a_error;
	unsigned char integ0; 		// unsigned char (1 byte) 0 - 255
	unsigned char integ1;
	unsigned char integ2;
	unsigned char prop0;
	unsigned char prop1;
	unsigned char prop2;
	unsigned char pid_out0;
	unsigned char pid_out1;
	unsigned char pid_out2;
	unsigned char kp;
	unsigned char ki;
	unsigned char pid_sign;

/************
 * MAIN LOOP
 ************/

void main()
{

Initialize();

	while(1)
	{
		unsigned char dig_input;
		unsigned char dig_output;
		unsigned char setpoint;
	

		ADCON0 = 0x80; 							// Clear ADCON0, A2D starts sampling again, selects A2D pin AN0.

		a2d_conversion();
	
		dig_input = ADRESH;
		dig_input = 130;

		if (dig_input<128) 						// If solar panel (Vin) to boost converter is less than 10V don't boost.
		{
		CCPR1L = 0x64; 							// Sets pwm to 100% this gets inverteted to 0% by driver i.e don't boost.
		CCP1CON = 0x0C; 
		}

	else
			{
	
		CCPR1L = 0x03; 							// Sets pwm duty cycle (86.5%) to achieve 100V at Vin = 15.
		CCP1CON = 0x1C;

		ADCON0 = 0x88; 							// Clear ADCON0 A2D starts sampling again, selects A2D pin AN0.
		a2d_conversion();	             // Converts setpoint to digital value
		
		dig_output = ADRESH; 	// Measured output of plant (output of converter).
	
		error = setpoint - dig_output;   // Difference between setpoint and measured output of plant.
		error = 3; //REMOVE
		if (error= 0) 		// If error = 0 no change in duty cycle is needed.
				{
					//No-op;					// No change to duty cycle is necesaary.
				}
		
		else									// Error exists (Vout is not 100V)
				{

				PImain();
			
				if (pid_sign == 1) 				// Final PI result is positive - need to increase the duty cycle.
					{
					CCPR1L = 0x03 + pid_out;
					CCP1CON = 0x1C + pid_out;
					}
		
				else							// Final PI result is negative - need to decrease the duty cycle.
					{
					CCPR1L = 0x03 - pid_out;
					CCP1CON = 0x1C - pid_out;
					}

				}
			}
	}
}


/*******************
 * Initialisation
 *******************/

void Initialize()
{


	unsigned char err_zero;
	unsigned char a_err_zero;
	unsigned char p_err_sign;
	unsigned char a_err_sign;
	unsigned char a_err_1_lim;
	unsigned char a_err_2_lim;
	unsigned char setpoint;

	TRISA = 0x01; 			//Set port A as outputs
	TRISC = 0x00; 			//Set port C as inputs
	PORTC = 0x00;

	PR2 = 	0x18;			//Set PWM period			
	T2CON = 0x05;			//Enable PWM mode and set prescaler value

	ADCON1 = 0x00;			//Set up A2D
	ADCON1 = 0x02; 	

/******Interrupt******/

	RCONbits.IPEN = 1; 	// Set Priorities
	INTCONbits.GIEL = 0; 	// Disable low priority interrupts
	INTCONbits.GIEH = 1; 	// Enable high priority interrupts

	T1CON = 0x89; 		// Make a 16 bit timer 1:1 prescale and enable the timer
	IPR1bits.TMR1IP = 1; 	// Make timer1 low interrupt high priority
	TMR1H=0xF8;		// Sample at 1KHz
	TMR1L=0x2F;
	PIE1bits.TMR1IE = 1;
	PIR1bits.TMR1IF = 0;	// Clear T1 flag

/*******PI Initialisation******/

	err_zero = 0;			// Start with error not equal to zero
	a_err_zero = 1;			// Start with a_error equal to zero
	p_err_sign = 1;			// Start with previous error = positive
	a_err_sign = 1;			// Start with accumulated error = positive
	pid_sign = 0;			// Start with PID result sign = negative;

	kp = 10;			// Set proportional gain value
	ki = 1;			// Set integral gain value

	setpoint = 98;	             // Define that DC bus should be maintained at 100V

	a_err_1_lim = 0;		// To avoid integral wind-up accumulative error limits are set
	a_err_2_lim = 00;
}


/************
 * PI Main
 ************/

void PImain()
{

prop = kp * error;			// Calculate proportional term	
integ = ki * a_error;		// Calculate integral term

pid_out0 = prop0 + integ0;	// Calculate the pid result for each of the three bytes
pid_out1 = prop1 + integ1;
pid_out2 = prop2 + integ2;

pid_out = pid_out0 + pid_out1 + pid_out2;	// Sums each 8 bit result to find total pid result (24 bits)

}


/*********************************
 * Analogue to Digital Conversion
 *********************************/

void a2d_conversion()
{

unsigned char delay;

delay = 0x05;

ADCON0bits.ADON = 1; // turn A2D on

	while (delay != 0)
		{
			delay -= 1;
		}
	
ADCON0bits.GO = 1; //captures analogue voltage on pin.
	
	while (ADCON0bits.GO)
		{
			//No-op;		
		}
}

Anyone see where i'm going wrong?

Am i right in using the statment #define pid_100 to make the pid result a percentage? does this mean the pid result should be a value between 0-100 or 0-1?

Your help on this would be greatly appreciated,

Thanks
 
When I wrote:-

;----------------------
pid_out0= prop0+ integ0;
pid_out1= prop1+ integ1;
pid_out2= prop2+ integ2;

final pid result = pid_out0+pid_out1+pid_out2

That is correct, but you also have to consider the carry bits, and pid_out0 is 256 times as significant as pid_out_1 etc
;-------------------------

I may have given the wrong idea. I was talking about the general method with assembly programming, but that is not how it is written. The example that I gave shows how two 16 bit numbers are added in assembly, but if you are using C it you can just write pid_out= prop+ integ; and the compiler will do all the tedious stuff of handling all the bytes you need.

I am sorry that I don't understand your code. Specifically, I can't see where the PID code from AN937 or something similar is included.

There are ways of using assembly subroutines with C, and passing data from one to the other, but I can't remember what they are.
 
I think your error is in the line,
if (error = 0)

I think it should be,
if (error == 0)

HTH

Mike.
 
Hi pommie, i literally just spotted that before i checked back onto the forum! thanks for your advice.

I simulated my code again and i'm getting values in my pid variables now, just a matter of putting some code in to apply the pid result to my application.

Thanks guys!
 
I have simulated my code but the variable a_error is remaining at 0, hence the integral term remains at 0 (I have tested my code on my circuit and am producing a constant 80V not 100V with varying input) which implies to me that my integral term is not working correctly....

When timer1 overflows the error should be added to the accumulated error.

As timer1 is interrupt driven, i checked the source code's "pid interrupt" section:

Code:
PidInterrupt:
	GLOBAL	PidInterrupt
	
	btfsc	pidStat1,err_z				;Is error = 00 ?
	return								;YES, done.
	call	GetA_Error					;get a_error, is a_error = 00? reached limits?
	
derivative_ready?
	decfsz	derivCount,f 				;is it time to calculate d_error ?
	bra		skip_deriv					;NO, finish ISR
	call	GetDeltaError				;error - p_error
				
	movlw	derivCountVal				;prepare for next delta error
	movwf	derivCount					;delta error = TMR1H:TMR1L * derivCount

skip_deriv	
	movlw	timer1Hi					;reload T1 registers with constant time count (user defined)
	movwf	TMR1H
	movlw	timer1Lo
	movwf	TMR1L
	return								;return back to the application's ISR
		
	END               					;directive 'end of program'

The only difference i can see from the source code to my interrupt section (see full code from previous post) is that the function "GetA_Error" is called:

Code:
GetA_Error:				
	movff	a_Error0,BARGB0			;load error & a_error 
	movff	a_Error1,BARGB1
	movff	a_Error2,BARGB2
	clrf	AARGB0
	movff	error0,AARGB1
	movff	error1,AARGB2
	
	call	SpecSign				;call routine for add/sub sign numbers
	btfss	pidStat1,mag			;which is greater in magnitude ?
	bra		a_err_zero				;bargb, keep sign as is or both are same sign
	
	bcf		pidStat1,a_err_sign		;aargb, make sign same as error, a_error is negative
	btfsc	pidStat1,err_sign			
	bsf		pidStat1,a_err_sign		;a_error is positive
	
a_err_zero
	bcf		pidStat1,a_err_z		;clear a_error zero flag
	movlw	0
	cpfseq	AARGB0					;is byte 0 = 00
	bra		chk_a_err_limit			;NO, done checking
	
	cpfseq	AARGB1					;is byte 1 = 00
	bra		chk_a_err_limit			;NO, done checking
	
	cpfseq	AARGB2					;is byte 2 = 00
	bra		chk_a_err_limit			;NO, done checking
	bsf		pidStat1,a_err_z		;YES, set zero flag
	
	movff	AARGB0,a_Error0			;store the a_error
	movff	AARGB1,a_Error1	
	movff	AARGB2,a_Error2	
	return							;a_error = 00, return 
	
chk_a_err_limit
	movff	AARGB0,a_Error0			;store the a_error
	movff	AARGB1,a_Error1	
	movff	AARGB2,a_Error2	
	
	movlw	0						;a_error reached limits?
	cpfseq	a_Error0				;Is a_Error0 > 0 ??, if yes limit has been exceeded
	bra		restore_limit			;YES, restore limit value
	
	cpfseq	a_Error1				;Is a_Error1 = 0 ??, if yes, limit not exceeded
	bra		chk_a_Error1			;NO
	return							;YES
chk_a_Error1	
	movlw	aErr1Lim			
	cpfsgt	a_Error1				;Is a_Error1 > aErr1Lim??
	bra		equal_value				;NO, check for a_Error1 = aErr1Lim ?
	bra		restore_limit			;YES, restore limit value	
equal_value
	cpfseq	a_Error1				;a_Error1 = aErr1Lim?
	return							;no, done checking a_error
chk_a_Error2	
	movlw	aErr2Lim				;Yes, a_Error1 = aErr1Lim
	cpfsgt	a_Error2				;Is a_Error2 > aErr2Lim ??
	return							;NO, return to mainline code
	
restore_limit
	clrf	a_Error0				;YES, a_error limit has been exceeded
	movlw	aErr1Lim				
	movwf	a_Error1		
	movlw	aErr2Lim
	movwf	a_Error2	
	return							;return to mainline code

So i'm obviously missing something. Could someone with knowledge of assembler explain what is going on in this function?

i can see that a_error is compared to the limits of a_error to avoid integral wind up but don't understand how the code gets the value of a_error and uses it....

Your help on this would be greatly appreciated!

elec123
 
....anyone?

Oh.. Happened to see your thread..

I'm doing the same thing as u are doing now but i would like to maintain my output at constant 325Vdc.

May I know how u do your integration using C programming for the Integral part as the formula should be I = Ki * integrate of E(t).
 
Status
Not open for further replies.

Latest threads

New Articles From Microcontroller Tips

Back
Top