Help with PID in AVR assembly for final project. - Page 1
Forum section: electronics / beginners

author: cbc02009
author_rank: Regular Contributor
timestamp: November 25, 2017, 05:06:12 pm
content: Hi guys,For my final project for microcontrollers, I'm designing a PWM controller for a LTC4440 MOSFET driver. (datasheet:http://cds.linear.com/docs/en/datasheet/4440fb.pdf) I'm writing the code (it has to be in assembly for the project) for either an atmega2560 or an atmega328p (I have the arduino uno, arduino mega, and a 328p DIP to work with).Here is my schematic (the attiny85 is a placeholder for the atmega since it has less pins):The circuit takes US wall voltage, and (hopefully) outputs ~20Vdc at ~2-3Amps. I'm starting with just the P controller to make it easy, then will add the ID parts later.The code, as I see it, is split into three parts. The ADC control, the PWM control, and the PID calculations.1. The PWM ControlCode:;Definitions.DEF init1 = r16;Variables used to initialize registers.DEF init2 = r17.DEF init3 = r18.DEF kp = r19.org 0x0000 ;memory (PC) location of reset handlerrjmp Reset.org 0x0020;memory location for T1 Overflow handlerRJMP TIM1_CAPT.org 0x003A;memory location for adc ready handlerRJMP ADC_RDY.org 0x0040; memory location for T3 compare A handlerRJMP TIM3_COMPA;============Reset:;initialize system clock to 16MHzCLILDI init1, 0b1000_0000;enable CLKPR to allow to change frequencyLDI init2, 0b0000_0001;set main prescaler to 8 so it can be seen on cheap oscilliscopeSTS CLKPR, init1STS CLKPR, init2;Initial setup of the PWM signal (timer 3)LDIinit1, 0b1000_0010;Fast PWM mode 14TCCR3ALDIinit2, 0b0001_1001;Set scaler to 1TCCR3BSTSTCCR3A, init1STSTCCR3B, init2LDI init1, 0b0000_0010;turn on Interrupt forTIMSK3LDI init2, 0x00;Max value of PWM (can use 1+2 to make 16-bit)LDIinit3, 0xA0; A0 = 160 = 16MHz/100KhzSTS TIMSK3, init1STS ICR3H,init2STS ICR3L,init3LDIinit2, 100;starting value of OCR1A (Duty Cycle OCR1A/ICR1L)STSOCR3AL, init2LDIinit1, 0xFFOUTDDRE, init1CLR init1STS TCNT1H, init1STS TCNT1L, init1STS TCNT3H, init1STS TCNT3L, init1The PWM needs to output at 100KHz for the LTC4440. So using PWM mode 14, it counts till the value in OCR3AL then turns on the output, then counts to ICR3H/L and resets back to zero and turns off the signal.Running at 16MHz, the atmega2560 only gives me 160 values for the control of the duty cycle (16Mhz/100Khz). I can run the 328p at up to 20MHz, which would give me 200 values, but that's still not a full 8 bits, so would there be any advantage to using the 328p, since I already started writing for the 2560?The ADC:Code:;Set up ADCLDIinit1, 0b0000_0001;should set ADC port to inputSTS DIDR0, init1LDI init1, 0b0000_0000;should disable all other adc portsSTS DIDR2, init1LDI init1, 0b0100_0000;ADMUXVcc reference, ADC input on ADC0 -A0LDI init2, 0b0000_0110;ADCSRBAuto trigger on timer1 overflowLDI init3, 0b1110_1111;ADCSRAinterrupt enable, prescaler 2STS ADMUX, init1STS ADCSRB, init2STS ADCSRA, init3;Set up ADC timer (timer 1)LDI init1, 0b0100_0000;TCCR1ACTC mode max = OCR1ALDI init2, 0b0001_1001;TCCR1Bprescaler 1LDI init3, 0b0000_0001;TIMSK1enable overflow interruptSTS TCCR1A, init1STS TCCR1B, init2STS TIMSK1, init3LDIinit1,0x0F;ICR1AHcountermax = 4000.LDI init2,0xA0;ICR1ALshould trigger every 500us (16Mhz/2/4000)^-1STS ICR1H, init1STS ICR1L, init2The ADC is the easy part. I already have it working. it starts it's conversion when timer 1 overflows automatically, which is ~ once every 500us, and returns a 10bit value that is relative to the internal 5V Vcc.The PID ControlThis is where I could really use some helpSo according to the voltage divider on Vout, I want the reference value for the PID to be 20V * 120k/(120K+620K) = ~3.24V which is an ADC value of (3.24V/5V)*1024 = ~664, correct?I understand that e(t) = ref - y(t) where y(t) is the value read by the adc, so e(t) will give me any value from -360 (664-1024) to 664 (664- 0), then I multiply that by some value kp to get the u(t) that gets fed back into the system. So far so good.What I'm stuck on is what that means for the PWM signal.I think that if the value is over 664, it should shorten the duty cycle to drop the voltage, and if it's below 664, it should increase the duty cycle to raise the voltage. Is that correct?I think I am supposed to set a default duty cycle of 50% and then add u(t) to it. That way if it's negative, it will reduce the duty cycle, and if it's positive it will increase the duty cycle, thereby varying the voltage.How do I jam my 10bit u(t) into my 160 values that I can use for my duty cycle? Do I just rotate right until I get rid of the extra digits?The avr code has fractional multiply, but I have no idea how it works, and I'm not sure if it's useful. I might have bitten off more than I can chew, since he let us pick a project, and I was already trying to design the buck converter circuit with another professor, I chose to do this. Plus we did the whole class on the MSP430, so I made things way harder for myself by switching to AVR, but I don't think I could have done this on the MSP430 since it doesn't have built in multiplication.I have attached a zip file with my schematic and my code. The code under the ADC_READY interrupt is just what I was using to test the ADC to make sure it was working. it lights up 1 - 5 LEDs on port C depending on how many volts the ADC reads. It won't be in the final version of my code.

