树莓派 WIFI 遥控小车

温馨提示:由于 Win 10 IoT 的不断更新,大家在阅读本文时,请结合官方最新的版本。

WIFI 小车


微软对 Windows 10 IoT Core 经过几次更新后,总算是支持了 WIFI 了,但是目前支持的 WIFI 适配器只有一个,即 RASPBERRY PI USB WIFI DONGLE。之前要是想遥控小车,那必须面对拖着一根长长网线的尴尬,现在可以通过使用指定的适配器来抛弃网线了,但是.......$%#@^&* 为嘛只支持这一款,而且国内还没卖。无奈只能在 SWAP 官网买了,本来一个适配器没大概折合人民币也就70元左右,结果光运费就花了300多.....


英文版请点击

所需材料

先来看一下所需要的硬件(本人从来不打广告,所以如果需要购买地址请单独发邮件问我):

数 量名 称作 用配 图
一个树莓派 2 Model B核心控制模块
一个双 L293D 四路电机直流驱动模块用来驱动4个直流电击转动,同时为树莓派供电 
一个WIFI 适配器用来接受无线信号
一套小车底盘套件(含4个车轮、4个小车专用马达(含碳刷、屏蔽环))小车的零部件
两节18650锂电池(3.7V)小车的动力源

一个

(可选)

三线电压表监测电池剩余电压
一个5V 2A 直流升压 USB 模块直流驱动模块输出的电压达不到为树莓派供电的能力,因此需要通过该模块将电压升高到5V
若干母对母杜邦线、micro usb 转 USB 数据线、导线


电路设计

连接树莓派与电机驱动模块

小车的电路并不复杂,思路也很简单,就是利用树莓派的8个 GPIO 口分别控制直流电机驱动模块的8个输入口。所以只要把这8个口子对应的连接起来就已经成功了一大半了。

树莓派与电机驱动模块的连线示意图


没找到一模一样的驱动模块电路图,就用上下两个 L293D 模块来代替下(原理差不多),每一个模块都可以用来控制2个电机,所以总共可以控制4个电机,对应4个车轮 -- 名副其实的四驱车。从上图中可以看出每2个输入控制一个电机,而这2个输入的高低电平直接影响了车轮是否前进、后退,亦或是停止的三种状态。高低电平直接通过编程控制树莓派进行输出,因此需要使用母对母杜绑线把树莓派和电机驱动模块进行连接。(稍后我会介绍下如何控制这三种状态的输出)


连接电机驱动模块与电机

然后就是把驱动电机的8根输出引线与4个电机(马达)的触点(即,下图中的黄色圈内的金属片)进行连接。

连接电机与 USB 升压模块

树莓派需要 5V 的输入电压,要想让小车自由行驶就不可能直接从小车之外的电源进行供电,正好电机驱动模块本身就提供了 5V 的输出电压。可问题是如何进行连接? 考虑到树莓派的电源输入是通过 Micro USB 作为输入接口,因此很自然的就想到了 USB 转 Micro USB 的方式。

基本思路便是将电机驱动模块的 5V 连接到 USB 接口上,然后再通过 USB 转 Micro USB 线把5V电压输入到树莓派上。在某宝上搜索之后,会发现 USB 模块基本有两种,一种是把高于5V的电压转换成 5V 的降压方式;另一种是把低于 5V 的升压到 5V 并输出。这里我选择了后者,

升压 Usb 模块(摘自某宝)


正极与电机驱动模块的5V输出连接,负极与电机驱动模块的接地端连接。

电机驱动模块侧面(摘自某宝)

主要部件连接完毕后,只要把电源的正负极接入到电机驱动模块的电源输入端就算大功告成了,所有部件连接完成后,便会得到和如下所示类示的成品。


三线电压表为可选项,主要用于连接在 2节 锂电池上,用于观察当前剩余电量,只要通过并联的方式连接在电源的两端即可,此处不再赘述。


L293D 与 PWM

要想控制电机驱动模块正常驱动电机转动,有必要先对这个 L293D 芯片有所了解。

L293D 芯片内部为 2 个 H 桥电路,可用于驱动 2 个电机转动。电路图如下:

L293D 电路图


