Source code for qwiic_scmd

#-----------------------------------------------------------------------------
# qwiic_scmd.py
#
# Qwii interface for the Serial Control Motor Driver
#------------------------------------------------------------------------
#
# Written by  SparkFun Electronics, May 2019
#
#
# More information on qwiic is at https:# www.sparkfun.com/qwiic
#
# Do you like this library? Help support SparkFun. Buy a board!
#
#==================================================================================
# Copyright (c) 2019 SparkFun Electronics
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#==================================================================================
#
# This is mostly a port of existing Arduino functionaly, so pylint is sad.
# The goal is to keep the public interface pthonic, but internal is internal
#
# pylint: disable=line-too-long, bad-whitespace, invalid-name
# pylint: disable=too-many-public-methods, too-many-instance-attributes
#

"""
qwiic_scmd
============
Python module for the serial control motor driver.

This python package is a port of the existing [SparkFun Serial Controlled Motor Driver Arduino Library](https://github.com/sparkfun/SparkFun_Serial_Controlled_Motor_Driver_Arduino_Library)

This package can be used in conjunction with the overall [SparkFun qwiic Python Package](https://github.com/sparkfun/Qwiic_Py)

New to qwiic? Take a look at the entire [SparkFun qwiic ecosystem](https://www.sparkfun.com/qwiic).

"""
from __future__ import print_function

import qwiic_i2c

# Define the device name and I2C addresses. These are set in the class defintion
# as class variables, making them avilable without having to create a class instance.
#
# The name of this device - note this is private
_DEFAULT_NAME = "Qwiic Serial Control Motor Driver"

# Some devices have multiple availabel addresses - this is a list of these addresses.
# NOTE: The first address in this list is considered the default I2C address for the
# device.
_AVAILABLE_I2C_ADDRESS = [0x5D, 0x58, 0x59, 0x5A, 0x5C]

