如果你需要精确控制设备的转动角度,普通电机是做不到的,通常我们会选用步进马达。比如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);
    }
}

三、运行