mirror of
https://github.com/tillitis/tillitis-key1.git
synced 2025-04-29 11:26:27 -04:00
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:
parent
3897a8269b
commit
12f6575afd
@ -1,11 +1,15 @@
|
|||||||
SHELL := /bin/bash
|
SHELL := /bin/bash
|
||||||
|
|
||||||
PYTHON_FILES = \
|
PYTHON_FILES = \
|
||||||
usb_test.py \
|
|
||||||
icenvcm.py \
|
|
||||||
icebin2nvcm.py \
|
|
||||||
encode_usb_strings.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
|
# autopep8: Fixes simple format errors automatically
|
||||||
# mypy: static type hint analysis
|
# mypy: static type hint analysis
|
||||||
@ -16,14 +20,17 @@ lint:
|
|||||||
pylint --generated-member=usb1.TRANSFER_COMPLETED,usb1.USBErrorInterrupted,usb1.USBErrorIO ${PYTHON_FILES}
|
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
|
# Check that the NVCM generator gives a correct output for a known binary
|
||||||
verify-nvcm:
|
verify-pybin2nvcm:
|
||||||
./icebin2nvcm.py nvcm_test/application_fpga.bin verify.nvcm
|
./pybin2nvcm.py nvcm_test/application_fpga.bin verify.nvcm
|
||||||
cmp verify.nvcm nvcm_test/application_fpga.nvcm
|
cmp verify.nvcm nvcm_test/application_fpga.nvcm
|
||||||
|
|
||||||
verify:
|
verify-nvcm:
|
||||||
time ./icenvcm.py --verify nvcm_test/application_fpga.bin
|
time ./pynvcm.py --verify nvcm_test/application_fpga.bin
|
||||||
|
|
||||||
program:
|
program-nvcm-danger:
|
||||||
./icenvcm.py -i
|
./pynvcm.py -i
|
||||||
time ./icenvcm.py --my-design-is-good-enough --ignore-blank --write ../application_fpga/application_fpga.bin --verify nvcm_test/application_fpga.bin
|
time ./pynvcm.py --my-design-is-good-enough --ignore-blank --write ../application_fpga/application_fpga.bin --verify nvcm_test/application_fpga.bin
|
||||||
./icenvcm.py -b
|
./pynvcm.py -b
|
||||||
|
|
||||||
|
randomize-production-test:
|
||||||
|
./production_tests.py
|
||||||
|
@ -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
|
|
||||||
|
|
177
hw/production_test/production_test_runner.py
Executable file
177
hw/production_test/production_test_runner.py
Executable 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
|
593
hw/production_test/production_tests.py
Executable file
593
hw/production_test/production_tests.py
Executable 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')
|
@ -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
|
"""Convert an ice40 bitstream into an NVCM program
|
||||||
|
|
||||||
The NVCM format is a set of commands that are run against the
|
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:
|
with open(args.infile, 'rb') as f_in:
|
||||||
data = f_in.read()
|
data = f_in.read()
|
||||||
|
|
||||||
cmds = icebin2nvcm(data)
|
cmds = pybin2nvcm(data)
|
||||||
|
|
||||||
with open(args.outfile, 'w', encoding='utf-8') as f_out:
|
with open(args.outfile, 'w', encoding='utf-8') as f_out:
|
||||||
for cmd in cmds:
|
for cmd in cmds:
|
@ -27,7 +27,18 @@ import sys
|
|||||||
import struct
|
import struct
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from usb_test import IceFlasher
|
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():
|
class Nvcm():
|
||||||
@ -57,7 +68,7 @@ class Nvcm():
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
pins: dict,
|
pins: dict,
|
||||||
spi_speed: int = 12,
|
spi_speed: int,
|
||||||
debug: bool = False) -> None:
|
debug: bool = False) -> None:
|
||||||
self.pins = pins
|
self.pins = pins
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
@ -76,7 +87,7 @@ class Nvcm():
|
|||||||
self.flasher.gpio_set_direction(pins['crst'], True)
|
self.flasher.gpio_set_direction(pins['crst'], True)
|
||||||
self.flasher.gpio_set_direction(pins['cdne'], False)
|
self.flasher.gpio_set_direction(pins['cdne'], False)
|
||||||
|
|
||||||
self.flasher.spi_pins_set(
|
self.flasher.spi_configure(
|
||||||
pins['sck'],
|
pins['sck'],
|
||||||
pins['ss'],
|
pins['ss'],
|
||||||
pins['mosi'],
|
pins['mosi'],
|
||||||
@ -85,11 +96,11 @@ class Nvcm():
|
|||||||
)
|
)
|
||||||
|
|
||||||
def power_on(self) -> None:
|
def power_on(self) -> None:
|
||||||
"""Disable power to the DUT"""
|
"""Enable power to the DUT"""
|
||||||
self.flasher.gpio_put(self.pins['5v_en'], True)
|
self.flasher.gpio_put(self.pins['5v_en'], True)
|
||||||
|
|
||||||
def power_off(self) -> None:
|
def power_off(self) -> None:
|
||||||
"""Enable power to the DUT"""
|
"""Disable power to the DUT"""
|
||||||
self.flasher.gpio_put(self.pins['5v_en'], False)
|
self.flasher.gpio_put(self.pins['5v_en'], False)
|
||||||
|
|
||||||
def enable(self, cs: bool, reset: bool = True) -> None:
|
def enable(self, cs: bool, reset: bool = True) -> None:
|
||||||
@ -111,19 +122,17 @@ class Nvcm():
|
|||||||
|
|
||||||
self.flasher.spi_write(data, toggle_cs)
|
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
|
"""Perform a full-duplex write/read on the target device
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
s -- data to send (formatted as a string of hex data)
|
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":
|
if self.debug and not s == "0500":
|
||||||
print("TX", s)
|
print("TX", s)
|
||||||
x = bytes.fromhex(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":
|
if self.debug and not s == "0500":
|
||||||
print("RX", b.hex())
|
print("RX", b.hex())
|
||||||
@ -496,15 +505,16 @@ class Nvcm():
|
|||||||
|
|
||||||
contents = bytearray()
|
contents = bytearray()
|
||||||
|
|
||||||
for offset in range(0, length, 8):
|
# for offset in range(0, length, 8):
|
||||||
if offset % (1024 * 8) == 0:
|
# if offset % (1024 * 8) == 0:
|
||||||
print("%6d / %6d bytes" % (offset, length))
|
# print("%6d / %6d bytes" % (offset, length))
|
||||||
|
|
||||||
nvcm_addr = int(offset / 328) * 4096 + (offset % 328)
|
# nvcm_addr = int(offset / 328) * 4096 + (offset % 328)
|
||||||
contents += self.read_bytes(0x03, nvcm_addr, 8)
|
# contents += self.read_bytes(0x03, nvcm_addr, 8)
|
||||||
self.delay(2)
|
# 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:
|
def read_file(self, filename: str, length: int) -> None:
|
||||||
""" Read the contents of the NVCM to a file
|
""" Read the contents of the NVCM to a file
|
||||||
@ -557,7 +567,7 @@ class Nvcm():
|
|||||||
print('Verification complete, NVCM contents match file')
|
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
|
""" Put the SPI bootloader flash in deep sleep mode
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
@ -582,33 +592,23 @@ def sleep_flash(pins: dict) -> None:
|
|||||||
flasher.gpio_set_direction(pins['sck'], True)
|
flasher.gpio_set_direction(pins['sck'], True)
|
||||||
flasher.gpio_set_direction(pins['miso'], True)
|
flasher.gpio_set_direction(pins['miso'], True)
|
||||||
|
|
||||||
flasher.spi_pins_set(
|
flasher.spi_configure(
|
||||||
pins['sck'],
|
pins['sck'],
|
||||||
pins['ss'],
|
pins['ss'],
|
||||||
pins['miso'],
|
pins['miso'],
|
||||||
pins['mosi'],
|
pins['mosi'],
|
||||||
12
|
spi_speed
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sleep(0.5)
|
||||||
|
|
||||||
|
# Wake the flash up
|
||||||
flasher.spi_write(bytes([0xAB]))
|
flasher.spi_write(bytes([0xAB]))
|
||||||
|
|
||||||
# Confirm we can talk to flash
|
# Confirm we can talk to flash
|
||||||
data = flasher.spi_rxtx(bytes([0x9f, 0, 0]))
|
data = flasher.spi_rxtx(bytes([0x9f, 0, 0]))
|
||||||
|
|
||||||
print('flash ID while awake:', ' '.join(
|
assert_bytes_equal('flash_id', bytes([0xff, 0xef, 0x40]), data)
|
||||||
[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])
|
|
||||||
|
|
||||||
# put the flash to sleep
|
# put the flash to sleep
|
||||||
flasher.spi_write(bytes([0xb9]))
|
flasher.spi_write(bytes([0xb9]))
|
||||||
@ -616,9 +616,7 @@ def sleep_flash(pins: dict) -> None:
|
|||||||
# Confirm flash is asleep
|
# Confirm flash is asleep
|
||||||
data = flasher.spi_rxtx(bytes([0x9f, 0, 0]))
|
data = flasher.spi_rxtx(bytes([0x9f, 0, 0]))
|
||||||
|
|
||||||
print('flash ID while asleep:', ' '.join(
|
assert_bytes_equal('flash_sleep', bytes([0xff, 0xff, 0xff]), data)
|
||||||
[f'{b:02x}' for b in data]))
|
|
||||||
assert data == bytes([0xff, 0xff, 0xff])
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@ -718,11 +716,11 @@ if __name__ == "__main__":
|
|||||||
}
|
}
|
||||||
|
|
||||||
if args.sleep_flash:
|
if args.sleep_flash:
|
||||||
sleep_flash(tp1_pins)
|
sleep_flash(tp1_pins, args.spi_speed)
|
||||||
|
|
||||||
nvcm = Nvcm(
|
nvcm = Nvcm(
|
||||||
tp1_pins,
|
tp1_pins,
|
||||||
spi_speed=args.spi_speed,
|
args.spi_speed,
|
||||||
debug=args.verbose)
|
debug=args.verbose)
|
||||||
nvcm.power_on()
|
nvcm.power_on()
|
||||||
|
|
||||||
@ -737,12 +735,12 @@ if __name__ == "__main__":
|
|||||||
with open(args.write_file, "rb") as in_file:
|
with open(args.write_file, "rb") as in_file:
|
||||||
bitstream = in_file.read()
|
bitstream = in_file.read()
|
||||||
print(f"read {len(bitstream)} bytes")
|
print(f"read {len(bitstream)} bytes")
|
||||||
cmds = icebin2nvcm(bitstream)
|
cmds = pybin2nvcm(bitstream)
|
||||||
|
|
||||||
if not args.ignore_blank:
|
if not args.ignore_blank:
|
||||||
nvcm.trim_blank_check()
|
nvcm.trim_blank_check()
|
||||||
# how much should we check?
|
# how much should we check?
|
||||||
nvcm.blank_check(0x100)
|
nvcm.blank_check(100000)
|
||||||
|
|
||||||
# this is it!
|
# this is it!
|
||||||
nvcm.program(cmds)
|
nvcm.program(cmds)
|
@ -1,6 +1,5 @@
|
|||||||
libusb1==3.0.0
|
libusb1==3.0.0
|
||||||
pyusb==1.2.1
|
pyusb==1.2.1
|
||||||
numpy==1.23.4
|
|
||||||
pyserial==3.5
|
pyserial==3.5
|
||||||
autopep8==2.0.1
|
autopep8==2.0.1
|
||||||
mypy==1.0.1
|
mypy==1.0.1
|
||||||
|
@ -19,4 +19,4 @@ else
|
|||||||
. ./venv/bin/activate
|
. ./venv/bin/activate
|
||||||
fi
|
fi
|
||||||
|
|
||||||
./production_test.py
|
./production_test_runner.py
|
||||||
|
@ -27,17 +27,16 @@ import usb1 # type: ignore
|
|||||||
class IceFlasher:
|
class IceFlasher:
|
||||||
""" iCE40 programming tool based on an RPi Pico """
|
""" iCE40 programming tool based on an RPi Pico """
|
||||||
|
|
||||||
FLASHER_REQUEST_LED_SET = 0x00
|
COMMAND_PIN_DIRECTION = 0x30
|
||||||
FLASHER_REQUEST_PIN_DIRECTION_SET = 0x10
|
COMMAND_PULLUPS = 0x31
|
||||||
FLASHER_REQUEST_PULLUPS_SET = 0x12
|
COMMAND_PIN_VALUES = 0x32
|
||||||
FLASHER_REQUEST_PIN_VALUES_SET = 0x20
|
|
||||||
FLASHER_REQUEST_PIN_VALUES_GET = 0x30
|
COMMAND_SPI_CONFIGURE = 0x40
|
||||||
FLASHER_REQUEST_SPI_BITBANG_CS = 0x41
|
COMMAND_SPI_XFER = 0x41
|
||||||
FLASHER_REQUEST_SPI_BITBANG_NO_CS = 0x42
|
COMMAND_SPI_CLKOUT = 0x42
|
||||||
FLASHER_REQUEST_SPI_PINS_SET = 0x43
|
|
||||||
FLASHER_REQUEST_SPI_CLKOUT = 0x44
|
COMMAND_ADC_READ = 0x50
|
||||||
FLASHER_REQUEST_ADC_READ = 0x50
|
COMMAND_BOOTLOADER = 0xE0
|
||||||
FLASHER_REQUEST_BOOTLOADER = 0xFF
|
|
||||||
|
|
||||||
SPI_MAX_TRANSFER_SIZE = 2048 - 8
|
SPI_MAX_TRANSFER_SIZE = 2048 - 8
|
||||||
|
|
||||||
@ -56,9 +55,29 @@ class IceFlasher:
|
|||||||
# Device not present, or user is not allowed to access
|
# Device not present, or user is not allowed to access
|
||||||
# device.
|
# device.
|
||||||
raise ValueError('Device not found')
|
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)
|
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:
|
def _wait_async(self) -> None:
|
||||||
|
# Wait until all submitted transfers can be cleared
|
||||||
while any(transfer.isSubmitted()
|
while any(transfer.isSubmitted()
|
||||||
for transfer in self.transfer_list):
|
for transfer in self.transfer_list):
|
||||||
try:
|
try:
|
||||||
@ -75,7 +94,13 @@ class IceFlasher:
|
|||||||
transfer.getStatus(),
|
transfer.getStatus(),
|
||||||
usb1.TRANSFER_COMPLETED)
|
usb1.TRANSFER_COMPLETED)
|
||||||
|
|
||||||
def _write(self, request_id: int, data: bytes) -> None:
|
def _write(
|
||||||
|
self,
|
||||||
|
request_id: int,
|
||||||
|
data: bytes,
|
||||||
|
nonblocking: bool = False) -> None:
|
||||||
|
|
||||||
|
if nonblocking:
|
||||||
transfer = self.handle.getTransfer()
|
transfer = self.handle.getTransfer()
|
||||||
transfer.setControl(
|
transfer.setControl(
|
||||||
# usb1.ENDPOINT_OUT | usb1.TYPE_VENDOR |
|
# usb1.ENDPOINT_OUT | usb1.TYPE_VENDOR |
|
||||||
@ -91,11 +116,14 @@ class IceFlasher:
|
|||||||
)
|
)
|
||||||
transfer.submit()
|
transfer.submit()
|
||||||
self.transfer_list.append(transfer)
|
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:
|
def _read(self, request_id: int, length: int) -> bytes:
|
||||||
self._wait_async()
|
# self._wait_async()
|
||||||
return self.handle.controlRead(
|
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:
|
def gpio_set_direction(self, pin: int, direction: bool) -> None:
|
||||||
"""Set the direction of a single GPIO pin
|
"""Set the direction of a single GPIO pin
|
||||||
@ -109,7 +137,7 @@ class IceFlasher:
|
|||||||
((1 if direction else 0) << pin),
|
((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(
|
def gpio_set_pulls(
|
||||||
self,
|
self,
|
||||||
@ -129,7 +157,7 @@ class IceFlasher:
|
|||||||
((1 if pulldown else 0) << pin),
|
((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:
|
def gpio_put(self, pin: int, val: bool) -> None:
|
||||||
"""Set the output level of a single GPIO pin
|
"""Set the output level of a single GPIO pin
|
||||||
@ -143,11 +171,11 @@ class IceFlasher:
|
|||||||
(1 if val else 0) << pin,
|
(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:
|
def gpio_get_all(self) -> int:
|
||||||
"""Read the input levels of all GPIO pins"""
|
"""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)
|
[gpio_states] = struct.unpack('>I', msg_in)
|
||||||
|
|
||||||
return gpio_states
|
return gpio_states
|
||||||
@ -162,7 +190,7 @@ class IceFlasher:
|
|||||||
|
|
||||||
return ((gpio_states >> pin) & 0x01) == 0x01
|
return ((gpio_states >> pin) & 0x01) == 0x01
|
||||||
|
|
||||||
def spi_pins_set(
|
def spi_configure(
|
||||||
self,
|
self,
|
||||||
sck_pin: int,
|
sck_pin: int,
|
||||||
cs_pin: int,
|
cs_pin: int,
|
||||||
@ -187,7 +215,9 @@ class IceFlasher:
|
|||||||
msg = bytearray()
|
msg = bytearray()
|
||||||
msg.extend(header)
|
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(
|
def spi_write(
|
||||||
self,
|
self,
|
||||||
@ -199,13 +229,7 @@ class IceFlasher:
|
|||||||
buf -- Byte buffer to send.
|
buf -- Byte buffer to send.
|
||||||
toggle_cs -- (Optional) If true, toggle the CS line
|
toggle_cs -- (Optional) If true, toggle the CS line
|
||||||
"""
|
"""
|
||||||
max_chunk_size = self.SPI_MAX_TRANSFER_SIZE
|
self._spi_xfer(buf, toggle_cs, False)
|
||||||
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)
|
|
||||||
|
|
||||||
def spi_rxtx(
|
def spi_rxtx(
|
||||||
self,
|
self,
|
||||||
@ -218,23 +242,43 @@ class IceFlasher:
|
|||||||
toggle_cs -- (Optional) If true, toggle the CS line
|
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()
|
ret = bytearray()
|
||||||
|
|
||||||
max_chunk_size = self.SPI_MAX_TRANSFER_SIZE
|
if len(buf) <= self.SPI_MAX_TRANSFER_SIZE:
|
||||||
for i in range(0, len(buf), max_chunk_size):
|
return self._spi_xfer_inner(
|
||||||
chunk = buf[i:i + max_chunk_size]
|
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(
|
ret.extend(
|
||||||
self._spi_bitbang_inner(
|
self._spi_xfer_inner(
|
||||||
buf=chunk,
|
chunk,
|
||||||
toggle_cs=toggle_cs))
|
False,
|
||||||
|
read_after_write))
|
||||||
|
|
||||||
|
if toggle_cs:
|
||||||
|
self.gpio_put(self.cs_pin, True)
|
||||||
|
|
||||||
return bytes(ret)
|
return bytes(ret)
|
||||||
|
|
||||||
def _spi_bitbang_inner(
|
def _spi_xfer_inner(
|
||||||
self,
|
self,
|
||||||
buf: bytes,
|
buf: bytes,
|
||||||
toggle_cs: bool = True,
|
toggle_cs: bool,
|
||||||
read_after_write: bool = True) -> bytes:
|
read_after_write: bool) -> bytes:
|
||||||
"""Bitbang a SPI transfer using the specificed GPIO pins
|
"""Bitbang a SPI transfer using the specificed GPIO pins
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
@ -247,23 +291,18 @@ class IceFlasher:
|
|||||||
'Message too large, '
|
'Message too large, '
|
||||||
+ f'size:{len(buf)} max:{self.SPI_MAX_TRANSFER_SIZE}')
|
+ 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 = bytearray()
|
||||||
msg.extend(header)
|
msg.extend(header)
|
||||||
msg.extend(buf)
|
msg.extend(buf)
|
||||||
|
|
||||||
if toggle_cs:
|
self._write(self.COMMAND_SPI_XFER, msg)
|
||||||
cmd = self.FLASHER_REQUEST_SPI_BITBANG_CS
|
|
||||||
else:
|
|
||||||
cmd = self.FLASHER_REQUEST_SPI_BITBANG_NO_CS
|
|
||||||
|
|
||||||
self._write(cmd, msg)
|
|
||||||
|
|
||||||
if not read_after_write:
|
if not read_after_write:
|
||||||
return bytes()
|
return bytes()
|
||||||
|
|
||||||
msg_in = self._read(
|
msg_in = self._read(
|
||||||
self.FLASHER_REQUEST_SPI_BITBANG_CS,
|
self.COMMAND_SPI_XFER,
|
||||||
len(buf))
|
len(buf))
|
||||||
|
|
||||||
return msg_in
|
return msg_in
|
||||||
@ -283,7 +322,7 @@ class IceFlasher:
|
|||||||
msg = bytearray()
|
msg = bytearray()
|
||||||
msg.extend(header)
|
msg.extend(header)
|
||||||
self._write(
|
self._write(
|
||||||
self.FLASHER_REQUEST_SPI_CLKOUT,
|
self.COMMAND_SPI_CLKOUT,
|
||||||
msg)
|
msg)
|
||||||
|
|
||||||
def adc_read_all(self) -> tuple[float, float, float]:
|
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
|
The firmware will read the values for each input multiple
|
||||||
times, and return averaged values for each input.
|
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)
|
[ch0, ch1, ch2] = struct.unpack('>III', msg_in)
|
||||||
|
|
||||||
return ch0 / 1000000, ch1 / 1000000, ch2 / 1000000
|
return ch0 / 1000000, ch1 / 1000000, ch2 / 1000000
|
||||||
@ -304,12 +343,6 @@ class IceFlasher:
|
|||||||
picotool, or by copying a file to the uf2 drive.
|
picotool, or by copying a file to the uf2 drive.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self._write(self.FLASHER_REQUEST_BOOTLOADER, bytes())
|
self._write(self.COMMAND_BOOTLOADER, bytes())
|
||||||
except usb1.USBErrorIO:
|
except usb1.USBErrorIO:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
flasher = IceFlasher()
|
|
||||||
|
|
||||||
flasher.spi_pins_set(1, 2, 3, 4, 15)
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user