Support for smooth acceleration/deceleration in run (#900)

* removed logging

* removing more logging

* always use step for single/multiple motors

* refactored schedule

* account for accel ramp up and down

* added default acc/decel

* rounding speed/angle

* remove hack

* use acceleration time in run too

* handle missing case

* adding notes on motors

* adding sample

* fixed ramp simulation

* clear defaults

* some docs, more later

* adding basic examples

* remove debug msg

* clean json

* added move schedule

* docs

* basic docs
This commit is contained in:
Peli de Halleux 2019-09-02 20:57:23 -07:00 committed by GitHub
parent 7e9cc791ec
commit 56bbcde299
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 281 additions and 109 deletions

View File

@ -6,9 +6,9 @@ Set the rotation speed of the motor as a percentage of maximum speed.
motors.largeA.run(50) motors.largeA.run(50)
``` ```
The speed setting is a pecentage of the motor's full speed. Full speed is the speed that the motor runs when the brick supplies maximum output voltage to the port. The speed setting is a percentage of the motor's full speed. Full speed is the speed that the motor runs when the brick supplies maximum output voltage to the port.
If you use just the **speed** number, the motor runs continously and won't stop unless you tell it to. You can also give a value for a certain amount of distance you want the motor to rotate for. The **value** can be an amount of time, a turn angle in degrees, or a number of full rotations. If you use just the **speed** number, the motor runs continuously and won't stop unless you tell it to. You can also give a value for a certain amount of distance you want the motor to rotate for. The **value** can be an amount of time, a turn angle in degrees, or a number of full rotations.
If you decide to use a **value** of rotation distance, you need to choose a type of movement **unit**. If you decide to use a **value** of rotation distance, you need to choose a type of movement **unit**.
@ -30,8 +30,8 @@ Here is how you use each different movement unit to run the motor for a fixed ro
// Run motor for 700 Milliseconds. // Run motor for 700 Milliseconds.
motors.largeA.run(25, 700, MoveUnit.MilliSeconds); motors.largeA.run(25, 700, MoveUnit.MilliSeconds);
// Run motor for 700 Milliseconds again but no units specified. // Run motors B and C for 700 Milliseconds again but no units specified.
motors.largeA.run(25, 700); motors.largeBC.run(25, 700);
// Run the motor for 45 seconds // Run the motor for 45 seconds
motors.largeA.run(50, 45, MoveUnit.Seconds); motors.largeA.run(50, 45, MoveUnit.Seconds);
@ -61,6 +61,14 @@ motors.largeB.run(-25)
## ~ ## ~
## Multiple motors
When using **run** with multiple motors, there is no guarantee that their speed will stay in sync. Use [tank](/reference/motors/tank) or [steer](/reference/motors/steer) for synchronized motor operations.
```blocks
motors.largeBC.run(50)
```
## Examples ## Examples
### Drive the motor for 20 seconds ### Drive the motor for 20 seconds

View File

@ -0,0 +1,22 @@
# Schedule
Schedules an acceleration, constant and deceleration phase at a given speed.
```sig
motors.largeA.schedule(50, 100, 500, 100)
```
The speed setting is a percentage of the motor's full speed. Full speed is the speed that the motor runs when the brick supplies maximum output voltage to the port.
## Parameters
* **speed**: a [number](/types/number) that is the percentage of full speed. A negative value runs the motor in the reverse direction.
* **acceleration**: the [number](/types/number) of movement units to rotate for while accelerating.
* **value**: the [number](/types/number) of movement units to rotate for.
* **deceleration**: the [number](/types/number) of movement units to rotate for while decelerating.
* **unit**: the movement unit of rotation. This can be `milliseconds`, `seconds`, `degrees`, or `rotations`. If the number for **value** is `0`, this parameter isn't used.
## See also
[tank](/reference/motors/synced/tank), [steer](/reference/motors/synced/steer), [stop](/reference/motors/motor/stop)

View File

@ -0,0 +1,20 @@
# Set Run Acceleration Ramp
```sig
motors.largeD.setRunAccelerationRamp(1, MoveUnit.Seconds)
```
## Examples
```blocks
brick.buttonEnter.onEvent(ButtonEvent.Pressed, function () {
motors.largeB.run(50, 6, MoveUnit.Rotations)
})
brick.buttonLeft.onEvent(ButtonEvent.Pressed, function () {
motors.largeC.run(50, 6, MoveUnit.Seconds)
})
motors.largeB.setRunAccelerationRamp(360, MoveUnit.Degrees)
motors.largeB.setRunDecelerationRamp(360, MoveUnit.Degrees)
motors.largeC.setRunAccelerationRamp(2, MoveUnit.Seconds)
motors.largeC.setRunDecelerationRamp(2, MoveUnit.Seconds)
```

View File

@ -0,0 +1,20 @@
# Set Run Deceleration Ramp
```sig
motors.largeD.setRunDecelerationRamp(1, MoveUnit.Seconds)
```
## Examples
```blocks
brick.buttonEnter.onEvent(ButtonEvent.Pressed, function () {
motors.largeB.run(50, 6, MoveUnit.Rotations)
})
brick.buttonLeft.onEvent(ButtonEvent.Pressed, function () {
motors.largeC.run(50, 6, MoveUnit.Seconds)
})
motors.largeB.setRunAccelerationRamp(360, MoveUnit.Degrees)
motors.largeB.setRunDecelerationRamp(360, MoveUnit.Degrees)
motors.largeC.setRunAccelerationRamp(2, MoveUnit.Seconds)
motors.largeC.setRunDecelerationRamp(2, MoveUnit.Seconds)
```

View File

@ -15,7 +15,7 @@
"cardType": "tutorial", "cardType": "tutorial",
"url":"/tutorials/touch-sensor-values", "url":"/tutorials/touch-sensor-values",
"imageUrl":"/static/tutorials/touch-sensor-values.png" "imageUrl":"/static/tutorials/touch-sensor-values.png"
}, }, {
"name": "Pause Until Pressed", "name": "Pause Until Pressed",
"description": "Waits for the sensor to be pressed before continuing the program", "description": "Waits for the sensor to be pressed before continuing the program",
"cardType": "tutorial", "cardType": "tutorial",

View File

@ -127,31 +127,43 @@ namespace motors {
reset(Output.ALL) reset(Output.ALL)
} }
interface MoveSchedule {
speed: number;
useSteps: boolean;
steps: number[];
}
//% fixedInstances //% fixedInstances
export class MotorBase extends control.Component { export class MotorBase extends control.Component {
protected _port: Output; protected _port: Output;
protected _portName: string; protected _portName: string;
protected _brake: boolean; protected _brake: boolean;
protected _regulated: boolean;
private _pauseOnRun: boolean; private _pauseOnRun: boolean;
private _initialized: boolean; private _initialized: boolean;
private _brakeSettleTime: number; private _brakeSettleTime: number;
private _init: () => void; private _init: () => void;
private _run: (speed: number) => void; private _accelerationSteps: number;
private _move: (steps: boolean, stepsOrTime: number, speed: number) => void; private _accelerationTime: number;
private _decelerationSteps: number;
private _decelerationTime: number;
protected static output_types: number[] = [0x7, 0x7, 0x7, 0x7]; protected static output_types: number[] = [0x7, 0x7, 0x7, 0x7];
constructor(port: Output, init: () => void, run: (speed: number) => void, move: (steps: boolean, stepsOrTime: number, speed: number) => void) { constructor(port: Output, init: () => void) {
super(); super();
this._port = port; this._port = port;
this._portName = outputToName(this._port); this._portName = outputToName(this._port);
this._brake = false; this._brake = false;
this._regulated = true;
this._pauseOnRun = true; this._pauseOnRun = true;
this._initialized = false; this._initialized = false;
this._brakeSettleTime = 10; this._brakeSettleTime = 10;
this._init = init; this._init = init;
this._run = run; this._accelerationSteps = 0;
this._move = move; this._accelerationTime = 0;
this._decelerationSteps = 0;
this._decelerationTime = 0;
} }
/** /**
@ -263,6 +275,34 @@ namespace motors {
reset(this._port); reset(this._port);
} }
private normalizeSchedule(speed: number, step1: number, step2: number, step3: number, unit: MoveUnit): MoveSchedule {
const r: MoveSchedule = {
speed: Math.clamp(-100, 100, speed >> 0),
useSteps: true,
steps: [step1, step2, step3]
}
let scale = 1;
switch (unit) {
case MoveUnit.Rotations:
scale = 360;
r.useSteps = true;
break;
case MoveUnit.Degrees:
r.useSteps = true;
break;
case MoveUnit.Seconds:
scale = 1000;
r.useSteps = false;
break;
default:
r.useSteps = false;
break;
}
for (let i = 0; i < r.steps.length; ++i)
r.steps[i] = Math.max(0, (r.steps[i] * scale) | 0);
return r;
}
/** /**
* Runs the motor at a given speed for limited time or distance. * Runs the motor at a given speed for limited time or distance.
* @param speed the speed from ``100`` full forward to ``-100`` full backward, eg: 50 * @param speed the speed from ``100`` full forward to ``-100`` full backward, eg: 50
@ -277,41 +317,162 @@ namespace motors {
//% help=motors/motor/run //% help=motors/motor/run
run(speed: number, value: number = 0, unit: MoveUnit = MoveUnit.MilliSeconds) { run(speed: number, value: number = 0, unit: MoveUnit = MoveUnit.MilliSeconds) {
this.init(); this.init();
speed = Math.clamp(-100, 100, speed >> 0); const schedule = this.normalizeSchedule(speed, 0, value, 0, unit);
// stop if speed is 0 // stop if speed is 0
if (!speed) { if (!schedule.speed) {
this.stop(); this.stop();
return; return;
} }
// special: 0 is infinity // special: 0 is infinity
if (value == 0) { if (schedule.steps[0] + schedule.steps[1] + schedule.steps[2] == 0) {
this._run(speed); this._run(schedule.speed);
return;
}
// timed motor moves
const steps = schedule.steps;
const useSteps = schedule.useSteps;
// compute ramp up and down
steps[0] = (useSteps ? this._accelerationSteps : this._accelerationTime) || 0;
steps[2] = (useSteps ? this._decelerationSteps : this._decelerationTime) || 0;
if (steps[0] + steps[2] > steps[1]) {
// rescale
const r = steps[1] / (steps[0] + steps[2]);
steps[0] = Math.floor(steps[0] * r);
steps[2] *= Math.floor(steps[2] * r);
}
steps[1] -= (steps[0] + steps[2]);
// send ramped command
this._schedule(schedule);
this.pauseOnRun(steps[0] + steps[1] + steps[2]);
}
/**
* Schedules a run of the motor with an acceleration, constant and deceleration phase.
* @param speed the speed from ``100`` full forward to ``-100`` full backward, eg: 50
* @param acceleration acceleration phase measured distance or rotation
* @param value measured distance or rotation
* @param deceleration deceleration phase measured distance or rotation
* @param unit (optional) unit of the value
*/
//% blockId=motorSchedule block="schedule %motor at %speed=motorSpeedPicker|\\%|for %acceleration|%value|%deceleration||%unit"
//% weight=99 blockGap=8
//% group="Move"
//% motor.fieldEditor="motors"
//% help=motors/motor/schedule
//% inlineInputMode=inline
schedule(speed: number, acceleration: number, value: number, deceleration: number, unit: MoveUnit = MoveUnit.MilliSeconds) {
this.init();
const schedule = this.normalizeSchedule(speed, acceleration, value, deceleration, unit);
// stop if speed is 0
if (!schedule.speed) {
this.stop();
return;
}
// special case: do nothing
if (schedule.steps[0] + schedule.steps[1] + schedule.steps[2] == 0) {
return; return;
} }
// timed motor moves // timed motor moves
let useSteps: boolean; const steps = schedule.steps;
let stepsOrTime: number; // send ramped command
switch (unit) { this._schedule(schedule);
case MoveUnit.Rotations: this.pauseOnRun(steps[0] + steps[1] + steps[2]);
stepsOrTime = (value * 360) >> 0;
useSteps = true;
break;
case MoveUnit.Degrees:
stepsOrTime = value >> 0;
useSteps = true;
break;
case MoveUnit.Seconds:
stepsOrTime = (value * 1000) >> 0;
useSteps = false;
break;
default:
stepsOrTime = value;
useSteps = false;
break;
} }
this._move(useSteps, stepsOrTime, speed); /**
this.pauseOnRun(stepsOrTime); * Specifies the amount of rotation or time for the acceleration
* of run commands.
*/
//% blockId=outputMotorsetRunAcceleration block="set %motor|run acceleration to $value||$unit"
//% motor.fieldEditor="motors"
//% weight=21 blockGap=8
//% group="Properties"
//% help=motors/motor/set-run-acceleration-ramp
setRunAccelerationRamp(value: number, unit: MoveUnit = MoveUnit.MilliSeconds) {
switch (unit) {
case MoveUnit.Rotations:
this._accelerationSteps = Math.max(0, (value * 360) | 0);
break;
case MoveUnit.Degrees:
this._accelerationSteps = Math.max(0, value | 0);
break;
case MoveUnit.Seconds:
this._accelerationTime = Math.max(0, (value * 1000) | 0);
break;
case MoveUnit.MilliSeconds:
this._accelerationTime = Math.max(0, value | 0);
break;
}
}
/**
* Specifies the amount of rotation or time for the acceleration
* of run commands.
*/
//% blockId=outputMotorsetRunDeceleration block="set %motor|run deceleration ramp to $value||$unit"
//% motor.fieldEditor="motors"
//% weight=20 blockGap=8
//% group="Properties"
//% help=motors/motor/set-run-deceleration-ramp
setRunDecelerationRamp(value: number, unit: MoveUnit = MoveUnit.MilliSeconds) {
switch (unit) {
case MoveUnit.Rotations:
this._decelerationSteps = Math.max(0, (value * 360) | 0);
break;
case MoveUnit.Degrees:
this._decelerationSteps = Math.max(0, value | 0);
break;
case MoveUnit.Seconds:
this._decelerationTime = Math.max(0, (value * 1000) | 0);
break;
case MoveUnit.MilliSeconds:
this._decelerationTime = Math.max(0, value | 0);
break;
}
}
private _run(speed: number) {
// ramp up acceleration
if (this._accelerationTime) {
this._schedule({ speed: speed, useSteps: false, steps: [this._accelerationTime, 100, 0] });
pause(this._accelerationTime);
}
// keep going
const b = mkCmd(this._port, this._regulated ? DAL.opOutputSpeed : DAL.opOutputPower, 1)
b.setNumber(NumberFormat.Int8LE, 2, speed)
writePWM(b)
if (speed) {
writePWM(mkCmd(this._port, DAL.opOutputStart, 0))
}
}
private _schedule(schedule: MoveSchedule) {
const p = {
useSteps: schedule.useSteps,
step1: schedule.steps[0],
step2: schedule.steps[1],
step3: schedule.steps[2],
speed: this._regulated ? schedule.speed : undefined,
power: this._regulated ? undefined : schedule.speed,
useBrake: this._brake
};
step(this._port, p)
}
/**
* Indicates if the motor(s) speed should be regulated. Default is true.
* @param value true for regulated motor
*/
//% blockId=outputMotorSetRegulated block="set %motor|regulated %value=toggleOnOff"
//% motor.fieldEditor="motors"
//% weight=58 blockGap=8
//% group="Properties"
//% help=motors/motor/set-regulated
setRegulated(value: boolean) {
this._regulated = value;
} }
/** /**
@ -338,6 +499,10 @@ namespace motors {
pauseUntil(() => this.isReady(), timeOut); pauseUntil(() => this.isReady(), timeOut);
} }
setRunSmoothness(accelerationPercent: number, decelerationPercent: number) {
}
protected setOutputType(large: boolean) { protected setOutputType(large: boolean) {
for (let i = 0; i < DAL.NUM_OUTPUTS; ++i) { for (let i = 0; i < DAL.NUM_OUTPUTS; ++i) {
if (this._port & (1 << i)) { if (this._port & (1 << i)) {
@ -364,12 +529,10 @@ namespace motors {
//% fixedInstances //% fixedInstances
export class Motor extends MotorBase { export class Motor extends MotorBase {
private _large: boolean; private _large: boolean;
private _regulated: boolean;
constructor(port: Output, large: boolean) { constructor(port: Output, large: boolean) {
super(port, () => this.__init(), (speed) => this.__setSpeed(speed), (steps, stepsOrTime, speed) => this.__move(steps, stepsOrTime, speed)); super(port, () => this.__init());
this._large = large; this._large = large;
this._regulated = true;
this.markUsed(); this.markUsed();
} }
@ -381,44 +544,6 @@ namespace motors {
this.setOutputType(this._large); this.setOutputType(this._large);
} }
private __setSpeed(speed: number) {
const b = mkCmd(this._port, this._regulated ? DAL.opOutputSpeed : DAL.opOutputPower, 1)
b.setNumber(NumberFormat.Int8LE, 2, speed)
writePWM(b)
if (speed) {
writePWM(mkCmd(this._port, DAL.opOutputStart, 0))
}
}
private __move(steps: boolean, stepsOrTime: number, speed: number) {
control.dmesg("motor.__move")
const p = {
useSteps: steps,
step1: 0,
step2: stepsOrTime,
step3: 0,
speed: this._regulated ? speed : undefined,
power: this._regulated ? undefined : speed,
useBrake: this._brake
};
control.dmesg("motor.1")
step(this._port, p)
control.dmesg("motor.__move end")
}
/**
* Indicates if the motor speed should be regulated. Default is true.
* @param value true for regulated motor
*/
//% blockId=outputMotorSetRegulated block="set %motor|regulated %value=toggleOnOff"
//% motor.fieldEditor="motors"
//% weight=58 blockGap=8
//% group="Properties"
//% help=motors/motor/set-regulated
setRegulated(value: boolean) {
this._regulated = value;
}
/** /**
* Gets motor actual speed. * Gets motor actual speed.
* @param motor the port which connects to the motor * @param motor the port which connects to the motor
@ -506,7 +631,7 @@ namespace motors {
export class SynchedMotorPair extends MotorBase { export class SynchedMotorPair extends MotorBase {
constructor(ports: Output) { constructor(ports: Output) {
super(ports, () => this.__init(), (speed) => this.__setSpeed(speed), (steps, stepsOrTime, speed) => this.__move(steps, stepsOrTime, speed)); super(ports, () => this.__init());
this.markUsed(); this.markUsed();
} }
@ -518,24 +643,6 @@ namespace motors {
this.setOutputType(true); this.setOutputType(true);
} }
private __setSpeed(speed: number) {
syncMotors(this._port, {
speed: speed,
turnRatio: 0, // same speed
useBrake: !!this._brake
})
}
private __move(steps: boolean, stepsOrTime: number, speed: number) {
syncMotors(this._port, {
useSteps: steps,
speed: speed,
turnRatio: 0, // same speed
stepsOrTime: stepsOrTime,
useBrake: this._brake
});
}
/** /**
* The Move Tank block can make a robot drive forward, backward, turn, or stop. * The Move Tank block can make a robot drive forward, backward, turn, or stop.
* Use the Move Tank block for robot vehicles that have two Large Motors, * Use the Move Tank block for robot vehicles that have two Large Motors,
@ -745,26 +852,15 @@ namespace motors {
return return
} }
speed = Math.clamp(-100, 100, speed) speed = Math.clamp(-100, 100, speed)
control.dmesg('speed: ' + speed)
let b = mkCmd(out, op, 15) let b = mkCmd(out, op, 15)
control.dmesg('STEP 5')
b.setNumber(NumberFormat.Int8LE, 2, speed) b.setNumber(NumberFormat.Int8LE, 2, speed)
// note that b[3] is padding // note that b[3] is padding
control.dmesg('STEP 1')
b.setNumber(NumberFormat.Int32LE, 4 + 4 * 0, opts.step1) b.setNumber(NumberFormat.Int32LE, 4 + 4 * 0, opts.step1)
control.dmesg('STEP 2')
b.setNumber(NumberFormat.Int32LE, 4 + 4 * 1, opts.step2) b.setNumber(NumberFormat.Int32LE, 4 + 4 * 1, opts.step2)
control.dmesg('STEP 3')
b.setNumber(NumberFormat.Int32LE, 4 + 4 * 2, opts.step3) b.setNumber(NumberFormat.Int32LE, 4 + 4 * 2, opts.step3)
control.dmesg('STEP 4')
control.dmesg('br ' + opts.useBrake);
const br = !!opts.useBrake ? 1 : 0; const br = !!opts.useBrake ? 1 : 0;
control.dmesg('Step 4.5 ' + br)
b.setNumber(NumberFormat.Int8LE, 4 + 4 * 3, br) b.setNumber(NumberFormat.Int8LE, 4 + 4 * 3, br)
control.dmesg('STEP 5')
writePWM(b) writePWM(b)
control.dmesg('end step')
} }
} }

View File

@ -1,4 +1,5 @@
namespace pxsim { namespace pxsim {
const MIN_RAMP_SPEED = 3;
export class MotorNode extends BaseNode { export class MotorNode extends BaseNode {
isOutput = true; isOutput = true;
@ -30,11 +31,11 @@ namespace pxsim {
} }
getSpeed() { getSpeed() {
return this.speed * (!this._synchedMotor && this.polarity == 0 ? -1 : 1); return Math.round(this.speed * (!this._synchedMotor && this.polarity == 0 ? -1 : 1));
} }
getAngle() { getAngle() {
return this.angle; return Math.round(this.angle);
} }
// returns the slave motor if any // returns the slave motor if any
@ -160,13 +161,19 @@ namespace pxsim {
const dstep = isTimeCommand const dstep = isTimeCommand
? pxsim.U.now() - this.speedCmdTime ? pxsim.U.now() - this.speedCmdTime
: this.tacho - this.speedCmdTacho; : this.tacho - this.speedCmdTacho;
if (dstep < step1) // rampup if (step1 && dstep < step1) { // rampup
this.speed = speed * dstep / step1; this.speed = speed * dstep / step1;
// ensure non-zero speed
this.speed = Math.max(MIN_RAMP_SPEED, Math.ceil(Math.abs(this.speed))) * Math.sign(speed);
}
else if (dstep < step1 + step2) // run else if (dstep < step1 + step2) // run
this.speed = speed; this.speed = speed;
else if (dstep < step1 + step2 + step3) else if (step2 && dstep < step1 + step2 + step3) {
this.speed = speed * (step1 + step2 + step3 - dstep) / (step1 + step2 + step3); this.speed = speed * (step1 + step2 + step3 - dstep)
else { / (step1 + step2 + step3) + 5;
// ensure non-zero speed
this.speed = Math.max(MIN_RAMP_SPEED, Math.ceil(Math.abs(this.speed))) * Math.sign(speed);
} else {
if (brake) this.speed = 0; if (brake) this.speed = 0;
if (!isTimeCommand) { if (!isTimeCommand) {
// we need to patch the actual position of the motor when // we need to patch the actual position of the motor when
@ -227,11 +234,10 @@ namespace pxsim {
this.angle = this.manualReferenceAngle + this.manualAngle; this.angle = this.manualReferenceAngle + this.manualAngle;
this.setChangedState(); this.setChangedState();
} }
this.speed = Math.round(this.speed); // integer only // don't round speed
// compute delta angle // compute delta angle
const rotations = this.getSpeed() / 100 * this.rotationsPerMilliSecond * elapsed; const rotations = this.speed / 100 * this.rotationsPerMilliSecond * elapsed;
const deltaAngle = Math.round(rotations * 360); const deltaAngle = rotations * 360;
if (deltaAngle) { if (deltaAngle) {
this.angle += deltaAngle; this.angle += deltaAngle;
this.tacho += Math.abs(deltaAngle); this.tacho += Math.abs(deltaAngle);