mirror of
https://github.com/tillitis/tillitis-key1.git
synced 2024-12-25 07:29:25 -05:00
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:
parent
a32ecade54
commit
69ef6dde8b
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@ -99,10 +99,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
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
|
- name: make application FPGA gateware
|
||||||
working-directory: hw/application_fpga
|
working-directory: hw/application_fpga
|
||||||
run: make all
|
run: make all
|
||||||
|
2
hw/production_test/.gitignore
vendored
2
hw/production_test/.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
venv
|
|
||||||
wipedonce
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
@ -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.
Binary file not shown.
Binary file not shown.
@ -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)))
|
|
@ -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()
|
|
@ -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,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,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')
|
|
@ -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
|
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -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()
|
|
@ -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()
|
|
@ -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')
|
|
@ -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)
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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))
|
|
||||||
|
|
@ -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')
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user