398 lines
12 KiB
Python
Executable File
398 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# Copyright (C) 2020 Max Regan
|
|
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
# THE SOFTWARE.
|
|
|
|
import pytest
|
|
import serial
|
|
import serial.tools.list_ports
|
|
import logging
|
|
import pylink
|
|
import time
|
|
import os
|
|
import sys
|
|
import re
|
|
import subprocess
|
|
|
|
PYTEST_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
DEFAULT_FW_DIR = os.path.abspath(PYTEST_DIR + "../../../../firmware/")
|
|
SAMPLE_TIME_MS = 2500
|
|
SAMPLES_PER_SEC = 12000000
|
|
DRIVER = "fx2lafw"
|
|
|
|
TEST_START_TEXT = b"TEST_BEGIN\r\n"
|
|
TEST_PASS_TEXT = b"TEST_PASS\r\n"
|
|
TEST_FAIL_TEXT = b"TEST_FAIL\r\n"
|
|
|
|
|
|
@pytest.fixture
|
|
def logger():
|
|
logging.basicConfig()
|
|
return logging.getLogger(__name__)
|
|
|
|
|
|
@pytest.fixture
|
|
def context_factory():
|
|
def create_context(
|
|
fw_rel_path: str,
|
|
mcu: str = "STM32L010C6",
|
|
addr: int = 0x8000000,
|
|
leave_halted: bool = False,
|
|
):
|
|
ports = [
|
|
p
|
|
for p in serial.tools.list_ports.comports()
|
|
if p.product == "FT232R USB UART"
|
|
]
|
|
|
|
if len(ports) == 0:
|
|
raise RuntimeError("No serial devices found")
|
|
if len(ports) > 1:
|
|
raise RuntimeError(
|
|
"Too many serial devices, not sure which to use. "
|
|
"This should be made configurable"
|
|
)
|
|
|
|
jlink = pylink.jlink.JLink()
|
|
jlink.open()
|
|
jlink.disable_dialog_boxes()
|
|
jlink.set_tif(pylink.enums.JLinkInterfaces.SWD)
|
|
jlink.connect(mcu)
|
|
fw = DEFAULT_FW_DIR + "/" + fw_rel_path
|
|
logging.info("Flashing {}...".format(fw))
|
|
jlink.flash_file(fw, addr)
|
|
logging.info("Flashing done.")
|
|
assert jlink.halted()
|
|
jlink.reset(halt=True)
|
|
|
|
serial_dev = serial.Serial(port=ports[0].device, baudrate=115200, timeout=2)
|
|
if leave_halted:
|
|
return serial_dev, jlink
|
|
|
|
# Start test test, and check that it started correctly over serial
|
|
jlink.reset(halt=False)
|
|
while True:
|
|
try:
|
|
logging.info("Waiting for firmware to start...")
|
|
assert serial_dev.read_until(TEST_START_TEXT).endswith(
|
|
TEST_START_TEXT
|
|
), "Timed out starting test firmware application"
|
|
logging.debug("Test execution started")
|
|
except serial.serialutil.SerialException:
|
|
continue
|
|
break
|
|
return serial_dev, jlink
|
|
|
|
return create_context
|
|
|
|
|
|
def measure_frequency(
|
|
period: float,
|
|
pin_name: str,
|
|
executable: str = "sigrok-cli",
|
|
driver_name: str = "fx2lafw",
|
|
trigger: str = "r",
|
|
):
|
|
|
|
cmd = [
|
|
executable,
|
|
"-C",
|
|
pin_name,
|
|
"-d",
|
|
driver_name,
|
|
"-c",
|
|
"samplerate=1M",
|
|
"--time",
|
|
"{}ms".format(int(period * 1000)),
|
|
"-t",
|
|
"{}={}".format(pin_name, trigger),
|
|
"-P",
|
|
"timing:data={}".format(pin_name),
|
|
"-A",
|
|
"timing=time",
|
|
]
|
|
|
|
print("sigrok-cli cmd {}".format(cmd))
|
|
proc = subprocess.run(cmd, capture_output=True, check=True)
|
|
lines = proc.stdout.decode("utf-8").split("\n")
|
|
reg = re.compile(".*:\\W(\\d+.\\d+)\\W(\\w+)")
|
|
periods = []
|
|
for line in lines:
|
|
m = reg.match(line)
|
|
if not m:
|
|
break
|
|
num = float(m.groups(1)[0])
|
|
units = m.groups(1)[1]
|
|
if units == "s":
|
|
periods.append(num)
|
|
elif units == "ms":
|
|
periods.append(num / 1000)
|
|
elif units == "μs":
|
|
periods.append(num / 1000000)
|
|
else:
|
|
assert False, "Couldnt find units in line '{}', units were '{}'".format(
|
|
line, units
|
|
)
|
|
|
|
return periods[::2], periods[1:][::2]
|
|
|
|
|
|
def test_meta_pass(context_factory, logger):
|
|
serial_dev, jlink = context_factory("Test/Apps/pass.bin")
|
|
text = serial_dev.read_until(TEST_PASS_TEXT)
|
|
print("Got serial output: {}".format(text))
|
|
assert text.endswith(TEST_PASS_TEXT)
|
|
|
|
|
|
def test_meta_fail(context_factory, logger):
|
|
serial_dev, jlink = context_factory("Test/Apps/fail.bin")
|
|
text = serial_dev.read_until(TEST_PASS_TEXT)
|
|
print("Got serial output: {}".format(text))
|
|
assert not text.endswith(TEST_PASS_TEXT)
|
|
|
|
|
|
def test_meta_timeout(context_factory, logger):
|
|
serial_dev, jlink = context_factory("Test/Apps/timeout.bin")
|
|
text = serial_dev.read_until(TEST_PASS_TEXT)
|
|
assert not text.endswith(TEST_PASS_TEXT)
|
|
|
|
|
|
def test_meta_nostart(context_factory, logger):
|
|
with pytest.raises(AssertionError):
|
|
serial_dev, jlink = context_factory("Test/Apps/no_start.bin")
|
|
|
|
|
|
def test_watch(context_factory, logger):
|
|
serial_dev, jlink = context_factory("Application/main.bin", leave_halted=True)
|
|
jlink.reset(halt=False)
|
|
|
|
|
|
def test_set_time(context_factory, logger):
|
|
serial_dev, jlink = context_factory("Test/Apps/set_time.bin")
|
|
text = serial_dev.read_until(TEST_PASS_TEXT)
|
|
print("Text:", text.decode())
|
|
assert text.endswith(TEST_PASS_TEXT)
|
|
|
|
|
|
def test_periodic_alarms(context_factory, logger):
|
|
serial_dev, jlink = context_factory("Test/Apps/periodic_alarms.bin")
|
|
serial_dev.timeout = 6
|
|
text = serial_dev.read_until(TEST_PASS_TEXT)
|
|
print("Text:", text.decode())
|
|
assert text.endswith(TEST_PASS_TEXT)
|
|
|
|
|
|
def test_button_slow(context_factory, logger):
|
|
serial_dev, jlink = context_factory("Test/Apps/button.bin")
|
|
serial_dev.timeout = 0.3
|
|
ASSERTED = True
|
|
|
|
serial_dev.dtr = not ASSERTED
|
|
while (line := serial_dev.readline()) is not None and len(line) > 0:
|
|
pass
|
|
|
|
for _ in range(5):
|
|
serial_dev.dtr = ASSERTED
|
|
press_line = serial_dev.readline()
|
|
serial_dev.dtr = not ASSERTED
|
|
release_line = serial_dev.readline()
|
|
assert press_line == b"up:pressed\r\n"
|
|
assert release_line == b"up:released\r\n"
|
|
|
|
|
|
def test_button_fast(context_factory, logger):
|
|
serial_dev, jlink = context_factory("Test/Apps/button.bin")
|
|
serial_dev.timeout = 0.3
|
|
ASSERTED = True
|
|
|
|
serial_dev.dtr = not ASSERTED
|
|
time.sleep(0.3)
|
|
serial_dev.timeout = 0
|
|
while (line := serial_dev.readline()) is not None and len(line) > 0:
|
|
pass
|
|
|
|
for _ in range(25):
|
|
serial_dev.dtr = ASSERTED
|
|
time.sleep(0.075)
|
|
serial_dev.dtr = not ASSERTED
|
|
time.sleep(0.075)
|
|
|
|
serial_dev.timeout = 0.3
|
|
for _ in range(25):
|
|
press_line = serial_dev.readline()
|
|
release_line = serial_dev.readline()
|
|
assert press_line == b"up:pressed\r\n"
|
|
assert release_line == b"up:released\r\n"
|
|
|
|
serial_dev.timeout = 0
|
|
assert serial_dev.readline() == b""
|
|
|
|
|
|
def test_button_lowpower(context_factory, logger):
|
|
serial_dev, jlink = context_factory("Test/Apps/button_lowpower.bin")
|
|
serial_dev.timeout = 0.3
|
|
ASSERTED = True
|
|
|
|
serial_dev.dtr = not ASSERTED
|
|
while (line := serial_dev.readline()) is not None and len(line) > 0:
|
|
pass
|
|
|
|
for _ in range(5):
|
|
serial_dev.dtr = ASSERTED
|
|
press_line = serial_dev.readline()
|
|
serial_dev.dtr = not ASSERTED
|
|
release_line = serial_dev.readline()
|
|
assert press_line == b"up:pressed\r\n"
|
|
assert release_line == b"up:released\r\n"
|
|
|
|
|
|
def test_clock(context_factory, logger):
|
|
serial_dev, jlink = context_factory("Test/Apps/clock.bin")
|
|
EXPECTED_RUNTIME = 10
|
|
TOLERANCE = 0.2
|
|
|
|
serial_dev.timeout = EXPECTED_RUNTIME * 1.2
|
|
|
|
START_MARKER = b"GO\r\n"
|
|
END_MARKER = b"STOP\r\n"
|
|
|
|
start_text = serial_dev.read_until(START_MARKER)
|
|
start = time.monotonic()
|
|
print("Start text:", start_text)
|
|
assert start_text.endswith(START_MARKER)
|
|
|
|
end_text = serial_dev.read_until(END_MARKER)
|
|
end = time.monotonic()
|
|
print("Time:", end - start)
|
|
assert end_text.endswith(END_MARKER)
|
|
|
|
delta = end - start
|
|
logger.info("Serial text: {}".format(start_text))
|
|
logger.info("Serial text: {}".format(end_text))
|
|
logger.info("Start time: {}".format(start))
|
|
logger.info("End time: {}".format(end))
|
|
logger.info("Delta time: {}".format(delta))
|
|
|
|
# TODO: Using a single pin, instead of UART, would make this more
|
|
# accurate. Add support via sigrok.
|
|
assert abs(delta - EXPECTED_RUNTIME) < TOLERANCE
|
|
|
|
|
|
def test_wakeup_irq(context_factory, logger):
|
|
serial_dev, jlink = context_factory("Test/Apps/wakeup_irq.bin")
|
|
serial_dev.timeout = 65
|
|
|
|
pattern = re.compile("Requested=(\\d*) Actual=(\\d*) Wakeups=(\\d*)")
|
|
while True:
|
|
line = serial_dev.readline()
|
|
if line == TEST_PASS_TEXT:
|
|
break
|
|
|
|
line = line.decode("ascii", errors="ignore")
|
|
print(line.strip())
|
|
match = pattern.match(line)
|
|
assert match
|
|
req = int(match.group(1))
|
|
actual = int(match.group(2))
|
|
wakeups = int(match.group(3))
|
|
delta = req - actual
|
|
|
|
assert wakeups == 1, "Expected one wakeup per line of test output"
|
|
if req < 32000:
|
|
assert abs(delta < req * (2.0 / 100.0)) or (delta <= 1)
|
|
else:
|
|
# Delays > 32sec have reduced resolution (1 sec)
|
|
assert abs(delta) < 1000
|
|
|
|
|
|
def test_lptim(context_factory, logger):
|
|
serial_dev, jlink = context_factory("Test/Apps/lptim.bin")
|
|
state0_periods, state1_periods = measure_frequency(1, "D0")
|
|
num_periods = min(len(state0_periods), len(state1_periods))
|
|
periods = [state0_periods[i] + state1_periods[i] for i in range(num_periods)]
|
|
freqs = list(map(lambda x: 1 / x, periods))
|
|
assert (
|
|
periods
|
|
), "No LPTIM state changes detected, is the right analyzer being used? Is the device connected?"
|
|
|
|
min_f = min(freqs)
|
|
max_f = max(freqs)
|
|
avg_f = sum(freqs) / len(freqs)
|
|
print("min_f:{}, max_f:{}, avg_f:{}".format(min_f, max_f, avg_f))
|
|
assert abs(avg_f - 50) < 0.25
|
|
assert min_f > 49
|
|
assert max_f < 51
|
|
|
|
|
|
def test_app_lowpower(context_factory, logger):
|
|
serial_dev, jlink = context_factory("Application/main.bin", leave_halted=True)
|
|
jlink.reset(halt=False)
|
|
state0_periods, state1_periods = measure_frequency(10, "D0")
|
|
num_periods = min(len(state0_periods), len(state1_periods))
|
|
periods = [state0_periods[i] + state1_periods[i] for i in range(num_periods)]
|
|
freqs = list(map(lambda x: 1 / x, periods))
|
|
assert (
|
|
periods
|
|
), "No debug pin state changes detected, is the right analyzer being used? Is the device connected?"
|
|
|
|
min_f = min(freqs)
|
|
max_f = max(freqs)
|
|
avg_f = sum(freqs) / len(freqs)
|
|
pct_sleep = sum(state1_periods) * 100 / sum(state0_periods + state1_periods)
|
|
print(
|
|
"min_f:{}, max_f:{}, avg_f:{}, pct_sleep:{}".format(
|
|
min_f, max_f, avg_f, pct_sleep
|
|
)
|
|
)
|
|
assert len(periods) >= 5
|
|
assert pct_sleep > 99.95, "Spent too much time awake"
|
|
|
|
|
|
def test_stop(context_factory, logger):
|
|
serial_dev, jlink = context_factory("Test/Apps/stop.bin")
|
|
serial_dev.timeout = 70
|
|
|
|
pattern = re.compile("Requested=(\\d*) Actual=(\\d*)")
|
|
while True:
|
|
line = serial_dev.readline()
|
|
if line == TEST_PASS_TEXT:
|
|
break
|
|
|
|
line = line.decode("ascii", errors="ignore")
|
|
print(line.strip())
|
|
match = pattern.match(line)
|
|
assert match
|
|
req = int(match.group(1))
|
|
actual = int(match.group(2))
|
|
delta = req - actual
|
|
|
|
if req < 32000:
|
|
assert abs(delta < req * (2.0 / 100.0)) or (delta <= 1)
|
|
else:
|
|
# Delays > 32sec have reduced resolution (1 sec)
|
|
assert abs(delta) < 1000
|
|
|
|
|
|
def main():
|
|
pytest.main(sys.argv)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|