Update raw_usb interface

* Update raw_usb interface to the 0200 version
* Rename ice* commands to be less confusing
* Split production test into a runner script and test library
* Add continuous randomized test for test library
* Speed improvements for nvcm commands
This commit is contained in:
Matt Mets 2023-03-08 01:21:35 +01:00 committed by Michael Cardell Widerkrantz
parent 3897a8269b
commit 12f6575afd
No known key found for this signature in database
GPG Key ID: D3DB3DDF57E704E5
9 changed files with 931 additions and 640 deletions

View File

@ -1,11 +1,15 @@
SHELL := /bin/bash
PYTHON_FILES = \
usb_test.py \
icenvcm.py \
icebin2nvcm.py \
encode_usb_strings.py \
reset.py
production_test_runner.py \
production_tests.py \
pybin2nvcm.py \
pynvcm.py \
random_production_test.py \
reset.py \
usb_test.py
# autopep8: Fixes simple format errors automatically
# mypy: static type hint analysis
@ -16,14 +20,17 @@ lint:
pylint --generated-member=usb1.TRANSFER_COMPLETED,usb1.USBErrorInterrupted,usb1.USBErrorIO ${PYTHON_FILES}
# Check that the NVCM generator gives a correct output for a known binary
verify-nvcm:
./icebin2nvcm.py nvcm_test/application_fpga.bin verify.nvcm
verify-pybin2nvcm:
./pybin2nvcm.py nvcm_test/application_fpga.bin verify.nvcm
cmp verify.nvcm nvcm_test/application_fpga.nvcm
verify:
time ./icenvcm.py --verify nvcm_test/application_fpga.bin
verify-nvcm:
time ./pynvcm.py --verify nvcm_test/application_fpga.bin
program:
./icenvcm.py -i
time ./icenvcm.py --my-design-is-good-enough --ignore-blank --write ../application_fpga/application_fpga.bin --verify nvcm_test/application_fpga.bin
./icenvcm.py -b
program-nvcm-danger:
./pynvcm.py -i
time ./pynvcm.py --my-design-is-good-enough --ignore-blank --write ../application_fpga/application_fpga.bin --verify nvcm_test/application_fpga.bin
./pynvcm.py -b
randomize-production-test:
./production_tests.py

View File

