Difference between revisions of "ME 333 Lab 4"

From Mech
Jump to navigationJump to search
 
(92 intermediate revisions by 3 users not shown)
Line 1: Line 1:
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.
In this lab, we are going to use the PIC32 microcontroller to control the motor's position with a proportional feedback controller. Review the lecture on Timers, Interrupts and PWM and what this lab is about on [[Media:Timers_Interrupts_PWM.pdf | this pdf]]. A block diagram of the overview of this lab is shown below.


[[Image: Lab4_Feedback.jpg]]
<br>
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 will be set by your PC, communicating with the PIC32.


The quadrature encoder attached to your motor creates two signals (channels A and B). 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.
<br>
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 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 a motor control signal (u). This is shown in the equation below:
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.


u = kp ( ref - output)
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.
The control signal is then converted into a PWM to cause the motor to move and produce a new output.


So how do we do this?
So how do we do this?
Line 31: Line 33:
==Getting Started==
==Getting Started==
This section details the code required for Feedback Control of Motor Position with the PIC32.
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.
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 [[Media:HelloWorld_NU32.zip|here]]
* Put HardwareProfile.h, HardwareProfile_NU32.h, and procdefs.ld in the folder. These are the same as in HelloWorld. The can be downloaded [[Media:HelloWorld_NU32.zip|here]].
- Create a new MPLAB project using the normal procedure
* 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.
* 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.

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