author: brucehoult
author_rank: Super Contributor
timestamp: November 25, 2017, 06:26:29 pm
content: PID code is reasonably tricky to get right and stable using floating point When I've done PID on an Arduino I've just used FP. A 16 MHz 328 will do about 100,000 software FP operations a second and there are about 10 operations in a PID calculation and 10 kHz control has always been enough for my applications -- hell .. 100 Hz is enough for controlling real world motors etc.PID code is quite a bit more tricky to get right using fixed point calculations. Different values have different precision and are scaled differently. Even if your inputs and outputs are 8 bits you'll probably want 32 bits for some values.Doing it in assembler is just a whole new level, especially as AVR only does 8 x 8 -> 16 multiply so you're going to have to use multiple precision multiplies, adding up the partial products. And shifting/scaling after.It might be useful for you to read what Atmel has to say on the subject:http://www.atmel.com/Images/Atmel-2558-Discrete-PID-Controller-on-tinyAVR-and-megaAVR_ApplicationNote_AVR221.pdfYou can find their code by going tohttp://start.atmel.com/"BROWSE EXAMPLES" enter "pid" in the search box, "OPEN SELECTED EXAMPLE" "{} VIEW CODE".That's in C, using some 16 bit and some 32 bit integers.

author: cbc02009
author_rank: Regular Contributor
timestamp: November 25, 2017, 07:44:36 pm
content: Quote from: brucehoult on November 25, 2017, 06:26:29 pmThank you for taking the time to reply to my comment. Unfortunately, since the class was on assembly the code has to be written in assembly. Also unfortunately, I am more comfortable in assembly than C, as for some reason my college does the C class after the assembly class, and therefore I don't have a whole lot of background in C.I still have time to change my project idea, but it kind of sucks since I'm so far into the project. I'll look into it a little bit more before I give up though.If you have the time, and since I'm having trouble with the C that's in the atmel documents, do you think you could help me understand at least how PID and PWM work together? Am I at least on the right track?

author: IanB
author_rank: Super Contributor
timestamp: November 25, 2017, 07:51:06 pm
content: Quote from: cbc02009 on November 25, 2017, 05:06:12 pmYes, this is one thing textbooks don't often explain about practical implementations of controllers. If you wish to have a proportional-only controller then the output signal has to have a bias point at zero error that it can swing above and below. It would be reasonable to fix this bias point in the middle of the output range.This is only a problem with P-only control. As soon as you add integral action then the integral part of the output automatically provides the necessary bias for the proportional action.

author: brucehoult
author_rank: Super Contributor
timestamp: November 25, 2017, 08:43:30 pm
content: Quote from: cbc02009 on November 25, 2017, 07:44:36 pmFortunately, it's relatively easy to do a simple conversion of C into assembly, and it's even possible to get programs that can do this for you. This would be especially helpful in seeing how operations on 16 bit or 32 bit C types are converted into a series of 8 bit multiplies and adds in assembly.Unless of course you've covered how to do multi-precision arithmetic in assembly. I hope so, because it's going to be essential.

