**PWM Intuition** We looked at PWM in lecture on Thursday. We can see an example response of a system to three different duty cycles. Note that the signals all go to different steady-state values, have various ripples, and converge to their steady state values at different times... These files were generated with the PWM code at the end which let's you vary the Time-period of the driving signal, the time constant, the duty cycle, etc...Let's go over why this is and how to quantify it.   First up let's make sure we make peace with the fact that if we drive the LR circuit with a 50% duty cycle signal (one that is ON and OFF equal amounts), it will build up a current through it. If our voltage alternates between 0 and 5V, and the $$\tau$$ is something like 1 second (for the sake of easy math), how will our system evolve if we alternate the input voltages every 0.1 seconds (period of 0.2 seconds). Let's do some math out manually. 1. $v_s$ transitions to 5V. The current starts at 0 and wants to ultimately be $\frac{5V}{R}$...Let's say that works out to be 5A, again, just for the sake of easy math. It will follow the equation: $i(t)=5-5e^{-t} \text{ A}$. The circuit gets the point of $t=0.1$ until the switch to 0. At that point, $i(0.1) = 5-5e^{-0.1} = 0.4785 \text{ A}$. This will become our starting current in the next phase: 1. $v_s$ transitions to 0V. The current will start at $0.4785 \text{ A}$ and then want to decay to $0\text{ A}$. As a result, our system will follow the equation $i(t) = 0.4785e{-(t-0.1)} \text{ A}$. Solving for where this ends up at $t=0.2$ (when the next transition happens) yields: $i(0.2) = 0 + (0.4785-0)e^{-(0.2-0.1)} = 0.4785e^{-(0.2-0.1)} = 0.4305\text{ A}$. Note that after the first full cycle, we're at a higher current than we were when we started! Weird, but oddly satisfying. 1. $v_s$ transitions to 5V. The current starts at $0.4305\text{ A}$ and wants to go to ultimately be $5\text{ A}$... It will follow the equation: $i(t)=5-(0.4305-5)e^{-(t-0.2)} \text{ A}$. The circuit gets the point of $t=0.3$ until the switch to 0. At that point, $i(0.3) = 5-4.5695e^{-(0.3-0.2)} = 0.8653 \text{ A}$. This will become our starting current in the next phase: 1. $v_s$ transitions to 0V. The current will start at $0.8653 \text{ A}$ and then want to decay to $0\text{ A}$. As a result, our system will follow the equation $i(t) = 0 + (0.8653-0)e{-(t-0.3)} \text{ A}$. Solving for where this ends up at $t=0.2$ (when the next transition happens) yields: $i(0.2) = 0 + (0.8653-0)e^{-(0.2-0.3)} = 0.8653^{-(0.2-0.3)} = 0.7830\text{ A}$. Note that after the second full cycle, we're at an even higher current than we were when we started! Weird, but oddly satisfying. So in response to these voltage pulses, the inductor has the following current through it: 1. FIRST ON: Starts at $0\text{ A}$ goes to $0.4785 \text{ A}$ 1. FIRST OFF: Starts at $0.4785\text{ A}$ goes to $0.4305 \text{ A}$ 1. SECOND ON: Starts at $0.4305\text{ A}$ goes to $0.8653 \text{ A}$ 1. SECOND OFF: Starts at $0.8653\text{ A}$ goes to $0.7830 \text{ A}$ Look at that current climbing! This asymmetry that arises from natural exponential charging/discharging is key to why a non-0 average will build up. # PWM Math In lecture we saw that by driving a On-Off-On signal into a first order system at frequencies faster than the time constant we can get some really neat result behaviors. Let's look at the ability of PWM signals to result in various time-averaged outputs. We'll focus on our RL circuit from lecture but the same math works for RC stuff too (with different variables)!!!: ## Time Average If we remember our KVL expression for the Voltage Source driving an R-L load we have: $$ L\frac{di(t)}{dt}+i(t)R = v_s(t) $$ If we say that the time-average of a periodic signal $x(t)$ $\bar{x}$ which is defined as: $$ \bar{x} = \frac{1}{T}\int_{t-T}^{t}x(s)ds $$ If we do this to both sides of our KVL expression earlier on the page to generate the time average of both voltages (sides). $$ \frac{1}{T}\int_{t-T}^{t}\left(L\frac{di(t)}{dt}+i(t)R\right)ds = \frac{1}{T}\int_{t-T}^{t}v_s(t)ds $$ which turns into: $$ \frac{L}{T}\left(i(t) - i(t-T) \right) + R\bar{i} = \bar{v_s} $$ In the steady-state situation (after the ramp up.) we can safely say/assert that $i(t) = i(t-T)$. As a result, the equation above, will simplify greatly to $$ \frac{L}{T}\left(i(t) - i(t) \right) + R\bar{i} = \bar{v_s} $$ and then to: $$ \bar{i} = \frac{\bar{v_s}}{R} $$ This is extremely powerful, because what it is saying is that the average current through the circuit will be directly based on the average voltage you apply...So it is directly proportional! Noice. The same thing can be done for our capacitor situation. ## Peak to Peak Let's define our peak-to-peak wiggle of our signal (when in steady-state) as $\Delta i$, If we define our Duty Cycle as $D$ and it can range from $0$ to $1.0$, where $0.5$ refers to 50% duty cycle, we can approximate the original $$ \frac{L}{T}\left(i(t) - i(t) \right) + R\bar{i} = \bar{v_s} $$ to be the following when the ON signal is present: $$ L\frac{\Delta i}{DT}+R\bar{i} \approx V_S $$ and teh following when the OFF signal is present $$ L\frac{-\Delta i}{(1-D)T}+R\bar{i} \approx 0 $$ Merge both of these to remove the $R\bar{i}$ term so that we end up with: $$ V_S = \frac{L\Delta i}{T}\left(\frac{1}{D}+\frac{1}{1-D}\right) = \frac{L\Delta i}{TD(1-D)} $$ And as a result the peak-to-peak ripple can be approximated as: $$ \Delta i \approx \frac{V_STD(1-D)}{L} $$ Again, you could do a similar thing for your capacitor. Another thing you'll ## Dynamics One final thing about a system's PWM response is that even though we're stimulating it at a higher frequency and it is jumping up and down, its average value will converge to the $\bar{x}$ value with the system's time constant. So if you experiment with the code below, you'll see that the average value converges just as fast as the system value would respond without the PWM. ## Python Simulation of PWM The Python code below let me create those awesome PWM images included in lab. Feel free to run/mess with it however much/little you want. I feel like I always benefit from having a toy model of a phenomenon to play with to convince myself of the math ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ python linenumbers import numpy as np import math from bokeh.plotting import output_file, show, figure from bokeh.models import Legend from bokeh.layouts import column from bokeh.io import export_png '''this code let's you run arbitrarily long simulations of the response of systems with a specified time constant (tau can specified in terms of R and C or L and R or directly). The simulation isn't really in terms of a voltage so much as a driving source with min and max value and a stimulation period. Is this elegant code? No, but it works. You need bokeh and numpy for this to work. In a terminal do: pip install bokeh pip install numpy if you don't already have these, they will generate a plot in your browser window so don't freak out when the browser opens up. ''' R = 1e2 C = 10e-6 L = 1e-3 tau = R*C tau = 0.001 MAX_VAL = 5 MIN_VAL = 0 T = 0.0001 DUTY_CYCLES = [0.25,0.5,0.75] #specify your duty cycles here! DURATION = 30 #how many pulses) DUTY_CYCLE = DUTY_CYCLES[0] ta = np.linspace(0,2*T,100) va = ta*0 vsa = ta*0 for x in range(2,DURATION): t1 = np.linspace(T*x,T*x+T*DUTY_CYCLE, int(100*DUTY_CYCLE)) v1 = MAX_VAL + (va[-1]-MAX_VAL)*np.power(math.exp(1),(-(t1-T*x)/(tau))) vs1 = MAX_VAL+t1*0 t2 = np.linspace(T*x+T*DUTY_CYCLE,T*(x+1), int(100*(1-DUTY_CYCLE))) v2 = MIN_VAL + (v1[-1]-MIN_VAL)*np.power(math.exp(1),-(t2-(T*x+DUTY_CYCLE*T))/(tau)) vs2 = MIN_VAL+t2*0 ta=np.concatenate((ta,t1)) ta=np.concatenate((ta,t2)) va= np.concatenate((va,v1)) va= np.concatenate((va,v2)) vsa = np.concatenate((vsa,vs1)) vsa = np.concatenate((vsa,vs2)) DUTY_CYCLE = DUTY_CYCLES[1] tb = np.linspace(0,2*T,100) vb = tb*0 vsb = tb*0 for x in range(2,DURATION): t1 = np.linspace(T*x,T*x+T*DUTY_CYCLE, int(100*DUTY_CYCLE)) v1 = MAX_VAL + (vb[-1]-MAX_VAL)*np.power(math.exp(1),(-(t1-T*x)/(tau))) vs1 = MAX_VAL+t1*0 t2 = np.linspace(T*x+T*DUTY_CYCLE,T*(x+1), int(100*(1-DUTY_CYCLE))) v2 = MIN_VAL + (v1[-1]-MIN_VAL)*np.power(math.exp(1),-(t2-(T*x+DUTY_CYCLE*T))/(tau)) vs2 = t2*0+MIN_VAL tb=np.concatenate((tb,t1)) tb=np.concatenate((tb,t2)) vb= np.concatenate((vb,v1)) vb= np.concatenate((vb,v2)) vsb = np.concatenate((vsb,vs1)) vsb = np.concatenate((vsb,vs2)) DUTY_CYCLE = DUTY_CYCLES[2] tc = np.linspace(0,2*T,100) vc = tc*0 vsc = tc*0 for x in range(2,DURATION): t1 = np.linspace(T*x,T*x+T*DUTY_CYCLE, int(100*DUTY_CYCLE)) v1 = MAX_VAL + (vc[-1]-5)*np.power(math.exp(1),(-(t1-T*x)/(tau))) vs1 = MAX_VAL+ t1*0 t2 = np.linspace(T*x+T*DUTY_CYCLE,T*(x+1), int(100*(1-DUTY_CYCLE))) v2 = MIN_VAL + (v1[-1]-MIN_VAL)*np.power(math.exp(1),-(t2-(T*x+DUTY_CYCLE*T))/(tau)) vs2 = 0*t2 + MIN_VAL tc=np.concatenate((tc,t1)) tc=np.concatenate((tc,t2)) vc= np.concatenate((vc,v1)) vc= np.concatenate((vc,v2)) vsc = np.concatenate((vsc,vs1)) vsc = np.concatenate((vsc,vs2)) output_file("legend.html") p = figure(plot_width=900, plot_height=400) #p = figure(plot_width=900, plot_height=600,toolbar_location="above") a=p.line(ta, va, line_color="red",line_width=2) b=p.line(tb, vb, line_color="blue",line_width=2) c=p.line(tc, vc, line_color="green",line_width=2) legend = Legend(items=[ (f"{DUTY_CYCLES[0]*100}% Duty Cycle" , [a]), (f"{DUTY_CYCLES[1]*100}% Duty Cycle" , [b]), (f"{DUTY_CYCLES[2]*100}% Duty Cycle" , [c]), ], location="center") #p.legend.location = "bottom_left" p.add_layout(legend, 'right') p.xaxis.axis_label = "Time (s)" p.yaxis.axis_label = "Signal" p.toolbar.logo = None p.toolbar_location = None q = figure(plot_width=900, plot_height=100) #p = figure(plot_width=900, plot_height=600,toolbar_location="above") a=q.line(ta, vsa, line_color="red",line_width=2) legend = Legend(items=[ (f"{DUTY_CYCLES[0]*100}% Duty Cycle" , [a]), ], location="center") #p.legend.location = "bottom_left" q.add_layout(legend, 'right') q.xaxis.axis_label = "Time (s)" q.yaxis.axis_label = "source(t)" q.toolbar.logo = None q.toolbar_location = None qq = figure(plot_width=900, plot_height=100) #p = figure(plot_width=900, plot_height=600,toolbar_location="above") a=qq.line(tb, vsb, line_color="blue",line_width=2) legend = Legend(items=[ (f"{DUTY_CYCLES[1]*100}% Duty Cycle" , [a]), ], location="center") #p.legend.location = "bottom_left" qq.add_layout(legend, 'right') qq.xaxis.axis_label = "Time (s)" qq.yaxis.axis_label = "source(t)" qq.toolbar.logo = None qq.toolbar_location = None qqq = figure(plot_width=900, plot_height=100) #p = figure(plot_width=900, plot_height=600,toolbar_location="above") a=qqq.line(tc, vsc, line_color="green",line_width=2) legend = Legend(items=[ (f"{DUTY_CYCLES[2]*100}% Duty Cycle" , [a]), ], location="center") #p.legend.location = "bottom_left" qqq.add_layout(legend, 'right') qqq.xaxis.axis_label = "Time (s)" qqq.yaxis.axis_label = "source(t)" qqq.toolbar.logo = None qqq.toolbar_location = None show(column(p,q,qq,qqq)) #show(p) from bokeh.io import export_png export_png(column(p,q), filename="pwm_sim.png") ~~~~~~~~~