Радиоуправляемый танк. Часть 2 – Расширенный функционал

После того как я построил танк, а вернее его прототип, сразу же появились идеи по его улучшению. Вот небольшой список:

  1. Убрать избыточное оборудование из танка.
  2. Припаять все на макетную плату.
  3. Изменить алгоритм и добавить возможность осуществлять поворот разными способами.
  4. Добавить освещение.
  5. Добавить возможность изменять скорость.

Чтобы несколько раз не писать скетчи для пульта и танка, я напишу оба скетча в конце статьи со всеми добавленными функциями.

Убираем избыточное оборудование.

Танк в первой версии имел некоторое оборудование, которое можно убрать. Например, радио модуль подключен через адаптер питания. Но т.к. все равно пришлось припаивать конденсатор – можно убрать адаптер питания. Убираем адаптер, и пин VCC на радио модуле соединяем с пином 3v3 ардуино, остальное подключаем по схеме.

Так же, можно поставить менее мощный драйвер двигателя, тот который установлен сейчас можно использовать для более мощных моторов. Так же он большой, чтобы разместить его на печатной плате. Будем устанавливать драйвер DRV8833. В общем, можно просто поменять один драйвер на другой, но т.к. есть план добавить возможность регулировать скорость – необходимо изменить соединение драйвера с ардуино. Это будет описано в другой части статьи.

Добавляем режимы поворота

Сейчас танк может поворачивать только одним образом: для поворота налево – начинаем вращать правую гусеницу вперед, для поворота направо – левую вперед. Но реализовать поворот можно по-разному. Например, поворот на месте: при повороте налево – вращаем правую гусеницу вперед, а левую назад. Так же можно повернуть налево, двигая левую гусеницу назад, а правую оставить неподвижной. Хотелось бы научить танк поворачивать всеми этими способами.

Я решил сделать следующий алгоритм: при нажатии на стик джойстика – будет последовательно меняться режим поворота (1-2-3-1-2-3-1… и т.д.).

При включении будет устанавливаться тот режим, который был по умолчанию:

  • Поворот влево – вращаем правую гусеницу вперед, левая стоит на месте.
  • Поворот вправо – вращаем левую гусеницу вперед, правая стоит на месте.

Второй режим:

  • Поворот влево – вращаем левую гусеницу назад, правая стоит на месте.
  • Поворот вправо – вращаем правую гусеницу назад, левая стоит на месте.

Третий режим (поворот на месте):

  • Поворот влево – вращаем правую гусеницу вперед, левую назад.
  • Поворот вправо – вращаем левую гусеницу вперед, правую назад.

Далее, при нажатии на стик джойстика – устанавливается первый режим и так по кругу.

Для реализации этого алгоритма, нужно изменить только скетч пульта. Т.к. только на пульте мы определяем в какую сторону нужно вращать ту или иную гусеницу. Итоговые скетчи для танка и для пульта будут далее.

Включаем свет

Тут все довольно просто. На пульте будем использовать кнопку SET (можно любую другую). Для этого нужно дописать еще несколько строчек в скетч пульта (схема при этом остается та же самая). Состояние диода будем отправлять в отдельном элементе массива values (0 – выключен, 1 – включен).

В танке необходимо подключить диод к пину D2. И так же модифицировать скетч (будет приведен в конце). Добавляется еще один элемент массива values, который будет отвечать за состояние диода.

Думаем, как регулировать скорость

Когда я испытывал предыдущую версию танка, я заметил, что при езде вперед – он иногда косит в одну из сторон. Думаю, что это может быть связано с качеством изготовления самой платформы и её моторов. Поэтому, я решил сделать не только общую регулировку скорости, но и отдельную регулировку скорости каждой гусеницы. 

Для регулировки скорости нам понадобится 3 потенциометра, которые будет расположены на пульте.

Сначала необходимо понять – как подключается потенциометр. Один из крайних входов потенциометра подключается к 5v пину, другой крайний к GND (Какой именно пин подключить к питанию, а какой к земле – решать вам, это влияет только на то, в какую сторону нужно крутить ручку потенциометра для увеличения/уменьшения значения). Вход посередине подключается к аналоговому входу (A0-A7). Читая значение на аналоговом входе, с помощью функции analogRead(<pin>) – получаем значение сопротивления в диапазоне 0-1024.

