Skip to content

Commit bdda91f

Browse files
committed
tests/extmod_hardware: Add a test for machine.PWM freq and duty.
This adds a hardware test for `machine.PWM`. It requires a jumper wire between two pins, uses `machine.PWM` to output on one of them, and `machine.time_pulse_us()` to time the PWM on the other pin (some boards test more than one pair of pins). It times both the high and low duty cycle (and hence the frequency) for a range of PWM frequencies and duty cycles (including full on and full off). Currently supported on: - esp32 (needs a minor hack for initialisation, and some tests still fail) - esp8266 (passes for frequencies 1kHz and less) - mimxrt / Teensy 4.0 (passes) - rp2 (passes) - samd21 (passes for frequencies 2kHz and less) Signed-off-by: Damien George <damien@micropython.org>
1 parent 94343e2 commit bdda91f

File tree

1 file changed

+166
-0
lines changed

1 file changed

+166
-0
lines changed

tests/extmod_hardware/machine_pwm.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# Test machine.PWM, frequncy and duty cycle (using machine.time_pulse_us).
2+
#
3+
# IMPORTANT: This test requires hardware connections: the PWM-output and pulse-input
4+
# pins must be wired together (see the variable `pwm_pulse_pins`).
5+
6+
import sys
7+
import time
8+
9+
try:
10+
from machine import time_pulse_us, Pin, PWM
11+
except ImportError:
12+
print("SKIP")
13+
raise SystemExit
14+
15+
import unittest
16+
17+
pwm_freq_limit = 1000000
18+
freq_margin_per_thousand = 0
19+
duty_margin_per_thousand = 0
20+
timing_margin_us = 5
21+
22+
# Configure pins based on the target.
23+
if "esp32" in sys.platform:
24+
pwm_pulse_pins = ((4, 5),)
25+
freq_margin_per_thousand = 2
26+
duty_margin_per_thousand = 1
27+
timing_margin_us = 20
28+
elif "esp8266" in sys.platform:
29+
pwm_pulse_pins = ((4, 5),)
30+
pwm_freq_limit = 1_000
31+
duty_margin_per_thousand = 3
32+
timing_margin_us = 50
33+
elif "mimxrt" in sys.platform:
34+
if "Teensy" in sys.implementation._machine:
35+
# Teensy 4.x
36+
pwm_pulse_pins = (
37+
("D0", "D1"), # FLEXPWM X and UART 1
38+
("D2", "D3"), # FLEXPWM A/B
39+
("D11", "D12"), # QTMR and MOSI/MISO of SPI 0
40+
)
41+
else:
42+
pwm_pulse_pins = (("D0", "D1"),)
43+
elif "rp2" in sys.platform:
44+
pwm_pulse_pins = (("GPIO0", "GPIO1"),)
45+
elif "samd" in sys.platform:
46+
pwm_pulse_pins = (("D0", "D1"),)
47+
if "SAMD21" in sys.implementation._machine:
48+
# MCU is too slow to capture short pulses.
49+
pwm_freq_limit = 2_000
50+
else:
51+
print("Please add support for this test on this platform.")
52+
raise SystemExit
53+
54+
55+
# Test a specific frequency and duty cycle.
56+
def _test_freq_duty(self, pulse_in, pwm, freq, duty_u16):
57+
print("freq={:<5} duty_u16={:<5} :".format(freq, duty_u16), end="")
58+
59+
# Check configured freq/duty_u16 is within error bound.
60+
freq_error = abs(pwm.freq() - freq) * 1000 // freq
61+
duty_error = abs(pwm.duty_u16() - duty_u16) * 1000 // (duty_u16 or 1)
62+
print(" freq={} freq_er={}".format(pwm.freq(), freq_error), end="")
63+
print(" duty={} duty_er={}".format(pwm.duty_u16(), duty_error), end="")
64+
print(" :", end="")
65+
self.assertLessEqual(freq_error, freq_margin_per_thousand)
66+
self.assertLessEqual(duty_error, duty_margin_per_thousand)
67+
68+
# Calculate expected timing.
69+
expected_total_us = 1_000_000 // freq
70+
expected_high_us = expected_total_us * duty_u16 // 65535
71+
expected_low_us = expected_total_us - expected_high_us
72+
expected_us = (expected_low_us, expected_high_us)
73+
timeout = 2 * expected_total_us
74+
75+
# Wait for output to settle.
76+
time_pulse_us(pulse_in, 0, timeout)
77+
time_pulse_us(pulse_in, 1, timeout)
78+
79+
if duty_u16 == 0 or duty_u16 == 65535:
80+
# Expect a constant output level.
81+
no_pulse = (
82+
time_pulse_us(pulse_in, 0, timeout) < 0 and time_pulse_us(pulse_in, 1, timeout) < 0
83+
)
84+
self.assertTrue(no_pulse)
85+
if expected_high_us == 0:
86+
# Expect a constant low level.
87+
self.assertEqual(pulse_in(), 0)
88+
else:
89+
# Expect a constant high level.
90+
self.assertEqual(pulse_in(), 1)
91+
else:
92+
# Test timing of low and high pulse.
93+
n_averaging = 10
94+
for level in (0, 1):
95+
t = 0
96+
time_pulse_us(pulse_in, level, timeout)
97+
for _ in range(n_averaging):
98+
t += time_pulse_us(pulse_in, level, timeout)
99+
t //= n_averaging
100+
expected = expected_us[level]
101+
print(" level={} timing_er={}".format(level, abs(t - expected)), end="")
102+
self.assertLessEqual(abs(t - expected), timing_margin_us)
103+
104+
print()
105+
106+
107+
# Test a specific frequency with multiple duty cycles.
108+
def _test_freq(self, freq):
109+
print()
110+
self.pwm.freq(freq)
111+
for duty in (0, 10, 25, 50, 75, 90, 100):
112+
duty_u16 = duty * 65535 // 100
113+
if sys.platform == "esp32":
114+
# TODO why is this bit needed to get it working on esp32?
115+
self.pwm.init(freq=freq, duty_u16=duty_u16)
116+
time.sleep(0.1)
117+
self.pwm.duty_u16(duty_u16)
118+
_test_freq_duty(self, self.pulse_in, self.pwm, freq, duty_u16)
119+
120+
121+
# Given a set of pins, this test class will test multiple frequencies and duty cycles.
122+
class TestBase:
123+
@classmethod
124+
def setUpClass(cls):
125+
print("set up pins:", cls.pwm_pin, cls.pulse_pin)
126+
cls.pwm = PWM(cls.pwm_pin)
127+
cls.pulse_in = Pin(cls.pulse_pin, Pin.IN)
128+
129+
@classmethod
130+
def tearDownClass(cls):
131+
cls.pwm.deinit()
132+
133+
def test_freq_50(self):
134+
_test_freq(self, 50)
135+
136+
def test_freq_100(self):
137+
_test_freq(self, 100)
138+
139+
def test_freq_500(self):
140+
_test_freq(self, 500)
141+
142+
def test_freq_1000(self):
143+
_test_freq(self, 1000)
144+
145+
@unittest.skipIf(pwm_freq_limit < 2000, "frequency too high")
146+
def test_freq_2000(self):
147+
_test_freq(self, 2000)
148+
149+
@unittest.skipIf(pwm_freq_limit < 5000, "frequency too high")
150+
def test_freq_5000(self):
151+
_test_freq(self, 5000)
152+
153+
@unittest.skipIf(pwm_freq_limit < 10000, "frequency too high")
154+
def test_freq_10000(self):
155+
_test_freq(self, 10000)
156+
157+
158+
# Generate test classes, one for each set of pins to test.
159+
for pwm, pulse in pwm_pulse_pins:
160+
cls_name = "Test_{}_{}".format(pwm, pulse)
161+
globals()[cls_name] = type(
162+
cls_name, (TestBase, unittest.TestCase), {"pwm_pin": pwm, "pulse_pin": pulse}
163+
)
164+
165+
if __name__ == "__main__":
166+
unittest.main()

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy