Hello all. I am desiging a PID controller in assembly for a Microcontrollers final project. It's designed to drive an LTC4440, but for the project I just need to prove to the professor that the PWM output varies with the reading of the ADC (for now. I'd like to get the whole circuit working eventually.) Right now I'm working on just the Proportional part, and I will add the rest later if I get a chance (the P part is what he's really looking for, the rest is optional.)
The program I've written uses two timers on the atmega2560 (which is on the arduino mega board), and the ADC. the first timer (timer1) controls the ADC using the ADATE bit and the timer 1 compare B flag. When the ADC is done with its conversion it sets an interrupt (is that the right phrase?) and converts the value in the ACDCH/L register into a value that changes duty cycle of the PWM, which it then dumps into OCRB3H/L. Timer 3 is the PWM output, which is output on OCR3B.
The problem I am having is that while the program works like its supposed to. it only does so once. I use a potentiometer to feed a value between 0 and 5V to the ADC (analog pin 0), and when the program first runs, it correctly reads the ADC and outputs a proportional PWM, but if I change the potentiometer while the program is running, the PWM duty cycle does not change. Currently I need to press the reset button after I change the potentiometer to have the PWM duty cycle change, even though the ADC should be triggering ever 500us based on timer1
Here is the code:
; ; AssemblerApplication1.asm ; ; Created: 11/18/2017 2:54:17 PM ; Author : Chris ; ;Definitions .DEF PWM_OUT = r20 .DEF init1 = r16 ;Variables used to initialize registers .DEF init2 = r17 .DEF init3 = r18 .DEF rmp = r19 .DEF light = r14 .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 0x0024 RJMP TIM1_COMPB .org 0x003A ;memory location for adc ready handler RJMP ADC_RDY ;============ Reset: LDI VrefH, 0x01 ; Value to compare adc to LDI VrefL, 0xFF LDI PWMHIGH, 160 LDI Kp, 3 ;initialize system clock to 16MHz CLI LDI init1, (1<<CLKPCE) ;enable CLKPR to allow to change frequency LDI init2, (1<<CLKPS0) ;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, (1<<COM1A1) ;TCCR1A CTC mode max = OCR1A LDI init2, (1<<WGM12)|(1<<CS10) ;TCCR1B LDI init3, (1<<OCIE1B) ;TIMSK1 STS TCCR1A, init1 STS TCCR1B, init2 STS TIMSK1, init3 LDI init1, 0x0F ;countermax = 4000. LDI init2, 0xA0 ;should trigger every 500us (16Mhz/2/4000)^-1 STS OCR1BH, init1 STS OCR1BL, init2 ;Set up ADC LDI init1, (1<<ADC0D) ;should set ADC port to input STS DIDR0, init1 LDI init1, (1<<REFS0) ;ADMUX Vcc reference, ADC input on ADC0 -A0 LDI init2, (1<<ADTS2)|(1<<ADTS0) ;ADCSRB Auto trigger on timer1 comp b LDI init3, (1<<ADEN)|(1<<ADSC)|(1<<ADATE)|(1<<ADIE)|(1<<ADPS1)|(1<<ADPS0) ;ADCSRA interrupt enable, prescaler 2 STS ADMUX, init1 STS ADCSRB, init2 STS ADCSRA, init3 ;Initial setup of the PWM signal (timer 3) LDI init1,(1<<COM3B1)|(1<<WGM31)|(1<<WGM30) ; TCCR3A LDI init2, (1<<WGM33)|(1<<WGM32)|(0<<CS32)|(1<<CS31)|(1<<CS30) ; TCCR3B STS TCCR3A, init1 STS TCCR3B, init2 LDI init1, 0 ;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 OCR3B (Duty Cycle OCR3B/OCRA) 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_COMPB: RETI ADC_RDY: CLR init1 ;initialize register for use in compare CLR init2 CLR init3 CLR r0 CLR R1 CLR r2 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 BRLT Negative ;If e(t) is negative, set PWM value to zero CP r1, init1 CPC r2, init1 BREQ Zero ;If e(t) = 0 don't change anything LDI init1, 0xFF ;Check for impossible value from 10bit ADC, saturate high (put back 03FF) LDI init2, 0x03 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 LDS init3, PINC ORI init3, 0x02 MOV r2, r1 ;move out of r1, since product is stored in r1:r0 CLR r0 CLR r1 CLR init1 MUL r2, kp CP r0, PWMHIGH ;check for value higher than PWMHIGH, and saturate if true CPC r1, init1 BRSH Too_high LDS init3, PINC ORI init3, 0x12 MOV PWM_OUT, r0 RJMP calibration_done Too_high: LDS init3, PINC ORI init3, 0x01 MOV PWM_OUT, PWMHIGH RJMP Calibration_done Negative: LDS init3, PINC ORI init3, 0x04 LDI PWM_OUT, 0 RJMP Calibration_done Zero: CLR PWM_OUT RJMP Calibration_done ;If value is where you want it, don't change anything. Calibration_done: LDI init1, 0 STS OCR3BH, init1 STS OCR3BL, PWM_OUT OUT PORTC, init3 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
The fuses (read in atmel studio from an AVR ISP mkii clone) are Extended 0xFD HIGH 0xD8 and LOW 0xFF.
The simulation does the same thing, but I think it says that the analog components don't work in simulation, so I thought maybe that's why.
Oh, also using atmel studios 7 to write the code. Was using avrdude -cwiring in external tools to download the code, but for some reason that stopped working, so I am downloading straight to the chip with the AVR ISP mkii via atmel studios currently.
I have tried to be as thorough as I can, and apologize if I'm forgetting anything obvious. If there's any other information I could provide please do not hesitate to let me know.
EDIT: I'm sorry, I just realized I haven't really kept up with the comments in the initialization section, so they aren't correct anymore. My sincerest apologies. I will try to update them ASAP.
Everything in the code that has to do with the lights and port C was just test code I was using to see which parts of the program were actually happening. They aren't integral to the program in any way.
EDIT2: fixed the control register loading to be more legible thanks to suggestions.