Подключение потенциометров

В нашем случае нужно чтобы вращение по часовой стрелке повышало напряжение, против- уменьшало. Поэтому левые контакты (если смотреть на ручку потенциометра) подключаем к 5v, а правые к земле, контакты посередине подключим к аналоговым входам, в нашем случае – это будет A0 (регулирует общую скорость), А1(регулирует скорость правой гусеницы), А2(регулирует скорость левой гусеницы).

Танк тоже придется модифицировать. Для регулировки скорости двигателя – нужно подключить управляющие входы драйвера двигателя к пинам, которые могут генерировать ШИМ сигнал. Такими пинами у Arduino nano являются D11, D10, D9, D5 и D3. Но D11, D10 и D9 заняты радио модулем, поэтому пины, подключенные к D9 и D10 можно переключить на любые другие (это нужно будет учесть при написании скетча). Перекинув D9 и D10, у нас как раз появляется 4 пина с ШИМ (D10, D9, D5 и D3), к которым мы и подключим драйвер.

Изменение скетчей для управления скоростью

Так же у нас изменятся скетчи, на пульте мы будем получать показания потенциометров для левой и правой гусеницы, а потом уменьшать эти значения пропорционально значению 3-го (общего) потенциометра. Показания потенциометров изменяются в диапазоне 0 -1024, а ШИМ сигнал имеет диапазон 0-256, поэтому, получив значение с потенциометров управляющих скоростями отдельных гусениц сразу делим их на 4.

Чтобы третьим потенциометром пропорционально уменьшить значение скорости на гусеницах нужно получить множитель в диапазоне от 0,1 до 1, чтобы им умножить значений скоростей гусениц.  Для реализации этого придуман следующий алгоритм:

  1. Снимаем показания с 3-го (общего) потенциометра;
  2. Делим его на 4. Получаем значение в привычном нам диапазоне 0-256;
  3. Переводим получившееся значение в тип double;
  4. Делим его на 256;

В итоге мы получаем множитель от 0,1 до 1 для значений скорости отдельных гусениц.  Значения скоростей гусениц умножаем на полученный множитель и записываем в отдельные элементы массива values.

В танке, в свою очередь, мы будем подавать это значение на нужные управляющие пины драйвера с помощью функции analogWrite(<скорость>). 

Массив values теперь у нас будет иметь 5 элемента (с учетом света и двух значений скорости).

Уже после постройки, я нашел удобный модуль, состоящий из 3- потенциометров.

Отлавливаем нажатия на кнопки

Для включения света и изменения режима поворота нужно нажимать назначенные для этого кнопки. Но в нашем распоряжении только кнопки без фиксации, то есть пока кнопка нажата – мы отлавливаем это положение, если отпустили – то, соответственно, и управляющий сигнал перестает поступать на пин.

Для света можно было приделать дополнительный переключатель с фиксацией, но хотелось обойтись тем, что есть. Проблема заключается в том, что даже за самое кратковременное нажатие проходит несколько циклов работы контроллера и однозначно отследить однократное нажатие нельзя. Поэтому нужно реализовать небольшой алгоритм, который будет отлавливать кратковременное нажатие пользователя на кнопку и делать необходимые действия.

Для реализации алгоритма нужно две переменные:

  • Флаг нажатия клавиши (boolean) изначально false
  • Таймер нажатия (unsigned int) изначально 0

Алгоритм примерно следующий:

  1. Проверяем нажата ли кнопка
  2. Если кнопка нажата и флаг нажатия установлен в false –переходим в алгоритм «в кнопке»
  3. Устанавливаем флаг нажатия в состояние истина
  4. Выполняем все необходимые нам действия (включаем свет, либо изменяем режим поворота)
  5. Засекаем таймер функцией millis()

Т.к. у нас теперь флаг = false – обработка алгоритма кнопки не произойдет в следующем цикле.

  • В общем цикле проверяем, прошло ли 200 мс с момента нажатия,
  • если прошло ставим флаг нажатия в false.

