Arduino sketch interface to a servo motor.

1st off I am not sure if this the right place to be asking this but here goes.
The board is a Feather Huzzah with a PCA9685 controlling the servo motors, and there are 2 of them.
I inherited the sketch and have been trying to make it work with some success.
However I am wanting to change out the servo motors from the 3 wire type to a 2 wire type (brushless or brushes, I dont know the difference).
The reason for the change was that when the 3wires were working they moved too darn fast and erratic, they really leaped around.
The 2wires are nice and steady and slow, which is what I am looking for.

My problem is the sketch and the servo control board assumes 3 wires.
V+, Gnd, and PWM.
My preferred motors are 2 wires, V+ and Gnd.
so I am looking for suggestions as to what I have to do. I would prefer to not make too many changes to the sketch if I dont have to. The fellow that wrote it was not very clear in his comments nor organization.

So, is there a simple(relatively?) way to drive a 2 wire motor from the PCA9685 or perhaps directly from the Feather itself ?

thanks

Please post links to the data sheets of the "2 wire motor" and to the driver. Code is often needed.....
My chrystal ball is not awailble for the moment.

Sorry I've never heard of a 2-wire servo. Perhaps you can post details of what exactly you're talking about.

If you mean a standard brushed DC motor then you can't drive that directly from a PCA9685.

If you mean a brushless motor they have 3 wires but need to be driven by an ESC or motor driver. The ESC can probably be controlled by the PCA9685.

Either way the code connected with the servos/motors will almost certainly need to be completely different though since you haven't shown us the code or described what it does it's difficult to tell.

Steve

This is the code to the sub-program and it is too large to fit into the 9000 max so splitting it into 2 if possible.

// class to control two motors to track a target az and el using the Adafruit I2C interface


#include "Gimbal.h"

/* issue raw motor command in microseconds pulse width, clamped at limit
 */
void Gimbal::setMotorPosition (uint8_t motn, uint16_t newpos)
{
	if (motn >= NMOTORS || !gimbal_found)
	    return;
	MotorInfo *mip = &motor[motn];

	mip->atmin = (newpos <= mip->min);
	if (mip->atmin)
	    newpos = mip->min;
	mip->atmax = (newpos >= mip->max);
	if (mip->atmax)
	    newpos = mip->max;

	mip->del_pos = (int)newpos - (int)mip->pos;
	mip->pos = newpos;
	pwm->setPWM(mip->servo_num, 0, mip->pos/US_PER_BIT);
	// Serial.print(mip->servo_num); Serial.print(" "); Serial.println (newpos);
}

/* constructor 
 */
Gimbal::Gimbal ()
{
	// first confirm whether controller is present
	resetWatchdog();
	Wire.beginTransmission(GIMBAL_I2C_ADDR);
	gimbal_found = (Wire.endTransmission() == 0);
	
	 Serial.print ("Gimbal I2C=");
     Serial.println (GIMBAL_I2C_ADDR);
	
	if (!gimbal_found) {
	    Serial.println (F("PWM controller not found"));
	    return;
	}
	Serial.println (F("PWM controller found ok"));

	// instantiate PWM controller
	pwm = new Adafruit_PWMServoDriver(GIMBAL_I2C_ADDR);
	pwm->begin();
	pwm->setPWMFreq(SERVO_FREQ);

	// record axis assignments
	motor[0].servo_num = MOT1_UNIT;
	motor[1].servo_num = MOT2_UNIT;

	// init each motor state
	nv->get();
	motor[0].min = nv->mot0min;
	motor[0].max = nv->mot0max;
	motor[0].pos = 0;
	motor[0].atmin = false;
	motor[0].atmax = false;
	motor[0].az_scale = 0;
	motor[0].el_scale = 0;

	motor[1].min = nv->mot1min;
	motor[1].max = nv->mot1max;
	motor[1].pos = 0;
	motor[1].atmin = false;
	motor[1].atmax = false;
	motor[1].az_scale = 0;
	motor[1].el_scale = 0;

	// init to arbitrary, but at least defined, state
	init_step = 0;
	best_azmotor = 0;
	last_update = 0;
	prevfast_az = prevfast_el = -1000;
	prevstop_az = prevstop_el = -1000;
}

