├── VFD_Prototype_Schematics.pdf ├── References ├── Motor_Wiring_Diagram.jpg ├── IPM Interface Reference Design.pdf ├── SLLIMM-nano 2nd series small low-loss intelligent molded module.pdf └── Links ├── HW_Changes ├── README.md ├── StandAlone_Atmega └── Links ├── V_To_I.py ├── Transformations.py ├── Id_Iq.py └── VFD.ino /VFD_Prototype_Schematics.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatanPazi/VFD/HEAD/VFD_Prototype_Schematics.pdf -------------------------------------------------------------------------------- /References/Motor_Wiring_Diagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatanPazi/VFD/HEAD/References/Motor_Wiring_Diagram.jpg -------------------------------------------------------------------------------- /References/IPM Interface Reference Design.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatanPazi/VFD/HEAD/References/IPM Interface Reference Design.pdf -------------------------------------------------------------------------------- /HW_Changes: -------------------------------------------------------------------------------- 1 | 1. Switch between 5V power supply outputs (5V and GND) 2 | 2. Change pull up resistor for Atmega reset pin from 3.3[KOhm] to 10[KOhm] 3 | -------------------------------------------------------------------------------- /References/SLLIMM-nano 2nd series small low-loss intelligent molded module.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatanPazi/VFD/HEAD/References/SLLIMM-nano 2nd series small low-loss intelligent molded module.pdf -------------------------------------------------------------------------------- /References/Links: -------------------------------------------------------------------------------- 1 | SLLIMM-nano 2nd series small low-loss intelligent molded module: 2 | https://www.st.com/resource/en/application_note/an4840-sllimmnano-2nd-series-small-lowloss-intelligent-molded-module-stmicroelectronics.pdf 3 | 4 | 5 | IPM Interface Reference Design: 6 | https://www.ti.com/lit/ug/tidudo8/tidudo8.pdf?ts=1636199546738&ref_url=https%253A%252F%252Fwww.google.com%252F 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Code for DIY Variable Frequency Drive 2 | 3 | Check out a series of videos I made on the project: 4 | [How to make a Variable Frequency Drive (VFD) | 1: Overview & Basics](https://youtu.be/XGlXLaUbKLw) 5 | [How to make a Variable Frequency Drive (VFD) | 2: Hardware Design](https://youtu.be/wUGCEtSXV1I) 6 | [How to make a Variable Frequency Drive (VFD) | 3: Software](https://youtu.be/HGHpkghiIs8) 7 | -------------------------------------------------------------------------------- /StandAlone_Atmega/Links: -------------------------------------------------------------------------------- 1 | Upload using Arduino Mega 2560: 2 | https://youtu.be/aDcdOgW0OU0 3 | 4 | Upload using Arduino Uno: 5 | https://www.youtube.com/watch?v=CQW6Mz6ULrc 6 | 7 | 8 | 9 | General guides: 10 | https://www.circuito.io/blog/atmega328p-bootloader/ 11 | 12 | http://eng-shady-mohsen.blogspot.com/2018/03/programming-atmel-atmega328-pu.html 13 | 14 | 15 | If using an Arduino Mega as the programmer, remember to change the SPI pin numbers 16 | 17 | 18 | Uncomment this (Or use the SPI header). 19 | // #define USE_OLD_STYLE_WIRING 20 | 21 | #ifdef USE_OLD_STYLE_WIRING 22 | 23 | #define PIN_MOSI 11 (This is 51) 24 | #define PIN_MISO 12 (This is 50) 25 | #define PIN_SCK 13 (This is 52) 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /V_To_I.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Matan Pazi 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | ## .py script to show voltage to current transfer function and the use of PWM signals to generate current waveforms. 24 | import numpy as np 25 | import matplotlib.pyplot as plt 26 | from scipy import signal 27 | 28 | USE_PWM = 0 29 | 30 | omega = 2*np.pi*5 # Sinewave freqeucny [rad/sec] 31 | t = np.linspace(0,1,10000) # Time array, 1 second, 1000 sample points. 32 | R = 1 # [Ohm] 33 | L = 0.01 # [Henry] 34 | 35 | V = 1 + np.sin(omega*t) # Desired Voltage Waveform 36 | PWM_Freq = omega * 20 # PWM frequency 37 | PWM = 1 + signal.square(PWM_Freq * t, duty=(V)/2) # PWM signal correlating to the voltage sinusoidal waveform 38 | 39 | # Find voltage by using V to I transfer function. 40 | V_To_I_TF_num = [1] 41 | V_To_I_TF_den = [L, R] 42 | V_To_I_TF = signal.TransferFunction(V_To_I_TF_num, V_To_I_TF_den) 43 | 44 | if (not USE_PWM): 45 | tout,I,xout = signal.lsim(V_To_I_TF, V, t, X0 = 0.01) 46 | else: 47 | tout,I,xout = signal.lsim(V_To_I_TF, PWM, t, X0 = 0.01) 48 | 49 | plt.xlabel('Time [Sec]') 50 | plt.ylabel('[A] OR [V]') 51 | if (not USE_PWM): 52 | plt.plot(t,V, label='V') 53 | else: 54 | plt.plot(t,PWM, label='V') 55 | plt.plot(t,I, label='I') 56 | plt.legend() 57 | plt.show() 58 | -------------------------------------------------------------------------------- /Transformations.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python3 2 | # This file is a library for transformation functions 3 | 4 | import numpy as np 5 | 6 | # Clarke Transform: ABC to Alpha-Beta-0 7 | def abc_to_alphaBeta0(a, b, c): 8 | alpha = (2/3)*(a - b/2 - c/2) 9 | beta = (2/3)*(np.sqrt(3)*(b-c)/2) 10 | return alpha, beta 11 | 12 | # Inverse Clarke Transform: alphaBeta0 to abc 13 | def alphaBeta0_to_abc(alpha, beta): 14 | a = alpha 15 | b = -alpha/2 + beta*np.sqrt(3)/2 16 | c = -alpha/2 - beta*np.sqrt(3)/2 17 | return a, b, c 18 | 19 | 20 | # Inverse Park Transform 21 | # def dq0_to_abc(d, q, wt): 22 | # a = d * np.cos(wt) - q * np.sin(wt) 23 | # b = d * np.cos(wt - 2*np.pi/3) - q * np.sin(wt - 2*np.pi/3) 24 | # c = d * np.cos(wt + 2*np.pi/3) - q * np.sin(wt + 2*np.pi/3) 25 | # return a, b, c 26 | 27 | # Park Transform: 28 | # d-axis aligned with the α-axis. 29 | def alphaBeta0_to_dq0(alpha, beta, wt): 30 | d = alpha*np.cos(wt) + beta*np.sin(wt) 31 | q = -alpha*np.sin(wt) + beta*np.cos(wt) 32 | return q, d 33 | 34 | # # q-axis aligned with the α-axis. 35 | # def alphaBeta0_to_dq0(alpha, beta, wt): 36 | # d = alpha*np.sin(wt) - beta*np.cos(wt) 37 | # q = alpha*np.cos(wt) + beta*np.sin(wt) 38 | # return q, d 39 | 40 | 41 | 42 | 43 | # Inverse Park Transformation: 44 | # d-axis aligned with the α-axis. 45 | def InverseParkTransformation(q, d, wt, delta): 46 | alpha = d * np.cos(wt + delta) - q * np.sin(wt + delta) 47 | beta = d * np.sin(wt + delta) + q * np.cos(wt + delta) 48 | return alpha, beta 49 | 50 | # # q-axis aligned with the α-axis. 51 | # def InverseParkTransformation(q, d, wt, delta): 52 | # alpha = d * np.sin(wt + delta) + q * np.cos(wt + delta) 53 | # beta = -d * np.cos(wt + delta) + q * np.sin(wt + delta) 54 | # return alpha, beta 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | # Inverse Park Transform 68 | def dq0_to_abc(d, q, wt, delta): 69 | a = d*np.sin(wt+delta) + q*np.cos(wt+delta) 70 | b = d*np.sin(wt-(2*np.pi/3)+delta) + q*np.cos(wt-(2*np.pi/3)+delta) 71 | c = d*np.sin(wt+(2*np.pi/3)+delta) + q*np.cos(wt+(2*np.pi/3)+delta) 72 | return a, b, c 73 | 74 | # # Park Transform: abc to dq0 75 | # def abc_to_dq0(a, b, c, wt, delta): 76 | # d = (2/3)*(a*np.sin(wt+delta) + b*np.sin(wt+delta-(2*np.pi/3)) + c*np.sin(wt+delta+(2*np.pi/3))) 77 | # q = (2/3)*(a*np.cos(wt+delta) + b*np.cos(wt+delta-(2*np.pi/3)) + c*np.cos(wt+delta+(2*np.pi/3))) 78 | # return d, q 79 | 80 | # # Park Transform: abc to dq0 81 | # def abc_to_dq0(a, b, c, wt): 82 | # d = (2/3) * (a * np.cos(wt) + b * np.cos(wt - 2*np.pi/3) + c * np.cos(wt + 2*np.pi/3)) 83 | # q = (2/3) * (-a * np.sin(wt) - b * np.sin(wt - 2*np.pi/3) - c * np.sin(wt + 2*np.pi/3)) 84 | # return d, q -------------------------------------------------------------------------------- /Id_Iq.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python3 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | from scipy import signal 6 | import Transformations as trs 7 | import matplotlib.animation as animation 8 | 9 | parameter_to_change = 'none' 10 | # parameter_to_change = 'speed' 11 | # parameter_to_change = 'idiq' 12 | 13 | OMEGA = 2*np.pi*50 # Speed constant [rad/sec] 14 | N = 500 # Number of sampled 15 | T = 0.2 # Plot time [s] 16 | t = np.linspace(0,T,N) # Time array 17 | omega = OMEGA*np.ones(N)# Speed array 18 | 19 | # Index at which to change the selected parameter 20 | chg_index = N//2 21 | 22 | if (parameter_to_change == 'speed'): 23 | omega[chg_index : -1] *= 2 24 | 25 | R = 0.1 # [Ohm] 26 | L = 0.002 # [H] 27 | 28 | # BEMF 29 | Ke = -0.5 # [V/rad/sec] 30 | KeA = Ke * np.sin(omega*t) 31 | KeB = Ke * np.sin(omega*t - np.deg2rad(120)) 32 | KeC = Ke * np.sin(omega*t - np.deg2rad(240)) 33 | BEMFA = KeA * omega 34 | BEMFB = KeB * omega 35 | BEMFC = KeC * omega 36 | 37 | # Desired stator current amplitude 38 | Is = 100 # [A] 39 | dqAng = np.deg2rad(90)*np.ones(N) # dqAng = 0[deg] -> Only Id. dqAng = 90[deg] -> Only Iq 40 | if (parameter_to_change == 'idiq'): 41 | dqAng[chg_index : -1] = np.deg2rad(45) # Change to 45[deg], so 0.707 Iq and negative 0.707 Id 42 | 43 | Iq = Is * np.sin(dqAng) 44 | Id = -Is * np.cos(dqAng) 45 | 46 | # Find ABC current 47 | dAlphaAng = np.deg2rad(0) # Angle in rad betwen Id and alpha, see: https://www.mathworks.com/help/mcb/ref/inverseparktransform.html 48 | Ialpha, Ibeta = trs.InverseParkTransformation(Iq, Id, omega*t, dAlphaAng) 49 | Ia, Ib, Ic = trs.alphaBeta0_to_abc(Ialpha, Ibeta) 50 | 51 | # Find correlating phase voltages (V - BEMF) using I to V transfer function. Non-causal. Add far away pole (At FarPoleFreq) to make it causal. 52 | FarPoleFreq = 10000*R/L 53 | IToV_TF_num = [FarPoleFreq*L, FarPoleFreq*R] 54 | IToV_TF_den = [1, FarPoleFreq] 55 | IToV_TF = signal.TransferFunction(IToV_TF_num, IToV_TF_den) 56 | 57 | tout,Va,xout = signal.lsim(IToV_TF, Ia, t) 58 | tout,Vb,xout = signal.lsim(IToV_TF, Ib, t) 59 | tout,Vc,xout = signal.lsim(IToV_TF, Ic, t) 60 | 61 | DrivingVa = Va + BEMFA 62 | DrivingVb = Vb + BEMFB 63 | DrivingVc = Vc + BEMFC 64 | 65 | Valpha, Vbeta = trs.abc_to_alphaBeta0(DrivingVa, DrivingVb, DrivingVc) 66 | Vq, Vd = trs.alphaBeta0_to_dq0(Valpha, Vbeta, omega*t) 67 | Vamp = np.sqrt(Vq*Vq + Vd*Vd) 68 | 69 | # Animating the graphs 70 | fig, (ax, ax_chg) = plt.subplots(2,1) 71 | # Find min max values of represented data 72 | max_y = int(max(max(Ia), max(BEMFA), max(Va), max(DrivingVa))) + 10 73 | min_y = int(min(min(Ia), min(BEMFA), min(Va), min(DrivingVa))) - 10 74 | 75 | line1 = ax.plot(t[1], Ia[1], label='Phase Current [A]')[0] 76 | line2 = ax.plot(t[1], BEMFA[1], label='Back EMF [V]')[0] 77 | line3 = ax.plot(t[1], Va[1], label='Phase Voltage [V]')[0] 78 | line4 = ax.plot(t[1], DrivingVa[1], label='Driving Voltage [V]')[0] 79 | 80 | ax.set(xlim=[0, T], ylim=[min_y, max_y], xlabel='Time [s]', ylabel='Parameters') 81 | ax.legend() 82 | 83 | if (parameter_to_change == 'idiq'): 84 | line5 = ax_chg.plot(t[1], Iq[1], label='Iq [A]')[0] 85 | line6 = ax_chg.plot(t[1], Id[1], label='Id [A]')[0] 86 | ax_chg.set(xlim=[0, T], ylim=[min(min(Iq - 10), min(Id - 10)), max(max(Iq), max(Id)) + 10], xlabel='Time [s]', ylabel='Current [A]') 87 | ax_chg.legend() 88 | else: 89 | line5 = ax_chg.plot(t[1], omega[1], label='Omega[Rad/sec]')[0] 90 | ax_chg.set(xlim=[0, T], ylim=[-10, max(omega)+10], xlabel='Time [s]', ylabel='Omega [rad/sec]') 91 | ax_chg.legend() 92 | 93 | 94 | def update(frame): 95 | # update the line plot: 96 | line1.set_xdata(t[:frame]) 97 | line1.set_ydata(Ia[:frame]) 98 | line2.set_xdata(t[:frame]) 99 | line2.set_ydata(BEMFA[:frame]) 100 | line3.set_xdata(t[:frame]) 101 | line3.set_ydata(Va[:frame]) 102 | line4.set_xdata(t[:frame]) 103 | line4.set_ydata(DrivingVa[:frame]) 104 | line5.set_xdata(t[:frame]) 105 | if (parameter_to_change == 'idiq'): 106 | line5.set_ydata(Iq[:frame]) 107 | line6.set_xdata(t[:frame]) 108 | line6.set_ydata(Id[:frame]) 109 | return (line1, line2, line3, line4, line5, line6) 110 | else: 111 | line5.set_ydata(omega[:frame]) 112 | return (line1, line2, line3, line4, line5) 113 | 114 | ani = animation.FuncAnimation(fig=fig, func=update, interval=1, frames=N, repeat=False) 115 | manager = plt.get_current_fig_manager() 116 | manager.full_screen_toggle() 117 | plt.show() 118 | -------------------------------------------------------------------------------- /VFD.ino: -------------------------------------------------------------------------------- 1 | /* MIT License 2 | 3 | Copyright (c) 2023 Matan Pazi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | 24 | 25 | 15 sample sine wave look up table. 26 | 16Mhz CPU clk & clkI/O -> Prescaler of 2 -> 8Mhz 27 | 1 OVF scenario will occur every 256 * 2 = 512 samples. 28 | 1/8MHz * 512 = 64[us] 29 | going over the 15 sample look-up table will take 64us * 15 = 0.96[ms] -> 1,041.66[Hz] 30 | I'm aiming for ~100[Hz] and below. 31 | So to achieve a 100[Hz] sine wave, I will need to increment the sine table every 32 | 1,041.66 / 100 = 10.41 OVF scenarios 33 | for 90[Hz] -> 1,041.66 / 90 = 11.57 OVF scenarios etc. 34 | Lower resolution at higher frequencies (Rounded to a whole number) 35 | The amplitude will be derived from the desired frequency and the V/f value (230[VAC]/60[Hz]=3.8333). 36 | For example, w/ a desired freq of 30[Hz], the amplitude will be (3.8333 * 30) / 230 = 0.5 37 | Due to low resolution of compare registers (only 1 byte), attenuating by dividing the sine table values 38 | will eventually lead to a distorted sine wave so need to set a minimum Amp value. 39 | If operating a 3 phase motor, the 3 sine waves need to be 120 def apart 40 | If operating a single phase motor, the phase shift between the main and auxiliary windings won't be 90, but at 120 degrees, will still be reasonable. 41 | phase V will always be connected to GND. 42 | LED Display tutorial: 43 | https://lastminuteengineers.com/tm1637-arduino-tutorial/ 44 | //Atmega328 pin numbers: 45 | http://www.learningaboutelectronics.com/Articles/Atmega328-pinout.php 46 | Dead-time: 47 | Datasheet recommends 1 [us] for each input signal. However, when looking at the datasheet, the turn-on time is 290[ns] and turn off time is 515[ns], so I need a minimum of 48 | 515-290 = 225[ns] between PWM signals. Each clock takes 125[ns] (8[MHz]), so I'll take 125*5 = 625[ns] dead time, just in case since the isolators add delay as well. 49 | // 50 | //To-do ***************************************************************************** 51 | 52 | ************************************************************************************* 53 | */ 54 | #define _DISABLE_ARDUINO_TIMER0_INTERRUPT_HANDLER_ //These 2 lines were added to be able to compile. Also changed wiring.c file. Disables the previous overflow handles used for millis(), micros(), delay() etc. 55 | #include //Reference: https://stackoverflow.com/questions/46573550/atmel-arduino-isrtimer0-ovf-vect-wont-compile-first-defined-in-vector/48779546 56 | #include 57 | #include 58 | #include 59 | #include 60 | #include 61 | 62 | // Module connection pins (Digital Pins) 63 | #define CLK1 12 64 | #define DIO1 2 65 | #define CLK2 7 66 | #define DIO2 4 67 | #define CURR_INPUT A0 68 | #define POT_INPUT A2 69 | // 70 | #define THREE_PH 0 71 | #define ONE_PH 1 72 | #define PWM_RUNNING 2 73 | #define PWM_NOT_RUNNING 1 74 | #define PWM_NOT_SET 0 75 | #define DEADTIME_ADD 5 76 | #define DEADTIME_SUB 2 77 | //millis(), delay() don't work as expected due to use of timers in PWM. 78 | //Chosen through trial and error. 79 | #define SHORT_CLICK 10 80 | #define LONG_CLICK 500 81 | #define POT_SWITCH_SAMPLES 2 82 | #define BOOT_CAP_CHARGE_TIME 160 // Need at least 10 [ms] to charge the boot caps. Chosen through trial and error. 83 | #define RELAY_CHARGE_WAIT 2000000 // Approx. 8 seconds 84 | #define DISPLAY_BLINK 100 85 | #define MIN_PWM_VAL 6 // Due to isolator response time. Found through trial and error 86 | #define BUTTON_IS_PRESSED (!((PINC >> PINC4) & 1)) // Pin 4 of port C (Button) is pressed. when button is pushed, PINC4 is pulled LOW. 87 | #define POT_SWITCH_IS_ON (!((PINC >> PINC3) & 1)) // Pin 3 of port C (Potentiometer switch) is on. when switch is on, PINC3 is pulled LOW. 88 | 89 | const uint8_t ONE_PHASE[] = { 90 | SEG_B | SEG_C, // 1 91 | SEG_A | SEG_B | SEG_E | SEG_F | SEG_G, // P 92 | SEG_B | SEG_C | SEG_E | SEG_F | SEG_G, // H 93 | 0, // space 94 | }; 95 | const uint8_t THREE_PHASE[] = { 96 | SEG_A | SEG_B | SEG_C | SEG_D | SEG_G, // 3 97 | SEG_A | SEG_B | SEG_E | SEG_F | SEG_G, // P 98 | SEG_B | SEG_C | SEG_E | SEG_F | SEG_G, // H 99 | 0, // space 100 | }; 101 | const uint8_t SPACE[] = { 102 | 0, // space 103 | }; 104 | 105 | // Generated using: https://www.daycounter.com/Calculators/Sine-Generator-Calculator.phtml 106 | const float Sine[] = {125.0,179.0,223.0,246.0,246.0,223.0,179.0,125.0,71.0,27.0,6.0,6.0,27.0,71.0,125.0}; 107 | const uint8_t Sine_Len = 15; //Sine table length 108 | const uint8_t Min_Freq = 20; //Minimal demanded sine wave frequency 109 | const uint8_t Max_Freq = 120; //Maximal demanded sine wave frequency 110 | const uint16_t Base_Freq = 1041; //[Hz] Maximal frequency if the sine wave array index is incremented every OVF occurance 111 | const float Min_Amp = 0.1; //Minimal allowed sine wave amplitude 112 | const float Max_Amp = 1.0; //Maximal allowed sine wave amplitude 113 | const float V_f = 3.8333; //V/f value. ~230[VAC] w/ 60[Hz] 114 | const float VBus = 230.0; //AC voltage [VAC] 115 | bool Phase_Config = 0; //0: 3 phase, 1: 1 phase 116 | bool Config_Editable = 0; //Is the configuration editable or not (Between 2 long clicks). 0: No, 1: Yes 117 | // int8_t DT = 1; //Dead time to prevent short-circuit betweem high & low mosfets 118 | int16_t Sine_Used[] = {125,179,223,246,246,223,179,125,71,27,6,6,27,71,125}; 119 | uint8_t Click_Type = 0; //1: Short, 2: Long 120 | uint8_t PWM_Running = PWM_NOT_SET; //Indicates if the PWM is operating or not. 2 is running, 1 is not, initialized to 0 to indicate not yet set. 121 | uint32_t Timer = 0; //Timer, increments every loop 122 | uint32_t Click_Timer = 0; //Increments while button is clicked 123 | uint32_t Pot_Switch_Off_Timer = 0; //Increments while potentiometer switch is OFF 124 | uint32_t Pot_Switch_On_Timer = 0; //Increments while potentiometer switch is ON 125 | uint32_t Display_Timer = 0; //Used for delay of the blinking display (When configurable) 126 | uint32_t Timer_Temp = 0; //Used to make sure of consecutive executions 127 | uint32_t Timer_Temp1 = 0; //Used to make sure of consecutive executions 128 | uint32_t Init_PWM_Counter = 0; //Used for charging the bootstrap capacitors, source below 129 | uint16_t Curr_Value = 0; //Current value measured in [mA] 130 | uint8_t Sine_Index = 0; //3 sine wave indices are used to allow for phase shifted sine waves. 131 | uint8_t Sine_Index_120 = Sine_Len / 3; 132 | uint8_t Sine_Index_240 = (Sine_Len * 2) / 3; //Sine_Len must be lower than 128, otherwise, change eq. 133 | uint8_t OVF_Counter = 0; //Increments every Timer0 overflow 134 | uint8_t OVF_Counter_Compare = 0; //Compare OVF_Counter to this value (Base freq / desired freq). 135 | 136 | 137 | 138 | TM1637Display Display1(CLK1, DIO1); 139 | TM1637Display Display2(CLK2, DIO2); 140 | 141 | void setup() 142 | { 143 | //Load the latest chosen phase configuration, unless this is the first time. 144 | if (EEPROM.read(256) == 123) 145 | { 146 | EEPROM.get(0, Phase_Config); //Set Phase_Config to first byte in EEPROM. 147 | } 148 | cli(); //Disable interrupts 149 | CLKPR = (1 << CLKPCE); //Enable change of the clock prescaler 150 | CLKPR = (1 << CLKPS0); //Set system clock prescaler to 2. Beforehand DT had to be increased to a large value due to IPM, and at low amplitudes distorted sine wave. When reducing the prescaler, this allows for the DT value to be small. 151 | sei(); 152 | //Serial.begin(19200); //Set the baud rate to double that which is set in "Serial Monitor" due to the prescaler being 2 instead of 1. 153 | Display1.setBrightness(0x02); 154 | Display2.setBrightness(0x02); 155 | Display1.clear(); 156 | Display2.clear(); 157 | PORTC = (1 << PORTC3) | (1 << PORTC4); //Activates internal pull up for PC3 (ADC3) and PC4 (ADC4). Default pin state is input. Potentiometer switch and button respectively 158 | DDRD = (1 << DDD6) | (1 << DDD5) | (1 << DDD3); //Sets the OC0A, OC0B and OC2B pins to outputs (Default is LOW) 159 | DDRB = (1 << DDB3) | (1 << DDB2) | (1 << DDB1) | (1 << DDB0); //Sets the OC2A, OC1B and OC1A pins to outputs (Default is LOW) 160 | //And sets PB0 pin to output (Default is LOW). Commands the relay 161 | Wait_A_Bit(RELAY_CHARGE_WAIT); //Using this function since delay() doesn't seem to work correctly when ISR is activated. Not needed to delay if interrupts are enabled. 162 | //Waiting this delay to let the capacitors charge up. Requires ~3 seconds. 163 | PORTB = (1 << PORTB0); //Set output pin to the relay high, to bypass the high power resistor after the caps were sufficiently charged 164 | } 165 | void loop() 166 | { 167 | // Local variables 168 | uint8_t Desired_Freq; //Desired sine wave freq [Hz] 169 | uint8_t OVF_Counter_Compare_Temp; //Used temporarily for OVF_Counter_Compare 170 | float Amp; //Sine wave amplitude 171 | 172 | //Calculate variables 173 | Curr_Value = analogRead(CURR_INPUT) << 3; //A value of 1023 (5V) -> 8000[mA]. Multiply the analog reading by 8. Gives a resolution of ~8[mA] 174 | Desired_Freq = ((uint8_t)(analogRead(POT_INPUT) >> 3)); //A value of 1023 (5V) -> 128[Hz]. Divide result by 8 to get value in Hz. Gives resolution of 1[Hz] 175 | if (Desired_Freq < Min_Freq) Desired_Freq = Min_Freq; 176 | else if (Desired_Freq > Max_Freq) Desired_Freq = Max_Freq; 177 | OVF_Counter_Compare_Temp = (uint8_t)(Base_Freq / Desired_Freq); //Decides after how many interrupts should I increment the look-up table index. 178 | //ATOMIC_BLOCK(ATOMIC_RESTORESTATE) // See why ATOMIC_BLOCK may be needed in comments below. Pretty much disables interrupts for this calculation. 179 | { 180 | OVF_Counter_Compare = OVF_Counter_Compare_Temp; // If ISR interrupts in middle of calculation, may give completley false OVF_Counter_Compare value. May be problematic. 181 | // Using temp variable in previous line so the ISR won't interrupt in the middle of calclating this parameter. 182 | // Setting a local 8 bit variable takes 1 clock cycle, see reference: https://ww1.microchip.com/downloads/en/Appnotes/doc1497.pdf 183 | // Still consider using ATOMIC_BLOCK(ATOMIC_RESTORESTATE) for this calculation since these variables are global. 184 | // Using global variables takes longer due to LDS and STS, See page 12 in reference above. 185 | // So setting may still take more than a clock cycle (?) 186 | // See reference for ATOMIC_BLOCK: https://forum.arduino.cc/t/demonstration-atomic-access-and-interrupt-routines/73135 187 | } 188 | Amp = ((float)(Desired_Freq) * V_f) / VBus; // Calculating the sine wave amplitude based on the desired frequency and the V/f value. 189 | if (Amp < Min_Amp) Amp = Min_Amp; 190 | else if (Amp > Max_Amp) Amp = Max_Amp; 191 | //ATOMIC_BLOCK(ATOMIC_RESTORESTATE) // ATOMIC_BLOCK may be needed here as well... 192 | { 193 | for (int i = 0; i < Sine_Len; i++) 194 | { 195 | Sine_Used[i] = (int16_t)(Amp * Sine[i]); 196 | } 197 | } 198 | //Run functions 199 | Pot_Switch_State_Check(); 200 | if (PWM_Running != PWM_RUNNING) Button_Click(); 201 | if (PWM_Running != PWM_NOT_SET) Display(PWM_Running, Config_Editable, Desired_Freq); 202 | Timer++; 203 | } 204 | 205 | 206 | 207 | /* Pot_Switch_State_Check(): Detects the state of the potentiometer switch 208 | If it is ON a sufficiently long time (Avoid false triggers) PWM starts running with defined configuration and displays the measured current and the desired frequency. 209 | Otherwise, if it is OFF a sufficiently long time (Avoid false triggers) PWM is disabled and the configuration 210 | is displayed (1PH/3PH) and allowed to be altered using the button. 211 | */ 212 | void Pot_Switch_State_Check() 213 | { 214 | if (POT_SWITCH_IS_ON) 215 | { 216 | if (PWM_Running != PWM_RUNNING) //If PWM isn't running (If it was already running, no need to update anything). 217 | { 218 | if (Timer - Timer_Temp > 1) Pot_Switch_On_Timer = 0; //To make sure these increments are consecutive 219 | else Pot_Switch_On_Timer++; 220 | Timer_Temp = Timer; 221 | Pot_Switch_Off_Timer = 0; 222 | if (Pot_Switch_On_Timer > POT_SWITCH_SAMPLES) //If potentiometer switch was ON a sufficient amount of time 223 | { 224 | Pwm_Config(); 225 | Pot_Switch_On_Timer = 0; 226 | Pot_Switch_Off_Timer = 0; 227 | Display1.clear(); 228 | Display2.clear(); 229 | } 230 | } 231 | } 232 | else //Potentiometer switch OFF 233 | { 234 | if (PWM_Running != PWM_NOT_RUNNING) //If PWM is running (If it was already not running, no need to update anything). 235 | { 236 | if (Timer - Timer_Temp > 1) Pot_Switch_Off_Timer = 0; //To make sure these increments are consecutive 237 | else Pot_Switch_Off_Timer++; 238 | Timer_Temp = Timer; 239 | Pot_Switch_On_Timer = 0; 240 | if (Pot_Switch_Off_Timer > POT_SWITCH_SAMPLES) //If potentiometer switch was OFF a sufficient amount of time 241 | { 242 | Pwm_Disable(); 243 | Pot_Switch_On_Timer = 0; 244 | Pot_Switch_Off_Timer = 0; 245 | Display1.clear(); 246 | Display2.clear(); 247 | } 248 | } 249 | } 250 | } 251 | 252 | 253 | /* Button_Click(): Detects a button click. Determines if it was a short click: 254 | switch between configuration if multiple are available or alter the configuration. 255 | Or a long click: Enables and disables the ability to alter the configuration. 256 | */ 257 | void Button_Click() 258 | { 259 | if (BUTTON_IS_PRESSED) 260 | { 261 | if (Timer - Timer_Temp1 > 1) Click_Timer = 0; //To make sure these increments are consecutive 262 | else Click_Timer++; 263 | Timer_Temp1 = Timer; 264 | if (Click_Timer == LONG_CLICK) 265 | { 266 | Config_Editable = !Config_Editable; //Toggle 267 | } 268 | } 269 | else if (Click_Timer > SHORT_CLICK && Click_Timer < LONG_CLICK) 270 | { 271 | if (Config_Editable) 272 | { 273 | Phase_Config = !Phase_Config; //Toggle 274 | EEPROM.write(0, Phase_Config); //Save latest value to EEPROM 275 | EEPROM.write(256, 123); //Update that the Phase_Config was saved to EEPROM (Write a value of 123 to byte 256, arbitrary numbers) 276 | } 277 | Click_Timer = 0; 278 | } 279 | else Click_Timer = 0; 280 | } 281 | 282 | 283 | /* Display(): Chooses what and how to display on the 2 available LED displays. 284 | 3 inputs: 285 | 1. PWM_Running, whether or not the PWM is active or not. 286 | 2. Blink, whether to cause the display to blink or not. signifies if the configuration is editable or not. 287 | Blinking: Editable, Not blinking: Not editable. 288 | 3. Desired_Freq, the desired frequency to display. 289 | */ 290 | void Display(uint8_t PWM_Running, bool Blink, uint8_t Desired_Freq) 291 | { 292 | if (PWM_Running == PWM_RUNNING) 293 | { 294 | //Display 1, displays desired frequency in [Hz] 295 | if (Desired_Freq > 99) Display1.showNumberDec(Desired_Freq, false, 3, 1); 296 | else 297 | { 298 | Display1.setSegments(SPACE, 1, 1); 299 | Display1.showNumberDec(Desired_Freq, false, 2, 2); 300 | } 301 | //Display 2, displays measured current in [mA] 302 | if (Curr_Value > 999) Display2.showNumberDec(Curr_Value, false, 4, 0); 303 | else 304 | { 305 | Display2.setSegments(SPACE, 1, 0); 306 | Display2.showNumberDec(Curr_Value, false, 3, 1); 307 | } 308 | } 309 | else 310 | { 311 | Display_Timer++; 312 | if (Blink && (Display_Timer == DISPLAY_BLINK)) 313 | { 314 | Display1.clear(); 315 | Display2.clear(); 316 | } 317 | else if (Display_Timer > (2*DISPLAY_BLINK)) 318 | { 319 | if (Phase_Config) Display1.setSegments(ONE_PHASE); 320 | else Display1.setSegments(THREE_PHASE); 321 | Display_Timer = 0; 322 | } 323 | } 324 | } 325 | 326 | 327 | 328 | /* Wait_A_Bit(): Delays for a specified amount of executions 329 | */ 330 | void Wait_A_Bit(uint32_t Executions_To_Wait) 331 | { 332 | volatile uint32_t Timer_Temp2 = 0; 333 | while (Timer_Temp2 < Executions_To_Wait) 334 | { 335 | Timer_Temp2++; 336 | } 337 | } 338 | 339 | 340 | 341 | 342 | /* Pwm_Disable(): Disables the PWM and zeros some parameters 343 | PWM_Running indicates the PWM is disabled 344 | Init_PWM_Counter is used to charge the bootstrap capacitors everytime the PWM is enabled 345 | */ 346 | void Pwm_Disable() 347 | { 348 | PWM_Running = PWM_NOT_RUNNING; 349 | cli(); 350 | Init_PWM_Counter = 0; 351 | TCCR0A = 0; 352 | TCCR0B = 0; 353 | TCCR1A = 0; 354 | TCCR1B = 0; 355 | TCCR2A = 0; 356 | TCCR2B = 0; 357 | sei(); 358 | } 359 | 360 | void Pwm_Config() 361 | { 362 | //***Check in scope. Need to make sure the pins are LOW prior to and after setting them to outputs so don't accidentally cause short in IPM. 363 | if (Phase_Config == THREE_PH) 364 | { 365 | cli(); //Disable interrupts 366 | PWM_Running = PWM_RUNNING; 367 | //Synchronising all 3 timers 1st segment. Source: http://www.openmusiclabs.com/learning/digital/synchronizing-timers/index.html 368 | GTCCR = (1<= OVF_Counter_Compare) 438 | { 439 | if (Sine_Index == Sine_Len) Sine_Index = 0; 440 | if (Sine_Index_120 == Sine_Len) Sine_Index_120 = 0; 441 | if (Sine_Index_240 == Sine_Len) Sine_Index_240 = 0; 442 | // 443 | if ((Sine_Used[Sine_Index] - DEADTIME_SUB) < MIN_PWM_VAL) 444 | { 445 | OCR0A = 0; 446 | } 447 | else 448 | { 449 | OCR0A = uint8_t(Sine_Used[Sine_Index] - DEADTIME_SUB); 450 | } 451 | OCR0B = OCR0A + DEADTIME_ADD; 452 | 453 | if ((Sine_Used[Sine_Index_120] - DEADTIME_SUB) < MIN_PWM_VAL) 454 | { 455 | OCR1A = 0; 456 | } 457 | else 458 | { 459 | OCR1A = uint8_t(Sine_Used[Sine_Index_120] - DEADTIME_SUB); 460 | } 461 | OCR1B = OCR1A + DEADTIME_ADD; 462 | 463 | if ((Sine_Used[Sine_Index_240] - DEADTIME_SUB) < MIN_PWM_VAL) 464 | { 465 | OCR2A = 0; 466 | } 467 | else 468 | { 469 | OCR2A = uint8_t(Sine_Used[Sine_Index_240] - DEADTIME_SUB); 470 | } 471 | OCR2B = OCR2A + DEADTIME_ADD; 472 | 473 | OVF_Counter = 0; 474 | Sine_Index++; 475 | Sine_Index_120++; 476 | Sine_Index_240++; 477 | } 478 | } 479 | --------------------------------------------------------------------------------