Чтобы для каждой кнопки не заводить отдельную переменную, я завел массив таймеров и массив флагов.

Фрагмент алгоритма (для включения света):

 if(buttonTriggers[HEADLIGHTS_BTN] && (millis() - buttonTimers[HEADLIGHTS_BTN] > 200)){
    buttonTriggers[HEADLIGHTS_BTN] = false;
  }

  if(!digitalRead(7) && !buttonTriggers[HEADLIGHTS_BTN]){
    buttonTriggers[HEADLIGHTS_BTN] = true;
    buttonTimers[HEADLIGHTS_BTN] = millis();
    if(values[HEADLIGHTS] == OFF){
      values[HEADLIGHTS] = ON;
    } else {
      values[HEADLIGHTS] = OFF;
    }
    
  }

Можно использовать функцию delay(время), и не использовать флаг нажатия и таймер, но тогда танк перестанет реагировать на что либо, пока выполняется функция delay.

Убираем просадки по напряжению

Электродвигатели крайне неравномерно потребляют электричество. В результате чего я столкнулся с проблемой: периодически, при движении вперед или назад (когда задействованы 2 двигателя сразу) – контроллер отключался и перезагружался. После загрузки контроллера, танк опять возвращается в рабочее состояние. На максимальной скорости перезагрузка происходила сразу после попытки начать движение, поэтому приходилось уменьшать скорость для старта.

Для решения проблемы нужно припаять конденсатор на 10-25 вольт, ёмкостью примерно 680 микрофарад к пинам драйвера двигателя (VCC и GND). Рекомендую начать с емкости 470 и постепенно повышать, пока отключения не пройдут. В моем случае помог конденсатор с емкостью 680 микрофарад.

Итоговая схема пульта:

Схема пульта
Рисунок 1. Схема пульта.
Схема танка
Рисунок 2. Схема танка.

Скетч для пульта:

#include <SPI.h>
#include <RF24.h>
//Элементы массива
#define LEFT_TRUCK 0
#define RIGHT_TRUCK 1
#define HEADLIGHTS 2
#define SPEED_LEFT 3
#define SPEED_RIGHT 4

//Значения элементов массива
#define FORVARD 50
#define BACKWARD 100
#define STOP 0
#define OFF 0
#define ON 1

//Элементы массивов тригеров и таймеров
#define HEADLIGHTS_BTN 1
#define TURN_MODE_BTN 0

//Режимы поворота
#define TURN_FORWARD 0
#define TURN_BACKWARD 1
#define TURN_IN_PLACE 2

//Пины контроллера
#define UP 8
#define DOWN 7
#define LEFT 6
#define RIGHT 5
#define MID 4
#define SET 3
#define RST 2 

RF24 radio(9, 10);

const uint32_t pipe = 111156789;

byte values[5];

unsigned long buttonTimers[2];
bool buttonTriggers[2];
int turnMode;

void setup() {

  //Инициализация радио модуля
  radio.begin();  
  radio.setChannel(0x6f); 
  radio.setDataRate (RF24_1MBPS);
  radio.setPALevel(RF24_PA_HIGH);
  radio.openWritingPipe(pipe); 

  //Инициализация управляющих пинов
  pinMode(UP, INPUT_PULLUP);
  pinMode(DOWN, INPUT_PULLUP);
  pinMode(LEFT, INPUT_PULLUP);
  pinMode(RIGHT, INPUT_PULLUP);
  pinMode(MID, INPUT_PULLUP);
  pinMode(SET, INPUT_PULLUP);
  pinMode(RST, INPUT_PULLUP);

  pinMode(A0, INPUT);
  pinMode(A1, INPUT);
  pinMode(A2, INPUT);

  //Устанавливаем начальные значения массивов
  values[LEFT_TRUCK] = STOP;
  values[RIGHT_TRUCK] = STOP;
  values[HEADLIGHTS] = OFF;

  buttonTriggers[TURN_MODE_BTN] = false;
  buttonTriggers[HEADLIGHTS_BTN] = false;

  turnMode = TURN_FORWARD;
}

