Skip to content

Commit 7014c04

Browse files
committed
add pwm fixes
1 parent 520e138 commit 7014c04

22 files changed

+247
-287
lines changed

ports/atmel-samd/common-hal/pwmio/PWMOut.c

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,8 @@ extern void common_hal_pwmio_pwmout_set_duty_cycle(pwmio_pwmout_obj_t *self, uin
249249
// Track it here so that if frequency is changed we can use this value to recalculate the
250250
// proper duty cycle.
251251
// See https://github.com/adafruit/circuitpython/issues/2086 for more details
252-
self->duty_cycle = duty;
253252

253+
self->duty_cycle = duty;
254254
const pin_timer_t *t = self->timer;
255255
if (t->is_tc) {
256256
uint16_t adjusted_duty = tc_periods[t->index] * duty / 0xffff;
@@ -271,8 +271,6 @@ extern void common_hal_pwmio_pwmout_set_duty_cycle(pwmio_pwmout_obj_t *self, uin
271271
}
272272
uint8_t channel = tcc_channel(t);
273273
Tcc *tcc = tcc_insts[t->index];
274-
// Enable double-buffering
275-
tcc->CTRLBCLR.bit.LUPD = 1;
276274
#ifdef SAMD21
277275
tcc->CCB[channel].reg = adjusted_duty;
278276
#endif

tools/pwm/README.md

Lines changed: 16 additions & 267 deletions
Large diffs are not rendered by default.

tools/pwm/code.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
import sys
88
import random
99

10-
cr10 = 10 ** (1 / 3)
11-
1210
exponents = [
1311
2,
1412
2.333,
@@ -27,15 +25,15 @@
2725
6.667,
2826
7,
2927
]
28+
3029
freqs = [int(10**f) for f in exponents]
3130

3231
top = 65536
3332
den = 10
3433
duties = [int(top * num / den) for num in range(1, den)]
3534
duties = [1, 65534, 1] + duties
36-
3735
freq_duration = 1.0
38-
duty_duration = 0.00000001
36+
duty_duration = 0.000000001
3937

4038
print("\n\n")
4139
board_name = sys.implementation[2]
@@ -45,6 +43,7 @@
4543
"Grand Central": (("D51", "TCC"), ("A15", "TC")),
4644
"Metro M0": (("A2", "TC"), ("A3", "TCC")),
4745
"ESP32-S3-DevKit": (("IO6", ""),), # marked D5 on board for XIAO-ESP32-s3
46+
"Feather ESP32-S2": (("D9", ""),),
4847
"XIAO nRF52840": (("D9", ""),),
4948
}
5049

tools/pwm/duty.py

Lines changed: 132 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,135 @@
1-
def duty(filename):
1+
import math
2+
import matplotlib.pyplot as plt
3+
import numpy as np
4+
from PIL import Image
5+
from PIL import Image
6+
from PIL import ImageFont
7+
from PIL import ImageDraw
8+
9+
10+
def read(
11+
filename,
12+
image_filename=None,
13+
height=480,
14+
width=640,
15+
f_min=10,
16+
f_max=1e8,
17+
title="",
18+
subtitle="",
19+
version="",
20+
duty_labels=(0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9),
21+
freq_calls=tuple(),
22+
margin=0.01,
23+
duty_color=(255, 0, 0),
24+
freq_color=(0, 255, 0),
25+
calls_color=(0, 255, 255),
26+
title_color=(255, 255, 255),
27+
):
28+
"""Read a one channel logic analyzer raw csv data file and generate a plot visualizing the PWM signal
29+
captured in the file. Each line of the file is a <time, level> pair indicating the times (in seconds)
30+
at which the signal transitioned to that level. For example:
31+
1.726313020,0
32+
1.726313052,1
33+
1.726313068,0
34+
1.727328804,1
35+
"""
36+
left = 80
37+
right = 80
38+
bottom = 20
39+
top = 60
40+
x0 = left
41+
y0 = top
42+
y1 = height - bottom
43+
x1 = width - right
244
rising_edge = None
345
falling_edge = None
46+
pixels = np.zeros((height, width, 3), dtype=np.uint8) * 255
47+
t0 = None
48+
t1 = None
49+
val = None
450
with open(filename, "r") as f:
5-
line = f.readline()
6-
t, val = line.split(",")
7-
t = float(t)
8-
val = int(val)
9-
if val == 0:
10-
if falling_edge and rising_edge is not None:
11-
period = t - falling_edge
12-
duty_cycle = (t - rising_edge) / period
13-
print(t, val, period, duty_cycle)
14-
falling_edge = t
15-
else:
16-
rising_edge = t
51+
first = True
52+
for line in f: # find min and max t, excluding first and last values
53+
if val is not None:
54+
if not first:
55+
if t0 is None or t < t0:
56+
t0 = t
57+
if t1 is None or t > t1:
58+
t1 = t
59+
else:
60+
first = False
61+
t, val = line.split(",")
62+
try:
63+
t = float(t)
64+
val = int(val)
65+
except ValueError:
66+
val = None
67+
print("plotting", t1 - t0, "seconds")
68+
69+
with open(filename, "r") as f:
70+
pts = 0
71+
f_log_max = int(math.log10(f_max))
72+
f_log_min = int(math.log10(f_min))
73+
f_log_span = f_log_max - f_log_min
74+
for line in f:
75+
t, val = line.split(",")
76+
try:
77+
t = float(t)
78+
val = int(val)
79+
except ValueError:
80+
val = None
81+
if val == 1:
82+
if falling_edge is not None and rising_edge is not None:
83+
period = t - rising_edge
84+
frequency = 1 / period
85+
duty_cycle = (falling_edge - rising_edge) / period
86+
x = int((x1 - x0) * (t - t0) / (t1 - t0)) + x0
87+
y_duty = int((1 - duty_cycle) * (y1 - y0)) + y0
88+
y_freq = (
89+
int((y1 - y0) * (1 - (math.log10(frequency) - f_log_min) / f_log_span))
90+
+ y0
91+
)
92+
x = max(x0, min(x, x1 - 1))
93+
y_duty = max(y0, min(y_duty, y1 - 1))
94+
y_freq = max(y0, min(y_freq, y1 - 1))
95+
pixels[y_duty, x] = duty_color
96+
pixels[y_freq, x] = freq_color
97+
pts += 1
98+
rising_edge = t
99+
elif val == 0:
100+
falling_edge = t
101+
image = Image.fromarray(pixels)
102+
draw = ImageDraw.Draw(image)
103+
draw.text((left - 10, top), "Duty", duty_color, anchor="rt")
104+
draw.text((0, top), "Calls", calls_color, anchor="lt")
105+
draw.text((width - right / 2, top), "Freq", freq_color, anchor="mt")
106+
107+
for duty in duty_labels:
108+
draw.text(
109+
(left - 10, y0 + (y1 - y0) * (1 - duty)),
110+
f"{int(100*duty):d}%",
111+
duty_color,
112+
anchor="rm",
113+
)
114+
for exponent in range(f_log_min + 1, f_log_max):
115+
draw.text(
116+
(width - right / 2, y0 + (y1 - y0) * (1 - (exponent - f_log_min) / (f_log_span))),
117+
str(10**exponent) + " Hz",
118+
freq_color,
119+
anchor="mm",
120+
)
121+
for freq, count in freq_calls:
122+
draw.text(
123+
(0, y0 + (y1 - y0) * (1 - (math.log10(freq) - f_log_min) / (f_log_span))),
124+
f"{count} Hz",
125+
calls_color,
126+
anchor="lm",
127+
)
128+
subtitle += f", showing {pts} PWM cycles"
129+
draw.text((width * 0.5, height * margin), title, title_color, anchor="mm")
130+
draw.text((width * 0.5, height * 4 * margin), version, title_color, anchor="mm")
131+
draw.text((width * 0.5, height * 8 * margin), subtitle, title_color, anchor="mm")
132+
image.show()
133+
if image_filename is not None:
134+
image.save(image_filename)
135+
return image

tools/pwm/pr.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Fixing API delays and rounding in samd PWM
2+
3+
Blocking delays are observed when setting `duty_cycle` in the samd port of CircuitPython (see https://github.com/adafruit/circuitpython/issues/7653), behavior which interferes with some applications of PWM, and which differs from from that of other ports. Here is a comparison of execution times of several basic operations on three different ports, showing the off-the-charts slowness of samd in setting PWM duty cycle:
4+
5+
![](samd51_benchmarks.png)
6+
7+
Investigation of this issue revealed another problem. In some cases, the API will round a non-zero duty cycle down to zero, causing the PWM signal to stop cycling at all. The cartoon below shows how for instance rounding can transform three PWM cycles, with the first almost 100% duty and the second two close to 0% duty, into one PWM cycle with three times the period and ~33% duty:
8+
9+
![](pwm_rounding.png)
10+
11+
The changes proposed in this PR address both of these issues.
12+
13+
## Implementation
14+
15+
The CP samd21/51 port serves two different processor families, each of which have two different types of PWM peripherals (TC and TCC). Moreover, the TC peripheral for the samd21 is not identical to that of the samd51, as is hinted at by the datasheet excerpts below. The description of TC for the samd51 notably adds mention of double-buffering, while the samd21/51 descriptions for TCC are identical:
16+
17+
<details>
18+
<summary>Datasheet summaries of samd21 and samd51 TC and TCC peripherals</summary>
19+
20+
![](samd_tc_tcc_datasheet_blurbs.png)
21+
22+
</details>
23+
24+
Conditional compilations and `if` statements in `PWMOUT.c` provide separate handling for each of the four combinations of samd21/samd51 and TC/TCC. The code modifications consist of (1) removing synchronization delays and PWM configuration which were found to do more harm than good, and (2) adding `if` statements to disallow rounding down to zero.
25+
26+
## Testing
27+
28+
All combinations of samd21/samd51 and TC/TCC were tested separately by selecting board pins which support only one or the other of TC or TCC. The boards and pins used (outlined in red) are shown in the diagram below:
29+
30+
<details>
31+
<summary>Pins implementing TC and TCC on samd21/51</summary>
32+
33+
![](samd21_51_board_pinouts.png)
34+
35+
</details>
36+
37+
Data gathered from each of the four types of PWM peripheral before and after code modifications are shown below. Details of the test tools and procedure are [here](/tools/pwm/README.md).
38+
39+
### samd51 TCC
40+
41+
| Before | After |
42+
| ----------- | ---------- |
43+
| ![](ramps_samd51_tcc_910.png) | ![](ramps_samd51_tcc_910_fixed.png) |
44+
45+
In the "before" plot, the variation with frequency of call throughput (shown in cyan) and PWM ramp slope points out the frequency-dependent delay found when setting duty cycle. The clutter in duty cycle and frequency measurements results from zero-rounding. Neither is seen in the "after" plot - the PWM ramp slope is nearly constant, and duty cycle and frequency measurements contain no outliers other than at frequency transitions.
46+
47+
### samd51 TC
48+
49+
| Before | After |
50+
| ----------- | ---------- |
51+
| ![](ramps_samd51_tc_910.png) | ![](ramps_samd51_tc_910_fixed.png) |
52+
53+
These results are very similar to those for TCC. Both delay and rounding artifacts are seen in the "before" plot and not in the "after" plot.
54+
55+
### samd21 TCC
56+
57+
| Before | After |
58+
| ----------- | ---------- |
59+
| ![](ramps_samd21_tcc_910.png) | ![](ramps_samd21_tcc_910_fixed.png) |
60+
61+
No frequency-dependent delay is seen for samd21 TCC, only rounding artifacts. Compared to samd51, call throughput is ~4x slower (as would be expected for samd21's lower clock rate). No rounding artifacts are seen in the "after" plot.
62+
63+
### samd21 TC
64+
65+
| Before | After |
66+
| ----------- | ---------- |
67+
| ![](ramps_samd21_tc_910.png) | ![](ramps_samd21_tc_910_fixed.png) |
68+
69+
So, this is the problem child. samd21 TC does not provide double buffering of duty cycle registers, so writing to registers may or may not induce duty cycle artifacts depending on when exactly the write happens. My takeaway regarding samd21 TC is, don't expect too much of it, but it's fine for less demanding applications. Here is samd21 tc updating once a second just fine:
70+
71+
![](slow_samd21_tc_910_fixed.png)
72+
73+
High-rate samd21 TC should work well if updated in a DMA/ISR framework like that implemented in `audiopwmio` in other ports. A logical next step for samd21/51 is to implement `audiopwmio`.
74+
75+
## Other ports
76+
77+
This PR describes PWM fix-ups for samd21/51, but a quick look indicates that other ports could use some attention as well. Here is some test data for three other ports.
78+
79+
### CircuitPython 9.04 on RP2040
80+
81+
<img src="ramps_rp2040_904.png" />
82+
83+
This looks pretty good except it shows the same rounding down to zero issue as samd51. Probably all ports have this.
84+
85+
### CircuitPython 9.01 on nRF52840
86+
87+
<img src="ramps_nrf52840_901.png" />
88+
89+
Similar to above, looks pretty good except for rounding to zero. Tops out below 5 MHz due to lower clock speed.
90+
91+
### CircuitPython 9.05 on ESP32
92+
93+
<img src="ramps_esp32s2_905.png" />
94+
95+
This is more complicated - looks like frequency tops out at 10 kHz, but duty cycle continues to be adjusted downward as if frequency has increased. Needs investigation.

tools/pwm/pwm_plot_explainer.png

5.25 KB
Loading

tools/pwm/pwm_rounding.png

30.8 KB
Loading

tools/pwm/ramps_esp32s2_905.png

21.9 KB
Loading

tools/pwm/ramps_nrf52840_901.png

25.7 KB
Loading

tools/pwm/ramps_rp2040_904.png

28.4 KB
Loading

0 commit comments

Comments
 (0)