/* move motors towards the given new target az and el 
 */
void Gimbal::moveToAzEl (float az_t, float el_t)
{
	// only update every UPD_PERIOD
	uint32_t now = millis();
	if (now - last_update < UPD_PERIOD)
	    return;
	last_update = now;

	// read current sensor direction
	float az_s, el_s;
	sensor->getAzEl (&az_s, &el_s);

	// only check further when motion has stopped as evidenced by stable sensor values
	if (fabs (azDist (prevfast_az, az_s)) < MAX_SETTLE && fabs (el_s - prevfast_el) < MAX_SETTLE) {

	    // calibrate if not already else seek target
	    if (!calibrated())

		calibrate (az_s, el_s);

	    else

		seekTarget (az_t, el_t, az_s, el_s);
	    

	    // preserve for next stopped iteration
	    prevstop_az = az_s;
	    prevstop_el = el_s;

	}

	// preserve for next fast iteration
	prevfast_az = az_s;
	prevfast_el = el_s;
}

/* run the next step of the initial scale calibration series.
 * steps proceed using init_step up to N_INIT_STEPS
 */
void Gimbal::calibrate (float& az_s, float& el_s)
{
	// handy step ranges
	uint16_t range0 = motor[0].max - motor[0].min;
	uint16_t range1 = motor[1].max - motor[1].min;

	switch (init_step++) {

	case 0:

	    // move near min of each range
	    setMotorPosition (0, motor[0].min + (1-CAL_FRAC)/2*range0);
	    setMotorPosition (1, motor[1].min + (1-CAL_FRAC)/2*range1);
	    break;

	case 1:

	    // move just motor 0 a subtantial distance
	    
	    Serial.print(F("Init 1: Mot 0 starts at:\t"));
		Serial.print(az_s); Serial.print(F("\t"));
		Serial.print (el_s); Serial.print(F("\tMoves\t"));
		Serial.println(CAL_FRAC*range0, 0);
	    
	    setMotorPosition (0, motor[0].pos + CAL_FRAC*range0);
	    break;

	case 2:

	    // calculate scale of motor 0
	    motor[0].az_scale = CAL_FRAC*range0/azDist(prevstop_az, az_s);
	    motor[0].el_scale = CAL_FRAC*range0/(el_s - prevstop_el);
	    
	    Serial.print(F("Init 2: Mot 0 ended  at:\t"));
		Serial.print(az_s); Serial.print(F("\t"));
		Serial.print (el_s); Serial.print(F("\tusec:\t"));
		Serial.print (CAL_FRAC*range0); Serial.print (F("\tDel usec/Deg:\t"));
		Serial.print (motor[0].az_scale); Serial.print (F("\t"));
		Serial.println (motor[0].el_scale);
	    

	    // repeat procedure for motor 1
	    
	    Serial.print(F("Init 2: Mot 1 starts at:\t"));
		Serial.print(az_s); Serial.print(F("\t"));
		Serial.print (el_s); Serial.print(F("\tMoves\t"));
		Serial.println(CAL_FRAC*range1, 0);
	    
	    setMotorPosition (1, motor[1].pos + CAL_FRAC*range1);
	    break;

	case 3:

	    // calculate scale of motor 1
	    motor[1].az_scale = CAL_FRAC*range1/azDist(prevstop_az, az_s);
	    motor[1].el_scale = CAL_FRAC*range1/(el_s - prevstop_el);
	    
	    Serial.print(F("Init 3: Mot 1 ended  at:\t"));
		Serial.print(az_s); Serial.print(F("\t"));
		Serial.print (el_s); Serial.print(F("\tusec:\t"));
		Serial.print (CAL_FRAC*range1); Serial.print (F("\tDel usec/Deg:\t"));
		Serial.print (motor[1].az_scale); Serial.print (F("\t"));
		Serial.println (motor[1].el_scale);
	    

	    // select best motor for az
	    best_azmotor = fabs(motor[0].az_scale) < fabs(motor[1].az_scale) ? 0 : 1;
	    Serial.print (F("Best Az motor:\t"));
		Serial.print (best_azmotor); Serial.print (F("\tScale:\t"));
		Serial.print (motor[best_azmotor].az_scale);
		Serial.print (F("\tEl motor:\t"));
		Serial.print (!best_azmotor); Serial.print (F("\tScale:\t"));
		Serial.println (motor[!best_azmotor].el_scale);

	    // report we have finished calibrating
	    target->setTrackingState (true);
	    break;

	default:

	    webpage->setUserMessage (F("BUG! Bogus init_step"));
	    break;
	}
}

