diff --git a/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Datasheet/BLD-510B_manual.pdf b/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Datasheet/BLD-510B_manual.pdf new file mode 100644 index 000000000..816ab29dd Binary files /dev/null and b/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Datasheet/BLD-510B_manual.pdf differ diff --git a/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Driver/Controllers/BLD510B.cs b/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Driver/Controllers/BLD510B.cs new file mode 100644 index 000000000..00f6b8b1b --- /dev/null +++ b/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Driver/Controllers/BLD510B.cs @@ -0,0 +1,278 @@ +using Meadow.Modbus; +using Meadow.Peripherals; +using System; +using System.Threading.Tasks; + +namespace Meadow.Foundation.MotorControllers.StepperOnline; + +public class BLD510B : ModbusPolledDevice +{ + public event EventHandler ErrorConditionsChanged; + + private ErrorConditions _lastError; + private ushort _state; + + /// + /// The default Modbus address for the V10x device. + /// + public const int DefaultModbusAddress = 1; + + /// + /// The default baud rate for communication with the V10x device. + /// + public const int DefaultBaudRate = 9600; + + public BLD510B(ModbusRtuClient client, byte modbusAddress = 0x01, TimeSpan? refreshPeriod = null) + : base(client, modbusAddress, refreshPeriod) + { + MapHoldingRegistersToField( + startRegister: 0x801b, + registerCount: 1, + fieldName: nameof(_state), + conversionFunction: StateCheckerFunction + ); + } + + public async Task GetStartStopTerminal() + { + var r = await ReadHoldingRegisters(0x8000, 1); + + return ((r[0] >> 8) & 1) != 0; + } + + public async Task SetStartStopTerminal(bool startEnabled) + { + var current = (int)(await ReadHoldingRegisters(0x8000, 1))[0]; + if (startEnabled) + { // set bit 0 + current = current | (1 << 8); + } + else + { + current = current & ~(1 << 8); + } + await WriteHoldingRegister(0x8000, (ushort)current); + } + + public async Task GetDirectionTerminal() + { + var r = await ReadHoldingRegisters(0x8000, 1); + return ((r[0] >> 9) & 1) == 0 ? RotationDirection.Clockwise : RotationDirection.CounterClockwise; + } + + public async Task SetDirectionTerminal(RotationDirection direction) + { + int current = ReadHoldingRegisters(0x8000, 1).Result[0]; + if (direction == RotationDirection.Clockwise) + { // clear bit 1 + current = current & ~(1 << 9); + } + else + { + current = current | (1 << 9); + } + await WriteHoldingRegister(0x8000, (ushort)current); + } + + public async Task GetBrakeTerminal() + { + var r = await ReadHoldingRegisters(0x8000, 1); + return ((r[0] >> 10) & 1) != 0; + } + + public async Task SetBrakeTerminal(bool brakeEnabled) + { + int current = ReadHoldingRegisters(0x8000, 1).Result[0]; + if (brakeEnabled) + { // set bit 2 + current = current | (1 << 10); + } + else + { + current = current & ~(1 << 10); + } + await WriteHoldingRegister(0x8000, (ushort)current); + } + + public async Task GetSpeedControl() + { + var r = await ReadHoldingRegisters(0x8000, 1); + return (SpeedControl)((r[0] >> 11) & 1); + } + + public async Task SetSpeedControl(SpeedControl speedControl) + { + int current = ReadHoldingRegisters(0x8000, 1).Result[0]; + if (speedControl == SpeedControl.AnalogPot) + { // clear bit 4 + current = current & ~(1 << 11); + } + else + { + current = current | (1 << 11); + } + await WriteHoldingRegister(0x8000, (ushort)current); + } + + public async Task GetNumberOfMotorPolePairs() + { + var r = await ReadHoldingRegisters(0x8000, 1); + return (byte)(r[0] & 0xff); + } + + public async Task SetNumberOfMotorPolePairs(byte numberOfMotorPolePairs) + { + var current = (int)(await ReadHoldingRegisters(0x8000, 1))[0]; + current &= 0xff00; + current |= numberOfMotorPolePairs; + // always disable EN if we're doing this operation + current &= ~(1 << 8); + await WriteHoldingRegister(0x8000, (ushort)current); + } + + public async Task GetStartupTorque() + { + var r = await ReadHoldingRegisters(0x8002, 1); + return (byte)(r[0] >> 8); + } + + public async Task SetStartupTorque(byte value) + { + var r = await ReadHoldingRegisters(0x8002, 1); + var current = r[0] & 0x00ff; + current |= value << 8; + await WriteHoldingRegister(0x8002, (ushort)current); + } + + public async Task GetStartupSpeed() + { + var r = await ReadHoldingRegisters(0x8002, 1); + return (byte)(r[0] & 0xff); + } + + public async Task SetStartupSpeed(byte value) + { + var r = await ReadHoldingRegisters(0x8002, 1); + var current = r[0] & 0xff00; + current |= value; + await WriteHoldingRegister(0x8002, (ushort)current); + } + + public async Task GetAccelerationTime() + { + var r = await ReadHoldingRegisters(0x8003, 1); + return TimeSpan.FromSeconds((r[0] >> 8) / 10d); + } + + public async Task SetAccelerationTime(TimeSpan value) + { + if (value.TotalSeconds < 0 || value.TotalSeconds > 25.5) throw new ArgumentOutOfRangeException(); + var r = await ReadHoldingRegisters(0x8002, 1); + var current = r[0] & 0x00ff; + current |= (byte)(value.TotalSeconds * 10) << 8; + await WriteHoldingRegister(0x8003, (ushort)current); + } + + public async Task GetDecelerationTime() + { + var r = await ReadHoldingRegisters(0x8003, 1); + return TimeSpan.FromSeconds((r[0] & 0xff) / 10d); + } + + public async Task SetDecelerationTime(TimeSpan value) + { + if (value.TotalSeconds < 0 || value.TotalSeconds > 25.5) throw new ArgumentOutOfRangeException(); + var r = await ReadHoldingRegisters(0x8003, 1); + var current = r[0] & 0xff00; + current |= (byte)(value.TotalSeconds * 10); + await WriteHoldingRegister(0x8003, (ushort)current); + } + + public async Task GetMaxCurrent() + { + var r = await ReadHoldingRegisters(0x8004, 1); + return (byte)(r[0] >> 8); + } + + public async Task SetMaxCurrent(byte value) + { + var r = await ReadHoldingRegisters(0x8004, 1); + var current = r[0] & 0x00ff; + current |= value << 8; + await WriteHoldingRegister(0x8004, (ushort)current); + } + + public async Task GetMotorType() + { + var r = await ReadHoldingRegisters(0x8004, 1); + return (MotorType)(r[0] & 0xff); + } + + public async Task SetMotorType(MotorType value) + { + var r = await ReadHoldingRegisters(0x8004, 1); + var current = r[0] & 0xff00; + current |= (int)value; + await WriteHoldingRegister(0x8004, (ushort)current); + } + + /// + /// The desired speed when using control mode of RS485, this is ignored when control is analog + /// + public async Task GetDesiredSpeed() + { + var r = await ReadHoldingRegisters(0x8005, 1); + return r[0]; + } + + public async Task SetDesiredSpeed(ushort speed) + { + // swap endianness + var s = speed << 8 | speed >> 8; + await WriteHoldingRegister(0x8005, (ushort)s); + } + + public async Task GetModbusAddress() + { + var r = await ReadHoldingRegisters(0x8007, 1); + return (byte)r[0]; + } + + public async Task SetModbusAddress(byte value) + { + if (value <= 0 || value > 250) throw new ArgumentOutOfRangeException(); + + await WriteHoldingRegister(0x8007, value); + } + + public async Task GetActualSpeed() + { + ushort[] data; + do + { + data = await ReadHoldingRegisters(0x8018, 1); + } while (data.Length == 0); + + // swap endianness + return (ushort)(data[0] >> 8 | data[0] << 8); + } + + public ErrorConditions ErrorConditions + { + get => _lastError; + } + + private object StateCheckerFunction(ushort[] data) + { + // we use this function to set events + var state = (ErrorConditions)(data[0] >> 8); + + if (state != _lastError) + { + _lastError = state; + ErrorConditionsChanged?.Invoke(this, state); + } + + return data[0]; + } +} diff --git a/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Driver/ErrorConditions.cs b/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Driver/ErrorConditions.cs new file mode 100644 index 000000000..1fa080e36 --- /dev/null +++ b/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Driver/ErrorConditions.cs @@ -0,0 +1,15 @@ +using System; + +namespace Meadow.Foundation.MotorControllers.StepperOnline; + +[Flags] +public enum ErrorConditions +{ + None = 0x00, + LockedRotor = 0x01, + OverCurrent = 0x02, + HallValueAbnormal = 0x04, + BusVoltageLow = 0x08, + BusVoltageHigh = 0x10, + CurrentPeak = 0x20 +} diff --git a/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Driver/Meadow.Foundation.Motors.StepperOnline.csproj b/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Driver/Meadow.Foundation.Motors.StepperOnline.csproj new file mode 100644 index 000000000..df9b334ce --- /dev/null +++ b/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Driver/Meadow.Foundation.Motors.StepperOnline.csproj @@ -0,0 +1,13 @@ + + + + netstandard2.1 + enable + 11 + + + + + + + diff --git a/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Driver/MotorType.cs b/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Driver/MotorType.cs new file mode 100644 index 000000000..c71b3aee7 --- /dev/null +++ b/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Driver/MotorType.cs @@ -0,0 +1,7 @@ +namespace Meadow.Foundation.MotorControllers.StepperOnline; + +public enum MotorType +{ + Sensored = 0x0f, + Sensorless = 0x10 +} diff --git a/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Driver/Motors/F55B150_24GL_30S.cs b/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Driver/Motors/F55B150_24GL_30S.cs new file mode 100644 index 000000000..fa3bbd8fe --- /dev/null +++ b/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Driver/Motors/F55B150_24GL_30S.cs @@ -0,0 +1,95 @@ +using Meadow.Foundation.MotorControllers.StepperOnline; +using Meadow.Peripherals; +using Meadow.Peripherals.Motors; +using Meadow.Units; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Meadow.Foundation.Motors.StepperOnline; + +/// +/// A 24V, 100RPM, 30:1 gear-reduction BLDC motor +/// +public class F55B150_24GL_30S : IMotor +{ + private BLD510B controller; + + public const RotationDirection DefaultRotationDirection = RotationDirection.Clockwise; + public static AngularVelocity DefaultSpeed = new AngularVelocity(100, AngularVelocity.UnitType.RevolutionsPerMinute); + + /// + public RotationDirection Direction { get; private set; } + + /// + public bool IsMoving + { + get => controller.GetActualSpeed().Result > 0; + } + + public F55B150_24GL_30S(BLD510B controller) + { + this.controller = controller; + + Initialize().Wait(); + } + + public Task SetSpeed(AngularVelocity desiredSpeed) + { + var val = desiredSpeed.RevolutionsPerMinute * 75; + if (val > 65535) throw new ArgumentOutOfRangeException(nameof(desiredSpeed)); + return controller.SetDesiredSpeed((ushort)val); + } + + private async Task Initialize() + { + await controller.SetStartStopTerminal(false); + await controller.SetNumberOfMotorPolePairs(10); + await controller.SetSpeedControl(SpeedControl.RS485); + Direction = DefaultRotationDirection; + await controller.SetDirectionTerminal(Direction); + await SetSpeed(DefaultSpeed); + } + + public async Task Run(RotationDirection direction, CancellationToken cancellationToken = default) + { + Direction = direction; + await controller.SetDirectionTerminal(direction); + await controller.SetStartStopTerminal(true); + } + + public async Task RunFor(TimeSpan runTime, RotationDirection direction, CancellationToken cancellationToken = default) + { + Direction = direction; + await controller.SetDirectionTerminal(direction); + await controller.SetStartStopTerminal(true); + await Task.Delay(runTime, cancellationToken); + await controller.SetStartStopTerminal(true); + } + + public Task Stop(CancellationToken cancellationToken = default) + { + return controller.SetStartStopTerminal(false); + } + + public Task SetBrakeState(bool enabled) + { + return controller.SetBrakeTerminal(enabled); + } + + public Task SetAccelerationTime(TimeSpan time) + { + return controller.SetAccelerationTime(time); + } + + public Task SetDecelerationTime(TimeSpan time) + { + return controller.SetDecelerationTime(time); + } + + public async Task GetActualSpeed() + { + var rawSpeed = await controller.GetActualSpeed(); + return new AngularVelocity(rawSpeed * 0.0133, AngularVelocity.UnitType.RevolutionsPerMinute); + } +} diff --git a/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Driver/SpeedControl.cs b/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Driver/SpeedControl.cs new file mode 100644 index 000000000..916b632a9 --- /dev/null +++ b/Source/Meadow.Foundation.Peripherals/Motors.StepperOnline/Driver/SpeedControl.cs @@ -0,0 +1,7 @@ +namespace Meadow.Foundation.MotorControllers.StepperOnline; + +public enum SpeedControl +{ + AnalogPot = 0, + RS485 = 1 +} diff --git a/Source/Meadow.Foundation.sln b/Source/Meadow.Foundation.sln index 7d69dddc4..951c3f3e1 100644 --- a/Source/Meadow.Foundation.sln +++ b/Source/Meadow.Foundation.sln @@ -1629,6 +1629,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ICs.IOExpanders.Ads1263", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ads1263_Sample", "Meadow.Foundation.Peripherals\ICs.IOExpanders.Ads1263\Samples\Ads1263_Sample\Ads1263_Sample.csproj", "{3A402704-9390-4492-93D4-B377BE8F034C}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StepperOnline", "StepperOnline", "{CC1FB401-1B19-4254-9208-339661134D9B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meadow.Foundation.Motors.StepperOnline", "Meadow.Foundation.Peripherals\Motors.StepperOnline\Driver\Meadow.Foundation.Motors.StepperOnline.csproj", "{D8D26232-ECA0-4042-88F4-7ECF1BC3E62F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -3937,6 +3941,10 @@ Global {3A402704-9390-4492-93D4-B377BE8F034C}.Release|Any CPU.ActiveCfg = Release|Any CPU {3A402704-9390-4492-93D4-B377BE8F034C}.Release|Any CPU.Build.0 = Release|Any CPU {3A402704-9390-4492-93D4-B377BE8F034C}.Release|Any CPU.Deploy.0 = Release|Any CPU + {D8D26232-ECA0-4042-88F4-7ECF1BC3E62F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8D26232-ECA0-4042-88F4-7ECF1BC3E62F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8D26232-ECA0-4042-88F4-7ECF1BC3E62F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8D26232-ECA0-4042-88F4-7ECF1BC3E62F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -4748,6 +4756,8 @@ Global {BEAD51B2-CF51-4836-ACBA-0911841E00F6} = {6D5016B9-7BF5-4E9D-BBA4-A621BAE0E638} {B414A340-EA69-4CD3-AB8B-D0B3835527B2} = {6D5016B9-7BF5-4E9D-BBA4-A621BAE0E638} {3A402704-9390-4492-93D4-B377BE8F034C} = {BEAD51B2-CF51-4836-ACBA-0911841E00F6} + {CC1FB401-1B19-4254-9208-339661134D9B} = {2486B48D-D4A2-4505-BF50-F33B2E15DA97} + {D8D26232-ECA0-4042-88F4-7ECF1BC3E62F} = {CC1FB401-1B19-4254-9208-339661134D9B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AF7CA16F-8C38-4546-87A2-5DAAF58A1520}