Introduction: RC Hovercraft + Customized Remote Control


Do you like RC vehicles? Do you want to fly but are afraid of heights? Or do you just like the challenge of controlling a vehicle with low ground friction?... or perhaps you just fancy hovercrafts for some or no particular reason...

If you like hovercrafts and don't mind to get you hands dirty this Instructable might just be for you, so do stick around... and you may end up with something like what can be seen the in video below.


WARNING: Check your sound volume before playing the video. The thrust/propulsion fan can make quite the noise!

Step 1: Materials, Tools & Required Skills Overview

-> Basic Materials:

- Styrofoam board
- Expanded PVC sheet
- Hot glue sticks
- Assorted screws
- Terminal connectors
- Rigid electric insulated copper wire
- Wire coat hanger
- Duct tape
- Sewing line
- Double side tape
- Small cylindrical recipient (ex. empty plastic pickles vat)

- Assorted electronic components (resistors, capacitors, etc.)

-> "Fancy" materials:

- 1x Brushless motor for LIFTING  (I've used the EMAX CF2812)
- 1x Brushless Motor for THRUST (I've used a 64mm Outrunner Ducted 4500KV 320W)
(an hovercraft needs a more powerful motor for thrust than for lifting)
- 2x 30A Electronic Speed Controllers (ESCs) -- the rating of these will depend on how your motors are rated.
- 1x 2S LiPo (nominal 7.4V) rated for 1800mAh or above
- 2x nRF24L01+ radio modules
- 1x propeller (choose according to the lift motor's recommendation)
- 1x micro servo

- 2x atmega328p chips (alternative you can get 2 ready made Arduino Uno boards, or compatible)
- 1x joystick
- 4x momentary push buttons with caps
- 16x2 LCD module

-> Tools:

- Box cutter/ X-acto knife
- Hot glue gun
- Screwdriver
- Pliers

Optional:
- Sewing machine
- Soldering iron
- Protactor

-> Required skills:

- Cutting styrofoam
- Using an hot glue gun
- Cutting wire

- DIY PCB transfer, etching & soldering (or alternatively... buying a couple of Arduino boards or compatible)

Step 2: The Remote Control

For the remote control you can go two routes: the easy way and the hard and messy way!

Going the easy way (and recommended way) you just buy pre-made board modules and assemble them together.

You can get a kit like the FreArduino ATMega328 + Joystick Shield V2.2 which is just a an arduino clone plus a convenient joystick shield that has a joystick, 4 buttons and conviniently even an header for the nRF24L01+ radio module.

- Stack joystick shield on --> *duino
- Plug nRF24l01+ radio module on --> joystick shield
- Upload code to the *duino board chip
- Power the whole assembly and you should be good to go.

Going the hard and messy way will involve:

- Sourcing a greater number of individual parts
- Fabricating the PCB board
- Populating and soldering in the components
- Overall using up more time

WARNING:
If you intend to go this way you should be familiar with both DIY PCB fabrication and Soldering as I will not cover them here. Furthermore the boards' design is amateurish and does not follow any specific design recommendation although having some minor flaws it works just the same.

--> You can find the schematics & board layout files for download HERE & HERE .

- Produce the boards using your favourite DIY method, including transfer, etching and drilling.

Above you can see my progress from a board with the partial components soldered in and the final remote.

Step 3: The Remote Control Code

If you went the hard & messy way and fabricated a PCB board like mine and if all went well then this code below should work like a charm. If you went the easy way with off-the the shelf per-fabricated boards some minor to medium code editing might be required. This step assumes you're familiar with Arduino C programming. Regardless the code has plenty of comments which I believe might be helpful to navigate it.




--- THE REMOTE CONTROLLER CODE ---

// include the library code:
#include <LiquidCrystal.h>
#include <SPI.h>
#include "nRF24L01.h"
#include "RF24.h"



/* ---------------------- MY CUSTOM-MADE 'ENVELOPE' STRUCTS ---------------------- */


// struct to carry messages
typedef struct{
  int X;
  int Y;
  boolean buttons[4];
}
Payload;


// struct to get
// telemetric data
typedef struct{
  int rudderAng;
  int Z;
  int S;

Feedback;


/* ---------------------- BATTERY MONITORING RELATED VARIABLES ---------------------- */


// analog pin reading V_out/V2 from voltage divider
const int battPin = A5;

// default reference on circuit used (5.0, 3.3, 1.1)
const double refV = 5.0;
// How many volts does 1 ADC measure?
const double V_incr = refV/1024.0;

// values of resistances used for voltage divider
const double R1 = 68.4;
const double R2 = 46.6;

// determine voltage divider ratio
const double voltageRatio = (R2/(R1 + R2));



/* --------------------------- BUTTON-RELATED VARIABLES --------------------------- */

// button 'pad' pin
const int buttonPin = A3;

// Button setup schematics

//         Analog pin 5
//            |
//Ground--1K--|--------|--------|-------|
//            |        |        |       |
//           btn1     btn2     btn3    btn4
//            |        |        |       |
//         220 Ohm  390 Ohm  680 Ohm   2.2K
//            |--------|--------|-------|-- +5V

int j = 1;
// 'j' is the integer used in scanning the array designating column number
// these ranges below are dependent on the schematics above and may
// need to be adjusted manually to compensate for various factors
int Button[15][3] = {{1, 836, 840}, // button 1
                     {2, 732, 738}, // button 2
                     {3, 600, 625}, // button 3
                     {4, 310, 335}, // button 4

                     {5, 890, 900}, // button 1 + 2
                     {6, 870, 880}, // button 1 + 3
                     {7, 840, 860}, // button 1 + 4
                     {8, 810, 830}, // button 2 + 3
                     {9, 760, 780}, // button 2 + 4
                     {10, 665, 685} // button 3 + 4                                      
                     };


int label = 0;            // for reporting the button label
int counter = 0;          // how many times we have seen new value
long time = 0;            // the last time the output pin was sampled
int debounce_count = 5;   // number of millis/samples to consider before declaring a debounced input
int current_state = 0;    // the debounced input value

int ButtonVal;

/* ---------------------------------------------------------------------------------------------------- */


// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(8, 7, 5, 4, 3, 2);

// initialize radio with CE, CN pin numbers
RF24 radio(9,10);

const uint64_t pipes[2] = {
  0xF0F0F0F0E1LL, 0xF0F0F0F0D2LL };

// OUT-'pigeon'
Payload package;

// IN-'pigeon'
Feedback telemetrics;


long previousMillis = 0;
long interval = 25;


/* ---------------------------------------------------------------------------------------------------- */

/* SETUP */

void setup() {

  Serial.begin(57600);
  radio.begin();

  radio.openWritingPipe(pipes[0]);
  radio.openReadingPipe(1,pipes[1]);
  radio.startListening();

  pinMode(buttonPin, INPUT);

  // set up the LCD's number of COLUMNS and ROWS:
  lcd.begin(16, 2);

}


/* ------------------------------------------------------------------------ */ 
/* ---------------- WELCOME TO THE LOOP  ------------------ */
/* ------------------------------------------------------------------------ */ 


void loop() {

  // monitor battery voltage on remote
  int val = analogRead(battPin);  
  double battV = VoltageCheck(val);


  package.X = analogRead(A0);
  delay(1);
  package.Y = analogRead(A1);
  delay(1);



  /* ------------------- HANDLE BUTTON PRESSES -------------------------- */ 

  // If we have gone on to the next millisecond
  if (millis() != time)
  {
    // check analog pin for the button value and save it to ButtonVal
    ButtonVal = analogRead(buttonPin);
    delay(1);

    //Serial.println(ButtonVal);    // DEBUG

    if(ButtonVal == current_state && counter >0)
    {
      counter--;
    }
    if(ButtonVal != current_state)
    {
      counter++;
    }
    // If ButtonVal has shown the same value for long enough let's switch it
    if (counter >= debounce_count)
    {
      counter = 0;
      current_state = ButtonVal;     
      //Checks which button or button combo has been pressed
      if (ButtonVal > 100){
      ButtonCheck();}else{

        package.buttons[0] = 0;
        package.buttons[1] = 0;
        package.buttons[2] = 0;
        package.buttons[3] = 0;}
    }
    time = millis();

  }


  /* --------------- RADIO 'TOWER' GROUND CONTROL -------------------- */ 


  if ( radio.available() )
  {


    bool done = false;
    while (!done)
    {
      done = radio.read( &telemetrics , sizeof(telemetrics) ); 
    }


    radio.stopListening();
    bool ok = radio.write( &package, sizeof(package) );
    radio.startListening();

  }



  /* ------------------- HANDLE LCD DISPLAY -------------------------- */

  /* LINE 1 */ 

  lcd.setCursor(0,0);
  // dirty hard-coded hack below 
  if(telemetrics.Z < 100){
    lcd.setCursor(2,0);
    lcd.print(" ");
    lcd.setCursor(0,0);
  }
  lcd.print(telemetrics.Z);

  lcd.setCursor(6,0);
  // dirty hard-coded hack below 
  if(telemetrics.rudderAng < 100){
    lcd.setCursor(8,0);
    lcd.print(" ");
    lcd.setCursor(6,0);
  }
  lcd.print(telemetrics.rudderAng);

  lcd.setCursor(12,0);
  if(telemetrics.S < 100){
   lcd.setCursor(14,0);
   lcd.print(" ");
   lcd.setCursor(12,0);
  } 
  lcd.print(telemetrics.S);


  /* LINE 2 */ 

  lcd.setCursor(0, 1);
  lcd.print("X: ");
  lcd.setCursor(2, 1);
  lcd.print(package.X);

  lcd.setCursor(6, 1);
  lcd.print("Y: ");
  lcd.setCursor(8, 1);
  lcd.print(package.Y);

  lcd.setCursor(12,1);
  lcd.print(battV);

  /* --------------------------------------------------------------------------- */


}   // close loop()



/* ------------------------------ HELPING FUNCTIONS ------------------------------ */


// read that battery voltage
double VoltageCheck(int v){

  double pinV = v * V_incr;
  double volts_in = pinV * 1/voltageRatio;

  return volts_in;
}


// checks which button (or combo of 2) has been pressed, if any...
void ButtonCheck()
{
  // loop for scanning the button array.
  for(int i = 0; i <= 10; i++)
  {
    // checks the ButtonVal against the high and low vales in the array
    if(ButtonVal >= Button[i][j] && ButtonVal <= Button[i][j+1])
    {
      // stores the button number to a variable
      label = Button[i][0];

      switch (label) {
          case 1:
            package.buttons[0] = 1;
            package.buttons[1] = 0;
            package.buttons[2] = 0;
            package.buttons[3] = 0;
            //Serial.println("button 1");           
            break;
          case 2:
            package.buttons[0] = 0;
            package.buttons[1] = 1;
            package.buttons[2] = 0;
            package.buttons[3] = 0;
            //Serial.println("button 2");           
            break;
          case 3:
            package.buttons[0] = 0;
            package.buttons[1] = 0;
            package.buttons[2] = 1;
            package.buttons[3] = 0;
            //Serial.println("button 3");           
            break;
          case 4:
            package.buttons[0] = 0;
            package.buttons[1] = 0;
            package.buttons[2] = 0;
            package.buttons[3] = 1;
            //Serial.println("button 4");           
            break;
          case 5: 
            //Serial.println("button 1 + 2");
            package.buttons[0] = 1;
            package.buttons[1] = 1;
            package.buttons[2] = 0;
            package.buttons[3] = 0;
            break;
          case 6:
            //Serial.println("button 1 + 3");
            package.buttons[0] = 1;
            package.buttons[1] = 0;
            package.buttons[2] = 1;
            package.buttons[3] = 0;
            break;
          case 7:
            //Serial.println("button 1 + 4");
            package.buttons[0] = 1;
            package.buttons[1] = 0;
            package.buttons[2] = 0;
            package.buttons[3] = 1;
            break;
          case 8:
            //Serial.println("button 2 + 3");
            package.buttons[0] = 0;
            package.buttons[1] = 1;
            package.buttons[2] = 1;
            package.buttons[3] = 0;
            break;
          case 9:
            //Serial.println("button 2 + 4"); 
            package.buttons[0] = 0;
            package.buttons[1] = 1;
            package.buttons[2] = 0;
            package.buttons[3] = 1;
            break;
          case 10:
            //Serial.println("button 3 + 4");
            package.buttons[0] = 0;
            package.buttons[1] = 0;
            package.buttons[2] = 1;
            package.buttons[3] = 1;
            break;
      } // close switch     

    }
  }
}

Step 4: Shape Up Your Hovercraft Frame

- Get your styrofoam board and cut it into your favourite size and shape.

WARNINGS:

- When choosing board size you should do it according to your motors power.
- Also the lift motor will define the size of propeller to use. You can use your propeller, a screw drive and a magic marker to mark the circumference where the central lift duct will be.
- Despite the hole here being shown near the nose of the hovercraft, for balance reasons it may be easier to choose a more central position for it.
- Also a bullet shaped hovercraft like mine will require more sewing skills if you plan to fit it with a fancy skirt (the actual name of the hovercraft's inflatable bag -- skirt, not fancy).

- You'll be wanting airflow in your hovercraft's skirt, so I took advantage that my styrofoam board was ridged and faced two same shape pieces ridges against ridges in order to make up channels for airflow coming from the lift motor duct.
- I used double side tape to held both styrofoam board pieces held together.

Step 5: Thrust Motor Fan Mount

Well you'll be wanting to make the hovercraft go forward and even have some control over the direction it turns. So this is where the Thrust duct fan mount come in.

- Build around the Electric Ducted Fan (EDF) unit a block  that secure it.
- I've used an empty plastic nearly cylindrical vat to encase the EDF first.
- This made it easier to secure it to a block of styrofoam.
- At the mouth of the plastic vat I've attached 2 rudders (cut out of a plastic container) attached via 2 lengths of hard wire
- Also the rudders are connected to each other via another length of wire.

Refer to the pictures for better understanding of the assembly.

Step 6: The Receiver

The receiver that I've done is basically a custom *duino board with the convenience of having an ON/OFF switch and an header for the nRF24L01+ radio module.

So, I advise getting an Arduino (or compatible board) and a just connect the other nRF24l01+ radio module to it's respective needed pins.

MOSI - Digital pin 11
MISO - Digital pin 12
SCK - Digital pin 13

Aditionally you'll need to connect pins CE and CSN (from the radio module) to some free pins of your choosing on the Arduino (or compatible) board. The default in the code that will follow are pins 9 and 10 respectively.

Since the radio module needs power you'll also need to connect the GND and 3.3V volts pins.

WARNING: IT'S VERY IMPORTANT THAT YOU POWER THE RADIO MODULE WITH 3.3V OTHERWISE YOU'LL TOAST IT.


You should upload a modified version of the code below to the receiver board. You'll only need to modify pin assignments if you used different pins, and more importantly you'll probably will need to modify the ESC arming code. If you haven't work with Arduino + ESC before I heavily recommend you test them standalone, arming procedures vary from ESC to ESC. The code below works well for two Mystery brand ESCs rated for 30A. For other ESCs you might have to investigate to some length on how to properly arm them. 




#include <Servo.h>
#include <SPI.h>
#include "nRF24L01.h"
#include "RF24.h"


/* ------------- TRAVELLING DATA STRUCTS -------------- */

// commands goes inside
typedef struct{
  int X;
  int Y;
  boolean buttons[4];
}
Payload;

// feedback goes inside
typedef struct{
  int rudderAng;
  int Z;
  int S;

Feedback;

/* -------------- DEBOUNCING VARIABLES ------------------------- */

long previousMillis = 0;        // timer for servo
long previousButtonMillis = 0;  // timer for buttons
//long previousTX = 0;            // timer for transmissions
//long TXinterval = 25;           // interval to transmit telemetry
const int interval = 30;        // interval to update servo
const int binterval = 70;       // interval for debouncing
const int TX_timeout = 100;     // remote - craft  link timeout

/* ------------------------------------------------------------- */


Servo thrust;
Servo lift;

Payload package;
Feedback telemetrics;

Servo rudder;

int oldAngle, newAngle = 0;     // servo positions

int lift_ang;                   // lift motor pseudo-'angular' velocity

int vel;                        // thrust pseudo-'angular' velocity

boolean run = false;

RF24 radio(9, 10);

const uint64_t pipes[2] = {
  0xF0F0F0F0E1LL, 0xF0F0F0F0D2LL };


/* ----------------------------------------------------- */


/* SET UP THE RECEIVER NODE */

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

  radio.begin();
  radio.openWritingPipe(pipes[1]);
  radio.openReadingPipe(1,pipes[0]);
  radio.startListening();


  rudder.attach(7);

  // It is important to arm
  // EACH ESC sequentially
  lift.attach(5);  
  armESC(lift);
  lift_ang = 50;

  delay(100);

  thrust.attach(6);
  armESC(thrust);
  vel = 50;
}

void loop(void)
{

  unsigned long started_waiting_at = millis();
  boolean timeout = false;
  while ( ! radio.available() && ! timeout )
    if (millis() - started_waiting_at > TX_timeout )
      timeout = true;

  if ( timeout ){
    //Serial.println("Failed, response timed out.");
    run = false;   
  }
  else{
    radio.read( &package, sizeof(package) );
    run = true;
  }



  // if TX-RX link is alive () then...  
  if(run)
  {

    /* ------------ RUN RECEIVED COMMANDS ---------------- */

    int X = package.X;
    int Y = package.Y;


    // RUDDER CONTROL
    // ------------------------------------------------------------
    if (X > 480 && X < 530 ){
      newAngle = 90;             // 90 = servo in center position
    }
    if (X < 480){
      newAngle = (map(X, 480, 155, 91, 150));
    }
    if (X > 530){
      newAngle = (map(X, 530, 815, 89, 30));
    }

    // FORWARD THRUST CONTROL... VROOOOMMMMMMM
    // ---------------------------------------------------------------------
    if (Y > 550 && Y < 635 ){
      thrust.write(50);           // neutral position -- motor not spinning
      telemetrics.S = 50;
    } 
    if (Y >= 635){
      vel = (map(Y, 635, 925, 60, 90));
      thrust.write(vel);
      telemetrics.S = vel;
    }

    unsigned long currentMillis = millis();

    // Issue rudder repositioning command only if desired
    // position changes and (debouncing) interval is over
    if(oldAngle != newAngle && currentMillis - previousMillis > interval) {     
      previousMillis = currentMillis;
      oldAngle = newAngle;
      rudder.write(newAngle);     // tell servo to go to position in variable 'pos'
    } // close if millis-land (for rudder servo)


   // HANDLE BUTTON PRESSES
   // ------------------------------------------------------------------
   currentMillis = millis();

   if (currentMillis - previousButtonMillis > binterval) {

      // button press for instant MIN (none) lift speed (NO throtle)
      if (package.buttons[0] == 1){
        lift_ang = 50;
      } 
      // button press for lift speed INCREMENT  
      if (package.buttons[1] == 1){
        if( lift_ang < 120 ){ lift_ang += 5; }     
      }
      // button press for instant (sort of) "stationary safe" hovering      
      if (package.buttons[2] == 1){  
        lift_ang = 90;   
      }   
      // button press for lift speed DECREMENT       
      if (package.buttons[3] == 1){
        if( lift_ang > 50 && lift_ang <= 115 ){ lift_ang -= 5; }  
      } 

      previousButtonMillis = currentMillis;

      lift.write(lift_ang);

   } // close if "millis-land" (secundary test) -- button millis debounce


    /* ---------------------------------------------------------------------- */

    // Set telemetry data to send
    telemetrics.Z = lift_ang;
    telemetrics.rudderAng = newAngle;


  } // close "if(run)" statement 
  else{
    //Serial.println("No radio available");
    emergencyStop();
  }

  // TO transmit TELEMETRICS use this:

  radio.stopListening();
  bool ok = radio.write( &telemetrics, sizeof(telemetrics) );
  radio.startListening();


  // OR this: (need to uncomment variables previousTX & TXinterval
  //           if using this option)

//  unsigned long currentMillis = millis();
//
//  if(currentMillis - previousTX > TXinterval) {
//    previousTX = currentMillis; 
//
//    radio.stopListening();
//    bool ok = radio.write( &telemetrics, sizeof(telemetrics) );
//    radio.startListening();
//  }



} // close the "loop()" block



// Accessory functions

void emergencyStop()
{
  thrust.write(50);
  lift.write(50);
}

void armESC(Servo esc)
{
  esc.write(10);
  delay(2000);
  esc.write(50);
  delay(1000);
}

Step 7: Top Assemby & Cable Routing

- Secure the lift motor on a expanded PVC strip
- Center the lift motor on the lift duct fan and mounted facing downwards (secure it with screws)
- Attach the motor propeller (not shown)
- Secure the thrust motor mount on the hovercraft's rear
- Connect each respective ESC to each motor
- Use terminal junctions to aggregate power leads together

- Mount the micro servo on the back of the craft and connect its horn to the rudders via a length of hard wire as shown in the pictures

- Assure that ALL your sub-circuits have a COMMON GROUND (connect all the ground wires)
- Route the servo's and ESC's respective signal leads to the respective chosen pins on the Arduino board.

Step 8: The Skirt

For the skirt you can either go simple and lo-fi or go all-seamstress on it. I went both, sequentially and in that order.

At a first instance, I just used a garbage plastic bag to make an impromptu skirt. It's simple to do and it works great indoors, but for outdoors I would expect it to be rip to shreds in no time.


The second method involves a bit of engineering and seamtress-ing... you need to calculate/design individual segments of a skirt, so the more angular and the less angles a hovercraft base board has, the better. The easiest shape to design and sew a skirt for would be a rectangle. That way you could make the the skirt out of 4 segments joining at 90º angles.

The a good material to use for a more robust outside hovercraft skirt would be "ripstop nylon". An alternative to this would be just to replace the plastic bags as they get ripped to shreds. They are easily obtainable, it's easy to make a skirt out of them.



So, after your hover is properly dressed ;-) It's just time to go out and have some fun. :-)

Arduino Contest

Participated in the
Arduino Contest