Part 2

/* run the next step of seeking the given target given the current stable az/el sensor values
 */
void Gimbal::seekTarget (float& az_t, float& el_t, float& az_s, float& el_s)
{
	// find pointing error in each dimension as a move from sensor to target
	float az_err = azDist (az_s, az_t);
	float el_err = el_t - el_s;

	// correct each error using motor with most effect in that axis
	MotorInfo *azmip = &motor[best_azmotor];
	MotorInfo *elmip = &motor[!best_azmotor];

	
	Serial.print (F("Az:\t"));
	    Serial.print(az_s); Serial.print(F("\t"));
	    Serial.print(azmip->pos); Serial.print (F("\t"));
	    Serial.print(az_err, 1); Serial.print (F("\t"));
	    Serial.print(az_err*azmip->az_scale, 0);
	Serial.print (F("\tEl:\t"));
	    Serial.print(el_s); Serial.print(F("\t"));
	    Serial.print(elmip->pos); Serial.print (F("\t"));
	    Serial.print(el_err, 1); Serial.print (F("\t"));
	    Serial.println(el_err*elmip->el_scale, 0);
	


	// tweak scale if move was substantial and sanity check by believing only small changes
	const float MIN_ANGLE = 30;		// min acceptable move
	const float MAX_CHANGE = 0.1;		// max fractional scale change
	float az_move = azDist (prevstop_az, az_s);
	if (fabs(az_move) >= MIN_ANGLE) {
	    float new_az_scale = azmip->del_pos/az_move;
	    if (fabs((new_az_scale - azmip->az_scale)/azmip->az_scale) < MAX_CHANGE) {
		Serial.print (F("New Az scale\t"));
		    Serial.print (azmip->az_scale); Serial.print (F("\t->\t"));
		    Serial.println(new_az_scale);
		azmip->az_scale = new_az_scale;
	    }
	}
	float el_move = el_s - prevstop_el;
	if (fabs(el_move) >= MIN_ANGLE) {
	    float new_el_scale = elmip->del_pos/el_move;
	    if (fabs((new_el_scale - elmip->el_scale)/elmip->el_scale) < MAX_CHANGE) {
		Serial.print (F("New El scale\t"));
		    Serial.print (elmip->el_scale); Serial.print (F("\t->\t"));
		    Serial.println(new_el_scale);
		elmip->el_scale = new_el_scale;
	    }
	}


	// move each motor to reduce error, but if at Az limit then swing back to near opposite limit
	if (azmip->atmin) {
	    // Serial.println (F("At Az Min"));
	    setMotorPosition (best_azmotor, azmip->min + 0.8*(azmip->max - azmip->min));
	} else if (azmip->atmax) {
	    // Serial.println (F("At Az Max"));
	    setMotorPosition (best_azmotor, azmip->min + 0.2*(azmip->max - azmip->min));
	} else
	    setMotorPosition (best_azmotor, azmip->pos + az_err*azmip->az_scale);
	setMotorPosition (!best_azmotor, elmip->pos + el_err*elmip->el_scale);

}

/* given two azimuth values, return path length going shortest direction
 */
float Gimbal::azDist (float &from, float &to)
{
	float d = to - from;
	if (d < -180)
	    d += 360;
	else if (d > 180)
	    d -= 360;
	return (d);
}

