|
| 1 | +--- |
| 2 | +title: PWM |
| 3 | +weight: 1 |
| 4 | +--- |
| 5 | + |
| 6 | +## What is PWM? |
| 7 | + |
| 8 | +PWM, short for Pulse Width Modulation, is a trick to dim a digital output by switching the output on and off quickly enough so that the average remains at a value somewhere between on and off. Using this trick, a digital output starts to behave like an analog output. One very common application is dimming of LEDs. |
| 9 | + |
| 10 | +In the example below you can control a simulated LED using PWM. You can change the duty cycle and the speed and see how that affects the LED. Notice how, if the frequency is high enough, the output appears continuous. |
| 11 | + |
| 12 | +<label> |
| 13 | + Duty cycle: <span id="pwm-dutycycle">50%</span> |
| 14 | + <input id="pwm-dutycycle-input" type="range" value="50" min="0" max="100"/> |
| 15 | +</label> |
| 16 | + |
| 17 | +<label> |
| 18 | + Speed: <span id="pwm-period">1000ms</span> or <span id="pwm-frequency">1Hz</span> |
| 19 | + <input id="pwm-period-input" type="range" value="0" min="0" max="30"/> |
| 20 | +</label> |
| 21 | + |
| 22 | +<svg viewBox="0 0 340 30" width="340px" height="30px"> |
| 23 | + <rect x="0.5" y="0.5" width="302" height="28" fill="transparent" stroke="gray"/> |
| 24 | + <path id="pwm-path" fill="transparent" stroke="black" stroke-linecap="square" d="M 1.5 27.5 H 1.5 V 1.5 H 151.5 V 27.5 H 301.5"/> |
| 25 | + <circle id="pwm-led" cx="325" cy="14.5" r="10" fill="gray" stroke-width="1" stroke="gray"/> |
| 26 | +</svg> |
| 27 | + |
| 28 | +<style id="pwm-styles"> |
| 29 | +/* Note: these properties will be modified from JavaScript using CSSOM. */ |
| 30 | +#pwm-led { |
| 31 | + animation-duration: 1s; |
| 32 | + animation-name: pwm; |
| 33 | + animation-iteration-count: infinite; |
| 34 | +} |
| 35 | +@keyframes pwm { |
| 36 | + from { |
| 37 | + fill: transparent; |
| 38 | + } |
| 39 | + 10% { |
| 40 | + fill: red; |
| 41 | + } |
| 42 | + 50% { |
| 43 | + fill: red; |
| 44 | + } |
| 45 | + 60% { |
| 46 | + fill: transparent; |
| 47 | + } |
| 48 | + to { |
| 49 | + fill: transparent; |
| 50 | + } |
| 51 | +} |
| 52 | +</style> |
| 53 | + |
| 54 | +<script> |
| 55 | +var period, frequency, dutyCycle; |
| 56 | + |
| 57 | +// Update the graph and the LED animation. |
| 58 | +function updateView() { |
| 59 | + if (frequency === undefined) |
| 60 | + return; // updateSpeed not yet called |
| 61 | + |
| 62 | + // Update the graph. |
| 63 | + let graphTop = 1.5; |
| 64 | + let graphBottom = 27.5; |
| 65 | + let graphStart = 1.5; |
| 66 | + let graphEnd = 301.5; |
| 67 | + let width = graphEnd - graphStart |
| 68 | + let widthTime = 1; // 1s |
| 69 | + let path; |
| 70 | + if (dutyCycle == 0) { |
| 71 | + // Duty cycle is 0%, draw a straight line at the bottom. |
| 72 | + path = 'M ' + graphStart + ' ' + graphBottom + ' H ' + graphEnd; |
| 73 | + } else if (dutyCycle == 1) { |
| 74 | + // Duty cycle is 100%, draw a straight line at the top. |
| 75 | + path = 'M ' + graphStart + ' ' + graphTop + ' H ' + graphEnd; |
| 76 | + } else { |
| 77 | + // Duty cycle is somewhere in between, draw a line starting at the start of |
| 78 | + // a pulse. |
| 79 | + path = 'M ' + graphStart + ' ' + graphBottom; |
| 80 | + for (let t=0; t<widthTime; t+=period) { |
| 81 | + let pulseStart = graphStart + t*width; |
| 82 | + path += ' H ' + pulseStart + ' V ' + graphTop; |
| 83 | + let pulseEnd = graphStart + (t+period*dutyCycle)*width; |
| 84 | + if (pulseEnd > graphEnd) { |
| 85 | + // End of the graph, don't move the line back. |
| 86 | + continue; |
| 87 | + } |
| 88 | + if (dutyCycle >= 1) { |
| 89 | + continue; |
| 90 | + } |
| 91 | + path += ' H ' + pulseEnd + ' V ' + graphBottom; |
| 92 | + } |
| 93 | + } |
| 94 | + path += ' H ' + graphEnd; |
| 95 | + document.querySelector('#pwm-path').setAttribute('d', path); |
| 96 | + |
| 97 | + // Update the LED animation CSS. |
| 98 | + let animation; |
| 99 | + let style; |
| 100 | + for (let rule of document.querySelector('#pwm-styles').sheet.cssRules) { |
| 101 | + if (rule.type == CSSRule.KEYFRAMES_RULE && rule.name == 'pwm') { |
| 102 | + animation = rule; |
| 103 | + } else if (rule.type == CSSRule.STYLE_RULE && rule.selectorText == '#pwm-led') { |
| 104 | + style = rule; |
| 105 | + } |
| 106 | + } |
| 107 | + if (dutyCycle == 1) { |
| 108 | + style.style.animationName = ''; |
| 109 | + style.style.fill = 'red'; |
| 110 | + } else if (dutyCycle == 0) { |
| 111 | + style.style.animationName = ''; |
| 112 | + style.style.fill = 'transparent'; |
| 113 | + } else if (period < 0.035) { |
| 114 | + // Speed is too high for most monitors/browsers to display properly. |
| 115 | + // This is a bit cheating, but there is no other way to make it show what |
| 116 | + // it is intended to show. |
| 117 | + style.style.animationName = ''; |
| 118 | + style.style.fill = 'rgba(255, 0, 0, ' + dutyCycle + ')'; |
| 119 | + } else { |
| 120 | + let delayPercent = (frequency/50) * 100; // soften transition in 20ms |
| 121 | + animation.cssRules[1].keyText = delayPercent + '%'; |
| 122 | + animation.cssRules[2].keyText = (dutyCycle*100) + '%'; |
| 123 | + animation.cssRules[3].keyText = (dutyCycle*100 + delayPercent) + '%'; |
| 124 | + style.style.animationName = 'pwm'; |
| 125 | + style.style.animationDuration = period + 's'; |
| 126 | + } |
| 127 | +} |
| 128 | + |
| 129 | +// Called when the duty cycle slider is changed. |
| 130 | +function updateDutyCycle() { |
| 131 | + let input = document.querySelector('#pwm-dutycycle-input'); |
| 132 | + dutyCycle = input.value / 100; |
| 133 | + document.querySelector('#pwm-dutycycle').textContent = input.value + '%'; |
| 134 | + updateView(); |
| 135 | +} |
| 136 | + |
| 137 | +// Called when the speed slider is changed. |
| 138 | +function updateSpeed() { |
| 139 | + let input = document.querySelector('#pwm-period-input'); |
| 140 | + period = Math.pow(10, (30-input.value) / 10) / 1000; |
| 141 | + frequency = 1 / period; |
| 142 | + let periodString = (period * 1000).toFixed(0)+'ms'; |
| 143 | + if (period < 0.01) { |
| 144 | + periodString = (period * 1e3).toFixed(1)+'ms'; |
| 145 | + } |
| 146 | + if (period < 0.001) { |
| 147 | + periodString = (period * 1e6).toFixed(0)+'µs'; |
| 148 | + } |
| 149 | + document.querySelector('#pwm-period').textContent = periodString; |
| 150 | + let frequencyString = frequency.toFixed(0) + 'Hz'; |
| 151 | + document.querySelector('#pwm-frequency').textContent = frequencyString; |
| 152 | + updateView(); |
| 153 | +} |
| 154 | + |
| 155 | +// Listen to slider changes. |
| 156 | +document.querySelector('#pwm-dutycycle-input').addEventListener('input', updateDutyCycle); |
| 157 | +document.querySelector('#pwm-period-input').addEventListener('input', updateSpeed); |
| 158 | + |
| 159 | +// Make sure the graph and LED are initialized properly. |
| 160 | +updateDutyCycle(); |
| 161 | +updateSpeed(); |
| 162 | +</script> |
| 163 | + |
| 164 | +## How does a PWM peripheral work? |
| 165 | + |
| 166 | +In most cases, a PWM is a combination of a counter that counts up or down and a few channels with a value that this counter is continuously compared against. |
0 commit comments