/*
/*
Line 88: Line 87:


==Motor PWM==
==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...
The PIC32 Output Compare Module has 5 pins (OC1:OC5) that can be used for pulse-width modulation (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.


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.
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.
Line 100: Line 99:
*A2 - digital output for enable pin
*A2 - digital output for enable pin
*A3 - digital output for direction
*A3 - digital output for direction
*A14 - digital output to oscilloscope, to see how long your control calculation takes
*D0 - PWM pin
*D0 - PWM pin


Line 106: Line 106:
#define ENABLE_PIN LATAbits.LATA2
#define ENABLE_PIN LATAbits.LATA2
#define DIRECTION_PIN LATAbits.LATA3
#define DIRECTION_PIN LATAbits.LATA3
#define LOOP_TIME_PIN LATAbits.LATA14


We also want to define constants for the direction. Put these lines of code in the "Constants" section.
We also want to define constants for the direction. Put these lines of code in the "Constants" section.
Line 111: Line 112:
#define REVERSE 1
#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.
Copy and paste the following function into the "Other Functions" section. Note that this function is not complete because there are several things that have X's in them.


void initMotorPWM(void)
void initMotorPWM(void)
{
{
//Set Enable and Direction Pins (A2, A3) as digital outputs
//Set Enable, Direction and Loop Time Pins (A2, A3) as digital outputs
// Initialize as low
// Initialize as low
LATA |= 0xXXXX; TRISA &= 0xXXXX;
LATA |= 0xXXXX; TRISA &= 0xXXXX;
Line 122: Line 123:
OpenOC1( OC_ON | OC_TIMER2_SRC | OC_PWM_FAULT_PIN_DISABLE, 0, 0);
OpenOC1( OC_ON | OC_TIMER2_SRC | OC_PWM_FAULT_PIN_DISABLE, 0, 0);
// init Timer2 mode and period (PR2) // produces 1ms period
// init Timer2 mode and period (PR2) // set for 20kHz
OpenTimer2( T2_ON | T2_PS_1_X | T2_SOURCE_INT, 0xXXXX);
OpenTimer2( T2_ON | T2_PS_1_X | T2_SOURCE_INT, 0xXXXX);
Line 131: Line 132:
*Determine the Hex Number to initialize A2 and A3 as digital outputs and leave everything else alone.
*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.
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:
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)
Period = [(PR + 1) Tpb (TMR_Prescaler_Value)]

Frequency = 1 / Period
Frequency = 1 / Period
where Tpb is the period of the peripheral bus (1/80Mhz for our PIC32)
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.
To complete our initMotorPWM() function, we need to determine the prescaler 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 prescaler Value.
* Calculate PR for a 20kHz frequency and a prescalar value of 1.
* Calculate PR for a 20kHz frequency and a prescaler 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)
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 prescaler 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 prescaler 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)
*With the PR, put that number for the second input. (It's 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.
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
*Put the following line of code in the "Constants" section:
#define MAX_RESOLUTION 0xXXXX // Proportional to period of PWM
#define MAX_RESOLUTION 0xXXXX // Proportional to period of PWM
where 0xXXXX is the hex value for the calculated PR.
where 0xXXXX is the hex value for the calculated PR.
Line 154: Line 156:
*Put the following line of code after mInitAllLEDs(); in your main function
*Put the following line of code after mInitAllLEDs(); in your main function
initMotorPWM();
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.
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==
==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 PWM 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 we are using Timer 3. The period resulting from the prescaler and PR will be the interrupt period.
*Choose a prescaler value and calculate the PR value to produce a 1ms interrupt period. (Remember that PR < 65536). Available prescaler 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. This code reads the encoder and calculates the new control PWM.
* 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 control calculation 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==
==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 down counts 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 prescaler 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 = 500; // period in ms
int refAmplitude = 0; // in encoder counts

The reference period is in ms and is initialized to be a 1/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 want to see on the oscilloscope how long the calculation takes. If it more than 1ms, we know we cannot do our controls at 1kHz.
* Put the following line at the beggining of the Timer 3 interrupt handler:
LOOP_TIME_PIN = TRUE;

Next, we are going to calculate the error based on the reference signal and the encoder information.
*Put the following lines of code in 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:
unsigned 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:
unsigned int setPWMandDirection(signed int error)
{
unsigned 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(MAX_RESOLUTION - pwmMagn);
}
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.

{| border="1"
! style="background:#efefef;" | Direction
!! style="background:#efefef;" | PWM
!! style="background:#efefef;" | 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.

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:
unsigned 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:
unsigned int getPWMmagn(signed int error)
{
unsigned 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 = 50; // 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 > refPeriod)
{
globalIndex = 0;
mLED_1_Toggle();
}

Now let's bring the Loop Time pin low to see how long it takes to run the Timer 3 interrupt service routine (i.e.., how long it takes to calculate and set the next control). You should look at this pin on the oscilloscope to get an idea of how much time you are using in the ISR. This gives you an idea of how complex your control calculation can be before 1 ms is not enough time between control interrupts.
*Put the following line of code above the clear flag function
LOOP_TIME_PIN = FALSE;

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==
==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 (19200) // 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 19200 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 line with the other initialization functions in the main function:
initUART2(pbClk);

*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())
{
// Clear the RX interrupt Flag
mU2RXClearIntFlag();
data = ReadUART2();
// Echo what we just received.
putcUART2(data);
switch(data)
{
case 'f':
kp+=1;
break;
case 'b':
kp-=1;
break;
case 'q':
refAmplitude+=25;
break;
case 'w':
refAmplitude-=25;
break;
case 'p': // record and print data
recordData = TRUE;
recordIndex = 0;
break;
}
// Toggle LED to indicate UART activity
mLED_0_Toggle();
}
// 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:
*'f' - increases the proportional gain
*'b' - decreases the proportional gain
*'q' - increases the amplitude
*'w' - decreases the amplitude
*'p' - collects 640 encoder positions, turns the motor off, and reports the data over RS232

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.

Our final task is to store and send encoder data to the PC. This is initiated by pressing a 'p' on the keyboard.
RS232 communication is slow, so we can't send our data back every 1ms. Therefore, we are going to store 640 data points and then send all the data when we are done collecting. 640 data points is arbitrary, but for this lab you will use a program on your PC to communicate with your PIC and plot the data, and it is coded to accept 640 data points at a time.

*Put the following line of code in the 'Constants' section to indicate the number of sample points.
#define NUM_DATA_POINTS 640

In the interrupt handler for UART2, when a 'p' is pressed, the variable recordData is set to TRUE and recordIndex is set to 0. We need to set up these global variables
*Put the following lines of code in the 'Global Variables' section:
int recordIndex = 0;
int recordData = FALSE;

We are going to store both the reference signal and the encoder signal in two global arrays of size NUM_DATA_POINTS.
*Put the following lines of code in the 'Global Variables' section:
int encoderCounts[NUM_DATA_POINTS];
int referenceData[NUM_DATA_POINTS];

Similar to Lab 2, we are going to use sprintf to make strings with numbers; therefore, we are going to create an RS232 buffer.
*Put this line of code in the 'Global Variables' section:
char RS232_Out_Buffer[32];

*In the timer 3 interrupt, put the following lines of code before the lines about globalIndex.
if(recordData)
{
encoderCounts[recordIndex] = currentPosition;
referenceData[recordIndex] = ref;
if(++recordIndex == NUM_DATA_POINTS)
{
DIRECTION_PIN = FORWARD;
SetDCOC1PWM(0);
sendDataRS232();
}
}

These lines of code will only be run when recordData is true. They store the currentPosition and ref values for NUM_DATA_POINTS. When the recordIndex is equal to NUM_DATA_POINTS, the data is sent with a new function called sendDataRS232(); This is defined below.

*Put the following line of code in the 'Function Declarations' section:
void sendDataRS232(void);
*Put the following lines of code in the 'Other Functions' section:
void sendDataRS232()
{
int i;
recordData = FALSE;
recordIndex = 0;
for(i = 0; i < NUM_DATA_POINTS; i++)
{
sprintf(RS232_Out_Buffer,"%d %d\n",encoderCounts[i], referenceData[i]);
putsUART2(RS232_Out_Buffer);
}
}

This function changes recordData to false.
In a for loop, the encoder count and reference data is sent to the PC.
This function will not be interrupted because it is currently being called in a high priority interrupt, so our 1kHz control algorithm will not operate. The PWM sent to the motor when sendDataRS232() is called will remain, which is not desirable, so we set the motor direction to forward and the PWM to 0 before calling sendDataRS232(), to stop the motor before sending data.

==Programming Summary==
Wow! That was a lot of programming. This section summarizes what you did above. You created code to do the following:

*Initialize:
**PWM based on Internal Timer 2
**1ms Interrupts based on Internal Timer 3
**Encoder Counters based on External Timers 4 and 5
**RS232 Communication using UART2
*Interrupt Every 1 ms to:
**Get the Reference Signal
**Calculate Current Position
**Calculate Error and PWM based on Proportional Controller
*RS232 Interrupt to:
**Increase or decrease the proportional gain, kp
**Increase or decrease the amplitude of the reference signal
**Initiate storage and printing of data
*Store on the PIC32 and Print 640 data points to the PC

While creating this code, you have seen how to create interrupts and create modular code.


=Circuit=
=Circuit=
Some of the motor encoder cables cannot be plugged into your breadboard. Make an adapter out of an 8-pin dip adapter like so:

[[Image:encoder_cable.jpg]]

Download the motor - encoder circuit diagram [[Media: Motor_Encoder_circuit.pdf|here]].

You will need an LS7083 (encoder/decoder), L293D (h-bridge), 3x 0.1 uF capacitor and a 100kOhm resistor.

Your supply voltage (Vs) for the H-bridge can be the 5V of your PIC32 board for this lab. Normally, you would use a higher voltage such as 12 V or 24 V.


=Feedback Control=
=Feedback Control=
Lets play with the proportional gain on of the controller and see how it effects the output position of the motor shaft.

In an analog controller, you can watch the effect of changing the gain on an oscilloscope. On a digital controller, we must save encoder positions in memory and report them back to the PC for plotting.

Visit [http://processing.org/ Processing.org] and download and install Processing ([http://processing.org/download/index.html download Processing]). Processing is an easy to learn IDE based on Java. We will use it to open a serial port on your PC and communicate with the PIC32. Encoder data from the PIC32 will be plotted when you hit a button in Processing.

Download [[Media:processing_lab4.zip | this zip]] containing the Processing code and images for this lab. Unzip processing_lab4.zip in the Processing folder in My Documents. Open Processing and select File->Open and navigate to me333_lab4.pde. This code will do all the communication and plotting work for you. We will go over how to write your own code in Processing soon.

The only thing you may have to change in this code is which COM port to use. Follow the documentation in the code or see the TA if you need to change the COM port.

This code can send the characters p, f, b, q and w over the serial line to the PIC. You have coded your PIC to react to these characters.

[[Image:processing_motor_control.jpg]]

*p collects 640 data points at 1kHz and plots the encoder position and reference position so you can see how well your controller is doing
*f will increment Kp by 1
*b will decrement Kp by 1
*q will increment amplitude by 25
*w will decrement amplitude by 25

Play around with your PIC and Processing code.

Bring your circuit and code to Lab and demonstrate it to the TA. Turn in your PIC code and several screenshots of your controller from Processing.

Latest revision as of 19:41, 25 January 2010

In this lab, we are going to use the PIC32 microcontroller to control the motor's position with a proportional feedback controller. Review the lecture on Timers, Interrupts and PWM and what this lab is about on this pdf. A block diagram of the overview of this lab is shown below.

Lab4 Feedback.jpg
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 will be set by your PC, communicating with the PIC32.

The quadrature encoder attached to your motor creates two signals (channels A and B). 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 a motor control signal (u). This is shown in the equation below:

u = kp ( ref - output)

The control 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.

Create a new project folder and call it MotorPositionController.

  • Put HardwareProfile.h, HardwareProfile_NU32.h, and procdefs.ld in the folder. 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 modulation (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.

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
  • A14 - digital output to oscilloscope, to see how long your control calculation takes
  • 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 
#define LOOP_TIME_PIN		LATAbits.LATA14

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 things that have X's in them.

void initMotorPWM(void)
{
	//Set Enable, Direction and Loop Time 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) // set for 20kHz
	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)]

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 prescaler 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 prescaler Value.

  • Calculate PR for a 20kHz frequency and a prescaler 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 prescaler value of 1. (Note that you don't need to for this section, but we will in the next section.)

  • With the prescaler value, fix the X in that constant.
  • With the PR, put that number for the second input. (It's 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 PWM 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 we are using Timer 3. The period resulting from the prescaler and PR will be the interrupt period.

  • Choose a prescaler value and calculate the PR value to produce a 1ms interrupt period. (Remember that PR < 65536). Available prescaler 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. This code reads the encoder and calculates the new control PWM.

  • 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 control calculation 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 down counts 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 prescaler 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 = 500; // period in ms
int refAmplitude = 0; // in encoder counts

The reference period is in ms and is initialized to be a 1/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 want to see on the oscilloscope how long the calculation takes. If it more than 1ms, we know we cannot do our controls at 1kHz.

  • Put the following line at the beggining of the Timer 3 interrupt handler:
LOOP_TIME_PIN = TRUE;

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

  • Put the following lines of code in 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:
unsigned 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:
unsigned int setPWMandDirection(signed int error)
{
	unsigned 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(MAX_RESOLUTION - pwmMagn);
	}
	
	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.

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:
unsigned 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:
unsigned int getPWMmagn(signed int error)
{
	unsigned 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 = 50; // 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 > refPeriod)
{
	globalIndex = 0;
	mLED_1_Toggle();
}

Now let's bring the Loop Time pin low to see how long it takes to run the Timer 3 interrupt service routine (i.e.., how long it takes to calculate and set the next control). You should look at this pin on the oscilloscope to get an idea of how much time you are using in the ISR. This gives you an idea of how complex your control calculation can be before 1 ms is not enough time between control interrupts.

  • Put the following line of code above the clear flag function
LOOP_TIME_PIN = FALSE;

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    	(19200)      // 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 19200 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 line with the other initialization functions in the main function:
initUART2(pbClk);
  • 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())
	{
		// Clear the RX interrupt Flag
	    mU2RXClearIntFlag();
	
		data = ReadUART2();
		// Echo what we just received.
		putcUART2(data);
		
		switch(data)
		{

			case 'f':
				kp+=1;				
				break;
			case 'b':
				kp-=1;			
				break;
			case 'q':
				refAmplitude+=25;				
				break;
			case 'w':
 				refAmplitude-=25;			
				break;
			case 'p': // record and print data
				recordData = TRUE;
				recordIndex = 0;
				break;	
		}			
 
		// Toggle LED to indicate UART activity
		mLED_0_Toggle();
 
	}

	// 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:

  • 'f' - increases the proportional gain
  • 'b' - decreases the proportional gain
  • 'q' - increases the amplitude
  • 'w' - decreases the amplitude
  • 'p' - collects 640 encoder positions, turns the motor off, and reports the data over RS232

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.

Our final task is to store and send encoder data to the PC. This is initiated by pressing a 'p' on the keyboard. RS232 communication is slow, so we can't send our data back every 1ms. Therefore, we are going to store 640 data points and then send all the data when we are done collecting. 640 data points is arbitrary, but for this lab you will use a program on your PC to communicate with your PIC and plot the data, and it is coded to accept 640 data points at a time.

  • Put the following line of code in the 'Constants' section to indicate the number of sample points.
#define NUM_DATA_POINTS 640

In the interrupt handler for UART2, when a 'p' is pressed, the variable recordData is set to TRUE and recordIndex is set to 0. We need to set up these global variables

  • Put the following lines of code in the 'Global Variables' section:
int recordIndex = 0;
int recordData = FALSE;

We are going to store both the reference signal and the encoder signal in two global arrays of size NUM_DATA_POINTS.

  • Put the following lines of code in the 'Global Variables' section:
int encoderCounts[NUM_DATA_POINTS];
int referenceData[NUM_DATA_POINTS];

Similar to Lab 2, we are going to use sprintf to make strings with numbers; therefore, we are going to create an RS232 buffer.

  • Put this line of code in the 'Global Variables' section:
char RS232_Out_Buffer[32];
  • In the timer 3 interrupt, put the following lines of code before the lines about globalIndex.
if(recordData)
	{
		encoderCounts[recordIndex] = currentPosition;
		referenceData[recordIndex] = ref;
		if(++recordIndex == NUM_DATA_POINTS)
		{
			DIRECTION_PIN = FORWARD;
			SetDCOC1PWM(0);
			sendDataRS232();
		}
	}

These lines of code will only be run when recordData is true. They store the currentPosition and ref values for NUM_DATA_POINTS. When the recordIndex is equal to NUM_DATA_POINTS, the data is sent with a new function called sendDataRS232(); This is defined below.

  • Put the following line of code in the 'Function Declarations' section:
void sendDataRS232(void);
  • Put the following lines of code in the 'Other Functions' section:
void sendDataRS232()
{
	int i;
	recordData = FALSE;
	recordIndex = 0;
	for(i = 0; i < NUM_DATA_POINTS; i++)
	{
		sprintf(RS232_Out_Buffer,"%d %d\n",encoderCounts[i], referenceData[i]);
		putsUART2(RS232_Out_Buffer);
	}
}

This function changes recordData to false. In a for loop, the encoder count and reference data is sent to the PC. This function will not be interrupted because it is currently being called in a high priority interrupt, so our 1kHz control algorithm will not operate. The PWM sent to the motor when sendDataRS232() is called will remain, which is not desirable, so we set the motor direction to forward and the PWM to 0 before calling sendDataRS232(), to stop the motor before sending data.

Programming Summary

Wow! That was a lot of programming. This section summarizes what you did above. You created code to do the following:

  • Initialize:
    • PWM based on Internal Timer 2
    • 1ms Interrupts based on Internal Timer 3
    • Encoder Counters based on External Timers 4 and 5
    • RS232 Communication using UART2
  • Interrupt Every 1 ms to:
    • Get the Reference Signal
    • Calculate Current Position
    • Calculate Error and PWM based on Proportional Controller
  • RS232 Interrupt to:
    • Increase or decrease the proportional gain, kp
    • Increase or decrease the amplitude of the reference signal
    • Initiate storage and printing of data
  • Store on the PIC32 and Print 640 data points to the PC

While creating this code, you have seen how to create interrupts and create modular code.

Circuit

Some of the motor encoder cables cannot be plugged into your breadboard. Make an adapter out of an 8-pin dip adapter like so:

Encoder cable.jpg

Download the motor - encoder circuit diagram here.

You will need an LS7083 (encoder/decoder), L293D (h-bridge), 3x 0.1 uF capacitor and a 100kOhm resistor.

Your supply voltage (Vs) for the H-bridge can be the 5V of your PIC32 board for this lab. Normally, you would use a higher voltage such as 12 V or 24 V.

Feedback Control

Lets play with the proportional gain on of the controller and see how it effects the output position of the motor shaft.

In an analog controller, you can watch the effect of changing the gain on an oscilloscope. On a digital controller, we must save encoder positions in memory and report them back to the PC for plotting.

Visit Processing.org and download and install Processing (download Processing). Processing is an easy to learn IDE based on Java. We will use it to open a serial port on your PC and communicate with the PIC32. Encoder data from the PIC32 will be plotted when you hit a button in Processing.

Download this zip containing the Processing code and images for this lab. Unzip processing_lab4.zip in the Processing folder in My Documents. Open Processing and select File->Open and navigate to me333_lab4.pde. This code will do all the communication and plotting work for you. We will go over how to write your own code in Processing soon.

The only thing you may have to change in this code is which COM port to use. Follow the documentation in the code or see the TA if you need to change the COM port.

This code can send the characters p, f, b, q and w over the serial line to the PIC. You have coded your PIC to react to these characters.

Processing motor control.jpg

  • p collects 640 data points at 1kHz and plots the encoder position and reference position so you can see how well your controller is doing
  • f will increment Kp by 1
  • b will decrement Kp by 1
  • q will increment amplitude by 25
  • w will decrement amplitude by 25

Play around with your PIC and Processing code.

Bring your circuit and code to Lab and demonstrate it to the TA. Turn in your PIC code and several screenshots of your controller from Processing.