/* send latest web values.
 * N.B. must match id's in main web page
 */
void Gimbal::sendNewValues (WiFiClient client)
{
	if (!gimbal_found) {
	    client.println (F("G_Status=Not found!"));
	    return;
	}

	client.print (F("G_Mot1Pos="));
	if (init_step > 0)
	    client.println (motor[0].pos);
	else
	    client.println (F(""));
	client.print (F("G_Mot1Min=")); client.println (motor[0].min);
	client.print (F("G_Mot1Max=")); client.println (motor[0].max);

	client.print (F("G_Mot2Pos="));
	if (init_step > 0)
	    client.println (motor[1].pos);
	else
	    client.println (F(""));
	client.print (F("G_Mot2Min=")); client.println (motor[1].min);
	client.print (F("G_Mot2Max=")); client.println (motor[1].max);

	client.print (F("G_Status="));
	    if (motor[0].atmin)
		client.println (F("1 at Min!"));
	    else if (motor[0].atmax)
		client.println (F("1 at Max!"));
	    else if (motor[1].atmin)
		client.println (F("2 at Min!"));
	    else if (motor[1].atmax)
		client.println (F("2 at Max!"));
	    else
		client.println (F("Ok+"));

}

/* process name = value.
 * return whether we recognize it
 */
bool Gimbal::overrideValue (char *name, char *value)
{
	const __FlashStringHelper *nog = F("No gimbal!");

	if (!strcmp (name, "G_Mot1Pos")) {
	    if (gimbal_found) {
		target->setTrackingState(false);
		setMotorPosition (0, atoi(value));
	    } else
		webpage->setUserMessage (nog);
	    return (true);
	}
	if (!strcmp (name, "G_Mot1Min")) {
	    if (gimbal_found) {
		nv->mot0min = motor[0].min = atoi(value);
		nv->put();
		webpage->setUserMessage (F("Servo 1 minimum saved in EEPROM+"));
	    } else
		webpage->setUserMessage (nog);
	    return (true);
	}
	if (!strcmp (name, "G_Mot1Max")) {
	    if (gimbal_found) {
		nv->mot0max = motor[0].max = atoi(value);
		nv->put();
		webpage->setUserMessage (F("Servo 1 maximum saved in EEPROM+"));
	    } else
		webpage->setUserMessage (nog);
	    return (true);
	}
	if (!strcmp (name, "G_Mot2Pos")) {
	    if (gimbal_found) {
		target->setTrackingState(false);
		setMotorPosition (1, atoi(value));
	    } else
		webpage->setUserMessage (nog);
	    return (true);
	}
	if (!strcmp (name, "G_Mot2Min")) {
	    if (gimbal_found) {
		nv->mot1min = motor[1].min = atoi(value);
		nv->put();
		webpage->setUserMessage (F("Servo 2 minimum saved in EEPROM+"));
	    } else
		webpage->setUserMessage (nog);
	    return (true);
	}
	if (!strcmp (name, "G_Mot2Max")) {
	    if (gimbal_found) {
		nv->mot1max = motor[1].max = atoi(value);
		nv->put();
		webpage->setUserMessage (F("Servo 2 maximum saved in EEPROM+"));
	    } else
		webpage->setUserMessage (nog);
	    return (true);
	}
	return (false);

I know that it is a lot to have you look at the code, though I was just hoping that there might be a moderately easy solution.

Here is the .h file

// class to control two motors to track a target az and el using the Adafruit I2C interface

#ifndef _GIMBAL_H
#define _GIMBAL_H

#include "Wire.h"
#include "WiFiClient.h"
#include "Adafruit_PWMServoDriver.h"

#include "AutoSatTracker-ESP.h"
#include "Target.h"
#include "Sensor.h"

class Gimbal {

    private:

	// I2C servo interface
	Adafruit_PWMServoDriver *pwm;
  
	static const uint8_t GIMBAL_I2C_ADDR = 0x40;	// I2C bus address of servo controller
	static const uint8_t SERVO_FREQ = 50;		// typical servo pulse frequency, Hz
	static constexpr float US_PER_BIT = (1e6/SERVO_FREQ/4096);	// usec per bit @ 12 bit resolution
	static const uint8_t MOT1_UNIT = 0;		// motor 1 I2C unit number
	static const uint8_t MOT2_UNIT = 1;		// motor 2 I2C unit number
	bool gimbal_found;				// whether PWM controller is present

	// motor info
	typedef struct {
	    float az_scale, el_scale;			// az and el scale: steps (del usec) per degree
	    uint16_t min, max;				// position limits, usec
	    uint16_t pos;				// last commanded position, usec
	    int16_t del_pos;				// change in pos since previous move
	    bool atmin, atmax;				// (would have been commanded to) limit
	    uint8_t servo_num;				// I2C bus address 0..15
	} MotorInfo;
	static const uint8_t NMOTORS = 2;		// not easily changed
	MotorInfo motor[NMOTORS];

	// search info
	// N.B.: max az physical motion must be < 180/CAL_FRAC
	static const uint16_t UPD_PERIOD = 500;		// ms between updates
	static constexpr float MAX_SETTLE = 5.0;	// considered stopped, degs
	static const uint8_t N_INIT_STEPS = 4;		// number of init_steps
	static constexpr float CAL_FRAC = 0.333;	// fraction of full range to move for calibration
							// N.B.: max physical motion must be < 180/CAL_FRAC
	uint8_t init_step;				// initialization sequencing
	uint8_t best_azmotor;				// after cal, motor[] index with most effect in az
	uint32_t last_update;				// millis() time of last move
	float prevfast_az, prevfast_el;			// previous pointing position
	float prevstop_az, prevstop_el;			// previous stopped position

	void setMotorPosition (uint8_t motn, uint16_t newpos);
	void calibrate (float &az_s, float &el_s);
	void seekTarget (float& az_t, float& el_t, float& az_s, float& el_s);
	float azDist (float &from, float &to);

    public:

	Gimbal();

	void moveToAzEl (float az_t, float el_t);
	void sendNewValues (WiFiClient client);
	bool overrideValue (char *name, char *value);
	bool connected() { return (gimbal_found); };
	bool calibrated() { return (init_step >= N_INIT_STEPS); }
};

extern Gimbal *gimbal;

#endif // _GIMBAL_H

and this is a picture of the motor

index.jpg

index.jpg

The latest image is clearly not of a servo motor.

I know that it is a lot to have you look at the code, though I was just hoping that there might be a moderately easy solution.

Nope. The motors will need position feedback, and the method of driving will need to fundamentally change.

Here's a link to the data sheet: http://www.tsinymotor.com/Products/Worm%20Gear%20Motors/2014/0512/15.html

It's a geared DC motor, a lot different from a servo. Additional electronics is neeed for positioning as well as code for controlling this motor.

And then I saw this new board and wondered if it would be useful. What do you think ?

The picture in reply #6 shows "9rpm/min".

That's an acceleration, surely?

hextejas:
And then I saw this new board and wondered if it would be useful. What do you think ?

Post a link to the data sheet. That picture is useless for a decision.

Is this what you want ?

Yes, good data sheet. I question if that device is trong enough regarding current capacity.

Railroader:
Yes, good data sheet. I question if that device is trong enough regarding current capacity.

Am I reading this right as :
Avg = 1.2A
Peak = 3.2A
Though another section seems to says 2A continuous and 3.2A single.
And I think it is per motor.
I don't have the experience to say if that's a lot, enough, or barely minimum. Most of the rantings that I have seen have been mA.
I guess that I better take a look at the motor though the motor spec won't tell me what the load will be.

Read the details in the spec! Average is 1.2 A and the peak values are during a very short time, too short for a motor start in my opinion.
Read page 3! https://cdn-shop.adafruit.com/datasheets/TB6612FNG_datasheet_en_20121101.pdf