void loop() {

  //Управление движением
  values[LEFT_TRUCK] = STOP;
  values[RIGHT_TRUCK] = STOP;
  if(!digitalRead(RIGHT)){
    right();
  }
  if(!digitalRead(LEFT)){
    left();
  }
  if(!digitalRead(DOWN)){
    backward();
  }
  if(!digitalRead(UP)){
    forward();
  }

  //Проверка нажатия кнопки фонаря
  if(buttonTriggers[HEADLIGHTS_BTN] && (millis() - buttonTimers[HEADLIGHTS_BTN] > 200)){
    buttonTriggers[HEADLIGHTS_BTN] = false;
  }

  if(!digitalRead(SET) && !buttonTriggers[HEADLIGHTS_BTN]){
    buttonTriggers[HEADLIGHTS_BTN] = true;
    buttonTimers[HEADLIGHTS_BTN] = millis();
    if(values[HEADLIGHTS] == OFF){
      values[HEADLIGHTS] = ON;
    } else {
      values[HEADLIGHTS] = OFF;
    }
    
  }

  //Проверка нажатия кнопки переключения режима поворотов
  if(buttonTriggers[TURN_MODE_BTN] && (millis() - buttonTimers[TURN_MODE_BTN] > 200)){
    buttonTriggers[TURN_MODE_BTN] = false;
  }

  if(!digitalRead(MID) && !buttonTriggers[TURN_MODE_BTN]){
    buttonTriggers[TURN_MODE_BTN] = true;
    buttonTimers[TURN_MODE_BTN] = millis();

    if(turnMode == TURN_FORWARD){
      turnMode = TURN_BACKWARD;
    } else if(turnMode == TURN_BACKWARD) {
      turnMode = TURN_IN_PLACE;
    } else {
      turnMode = TURN_FORWARD;
    }
    
  }
  //Вычисление скорости каждой гусеницы
  double multiplier =  ((double) analogRead(A0)/4) / 256;
  values[SPEED_LEFT] = analogRead(A2)/4;
  values[SPEED_RIGHT] = analogRead(A1)/4;
  values[SPEED_LEFT] = values[SPEED_LEFT] * multiplier;
  values[SPEED_RIGHT] = values[SPEED_RIGHT] * multiplier;
    

  //Передача массива на танк
  radio.write(&values, sizeof(values));

}

//Функции движения
void forward(){

  values[RIGHT_TRUCK] = FORVARD;
  values[LEFT_TRUCK] = FORVARD;

}

void backward(){
  
  values[RIGHT_TRUCK] = BACKWARD;
  values[LEFT_TRUCK] = BACKWARD;

}

void left(){
  switch(turnMode){
    case TURN_FORWARD:{
      values[RIGHT_TRUCK] = FORVARD;
      break;
    }
    case TURN_BACKWARD:{
      values[LEFT_TRUCK] = BACKWARD;
      break;
    }
    case TURN_IN_PLACE:{
      values[RIGHT_TRUCK] = FORVARD;
      values[LEFT_TRUCK] = BACKWARD;
      break;
    }
  }
}

void right(){
  switch(turnMode){
    case TURN_FORWARD:{
      values[LEFT_TRUCK] = FORVARD;
      break;
    }
    case TURN_BACKWARD:{
      values[RIGHT_TRUCK] = BACKWARD;
      break;
    }
    case TURN_IN_PLACE:{
      values[LEFT_TRUCK] = FORVARD;
      values[RIGHT_TRUCK] = BACKWARD;
      break;
    }
  }
}

Скетч для танка:

#include <SPI.h>
#include <RF24.h>
//Элементы массива
#define LEFT_TRUCK 0
#define RIGHT_TRUCK 1
#define HEADLIGHTS 2
#define SPEED_LEFT 3
#define SPEED_RIGHT 4

//Значения элементов массива
#define FORVARD 50
#define BACKWARD 100
#define STOP 0

//Пины управления
#define PIN_HEADLIGHT 2
#define PIN_LEFT_F 6
#define PIN_LEFT_B 9
#define PIN_RIGHT_F 3
#define PIN_RIGHT_B 5

