二十四只氦气球,三个螺旋桨,像飞屋环游记里一样乘着气球飞翔!

下面描述技术细节:

技术细节

机械

从机械上来看,可以分为两部分:一部分是小巧的纤维板机架,上面有Arduino Nano单片机板、蓝牙模块、锂电池、电源模块、电机驱动模块、高速电机和螺旋桨;另一部分就是分多组挂在机架顶端的氦气球,共有24个。

氦气提供的升力大约是1.33千克/立方米。氦气球自身的升力与重力比并不高,所以气球充气尽量多可以增大比升力。为了让气球不至于过大,机架的重量就要控制在200g以内。氦气球的升力要接近但略小于机架的重力,这样可以在螺旋桨的升力作用下保持动态平衡,又能无动力自然降落。氦气球扎紧后,氦气会在两三天内逐渐泄漏。为防止泄漏,可以在充气口上涂抹胶水。为了让升力平衡,可以将氦气球分几组扎好,再挂到机架上。

机架是一个Y字形,左右两个角上各有一个朝向前的螺旋桨,底部有一个朝下的螺旋桨。左右两个螺旋桨控制前进、后退以及转向;底部螺旋桨负责垂直平衡与升降。

电路

电路主要有这么几个模块:Arduino Nano单片机板,蓝牙模块,锂电池,升压模块,电机驱动模块,高速电机。

Arduino Nano是Arduino的一个型号,特点是小巧,Mega328芯片。

升压模块是将3.7v锂电池升压到5v供给Arduino Nano用的。

锂电池是3.7V的,500mAh,仅有几十克重。使用过程中发现机器耗电量是十分省的。

蓝牙模块用于和手机通讯,通过手机控制飞行。

两个电机驱动模块驱动三个电机。

比较特别的一点是我们用了6路PWM来控制三个电机。这是因为电机驱动模块比较特别,没有方向控制。当A路为PWM,B路为低电平,则为正转;反过来就是反转了。

程序

程序分两部分,手机端和单片机端。

单片机端的程序是用Arduino的简化编程语言写的。

int motorPin[3][2] = {{ 3, 5 }, { 6, 9 }, { 10, 11 }};

int id;

char input;
String value;
String name;

const int END = 0;
const int NAME = 1;
const int VALUE = 2;

int readProcess = NAME;

void setup() {
  Serial.begin(9600);

  for(int i = 0; i < 3; i++){
    for(int j = 0; j < 2; j++){
      pinMode(motorPin[i][j], OUTPUT);
      digitalWrite(motorPin[i][j], LOW);
    }
  }
}

void loop() {  
  if(Serial.available()){
    input = Serial.read();
    process();
  }
}

void process() {
  if(input==','){
    readProcess = VALUE;
    Serial.println(name);
  }
  else if(input==';'){
    Serial.println(value);
    run(name, value);
    name = "";
    value = "";
    readProcess = NAME;
  }
  else if(readProcess==NAME){
    name+=input;
  }
  else if(readProcess==VALUE){
    value+=input;
  }
}

void run(String name, String value){
  if(name=="id"){
    int num = value.toInt();
    id = num;
  }
  else if(name=="speed"){
    int num = value.toInt();
    if(num>=0){
      analogWrite(motorPin[id][1], num);
      analogWrite(motorPin[id][0], 0);
    }
    else{
      analogWrite(motorPin[id][1], 0);
      analogWrite(motorPin[id][0], -num);
    }
  }
}

程序的作用很单纯,将“speed,128;”之类的指令字符串解释执行,控制电机速度。每条指令都是“指令类型,指令参数;”形式,能够方便的传输多种类型的指令,如电机ID,电机速度,同时易于扩展新的指令。

而手机上的程序就更加简单,通过控制界面发送指令字符串就好了。由于使用了原来已经设计好的库,这个程序本身只有几十行代码而已。

package org.faguzo.nabla;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.SeekBar;
import android.widget.Switch;

import org.faguzo.nabla.element.Motor;
import org.faguzo.nabla.system.Hub;

public class MainActivity extends Activity {

	Button upButton, downButton, foreButton, backButton, leftButton, rightButton;
	SeekBar leftMotorBar, rightMotorBar, bottomMotorBar;
	Switch globalSwitch;
	Hub hub;
	Motor leftMotor, rightMotor, bottomMotor;

	int bottomSpeed = 0, leftSpeed = 0, rightSpeed = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        hub = new Hub("20:13:01:30:18:65");
        leftMotor = new Motor(0, hub);
        rightMotor = new Motor(1, hub);
        bottomMotor = new Motor(2, hub);

        upButton = (Button)findViewById(R.id.button_up);
        downButton = (Button)findViewById(R.id.button_down);
        leftButton = (Button)findViewById(R.id.button_left);
        rightButton = (Button)findViewById(R.id.button_right);
        foreButton = (Button)findViewById(R.id.button_fore);
        backButton = (Button)findViewById(R.id.button_back);

