diff --git a/README.md b/README.md index aea70bd04..69083a061 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,8 @@ Requirements - macOS, Linux, or Windows 7 or newer - Microcontroller with an Arm Cortex-M CPU - Supported debug probe - - [CMSIS-DAP](http://www.keil.com/pack/doc/CMSIS/DAP/html/index.html), such as an on-board debug - probe using [DAPLink](https://os.mbed.com/handbook/DAPLink) firmware. + - [CMSIS-DAP](http://www.keil.com/pack/doc/CMSIS/DAP/html/index.html) v1 (HID) and v2 (WinUSB), + such as an on-board debug probe using [DAPLink](https://os.mbed.com/handbook/DAPLink) firmware. - STLinkV2, either on-board or the standalone version. diff --git a/pyocd/probe/pydapaccess/cmsis_dap_core.py b/pyocd/probe/pydapaccess/cmsis_dap_core.py index daa289ed0..2790be6f3 100644 --- a/pyocd/probe/pydapaccess/cmsis_dap_core.py +++ b/pyocd/probe/pydapaccess/cmsis_dap_core.py @@ -84,6 +84,7 @@ class Pin: class DAPSWOTransport: NONE = 0 DAP_SWO_DATA = 1 + DAP_SWO_EP = 2 # SWO mode options. class DAPSWOMode: diff --git a/pyocd/probe/pydapaccess/dap_access_cmsis_dap.py b/pyocd/probe/pydapaccess/dap_access_cmsis_dap.py index e19dccfd6..c9dbdbe55 100644 --- a/pyocd/probe/pydapaccess/dap_access_cmsis_dap.py +++ b/pyocd/probe/pydapaccess/dap_access_cmsis_dap.py @@ -24,7 +24,7 @@ from .dap_settings import DAPSettings from .dap_access_api import DAPAccessIntf from .cmsis_dap_core import CMSISDAPProtocol -from .interface import (INTERFACE, USB_BACKEND, WS_BACKEND) +from .interface import (INTERFACE, USB_BACKEND, USB_BACKEND_V2, WS_BACKEND) from .cmsis_dap_core import (Command, Pin, Capabilities, DAP_TRANSFER_OK, DAP_TRANSFER_FAULT, DAP_TRANSFER_WAIT, DAPSWOTransport, DAPSWOMode, DAPSWOControl, @@ -50,10 +50,16 @@ class SWOStatus: def _get_interfaces(): """Get the connected USB devices""" + # Get CMSIS-DAPv2 interfaces. if DAPSettings.use_ws: - return INTERFACE[WS_BACKEND].get_all_connected_interfaces(DAPSettings.ws_host, DAPSettings.ws_port) + interfaces = INTERFACE[WS_BACKEND].get_all_connected_interfaces(DAPSettings.ws_host, DAPSettings.ws_port) else: - return INTERFACE[USB_BACKEND].get_all_connected_interfaces() + interfaces = INTERFACE[USB_BACKEND].get_all_connected_interfaces() + + # Add in CMSIS-DAPv2 interfaces. + interfaces += INTERFACE[USB_BACKEND_V2].get_all_connected_interfaces() + + return interfaces def _get_unique_id(interface): @@ -675,10 +681,16 @@ def swo_configure(self, enabled, rate): try: if enabled: - if self._protocol.swo_transport(DAPSWOTransport.DAP_SWO_DATA) != 0: + # Select the streaming SWO endpoint if available. + if self._interface.has_swo_ep: + transport = DAPSWOTransport.DAP_SWO_EP + else: + transport = DAPSWOTransport.DAP_SWO_DATA + + if self._protocol.swo_transport(transport) != 0: self._swo_disable() return False - if self._protocol.swo_mode(DAP_SWO_MODE.UART) != 0: + if self._protocol.swo_mode(DAPSWOMode.UART) != 0: self._swo_disable() return False if self._protocol.swo_baudrate(rate) == 0: @@ -709,9 +721,13 @@ def swo_control(self, start): if start: self._protocol.swo_control(DAPSWOControl.START) + if self._interface.has_swo_ep: + self._interface.start_swo() self._swo_status = SWOStatus.RUNNING else: self._protocol.swo_control(DAPSWOControl.STOP) + if self._interface.has_swo_ep: + self._interface.stop_swo() self._swo_status = SWOStatus.CONFIGURED return True @@ -719,9 +735,13 @@ def get_swo_status(self): return self._protocol.swo_status() def swo_read(self, count=None): - if count is None: - count = self._packet_size - return self._protocol.swo_data(count) + if self._interface.has_swo_ep: + return self._interface.read_swo() + else: + if count is None: + count = self._packet_size + status, count, data = self._protocol.swo_data(count) + return bytearray(data) def write_reg(self, reg_id, value, dap_index=0): assert reg_id in self.REG diff --git a/pyocd/probe/pydapaccess/interface/__init__.py b/pyocd/probe/pydapaccess/interface/__init__.py index f884d9340..1b227b250 100644 --- a/pyocd/probe/pydapaccess/interface/__init__.py +++ b/pyocd/probe/pydapaccess/interface/__init__.py @@ -19,12 +19,14 @@ import logging from .hidapi_backend import HidApiUSB from .pyusb_backend import PyUSB +from .pyusb_v2_backend import PyUSBv2 from .pywinusb_backend import PyWinUSB from .ws_backend import WebSocketInterface INTERFACE = { 'hidapiusb': HidApiUSB, 'pyusb': PyUSB, + 'pyusb_v2': PyUSBv2, 'pywinusb': PyWinUSB, 'ws': WebSocketInterface } @@ -58,3 +60,4 @@ else: raise Exception("No USB backend found") +USB_BACKEND_V2 = "pyusb_v2" diff --git a/pyocd/probe/pydapaccess/interface/common.py b/pyocd/probe/pydapaccess/interface/common.py new file mode 100644 index 000000000..e28581c34 --- /dev/null +++ b/pyocd/probe/pydapaccess/interface/common.py @@ -0,0 +1,24 @@ +# pyOCD debugger +# Copyright (c) 2019 Arm Limited +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# 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. + +USB_CLASS_COMPOSITE = 0x00 +USB_CLASS_MISCELLANEOUS = 0xef + +CMSIS_DAP_USB_CLASSES = [ + USB_CLASS_COMPOSITE, + USB_CLASS_MISCELLANEOUS, + ] + diff --git a/pyocd/probe/pydapaccess/interface/pyusb_backend.py b/pyocd/probe/pydapaccess/interface/pyusb_backend.py index cace950a0..39f9dadfb 100644 --- a/pyocd/probe/pydapaccess/interface/pyusb_backend.py +++ b/pyocd/probe/pydapaccess/interface/pyusb_backend.py @@ -16,6 +16,7 @@ """ from .interface import Interface +from .common import CMSIS_DAP_USB_CLASSES from ..dap_access_api import DAPAccessIntf import logging import os @@ -256,6 +257,10 @@ def __init__(self, serial=None): def __call__(self, dev): """Return True if this is a DAP device, False otherwise""" + # Check if the device class is a valid one for CMSIS-DAP. + if dev.bDeviceClass not in CMSIS_DAP_USB_CLASSES: + return False + try: device_string = dev.product except ValueError as error: diff --git a/pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py b/pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py new file mode 100644 index 000000000..2811db77b --- /dev/null +++ b/pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py @@ -0,0 +1,304 @@ +# pyOCD debugger +# Copyright (c) 2019 Arm Limited +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# 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. + +from .interface import Interface +from .common import CMSIS_DAP_USB_CLASSES +from ..dap_access_api import DAPAccessIntf +import logging +import os +import threading +import six +from time import sleep +import errno + +LOG = logging.getLogger(__name__) + +try: + import usb.core + import usb.util +except: + IS_AVAILABLE = False +else: + IS_AVAILABLE = True + +class PyUSBv2(Interface): + """! + @brief CMSIS-DAPv2 interface using pyUSB. + """ + + isAvailable = IS_AVAILABLE + + def __init__(self): + super(PyUSBv2, self).__init__() + self.vid = 0 + self.pid = 0 + self.product_name = None + self.vendor_name = None + self.ep_out = None + self.ep_in = None + self.ep_swo = None + self.dev = None + self.intf_number = None + self.serial_number = None + self.kernel_driver_was_attached = False + self.closed = True + self.thread = None + self.rx_stop_event = None + self.swo_thread = None + self.swo_stop_event = None + self.rcv_data = [] + self.swo_data = [] + self.read_sem = threading.Semaphore(0) + self.packet_size = 512 + self.is_swo_running = False + + @property + def has_swo_ep(self): + return self.ep_swo is not None + + def open(self): + assert self.closed is True + + # Get device handle + dev = usb.core.find(custom_match=HasCmsisDapv2Interface(self.serial_number)) + if dev is None: + raise DAPAccessIntf.DeviceError("Device %s not found" % + self.serial_number) + + # get active config + config = dev.get_active_configuration() + + # Get CMSIS-DAPv2 interface + interface = usb.util.find_descriptor(config, custom_match=match_cmsis_dap_interface_name) + if interface is None: + raise DAPAccessIntf.DeviceError("Device %s has no CMSIS-DAPv2 interface" % + self.serial_number) + interface_number = interface.bInterfaceNumber + + # Find endpoints. CMSIS-DAPv2 endpoints are in a fixed order. + try: + ep_out = interface.endpoints()[0] + ep_in = interface.endpoints()[1] + ep_swo = interface.endpoints()[2] if len(interface.endpoints()) > 2 else None + except IndexError: + raise DAPAccessIntf.DeviceError("CMSIS-DAPv2 device %s is missing endpoints" % + self.serial_number) + + # Explicitly claim the interface + try: + usb.util.claim_interface(dev, interface_number) + except usb.core.USBError as exc: + raise six.raise_from(DAPAccessIntf.DeviceError("Unable to open device"), exc) + + # Update all class variables if we made it here + self.ep_out = ep_out + self.ep_in = ep_in + self.ep_swo = ep_swo + self.dev = dev + self.intf_number = interface_number + + # Start RX thread as the last step + self.closed = False + self.start_rx() + + def start_rx(self): + # Flush the RX buffers by reading until timeout exception + try: + while True: + self.ep_in.read(self.ep_in.wMaxPacketSize, 1) + except usb.core.USBError: + # USB timeout expected + pass + + # Start RX thread + self.rx_stop_event = threading.Event() + thread_name = "CMSIS-DAP receive (%s)" % self.serial_number + self.thread = threading.Thread(target=self.rx_task, name=thread_name) + self.thread.daemon = True + self.thread.start() + + def start_swo(self): + self.swo_stop_event = threading.Event() + thread_name = "SWO receive (%s)" % self.serial_number + self.swo_thread = threading.Thread(target=self.swo_rx_task, name=thread_name) + self.swo_thread.daemon = True + self.swo_thread.start() + self.is_swo_running = True + + def stop_swo(self): + self.swo_stop_event.set() + self.swo_thread.join() + self.swo_thread = None + self.swo_stop_event = None + self.is_swo_running = False + + def rx_task(self): + try: + while not self.rx_stop_event.is_set(): + self.read_sem.acquire() + if not self.rx_stop_event.is_set(): + self.rcv_data.append(self.ep_in.read(self.ep_in.wMaxPacketSize, 10 * 1000)) + finally: + # Set last element of rcv_data to None on exit + self.rcv_data.append(None) + + def swo_rx_task(self): + try: + while not self.swo_stop_event.is_set(): + try: + self.swo_data.append(self.ep_swo.read(self.ep_swo.wMaxPacketSize, 10 * 1000)) + except usb.core.USBError: + pass + finally: + # Set last element of swo_data to None on exit + self.swo_data.append(None) + + @staticmethod + def get_all_connected_interfaces(): + """! @brief Returns all the connected devices with a CMSIS-DAPv2 interface.""" + # find all cmsis-dap devices + try: + all_devices = usb.core.find(find_all=True, custom_match=HasCmsisDapv2Interface()) + except usb.core.NoBackendError: + # Print a warning if pyusb cannot find a backend, and return no probes. + LOG.warning("CMSIS-DAPv2 probes are not supported because no libusb library was found.") + return [] + + # iterate on all devices found + boards = [] + for board in all_devices: + new_board = PyUSBv2() + new_board.vid = board.idVendor + new_board.pid = board.idProduct + new_board.product_name = board.product + new_board.vendor_name = board.manufacturer + new_board.serial_number = board.serial_number + boards.append(new_board) + + return boards + + def write(self, data): + """! @brief Write data on the OUT endpoint.""" + + report_size = self.packet_size + if self.ep_out: + report_size = self.ep_out.wMaxPacketSize + + for _ in range(report_size - len(data)): + data.append(0) + + self.read_sem.release() + + self.ep_out.write(data) + #logging.debug('sent: %s', data) + + def read(self): + """! @brief Read data on the IN endpoint.""" + while len(self.rcv_data) == 0: + sleep(0) + + if self.rcv_data[0] is None: + raise DAPAccessIntf.DeviceError("Device %s read thread exited unexpectedly" % self.serial_number) + return self.rcv_data.pop(0) + + def read_swo(self): + # Accumulate all available SWO data. + data = bytearray() + while len(self.swo_data): + if self.swo_data[0] is None: + raise DAPAccessIntf.DeviceError("Device %s SWO thread exited unexpectedly" % self.serial_number) + data += self.swo_data.pop(0) + + return data + + def set_packet_count(self, count): + # No interface level restrictions on count + self.packet_count = count + + def set_packet_size(self, size): + self.packet_size = size + + def get_serial_number(self): + return self.serial_number + + def close(self): + """! @brief Close the USB interface.""" + assert self.closed is False + + if self.is_swo_running: + self.stop_swo() + self.closed = True + self.rx_stop_event.set() + self.read_sem.release() + self.thread.join() + assert self.rcv_data[-1] is None + self.rcv_data = [] + self.swo_data = [] + usb.util.release_interface(self.dev, self.intf_number) + usb.util.dispose_resources(self.dev) + self.ep_out = None + self.ep_in = None + self.ep_swo = None + self.dev = None + self.intf_number = None + self.thread = None + +def match_cmsis_dap_interface_name(desc): + interface_name = usb.util.get_string(desc.device, desc.iInterface) + return (interface_name is not None) and ("CMSIS-DAP" in interface_name) + +class HasCmsisDapv2Interface(object): + """! @brief CMSIS-DAPv2 match class to be used with usb.core.find""" + + def __init__(self, serial=None): + """! @brief Create a new FindDap object with an optional serial number""" + self._serial = serial + + def __call__(self, dev): + """! @brief Return True if this is a CMSIS-DAPv2 device, False otherwise""" + # Check if the device class is a valid one for CMSIS-DAP. + if dev.bDeviceClass not in CMSIS_DAP_USB_CLASSES: + return False + + try: + config = dev.get_active_configuration() + cmsis_dap_interface = usb.util.find_descriptor(config, custom_match=match_cmsis_dap_interface_name) + except OSError as error: + if error.errno == errno.EACCES: + LOG.debug(("Error \"{}\" while trying to access the USB device configuration " + "for VID=0x{:04x} PID=0x{:04x}. This can probably be remedied with a udev rule.") + .format(error, dev.idVendor, dev.idProduct)) + else: + LOG.warning("OS error getting USB interface string: %s", error) + return False + except usb.core.USBError as error: + LOG.warning("Exception getting product string: %s", error) + return False + except IndexError as error: + LOG.warning("Internal pyusb error: %s", error) + return False + + if cmsis_dap_interface is None: + return False + + # Check the class and subclass are vendor-specific. + if (cmsis_dap_interface.bInterfaceClass != 0xff) or (cmsis_dap_interface.bInterfaceSubClass != 0): + return False + + if self._serial is not None: + if self._serial != dev.serial_number: + return False + return True diff --git a/udev/50-pyocd.rules b/udev/50-pyocd.rules index 6206d58a2..e68967913 100644 --- a/udev/50-pyocd.rules +++ b/udev/50-pyocd.rules @@ -16,5 +16,6 @@ SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="374e", MODE:="666" SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="374f", MODE:="666" SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="3753", MODE:="666" - +# c251:2750 Keil ULINKplus +SUBSYSTEM=="usb", ATTR{idVendor}=="c251", ATTR{idProduct}=="2750", MODE:="666"