# pylint: disable=too-few-public-methods
# Simple replication of the diagnostic class.
[docs]class SCMDDiagnostics(object): """ SCMDDiagnostics Object used for diagnostic reporting. :ivar numberOfSlaves: :ivar U_I2C_RD_ERR: :ivar U_I2C_WR_ERR: :ivar U_BUF_DUMPED: :ivar E_I2C_RD_ERR: :ivar E_I2C_WR_ERR: :ivar LOOP_TIME: :ivar SLV_POLL_CNT: :ivar MST_E_ERR: :ivar MST_E_STATUS: :ivar FSAFE_FAULTS: :ivar REG_OOR_CNT: :ivar REG_RO_WRITE_CNT: """ def __init__(self): super(SCMDDiagnostics, self).__init__() # Attainable metrics from SCMD self.numberOfSlaves = 0 self.U_I2C_RD_ERR = 0 self.U_I2C_WR_ERR = 0 self.U_BUF_DUMPED = 0 self.E_I2C_RD_ERR = 0 self.E_I2C_WR_ERR = 0 self.LOOP_TIME = 0 self.SLV_POLL_CNT = 0 self.MST_E_ERR = 0 self.MST_E_STATUS = 0 self.FSAFE_FAULTS = 0 self.REG_OOR_CNT = 0 self.REG_RO_WRITE_CNT = 0
[docs]class QwiicScmd(object): """ QwiicScmd :param address: The I2C address to use for the device. If not provided, the default address is used. :param i2c_driver: An existing i2c driver object. If not provided a driver object is created. :return: The Serial Control Motor Driver device object. :rtype: Object """ # Constructor device_name = _DEFAULT_NAME available_addresses = _AVAILABLE_I2C_ADDRESS # Define the flags for the device # defaults ( Set config in PSoC, use for reference in Arduino ) ID_WORD = 0xA9 # Device ID to be programmed into memory for reads START_SLAVE_ADDR = 0x50 # Start address of slaves MAX_SLAVE_ADDR = 0x5F # Max address of slaves MASTER_LOCK_KEY = 0x9B USER_LOCK_KEY = 0x5C FIRMWARE_VERSION = 0x07 POLL_ADDRESS = 0x4A # Address of an unasigned, ready slave MAX_POLL_LIMIT = 0xC8 # 200 # SCMD_STATUS_1 bits SCMD_ENUMERATION_BIT = 0x01 SCMD_BUSY_BIT = 0x02 SCMD_REM_READ_BIT = 0x04 SCMD_REM_WRITE_BIT = 0x08 SCMD_HW_EN_BIT = 0x10 # SCMD_CONTROL_1 bits SCMD_FULL_RESET_BIT = 0x01 SCMD_RE_ENUMERATE_BIT = 0x02 # SCMD_FSAFE_CTRL bits and masks SCMD_FSAFE_DRIVE_KILL = 0x01 SCMD_FSAFE_RESTART_MASK = 0x06 SCMD_FSAFE_REBOOT = 0x02 SCMD_FSAFE_RE_ENUM = 0x04 SCMD_FSAFE_CYCLE_USER = 0x08 SCMD_FSAFE_CYCLE_EXP = 0x10 # SCMD_MST_E_IN_FN bits and masks SCMD_M_IN_RESTART_MASK = 0x03 SCMD_M_IN_REBOOT = 0x01 SCMD_M_IN_RE_ENUM = 0x02 SCMD_M_IN_CYCLE_USER = 0x04 SCMD_M_IN_CYCLE_EXP = 0x08 # Address map SCMD_FID = 0x00 SCMD_ID = 0x01 SCMD_SLAVE_ADDR = 0x02 SCMD_CONFIG_BITS = 0x03 SCMD_U_I2C_RD_ERR = 0x04 SCMD_U_I2C_WR_ERR = 0x05 SCMD_U_BUF_DUMPED = 0x06 SCMD_E_I2C_RD_ERR = 0x07 SCMD_E_I2C_WR_ERR = 0x08 SCMD_LOOP_TIME = 0x09 SCMD_SLV_POLL_CNT = 0x0A SCMD_SLV_TOP_ADDR = 0x0B SCMD_MST_E_ERR = 0x0C SCMD_MST_E_STATUS = 0x0D SCMD_FSAFE_FAULTS = 0x0E SCMD_REG_OOR_CNT = 0x0F SCMD_REG_RO_WRITE_CNT = 0x10 SCMD_GEN_TEST_WORD = 0x11 SCMD_MOTOR_A_INVERT = 0x12 SCMD_MOTOR_B_INVERT = 0x13 SCMD_BRIDGE = 0x14 SCMD_LOCAL_MASTER_LOCK = 0x15 SCMD_LOCAL_USER_LOCK = 0x16 SCMD_MST_E_IN_FN = 0x17 SCMD_U_PORT_CLKDIV_U = 0x18 SCMD_U_PORT_CLKDIV_L = 0x19 SCMD_U_PORT_CLKDIV_CTRL = 0x1A SCMD_E_PORT_CLKDIV_U = 0x1B SCMD_E_PORT_CLKDIV_L = 0x1C SCMD_E_PORT_CLKDIV_CTRL = 0x1D SCMD_U_BUS_UART_BAUD = 0x1E SCMD_FSAFE_CTRL = 0x1F SCMD_MA_DRIVE = 0x20 SCMD_MB_DRIVE = 0x21 SCMD_S1A_DRIVE = 0x22 SCMD_S1B_DRIVE = 0x23 SCMD_S2A_DRIVE = 0x24 SCMD_S2B_DRIVE = 0x25 SCMD_S3A_DRIVE = 0x26 SCMD_S3B_DRIVE = 0x27 SCMD_S4A_DRIVE = 0x28 SCMD_S4B_DRIVE = 0x29 SCMD_S5A_DRIVE = 0x2A SCMD_S5B_DRIVE = 0x2B SCMD_S6A_DRIVE = 0x2C SCMD_S6B_DRIVE = 0x2D SCMD_S7A_DRIVE = 0x2E SCMD_S7B_DRIVE = 0x2F SCMD_S8A_DRIVE = 0x30 SCMD_S8B_DRIVE = 0x31 SCMD_S9A_DRIVE = 0x32 SCMD_S9B_DRIVE = 0x33 SCMD_S10A_DRIVE = 0x34 SCMD_S10B_DRIVE = 0x35 SCMD_S11A_DRIVE = 0x36 SCMD_S11B_DRIVE = 0x37 SCMD_S12A_DRIVE = 0x38 SCMD_S12B_DRIVE = 0x39 SCMD_S13A_DRIVE = 0x3A SCMD_S13B_DRIVE = 0x3B SCMD_S14A_DRIVE = 0x3C SCMD_S14B_DRIVE = 0x3D SCMD_S15A_DRIVE = 0x3E SCMD_S15B_DRIVE = 0x3F SCMD_S16A_DRIVE = 0x40 SCMD_S16B_DRIVE = 0x41 SCMD_INV_2_9 = 0x50 SCMD_INV_10_17 = 0x51 SCMD_INV_18_25 = 0x52 SCMD_INV_26_33 = 0x53 SCMD_BRIDGE_SLV_L = 0x54 SCMD_BRIDGE_SLV_H = 0x55 # SCMD_PAGE_SELECT = 0x6F SCMD_DRIVER_ENABLE = 0x70 SCMD_UPDATE_RATE = 0x71 SCMD_FORCE_UPDATE = 0x72 SCMD_E_BUS_SPEED = 0x73 SCMD_MASTER_LOCK = 0x74 SCMD_USER_LOCK = 0x75 SCMD_FSAFE_TIME = 0x76 SCMD_STATUS_1 = 0x77 SCMD_CONTROL_1 = 0x78 SCMD_REM_ADDR = 0x79 SCMD_REM_OFFSET = 0x7A SCMD_REM_DATA_WR = 0x7B SCMD_REM_DATA_RD = 0x7C SCMD_REM_WRITE = 0x7D SCMD_REM_READ = 0x7E def __init__(self, address=None, i2c_driver=None): # Did the user specify an I2C address? self.address = address if address is not None else self.available_addresses[0] # load the I2C driver if one isn't provided if i2c_driver is None: self._i2c = qwiic_i2c.getI2CDriver() if self._i2c is None: print("Unable to load I2C driver for this platform.") return else: self._i2c = i2c_driver #----------------------------------------------
[docs] def is_connected(self): """ Determine if a SCMD device is conntected to the system.. :return: True if the device is connected, otherwise False. :rtype: bool """ return qwiic_i2c.isDeviceConnected(self.address)
connected = property(is_connected)
[docs] def begin(self): """ Initialize the operation of the SCMD module :return: Returns true of the initializtion was successful, otherwise False. :rtype: bool """ # dummy read self._i2c.readByte(self.address, self.SCMD_ID) return self._i2c.readByte(self.address, self.SCMD_ID)
# check if enumeration is complete
[docs] def ready(self): """ Returns if the driver is ready :return: Ready status :rtype: boolean """ statusByte = self._i2c.readByte(self.address, self.SCMD_STATUS_1) return statusByte & self.SCMD_ENUMERATION_BIT and statusByte != 0xFF #wait for ready flag and not 0xFF
[docs] def busy(self): """ Returns if the driver is busy :return: busy status :rtype: boolean """ statusByte = self._i2c.readByte(self.address, self.SCMD_STATUS_1) return statusByte & (self.SCMD_BUSY_BIT | self.SCMD_REM_READ_BIT | self.SCMD_REM_WRITE_BIT) != 0
# Enable and disable functions. Call after begin to enable the h-bridges
[docs] def enable(self): """ Enable driver functions :return: No return value """ self._i2c.writeByte(self.address, self.SCMD_DRIVER_ENABLE, 0x01)
[docs] def disable(self): """ Disable driver functions :return: No return value """ self._i2c.writeByte(self.address, self.SCMD_DRIVER_ENABLE, 0x00)
# this is a hack in the Arduino lib - the placeholder is for compatablity
[docs] def reset(self): """ This is a hack in the Arduino lib - the placeholder is for compatablity """ pass
# ****************************************************************************# # # Drive Section # # ****************************************************************************# # set_drive( ... ) # # Drive a motor at a level # # uint8_t motorNum -- Motor number from 0 to 33 # uint8_t direction -- 0 or 1 for forward and backward # int8_t level -- (-255) to 255 for drive strength
[docs] def set_drive(self, motorNum, direction, level): """ Drive a motor at a level :param motoNum: Motor number from 0 to 33 :param direction: 0 or 1 for forward and backward :param level: (-255) to 255 for drive strength :return: No return value """ # Convert value to a 7-bit int and match the indexing for uint8_t values as needed in Arduino library level = int(round((level + 1 - direction)/2)) driveValue = 0 # use to build value to actually write to register # Make sure the motor number is valid if motorNum < 34: driveValue = (level * direction) + (level * (direction - 1)) # set to 1/2 drive if direction = 1 or -1/2 drive if direction = 0 (level * direction) driveValue += 128 self._i2c.writeByte(self.address, self.SCMD_MA_DRIVE + motorNum, driveValue)
# inversion_mode( ... ) # # Configure a motor's direction inversion # # motorNum -- Motor number from 0 to 33 # polarity -- 0 or 1 for default or inverted
[docs] def inversion_mode(self, motorNum, polarity): """ Configure a motor's direction inversion :param motoNum: Motor number from 0 to 33 :param polarity: 0 or 1 for default or inverted :return: No return value """ regTemp = 0 # Select target register if motorNum < 2: # master if motorNum == 0: self._i2c.writeByte(self.address, self.SCMD_MOTOR_A_INVERT, polarity & 0x01) if motorNum == 1: self._i2c.writeByte(self.address, self.SCMD_MOTOR_B_INVERT, polarity & 0x01) else: if motorNum < 10: # register: SCMD_INV_2_9 regTemp = self.SCMD_INV_2_9 motorNum -= 2 elif motorNum < 18: # register: SCMD_INV_10_17 regTemp = self.SCMD_INV_10_17 motorNum -= 10 elif motorNum < 26: # register: SCMD_INV_18_25 regTemp = self.SCMD_INV_18_25 motorNum -= 18 elif motorNum < 34: # register: SCMD_INV_26_33 regTemp = self.SCMD_INV_26_33 motorNum -= 26 else: # out of range return # convert motorNum to one-hot mask data = self._i2c.readByte(self.address, regTemp) & (~(1 << motorNum) & 0xFF) self._i2c.writeByte(self.address, regTemp, data | ((polarity & 0x01) << motorNum))
# bridgingMode( ... ) # # Configure a driver's bridging state # # driverNum -- Number of driver. Master is 0, slave 1 is 1, etc. 0 to 16 # bridged -- 0 or 1 for forward and backward
[docs] def bridging_mode(self, driverNum, bridged): """ Configure a driver's bridging state :param driverNum: Number of driver. Master is 0, slave 1 is 1, etc. 0 to 16 :param bridged: 0 or 1 for forward and backward :return: No return value """ regTemp = 0 # Select target register if driverNum < 1: # master self._i2c.writeByte(self.address, self.SCMD_BRIDGE, bridged & 0x01) else: if driverNum < 9: # register: SCMD_BRIDGE_SLV_L regTemp = self.SCMD_BRIDGE_SLV_L driverNum -= 1 elif driverNum < 17: # register: SCMD_BRIDGE_SLV_H regTemp = self.SCMD_BRIDGE_SLV_H driverNum -= 9 else: # out of range return # convert driverNum to one-hot mask data = self._i2c.readByte(regTemp) & (~(1 << driverNum ) & 0xFF) self._i2c.writeByte(self.address, regTemp, data | ((bridged & 0x01) << driverNum))
# ****************************************************************************# # # Diagnostics # # ****************************************************************************# # getDiagnostics( ... ) # # Get diagnostic information from the master # # Object returned with properties that are the diagnostic info
[docs] def get_diagnostics(self): """ Get diagnostic information from the masterd :return: Object returned with properties that are the diagnostic info :rtype: Object - SCMDDiagnostics() """ myDiag = SCMDDiagnostics() myDiag.U_I2C_RD_ERR = self._i2c.readByte(self.address, self.SCMD_U_I2C_RD_ERR) myDiag.U_I2C_WR_ERR = self._i2c.readByte(self.address, self.SCMD_U_I2C_WR_ERR) myDiag.U_BUF_DUMPED = self._i2c.readByte(self.address, self.SCMD_U_BUF_DUMPED) myDiag.E_I2C_RD_ERR = self._i2c.readByte(self.address, self.SCMD_E_I2C_RD_ERR) myDiag.E_I2C_WR_ERR = self._i2c.readByte(self.address, self.SCMD_E_I2C_WR_ERR) myDiag.LOOP_TIME = self._i2c.readByte(self.address, self.SCMD_LOOP_TIME) myDiag.SLV_POLL_CNT = self._i2c.readByte(self.address, self.SCMD_SLV_POLL_CNT) # Count slaves topAddr = self._i2c.readByte(self.address, self.SCMD_SLV_TOP_ADDR) if topAddr >= self.START_SLAVE_ADDR and topAddr < (self.START_SLAVE_ADDR + 16): # in valid range myDiag.numberOfSlaves = topAddr - self.START_SLAVE_ADDR + 1 myDiag.MST_E_ERR = self._i2c.readByte(self.address, self.SCMD_MST_E_ERR) myDiag.MST_E_STATUS = self._i2c.readByte(self.address, self.SCMD_MST_E_STATUS) myDiag.FSAFE_FAULTS = self._i2c.readByte(self.address, self.SCMD_FSAFE_FAULTS) myDiag.REG_OOR_CNT = self._i2c.readByte(self.address, self.SCMD_REG_OOR_CNT) myDiag.REG_RO_WRITE_CNT = self._i2c.readByte(self.address, self.SCMD_REG_RO_WRITE_CNT) return myDiag
# get_remote_diagnostics( ... ) # # Get diagnostic information from a slave # # uint8_t address -- Address of slave to read. Can be 0x50 to 0x5F for slave 1 to 16. # SCMDDiagnostics &diagObjectReference -- Object to contain returned data
[docs] def get_remote_diagnostics(self, address): """ Get diagnostic information from a slave :param address: Address of slave to read. Can be 0x50 to 0x5F for slave 1 to 16. :return: Object returned with properties that are the diagnostic info :rtype: Object - SCMDDiagnostics() """ myDiag = SCMDDiagnostics() myDiag.numberOfSlaves = 0 myDiag.U_I2C_RD_ERR = 0 myDiag.U_I2C_WR_ERR = 0 myDiag.U_BUF_DUMPED = 0 myDiag.E_I2C_RD_ERR = self.read_remote_register(address, self.SCMD_E_I2C_RD_ERR) myDiag.E_I2C_WR_ERR = self.read_remote_register(address, self.SCMD_E_I2C_WR_ERR) myDiag.LOOP_TIME = self.read_remote_register(address, self.SCMD_LOOP_TIME) myDiag.SLV_POLL_CNT = 0 myDiag.MST_E_ERR = 0 myDiag.MST_E_STATUS = 0 myDiag.FSAFE_FAULTS = self.read_remote_register(address, self.SCMD_FSAFE_FAULTS) myDiag.REG_OOR_CNT = self.read_remote_register(address, self.SCMD_REG_OOR_CNT) myDiag.REG_RO_WRITE_CNT = self.read_remote_register(address, self.SCMD_REG_RO_WRITE_CNT) return myDiag
# reset_diagnostic_counts( ... ) # # Reset the master's diagnostic counters #
[docs] def reset_diagnostic_counts(self): """ Reset the master's diagnostic counters :return: No return value """ self._i2c.writeByte(self.address, self.SCMD_U_I2C_RD_ERR, 0) self._i2c.writeByte(self.address, self.SCMD_U_I2C_WR_ERR, 0) self._i2c.writeByte(self.address, self.SCMD_U_BUF_DUMPED, 0) self._i2c.writeByte(self.address, self.SCMD_E_I2C_RD_ERR, 0) self._i2c.writeByte(self.address, self.SCMD_E_I2C_WR_ERR, 0) # Clear uport time self._i2c.writeByte(self.address, self.SCMD_LOOP_TIME, 0) self._i2c.writeByte(self.address, self.SCMD_MST_E_ERR, 0) self._i2c.writeByte(self.address, self.SCMD_FSAFE_FAULTS, 0) self._i2c.writeByte(self.address, self.SCMD_REG_OOR_CNT, 0) self._i2c.writeByte(self.address, self.SCMD_REG_RO_WRITE_CNT, 0)
# reset_remote_diagnostic_counts( ... ) # # Reset a slave's diagnostic counters # # uint8_t address -- Address of slave to read. Can be 0x50 to 0x5F for slave 1 to 16.
[docs] def reset_remote_diagnostic_counts(self, address): """ Reset a slave's diagnostic counters :param address: Address of slave to read. Can be 0x50 to 0x5F for slave 1 to 16. :return: No return value """ self.write_remote_register(address, self.SCMD_U_I2C_RD_ERR, 0) self.write_remote_register(address, self.SCMD_U_I2C_WR_ERR, 0) self.write_remote_register(address, self.SCMD_U_BUF_DUMPED, 0) self.write_remote_register(address, self.SCMD_E_I2C_RD_ERR, 0) self.write_remote_register(address, self.SCMD_E_I2C_WR_ERR, 0) # Clear uport time self.write_remote_register(address, self.SCMD_LOOP_TIME, 0) self.write_remote_register(address, self.SCMD_ID, 0) self.write_remote_register(address, self.SCMD_FSAFE_FAULTS, 0) self.write_remote_register(address, self.SCMD_REG_OOR_CNT, 0) self.write_remote_register(address, self.SCMD_REG_RO_WRITE_CNT, 0)
# read_remote_register( ... ) # # Read data from a slave. Note that this waits 5ms for slave data to be aquired # before making the final read. # # address -- Address of slave to read. Can be 0x50 to 0x5F for slave 1 to 16. # offset -- Address of data to read. Can be 0x00 to 0x7F
[docs] def read_remote_register(self, address, offset): """ Read data from a slave. Note that this waits 5ms for slave data to be aquired before making the final read. :param address: Address of slave to read. Can be 0x50 to 0x5F for slave 1 to 16. :param offset: Address of data to read. Can be 0x00 to 0x7F :return: Register Value :rtype: integer """ self._i2c.writeByte(self.address, self.SCMD_REM_ADDR, address) self._i2c.writeByte(self.address, self.SCMD_REM_OFFSET, offset) self._i2c.writeByte(self.address, self.SCMD_REM_READ, 1) while self.busy(): pass return self._i2c.readByte(self.address, self.SCMD_REM_DATA_RD)
# write_remote_register( ... ) # # Write data from a slave # # address -- Address of slave to read. Can be 0x50 to 0x5F for slave 1 to 16. # offset -- Address of data to write. Can be 0x00 to 0x7F # dataToWrite -- Data to write.
[docs] def write_remote_register(self, address, offset, dataToWrite): """ Write data from a slave :param address: Address of slave to read. Can be 0x50 to 0x5F for slave 1 to 16. :param offset: Address of data to read. Can be 0x00 to 0x7F :param dataToWrite: The data to write :return: No return value """ while self.busy(): pass self._i2c.writeByte(self.address, self.SCMD_REM_ADDR, address) self._i2c.writeByte(self.address, self.SCMD_REM_OFFSET, offset) self._i2c.writeByte(self.address, self.SCMD_REM_DATA_WR, dataToWrite) self._i2c.writeByte(self.address, self.SCMD_REM_WRITE, 1) while self.busy(): pass