Skip to content

Commit

Permalink
pwm in depth
Browse files Browse the repository at this point in the history
  • Loading branch information
ImplFerris committed Dec 25, 2024
1 parent 7ca2c36 commit 2afae85
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 35 deletions.
1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- [Voltage Divider](./core-concepts/voltage-divider.md)
- [ADC](./core-concepts/adc.md)
- [PWM](./core-concepts/pwm/index.md)
- [PWM in Depth](./core-concepts/pwm/pwm-in-depth.md)
- [LED PWM Controller](./core-concepts/pwm/led-pwm-controller.md)
- [no_std, no_main in Rust](./core-concepts/no-std-main.md)
- [Dimming LED](./dimming-led/index.md)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed src/core-concepts/images/pwm-duty-cycle.png
Binary file not shown.
66 changes: 31 additions & 35 deletions src/core-concepts/pwm/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@
margin: 10px auto;
}
#pwmCanvas {
background-color: #fefefe;
background-color: #fefefe;
}
</style>

In this section, we will explore what is PWM and why we need it.
Expand All @@ -86,19 +87,38 @@ PWM stands for **Pulse Width Modulation**, creates an analog-like signal by rapi

<img style="display: block; margin: auto;" alt="LED PWM" src="../images/led-pwm.jpg" />

## Duty Cycle

The **duty cycle** of the signal determines how long it stays on compared to how long it stays off.
The **duty cycle** is the percentage of time a signal is "on" during one complete cycle.

- **Duty Cycle**:
The percentage of time the signal is on during one cycle.
- For example:
- 100% duty cycle means the signal is always on.
- 50% duty cycle means the signal is on half the time and off half the time.
- 0% duty cycle means the signal is always off.
<img style="display: block; margin: auto;" alt="Duty Cycle" src="../images/pwm-duty-cycle.png" />
<span style="text-align: center;display: block; margin: auto; font-size: 12px;">Image Credit: Wikipedia</span>
For example:
- 100% duty cycle means the signal is always on.
- 50% duty cycle means the signal is on half the time and off half the time.
- 0% duty cycle means the signal is always off.


Here is the interactive simulation. Use the sliders to adjust the duty cycle and frequency, and watch how the pulse width and LED brightness change. The upper part of the square wave represents when the signal is high (on). The lower part represents when the signal is low(off)

<canvas id="pwmCanvas" width="800" height="200"></canvas>
<div class="led-container">
<div class="led-body" id="ledBody"></div>
<div class="led-pin anode"></div>
<div class="led-pin cathode"></div>
</div>

<div class="slider-container">
<label for="dutyCycle">Duty Cycle (%): </label>
<input type="range" id="dutyCycle" min="0" max="100" value="50">
<span id="dutyCycleValue">50</span>%
</div>
<div class="slider-container">
<label for="frequency">Frequency (Hz): </label>
<input type="range" id="frequency" min="1" max="50" value="10">
<!-- <span id="frequencyValue">x</span> Hz -->
</div>

If you change the duty cycle from "low to high" and "high to low" in the simulation, you should notice the LED kind of giving a dimming effect.

## Period and Frequency
Period is the total time for one on-off cycle to complete.

Expand All @@ -121,31 +141,6 @@ For example, if the period is 20ms(0.02s), the frequency will be 50Hz.
\\]


## PWM Simulation

Here is the interactive simulation. Use the sliders to adjust the duty cycle and frequency, and watch how the pulse width and LED brightness change.


<canvas id="pwmCanvas" width="800" height="200"></canvas>
<div class="led-container">
<div class="led-body" id="ledBody"></div>
<div class="led-pin anode"></div>
<div class="led-pin cathode"></div>
</div>

<div class="slider-container">
<label for="dutyCycle">Duty Cycle (%): </label>
<input type="range" id="dutyCycle" min="0" max="100" value="50">
<span id="dutyCycleValue">50</span>%
</div>
<div class="slider-container">
<label for="frequency">Frequency (Hz): </label>
<input type="range" id="frequency" min="1" max="50" value="10">
<!-- <span id="frequencyValue">x</span> Hz -->
</div>



**Calculating Cycle count from Frequency per second**

The Formula to calculate cycle count:
Expand All @@ -155,6 +150,7 @@ The Formula to calculate cycle count:

If a PWM signal has a frequency of 50Hz, it means it completes 50 cycles in one second.


