tillitis-key/hw/production_test/production_tests.py
2023-03-21 14:38:29 +01:00

580 lines
15 KiB
Python
Executable File

#!/usr/bin/env python
"""Production test definitions for Tillitis TK1 and TP1
The test runner looks for these objects in this file:
parameters: Dictionary of string variables containing overrideable
parameters, such as locations of external programs.
These parameters are automatically added as command line
arguements to the test runner.
manual_tests: List of functions that implement manual tests. These
tests will be added to the 'manual test' menu. Each
test should have a pep257 formatted docstring, which
will be displayed to introduce the text. The tests
must not take any paraemters, and return True if the
test passed successfully, or False if it failed.
Manual tests should be able to run independenly (for
example, they shouldn't rely on a previous test turning
on a power supply).
test_sequences: Dictionary of production test sequences. Each entry
in the dictionary defines a sequence of manual tests
that, once performed in the specified order, fully
test a device.
reset(): Cleanup function that will be called if a running test fails
at any time. This function should catch any exceptions that
might happen as a result of the cleanup actions (such as
trying to reset a device that has been removed, etc).
"""
from typing import Any
import time
from subprocess import run
import uuid
import shutil
import os
import serial # type: ignore
import serial.tools.list_ports # type: ignore
import usb.core # type: ignore
import encode_usb_strings
from iceflasher import IceFlasher
# Locations for external utilities and files referenced by the test
# program
parameters = {
'iceprog': 'tillitis-iceprog',
'chprog': 'chprog',
'app_gateware': 'binaries/top.bin',
'ch552_firmware': 'binaries/usb_device_cdc.bin',
'ch552_firmware_blank': 'binaries/blank.bin',
'ch552_firmware_injected': '/tmp/ch552_fw_injected.bin',
'pico_bootloader_source': 'binaries/main.uf2',
'pico_bootloader_target_dir': '/media/lab/RPI-RP2/'
}
TP1_PINS = {
'5v_en': 7,
'tx': 8,
'rx': 9,
'sck': 10,
'mosi': 11,
'ss': 12,
'miso': 13,
'crst': 14,
'cdne': 15,
'rts': 16,
'cts': 17,
'gpio1': 18,
'gpio2': 19,
'gpio3': 20,
'gpio4': 21,
}
def enable_power() -> bool:
"""Enable power to the TK-1"""
flasher = IceFlasher()
flasher.gpio_set_direction(TP1_PINS['5v_en'], True)
flasher.gpio_put(TP1_PINS['5v_en'], True)
time.sleep(0.3)
return True
def disable_power() -> bool:
"""Disable power to the TK-1"""
time.sleep(.1)
flasher = IceFlasher()
flasher.gpio_set_direction(TP1_PINS['5v_en'], True)
flasher.gpio_put(TP1_PINS['5v_en'], False)
return True
def measure_voltages(device: IceFlasher,
sample_count: int) -> dict[str, float]:
"""Measure the voltage levels of the tk-1 power rails
Keyword arguments:
device -- programmer
sample_count -- number of samples to average
"""
adc_vals = [0.0, 0.0, 0.0]
for _ in range(0, sample_count):
adc_vals = [
total + sample for total,
sample in zip(
adc_vals,
device.adc_read_all())]
adc_vals = [total / sample_count for total in adc_vals]
return dict(zip(['1.2', '2.5', '3.3'], adc_vals))
def voltage_test() -> bool:
"""Measure 3.3V 2.5V, and 1.2V voltage rails on the TK-1"""
enable_power()
flasher = IceFlasher()
vals = measure_voltages(flasher, 20)
flasher.close()
disable_power()
print(
'voltages:',
', '.join(
f'{val[0]}V:{val[1]:.3f}' for val in vals.items()))
if (
(abs(vals['1.2'] - 1.2) > .2)
| (abs(vals['2.5'] - 2.5) > .2)
| (abs(vals['3.3'] - 3.3) > .2)
):
return False
return True
def flash_validate_id() -> bool:
"""Read the ID from TK-1 SPI flash, and compare to known values"""
result = run([
parameters['iceprog'],
'-t'
],
capture_output=True,
check=True)
disable_power()
err = result.stderr.split(b'\n')
for line in err:
if line.startswith(b'flash ID:'):
vals_b = line.split(b' 0x')[1:]
flash_id = int(b''.join(vals_b), 16)
print(line, hex(flash_id))
# Note: Flash IDs reported by iceprog are slightly wrong
flash_types = {
0xb40140b40140b40140b40140b4014: 'XT25F08BDFIGT-S (MTA1-USB-V1)',
0xef401400: 'W25Q80DVUXIE (TK-1)'}
flash_type = flash_types.get(flash_id)
if flash_type is None:
print('Flash ID invalid')
return False
print(f'Detected flash type: {flash_type}')
return True
return result.returncode == 0
def flash_program() -> bool:
"""Program and verify the TK-1 SPI flash with the application test gateware"""
result = run([
parameters['iceprog'],
parameters['app_gateware']
],
check=True)
disable_power()
print(result)
return result.returncode == 0
def flash_check() -> bool:
"""Verify the TK-1 SPI flash is programmed with the application test gateware"""
result = run([
parameters['iceprog'],
'-c',
parameters['app_gateware']
],
check=True)
disable_power()
print(result)
return result.returncode == 0
def test_extra_io() -> bool:
"""Test the TK-1 RTS, CTS, and GPIO1-4 lines"""
flasher = IceFlasher()
for pin in TP1_PINS.values():
flasher.gpio_set_direction(pin, False)
flasher.close()
disable_power()
time.sleep(1)
enable_power()
time.sleep(0.2)
flasher = IceFlasher()
flasher.gpio_put(TP1_PINS['rts'], False)
flasher.gpio_set_direction(TP1_PINS['rts'], True)
expected_results = [1 << (i % 5) for i in range(9, -1, -1)]
results = []
for _ in range(0, 10):
vals = flasher.gpio_get_all()
pattern = (vals >> 17) & 0b11111
# print(f'{vals:016x} {pattern:04x}')
results.append(pattern)
flasher.gpio_put(TP1_PINS['rts'], True)
flasher.gpio_put(TP1_PINS['rts'], False)
flasher.gpio_set_direction(TP1_PINS['rts'], False)
flasher.close()
disable_power()
print(results, expected_results, results == expected_results)
return results == expected_results
def test_found_bootloader() -> bool:
"""Search for a CH552 in USB bootloader mode"""
print('\n\n\nSearching for CH552 bootloader, plug in USB cable now (times out in 10 seconds)!')
for _ in range(0, 100): # retry every 0.1s, up to 10 seconds
devices = usb.core.find(
idVendor=0x4348,
idProduct=0x55e0,
find_all=True)
count = len(list(devices))
if count == 1:
return True
time.sleep(0.1)
post = usb.core.find(
idVendor=0x4348,
idProduct=0x55e0,
find_all=True)
post_count = len(list(post))
return post_count == 1
def inject_serial_number(
infile: str,
outfile: str,
serial_num: str) -> None:
"""Inject a serial number into the specified CH552 firmware file"""
magic = encode_usb_strings.string_to_descriptor(
"68de5d27-e223-4874-bc76-a54d6e84068f")
replacement = encode_usb_strings.string_to_descriptor(serial_num)
with open(infile, 'rb') as fin:
firmware_data = bytearray(fin.read())
pos = firmware_data.find(magic)
if pos < 0:
raise ValueError('failed to find magic string')
firmware_data[pos:(pos + len(magic))] = replacement
with open(outfile, 'wb') as fout:
fout.write(firmware_data)
def flash_ch552(serial_num: str) -> bool:
"""Flash an attached CH552 device with the USB CDC firmware"""
print(serial_num)
inject_serial_number(
parameters['ch552_firmware'],
parameters['ch552_firmware_injected'],
serial_num)
# Program the CH552 using CHPROG
result = run([
parameters['chprog'],
parameters['ch552_firmware_injected']
],
check=True)
print(result)
return result.returncode == 0
def erase_ch552() -> bool:
"""Erase an attached CH552 device"""
# Program the CH552 using CHPROG
result = run([
parameters['chprog'],
parameters['ch552_firmware_blank']
],
check=True)
print(result)
return result.returncode == 0
def find_serial_device(desc: dict[str, Any]) -> str:
"""Look for a serial device that has the given attributes"""
for port in serial.tools.list_ports.comports():
matched = True
for key, value in desc.items():
if not getattr(port, key) == value:
matched = False
if matched:
print(port.device)
return port.device
raise NameError('Serial device not found')
def find_ch552(serial_num: str) -> bool:
"""Search for a serial device that has the correct description"""
time.sleep(1)
description = {
'vid': 0x1207,
'pid': 0x8887,
'manufacturer': 'Tillitis',
'product': 'MTA1-USB-V1',
'serial_number': serial_num
}
try:
find_serial_device(description)
except NameError:
return False
return True
def ch552_program() -> bool:
"""Load the firmware onto a CH552, and verify that it boots"""
if not test_found_bootloader():
print('Error finding CH552!')
return False
serial_num = str(uuid.uuid4())
if not flash_ch552(serial_num):
print('Error flashing CH552!')
return False
if not find_ch552(serial_num):
print('Error finding flashed CH552!')
return False
return True
def ch552_erase() -> bool:
"""Erase the firmware from a previously programmed CH552"""
if not test_found_bootloader():
print('Error finding CH552!')
return False
if not erase_ch552():
print('Error erasing CH552!')
return False
return True
def test_txrx_touchpad() -> bool:
"""Test UART communication, RGB LED, and touchpad"""
description = {
'vid': 0x1207,
'pid': 0x8887,
'manufacturer': 'Tillitis',
'product': 'MTA1-USB-V1'
}
dev = serial.Serial(
find_serial_device(description),
9600,
timeout=.2)
if not dev.isOpen():
print('couldn\'t find/open serial device')
return False
for _ in range(0, 5):
# Attempt to clear any buffered data from the serial port
dev.write(b'0123')
time.sleep(0.2)
dev.read(20)
try:
dev.write(b'0')
[count, touch_count] = dev.read(2)
print(
f'read count:{count}, touch count:{touch_count}')
input(
'\n\n\nPress touch pad once and check LED, then press Enter')
dev.write(b'0')
[count_post, touch_count_post] = dev.read(2)
print(
'read count:{count_post}, touch count:{touch_count_post}')
if (count_post -
count != 1) or (touch_count_post -
touch_count != 1):
print('Unexpected values returned, trying again')
continue
return True
except ValueError as error:
print(error)
continue
print('Max retries exceeded, failure!')
return False
def program_pico() -> bool:
"""Load the ice40 flasher firmware onto the TP-1"""
print('Attach test rig to USB (times out in 10 seconds)')
firmware_filename = os.path.split(
parameters['pico_bootloader_source'])[1]
for _ in range(0, 100): # retry every 0.1s
try:
shutil.copyfile(
parameters['pico_bootloader_source'],
parameters['pico_bootloader_target_dir'] +
firmware_filename)
return True
except FileNotFoundError:
time.sleep(0.1)
except PermissionError:
time.sleep(0.1)
return False
def sleep_2() -> bool:
"""Sleep for 2 seconds"""
time.sleep(2)
return True
manual_tests = [
program_pico,
voltage_test,
flash_validate_id,
flash_program,
flash_check,
test_extra_io,
ch552_program,
ch552_erase,
test_txrx_touchpad,
enable_power,
disable_power
]
test_sequences = {
'tk1_test_sequence': [
voltage_test,
flash_validate_id,
flash_program,
sleep_2,
test_extra_io,
ch552_program,
test_txrx_touchpad
],
'tp1_test_sequence': [
program_pico,
sleep_2,
flash_validate_id
],
'mta1_usb_v1_programmer_test_sequence': [
program_pico,
sleep_2,
voltage_test,
flash_validate_id,
sleep_2,
test_extra_io
],
}
# This function will be called if a test fails
def reset() -> None:
"""Attempt to reset the board after test failure"""
try:
disable_power()
except AttributeError:
pass
except OSError:
pass
except ValueError:
pass
def random_test_runner() -> None:
""""Run the non-interactive production tests in a random order
This routine is intended to be used for finding edge-cases with
the production tests. It runs the non-interactive tests (as well
as some nondestructive tests from the nvcm module) in a random
order, and runs continuously.
"""
def nvcm_read_info() -> bool:
"""Check that the nvcm read info command runs"""
pynvcm.sleep_flash(TP1_PINS, 15)
nvcm = pynvcm.Nvcm(TP1_PINS, 15)
nvcm.power_on()
nvcm.init()
nvcm.nvcm_enable()
nvcm.info()
nvcm.power_off()
return True
def nvcm_verify_blank() -> bool:
"""Verify that the NVCM memory is blank"""
pynvcm.sleep_flash(TP1_PINS, 15)
nvcm = pynvcm.Nvcm(TP1_PINS, 15)
nvcm.power_on()
nvcm.init()
nvcm.nvcm_enable()
nvcm.trim_blank_check()
nvcm.power_off()
return True
tests = [
nvcm_read_info,
nvcm_verify_blank,
voltage_test,
flash_validate_id,
flash_program,
flash_check,
test_extra_io,
enable_power,
disable_power
]
pass_count = 0
while True:
i = random.randint(0, (len(tests) - 1))
print(f'\n\n{pass_count}: running: {tests[i].__name__}')
if not tests[i]():
sys.exit(1)
pass_count += 1
if __name__ == "__main__":
import random
import pynvcm
import sys
random_test_runner()