mirror of
https://github.com/tillitis/tillitis-key1.git
synced 2024-12-24 15:09:27 -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: |
|
||||
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
|
||||
|
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