如果你需要精确控制设备的转动角度,普通电机是做不到的,通常我们会选用步进马达。比如28BYJ-48这个型号的,很容易买到。
步进马达得配合驱动板使用,最常用的是ULN2003芯片的驱动板,就像下图。不过注意,你买到的驱动板长相可能不太一样,不过没关系,只要芯片上写的是ULN2003,就可以使用,它们的接口都是一样的。关于步进马达的原理,可以看这篇: https://en.wikipedia.org/wiki/Stepper_motor
拿到驱动板和步进马达后,将马达插入驱动板的白色插槽中,这个接口有防呆设计,所以不会插反。
关于Windows 10 IoT如何驱动步进马达,有一篇很好的英文材料:
https://www.hackster.io/erickbp/stepper-motor-and-windows-10-iot-core-d3c5d6
我的例子就是基于上面这篇文章的改进和补充。
一、物理连接
首先,不建议把步进马达驱动板的5v电源接入树莓派的5v输出,运行时侯树莓派会报电压不足的提示的,如果你还有别的什么设备连接在树莓派上,很可能会导致机器重启。所以建议大家用外接的5v电源,正负极可以完全独立,负极是可以不接入树莓派的GND的,这和那篇英文资料里说的不太一样。不过我不清楚这样做会不会爆炸。反正我没爆。
我用的外接电源是一根废旧的USB鼠标线改的,USB的输出就是5v,最方便了。
接好电源以后,使用4根杜邦线,把IN1-IN4接入树莓派的GPIO端口,对应关系如下(当然你可以自己改,程序也要做相应的修改):
驱动板端口 | 树莓派端口 |
---|---|
IN1 | GPIO 26 |
IN2 | GPIO 13 |
IN3 | GPIO 6 |
IN4 | GPIO 5 |
驱动板端:
树莓派端:
二、爆代码
原版代码在这里:https://github.com/erickbp/IoT/blob/master/Stepper%20Motor/Stepper%20Motor/Uln2003Driver.cs
我做了一些改进。先贴出完整代码:
using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Windows.Devices.Gpio; namespace Uln2003StepMotor { public class Uln2003Driver : IDisposable { public int IntervalMs { get; set; } private readonly GpioPin[] _gpioPins = new GpioPin[4]; private readonly GpioPinValue[][] _waveDriveSequence = { new[] {GpioPinValue.High, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low}, new[] {GpioPinValue.Low, GpioPinValue.High, GpioPinValue.Low, GpioPinValue.Low}, new[] {GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.High, GpioPinValue.Low}, new[] {GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.High} }; private readonly GpioPinValue[][] _fullStepSequence = { new[] {GpioPinValue.High, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.High}, new[] {GpioPinValue.High, GpioPinValue.High, GpioPinValue.Low, GpioPinValue.Low}, new[] {GpioPinValue.Low, GpioPinValue.High, GpioPinValue.High, GpioPinValue.Low}, new[] {GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.High, GpioPinValue.High } }; private readonly GpioPinValue[][] _halfStepSequence = { new[] {GpioPinValue.High, GpioPinValue.High, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.High}, new[] {GpioPinValue.Low, GpioPinValue.High, GpioPinValue.High, GpioPinValue.High, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low}, new[] {GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.High, GpioPinValue.High, GpioPinValue.High, GpioPinValue.Low, GpioPinValue.Low}, new[] {GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.High, GpioPinValue.High, GpioPinValue.High } }; public Uln2003Driver(GpioController gpioController, int wireIn1, int wireIn2, int wireIn3, int wireIn4, GpioSharingMode sharingMode = GpioSharingMode.Exclusive, int intervalMs = 5) { var gpio = gpioController ?? GpioController.GetDefault(); _gpioPins[0] = gpio.OpenPin(wireIn1, sharingMode); _gpioPins[1] = gpio.OpenPin(wireIn2, sharingMode); _gpioPins[2] = gpio.OpenPin(wireIn3, sharingMode); _gpioPins[3] = gpio.OpenPin(wireIn4, sharingMode); foreach (var gpioPin in _gpioPins) { gpioPin.Write(GpioPinValue.Low); gpioPin.SetDriveMode(GpioPinDriveMode.Output); } IntervalMs = intervalMs; } public async Task TurnAsync(TurnDirection direction, CancellationToken ct, DrivingMethod drivingMethod = DrivingMethod.FullStep) { bool stop = false; GpioPinValue[][] methodSequence; switch (drivingMethod) { case DrivingMethod.WaveDrive: methodSequence = _waveDriveSequence; break; case DrivingMethod.FullStep: methodSequence = _fullStepSequence; break; case DrivingMethod.HalfStep: methodSequence = _halfStepSequence; break; default: throw new ArgumentOutOfRangeException(nameof(drivingMethod), drivingMethod, null); } while (!stop) { for (var j = 0; j < methodSequence[0].Length; j++) { for (var i = 0; i < 4; i++) { _gpioPins[i].Write(methodSequence[direction == TurnDirection.Right ? i : 3 - i][j]); } // don't pass cancellation token, will blow up. await Task.Delay(IntervalMs); if (ct.IsCancellationRequested) { Debug.WriteLine("Cancel Requested, stop now."); stop = true; break; } } } Stop(); } public async Task TurnAsync(int degree, TurnDirection direction, CancellationToken ct, DrivingMethod drivingMethod = DrivingMethod.FullStep) { var steps = 0; GpioPinValue[][] methodSequence; switch (drivingMethod) { case DrivingMethod.WaveDrive: methodSequence = _waveDriveSequence; steps = (int)Math.Ceiling(degree / 0.1767478397486253); break; case DrivingMethod.FullStep: methodSequence = _fullStepSequence; steps = (int)Math.Ceiling(degree / 0.1767478397486253); break; case DrivingMethod.HalfStep: methodSequence = _halfStepSequence; steps = (int)Math.Ceiling(degree / 0.0883739198743126); break; default: throw new ArgumentOutOfRangeException(nameof(drivingMethod), drivingMethod, null); } var counter = 0; while (counter < steps) { for (var j = 0; j < methodSequence[0].Length; j++) { for (var i = 0; i < 4; i++) { _gpioPins[i].Write(methodSequence[direction == TurnDirection.Right ? i : 3 - i][j]); } // don't pass cancellation token, will blow up. await Task.Delay(IntervalMs); if (ct.IsCancellationRequested) { Debug.WriteLine("Cancel Requested, stop now."); counter = steps; } else { counter++; } if (counter == steps) { break; } } } Stop(); } public void Stop() { foreach (var gpioPin in _gpioPins) { gpioPin.Write(GpioPinValue.Low); } } public void Dispose() { foreach (var gpioPin in _gpioPins) { gpioPin.Write(GpioPinValue.Low); gpioPin.Dispose(); } } } public enum DrivingMethod { WaveDrive, FullStep, HalfStep } public enum TurnDirection { Left, Right } }
改进的地方是:
1. TurnAsync方法增加了CancellationToken,可以转动到一般的时候强制停止转动。
public async Task TurnAsync(int degree, TurnDirection direction, CancellationToken ct, DrivingMethod drivingMethod = DrivingMethod.FullStep)
// don't pass cancellation token, will blow up. await Task.Delay(IntervalMs); if (ct.IsCancellationRequested) { Debug.WriteLine("Cancel Requested, stop now."); counter = steps; } else { counter++; }
2. TurnAsync增加一个重载,用途是不指定角度,不停的往一个方向转动。然后通过CancellationToken来停止。
public async Task TurnAsync(TurnDirection direction, CancellationToken ct, DrivingMethod drivingMethod = DrivingMethod.FullStep)
使用方法:
XAML
<Button x:Name="BtnLeftForever" Content="Trun Left Forever" Click="BtnLeftForever_OnClick" /> <Button x:Name="BtnLeft90" Content="Turn Left 90" Click="BtnLeft90_OnClick" Margin="0,10,0,0" /> <Button x:Name="BtnRight90" Content="Turn Right 90" Margin="0,10,0,0" Click="BtnRight90_OnClick" /> <Button x:Name="BtnStop" Content="Stop" Margin="0,10,0,0" Click="BtnStop_OnClick"/> <TextBox Text="10" Header="Degree" x:Name="TxtDegree" /> <Button x:Name="TurnDegree" Click="TurnDegree_OnClick" Content="Trun Left" />
后台
public sealed partial class MainPage : Page { public CancellationTokenSource Cts { get; private set; } public Uln2003Driver Uln2003Driver { get; set; } public MainPage() { this.InitializeComponent(); var controller = GpioController.GetDefault(); Uln2003Driver = new Uln2003Driver(controller, 26, 13, 6, 5); } private async Task TurnMotor(int degree, TurnDirection direction) { Cts = new CancellationTokenSource(); await Uln2003Driver.TurnAsync(degree, direction, Cts.Token); } private async void BtnLeft90_OnClick(object sender, RoutedEventArgs e) { await TurnMotor(90, TurnDirection.Left); } private async void BtnRight90_OnClick(object sender, RoutedEventArgs e) { await TurnMotor(90, TurnDirection.Right); } private async void TurnDegree_OnClick(object sender, RoutedEventArgs e) { await TurnMotor(int.Parse(TxtDegree.Text), TurnDirection.Left); } private void BtnStop_OnClick(object sender, RoutedEventArgs e) { Cts.Cancel(); } private async void BtnLeftForever_OnClick(object sender, RoutedEventArgs e) { Cts = new CancellationTokenSource(); await Uln2003Driver.TurnAsync(TurnDirection.Left, Cts.Token); } }
三、运行