Remove production_test files

production_test related files are moved out of this repository, since it
relates to production of the hardware and not the fpga construction or
firmware.
This commit is contained in:
Daniel Jobson 2024-10-14 16:06:00 +02:00
parent a32ecade54
commit 69ef6dde8b
No known key found for this signature in database
GPG Key ID: 3707A9DBF4BB8F1A
30 changed files with 0 additions and 14074 deletions

View File

@ -99,10 +99,6 @@ jobs:
run: |
git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: make production test gateware
working-directory: hw/production_test/application_fpga_test_gateware
run: make
- name: make application FPGA gateware
working-directory: hw/application_fpga
run: make all

View File

@ -1,2 +0,0 @@
venv
wipedonce

View File

@ -1,35 +0,0 @@
SHELL := /bin/bash
PYTHON_FILES = \
encode_usb_strings.py \
production_test_runner.py \
production_tests.py \
pybin2nvcm.py \
pynvcm.py \
reset.py \
iceflasher.py
# autopep8: Fixes simple format errors automatically
# mypy: static type hint analysis
# pylint: pep8 and static code analysis
lint:
autopep8 --in-place --max-line-length 70 --aggressive --aggressive ${PYTHON_FILES}
mypy --disallow-untyped-defs ${PYTHON_FILES}
pylint --generated-member=usb1.TRANSFER_COMPLETED,usb1.USBErrorInterrupted,usb1.USBErrorIO --max-line-length 70 ${PYTHON_FILES}
# Check that the NVCM generator gives a correct output for a known binary
verify-pybin2nvcm:
./pybin2nvcm.py nvcm_test/application_fpga.bin verify.nvcm
cmp verify.nvcm nvcm_test/application_fpga.nvcm
verify-nvcm:
time ./pynvcm.py --verify nvcm_test/application_fpga.bin
program-nvcm-danger:
./pynvcm.py -i
time ./pynvcm.py --my-design-is-good-enough --ignore-blank --write ../application_fpga/application_fpga.bin --verify nvcm_test/application_fpga.bin
./pynvcm.py -b
randomize-production-test:
./production_tests.py

View File