@ -1,516 +0,0 @@
#!/usr/bin/env python
from usb_test import IceFlasher
import time
import numpy
from subprocess import run
import usb.core
import uuid
import encode_usb_strings
import serial
import serial.tools.list_ports;
import shutil
import os
# Locations for external utilities and files referenced by the test program
file_locations = {
'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/'
}
def enable_power():
"""Enable power to the TK-1"""
d = IceFlasher()
d.gpio_set_direction(7, True)
d.gpio_put(7, True)
d.close()
time.sleep(.2)
return True
def disable_power():
"""Disable power to the TK-1"""
time.sleep(.1)
d = IceFlasher()
d.gpio_set_direction(7, True)
d.gpio_put(7, False)
d.close()
return True
def measure_voltages(device, samples):
"""Measure the voltage levels of the three mta1 power rails multiple times, and return the average values"""
adc_vals = numpy.array([0,0,0])
for i in range(0,samples):
adc_vals = adc_vals + device.adc_read_all()
adc_vals = dict(zip(['1.2','2.5','3.3'],adc_vals/samples))
return adc_vals
def voltage_test():
"""Measure 3.3V 2.5V, and 1.2V voltage rails on the TK-1"""
enable_power()
d = IceFlasher()
vals = measure_voltages(d,20)
d.close()
disable_power()
print('voltages:',', '.join('{:}V:{:.3f}'.format(val[0],val[1]) 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():
"""Read the ID from TK-1 SPI flash, and verify that it matches the expected value"""
result = run([
file_locations['iceprog'],
'-t'
],
capture_output=True)
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 == None:
print('Flash ID invalid')
return False
print('Detected flash type: {:}'.format(flash_type))
return True
return (result.returncode == 0)
def flash_program():
"""Program and verify the TK-1 SPI flash with the application test gateware"""
result = run([
file_locations['iceprog'],
file_locations['app_gateware']
])
print(result)
return (result.returncode == 0)
def flash_check():
"""Verify the TK-1 SPI flash is programmed with the application test gateware"""
result = run([
file_locations['iceprog'],
'-c',
file_locations['app_gateware']
])
print(result)
return (result.returncode == 0)
def test_extra_io():
"""Test the TK-1 RTS, CTS, and GPIO1-4 lines by measuring a test pattern generated by the app_test gateware"""
enable_power()
time.sleep(.1)
d = IceFlasher()
d.gpio_put(16, False)
d.gpio_set_direction(16, True)
expected_results = [1<<(i%5) for i in range(9,-1,-1)]
results = []
for i in range(0,10):
vals = d.gpio_get_all()
pattern = (vals >> 17) & 0b11111
results.append(pattern)
d.gpio_put(16, True)
d.gpio_put(16, False)
d.gpio_set_direction(16, False)
d.close()
disable_power()
print(results,expected_results,results == expected_results)
return results == expected_results
def test_found_bootloader():
"""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 trys 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, outfile, serial):
"""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)
f = bytearray(open(infile, 'rb').read())
pos = f.find(magic)
if pos < 0:
print('failed to find magic string')
exit(1)
f[pos:(pos+len(magic))] = replacement
with open(outfile, 'wb') as of:
of.write(f)
def flash_ch552(serial):
"""Flash an attached CH552 device with the USB CDC firmware, injected with the given serial number"""
print(serial)
inject_serial_number(
file_locations['ch552_firmware'],
file_locations['ch552_firmware_injected'],
serial)
# Program the CH552 using CHPROG
result = run([
file_locations['chprog'],
file_locations['ch552_firmware_injected']
])
print(result)
return (result.returncode == 0)
def erase_ch552():
"""Erase an attached CH552 device"""
# Program the CH552 using CHPROG
result = run([
file_locations['chprog'],
file_locations['ch552_firmware_blank']
])
print(result)
return (result.returncode == 0)
def find_serial_device(desc):
"""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
return None
def find_ch552(serial):
"""Search all serial devices for one that has the correct description and serial number"""
time.sleep(1)
description = {
'vid':0x1207,
'pid':0x8887,
'manufacturer':'Tillitis',
'product':'MTA1-USB-V1',
'serial_number':serial
}
if find_serial_device(description) == None:
return False
return True
def ch552_program():
"""Load the CDC ACM firmware onto a CH552 with a randomly generated serial number, and verify that it boots correctly"""
if not test_found_bootloader():
print('Error finding CH552!')
return False
serial = str(uuid.uuid4())
if not flash_ch552(serial):
print('Error flashing CH552!')
return False
if not find_ch552(serial):
print('Error finding flashed CH552!')
return False
return True
def ch552_erase():
"""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():
"""Test UART communication, RGB LED, and touchpad by asking the operator to interact with the touch pad"""
description = {
'vid':0x1207,
'pid':0x8887,
'manufacturer':'Tillitis',
'product':'MTA1-USB-V1'
}
s = serial.Serial(find_serial_device(description),9600, timeout=.2)
if not s.isOpen():
print('couldn\'t find/open serial device')
return False
for i in range(0,5):
# Attempt to clear any buffered data from the serial port
s.write(b'0123')
time.sleep(0.2)
s.read(20)
try:
s.write(b'0')
[count, touch_count] = s.read(2)
print('read count:{:}, touch count:{:}'.format(count,touch_count))
input('\n\n\nPress touch pad once and check LED, then press Enter')
s.write(b'0')
[count_post, touch_count_post] = s.read(2)
print('read count:{:}, touch count:{:}'.format(count_post,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 e:
print(e)
continue
print('Max retries exceeded, failure!')
return False
def program_pico():
"""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(file_locations['pico_bootloader_source'])[1]
for trys in range(0,100): # retry every 0.1s
try:
shutil.copyfile(
file_locations['pico_bootloader_source'],
file_locations['pico_bootloader_target_dir'] + firmware_filename
)
# TODO: Test if the pico identifies as a USB-HID device after programming
return True
except FileNotFoundError:
time.sleep(0.1)
except PermissionError:
time.sleep(0.1)
return False
def sleep_2():
"""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
],
}
pass_msg = '''
_____ _____ _____
| __ \ /\ / ____| / ____|
| |__) | / \ | (___ | (___
| ___/ / /\ \ \___ \ \___ \
| | / ____ \ ____) | ____) |
|_| /_/ \_\ |_____/ |_____/
'''
fail_msg = '''
______ _____ _
| ____| /\ |_ _| | |
| |__ / \ | | | |
| __| / /\ \ | | | |
| | / ____ \ _| |_ | |____
|_| /_/ \_\ |_____| |______|
'''
ANSI = {
'fg_black':"\u001b[30m",
'fg_red':"\u001b[31m",
'fg_green':"\u001b[32m",
'fg_yellow':"\u001b[33m",
'fg_blue':"\u001b[34m",
'fg_magenta':"\u001b[35m",
'fg_cyan':"\u001b[36m",
'fg_white':"\u001b[37m",
'bg_black':"\u001b[40m",
'bg_red':"\u001b[41m",
'bg_green':"\u001b[42m",
'bg_yellow':"\u001b[43m",
'bg_blue':"\u001b[44m",
'bg_magenta':"\u001b[45m",
'bg_cyan':"\u001b[46m",
'bg_white':"\u001b[47m",
'reset':"\u001b[0m",
'bold':"\u001b[1m",
'underline':"\u001b[4m"
}
def run_tests(test_list):
for test in test_list:
print("\n{:}Test step: {:}{:} ({:})".format(ANSI['bold'],test.__name__, ANSI['reset'], test.__doc__))
if not test():
print('error running test step ' + test.__name__)
return False
return True
if __name__ == '__main__':
last_a = 1
# Allow any of the settings in the file_locations structure to be overridden
import argparse
parser = argparse.ArgumentParser()
for setting, value in file_locations.items():
parser.add_argument('--' + setting, help='Default setting: ' + value)
args = parser.parse_args()
for arg in args.__dict__:
if args.__dict__[arg] is not None:
print(arg, args.__dict__[arg])
file_locations[arg] = args.__dict__[arg]
print(file_locations)
print('\n\nTillitis TK-1 and TP-1 Production tests')
while True:
print('\n\n')
options = []
print('=== Test sequences ===')
i = 1
for name, tests in test_sequences.items():
print('{:}{:}. {:}{:}: {:}'.format(ANSI['bold'], i, name, ANSI['reset'], ', '.join([test.__name__ for test in tests])))
options.append(tests)
i += 1
print('\n=== Manual tests ===')
for test in manual_tests:
print('{:}{:}. {:}{:}: {:}'.format(ANSI['bold'], i, test.__name__, ANSI['reset'], test.__doc__))
options.append([test])
i += 1
a = input('\n\n\nPress return to run test {:}, or type in a new option number and press return:'.format(last_a))
if a == '':
a = last_a
try:
test_sequence = options[int(a)-1]
except IndexError as e:
print('Invalid input')
continue
except ValueError as e:
print('Invalid input')
continue
try:
result = run_tests(test_sequence)
except Exception as e:
print(e)
result = False
if not result:
print(ANSI['bg_red'] + fail_msg + ANSI['reset'])
try:
disable_power()
except AttributeError as e:
pass
except OSError as e:
pass
except ValueError as e:
pass
else:
print(ANSI['bg_green'] + pass_msg + ANSI['reset'])
last_a = a

View File

@ -0,0 +1,177 @@
#!/usr/bin/env python
from typing import Any
import production_tests
pass_msg = '''
_____ _____ _____
| __ \\ /\\ / ____| / ____|
| |__) | / \\ | (___ | (___
| ___/ / /\\ \\ \\___ \\ \\___ \\
| | / ____ \\ ____) | ____) |
|_| /_/ \\_\\ |_____/ |_____/
'''
fail_msg = '''
______ _____ _
| ____| /\\ |_ _| | |
| |__ / \\ | | | |
| __| / /\\ \\ | | | |
| | / ____ \\ _| |_ | |____
|_| /_/ \\_\\ |_____| |______|
'''
ANSI = {
'fg_black': "\u001b[30m",
'fg_red': "\u001b[31m",
'fg_green': "\u001b[32m",
'fg_yellow': "\u001b[33m",
'fg_blue': "\u001b[34m",
'fg_magenta': "\u001b[35m",
'fg_cyan': "\u001b[36m",
'fg_white': "\u001b[37m",
'bg_black': "\u001b[40m",
'bg_red': "\u001b[41m",
'bg_green': "\u001b[42m",
'bg_yellow': "\u001b[43m",
'bg_blue': "\u001b[44m",
'bg_magenta': "\u001b[45m",
'bg_cyan': "\u001b[46m",
'bg_white': "\u001b[47m",
'reset': "\u001b[0m",
'bold': "\u001b[1m",
'underline': "\u001b[4m"
}
def run_tests(test_list: list[Any]) -> bool:
try:
for test in test_list:
print("\n{:}Test step: {:}{:} ({:})".format(
ANSI['bold'],
test.__name__,
ANSI['reset'],
test.__doc__
))
if not test():
print(
'Failure at test step "{:}"'.format(
test.__name__))
return False
except Exception as e:
print(
'Error while running test step "{:}", exception:{:}'.format(
test.__name__, str(e)))
return False
return True
if __name__ == '__main__':
last_a = '1'
# Allow any of the settings in the parameters structure to be
# overridden
import argparse
parser = argparse.ArgumentParser()
parser.add_argument(
'-l',
'--list',
action='store_true',
help='List available tests, thenexit')
parser.add_argument(
'-r',
'--run_test',
required=False,
help='Run the specified test sequence or manual test, then exit')
for setting, value in production_tests.parameters.items():
parser.add_argument(
'--' + setting,
help='Default setting: ' + value)
args = parser.parse_args()
for arg in args.__dict__:
if args.__dict__[arg] is not None:
production_tests.parameters[arg] = args.__dict__[arg]
if args.list:
print("available tests:")
for name, test_list in production_tests.test_sequences.items():
print('{:}: {:}'.format(name, ', '.join(
[test.__name__ for test in test_list])))
for test in production_tests.manual_tests:
print('{:}: {:}'.format(test.__name__, test.__doc__))
exit(0)
if args.run_test is not None:
result = False
found = False
if args.run_test in production_tests.test_sequences:
result = run_tests(
production_tests.test_sequences[args.run_test])
found = True
else:
for test in production_tests.manual_tests:
if args.run_test == test.__name__:
result = run_tests([test])
found = True
break
if not found:
print('Test not found:{:}'.format(args.run_test))
exit(1)
if not result:
production_tests.reset()
exit(1)
exit(0)
print('\n\nProduction test system')
while True:
print('\n\n')
options = []
print('=== Test sequences ===')
i = 1
for name, test_list in production_tests.test_sequences.items():
print('{:}{:}. {:}{:}: {:}'.format(ANSI['bold'], i, name, ANSI['reset'], ', '.join(
[test.__name__ for test in test_list])))
options.append(test_list)
i += 1
print('\n=== Manual tests ===')
for test in production_tests.manual_tests:
print(
'{:}{:}. {:}{:}: {:}'.format(
ANSI['bold'],
i,
test.__name__,
ANSI['reset'],
test.__doc__))
options.append([test])
i += 1
a = input(
'\n\n\nPress return to run test {:}, or type in a new option number and press return:'.format(last_a))
if a == '':
a = last_a
try:
test_sequence = options[int(a) - 1]
except IndexError as e:
print('Invalid input')
continue
except ValueError as e:
print('Invalid input')
continue
if not run_tests(test_sequence):
print(ANSI['bg_red'] + fail_msg + ANSI['reset'])
production_tests.reset()
else:
print(ANSI['bg_green'] + pass_msg + ANSI['reset'])
last_a = a

View File

@ -0,0 +1,593 @@
#!/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 usb_test 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"""
d = IceFlasher()
d.gpio_set_direction(tp1_pins['5v_en'], True)
d.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)
d = IceFlasher()
d.gpio_set_direction(tp1_pins['5v_en'], True)
d.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 i 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()
d = IceFlasher()
vals = measure_voltages(d, 20)
d.close()
disable_power()
print(
'voltages:',
', '.join(
'{:}V:{:.3f}'.format(
val[0],
val[1]) 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 verify that it matches the expected value"""
result = run([
parameters['iceprog'],
'-t'
],
capture_output=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('Detected flash type: {:}'.format(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']
])
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']
])
disable_power()
print(result)
return result.returncode == 0
def test_extra_io() -> bool:
"""Test the TK-1 RTS, CTS, and GPIO1-4 lines by measuring a test pattern generated by the app_test gateware"""
d = IceFlasher()
for pin in tp1_pins.values():
d.gpio_set_direction(pin, False)
d.close()
disable_power()
time.sleep(1)
enable_power()
time.sleep(0.2)
d = IceFlasher()
d.gpio_put(tp1_pins['rts'], False)
d.gpio_set_direction(tp1_pins['rts'], True)
expected_results = [1 << (i % 5) for i in range(9, -1, -1)]
results = []
for i in range(0, 10):
vals = d.gpio_get_all()
pattern = (vals >> 17) & 0b11111
# print(f'{vals:016x} {pattern:04x}')
results.append(pattern)
d.gpio_put(tp1_pins['rts'], True)
d.gpio_put(tp1_pins['rts'], False)
d.gpio_set_direction(tp1_pins['rts'], False)
d.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)
f = bytearray(open(infile, 'rb').read())
pos = f.find(magic)
if pos < 0:
print('failed to find magic string')
exit(1)
f[pos:(pos + len(magic))] = replacement
with open(outfile, 'wb') as of:
of.write(f)
def flash_ch552(serial_num: str) -> bool:
"""Flash an attached CH552 device with the USB CDC firmware, injected with the given serial number"""
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']
])
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']
])
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 all serial devices for one that has the correct description and serial number"""
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 CDC ACM firmware onto a CH552 with a randomly generated serial number, and verify that it boots correctly"""
if not test_found_bootloader():
print('Error finding CH552!')
return False
serial = str(uuid.uuid4())
if not flash_ch552(serial):
print('Error flashing CH552!')
return False
if not find_ch552(serial):
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 by asking the operator to interact with the touch pad"""
description = {
'vid': 0x1207,
'pid': 0x8887,
'manufacturer': 'Tillitis',
'product': 'MTA1-USB-V1'
}
s = serial.Serial(
find_serial_device(description),
9600,
timeout=.2)
if not s.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
s.write(b'0123')
time.sleep(0.2)
s.read(20)
try:
s.write(b'0')
[count, touch_count] = s.read(2)
print(
'read count:{:}, touch count:{:}'.format(
count, touch_count))
input(
'\n\n\nPress touch pad once and check LED, then press Enter')
s.write(b'0')
[count_post, touch_count_post] = s.read(2)
print(
'read count:{:}, touch count:{:}'.format(
count_post,
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 e:
print(e)
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)
# TODO: Test if the pico identifies as a USB-HID device
# after programming
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 as e:
pass
except OSError as e:
pass
except ValueError as e:
pass
if __name__ == "__main__":
# Runs the non-interactive production tests continuously in a
# random order, to look for interaction bugs
import random
import pynvcm
def nvcm_read_info() -> bool:
tp1_pins = {
'5v_en': 7,
'sck': 10,
'mosi': 11,
'ss': 12,
'miso': 13,
'crst': 14,
'cdne': 15
}
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:
tp1_pins = {
'5v_en': 7,
'sck': 10,
'mosi': 11,
'ss': 12,
'miso': 13,
'crst': 14,
'cdne': 15
}
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
]
parameters['iceprog'] = '/home/matt/repos/tillitis--icestorm/iceprog/iceprog'
while True:
i = random.randint(0, (len(tests) - 1))
print(f'\n\n{i} running: {tests[i].__name__}')
if not tests[i]():
raise Exception('oops')

View File

@ -4,7 +4,7 @@ in https://github.com/YosysHQ/icestorm/pull/272
"""
def icebin2nvcm(bitstream: bytes) -> list[str]:
def pybin2nvcm(bitstream: bytes) -> list[str]:
"""Convert an ice40 bitstream into an NVCM program
The NVCM format is a set of commands that are run against the
@ -76,7 +76,7 @@ if __name__ == "__main__":
with open(args.infile, 'rb') as f_in:
data = f_in.read()
cmds = icebin2nvcm(data)
cmds = pybin2nvcm(data)
with open(args.outfile, 'w', encoding='utf-8') as f_out:
for cmd in cmds:

View File

@ -27,7 +27,18 @@ import sys
import struct
from time import sleep
from usb_test import IceFlasher
from icebin2nvcm import icebin2nvcm
from pybin2nvcm import pybin2nvcm
def assert_bytes_equal(
name: str,
expected: bytes,
val: bytes) -> None:
if expected != val:
expected_str = ' '.join([f'{x:02x}' for x in expected])
val_str = ' '.join([f'{x:02x}' for x in val])
raise AssertionError(
f'{name} expected:[{expected_str}] read:[{val_str}]')
class Nvcm():
@ -57,7 +68,7 @@ class Nvcm():
def __init__(
self,
pins: dict,
spi_speed: int = 12,
spi_speed: int,
debug: bool = False) -> None:
self.pins = pins
self.debug = debug
@ -76,7 +87,7 @@ class Nvcm():
self.flasher.gpio_set_direction(pins['crst'], True)
self.flasher.gpio_set_direction(pins['cdne'], False)
self.flasher.spi_pins_set(
self.flasher.spi_configure(
pins['sck'],
pins['ss'],
pins['mosi'],
@ -85,11 +96,11 @@ class Nvcm():
)
def power_on(self) -> None:
"""Disable power to the DUT"""
"""Enable power to the DUT"""
self.flasher.gpio_put(self.pins['5v_en'], True)
def power_off(self) -> None:
"""Enable power to the DUT"""
"""Disable power to the DUT"""
self.flasher.gpio_put(self.pins['5v_en'], False)
def enable(self, cs: bool, reset: bool = True) -> None:
@ -111,19 +122,17 @@ class Nvcm():
self.flasher.spi_write(data, toggle_cs)
def sendhex(self, s: str, toggle_cs: bool = True) -> bytes:
def sendhex(self, s: str) -> bytes:
"""Perform a full-duplex write/read on the target device
Keyword arguments:
s -- data to send (formatted as a string of hex data)
toggle_cs -- If true, automatically lower the CS pin before
transmit, and raise it after transmit
"""
if self.debug and not s == "0500":
print("TX", s)
x = bytes.fromhex(s)
b = self.flasher.spi_rxtx(x, toggle_cs)
b = self.flasher.spi_rxtx(x)
if self.debug and not s == "0500":
print("RX", b.hex())
@ -496,15 +505,16 @@ class Nvcm():
contents = bytearray()
for offset in range(0, length, 8):
if offset % (1024 * 8) == 0:
print("%6d / %6d bytes" % (offset, length))
# for offset in range(0, length, 8):
# if offset % (1024 * 8) == 0:
# print("%6d / %6d bytes" % (offset, length))
nvcm_addr = int(offset / 328) * 4096 + (offset % 328)
contents += self.read_bytes(0x03, nvcm_addr, 8)
self.delay(2)
# nvcm_addr = int(offset / 328) * 4096 + (offset % 328)
# contents += self.read_bytes(0x03, nvcm_addr, 8)
# self.delay(2)
return bytes(contents)
# return bytes(contents)
return self.read_bytes(0x03, 0x000000, length)
def read_file(self, filename: str, length: int) -> None:
""" Read the contents of the NVCM to a file
@ -557,7 +567,7 @@ class Nvcm():
print('Verification complete, NVCM contents match file')
def sleep_flash(pins: dict) -> None:
def sleep_flash(pins: dict, spi_speed: int) -> None:
""" Put the SPI bootloader flash in deep sleep mode
Keyword arguments:
@ -582,33 +592,23 @@ def sleep_flash(pins: dict) -> None:
flasher.gpio_set_direction(pins['sck'], True)
flasher.gpio_set_direction(pins['miso'], True)
flasher.spi_pins_set(
flasher.spi_configure(
pins['sck'],
pins['ss'],
pins['miso'],
pins['mosi'],
12
spi_speed
)
sleep(0.5)
# Wake the flash up
flasher.spi_write(bytes([0xAB]))
# Confirm we can talk to flash
data = flasher.spi_rxtx(bytes([0x9f, 0, 0]))
print('flash ID while awake:', ' '.join(
[f'{b:02x}' for b in data]))
assert data == bytes([0xff, 0xef, 0x40])
# Test that the flash will ignore a sleep command that doesn't
# start on the first byte
flasher.spi_write(bytes([0, 0xb9]))
# Confirm we can talk to flash
data = flasher.spi_rxtx(bytes([0x9f, 0, 0]))
print('flash ID while awake:', ' '.join(
[f'{b:02x}' for b in data]))
assert data == bytes([0xff, 0xef, 0x40])
assert_bytes_equal('flash_id', bytes([0xff, 0xef, 0x40]), data)
# put the flash to sleep
flasher.spi_write(bytes([0xb9]))
@ -616,9 +616,7 @@ def sleep_flash(pins: dict) -> None:
# Confirm flash is asleep
data = flasher.spi_rxtx(bytes([0x9f, 0, 0]))
print('flash ID while asleep:', ' '.join(
[f'{b:02x}' for b in data]))
assert data == bytes([0xff, 0xff, 0xff])
assert_bytes_equal('flash_sleep', bytes([0xff, 0xff, 0xff]), data)
if __name__ == "__main__":
@ -718,11 +716,11 @@ if __name__ == "__main__":
}
if args.sleep_flash:
sleep_flash(tp1_pins)
sleep_flash(tp1_pins, args.spi_speed)
nvcm = Nvcm(
tp1_pins,
spi_speed=args.spi_speed,
args.spi_speed,
debug=args.verbose)
nvcm.power_on()
@ -737,12 +735,12 @@ if __name__ == "__main__":
with open(args.write_file, "rb") as in_file:
bitstream = in_file.read()
print(f"read {len(bitstream)} bytes")
cmds = icebin2nvcm(bitstream)
cmds = pybin2nvcm(bitstream)
if not args.ignore_blank:
nvcm.trim_blank_check()
# how much should we check?
nvcm.blank_check(0x100)
nvcm.blank_check(100000)
# this is it!
nvcm.program(cmds)

View File

@ -1,6 +1,5 @@
libusb1==3.0.0
pyusb==1.2.1
numpy==1.23.4
pyserial==3.5
autopep8==2.0.1
mypy==1.0.1

View File

@ -19,4 +19,4 @@ else
. ./venv/bin/activate
fi
./production_test.py
./production_test_runner.py

View File

@ -27,17 +27,16 @@ import usb1 # type: ignore
class IceFlasher:
""" iCE40 programming tool based on an RPi Pico """
FLASHER_REQUEST_LED_SET = 0x00
FLASHER_REQUEST_PIN_DIRECTION_SET = 0x10
FLASHER_REQUEST_PULLUPS_SET = 0x12
FLASHER_REQUEST_PIN_VALUES_SET = 0x20
FLASHER_REQUEST_PIN_VALUES_GET = 0x30
FLASHER_REQUEST_SPI_BITBANG_CS = 0x41
FLASHER_REQUEST_SPI_BITBANG_NO_CS = 0x42
FLASHER_REQUEST_SPI_PINS_SET = 0x43
FLASHER_REQUEST_SPI_CLKOUT = 0x44
FLASHER_REQUEST_ADC_READ = 0x50
FLASHER_REQUEST_BOOTLOADER = 0xFF
COMMAND_PIN_DIRECTION = 0x30
COMMAND_PULLUPS = 0x31
COMMAND_PIN_VALUES = 0x32
COMMAND_SPI_CONFIGURE = 0x40
COMMAND_SPI_XFER = 0x41
COMMAND_SPI_CLKOUT = 0x42
COMMAND_ADC_READ = 0x50
COMMAND_BOOTLOADER = 0xE0
SPI_MAX_TRANSFER_SIZE = 2048 - 8
@ -56,9 +55,29 @@ class IceFlasher:
# Device not present, or user is not allowed to access
# device.
raise ValueError('Device not found')
# Check the device firmware version
bcd_device = self.handle.getDevice().getbcdDevice()
if bcd_device != 0x0200:
raise ValueError(
'Pico firmware version out of date- please upgrade it!')
self.handle.claimInterface(0)
def __del__(self) -> None:
self.close()
def close(self) -> None:
self._wait_async()
if self.handle is not None:
self.handle.close()
self.handle = None
self.context.close()
self.context = None
def _wait_async(self) -> None:
# Wait until all submitted transfers can be cleared
while any(transfer.isSubmitted()
for transfer in self.transfer_list):
try:
@ -75,27 +94,36 @@ class IceFlasher:
transfer.getStatus(),
usb1.TRANSFER_COMPLETED)
def _write(self, request_id: int, data: bytes) -> None:
transfer = self.handle.getTransfer()
transfer.setControl(
# usb1.ENDPOINT_OUT | usb1.TYPE_VENDOR |
# usb1.RECIPIENT_DEVICE, #request type
0x40,
request_id, # request
0, # index
0,
data, # data
callback=None, # callback functiopn
user_data=None, # userdata
timeout=1000
)
transfer.submit()
self.transfer_list.append(transfer)
def _write(
self,
request_id: int,
data: bytes,
nonblocking: bool = False) -> None:
if nonblocking:
transfer = self.handle.getTransfer()
transfer.setControl(
# usb1.ENDPOINT_OUT | usb1.TYPE_VENDOR |
# usb1.RECIPIENT_DEVICE, #request type
0x40,
request_id, # request
0, # index
0,
data, # data
callback=None, # callback functiopn
user_data=None, # userdata
timeout=1000
)
transfer.submit()
self.transfer_list.append(transfer)
else:
self.handle.controlWrite(
0x40, request_id, 0, 0, data, timeout=100)
def _read(self, request_id: int, length: int) -> bytes:
self._wait_async()
# self._wait_async()
return self.handle.controlRead(
0xC0, request_id, 0, 0, length)
0xC0, request_id, 0, 0, length, timeout=100)
def gpio_set_direction(self, pin: int, direction: bool) -> None:
"""Set the direction of a single GPIO pin
@ -109,7 +137,7 @@ class IceFlasher:
((1 if direction else 0) << pin),
)
self._write(self.FLASHER_REQUEST_PIN_DIRECTION_SET, msg)
self._write(self.COMMAND_PIN_DIRECTION, msg)
def gpio_set_pulls(
self,
@ -129,7 +157,7 @@ class IceFlasher:
((1 if pulldown else 0) << pin),
)
self._write(self.FLASHER_REQUEST_PULLUPS_SET, msg)
self._write(self.COMMAND_PULLUPS, msg)
def gpio_put(self, pin: int, val: bool) -> None:
"""Set the output level of a single GPIO pin
@ -143,11 +171,11 @@ class IceFlasher:
(1 if val else 0) << pin,
)
self._write(self.FLASHER_REQUEST_PIN_VALUES_SET, msg)
self._write(self.COMMAND_PIN_VALUES, msg)
def gpio_get_all(self) -> int:
"""Read the input levels of all GPIO pins"""
msg_in = self._read(self.FLASHER_REQUEST_PIN_VALUES_GET, 4)
msg_in = self._read(self.COMMAND_PIN_VALUES, 4)
[gpio_states] = struct.unpack('>I', msg_in)
return gpio_states
@ -162,7 +190,7 @@ class IceFlasher:
return ((gpio_states >> pin) & 0x01) == 0x01
def spi_pins_set(
def spi_configure(
self,
sck_pin: int,
cs_pin: int,
@ -187,7 +215,9 @@ class IceFlasher:
msg = bytearray()
msg.extend(header)
self._write(self.FLASHER_REQUEST_SPI_PINS_SET, msg)
self._write(self.COMMAND_SPI_CONFIGURE, msg)
self.cs_pin = cs_pin
def spi_write(
self,
@ -199,13 +229,7 @@ class IceFlasher:
buf -- Byte buffer to send.
toggle_cs -- (Optional) If true, toggle the CS line
"""
max_chunk_size = self.SPI_MAX_TRANSFER_SIZE
for i in range(0, len(buf), max_chunk_size):
chunk = buf[i:i + max_chunk_size]
self._spi_bitbang_inner(
buf=chunk,
toggle_cs=toggle_cs,
read_after_write=False)
self._spi_xfer(buf, toggle_cs, False)
def spi_rxtx(
self,
@ -218,23 +242,43 @@ class IceFlasher:
toggle_cs -- (Optional) If true, toggle the CS line
"""
return self._spi_xfer(buf, toggle_cs, True)
def _spi_xfer(
self,
buf: bytes,
toggle_cs: bool,
read_after_write: bool) -> bytes:
ret = bytearray()
max_chunk_size = self.SPI_MAX_TRANSFER_SIZE
for i in range(0, len(buf), max_chunk_size):
chunk = buf[i:i + max_chunk_size]
if len(buf) <= self.SPI_MAX_TRANSFER_SIZE:
return self._spi_xfer_inner(
buf,
toggle_cs,
read_after_write)
if toggle_cs:
self.gpio_put(self.cs_pin, False)
for i in range(0, len(buf), self.SPI_MAX_TRANSFER_SIZE):
chunk = buf[i:i + self.SPI_MAX_TRANSFER_SIZE]
ret.extend(
self._spi_bitbang_inner(
buf=chunk,
toggle_cs=toggle_cs))
self._spi_xfer_inner(
chunk,
False,
read_after_write))
if toggle_cs:
self.gpio_put(self.cs_pin, True)
return bytes(ret)
def _spi_bitbang_inner(
def _spi_xfer_inner(
self,
buf: bytes,
toggle_cs: bool = True,
read_after_write: bool = True) -> bytes:
toggle_cs: bool,
read_after_write: bool) -> bytes:
"""Bitbang a SPI transfer using the specificed GPIO pins
Keyword arguments:
@ -247,23 +291,18 @@ class IceFlasher:
'Message too large, '
+ f'size:{len(buf)} max:{self.SPI_MAX_TRANSFER_SIZE}')
header = struct.pack('>I', len(buf))
header = struct.pack('>BI', toggle_cs, len(buf))
msg = bytearray()
msg.extend(header)
msg.extend(buf)
if toggle_cs:
cmd = self.FLASHER_REQUEST_SPI_BITBANG_CS
else:
cmd = self.FLASHER_REQUEST_SPI_BITBANG_NO_CS
self._write(cmd, msg)
self._write(self.COMMAND_SPI_XFER, msg)
if not read_after_write:
return bytes()
msg_in = self._read(
self.FLASHER_REQUEST_SPI_BITBANG_CS,
self.COMMAND_SPI_XFER,
len(buf))
return msg_in
@ -283,7 +322,7 @@ class IceFlasher:
msg = bytearray()
msg.extend(header)
self._write(
self.FLASHER_REQUEST_SPI_CLKOUT,
self.COMMAND_SPI_CLKOUT,
msg)
def adc_read_all(self) -> tuple[float, float, float]:
@ -292,7 +331,7 @@ class IceFlasher:
The firmware will read the values for each input multiple
times, and return averaged values for each input.
"""
msg_in = self._read(self.FLASHER_REQUEST_ADC_READ, 3 * 4)
msg_in = self._read(self.COMMAND_ADC_READ, 3 * 4)
[ch0, ch1, ch2] = struct.unpack('>III', msg_in)
return ch0 / 1000000, ch1 / 1000000, ch2 / 1000000
@ -304,12 +343,6 @@ class IceFlasher:
picotool, or by copying a file to the uf2 drive.
"""
try:
self._write(self.FLASHER_REQUEST_BOOTLOADER, bytes())
self._write(self.COMMAND_BOOTLOADER, bytes())
except usb1.USBErrorIO:
pass
if __name__ == '__main__':
flasher = IceFlasher()
flasher.spi_pins_set(1, 2, 3, 4, 15)