├── Examples ├── Example_python_FOPDT_on_off.py ├── Examples.md ├── README.md ├── class_controller_pid_awm_example.py ├── drawnings │ ├── isa_awsel0_neg.png │ ├── isa_awsel1_neg.png │ ├── o.txt │ ├── on_off_FOPDT_example2_1_neg.png │ ├── on_off_FOPDT_example2_neg.png │ ├── on_off_control_fopdt_example_neg.png │ ├── pid_awm_class_p1_neg.png │ └── pid_isa_awm_1_neg.png ├── example_isa_awm_1.py └── on_off_control_example.py ├── LICENSE ├── README.md ├── drawnings ├── PID_diagram_neg.png ├── curve_gen2_neg.png ├── curve_gen_neg.png ├── deadband_block.png ├── deadband_graph.png ├── limit_block.png ├── limit_graph.png ├── mv_tracking_signal_neg.png ├── norm_block.png ├── norm_graph.png ├── norm_sqrt_block.png ├── norm_sqrt_graph.png ├── pid_aw_neg.png ├── pid_awm_neg.png ├── pid_block_schema_neg.png ├── pid_isa_schema_neg.png ├── rateLimit_block.png ├── read.txt ├── relay2h_block.png ├── relay2h_graph.png ├── relay3_block.png ├── relay3_graph.png ├── relay3h_block.png ├── relay3h_graph.png ├── relay_block.png └── relay_graph.png ├── functional_description.md ├── process_models ├── RingBuffer.py ├── readme.md └── simple_models_esp.py ├── python_simulation ├── read_me.txt └── simple_pid_FOPDT.py └── src ├── curve_generator.py ├── info.txt ├── mv_processing.py ├── on_off_control.py ├── pid_aw.py ├── pid_isa.py ├── pv_processing.py ├── sp_processing.py ├── thermocouples ├── README.md ├── benchmark.txt ├── its90_J.py ├── its90_J_blookup.py ├── its90_J_lookup.py ├── its90_K.py ├── its90_K_blookup.py ├── its90_K_lookup.py ├── lookup_search.py ├── model_K.py └── test_its90_K_thermo.py └── utils_pid_esp32.py /Examples/Example_python_FOPDT_on_off.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Onoff R&D simulator for display 4 | data 5 | 6 | @author: 2dof 7 | controller clasee/functions: 8 | relay2h() 9 | OnOf_controller) 10 | 11 | process model classes/functions: 12 | ring_buffer() 13 | discrete_process() 14 | FOPDT_model() 15 | """ 16 | 17 | 18 | # model process 19 | class ring_buffer(object): 20 | def __init__(self, bufferSize =5): 21 | 22 | self.bufferSize = bufferSize 23 | self.data = np.zeros(bufferSize) 24 | self.ii = 0 # index of next "head" position 25 | self.it = 0 # index of tail position 26 | self.ndata = 0 # No of data 27 | 28 | def add_data(self,data): 29 | 30 | if self.ndata>=self.bufferSize: 31 | self.ndata-=1 32 | 33 | self.data[self.ii] = data 34 | self.ndata+=1 35 | self.ii = (self.ii+1) % self.bufferSize # 36 | 37 | def get_data(self): 38 | 39 | if self.ndata ==0 : 40 | return [] 41 | 42 | x = self.data[self.it] 43 | self.ndata-=1 44 | self.it = (self.it+1) % self.bufferSize # 45 | 46 | return x 47 | 48 | 49 | def discrete_process(y_1,Ts,u,Kp,taup): 50 | 51 | yk= taup/(taup+Ts)*y_1 + Kp*Ts/(taup+Ts) * u 52 | return yk 53 | 54 | class FOPDT_model(object): 55 | def __init__(self,Kp=2.0,taup=10.0,delay=3.0, y0=0.0,Ts=0.1): 56 | 57 | self.Kp = Kp 58 | self.taup = taup 59 | self.delay = delay 60 | self.Ts = Ts 61 | self.y_1 = y0 62 | self.ndelay = int(self.delay/self.Ts) 63 | self.buf = ring_buffer(bufferSize =self.ndelay+1) 64 | self.buf.ii = self.ndelay 65 | 66 | def update(self,u): 67 | 68 | self.buf.add_data(u) # delay line 69 | uk=self.buf.get_data() 70 | yk= discrete_process(self.y_1,self.Ts,uk,self.Kp,self.taup) 71 | self.y_1 =yk 72 | 73 | return yk 74 | 75 | #===Controller================================ ======= 76 | class relay2h(object): 77 | """relay 2 position with hysteresis 78 | wL : low level of hysteresis 79 | wH : High level of hysteresis 80 | """ 81 | def __init__(self, wL=-1,wH=1): 82 | self.wL=wL 83 | self.wH=wH 84 | self.y_1=-1 85 | 86 | def get(self,x): 87 | """ 88 | """ 89 | if ((x>=self.wH)|((x>self.wL)&(self.y_1==1))): 90 | self.y_1=1 91 | return 1 92 | elif((x<=self.wL)|((x histL : 101 | self.relay =relay2h(histL ,histH) 102 | else: 103 | self.relay =relay2h(-1 ,1) 104 | 105 | self.uk=0 106 | self.ek = 0.0 # sp-pv 107 | self.Fstart = False # 108 | 109 | def start(self): # start/stop calculationg control 110 | self.Fstart = True 111 | 112 | def stop(self): 113 | self.Fstart = False 114 | self.uk=0 115 | 116 | def tune(self,histL,histH): 117 | 118 | if histH> histL: 119 | self.relay.wL = histL 120 | self.relay.wH = histH 121 | 122 | else: 123 | self.relay.wL=-1 124 | self.relay.wH= 1 125 | print('wrong Hysteresis params, must be: histL> histH') 126 | 127 | def reset(self): # reset all internal state 128 | 129 | self.relay.y_1=-1 130 | self.ek = 0.0 131 | self.uk =0 132 | 133 | def updateControl(self,sp,pv): 134 | 135 | self.ek = sp - pv 136 | 137 | if self.Fstart: 138 | 139 | self.uk = self.relay.get(self.ek) # will return -1 or 1 so 140 | self.uk =(self.uk+1)//2 # we shift from 0 to 1 141 | 142 | else: 143 | self.reset() 144 | 145 | return self.uk 146 | 147 | 148 | if __name__ == "__main__": 149 | 150 | from matplotlib.pylab import * 151 | import matplotlib.pyplot as plt 152 | plt.rcParams['lines.linewidth'] = 1.0 153 | plt.rcParams['lines.markersize'] = 2 154 | plt.rcParams["savefig.facecolor"]='white' 155 | plt.rcParams["figure.facecolor"]='white' 156 | plt.rcParams["axes.facecolor"]='white' 157 | plt.ion() 158 | matplotlib.pyplot.close 159 | 160 | from numpy import random 161 | 162 | # specify simulation time and number of steps 163 | Tstop=1500 # total time simulation 164 | Ts = 0.25 # sampling time 165 | Ns=int(Tstop/Ts)+1 166 | t = np.linspace(0,Tstop,Ns) # define time vector (for plotting) 167 | 168 | #[1] process model FOPDT 169 | yo=21 # initial value (21 Celsjus) 170 | Lo=1 # proces delay 171 | To=200 # process time constant 172 | model = FOPDT_model(Kp=12.0,taup=To,delay=Lo, y0=yo,Ts=Ts) 173 | 174 | # just for plotting 175 | xsp = np.zeros(Ns) 176 | y = np.zeros(Ns) 177 | xu = np.zeros(Ns) 178 | y0 = np.zeros(Ns) 179 | xe = np.zeros(Ns) 180 | xupid = np.zeros(Ns) 181 | 182 | # init simulation 183 | sp = 50. # setpoint 184 | yk = 0. # proces outpout value (measured) 185 | uk = 0. # control value 186 | 187 | # ON-OFF controller initializasion 188 | controller = OnOf_controller(-1,1) 189 | controller.start() 190 | 191 | # main simulation loop 192 | # yk : proces output 193 | # pv = yk +vk : proces value measurement with measurement noise 194 | # sp : setpoint 195 | # uk : control output 196 | # controller.ek: ek = sp-pv controll error 197 | 198 | for k in range(0,Ns): 199 | 200 | # proces simulation 201 | yk=model.update(uk) 202 | 203 | # pv measuring (add measurement noise) 204 | vk = random.uniform(0, 1) # 205 | pv = yk +vk 206 | 207 | 208 | # change SP during simulation 209 | 210 | if k*Ts >=250 and k*Ts<400: 211 | sp = 40 212 | 213 | if (k*Ts >=400) and (k*Ts <775): 214 | sp+=0.04 215 | 216 | if (k*Ts >=1000) and (sp>40): 217 | sp-=0.04 218 | 219 | 220 | 221 | 222 | # update control 223 | uk = controller.updateControl(sp, pv) 224 | 225 | # save data 226 | xsp[k], y[k], xu[k],xe[k]= sp,pv,uk ,controller.ek 227 | 228 | 229 | uk = uk*1 # gain = 0 230 | 231 | 232 | # plot sumulation results 233 | plt.close('all') 234 | fig, (ax1, ax2) = plt.subplots(2, sharex=True) 235 | ax1.plot(t,xsp,'r-',label=r'$sp$') 236 | ax1.plot(t,y,'b-',label=r'$pv$') 237 | ax1.set_xlabel('t[s]') 238 | ax1.grid(True); ax1.legend(fontsize=10) 239 | 240 | ax2.plot(t,xu,'r-',label=r'$uk$') 241 | #ax2.plot(t,xe,'g-',label=r'$ek$') 242 | ax2.set_xlabel('t[s]') 243 | #ax2.plot(t,xupid,'r--',label=r'$upidk$') 244 | ax2.grid(True); ax2.legend(fontsize=10) 245 | 246 | 247 | 248 | 249 | -------------------------------------------------------------------------------- /Examples/Examples.md: -------------------------------------------------------------------------------- 1 | 2 | **Example 1: PID-ISA with anti-windup** 3 | 4 | In this example we show how to implement a anti-windup scheme for pid-isa controller (see block diagram below), 5 | additionaly, we will use manual value (MV) processing with Man/Auto switch to show how 6 | MV processing will be tracking a control value (CV). 7 | 8 | 9 | 10 | Simulation will be done "in the loop" with First Order Process with Delay Time (FOPDT) as controlled process. 11 | 12 | Whole code: example_isa_awm_1.py 13 | 14 | 15 | ```python 16 | from pid_isa import * 17 | from mv_processing import * 18 | from simple_models import FOPDT_model 19 | 20 | from random import uniform 21 | 22 | # simulation time init 23 | Tstop=20 24 | Ts = 0.1 25 | Ns=int(Tstop/Ts) 26 | ``` 27 | 28 | 29 | 30 | ``` Python 31 | #[1] process model FOPDT 32 | model = FOPDT_model(Kp=2.0,taup=3.0,delay=1.0, y0=0.0,Ts=Ts) 33 | 34 | #[2] PID Controller initialization 35 | pid_buf=bytearray(128) # size of ISA_REGS is 128 bytes, 36 | PID = uctypes.struct(uctypes.addressof(pid_buf), ISA_REGS, uctypes.LITTLE_ENDIAN) 37 | isa_init0(PID) 38 | 39 | #[3] Manual Value initialization 40 | mv_buf=bytearray(41) 41 | MVR = uctypes.struct(uctypes.addressof(mv_buf), MV_REGS, uctypes.LITTLE_ENDIAN) 42 | mv_init0(MVR) 43 | 44 | MVR.Ts = Ts 45 | MVR.MvHL = 0.95* PID.Umax # we seta limits as 95% of Umax andUmin 46 | MVR.MvLL = 0.95* PID.Umin 47 | mv_tune(MVR) 48 | ``` 49 | 50 | 51 | 52 | ``` Python 53 | #4 - pid tuning 54 | # we assume that we know process params calculated from proces step responce 55 | # see https://yilinmo.github.io/EE3011/Lec9.html#org2a34bd4 56 | 57 | Ko = 2 # -> Kp 58 | To = 3. # -> taup 59 | Lo = 1. # -> delay 60 | 61 | # calculate The PID parameters from Ziegler-Nichols Rules 62 | # PID.Ts = Ts 63 | # PID.Kp = 1.2*To/(Ko*Lo) 64 | # PID.Ti = 2*Lo 65 | # PID.Td = 0.5*Lo 66 | # PID.Tm = PID.Td/10. 67 | 68 | #Tuning based on IMC (internal Model Control) for 2-dof pid 69 | #tauc = max(1*Ko, 8*Lo) # 'moderate' tutning 70 | tauc = max(1*To, 1*Lo) # 'agressive' tuning 71 | PID.Kp =((To+0.5*Lo)/(tauc+0.5*Lo))#/Ko 72 | PID.Ti =(To+0.5*Lo) 73 | PID.Td = To*Lo/(2*To+Lo) 74 | PID.Tm = PID.Td/10. 75 | 76 | isa_tune(PID) # P-I-I secetect 77 | 78 | PID.CFG.Awsel = False # True 79 | PID.CFG.Dsel = True # True 80 | ``` 81 | 82 | 83 | ```python 84 | # init simulation 85 | sp = 50. # setpoint 86 | yk = 0. # proces outpout value (measured) 87 | uk = 0. # control value 88 | dmv = 0. # change in manual value 89 | mv = 0. # manual value 90 | utr = uk 91 | 92 | for i in range(Ns): 93 | #[a] Read process value (in real time wee read from ADC and do pv processing) 94 | yk = process_model.update(uk) 95 | #vn = uniform(-0.4,0.4) # white noise 96 | pv = yk #+ vn 97 | 98 | # [b]update setpoint (in real solution we do some sp processing) 99 | sp=50 100 | if i >=100: 101 | sp = -50 102 | 103 | # #[c] update mv processing 104 | mv = mv_update(MVR,dmv,uk) 105 | 106 | # #[d] update control pid, 107 | u = isa_updateControl(PID,sp , pv, utr,ubias = 0.) 108 | 109 | # # [e]We are in AUTO mode (PID.CFG.Mansel=False) so we do not mv this time. 110 | if PID.CFG.Mansel: 111 | u = mv # we get manual value 112 | 113 | # # [f] saturation checking 114 | uk = limit(u, PID.Umin,PID.Umax) 115 | 116 | utr = uk # do not forget update tracking 117 | 118 | #[g] in real time do some control value processing we sent uk to DAC,and wait Ts 119 | 120 | print("sp:",sp,"pv:",pv,"uk:",uk,'mv:',mv) 121 | 122 | ``` 123 | 124 | 125 | 126 | 130 | 135 |
127 |

figure 1.0. Anti-windup Off
128 | 129 |

131 |

figure 1.1. Anti-windup ON
132 | 133 | 134 |

136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /Examples/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Contents 4 | 1. [Example 1: PID-ISA with anti-windup](#1-pid-isa-with-anti-windup) 5 | 2. [Example 2: Class controller example - implementing OOP tutorial](#2-class-controller-example ) 6 | 2. [Example 3: ON-OFF controller: simulation](#3-on-off-controller) 7 | 8 | 9 | 10 | 11 | 12 | # 1. PID-ISA with anti-windup 13 | 14 | In this example we show how to implement a anti-windup scheme for pid-isa controller (see block diagram below), 15 | we will use manual value (MV) processing with Man/Auto switch to show how MV processing will be tracking a control value (CV). 16 | 17 | 18 | 19 | Simulation will be done "in the loop" with First Order Process with Delay Time (FOPDT) as controlled process. 20 | 21 | Whole code: example_isa_awm_1.py 22 | 23 | First, import pid control, manual value processing functions and process model from ```simple_models_esp.py```, We set simulation time and 24 | define sampling time as Ts =0.1. 25 | 26 | ```python 27 | from pid_isa import * 28 | from mv_processing import * 29 | from simple_models_esp import FOPDT_model 30 | 31 | from random import uniform 32 | 33 | # simulation time init 34 | Tstop=20 35 | Ts = 0.1 36 | Ns=int(Tstop/Ts) 37 | ``` 38 | 39 | We create FOPDT process model with delay 1 sec, proces time constant taup=3.0 sec, and process gain Kp=2, next 40 | we initialize a P-I-D controller and (PID structure) and Manual Value MVR structure, We set 41 | MvHL ,MvLL slightly smaller than control Umin/Umax. 42 | 43 | ``` Python 44 | #[1] process model FOPDT 45 | process_model = FOPDT_model(Kp=2.0,taup=3.0,delay=1.0, y0=0.0,Ts=Ts) 46 | 47 | #[2] PID Controller initialization 48 | pid_buf=bytearray(128) # size of ISA_REGS is 128 bytes, 49 | PID = uctypes.struct(uctypes.addressof(pid_buf), ISA_REGS, uctypes.LITTLE_ENDIAN) 50 | isa_init0(PID) 51 | 52 | #[3] Manual Value initialization 53 | mv_buf=bytearray(41) 54 | MVR = uctypes.struct(uctypes.addressof(mv_buf), MV_REGS, uctypes.LITTLE_ENDIAN) 55 | mv_init0(MVR) 56 | 57 | MVR.Ts = Ts 58 | MVR.MvHL = 0.95* PID.Umax # we seta limits as 95% of Umax andUmin 59 | MVR.MvLL = 0.95* PID.Umin 60 | mv_tune(MVR) 61 | ``` 62 | 63 | Next we perform a PID tuning, as example two method are presented: a standard step responce (Ziegler-Nichols method) and 64 | tuning based on IMC (internal Model Control) , note that IMC is for 2-dof controller (i.e D-action is calculated not for 65 | error = sp - pv but for -y value - we use standard error = sp - pv for D-calculation). 66 | After calling ```isa_tune(PID)``` we set selectors for anti-windup (Awsel) and PI/PID select control - We will change selectors 67 | for testing our setup. 68 | 69 | ``` Python 70 | #4 - pid tuning 71 | # we assume that we know process params calculated from proces step responce 72 | # see https://yilinmo.github.io/EE3011/Lec9.html#org2a34bd4 73 | 74 | Ko = 2 # -> Kp 75 | To = 3. # -> taup 76 | Lo = 1. # -> delay 77 | 78 | # calculate The PID parameters from Ziegler-Nichols Rules 79 | # PID.Ts = Ts 80 | # PID.Kp = 1.2*To/(Ko*Lo) 81 | # PID.Ti = 2*Lo 82 | # PID.Td = 0.5*Lo 83 | # PID.Tm = PID.Td/10. 84 | 85 | #Tuning based on IMC (internal Model Control) for 2-dof pid 86 | #tauc = max(1*Ko, 8*Lo) # 'moderate' tutning 87 | tauc = max(1*To, 1*Lo) # 'agressive' tuning 88 | PID.Kp =((To+0.5*Lo)/(tauc+0.5*Lo))#/Ko. 89 | PID.Ti =(To+0.5*Lo) 90 | PID.Td = To*Lo/(2*To+Lo) 91 | PID.Tm = PID.Td/10. 92 | 93 | isa_tune(PID) # P-I-I secetect 94 | 95 | PID.CFG.Awsel = False # True 96 | PID.CFG.Dsel = True # True 97 | ``` 98 | Because we simulate control in the loop we: 99 | - initialize parameters, setpoint sp = 50 (we change in middle of the iteration to sp=-50 100 | - initialize other parameters. 101 | 102 | a) - read value yk from process model and simulate a measurement ```pv = yk + vn```,(vn- noise), for first test we do not add noise. 103 | 104 | b) - set sp value and change during simulation 105 | 106 | c) - update manual value processing ( we will just use to show tracking functionality) 107 | 108 | d) - update control value, because pid-isa (isa_updateControl()) has not implemented Man/Auto switch (but PID structure has selector) we 109 | add selector Man/Auto 110 | 111 | e) in Manual mode (Mansel =True) we just overwrite output control value form ```isa_updateControl()``` with manual value mv (We will not use 112 | switch Man/Auto in this example) 113 | 114 | f) - we call ```limit()``` function to limit control signal, and we feed-up output uk control signal to tracking input utr in ```isa_updateControl()``` function, 115 | when PID.CFG.Awsel = True the anti-windup (with back calculation) will be active. 116 | 117 | g) last step is just to print signals. 118 | 119 | ```python 120 | # init simulation 121 | sp = 50. # setpoint 122 | yk = 0. # proces outpout value (measured) 123 | uk = 0. # control value 124 | dmv = 0. # change in manual value 125 | mv = 0. # manual value 126 | utr = uk 127 | 128 | for i in range(Ns): 129 | #[a] Read process value (in real time wee read from ADC and do pv processing) 130 | yk = process_model.update(uk) 131 | #vn = uniform(-0.4,0.4) # white noise 132 | pv = yk #+ vn 133 | 134 | # [b]update setpoint (in real solution we do some sp processing) 135 | sp=50 136 | if i >=100: 137 | sp = -50 138 | 139 | # #[c] update mv processing 140 | mv = mv_update(MVR,dmv,uk) 141 | 142 | # #[d] update control pid, 143 | u = isa_updateControl(PID,sp , pv, utr,ubias = 0.) 144 | 145 | # # [e]We are in AUTO mode (PID.CFG.Mansel=False) so we do not mv this time. 146 | if PID.CFG.Mansel: 147 | u = mv # we get manual value 148 | 149 | # # [f] saturation checking 150 | uk = limit(u, PID.Umin,PID.Umax) 151 | 152 | utr = uk # do not forget update tracking 153 | 154 | #[g] in real time do some control value processing we sent uk to DAC,and wait Ts 155 | 156 | print("sp:",sp,"pv:",pv,"uk:",uk,'mv:',mv) 157 | 158 | ``` 159 | Below printed data are presented on plots for easier analysis. Figure 1. present process control without 160 | anti-windup, the second with antntiwindup active. 161 | On the bottom chart we can see how manual value (mv) track a control signal to ensure bumpless swiching from Auto to Manual control mode. 162 | 163 | 164 | 165 | 169 | 173 |
166 |

figure 1.0. Anti-windup Off
167 | 168 |

170 |

figure 1.1. Anti-windup ON
171 | 172 |

174 | 175 | In real time implementation all signal processing ( from reading process, value to control calcupation) shoud be implemented as 176 | timer interrupt callback function, all parameters update and tuning shoud be done after end of control process update. 177 | 178 | 179 | 180 | # 2. Class controller example 181 | 182 | This tutorial will cover: 183 | - Part 1: Implementing basic p-i-d controller as class object 184 | - Part 2: Using timer interrupt (esp32 micropython) 185 | - Part 3: Using uasyncio instead of timer interrupt 186 | 187 | 188 | Tutorial will not cover how to implement fully application (menu system, loading data from memory) but some sugestions will be added in part 2. 189 | 190 | In this example, a ```pid_awm_updateControl()``` function from pid_aw.py will be used as the main p-i-d algorithm, and a FOPDT_model (from simple_models_esp.py) will be used for simulation. 191 | 192 | **introduction** 193 | 194 | A basic sequence in digital (discrete) p-i-d implementation is ("PID Controllers - Theory, Design and Tuning" by Karl J. Aström and Tore Hägglund, sec. 3.6 Digital Implementation). 195 | 196 | (1) Wait for clock interrupt. 197 | (2) Read analog input (Process value, (Setpoint)). 198 | (3) Compute control signal. 199 | (4) Set analog output ( before do some preprocessing) 200 | (5) Update controller variables 201 | (6) Go to 1 202 | 203 | From above, we can notice that step (5) is done after setting physical output (the control value (Cv) should be calculated and updated as fast as possible), which means that the controller's parameters and variables can't be modified during Cv calculations (to avoid unexpected behavior). Because p-i-d parameters: Kp, Ti, Td, Tm, Td, Umax, Umin, dUlim and Ts (see  [2.2 PID with anti-windup](https://github.com/2dof/esp_control/#22-pid-with-anti-windup) for full structure description) can be changed at any time by the user, we need a copy of parameters, and that functionality will be implemented in Part 1 of the tutorial. 204 | 205 | In step (2) some additional signal processing (at least unit conversion) is needed, and the setpoint (Sp) can be set by a potentiometer, so noise filtering may be necessary. In hardware solution, at least Sp, Pv, error, Cv  should be presented on display, and for Sp value setting (user->Sp setting (with potentiometer)-display->user), the frequency of the display update can differ from the p-i-d sampling time (especially when we control a slow process, a Cv can be calculated even every few seconds); in that case, a user menu system (for the keyboard and display) should take care of Sp noise filtration). 206 | In step (4), we usually set a PWM output as an analog output, but it can also be  PWM + DO (digital output) (ffor DC motor control with direction), or just DO (On-off control). 207 | 208 | **Part 1: P-I-D Class** 209 | 210 | The code for the pid_awm_controller() class is presented below. As a basic, we have: 211 | - pid structure, descibed in [2.2 PID with anti-windup](https://github.com/2dof/esp_control/#22-pid-with-anti-windup)) 212 | - local copy of parameters Kp,Ti,Td,Tm, Td, Umax,Umin, dUlim, Ts 213 | - flag ``` Fparam ``` for informing if parameters has been change and recalculation will be nedeed 214 | 215 | As additional to Cv calculation a rate limiter has beed added in updateControl() function. By setting parametes with function ```set_(value)``` we set 216 | local parameter and set ``` Fparam ``` flag, but only function ``` tune() ``` will recalculate all variables and will update p-i-d structure ```self.pid ``` 217 | 218 | file class_controller_pid_awm_example.py (with simulation in ```__main__```): 219 | ``` 220 | class pid_awm_controller(object): __init__(self,Kp,Ti,Td,Tm,Tt,Umax=100,Umin =0 ,dUlim=100,Ts = 1.0) 221 | ├── 222 | ├── updateControl(self,sp,pv,ubias=0.,mv=0.) 223 | ├── tune() - recalculate p-i-d variables 224 | ├── set_Kp(value) - set local Kp parameter. 225 | ├── set_Ti(value) - set local Ti parameter 226 | ├── set_Td(value) 227 | ├── set_Tm(value) 228 | ├── set_Tt(value) 229 | ├── set_Umax(value) 230 | ├── set_Umin(value) 231 | ├── set_dUlim(value) 232 | └── set_Ts(value) - set local Ts paramwter 233 | ``` 234 | 235 | This way we created a basic pid 'block' where we can set parameters in any time, and update controller's variable later. 236 | Note that: 237 | - in ```set_(value)``` functions, value error checking hasn't beed added. 238 | - when we analyze source code of pid_awm_updateControl() function, we notice that computation of variables are done in 239 | pid_tune(pid) function, and only parameter Kp is used directly in P-term calculation in ```pid_awm_updateControl()```. That mean a most of parameter (Ti,Td,..) apart from Kp can be changed directrly in struct without affecting Cv calculation, so class can be optimized (not need to keep coopy of (Ti,Td,..), but for consistency we keep all parameters as auxiliary values. 240 | - Depending on your preferences a decorators: ```@setter, @property ``` can be used or even setattr() in setting class atribute value. 241 | 242 | 243 | 244 | class pid_awm_controller() implementation: 245 | ```python 246 | from pid_aw import * 247 | 248 | class pid_awm_controller(object): 249 | def __init__(self,Kp,Ti,Td,Tm,Tt,Umax=100,Umin =0 ,dUlim=100,Ts = 1.0): 250 | 251 | self.pid_buf=bytearray(101) # size of PID_REGS is 101 bytes, 252 | self.pid = uctypes.struct(uctypes.addressof(self.pid_buf), PID_REGS, uctypes.LITTLE_ENDIAN) 253 | 254 | self.Kp = Kp 255 | self.Ti = Ti 256 | self.Td = Td 257 | self.Tm = Tm 258 | self.Tt = Tt 259 | self.Umax = Umax 260 | self.Umin = Umin 261 | self.dUlim = dUlim 262 | self.Ts = Ts 263 | 264 | self.tune() # recalculate controller variables and parameters. 265 | 266 | self.Fparam=False 267 | 268 | self.pid.CFG.Psel = True 269 | self.pid.CFG.Isel = True 270 | self.pid.CFG.Dsel = False 271 | 272 | def updateControl(self,sp,pv,ubias=0.,mv=0.): 273 | """ calculate control output; rate limiter implementes """ 274 | uk = pid_awm_updateControl(self.pid,sp,pv,ubias,mv) 275 | 276 | if self.pid.CFG.Rlimsel: 277 | 278 | delta=self.pis.dUlim * PID.Ts 279 | du=self.pid.u-self.pid.u1 280 | 281 | if (abs(du)>delta): 282 | if (du<0): 283 | delta *=-1 284 | 285 | du = delta 286 | self.pid.u=self.pid.u1+du 287 | uk= self.pid.u 288 | 289 | return uk 290 | 291 | def tune(self): 292 | """ update self.pid structure and recalculate controller variables and parameters.""" 293 | self.pid.Kp = self.Kp 294 | self.pid.Ti = self.Ti 295 | self.pid.Td = self.Td 296 | self.pid.Tm = self.Tm 297 | self.pid.Tt = self.Tt 298 | self.pid.Ts = self.Ts 299 | self.pid.Umax = self.Umax 300 | self.pid.Umin = self.Umin 301 | self.pid.dUlim= self.dUlim 302 | 303 | pid_tune(self.pid) 304 | self.Fparam =False 305 | 306 | def set_Kp(self,value): 307 | self.Kp = value 308 | self.Fparam =True 309 | 310 | def set_Ti(self,value): 311 | self.Ti = value 312 | self.Fparam =True 313 | 314 | def set_Td(self,value): 315 | self.Td = value 316 | self.Fparam =True 317 | 318 | def set_Tm(self,value): 319 | self.Tm = value 320 | self.Fparam =True 321 | 322 | def set_Ts(self,value): 323 | self.Ts = value 324 | self.Fparam =True 325 | 326 | def set_Tm(self,value): 327 | self.Tt = value 328 | self.Fparam =True 329 | 330 | def set_Umax(self,value): 331 | self.Umax = value 332 | self.Fparam =True 333 | 334 | def set_Umin(self,value): 335 | self.Umin = value 336 | self.Fparam =True 337 | 338 | def set_dUlim(self,value): 339 | self.dUlim = value 340 | self.Fparam =True 341 | 342 | ``` 343 | 344 | **Simulation** 345 | 346 | As simulation example we control a FOPDT (first order Process with delay time) as a very simple thermal process. Let's assume that we controll some boiler with SSR as actuator with continuous control (by PWM : 0% to 100 % ), and we measure proces value as °C. 347 | As a basic PID configuration we use: 348 | - P-I controller 349 | - Control Mode: normal (controller.pid.CFG.Rlimsel = False -> error = sp - pv) 350 | - Rate limiter off ( controller.pid.CFG.Rlimsel=False) 351 | - Control limit Umin =0.0 352 | 353 | We change setpoint during simulation from 50 °C to 30 °C and then to 60 °C. As a result of simulation we get signals presented on waveforms (during simulation results will be printed): 354 | 355 | 356 | 357 | The upper waveforms show the setpoint (sp) and process value (pv), the lower control value output (u), and the control value (uk) before limiting [Umin, Umax]  358 | Let's notice that when changing the setpoint from 50 °C  do 30 °C the process control temperature dynamics is much slower than when we increase Sp.   359 | Since the possible physical value for SSR PWM will be 0% (power off) the controller is unable to set Cv below the physical limit in the first 30 seconds the temperature will drop only with the speed of process dynamics (cooling). 360 | 361 | simlulation 'in the loop': 362 | ```python 363 | .... 364 | if __name__ == '__main__': 365 | 366 | from simple_models_esp import FOPDT_model 367 | 368 | # simulation time amd sampling 369 | Ts =.25 370 | Tstop= 50 371 | Ns = int(Tstop/Ts) 372 | 373 | #[1] process model FOPDT 374 | y0=21 # Initial value 375 | To=200 # proces time constant 376 | Lo=1 # proces delay [s] 377 | Ko=100 # process Gain [s] 378 | process_model = FOPDT_model(Kp=Ko,taup=To,delay=Lo, y0=y0,Ts=Ts) 379 | 380 | # P-I controller settings 381 | Kp0 = 0.5 382 | Ti0 = 10 383 | Td0 =1.0 384 | Tm0 = 0.25 385 | Tt0= Ts 386 | 387 | controller=pid_awm_controller(Kp=Kp0,Ti=Ti0,Td=Td0,Tm=Tm0,Tt=Tt0,Umax=100,Umin =0 ,dUlim=100,Ts =.25) 388 | 389 | controller.pid.CFG.Dsel = False 390 | 391 | sp = 50 392 | pv = 0.0 393 | uk = 0.0 394 | 395 | for i in range(Ns): 396 | # ------------------------------------ (1) ------------------------------------ 397 | # in real time wee would wait for interrupt 398 | 399 | # ------------------------------------ (2) ------------------------------------ 400 | #[a]changing Setpoint 401 | sp=50 # 402 | if k*Ts >=150: 403 | sp = 30 404 | 405 | if k*Ts >=300: 406 | sp = 60 407 | 408 | #[b] Read process value (in realtime we read from ADC and do pv processing) 409 | yk = process_model.update(uk) 410 | #vn = uniform(-0.4,0.4) # white noise 411 | pv = yk #+ vn 412 | # ------------------------------------ (3) ------------------------------------ 413 | #[c] calculate control value 414 | uk = controller.updatecontrol(sp,pv) # dafault ubias=0, mv = 0 415 | 416 | # ------------------------------------ (4) ------------------------------------ 417 | # [d] in real time we would perfomr some CV processing and set cotroller output ( PWM, On/Off GPIO in on/off control or other 418 | # GPIO operation , after that we can update controller parameters and variables. 419 | 420 | # ------------------------------------ (5) ------------------------------------ 421 | if controller.Fparam: 422 | controller.tune() 423 | 424 | print(i,"sp:",sp,"pv:",pv,"uk:",uk) 425 | ``` 426 | 427 | 428 | 429 | **Part2: timer interrupt** 430 | 431 | Before some examples solutions will be presented familiarize yourself with micropython timer interrupts in esp32 and 432 | [Writing interrupts handlers: isr_rules](https://docs.micropython.org/en/latest/reference/isr_rules.html) 433 | Main conclusions: 434 | - esp32 timers interrups are implemented as soft interrupts 435 | - in general it is best to avoid using floats in ISR code 436 | - ISR cannot pass a bound method to a function but: 437 | - section 'Creation of Python objects' in isr_rules desctribe how to deal with floating point operations during control value calculation by "(...)creating a reference to the bound method in the class constructor and to pass that reference in the ISR.". This way we can pass all computation as a Task run by scheduler. 438 | 439 | Using ISR with uasyncio there are [some restrictions](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/INTERRUPTS.md), aslo 440 | [Linking uasyncio and other contexts](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/THREADING.md#12-soft-interrupt-service-routines) 441 | should be read if in Your App You will going to use uasyncio. 442 | 443 | 444 | **Part3: Using uasyncio instead of timer interrupt ** 445 | 446 | # To-Do 447 | 448 | 449 | # 3. ON-OFF Controller 450 | 451 | This is extension for [2.3 On-Off-controller](https://github.com/2dof/esp_control/blob/main/README.md#23-on-off-controller) description and implementation of on-of controler described in main topic. 452 | Related files for this champer: 453 | - on_off_control_example.py - implementation and simulation in micropython (copy code from main src) 454 | - Example_python_FOPDT_on_off.py - implementation and simulation in Python (that same on-off controller code - but matplotlib is used in visualisation) 455 | 456 | Example in python was modified in simulation part to show how constant change in time of Setpoint value affect follow-up regulation. 457 | 458 | The simulation (simple model of some thermal process) result is presented below. during heating (control ON) the SP value change (0.16 C/sec) has been set to show how process dynamics (speed PV value change) reacts for follow-up regulation and between 660 sec and 780 sec of simulation we can notice that process dynamics (pv value) is unable to "overtake" the set point ( we are near max value of PV ( process gain Kp=120), and since we use on-off control, then max control value uk = 1 then we can't increase control signal. 459 | 460 | 461 | 462 | First conclusion is that we can use on-off controller for follow-up set point and max error should not be greater than settings of hysteresis, but in real-live application 463 | it may happen that overshooting will be bigger than expected. 464 | For example: let consider control of temperature in (small) electric pottery kiln, in that case on-off controller will on full power (ON) or zero power (Off), but since thermocouple will be measuring temperature near heating element (not on the his surface) then there will be some delay, when conntroller will turn-of power, heat form heating element (wire) will be still delivered to the system (simply temperature of electric fire will bi higher), and effect of control will be similar to waveforms 465 | 466 | 467 | 468 | Effect will be more noticeable when power source will be relatively greater in relation to the requirements of process demands. Then hysteresis adjustment is needed, or change of control strategy ( PI (PID) controller). In case of ceramic kiln (with electric heating) we will be working in range 800 - 1200 Celsius, then effect will not be significant (by design of kiln working condition and power calculation we will be in our control range), but for low temperatures (start-up, cooling) it will be more noticeable. 469 | -------------------------------------------------------------------------------- /Examples/class_controller_pid_awm_example.py: -------------------------------------------------------------------------------- 1 | # MicroPython p-i-d-isa library 2 | #The MIT License (MIT), Copyright (c) 2022-2023 L.Szydlowski 3 | # __version__ = "1.0.1" 4 | ##https://github.com/2dof/esp_control 5 | 6 | 7 | from pid_aw import * # pid alghoruthm 8 | 9 | 10 | #------------------------------------------------------------ 11 | class pid_awm_controller(object): 12 | def __init__(self,Kp,Ti,Td,Tm,Tt,Umax=100,Umin =0 ,dUlim=100,Ts = 1.0): 13 | 14 | self.pid_buf=bytearray(101) # size of PID_REGS is 101 bytes, 15 | self.pid = uctypes.struct(uctypes.addressof(self.pid_buf), PID_REGS, uctypes.LITTLE_ENDIAN) 16 | 17 | #-local params copy ( our "menu system" can store params, and later we can recalculate variables) 18 | self.Kp = Kp 19 | self.Ti = Ti 20 | self.Td = Td 21 | self.Tm = Tm 22 | self.Tt = Tt 23 | self.Umax = Umax 24 | self.Umin = Umin 25 | self.dUlim = dUlim 26 | self.Ts = Ts 27 | 28 | self.tune() # do not forget perform recalculation self.pid structure 29 | 30 | self.Fparam=False # no parameter to update 31 | # ---------- 32 | # select P-I config 33 | self.pid.CFG.Psel = True 34 | self.pid.CFG.Isel = True 35 | self.pid.CFG.Dsel = False 36 | 37 | 38 | 39 | 40 | def updatecontrol(self,sp,pv,ubias=0.,mv=0.): 41 | 42 | uk = pid_awm_updateControl(self.pid,sp,pv,ubias,mv) 43 | 44 | if self.pid.CFG.Rlimsel: 45 | 46 | delta=self.pis.dUlim * PID.Ts 47 | du=self.pid.u-self.pid.u1 48 | 49 | if (abs(du)>delta): 50 | if (du<0): 51 | delta *=-1 52 | 53 | du = delta 54 | self.pid.u=self.pid.u1+du 55 | uk= self.pid.u 56 | 57 | return uk 58 | 59 | 60 | def tune(self): 61 | 62 | self.pid.Kp = self.Kp 63 | self.pid.Ti = self.Ti 64 | self.pid.Td = self.Td 65 | self.pid.Tm = self.Tm 66 | self.pid.Tt = self.Tt 67 | self.pid.Ts = self.Ts 68 | self.pid.Umax = self.Umax 69 | self.pid.Umin = self.Umin 70 | self.pid.dUlim= self.dUlim 71 | print(self.pid.Kp,self.pid.Ti) 72 | pid_tune(self.pid) 73 | 74 | self.Fparam =False 75 | 76 | def set_Kp(self,value): 77 | self.Kp = value 78 | self.Fparam =True 79 | 80 | def set_Ti(self,value): 81 | self.Ti = value 82 | self.Fparam =True 83 | 84 | def set_Td(self,value): 85 | self.Td = value 86 | self.Fparam =True 87 | 88 | def set_Tm(self,value): 89 | self.Tm = value 90 | self.Fparam =True 91 | 92 | def set_Ts(self,value): 93 | self.Ts = value 94 | self.Fparam =True 95 | 96 | def set_Tm(self,value): 97 | self.Tt = value 98 | self.Fparam =True 99 | 100 | def set_Umax(self,value): 101 | self.Umax = value 102 | self.Fparam =True 103 | 104 | def set_Umin(self,value): 105 | self.Umin = value 106 | self.Fparam =True 107 | 108 | def set_dUlim(self,value): 109 | self.dUlim = value 110 | self.Fparam =True 111 | 112 | 113 | if __name__ == '__main__': 114 | 115 | from simple_models_esp import FOPDT_model 116 | 117 | # simulation time amd sampling 118 | Ts =.25 119 | Tstop= 500 120 | Ns = int(Tstop/Ts) 121 | 122 | #[1] process model FOPDT 123 | y0=21 # Initial value 124 | To=200 # proces time constant 125 | Lo=1 # proces delay [s] 126 | Ko=100 # process Gain [s] 127 | process_model = FOPDT_model(Kp=Ko,taup=To,delay=Lo, y0=y0,Ts=Ts) 128 | 129 | # P-I controller settings 130 | Kp0 = 0.5 131 | Ti0 = 10 132 | Td0 =1.0 133 | Tm0 = 0.25 134 | Tt0= Ts 135 | 136 | controller=pid_awm_controller(Kp=Kp0,Ti=Ti0,Td=Td0,Tm=Tm0,Tt=Tt0,Umax=100,Umin =0 ,dUlim=100,Ts =.25) 137 | 138 | controller.pid.CFG.Dsel = False 139 | 140 | sp = 50 141 | pv = 0.0 142 | uk = 0.0 143 | 144 | for i in range(Ns): 145 | 146 | #[a]changing Setpoint 147 | sp=50 # 148 | if i*Ts >=150: 149 | sp = 30 150 | 151 | if i*Ts >=300: 152 | sp = 60 153 | 154 | #[a] Read process value (in real time wee read from ADC and do pv processing) 155 | yk = process_model.update(uk) 156 | #vn = uniform(-0.4,0.4) # white noise 157 | pv = yk #+ vn 158 | 159 | uk = controller.updatecontrol(sp,pv) # dafault ubias=0, mv = 0 160 | 161 | print("sp:",sp,"pv:",pv,"uk:",uk) 162 | #print("sp:",sp,"pv:",pv,"uk:",uk,'mv:',mv) 163 | #--boot.y----------------------- 164 | # derive 165 | # SP_IN = ADC(Pin(36)) 166 | # SP_IN.atten(ADC.ATTN_11DB) #the full range voltage: 3.3V 167 | # SP_IN.width(ADC.WIDTH_10BIT) 168 | # 169 | # pot_value = SP_IN.read() 170 | # print('---') 171 | # print(pot_value) 172 | 173 | #---------------------------- 174 | 175 | 176 | 177 | #-------------------------------------- 178 | # 179 | # timer = Timer(0) 180 | # 181 | # Control=pid_controller() 182 | # 183 | # Control.timer = timer 184 | # utime.sleep_ms(500) 185 | 186 | 187 | 188 | # ##timer.init(period=Control.Ts, mode=Timer.PERIODIC, callback=Control.handleInterrupt) 189 | # 190 | 191 | 192 | 193 | # 194 | # 195 | # def handleInterrupt(timer): 196 | # global CNT, Ncalls, t0 197 | # 198 | # delta = utime.ticks_diff(utime.ticks_ms(), t0) 199 | # print(delta) 200 | # 201 | # tf = esp32.raw_temperature() 202 | # tc = (tf-32.0)/1.8 203 | # print("T = {0:4d} deg F or {1:5.1f} deg C".format(tf,tc)) 204 | # t0 = utime.ticks_ms() 205 | # 206 | # if CNT >= Ncalls: 207 | # timer.deinit() 208 | # print('timer stopped') 209 | # 210 | # CNT+=1 211 | # 212 | # print('timer one shot') 213 | # 214 | # timer = Timer(0) 215 | # t0 = utime.ticks_ms() 216 | # timer.init(period=1000, mode=Timer.PERIODIC, callback=handleInterrupt) 217 | -------------------------------------------------------------------------------- /Examples/drawnings/isa_awsel0_neg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/Examples/drawnings/isa_awsel0_neg.png -------------------------------------------------------------------------------- /Examples/drawnings/isa_awsel1_neg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/Examples/drawnings/isa_awsel1_neg.png -------------------------------------------------------------------------------- /Examples/drawnings/o.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Examples/drawnings/on_off_FOPDT_example2_1_neg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/Examples/drawnings/on_off_FOPDT_example2_1_neg.png -------------------------------------------------------------------------------- /Examples/drawnings/on_off_FOPDT_example2_neg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/Examples/drawnings/on_off_FOPDT_example2_neg.png -------------------------------------------------------------------------------- /Examples/drawnings/on_off_control_fopdt_example_neg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/Examples/drawnings/on_off_control_fopdt_example_neg.png -------------------------------------------------------------------------------- /Examples/drawnings/pid_awm_class_p1_neg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/Examples/drawnings/pid_awm_class_p1_neg.png -------------------------------------------------------------------------------- /Examples/drawnings/pid_isa_awm_1_neg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/Examples/drawnings/pid_isa_awm_1_neg.png -------------------------------------------------------------------------------- /Examples/example_isa_awm_1.py: -------------------------------------------------------------------------------- 1 | #The MIT License (MIT), Copyright (c) 2022-2023 L.Szydlowski 2 | #https://github.com/2dof/esp_control 3 | # 4 | 5 | from pid_isa import * 6 | from mv_processing import * 7 | from simple_models import FOPDT_model 8 | 9 | from random import uniform 10 | 11 | # simulation time init 12 | Tstop=20 13 | Ts = 0.1 14 | Ns=int(Tstop/Ts) 15 | 16 | #[1] process model FOPDT 17 | 18 | 19 | model = FOPDT_model(Kp=2.0,taup=3.0,delay=1.0, y0=0.0,Ts=Ts) 20 | 21 | #[2] PID Controller initialization 22 | pid_buf=bytearray(128) # size of ISA_REGS is 128 bytes, 23 | PID = uctypes.struct(uctypes.addressof(pid_buf), ISA_REGS, uctypes.LITTLE_ENDIAN) 24 | isa_init0(PID) 25 | 26 | 27 | #[3] Manual Value initialization 28 | mv_buf=bytearray(41) 29 | MVR = uctypes.struct(uctypes.addressof(mv_buf), MV_REGS, uctypes.LITTLE_ENDIAN) 30 | mv_init0(MVR) 31 | 32 | MVR.Ts = Ts 33 | MVR.MvHL = 0.95* PID.Umax # we seta limits as 95% of Umax and 5% if umin 34 | MVR.MvLL = 0.95* PID.Umin 35 | mv_tune(MVR) 36 | 37 | 38 | #4 - pid tuning 39 | # we assume that we know process params calculated from proces step responce 40 | # see https://yilinmo.github.io/EE3011/Lec9.html#org2a34bd4 41 | 42 | Ko = 2 # -> Kp 43 | To = 3. # -> taup 44 | Lo = 1. # -> delay 45 | 46 | # calculate The PID parameters from Ziegler-Nichols Rules 47 | # PID.Ts = Ts 48 | # PID.Kp = 1.2*To/(Ko*Lo) 49 | # PID.Ti = 2*Lo 50 | # PID.Td = 0.5*Lo 51 | # PID.Tm = PID.Td/10. 52 | 53 | #Tuning based on IMC (internal Model Control) for 2-dof pid (see D action calculation comment:(!)) 54 | #tauc = max(1*Ko, 8*Lo) # 'moderate' tutning 55 | tauc = max(1*To, 1*Lo) # 'agressive' tuning 56 | PID.Kp =((To+0.5*Lo)/(tauc+0.5*Lo))#/Ko 57 | PID.Ti =(To+0.5*Lo) 58 | PID.Td = To*Lo/(2*To+Lo) 59 | PID.Tm = PID.Td/10. 60 | 61 | isa_tune(PID) # P-I-I secetect 62 | 63 | PID.CFG.Awsel = False # True 64 | PID.CFG.Dsel = True # True 65 | 66 | 67 | # init simulation 68 | sp = 50. # setpoint 69 | yk = 0. # proces outpout value (measured) 70 | uk = 0. # control value 71 | dmv = 0. # change in manual value 72 | mv = 0. # manual value 73 | utr = uk 74 | 75 | for i in range(Ns): 76 | #[a] Read process value (in real time wee read from ADC and do pv processing) 77 | yk = model.update(uk) 78 | #vn = uniform(-0.4,0.4) # white noise 79 | pv = yk #+ vn 80 | 81 | # [b]update setpoint (in real solution we do some sp processing) 82 | sp=50 83 | if i >=100: 84 | sp = -50 85 | 86 | # #[c] update mv processing 87 | mv = mv_update(MVR,dmv,uk) 88 | # 89 | # #[d] update control pid, 90 | u = isa_updateControl(PID,sp , pv, utr,ubias = 0.) 91 | # 92 | # # [e]We are in AUTO mode (PID.CFG.Mansel=False) so we do not mv this time. 93 | if PID.CFG.Mansel: 94 | u = mv # we get manual value 95 | # 96 | # # [f] saturation checking 97 | uk = limit(u, PID.Umin,PID.Umax) 98 | # 99 | utr = uk # do not forget update tracking 100 | 101 | #[g] in real time do some control value processing we sent uk to DAC,and wait Ts 102 | 103 | print("sp:",sp,"pv:",pv,"uk:",uk) 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /Examples/on_off_control_example.py: -------------------------------------------------------------------------------- 1 | # MicroPython p-i-d-isa library 2 | #The MIT License (MIT), Copyright (c) 2022-2023 L.Szydlowski 3 | # __version__ = "1.0.0" 4 | ##https://github.com/2dof/esp_control 5 | 6 | # just copied from https://github.com/2dof/esp_control/blob/main/src/utils_pid_esp32.py 7 | # doc: relay with hysteresis https://github.com/2dof/esp_control/blob/main/functional_description.md 8 | class relay2h: 9 | def __init__(self, wL=-1.0,wH=1.0): 10 | self.wL=wL 11 | self.wH=wH 12 | self._y_1= -1 13 | 14 | def relay(self,x: float): 15 | 16 | if ((x>=self.wH)|((x>self.wL)&(self._y_1==1))): 17 | self._y_1=1.0 18 | return 1 19 | elif((x<=self.wL)|((x hystL : 28 | self.relay =relay2h(hystL ,hystH) 29 | else: 30 | self.relay =relay2h(-1 ,1) 31 | 32 | self.uk=0 33 | self.ek = 0.0 34 | 35 | self.Fstart = False # 36 | 37 | def start(self): # start/stop calculationg control 38 | self.Fstart = True 39 | 40 | def stop(self): 41 | self.Fstart = False 42 | self.uk=0 43 | 44 | def tune(self,hystL,hystH): 45 | 46 | if hystH> hystL: 47 | self.relay.wL = hystL 48 | self.relay.wH = hystH 49 | 50 | else: 51 | self.relay.wL=-1 52 | self.relay.wH= 1 53 | print('wrong Hysteresis params, must be: histL> histH') 54 | 55 | def reset(self): 56 | 57 | self.relay.y_1=-1 58 | self.ek = 0.0 59 | self.uk =0 60 | 61 | def updateControl(self,sp,pv): 62 | 63 | self.ek = sp - pv 64 | 65 | if self.Fstart: 66 | 67 | self.uk = self.relay.relay(self.ek) # will return -1 or 1 so 68 | self.uk =(self.uk+1)//2 # we shift to 0 to 1 69 | 70 | else: 71 | self.reset() 72 | 73 | return self.uk 74 | 75 | 76 | 77 | if __name__ == '__main__': 78 | 79 | from simple_models_esp import FOPDT_model 80 | from random import uniform 81 | 82 | # simulation time init 83 | Tstop=100 84 | Ts = 1 85 | Ns=int(Tstop/Ts) 86 | 87 | #[1] process model FOPDT 88 | process_model = FOPDT_model(Kp=100.0,taup=100.0,delay=5.0, y0=0.0,Ts=Ts) 89 | 90 | #[2] On-Off Controller initialization 91 | hystL= -1 92 | hystH = 1 # width of hysteresis: hystH - hystL 93 | controller = OnOf_controller(hystL ,hystH) 94 | controller.start() 95 | 96 | #[3] simulation 97 | sp = 50. # setpoint 98 | yk = 0. # proces outpout value 99 | uk = 0. # control value out 100 | 101 | for i in range(Ns): 102 | #[a] Read process value (in real time wee read from ADC and do pv processing) 103 | yk = process_model.update(uk) 104 | 105 | #vn = uniform(-0.5,0.5) # white noise , we model some measurment noise 106 | pv = yk #+ vn 107 | 108 | uk = controller.updateControl(sp, pv) 109 | 110 | print("sp:",sp,"pv:",pv,"uk:",uk) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Copyright 2023 Lukas Szydlowski 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # ESP32 MIKROPYTHON CONTROL LIB 4 | 5 | P-I-D control library for esp32 Micropython 6 | 7 |

8 |
figure A0.

9 | 10 | 11 | ## Contents 12 | 1. [Overview](#overview) 13 | 2. [Control Processing functions](#2-control-processing-functions) 14 | 2.1 [PID ISA](#21-pid-isa) 15 | 2.2 [PID with anti-windup](#22-pid-with-anti-windup) 16 | 2.3 [On-OFF controller](#23-on-off-controller) 17 | 3. [Setpoint (SP) processing](#3-setpoint-processing) 18 | 4. [Process value(PV) processing](#4-process-value-processing) 19 | 5. [Manual value (MV) processing](#5-manual-value-processing) 20 | 6. [Control value (CV) processing](#6-control-value-processing ) 21 | 7. [Setpoint curve ceneration](#7-setpoint-curve-generation ) 22 | 8. [Signal processing](#8-signal-processing ) 23 | 8.1 [Thermocouples](#81-thermocouples) 24 | 9. [Benchmark](#9-benchmark ) 25 | 10. [Examples](#10-examples ) 26 | 11. [process models](#11-process-models ) 27 | 12. [Hardware implementation- notes](#12-hardware-implementation-notes) 28 | 14. [Project summary](#13-project-summary) 29 | 30 | 31 | 32 | ## Overview 33 | 34 | library provide functionalities: 35 | 36 | **P-I-D algorithms:** 37 | PID-ISA: 38 | - P, P-I ,P-D, P-I-D selection 39 | - control signal limit and antiwind-up on/off selector 40 | - control output rate limit selector 41 | - control error dead band on/off selector 42 | - Setpoing Weighting for P-action and D-Action 43 | - Direct /Indirect control selector 44 | 45 | 46 | PID with build-in anti-windup: 47 | - P, P-I ,P-D, P-I-D selection 48 | - build-in Man/Auto selector 49 | 50 | **Setpoint (SP processing) signal processing** 51 | 52 | - external/internal setpoint input selection 53 | - rate limit on/off selection 54 | - signal limit function 55 | - normalization function 56 | - setpoint signal generation 57 | 58 | **Process Value (PV processing) signal processing** 59 | - signal linear normalization 60 | - signal noise filtration 61 | - SQRT normalization on/off selection 62 | 63 | **Signal processing functions** 64 | - relay functions: simple relay, relay with hysteresis, three-Step relay with Hysteresis 65 | - value limit, rate limit, 66 | - deadband function 67 | - noise filter function 68 | - linear normalization, sqrt normalization 69 | 70 | **Curve generation** 71 | - Signal Curve generation base on time-stamps points. 72 | 73 | 74 | ###### [Contents](./README.md#contents) 75 | 76 | *py Files: 77 | ```python 78 | ├── [src] 79 | │ ├── pid_isa.py see p. 2.1 PID-ISA 80 | │ ├── pid_aw.py see p. 2.2 PID with anti-windup 81 | │ ├── on_off_control.py see p. 2.3 On-Off controller 82 | │ ├── sp_processing.py see p.3 Setpoint Processing 83 | │ ├── pv_processing.py see p.4 Process Value Processing 84 | │ ├── mv_processing.py see: p.5 Manual value processing 85 | │ ├── curve_generator.py see: p.7 Sztpoint curve generation 86 | │ ├── utils_pid_esp32.py see: (functional_description.md) 87 | │ | 88 | | └── [thermocouples] 89 | | ├──model_K.py # model based on based on ITS-90 from IEC 60584-1/2013 90 | | ├──its90_K.py # caluclation Temperature (Celsius) based on ITS-90 from IEC 60584-1/2013 91 | | ├──its90_K_lookup.py # lookup table on array 92 | | ├──its90_K_blookup.py # lookup table on bytes array 93 | | ├──its90_J.py # caluclation Temperature (Celsius) based on ITS-90 from IEC 60584-1/2013 94 | | ├──its90_J_lookup.py # lookup table on array 95 | | ├──its90_J_blookup.py # lookup table on bytes array 96 | | ├──lookup_search.py # lookup search functions 97 | | | 98 | | ├──test_its90_K_thermo.py 99 | | 100 | ├── [process_model] 101 | │ ├── simple_models_esp.py # 102 | │ └── RingBuffer.py # 103 | │ 104 | ├── [Examples] 105 | │ ├── example_isa_awm_1.py # 106 | │ ├── class_controller_pid_awm_example.py # 107 | │ 108 | └── ... 109 | 110 | ``` 111 | 112 | # 2. Control Processing functions 113 | 114 | ALL PID algorithms are implemented as uctypes.struct() for parameters storage and dedicated functions for processing. 115 | 116 | ## 2.1 PID-ISA 117 | 118 |

119 |
figure A1.

120 | 121 | discrete implementation of Two-Degree-of-Freedom PID Controller (standard form) described by: 122 | 123 | $$ u=K_{p}[(br-y)+\frac{1}{T_{i}s}(r-y)+\frac{T_{d}s}{T_{m}s+1}(cr-y))]+u_{bias}$$ 124 | ```math 125 | \small r: \text{setpoint; }\\ 126 | \small y: \text{proces value; }\\ 127 | \small b,c: \text{weighting parametes} 128 | ``` 129 | called by function: 130 | ```python 131 | def isa_updateControl(pid,sp,pv,utr = 0.,ubias = 0.): # pid- pid-isa structure, sp -setpoint, pv -proces value, utr -tracking input, ubias -bias input; 132 | ``` 133 | which return control value. 134 | 135 | 136 | **Setting up P-I-D controller** 137 | 138 | *pid* object is created as uctypes.struct() based on layout defined in *ISA_REGS* dictionary. 139 | *ISA_REGS* define all parametar and Configuration Register (defined by ISA_FIELDS dict (bit fields)): 140 | 141 | ```python 142 | form pid_isa import * 143 | 144 | pid_buf=bytearray(128) # memory allocation 145 | PID = uctypes.struct(uctypes.addressof(pid_buf), ISA_REGS, uctypes.LITTLE_ENDIAN) # 146 | 147 | isa_init0(PID) # custom method for setting pid parameters 148 | isa_tune(PID) # recalculate parameters 149 | ``` 150 | All PID tunable parameters need to be initialized and Configuration setting selected by custom function ```isa_init0(PID) ```(or by direct acces) and recalculated by ```isa_tune(PID) ``` function. 151 | 152 | ```isa_init0() ``` is a custom function for setting up parameters,but parameters are accessible directly from PID struct. 153 | 154 | ```python 155 | PID.Kp = 2 156 | PID.Ti = 1 157 | ... 158 | PID.Ts =0.1 # [sec] 159 | ... 160 | # P-i-D action selection 161 | PID.CFG.Psel = True 162 | PID.CFG.Isel = True 163 | PID.CFG.Dsel = True # or set by direct bute value writing. 164 | # PID.CFG_REG =0x07 # == Psel,Isel,Dsel = True 165 | 166 | isa_tune(PID) # recalculate parameters 167 | ``` 168 | Configuration setting is selected by setting CFG register by setting bits ( ```PID.CFG.Psel = True ```) or by direct byte value writing ( ```pid.CFG_REG =0x07 ```). 169 | 170 | :exclamation: → ALLWAYS CALL ```isa_tune() ``` function after changing parameters. 171 | 172 | When setting is finished then just call ```python isa_updateControl(pid,sp,pv,utr,ubias) ``` in timer callback or in the loop every Ts interval. 173 | 174 | Sometimes a reset of PID controller is needed, then call ```isa_reset(PID)``` to reset the values of Pk, Ik, Dk , u, u1, ed1 ( = 0.0 ). 175 | 176 | :exclamation: 177 | Go to [Examples](https://github.com/2dof/esp_control/blob/main/Examples/Examples.md) to learn more. 178 | 179 | **PID struct field description** 180 | 181 | P-I-D structure defined is by ISA_REGS dictionary (see in file pid_isa.py), all parameters are defined as FLOAT32 type values. 182 | 183 | Structrue description: 184 | ```python 185 | PID. 186 | Kp - proportional gain 187 | Ti - integrator time 188 | Td - derivative time 189 | Tm - derivative filter time 190 | Tt - antiwindup time 191 | b - setpoint weight on proportional term 192 | c - setpoint weight on derivative term 193 | Umax - max limit of control 194 | Umin - low limit of control 195 | dUlim - control rate limit 196 | Ts - sampling time 197 | Deadb - error deadband value 198 | Pk - calculated P-action value 199 | Ik - calculated I-action value 200 | Dk - calculated D-action value 201 | Ki - calculated integrator gain 202 | Kd - calculated derivative gain 203 | ai - calculated parameter for I-action 204 | bi - calculated parameter for I-action 205 | ad - calculated parameter for D-action 206 | bd - calculated parameter for D-action 207 | ek - ek=(sp-pv) control error 208 | ed - ek=(c*sp-pv) control error for D-action 209 | ep - ek=(b*sp-pv) control error for P-action 210 | du - control rate value du/dt (calculated) 211 | u - control value (calculated) 212 | ed1 - store ed(k-1) (calculated) 213 | u1 - store u(k-1) (caluclated) 214 | CFG_REG - Congfiguration register ( byte access) 215 | CFG - configurration register ( bit filelds access) 216 | ``` 217 | 218 | bit field names: 219 | ```python 220 | CFG. 221 | Psel - bit:0 - P-action selection 222 | Isel - bit:1 - I-action selection 223 | Dsel - bit:2 - D-action selection 224 | Awsel - bit:3 - Antiwindup selection 225 | Mansel - bit:4 - Manual selection 226 | Modesel - bit:5 - Mode selection(0-direct, 1-indirect) 227 | Deadsel - bit:6 - Dead band selection 228 | Rlimsel - bit:7 - Rate limit selection 229 | ``` 230 | 231 | 232 | ###### [Contents](./README.md#contents) 233 | 234 | 235 | ## 2.2 PID with anti-windup 236 | 237 | PID controllers with 'build in' Anti-windup. 238 | 239 | 240 | 241 | 248 | 257 |
242 |

figure A.3
243 | 244 | ```python 245 | def pid_aw_updateControl(pid,sp,pv,ubias = 0.) 246 | ``` 247 |

249 |

figure A.4
250 | 251 | ```python 252 | def pid_awm_updateControl(pid,sp,pv,ubias = 0.,mv = 0.) 253 | 254 | # pid: 'PID_REGS' structure,sp: setpoint, pv: process value, mv: manual value 255 | ``` 256 |

258 | 259 | Discrete implementations of PID Controller described by general equation: 260 | 261 | $$ u = K_{p}[e_{k}+\frac{1}{T_{i}s}e_{k}+\frac{T_{d}s}{T_{m}s+1}e_{k})]+u_{bias}$$ 262 | 263 | $$ e_{k} = r - y $$ 264 | 265 | ```math 266 | \small r: \text{setpoint SP; }\\ 267 | \small y: \text{proces value PV; }\\ 268 | \small u_{bias}: \text{ bias input} 269 | ``` 270 | with build-in Anti-windup (with back-calculation) scheme. 271 | functions: ```pid_aw_updateControl()``` and ```pid_awm_updateControl()``` implement that same algorithm, but second one incorporate switching between Auto/Manual mode into control value calculation (i.e : I-action will track change in mv value in Manual control) 272 | 273 | **Difference from P-I-D ISA** 274 | - build in anti-windup. 275 | - backward difference approximation was used for I and D action ( PID-ISA: I-action: backward, D-action: Trapez approximation). 276 | - no dead-band, no rate limit, no du/dt calculation, no SP and D-action weighting. 277 | - structure of parameters is different (can't be used interchangeably). 278 | 279 | [Example of class implementation](https://github.com/2dof/esp_control/tree/main/Examples#2-class-controller-example) 280 | 281 | **Setting up P-I-D controller** 282 | 283 | *pid* object is created as uctypes.struct() based on layout defined in *PID_REGS* dictionary. 284 | *PID_REGS* define all parametar and Configuration Register (defined by PID_FIELDS dict (bit fields)): 285 | 286 | 287 | ```python 288 | form pid_aw import * 289 | 290 | # create and init pid structure 291 | pid_buf=bytearray(101) # size of PID_REGS is 101 bytes, 292 | PID1 = uctypes.struct(uctypes.addressof(pid_buf), PID_REGS, uctypes.LITTLE_ENDIAN) 293 | pid_init0(PID1) 294 | 295 | # change some parameters 296 | PID1.Kp = 2 297 | PID1.Tt = PID1.Ts # 0.1 298 | pid_tune(PID1) 299 | 300 | # and test some settings 301 | PID1.CFG.Modesel = False # True 302 | 303 | # get step responce 304 | sp = 10. # setpoint 305 | pv = 0. # proces value 306 | mw = 0 # manual value 307 | 308 | for i in range(0,25): 309 | u = pid_aw_updateControl(PID1,sp,pv,ubias = 0.) 310 | #u = pid_awm_updateControl(PID1,sp,pv,ubias=0.,mv ) 311 | 312 | print("u:",u,"uk:",PID1.uk) 313 | ``` 314 | 315 | 316 | Structrue description: 317 | ```python 318 | PID. 319 | Kp - proportional gain 320 | Ti - integrator time 321 | Td - derivative time 322 | Tm - derivative filter time 323 | Tt - antiwindup time 324 | Umax - max limit of control 325 | Umin - low limit of control 326 | dUlim - control rate limit 327 | Ts - sampling time 328 | Pk - calculated P-action value 329 | Ik - calculated I-action value 330 | Dk - calculated D-action value 331 | Ki - calculated integrator gain 332 | Kd - calculated derivative gain 333 | ai - calculated parameter for I-action 334 | bi - calculated parameter for I-action 335 | ad - calculated parameter for D-action 336 | bd - calculated parameter for D-action 337 | ek - ek=(sp-pv) control error 338 | u - control value (calculated) output 339 | uk - control value uk = P+I+D (calculated) 340 | ek1 - store ek(k-1) (caluclated) 341 | u1 - store u(k-1) (caluclated) 342 | CFG_REG - Congfiguration register ( byte access) 343 | CFG - configurration register ( bit filelds access) 344 | ``` 345 | 346 | bit field names: 347 | ```python 348 | CFG. 349 | Psel - bit:0 - P-action selection 350 | Isel - bit:1 - I-action selection 351 | Dsel - bit:2 - D-action selection 352 | Mansel - bit:3 - Manual selection 353 | Modesel - bit:4 - Mode selection(0-direct, 1-indirect) 354 | Rlimsel - bit:5 - Rate limit selection 355 | F6 - bit:6 - for user use 356 | F7 - bit:7 - for user use 357 | ``` 358 | 359 | 360 | 361 | PID processing functions: 362 | ```python 363 | pid_aw.py 364 | ├── PID_REGS = {...} - dictionary description of pid structure 365 | ├── def pid_aw_updateControl(pid,sp,pv ,ubias=0.) - p-i-d controller with Anti-windup (back-calculation) 366 | ├── def pid_awm_updateControl(pid,sp,pv ,ubias=0, mv=0.) - p-i-d controller with Anti-windup (back-calculation) and Auto/Man bumpless switching 367 | ├── def pid_tune(pid) - tune pid parameters 368 | └── def pid_init0(pid) - init pid parameters (function can be edited by user) 369 | ``` 370 | 371 | 372 | ## 2.3 On-Off-controller 373 | 374 | A simple implementation of the on-off controller based on a relay with hysteresis. 375 | 376 | in file ```on_off_control.py``` in section ``` '__main__': ``` a simple simmulation in the loop has beed added, delete if You will use our real applications (to safe memory space). 377 | 378 | A functionality 'start/stop' is used to turn on and off controller without stoppind timer interrupt (or asynchio task) or reading measurments. 379 | 380 | 381 | File/class description: 382 | ```python 383 | on_off_control.py 384 | ├── class OnOff_controller(object) __init__(hystL =-1 , hystH =1): 385 | | ├── 386 | | | ├──.relay() - (class of relay2h()) relay with hysteresis 387 | | | ├──.uk - control output (0/1) 388 | | | ├──.ek - control error (sp-pv) 389 | | | ├──.Fstart - start/stop flag (True/False), if True then allow relay switching otherwise stop: uk= 0 390 | | | 391 | | ├── .start() - command start (set Fstart ) , allow to compute control signal 392 | | ├── .stop() - command stop (reset Fstart) , set control value uk = 0, 393 | | ├── .tune(hystL,hystH) - change width of hysteresis ( lower, upper limit) 394 | | ├── .reset() - reset relay to init position (off) 395 | | ├── .updateControl(sp,pv) - update control value , return uk 396 | | 397 | └── class relay2h() - copy from utils_pid_esp32.py 398 | ``` 399 | 400 | **simulation** 401 | 402 | Example simulation of thermal process based on simple FOPDT model ('in the loop simulation') with Hysteresis width 2 °C ( ±1 °C around sp). 403 | Simulation code is in [on_off_control.py](https://github.com/2dof/esp_control/blob/main/src/on_off_control.py) in ``` if __name__ == '__main__' ``` section. 404 | ( delete simulation section and imorted models if You will use controller in Your control application), or in 405 | [Examples: -> Example 3: ON-OFF controller: simulation](https://github.com/2dof/esp_control/blob/main/Examples/Examples.md, were You find find also Python simulation (Example_python_FOPDT_on_off.py) 406 | 407 | 408 | 409 | 410 |

411 |
figure A 5.1

412 | 413 | 414 | Learn more: [on-off-control-system](https://x-engineer.org/on-off-control-system/) 415 | 416 | 417 | 418 | # 3. Setpoint processing 419 | 420 | Setpoint Value processing called by function: 421 | ```python 422 | def sp_update(spr,spi,spe = 0.0) # spr- setpoint structure , spi - internal setpoint,spi - extermal setpoint 423 | ``` 424 | perform basic setpoint signal processing: linear normalization (for external setpoint), min/max and rate value limitation according 425 | to selected configuration. 426 | Internal setpoint is value set by user/signal generation, external setpoint is selected for example in cascade control configuration. 427 | 428 | **Setting-up SP processing** 429 | 430 | *SPR* object is created as uctypes.struct() (size of 64 bytes) based on layout defined in *SP_REGS* dictionary. 431 | *SP_REGS# define all parametar and Configuration Register (defined by SP_FIELDS dict (bit fields)). 432 | 433 | ```python 434 | sp_buf=bytearray(64) # memory allocation 435 | SPR = uctypes.struct(uctypes.addressof(sp_buf), SP_REGS, uctypes.LITTLE_ENDIAN) 436 | sp_init0(SPR) 437 | 438 | # tuning by direct acces 439 | SPR.SpeaL = -100. 440 | sp_tune(SPR) 441 | ``` 442 | All SPR tunable parameters need to be initialized and Configuration setting selected by custom function ```sp_init0(PID) ```(or by direct acces) and recalculated by ```sp_tune(PID) ``` function. 443 | ```sp_init0(SPR) ``` is a custom function (edited by user) for setting up parameters, also parameters are accessible directly from structure. 444 | 445 | When reset of SP structure is required, then ```sp_reset(pid)``` function should be used. 446 | 447 | :exclamation: → ALWAYS CALL ```sp_tune() ``` function after changing tunable parameters 448 | 449 | SP structure fields description: 450 | ```python 451 | SPR. 452 | SpLL - SP low limit 453 | SpHL - SP High limit 454 | SpeaL - external SP norm aL point (x) 455 | SpeaH - external SP norm aH point (x) 456 | SpebL - external SP norm bL point (y) 457 | SpebH - external SP norm bH point (y) 458 | Rlim - rate limit value in unit/sec 459 | Ts - sampling time 460 | sp - sp value 461 | sclin - calucated scaling factor for linear normalizacion 462 | sp1 - previous sp value: sp(k-1) 463 | dx - calculated dx/dt value 464 | CFG_REG - Congfiguration register ( byte access) 465 | CFG - configurration register ( bit filelds access) 466 | ``` 467 | bit field names: 468 | ```python 469 | CFG. 470 | SPesel - external setpoint selection (SPesel =True) 471 | Rlimsel - SP rate limit selection (Rlimsel =True) 472 | SPgen - Setpoint Curve generation (SPgen = True) 473 | f3 - for user definition 474 | f4 - ... 475 | f5 - ... 476 | F6 - ... 477 | F7 - for user definition 478 | ``` 479 | 480 | 481 | ###### [Contents](./README.md#contents) 482 | 483 | # 4. Process value processing 484 | 485 | Process Value processing called by function: 486 | ```python 487 | def pv_update(pvr,pve,pvi = 0.0): # pvr- pv structure , pve - external setpoint, pvi - internal setpoint value 488 | ``` 489 | perform basic process value signal processing: linear normalization, noise filter and sqrt normalization depending on the selected option. 490 | 491 | two imput signals: external pv value (pve) is a physical sensor value measuring with ADC; internal process value (pvi) is selected for example in cascade control configuration. 492 | 493 | **Setting PV processing** 494 | 495 | *PVR* object is created as uctypes.struct() (size of 74 bytes) based on layout defined in *PV_REGS* dictionary. 496 | *PV_REGS* define all parametar and Configuration Register (defined by PV_FIELDS dict (bit fields)): 497 | 498 | ```python 499 | pv_buf=bytearray(72) 500 | PVR = uctypes.struct(uctypes.addressof(pv_buf), PV_REGS, uctypes.LITTLE_ENDIAN) 501 | pv_init0(PVR) 502 | 503 | # tuning by direct acces 504 | PVR.PvaL = -100 505 | PVR.PvaH = 100. 506 | PVR.PvbL = 0.0 507 | PVR.PvbH = 100. 508 | pv_tune(PVR) 509 | ``` 510 | 511 | All process value tunable parameters need to be initialized, and Configuration setting selected by custom function ```pv_init0(PID) ```(or by direct access) and recalculated by ```pv_tune(PID) ``` function. 512 | ```pv_init0(SPR) ``` is a custom function (edited by user) for setting up parameters, also parameters are accessible directly from structure. 513 | 514 | When reset of SP structure is required, then ```pv_reset(pid)``` function should be used. 515 | 516 | :exclamation: → ALWAYS CALL ```pv_tune() ``` function after changing tunable parameters. 517 | 518 | ```python 519 | PVR. 520 | PvLL - Pv low limit 521 | PvHL - Pv High imit 522 | PvaL - Pv linear norm aL point (x) 523 | PvaH - Pv linear norm aH point (x) 524 | PvbL - Pv linear norm bL point (y) 525 | PvbH - Pv linear norm bH point (y) 526 | SqrtbL - Pv sqrt norm bL point (y) 527 | SqrtbH - Pv sqrt norm bL point (y) 528 | Ts - Sampling time 529 | Tf - noise filter time constans 530 | pv - process value 531 | yf - filter value out 532 | sclin - calucated scaling factor for linear normalizacion 533 | scsqrt - calucated scaling factor for sqrtnormalizacion 534 | CFG_REG - Congfiguration register ( byte access) 535 | CFG - configurration register ( bit filelds access) 536 | ``` 537 | bit field names: 538 | ```python 539 | CFG. 540 | Pvisel - internal PV selection 541 | Sqrtsel - SQRT normalization selection 542 | Fltsel - noise filter selection 543 | f3 - ... 544 | f4 - ... 545 | f5 - ... 546 | F6 - ... 547 | F7 - for user definition 548 | ``` 549 | 550 | ###### [Contents](./README.md#contents) 551 | 552 | 553 | # 5. Manual value processing 554 | 555 | Manual Value (MV) processing called by function: 556 | ```python 557 | def mv_update(mvr,dmv,tr =0.0) # mvr- mv structure , dmv - "+/-" change in manual value input ,tr - tracking input 558 | ``` 559 | which perform basic manual value signal processig: incremental change from input dmv of manual value with tracking input (from control signal), limit 560 | min/max value. 561 | 562 | 563 | **Setting-up MV processing** 564 | 565 | *MVR* object is created as uctypes.struct() (size of 41 bytes) based on layout defined in *MV_REGS* dictionary. 566 | *MV_REGS* define all parametar. 567 | 568 | ```python 569 | from mv_processing import * 570 | 571 | # setting up 572 | mv_buf=bytearray(41) 573 | MVR = uctypes.struct(uctypes.addressof(mv_buf), MV_REGS, uctypes.LITTLE_ENDIAN) 574 | 575 | mv_init0(MVR) # init 576 | 577 | MVR.MvLL = 0.0 # lets set new saturation parameter MvLL 578 | mv_tune(MVR) # Always tune parameter after changing 579 | dx=0.0 # incremental input 580 | 581 | for i in range(0,40): # do some testing 582 | 583 | ytr = 2*sin(6.28*0.05*i) 584 | 585 | y = mv_update(MVR,dx,ytr) 586 | print("ytr:",ytr,"mv:",y) 587 | 588 | if i == 22: # check how increasing Tt affect mv output to track 589 | 590 | MVR.Tt =0.5 591 | mv_tune(MVR) 592 | ``` 593 | 594 | All tunable parameters need to be initialized, by custom function ```MV_init0(MVR) ```(or by direct access) and recalculated by ```MV_tune(MVR) ``` function. 595 | ```MV_init0(MVR) ``` is a custom function (edited by user) for setting-up parameters, also parameters are accessible directly from structure. 596 | When reset of MV structure is required, then use ```mv_reset(MVR)``` . 597 | 598 | :exclamation: → ALWAYS CALL ```MV_tune() ``` function after changing tunable parameters. 599 | 600 | From the code above we will get (waveforms ploted in thonny): 601 |

602 |
figure C1.

603 | 604 | Because we change ```MVR.MvLL = 0.0 ``` then manual value will be cut-off at bottom, also by changing value of Tf we affect the delay/lag of MV value. 605 | 606 | **Changing Tt or Tm** 607 | 608 | Tracking dynamic - increasing value of Tt (in relation to sampling time (Ts))introduce more lag effect (see figure C1.), for fast response keep Tf<=0.1 Ts, 609 | Incremental change of input dmv - both Tm and Tr acts as scaling factor ( ~ Tt/Tm) for input dmv affecting output value. 610 | 611 | **Changing value "+/-" in dmv input ** 612 | Because dmv input is incremental input then the bigger dmv value then faster output will be changing, constant value of input dmv will accect constant increasing 613 | of output (interating). 614 | 615 | MV structure parameters: 616 | ```python 617 | MVR. 618 | MvLL - Manual value Low Limit ( set as 0.95-1.0 of control Umax) 619 | MvHL - Manual value High limit ( set as 0. to 0.05 of control Umin) 620 | Tt - time constant for tracking input ( set to <=0.1 of Ts to fast responce) 621 | Tm - time constant for increment change of manual input 622 | Ts - sampling time 623 | mvi - Manual value before saturation checkong 624 | mvo - Manual value oputput 625 | at - calculated, tracking block parameter 626 | bt - calculated, tracking block parameter 627 | ct - calculated, tracking block parameter 628 | ``` 629 | 630 | MV processing functions: 631 | ```python 632 | mv_processing.py 633 | ├── MV_REGS = {...} - dictionary description of mv structure 634 | ├── def mv_update(mvr,dmv,tr =0.0) - return manual value according to change in 'dmv' or 'tr' inputs, update internal states 635 | ├── def mv_tune(mvr) - recalulate internal parameters of mvr when tunable parameters are change. 636 | ├── def mv_reset(mvr) - reset internal state , manual value mv = 0 637 | └── def mv_init0(mvr) - edited by user, initialize 'mvr' structure. 638 | ``` 639 | 640 | ###### [Contents](./README.md#contents) 641 | 642 | # 6. Control value processing 643 | 644 | Basic control signal processing are based on methods provides by [Signal processing](#8-signal-processing). 645 | Depending on desired solution it is possible to implement, for example: 646 | - standard PWM generation out 647 | - 2 or 3 Step Controller ( with or without Position Feedback Signal) 648 | 649 | 650 | 651 | ###### [Contents](./README.md#contents) 652 | 653 | # 7. Setpoint Curve Generation 654 | 655 | 656 | 657 | 659 | 661 |
. 658 |

figure B1.

660 |

figure B2

662 | 663 | Generation of curve is based on defining a list of points coordinates consisting of time slices and out values (on end of time slice) (figure B1.) 664 | 665 | sp_ramp =[p0, p1, p2, ....pN] , whrere 666 | p0 = [t0,val0] - t0 = 0 - always 0 as start point, start from val0 667 | p1 = [t1,Val1] - in slice of time t1, change value from val0 to val1 668 | p2 = [t2,Val2] - in slice of time t2, change value from val2 to val2 669 | ... 670 | pN = [tN,ValN] 671 | 672 | 673 | Then, supplying our curve profile to ```class Ramp_generator() ``` and defining time unit (Time slices (intervals) can be only in seconds ('s') or minutes ['m'] ) 674 | we create curve generator based on line interpolation between every 2 points. 675 | 676 | Example: 677 | We want to generate setpoint curve profile: start from actual Setpoint value SP_val and generate values every dt = Ts = 1 sec 678 | - first define starting point p0=[0,0.0], we assume we dont know what actual SP_val is, so we assume value 0.0 (curve profile can be loaded from memory or a file.) 679 | - in 4 min go to 20 (p1=[4,20]), 680 | - hold value 20 for 4 min (p2=[4,20.]), next 681 | - raise value to 50 in 2 min (p3=[2,50.]), next 682 | - hold value 50 for 4 min (p4=[4,50.]), next 683 | - drop value to 25 in 4 min ([4,25.]), next 684 | - hold value 25 for 4 min (p2=[4,25.]) 685 | 686 | As in the example below we define our Ramp profile, and create Setpoint generator ```SP_generator```. 687 | When we are ready, we start generator ```SP_generator.start(SP_val) ``` from actual Setpoint value. It causes to write SP_val to the starting point 688 | (p0 = [0, SP_val]), and set ```Fgen = True ```. From this moment, we allow to generate values by calling ```SP_generator.get_value(Ts) ``` which return 689 | next values every dt =Ts period (see figure B2.) 690 | When generation is finished (i.e last point of curve was generated, then flag is reset ( ```SP_generator.Fgen = False ``` ) and ```.get_value(Ts)``` will return last generated value in next call. 691 | 692 | ```python 693 | from curve_generator import * 694 | 695 | # p0 p1 p2 p3 p4 p5 p6 696 | Ramp = [[0,0],[4,20],[4,20],[2,50],[4,50],[4,25],[4,25]] 697 | 698 | Ts = 1.0 # sampling time 699 | 700 | SP_generator =Ramp_generator(Ramp, unit='m') # create generator 701 | 702 | SP_val = 10.0 # at the moment of start generation SP_val =10.0 703 | 704 | SP_generator.start(SP_val) # we start (allow) to generate values 705 | 706 | for i in range(0,1400): # simulate a "control loop" 707 | 708 | y = SP_generator.get_value(Ts) 709 | print(y) 710 | 711 | if SP_generator.Fgen == False: # just brake the loop when done 712 | break 713 | # utime.sleep(Ts) 714 | 715 | ``` 716 | 717 | ```python class Ramp_generator() ``` allow to stop (halt); resume generation; add point(on the end of profile) or load new ramp. 718 | Also in any time we can get elapsed or remaining time during generation (see description below). 719 | 720 | ```python 721 | curve_generator.py 722 | │──class Ramp_generator(object) - Ramp generator __init__(Ramp,unit='m') -> Ramp: list of points, unit: 'm'-> minutes, 's'->seconds 723 | │ ├── .start(val0) - command start allowing to generate values every call of .get_value(dt), dt: time interval 724 | │ ├── .stop() - stop generating, even if .get_value(dt) is called, only last value is returned before .stop() 725 | │ ├── .resume() - resume generating after stop 726 | │ ├── .get_value(dt) - generate next value in dt interval. 727 | │ ├── .add_point([tn,valn]) - add new point to the end of ramp. 728 | │ ├── .load_ramp(Ramp,unit) - load new ramp to generator (only when generator not active (Fgen=False) . 729 | │ ├── .elapsed_time() - return time from start of generation in (hh,mm,ss) format 730 | │ └── .remaining_time() - return remaining tome to end of generation in (hh,mm,ss) format 731 | │ 732 | └── def sec_to_hhmmss(sec) - calculate (hh,mm,ss) time format from given seconds (sec) 733 | 734 | ``` 735 | 736 | 737 | 738 | 739 | ###### [Contents](./README.md#contents) 740 | 741 | # 8. Signal processing 742 | 743 | Basic signal processing functions are described in [functional_description](functional_description.md) 744 | 745 | ## 8.1 Thermocouples 746 | 747 | Thermocouples signal processing are descibed in [src/thermocouples](https://github.com/2dof/esp_control/tree/main/src/thermocouples) 748 | 749 | 750 | ###### [Contents](./README.md#contents) 751 | 752 | # 9. Benchmark 753 | 754 | Condition for time measurement: 755 | - for pid, sp, pv, mv, etc. processing all selectable configuration were selected (i.e pid-isa: Psel, Isel, Dsel, Awsel, Modesel, Deadsel, Rlimsel =True ) 756 | - results are rounded-up with 0.05 ms accuracy. 757 | 758 | ``` 759 | MicroPython v1.19.1 on 2022-06-18. 760 | | -------------------------- freq: 160M Hz 761 | │── isa_updateControl() - 0.7 ms 762 | ├── pid_aw_updateControl() - 0.5 ms 763 | ├── pid_awm_updateControl() - 0.5 ms 764 | ├── sp_update() - 0.3 ms 765 | ├── pv_update() - 0.35 ms 766 | ├── mv_update() - 0.25 ms 767 | ├── Ramp_generator.get_value() - 0.3 ms 768 | └── 769 | ``` 770 | 771 | An @timed_function() was used to time measure (see [Identifying the slowest section of code](https://docs.micropython.org/en/latest/reference/speed_python.html)) 772 | 773 | 774 | 775 | ###### [Contents](./README.md#contents) 776 | 777 | # 10. Examples 778 | 779 | [Go to Examples ](./Examples/README.md) 780 | 781 | 782 | 783 | ###### [Contents](./README.md#contents) 784 | 785 | # 11. Process models 786 | 787 | [Go to Pocess models ](./process_models/readme.md) 788 | 789 | ###### [Contents](./README.md#contents) 790 | 791 | 792 | # 12. Hardware implementation-notes 793 | 794 | Here You will find some notes and comments how to avoid problems with hardware implementation: 795 | 796 | I You want implement a controller as a (fixed) driver (i.e for DC motor) choose simplest implementation (fixed PI/PID architecture, SP/PV processing nessesary for ADC scaling/normalization. If You want build some universal platform like industrial temperature controllers (Keyboard, LCD) then (in my opinion) design interface and user interaction will be more importnant part of project. 797 | 798 | Notes: 799 | - Always choose safety as main goal during design i.e. Alarm Indication ( screen/dione blinking, buzzer), sensor fault detection, emergency shut down etc. 800 | - Proper Startu-up/shut-down of application (power-on/off controller) is half off succes (load configuration asnd settings/ save setting) 801 | - list of functionality of Your automation project, and then just select best platform/MCU and perypherials/ 802 | 803 | *HARDWARE REMARKS* 804 | 805 | -some platforms during/after program reset/boot can have Hi level on GPIO (for example in esp32 some pins go [high on boot](https://espeasy.readthedocs.io/en/latest/Reference/GPIO.html)) 806 | 807 | - The ADC of the ESP32 issues 808 | - The V/ADC relation is not linear ( [see tests](https://github.com/bboser/IoT49/blob/master/doc/analog_io.md). 809 | - ADC2 cannot be used with enabled WiFi also on most IoT platforms ADC reading directly from AI and using WI-FI/BT simultaneously in most cases will add lot of high level noise in measurment 810 | - noise and value fluctiation. 811 | - The ADC can only measure a voltage between 0 and 3.3V 812 | - Other ADC Limitations (https://docs.espressif.com/projects/esp-idf/en/v4.4/esp32/api-reference/peripherals/adc.html) 813 | - High PWM frequency may have an impact in i2C communication 814 | :exclamation: → some issue (adc nonlinearity) can be corrected, but I ussualy recommend use external converter ( i.e ADS1115 or other). 815 | 816 | 817 | 818 | 819 | ###### [Contents](./README.md#contents) 820 | 821 | # 13. Project Summary 822 | 823 | *DOCUMENTATION* 824 | - [x] Project architecture and algorithms description (doc) - not public 825 | - [x] micropython usage documentation 826 | 827 | **IMPLEMENTATION** 828 | - [x] Micropython implementation (code based on structures) 829 | - [x] Python implementation (code based on classes), :exclamation: - not public 830 | - [ ] C implementation (code based on structures) :exclamation: - not public 831 | 832 | *Tools* 833 | - [x] serial protocol communication ( data exchange and controller configuration)- in progress 834 | - [ ] desktop APP for configuration, simulation and testing - - not public/ in progress 835 | 836 | **END NOTE:** with hope in the future, i will add more functionalities like: 837 | - more P-I-D algorithms implementations 838 | - PID controller autotuning functions 839 | - more advanced API: Cascade, fed-forward control implementation examples 840 | 841 | 842 | 843 | 844 | 845 | -------------------------------------------------------------------------------- /drawnings/PID_diagram_neg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/PID_diagram_neg.png -------------------------------------------------------------------------------- /drawnings/curve_gen2_neg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/curve_gen2_neg.png -------------------------------------------------------------------------------- /drawnings/curve_gen_neg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/curve_gen_neg.png -------------------------------------------------------------------------------- /drawnings/deadband_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/deadband_block.png -------------------------------------------------------------------------------- /drawnings/deadband_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/deadband_graph.png -------------------------------------------------------------------------------- /drawnings/limit_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/limit_block.png -------------------------------------------------------------------------------- /drawnings/limit_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/limit_graph.png -------------------------------------------------------------------------------- /drawnings/mv_tracking_signal_neg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/mv_tracking_signal_neg.png -------------------------------------------------------------------------------- /drawnings/norm_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/norm_block.png -------------------------------------------------------------------------------- /drawnings/norm_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/norm_graph.png -------------------------------------------------------------------------------- /drawnings/norm_sqrt_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/norm_sqrt_block.png -------------------------------------------------------------------------------- /drawnings/norm_sqrt_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/norm_sqrt_graph.png -------------------------------------------------------------------------------- /drawnings/pid_aw_neg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/pid_aw_neg.png -------------------------------------------------------------------------------- /drawnings/pid_awm_neg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/pid_awm_neg.png -------------------------------------------------------------------------------- /drawnings/pid_block_schema_neg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/pid_block_schema_neg.png -------------------------------------------------------------------------------- /drawnings/pid_isa_schema_neg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/pid_isa_schema_neg.png -------------------------------------------------------------------------------- /drawnings/rateLimit_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/rateLimit_block.png -------------------------------------------------------------------------------- /drawnings/read.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /drawnings/relay2h_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/relay2h_block.png -------------------------------------------------------------------------------- /drawnings/relay2h_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/relay2h_graph.png -------------------------------------------------------------------------------- /drawnings/relay3_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/relay3_block.png -------------------------------------------------------------------------------- /drawnings/relay3_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/relay3_graph.png -------------------------------------------------------------------------------- /drawnings/relay3h_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/relay3h_block.png -------------------------------------------------------------------------------- /drawnings/relay3h_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/relay3h_graph.png -------------------------------------------------------------------------------- /drawnings/relay_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/relay_block.png -------------------------------------------------------------------------------- /drawnings/relay_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2dof/esp_control/d249f6391b0680298fbc4ebb11698a02ca119477/drawnings/relay_graph.png -------------------------------------------------------------------------------- /functional_description.md: -------------------------------------------------------------------------------- 1 | ## Functional desciption ## 2 | 3 | [RERUTN TO MAIN](/README.md) 4 | 5 | 6 | ## Singal processing ## 7 | 8 | Basic signal procerssign functions implemented in utils_pid_esp32.py 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 29 | 30 | 31 | 32 | 33 | 38 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 69 | 70 | 71 | 90 | 91 | 92 | 93 | 94 | 95 | 104 | 105 | 106 | 107 | 108 | 118 | 119 | 120 | 121 | 122 | 140 | 141 | 142 | 143 | 145 | 146 | 157 | 158 | 159 | 160 | 161 | 172 | 173 | 174 | 175 | 176 |
---------- ----- Description ------ example
simple relay
description: y(x)= h for x>=w, -h otherwise 19 |
21 | 22 | ```python 23 | # y = relay2(x,h,w) 24 | y = relay2(0, 1, 0.5) # will return: -1 25 | y = relay2(0.6, 1, 0.5) # will return: 1 26 | ``` 27 | 28 |
Relay with hysteresis
34 | wL - low treshold of hysteresis
35 | wH - high (wH>=wL) of hysteresis 36 |
37 |
39 | 40 | ```python 41 | # class relay2h(wL=-1.0,wH=1.0) 42 | relay_2h=relay2h() # -> init:(wL=-1,wH=1) and state out: -1.0 43 | 44 | y1 = relay_2h.relay(0.0) # y1 = -1 45 | y2 = relay_2h.relay(1.0) # y2 = 1. 46 | y3 = relay_2h.relay(2.0) # y2 = 1. 47 | y4 = relay_2h.relay(0.0) # y3 = 1. 48 | y5 = relay_2h.relay(-1.0) # y3 = -1. 49 | y6 = relay_2h.relay(-2.0) # y3 = -1. 50 | y7 = relay_2h.relay(0.0) # y3 = -1. 51 | ``` 52 |
3 step relay
description
58 | 59 | ```python 60 | # y = relay3(x,h,w) 61 | y1 = relay3(0.,1,0.5) # y1 = 0. 62 | y2 = relay3(0.5,1,0.5) # y2 = 1. 63 | y3 = relay3(-0.5,1,0.5) # y3 = -1. 64 | 65 | ``` 66 |
3 step relay with hysteresis
description
72 | 73 | ```python 74 | # class relay3h(wL=0.5,wH=1) and init state out: -1.0 75 | 76 | x = [-2.,-1.1,-1,-0.6,-0.5,0,0.5,0.9,1,2,1,0.6,0.5,0.4,0,-0.5,-1,-1.1,-2] 77 | yref = [-1,-1,-1,-1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0,-1,-1] 78 | yout = [] 79 | 80 | relay_3h =relay3h() # (wL=0.5,wH=1) 81 | 82 | for i in range(0,len(x3h)): 83 | y = relay_3h.relay(x[i]) 84 | yout.append(y) 85 | 86 | print(yref) 87 | print(yout) 88 | ``` 89 |
limit (saturation) function
description
96 | 97 | ```python 98 | # y = limit(x,HL,HH) 99 | y1 = limit(0.5.,-1,1) # y1 = 0.5 100 | y2 = limit(2.,-1,1) # y2 = 1. 101 | y3 = limit(-2.,-1,1) # y3 = -1 102 | ``` 103 |
deadband function
description
109 | 110 | ```python 111 | # y = deadband(x,w) 112 | y1 = deadband(0,0.5) # y1 = 0. 113 | y2 = deadband(0.5,0.5) # y2 = 0. 114 | y3 = deadband(1.,0.5) # y3 = 0.5 115 | y4 = deadband(-1.,0.5) # y4 = -0.5 116 | ``` 117 |
rate limit
description
123 | 124 | ```python 125 | # class ratelimit(dH=1,Ts=1) , init: 1 [unit/sec] , Ts = 1 126 | # y =ratelimit.limit(x) 127 | 128 | rate=ratelimit(); 129 | 130 | y1 = rate.limit(4) # y1 = 1 131 | y2 = rate.limit(4) # y2 = 2 132 | y3 = rate.limit(4) # y3 = 3 133 | print(rate.dx) # dx = 1 -> actual rate 134 | y4 = rate.limit(4) # y4 = 4 135 | y4 = rate.limit(4) # y4 = 4 136 | print(rate.dx) # dx = 0 -> actual rate 137 | 138 | ``` 139 |
linear normalization
y= (x-aL)*(bH-bL)/(aH-aL) + bL 144 |
147 | 148 | ```python 149 | # class lin_norm(aL=0,aH=1,bL=0,bH=100) -> input form <0..1> to <0..100> 150 | 151 | norm = lin_norm() 152 | y1 = norm.normalize(0.5) # y1 = 50 153 | y2 = norm.normalize(1.0) # y2 = 100 154 | y3 = norm.normalize(1.5) # y2 = 150 155 | ``` 156 |
SQRT normalization
y = sqrt(x)((bH-bL)/10+bL)
162 | 163 | ```python 164 | # class sqrt_norm(bL=0,bH=100) 165 | 166 | norm2 = sqrt_norm() 167 | y1 =norm2.normalize(0) # 0 168 | y2 =norm2.normalize(100.0) # 100 169 | y3 = norm2.normalize(16) # 40 170 | ``` 171 |
177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /process_models/RingBuffer.py: -------------------------------------------------------------------------------- 1 | import array 2 | #from benchmark import timed_function 3 | 4 | # ring bufffer for float data 5 | class ring_buffer(object): 6 | def __init__(self, bufferSize =5,dtype='f'): 7 | 8 | self.bufferSize = bufferSize 9 | self.data = array.array(dtype, [0]*bufferSize) 10 | self.it = 0 # index of tail position 11 | self.ndata = 0 # No of data 12 | self.ii =0 # index of nex head 13 | 14 | @micropython.native 15 | def any(self): 16 | if self.ndata ==0: 17 | return False 18 | return True 19 | 20 | 21 | @micropython.native 22 | def add_data(self,value): 23 | 24 | if self.ndata>=self.bufferSize: 25 | self.ndata-=1 # overflow 26 | self.it = (self.it+1) % self.bufferSize 27 | 28 | self.data[self.ii] = value 29 | self.ndata+=1 30 | self.ii = (self.ii+1) % self.bufferSize 31 | 32 | @micropython.native 33 | def get_data(self): 34 | 35 | if self.ndata ==0 : 36 | return None 37 | 38 | x = self.data[self.it] 39 | self.ndata-=1 40 | self.it = (self.it+1) % self.bufferSize # 41 | 42 | return x 43 | 44 | 45 | # 46 | # buf = ring_buffer(bufferSize =5) 47 | # 48 | # buf.add_data(1) 49 | # buf.add_data(2) 50 | # buf.add_data(3) 51 | # buf.add_data(4) 52 | # buf.add_data(5) 53 | # buf.add_data(6) 54 | # print('--') 55 | # buf.get_data() 56 | # buf.get_data() 57 | -------------------------------------------------------------------------------- /process_models/readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Simple process models 4 | 5 | Files 6 | ```python 7 | simple_models_esp.py 8 | ├── def.discrete_FOP() - First Order Process discrete model 9 | ├── class FOPDT_model() - First Order Process with Delay Time discrete model 10 | ├── class dc_model() - disctere dc motor model 11 | └── 12 | RingBuffer.py 13 | └── class ring_buffer() - ring buffer ( for creating delay line) 14 | ``` 15 | 16 | 17 | ## First Order Proces with Delay Time 18 | 19 | First Order Proces with Delay time ```FOPDT_model() ``` described by diff. equation: 20 | 21 | $$ \tau\dot{y}(t) = - y(t) + K_{p}u(t-t_{d})$$ 22 | ```math 23 | \small Kp: \text{ process gain }\\ 24 | \small \tau : \text{proces time constans; }\\ 25 | \small t_{d}: \text{time delay}\\ 26 | ``` 27 | 28 | ## Simple DC motor 29 | Discrete DC motor (permament magnet) model: ```dc_motor() ``` described by diff. equation: 30 | 31 | $$ L\dot{i} = V -Ri+ K_{b}\dot{\phi}$$ 32 | 33 | $$ J\ddot{\phi} = K_{t}i -b_{b}\dot{\phi} - T_{d} $$ 34 | 35 | ```math 36 | \small i: \text{ current [A] }\\ 37 | \small \phi\: \text{ angle position [rad]}\\ 38 | \small R: \text{ motor resistance [Ohm] }\\ 39 | \small L : \text{motor inductance [H]}\\ 40 | \small J : \text{rotor inertia [kg.m**2/s**2]}\\ 41 | \small K_{t}: \text{torque constant (N-m/a)}\\ 42 | \small K_{b}: \text{back emf constant (volt-sec/rad)}\\ 43 | \small b_{m}: \text{motor mechanical damping [Nms]}\\ 44 | \small T_{d}: \text{load torque [Nm]}\\ 45 | ``` 46 | 47 | DC motor $$\phi\ , dot(\phi\), i $$ are storend in _x0, _x1, _x2. Calling ```.update(V, Td, Ts) will return angular speed (_x2). by defauld Td = 0.0 Nm, Ts =0.01 sec. Rotor angle position is calculated in rane 0 - 2 PI radians. 48 | 49 | :exclamation: 50 | - static friction model not implemented. 51 | - note that by substituting catalog values of pmdc motor parameters, model will compute higher speed value than real one (but time constants should be similar). 52 | - to add Friction Torque (Tf) ( given from motor catalog) to the model: ```Td += Tf*sign(sinmodel._x1)```, since micropython have not sign() math function implemented then use if else condiition. 53 | - sampling time (Ts) for dc motor model should be at lest 5 times smaller than min(Te,tm), Te: electrical time constant, Tm: mechanical time constant. 54 | 55 | example: 56 | ```python 57 | 58 | model = dc_motor() 59 | # model._x0 -> angular rotor position[rad] 60 | # model._x1 -> angular speed [ rad/s] 61 | # model._x2 -> current [A] 62 | 63 | print('model step responce') 64 | for i in range(0,100): 65 | yk = model.update(1.0) # .update(self, V,Td=0,Ts=0.01) 66 | print(i, yk) 67 | 68 | ``` 69 | 70 | 71 | -------------------------------------------------------------------------------- /process_models/simple_models_esp.py: -------------------------------------------------------------------------------- 1 | #MIT License 2 | #Copyright (c) 2023 Lukasz Szydlowski, 3 | ## simple_models_esp.py - discrere models of LTI process 4 | 5 | import array 6 | from RingBuffer import ring_buffer 7 | 8 | def discrete_FOP(y_1,Ts,u,Kp,taup): 9 | yk= taup/(taup+Ts)*y_1 + Kp*Ts/(taup+Ts) * u 10 | return yk 11 | 12 | #First Order Process with delay time 13 | class FOPDT_model(object): 14 | def __init__(self,Kp=2.0,taup=5.0,delay=3.0, y0=0.0,Ts=0.1): 15 | 16 | self.Kp = Kp 17 | self.taup = taup 18 | self.delay = delay 19 | self.Ts = Ts 20 | self.y_1 = y0 21 | self.ndelay = int(self.delay/self.Ts) 22 | self.buf = ring_buffer(bufferSize =self.ndelay+1) 23 | self.buf.ii = self.ndelay 24 | 25 | def update(self,u): 26 | 27 | self.buf.add_data(u) 28 | uk=self.buf.get_data() # 29 | yk= discrete_FOP(self.y_1,self.Ts,uk,self.Kp,self.taup) 30 | self.y_1 =yk 31 | 32 | return yk 33 | 34 | 35 | # --------------------------------------- 36 | class dc_motor(): 37 | def __init__(self,Ts =0.01,R=2.0,L =0.5 ,Kt=0.1,Kb= 0.1,bm=0.2,J=0.02): 38 | 39 | self.R = R 40 | self.L = L 41 | self.Kt =Kt 42 | self.Kb = Kb 43 | self.bm = bm 44 | self.J = J 45 | self._x0 = 0. # phi [rad] 46 | self._x1 = 0. # dot(phi) [rad/s] 47 | self._x2 = 0. # i: current [A] 48 | 49 | def update(self, V,Td=0,Ts=0.01): 50 | 51 | self._x0 = self._x0 + Ts* self._x1 52 | self._x1 = self._x1 + Ts/self.J*(self.Kt*self._x2 - self.bm*self._x1 - Td) 53 | self._x2 = self._x2 + Ts/self.L*(V - self.R*self._x2 - self.Kb*self._x1) 54 | self._x0 -= (self._x0//6.2832) * 6.2832 55 | return self._x1 56 | 57 | 58 | if __name__ == '__main__': 59 | 60 | model = FOPDT_model() #dc_motor() # 61 | print('model step responce') 62 | for i in range(0,100): 63 | yk = model.update(1.0) 64 | print(i, yk) 65 | 66 | 67 | -------------------------------------------------------------------------------- /python_simulation/read_me.txt: -------------------------------------------------------------------------------- 1 | Python simple simulation but can be adapted to micropython 2 | 3 | simple_pid_FOPDT.py : simple pid control for first order proces with delay time. script simulate: 4 | - proces step responce 5 | - pid controller step responce 6 | - proces control simulation 7 | 8 | functions: 9 | process(y,t,u,Kp,taup): firs order proces G=K/(Ts+1) ( without delay 10 | 11 | process_step_response(t)): step responce with FOPDT , where delay time has been taken into input signal (step signal) 12 | G(s)=P(s)/U(s) =>K/(Ts+1) * exp(-tau*s) => 13 | P= K/(Ts+1) exp(-tau*s) * U(s) -> K/(Ts+1) u(t-tau) 14 | 15 | 16 | pid_step_response(t): calculate pid step responce 17 | 18 | 19 | simple_pid(sp,pv,sp_last,pv_last,I,dt) : implemnt simple pid alghoritms with pid parameters tutning rules for 20 | two pid alghoritm: standard pid and 2-dof pid ( differ from calculation 21 | of D action (standard pid: D=Kd * de/dt (e=sp=pv) , 22 | 2dof pid: D=Kd dpv/dt (pv - process value) 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /python_simulation/simple_pid_FOPDT.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Mon Dec 12 13:39:03 2022 4 | 5 | @author: szydl 6 | """ 7 | 8 | import numpy as np 9 | import matplotlib.pyplot as plt 10 | plt.rcParams['lines.linewidth'] = 1 11 | plt.rcParams['lines.markersize'] = 1 12 | plt.rcParams["savefig.facecolor"]='white' 13 | plt.rcParams["figure.facecolor"]='white' 14 | plt.rcParams["axes.facecolor"]='white' 15 | plt.ion() 16 | plt.close('all') 17 | 18 | from scipy.integrate import odeint 19 | 20 | # specify number of steps 21 | Tstop=50 22 | Ts =0.1 23 | ns=int(Tstop/Ts) 24 | # define time points 25 | t = np.linspace(0,Tstop,ns+1) 26 | 27 | # process parameters ---------------------------- 28 | Kp = 2.0 # static Gain 29 | taup = 5.0 # time constans 30 | thetap = 3.0 # proces delay 31 | 32 | 33 | # model of process without delay 34 | 35 | def process(y,t,u,Kp,taup): 36 | # Kp : process gain 37 | # taup : process time constant 38 | dydt = -y/taup + Kp/taup * u 39 | return dydt 40 | 41 | def process_step_response(t): 42 | # step responce of FOPDT (First-Order-process with Delay Time ) 43 | # !! peroces delay is implemented as delay in input signal 44 | # Input: 45 | # t : time points 46 | # specify number of steps 47 | ns = len(t)-1 48 | delta_t = t[1]-t[0] # sampling time 49 | 50 | # storage for recording values 51 | uout = np.zeros(ns+1) # controller output 52 | pv = np.zeros(ns+1) # process variable 53 | 54 | # step input 55 | uout[1:]=2.0 56 | 57 | # Simulate time delay 58 | ndelay = int(np.ceil(thetap / delta_t)) 59 | 60 | # loop through time steps 61 | for i in range(1,ns): 62 | # implement time delay 63 | iop = max(0,i-ndelay) 64 | y = odeint(process,pv[i],[0,delta_t],args=( uout[iop],Kp,taup)) 65 | pv[i+1] = y[-1] 66 | return pv, uout 67 | 68 | 69 | def simple_pid(sp,pv,sp_last,pv_last,I,dt): 70 | # ------------------ 71 | # sp : setpoint 72 | # pv : proces value 73 | # pv_last : last proces value 74 | # I = integral action 75 | # dt : sampling time, 76 | # outputs ------ 77 | # u : control value 78 | # P : proportional Action 79 | # I : integral action 80 | # D : derivative action 81 | 82 | # PID parameters 83 | # Kp = 0.1 # K 84 | # Ti = 5.0 # sec 85 | # Td = 2.0 # sec 86 | #--------------------------------------------------- 87 | #Tuning based on IMC (internal Model Control) for 2-dof pid (see D action calculation comment:(!)) 88 | tauc = max(1*taup, 8*thetap) # 'moderate' tutning 89 | #tauc = max(1*taup, 1*thetap) # 'agressive' tuning 90 | Kp = ((taup+0.5*thetap)/(tauc+0.5*thetap)) 91 | Ti = (taup+0.5*thetap) 92 | Td = taup*thetap/(2*taup+thetap) 93 | 94 | # -------------------------------------------------------- 95 | # Tuning based on step Response Method (https://yilinmo.github.io/EE3011/Lec9.html) 96 | # read from figure(1) , step responce of system ( see comment for D action calculation for standard pid controller ) 97 | 98 | 99 | # Ko = 4/2 # K0=(y-yo)/(u-uo): the system gain -> calculated from proces step responce 100 | # To = 5.0 # 101 | # Lo = 3. # 102 | # # calculate The PID parameters from Ziegler-Nichols Rules 103 | # Kp = 1.2*To/(Ko*Lo) 104 | # Ti = 2*Lo 105 | # Td = 0.5*Lo 106 | 107 | # -------------------------------------- 108 | 109 | # PID Ki and Kd 110 | Ki = Kp/Ti 111 | Kd = Kp*Td 112 | #upper and lower bounds on control output in % 113 | outMin = 0.0 # 114 | outMax = 100 # as % of max PWM clock 65535.0 115 | 116 | # calculate controll error 117 | error = sp - pv 118 | 119 | # P action calculation 120 | P = Kp * error 121 | 122 | # I action 123 | ie = Ki * error * dt 124 | I = I + ie 125 | 126 | # D action : 127 | D = Kd * ((pv - pv_last ) /dt) #(!) for sp = const we have 2-dof controller 128 | # D = Kd * ((error -( sp_last- pv_last)) /dt) # (!!) standard pid controller 129 | 130 | # calculate control outout 131 | uout = P+I+D 132 | 133 | # Control saturation checking and anti-windup implementation 134 | if (uout < outMin): 135 | uout = outMin 136 | I = I - ie # anti-reset windup 137 | 138 | elif (uout > outMax): 139 | uout = outMax 140 | I = I - ie # anti-reset windup 141 | 142 | return[uout,P,I,D] 143 | 144 | 145 | 146 | def pid_step_response(t): 147 | # t = time points 148 | 149 | # specify number of steps 150 | ns = len(t) 151 | delta_t = t[1]-t[0] # sampling time 152 | 153 | # storage for recording values 154 | sp = np.zeros(ns) # setpoint 155 | uo = np.zeros(ns) # 156 | Po = np.zeros(ns) # 157 | Io = np.zeros(ns) # 158 | Do = np.zeros(ns) # 159 | # step input 160 | sp[1:]=2.0 161 | pv = 0.0 162 | pv_last = pv 163 | 164 | # PID precondition/initialization 165 | I = 0.0 # 166 | uo[0] = 0.0 167 | 168 | # loop through time steps 169 | for i in range(1,ns): 170 | 171 | [uout,P,I,D] =simple_pid(sp[i],pv,sp[i-1],pv_last,I,delta_t) 172 | uo[i] = uout 173 | Po[i],Io[i],Do[i] = P,I,D 174 | 175 | return uo, sp,Po,Io,Do 176 | 177 | # calculate step response 178 | 179 | pv,uo = process_step_response(t) # process step responce 180 | 181 | uo1,sp,Po,Io,Do = pid_step_response(t) 182 | 183 | 184 | plt.figure(1) 185 | plt.plot(t,pv,'b-',label=r'$y(t)$') 186 | plt.plot(t,uo,'r-',label=r'$u(t)$') 187 | plt.legend(loc='best'); plt.grid('True') 188 | plt.ylabel('Process Output') ; plt.xlabel('t [s]') 189 | plt.title('step responce off process') 190 | 191 | plt.figure(2) 192 | plt.subplot(2,1,1) 193 | plt.plot(t,sp,'b-',label=r'$x(t)$') 194 | plt.plot(t,uo1,'r-',label=r'$u(t)$') ; 195 | plt.legend(loc='best'); plt.grid('True') 196 | plt.ylabel(' ') 197 | plt.subplot(2,1,2) 198 | plt.plot(t,Po,'r--',label=r'$P(t)$') 199 | plt.plot(t,Io,'g--',label=r'$I(t)$') 200 | plt.plot(t,Do,'b--',label=r'$D(t)$') 201 | plt.legend(loc='best'); plt.grid('True') 202 | plt.xlabel('t [s]') 203 | 204 | # ========== PROCES CONTROL SIMULATION ================= 205 | 206 | #def process_step_response(t): 207 | if True: 208 | 209 | # step responce of FOPDT (First-Order-process with Delay Time ) 210 | # !! peroces delay is implemented as delay in input signal 211 | # Input: 212 | # t : time points 213 | # specify number of steps 214 | ns = len(t) 215 | delta_t = t[1]-t[0] # sampling time 216 | 217 | 218 | 219 | # storage for recording values 220 | uo = np.zeros(ns) # controller output 221 | pv = np.zeros(ns) # process variable 222 | Po = np.zeros(ns) # 223 | Io = np.zeros(ns) # 224 | Do = np.zeros(ns) # 225 | 226 | 227 | # Simulate time delay 228 | ndelay = int(np.ceil(thetap / delta_t)) 229 | 230 | # --- setpoint generation 231 | sp = np.zeros(ns) # setpoint 232 | 233 | sp[20:]=50.0 # [C] 234 | 235 | 236 | # PID precondition/initialization 237 | I = 0.0 # reset Integral action 238 | uout = 0.0 # initial control value 239 | uo[0] = uout 240 | 241 | #Proces value preconditions 242 | pv[0] = 0.0 # [Celsjus] 243 | 244 | # process noise (use if neded) 245 | mean = 0 246 | std = 1 247 | 248 | 249 | # loop through time steps 250 | for i in range(1,ns): 251 | 252 | # FOPTD simulation 253 | # implement time delay in control singal 254 | iop = max(0,i-ndelay) 255 | u= uo[iop] # delayed control 256 | # process simulation 257 | y = odeint(process,pv[i-1],[0,delta_t],args=(u,Kp,taup)) 258 | # noise 259 | nv = 0.0# 0.2 *np.random.normal(mean, std) # 260 | 261 | pv[i] = y[-1] +nv # measurment with white nosie, 262 | 263 | pv_value = pv[i] 264 | #----------------------------------------------- 265 | 266 | # PID outout calculation 267 | 268 | [uout,P,I,D] =simple_pid(sp[i],pv[i],sp[i-1],pv[i-1],I,delta_t) 269 | 270 | uo[i]=uout 271 | Po[i],Io[i],Do[i] = P,I,D 272 | 273 | 274 | 275 | plt.figure(3) 276 | plt.plot(t,sp,'g-',label=r'sp(t)$') 277 | plt.plot(t,pv,'b-',label=r'$pv(t)$') 278 | plt.plot(t,uo,'r-',label=r'$u(t)$') 279 | plt.legend(loc='best'); plt.grid('True') 280 | plt.ylabel('Process Output') ; plt.xlabel('t [s]') 281 | plt.title('proces control simulatio') 282 | 283 | 284 | -------------------------------------------------------------------------------- /src/curve_generator.py: -------------------------------------------------------------------------------- 1 | # MicroPython p-i-d-isa library 2 | #The MIT License (MIT), Copyright (c) 2022-2023 L.Szydlowski 3 | #https://github.com/2dof/esp_control 4 | 5 | #from benchmark import timed_function 6 | #import utime 7 | 8 | 9 | #@timed_function 10 | def sec_to_hhmmss(sec): 11 | hh = sec // 3600 12 | sec %= 3600 13 | mm = sec // 60 14 | sec %= 60 15 | 16 | return (hh, mm, sec) 17 | 18 | 19 | class Ramp_generator(object): 20 | def __init__(self,Ramp,unit='m'): 21 | 22 | self.Fgen: bool = False 23 | self.load_Ramp(Ramp,unit) 24 | 25 | def add_point(self,pnt): 26 | 27 | self.Ramp[-1]=pnt 28 | self.Ramp.append(pnt) 29 | self._Npts = len(self.Ramp)-1 30 | self._Nsec +=pnt[0]*self.unitval 31 | 32 | def load_Ramp(self,Ramp,unit='m'): 33 | 34 | if not self.Fgen: 35 | 36 | self.unit = unit 37 | self.unitval = 60 if (self.unit=='m') else 1 38 | self.Ramp = Ramp 39 | self.Ramp.append(Ramp[-1]) 40 | self._Npts = len(self.Ramp) -1 41 | self._nk = 0 42 | self._Cnt = 0 43 | self._Tcnt = 0. 44 | self.value = 0. 45 | self._Nsec = int(0) 46 | 47 | for pnt in self.Ramp[0:-1]: 48 | self._Nsec += pnt[0] 49 | 50 | self._Nsec *= self.unitval 51 | 52 | if len(self.Ramp)>1: 53 | self._Cnt=self.Ramp[1][0]*self.unitval 54 | 55 | return True 56 | 57 | else: 58 | return False 59 | 60 | def start(self,val0=0): 61 | 62 | if val0 != 0: 63 | self.Ramp[0]=[0,val0] 64 | 65 | self.Fgen=True 66 | self._Tcnt = 0 67 | 68 | if len(self.Ramp)>1: 69 | self._Cnt=self.Ramp[1][0]*self.unitval 70 | else: 71 | self.Fgen=False 72 | 73 | def resume(self): 74 | 75 | if (self._Tcnt float: 83 | 84 | if self.Fgen: 85 | self.value = self.Ramp[self._nk+1][1] - (self._Cnt/(self.Ramp[self._nk+1][0]*self.unitval))*(self.Ramp[self._nk+1][1]-self.Ramp[self._nk][1]) 86 | 87 | self._Cnt-= Ts 88 | self._Tcnt+=Ts 89 | 90 | if (self._Cnt<=0): 91 | self._nk+=1 92 | 93 | if (self._nk > (self._Npts-1)): 94 | self._nk=self._Npts-1 95 | 96 | self._Cnt=self.Ramp[self._nk+1][0]*self.unitval 97 | 98 | if (self._Tcnt >self._Nsec)&(self.value==self.Ramp[self._Npts][1]): 99 | self.Fgen=False 100 | 101 | return self.value 102 | 103 | def elapsed_time(self): 104 | 105 | return sec_to_hhmmss(self._Tcnt) 106 | 107 | def remaining_time(self): 108 | 109 | return sec_to_hhmmss(self._Nsec-self._Tcnt+1) 110 | 111 | 112 | if __name__ == "__main__": 113 | 114 | Rampa = [[0,0],[4,20],[4,20],[2,50],[4,50],[4,25],[4,25]] 115 | Rampb= [[0,0],[4,40],[4,40],[2,100],[4,100],[4,50],[4,50]] 116 | 117 | Ts=1 118 | 119 | SP_generator =Ramp_generator(Rampa, unit='s') 120 | 121 | SP_generator.add_point([5,10]) 122 | 123 | # SP_generator.load_Ramp(Rampb,unit='s') # or 124 | 125 | SP_generator.start(val0=10) 126 | 127 | for i in range(0,1400): 128 | 129 | y = SP_generator.get_value(Ts) 130 | 131 | print(y) 132 | #print(SP_generator.elapsed_time()) 133 | #print(SP_generator.remaining_time()) 134 | #if i==20: SP_generator.stop() 135 | #utime.sleep(1) 136 | if SP_generator.Fgen ==False: 137 | 138 | break 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /src/info.txt: -------------------------------------------------------------------------------- 1 | MicroPython esp32 p-i-d library 2 | 3 | # Copyright (c) 2022-2023 L.Szydlowski 4 | # Released under the MIT License (MIT) - see LICENSE file 5 | 6 | 7 | 1. file structure: 8 | 9 | __version__ = "1.0.0" 10 | 11 | *py Files: 12 | ```python 13 | ├── [src] 14 | │ ├── pid_isa.py 15 | │ ├── pid_aw.py 16 | │ ├──on_off_control.py 17 | │ ├── sp_processing.py 18 | │ ├── pv_processing.py 19 | │ ├── mv_processing.py 20 | │ ├── curve_generator.py 21 | │ ├── utils_pid_esp32.py 22 | | | 23 | | └── [thermocouples] 24 | | ├──model_K.py # model based on based on ITS-90 from IEC 60584-1/2013 25 | | ├──its90_K.py # caluclation Temperature (Celsius) based on ITS-90 from IEC 60584-1/2013 26 | | ├──its90_K_lookup.py # lookup table on array 27 | | ├──its90_K_blookup.py # lookup table on bytes array 28 | | ├──its90_J.py # caluclation Temperature (Celsius) based on ITS-90 from IEC 60584-1/2013 29 | | ├──its90_J_lookup.py # lookup table on array 30 | | ├──its90_J_blookup.py # lookup table on bytes array 31 | | ├──lookup_search.py # 32 | | | 33 | | ├──test_its90_K_thermo.py 34 | | ├──benchmark.txt # 35 | | 36 | ├── [process_model] 37 | │ ├── simple_models_esp.py # 38 | │ └── RingBuffer.py # 39 | │ 40 | ├── [Examples] 41 | │ ├── example_isa_awm_1.py 42 | │ ├── class_controller_pid_awm_example.py 43 | │ 44 | │ 45 | └── ... 46 | 47 | 48 | ``` 49 | -------------------------------------------------------------------------------- /src/mv_processing.py: -------------------------------------------------------------------------------- 1 | # MicroPython p-i-d-isa library 2 | #The MIT License (MIT), Copyright (c) 2022-2023 L.Szydlowski 3 | # 4 | # __version__ = "1.0.0" 5 | 6 | import uctypes 7 | from uctypes import FLOAT32 8 | 9 | from math import fabs, sqrt 10 | from utils_pid_esp32 import limit 11 | 12 | # MV 10*4=bytes 13 | MV_REGS = { 14 | "MvLL": 0x00|FLOAT32, 15 | "MvHL": 0x04|FLOAT32, 16 | "Tt": 0x08|FLOAT32, 17 | "Tm": 0x0C|FLOAT32, 18 | "Ts": 0x10|FLOAT32, 19 | "mvi": 0x14|FLOAT32, 20 | "mvo": 0x18|FLOAT32, 21 | "at" : 0x1C|FLOAT32, 22 | "bt" : 0x20|FLOAT32, 23 | "ct" : 0x24|FLOAT32 24 | } 25 | 26 | def mv_update(mvr,dmv,tr =0.0): 27 | 28 | mvr.mvi = mvr.mvi*mvr.at + dmv*mvr.bt + tr*mvr.ct # tracking input tr 29 | 30 | mvr.mvo = limit(mvr.mvi ,mvr.MvLL,mvr.MvHL) 31 | 32 | return mvr.mvo 33 | 34 | 35 | def mv_tune(mvr): 36 | # tracking block param update 37 | mvr.at=mvr.Tt/(mvr.Tt+mvr.Ts) 38 | mvr.bt=(mvr.Tt*mvr.Ts)/(mvr.Tm*(mvr.Tt+mvr.Ts)) 39 | mvr.ct= mvr.Ts/(mvr.Tt+mvr.Ts) 40 | 41 | def mv_reset(mvr): 42 | mvr.mvi = 0.0 43 | 44 | def mv_init0(mvr): 45 | 46 | mvr.MvLL =-100. 47 | mvr.MvHL =100 48 | mvr.Ts =1.0 49 | mvr.Tt =.5 # 50 | mvr.Tm =1. 51 | mvr.mvi =0. 52 | mvr.mvo =0. 53 | mv_tune(mvr) 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/on_off_control.py: -------------------------------------------------------------------------------- 1 | # MicroPython p-i-d-isa library 2 | #The MIT License (MIT), Copyright (c) 2022-2023 L.Szydlowski 3 | # __version__ = "1.0.0" 4 | ##https://github.com/2dof/esp_control 5 | 6 | # just copied from https://github.com/2dof/esp_control/blob/main/src/utils_pid_esp32.py 7 | # doc: relay with hysteresis https://github.com/2dof/esp_control/blob/main/functional_description.md 8 | class relay2h: 9 | def __init__(self, wL=-1.0,wH=1.0): 10 | self.wL=wL 11 | self.wH=wH 12 | self._y_1= -1 13 | 14 | def relay(self,x: float): 15 | 16 | if ((x>=self.wH)|((x>self.wL)&(self._y_1==1))): 17 | self._y_1=1.0 18 | return 1 19 | elif((x<=self.wL)|((x hystL : 28 | self.relay =relay2h(hystL ,hystH) 29 | else: 30 | self.relay =relay2h(-1 ,1) 31 | 32 | self.uk=0 33 | self.ek = 0.0 34 | 35 | self.Fstart = False # 36 | 37 | def start(self): # start/stop calculationg control 38 | self.Fstart = True 39 | 40 | def stop(self): 41 | self.Fstart = False 42 | self.uk=0 43 | 44 | def tune(self,hystL,hystH): 45 | 46 | if hystH> hystL: 47 | self.relay.wL = hystL 48 | self.relay.wH = hystH 49 | 50 | else: 51 | self.relay.wL=-1 52 | self.relay.wH= 1 53 | print('wrong Hysteresis params, must be: histL> histH') 54 | 55 | def reset(self): 56 | 57 | self.relay.y_1=-1 58 | self.ek = 0.0 59 | self.uk =0 60 | 61 | def updateControl(self,sp,pv): 62 | 63 | self.ek = sp - pv 64 | 65 | if self.Fstart: 66 | 67 | self.uk = self.relay.relay(self.ek) # will return -1 or 1 so 68 | self.uk =(self.uk+1)//2 # we shift to 0 to 1 69 | 70 | else: 71 | self.reset() 72 | 73 | return self.uk 74 | 75 | 76 | 77 | if __name__ == '__main__': 78 | 79 | from simple_models_esp import FOPDT_model 80 | from random import uniform 81 | 82 | # simulation time init 83 | Tstop=100 84 | Ts = 1 85 | Ns=int(Tstop/Ts) 86 | 87 | #[1] process model FOPDT 88 | process_model = FOPDT_model(Kp=100.0,taup=100.0,delay=5.0, y0=21,Ts=Ts) 89 | 90 | #[2] On-Off Controller initialization 91 | hystL= -1 92 | hystH = 1 # width of hysteresis: hystH - hystL 93 | controller = OnOf_controller(hystL ,hystH) 94 | controller.start() 95 | 96 | #[3] simulation 97 | sp = 50. # setpoint 98 | yk = 0. # proces outpout value 99 | uk = 0. # control value out 100 | 101 | for i in range(Ns): 102 | #[a] Read process value (in real time wee read from ADC and do pv processing) 103 | yk = process_model.update(uk) 104 | 105 | #vn = uniform(-0.5,0.5) # white noise , we model some measurment noise 106 | pv = yk #+ vn 107 | 108 | sp=50 # 109 | if k*Ts >=250: 110 | sp = 40 111 | 112 | uk = controller.updateControl(sp, pv) 113 | 114 | print("sp:",sp,"pv:",pv,"uk:",uk) -------------------------------------------------------------------------------- /src/pid_aw.py: -------------------------------------------------------------------------------- 1 | # MicroPython p-i-d-isa library 2 | #The MIT License (MIT), Copyright (c) 2022-2023 L.Szydlowski 3 | # __version__ = "1.0.1" 4 | ##https://github.com/2dof/esp_control 5 | 6 | import uctypes 7 | from uctypes import BF_POS, BF_LEN, BFUINT8,UINT8 ,FLOAT32, 8 | from utils_pid_esp32 import deadband, limit 9 | 10 | 11 | 12 | # dictionary 13 | PID_FIELDS = { 14 | "Psel": 0<(delta)): 118 | if (pid.du<0): 119 | delta *=-1 120 | pid.du = delta 121 | pid.u=pid.u1+delta 122 | 123 | pid.ed1 = pid.ed 124 | pid.u1 = pid.u 125 | 126 | return pid.u 127 | 128 | def isa_init0(pid): 129 | # for step responce test 130 | pid.Kp = 1 131 | pid.Ti = 1 132 | pid.Td = 1 133 | pid.Tm = 0.1 134 | pid.Tt = 1 135 | pid.b = 1 136 | pid.c = 1 137 | pid.Umax = 100 138 | pid.Umin = -100 139 | pid.dUlim =100 140 | pid.Ts =.1 141 | pid.Deadb = 0.1 142 | pid.Ik = 0. 143 | pid.Dk = 0. 144 | pid.u = 0. 145 | pid.du = 0. 146 | pid.u1 = 0. 147 | pid.ed1 = 0.0 148 | pid.CFG_REG =0x07 # Psel,Isel,Dsel = True 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /src/pv_processing.py: -------------------------------------------------------------------------------- 1 | # MicroPython p-i-d-isa library 2 | #The MIT License (MIT), Copyright (c) 2022-2023 L.Szydlowski 3 | ##https://github.com/2dof/esp_control 4 | # __version__ = "1.0.0" 5 | 6 | import uctypes 7 | from uctypes import BF_POS, BF_LEN, BFUINT8,UINT8, UINT16 ,FLOAT32, UINT32, BFUINT32 8 | 9 | from math import fabs, sqrt 10 | from utils_pid_esp32 import deadband, limit 11 | 12 | from benchmark import timed_function 13 | 14 | # PV 15 | PV_FIELDS = { 16 | "Pvisel": 0<(delta)): 57 | if (spr.dx<0): 58 | delta *= -1 59 | spr.dx = delta 60 | spr.sp = spr.sp1+delta 61 | 62 | spr.sp = limit(spr.sp,spr.SpLL,spr.SpHL) 63 | spr.sp1= spr.sp 64 | 65 | return spr.sp 66 | 67 | def sp_tune(spr): 68 | spr.sclin = (spr.SpebH-spr.SpebL)/(spr.SpeaH-spr.SpeaL) 69 | 70 | def sp_reset(spr): 71 | spr.sp1 = 0.0 72 | spr.dx = 0.0 73 | 74 | def sp_init0(spr): 75 | spr.SpLL =0. 76 | spr.SpHL =100 77 | spr.SpeaL = 0. 78 | spr.SpeaH = 100 79 | spr.SpebL = 0. 80 | spr.SpebH = 100. 81 | spr.Ts = 0.1 82 | spr.Rlim =1. 83 | spr.sclin = (spr.SpebH-spr.SpebL)/(spr.SpeaH-spr.SpeaL) 84 | spr.sp1 = 0.0 85 | spr.dx = 0.0 86 | spr.CFG.SPesel =True 87 | 88 | -------------------------------------------------------------------------------- /src/thermocouples/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Thermocouple library implements a ITS-90 thermocouple Temperature reference functions based on ITS-90 Polynomials from IEC 60584-1/2013 and 4 | NIST Thermoelectric Voltage Lookup Tables 5 | 6 | 7 | source of polynomials: IEC 60584-1/2013 or https://www.omega.co.uk/temperature/z/pdf/z198-201.pdf) 8 | 9 | Source of tables: [https://srdata.nist.gov/its90/main/0](https://srdata.nist.gov/its90/main/) 10 | 11 | 12 | ## Contents 13 | 14 | 1. [Thermocouple type K](#1-thermocouple-type-k) 15 | 2. [Thermocouple type J](#2-thermocouple-type-j) 16 | 3. [Benchmark](#3-benchmark) 17 | 4. [AD849x series amplifiers](#4-ad849x-series-amplifiers) 18 | 19 | 20 | 21 | 22 | 23 | # 1. Thermocouple type K 24 | 25 | **Note** 26 | Polynomials on the NIST site are for [mV] values, but they are not valid since implementation based on them do not give results 27 | like in tables. All polynomials in function implementation are from IEC 60584, and they return results (in uV) consistent with NIST Tables. 28 | Lookup tables are implemented based on  NIST Tables.  29 | 30 | implemented Tables are based on full decimal values from NIST.  31 | Go to [Benchmark](benchmark) to select best function for your needs (seed and memory size). 32 | 33 | 34 | Function: ```its90model_K(temp)``` return EMV value [μV] for input temp [C] 35 | ``` 36 | from model_K import its90model_K 37 | 38 | T = 250 #[C] 39 | E = its90model_K(T) # E is EMF, expressed in microvolts (μV); 40 | ``` 41 | 42 | Function: ``` its90_K(E) ``` calculate Temperature in [C] for input E expressed in microvolts (μV), implementation based on Polynomials 43 | ```python 44 | from its90_K import its90_K 45 | 46 | E = 10153 # [uV] -> 250 [C] 47 | 48 | T= its90_K(E) 49 | ``` 50 | 51 | function:```its90_K_lookup(E,low=0, high=163) ``` calculate Temperature in [C] for input E expressed in microvolts (μV), implementation based on approximation form lookup table in range of E for temp: -270 to 1370 . 52 | 53 | ```python 54 | from its90_K_lookup import its90_K_lookup 55 | 56 | E = 10153 # [uV] -> 250 [C] 57 | 58 | T,idx = its90_K_lookup(E[i]) 59 | T,idx = its90_K_lookup(E[i],idx-1,idx+1) 60 | 61 | ``` 62 | 63 | function ``` its90_K_blookup(E,low=0, high=137) ``` calculate Temperature in [C] for input E expressed in microvolts (μV), implementation based bytes lookup table 64 | in range of E for temp: 0 to 1370. 65 | 66 | ```python 67 | from its90_K_blookup import its90_K_blookup 68 | 69 | E = 10153 # [uV] -> 250 [C] 70 | 71 | T,idx = its90_K_blookup(E) 72 | T,idx = its90_K_blookup(E,idx-1,idx+1) 73 | ``` 74 | 75 | 76 | 77 | ``` 78 | Thermocouple Type K 79 | ├──model_K.py # thermocoule type K model based on ITS-90 polynomials 80 | ├──its90_K.py # caluclation Temperature (Celsius) based on ITS-90 Polynomials from IEC 60584-1/2013 81 | ├──its90_K_lookup.py # lookup table based on array. Caluclation temperature from -270 C to 1370 C 82 | ├──its90_K_blookup.py # lookup table on bytes. Caluclation temperature from 0.0 C to 1370 C 83 | ├──lookup_search.py # 84 | 85 | ``` 86 | 87 | ###### [Contents](./README.md#contents) 88 | 89 | 90 | # 2. Thermocouple type J 91 | 92 | Function: ```its90model_J(temp)``` return EMV value [μV] for input temp [C] 93 | 94 | ```python 95 | from model_J import its90model_J 96 | 97 | E = [-2431,-501,0,507,1019,1537,2059,12445,13555,15773,16604,16881,22400,27673,29080,29307,29647,33102,42919,45494,51877,57953,69553] # [uV] 98 | T = [-50,-10, 0, 10, 20, 30, 40,230,250,290,305,310,410,505, 530, 534,540 ,600,760,800,900, 1000, 1200] 99 | 100 | for i in range(len(E)): 101 | Ei = its90model_J(T[i]) 102 | print(i,E[i],Ei,' err:',E[i]-Ei) 103 | 104 | ``` 105 | Function: ``` its90_J(E) ``` calculate Temperature in [C] for input E expressed in microvolts (μV), implementation based on Polynomials 106 | ```python 107 | from its90_J import its90_J 108 | 109 | E = 15773 # [uV] -> 290 [C] 110 | 111 | T= its90_J(E) 112 | ``` 113 | 114 | 115 | 116 | 117 | ``` 118 | Thermocouple Type J 119 | ├──model_J.py # thermocoule type J model based on ITS-90 polynomials 120 | ├──its90_J.py # caluclation Temperature [C] based on ITS-90 Polynomials from IEC 60584-1/2013. Caluclation temperature from -210 C to 1200 C 121 | ├──its90_J_lookup.py # lookup table based on array. Caluclation temperature from 0C to 760 C 122 | ├──its90_J_blookup.py # lookup table on bytes. Caluclation temperature from 0.0 C to 760 C 123 | 124 | ``` 125 | 126 | ###### [Contents](./README.md#contents) 127 | 128 | 129 | 130 | # 3. Benchmark 131 | 132 | ``` 133 | ESP32: MicroPython v1.19.1 134 | freq : 160M Hz tables 135 | | the worst size 136 | ├──model_K() x x 137 | ├──its90_K() 0.704 ms 672 bytes - stored as 3 array.array of float32 138 | ├──its90_K_lookup() 0.705 ms 1072 bytes - stored as array.array of uint16 (165 values) 139 | ├──its90_K_blookup() 0.824 ms 276 bytes - stored as bytes ( 138 values) 140 | 141 | ├──model_J() x x 142 | ├──its90_J() 0.3 ms 944 bytes - stored as 3 array.array of float32 143 | ├──its90_J_lookup() 0.35 ms 192 bytes - stored as array.array of uint16 (19 values) 144 | ├──its90_J_blookup() 0.6 ms 38 bytes - stored as bytes ( 19 values) 145 | 146 | 147 | ``` 148 | 149 | details: [benchmark.txt](https://github.com/2dof/esp_control/blob/main/src/thermocouples/benchmark.txt) 150 | 151 | Calculations speed based on lookups can be improved if we limit the scope of table search using 152 |  returned index from previous calculation and use to limit search bonduary. 153 | 154 | ```python 155 | lo = 0 156 | hi = len(_ITS90_EKB)//2)-1 157 | 158 | T,idx = its90_K_blookup(E,lo,hi) 159 | 160 | lo , hi =idx-1 idx+1 161 | 162 | T,idx = its90_K_lookupB(E,lo,hi) 163 | ... 164 | ``` 165 | With such an approach we able to speed casculations up x1.5/2, but it is nessesary to implement 166 | condition for recalculation on full range when value will be out of actual bonduaries. 167 | 168 | ###### [Contents](./README.md#contents) 169 | 170 | # 4. AD849x series amplifiers 171 | 172 |  On [AN-1087: Thermocouple Linearization..](https://www.analog.com/en/app-notes/an-1087.html) notes, the author describedhow to use the NIST Lookup Table to perform Linearity Correction for AD849x or when they are used outside of their measurement range (Table 1. AD849x ±2°C Accuracy Temperature Ranges).  173 | 174 | **Note** 175 | Remember that AD849x correction functiona use measutment in mV and You need to convert to uV for using with provided 176 | function. 177 | 178 | ```python 179 | Vout = 1.0* 1000 # [mV] 180 | #Trj = 25 #[mV] Reference junction Temp. 181 | Vref = 0.0 #[mV] 182 | Gain = 122.4 183 | #Cjc = 4.95 #(mV/°C) 184 | Voffset = 1.25 # [mV] 185 | 186 | 187 | #[2]AD849x NIST Thermocouple Nonlinearity Compensation 188 | Euv = (Vout - Vref -Voffset)/Gain *1000 #*1000 mV 189 | 190 | Tmj = its90_K_blookup(Euv) # using Lookup table 191 | Tmj2 = its90_K(Euv) # using polynomials func. 192 | 193 | ``` 194 | 195 | 196 | ## AD7793 and ADT7320 197 | 198 | In [measuring-temp-using-thermocouples](https://www.analog.com/en/analog-dialogue/articles/measuring-temp-using-thermocouples.html), authorsdescribed measurement method (section: Measurement Solution 2: Optimized for Accuracy and Flexibility) based on AD7793 as the main ADC instrumentation amplifierfor thermocouple junction voltage measurement; an ADT7320 for reference junction temperature compensation; and a mirocontroller as the main calculation unit. 199 | 200 | By implementing thermocouple functions or lookup tables in the microcontroller, we can build a universal measurement unit for all types of thermocouples. 201 | 202 | 203 | ###### [Contents](./README.md#contents) 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /src/thermocouples/benchmark.txt: -------------------------------------------------------------------------------- 1 | 2 | ``` 3 | its90_K() its90_K_lookup() its90_K_lookup() 4 | no. Tref [ms] calc value [ms] calc value [ms] calc value 5 | ---- ------- -------------------- ------------------ -------------------- 6 | 1 -270 0.704 -245.7565 0.705 -270.0 1.131 (-162.7) 7 | 2 -110 0.398 -109.9817 0.411 -110.0 0.804 (-97.028) 8 | 3 10 0.377 9.956843 0.415 10.00 0.759 10.0 9 | 4 25 0.368 24.98365 0.330 24.987 0.783 25.0 10 | 5 250 0.383 249.9873 0.386 250.02 0.799 250.024 11 | 6 500 0.377 499.9819 0.416 500.00 0.538 500.0 12 | 7 555 0.326 555.0363 0.415 555.01 0.781 555.012 13 | 8 600 0.317 599.9939 0.357 600.00 0.590 600.0 14 | 9 665 0.324 664.9790 0.385 665.00 0.779 665.0 15 | 10 770 0.316 770.0056 0.418 770.02 0.782 770.024 16 | 11 776 0.324 776.0187 0.410 776.03 0.786 776.029 17 | 12 910 0.324 909.9952 0.416 909.99 0.705 909.9999 18 | 13 1200 0.335 1200.021 0.408 1200.00 0.533 1200.0 19 | 14 1373 0.323 1372.044 0.439 1372.01 0.824 1372.006 20 | ``` 21 | -------------------------------------------------------------------------------- /src/thermocouples/its90_J.py: -------------------------------------------------------------------------------- 1 | # https://github.com/2dof/esp_control 2 | #The MIT License (MIT), Copyright (c) 2022-2023 L.Szydlowski 3 | # __version__ = "1.0.0" 4 | 5 | import array 6 | from benchmark import timed_function 7 | 8 | import gc 9 | # 10 | gc.collect() 11 | start = gc.mem_free() 12 | # all polybomials 104 bytes but as arrays 672) 13 | 14 | 15 | 16 | # # -8,095μV to 0 μV (-210 - 0 C ) 17 | _CJi0 =array.array('f',[0.0000000, 18 | 1.9528268*1e-2, 19 | -1.2286185*1e-6, 20 | -1.0752178*1e-9, 21 | -5.9086933*1e-13, 22 | -1.7256713*1e-16, 23 | -2.8131513*1e-20, 24 | -2.3963370*1e-24, 25 | -8.3823321*1e-29]) 26 | # # 0 μV to 42919 μV (0-760 C) 27 | _CJi1=array.array('f',[0.000000, 28 | 1.978425*1e-2, 29 | -2.001204*1e-7, 30 | 1.036969*1e-11, 31 | -2.549687*1e-16, 32 | 3.585153*1e-21, 33 | -5.344285*1e-26, 34 | 5.099890*1e-31]) 35 | # # 42919 to 69553 μV (760 -1210 C) 36 | _CJi2=array.array('f',[-3.11358187*1e3, 37 | 3.00543684*1e-1, 38 | -9.94773230*1e-6, 39 | 1.70276630*1e-10, 40 | -1.43033468*1e-15, 41 | 4.73886084*1e-21]) 42 | 43 | print(start - gc.mem_free()) 44 | 45 | @timed_function 46 | def its90_J(E): 47 | 48 | 49 | Ev = 1. 50 | T=0.0 51 | if E > 42919: # 42919 to 69553 μV (760 -1210 C) 52 | T =_CJi2[0] 53 | 54 | for i in range(1,6): 55 | Ev*=E 56 | T+=_CJi2[i]*Ev 57 | 58 | elif E <= 0.: #(-210 - 0 C ) 59 | T =_CJi0[0] 60 | for i in range(1,9): 61 | Ev*=E 62 | T+=_CJi0[i]*Ev 63 | 64 | 65 | else: # 0 μV to 42919 μV (0-760 C) 66 | 67 | T =_CJi1[0] 68 | 69 | for i in range(1,8): 70 | Ev*=E 71 | T+=_CJi1[i]*Ev 72 | 73 | return T 74 | 75 | if __name__ == '__main__': 76 | # 77 | E = [-2431,-501,0,507,1019,1537,2059,12445,13555,15773,16604,16881,22400,27673,29080,29307,29647,33102,42919,45494,51877,57953,69553] # [uV] 78 | Tref= [-50,-10, 0, 10, 20, 30, 40,230,250,290,305,310,410,505, 530, 534,540 ,600,760,800,900, 1000, 1200] 79 | 80 | 81 | for i in range(len(E)): 82 | T= its90_J(E[i]) 83 | #print(Tref[i],T) -------------------------------------------------------------------------------- /src/thermocouples/its90_J_blookup.py: -------------------------------------------------------------------------------- 1 | # https://github.com/2dof/esp_control 2 | #The MIT License (MIT), Copyright (c) 2022-2023 L.Szydlowski 3 | # __version__ = "1.0.0" 4 | 5 | 6 | #import array 7 | from lookup_search import bytes_search 8 | 9 | # -0....760 , every 40 C 10 | _ITS90_EJB = b'\x0b\x08[\x10\xd8\x18r!\x1b*\xc82s;\x1aD\xbaLXU\xf9]\xa4fdoDxN\x81\x8e\x8a\x08\x94\xbe\x9d\xa7\xa7' 11 | 12 | def its90_J_blookup(E,low=0, high=19): 13 | 14 | idx = bytes_search(_ITS90_EJB,low,high, E) 15 | 16 | if idx==-1: 17 | y=(0.01943*E) if E<2059 else (0.01577*E + 83) 18 | else: 19 | i=2*idx 20 | v1=int.from_bytes(_ITS90_EJB[i:(i+2)], 'little') 21 | a=40/(int.from_bytes(_ITS90_EJB[(i+2):(i+4)], 'little') -v1) 22 | y= a*(E-v1)+(idx+1)*40 23 | 24 | return y,idx 25 | 26 | #if __name__ == '__main__': 27 | # 28 | # E = [-2431,-501,0,507,1019,1537,2059,12445,13555,15773,16604,16881,22400,27673,29080,29307,29647,33102,42919,45494,51877,57953,69553] # [uV] 29 | # Tref= [-50,-10, 0, 10, 20, 30, 40,230,250,290,305,310,410,505, 530, 534,540 ,600,760,800,900, 1000, 1210] 30 | 31 | # for i in range(len(E)): 32 | # T,idx = its90_J_blookup(E[i]) 33 | # print(Tref[i],T) 34 | 35 | -------------------------------------------------------------------------------- /src/thermocouples/its90_J_lookup.py: -------------------------------------------------------------------------------- 1 | # https://github.com/2dof/esp_control 2 | #The MIT License (MIT), Copyright (c) 2022-2023 L.Szydlowski 3 | # __version__ = "1.0.0" 4 | 5 | 6 | import array 7 | from lookup_search import idx_search 8 | 9 | 10 | # 0 - 760 C 11 | ITS90_EJ=array.array('H',[2059,4187,6360,8562,10779,13000,15219, 12 | 17434,19642,21848,24057,26276,28516,30788,33102,35470,37896,40382,42919]) 13 | 14 | 15 | def its90_J_lookup(E,low=0, high=19): 16 | 17 | idx =idx_search(ITS90_EJ,low,high, E) 18 | if idx==-1: 19 | a=40/(ITS90_EJ[0]) 20 | y = a*E 21 | 22 | else: 23 | a=40/(ITS90_EJ[idx+1]-ITS90_EJ[idx]) 24 | y = a*(E-ITS90_EJ[idx])+(idx+1)*40 25 | 26 | return y,idx 27 | 28 | 29 | # if __name__ == '__main__': 30 | # 31 | # 32 | # E = [-2431,-501,0,507,1019,1537,2059,12445,13555,15773,16604,16881,22400,27673,29080,29307,29647,33102,42919,45494,51877,57953,69553] # [uV] 33 | # Tref= [-50,-10, 0, 10, 20, 30, 40,230,250,290,305,310,410,505, 530, 534,540 ,600,760,800,900, 1000, 1210] 34 | # 35 | # 36 | # 37 | # for i in range(len(E)): 38 | # T,idx = its90_J_lookup(E[i]) 39 | # print(Tref[i],T) 40 | 41 | -------------------------------------------------------------------------------- /src/thermocouples/its90_K.py: -------------------------------------------------------------------------------- 1 | # https://github.com/2dof/esp_control 2 | #The MIT License (MIT), Copyright (c) 2022-2023 L.Szydlowski 3 | # __version__ = "1.0.0" 4 | 5 | import array 6 | from benchmark import timed_function 7 | 8 | import gc 9 | # 10 | gc.collect() 11 | start = gc.mem_free() 12 | # all polybomials 104 bytes but as arrays 672) 13 | _CKi0 =array.array('f',[0.0000000, 14 | 2.5173462e-2, 15 | -1.1662878e-6, 16 | -1.0833638e-9, 17 | -8.9773540e-13, 18 | -3.7342377e-16, 19 | -8.6632643e-20, 20 | -1.0450598e-23, 21 | -5.1920577e-28]) 22 | # 0 μV to 20,644 μV (0-500 C) 23 | _CKi1=array.array('f',[0.000000, 24 | 2.508355e-2, 25 | 7.860106e-8, 26 | -2.503131e-10, 27 | 8.315270e-14, 28 | -1.228034e-17, 29 | 9.804036e-22, 30 | -4.413030e-26, 31 | 1.057734e-30, 32 | -1.052755e-35]) 33 | # 20,644 to 54,886 μV (500 -1373 C) 34 | _CKi2=array.array('f',[-1.318058e2, 35 | 4.830222e-2, 36 | -1.646031e-6, 37 | 5.464731e-11, 38 | -9.650715e-16, 39 | 8.802193e-21, 40 | -3.110810e-26]) 41 | 42 | print(start - gc.mem_free()) 43 | 44 | 45 | 46 | #@timed_function 47 | def its90_K(E): 48 | 49 | Ev = E 50 | T= 0.0 51 | 52 | if E > 20644: #20,644 to 54,886 μV (500 -1373 C) 53 | T = _CKi2[0] 54 | 55 | for i in range(1,7): 56 | T+= _CKi2[i]*Ev 57 | Ev*= E 58 | 59 | elif E <= 0.: 60 | T = _CKi0[0] 61 | for i in range(1,9): 62 | T+=_CKi0[i]*Ev 63 | Ev*=E 64 | 65 | else: # 0 μV to 20644 μV (0-500 C) 66 | 67 | T =_CKi1[0] 68 | #Ev = E 69 | for i in range(1,8): 70 | T+= _CKi1[i]*Ev 71 | Ev*=E 72 | T+= _CKi1[8]*Ev 73 | a = _CKi1[9]*Ev 74 | a*= E 75 | T+=a 76 | 77 | return T 78 | 79 | # if __name__ == '__main__': 80 | 81 | 82 | # E=[-6458,-3852 ,397 ,1000,10153,20644,22990,24905,27658,32041,32289,37725,48838,54886] # [uV] 83 | # Tref = [-270,- 110,10, 25,250,500,555, 600, 665, 770,776, 910,1200,1372] # [C] 84 | # 85 | # for i in range(len(E)): 86 | # T= its90_K(E[i]) 87 | # print(Tref[i]) 88 | # 89 | # -------------------------------------------------------------------------------- /src/thermocouples/its90_K_blookup.py: -------------------------------------------------------------------------------- 1 | # https://github.com/2dof/esp_control 2 | #The MIT License (MIT), Copyright (c) 2022-2023 L.Szydlowski 3 | # __version__ = "1.0.0" 4 | 5 | 6 | #import array 7 | from lookup_search import bytes_search 8 | from benchmark import timed_function 9 | 10 | # -0....1370 , every 10 C 11 | _ITS90_EKB = b'\x00\x00\x8d\x01\x1d\x03\xb3\x04K\x06\xe7\x07\x84\t#\x0b\xc3\x0cb\x0e\x00\x10\x9c\x118\x13\xcf\x14g\x16\xfa\x17\x8c\x19\x1d\x1b\xac\x1c;\x1e\xca\x1f[!\xec"\x7f$\x13&\xa8\'A)\xda*v,\x13.\xb1/P1\xf02\x91426\xd57y9\x1d;\xc2\r@\xb4A[C\x03E\xaaFTH\xfdI\xa6KPM\xfaN\xa4PNR\xf9S\xa4UNW\xf8X\xa3ZM\\\xf7]\xa0_Ia\xf2b\x9bdCf\xe9g\x91i7k\xddl\x81n&p\xc9qls\ru\xadvNx\xedy\x8c{(}\xc5~a\x80\xfb\x81\x95\x83-\x85\xc5\x86\\\x88\xf1\x89\x86\x8b\x19\x8d\xac\x8e=\x90\xce\x91]\x93\xec\x94z\x96\x06\x98\x92\x99\x1c\x9b\xa5\x9c.\x9e\xb5\x9f<\xa1\xc1\xa2E\xa4\xc8\xa5J\xa7\xcb\xa8K\xaa\xca\xabF\xad\xc4\xae?\xb0\xb9\xb11\xb3\xa8\xb4\x1f\xb6\x93\xb7\x07\xb9x\xba\xe9\xbbY\xbd\xc6\xbe2\xc0\x9d\xc1\x06\xc3n\xc4\xd4\xc58\xc7\x9b\xc8\xfc\xc9\\\xcb\xba\xcc\x16\xcer\xcf\xcb\xd0#\xd2z\xd3\xcf\xd4"\xd6' 12 | 13 | #@timed_function 14 | def its90_K_blookup(E,low=0, high=137): 15 | 16 | idx = bytes_search(_ITS90_EKB,low,high, E) 17 | 18 | if idx ==-1: 19 | idx =136 20 | if E < 0: 21 | idx=0 22 | 23 | i=2*idx 24 | v1=int.from_bytes(_ITS90_EKB[i:(i+2)], 'little') 25 | a=10/(int.from_bytes(_ITS90_EKB[(i+2):(i+4)], 'little') -v1) 26 | y= a*(E-v1)+idx*10 27 | 28 | return (y,idx) 29 | 30 | # if __name__ == '__main__': 31 | # 32 | # E=[-6458,-3852 ,397 ,1000,10153,20644,22990,24905,27658,32041,32289,37725,48838, 54886] # [uV] 33 | # Tref = [-270,- 110,10, 25,250,500,555, 600, 665, 770,776, 910,1200,1372] # [C] 34 | # 35 | # for i in range(len(E)): 36 | # T,idx = its90_K_blookup(E[i]) 37 | # print(T) -------------------------------------------------------------------------------- /src/thermocouples/its90_K_lookup.py: -------------------------------------------------------------------------------- 1 | # https://github.com/2dof/esp_control 2 | #The MIT License (MIT), Copyright (c) 2022-2023 L.Szydlowski 3 | # __version__ = "1.0.0" 4 | 5 | 6 | import array 7 | from lookup_search import idx_search 8 | from benchmark import timed_function 9 | import gc 10 | 11 | gc.collect() 12 | start = gc.mem_free() 13 | ITS90_EK=array.array('H',[ 0, 17, 54, 114, 195, 300, 423, 567, 727, 14 | 908, 1104, 1317, 1545, 1789, 2046, 2320, 2606, 2904, 15 | 3215, 3538, 3871, 4215, 4569, 4931, 5302, 5680, 6066, 16 | 6458, 6855, 7256, 7661, 8070, 8481, 8894, 9309, 9725, 17 | 10140, 10554, 10966, 11378, 11786, 12193, 12596, 12998, 13399, 18 | 13798, 14197, 14596, 14997, 15398, 15801, 16204, 16610, 17019, 19 | 17429, 17840, 18253, 18667, 19082, 19497, 19915, 20332, 20750, 20 | 21171, 21591, 22012, 22433, 22855, 23278, 23701, 24125, 24549, 21 | 24973, 25399, 25823, 26250, 26676, 27102, 27528, 27955, 28381, 22 | 28808, 29234, 29661, 30087, 30512, 30938, 31363, 31787, 32213, 23 | 32637, 33059, 33483, 33905, 34327, 34747, 35168, 35587, 36006, 24 | 36423, 36839, 37256, 37671, 38086, 38498, 38911, 39323, 39733, 25 | 40143, 40551, 40958, 41366, 41771, 42176, 42579, 42982, 43382, 26 | 43784, 44183, 44582, 44980, 45376, 45772, 46166, 46559, 46952, 27 | 47343, 47734, 48123, 48511, 48897, 49284, 49669, 50053, 50436, 28 | 50816, 51198, 51577, 51955, 52331, 52706, 53081, 53452, 53824, 29 | 54194, 54562, 54931, 55296, 55660, 56022, 56384, 56744, 57102, 30 | 57458, 57812, 58166, 58518, 58867, 59216, 59564, 59909, 60253, 31 | 60596, 60937, 61276]) 32 | print(start - gc.mem_free()) 33 | 34 | 35 | 36 | 37 | @timed_function 38 | def its90_K_lookup(E,low=0, high=163): 39 | 40 | Ei = E +6458 # 'shift ITS90_EK ranges 41 | 42 | idx = idx_search(ITS90_EK, low,high, Ei) 43 | 44 | if idx ==-1: 45 | idx =163 46 | if E < 0: 47 | idx=0 48 | 49 | a=10/(ITS90_EK[idx+1]-ITS90_EK[idx]) 50 | y= a*(Ei-ITS90_EK[idx])+idx*10-270 51 | 52 | return (y,idx) 53 | 54 | if __name__ == '__main__': 55 | 56 | # 57 | # E=[-6458,-3852 ,397 ,1000,10153,20644,22990,24905,27658,32041,32289,37725,48838, 54886] # [uV] 58 | # Tref = [-270,- 110,10, 25,250,500,555, 600, 665, 770,776, 910,1200,1372] # [C] 59 | # 60 | # for i in range(len(E)): 61 | # T,idx = its90_K_lookup(E[i]) 62 | # T,idx = its90_K_lookup(E[i],idx-1,idx+1) 63 | # print('---') 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/thermocouples/lookup_search.py: -------------------------------------------------------------------------------- 1 | # https://github.com/2dof/esp_control 2 | #The MIT License (MIT), Copyright (c) 2022-2023 L.Szydlowski 3 | # __version__ = "1.0.0" 4 | 5 | 6 | def idx_search(arr,low,high, x): 7 | mid = 0 8 | 9 | while low <= high: 10 | 11 | mid = (high + low) // 2 12 | 13 | if (mid+1) >= len(arr): 14 | return mid-1 15 | 16 | if (x >= arr[mid]) & (x <= arr[mid+1]): 17 | return mid 18 | 19 | # If x is greater, ignore left half 20 | if arr[mid] < x: 21 | low = mid + 1 22 | 23 | # If x is smaller, ignore right half 24 | elif arr[mid] > x: 25 | high = mid - 1 26 | 27 | return -1 28 | 29 | def bytes_search(arr,low,high, x): 30 | 31 | mid = 0 32 | while low <= high: 33 | 34 | mid = (high + low) // 2 35 | i=2*mid 36 | v1=int.from_bytes(arr[i:(i+2)], 'little') #arr[mid] 37 | v2 = int.from_bytes(arr[(i+2):(i+4)], 'little')# arr[mid+1] 38 | 39 | if (mid+2) >= len(arr): 40 | 41 | return mid-1 42 | 43 | if (x >= v1) & (x <= v2): 44 | return mid 45 | 46 | if v1 < x: 47 | low = mid + 1 48 | 49 | elif v1 > x: 50 | high = mid - 1 51 | 52 | return -1 -------------------------------------------------------------------------------- /src/thermocouples/model_K.py: -------------------------------------------------------------------------------- 1 | # https://github.com/2dof/esp_control 2 | #The MIT License (MIT), Copyright (c) 2022-2023 L.Szydlowski 3 | # __version__ = "1.0.0" 4 | 5 | import array 6 | from math import exp 7 | from micropython import const 8 | #from benchmark import timed_function 9 | # ITS-90: IEC-60584-1-2013.pdf 10 | _CK1=array.array('f',[-1.7600413686e1, 11 | 3.8921204975e1, 12 | 1.8558770032e-2, 13 | -9.9457592874e-5, 14 | 3.1840945719e-7, 15 | -5.6072844889e-10, 16 | 5.6075059059e-13, 17 | -3.2020720003e-16, 18 | 9.7151147152e-20, 19 | -1.2104721275e-23]) 20 | 21 | # co -270 to 0 st c 22 | _CK0=array.array('f',[0.0000, 23 | 3.9450128025e1, 24 | 2.3622373598e-2, 25 | -3.2858906784e-4, 26 | -4.9904828777e-6, 27 | -6.7509059173e-8, 28 | -5.7410327428e-10, 29 | -3.1088872894e-12, 30 | -1.0451609365e-14, 31 | -1.9889266878e-17, 32 | -1.6322697486e-20]) 33 | 34 | 35 | def its90model_K(temp): 36 | 37 | T = 1 38 | E = 0.0 39 | if temp>0: 40 | E = _CK1[0] 41 | for i in range(1,10): 42 | T*=temp 43 | E+=T*_CK1[i] 44 | 45 | 46 | x=-0.0001183432*(temp-126.9686)**2 47 | x=118.5976*exp(x) 48 | E+=x 49 | else: 50 | E = _CK0[0] 51 | for i in range(1,11): 52 | T*=temp 53 | E+=T*_CK0[i] 54 | 55 | return E # [uv] 56 | 57 | if __name__ == '__main__': 58 | # 59 | Eref=[-6458,-6404,-3852,-1889,0, 397 ,1000,10153,20644,22990,24905,27658,32041,32289,37725,48838,54886] # [uV] 60 | T = [-270.,-250, -110.,-50.0,0.0,10.0,25.0,250.0,500.0,555.0,600.0,665.0,770.0,776.0,910.0,1200.,1372.] # [C] 61 | # 62 | for i in range(len(Eref)): 63 | Ei = its90model_K(T[i]) 64 | print(i,Eref[i],Ei,' err:',Eref[i]-Ei) 65 | 66 | -------------------------------------------------------------------------------- /src/thermocouples/test_its90_K_thermo.py: -------------------------------------------------------------------------------- 1 | # https://github.com/2dof/esp_control 2 | #The MIT License (MIT), Copyright (c) 2022-2023 L.Szydlowski 3 | # __version__ = "1.0.0" 4 | 5 | import array 6 | from benchmark import timed_function 7 | 8 | from model_J import its90model_J 9 | 10 | 11 | 12 | E = [-2431,-501,0,507,1019,1537,2059,12445,13555,15773,16604,16881,22400,27673,29080,29307,29647,33102,42919,45494,51877,57953,69553] # [uV] 13 | T = [-50,-10, 0, 10, 20, 30, 40,230,250,290,305,310,410,505, 530, 534,540 ,600,760,800,900, 1000, 1210] 14 | 15 | 16 | if __name__ == '__main__': 17 | 18 | 19 | print('testing: its90model_J:') 20 | print('i,Eref,E,err') 21 | 22 | for i in range(len(E)): 23 | Ei = its90model_J(T[i]) 24 | print(i,E[i],Ei,' err:',E[i]-Ei) -------------------------------------------------------------------------------- /src/utils_pid_esp32.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # utils_pis_esp32_ MicroPython utils library for pid control library 3 | ##https://github.com/2dof/esp_control 4 | #The MIT License (MIT), Copyright (c) 2022-2023 L.Szydlowski 5 | #__version__ = "1.0.0" 6 | 7 | from math import fabs, sqrt 8 | 9 | def limit(x,HL,HH): 10 | 11 | if (x>=HH): 12 | return HH 13 | elif (x<=HL): 14 | return HL 15 | else: 16 | return x 17 | 18 | def deadband(x,w): 19 | 20 | if (x>w): 21 | return x-w 22 | elif (x<-w): 23 | return x+w 24 | else: 25 | return 0 26 | 27 | def relay2(x,h,w): 28 | 29 | if (x>=w): 30 | return h 31 | else: 32 | return -h 33 | 34 | def relay3(x,h,w): 35 | 36 | if (x>=w): 37 | return h 38 | elif (x<=-w): 39 | return -h 40 | else: 41 | return 0 42 | 43 | 44 | class relay2h: 45 | 46 | def __init__(self, wL=-1.0,wH=1.0): 47 | self.wL=wL 48 | self.wH=wH 49 | self._y_1=-1 50 | 51 | def relay(self,x: float): 52 | 53 | if ((x>=self.wH)|((x>self.wL)&(self._y_1==1))): 54 | self._y_1=1.0 55 | return 1 56 | elif((x<=self.wL)|((x=self.wH)|((x>self.wL)&(self._y_1==1))): 69 | self._y_1=1 70 | return 1 71 | elif((x>=-self.wL)&(x<=self.wL)|((x=-self.wH))): 72 | self._y_1=0 73 | return 0 74 | if ((x<-self.wH)|((x<-self.wL)&(self._y_1==-1))): 75 | self._y_1=-1 76 | return -1 77 | 78 | 79 | class lin_norm: 80 | def __init__(self, aL=0,aH=1,bL=0,bH=100): 81 | self.__aL = aL 82 | self.__aH = aH 83 | self.__bL = bL 84 | self.__bH = bH 85 | self.__scale_calc() 86 | 87 | def __scale_calc(self): 88 | self._scale = (self.__bH-self.__bL)/(self.__aH-self.__aL) 89 | 90 | @property 91 | def aL(self): 92 | return self.__aL 93 | 94 | @aL.setter 95 | def aL(self, value): 96 | self.__aL = value 97 | self.__scale_calc() 98 | 99 | @property 100 | def aH(self): 101 | return self.__aH 102 | 103 | @aH.setter 104 | def aH(self, value): 105 | self.__aH = value 106 | self.__scale_calc() 107 | @property 108 | def bL(self): 109 | return self.__bL 110 | 111 | @bL.setter 112 | def bL(self, value): 113 | self.__bL = value 114 | self.__scale_calc() 115 | 116 | @property 117 | def bH(self): 118 | return self.__bH 119 | 120 | @bH.setter 121 | def bH(self, value): 122 | self.__bH = value 123 | self.__scale_calc() 124 | 125 | def normalize(self,x): 126 | 127 | return (x-self.__aL)*self._scale+self.__bL 128 | 129 | 130 | class sqrt_norm: 131 | def __init__(self,bL=0,bH=100): 132 | 133 | self.__bL = bL 134 | self.__bH = bH 135 | self._scale = (self.__bH-self.__bL)/10 136 | 137 | def __scale_calc(self): 138 | self._scale = (self.__bH-self.__bL)/10 139 | 140 | @property 141 | def bL(self): 142 | return self.__bL 143 | 144 | @bL.setter 145 | def bL(self, value): 146 | self.__bL = value 147 | self.__scale_calc() 148 | 149 | @property 150 | def bH(self): 151 | return self.__bH 152 | 153 | @bH.setter 154 | def bH(self, value): 155 | self.__bH = value 156 | self.__scale_calc() 157 | 158 | def normalize(self,x): 159 | 160 | return self._scale*sqrt(x)+self.__bL 161 | 162 | class ratelimit: 163 | def __init__(self,dH=1,Ts=1): 164 | self.dH=dH 165 | self.Ts=Ts 166 | self.x_1=0 167 | self.fh =False 168 | self.dx = 0 169 | 170 | def reset(self,x0=0): 171 | self.x_1=x0 172 | 173 | def limit(self,x): 174 | 175 | self.dx = x-self.x_1 176 | delta=self.dH*self.Ts 177 | 178 | if (fabs(self.dx)