@ -1,336 +0,0 @@
# TK-1 and TP-1 production tests
Production tests for the TK-1 and TP-1 PCBs
## Usage
These instructions are tested on Ubuntu 22.10.
Set up a python virtualenv to run the production test:
sudo apt install python3.10-venv
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
deactivate
To run the production test script:
source venv/bin/activate
./production_test.py
The script will then print a menu with all available tests:
Tillitis TK-1 and TP-1 Production tests
=== Test sequences ===
1. tk1_test_sequence: voltage_test, flash_validate_id, flash_program, sleep_2, test_extra_io, ch552_program, test_txrx_touchpad
2. tp1_test_sequence: program_pico, sleep_2, flash_validate_id
3. mta1_usb_v1_programmer_test_sequence: program_pico, sleep_2, voltage_test, flash_validate_id, sleep_2, test_extra_io
=== Manual tests ===
4. program_pico: Load the ice40 flasher firmware onto the TP-1
5. voltage_test: Measure 3.3V 2.5V, and 1.2V voltage rails on the TK-1
6. flash_validate_id: Read the ID from TK-1 SPI flash, and verify that it matches the expected value
7. flash_program: Program and verify the TK-1 SPI flash with the application test gateware
8. flash_check: Verify the TK-1 SPI flash is programmed with the application test gateware
9. test_extra_io: Test the TK-1 RTS, CTS, and GPIO1-4 lines by measuring a test pattern generated by the app_test gateware
10. ch552_program: Load the CDC ACM firmware onto a CH552 with a randomly generated serial number, and verify that it boots correctly
11. test_txrx_touchpad: Test UART communication, RGB LED, and touchpad by asking the operator to interact with the touch pad
12. enable_power: Enable power to the TK-1
13. disable_power: Disable power to the TK-1
Please type an option number and press return:
There are two types of tests listed: test sequences, which are used
to run full production tests, and manual tests, which are used to
test a single subcircuit. It is recommended to test all boards using
a test sequence first, and to use the manual tests for diagnosing
issues with individual boards, or for re-testing repaired boards.
## Test Sequences
These sequences are used as production tests for the TK-1 and TP-1.
### TK-1 production test (tk1_test_sequence)
This test checks all major subcircuits on the TK-1, and is used to
verify that the PCBA was assembled correctly. It should be run on all
newly assembled TK-1 boards.
Requirements:
* Programmed MTA1_USB_V1_Programmer board, fitted with a wider (green)
plastic clip
* Unprogrammed TK-1
* USB micro cable to attach TP-1 board to computer
* USB C extension cable to attach TK-1 to computer
It runs the following tests, in order:
1. voltage_test
2. flash_validate_id
3. flash_program
4. sleep_2
5. test_extra_io
6. ch552_program
7. test_txrx_touchpad
Note: If the CH552 has been programmed already, then the test
sequence will fail. In that case, manually run the other tests
in the sequence, skipping the 'ch554_program' test.
### TP-1 (tp1_test_sequence)
This test programs a TP-1, then tests that it can program a TK-1.
It should be run on all newly assembled TP-1 boards.
Requirements:
* Unprogrammed TP-1
* TK-1 programmed with the application test gateware
* USB micro cable to attach TP-1 board to computer
The TP-1 production test runs the following tests, in order:
1. program_pico
2. sleep_2
3. flash_validate_id
### MTA1_USB_V1_Programmer (mta1_usb_v1_programmer_test_sequence)
Requirements:
* Unprogrammed MTA1_USB_V1_Programmer
* TK-1 programmed with the application test gateware
* USB micro cable to attach MTA1_USB_V1_Programmer board to computer
The TP-1 production test runs the following tests, in order:
1. program_pico
2. sleep_2
3. voltage_test
4. flash_validate_id
5. sleep_2
6. test_extra_io
## Individual tests
These tests target a specific sub-circuit or funcationality on the
TP-1 and TK-1. Each test is designed to be run successfully in
isolation.
### program_pico: Load the ice40 flasher firmware onto the TP-1
This test loads the ice40_flasher firmware into the TP-1.
Usage instructions:
1. Attach an unprogrammed TP-1 to the computer using a micro USB
cable.
2. Run the test.
3. The test program will copy the firmware onto the TP-1.
4. Once the firmware is copied, the TP-1 will automatically reset,
and re-initialize as a ice40_flasher.
Notes:
* This test assumes that the computer is configured to auto-mount USB
storage devices, and that the Pico will be mounted to
/media/lab/RPI-RP2. This is true for a computer runnnig Ubuntu 22.10
when a user is logged into a Gnome GUI session. The script will need
to be adjusted for other environments.
### voltage_test: Measure 3.3V 2.5V, and 1.2V voltage rails on the TK-1
This test uses ADC in the Pico to measure the power supplies on the
TK-1. It samples the voltage of each power supply multiple times,
averages the result, and verifies that they are within +/-0.2V of the
specification.
Usage instructions:
1. Attach a programmed/tested MTA1_USB_V1_Programmer to the computer
using a micro USB cable.
2. Place a TK-1 into the MTA1_USB_V1_Programmer. The TK-1 can be
programmed or unprogrammed.
3. Run the test.
4. The test will use the MTA1_USB_V1_Programmer to power on the TK-1,
measure the voltage rails, then power off the TK-1.
5. The test will report the measurements and pass/fail metric.
Notes:
* The accuracy of the ADC is poor; external hardware would be
required to do a more extensive test. The power supplies used are
all fixed-voltage devices, so the chance of of an off-spec (but
still working) device is considered to be low.
* This test does not verify that the power sequencing is correct.
### flash_validate_id: Read the ID from TK-1 SPI flash, and verify that it's not all 0's or 1's
This test uses the TP-1 or MTA1_USB_V1_Programmer to read a TK-1 SPI
flash ID. It can be used to quickly check if a TK-1 device is
inserted properly into the programmger.
Usage instructions:
1. Attach a programmed/tested MTA1_USB_V1_Programmer or TP-1 to the
computer using a micro USB cable.
2. Place a TK-1 into the MTA1_USB_V1_Programmer. The TK-1 can be
programmed or unprogrammed.
3. Run the test.
4. The test will use the programmer to power on the TP-1, read out
the SPI flash ID, then power off the TK-1.
5. The test will check if the flash ID matches any known valid flash
ID types.
6. If the flash ID matches an known value, the test will print the
type and return a pass metric.
7. If the flash ID does not match a known value, the test will print
the type and return a pass metric.
Notes:
* An earlier version of this test just checked if the flash ID was
0x00000000 or 0xFFFFFFFF; this version is more exact.
### flash_program: Program and verify the TK-1 SPI flash with the application test gateware
This test uses the TP-1 or MTA1_USB_V1_Programmer to write the
application test gateware a TK-1 SPI flash. This gateware is needed
to run the test_extra_io and test_txrx_touchpad tests. The test uses
the external iceprog utility to perform the flash operation.
Usage instructions:
1. Attach a programmed/tested MTA1_USB_V1_Programmer or TP-1 to the
computer using a micro USB cable.
2. Place a TK-1 into the MTA1_USB_V1_Programmer. The TK-1 can be
programmed or unprogrammed.
3. Run the test.
4. The test will use the programmer to power on the TP-1, program the
SPI flash with the gateware, verify the flash by reading the data
back out, then power off the TK-1.
5. The test will report a pass/fail metric vased on the result of the
verification phase.
### flash_check: Verify the TK-1 SPI flash is programmed with the application test gateware
This test uses the TP-1 or MTA1_USB_V1_Programmer to verify that the
application test gateware is written to a TK-1 SPI flash. The test
uses the external iceprog utility to perform the verification
operation.
Usage instructions:
1. Attach a programmed/tested MTA1_USB_V1_Programmer or TP-1 to the
computer using a micro USB cable.
2. Place a programmed TK-1 into the MTA1_USB_V1_Programmer.
3. Run the test.
4. The test will use the programmer to power on the TP-1, verify the
flash by reading the data back out, then power off the TK-1.
5. The test will report a pass/fail metric vased on the result of the
verification phase.
### test_extra_io: Test the TK-1 RTS, CTS, and GPIO1-4 lines by measuring a test pattern generated by the app_test gateware
This test uses MTA1_USB_V1_Programmer to verify that the RTS, CTS, and
GPIO1-4 lines are connected correctly to the ICE40 FPGA.
On the FPGA side, the application gateware implements a simple state
machine for this test. The RTS line is configured as an input, and
the CTS, GPIO1, GPIO2, GPIO3, and GPIO4 lines are configured as
outputs. The values of the outputs are configured so that only one
output is high at a time, while the rest are low. After reset, the
GPIO4 line is high. Each time the RTS line is toggled, the next
output on the list is set high.
On the programmer side, the Pico GPIO pin connected to the TK-1 RTS
line is configured as an output, and the Pico GPIO pins connected to
the other TK-1 line are configured as inputs. The pico checks that
each input line is working by cycling the RTS output high and low,
then reading the values of each input. This process is repeated 5
times until all output lines are measured, then the results are
compared to a table of expected values.
Usage instructions:
1. Attach a programmed/tested MTA1_USB_V1_Programmer or TP-1 to the
computer using a micro USB cable.
2. Place a programmed TK-1 into the MTA1_USB_V1_Programmer.
3. Run the test.
4. The test will use the programmer to power on the TP-1, run the IO
test, then power off the TK-1.
5. The test will report a pass/fail metric vased on the result of the
test.
### ch552_program: Load the CDC ACM firmware onto a CH552 with a randomly generated serial number, and verify that it boots correctly
TODO
### test_txrx_touchpad: Test UART communication, RGB LED, and touchpad by asking the operator to interact with the touch pad
TODO
### enable_power: Enable power to the TK-1
This test uses the TP-1 or MTA1_USB_V1_Programmer to enable power to
an attached TK-1. This isn't a functional test, but can be used for
manual testing such as measuring voltage rails with a multimeter,
probing clock signals with an oscilloscope, etc.
Usage instructions:
1. Attach a programmed/tested MTA1_USB_V1_Programmer or TP-1 to the
computer using a micro USB cable.
2. Place a TK-1 into the MTA1_USB_V1_Programmer. The TK-1 can be
programmed or unprogrammed.
3. Run the test.
4. The test will use the programmer to power on the TP-1
5. The test will report a pass metric if the command completed
successfully.
### disable_power: Disable power to the TK-1
This test uses the TP-1 or MTA1_USB_V1_Programmer to disable power to
an attached TK-1. This isn't a functional test, but can be used after
the enable_power command was used to turn on a device.
Usage instructions:
1. Attach a programmed/tested MTA1_USB_V1_Programmer or TP-1 to the
computer using a micro USB cable.
2. Place a TK-1 into the MTA1_USB_V1_Programmer. The TK-1 can be
programmed or unprogrammed.
3. Run the test.
4. The test will use the programmer to power off the TP-1
5. The test will report a pass metric if the command completed
successfully.
## Firmware binaries
To make the test environment easier to set up, some pre-compiled
binares are included in the binaries/ subdirectory. These can also be
built from source, by following the below instructions.
Before building the firmware, follow the [toolchain setup](https://github.com/tillitis/tillitis-key1/blob/main/doc/toolchain_setup.md)
instructions.
### CH552 firmware
See the build instructions in the
[ch552_fw](../usb_interface/ch552_fw/README.md) directory.
### Test Application gateware
This uses the Symbiflow toolchain:
cd ~/tillitis-key1/hw/production_test/application_fpga_test_gateware
make
cp top.bin ../binaries/
### TP-1 Raspberry Pi Pico firmware
Follow the instructions in the [ice40_flasher](https://github.com/Blinkinlabs/ice40_flasher#building-the-firmware)
repository, then copy the output 'main.uf2' file to the binaries
directory.

View File

@ -1,55 +0,0 @@
TARGET = top
VERILOG_FILES = \
top.v
PIN_CONFIG_FILE = app.pcf
default: $(TARGET).bin
$(TARGET).json: $(VERILOG_FILES)
yosys \
-q \
-p "synth_ice40 -abc2 -relut -top ${TARGET} -json $(TARGET).json" \
-l $(TARGET)-yosys.log \
$(VERILOG_FILES)
TARGET_MODULE = top
view-ideal:
yosys -p 'read_verilog ${VERILOG_FILES}; proc; opt; select ${TARGET_MODULE}; show -format dot -viewer xdot -pause' &
view-real:
yosys -p 'read_verilog ${VERILOG_FILES}; proc; opt; synth_ice40; show -format dot -viewer xdot -pause' &
$(TARGET).asc: $(TARGET).json $(PIN_CONFIG_FILE)
nextpnr-ice40 \
--up5k \
--package sg48 \
--json $(TARGET).json \
--pcf $(PIN_CONFIG_FILE) \
--asc $(TARGET).asc \
-l $(TARGET)-nextpnr.log
$(TARGET).bin: $(TARGET).asc
icepack $(TARGET).asc $(TARGET).bin
stats: $(TARGET).json $(TARGET).asc
sed -n '/=== top ===/,/6\.28/p' $(TARGET)-yosys.log
sed -n '/Info: Device utilisation/,/Info: Placed/p' $(TARGET)-nextpnr.log
fgrep 'Info: Max frequency for clock' $(TARGET)-nextpnr.log
lint: $(VERILOG_FILES)
verilator --lint-only -Wall -Wno-DECLFILENAME -Ihw_blocks $(VERILOG_FILES)
.PHONY: flash
flash: ${TARGET}.bin
iceprog ${TARGET}.bin
.PHONY: clean
clean:
$(RM) -f \
$(TARGET).json \
$(TARGET).asc \
$(TARGET)-yosys.log \
$(TARGET)-nextpnr.log \
$(TARGET).bin

View File

@ -1,17 +0,0 @@
module SB_HFOSC #(
parameter CLKHF_DIV
) (
input CLKHFPU,
input CLKHFEN,
output reg CLKHF
);
// Nonfunctional, for linting only.
always @(*) begin
CLKHF = (CLKHFPU & CLKHFEN);
end
endmodule

View File

@ -1,34 +0,0 @@
module SB_RGBA_DRV #(
parameter CURRENT_MODE,
parameter RGB0_CURRENT,
parameter RGB1_CURRENT,
parameter RGB2_CURRENT
) (
output reg RGB0,
output reg RGB1,
output reg RGB2,
input RGBLEDEN,
input RGB0PWM,
input RGB1PWM,
input RGB2PWM,
input CURREN
);
// Nonfunctional, for linting only.
always @(*) begin
RGB0 = (RGBLEDEN & CURREN & RGB0PWM);
end
always @(*) begin
RGB1 = (RGBLEDEN & CURREN & RGB1PWM);
end
always @(*) begin
RGB2 = (RGBLEDEN & CURREN & RGB2PWM);
end
endmodule

View File

@ -1,18 +0,0 @@
# Host Communication
set_io RX 25
set_io TX 26
set_io CTS 27
set_io RTS 28
# Expansion
set_io APP_GPIO1 36
set_io APP_GPIO2 38
set_io APP_GPIO3 45
set_io APP_GPIO4 46
## User IO
set_io TOUCH_EVENT 6
set_io RGB0 39
set_io RGB1 40
set_io RGB2 41

