mirror of
https://github.com/openhwgroup/cvw
synced 2025-02-11 06:05:49 +00:00
convert debug script to TCL interface, remove telnetlib dependency
This commit is contained in:
parent
c3243caacf
commit
7f63daa49c
@ -1,429 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
# hw_interface.py
|
|
||||||
#
|
|
||||||
# Written: matthew.n.otto@okstate.edu
|
|
||||||
# Created: 19 April 2024
|
|
||||||
#
|
|
||||||
# Purpose: Send debugging commands to OpenOCD via local telnet connection
|
|
||||||
#
|
|
||||||
# A component of the CORE-V-WALLY configurable RISC-V project.
|
|
||||||
# https:#github.com/openhwgroup/cvw
|
|
||||||
#
|
|
||||||
# Copyright (C) 2021-24 Harvey Mudd College & Oklahoma State University
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: Apache-2.0 WITH SHL-2.1
|
|
||||||
#
|
|
||||||
# Licensed under the Solderpad Hardware License v 2.1 (the “License”); you may not use this file
|
|
||||||
# except in compliance with the License, or, at your option, the Apache License version 2.0. You
|
|
||||||
# may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# https:#solderpad.org/licenses/SHL-2.1/
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, any work distributed under the
|
|
||||||
# License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
|
||||||
# either express or implied. See the License for the specific language governing permissions
|
|
||||||
# and limitations under the License.
|
|
||||||
#########################################################################################
|
|
||||||
|
|
||||||
# This script uses python to send text commands to OpenOCD via telnet
|
|
||||||
# OpenOCD also supports tcl commands directly
|
|
||||||
|
|
||||||
import atexit
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
from telnetlib import Telnet
|
|
||||||
|
|
||||||
debug = False
|
|
||||||
|
|
||||||
# TODO: if JTAG clk is fast enough, need to check for busy between absract commands
|
|
||||||
|
|
||||||
def dump_GPR():
|
|
||||||
gpr = {}
|
|
||||||
for i in range(1,32):
|
|
||||||
addr = f"X{i}"
|
|
||||||
gpr[addr] = read_data(addr)
|
|
||||||
# DM will assert Abstract Command Err if GPR X16-X31 isn't implemented (CMDERR_EXCEPTION)
|
|
||||||
# This will clear that error and return early.
|
|
||||||
if i == 16:
|
|
||||||
abstractcs = int(read_dmi("0x16"), 16)
|
|
||||||
cmderr = (abstractcs & 0x700) >> 8
|
|
||||||
if cmderr == 3:
|
|
||||||
clear_abstrcmd_err()
|
|
||||||
break
|
|
||||||
return gpr
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def write_data(register, data):
|
|
||||||
"""Writes data of width XLEN to specified register"""
|
|
||||||
# Translate register alias to DM regno
|
|
||||||
regno = int(register_translations[register], 16)
|
|
||||||
# Write data to 32 bit message registers
|
|
||||||
data = int(data, 16)
|
|
||||||
write_dmi("0x4", hex(data & 0xffffffff))
|
|
||||||
if XLEN == 64:
|
|
||||||
write_dmi("0x5", hex((data >> 32) & 0xffffffff))
|
|
||||||
if XLEN == 128:
|
|
||||||
write_dmi("0x6", hex((data >> 64) & 0xffffffff))
|
|
||||||
write_dmi("0x7", hex((data >> 96) & 0xffffffff))
|
|
||||||
# Transfer data from msg registers to target register
|
|
||||||
access_register(write=True, regno=regno, addr_size=XLEN)
|
|
||||||
# Check that operations completed without error
|
|
||||||
if acerr := check_absrtcmderr():
|
|
||||||
raise Exception(acerr)
|
|
||||||
|
|
||||||
|
|
||||||
def read_data(register):
|
|
||||||
"""Read data of width XLEN from specified register"""
|
|
||||||
# Translate register alias to DM regno
|
|
||||||
regno = int(register_translations[register], 16)
|
|
||||||
# Transfer data from target register to msg registers
|
|
||||||
access_register(write=False, regno=regno, addr_size=XLEN)
|
|
||||||
# Read data from 32 bit message registers
|
|
||||||
data = ""
|
|
||||||
data = read_dmi("0x4").replace("0x", "").zfill(8)
|
|
||||||
if XLEN >= 64:
|
|
||||||
data = read_dmi("0x5").replace("0x", "").zfill(8) + data
|
|
||||||
if XLEN == 128:
|
|
||||||
data = read_dmi("0x6").replace("0x", "").zfill(8) + data
|
|
||||||
data = read_dmi("0x7").replace("0x", "").zfill(8) + data
|
|
||||||
# Check that operations completed without error
|
|
||||||
if acerr := check_absrtcmderr():
|
|
||||||
raise Exception(acerr)
|
|
||||||
return f"0x{data}"
|
|
||||||
|
|
||||||
|
|
||||||
def access_register(write, regno, addr_size):
|
|
||||||
"""3.7.1.1
|
|
||||||
Before starting an abstract command, a debugger must ensure that haltreq, resumereq, and
|
|
||||||
ackhavereset are all 0."""
|
|
||||||
addr = "0x17"
|
|
||||||
data = 1 << 17 # transfer bit always set
|
|
||||||
if addr_size == 32:
|
|
||||||
data += 2 << 20
|
|
||||||
elif addr_size == 64:
|
|
||||||
data += 3 << 20
|
|
||||||
elif addr_size == 128:
|
|
||||||
data += 4 << 20
|
|
||||||
else:
|
|
||||||
raise Exception("must provide valid register access size (32, 64, 128). See: 3.7.1.1 aarsize")
|
|
||||||
if write:
|
|
||||||
data += 1<<16
|
|
||||||
data += regno
|
|
||||||
data = hex(data)
|
|
||||||
write_dmi(addr, data)
|
|
||||||
|
|
||||||
|
|
||||||
def halt():
|
|
||||||
write_dmi("0x10", "0x80000001")
|
|
||||||
check_errors()
|
|
||||||
|
|
||||||
|
|
||||||
def resume():
|
|
||||||
write_dmi("0x10", "0x40000001")
|
|
||||||
check_errors()
|
|
||||||
|
|
||||||
|
|
||||||
def step():
|
|
||||||
write_dmi("0x10", "0xC0000001")
|
|
||||||
check_errors()
|
|
||||||
|
|
||||||
|
|
||||||
def set_haltonreset():
|
|
||||||
write_dmi("0x10", "0x9")
|
|
||||||
|
|
||||||
|
|
||||||
def clear_haltonreset():
|
|
||||||
write_dmi("0x10", "0x5")
|
|
||||||
|
|
||||||
|
|
||||||
def reset_hart():
|
|
||||||
write_dmi("0x10", "0x3")
|
|
||||||
write_dmi("0x10", "0x1")
|
|
||||||
|
|
||||||
|
|
||||||
def status():
|
|
||||||
dmstatus = int(read_dmi("0x11"), 16)
|
|
||||||
print("Core status:::")
|
|
||||||
print(f"Running: {bool((dmstatus >> 11) & 0x1)}")
|
|
||||||
print(f"Halted: {bool((dmstatus >> 9) & 0x1)}")
|
|
||||||
|
|
||||||
|
|
||||||
def check_errors():
|
|
||||||
# TODO: update this
|
|
||||||
"""Checks various status bits and reports any potential errors
|
|
||||||
Returns true if any errors are found"""
|
|
||||||
# check dtmcs
|
|
||||||
dtmcs = int(read_dtmcs(), 16)
|
|
||||||
errinfo = (dtmcs & 0x1C0000) >> 18
|
|
||||||
dmistat = (dtmcs & 0xC00) >> 10
|
|
||||||
if errinfo > 0 and errinfo < 4:
|
|
||||||
print(f"DTM Error: {errinfo_translations[errinfo]}")
|
|
||||||
return True
|
|
||||||
if dmistat:
|
|
||||||
print(f"DMI status error: {op_translations[dmistat]}")
|
|
||||||
return True
|
|
||||||
# check if DM is inactive
|
|
||||||
dm_active = int(read_dmi("0x10"), 16) & 0x1
|
|
||||||
if not dm_active:
|
|
||||||
print("DMControl Error: Debug module is not active")
|
|
||||||
return True
|
|
||||||
# check abstract command error
|
|
||||||
abstractcs = int(read_dmi("0x16"), 16)
|
|
||||||
busy = (abstractcs & 0x1000) >> 12
|
|
||||||
cmderr = (abstractcs & 0x700) >> 8
|
|
||||||
if not busy and cmderr:
|
|
||||||
print(f"Abstract Command Error: {cmderr_translations[cmderr]}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def check_busy():
|
|
||||||
"""If an Abstract Command OP is attempted while busy, an abstrcmderr will be asserted"""
|
|
||||||
abstractcs = int(read_dmi("0x16"), 16)
|
|
||||||
return bool((abstractcs & 0x1000) >> 12)
|
|
||||||
|
|
||||||
|
|
||||||
def check_absrtcmderr():
|
|
||||||
"""These errors must be cleared using clear_abstrcmd_err() before another OP can be executed"""
|
|
||||||
abstractcs = int(read_dmi("0x16"), 16)
|
|
||||||
# CmdErr is only valid if Busy is 0
|
|
||||||
busy = bool((abstractcs & 0x1000) >> 12)
|
|
||||||
while busy:
|
|
||||||
time.sleep(0.05)
|
|
||||||
abstractcs = int(read_dmi("0x16"), 16)
|
|
||||||
busy = bool((abstractcs & 0x1000) >> 12)
|
|
||||||
return cmderr_translations[(abstractcs & 0x700) >> 8]
|
|
||||||
|
|
||||||
|
|
||||||
def clear_abstrcmd_err():
|
|
||||||
write_dmi("0x16", "0x700")
|
|
||||||
|
|
||||||
|
|
||||||
def reset_dm():
|
|
||||||
deactivate_dm()
|
|
||||||
activate_dm()
|
|
||||||
|
|
||||||
|
|
||||||
def activate_dm():
|
|
||||||
write_dmi("0x10", "0x1")
|
|
||||||
return int(read_dmi("0x10"), 16) & 0x1
|
|
||||||
|
|
||||||
|
|
||||||
def deactivate_dm():
|
|
||||||
write_dmi("0x10", "0x0")
|
|
||||||
return not int(read_dmi("0x10"), 16) & 0x1
|
|
||||||
|
|
||||||
|
|
||||||
def dmi_reset():
|
|
||||||
"""Reset sticky dmi error status in DTM"""
|
|
||||||
write_dtmcs(dmireset=True)
|
|
||||||
check_errors()
|
|
||||||
|
|
||||||
|
|
||||||
def write_dmi(address, data):
|
|
||||||
cmd = f"riscv dmi_write {address} {data}"
|
|
||||||
rsp = execute(cmd)
|
|
||||||
if "Failed" in rsp:
|
|
||||||
print(rsp)
|
|
||||||
|
|
||||||
|
|
||||||
def read_dmi(address):
|
|
||||||
cmd = f"riscv dmi_read {address}"
|
|
||||||
return execute(cmd)
|
|
||||||
|
|
||||||
|
|
||||||
def write_dtmcs(dtmhardreset=False, dmireset=False):
|
|
||||||
data = 0
|
|
||||||
if dtmhardreset:
|
|
||||||
data += 0x1 << 17
|
|
||||||
if dmireset:
|
|
||||||
data += 0x1 << 16
|
|
||||||
execute(f"irscan {tapname} 0x10") # dtmcs instruction
|
|
||||||
execute(f"drscan {tapname} 32 {hex(data)}")
|
|
||||||
|
|
||||||
|
|
||||||
def read_dtmcs():
|
|
||||||
execute(f"irscan {tapname} 0x10") # dtmcs instruction
|
|
||||||
dtmcs = execute(f"drscan {tapname} 32 0x0")
|
|
||||||
return dtmcs
|
|
||||||
|
|
||||||
|
|
||||||
def trst():
|
|
||||||
execute("pathmove RESET IDLE")
|
|
||||||
|
|
||||||
|
|
||||||
def execute(cmd):
|
|
||||||
write(cmd)
|
|
||||||
return read()
|
|
||||||
|
|
||||||
|
|
||||||
def write(cmd):
|
|
||||||
if debug:
|
|
||||||
print(f"Executing command: '{cmd}'")
|
|
||||||
tn.write(cmd.encode('ascii') + b"\n")
|
|
||||||
tn.read_until(b"\n")
|
|
||||||
|
|
||||||
|
|
||||||
def read():
|
|
||||||
data = b""
|
|
||||||
data = tn.read_until(b"> ").decode('ascii')
|
|
||||||
data = data.replace("\r", "").replace("\n", "").replace("> ", "")
|
|
||||||
if debug:
|
|
||||||
print(data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def interrogate():
|
|
||||||
global XLEN
|
|
||||||
global tapname
|
|
||||||
write("scan_chain")
|
|
||||||
raw = tn.read_until(b"> ").decode('ascii')
|
|
||||||
scan_chain = raw.replace("\r", "").replace("> ", "")
|
|
||||||
scan_chain = [tap for tap in scan_chain.split("\n")[2:] if tap]
|
|
||||||
if len(scan_chain) > 1:
|
|
||||||
print(f"Found multiple taps. Selecting tap #0\n{raw}")
|
|
||||||
scan_chain = scan_chain[0]
|
|
||||||
tapname = re.search("\d\s+(.+?)\s+", scan_chain).group(1)
|
|
||||||
print(f"DM tapname: {tapname}")
|
|
||||||
|
|
||||||
write("riscv info")
|
|
||||||
info = tn.read_until(b"> ").decode('ascii').replace("\r", "").replace("> ", "").split("\n")
|
|
||||||
for line in info:
|
|
||||||
if XLEN := re.search("hart.xlen\s+(\d+)", line).group(1):
|
|
||||||
XLEN = int(XLEN)
|
|
||||||
break
|
|
||||||
print(f"XLEN: {XLEN}")
|
|
||||||
|
|
||||||
|
|
||||||
def init():
|
|
||||||
global tn
|
|
||||||
tn = Telnet("127.0.0.1", 4444)
|
|
||||||
atexit.register(cleanup)
|
|
||||||
read() # clear welcome message from read buffer
|
|
||||||
interrogate()
|
|
||||||
activate_dm()
|
|
||||||
# TODO: query gpr count
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup():
|
|
||||||
tn.close()
|
|
||||||
|
|
||||||
|
|
||||||
# 6.1.4 dtmcs errinfo translation table
|
|
||||||
errinfo_translations = {
|
|
||||||
0 : "not implemented",
|
|
||||||
1 : "dmi error",
|
|
||||||
2 : "communication error",
|
|
||||||
3 : "device error",
|
|
||||||
4 : "unknown",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 6.1.5 DMI op translation table
|
|
||||||
op_translations = {
|
|
||||||
0 : "success",
|
|
||||||
1 : "reserved",
|
|
||||||
2 : "failed",
|
|
||||||
3 : "busy",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 3.14.6 Abstract command CmdErr value translation table
|
|
||||||
cmderr_translations = {
|
|
||||||
0 : None,
|
|
||||||
1 : "busy",
|
|
||||||
2 : "not supported",
|
|
||||||
3 : "exception",
|
|
||||||
4 : "halt/resume",
|
|
||||||
5 : "bus",
|
|
||||||
6 : "reserved",
|
|
||||||
7 : "other",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Register alias to regno translation table
|
|
||||||
register_translations = {
|
|
||||||
"MISA" : "0x0301",
|
|
||||||
"TRAPM" : "0xC000",
|
|
||||||
"PCM" : "0xC001",
|
|
||||||
"INSTRM" : "0xC002",
|
|
||||||
"MEMRWM" : "0xC003",
|
|
||||||
"INSTRVALIDM" : "0xC004",
|
|
||||||
"WRITEDATAM" : "0xC005",
|
|
||||||
"IEUADRM" : "0xC006",
|
|
||||||
"READDATAM" : "0xC007",
|
|
||||||
"x0 (zero)" : "0x1000",
|
|
||||||
"x1 (ra)" : "0x1001",
|
|
||||||
"x2 (sp)" : "0x1002",
|
|
||||||
"x3 (gp)" : "0x1003",
|
|
||||||
"x4 (tp)" : "0x1004",
|
|
||||||
"x5 (t0)" : "0x1005",
|
|
||||||
"x6 (t1)" : "0x1006",
|
|
||||||
"x7 (t2)" : "0x1007",
|
|
||||||
"x8 (s0/fp)" : "0x1008",
|
|
||||||
"x9 (s1)" : "0x1009",
|
|
||||||
"x10 (a0)" : "0x100A",
|
|
||||||
"x11 (a1)" : "0x100B",
|
|
||||||
"x12 (a2)" : "0x100C",
|
|
||||||
"x13 (a3)" : "0x100D",
|
|
||||||
"x14 (a4)" : "0x100E",
|
|
||||||
"x15 (a5)" : "0x100F",
|
|
||||||
"x16 (a6)" : "0x1010",
|
|
||||||
"x17 (a7)" : "0x1011",
|
|
||||||
"x18 (s2)" : "0x1012",
|
|
||||||
"x19 (s3)" : "0x1013",
|
|
||||||
"x20 (s4)" : "0x1014",
|
|
||||||
"x21 (s5)" : "0x1015",
|
|
||||||
"x22 (s6)" : "0x1016",
|
|
||||||
"x23 (s7)" : "0x1017",
|
|
||||||
"x24 (s8)" : "0x1018",
|
|
||||||
"x25 (s9)" : "0x1019",
|
|
||||||
"x26 (s10)" : "0x101A",
|
|
||||||
"x27 (s11)" : "0x101B",
|
|
||||||
"x28 (t3)" : "0x101C",
|
|
||||||
"x29 (t4)" : "0x101D",
|
|
||||||
"x30 (t5)" : "0x101E",
|
|
||||||
"x31 (t6)" : "0x101F",
|
|
||||||
"f0 (ft0)" : "0x1020",
|
|
||||||
"f1 (ft1)" : "0x1021",
|
|
||||||
"f2 (ft2)" : "0x1022",
|
|
||||||
"f3 (ft3)" : "0x1023",
|
|
||||||
"f4 (ft4)" : "0x1024",
|
|
||||||
"f5 (ft5)" : "0x1025",
|
|
||||||
"f6 (ft6)" : "0x1026",
|
|
||||||
"f7 (ft7)" : "0x1027",
|
|
||||||
"f8 (fs0)" : "0x1028",
|
|
||||||
"f9 (fs1)" : "0x1029",
|
|
||||||
"f10 (fa0)" : "0x102A",
|
|
||||||
"f11 (fa1)" : "0x102B",
|
|
||||||
"f12 (fa2)" : "0x102C",
|
|
||||||
"f13 (fa3)" : "0x102D",
|
|
||||||
"f14 (fa4)" : "0x102E",
|
|
||||||
"f15 (fa5)" : "0x102F",
|
|
||||||
"f16 (fa6)" : "0x1030",
|
|
||||||
"f17 (fa7)" : "0x1031",
|
|
||||||
"f18 (fs2)" : "0x1032",
|
|
||||||
"f19 (fs3)" : "0x1033",
|
|
||||||
"f20 (fs4)" : "0x1034",
|
|
||||||
"f21 (fs5)" : "0x1035",
|
|
||||||
"f22 (fs6)" : "0x1036",
|
|
||||||
"f23 (fs7)" : "0x1037",
|
|
||||||
"f24 (fs8)" : "0x1038",
|
|
||||||
"f25 (fs9)" : "0x1039",
|
|
||||||
"f26 (fs10)" : "0x103A",
|
|
||||||
"f27 (fs11)" : "0x103B",
|
|
||||||
"f28 (ft8)" : "0x103C",
|
|
||||||
"f29 (ft9)" : "0x103D",
|
|
||||||
"f30 (ft10)" : "0x103E",
|
|
||||||
"f31 (ft11)" : "0x103F",
|
|
||||||
}
|
|
||||||
|
|
||||||
nonstandard_register_lengths = {
|
|
||||||
"TRAPM" : 1,
|
|
||||||
"INSTRM" : 32,
|
|
||||||
"MEMRWM" : 2,
|
|
||||||
"INSTRVALIDM" : 1,
|
|
||||||
"READDATAM" : 64
|
|
||||||
}
|
|
@ -6,7 +6,7 @@
|
|||||||
# Written: matthew.n.otto@okstate.edu
|
# Written: matthew.n.otto@okstate.edu
|
||||||
# Created: 19 April 2024
|
# Created: 19 April 2024
|
||||||
#
|
#
|
||||||
# Purpose: Send test commands to OpenOCD via local telnet connection
|
# Purpose: script to automate testing of hardware debug interface
|
||||||
#
|
#
|
||||||
# A component of the CORE-V-WALLY configurable RISC-V project.
|
# A component of the CORE-V-WALLY configurable RISC-V project.
|
||||||
# https:#github.com/openhwgroup/cvw
|
# https:#github.com/openhwgroup/cvw
|
||||||
@ -30,98 +30,96 @@
|
|||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import hw_debug_interface
|
from openocd_tcl_wrapper import OpenOCD
|
||||||
from hw_debug_interface import *
|
|
||||||
|
|
||||||
random_stimulus = False
|
random_stimulus = True
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
registers = dict.fromkeys(register_translations.keys(),[])
|
with OpenOCD() as cvw:
|
||||||
reg_addrs = list(registers.keys())
|
registers = dict.fromkeys(cvw.register_translations.keys(),[])
|
||||||
|
reg_addrs = list(registers.keys())
|
||||||
|
|
||||||
init()
|
global XLEN
|
||||||
global XLEN
|
XLEN = cvw.LLEN
|
||||||
XLEN = hw_debug_interface.XLEN
|
global nonstandard_register_lengths
|
||||||
reset_dm()
|
nonstandard_register_lengths = cvw.nonstandard_register_lengths
|
||||||
reset_hart()
|
|
||||||
|
|
||||||
time.sleep(70) # wait for OpenSBI
|
|
||||||
|
|
||||||
halt()
|
cvw.reset_dm()
|
||||||
status()
|
cvw.reset_hart()
|
||||||
|
|
||||||
# dump data in all registers
|
time.sleep(70) # wait for OpenSBI
|
||||||
for r in reg_addrs:
|
|
||||||
try:
|
cvw.halt()
|
||||||
data = read_data(r)
|
|
||||||
registers[r] = data
|
# dump data in all registers
|
||||||
print(f"{r}: {data}")
|
for r in reg_addrs:
|
||||||
except Exception as e:
|
try:
|
||||||
if e.args[0] == "exception": # Invalid register (not implemented)
|
data = cvw.read_data(r)
|
||||||
del registers[r]
|
registers[r] = data
|
||||||
clear_abstrcmd_err()
|
print(f"{r}: {data}")
|
||||||
else:
|
except Exception as e:
|
||||||
|
if e.args[0] == "exception": # Invalid register (not implemented)
|
||||||
|
del registers[r]
|
||||||
|
cvw.clear_abstrcmd_err()
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
input("Compare values to ILA, press any key to continue")
|
||||||
|
|
||||||
|
# Write random data to all registers
|
||||||
|
reg_addrs = list(registers.keys())
|
||||||
|
if random_stimulus:
|
||||||
|
random.shuffle(reg_addrs)
|
||||||
|
test_reg_data = {}
|
||||||
|
for r in reg_addrs:
|
||||||
|
test_data = random_hex(r)
|
||||||
|
try:
|
||||||
|
cvw.write_data(r, test_data)
|
||||||
|
test_reg_data[r] = test_data
|
||||||
|
print(f"Writing {test_data} to {r}")
|
||||||
|
except Exception as e:
|
||||||
|
if e.args[0] == "not supported": # Register is read only
|
||||||
|
del registers[r]
|
||||||
|
cvw.clear_abstrcmd_err()
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
# GPR X0 is always 0
|
||||||
|
test_reg_data["x0"] = "0x" + "0"*(cvw.LLEN//4)
|
||||||
|
|
||||||
|
# Confirm data was written correctly
|
||||||
|
reg_addrs = list(registers.keys())
|
||||||
|
if random_stimulus:
|
||||||
|
random.shuffle(reg_addrs)
|
||||||
|
for r in reg_addrs:
|
||||||
|
try:
|
||||||
|
rdata = cvw.read_data(r)
|
||||||
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
input("Compare values to ILA, press any key to continue")
|
if rdata != test_reg_data[r]:
|
||||||
|
print(f"Error: register {r} read did not return correct data: {rdata} != {test_reg_data[r]}")
|
||||||
# Write random data to all registers
|
|
||||||
reg_addrs = list(registers.keys())
|
|
||||||
if random_stimulus:
|
|
||||||
random.shuffle(reg_addrs)
|
|
||||||
test_reg_data = {}
|
|
||||||
for r in reg_addrs:
|
|
||||||
test_data = random_hex(r)
|
|
||||||
try:
|
|
||||||
write_data(r, test_data)
|
|
||||||
test_reg_data[r] = test_data
|
|
||||||
print(f"Writing {test_data} to {r}")
|
|
||||||
except Exception as e:
|
|
||||||
if e.args[0] == "not supported": # Register is read only
|
|
||||||
del registers[r]
|
|
||||||
clear_abstrcmd_err()
|
|
||||||
else:
|
else:
|
||||||
|
print(f"Reading {rdata} from {r}")
|
||||||
|
|
||||||
|
# Return all registers to original state
|
||||||
|
reg_addrs = list(registers.keys())
|
||||||
|
for r in reg_addrs:
|
||||||
|
print(f"Writing {registers[r]} to {r}")
|
||||||
|
try:
|
||||||
|
cvw.write_data(r, registers[r])
|
||||||
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
check_errors()
|
|
||||||
|
|
||||||
# GPR X0 is always 0
|
# Confirm data was written correctly
|
||||||
test_reg_data["X0"] = "0x" + "0"*(XLEN//4)
|
for r in reg_addrs:
|
||||||
|
try:
|
||||||
|
rdata = cvw.read_data(r)
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
if rdata != registers[r]:
|
||||||
|
raise Exception(f"Register {r} read did not return correct data: {rdata} != {registers[r]}")
|
||||||
|
print("All writes successful")
|
||||||
|
|
||||||
# Confirm data was written correctly
|
cvw.resume()
|
||||||
reg_addrs = list(registers.keys())
|
|
||||||
if random_stimulus:
|
|
||||||
random.shuffle(reg_addrs)
|
|
||||||
for r in reg_addrs:
|
|
||||||
try:
|
|
||||||
rdata = read_data(r)
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
if rdata != test_reg_data[r]:
|
|
||||||
print(f"Error: register {r} read did not return correct data: {rdata} != {test_reg_data[r]}")
|
|
||||||
else:
|
|
||||||
print(f"Reading {rdata} from {r}")
|
|
||||||
|
|
||||||
# Return all registers to original state
|
|
||||||
reg_addrs = list(registers.keys())
|
|
||||||
for r in reg_addrs:
|
|
||||||
print(f"Writing {registers[r]} to {r}")
|
|
||||||
try:
|
|
||||||
write_data(r, registers[r])
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
# Confirm data was written correctly
|
|
||||||
for r in reg_addrs:
|
|
||||||
try:
|
|
||||||
rdata = read_data(r)
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
if rdata != registers[r]:
|
|
||||||
raise Exception(f"Register {r} read did not return correct data: {rdata} != {registers[r]}")
|
|
||||||
print("All writes successful")
|
|
||||||
|
|
||||||
resume()
|
|
||||||
status()
|
|
||||||
|
|
||||||
|
|
||||||
def random_hex(reg_name):
|
def random_hex(reg_name):
|
||||||
|
392
bin/openocd_tcl_wrapper.py
Normal file
392
bin/openocd_tcl_wrapper.py
Normal file
@ -0,0 +1,392 @@
|
|||||||
|
#########################################################################################
|
||||||
|
# openocd_tcl_wrapper.py
|
||||||
|
#
|
||||||
|
# Written: matthew.n.otto@okstate.edu
|
||||||
|
# Created: 8 June 2024
|
||||||
|
#
|
||||||
|
# Purpose: Python wrapper library used to send debug commands to OpenOCD
|
||||||
|
#
|
||||||
|
# A component of the CORE-V-WALLY configurable RISC-V project.
|
||||||
|
# https://github.com/openhwgroup/cvw
|
||||||
|
#
|
||||||
|
# Copyright (C) 2021-24 Harvey Mudd College & Oklahoma State University
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0 WITH SHL-2.1
|
||||||
|
#
|
||||||
|
# Licensed under the Solderpad Hardware License v 2.1 (the “License”); you may not use this file
|
||||||
|
# except in compliance with the License, or, at your option, the Apache License version 2.0. You
|
||||||
|
# may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://solderpad.org/licenses/SHL-2.1/
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, any work distributed under the
|
||||||
|
# License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||||
|
# either express or implied. See the License for the specific language governing permissions
|
||||||
|
# and limitations under the License.
|
||||||
|
#########################################################################################
|
||||||
|
|
||||||
|
import math
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
|
||||||
|
ENDMSG = b'\x1a'
|
||||||
|
|
||||||
|
class OpenOCD:
|
||||||
|
def __init__(self):
|
||||||
|
self.tcl = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.tcl.connect(("127.0.0.1", 6666))
|
||||||
|
self.LLEN = 64 #TODO: find this
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, type, value, traceback):
|
||||||
|
try:
|
||||||
|
self.send("exit")
|
||||||
|
finally:
|
||||||
|
self.tcl.close()
|
||||||
|
|
||||||
|
def capture(self, cmd):
|
||||||
|
return self.send(f"capture \"{cmd}\"")
|
||||||
|
|
||||||
|
def send(self, cmd):
|
||||||
|
data = cmd.encode("ascii") + ENDMSG
|
||||||
|
self.tcl.send(data)
|
||||||
|
return self.receive()
|
||||||
|
|
||||||
|
def receive(self):
|
||||||
|
data = bytes()
|
||||||
|
while True:
|
||||||
|
byte = self.tcl.recv(1)
|
||||||
|
if byte == ENDMSG:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
data += byte
|
||||||
|
data = data.decode("ascii").rstrip()
|
||||||
|
return data
|
||||||
|
|
||||||
|
def trst(self):
|
||||||
|
self.send("pathmove RESET IDLE")
|
||||||
|
|
||||||
|
def write_dtmcs(self, dtmhardreset=False, dmireset=False):
|
||||||
|
"""Send reset commands to DTMCS. Used to clear sticky DMI OP error status"""
|
||||||
|
data = 0
|
||||||
|
data |= dtmhardreset << 17
|
||||||
|
data |= dmireset << 16
|
||||||
|
if not data:
|
||||||
|
print("Warning: not writing DTMCS (dtmhardreset and dmireset are both false)")
|
||||||
|
return
|
||||||
|
tapname = "cvw.cpu"
|
||||||
|
self.send(f"irscan {tapname} 0x10") # dtmcs instruction
|
||||||
|
self.send(f"drscan {tapname} 32 {hex(data)}")
|
||||||
|
op = self.capture(f"drscan {tapname} 32 0x0")
|
||||||
|
if (int(op) >> 10) & 0x3:
|
||||||
|
raise Exception("Error: failed to reset DTMCS (nonzero dmistat)")
|
||||||
|
|
||||||
|
def write_dmi(self, address, data):
|
||||||
|
cmd = f"riscv dmi_write {address} {data}"
|
||||||
|
rsp = self.capture(cmd)
|
||||||
|
if "Failed" in rsp:
|
||||||
|
raise Exception(rsp)
|
||||||
|
|
||||||
|
def read_dmi(self, address):
|
||||||
|
cmd = f"riscv dmi_read {address}"
|
||||||
|
return self.capture(cmd)
|
||||||
|
|
||||||
|
def activate_dm(self):
|
||||||
|
self.write_dmi("0x10", "0x1")
|
||||||
|
dmstat = int(self.read_dmi("0x10"), 16)
|
||||||
|
if not dmstat & 0x1:
|
||||||
|
raise Exception("Error: failed to activate debug module")
|
||||||
|
|
||||||
|
def reset_dm(self):
|
||||||
|
self.write_dmi("0x10", "0x0")
|
||||||
|
dmstat = int(self.read_dmi("0x10"), 16)
|
||||||
|
if dmstat & 0x1:
|
||||||
|
raise Exception("Error: failed to deactivate debug module")
|
||||||
|
self.activate_dm()
|
||||||
|
|
||||||
|
def reset_hart(self):
|
||||||
|
self.write_dmi("0x10", "0x3")
|
||||||
|
self.write_dmi("0x10", "0x1")
|
||||||
|
dmstat = int(self.read_dmi("0x11"), 16) # check HaveReset
|
||||||
|
if not ((dmstat >> 18) & 0x3):
|
||||||
|
raise Exception("Error: Hart failed to reset")
|
||||||
|
self.write_dmi("0x10", "0x10000001") # ack HaveReset
|
||||||
|
|
||||||
|
def set_haltonreset(self):
|
||||||
|
self.write_dmi("0x10", "0x9")
|
||||||
|
|
||||||
|
def clear_haltonreset(self):
|
||||||
|
self.write_dmi("0x10", "0x5")
|
||||||
|
|
||||||
|
def halt(self):
|
||||||
|
self.write_dmi("0x10", "0x80000001")
|
||||||
|
dmstat = int(self.read_dmi("0x11"), 16) # Check halted bit
|
||||||
|
if not ((dmstat >> 8) & 0x3):
|
||||||
|
raise Exception("Error: Hart failed to halt")
|
||||||
|
|
||||||
|
def resume(self):
|
||||||
|
self.write_dmi("0x10", "0x40000001") # Send resume command
|
||||||
|
dmstat = int(self.read_dmi("0x11"), 16) # Check resumeack bit
|
||||||
|
if not ((dmstat >> 16) & 0x3):
|
||||||
|
raise Exception("Error: Hart failed to resume")
|
||||||
|
self.write_dmi("0x10", "0x40000001") # Clear resumeack bit
|
||||||
|
|
||||||
|
def step(self):
|
||||||
|
self.write_dmi("0x10", "0xC0000001")
|
||||||
|
# BOZO: checking resumeack after halt is pointless until sdext halt method is added
|
||||||
|
dmstat = int(self.read_dmi("0x11"), 16)
|
||||||
|
if not ((dmstat >> 16) & 0x3):
|
||||||
|
raise Exception("Error: Hart failed to resume")
|
||||||
|
|
||||||
|
def access_register(self, write, regno, addr_size=None):
|
||||||
|
data = 1 << 17 # transfer bit always set
|
||||||
|
if not addr_size:
|
||||||
|
addr_size = self.LLEN
|
||||||
|
elif addr_size not in (32, 64, 128):
|
||||||
|
raise Exception("must provide valid register access size (32, 64, 128). See: 3.7.1.1 aarsize")
|
||||||
|
data += int(math.log2(addr_size // 8)) << 20
|
||||||
|
data += write << 16
|
||||||
|
data += regno
|
||||||
|
self.write_dmi("0x17", hex(data))
|
||||||
|
|
||||||
|
def write_data(self, register, data):
|
||||||
|
"""Write data to specified register"""
|
||||||
|
# Write data to 32 bit message registers
|
||||||
|
data = int(data, 16)
|
||||||
|
self.write_dmi("0x4", hex(data & 0xffffffff))
|
||||||
|
if self.LLEN >= 64:
|
||||||
|
self.write_dmi("0x5", hex((data >> 32) & 0xffffffff))
|
||||||
|
if self.LLEN == 128:
|
||||||
|
self.write_dmi("0x6", hex((data >> 64) & 0xffffffff))
|
||||||
|
self.write_dmi("0x7", hex((data >> 96) & 0xffffffff))
|
||||||
|
# Translate register alias to DM regno
|
||||||
|
regno = self.translate_regno(register)
|
||||||
|
# Transfer data from msg registers to target register
|
||||||
|
self.access_register(write=True, regno=regno)
|
||||||
|
# Check that operations completed without error
|
||||||
|
if acerr := self.check_abstrcmderr():
|
||||||
|
raise Exception(acerr)
|
||||||
|
|
||||||
|
def read_data(self, register):
|
||||||
|
"""Read data from specified register"""
|
||||||
|
# Translate register alias to DM regno
|
||||||
|
regno = self.translate_regno(register)
|
||||||
|
# Transfer data from target register to msg registers
|
||||||
|
self.access_register(write=False, regno=regno)
|
||||||
|
# Read data from 32 bit message registers
|
||||||
|
data = ""
|
||||||
|
data = self.read_dmi("0x4").replace("0x", "").zfill(8)
|
||||||
|
if self.LLEN >= 64:
|
||||||
|
data = self.read_dmi("0x5").replace("0x", "").zfill(8) + data
|
||||||
|
if self.LLEN == 128:
|
||||||
|
data = self.read_dmi("0x6").replace("0x", "").zfill(8) + data
|
||||||
|
data = self.read_dmi("0x7").replace("0x", "").zfill(8) + data
|
||||||
|
# Check that operations completed without error
|
||||||
|
if acerr := self.check_abstrcmderr():
|
||||||
|
raise Exception(acerr)
|
||||||
|
return f"0x{data}"
|
||||||
|
|
||||||
|
def translate_regno(self, register):
|
||||||
|
if register not in self.register_translations:
|
||||||
|
register = self.abi_translations[register]
|
||||||
|
return int(self.register_translations[register], 16)
|
||||||
|
|
||||||
|
def check_abstrcmderr(self):
|
||||||
|
"""These errors must be cleared using clear_abstrcmd_err() before another OP can be executed"""
|
||||||
|
abstractcs = int(self.read_dmi("0x16"), 16)
|
||||||
|
# CmdErr is only valid if Busy is 0
|
||||||
|
while True:
|
||||||
|
if not bool((abstractcs & 0x1000) >> 12): # if not Busy
|
||||||
|
break
|
||||||
|
time.sleep(0.05)
|
||||||
|
abstractcs = int(self.read_dmi("0x16"), 16)
|
||||||
|
return self.cmderr_translations[(abstractcs & 0x700) >> 8]
|
||||||
|
|
||||||
|
def clear_abstrcmd_err(self):
|
||||||
|
self.write_dmi("0x16", "0x700")
|
||||||
|
if self.check_abstrcmderr():
|
||||||
|
raise Exception("Error: failed to clear AbstrCmdErr")
|
||||||
|
|
||||||
|
# 6.1.4 dtmcs errinfo translation table
|
||||||
|
errinfo_translations = {
|
||||||
|
0 : "not implemented",
|
||||||
|
1 : "dmi error",
|
||||||
|
2 : "communication error",
|
||||||
|
3 : "device error",
|
||||||
|
4 : "unknown",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 6.1.5 DMI op translation table
|
||||||
|
op_translations = {
|
||||||
|
0 : "success",
|
||||||
|
1 : "reserved",
|
||||||
|
2 : "failed",
|
||||||
|
3 : "busy",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3.14.6 Abstract command CmdErr value translation table
|
||||||
|
cmderr_translations = {
|
||||||
|
0 : None,
|
||||||
|
1 : "busy",
|
||||||
|
2 : "not supported",
|
||||||
|
3 : "exception",
|
||||||
|
4 : "halt/resume",
|
||||||
|
5 : "bus",
|
||||||
|
6 : "reserved",
|
||||||
|
7 : "other",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Register alias to regno translation table
|
||||||
|
register_translations = {
|
||||||
|
"MISA" : "0x0301",
|
||||||
|
"TRAPM" : "0xC000",
|
||||||
|
"PCM" : "0xC001",
|
||||||
|
"INSTRM" : "0xC002",
|
||||||
|
"MEMRWM" : "0xC003",
|
||||||
|
"INSTRVALIDM" : "0xC004",
|
||||||
|
"WRITEDATAM" : "0xC005",
|
||||||
|
"IEUADRM" : "0xC006",
|
||||||
|
"READDATAM" : "0xC007",
|
||||||
|
"x0" : "0x1000",
|
||||||
|
"x1" : "0x1001",
|
||||||
|
"x2" : "0x1002",
|
||||||
|
"x3" : "0x1003",
|
||||||
|
"x4" : "0x1004",
|
||||||
|
"x5" : "0x1005",
|
||||||
|
"x6" : "0x1006",
|
||||||
|
"x7" : "0x1007",
|
||||||
|
"x8" : "0x1008",
|
||||||
|
"x9" : "0x1009",
|
||||||
|
"x10" : "0x100A",
|
||||||
|
"x11" : "0x100B",
|
||||||
|
"x12" : "0x100C",
|
||||||
|
"x13" : "0x100D",
|
||||||
|
"x14" : "0x100E",
|
||||||
|
"x15" : "0x100F",
|
||||||
|
"x16" : "0x1010",
|
||||||
|
"x17" : "0x1011",
|
||||||
|
"x18" : "0x1012",
|
||||||
|
"x19" : "0x1013",
|
||||||
|
"x20" : "0x1014",
|
||||||
|
"x21" : "0x1015",
|
||||||
|
"x22" : "0x1016",
|
||||||
|
"x23" : "0x1017",
|
||||||
|
"x24" : "0x1018",
|
||||||
|
"x25" : "0x1019",
|
||||||
|
"x26" : "0x101A",
|
||||||
|
"x27" : "0x101B",
|
||||||
|
"x28" : "0x101C",
|
||||||
|
"x29" : "0x101D",
|
||||||
|
"x30" : "0x101E",
|
||||||
|
"x31" : "0x101F",
|
||||||
|
"f0" : "0x1020",
|
||||||
|
"f1" : "0x1021",
|
||||||
|
"f2" : "0x1022",
|
||||||
|
"f3" : "0x1023",
|
||||||
|
"f4" : "0x1024",
|
||||||
|
"f5" : "0x1025",
|
||||||
|
"f6" : "0x1026",
|
||||||
|
"f7" : "0x1027",
|
||||||
|
"f8" : "0x1028",
|
||||||
|
"f9" : "0x1029",
|
||||||
|
"f10" : "0x102A",
|
||||||
|
"f11" : "0x102B",
|
||||||
|
"f12" : "0x102C",
|
||||||
|
"f13" : "0x102D",
|
||||||
|
"f14" : "0x102E",
|
||||||
|
"f15" : "0x102F",
|
||||||
|
"f16" : "0x1030",
|
||||||
|
"f17" : "0x1031",
|
||||||
|
"f18" : "0x1032",
|
||||||
|
"f19" : "0x1033",
|
||||||
|
"f20" : "0x1034",
|
||||||
|
"f21" : "0x1035",
|
||||||
|
"f22" : "0x1036",
|
||||||
|
"f23" : "0x1037",
|
||||||
|
"f24" : "0x1038",
|
||||||
|
"f25" : "0x1039",
|
||||||
|
"f26" : "0x103A",
|
||||||
|
"f27" : "0x103B",
|
||||||
|
"f28" : "0x103C",
|
||||||
|
"f29" : "0x103D",
|
||||||
|
"f30" : "0x103E",
|
||||||
|
"f31" : "0x103F",
|
||||||
|
}
|
||||||
|
|
||||||
|
abi_translations = {
|
||||||
|
"x0" : "zero",
|
||||||
|
"x1" : "ra",
|
||||||
|
"x2" : "sp",
|
||||||
|
"x3" : "gp",
|
||||||
|
"x4" : "tp",
|
||||||
|
"x5" : "t0",
|
||||||
|
"x6" : "t1",
|
||||||
|
"x7" : "t2",
|
||||||
|
"x8" : "s0/fp",
|
||||||
|
"x9" : "s1",
|
||||||
|
"x10" : "a0",
|
||||||
|
"x11" : "a1",
|
||||||
|
"x12" : "a2",
|
||||||
|
"x13" : "a3",
|
||||||
|
"x14" : "a4",
|
||||||
|
"x15" : "a5",
|
||||||
|
"x16" : "a6",
|
||||||
|
"x17" : "a7",
|
||||||
|
"x18" : "s2",
|
||||||
|
"x19" : "s3",
|
||||||
|
"x20" : "s4",
|
||||||
|
"x21" : "s5",
|
||||||
|
"x22" : "s6",
|
||||||
|
"x23" : "s7",
|
||||||
|
"x24" : "s8",
|
||||||
|
"x25" : "s9",
|
||||||
|
"x26" : "s10",
|
||||||
|
"x27" : "s11",
|
||||||
|
"x28" : "t3",
|
||||||
|
"x29" : "t4",
|
||||||
|
"x30" : "t5",
|
||||||
|
"x31" : "t6",
|
||||||
|
"f0" : "ft0",
|
||||||
|
"f1" : "ft1",
|
||||||
|
"f2" : "ft2",
|
||||||
|
"f3" : "ft3",
|
||||||
|
"f4" : "ft4",
|
||||||
|
"f5" : "ft5",
|
||||||
|
"f6" : "ft6",
|
||||||
|
"f7" : "ft7",
|
||||||
|
"f8" : "fs0",
|
||||||
|
"f9" : "fs1",
|
||||||
|
"f10" : "fa0",
|
||||||
|
"f11" : "fa1",
|
||||||
|
"f12" : "fa2",
|
||||||
|
"f13" : "fa3",
|
||||||
|
"f14" : "fa4",
|
||||||
|
"f15" : "fa5",
|
||||||
|
"f16" : "fa6",
|
||||||
|
"f17" : "fa7",
|
||||||
|
"f18" : "fs2",
|
||||||
|
"f19" : "fs3",
|
||||||
|
"f20" : "fs4",
|
||||||
|
"f21" : "fs5",
|
||||||
|
"f22" : "fs6",
|
||||||
|
"f23" : "fs7",
|
||||||
|
"f24" : "fs8",
|
||||||
|
"f25" : "fs9",
|
||||||
|
"f26" : "fs10",
|
||||||
|
"f27" : "fs11",
|
||||||
|
"f28" : "ft8",
|
||||||
|
"f29" : "ft9",
|
||||||
|
"f30" : "ft10",
|
||||||
|
"f31" : "ft11",
|
||||||
|
}
|
||||||
|
abi_translations |= dict(map(reversed, abi_translations.items())) # two way translations
|
||||||
|
|
||||||
|
nonstandard_register_lengths = {
|
||||||
|
"TRAPM" : 1,
|
||||||
|
"INSTRM" : 32,
|
||||||
|
"MEMRWM" : 2,
|
||||||
|
"INSTRVALIDM" : 1,
|
||||||
|
"READDATAM" : 64
|
||||||
|
}
|
@ -6,7 +6,7 @@ adapter driver ftdi
|
|||||||
# when multiple adapters with the same vid_pid are connected (ex: arty-a7 and usb-jtag)
|
# when multiple adapters with the same vid_pid are connected (ex: arty-a7 and usb-jtag)
|
||||||
# need to specify which usb port to drive
|
# need to specify which usb port to drive
|
||||||
# find numerical path using command "lsusb -t" (<bus>-<port>)
|
# find numerical path using command "lsusb -t" (<bus>-<port>)
|
||||||
adapter usb location 1-3
|
adapter usb location 1-4
|
||||||
|
|
||||||
ftdi vid_pid 0x0403 0x6010
|
ftdi vid_pid 0x0403 0x6010
|
||||||
ftdi channel 0
|
ftdi channel 0
|
||||||
|
Loading…
Reference in New Issue
Block a user