/* * Firmware for 1- or 3-phase frequency converter (variable frequency motor drive) * using PWM true sine wave generation * Just proof-of-concept alpha version at the moment * * Copyright (C) 2015-2016 Jiri Pittner * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * */ //lpc1114 14.7456MHz PLL=3 //PINOUT: cf. pwmsine kicad schematics /* * PIO0_0-1 = RESET+BOOT * PIO0_2 = switch1 * PIO0_3 = switch2 * PIO0_4,5 = SCL,SDA * PIO0_6 = 23017 interrupt * PIO0_7 = rot_l * PIO0_8 = rot_r * PIO0_9 = rot_push * PIO0_10 = bridge activation * PIO0_11 = AD0 V_sense10 (the only one presently used) * PIO1_0 = AD1 V_sense21 * PIO1_1-3 = CT32B1MAT0-2 PWM outputs * PIO1_4 = AD5 V_sense02 * PIO1_5 = BLUE LED * PIO1_6,7 = USART * PIO1_8 = GREEN LED * PIO1_9 = RED LED * * 23017 ports: * A0-3 = rows keyboard * A4-7 = columns keyboard * B0-3 = lcd_db4-7 * B4 = lcd_e * B5 = lcd_rw * B6= lcd_rs * B7 = lcd_light */ //TODO: ///home/jiri/htdocs/jiri/hobby/electronics/frequency_changer/ // //NOTE: it works but if done next time from scratch, I would: //NOTE: completely redesign - use 1 mcu for user interface, measurement and control and a CPLD or or second MCU only for DDS PWM generation to 100% avoid glitches in the PWM signal //NOTE: complete change of layout - several smaller boards //BLUE LED signal for debugging rather than brake SSR #undef hwdebug //print PWM values range //#define pwmdebug //include DC fixed pwm menu //#define debug_menumode //define this if using bootstrap diodes rather than DC/DC convertors for high side drive #undef BOOTSTRAP #include #include #include #include #include #include //exact sine wave //#include "sin_10_16.h" //intentionally distorted sine wave to achieve higher Vrms at given Vpp //Vpp is not rail2rail due to 1) duty cycle limitation because of bootstrapping the bridge driver (can be aleviated by DC/DC convertors replacing bootstrap diode) //and 2) duty cycle limitation due to interrupt routine duration //and 3) losses in the LC filter //empirically modified shape //#include "sin0.8_10_16.h" //HOWEVER THIS TURNS OUT TO BE THE BEST for 3-phase drive //include maximally 3rd harmonic without introducing new local minima on the curve //#include "sin_h9_10_16.h" //two versions automatically generated by ddsgenerate.c and merged to a single file #include "sin_samples.h" #include "cpu.h" #include "uart.h" #include "delay.h" #include "gpio.h" #include "adc.h" #include "mcp23017.h" #include "i2c.h" #include "i2clcd.h" #include "i2ckeyboard.h" //this has to be include after 23017 #include "rotcoder.h" //0 is highest priority, only 2 MSB are significant static inline void NVIC_SetPriority(IRQn_t IRQn, uint8_t p) { NVIC->IP[IRQn] = p; } void watchdog_init(uint32_t timeout) //timeout in counts at about 100khz { /* Setup the WDT clock */ SCB_WDTOSCCTRL = SCB_WDTOSCCTRL_FREQSEL_0_5MHZ | SCB_WDTOSCCTRL_DIVSEL_DIV2; /* Set clock source (use WDT oscillator to be independent from main clock) */ SCB_WDTCLKSEL = SCB_WDTCLKSEL_SOURCE_WATCHDOGOSC; SCB_WDTCLKUEN = SCB_WDTCLKUEN_UPDATE; SCB_WDTCLKUEN = SCB_WDTCLKUEN_DISABLE; SCB_WDTCLKUEN = SCB_WDTCLKUEN_UPDATE; /* Wait until updated */ while (!(SCB_WDTCLKUEN & SCB_WDTCLKUEN_UPDATE)); /* Set divider */ SCB_WDTCLKDIV = SCB_WDTCLKDIV_DIV1; /* Enable WDT clock */ SCB_PDRUNCFG &= ~(SCB_PDRUNCFG_WDTOSC); /* Enable AHB clock to the WDT domain. */ SCB_SYSAHBCLKCTRL |= SCB_SYSAHBCLKCTRL_WDT; /* Enable the WDT interrupt */ // NVIC_EnableIRQ(WDT_IRQn); /* Set timeout value (must be at least 0x000000FF) */ WDT_WDTC = timeout; /* Enable the watchdog timer (without system reset) */ WDT_WDMOD = WDT_WDMOD_WDEN_ENABLED | WDT_WDMOD_WDRESET_ENABLED; } void watchdog(void) //feed the dog { __disable_irq(); WDT_WDFEED = WDT_WDFEED_FEED1; WDT_WDFEED = WDT_WDFEED_FEED2; __enable_irq(); } #define ALPHANUMS "zyxwvutsrqponmlkjihgfedcba9876543210123456789abcdefghijklmnopqrstuvwxyz" int itoamy(unsigned int number, char* out, int base) { unsigned int t, count; char *p, *q; char c; p = q = out; if (base < 2 || base > 36) base = 10; do { t = number; number /= base; if (out) *p = ALPHANUMS[t+35 - number*base]; p++; } while (number); if (t < 0) { if (out) *p = '-'; p++; } count = p-out; if (out) { *p-- = '\0'; while(q < p) { c = *p; *p-- = *q; *q++ = c; } } return count; } #define loop_until_bit_is_set(port,bit) while(bit_is_clear(port,bit)) volatile uint16_t phaseshifts[3]; volatile uint32_t ddsstep; volatile uint32_t ddsstepnew; volatile int32_t ddsstepinc; uint16_t dutyscaleeff; uint32_t ddsstepeff; volatile uint32_t ddsacc=0; volatile uint16_t ddsindex; volatile uint32_t pwmtop; volatile uint16_t dutyscale; volatile float pwmfreq; #ifdef BOOTSTRAP volatile uint32_t pwm_forbidden; #endif #define MENUMODEMAX 2 volatile uint8_t menumode=0; #define VOLUMEBITS 15 #ifdef pwmdebug volatile uint32_t pwm_min[3],pwm_max[3]; #endif volatile uint32_t pwm_next[3]; volatile uint16_t softstart=0; volatile uint8_t startdc=0; volatile uint16_t rampdown=0; volatile uint32_t rampfreq=0; volatile uint32_t rampfreqtop=0; volatile uint8_t waveform=0; volatile uint8_t flag20=0; //true when switching off workmode 2 volatile uint8_t workmode=0; //0==off, 1==constant voltage, 2=V-f motor drive, 3=rampdown soft-off //interrupt happens at TOP defined by MR3, i.e. the following function is called with pwmfreq frequency //IMPORTANT!!! const uint32_t interrupt_duration=200; //assuming CPU freq = timer freq (no prescaler), measured by oscilloscope //this accounts for (1) interrupt jitter due to i2c interrupts, (2) for interrupt latency and (3) for duration of the start of the interrupt setting the match registers inline void TIMER32_1_IRQHandler(void) { //set values ASAP and perform calculation later for next run of the interrupt //otherwise we might be too late #ifdef hwdebug sbi(1,5); #endif __disable_irq(); TMR_TMR32B1MR0 = pwm_next[0]; TMR_TMR32B1MR1 = pwm_next[1]; TMR_TMR32B1MR2 = pwm_next[2]; TMR_TMR32B1IR = TMR_TMR32B1IR_MR3; __enable_irq(); #ifdef hwdebug cbi(1,5); #endif if(menumode==MENUMODEMAX) { if(startdc>0) //start gate drivers only after timing registers have been set properly { if(startdc==1) gpioSetValue(0,10,1); --startdc; } return; //just a test mode } if(softstart==0 && rampdown<=4) //bridge off - timely before last pwm pulse which exhibits a glitch { //stop pwm if(rampdown==0) { pwm_next[0]=pwm_next[1]=pwm_next[2] =0; } else --rampdown; //disable bridge gpioSetValue(0,10,0); return; } //handle softstart and rampdown #define SOFTSTARTBITS 14 { if(softstart == 2) //bridge on after reasonable MR values have been set previously { gpioSetValue(0,10,1); } if(softstart>0 && softstart<(1L< ddsstep && ddsstepinc > 0) ddsstep += ddsstepinc; else if(ddsstepnew < ddsstep && ddsstepinc < 0) ddsstep += ddsstepinc; else ddsstep=ddsstepnew; //done } dutyscaleeff=dutyscale; ddsstepeff= ddsstep; //in motor mode implement chirp start with linear v/f if(workmode==2||flag20) if(rampfreq>8)*(uint32_t)dutyscale/(rampfreqtop>>8); ddsstepeff = ((rampfreq>>8)*(ddsstep>>10)/(rampfreqtop>>8))<<10; } //increment DDS counter ddsacc += ddsstepeff; ddsindex = ddsacc >> (32-SAMPLEBITS); uint8_t i; for(i=0; i<3; ++i) { //load new match values based on DDS uint64_t hlp=sin_samples[waveform][(ddsindex+phaseshifts[i])&SAMPLEBITS_MASK]; //scale by required voltage //scale by current PWM TOP (which actually fits in 16 bits) //scale by softstart and/or rampdown hlp *= dutyscaleeff; hlp *= pwmtop; //it is not necessary to use further volatge lowering when v/f low freq starting the motor if(workmode==1 && softstart) hlp *= softstart; hlp >>= (VOLUMEBITS+RESBITS+((softstart && workmode==1)?SOFTSTARTBITS:0)); if(rampdown) { hlp *= rampdown; hlp >>= SOFTSTARTBITS; } pwm_next[i] = hlp; #ifdef BOOTSTRAP //hardware limitations //NEVER remove this, might lead to damage of IRS2184, which will oscillate with control input constantly high or duty close to 1 //UNLESS you use dc/dc convertors instead of bootstrap capacitor, then any duty is safe if(pwm_next[i]pwmtop-pwm_forbidden) pwm_next[i]=pwmtop-pwm_forbidden; #endif //Software limitation - interrupt latency+jitter+duration //it could happen that one pulse is skipped when we set match register too late //the price we pay is some distortion of the sine wave at full volume //It was a mistake to use LPC1114, one should rather avoid using i2c using a MCU with //more I/O pins to drive all peripherals directly if(pwm_next[i]pwm_max[i]) pwm_max[i]=pwm_next[i]; #endif } #ifdef hwdebug cbi(1,5); #endif return; } #define FOUTMIN 1. #define FOUTMAX 400. #define FPWMMIN 8000. #define FPWMMAX 25000. volatile float fixed_duty[3]={.5,0.1,0.9}; //const duty mode for hardware testing void set_duty(int8_t i, float d) { if(d<0.) d=0.; if(d>1.) d=1.; fixed_duty[i]=d; uint32_t tmp = d*pwmtop; #ifdef BOOTSTRAP //hardware limitations //NEVER remove this, might lead to damage of IRS2184, which will oscillate with control input constantly high or duty close to 1 if(tmppwmtop-pwm_forbidden) tmp=pwmtop-pwm_forbidden; #endif switch(i) { case 0: pwm_next[0]=TMR_TMR32B1MR0=tmp; break; case 1: pwm_next[1]=TMR_TMR32B1MR1=tmp; break; case 2: pwm_next[2]=TMR_TMR32B1MR2=tmp; break; } } //key routine to initialize PWM and DDS void set_freq(float pwmfreq0, float outfreq, int8_t phases, uint8_t wf, float volume) { if(pwmfreq0FPWMMAX) pwmfreq0=FPWMMAX; if(volume<0.) volume=0.; if(volume>1.) volume=1.; if(outfreqFOUTMAX) outfreq=FOUTMAX; if(wf >= WAVEFORM_MAX) wf=0; waveform=wf; //initialize PWM timer top SCB_SYSAHBCLKCTRL |= (SCB_SYSAHBCLKCTRL_CT32B1); pwmtop = (uint32_t) (XTAL*PLLMULT/pwmfreq0+.5f); #ifdef pwmdebug uint8_t i; for(i=0; i<3; ++i) {pwm_max[i]=0; pwm_min[i]=pwmtop;} #endif #ifdef BOOTSTRAP //!!!!!IMPORTANT!!!!!! //this is an empirically estimated safe value, due to the igbt driver IRS2184 where the charge pump cannot support duty close to 100% //it depends on the frequency of PWM, this value seems OK until 25kHz //WRONG VALUE CAN LEAD TO OSCILLATIONS AND DESTRUCTION OF IR2184 AND IGBTs!!!! // pwm_forbidden = pwmtop/32; #endif pwm_next[0]=pwm_next[1]=pwm_next[2]=pwmtop/2; //adjust freq for truncation error pwmfreq = XTAL*PLLMULT*1.f/pwmtop; TMR_TMR32B1MR3 = pwmtop-1; //set counter TOP //number of calls to PWM handler for making frequency ramping when starting a motor - 10 seconds times PWM frequency rampfreqtop = (uint32_t) (pwmfreq *10.); //allow PWM TMR_TMR32B1PWMC |= TMR_TMR32B1PWMC_PWM0_ENABLED; TMR_TMR32B1PWMC |= TMR_TMR32B1PWMC_PWM1_ENABLED; TMR_TMR32B1PWMC |= TMR_TMR32B1PWMC_PWM2_ENABLED; //setup DDS ddsstepnew= ddsstep = (1LL<<32)*outfreq/pwmfreq; //debug #if 0 //NOTICE: when called from interrupt, this debug is too slow and causes glitches - never use at full voltage {char z[32]; sprintf(z,"ddsstep=%d\n",ddsstep); print(z); } #endif //setup phase delays for DDS phaseshifts[0]=0; switch(phases) { case 3: phaseshifts[1] = SAMPLEBITS_N/3; phaseshifts[2] = 2*SAMPLEBITS_N/3; break; case 2: case -3: //reverse direction phaseshifts[2] = SAMPLEBITS_N/3; phaseshifts[1] = 2*SAMPLEBITS_N/3; break; case 1: default: phaseshifts[1] = SAMPLEBITS_N/2; phaseshifts[2] = SAMPLEBITS_N/4; break; } //setup volume dutyscale = ((1L<256) divisor=256; ADC_AD0CR = (ADC_AD0CR_SEL_AD0 | //channel AD0 ((divisor-1) << 8) | //clock must be under 4.5MHz and we want less due to the interrupt anyway, just enough to reasonably sample max supported frequency ADC_AD0CR_BURST_SWMODE | ADC_AD0CR_CLKS_10BITS | /* CLKS = 0, 11 clocks/10 bits */ ADC_AD0CR_START_NOSTART); /* START = 0 for HW mode*/ //obsolete version, now RMS is done by analog computation in HW #if 0 //allow interrupt on channel 0 (*(pREG32(ADC_AD0INTEN))) = ADC_AD0INTEN_ADINTEN0; NVIC_SetPriority(ADC_IRQn,192); NVIC_EnableIRQ(ADC_IRQn); #endif } //obsolete version, now RMS is done by analog computation in HW #if 0 static volatile uint32_t adccooked; static volatile uint32_t nadc=0,adcav=0; static volatile uint64_t adcsqrav=0; static volatile uint16_t lognsamples=13; static volatile uint64_t outadcsqrav; static volatile uint32_t outadcav; static volatile uint32_t adcmin=10000000,adcmax=0; static volatile uint32_t outadcmin,outadcmax; inline void ADC_IRQHandler(void) //about 16khz { static volatile uint32_t adcraw; static uint8_t adccountraw=0; uint16_t value= ((*(pREG32(ADC_AD0DR0))) >> 6 ) & 0x3ff; if(adccountraw++ < 4) {adcraw += value; return;} else {adccountraw=0; adccooked=adcraw; adcraw=0;} //accumulate averages - about 4khz; lognsamples>=12 covers the period even for 1Hz output if(++nadc == (1L<adcmax) adcmax=adccooked; if(adccooked 0) { a *= 10.0f; e--; } while (e < 0) { a *= 0.1f; e++; } return a; } //START DEFAULTS volatile float fpwm=16000.; volatile float fout=50.; volatile float volume=.25; volatile int8_t phases=3; void adjustvolume(uint8_t mode) { #ifdef pwmdebug //reset minimax accumulation uint8_t i; for(i=0; i<3; ++i) { pwm_min[i]=pwmtop; pwm_max[i]=0; } #endif if(mode==2) //VFMD at constant V/f { float volume0=(fout/50.+0.1)*volume; if(volume0>1.) volume0=1.; dutyscale = ((1L<FOUTMAX) fout=FOUTMAX; ddsstepnew = (1LL<<32)*fout/pwmfreq; ddsstepinc = ddsstepnew-ddsstep; ddsstepinc >>= INTERPBITS; adjustvolume(workmode); break; case 1: //control volume step= (workmode?.001:.01); volume += dir?step:-step; if(workmode==0) volume = step*floorf(volume/step+.5); if(volume<0.) volume=0.; if(volume>1.) volume=1.; adjustvolume(workmode); break; case MENUMODEMAX: //control pwm of channel 0 set_duty(0,fixed_duty[0]+(dir?0.01:-0.01)); break; } } void process_press(uint16_t length) { #ifdef debug_menumode menumode = (menumode+1)%(MENUMODEMAX+1); #else menumode = (menumode+1)%(MENUMODEMAX); #endif if(menumode==MENUMODEMAX) { uint8_t i; for(i=0; i<3; ++i) set_duty(i,fixed_duty[i]); } } void main(void) { watchdog_init(500000); //about every 5 seconds watchdog(); uint8_t temperature=0; uint8_t overheated=0; uint16_t voltage=0; //init GPIO and disable bridge gpioInit(); //activate clock initoutputs(); //setup gpio and special pin functions for this application uartInit(115200); print("Reset!\n"); init_rotcoder(7,8,9); //pio0 pin numbers //be prepared to read inputs of port A i2cInit(I2CMASTER); i2ckeyboard_init(); i2clcd_init(); light=1<<7; i2clcd_print("Reset!"); initadc(); set_freq(fpwm,fout,phases,waveform,volume); uint8_t inkbdlenold=0; //main loop while(1) { //check switch and set LED uint8_t workmode_new=0; if(gpioGetValue(0,2)==0) workmode_new=1; if(gpioGetValue(0,3)==0) workmode_new=2; switch(workmode_new) { case 0: gpioSetValue(1,8,0); gpioSetValue(1,9,0); break; case 1: gpioSetValue(1,8,1); gpioSetValue(1,9,0); break; case 2: gpioSetValue(1,8,0); gpioSetValue(1,9,1); break; } if(overheated) activate_bridge(workmode_new=0,workmode); else if(workmode_new!=workmode) { char z[2]; z[1]=0; z[0]='0'+workmode_new; print("WORKMODE "); print(z); print("\n"); adjustvolume(workmode_new); activate_bridge(workmode_new,workmode); } if(inkey) //process a single key press { char z[2]; z[0]=i2c_read_key(); z[1]=0; if(inkbdlen==1 && inkbdlenold==0) //erase bottom row when user started typing { i2clcd_cursor(1,0); i2clcd_print(" "); } i2clcd_cursor(1,z[0]==' '?inkbdlen:inkbdlen-1); i2clcd_print(z); } inkbdlenold=inkbdlen; //check keyboard input if(inkbdline) { if(workmode) //ignore keyboard while energized - abrupt changes could be dangerous { i2clcd_cursor(1,0); i2clcd_print("KBD blocked!"); watchdog(); delay_ms(500); } else switch(inkbd[0]) { case '.': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '0': //control frequency fout=myatof(inkbd); if(foutFOUTMAX) fout=FOUTMAX; ddsstepnew= ddsstep = (1LL<<32)*fout/pwmfreq; adjustvolume(workmode); break; case 'A': //control volume volume=myatof(inkbd+1); if(volume<0.) volume=0.; if(volume>1.) volume=1.; adjustvolume(workmode); break; case 'C': //control fpwm fpwm=myatof(inkbd+1); set_freq(fpwm,fout,phases,waveform,volume); adjustvolume(workmode); break; case 'D': //unused - now used as a delete key in the lower routine //toggle const duty test mode menumode = (inkbd[1]-'0')%(MENUMODEMAX+1); if(menumode==MENUMODEMAX) {uint8_t i; for(i=0; i<3; ++i) set_duty(i,fixed_duty[i]);} break; case 'B': //change number of phases (2,3 are 3-phase with opposite direction) { int8_t i=inkbd[1]-'1'; if(i<0) i=0; if(i>5) i=5; phases = 1+i%3; uint8_t wf= i/3; set_freq(fpwm,fout,phases,wf,volume); adjustvolume(workmode); } break; } inkbdlen=inkbdline=0; } //check uart communication if(inuartline) { watchdog(); switch(inuart[0]) { case 'M': //set menumode menumode = (inuart[1]-'0')%(MENUMODEMAX+1); if(menumode==MENUMODEMAX) {uint8_t i; for(i=0; i<3; ++i) set_duty(i,fixed_duty[i]);} break; case 'd': //set duty for const duty test mode { float a=myatof(inuart+2); set_duty(inuart[1]-'0',a); break; } case 'r': //reverse direction of 3phase { uint16_t tmp; tmp=phaseshifts[1]; phaseshifts[1]=phaseshifts[2]; phaseshifts[2]=tmp; } break; case 'P': //phases { phases= atoi10_32(inuart+1); set_freq(fpwm,fout,phases,waveform,volume); adjustvolume(workmode); } break; case 'w': //waveform { uint8_t wf = atoi10_32(inuart+1); set_freq(fpwm,fout,phases,wf,volume); adjustvolume(workmode); } break; case 'p': //pwm frequency { fpwm=myatof(inuart+1); set_freq(fpwm,fout,phases,waveform,volume); adjustvolume(workmode); } break; case 'f': //dds frequency { fout=myatof(inuart+1); if(foutFOUTMAX) fout=FOUTMAX; ddsstepnew= ddsstep = (1LL<<32)*fout/pwmfreq; adjustvolume(workmode); } break; case 'v': //volume { volume=myatof(inuart+1); adjustvolume(workmode); } break; default: print("unknown command: "); print(inuart); print("\n"); break; } inuartline=inuartlen=0; } //measure voltage #define LOGSAMPLES 5 //the following two constants are subject to calibration //processor voltage for the ADC #define VREF 3.302 //inverse gain of the whole aplifier and rms convertor chain #define GAIN 92.574 if(workmode==0) voltage=0; else voltage = (uint16_t)(GAIN * VREF * adcRead(0,LOGSAMPLES)/(1<<(10+LOGSAMPLES))); //refresh display { char z[17]; if(overheated) { i2clcd_cursor(0,0); i2clcd_print("OVERHEATED-RESET"); } else switch(menumode) { case 0: //control frequency case 1: //control volume i2clcd_cursor(0,0); sprintf(z,(fout>=100.?"%c=%6.2fH %c=%4.2f":"%c=%5.2fHz %c=%4.2f"),(menumode==0?'F':'f'),fout,(menumode==1?'V':'v'),volume); i2clcd_print(z); if(inkbdlen==0) //second line is free { sprintf(z,"T=%2dC %c=%1d U=%3dV",temperature,'P'+waveform,phases,voltage); i2clcd_cursor(1,0); i2clcd_print(z); } break; case MENUMODEMAX: //const pwm test mode i2clcd_cursor(0,0); sprintf(z,"D %4.2f %4.2f %4.2f",fixed_duty[0],fixed_duty[1],fixed_duty[2]); i2clcd_print(z); break; } } #ifdef pwmdebug { char deb[128]; sprintf(deb,"test %d %d %d %d %d %d top %d\n",pwm_min[0],pwm_min[1],pwm_min[2],pwm_max[0],pwm_max[1],pwm_max[2],pwmtop); print(deb); } #endif //check heatsink temperature temperature=TC74_Read(2,0); //change the address according to the TC74 version purchased if(temperature>100) overheated=1; //permanently stop operation until reset watchdog(); //interruptable delay { uint32_t ii; for(ii=0; ii