Author Topic: Help with PID in AVR assembly for final project.  (Read 3928 times)

0 Members and 1 Guest are viewing this topic.

Offline cbc02009Topic starter

  • Regular Contributor
  • *
  • Posts: 74
  • Country: us
Help with PID in AVR assembly for final project.
« on: November 25, 2017, 05:06:12 pm »
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 Control
Code: [Select]
;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 handler
rjmp Reset

.org 0x0020 ;memory location for T1 Overflow handler
RJMP TIM1_CAPT

.org 0x003A ;memory location for adc ready handler
RJMP ADC_RDY
                                       
.org 0x0040 ; memory location for T3 compare A handler
RJMP TIM3_COMPA




;============

Reset:
;initialize system clock to 16MHz
CLI
LDI init1, 0b1000_0000 ;enable CLKPR to allow to change frequency
LDI init2, 0b0000_0001 ;set main prescaler to 8 so it can be seen on cheap oscilliscope
STS CLKPR, init1
STS CLKPR, init2

;Initial setup of the PWM signal (timer 3)
LDI init1, 0b1000_0010 ;Fast PWM mode 14 TCCR3A
LDI init2, 0b0001_1001 ;Set scaler to 1 TCCR3B
STS TCCR3A, init1
STS TCCR3B, init2
LDI init1, 0b0000_0010 ;turn on Interrupt for TIMSK3
LDI init2, 0x00 ;Max value of PWM (can use 1+2 to make 16-bit)
LDI init3, 0xA0 ; A0 = 160 = 16MHz/100Khz
STS TIMSK3, init1
STS ICR3H, init2
STS ICR3L, init3
LDI init2, 100 ;starting value of OCR1A (Duty Cycle OCR1A/ICR1L)
STS OCR3AL, init2
LDI init1, 0xFF
OUT DDRE, init1
CLR init1
STS TCNT1H, init1
STS TCNT1L, init1
STS TCNT3H, init1
STS TCNT3L, init1

The 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: [Select]
;Set up ADC
LDI init1, 0b0000_0001 ;should set ADC port to input
STS DIDR0, init1
LDI init1, 0b0000_0000 ;should disable all other adc ports
STS DIDR2, init1
LDI init1, 0b0100_0000 ;ADMUX Vcc reference, ADC input on ADC0 -A0
LDI init2, 0b0000_0110 ;ADCSRB Auto trigger on timer1 overflow
LDI init3, 0b1110_1111 ;ADCSRA interrupt enable, prescaler 2
STS ADMUX, init1
STS ADCSRB, init2
STS ADCSRA, init3

;Set up ADC timer (timer 1)
LDI init1, 0b0100_0000 ;TCCR1A CTC mode max = OCR1A
LDI init2, 0b0001_1001 ;TCCR1B prescaler 1
LDI init3, 0b0000_0001 ;TIMSK1 enable overflow interrupt
STS TCCR1A, init1
STS TCCR1B, init2
STS TIMSK1, init3
LDI init1, 0x0F ;ICR1AH countermax = 4000.
LDI init2, 0xA0 ;ICR1AL should trigger every 500us (16Mhz/2/4000)^-1
STS ICR1H, init1
STS ICR1L, init2

The 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 Control
This is where I could really use some help

So 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.

 

Online brucehoult

  • Super Contributor
  • ***
  • Posts: 3998
  • Country: nz
Re: Help with PID in AVR assembly for final project.
« Reply #1 on: November 25, 2017, 06:26:29 pm »
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.pdf

You can find their code by going to http://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.
 
The following users thanked this post: cbc02009

Offline cbc02009Topic starter

  • Regular Contributor
  • *
  • Posts: 74
  • Country: us
Re: Help with PID in AVR assembly for final project.
« Reply #2 on: November 25, 2017, 07:44:36 pm »
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.pdf

You can find their code by going to http://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.

Thank 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?
 

Offline IanB

  • Super Contributor
  • ***
  • Posts: 11790
  • Country: us
