Updated code that works. I’ll put on Github eventually.
GroveMiniMotor.h
#pragma once
// This will load the definition for common Particle variable types
#include "Particle.h"
// See DRV8830 Datasheet for values
// Register sub address constants
#define CONTROL_ADDR 0x00
#define FAULT_ADDR 0x01
// Fault constants
#define FAULT 0x01
#define ILIMIT 0x10
#define OTS 0x08
#define UVLO 0x04
#define OCP 0x02
class MiniMotor
{
public:
MiniMotor(byte i2cChannelAddr);
void drive(int speed);
void stop();
void brake();
char getFault();
private:
void I2CWriteBytes(byte subAddr, byte *buffer, byte len);
void I2CReadBytes(byte subAddr, char *buffer, byte len);
byte _i2cChannelAddr;
};
GroveMiniMotor.cpp
// Grove Mini Motor Driver Wiki
// https://wiki.seeedstudio.com/Grove-Mini_I2C_Motor_Driver_v1.0/
// DRV8830 Datasheet
// https://files.seeedstudio.com/wiki/Grove-Mini_I2C_Motor_Driver_v1.0/res/DRV8830.pdf
// Wire(I2C) - Partile Refeence
// https://docs.particle.io/reference/device-os/firmware/argon/#begintransmission-
// Wire Library - Arduino Reference
// https://www.arduino.cc/en/reference/wire
#include "GroveMiniMotor.h"
// The address of the part is set by a jumper on the board.
// See datasheet or schematic for details.
MiniMotor::MiniMotor(byte i2cChannelAddr)
{
_i2cChannelAddr = i2cChannelAddr;
// We use a single instance per motor, so only call begin() once per Master device
if(!Wire.isEnabled())
{
// This sets the bit rate of the bus
// Wire.setSpeed(CLOCK_SPEED_100KHZ); // Default is 100KHZ
// Wire.setSpeed(CLOCK_SPEED_400KHZ);
// NOTE: Only send a device address parameter if the device running code is the SLAVE
Wire.begin();
// Datasheet says to delay at least 60 microseconds before writing first START command.
delayMicroseconds(100);
}
}
// Return the fault status of the DRV8830 chip. Also clears any existing faults.
char MiniMotor::getFault()
{
char* buffer = 0;
byte clearFault = 0x80;
// Read first
I2CReadBytes(FAULT_ADDR, buffer, 1);
// Clear next
I2CWriteBytes(FAULT_ADDR, &clearFault, 1);
// Return fault status
return buffer[0];
}
// Send the drive command over I2C to the DRV8830 chip. See DRV8830 specficiation document for values.
// Bits 7:2 are the speed setting;
// Range is 0-63
// Bits 1:0 are the mode setting:
// - 00 = Z-Z (Standby/Coast)
// - 01 = L-H Reverse
// - 10 = H-L Forward
// - 11 = H-H (brake)
void MiniMotor::drive(int speed)
{
// Before we do anything, we'll want to clear the fault status. To do that
// write 0x80 to register FAULT ADDR on the DRV8830.
byte regValue = 0x80;
I2CWriteBytes(FAULT_ADDR, ®Value, 1);
// Find the byte-ish abs value of the input
regValue = (byte)abs(speed);
// Cap the value at 63.
if (regValue > 63) {
regValue = 63;
}
// Left shift to make room for bits 1:0
regValue = regValue << 2;
if (speed < 0) {
regValue |= 0x01; // Set bits 1:0 based on sign of input.
} else {
regValue |= 0x02;
}
Serial.print("Sending drive speed: ");
Serial.println(regValue, HEX);
// Write to the CONTROL ADDR
I2CWriteBytes(CONTROL_ADDR, ®Value, 1);
}
void MiniMotor::stop()
{
byte regValue = 0; // See above for bit 1:0 explanation.
I2CWriteBytes(CONTROL_ADDR, ®Value, 1);
}
// Stop the motor by providing a heavy load on it.
void MiniMotor::brake()
{
byte regValue = 0x03; // See above for bit 1:0 explanation.
I2CWriteBytes(CONTROL_ADDR, ®Value, 1);
}
// Private function that reads some number of bytes from motor driver.
void MiniMotor::I2CReadBytes(byte subAddr, char* buffer, byte len)
{
Serial.print("Reading bytes from slave addr: 0x");
Serial.println(_i2cChannelAddr, HEX);
// Send a START condition to begin a transimission of bytes on the specified channel address
Wire.beginTransmission(_i2cChannelAddr);
// Sets the "Sub Address" which defines which register to read from (i.e. read from CONTROL: 0x00 or FAULT: 0x01 register)
Wire.write(subAddr);
// Send a STOP condition to transmit the queued bytes
Wire.endTransmission();
// Add small delay before reading
delay(100);
// Next, request the number of bytes from slave device
Wire.requestFrom(_i2cChannelAddr, len);
int bytesLength = Wire.available();
Serial.print("# of bytes availble from slave: ");
Serial.println(bytesLength);
if(bytesLength > 0)
{
Wire.readBytes(buffer, len);
Serial.print("Bytes: ");
Serial.println(buffer);
}
}
// Private function that writes some number of bytes to motor driver.
void MiniMotor::I2CWriteBytes(byte subAddr, byte* buffer, byte len)
{
Serial.print("Writing bytes to slave addr: ");
Serial.println(_i2cChannelAddr, HEX);
// Send a START condition to begin a transimission of bytes on the specified channel address
Wire.beginTransmission(_i2cChannelAddr);
// Sets the "Sub Address" which defines which register to write (i.e. write to CONTROL: 0x00 or FAULT: 0x01 register)
Wire.write(subAddr);
// Write the data next
Wire.write(buffer, len);
// Send a STOP condition to transmit the queued bytes
Wire.endTransmission(); // stop transmitting
}
GroveMiniMotor.cpp
// Grove Mini Motor Driver Wiki
// https://wiki.seeedstudio.com/Grove-Mini_I2C_Motor_Driver_v1.0/
// DRV8830 Datasheet
// https://files.seeedstudio.com/wiki/Grove-Mini_I2C_Motor_Driver_v1.0/res/DRV8830.pdf
// Wire(I2C) - Partile Refeence
// https://docs.particle.io/reference/device-os/firmware/argon/#begintransmission-
// Wire Library - Arduino Reference
// https://www.arduino.cc/en/reference/wire
#include "GroveMiniMotor.h"
// The address of the part is set by a jumper on the board.
// See datasheet or schematic for details.
MiniMotor::MiniMotor(byte i2cChannelAddr)
{
_i2cChannelAddr = i2cChannelAddr;
// We use a single instance per motor, so only call begin() once per Master device
if(!Wire.isEnabled())
{
// This sets the bit rate of the bus
// Wire.setSpeed(CLOCK_SPEED_100KHZ); // Default is 100KHZ
// Wire.setSpeed(CLOCK_SPEED_400KHZ);
// NOTE: Only send a device address parameter if the device running code is the SLAVE
Wire.begin();
// Datasheet says to delay at least 60 microseconds before writing first START command.
delayMicroseconds(100);
}
}
// Return the fault status of the DRV8830 chip. Also clears any existing faults.
char MiniMotor::getFault()
{
char* buffer = 0;
byte clearFault = 0x80;
// Read first
I2CReadBytes(FAULT_ADDR, buffer, 1);
// Clear next
I2CWriteBytes(FAULT_ADDR, &clearFault, 1);
// Return fault status
return buffer[0];
}
// Send the drive command over I2C to the DRV8830 chip. See DRV8830 specficiation document for values.
// Bits 7:2 are the speed setting;
// Range is 0-63
// Bits 1:0 are the mode setting:
// - 00 = Z-Z (Standby/Coast)
// - 01 = L-H Reverse
// - 10 = H-L Forward
// - 11 = H-H (brake)
void MiniMotor::drive(int speed)
{
// Before we do anything, we'll want to clear the fault status. To do that
// write 0x80 to register FAULT ADDR on the DRV8830.
byte regValue = 0x80;
I2CWriteBytes(FAULT_ADDR, ®Value, 1);
// Find the byte-ish abs value of the input
regValue = (byte)abs(speed);
// Cap the value at 63.
if (regValue > 63) {
regValue = 63;
}
// Left shift to make room for bits 1:0
regValue = regValue << 2;
if (speed < 0) {
regValue |= 0x01; // Set bits 1:0 based on sign of input.
} else {
regValue |= 0x02;
}
Serial.print("Sending drive speed: ");
Serial.println(regValue, HEX);
// Write to the CONTROL ADDR
I2CWriteBytes(CONTROL_ADDR, ®Value, 1);
}
// Coast to a stop by hi-z'ing the drivers.
void MiniMotor::stop()
{
byte regValue = 0; // See above for bit 1:0 explanation.
I2CWriteBytes(CONTROL_ADDR, ®Value, 1);
}
// Stop the motor by providing a heavy load on it.
void MiniMotor::brake()
{
byte regValue = 0x03; // See above for bit 1:0 explanation.
I2CWriteBytes(CONTROL_ADDR, ®Value, 1);
}
// Private function that reads some number of bytes from motor driver.
void MiniMotor::I2CReadBytes(byte subAddr, char* buffer, byte len)
{
Serial.print("Reading bytes from slave addr: 0x");
Serial.println(_i2cChannelAddr, HEX);
// Send a START condition to begin a transimission of bytes on the specified channel address
Wire.beginTransmission(_i2cChannelAddr);
// Sets the "Sub Address" which defines which register to read from (i.e. read from CONTROL: 0x00 or FAULT: 0x01 register)
Wire.write(subAddr);
// Send a STOP condition to transmit the queued bytes
Wire.endTransmission();
// Add small delay before reading
delay(100);
// Next, request the number of bytes from slave device
Wire.requestFrom(_i2cChannelAddr, len);
int bytesLength = Wire.available();
Serial.print("# of bytes availble from slave: ");
Serial.println(bytesLength);
if(bytesLength > 0)
{
Wire.readBytes(buffer, len);
Serial.print("Bytes: ");
Serial.println(buffer);
}
}
// Private function that writes some number of bytes to motor driver.
void MiniMotor::I2CWriteBytes(byte subAddr, byte* buffer, byte len)
{
Serial.print("Writing bytes to slave addr: 0x");
Serial.println(_i2cChannelAddr, HEX);
// Send a START condition to begin a transmission of bytes on the specified channel address
Wire.beginTransmission(_i2cChannelAddr);
// Sets the "Sub Address" which defines which register to write (i.e. write to CONTROL: 0x00 or FAULT: 0x01 register)
Wire.write(subAddr);
// Write the data next
Wire.write(buffer, len); // Used overload because we writing byte pointer
// Send a STOP condition to transmit the queued bytes
Wire.endTransmission();
}
.ino
/*
* Project Poolba
* Description: Robot for pool cleaning.
* Author: Devaron Ruggiero
* Date: 8/1/2020
*/
#include "Particle.h"
#include "GroveMiniMotor.h"
#define FAULTn 6 // Pin used for fault detection.
// IMPORTANT: Review jumper settings on back of board first and set accordingly
// Based on jumper settings: A1= 0, A1 = 0
#define MOTOR1_WRITE_ADDR 0x60 // 0xC4 << 1 (shifted 1 because read/write methods use 7bits)
// Based on jumper settings: A1= 1, A0 = 0
#define MOTOR2_WRITE_ADDR 0x65 // 0xC0 << 1 (shifted 1 because read/write methods use 7bits)
// Create two MiniMotor instances with different I2C channel addresses
MiniMotor motor1(MOTOR1_WRITE_ADDR); // CH1
MiniMotor motor2(MOTOR2_WRITE_ADDR); // CH2
void setup() {
Serial.begin(9600);
Serial.println("Hello, world!");
pinMode(FAULTn, INPUT);
}
void loop() {
Serial.println("Forward!");
motor1.drive(100);
motor2.drive(100);
delayUntil(10000);
Serial.println("Stop!");
motor1.stop();
motor2.stop();
delay(1000);
}
// delayUntil() is a little function to run the motor either for
// a designated time OR until a fault occurs. Note that this is
// a very simple demonstration; ideally, an interrupt would be
// used to service faults rather than blocking the application
// during motion and polling for faults.
void delayUntil(unsigned long elapsedTime) {
// See the "BlinkWithoutDelay" example for more details on how
// and why this loop works the way it does.
unsigned long startTime = millis();
while (startTime + elapsedTime > millis()) {
// If FAULTn goes low, a fault condition *may* exist. To be
// sure, we'll need to check the FAULT bit.
if (digitalRead(FAULTn) == LOW) {
Serial.println("Fault low. Reading fault status...");
// We're going to check both motors; the logic is the same
// for each...
char result = motor1.getFault();
Serial.print("Fault Status: ");
Serial.println(result, HEX);
// If result masked by FAULT is non-zero, we've got a fault
// condition, and we should report it.
// Motor 1
if (result & FAULT) {
Serial.print("Motor 1 fault: ");
if (result & OCP) {
Serial.println("Chip overcurrent!");
}
if (result & ILIMIT) {
Serial.println("Load current limit!");
}
if (result & UVLO) {
Serial.println("Undervoltage!");
}
if (result & OTS) {
Serial.println("Over temp!");
}
break; // We want to break out of the motion immediately,
// so we can stop motion in response to our fault.
}
// Motor 2
result = motor2.getFault();
if (result & FAULT) {
Serial.print("Motor 2 fault: ");
if (result & OCP) {
Serial.println("Chip overcurrent!");
}
if (result & ILIMIT) {
Serial.println("Load current limit!");
}
if (result & UVLO) {
Serial.println("Undervoltage!");
}
if (result & OTS) {
Serial.println("Over temp!");
}
break; // We want to break out of the motion immediately,
// so we can stop motion in response to our fault.
}
}
}
}