summaryrefslogtreecommitdiff
path: root/tests/extmod_hardware/machine_pwm.py
diff options
context:
space:
mode:
authorDamien George <damien@micropython.org>2024-11-04 16:55:16 +1100
committerDamien George <damien@micropython.org>2024-12-11 12:22:15 +1100
commitbdda91fe7414b2ff8d5259a176cc8da8b72a803f (patch)
tree87576727ff0aa6c1509c231f36e388555b651106 /tests/extmod_hardware/machine_pwm.py
parent94343e20e590473ff1e31bd31c654befa42307a7 (diff)
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>
Diffstat (limited to 'tests/extmod_hardware/machine_pwm.py')
-rw-r--r--tests/extmod_hardware/machine_pwm.py166
1 files changed, 166 insertions, 0 deletions
diff --git a/tests/extmod_hardware/machine_pwm.py b/tests/extmod_hardware/machine_pwm.py
new file mode 100644
index 000000000..014030be5
--- /dev/null
+++ b/tests/extmod_hardware/machine_pwm.py
@@ -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()