中间的上半部分(或下半部分)的4个三极管构成了 H 的造型,因此称为 H 桥路,而一个 L293D 由 2 个 H 桥路构成。每一个 H 桥路都接受 3 个输入(IN1,IN2,ENA),用来控制一个电机的转动。其中 IN1 和 IN2 用于控制电机转动的方向,从电路图中可知,当 ENA (使能端)为高电平的时候,IN1 与 IN2 分别输出高电平和低电平,即可驱使电机转动。如果 IN1 和 IN2 进行交换,即 IN1 输出低电平,IN2 输出高电平,则电机反向转动。当 ENA 为低电平或者 IN1 和 IN2 都为低电平时,电机停转。

通过该规律,我们就可以控制电机的先进方向。


PWM(Pulse Width Modulation,脉冲宽度调制)

脉冲宽度调制(PWM)是一种对模拟信号电平进行数字编码的方法,由于计算机不能输出模拟电压,只能输出0 或5V 的的数字电压值,我们就通过使用高分辨率计数器,利用方波的占空比被调制的方法来对一个具体模拟信号的电平进行编码。PWM 信号仍然是数字的,因为在给定的任何时刻,满幅值的直流供电要么是5V(ON),要么是0V(OFF)。电压或电流源是以一种通(ON)或断(OFF)的重复脉冲序列被加到模拟负载上去的。通的时候即是直流供电被加到负载上的时候,断的时候即是供电被断开的时候。只要带宽足够,任何模拟值都可以使用PWM 进行编码。输出的电压值是通过通和断的时间进行计算的。输出电压=(接通时间/脉冲时间)*最大电压值。

--- Windows on Device 项目实践 1 - PWM调光灯制作

因此,利用 PWM 就可以来输出模拟值,从而控制电机转动的速度。具体实现方式,就是将 PWM 信号输入到 L293D 的使能端,从而实现该目的。(由于 Windows 10 IoT 目前未提供 API 来实现 PWM 的输出,因此本小车也暂未支持调速功能)


代码实现

思路:在树莓派上运行接收端用于接收来自网络的数据,而发送端则可随意安装在电脑、平板或手机上。树莓派只要通电之后,接受端就会自动开始运行并一直等待数据。


接收端逻辑

构造一个小车的实体类

//Wheel,轮子只有两个方向:前、后

    public sealed class Wheel
    {
        private static GpioController controller = GpioController.GetDefault();

        private GpioPin _highPin;
        private GpioPin _lowPin;

        public enmWheel Name { get; set; }
        public float Speed { get; set; }

        public Wheel(enmWheel name, int highPin, int lowPin)
        {
            Name = name;

            _highPin = controller.OpenPin(highPin);
            _lowPin = controller.OpenPin(lowPin);

            _highPin.SetDriveMode(GpioPinDriveMode.Output);
            _lowPin.SetDriveMode(GpioPinDriveMode.Output);
        }

        public void Trigger(Direction direction, float speed)
        {
            switch (direction)
            {
                case Direction.Forward:
                    _highPin.Write(GpioPinValue.High);
                    _lowPin.Write(GpioPinValue.Low);
                    break;
                case Direction.Backward:
                    _highPin.Write(GpioPinValue.Low);
                    _lowPin.Write(GpioPinValue.High);
                    break;
                case Direction.None:
                default:
                    _highPin.Write(GpioPinValue.Low);
                    _lowPin.Write(GpioPinValue.Low);
                    break;
            }
        }
    }


//SmartCar

    public sealed class SmartCar
    {
        private GpioPin _PWMPin;
                
        public SmartCar()
        {
            //GpioController controller = GpioController.GetDefault();
            //_PWMPin = controller.OpenPin(6);
            //_PWMPin.SetDriveMode(GpioPinDriveMode.Output);
        }

        //init four wheels
        private static List<Wheel> wheels = new List<Wheel>
        {
            new Wheel(enmWheel.TopLeft, 22, 27),
            new Wheel(enmWheel.TopRight, 12, 13),
            new Wheel(enmWheel.BottomLeft, 24, 23),
            new Wheel(enmWheel.BottomRight, 16, 26)
        };

        public bool FowardBackword(Direction driection)
        {
            foreach (Wheel wheel in wheels)
            {
                wheel.Trigger(driection, 1);
            }

            return true;
        }

        public void Stop()
        {
            wheels.ForEach(wheel => wheel.Trigger(Direction.None, 0));
        }

        public void TurnLeft()
        {
            //TriggerWheel(enmWheel.)
            TriggerWheel(enmWheel.TopLeft, Direction.None, 0);
            TriggerWheel(enmWheel.TopRight, Direction.Forward, 0);
            TriggerWheel(enmWheel.BottomLeft, Direction.None, 0);
            TriggerWheel(enmWheel.BottomRight, Direction.Forward, 0);
        }

        public void TurnRight()
        {
            TriggerWheel(enmWheel.TopLeft, Direction.Forward, 0);
            TriggerWheel(enmWheel.TopRight, Direction.None, 0);
            TriggerWheel(enmWheel.BottomLeft, Direction.Forward, 0);
            TriggerWheel(enmWheel.BottomRight, Direction.None, 0);
        }

        public void TurnBackwardRight()
        {
            TriggerWheel(enmWheel.TopLeft, Direction.Backward, 0);
            TriggerWheel(enmWheel.TopRight, Direction.None, 0);
            TriggerWheel(enmWheel.BottomLeft, Direction.Backward, 0);
            TriggerWheel(enmWheel.BottomRight, Direction.None, 0);
        }

        public void TurnBackwardLeft()
        {
            TriggerWheel(enmWheel.TopLeft, Direction.None, 0);
            TriggerWheel(enmWheel.TopRight, Direction.Backward, 0);
            TriggerWheel(enmWheel.BottomLeft, Direction.None, 0);
            TriggerWheel(enmWheel.BottomRight, Direction.Backward, 0);
        }

        public void TriggerWheel(enmWheel wheel, Direction direction, float speed)
        {
            wheels.First(itm => itm.Name == wheel).Trigger(direction, speed);
        }

        //simulate PWM
        public async void SpeedTest()
        {
            while (true)
            {
                _PWMPin.Write(GpioPinValue.Low);
                await Task.Delay(100);
                _PWMPin.Write(GpioPinValue.High);
                await Task.Delay(100);
            }
        }
    }


定义完这两个类后,接着写调用的逻辑,由于接收端只需要在后台执行数据接收的逻辑就可以了,因此不需要界面。这里创建一个 Background App 项目,然后在 StartupTask 中加入下列代码:接收方采用的是 UDP 的方式。

    public sealed class StartupTask : IBackgroundTask
    {
        public void Run(IBackgroundTaskInstance taskInstance)
        {
            try
            {
                taskInstance.Canceled += TaskInstance_Canceled;
                StartListening();

                //Prevent from exit
                taskInstance.GetDeferral();
            }
            catch (Exception ex)
            {
                Debug.Write(ex.Message);
            }
        }

        private void TaskInstance_Canceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
        {
            Debug.Write(reason);
        }

        public async void StartListening()
        {
            foreach (HostName localHostInfo in NetworkInformation.GetHostNames())
            {
                if (localHostInfo.IPInformation != null)
                {
                    DatagramSocket socket = new DatagramSocket();
                    socket.MessageReceived += Sock_MessageReceived;

                    await socket.BindEndpointAsync(localHostInfo, "8888");
                }
            }
        }

        SmartCar car;
        private void Sock_MessageReceived(DatagramSocket sender, DatagramSocketMessageReceivedEventArgs args)
        {
            using (DataReader reader = args.GetDataReader())
            {
                string value = reader.ReadString(reader.UnconsumedBufferLength);
                switch (value.Trim())
                {
                    case "create":
                        car = new SmartCar();
                        break;
                    case "forward":
                        if (car != null)
                        {
                            car.FowardBackword(Direction.Forward);
                        }
                        break;
                    case "backward":
                        if (car != null)
                        {
                            car.FowardBackword(Direction.Backward);
                        }
                        break;
                    case "turnright":
                        if (car != null)
                        {
                            car.TurnRight();
                        }
                        break;
                    case "turnleft":
                        if (car != null)
                        {
                            car.TurnLeft();
                        }
                        break;
                    case "backright":
                        if (car != null)
                        {
                            car.TurnBackwardRight();
                        }
                        break;
                    case "backleft":
                        if (car != null)
                        {
                            car.TurnBackwardLeft();
                        }
                        break;
                    case "stop":
                        if (car != null)
                        {
                            car.Stop();
                        }
                        break;
                    case "speed":
                        if (car != null)
                        {
                            car.SpeedTest();
                        }
                        break;
                }
            }
        }
    }


