Welcome back. In this lab, we're going to apply a PID control system to control a simple LC oscillator circuit. I chose this as a model because it has the same second-order response found in many position control topologies such as robot arms. Building a real motion control system would require a power amplifier and a position encoder. And those parts are a little too expensive for this course, but we can get the exact same response and study everything that a position control system would do using just a simple series connection of an inductor and a capacitor. Our small DC motor will service the inductor, and we need only add a 4.7 microfarad capacitor in series, and we'll get that second-order response. In simple terms, that means there are two integrations between the input and the output. And remember, we get to define multi-input and the output is, and the response of the system in between those two points is very important, particularly, if the output is the integral of the input, that's considered a first-order response. If the output is two integrations of the input, that's a second-order response, and in a position control system, that's typically what you have. So by studying an RC, excuse me, LC oscillator, you get to study the same response that you get with a position control system. In a common motion control topology, a microprocessor writes a number to a circuit, and that circuit is designed to force the motor current to be proportional to that number. And since torque in a DC motor is proportional to current, the number that the microprocessor writes to the circuit becomes the torque in the motor. And divided by inertia, it becomes the acceleration of the motor. And acceleration integrated once is velocity. Integrate again, and you get position. And that is the second-order response of a typical position control system involving a motor and some load. The system is modeled as KT over JS-squared, the S being the plus transform notation for integral. RLC circuit model has the exact same type of response. There are two integrations from the input, which in this circuit is going to come from a DAC, and the output which is going to be the voltage measured at a capacitor. It's just like a position control system but it's easy to model with cheap components. Because this is not a position control class, or excuse me, this is not a controls class, we won't get too far into the mathematics but we will show you how to tune a PID system to get a good step response. We'll also show you how to write the code to look for and prevent common problems such as saturation and overflow. Let's start with the schematic. DAC_1 is a system input and the thing that we will control. The system output will be, by definition, the voltage across the capacitor. For practical reasons, I have used all four PSoC's high-power opamps in parallel to get enough drive current to run this circuit. Note that each is set to the high-power mode and uses the pins on PSoC that have direct low impedance connections to the outputs so that we'll get minimum amount of power loss at the point. You could see the internal connections by clicking on the Analog line item under Design Wide Resources. The output voltage, VCAP, is measured by these excessive approximation ADC, SAR_ADC, set to its fastest conversion speed to minimize phase delay. We have to calculate these numbers in software and that takes a certain amount of time. And it's surprising that even with a 67 megahertz microprocessor, how much phase delay you can build up. Software reads the voltage from the CAP, and calculates the derivative in integral terms. Multiplies those by gain, combines them with the feed forward term, basically just adds in a feed forward term, and adds the proportional integral derivative and feed forward terms together, and outputs that number to DAC_1. That is the input to the system that we want to control. The circuit can be observed using an oscilloscope. You can use the nscope, which I have, but I also have another scope. But also by outputting values using the USB-UART built into PSoC, and running that to a terminal emulation program on your PC. For terminal emulation program, I use Tera Term, but there are others that will work as well. The UART on the schematic, of course, must be set up to match the terminal program settings for data rate, number of stop bits, parity, and so on. Note that the COM port used will vary from PC to PC. So you're going to have to use Windows Control Panel and look for the COM port that is associated with the Cypress dev kit. You're going to have to put that number in the setup for Tera Term, or whatever terminal control program you use. It is not necessary to make another connection to the USB connector on your dev kit. For instance, J6 on the dev kit is a USB micro connection. You don't have to make a connection there, all the communication will occur through the printed circuit board connector that you use to power the board in the first place. Let's start by doing a walkthrough of the code that I use to execute this PID loop. Of course, I start out with a nice title block here and I always encourage my students to indicate what the program is about and who wrote it and when, and so on. We scroll down a little bit here, we'll see there's a bunch of variables that have been defined that are above main.c. That makes these variables global, and while that's considered to be poor practice for production in a test environment like this, it means that the variables will have a permanently assigned place in memory, and therefore, always be available to the debugger regardless of where you stop the code. So that's convenient to do. main.c begins here, and I leave some white space above that and put this line in of dots just to indicate that this is an important place in the code. And we start by powering up the components that are on the PSoC board that includes the ADC and the DACs and the opamps. The function call says Start, but what it really does is just apply power to those parts. Next, we put out some character return and line feed to the UART, and a title line that says PID_Lab, just to show on the terminal emulation program that we have good communication. And I like to start with a simple Power-On Self-Test, where I test the speed of my processor by toggling an output pin. In this case it's P1_6, which is down here on the bottom right of the schematic. And then we began with an infinite loop, and I also have a comment line and some whitespace to indicate this is an important place in the code. I always count every time I go through a loop, only for test purposes, but it's a useful thing to do. The first thing we do is we save the last measurement. Of course, the first time through the loop we don't have a last measurement, so this value will be 0. So there'll be a little glitch there unless we do something to take care of that, and we do. Then I use that same test point, P1_6, to test how long it takes the ADC to do a conversion here. So I start the conversion on line 116, and on line 123 and 124 I wait for the conversion to complete. And notice I have another diagnostic counter here called adc_wait_count, where I simply check to see how long that conversion takes. This is quite useful for test purposes. When the conversion is complete I write the digital test point to 0, and I can use an oscilloscope to verify how long the conversion took by just looking at the time from the pulse being high to the pulse being low. Here on line 128 I save that value. Here I'm incrementing it while the conversion is taking place, and then I save that value so that it's available for me to look at with the debugger later. And if this value is 0, or 1, or 2, I know the conversion is pretty fast compared to how fast the code executes. But if this code sat here and this thing counted up to a couple thousand, I might take a look at the converter and see why it's executing more slow than I expect. Line 132 we simply read the value as an 8-bit number for convenience, even though it's a 12-bit ADC. Just to keep things simple I'll use 8-bit values, and for test purposes I have a DAC labeled DAC_2. It's simply connected to a test point, and I output the value that I read from the ADC to that DAC offset by 127. And the reason I offset is because the DAC only can read unsigned integer values, eight bits, so that's 0 to 255. So if Vmeas_raw is 0, I'm going to write a 127 to the DAC. And that means that the DAC will output a value right in the middle of its range. Take a look at the schematic there, there's DAC_2, it's just hooked up to P2_0, it's just used as an analog test point. This is something very convenient that you can do with the PSOC chip. On line 139 I save that value in a buffer, and I increment the buffer every time I go through the loop. That is just, again, for test purposes. I then create an unsigned 32-bit integer value version of the measurement. Remember, I read this as an 8-bit number, and here I have a 32-bit number. And that makes overflow and underflow problems less apparent, although it doesn't eliminate them. Here and between lines 152 and 155 I take care of that glitch that I would get if the initial measurement was 0. Because when I calculate the delta value, which is the current minus the last, if the last was a 0 and the current was, let's say 100, well that's going to be a pretty big delta. So until I get a valid last measurement, I'll set the delta value to 0. That's the derivative term, by the way, the PID loop. It's the current measurement, Vmeas- Vmeas last. For text purposes, again, I have another DAC, DAC 3, and I write that Vmeasure value to the DAC, offset by 127. But here I multiply it by a gain term, because I don't have an oscilloscope here that can dial up the gain and offset as I want to arbitrarily, so I just do it in code. I multiply it by a gain term, so I could see what the delta term is with some resolution. And here on line 170 is the derivative term. Kd is the derivative gain, it's a fixed value that you can change in code. Times the delta term, and then I divide it by a scale factor. And the reason I do that is, if I multiply first then divide by a scale factor, I could create fractional gains using integer math. For instance, Kd might be 2, and Kd_scale_factor might be 4, so that's 2 divided by 4, a gain of 0.5. We could use floating point math, and I've done this in other labs, but I wanted to keep things fairly simple here and show you how you can do this just using integer math, which is actually a little bit quicker. But that's the derivative term, it's the gain times the delta, which we calculated previously on line 155, divided by scale factor, which is really part of the game. That's the Kd term in the PID control loop. And here's an important test, Technique that you should consider, which is, overflow and underflow can ruin your PID loop. So I'd limit the value of the Kd_term to +/-127. Remember, we read it as an 8-bit value. If I have to limit it positive, I keep track of that, I increment a value called Kd_term_overflow_count. It's purely for diagnostic reasons but it's quite useful. If I limit it, of course, to -127, then I increment a Kd_term_underflow_count. Those are just diagnostic values. So that completes the calculation of the derivative term for the PID control loop. Similarly, we calculate a proportional term. It's done almost exactly the same way, except this time we have a reference term minus the measure. The reference term is what we want the output to be. Measured is what we measure, this is the heart of feedback. We have something we want it to be, and we measure the output, and the difference is the error. The Kp term is just a gain factor times that error. And again, I'm limited to +/-127. And finally, we have a integral term. Notice that the integral term is just summing the errors. So if the error is a small value, let say it's +3, every time we go through the loop we're going to add it into the sum, and we'll very quickly build up Verror_sum to a rather large value. That's integration, that's the heart of integration. We are simply adding the error term to an integral term called Verror_sum. And we have the same sort of limiting and checking for overflow and underflow here that I did with the other variables. And again, here on line 216, The INTERCAL term is just a gain factor times that sum. And the sum is just the integration of the errors. And finally, we have a feedforward term and a little test we can do. We have a switch in the code called feedforward_only. If we set that variable using the debugger, we set it equal to 1, the output will just be the feedforward term. And that's the way to test the system, that's the open loop response. That means we write just a single number to DAC_1 and we watch what the output does. Close loop occurs when this term is zero. And then the Vout is equal to the sum of the feedforward term, the Kp term, the Ki term, and the Kd term, that's feedforward plus PID control. That's it, right there, PID control. And for protection, one more time, I know that the DAC that I'm going to write this number to is a 255 8-bit unsigned DAC. So I have to make sure that this number does not fall outside that range. Otherwise, we got an overflow situation and the DAC could get some crazy values. So I create a new variable called Vout limited. And the reason I created a new variable is so for test purposes. I can see what the Vout is originally, and I can see what I limited it to. I limit it to 0 to 255 because that's the range of the DAC. And if I have to apply that limit, I increment an overflow or an underflow count, and again, that's just for test purposes. If a system is working without saturation, and it's staying within it's linear range, these overflow and underflow counts will all be 0. And here's where I create the system output, DAC_1_SetValue to Vout_limited. Let's go back the schematic here. Where's DAC_1? Well here it is, in the upper left, that's DAC_1. It doesn't have the current capability to drive this circuit here. So I run it through four op-amps in parallel, all connected to their low impedance pins. I put them all together and I drive the inductor, which is basically that DC motor. It has 40 ohms of internal resistance, and I mentioned about 766 micro-Henries of inductance. I have the in series with adherence, says 0.2 micro-Farad, but later I changed it to 4.7 because I've got much better response, so ignore that 0.2. And the output by definition, I get to define it, I defined it as being the output of that capacitor. It goes to port 3_0, and that's into the ADC SAR register. And so that's it, that's PID control. The heart of it is really here on line, what a big here, line 224. There's feed forward term, which is our best guess at the output value that we need modified by PID terms. All of which are a function of the error or the derivative of the error that relative to the reference that we want. In order to test this circuit, I've come up with a little step response test. I simply say, if a loop count is less than some number, I set a digital output pin port 12 bit 3 that is just for test purposes. I can use that as a trigger point for my scope. If the value is less than that, we execute the PID control loop. If it gets to be greater than that, except the loop count is 0, I set this digital test point to 0. I set the output DAC to 0 and then I dump the values that I read for the output from that buffer. So I set the buffer_index to 0, and I dump it out to the terminal. So you can see both on the oscilloscope, but also as numbers read by the SAR ADC what the step response was. Now let's take a look at that and see what that looks like on the scope and on the terminal emulate. For this test, I'm going to put a breakpoint here at line 283. That's after we reach a loop count which exceeds the limit SAMPLE_LIMIT. And we dump the buffer out to the terminal emulator program, we'll just take a break there. That allows us after we take the break to possibly modify variables in the code. And we can do that using this so-called Watch window. You mouse over any variable in the code when the code is stopped, it will show you the value of that variable. And if you right-click, you can put it on a list, and that list will be updated any time the code stops. It won't be updated while the code is running. Well, that might be convenient. However, it will be updated when the code is stopped. Let's take a look at that. In this case, I put a number of terms on the list that I think will be useful. Starting with this value, Vref, that is the ADC count that I want to see at the output. Then after that, I have the Kp, Kd, and Ki terms, the proportional, derivative, integral terms, which I've deliberately patched to 0 here for demonstration purposes. But the feedforward term is 100. So when I start the code, what's going to happen is the DAC is just going to get 100. It's not going to get anything from the PID terms because those gains have all been set to 0. And that will show us the open loop response of the system. I've got a whole bunch of other things here. And since I've run this code a few times, you can see, for instance, that my overflow count for the integral term integrated a few times. And it's shown in red here. And this IDE will show in red, any value that changed since the last time you'd hit the breakpoint, and that's also quite useful. So this is what the output looks like on the PicoScope. You can get a very similar picture with the Enscope, but my PicoScope has little bit better picture quality, so I'm using it here. The red trace here is the voltage on the capacitor. And notice it overshoots a little bit, and then it undershoots, and it settles out rather quickly. We call that a pretty well-damped response. That's the voltage on the capacitor in the circuit shown on the schematic. The green trace is the output from the four op-amps that are tied together that are just providing drive current from the DAC, oops, let me redo that. The green trace is the output of the four op-amps tied together. That's just be the same voltage that I get at the DAC. But it should have a much higher drive current. And I show that here to show when the open loop response is supposed to start. And you notice there's a slight sag here in the voltage at that point. But it's not enough to really upset the circuit. But ideally, there'd be no sag at all here, because this is the driving input to the LC oscillator circuit. So why is this so well-damped? Well, that's because the The DC motor that we're using for an inductor has a pretty high resistance internal, and so this response decays to a steady state pretty quickly. That's analogous, for instance, to a position control system that had a relatively high amount of friction. However, if it didn't have a high amount of friction, this response would oscillate a little bit more. And we can demonstrate that by adding in deliberately some damping that's of the wrong sign. And let me show you what that looks like. So here, I've used the emulator to simply patch the Kd term to 500, and then I restarted the code. So now Kp is 0, Ki is 0, but Kd is 500. And notice that the response is much more oscillatory, so this would be analogous to a position control system that had less friction. Notice what's happened now to the driving function. This is the output of DAC_1 after the four opamps. Notice, it's going up and down because it's responding to this measured output. And notice the phase difference here. It's approximately 90 degrees, as it should be, because we're taking the derivative of the output here, multiplying it by a gain, and then adding it in to the feed forward term, and that's the green trace. That's the driving function. So I've deliberately made the output more oscillatory. Now, I can go back to the watch window and I can change the damping to be the opposite sign and run the code again. And this time, we see much less oscillation, hardly any overshoot at all, and it damps out quickly. That shows that the derivative term is doing exactly what we expected, right now the derivative term is -500. What if I make it twice that? I'll make it -1000. Still the Kp and the Ki is 0, so it's just Kd plus the feed forward term. Run it one more time, and at -1000, there's virtually no overshoot. >> Notice that the output rises without overshoot and stabilizes at 87 counts. >> What is that steady state value? This is where it gets really interesting. Remember that I said we want the output to read 120. That's the output of the analog-to-digital converter. That's the number we want. If you look at the output from the terminal emulator program, you will see that the output is really only going to 87 count. So this red trace here is not getting anywhere near 120, it's pretty low at 87. So what could we do about that? Well, there are several things, we could use full close loop control but let's just try it a step at a time. I'm going to go back here to the Kd term and get rid of that entirely and run it one more time, so just to show you what's the open loop response is and notice where the value of the red traces, I'll put a cursor there. Now, I'm going to increase the feed forward term, which had been 100, and maybe I'll make it like 140. And I'll click run from our break point. It will run once to the break point again. Well, now the output's higher. It's closer to 120, and as a matter of fact, if you look at the output from the terminal emulator, you'll see that the output is now 122. Which tells me that for this system, a feed forward value of 140 is about where you want to be. But we don't always know what all the characteristics of the system are. Maybe some other system, the feed forward value that works best is 100 like we had before, and I'll run that again. So why do you use closed loop control? Well, you can measure this output here and make adjustments. And let's try that next. Here's the reference case again. Remember, the red trace is the capacitor voltage, and in this picture, I've only used the feed forward term. In other words, at time0, the DAC is just set to this value which is 100 counts, it's an 8-bit number and the output responds like this with not feedback at all. All three games are set to 0. I've set the cursor here to show the reference level that we want the output to go to. So in a previous test, I verified that this level is about 120 counts, as measured by the ADC. Now, I'm going to back to the code and patch the value of Kp to be some arbitrary number, and I'm going to pick 500 here. And I'll run through the loop just once. And notice what happens, the steady state value becomes much closer the the desired value. In fact, I see from the terminal emulation program that the steady state value is 112 counts which is getting closer to 120. But notice the behavior of the oscillation. It's a higher frequency and there's a little bit more oscillation. Let's ignore that oscillation for a second and see, well, can I increase the Kp more? I'll go to 1000 units and we'll talk about what these units are later, but right now, they're just counts in a calculation done in software. I'll change it to 1000, I'll go back and run one more time through the loop, and we get maybe just slightly closer to 120, but now the oscillations are getting a little but more amplitude. And we see that we might be running out of headroom here in the forcing function, which is coming from DAC_1, through the four opamps. DAC_1 output is limited to 4.096 volts by hardware, and we're hitting that here at this point. And we're just touching 0 at that point. So we can't go too much further before we get into a nonlinear range. If I go and change the value of Kd to -1000 like we had before, let's see if this helps. Now Kp is 1000, Kd is -1000, still no integral term and I'll run it one more time. And well, that looks quite a bit better. There's less oscillation, there's still some significant overshoot but it damps out quickly and we get pretty close to 120. The value I have here from the terminal emulation program is 116 counts. But notice the forcing function from DAC_1 which is the green trace, we start to see these little spikes. And that has to do with the finite sampling time and calculations that we use for the derivative term. If we keep increasing Kd, and I'll try let's say, we make it -2000 here, and run one more time. Well, now these spikes become a little bit higher. And these are really a nonlinear event. And it shows that the Kd term is so large, it's probably acting on a very small error. There is either 0 or 1, and multiply by the Kd term and add it into the equation. This is the output we get, so the output's getting a little jumpy with a little bit of high frequency. Or, excuse me, the input is. The output still looks pretty good. Just a little bit of overshoot, gets pretty close to 120, and we might be satisfied with that. But we can actually continue to do better by using the integral term. Remember that with these gains, the output has a steady state value, a final value of about 116 counts, that's the red trace. It's a little bit shy of this marker line, the yellow dots here, which is indicating about 120 counts as measured by the ADC. Now, you might imagine that if we can observe that the count is low that maybe we could somehow bump up the forcing function at DAC_0, the input to the system. Bump it up a little bit, maybe slowly, and get that final value to get closer to 120. And that's exactly what the integral term does. Verror_sum is just the integral of the error term. That's the difference between the value we want, Vref, and the value we measure at the ADC. And if I run through this loop once with a Ki term equal to 0 and look at the watch window, I see that Verror_sum has changed. Because we've been adding that little bit of error into Verror_sum the whole time. And it's reading 1476. Units here are arbitrary. We'll talk about how to determine those later. If I run it again, well, the next time through the loop, it's reading 2947. And the third time through the loop, it's reading 4412. But we're not doing anything with it, because we're multiplying it by 0. So to show how this works, I'll set this Verror_sum back to 0 using the watch window, but I'll go over to the Ki term and I'll set that equal to 10. And I'm just guessing what the value is here. If I'm wrong, we'll see it in the result, either we'll see nothing happen or it'll go totally crazy. So I always start with a small number, and then slowly increase it until I see some effect. So I'm going to try a Ki value of 10. Remember, Kp is still 1000, Kd is still -1000. Ki is now 10 and Verror_sum is 0 because I set it back to 0. And I'm going to run once through the loop. And what happens is the final value is now 117 and Verror_sum is 1256. If I run it again, we get closer. Verror_sum is now 2232, indicating that the integral is working, but it's taking time to reach the final value that we want. And in this short pulse, which only takes about 2.5, let's see, milliseconds, it didn't have time to integrate up to the final value we want. But now Verror_sum is sitting at 2232. It has memory of what was done previously. And I'll run the loop again. And now the final value is very close to 120, it's 119. And again, it's 119, bouncing between 119 and 120. This time, it's 120. In practice, because of quantization, you can't really expect to get exactly 120. If you get within a couple of counts, you're doing real well, but notice how excellent this response now is. It rises rapidly. There's a little bit of overshoot, but it damps out quickly, and we flatten out at the desired final value, very close to where we want to be. And as a matter of fact, in this case, it's exact to three digits. Let's go through that one more time, step by step. Here I've set all three control gains, Kp, Kd, Ki, back to 0. But I still have a feedforward term of 100, which means at the start of the task, DAC_1 just gets a value of 100. And the ADC, we'll see this red trace here, which we call the open loop response. The dotted yellow line indicates a Vref value of 120. That's what we read at the ADC and that's what we would like the output to be. The final value of the open loop response is well short of that level. If I set Kp to 1000, I get a much more oscillatory response, but the output gets a lot closer to the reference value of 120. That's because the DAC_1 equation involves the error term, and it sees that the difference between the output and the measured value is not where it wants to be, multiplies by the gain Kp of 1000 and writes that out to DAC_1, and we get quite a bit closer here. Here, I've added a Kd term of -1000. Still no integral term, so Kp is 1000, Kd is -1000, and I have a pretty good response here. It rises rapidly, a little bit of overshoot, but it damps quickly. However, the final value is not quite where I want it to be because I have no integrator term. I'm getting a little bit of noise or spiking on my forcing function from DAC_1, but it's not unacceptably large. Finally, I include a Ki term with a gain of 10, again, arbitrary units here. So Kp is 1000, Kd is -1000, Ki is 10. And now the final value's right on the money. If you look at the terminal emulator program, Tera Term, you see that the final value is 119 or 120, and that's as close as you can get. Do the quantization, you're not going to get 119.5. If you get within one or two counts, that's as good as you can expect. So that's really an excellent step response done with a PID control loop. So what about those gains? What about the units? What does 1000 mean? What does -100 mean? Well, these are always numbers in the code that are either read from this ADC converter here or they're written to one of these DACs. And this DAC here, DAC_1, is what we call the forcing function of the input to the system. And remember, I've used four opamps just to get current gain. The voltage at this point here, the junction of P0_1, P0_0, etcetera is just the same as the voltage there. It just has more current drive capability. So if I write a 100 to this DAC, I have to take into account what the configuration of the DAC is. And if you right-click on that and look at this, you'll see that the maximum output of the DAC is 4.08 volts. So that occurs when we have 255 counts. So 100 counts is going to be a little bit less than 2 volts. So you can convert counts in the code to voltages. And that's how you can do the mathematics rigorously if you're taking control systems class. But I've learned in practice, it's easy, well, not easy, but it is not too difficult to do this experimentally and come up with gains that work quite well, as I did here. This concludes my lecture on PID control systems. I hope you found it useful. In the exam for this class, you'll be shown a series of waveforms, just like the ones you saw here. You'll be asked what changes I've made or what changes I should make to get a desired result. In order to get the right answer, you're going to have to set up the project and make these changes yourself. Thanks for watching.