Re: Help with PID in AVR assembly for final project.
« Reply #3 on: November 25, 2017, 07:51:06 pm »
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.

Yes, 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.
 
The following users thanked this post: cbc02009

Online brucehoult

  • Super Contributor
  • ***
  • Posts: 3998
  • Country: nz
Re: Help with PID in AVR assembly for final project.
« Reply #4 on: November 25, 2017, 08:43:30 pm »
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.

Fortunately, 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.
 

Offline Kohlrak

  • Contributor
  • Posts: 14
  • Country: us
Re: Help with PID in AVR assembly for final project.
« Reply #5 on: November 26, 2017, 12:10:57 am »
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.

Fortunately, 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.

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

Offline cbc02009Topic starter

  • Regular Contributor
  • *
  • Posts: 74
  • Country: us
Re: Help with PID in AVR assembly for final project.
« Reply #6 on: December 05, 2017, 11:08:43 pm »
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 out  :scared:

I 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: [Select]
;
; 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 handler
rjmp Reset

.org 0x0020 ;memory location for T1 Overflow handler
RJMP TIM1_CAPT

.org 0x0022
RJMP TIM1_COMPA

.org 0x003A ;memory location for adc ready handler
RJMP ADC_RDY

.org 0x0046
RJMP TIM3_OVF                     




;============



Reset:
LDI VrefH, 0x01 ; Value to compare adc to
LDI VrefL, 0x40
LDI PWMHIGH, 160
LDI Kp, 2
;initialize system clock to 16MHz
CLI
LDI init1, 0b1000_0000 ;enable CLKPR to allow to change frequency
LDI init2, 0b0000_0000 ;set main prescaler to 8 so it can be seen on cheap oscilliscope
STS CLKPR, init1
STS CLKPR, init2


;Set up ADC timer (timer 1)
LDI init1, 0b0100_0000 ;TCCR1A CTC mode max = OCR1A
LDI init2, 0b0000_1011 ;TCCR1B prescaler 1
LDI init3, 0b0000_0111 ;TIMSK1 enable overflow interrupt
STS TCCR1A, init1
STS TCCR1B, init2
STS TIMSK1, init3
LDI init1, 0x0F ;ICR1AH countermax = 4000.
LDI init2, 0xA0 ;ICR1AL should trigger every 500us (16Mhz/2/4000)^-1
STS OCR1BH, init1
STS OCR1BL, init2

;Set up ADC
LDI init1, 0b0000_0001 ;should set ADC port to input
STS DIDR0, init1
;LDI init1, 0b0000_0000 ;should disable all other adc ports
;STS DIDR2, init1
LDI init1, 0b0100_0000 ;ADMUX Vcc reference, ADC input on ADC0 -A0
LDI init2, 0b0000_0101 ;ADCSRB Auto trigger on timer1 capture
LDI init3, 0b1110_1111 ;ADCSRA interrupt enable, prescaler 2
STS ADMUX, init1
STS ADCSRB, init2
STS ADCSRA, init3

;Initial setup of the PWM signal (timer 3)
LDI init1, 0b0010_0011 ;Fast PWM mode 14 TCCR3A
LDI init2, 0b0001_1010 ;Set scaler to 1 TCCR3B
STS TCCR3A, init1
STS TCCR3B, init2
LDI init1, 0b0000_0001 ;turn on Interrupt for TIMSK3
LDI init2, 0x00 ;Max value of PWM (can use 1+2 to make 16-bit)
MOV init3, PWMHIGH ; A0 = 160 = 16MHz/100
STS TIMSK3, init1
STS ICR3H, init2
STS ICR3L, init3
LDI init3, 0
LDI init1, 0
LDI init2, 100 ;starting value of OCR3A (Duty Cycle OCR3A/ICR3L)
STS OCR3BH, init1
STS OCR3AL, init2
LDI init1, 0xFF
OUT DDRE, init1
CLR init1
;STS TCNT1H, init1
;STS TCNT1L, init1
;STS TCNT3H, init1
;STS TCNT3L, init1
SEI                   ; enable global interrupts