这样接收端就算写完了。


发送端逻辑

发送端需要提供一个用户界面,从而便于用户操作,如下图所示:


当点击 Forward 的时候就会前进,Backward 就会后退,依次类推。代码很简单:

public MainPage()
{
    this.InitializeComponent();
    
    //AddHandler 
    btnForward.AddHandler(PointerPressedEvent, new PointerEventHandler(btnForward_PointerPressed), true);
    btnForward.AddHandler(PointerReleasedEvent, new PointerEventHandler(btn_PointerReleased), true);
}

DataWriter writer;
private async void btnConnect_Click(object sender, RoutedEventArgs e)
{
    if (String.IsNullOrWhiteSpace(txtIPAddress.Text))
    {
        return;
    }

    await socket.ConnectAsync(new Windows.Networking.HostName(txtIPAddress.Text.Trim()), "8888");
    writer = new DataWriter(socket.OutputStream);

    Send("create");
}

private void btnForward_PointerPressed(object sender, PointerRoutedEventArgs e)
{
    Send("forward");
} 


private async void Send(string message)
{
    try
    {
        writer.WriteString(message);

        //commit and send the data through the OutputStream
        await writer.StoreAsync();
    }
    catch (Exception ex)
    {
        writer = new DataWriter(socket.OutputStream);
        Debug.Write(ex.Message);
    }
    finally
    {

    }

}


其它方向与之类似,不再重复。


部署到树莓派

部署本来应该是一件相对容易的事,但是 Win10 IoT 目前还不是一个稳定的系统,各方面的资料也比较少,所以花了许久时间才搞定,希望对大家有一定帮助。


把接收端的代码打包成 Appx

右键单击项目,选择 Store -> Create App Packages...


选择 No


选择 ARM,点击 Create


创建成功

在对应的路径下会找到如下几个文件(Dependencies 不一定会有,要看具体项目):

生成的文件


部 署

使用 WebM 的方式连接到 IoT 设备,在左边的菜单中选择 Appx。


在 Install app 部分,把我们刚才生成的几个文件分别添加上去,然后点击 Deploy 下面的 Go 便会开始部署,成功或失败都会在该按钮下显示。


设为启动项

上面那个步骤只是把 Appx 安装在 IoT 设备中了,但是并没有运行。因为 Background App 没有图形界面,无法通过 Run 来运行。此时,需要借助 PowerShell 来进行设置:

首先,连接到 IoT 设备,请参考官网

其次,需要将我们的 Appx 设为启动项,这里主要借助 IoTStartup 这个命令集中。

list 查看当前安装在系统中的 App

IoTStartup list

add 把 app 设置为启动项

请注意,尽管我们创建的是 Background App 项目,在安装到 IoT 设备中后,其实会变成 2 个:一个是 Headed (即有图形界面),另一个是 Headless(即无图形界面)。但通过我实际使用之后,前者除了让你抓狂之外,然并卵,而且会让你百撕不得骑姐,因为当你重启 IoT 设备后,首先运行的是这个假的 Header App,这将导致蓝屏,且再也无法进入系统了,唯一的办法就是重新刷系统。

根据名字应该很容易找到自己的 App,然后通过 add 方式添加为启动项:

IoTStartup add

添加完后会告诉你是否成功。


startup 查看当前的启动项

IoTStartup startup

可以发现我们的 App 已经成功添加到启动项了,接下去要做的便是重启 IoT 设备。


如果一切顺利,重启后,会看到 AppX 菜单中多了一个新的运行项:



运行效果

最后,来看一下运行效果吧:


参考资料

Adafruit的树莓派教程第九课:控制一个直流电机

L293中文资料【完整版】

Windows 10 IoT Core : Setting Startup App


源码

GitHub

文章索引

[隐 藏]

本站采用知识共享署名 3.0 中国大陆许可协议进行许可。 ©2014 Charley Box | 关于本站 | 浙ICP备13014059号