        leftMotorBar = (SeekBar)findViewById(R.id.seekBar_left);
        rightMotorBar = (SeekBar)findViewById(R.id.seekBar_right);
        bottomMotorBar = (SeekBar)findViewById(R.id.seekBar_bottom);

        globalSwitch = (Switch)findViewById(R.id.switch_global);

        upButton.setOnTouchListener(new Button.OnTouchListener(){
			@Override
			public boolean onTouch(View v, MotionEvent event) {
				// TODO Auto-generated method stub
				if(event.getAction()==MotionEvent.ACTION_DOWN){
					bottomSpeed = bottomMotorBar.getProgress()-255;
					bottomMotor.setSpeed(bottomSpeed + 128);
				}
				else if(event.getAction()==MotionEvent.ACTION_UP){
					bottomMotor.setSpeed(bottomSpeed);
				}

				return false;
			}
		});

        downButton.setOnTouchListener(new Button.OnTouchListener(){
			@Override
			public boolean onTouch(View v, MotionEvent event) {
				// TODO Auto-generated method stub
				if(event.getAction()==MotionEvent.ACTION_DOWN){
					bottomSpeed = bottomMotorBar.getProgress()-255;
					bottomMotor.setSpeed(bottomSpeed - 128);
				}
				else if(event.getAction()==MotionEvent.ACTION_UP){
					bottomMotor.setSpeed(bottomSpeed);
				}

				return false;
			}
		});

        leftButton.setOnTouchListener(new Button.OnTouchListener(){
			@Override
			public boolean onTouch(View v, MotionEvent event) {
				// TODO Auto-generated method stub
				if(event.getAction()==MotionEvent.ACTION_DOWN){
					leftSpeed = leftMotorBar.getProgress()-255;
					rightSpeed = rightMotorBar.getProgress()-255;
					leftMotor.setSpeed(leftSpeed-128);
					rightMotor.setSpeed(rightSpeed+128);
				}
				else if(event.getAction()==MotionEvent.ACTION_UP){
					leftMotor.setSpeed(leftSpeed);
					rightMotor.setSpeed(rightSpeed);
				}

				return false;
			}
		});

        rightButton.setOnTouchListener(new Button.OnTouchListener(){
			@Override
			public boolean onTouch(View v, MotionEvent event) {
				// TODO Auto-generated method stub
				if(event.getAction()==MotionEvent.ACTION_DOWN){
					leftSpeed = leftMotorBar.getProgress()-255;
					rightSpeed = rightMotorBar.getProgress()-255;
					leftMotor.setSpeed(leftSpeed+128);
					rightMotor.setSpeed(rightSpeed-128);
				}
				else if(event.getAction()==MotionEvent.ACTION_UP){
					leftMotor.setSpeed(leftSpeed);
					rightMotor.setSpeed(rightSpeed);
				}

				return false;
			}
		});

        foreButton.setOnTouchListener(new Button.OnTouchListener(){
			@Override
			public boolean onTouch(View v, MotionEvent event) {
				// TODO Auto-generated method stub
				if(event.getAction()==MotionEvent.ACTION_DOWN){
					leftSpeed = leftMotorBar.getProgress()-255;
					rightSpeed = rightMotorBar.getProgress()-255;
					leftMotor.setSpeed(leftSpeed+128);
					rightMotor.setSpeed(rightSpeed+128);
				}
				else if(event.getAction()==MotionEvent.ACTION_UP){
					leftMotor.setSpeed(leftSpeed);
					rightMotor.setSpeed(rightSpeed);
				}

				return false;
			}
		});

        backButton.setOnTouchListener(new Button.OnTouchListener(){
			@Override
			public boolean onTouch(View v, MotionEvent event) {
				// TODO Auto-generated method stub
				if(event.getAction()==MotionEvent.ACTION_DOWN){
					leftSpeed = leftMotorBar.getProgress()-255;
					rightSpeed = rightMotorBar.getProgress()-255;
					leftMotor.setSpeed(leftSpeed-128);
					rightMotor.setSpeed(rightSpeed-128);
				}
				else if(event.getAction()==MotionEvent.ACTION_UP){
					leftMotor.setSpeed(leftSpeed);
					rightMotor.setSpeed(rightSpeed);
				}

				return false;
			}
		});

        globalSwitch.setOnTouchListener(new Button.OnTouchListener(){
			@Override
			public boolean onTouch(View v, MotionEvent event) {
				// TODO Auto-generated method stub
				leftMotor.setSpeed(0);
				rightMotor.setSpeed(0);
				bottomMotor.setSpeed(0);

				leftMotorBar.setProgress(255);
				rightMotorBar.setProgress(255);
				bottomMotorBar.setProgress(255);

				return false;
			}
		});
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

}

成果展示


结语

因为飞艇可以控制升力平衡达到悬浮,控制就简单了很多。重要的是如何设计一个简洁稳定的结构,让重量足够轻,重心降低。