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 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

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 """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:

View File

@ -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)

View File

@ -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

View File

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

View File

@ -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)