<script>
const pwmCanvas = document.getElementById('pwmCanvas');
const pwmCtx = pwmCanvas.getContext('2d');
Expand Down
207 changes: 207 additions & 0 deletions src/core-concepts/pwm/pwm-in-depth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
# PWM in Depth

<style>
canvas {
border: 1px solid #000;
margin-top: 20px;
}
.controls {
margin: 20px;
}
.control {
margin: 10px 0;
}
#pwmCanvas {
background-color: #fefefe;
}
#timerCanvas {
background-color: #fefefe;
}
#simulation-div{
background-color: #fefefe;
text-align: center;

}
</style>

## Timer Operation
The timer plays a key role in the PWM generator. It counts from zero to a specified maximum value (stored in a register), then resets and starts the cycle over. This counting process determines the duration of one complete cycle, called the period.


## Compare Value
The timer's hardware compares its current count with a compare value (stored in a register). When the count is less than the compare value, the signal stays high; when the count exceeds the compare value, the signal goes low.


## PWM Resolution
In PWM (Pulse Width Modulation), resolution refers to how precisely the duty cycle can be controlled. This is determined by the number of bits used in the PWM's compare register.

The timer counts from 0 to a maximum value based on the resolution. The higher the resolution, the more finely the duty cycle can be adjusted.

For a system with **n** bits of resolution, the timer can count from 0 to \\(2^n - 1\\), which gives \\(2^n\\) possible levels for the duty cycle.

For example:
- 8-bit resolution allows the timer to count from 0 to 255, providing 256 possible duty cycle levels.
- 10-bit resolution allows the timer to count from 0 to 1023, providing 1024 possible duty cycle levels.

Higher resolution gives more precise control over the duty cycle but also means the timer must count more values within the same period, which could lower the frequency or require more processing power. Essentially, the resolution defines how many distinct duty cycle values can be set, with more bits offering finer adjustments.


## Simulation

You can modify the PWM resolution bits and duty cycle in this simulation. Adjusting the PWM resolution bits increases the maximum count but remains within the time period (it does not affect the duty cycle). Changing the duty cycle adjusts the on and off states accordingly, but it also stays within the period.

<div class="controls">
<div class="control">
<label for="resolution">PWM Resolution (Bits): </label>
<input type="range" id="resolution" min="4" max="20" value="8">
<input type="number" id="resolutionNumber" min="4" max="20" value="8">
</div>
<div class="control">
<label for="dutyCycle">Duty Cycle (%): </label>
<input type="range" id="dutyCycle" min="0" max="100" value="75">
<input type="number" id="dutyCycleNumber" min="0" max="100" value="75">
</div>
</div>
<div id='simulation-div'>
<canvas id="timerCanvas" width="800" height="200"></canvas>
<canvas id="pwmCanvas" width="800" height="150"></canvas>
</div>

## Relationship Between Duty Cycle, Frequency, and Resolution

This diagram illustrates the relationship between duty cycle, frequency, period, pulse width, and resolution. While it may seem a bit complex at first glance, breaking it down helps to clarify these concepts.

In this example, the timer resolution is 4 bits, meaning the timer counts from 0 to 15. When the timer reaches its maximum value, an overflow interrupt is triggered (indicated by the blue arrow), and the counter resets to 0. The time it takes for the timer to count from 0 to its maximum value is called as "period".

<img style="display: block; margin: auto;" alt="PWM" src="../images/pwm-duty-cycle-timer.jpg"/>

The duty cycle is configured to 50%, meaning the signal remains high for half the period. At each step in the counting process, the timer compares its current count with the duty cycle's compare value. When the timer count exceeds this compare value (marked by the yellow arrow), the signal transitions from high to low. This triggers the compare interrupt, signaling the state change.

The time during which the signal is high is referred to as the pulse width.

<script>
const timerCanvas = document.getElementById('timerCanvas');
const timerCtx = timerCanvas.getContext('2d');

const pwmCanvas = document.getElementById('pwmCanvas');
const pwmCtx = pwmCanvas.getContext('2d');

const resolutionInput = document.getElementById('resolution');
const resolutionNumber = document.getElementById('resolutionNumber');
const dutyCycleInput = document.getElementById('dutyCycle');
const dutyCycleNumber = document.getElementById('dutyCycleNumber');

const numCountLabels = 1;

