From 69ef6dde8be6f209e9c499c6775584d9165cc90c Mon Sep 17 00:00:00 2001 From: Daniel Jobson Date: Mon, 14 Oct 2024 16:06:00 +0200 Subject: [PATCH] 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. --- .github/workflows/ci.yaml | 4 - hw/production_test/.gitignore | 2 - hw/production_test/Makefile | 35 - hw/production_test/README.md | 336 - .../application_fpga_test_gateware/Makefile | 55 - .../application_fpga_test_gateware/SB_HFOSC.v | 17 - .../SB_RGBA_DRV.v | 34 - .../application_fpga_test_gateware/app.pcf | 18 - .../application_fpga_test_gateware/top.v | 154 - hw/production_test/binaries/blank.bin | Bin 8000 -> 0 bytes hw/production_test/binaries/main.uf2 | Bin 47616 -> 0 bytes hw/production_test/binaries/top.bin | Bin 104090 -> 0 bytes .../binaries/usb_device_cdc.bin | Bin 2903 -> 0 bytes .../connected_test/connected_test.py | 100 - .../connected_test/connected_test_gui.py | 78 - .../connected_test/usb_pins-good.csv | 24 - .../connected_test/usb_pins.csv | 24 - hw/production_test/encode_usb_strings.py | 103 - hw/production_test/iceflasher.py | 370 - .../nvcm_test/application_fpga.bin | Bin 104090 -> 0 bytes .../nvcm_test/application_fpga.nvcm | 10973 ---------------- hw/production_test/production_test_runner.py | 193 - hw/production_test/production_tests.py | 579 - hw/production_test/pybin2nvcm.py | 84 - hw/production_test/pynvcm.py | 773 -- hw/production_test/requirements.txt | 6 - hw/production_test/reset.py | 20 - hw/production_test/run | 22 - .../touch_stability_test/touch_analyzer.py | 49 - .../touch_stability_test/touch_recorder.py | 21 - 30 files changed, 14074 deletions(-) delete mode 100644 hw/production_test/.gitignore delete mode 100644 hw/production_test/Makefile delete mode 100644 hw/production_test/README.md delete mode 100644 hw/production_test/application_fpga_test_gateware/Makefile delete mode 100644 hw/production_test/application_fpga_test_gateware/SB_HFOSC.v delete mode 100644 hw/production_test/application_fpga_test_gateware/SB_RGBA_DRV.v delete mode 100644 hw/production_test/application_fpga_test_gateware/app.pcf delete mode 100644 hw/production_test/application_fpga_test_gateware/top.v delete mode 100644 hw/production_test/binaries/blank.bin delete mode 100644 hw/production_test/binaries/main.uf2 delete mode 100644 hw/production_test/binaries/top.bin delete mode 100644 hw/production_test/binaries/usb_device_cdc.bin delete mode 100755 hw/production_test/connected_test/connected_test.py delete mode 100755 hw/production_test/connected_test/connected_test_gui.py delete mode 100644 hw/production_test/connected_test/usb_pins-good.csv delete mode 100644 hw/production_test/connected_test/usb_pins.csv delete mode 100755 hw/production_test/encode_usb_strings.py delete mode 100755 hw/production_test/iceflasher.py delete mode 100644 hw/production_test/nvcm_test/application_fpga.bin delete mode 100644 hw/production_test/nvcm_test/application_fpga.nvcm delete mode 100755 hw/production_test/production_test_runner.py delete mode 100755 hw/production_test/production_tests.py delete mode 100755 hw/production_test/pybin2nvcm.py delete mode 100755 hw/production_test/pynvcm.py delete mode 100644 hw/production_test/requirements.txt delete mode 100755 hw/production_test/reset.py delete mode 100755 hw/production_test/run delete mode 100644 hw/production_test/touch_stability_test/touch_analyzer.py delete mode 100644 hw/production_test/touch_stability_test/touch_recorder.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 40a9022..71aa646 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -99,10 +99,6 @@ jobs: run: | git config --global --add safe.directory "$GITHUB_WORKSPACE" - - name: make production test gateware - working-directory: hw/production_test/application_fpga_test_gateware - run: make - - name: make application FPGA gateware working-directory: hw/application_fpga run: make all diff --git a/hw/production_test/.gitignore b/hw/production_test/.gitignore deleted file mode 100644 index 5aa5059..0000000 --- a/hw/production_test/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -venv -wipedonce diff --git a/hw/production_test/Makefile b/hw/production_test/Makefile deleted file mode 100644 index c2023e5..0000000 --- a/hw/production_test/Makefile +++ /dev/null @@ -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 diff --git a/hw/production_test/README.md b/hw/production_test/README.md deleted file mode 100644 index 4dab613..0000000 --- a/hw/production_test/README.md +++ /dev/null @@ -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. diff --git a/hw/production_test/application_fpga_test_gateware/Makefile b/hw/production_test/application_fpga_test_gateware/Makefile deleted file mode 100644 index 3da9e09..0000000 --- a/hw/production_test/application_fpga_test_gateware/Makefile +++ /dev/null @@ -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 diff --git a/hw/production_test/application_fpga_test_gateware/SB_HFOSC.v b/hw/production_test/application_fpga_test_gateware/SB_HFOSC.v deleted file mode 100644 index 86455e7..0000000 --- a/hw/production_test/application_fpga_test_gateware/SB_HFOSC.v +++ /dev/null @@ -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 - diff --git a/hw/production_test/application_fpga_test_gateware/SB_RGBA_DRV.v b/hw/production_test/application_fpga_test_gateware/SB_RGBA_DRV.v deleted file mode 100644 index 5825cc4..0000000 --- a/hw/production_test/application_fpga_test_gateware/SB_RGBA_DRV.v +++ /dev/null @@ -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 - diff --git a/hw/production_test/application_fpga_test_gateware/app.pcf b/hw/production_test/application_fpga_test_gateware/app.pcf deleted file mode 100644 index 262eb40..0000000 --- a/hw/production_test/application_fpga_test_gateware/app.pcf +++ /dev/null @@ -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 diff --git a/hw/production_test/application_fpga_test_gateware/top.v b/hw/production_test/application_fpga_test_gateware/top.v deleted file mode 100644 index 55184e8..0000000 --- a/hw/production_test/application_fpga_test_gateware/top.v +++ /dev/null @@ -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 diff --git a/hw/production_test/binaries/blank.bin b/hw/production_test/binaries/blank.bin deleted file mode 100644 index 975a03d03fdcdbfdb325e9dc0c1e44b152cd6305..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8000 zcmeIufdBvi0Dz$VsTV1P3IhfV7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkoOG CKmY*% diff --git a/hw/production_test/binaries/main.uf2 b/hw/production_test/binaries/main.uf2 deleted file mode 100644 index a2da8813fdbb8953cee13be01f8ea109af5606bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47616 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 diff --git a/hw/production_test/connected_test/connected_test.py b/hw/production_test/connected_test/connected_test.py deleted file mode 100755 index 959c324..0000000 --- a/hw/production_test/connected_test/connected_test.py +++ /dev/null @@ -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< 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') diff --git a/hw/production_test/iceflasher.py b/hw/production_test/iceflasher.py deleted file mode 100755 index d2b43bf..0000000 --- a/hw/production_test/iceflasher.py +++ /dev/null @@ -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 diff --git a/hw/production_test/nvcm_test/application_fpga.bin b/hw/production_test/nvcm_test/application_fpga.bin deleted file mode 100644 index 6c6a814f839f89ee65414005e76e93bebb66196e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 104090 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ⅅ$NnIF>&C_XUpT;om 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 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() diff --git a/hw/production_test/production_tests.py b/hw/production_test/production_tests.py deleted file mode 100755 index b7f13f0..0000000 --- a/hw/production_test/production_tests.py +++ /dev/null @@ -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() diff --git a/hw/production_test/pybin2nvcm.py b/hw/production_test/pybin2nvcm.py deleted file mode 100755 index ba2da06..0000000 --- a/hw/production_test/pybin2nvcm.py +++ /dev/null @@ -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') diff --git a/hw/production_test/pynvcm.py b/hw/production_test/pynvcm.py deleted file mode 100755 index 3d2b7b0..0000000 --- a/hw/production_test/pynvcm.py +++ /dev/null @@ -1,773 +0,0 @@ -#!/usr/bin/env python -# -# Copyright (C) 2021 -# -# * Trammell Hudson -# * 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) diff --git a/hw/production_test/requirements.txt b/hw/production_test/requirements.txt deleted file mode 100644 index 3eed091..0000000 --- a/hw/production_test/requirements.txt +++ /dev/null @@ -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 diff --git a/hw/production_test/reset.py b/hw/production_test/reset.py deleted file mode 100755 index 0c15e15..0000000 --- a/hw/production_test/reset.py +++ /dev/null @@ -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() diff --git a/hw/production_test/run b/hw/production_test/run deleted file mode 100755 index 9eada80..0000000 --- a/hw/production_test/run +++ /dev/null @@ -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 diff --git a/hw/production_test/touch_stability_test/touch_analyzer.py b/hw/production_test/touch_stability_test/touch_analyzer.py deleted file mode 100644 index 0e10b38..0000000 --- a/hw/production_test/touch_stability_test/touch_analyzer.py +++ /dev/null @@ -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)) - diff --git a/hw/production_test/touch_stability_test/touch_recorder.py b/hw/production_test/touch_stability_test/touch_recorder.py deleted file mode 100644 index 98bc5c1..0000000 --- a/hw/production_test/touch_stability_test/touch_recorder.py +++ /dev/null @@ -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('