const uint32_t pipe = 111156789;
RF24 radio(8, 10);
byte values[5];
byte speedLeft;
byte speedRight;

void setup() {

  Serial.begin(9600);
  
  radio.begin(); 
  radio.setChannel(0x6f); 
  radio.setDataRate (RF24_1MBPS);
  radio.setPALevel(RF24_PA_HIGH);
  radio.openReadingPipe (1, pipe);
  radio.startListening();

  pinMode(PIN_LEFT_B, OUTPUT);
  pinMode(PIN_LEFT_F, OUTPUT);
  pinMode(PIN_RIGHT_B, OUTPUT);
  pinMode(PIN_RIGHT_F, OUTPUT);
  pinMode(PIN_HEADLIGHT, OUTPUT);

  values[LEFT_TRUCK] = STOP;
  values[RIGHT_TRUCK] = STOP;
  digitalWrite(PIN_HEADLIGHT, LOW);
  speedLeft = 0;
  speedRight = 0;
  
}

void loop() { 

  if(radio.available()){
    radio.read(&values, sizeof(values));
  }
  speedLeft = values[SPEED_LEFT];
  speedRight = values[SPEED_RIGHT];
  move();
  digitalWrite(PIN_HEADLIGHT, values[HEADLIGHTS]);
}

void move(){

  if (values[LEFT_TRUCK] == FORVARD){
    analogWrite(PIN_LEFT_F, speedLeft);
    analogWrite(PIN_LEFT_B, 0);
  } else if (values[LEFT_TRUCK] == BACKWARD){
    analogWrite(PIN_LEFT_F, 0);
    analogWrite(PIN_LEFT_B, speedLeft);
  } else if (values[LEFT_TRUCK] == STOP){
    analogWrite(PIN_LEFT_F, 0);
    analogWrite(PIN_LEFT_B, 0);
  }

  if (values[RIGHT_TRUCK] == FORVARD){
    analogWrite(PIN_RIGHT_F, speedRight);
    analogWrite(PIN_RIGHT_B, 0);
  } else if (values[RIGHT_TRUCK] == BACKWARD){
    analogWrite(PIN_RIGHT_F, 0);
    analogWrite(PIN_RIGHT_B, speedRight);
  } else if (values[RIGHT_TRUCK] == STOP){
    analogWrite(PIN_RIGHT_F, 0);
    analogWrite(PIN_RIGHT_B, 0);
  }
  
}

После испытаний все работало. Регулировка скорости работала, фара включалась, а измененного драйвера вполне хватало.

Облагораживаем вид танка.

Было решено напаять все элементы танка на небольшую макетную плату пины соединить с помощью покрытой лаком медной проволоки, а также сделать удобный разъем. Был куплен моток медной проволоки, сечением 0.8 мм, провода, разъемы и все необходимое. Проволока загибалась в виде дрожек, и припаивалась к необходимым пинам устройства. Из подручных материалов, была изготовлена стойка для схемы, которая была вклеена в танк.

Далее я просто приведу несколько фотографий результата работы.

Танк без платы
Рисунок 3. Танк без платы управления.
Плата управления
Рисунок 4. Плата управления (лицевая сторона). Конденсатор еще не припаян.
Плата управления танком
Рисунок 5. Плата управления (обратная сторона).
Танк в сборе
Рисунок 6. Танк в работе.

Облагораживаем вид пульта

Пульт я построил на основе нескольких кусочков дерева покрашенных в черный цвет.На нем расположились все необходимые элементы, а также слот под батарейку крона – для питания, штекер для запуска от блока питания, переключатель, позволяющий переключать эти два режима питания. Фотографии ниже.

Пульт в сборе
Рисунок 7. Внешний вид пульта.
Пульт в сборе
Рисунок 8. Оборотная сторона пульта.

Статьи по проекту

Радиоуправляемый танк. Часть 1 – Базовый функционал

Радиоуправляемый танк. Часть 3 – FPV камера

3 комментария к “Радиоуправляемый танк. Часть 2 – Расширенный функционал”

Оставьте комментарий