author: Kohlrak
author_rank: Contributor
timestamp: November 26, 2017, 12:10:57 am
content: Quote from: brucehoult on November 25, 2017, 08:43:30 pm-S. You'll pass that to gcc. It'll create a .s file, where .S is assembly source that is input instead of output. You'll find that C gets converted to asm anyway, and you'd be surprised how it works in reality. There's very little fancy math being done, outside of making up for the lack of div, imul (i can't remember off hand, but i think imul doesn't exist on AVR), and idiv instructions.

author: cbc02009
author_rank: Regular Contributor
timestamp: December 05, 2017, 11:08:43 pm
content: Thanks to all of your help, I've almost got the P part finished. I'm just having one problem, and I can't figure it outI have the ADC hooked up to a potentiometer that I can vary between 0 and 5V. It seems like the program runs exactly once, even though I have the ADC set up to auto trigger when the the Timer 1 compare b interrupt flag is set.so if I change the pot while the program is running, nothing changes, but if I reset the chip after changing the pot, the output PWM changes like its supposed to. I think it must be some timing issue or some quirk of the chip I don't know about?Here is the code so far:Code:;; AssemblerApplication1.asm;; Created: 11/18/2017 2:54:17 PM; Author : Chris;;Definitions.DEF PWM_OUT = r14.DEF init1 = r16;Variables used to initialize registers.DEF init2 = r17.DEF init3 = r18.DEF rmp = r19.DEF light = r20.DEF Kp = r21.DEF VrefL = r22;Value used to compare with ADC to create E_t.DEF VrefH = r23.DEF E_t = r24.DEF PWMHIGH = r25;saves the value for PWM cycle so it can be changed.org 0x0000 ;memory (PC) location of reset handlerrjmp Reset.org 0x0020;memory location for T1 Overflow handlerRJMP TIM1_CAPT.org 0x0022RJMP TIM1_COMPA.org 0x003A;memory location for adc ready handlerRJMP ADC_RDY.org 0x0046RJMP TIM3_OVF;============Reset:LDI VrefH, 0x01; Value to compare adc toLDI VrefL, 0x40LDI PWMHIGH, 160LDI Kp, 2;initialize system clock to 16MHzCLILDI init1, 0b1000_0000;enable CLKPR to allow to change frequencyLDI init2, 0b0000_0000;set main prescaler to 8 so it can be seen on cheap oscilliscopeSTS CLKPR, init1STS CLKPR, init2;Set up ADC timer (timer 1)LDI init1, 0b0100_0000;TCCR1ACTC mode max = OCR1ALDI init2, 0b0000_1011;TCCR1Bprescaler 1LDI init3, 0b0000_0111;TIMSK1enable overflow interruptSTS TCCR1A, init1STS TCCR1B, init2STS TIMSK1, init3LDIinit1,0x0F;ICR1AHcountermax = 4000.LDI init2,0xA0;ICR1ALshould trigger every 500us (16Mhz/2/4000)^-1STS OCR1BH, init1STS OCR1BL, init2;Set up ADCLDIinit1, 0b0000_0001;should set ADC port to inputSTS DIDR0, init1;LDI init1, 0b0000_0000;should disable all other adc ports;STS DIDR2, init1LDI init1, 0b0100_0000;ADMUXVcc reference, ADC input on ADC0 -A0LDI init2, 0b0000_0101;ADCSRBAuto trigger on timer1 captureLDI init3, 0b1110_1111;ADCSRAinterrupt enable, prescaler 2STS ADMUX, init1STS ADCSRB, init2STS ADCSRA, init3;Initial setup of the PWM signal (timer 3)LDIinit1, 0b0010_0011;Fast PWM mode 14TCCR3ALDIinit2, 0b0001_1010;Set scaler to 1TCCR3BSTSTCCR3A, init1STSTCCR3B, init2LDI init1, 0b0000_0001;turn on Interrupt forTIMSK3LDI init2, 0x00;Max value of PWM (can use 1+2 to make 16-bit)MOVinit3, PWMHIGH; A0 = 160 = 16MHz/100STS TIMSK3, init1STS ICR3H,init2STS ICR3L,init3LDI init3, 0LDI init1, 0LDIinit2, 100;starting value of OCR3A (Duty Cycle OCR3A/ICR3L)STS OCR3BH, init1STSOCR3AL, init2LDIinit1, 0xFFOUTDDRE, init1CLR init1;STS TCNT1H, init1;STS TCNT1L, init1;STS TCNT3H, init1;STS TCNT3L, init1SEI ; enable global interrupts;Following code is just a test bed for the pwm codeStart:RJMP StartTIM1_CAPT:RETITIM1_COMPA:RETITIM3_OVF:LDI init1, 0LDS light, PORTCORI light, 0x10STS PORTC, lightSTS OCR3BH, init1STS OCR3BL, PWM_OUTRETIADC_RDY:LDI init1, 0;initialize register for use in compareCLI;stop interrupts for 16bit read/writeLDS init2, ADCL;AVR uses dst, src instead of src, dstLDS init3, ADCHPUSH VrefH;store vref to the stack while we mess with itPUSH VrefLSUB VrefL, init2;E(t) = Vref - ADC inputSBC VrefH, init3;subtract the upper bytes (with carry)MOVr1, VrefL;move e(t) into different register to regain Vref valuesMOV r2, VrefHPOP VrefL; Reload value of VrefPOP VrefHBRMI Negative;If e(t) is negative, set PWM value to zeroCPr1, init1CPC r2, init1BREQ Zero;If e(t) = 0 set PWM valute to zeroLDI init1, 0x00;Check for impossible value from 10bit ADC, saturate high (put back 03FF)LDI init2, 0x01CPr1, init1CPC r2, init2BRSH Too_highCALL ADC_Convert;Convert the 10-bit adc value into a value between 0 - 160CALL Round_numberKp_multiplication:;multiply value by KpLDI init1, 0MOV r2, r1;move out of r1, since product is stored in r1:r0MUL r2, kpCPr0, PWMHIGH;check for value higher than PWMHIGH, and saturate if trueCPC r1, init1LDS light, PORTCORI light, 0x02BRSH Too_highLDS light, PORTCORI light, 0x12MOV PWM_OUT, r0JMPcalibration_doneToo_high:LDS light, PORTCORI light, 0x01MOV PWM_OUT, PWMHIGHJMP calibration_doneNegative:LDS light, PORTCORI light, 0x04CLR PWM_OUTJMP calibration_doneZero:JMP Calibration_done;If value is where you want it, don't change anything.Calibration_done:OUT PORTC, lightSEIRETIADC_Convert:; this converts a value from 0-1023 to 0-160 by multiplying by 10,250; and then dividing by 65,536. the division is accomplished by ignoring; the last two bytes of the product.; Have to use subroutine for multiplication, since AVR can only have; a result of sixteen bits, and our result can be up to 24 bits;; Starting conditions:; +---+---+; | R2+ R1| Input number; +---+---+; +---+---+---+---+; | R6| R5| R4| R3| Multiplicant 10,250 = 0x280A; | 00| 00| 28| 0A|; +---+---+---+---+; +---+---+---+---+; |R10| R9| R8| R7| Result; | 00| 00| 00| 00|; +---+---+---+---+;CLR r6;set multiplicantCLR r5LDI rmp, 0x28MOV r4, rmpLDI rmp, 0x0AMOV r3, rmpCLR r10CLR r9CLR r8CLR r7Convert_Start:MOV rmp, r1OR rmp, r2;Check if there are any 1's leftBRNE Shift_DivRET;No 1's, return to programShift_Div:LSR r2;Shift MSB, div by 2ROR r1;Rotate right through carry on MSP430BRCC Skip_Add;don't add if LSB was 0ADD r7, r3;add #s in r6:r5:r4:r3ADC r8, r4ADC r9, r5ADC r10,r6Skip_Add:LSL r3;multiply r6::r3 by 2ROL r4ROL r5ROL r6RJMP Convert_start ;RJMP saves one cycle. go to next bitRound_number:;rounds the number using bit 15CLR rmpLSL r8;rotate bit 15 to carryADC r9, rmp;add the carry to LSbyteADC r10, rmp;add the new carry to MSbyteMOV r2, r10; do the final division by 65536MOV r1, r9RETagain, any help would be greatly appreciated! Thanks!

author: mikerj
author_rank: Super Contributor
timestamp: December 06, 2017, 09:14:28 am
content: You don't appear to be clearing the interrupt flag in your Timer 3 overflow handler (or any of the others, if they are used).