View File

@ -1,154 +0,0 @@
module top (
/* verilator lint_off UNUSED */
input RX,
/* verilator lint_on UNUSED */
output reg TX,
input RTS,
output CTS,
output APP_GPIO1,
output APP_GPIO2,
output APP_GPIO3,
output APP_GPIO4,
input TOUCH_EVENT,
output RGB0,
output RGB1,
output RGB2
);
//############ Clock / Reset ############################################
wire clk;
// Configure the HFOSC
SB_HFOSC #(
.CLKHF_DIV("0b01") // 00: 48MHz, 01: 24MHz, 10: 12MHz, 11: 6MHz
) u_hfosc (
.CLKHFPU(1'b1),
.CLKHFEN(1'b1),
.CLKHF(clk)
);
//############ Extra I/O test #####################################
reg [4:0] pintest;
initial begin
pintest = 5'h01;
end
assign {CTS, APP_GPIO1, APP_GPIO2, APP_GPIO3, APP_GPIO4} = pintest;
always @(posedge RTS) begin
case(pintest)
5'h1: pintest <= 5'h2;
5'h2: pintest <= 5'h4;
5'h4: pintest <= 5'h8;
5'h8: pintest <= 5'h10;
default: pintest <= 5'h01;
endcase
end
//############ LED pwm ##################################################
reg [24:0] led_divider;
reg [2:0] led_states;
initial begin
led_divider = 25'd0;
end
always @(posedge clk) begin
led_divider <= led_divider + 1;
case(led_divider[24:23])
2'h0: led_states <= 3'b100;
2'h1: led_states <= 3'b010;
2'h2: led_states <= 3'b001;
2'h3: led_states <= 3'b111;
endcase
end
SB_RGBA_DRV #(
.CURRENT_MODE("0b1"), // half-current mode
.RGB0_CURRENT("0b000001"), // 2 mA
.RGB1_CURRENT("0b000001"), // 2 mA
.RGB2_CURRENT("0b000001") // 2 mA
) RGBA_DRV (
.RGB0(RGB0),
.RGB1(RGB1),
.RGB2(RGB2),
.RGBLEDEN(~TOUCH_EVENT),
.RGB0PWM(led_states[2]),
.RGB1PWM(led_states[1]),
.RGB2PWM(led_states[0]),
.CURREN(1'b1)
);
//############ Serial output ############################################
reg [24:0] ticks; // about once a second
reg [7:0] touch_count;
reg [7:0] report_count;
initial begin
ticks = 25'd0;
touch_count = 8'd0;
report_count = 8'd0;
end
reg [19:0] uart_data;
reg [7:0] uart_bits_left;
reg [13:0] uart_count;
initial begin
TX = 1'b1;
end
reg touch_last;
reg tx_last;
initial begin
tx_last = 1;
end
always @(posedge clk) begin
// Sample + count touch events
touch_last <= TOUCH_EVENT;
tx_last <= TX;
if ((touch_last == 0) && (TOUCH_EVENT == 1)) begin
touch_count <= touch_count + 1;
end
// Create a ~1s delay between touch count reports
ticks <= ticks + 1;
TX <= uart_data[0];
// Periodically report the touch count
//if (ticks == 0) begin
if((TX == 0) && (tx_last == 1) && (uart_bits_left == 0)) begin
// LSB first: start bit, report count, stop bit, start bit, touch count, stop bit
uart_data <= {1'b1, touch_count[7:0], 1'b0, 1'b1, report_count[7:0], 1'b0};
uart_bits_left <= 8'd19;
uart_count <= 14'd2500;
report_count <= report_count + 1;
end
if (uart_bits_left > 0) begin
uart_count <= uart_count - 1;
if(uart_count == 0) begin
uart_data <= {1'b1, uart_data[19:1]};
uart_bits_left <= uart_bits_left - 1;
uart_count <= 14'd2500;
end
end
end
endmodule

Binary file not shown.

Binary file not shown.

View File

@ -1,100 +0,0 @@
#!/usr/bin/env python
import hid_test
def import_pins(pin_filename):
pins = {}
with open(pin_filename + '.csv','r') as f:
for line in f.readlines():
[gpio,name] = line.strip().split(',')
pins[name] = int(gpio)
return pins
def read_pin_states(pins):
d = hid_test.ice40_flasher()
# Set all pins as inputs, pull-down
for [name,gpio] in pins.items():
d.gpio_set_direction(gpio,False)
d.gpio_set_pulls(gpio,False,True)
# One by one, set each pin as an output, read the states of all pins, then set the pin as an input again.
changes = {}
for [name,gpio] in pins.items():
pre = d.gpio_get_all()
d.gpio_set_direction(gpio,True)
d.gpio_put(gpio,True)
mid = d.gpio_get_all()
d.gpio_set_direction(gpio,False)
post = d.gpio_get_all()
if (pre != 0) or (post != 0):
print('Error in pre/post condition, pin:{:} pre:{:08x} post:{:08x}'.format(name,pre,post))
change = []
for [n,g] in pins.items():
if (1<<g) & mid:
change.append(n)
changes[name] = change
# Set all pins as inputs with no pulls
for [name,gpio] in pins.items():
d.gpio_set_direction(gpio,False)
d.gpio_set_pulls(gpio,False,False)
return changes
def read_pin_changes(pin_filename):
changes_good = {}
with open(pin_filename + '-good.csv','r') as f:
for line in f.readlines():
[name,change] = line.strip().split(',')
# Handle empty change lists
if change == '':
changes_good[name] = []
else:
changes_good[name] = change.split(' ')
return changes_good
def save_pin_changes(pin_filename, changes):
with open(pin_filename + '-good.csv','w') as f:
for [pin, change] in changes.items():
f.write('{:},{:}\n'.format(pin, ' '.join([i for i in change])))
def check_changes(changes, changes_good):
issues = []
for [pin, change] in changes.items():
if change != changes_good[pin]:
issue = 'error:' + pin
#issue += ' got:[' + ','.join([i for i in change]) + ']'
#issue += ' expected:[' + ','.join([i for i in changes_good[pin]]) + ']'
issue += ' missing:[' + ','.join([x for x in changes_good[pin] if x not in change]) + ']'
issue += ' extra:[' + ','.join([x for x in change if x not in changes_good[pin]]) + ']'
issues.append(issue)
return issues
def record(pin_filename):
pins = import_pins(pin_filename)
changes = read_pin_states(pins)
save_pin_changes(pin_filename, changes)
def test(pin_filename):
pins = import_pins(pin_filename)
changes_good = read_pin_changes(pin_filename)
changes = read_pin_states(pins)
return check_changes(changes, changes_good)
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description='GPIO short connector test')
parser.add_argument('-pins', required=True, help='CSV file of pins to test')
parser.add_argument('-r', '--record', action='store_true', help='Set to record a known good board')
args = parser.parse_args()
if args.record:
record(args.pins)
else:
print('\n'.join(test(args.pins)))

View File

@ -1,78 +0,0 @@
#!/usr/bin/python
import tkinter as tk
from tkinter import filedialog as fd
from tkinter.messagebox import askyesno
import connected_test
import datetime
def handle_choose_file():
filetypes = (
('CSV files', '*.csv'),
)
filename = fd.askopenfilename(
title='Open a GPIO definition file',
initialdir='.',
filetypes=filetypes)
print(filename)
filename_entry.delete(0, 'end')
filename_entry.insert(0, filename)
def handle_record():
filename = filename_entry.get()
[filename,x] = filename.split('.')
answer = askyesno(title='Record GPIO states',
message='Record GPIO states? This will delete any previous state recordings')
if answer:
results_text.delete("1.0", "end") # if you want to remove the old data
results_text.insert('end',datetime.datetime.now())
results_text.insert('end',' recording gpio connections\n')
connected_test.record(filename)
changes_good = connected_test.read_pin_changes(filename)
for [pin, change] in changes_good.items():
results_text.insert('end','{:} [{:}]\n'.format(pin,','.join(change)))
def handle_test():
filename = filename_entry.get()
[filename,x] = filename.split('.')
results_text.delete("1.0", "end") # if you want to remove the old data
results_text.insert('end',datetime.datetime.now())
results_text.insert('end',' testing gpio connections\n')
results = connected_test.test(filename)
if results == []:
results = ['OK']
results_text.insert('end','\n'.join(results))
window = tk.Tk()
window.title('Pin change tester')
#window.geometry('1600x1200')
frame = tk.Frame()
# Results window will show the test result
results_text = tk.Text(master=frame, height=30,width=180)
results_text.pack()
choosefile_button = tk.Button(master=frame, text='Choose file', command=handle_choose_file)
choosefile_button.pack()
label = tk.Label(master=frame, text='filename:')
label.pack();
filename_entry = tk.Entry(master=frame, width=80)
filename_entry.pack();
record_button = tk.Button(master=frame, text='record', command=handle_record)
record_button.pack()
test_button = tk.Button(master=frame, text='test', command=handle_test)
test_button.pack()
frame.pack()
window.mainloop()

View File

@ -1,24 +0,0 @@
GND0,GND0 VCC0 VCC1 GND1 GND2 VCC2 B5 D+1 D-1 VCC3 GND3
A2,A2
A3,A3
VCC0,GND0 VCC0 VCC1 GND1 GND2 VCC2 B5 D+1 D-1 VCC3 GND3
A5,A5
D+0,D+0
D-0,D-0
A8,A8
VCC1,GND0 VCC0 VCC1 GND1 GND2 VCC2 B5 D+1 D-1 VCC3 GND3
A10,A10
A11,A11
GND1,GND0 VCC0 VCC1 GND1 GND2 VCC2 B5 D+1 D-1 VCC3 GND3
GND2,GND0 VCC0 VCC1 GND1 GND2 VCC2 B5 D+1 D-1 VCC3 GND3
B2,B2
B3,B3
VCC2,GND0 VCC0 VCC1 GND1 GND2 VCC2 B5 D+1 D-1 VCC3 GND3
B5,B5
D+1,GND0 VCC0 VCC1 GND1 GND2 VCC2 B5 D+1 D-1 VCC3 GND3
D-1,GND0 VCC0 VCC1 GND1 GND2 VCC2 B5 D+1 D-1 VCC3 GND3
B8,B8
VCC3,GND0 VCC0 VCC1 GND1 GND2 VCC2 B5 D+1 D-1 VCC3 GND3
B10,B10
B11,B11
GND3,GND0 VCC0 VCC1 GND1 GND2 VCC2 B5 D+1 D-1 VCC3 GND3
1 GND0 GND0 VCC0 VCC1 GND1 GND2 VCC2 B5 D+1 D-1 VCC3 GND3
2 A2 A2
3 A3 A3
4 VCC0 GND0 VCC0 VCC1 GND1 GND2 VCC2 B5 D+1 D-1 VCC3 GND3
5 A5 A5
6 D+0 D+0
7 D-0 D-0
8 A8 A8
9 VCC1 GND0 VCC0 VCC1 GND1 GND2 VCC2 B5 D+1 D-1 VCC3 GND3
10 A10 A10
11 A11 A11
12 GND1 GND0 VCC0 VCC1 GND1 GND2 VCC2 B5 D+1 D-1 VCC3 GND3
13 GND2 GND0 VCC0 VCC1 GND1 GND2 VCC2 B5 D+1 D-1 VCC3 GND3
14 B2 B2
15 B3 B3
16 VCC2 GND0 VCC0 VCC1 GND1 GND2 VCC2 B5 D+1 D-1 VCC3 GND3
17 B5 B5
18 D+1 GND0 VCC0 VCC1 GND1 GND2 VCC2 B5 D+1 D-1 VCC3 GND3
19 D-1 GND0 VCC0 VCC1 GND1 GND2 VCC2 B5 D+1 D-1 VCC3 GND3
20 B8 B8
21 VCC3 GND0 VCC0 VCC1 GND1 GND2 VCC2 B5 D+1 D-1 VCC3 GND3
22 B10 B10
23 B11 B11
24 GND3 GND0 VCC0 VCC1 GND1 GND2 VCC2 B5 D+1 D-1 VCC3 GND3

View File

@ -1,24 +0,0 @@
0,GND0
1,A2
2,A3
3,VCC0
4,A5
5,D+0
6,D-0
7,A8
8,VCC1
9,A10
10,A11
11,GND1
14,GND2
15,B2
16,B3
17,VCC2
18,B5
19,D+1
20,D-1
21,B8
22,VCC3
26,B10
27,B11
28,GND3
1 0 GND0
2 1 A2
3 2 A3
4 3 VCC0
5 4 A5
6 5 D+0
7 6 D-0
8 7 A8
9 8 VCC1
10 9 A10
11 10 A11
12 11 GND1
13 14 GND2
14 15 B2
15 16 B3
16 17 VCC2
17 18 B5
18 19 D+1
19 20 D-1
20 21 B8
21 22 VCC3
22 26 B10
23 27 B11
24 28 GND3

View File

@ -1,103 +0,0 @@
#!/usr/bin/env python
"""Convert python strings to UTF16 USB descriptors"""
def descriptor_to_string(descriptor: bytes) -> str:
""" Convert a USB string descriptor into a python string
Keyword arguments:
descriptor -- UTF-16 formatted USB descriptor string
"""
b_length = descriptor[0]
if b_length != len(descriptor):
raise ValueError(
'Length mismatch, ' +
f'length_field:{b_length} length:{len(descriptor)}')
b_descriptor_type = descriptor[1]
if b_descriptor_type != 0x03:
raise ValueError(
f'Type mismatch, bDescriptorType:{b_descriptor_type:02x}'
+ 'expected:0x03')
return descriptor[2:].decode('utf-16', errors='strict')
def string_to_descriptor(string: str) -> bytes:
""" Convert a python string into a USB string descriptor
Keyword arguments:
string: String to convert
"""
descriptor = bytearray()
descriptor.append(0x00) # placeholder for length
descriptor.append(0x03)
descriptor.extend(string.encode('utf-16')[2:]) # crop the BOM
descriptor[0] = len(descriptor)
return bytes(descriptor)
if __name__ == "__main__":
MANUFACTURER = 'Mullvad'
PRODUCT = 'MTA1-USB-V1'
SERIAL = "68de5d27-e223-4874-bc76-a54d6e84068f"
# serial = bytes([
# 0x14,0x03,
# 0x32,0x00,0x30,0x00,0x31,0x00,0x37,0x00,0x2D,0x00,
# 0x32,0x00,0x2D,0x00,
# 0x32,0x00,0x35,0x00
# ])
# print(descriptor_to_string(serial))
# sample_product = bytes([
# 0x14,0x03,
# 0x43,0x00,0x48,0x00,0x35,0x00,0x35,0x00,0x34,0x00,0x5F,0x00,
# 0x43,0x00,0x44,0x00,0x43,0x00
# ])
# print(descriptor_to_string(sample_product))
# rt = string_to_descriptor(descriptor_to_string(sample_product))
# print(descriptor_to_string(rt))
#
# print(['{:02x} '.format(i) for i in sample_product])
# print(['{:02x} '.format(i) for i in rt])
# sample_mfr = bytes([
# 0x0A,0x03,
# 0x5F,0x6c,0xCF,0x82,0x81,0x6c,0x52,0x60,
# ])
# print(descriptor_to_string(sample_mfr))
# rt = string_to_descriptor(descriptor_to_string(sample_mfr))
# print(descriptor_to_string(rt))
#
# print(['{:02x} '.format(i) for i in sample_mfr])
# print(['{:02x} '.format(i) for i in rt])
with open('usb_strings.h', 'w', encoding='utf-8') as f:
f.write('#ifndef USB_STRINGS\n')
f.write('#define USB_STRINGS\n')
f.write(
f'unsigned char __code Prod_Des[]={{ // "{PRODUCT}"\n')
f.write(' ')
f.write(', '.join([f'0x{i:02x}'
for i in string_to_descriptor(PRODUCT)]))
f.write('\n};\n')
f.write(
'unsigned char __code Manuf_Des[]={ ' +
f'// "{MANUFACTURER}"\n')
f.write(' ')
f.write(', '.join([f'0x{i:02x}'
for i in string_to_descriptor(MANUFACTURER)]))
f.write('\n};\n')
f.write(
f'unsigned char __code SerDes[]={{ // "{SERIAL}"\n')
f.write(' ')
f.write(', '.join([f'0x{i:02x}'
for i in string_to_descriptor(SERIAL)]))
f.write('\n};\n')
f.write('#endif\n')

View File

@ -1,370 +0,0 @@
#!/usr/bin/env python
"""IceFlasher, an iCE40 programming tool based on an RPi Pico"""
import struct
from typing import List, Any
import usb1 # type: ignore
# def processReceivedData(transfer):
# # print('got rx data',
# transfer.getStatus(),
# transfer.getActualLength())
#
# if transfer.getStatus() != usb1.TRANSFER_COMPLETED:
# # Transfer did not complete successfully, there is no
# # data to read. This example does not resubmit transfers
# # on errors. You may want to resubmit in some cases (timeout,
# # ...).
# return
# data = transfer.getBuffer()[:transfer.getActualLength()]
# # Process data...
# # Resubmit transfer once data is processed.
# transfer.submit()
class IceFlasher:
""" iCE40 programming tool based on an RPi Pico """
COMMAND_PIN_DIRECTION = 0x30
COMMAND_PULLUPS = 0x31
COMMAND_PIN_VALUES = 0x32
COMMAND_SPI_CONFIGURE = 0x40
COMMAND_SPI_XFER = 0x41
COMMAND_SPI_CLKOUT = 0x42
COMMAND_ADC_READ = 0x50
COMMAND_BOOTLOADER = 0xE0
SPI_MAX_TRANSFER_SIZE = 2048 - 8
handle = None
def _check_for_old_firmware(self) -> bool:
for device in self.context.getDeviceList():
if device.getVendorID() == 0xcafe and device.getProductID() == 0x4004:
return True
if device.getVendorID() == 0xcafe and device.getProductID() == 0x4010:
return True
return False
def __init__(self) -> None:
self.transfer_list: List[Any] = []
# See: https://github.com/vpelletier/python-libusb1#usage
self.context = usb1.USBContext()
try:
self.handle = self.context.openByVendorIDAndProductID(
0x1209,
0x8886,
skip_on_error=False
)
except usb1.USBErrorAccess as exp:
raise OSError('Programmer found, but unable to open- check device permissions!') from exp
if self.handle is None:
# Device not present, or user is not allowed to access
# device.
if self._check_for_old_firmware():
raise OSError(
'Programmer with outdated firmware found- please update!')
else:
raise OSError('Programmer not found- check USB cable')
# Check the device firmware version
bcd_device = self.handle.getDevice().getbcdDevice()
if bcd_device != 0x0200:
raise OSError(
'Pico firmware version out of date- please upgrade')
self.handle.claimInterface(0)
self.cs_pin = -1
def __del__(self) -> None:
self.close()
def close(self) -> None:
""" Release the USB device handle """
if self.handle is not None:
self._wait_async()
self.handle.close()
self.handle = None
self.context.close()
self.context = None
def _wait_async(self) -> None:
# Wait until all submitted transfers can be cleared
while any(transfer.isSubmitted()
for transfer in self.transfer_list):
try:
self.context.handleEvents()
except usb1.USBErrorInterrupted:
pass
for transfer in reversed(self.transfer_list):
if transfer.getStatus() == \
usb1.TRANSFER_COMPLETED:
self.transfer_list.remove(transfer)
else:
print(
transfer.getStatus(),
usb1.TRANSFER_COMPLETED)
def _write(
self,
request_id: int,
data: bytes,
nonblocking: bool = False) -> None:
if nonblocking:
transfer = self.handle.getTransfer()
transfer.setControl(
# usb1.ENDPOINT_OUT | usb1.TYPE_VENDOR |
# usb1.RECIPIENT_DEVICE, #request type
0x40,
request_id, # request
0, # index
0,
data, # data
callback=None, # callback functiopn
user_data=None, # userdata
timeout=1000
)
transfer.submit()
self.transfer_list.append(transfer)
else:
self.handle.controlWrite(
0x40, request_id, 0, 0, data, timeout=100)
def _read(self, request_id: int, length: int) -> bytes:
# self._wait_async()
return self.handle.controlRead(
0xC0, request_id, 0, 0, length, timeout=100)
def gpio_set_direction(self, pin: int, direction: bool) -> None:
"""Set the direction of a single GPIO pin
Keyword arguments:
pin -- GPIO pin number
value -- True: Set pin as output, False: set pin as input
"""
msg = struct.pack('>II',
(1 << pin),
((1 if direction else 0) << pin),
)
self._write(self.COMMAND_PIN_DIRECTION, msg)
def gpio_set_pulls(
self,
pin: int,
pullup: bool,
pulldown: bool) -> None:
"""Configure the pullup/down resistors for a single GPIO pin
Keyword arguments:
pin -- GPIO pin number
pullup -- True: Enable pullup, False: Disable pullup
pulldown -- True: Enable pulldown, False: Disable pulldown
"""
msg = struct.pack('>III',
(1 << pin),
((1 if pullup else 0) << pin),
((1 if pulldown else 0) << pin),
)
self._write(self.COMMAND_PULLUPS, msg)
def gpio_put(self, pin: int, val: bool) -> None:
"""Set the output level of a single GPIO pin
Keyword arguments:
pin -- GPIO pin number
val -- True: High, False: Low
"""
msg = struct.pack('>II',
1 << pin,
(1 if val else 0) << pin,
)
self._write(self.COMMAND_PIN_VALUES, msg)
def gpio_get_all(self) -> int:
"""Read the input levels of all GPIO pins"""
msg_in = self._read(self.COMMAND_PIN_VALUES, 4)
[gpio_states] = struct.unpack('>I', msg_in)
return gpio_states
def gpio_get(self, pin: int) -> bool:
"""Read the input level of a single GPIO pin
Keyword arguments:
pin -- GPIO pin number
"""
gpio_states = self.gpio_get_all()
return ((gpio_states >> pin) & 0x01) == 0x01
def spi_configure(
self,
sck_pin: int,
cs_pin: int,
mosi_pin: int,
miso_pin: int,
clock_speed: int) -> None:
"""Set the pins to use for SPI transfers
Keyword arguments:
sck_pin -- GPIO pin number to use as the SCK signal
cs_pin -- GPIO pin number to use as the CS signal
mosi_pin -- GPIO pin number to use as the MOSI signal
miso_pin -- GPIO pin number to use as the MISO signal
clock_speed -- SPI clock speed, in MHz
"""
header = struct.pack('>BBBBB',
sck_pin,
cs_pin,
mosi_pin,
miso_pin,
clock_speed)
msg = bytearray()
msg.extend(header)
self._write(self.COMMAND_SPI_CONFIGURE, msg)
self.cs_pin = cs_pin
def spi_write(
self,
buf: bytes,
toggle_cs: bool = True) -> None:
"""Write data to the SPI port
Keyword arguments:
buf -- Byte buffer to send.
toggle_cs -- (Optional) If true, toggle the CS line
"""
self._spi_xfer(buf, toggle_cs, False)
def spi_rxtx(
self,
buf: bytes,
toggle_cs: bool = True) -> bytes:
"""Bitbang a SPI transfer
Keyword arguments:
buf -- Byte buffer to send.
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()
if len(buf) <= self.SPI_MAX_TRANSFER_SIZE:
return self._spi_xfer_inner(
buf,
toggle_cs,
read_after_write)
if toggle_cs:
self.gpio_put(self.cs_pin, False)
for i in range(0, len(buf), self.SPI_MAX_TRANSFER_SIZE):
chunk = buf[i:i + self.SPI_MAX_TRANSFER_SIZE]
ret.extend(
self._spi_xfer_inner(
chunk,
False,
read_after_write))
if toggle_cs:
self.gpio_put(self.cs_pin, True)
return bytes(ret)
def _spi_xfer_inner(
self,
buf: bytes,
toggle_cs: bool,
read_after_write: bool) -> bytes:
"""Bitbang a SPI transfer using the specificed GPIO pins
Keyword arguments:
buf -- Byte buffer to send.
toggle_cs -- (Optional) If true, toggle the CS line
"""
if len(buf) > self.SPI_MAX_TRANSFER_SIZE:
raise ValueError(
'Message too large, '
+ f'size:{len(buf)} max:{self.SPI_MAX_TRANSFER_SIZE}')
header = struct.pack('>BI', toggle_cs, len(buf))
msg = bytearray()
msg.extend(header)
msg.extend(buf)
self._write(self.COMMAND_SPI_XFER, msg)
if not read_after_write:
return bytes()
msg_in = self._read(
self.COMMAND_SPI_XFER,
len(buf))
return msg_in
def spi_clk_out(self, byte_count: int) -> None:
"""Run the SPI clock without transferring data
This function is useful for SPI devices that need a clock to
advance their state machines.
Keyword arguments:
byte_count -- Number of bytes worth of clocks to send
"""
header = struct.pack('>I',
byte_count)
msg = bytearray()
msg.extend(header)
self._write(
self.COMMAND_SPI_CLKOUT,
msg)
def adc_read_all(self) -> tuple[float, float, float]:
"""Read the voltage values of ADC 0, 1, and 2
The firmware will read the values for each input multiple
times, and return averaged values for each input.
"""
msg_in = self._read(self.COMMAND_ADC_READ, 3 * 4)
[ch0, ch1, ch2] = struct.unpack('>III', msg_in)
return ch0 / 1000000, ch1 / 1000000, ch2 / 1000000
def bootloader(self) -> None:
"""Reset the programmer to bootloader mode
After the device is reset, it can be programmed using
picotool, or by copying a file to the uf2 drive.
"""
try:
self._write(self.COMMAND_BOOTLOADER, bytes())
except usb1.USBErrorIO:
pass

File diff suppressed because it is too large Load Diff

View File

@ -1,193 +0,0 @@
#!/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:
""" Run a test list
This executes the tests from the given list in order. The test
sequence is stopped if a test raises an assertion, or returns
False. If all tests return True successfully, then the test
sequence is considered to have passed.
Keyword parameters:
test_list -- List of test functions to call
"""
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 OSError as exp:
print(exp)
sys.exit(1)
except Exception as exp:
print(
'Error while running test step "{:}", exception:{:}'.format(
test.__name__, str(exp)))
return False
return True
def production_test_runner() -> None:
"""Interactive production test environment"""
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__))
sys.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:
raise ValueError('Test not found:{args.run_test}')
if not result:
production_tests.reset()
sys.exit(1)
sys.exit(0)
print('\n\nProduction test system')
last_a = '1'
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:
print('Invalid input')
continue
except ValueError:
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
if __name__ == '__main__':
import argparse
import sys
production_test_runner()

View File

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

View File

@ -1,84 +0,0 @@
#!/usr/bin/env python
""" bistream to NVCM command conversion is based on majbthrd's work
in https://github.com/YosysHQ/icestorm/pull/272
"""
def pybin2nvcm(bitstream: bytes) -> list[str]:
"""Convert an ice40 bitstream into an NVCM program
The NVCM format is a set of commands that are run against the
NVCM state machine, which instruct the state machine to write
the bitstream into the NVCM. It's somewhat convoluted!
Keyword arguments:
bitstream -- Bitstream to convert into NVCM format
"""
# ensure that the file starts with the correct bistream preamble
for origin in range(0, len(bitstream)):
if bitstream[origin:origin + 4] == bytes.fromhex('7EAA997E'):
break
if origin == len(bitstream):
raise ValueError('Preamble not found')
print(f'Found preamable at {origin:08x}')
# there might be stuff in the header with vendor tools,
# but not usually in icepack produced output, so ignore it for now
rows = []
rows.append('06')
for pos in range(origin, len(bitstream), 8):
row = bitstream[pos:pos + 8]
# pad out to 8-bytes
row += b'\0' * (8 - len(row))
if row == bytes(8):
# skip any all-zero entries in the bistream
continue
# NVCM addressing is very weird
addr = pos - origin
nvcm_addr = int(addr / 328) * 4096 + (addr % 328)
row_str = f'02{nvcm_addr:06x}{row.hex()}'
row_str = ' '.join([row_str[i:i + 2]
for i in range(0, len(row_str), 2)]) + ' '
rows.append(row_str)
rows.append('04')
return rows
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('infile',
type=str,
help='input bin file')
parser.add_argument('outfile',
type=str,
help='output nvcm file')
args = parser.parse_args()
with open(args.infile, 'rb') as f_in:
data = f_in.read()
cmds = pybin2nvcm(data)
with open(args.outfile, 'w', encoding='utf-8') as f_out:
for cmd in cmds:
f_out.write(cmd)
f_out.write('\n')

View File

@ -1,773 +0,0 @@
#!/usr/bin/env python
#
# Copyright (C) 2021
#
# * Trammell Hudson <hudson@trmm.net>
# * Matthew Mets https://github.com/cibomahto
# * Peter Lawrence https://github.com/majbthrd
#
# Permission to use, copy, modify, and/or distribute this software
# for any purpose with or without fee is hereby granted, provided
# that the above copyright notice and this permission notice
# appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
"""NVCM programming tool for iCE40 FPGAs"""
import os
import sys
import struct
from time import sleep
from iceflasher import IceFlasher
from pybin2nvcm import pybin2nvcm
def assert_bytes_equal(
name: str,
expected: bytes,
val: bytes) -> None:
""" Check if two bytes objects are equal
Keyword arguments:
name -- Description to print if the assertion fails
expected -- Expected value
val -- Value to check
"""
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():
"""NVCM programming interface for ICE40 FPGAs"""
id_table = {
0x06: "ICE40LP8K / ICE40HX8K",
0x07: "ICE40LP4K / ICE40HX4K",
0x08: "ICE40LP1K / ICE40HX1K",
0x09: "ICE40LP384",
0x0E: "ICE40LP1K_SWG16",
0x0F: "ICE40LP640_SWG16",
0x10: "ICE5LP1K",
0x11: "ICE5LP2K",
0x12: "ICE5LP4K",
0x14: "ICE40UL1K",
0x15: "ICE40UL640",
0x20: "ICE40UP5K",
0x21: "ICE40UP3K",
}
banks = {
'nvcm': 0x00,
'trim': 0x10,
'sig': 0x20
}
def __init__(
self,
pins: dict,
spi_speed: int,
debug: bool = False) -> None:
self.pins = pins
self.debug = debug
self.flasher = IceFlasher()
self.flasher.gpio_put(self.pins['5v_en'], False)
self.flasher.gpio_put(self.pins['crst'], False)
# Configure pins for talking to ice40
self.flasher.gpio_set_direction(pins['ss'], True)
self.flasher.gpio_set_direction(pins['mosi'], True)
self.flasher.gpio_set_direction(pins['sck'], True)
self.flasher.gpio_set_direction(pins['miso'], False)
self.flasher.gpio_set_direction(pins['5v_en'], True)
self.flasher.gpio_set_direction(pins['crst'], True)
self.flasher.gpio_set_direction(pins['cdne'], False)
self.flasher.spi_configure(
pins['sck'],
pins['ss'],
pins['mosi'],
pins['miso'],
spi_speed
)
def power_on(self) -> None:
"""Enable power to the DUT"""
self.flasher.gpio_put(self.pins['5v_en'], True)
def power_off(self) -> None:
"""Disable power to the DUT"""
self.flasher.gpio_put(self.pins['5v_en'], False)
def enable(self, chip_select: bool, reset: bool = True) -> None:
"""Set the CS and Reset pin states"""
self.flasher.gpio_put(self.pins['ss'], chip_select)
self.flasher.gpio_put(self.pins['crst'], reset)
def writehex(self, hex_data: str, toggle_cs: bool = True) -> None:
"""Write SPI data to the target device
Keyword arguments:
hex_data -- 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 hex_data == "0500":
print("TX", hex_data)
data = bytes.fromhex(hex_data)
self.flasher.spi_write(data, toggle_cs)
def sendhex(self, hex_data: str) -> bytes:
"""Perform a full-duplex write/read on the target device
Keyword arguments:
s -- data to send (formatted as a string of hex data)
"""
if self.debug and not hex_data == "0500":
print("TX", hex_data)
bytes_data = bytes.fromhex(hex_data)
ret = self.flasher.spi_rxtx(bytes_data)
if self.debug and not hex_data == "0500":
print("RX", ret.hex())
return ret
def delay(self, count: int) -> None:
"""'Delay' by sending clocks with CS de-asserted
Keyword arguments:
count -- Number of bytes to clock
"""
self.flasher.spi_clk_out(count)
def init(self) -> None:
"""Reboot the part and enter SPI command mode"""
if self.debug:
print("init")
self.enable(True, True)
self.enable(True, False)
self.enable(False, False)
self.enable(False, True)
sleep(0.1)
self.enable(True, True)
def status_wait(self) -> None:
"""Wait for the status register to clear"""
for _ in range(0, 1000):
self.delay(1250)
ret = self.sendhex("0500")
status = struct.unpack('>H', ret)[0]
if (status & 0x00c1) == 0:
return
raise ValueError("status failed to clear")
def command(self, cmd: str) -> None:
"""Send a command to the NVCM state machine"""
self.writehex(cmd)
self.status_wait()
self.delay(2)
def pgm_enable(self) -> None:
"""Enable program mode"""
self.command("06")
def pgm_disable(self) -> None:
"""Disable program mode"""
self.command("04")
def enable_access(self) -> None:
"""Send the 'access NVCM' instruction"""
self.command("7eaa997e010e")
def read_bytes(
self,
cmd: int,
address: int,
length: int = 8) -> bytes:
"""Read NVCM memory and return as a byte array
Known read commands are:
0x03: Read NVCM bank
0x84: Read RF
Keyword arguments:
cmd -- Read command
address -- NVCM memory address to read from
length -- Number of bytes to read
"""
msg = ''
msg += (f"{cmd:02x}{address:06x}")
msg += ("00" * 9) # dummy bytes
msg += ("00" * length) # read
ret = self.sendhex(msg)
return ret[4 + 9:]
def read_int(
self,
cmd: int,
address: int) -> int:
"""Read NVCM memory and return as an integer
Read commands are documented in read_bytes
Keyword arguments:
cmd -- Read command
address -- NVCM memory address to read from
"""
val = self.read_bytes(cmd, address, 8)
return struct.unpack('>Q', val)[0]
def write(self, cmd: int, address: int, data: str) -> None:
"""Write data to the NVCM memory
Keyword arguments:
cmd -- Write command
address -- NVCM memory address to write to
length -- Number of bytes to write
"""
self.writehex(f"{cmd:02x}{address:06x}" + data)
try:
self.status_wait()
except Exception as exc:
raise IOError(
f"WRITE FAILED: cmd={cmd:02x} address={address:%06x} data={data}"
) from exc
self.delay(2)
def bank_select(self, bank: str) -> None:
""" Select the active NVCM bank to target
Keyword arguments:
bank -- NVCM bank: nvcm, trim, or sig
"""
self.write(0x83, 0x000025, f"{self.banks[bank]:02x}")
def read_trim(self) -> int:
"""Read the RF trim register"""
self.enable_access()
# ! Shift in READ_RF(0x84) instruction;
# SDR 104 TDI(0x00000000000000000004000021);
val = self.read_int(0x84, 0x000020)
self.delay(2)
# print("FSM Trim Register %x" % (x))
self.bank_select('nvcm')
return val
def write_trim(self, data: str) -> None:
"""Write to the RF trim register
Keyword arguments:
data -- Hex-formatted string, should be 8 bytes of data
"""
# ! Setup Programming Parameter in Trim Registers;
# ! Shift in Trim setup-NVCM instruction;
# TRIMInstruction[1] = 0x000000430F4FA80004000041;
self.write(0x82, 0x000020, data)
def nvcm_enable(self) -> None:
"""Enable NVCM interface by sending knock command"""
if self.debug:
print("enable")
self.enable_access()
# ! Setup Reading Parameter in Trim Registers;
# ! Shift in Trim setup-NVCM instruction;
# TRIMInstruction[1] = 0x000000230000000004000041;
if self.debug:
print("setup_nvcm")
self.write_trim("00000000c4000000")
def enable_trim(self) -> None:
"""Enable NVCM write commands"""
# ! Setup Programming Parameter in Trim Registers;
# ! Shift in Trim setup-NVCM instruction;
# TRIMInstruction[1] = 0x000000430F4FA80004000041;
self.write_trim("0015f2f0c2000000")
def trim_blank_check(self) -> None:
"""Check that the NVCM trim parameters are blank"""
print("NVCM Trim_Parameter_OTP blank check")
self.bank_select('trim')
ret = self.read_bytes(0x03, 0x000020, 1)[0]
self.bank_select('nvcm')
if ret != 0x00:
raise ValueError(
'NVCM Trim_Parameter_OTP Block not blank. ' +
f'(read: 0x{ret:%02x})')
def blank_check(self, total_fuse: int) -> None:
"""Check if sub-section of the NVCM memory is blank
To check all of the memory, first determine how much NVCM
memory your part actually has, or at least the size of the
file that you plan to write to it.
Keyword arguments:
total_fuse -- Number of fuse bytes to read before stopping
"""
self.bank_select('nvcm')
status = True
print("NVCM main memory blank check")
contents = self.read_bytes(0x03, 0x000000, total_fuse)
for index in range(0, total_fuse):
val = contents[index]
if self.debug:
print(f"{index:08x}: {val:02x}")
if val != 0:
print(
f"{index:08x}: NVCM Memory Block is not blank.",
file=sys.stderr)
status = False
self.bank_select('nvcm')
if not status:
raise ValueError("NVCM Main Memory not blank")
def program(self, rows: list[str]) -> None:
"""Program the memory by running an NVCM command sequence
Keyword arguments:
rows -- List of NVCM commands to run, formatted as hex
strings
"""
print("NVCM Program main memory")
self.bank_select('nvcm')
self.enable_trim()
self.pgm_enable()
i = 0
for row in rows:
# print('data for row:',i, row)
if i % (1024 * 8) == 0:
print("%6d / %6d bytes" % (i, len(rows) * 8))
i += 8
try:
self.command(row)
except Exception as exc:
raise IOError(
"programming failed, row:{row}"
) from exc
self.pgm_disable()
def write_trim_pages(self, lock_bits: str) -> None:
"""Write to the trim pages
The trim pages can be written multiple times. Known usages
are to configure the device for NVCM boot, and to secure
the device by disabling the NVCM interface.
Keyword arguments:
lock_bits -- Mas of bits to set in the trim pages
"""
self.bank_select('nvcm')
self.enable_trim()
self.bank_select('trim')
self.pgm_enable()
# ! Program Security Bit row 1;
# ! Shift in PAGEPGM instruction;
# SDR 96 TDI(0x000000008000000C04000040);
# ! Program Security Bit row 2;
# SDR 96 TDI(0x000000008000000C06000040);
# ! Program Security Bit row 3;
# SDR 96 TDI(0x000000008000000C05000040);
# ! Program Security Bit row 4;
# SDR 96 TDI(0x00000000800000C07000040);
self.write(0x02, 0x000020, lock_bits)
self.write(0x02, 0x000060, lock_bits)
self.write(0x02, 0x0000a0, lock_bits)
self.write(0x02, 0x0000e0, lock_bits)
self.pgm_disable()
# verify a read back
val = self.read_int(0x03, 0x000020)
self.bank_select('nvcm')
lock_bits_int = int(lock_bits, 16)
if val & lock_bits_int != lock_bits_int:
raise ValueError(
"Failed to write trim lock bits: " +
f"{val:016x} != expected {lock_bits_int:016x}"
)
print(f"New state {val:016x}")
def trim_secure(self) -> None:
"""Disable NVCM readout by programming the security bits
Use with caution- the device will no longer respond to NVCM
commands after this command runs.
"""
print("NVCM Secure")
trim = self.read_trim()
if (trim >> 60) & 0x3 != 0:
print(
"NVCM already secure? trim=%016x" %
(trim), file=sys.stderr)
self.write_trim_pages("3000000100000000")
def trim_program(self) -> None:
"""Configure the device to boot from NVCM (?)
Use with caution- the device will no longer boot from
external SPI flash after this command runs.
"""
print("NVCM Program Trim_Parameter_OTP")
self.write_trim_pages("0015f2f1c4000000")
def info(self) -> None:
""" Print the contents of the configuration registers """
self.bank_select('sig')
sig1 = self.read_int(0x03, 0x000000)
self.bank_select('sig')
sig2 = self.read_int(0x03, 0x000008)
# have to switch back to nvcm bank before switching to trim?
self.bank_select('nvcm')
trim = self.read_trim()
# self.bank_select('nvcm')
self.bank_select('trim')
trim0 = self.read_int(0x03, 0x000020)
self.bank_select('trim')
trim1 = self.read_int(0x03, 0x000060)
self.bank_select('trim')
trim2 = self.read_int(0x03, 0x0000a0)
self.bank_select('trim')
trim3 = self.read_int(0x03, 0x0000e0)
self.bank_select('nvcm')
secured = (trim >> 60) & 0x3
device_id = (sig1 >> 56) & 0xFF
print("Device: %s (%02x) secure=%d" % (
self.id_table.get(device_id, "Unknown"),
device_id,
secured
))
print("Sig 0: %016x" % (sig1))
print("Sig 1: %016x" % (sig2))
print("TrimRF: %016x" % (trim))
print("Trim 0: %016x" % (trim0))
print("Trim 1: %016x" % (trim1))
print("Trim 2: %016x" % (trim2))
print("Trim 3: %016x" % (trim3))
def read_nvcm(self, length: int) -> bytes:
""" Read out the contents of the NVCM fuses
Keyword arguments:
length: Length of data to read
"""
self.bank_select('nvcm')
# contents = bytearray()
#
# for offset in range(0, length, 8):
# if offset % (1024 * 8) == 0:
# print("%6d / %6d bytes" % (offset, length))
# nvcm_addr = int(offset / 328) * 4096 + (offset % 328)
# contents += self.read_bytes(0x03, nvcm_addr, 8)
# self.delay(2)
# return bytes(contents)
return self.read_bytes(0x03, 0x000000, length)
def read_file(self, filename: str, length: int) -> None:
""" Read the contents of the NVCM to a file
Keyword arguments:
filename -- File to write to, or '-' to write to stdout
length -- Number of bytes to read from NVCM
"""
contents = bytearray()
# prepend a header to the file, to identify it as an FPGA
# bitstream
contents += bytes([0xff, 0x00, 0x00, 0xff])
contents += self.read_nvcm(length)
if filename == '-':
with os.fdopen(sys.stdout.fileno(),
"wb",
closefd=False) as out_file:
out_file.write(contents)
out_file.flush()
else:
with open(filename, "wb") as out_file:
out_file.write(contents)
out_file.flush()
def verify(self, filename: str) -> None:
""" Verify that the contents of the NVCM match a file
Keyword arguments:
filename -- File to compare
"""
with open(filename, "rb") as verify_file:
compare = verify_file.read()
assert len(compare) > 0
contents = bytearray()
contents += bytes([0xff, 0x00, 0x00, 0xff])
contents += self.read_nvcm(len(compare))
# We might have read more than needed because of read
# boundaries
if len(contents) > len(compare):
contents = contents[:len(compare)]
assert compare == contents
print('Verification complete, NVCM contents match file')
def sleep_flash(pins: dict, spi_speed: int) -> None:
""" Put the SPI bootloader flash in deep sleep mode
Keyword arguments:
pins -- Dictionary of pins to use for SPI interface
"""
flasher = IceFlasher()
# Disable board power
flasher.gpio_put(pins['5v_en'], False)
flasher.gpio_set_direction(pins['5v_en'], True)
# Pull CRST low to prevent FPGA from starting
flasher.gpio_set_direction(pins['crst'], True)
flasher.gpio_put(pins['crst'], False)
# Enable board power
flasher.gpio_put(pins['5v_en'], True)
# Configure pins for talking to flash
flasher.gpio_set_direction(pins['ss'], True)
flasher.gpio_set_direction(pins['mosi'], False)
flasher.gpio_set_direction(pins['sck'], True)
flasher.gpio_set_direction(pins['miso'], True)
flasher.spi_configure(
pins['sck'],
pins['ss'],
pins['miso'],
pins['mosi'],
spi_speed
)
sleep(0.5)
# Wake the flash up
flasher.spi_write(bytes([0xAB]))
# Confirm we can talk to flash
data = flasher.spi_rxtx(bytes([0x9f, 0, 0]))
assert_bytes_equal('flash_id', bytes([0xff, 0xef, 0x40]), data)
# put the flash to sleep
flasher.spi_write(bytes([0xb9]))
# Confirm flash is asleep
data = flasher.spi_rxtx(bytes([0x9f, 0, 0]))
assert_bytes_equal('flash_sleep', bytes([0xff, 0xff, 0xff]), data)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument(
'-v',
'--verbose',
dest='verbose',
action='store_true',
help='Show debug information and serial read/writes')
parser.add_argument(
'-f',
'--sleep_flash',
dest='sleep_flash',
action='store_true',
help='Put an attached SPI flash chip in deep sleep')
parser.add_argument(
'-b',
'--boot',
dest='do_boot',
action='store_true',
help='Deassert the reset line to allow the FPGA to boot')
parser.add_argument(
'--speed',
dest='spi_speed',
type=int,
default=15,
help='SPI clock speed, in MHz')
parser.add_argument('-i', '--info',
dest='read_info',
action='store_true',
help='Read chip ID, trim and other info')
parser.add_argument('--read',
dest='read_file',
type=str,
default=None,
help='Read contents of NVCM')
parser.add_argument('--verify',
dest='verify_file',
type=str,
default=None,
help='Verify the contents of NVCM')
parser.add_argument(
'--write',
dest='write_file',
type=str,
default=None,
help='bitstream file to write to NVCM ' +
'(warning: not reversable!)')
parser.add_argument('--ignore-blank',
dest='ignore_blank',
action='store_true',
help='Proceed even if the chip is not blank')
parser.add_argument(
'--secure',
dest='set_secure',
action='store_true',
help='Set security bits to prevent modification ' +
'(warning: not reversable!)')
parser.add_argument(
'--my-design-is-good-enough',
dest='good_enough',
action='store_true',
help='Enable the dangerous commands --write and --secure')
args = parser.parse_args()
if not args.good_enough \
and (args.write_file or args.set_secure):
print(
"Are you sure your design is good enough?",
file=sys.stderr)
sys.exit(1)
tp1_pins = {
'5v_en': 7,
'sck': 10,
'mosi': 11,
'ss': 12,
'miso': 13,
'crst': 14,
'cdne': 15
}
if args.sleep_flash:
sleep_flash(tp1_pins, args.spi_speed)
nvcm = Nvcm(
tp1_pins,
args.spi_speed,
debug=args.verbose)
nvcm.power_on()
# # Turn on ICE40 in CRAM boot mode
nvcm.init()
nvcm.nvcm_enable()
if args.read_info:
nvcm.info()
if args.write_file:
with open(args.write_file, "rb") as in_file:
bitstream = in_file.read()
print(f"read {len(bitstream)} bytes")
cmds = pybin2nvcm(bitstream)
if not args.ignore_blank:
nvcm.trim_blank_check()
# how much should we check?
nvcm.blank_check(100000)
# this is it!
nvcm.program(cmds)
# update the trim to boot from nvcm
nvcm.trim_program()
if args.read_file:
# read back after writing to the NVCM
nvcm.read_file(args.read_file, 104090)
if args.verify_file:
# read back after writing to the NVCM
nvcm.verify(args.verify_file)
if args.set_secure:
nvcm.trim_secure()
if args.do_boot:
# hold reset low for half a second
nvcm.enable(True, False)
sleep(0.5)
nvcm.enable(True, True)

View File

@ -1,6 +0,0 @@
libusb1==3.0.0
pyusb==1.2.1
pyserial==3.5
autopep8==2.0.1
mypy==1.0.1
pylint==2.16.3

View File

@ -1,20 +0,0 @@
#!/usr/bin/env python
"""Automatically reset a TK-1"""
from iceflasher import IceFlasher
def reset_tk1() -> None:
""" Reset a TK1 contained in a TP1 programmer
Manipulate the GPIO lines on the TP1 to issue a hardware reset
to the TK1. The result is that TK1 again will be in firmware
mode, so a new app can be loaded.
"""
flasher = IceFlasher()
flasher.gpio_set_direction(14, True)
flasher.gpio_put(14, False)
flasher.gpio_set_direction(14, False)
reset_tk1()

View File

@ -1,22 +0,0 @@
#!/bin/sh
set -eu
if [ -e /etc/debian_version ]; then
dpkg -s python3-venv || sudo apt install python3-venv
fi
# their current venv might have gone funky...
if [ -e venv ] && [ ! -e wipedonce ]; then
rm -rf venv
touch wipedonce
fi
if [ ! -e venv ]; then
python3 -m venv venv
. ./venv/bin/activate
pip3 install -r requirements.txt
else
. ./venv/bin/activate
fi
./production_test_runner.py

View File

@ -1,49 +0,0 @@
#!/usr/bin/python3
import struct
import argparse
# parser = argparse.ArgumentParser()
# parser.add_argument("port")
# args = parser.parse_args()
class args:
port = '/dev/ttyACM1'
def __init__(self):
pass
with open(args.port.replace('/','-'),'r') as f:
# Skip the first two lines since they might be corrupted
f.readline()
f.readline()
records = 0
tx_count_errors = 0
touch_count_changes = 0
[first_time,last_tx_count,last_touch_count] = [int(i) for i in f.readline().split(',')]
try:
while True:
[time,tx_count,touch_count] = [int(i) for i in f.readline().split(',')]
if tx_count != (last_tx_count + 1) % 256:
tx_count_errors += 1
if touch_count != last_touch_count:
touch_count_changes +=1
print('touch count change', time,touch_count,tx_count)
last_tx_count = tx_count
last_touch_count = touch_count
records += 1
except Exception as e:
print('tx_count errors:', tx_count_errors,
'touch_count_changes:', touch_count_changes,
'records:', records)
time_hours = (time - first_time)/60/60
print('run time:{:.1f} hours'.format(time_hours))

View File

@ -1,21 +0,0 @@
#!/usr/bin/python3
import serial
import struct
import argparse
import time
parser = argparse.ArgumentParser()
parser.add_argument("port")
args = parser.parse_args()
s = serial.Serial(args.port, 9600)
while True:
[report_count, touch_count] = struct.unpack('<BB', s.read(2))
data = ','.join([str(x) for x in (int(time.time()), report_count, touch_count)])
print(data)
with open(args.port.replace('/','-'),'a') as f:
f.write(data)
f.write('\n')