;Following code is just a test bed for the pwm code

Start:
RJMP Start


TIM1_CAPT:
RETI

TIM1_COMPA:
RETI

TIM3_OVF:
LDI init1, 0
LDS light, PORTC
ORI light, 0x10
STS PORTC, light
STS OCR3BH, init1
STS OCR3BL, PWM_OUT
RETI

ADC_RDY:
LDI init1, 0 ;initialize register for use in compare
CLI ;stop interrupts for 16bit read/write
LDS init2, ADCL ;AVR uses dst, src instead of src, dst
LDS init3, ADCH

PUSH VrefH ;store vref to the stack while we mess with it
PUSH VrefL
SUB VrefL, init2 ;E(t) = Vref - ADC input
SBC VrefH, init3 ;subtract the upper bytes (with carry)
MOV r1, VrefL ;move e(t) into different register to regain Vref values
MOV r2, VrefH
POP VrefL ; Reload value of Vref
POP VrefH
BRMI Negative ;If e(t) is negative, set PWM value to zero
CP r1, init1
CPC r2, init1
BREQ Zero ;If e(t) = 0 set PWM valute to zero
LDI init1, 0x00 ;Check for impossible value from 10bit ADC, saturate high (put back 03FF)
LDI init2, 0x01
CP r1, init1
CPC r2, init2
BRSH Too_high
CALL ADC_Convert ;Convert the 10-bit adc value into a value between 0 - 160
CALL Round_number
Kp_multiplication: ;multiply value by Kp
LDI init1, 0
MOV r2, r1 ;move out of r1, since product is stored in r1:r0
MUL r2, kp
CP r0, PWMHIGH ;check for value higher than PWMHIGH, and saturate if true
CPC r1, init1
LDS light, PORTC
ORI light, 0x02
BRSH Too_high
LDS light, PORTC
ORI light, 0x12
MOV PWM_OUT, r0
JMP calibration_done
Too_high:
LDS light, PORTC
ORI light, 0x01
MOV PWM_OUT, PWMHIGH
JMP calibration_done
Negative:
LDS light, PORTC
ORI light, 0x04
CLR PWM_OUT
JMP calibration_done
Zero:
JMP Calibration_done ;If value is where you want it, don't change anything.

Calibration_done:
OUT PORTC, light
SEI
RETI

ADC_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 multiplicant
CLR r5
LDI rmp, 0x28
MOV r4, rmp
LDI rmp, 0x0A
MOV r3, rmp
CLR r10
CLR r9
CLR r8
CLR r7
Convert_Start:
MOV rmp, r1
OR  rmp, r2 ;Check if there are any 1's left
BRNE Shift_Div
RET ;No 1's, return to program
Shift_Div:
LSR r2 ;Shift MSB, div by 2
ROR r1 ;Rotate right through carry on MSP430
BRCC Skip_Add ;don't add if LSB was 0
ADD r7, r3 ;add #s in r6:r5:r4:r3
ADC r8, r4
ADC r9, r5
ADC r10,r6
Skip_Add:
LSL r3 ;multiply r6::r3 by 2
ROL r4
ROL r5
ROL r6
RJMP Convert_start ;RJMP saves one cycle. go to next bit

Round_number: ;rounds the number using bit 15
CLR rmp
LSL r8 ;rotate bit 15 to carry
ADC r9, rmp ;add the carry to LSbyte
ADC r10, rmp ;add the new carry to MSbyte
MOV r2, r10 ; do the final division by 65536
MOV r1, r9
RET

again, any help would be greatly appreciated! Thanks!
 

Offline mikerj

  • Super Contributor
  • ***
  • Posts: 3233
  • Country: gb
Re: Help with PID in AVR assembly for final project.
« Reply #7 on: December 06, 2017, 09:14:28 am »
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).
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf