ME 333 Lab 4

From Mech
Revision as of 20:39, 21 January 2010 by Andrew Long (talk | contribs) (→‎RS232)
Jump to navigationJump to search

In this lab, we are going to use the PIC32 microcontroller to control the motor's position with a proportional feedback controller. A block diagram of the overview of this lab is shown below.



The PIC32 is going to generate a square wave reference signal (ref) that will cause the motor to alternate between two positions. This reference signal could be changed to any arbitrary signal such as a sinusoid or a triangle wave, but for this lab we are going to stick with a square wave. The period and amplitude of the square wave will be analog inputs to the PIC32.

The quadrature encoder attached to your motor creates two signals similar to those shown below. These signals will be sent to a encoder/decoder chip (LS7083) that converts the signals into up and down counts to be sent to the PIC32. The up and down counts will be read by the PIC32 and converted into a position for the motor (output). This algorithm will be explained in further detail in the encoder section.

The position error is the difference between the reference signal and the output. The error will be sent to a controller, in this case a proportional controller and converted into an input signal (u). This is shown in the equation below:

The input signal is then converted into a PWM to cause the motor to move and produce a new output.

So how do we do this?

First, we are going to write the code in which we will learn about the following programming topics:

  • Timers
  • Interrupts
  • PWM

After programming the PIC32, you will construct the circuit for this motor controller. This circuit will include the following hardware pieces:

  • H-bridge
  • Motor with Encoder
  • Encoder/Decoder Chip

After the circuit has been created, we will test the feedback control with different proportional gains (Kp).


Programming

Getting Started

This section details the code required for Feedback Control of Motor Position with the PIC32.

Overview: write something here

Create a new project folder and call it MotorPositionController. - Put HardwareProfile.h, HardwareProfile_NU32.h, and procdefs.ld. These are the same as in HelloWorld. The can be downloaded here - Create a new MPLAB project using the normal procedure - Make a new file in MPLAB and save as "MotorPositionController.c" without the quotes. This will be our main c file.

- Copy and paste the following lines of code that will serve as our template for the code.

/* 
	Motor Position Control
	Lab 4
*/

/** INCLUDES ***************************************************/
#include "HardwareProfile.h"

/** Constants **************************************************/

#define TRUE 		1
#define FALSE		0

/** Function Declarations **************************************/


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

/** Main Function **********************************************/

int main(void)
{
	int	pbClk;
		
	// Configure the proper PB frequency and the number of wait states
	pbClk = SYSTEMConfigPerformance(SYS_FREQ);
		
	// Allow vector interrupts
	INTEnableSystemMultiVectoredInt();
	
	mInitAllLEDs();
	
	while(1)
	{
		
	}

} //end main


/** Interrupt Handlers *****************************************/

/** Other Functions ********************************************/

All of the lines that have /*....*/ are just commented out lines referring to different sections of code. We will fill in these sections. The main.c function currently has SYSTEMConfigPerformance which optimizes the PB frequency and number of wait states. This function also returns the peripheral bus clock frequency, which we will need later. INTEnableSystemMultiVectoredInt() is a function that enables system wide interrupts. Interrupts will be discussed below. We are also initializing the LEDS on the NU32 board and creating an infinite while loop. In fact, this infinite while loop will remain empty for this entire lab. The rest of the code will be taken care of in interrupts (discussed below).

Motor PWM

The PIC32 Output Compare Module has 5 pins (OC1:OC5) that can be used for pulse-width modulution (PWM) output. PWM essentially creates variable voltage across the motor. The PWM period is based on a 16 bit period register of Timer 2 or Timer 3. These two timers can be combined to get a 32 bit period register. In this section, we are going to initialize PWM, Timer2 and create an interrupt based on Timer 3 to update the PWM duty cycle. Describe Interrupts...

All of our initializations could be placed in the main function, but to make our code more modular we are going to create functions to initialze different segments of our code.

In the "Function Declarations" section, put the following line of code:

  void initMotorPWM();

This function will initialize everything we need for PWM.

For this lab, we are going to use 3 pins to control the motor.

  • A2 - digital output for enable pin
  • A3 - digital output for direction
  • D0 - PWM pin