function drawTimerAndPWM(resolution, dutyCycle) {
const maxTicks = Math.pow(2, resolution) - 1;
const highTicks = Math.round(maxTicks * (dutyCycle / 100));
const periodWidth = pwmCanvas.width / 10;

timerCtx.clearRect(0, 0, timerCanvas.width, timerCanvas.height);
pwmCtx.clearRect(0, 0, pwmCanvas.width, pwmCanvas.height);

timerCtx.beginPath();
const stepsToDraw = Math.min(800, maxTicks);
const stepIncrement = Math.ceil(maxTicks / stepsToDraw);
for (let period = 0; period < 10; period++) {
const startX = period * periodWidth;
for (let tick = 0; tick <= maxTicks; tick += stepIncrement) {
const x1 = startX + (tick / maxTicks) * periodWidth;
const y1 = timerCanvas.height - (tick / maxTicks) * (timerCanvas.height - 100);
const y2 = timerCanvas.height - ((tick + stepIncrement) / maxTicks) * (timerCanvas.height - 100);
timerCtx.lineTo(x1, y1);
if (tick + stepIncrement <= maxTicks) {
timerCtx.lineTo(x1, y2);
}
}
}
timerCtx.strokeStyle = 'blue';
timerCtx.lineWidth = 2;
timerCtx.stroke();

timerCtx.font = '12px Arial';
timerCtx.fillStyle = 'black';
let labelValues = [];
if (numCountLabels == 1) {
labelValues = [0, maxTicks];
} else if (numCountLabels == 2) {
labelValues = [0, Math.round(maxTicks / 2), maxTicks];
} else {
labelValues = [0, Math.round(maxTicks / 3), Math.round(2 * maxTicks / 3), maxTicks];
}

for (let i = 0; i < labelValues.length; i++) {
const y = timerCanvas.height - (labelValues[i] / maxTicks) * (timerCanvas.height - 100);
timerCtx.fillText(Math.round(labelValues[i]), 5, y);
}

const dutyCycleY = timerCanvas.height - (highTicks / maxTicks) * (timerCanvas.height - 100);
timerCtx.beginPath();
timerCtx.moveTo(0, dutyCycleY);
timerCtx.lineTo(timerCanvas.width, dutyCycleY);
timerCtx.strokeStyle = 'red';
timerCtx.lineWidth = 1;
timerCtx.stroke();

const maxValueY = timerCanvas.height - (maxTicks / maxTicks) * (timerCanvas.height - 100);
timerCtx.beginPath();
timerCtx.moveTo(0, maxValueY);
timerCtx.lineTo(timerCanvas.width, maxValueY);
timerCtx.strokeStyle = 'gray';
timerCtx.lineWidth = 1;
timerCtx.setLineDash([5, 5]);
timerCtx.stroke();
timerCtx.setLineDash([]);

pwmCtx.beginPath();
for (let period = 0; period < 10; period++) {
const startX = period * periodWidth;
pwmCtx.moveTo(startX, pwmCanvas.height / 2);
pwmCtx.lineTo(startX + (dutyCycle / 100) * periodWidth, pwmCanvas.height / 2);
pwmCtx.lineTo(startX + (dutyCycle / 100) * periodWidth, pwmCanvas.height - 20);
pwmCtx.lineTo(startX + periodWidth, pwmCanvas.height - 20);
pwmCtx.lineTo(startX + periodWidth, pwmCanvas.height / 2);
}
pwmCtx.strokeStyle = 'green';
pwmCtx.lineWidth = 2;
pwmCtx.stroke();

timerCtx.font = '16px Arial';
timerCtx.fillStyle = 'black';
timerCtx.fillText(`Resolution: ${resolution} bits`, 10, 20);
timerCtx.fillText(`Max Timer Count: ${maxTicks}`, 10, 40);

pwmCtx.font = '16px Arial';
pwmCtx.fillStyle = 'black';
pwmCtx.fillText(`Duty Cycle: ${dutyCycle}%`, 10, 20);
}

function updateSimulation() {
const resolution = parseInt(resolutionInput.value);
const dutyCycle = parseInt(dutyCycleInput.value);
drawTimerAndPWM(resolution, dutyCycle);
}

resolutionInput.addEventListener('input', () => {
resolutionNumber.value = resolutionInput.value;
updateSimulation();
});

resolutionNumber.addEventListener('input', () => {
resolutionInput.value = resolutionNumber.value;
updateSimulation();
});

dutyCycleInput.addEventListener('input', () => {
dutyCycleNumber.value = dutyCycleInput.value;
updateSimulation();
});

dutyCycleNumber.addEventListener('input', () => {
dutyCycleInput.value = dutyCycleNumber.value;
updateSimulation();
});

updateSimulation();
</script>

0 comments on commit 2afae85

Please sign in to comment.