forked from micropython/micropython
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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>
- Loading branch information
Showing
1 changed file
with
166 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
# Test machine.PWM, frequncy and duty cycle (using machine.time_pulse_us). | ||
# | ||
# IMPORTANT: This test requires hardware connections: the PWM-output and pulse-input | ||
# pins must be wired together (see the variable `pwm_pulse_pins`). | ||
|
||
import sys | ||
import time | ||
|
||
try: | ||
from machine import time_pulse_us, Pin, PWM | ||
except ImportError: | ||
print("SKIP") | ||
raise SystemExit | ||
|
||
import unittest | ||
|
||
pwm_freq_limit = 1000000 | ||
freq_margin_per_thousand = 0 | ||
duty_margin_per_thousand = 0 | ||
timing_margin_us = 5 | ||
|
||
# Configure pins based on the target. | ||
if "esp32" in sys.platform: | ||
pwm_pulse_pins = ((4, 5),) | ||
freq_margin_per_thousand = 2 | ||
duty_margin_per_thousand = 1 | ||
timing_margin_us = 20 | ||
elif "esp8266" in sys.platform: | ||
pwm_pulse_pins = ((4, 5),) | ||
pwm_freq_limit = 1_000 | ||
duty_margin_per_thousand = 3 | ||
timing_margin_us = 50 | ||
elif "mimxrt" in sys.platform: | ||
if "Teensy" in sys.implementation._machine: | ||
# Teensy 4.x | ||
pwm_pulse_pins = ( | ||
("D0", "D1"), # FLEXPWM X and UART 1 | ||
("D2", "D3"), # FLEXPWM A/B | ||
("D11", "D12"), # QTMR and MOSI/MISO of SPI 0 | ||
) | ||
else: | ||
pwm_pulse_pins = (("D0", "D1"),) | ||
elif "rp2" in sys.platform: | ||
pwm_pulse_pins = (("GPIO0", "GPIO1"),) | ||
elif "samd" in sys.platform: | ||
pwm_pulse_pins = (("D0", "D1"),) | ||
if "SAMD21" in sys.implementation._machine: | ||
# MCU is too slow to capture short pulses. | ||
pwm_freq_limit = 2_000 | ||
else: | ||
print("Please add support for this test on this platform.") | ||
raise SystemExit | ||
|
||
|
||
# Test a specific frequency and duty cycle. | ||
def _test_freq_duty(self, pulse_in, pwm, freq, duty_u16): | ||
print("freq={:<5} duty_u16={:<5} :".format(freq, duty_u16), end="") | ||
|
||
# Check configured freq/duty_u16 is within error bound. | ||
freq_error = abs(pwm.freq() - freq) * 1000 // freq | ||
duty_error = abs(pwm.duty_u16() - duty_u16) * 1000 // (duty_u16 or 1) | ||
print(" freq={} freq_er={}".format(pwm.freq(), freq_error), end="") | ||
print(" duty={} duty_er={}".format(pwm.duty_u16(), duty_error), end="") | ||
print(" :", end="") | ||
self.assertLessEqual(freq_error, freq_margin_per_thousand) | ||
self.assertLessEqual(duty_error, duty_margin_per_thousand) | ||
|
||
# Calculate expected timing. | ||
expected_total_us = 1_000_000 // freq | ||
expected_high_us = expected_total_us * duty_u16 // 65535 | ||
expected_low_us = expected_total_us - expected_high_us | ||
expected_us = (expected_low_us, expected_high_us) | ||
timeout = 2 * expected_total_us | ||
|
||
# Wait for output to settle. | ||
time_pulse_us(pulse_in, 0, timeout) | ||
time_pulse_us(pulse_in, 1, timeout) | ||
|
||
if duty_u16 == 0 or duty_u16 == 65535: | ||
# Expect a constant output level. | ||
no_pulse = ( | ||
time_pulse_us(pulse_in, 0, timeout) < 0 and time_pulse_us(pulse_in, 1, timeout) < 0 | ||
) | ||
self.assertTrue(no_pulse) | ||
if expected_high_us == 0: | ||
# Expect a constant low level. | ||
self.assertEqual(pulse_in(), 0) | ||
else: | ||
# Expect a constant high level. | ||
self.assertEqual(pulse_in(), 1) | ||
else: | ||
# Test timing of low and high pulse. | ||
n_averaging = 10 | ||
for level in (0, 1): | ||
t = 0 | ||
time_pulse_us(pulse_in, level, timeout) | ||
for _ in range(n_averaging): | ||
t += time_pulse_us(pulse_in, level, timeout) | ||
t //= n_averaging | ||
expected = expected_us[level] | ||
print(" level={} timing_er={}".format(level, abs(t - expected)), end="") | ||
self.assertLessEqual(abs(t - expected), timing_margin_us) | ||
|
||
print() | ||
|
||
|
||
# Test a specific frequency with multiple duty cycles. | ||
def _test_freq(self, freq): | ||
print() | ||
self.pwm.freq(freq) | ||
for duty in (0, 10, 25, 50, 75, 90, 100): | ||
duty_u16 = duty * 65535 // 100 | ||
if sys.platform == "esp32": | ||
# TODO why is this bit needed to get it working on esp32? | ||
self.pwm.init(freq=freq, duty_u16=duty_u16) | ||
time.sleep(0.1) | ||
self.pwm.duty_u16(duty_u16) | ||
_test_freq_duty(self, self.pulse_in, self.pwm, freq, duty_u16) | ||
|
||
|
||
# Given a set of pins, this test class will test multiple frequencies and duty cycles. | ||
class TestBase: | ||
@classmethod | ||
def setUpClass(cls): | ||
print("set up pins:", cls.pwm_pin, cls.pulse_pin) | ||
cls.pwm = PWM(cls.pwm_pin) | ||
cls.pulse_in = Pin(cls.pulse_pin, Pin.IN) | ||
|
||
@classmethod | ||
def tearDownClass(cls): | ||
cls.pwm.deinit() | ||
|
||
def test_freq_50(self): | ||
_test_freq(self, 50) | ||
|
||
def test_freq_100(self): | ||
_test_freq(self, 100) | ||
|
||
def test_freq_500(self): | ||
_test_freq(self, 500) | ||
|
||
def test_freq_1000(self): | ||
_test_freq(self, 1000) | ||
|
||
@unittest.skipIf(pwm_freq_limit < 2000, "frequency too high") | ||
def test_freq_2000(self): | ||
_test_freq(self, 2000) | ||
|
||
@unittest.skipIf(pwm_freq_limit < 5000, "frequency too high") | ||
def test_freq_5000(self): | ||
_test_freq(self, 5000) | ||
|
||
@unittest.skipIf(pwm_freq_limit < 10000, "frequency too high") | ||
def test_freq_10000(self): | ||
_test_freq(self, 10000) | ||
|
||
|
||
# Generate test classes, one for each set of pins to test. | ||
for pwm, pulse in pwm_pulse_pins: | ||
cls_name = "Test_{}_{}".format(pwm, pulse) | ||
globals()[cls_name] = type( | ||
cls_name, (TestBase, unittest.TestCase), {"pwm_pin": pwm, "pulse_pin": pulse} | ||
) | ||
|
||
if __name__ == "__main__": | ||
unittest.main() |