Since, we are using two digital outputs, define the following lines of code in the "Constants" section.Note that the PWM pin does not need a constant because it will be initialized as a PWM pin.

#define ENABLE_PIN		LATAbits.LATA2
#define DIRECTION_PIN		LATAbits.LATA3 

We also want to define constants for the direction. Put these lines of code in the "Constants" section.

#define FORWARD					0
#define REVERSE					1

Copy and paste the following function into the "Other Functions" section. Note that this function is not complete because there are several 4 things that have X's in them.

void initMotorPWM(void)
{
	//Set Enable and Direction Pins (A2, A3) as digital outputs
	// Initialize as low
	LATA |= 0xXXXX; TRISA &= 0xXXXX;
	
	// init OC1 module, on pin D0
	OpenOC1( OC_ON | OC_TIMER2_SRC | OC_PWM_FAULT_PIN_DISABLE, 0, 0);
	
	// init Timer2 mode and period (PR2) // produces 1ms period
	OpenTimer2( T2_ON | T2_PS_1_X | T2_SOURCE_INT, 0xXXXX);
	
}

The first line of code needs to initialize pins A2 and A3 as digital outputs (see lab 2 for digital i/o information).

  • Determine the Hex Number to set A2 and A3 as Low with LATA
  • Determine the Hex Number to initialize A2 and A3 as digital outputs and leave everything else alone.

The second line of code (OpenOC1(), turns on the PWM for OC1 based on Timer2 with no fault pin.

Timer 2 is going to be the source of our PWM period. Timers basically increment a 16-bit variable TMRx where x is the Timer number. OpenTimerX() takes two inputs. The first input is the configuration constants and the second variable is known as the Period Register(PR). The configuration constants above turn on Timer 2, set a prescaler value and determine where the source of the clock is for the Timer. T2_SOURCE_INT indicates that the source of the clock will be internal, so the PB frequency is how fast the TMRx will be incremented. The combination of the prescaler value and PR determine the period of resetting TMRx back to zero. This resetting can be configured to trigger an interrupt flag. Interrupts are discussed in the next section. For PWM, this resetting sends the next pulse width, essentially creating a PWM period. The period of resetting is calculated using the following formula:

Period = [(PR + 1) Tpb (TMR_Prescaler_Value)] (convert to math) Frequency = 1 / Period where Tpb is the period of the peripheral bus (1/80Mhz for our PIC32)

To complete our initMotorPWM() function, we need to determine the prescalar value and the Period register. For PWM, the common frequencies are 5kHz - 40kHz. For this lab, we are going to use 20kHz as the frequency. Basically, a higher PR number results in higher resolution for the duty cycle. Therefore, we want to have the highest PR number we can afford, meaning that we want the lowest Prescalar Value.

  • Calculate PR for a 20kHz frequency and a prescalar value of 1.

The PR number is a 16bit integer, so PR needs to be less than 2^16 - 1 (65536). If you calculate PR to be greater than this value, you will need to increase the prescalar value of 1. (Note that you don't need to for this section, but we will in the next section)

  • With the prescalar value, fix the X in that constant.
  • With the PR, put that number for the second input. (Its nice to convert to a 16bit hex number, so you remember that it can't be greater than 65536)

PR refers to the maximum number you can use for your duty cycle of PWM, therefore, we want to record what this number is.

  • Put the following line of code in the "Constants" section
  #define MAX_RESOLUTION			0xXXXX		// Proportional to period of PWM

where 0xXXXX is the hex value for the calculated PR.

We now need to use the initMotorPWM function in the main function.

  • Put the following line of code after mInitAllLEDs(); in your main function
initMotorPWM();

We also need to turn on (enable) the H-bridge in the main function.

  • Put the following line of code before the infinite while loop
ENABLE_PIN = 1; // Enable the H-bridge
  • For good practice, put the following line of code after the infinite while loop. We can use this function to turn off the PWM, but we won't ever get to this function.
CloseOC1();

The function to set the duty cycle is SetDCOCxPWM(short) where x is the module number and short is the duty cycle. The percentage on will be short / PR * 100 percent. This function will be used in the Feedback Controller section.

At this point, you have now initialized the PWM and a couple digital outputs for controlling the motor. We need to update the duty cycle, which we are going to do in an interrupt service routine in the next section.

Interrupt Controller

In this section, we are going to initialize and create an interrupt to control the main chunk of our code. Interrupt flags can be generated by many different things such as key strokes on the keyboard for RS232, Timer overflows, external pins, etc. When a interrupt flag is generated, the program jumps to an interrupt service routine (ISR) and carries out the lines of code in the ISR before returning to the original code. Essentially, it interrupts (stops) the main code and jumps somewhere else performs an action and then returns to the interrupted line of code. Our code will be sitting in the infinite while loop until an interrupt is generated. When the interrupt is generated, it will carry out several actions such as checking the encoder and updating the duty cycle before returning back to the infinite while loop.

Timer based interrupts are set up similar to the PWM discussed above. We are going to create a new function to initialize this interrupt.

  • Put the following line of code in the "Function Declarations" section:
void initInterruptController();
  • Put the following lines of code in the "Other Functions" section:
void initInterruptController(void)
{
	// init Timer3 mode and period (PR3) // produces 1ms period
	OpenTimer3( T3_ON | T3_PS_1_X | T3_SOURCE_INT, 0xXXXX);
	
	mT3SetIntPriority( 7); 	// set Timer3 Interrupt Priority
	mT3ClearIntFlag(); 		// clear interrupt flag
	mT3IntEnable( 1);		// enable timer3 interrupts
}


The first line of code is the same as that for PWM except for with timer 3. The period resulting form the prescalar and PR will be the interrupt period.

  • Choose a prescalar value and calculate the PR value to produce a 1ms interrupt period. (Remember that PR < 65536). Available prescalar constants for the X are 1, 2, 4, 8, 16, 32, 64, and 256 as shown in timer.h.

The next line of code sets a priority for the interrupts. There will be some cases in which several interrupts are generated at the same time or an interrupt may be generated while another ISR is being carried out. To resolve this mess, priorities are set between 0 and 7. Higher priorities can interrupt lower priorities. If two interrupts are generated at the same time, higher priorities get selected. We want this interrupt to be very important so we are going to set it to 7.

The third line of code clears the interrupt flag, so an interrupt isn't generated immediately upon enabling the interrupt.

The last line enables the interrupt based with Timer 3.

  • Again, we need to include this in our main function. Put the following line below initMotorPWM();
initInterruptController();

Now we need to create our ISR for Timer 3 which is the code that the interrupt goes to every 1 ms.

  • Put the following lines of code in the "Interrupt Handler" section
// interrput code for the timer 3
void __ISR( _TIMER_3_VECTOR, ipl7) T3Interrupt( void)
{
	
	// clear interrupt flag and exit
	mT3ClearIntFlag();
} // T3 Interrupt

This is the skeleton for our Timer 3 interrupt. All interrupt handlers start with 'void__ISR('. The first constant is a vector constant defined in pic32mx460f512l.h. These vector constants indicate what causes the interrupt. For this case, timer 3 causes the interrupt. You will see another example of an interrupt vector for RS232 in the RS232 section. The second input (ipl7) indicates the priority level. This must be the same (7) as the priority level that you set in the initialization of the interrupt. If the priority level is set differently here, the interrupt probably will not work correctly. T3Interrupt is just a label for this interrupt (this is not actually used anywhere, just names the ISR).

In our interrupt, mT3ClearIntFlag() is called to clear the interrupt flag and returns back to the original line of code. Any code added to the interrupt must be placed above mT3ClearIntFlag(); We will fill in the interrupt as we go.

Encoder

This section details initializing encoder counting and a function to return the position of the motor.

  • Again, put the following line of code in the "Function Declarations" section.
void initEncoder(void);

As discussed above, Timers can be incremented internally by the peripheral bus or externally. We are going to use Timers 4 and 5 to count the up and downcounts from our encoder/decoder chip. There are 5 external counter pins labeled TxCK on the PIC32. Since they are associated with specific Timers, we cannot use T2CK and T3CK because these two timers are used for PWM and the Interrupt.

  • Put the following lines of code in the "Other Functions" section
void initEncoder(void)
{
	// init Timer4 and Timer5 mode and periods (PR4, PR5)
	OpenTimer4( T4_ON | T4_PS_1_1 | T4_SOURCE_EXT, 0xFFFF); 
	OpenTimer5( T5_ON | T5_PS_1_1 | T5_SOURCE_EXT, 0xFFFF); 
} 

These two lines of code simply open the timers. If we wanted to have the timers generate an interrupt we could modify the prescaler and the PR value. We are not going to use any interrupts with these timers, so we will set the prescalar to be 1:1 with a PR of 0xFFFF.

We are also going to define a function that calculates the current position of the motor based on these two counters.

  • Put the following line of code in the "Function Declarations" section:
int getEncoderPosition(void);

This function will return a 32 bit integer of the encoder position.

  • Put the following lines of code in the "Other Functions" section:
int getEncoderPosition()
{
	short count0 = ReadTimer4();  // in your routine this must be done at least every 32000 encoder counts to avoid rollover ambiguity
	short count1 = ReadTimer5(); 	

	bigcount += count0 - last0; // add on the recent up-counts, since the last time

	if (count0 < last0)
	{
		bigcount += 65536; // count0 only increments, so if it got lower it must have rolled over
	}

	last0 = count0;

	bigcount -= count1 - last1; // we're not worrying about rollover of the 32 bit bigcount total

	if (count1 < last1)
	{
		bigcount -= 65536;
	}

	last1 = count1; 

	return bigcount;
 }

This function reads the values of the up counts (Timer 4) and the down counts (Timer 5) to calculate the encoder position stored in bigcount. This function accounts for roll-over of the 2 counters, but does not consider roll-over of the 32 bit number bigcount. This function returns the encoder position (bigcount).

Since this function depends on the counts of the previous step and a global encoder position, we need to include three global variables.

  • Put the following lines of code in the 'Global Variables' section:
signed int bigcount = 0; 	// set encoder value initially to zero, it can go + or -
						 	// 32 bit number
short last0 = 0, last1 = 0; // 16 bit number, prev tmr4 and tmr5

Finally, we need to include our initialization in the main function.

  • Put the following line of code after initInterruptController(); in the main function.
initEncoder();

We have now initialized our external counters for the up and down counts and created a function to get our encoder position. This function will be used in the controller section.

Feedback Controller

Before we write our feedback controller, we need to create a reference signal as shown in the block diagram above.

For this lab, we are going to create a function that returns a scalar reference signal value based on a global timing index. The global timing index will be incremented each interrupt cycle of Timer 3. For our case, the global timing index will be a 1ms counter essentially. We are going to use a periodic reference signal with period 'refPeriod' and amplitude 'refAmplitude', so the global timing index will be reset at the end of each reference period.

Several global variables will be needed for the feedback controller and the reference signal.

  • Put the following lines of code in the 'Global Variable' section:
int globalIndex = 0;
int refPeriod = 2000; // period in ms
int refAmplitude = 0; // in encoder counts

The reference period is in ms and is initialized to be a 2 sec period. The reference amplitude is 0, so the motor will not move initially.

We are going to create a function that returns the scalar reference value based on the global index.

  • Put the following line of code in the 'Function Declarations' section:
int getReference(int index);

This function could be based on any type of reference signal. For this lab, we are going to use a simple square wave.

  • Put the following lines of code in the 'Other Functions' section:
int getReference(int index)
{
	if(index > refPeriod/2)
	{
		return refAmplitude;
	}
	else
	{
		return -refAmplitude;
	}

}

We are now going to begin to populate the Timer 3 interrupt handler.

First, we are going to calculate the error based on the reference signal and the encoder information.

  • Put the following lines of code at the beginning of the Timer 3 interrupt handler:
int currentPosition = getEncoderPosition();
int ref = getReference(globalIndex);
signed int error = ref - currentPosition;

With this calculated error, we are going to set the PWM and direction. We will do this in a new function.

  • Put the following line of code in the "Functions Declaration" section:
int setPWMandDirection(signed int error);

For debugging purposes, this function returns the magnitude of the PWM.

  • Put the following lines of code in the "Other Functions" section:
int setPWMandDirection(signed int error)
{
	int pwmMagn;
	
	pwmMagn = getPWMmagn(error);
	
	if (error > 0) 					// Go Forward r > y
	{
		DIRECTION_PIN = FORWARD;
		mLED_2_On();
                SetDCOC1PWM(pwmMagn);
	}
	else						// Go Reverse r < y
	{
		DIRECTION_PIN = REVERSE;
                mLED_2_Off();
		SetDCOC1PWM(~pwmMagn & 0xFFFF);
	}
	
	return pwmMagn;
}

This function begins by calculating the PWM magnitude with another new function (defined shortly).

For our motor control, we are sending a PWM signal to one input of the H-bridge and a direction pin to the other input. For this program, FORWARD is defined as low and REVERSE is defined as high. There are four cases for the motor control and are shown in the following table.

Direction PWM Motion
0 0 Brake
0 1 Forward
1 0 Reverse
1 1 Brake

For a typical PWM signal, the duty cycle (percentage high) is proportional to how fast you want your motor to operate. This works for forward direction because a high in the PWM causes the motor to go forward. However, for the reverse direction, the percentage low is proportional to how fast we want the motor to run. This is essentially a bit-wise inverse of the PWM signal. This is done if the error is less than zero with the ~ operator as shown above.

In the function above, if the error is greater than 0, the PWM is set normally, the Direction Pin is set to FORWARD and LED2 is turned on to indicate go FORWARD. If the error is less than 0, the PWM is inverted, the Direction Pin is set to REVERSE and LED2 is turned off to indicate go REVERSE.

We are now going to create our proportional controller for the getPWMmagn() function.

  • Put the following line of code in the "Function Declarations" section:
int getPWMmagn(signed int error);

This function returns the magnitude of the pwm signal.

For this lab, we are only using proportional control. Other forms of control include integral and derivative terms. This function as well as the set function could be modified to include the derivatives and integrals of the error. We are only going to use proportional control, so the only input is the error.

  • Put the following lines of code in the "Other Functions" section:
int getPWMmagn(signed int error)
{
	int pwmMagn = abs(error) * Kp + Offset; // Proportional Controller

	// condition ? value if true : value if false
	return pwmMagn > MAX_RESOLUTION ? MAX_RESOLUTION : pwmMagn;

}

This function first calculates magnitude of the PWM based on a proportional constant Kp and Offset. There will be a deadband of voltages that the motor will not rotate. Therefore, we are including an offset that will cause an error of zero to be on the verge of rotating. Since we have a MAX_RESOLUTION for the pwm magnitude, the return value is either pwmMagn calculated on the first line or the maximum resolution.

We need to define Kp and Offset.

  • Put the following lines of code in the "Global Variables" section:
int kp = 25; // proportional gain
int offset = 5000; // feedback offset

These variables are set as global variables instead of constants because you may choose to use analog inputs to adjust the gain and offset values.

You can now put the setPWMandDirection() function in the Timer 3 interrupt

  • Put the following line of code in the Timer 3 interrupt after you calculate the error.
setPWMandDirection(error);

As stated above, our reference signal is generated based on a global 1ms timer index. We are going to rezero this global index at the end of each period. These lines of code will toggle LED1 to indicate a new period.

  • Put the following lines of code below the setPWMandDirection() function but above the clear flag function
globalIndex++;
if (globalIndex > period)
{
	globalIndex = 0;
	mLED_1_Toggle();
}

We have now set up our feedback controller based on Timer 3 interrupt. However, it would be nice to change the period and amplitude of the square wave as well as see how our feedback is performing compared to the reference signal. Since we have already learned how to operate RS232, we are going to communicate to your computer using RS232 as discussed in the next section.

RS232

This section details how we are going to use RS232 communication with our motor controller.

  • Define the Baudrate in the "Constants" section
#define DESIRED_BAUDRATE    	(9600)      // The desired BaudRate 
  • Put the following line of code in the "Function Declarations" section, so we can initialize our RS232:
void initUART2(int pbClk);

Note that we are using the pbClk as an input.

  • Put the following lines of code in the "Other Functions" section:
void initUART2(int pbClk)
{
	// define setup Configuration 1 for OpenUARTx
		// Module Enable 
		// Work in IDLE mode 
		// Communication through usual pins 
		// Disable wake-up 
		// Loop back disabled 
		// Input to Capture module from ICx pin 
		// no parity 8 bit 
		// 1 stop bit 
		// IRDA encoder and decoder disabled 
		// CTS and RTS pins are disabled 
		// UxRX idle state is '1' 
		// 16x baud clock - normal speed
	#define config1 	UART_EN | UART_IDLE_CON | UART_RX_TX | UART_DIS_WAKE | UART_DIS_LOOPBACK | UART_DIS_ABAUD | UART_NO_PAR_8BIT | UART_1STOPBIT | UART_IRDA_DIS | UART_DIS_BCLK_CTS_RTS| UART_NORMAL_RX | UART_BRGH_SIXTEEN
	
	// define setup Configuration 2 for OpenUARTx
		// IrDA encoded UxTX idle state is '0'
		// Enable UxRX pin
		// Enable UxTX pin
		// Interrupt on transfer of every character to TSR 
		// Interrupt on every char received
		// Disable 9-bit address detect
		// Rx Buffer Over run status bit clear
	#define config2		UART_TX_PIN_LOW | UART_RX_ENABLE | UART_TX_ENABLE | UART_INT_TX | UART_INT_RX_CHAR | UART_ADR_DETECT_DIS | UART_RX_OVERRUN_CLEAR	

	// Open UART2 with config1 and config2
	OpenUART2( config1, config2, pbClk/16/DESIRED_BAUDRATE-1);	// calculate actual BAUD generate value.
		
	// Configure UART2 RX Interrupt with priority 2
	ConfigIntUART2(UART_INT_PR2 | UART_RX_INT_EN);
}

This sets up RS232 communication with 9600 baudrate, no parity bit, 8 data bits, 1 stop bit and no flow control. It also configures an interrupt based on every character received. The priority is set at 2 for the RS232 interrupt, so its not as important as the timer 3 interrupt.


  • Put the following lines of code in "Interrupt Handlers" section
// UART 2 interrupt handler
// it is set at priority level 2
 void __ISR(_UART2_VECTOR, ipl2) IntUart2Handler(void)
{
	char data;

	// Is this an RX interrupt?
	if(mU2RXGetIntFlag())
	{
		data = ReadUART2();
		// Echo what we just received.
		putcUART2(data);
		
		switch(data)
		{
			case 'r': // reset
				period = 2000; // Change to analog input
				amplitude = 1000; // Change to analog input				
				break;
			case 'f': // increase kp
				Kp+=1;				
				break;
			case 'b': // decrease kp
				Kp-=1;			
				break;
			case 'p': // record and print data
				recordData = TRUE;
				recordIndex = 0;
				break;
			case 's': // stop
				amplitude = 0;
				break;
			
		}			

		// Toggle LED to indicate UART activity
		mLED_0_Toggle();
		
		// Clear the RX interrupt Flag
	    mU2RXClearIntFlag();
	}

	// We don't care about TX interrupt
	if ( mU2TXGetIntFlag() )
	{
		mU2TXClearIntFlag();
	}
}

This interrupt handler is defined with _UART2_VECTOR and ipl2 to indicate that the ISR for RS232 with the second set of pins with a priority of 2. At the moment, this ISR is triggered by both receiving and transmitting characters. The interrupt first checks which type of interrupt flag was generated (receive or transmit). If the interrupt was receive interrupt, it first reads the key pressed and echos it back to the terminal. It then goes into a switch statement depending on the letter. The letters are described below:

  • 'r' - this resets the period and amplitude of the square wave. The period and amplitude maybe analog inputs, but are just constants right now.
  • 'f' - increases the proportional gain
  • 'b' - decreases the proportional gain
  • 'p' - causes the interrupt Timer 3 to save data and print it back to the terminal (discussed in more detail below)
  • 's' - stops the motor by setting the amplitude to zero

LED0 is then toggled to indicate RS232 activity.

If the interrupt was generated by the transmit, this interrupt function just clears the flag and returns to the other code.

Circuit

Feedback Control