В данной статье мы рассмотрим создание на основе платы Arduino и модуля NRF24L01 пульта дистанционного управления (радиопередатчика, RC controller), который будет работать на частоте 2,4 ГГц. Данный пульт дистанционного управления (ДУ) можно использовать для управления практически любыми устройствами, в которых приемник построен на основе модуля NRF24L01.
Для взаимодействия с данным передатчиком вы можете использовать приемник на Arduino, который работает практически на этой же самой элементной базе, либо же использовать свои собственные совместимые схемы приемников.
Также ранее на нашем сайте мы рассматривали аналогичный пульт дистанционного управления на Arduino, но попроще. Рассматриваемый в данной статье пульт более функциональный.
Обзор проекта
С помощью данного пульта ДУ можно управлять любым проектом на основе платы Arduino по беспроводной сети, сделав лишь небольшие изменения на стороне приемника. Этот передатчик также можно использовать как любой коммерческий радиоуправляемый передатчик для управления радиоуправляемыми игрушками, автомобилями, дронами и т. д. Для этой цели ему просто нужен простой приемник Arduino, который затем генерирует соответствующие сигналы для управления этими устройствами.
Радиосвязь этого контроллера основана на модуле приемопередатчика NRF24L01, который при использовании с усиленной антенной может иметь стабильную дальность действия до 700 метров на открытом пространстве. Он имеет 14 каналов, 6 из которых являются аналоговыми входами, а 8 - цифровыми входами.
Он имеет два джойстика, два потенциометра, два тумблера, шесть кнопок и, кроме того, внутренний измерительный блок, состоящий из акселерометра и гироскопа, который также можно использовать для управления объектами, просто перемещая или наклоняя контроллер.
Принципиальная схема радиопередатчика на Arduino
Для начала давайте взглянем на принципиальную схему. «Мозгом» этого радиоуправляемого контроллера является плата Arduino Pro Mini, который питается от двух LiPo-батареек напряжением около 7,4 В. Мы можем подключить их непосредственно к контакту RAW Pro Mini, который имеет регулятор напряжения, снижающий напряжение до 5 В. Обратите внимание, что существует две версии Arduino Pro Mini: одна у меня работает с напряжением 5 В, а другая — с напряжением 3,3 В.
С другой стороны, модулю NRF24L01 строго требуется напряжение 3,3 В, и рекомендуется получать его из специального источника. Поэтому нам нужно использовать стабилизатор напряжения 3,3 В, который подключается к батареям и преобразует 7,4 В в 3,3 В. Также нам необходимо использовать развязывающий конденсатор рядом с модулем, чтобы поддерживать более стабильное напряжение и, таким образом, радиосвязь также будет более стабильной. Модуль NRF24L01 обменивается данными с Arduino по протоколу SPI, а модуль акселерометра и гироскопа MPU6050 использует протокол I2C.
Необходимые компоненты
- Плата Arduino Pro Mini (купить на AliExpress).
- Модуль nRF24L01 + PA + LNA (купить на AliExpress).
- Модуль акселерометра и гироскопа MPU6050 (купить на AliExpress).
- Потенциометр.
- Серводвигатель.
- Тумблер.
- Джойстик.
- Джойстик без коммутационной панели.
- HT7333 (стабилизатор напряжения 3,3в) (купить на AliExpress).
- Регулятор напряжения AMS1117 3,3 В (купить на AliExpress).
Реклама: ООО "АЛИБАБА.КОМ (РУ)" ИНН: 7703380158
Проектирование печатной платы для проекта
В итоге я использовал все аналоговые и цифровые контакты Arduino Pro Mini. Так что теперь, если я попытаюсь соединить все вместе с помощью соединительных проводов, это будет полный беспорядок. Поэтому я разработал специальную печатную плату, используя бесплатное онлайн-программное обеспечение для проектирования схем EasyEDA.
Здесь я учел эргономику контроллера и спроектировал его так, чтобы его можно было легко держать двумя руками, а все элементы управления находились в пределах досягаемости пальцев. Я закруглил края и добавил несколько отверстий диаметром 3 мм, чтобы позже можно было прикрепить печатную плату к чему-нибудь. Я разместил контакты для программирования Arduino Pro Mini на верхней стороне контроллера, чтобы к ним можно было легко получить доступ, если мы захотим перепрограммировать Arduino. Здесь мы также можем заметить, что я использовал контакты RX и TX Arduino для кнопок джойстика. Однако эти две линии необходимо отключить от чего-либо, пока мы загружаем скетч в Arduino. Поэтому они прерываются двумя контактами, которые затем можно легко соединить с помощью простых перемычек.
Обратите внимание: убедитесь, что у вас есть правильная версия Arduino Pro Mini для обработки печатной платы или изменения конструкции печатной платы в соответствии с ней. Вот сравнение фотографий трех разных версий, в зависимости от вашего Arduino и регулятора напряжения.
Вот ссылка на файлы проекта этой печатной платы. Откроется три разные версии на отдельных вкладках, и вы сможете выбрать ту, которая вам нужна.
Итак, закончив проектирование, я создал файл Gerber, необходимый для изготовления печатной платы.
Гербер-файлы для изготовления печатной платы:
Внешний вид изготовленной печатной платы представлен на следующем рисунке. Качество печатной платы отличное, все точно так же, как и в дизайне.
Сборка электронной части пульта ДУ
Хорошо, теперь мы можем перейти к сборке печатной платы. Я начал с пайки разъемов контактов Arduino Pro Mini. Самый простой и хороший способ сделать это — разместить их на макетной плате, чтобы они прочно оставались на месте во время пайки.
Pro Mini также имеет контакты по бокам, но учтите, что расположение этих контактов может различаться в зависимости от производителя.
Для моей конкретной модели мне нужно по 5 контактов для каждой стороны, оставив при этом один контакт GND пустым, поскольку я использовал его область ниже на печатной плате для проведения некоторых трассировок. Я припаял Arduino Pro Mini непосредственно к печатной плате и обрезал разъемы по длине. Рядом с ним находится модуль акселерометра и гироскопа MPU6050.
Затем припаял стабилизатор напряжения 3,3В с конденсатором рядом и еще один конденсатор возле модуля NRF24L01. У этого модуля есть три разные версии, и мы можем использовать здесь любую из них.
Я продолжил работу с контактами для программирования Arduino, контактами RX и TX, контактами источника питания и выключателем питания.
Затем, чтобы припаять потенциометры к печатной плате, мне пришлось удлинить их контакты с помощью разъемов для контактов.
Здесь можно отметить, что я заранее обрезал ручки, чтобы можно было правильно надеть на них колпачки. Однако потенциометры к плате мы припаяем чуть позже.
Затем я вставил и припаял на место два тумблера и два джойстика.
Наконец, осталось припаять четыре кнопки. Однако у них нет подходящей высоты, поэтому я снова использовал разъемы, чтобы немного расширить их контакты.
Вот и все, наша печатная плата готова, и мы можем продолжить изготовление крышки. Поскольку мне нравится, как выглядит печатная плата, и я хочу, чтобы ее было видно, я решил использовать для покрытия прозрачный акрил.
Здесь у меня есть прозрачный акрил толщиной 4 мм, который в настоящее время имеет защитную фольгу и кажется синим. Идея крышки состоит в том, чтобы сделать две пластины по форме печатной платы и закрепить одну из них на верхней стороне, а другую на нижней стороне печатной платы.
Поэтому я отметил форму печатной платы и с помощью ручной пилы по металлу вырезал акрил в соответствии с ней.
Затем с помощью простого рашпиля я доработал форму акрила. Две пластины получились великолепными и идеально сочетаются с печатной платой.
Затем я отметил места, где мне нужно сделать отверстия для прохождения компонентов. С помощью сверла диаметром 3 мм я сначала проделал 4 отверстия для крепления пластин к печатной плате. Для этих отверстий я также сделал зенковки, чтобы болты можно было соединить с пластинами.
Для отверстий под тумблеры и потенциометры я использовал сверло диаметром 6 мм, а для отверстий джойстика — сверло Форстнера диаметром 25 мм. Опять же, используя рашпиль, я подправил все отверстия.
Прежде чем собирать крышку, обратите внимание: на самом деле я припаял контактный разъем блока питания в перевернутом положении, чтобы к нему можно было добраться с задней стороны, где будет расположен аккумулятор.
Хорошо, теперь можно приступить к сборке чехла. Я начал с снятия защитной фольги с акрила, что, должен признаться, меня очень удовлетворило, потому что акрил теперь был таким чистым. Поэтому сначала я закрепил два потенциометра на верхней пластине, вставил крепежные болты диаметром 3 мм и установил дистанционные кольца диаметром 11 мм.
Затем я аккуратно соединил и закрепил верхнюю пластину и печатную плату с помощью болтов. На этом этапе я наконец припаял потенциометры к плате, поскольку раньше я не знал точно, на какой высоте они будут размещены.
Затем на задней панели я прикрепил держатель аккумулятора с помощью 2 болтов. Я закончил сборку крышки, прикрепив заднюю пластину к задней стороне печатной платы с помощью четырех крепежных болтов.
Наконец, мы можем прикрепить линии батареи к контактам источника питания, вставить и закрепить ручки на потенциометрах, вставить ручки джойстиков и прикрепить антенну к модулю NRF24l01. Вот и все, мы наконец закончили сборку RC-передатчика Arduino своими руками.
Теперь осталось запрограммировать плату Arduino. Для программирования платы Pro Mini нам понадобится интерфейс USB-последовательный UART, который можно подключить к разъему для программирования, расположенному на верхней стороне нашего контроллера.
Затем в меню инструментов Arduino IDE нам нужно выбрать плату Arduino Pro или Pro Mini, выбрать подходящую версию процессора, выбрать порт и выбрать метод программирования «USBasp».
Итак, теперь мы можем загружать код в плату Arduino.
Код пульта ДУ на основе платы Arduino
Давайте рассмотрим как работает код нашего передатчика. Итак, сначала нам нужно подключить библиотеку SPI и RF24 для беспроводной связи, а также библиотеку I2C для модуля акселерометра. Затем нам нужно определить цифровые входы, некоторые переменные, необходимые для приведенной ниже программы, создать радиообъект и задать адрес связи.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <SPI.h> #include <nRF24L01.h> #include <RF24.h> #include <Wire.h> // Define the digital inputs #define jB1 1 // Joystick button 1 #define jB2 0 // Joystick button 2 #define t1 7 // Toggle switch 1 #define t2 4 // Toggle switch 1 #define b1 8 // Button 1 #define b2 9 // Button 2 #define b3 2 // Button 3 #define b4 3 // Button 4 const int MPU = 0x68; // MPU6050 I2C address float AccX, AccY, AccZ; float GyroX, GyroY, GyroZ; float accAngleX, accAngleY, gyroAngleX, gyroAngleY; float angleX, angleY; float AccErrorX, AccErrorY, GyroErrorX, GyroErrorY; float elapsedTime, currentTime, previousTime; int c = 0; RF24 radio(5, 6); // nRF24L01 (CE, CSN) const byte address[6] = "00001"; // Address |
Затем нам нужно задать структуру, в которой мы будем хранить 14 входных значений контроллера. Максимальный размер этой структуры может составлять 32 байта, поскольку это ограничение буфера NRF24L01 или объем данных, которые модуль может отправить одновременно.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/ Max size of this struct is 32 bytes - NRF24L01 buffer limit struct Data_Package { byte j1PotX; byte j1PotY; byte j1Button; byte j2PotX; byte j2PotY; byte j2Button; byte pot1; byte pot2; byte tSwitch1; byte tSwitch2; byte button1; byte button2; byte button3; byte button4; }; Data_Package data; //Create a variable with the above structure |
В разделе настройки нам необходимо инициализировать модуль MPU6050, а также мы можем рассчитать ошибку IMU, которая представляет собой значения, которые позже используются при расчете правильных углов модуля.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void initialize_MPU6050() { Wire.begin(); // Initialize comunication Wire.beginTransmission(MPU); // Start communication with MPU6050 // MPU=0x68 Wire.write(0x6B); // Talk to the register 6B Wire.write(0x00); // Make reset - place a 0 into the 6B register Wire.endTransmission(true); //end the transmission // Configure Accelerometer Wire.beginTransmission(MPU); Wire.write(0x1C); //Talk to the ACCEL_CONFIG register Wire.write(0x10); //Set the register bits as 00010000 (+/- 8g full scale range) Wire.endTransmission(true); // Configure Gyro Wire.beginTransmission(MPU); Wire.write(0x1B); // Talk to the GYRO_CONFIG register (1B hex) Wire.write(0x10); // Set the register bits as 00010000 (1000dps full scale) Wire.endTransmission(true); } |
Также на нашем сайте вы можете найти более подробную информацию о работе MEMS-акселерометра и гироскопа.
Затем нам нужно инициализировать радиосвязь, активировать внутренние подтягивающие резисторы Arduino для всех цифровых входов и установить начальные значения по умолчанию для всех переменных.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Define the radio communication radio.begin(); radio.openWritingPipe(address); radio.setAutoAck(false); radio.setDataRate(RF24_250KBPS); radio.setPALevel(RF24_PA_LOW); // Activate the Arduino internal pull-up resistors pinMode(jB1, INPUT_PULLUP); pinMode(jB2, INPUT_PULLUP); pinMode(t1, INPUT_PULLUP); pinMode(t2, INPUT_PULLUP); pinMode(b1, INPUT_PULLUP); pinMode(b2, INPUT_PULLUP); pinMode(b3, INPUT_PULLUP); pinMode(b4, INPUT_PULLUP); |
В разделе цикла начните с чтения всех аналоговых входов, сопоставьте их значения от 0 до 1023 с байтовыми значениями от 0 до 255, поскольку мы уже определили переменные в нашей структуре как байты. Каждый ввод сохраняется в конкретной переменной данных из нашей структуры.
1 2 3 4 5 6 7 |
// Read all analog inputs and map them to one Byte value data.j1PotX = map(analogRead(A1), 0, 1023, 0, 255); // Convert the analog read value from 0 to 1023 into a BYTE value from 0 to 255 data.j1PotY = map(analogRead(A0), 0, 1023, 0, 255); data.j2PotX = map(analogRead(A2), 0, 1023, 0, 255); data.j2PotY = map(analogRead(A3), 0, 1023, 0, 255); data.pot1 = map(analogRead(A7), 0, 1023, 0, 255); data.pot2 = map(analogRead(A6), 0, 1023, 0, 255); |
Следует лишь отметить, что, поскольку мы используем подтягивающие резисторы, показания цифровых выводов равны 0 при нажатии кнопок.
1 2 3 4 5 6 7 8 |
// Read all digital inputs data.j1Button = digitalRead(jB1); data.j2Button = digitalRead(jB2); data.tSwitch2 = digitalRead(t2); data.button1 = digitalRead(b1); data.button2 = digitalRead(b2); data.button3 = digitalRead(b3); data.button4 = digitalRead(b4); |
Таким образом, используя функцию radio.write(), мы просто отправляем значения со всех 14 каналов на приемник.
1 2 |
// Send the whole data from the structure to the receiver radio.write(&data, sizeof(Data_Package)); |
В случае, если тумблер 1 включен, то для управления мы используем данные акселерометра и гироскопа.
1 2 3 |
if (digitalRead(t1) == 0) { read_IMU(); // Use MPU6050 instead of Joystick 1 for controling left, right, forward and backward movements } |
Таким образом, вместо значений X и Y джойстика 1 мы используем значения углов, которые мы получаем от IMU, которые мы предварительно конвертируем из значений от -90 до +90 градусов в байтовые значения от 0 до 255 соответственно.
1 2 3 |
// Map the angle values from -90deg to +90 deg into values from 0 to 255, like the values we are getting from the Joystick data.j1PotX = map(angleX, -90, +90, 255, 0); data.j1PotY = map(angleY, -90, +90, 0, 255); |
Полный код для нашего пульта ДУ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
/* DIY Arduino based RC Transmitter by Dejan Nedelkovski, www.HowToMechatronics.com Library: TMRh20/RF24, https://github.com/tmrh20/RF24/ */ #include <SPI.h> #include <nRF24L01.h> #include <RF24.h> #include <Wire.h> // Define the digital inputs #define jB1 1 // Joystick button 1 #define jB2 0 // Joystick button 2 #define t1 7 // Toggle switch 1 #define t2 4 // Toggle switch 1 #define b1 8 // Button 1 #define b2 9 // Button 2 #define b3 2 // Button 3 #define b4 3 // Button 4 const int MPU = 0x68; // MPU6050 I2C address float AccX, AccY, AccZ; float GyroX, GyroY, GyroZ; float accAngleX, accAngleY, gyroAngleX, gyroAngleY; float angleX, angleY; float AccErrorX, AccErrorY, GyroErrorX, GyroErrorY; float elapsedTime, currentTime, previousTime; int c = 0; RF24 radio(5, 6); // nRF24L01 (CE, CSN) const byte address[6] = "00001"; // Address // Max size of this struct is 32 bytes - NRF24L01 buffer limit struct Data_Package { byte j1PotX; byte j1PotY; byte j1Button; byte j2PotX; byte j2PotY; byte j2Button; byte pot1; byte pot2; byte tSwitch1; byte tSwitch2; byte button1; byte button2; byte button3; byte button4; }; Data_Package data; //Create a variable with the above structure void setup() { Serial.begin(9600); // Initialize interface to the MPU6050 initialize_MPU6050(); // Call this function if you need to get the IMU error values for your module //calculate_IMU_error(); // Define the radio communication radio.begin(); radio.openWritingPipe(address); radio.setAutoAck(false); radio.setDataRate(RF24_250KBPS); radio.setPALevel(RF24_PA_LOW); // Activate the Arduino internal pull-up resistors pinMode(jB1, INPUT_PULLUP); pinMode(jB2, INPUT_PULLUP); pinMode(t1, INPUT_PULLUP); pinMode(t2, INPUT_PULLUP); pinMode(b1, INPUT_PULLUP); pinMode(b2, INPUT_PULLUP); pinMode(b3, INPUT_PULLUP); pinMode(b4, INPUT_PULLUP); // Set initial default values data.j1PotX = 127; // Values from 0 to 255. When Joystick is in resting position, the value is in the middle, or 127. We actually map the pot value from 0 to 1023 to 0 to 255 because that's one BYTE value data.j1PotY = 127; data.j2PotX = 127; data.j2PotY = 127; data.j1Button = 1; data.j2Button = 1; data.pot1 = 1; data.pot2 = 1; data.tSwitch1 = 1; data.tSwitch2 = 1; data.button1 = 1; data.button2 = 1; data.button3 = 1; data.button4 = 1; } void loop() { // Read all analog inputs and map them to one Byte value data.j1PotX = map(analogRead(A1), 0, 1023, 0, 255); // Convert the analog read value from 0 to 1023 into a BYTE value from 0 to 255 data.j1PotY = map(analogRead(A0), 0, 1023, 0, 255); data.j2PotX = map(analogRead(A2), 0, 1023, 0, 255); data.j2PotY = map(analogRead(A3), 0, 1023, 0, 255); data.pot1 = map(analogRead(A7), 0, 1023, 0, 255); data.pot2 = map(analogRead(A6), 0, 1023, 0, 255); // Read all digital inputs data.j1Button = digitalRead(jB1); data.j2Button = digitalRead(jB2); data.tSwitch2 = digitalRead(t2); data.button1 = digitalRead(b1); data.button2 = digitalRead(b2); data.button3 = digitalRead(b3); data.button4 = digitalRead(b4); // If toggle switch 1 is switched on if (digitalRead(t1) == 0) { read_IMU(); // Use MPU6050 instead of Joystick 1 for controling left, right, forward and backward movements } // Send the whole data from the structure to the receiver radio.write(&data, sizeof(Data_Package)); } void initialize_MPU6050() { Wire.begin(); // Initialize comunication Wire.beginTransmission(MPU); // Start communication with MPU6050 // MPU=0x68 Wire.write(0x6B); // Talk to the register 6B Wire.write(0x00); // Make reset - place a 0 into the 6B register Wire.endTransmission(true); //end the transmission // Configure Accelerometer Wire.beginTransmission(MPU); Wire.write(0x1C); //Talk to the ACCEL_CONFIG register Wire.write(0x10); //Set the register bits as 00010000 (+/- 8g full scale range) Wire.endTransmission(true); // Configure Gyro Wire.beginTransmission(MPU); Wire.write(0x1B); // Talk to the GYRO_CONFIG register (1B hex) Wire.write(0x10); // Set the register bits as 00010000 (1000dps full scale) Wire.endTransmission(true); } void calculate_IMU_error() { // We can call this funtion in the setup section to calculate the accelerometer and gury data error. From here we will get the error values used in the above equations printed on the Serial Monitor. // Note that we should place the IMU flat in order to get the proper values, so that we then can the correct values // Read accelerometer values 200 times while (c < 200) { Wire.beginTransmission(MPU); Wire.write(0x3B); Wire.endTransmission(false); Wire.requestFrom(MPU, 6, true); AccX = (Wire.read() << 8 | Wire.read()) / 4096.0 ; AccY = (Wire.read() << 8 | Wire.read()) / 4096.0 ; AccZ = (Wire.read() << 8 | Wire.read()) / 4096.0 ; // Sum all readings AccErrorX = AccErrorX + ((atan((AccY) / sqrt(pow((AccX), 2) + pow((AccZ), 2))) * 180 / PI)); AccErrorY = AccErrorY + ((atan(-1 * (AccX) / sqrt(pow((AccY), 2) + pow((AccZ), 2))) * 180 / PI)); c++; } //Divide the sum by 200 to get the error value AccErrorX = AccErrorX / 200; AccErrorY = AccErrorY / 200; c = 0; // Read gyro values 200 times while (c < 200) { Wire.beginTransmission(MPU); Wire.write(0x43); Wire.endTransmission(false); Wire.requestFrom(MPU, 4, true); GyroX = Wire.read() << 8 | Wire.read(); GyroY = Wire.read() << 8 | Wire.read(); // Sum all readings GyroErrorX = GyroErrorX + (GyroX / 32.8); GyroErrorY = GyroErrorY + (GyroY / 32.8); c++; } //Divide the sum by 200 to get the error value GyroErrorX = GyroErrorX / 200; GyroErrorY = GyroErrorY / 200; // Print the error values on the Serial Monitor Serial.print("AccErrorX: "); Serial.println(AccErrorX); Serial.print("AccErrorY: "); Serial.println(AccErrorY); Serial.print("GyroErrorX: "); Serial.println(GyroErrorX); Serial.print("GyroErrorY: "); Serial.println(GyroErrorY); } void read_IMU() { // === Read acceleromter data === // Wire.beginTransmission(MPU); Wire.write(0x3B); // Start with register 0x3B (ACCEL_XOUT_H) Wire.endTransmission(false); Wire.requestFrom(MPU, 6, true); // Read 6 registers total, each axis value is stored in 2 registers //For a range of +-8g, we need to divide the raw values by 4096, according to the datasheet AccX = (Wire.read() << 8 | Wire.read()) / 4096.0; // X-axis value AccY = (Wire.read() << 8 | Wire.read()) / 4096.0; // Y-axis value AccZ = (Wire.read() << 8 | Wire.read()) / 4096.0; // Z-axis value // Calculating angle values using accAngleX = (atan(AccY / sqrt(pow(AccX, 2) + pow(AccZ, 2))) * 180 / PI) + 1.15; // AccErrorX ~(-1.15) See the calculate_IMU_error()custom function for more details accAngleY = (atan(-1 * AccX / sqrt(pow(AccY, 2) + pow(AccZ, 2))) * 180 / PI) - 0.52; // AccErrorX ~(0.5) // === Read gyro data === // previousTime = currentTime; // Previous time is stored before the actual time read currentTime = millis(); // Current time actual time read elapsedTime = (currentTime - previousTime) / 1000; // Divide by 1000 to get seconds Wire.beginTransmission(MPU); Wire.write(0x43); // Gyro data first register address 0x43 Wire.endTransmission(false); Wire.requestFrom(MPU, 4, true); // Read 4 registers total, each axis value is stored in 2 registers GyroX = (Wire.read() << 8 | Wire.read()) / 32.8; // For a 1000dps range we have to divide first the raw value by 32.8, according to the datasheet GyroY = (Wire.read() << 8 | Wire.read()) / 32.8; GyroX = GyroX + 1.85; //// GyroErrorX ~(-1.85) GyroY = GyroY - 0.15; // GyroErrorY ~(0.15) // Currently the raw values are in degrees per seconds, deg/s, so we need to multiply by sendonds (s) to get the angle in degrees gyroAngleX = GyroX * elapsedTime; gyroAngleY = GyroY * elapsedTime; // Complementary filter - combine acceleromter and gyro angle values angleX = 0.98 * (angleX + gyroAngleX) + 0.02 * accAngleX; angleY = 0.98 * (angleY + gyroAngleY) + 0.02 * accAngleY; // Map the angle values from -90deg to +90 deg into values from 0 to 255, like the values we are getting from the Joystick data.j1PotX = map(angleX, -90, +90, 255, 0); data.j1PotY = map(angleY, -90, +90, 0, 255); } |
Код приемника (получателя)
Теперь давайте посмотрим, как мы можем получить эти данные. Вот простая схема приемника на Arduino и модуле NRF24L01. Конечно, вы можете использовать любую другую плату Arduino.
А вот простой код получателя, в котором мы будем получать данные и просто распечатывать их на последовательном мониторе, чтобы знать, что связь работает правильно. Нам снова нужно подключить библиотеку RF24 и определить объекты и структуру так же, как и в коде передатчика. В разделе настройки при определении радиосвязи нам необходимо использовать те же настройки, что и для передатчика, и установить модуль в качестве приемника с помощью функции radio.startListening().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
/* DIY Arduino based RC Transmitter Project == Receiver Code == by Dejan Nedelkovski, www.HowToMechatronics.com Library: TMRh20/RF24, https://github.com/tmrh20/RF24/ */ #include <SPI.h> #include <nRF24L01.h> #include <RF24.h> RF24 radio(10, 9); // nRF24L01 (CE, CSN) const byte address[6] = "00001"; unsigned long lastReceiveTime = 0; unsigned long currentTime = 0; // Max size of this struct is 32 bytes - NRF24L01 buffer limit struct Data_Package { byte j1PotX; byte j1PotY; byte j1Button; byte j2PotX; byte j2PotY; byte j2Button; byte pot1; byte pot2; byte tSwitch1; byte tSwitch2; byte button1; byte button2; byte button3; byte button4; }; Data_Package data; //Create a variable with the above structure void setup() { Serial.begin(9600); radio.begin(); radio.openReadingPipe(0, address); radio.setAutoAck(false); radio.setDataRate(RF24_250KBPS); radio.setPALevel(RF24_PA_LOW); radio.startListening(); // Set the module as receiver resetData(); } void loop() { // Check whether there is data to be received if (radio.available()) { radio.read(&data, sizeof(Data_Package)); // Read the whole data and store it into the 'data' structure lastReceiveTime = millis(); // At this moment we have received the data } // Check whether we keep receving data, or we have a connection between the two modules currentTime = millis(); if ( currentTime - lastReceiveTime > 1000 ) { // If current time is more then 1 second since we have recived the last data, that means we have lost connection resetData(); // If connection is lost, reset the data. It prevents unwanted behavior, for example if a drone has a throttle up and we lose connection, it can keep flying unless we reset the values } // Print the data in the Serial Monitor Serial.print("j1PotX: "); Serial.print(data.j1PotX); Serial.print("; j1PotY: "); Serial.print(data.j1PotY); Serial.print("; button1: "); Serial.print(data.button1); Serial.print("; j2PotX: "); Serial.println(data.j2PotX); } void resetData() { // Reset the values when there is no radio connection - Set initial default values data.j1PotX = 127; data.j1PotY = 127; data.j2PotX = 127; data.j2PotY = 127; data.j1Button = 1; data.j2Button = 1; data.pot1 = 1; data.pot2 = 1; data.tSwitch1 = 1; data.tSwitch2 = 1; data.button1 = 1; data.button2 = 1; data.button3 = 1; data.button4 = 1; } |
В основном цикле с помощью функции Available() мы проверяем, есть ли входящие данные. Если это правда, мы просто читаем данные и сохраняем их в переменных структуры. Теперь мы можем распечатать данные на последовательном мониторе, чтобы проверить, правильно ли работает передача. Также с помощью функции millis() и оператора if мы проверяем, продолжаем ли мы получать данные или если мы не получаем данные в течение периода более 1 секунды, затем мы сбрасываем переменные к значениям по умолчанию. Мы используем это для предотвращения нежелательного поведения, например, если у дрона есть газ, и мы теряем соединение, он может продолжать улетать, пока мы не сбросим значения.
Итак, это все. Теперь мы можем реализовать этот метод получения данных для любого проекта Arduino.
Управление роботом-муравьем на Arduino с помощью нашего пульта ДУ
Вы можете использовать представленный передатчик (пульт ДУ) для управления роботом-муравьем на Arduino из предыдущей статьи на нашем сайте. Нам просто нужно прочитать данные и в соответствии с ними выполнить соответствующие функции, такие как движение вперед, влево, вправо, укус, атака и так далее.
ESC и сервоуправление с помощью RC-передатчика
Наконец, давайте посмотрим, как можно использовать этот передатчик для управления коммерческими радиоуправляемыми устройствами.
Обычно для этих устройств нам необходимо управлять сервоприводами или бесщеточными двигателями. Поэтому после получения данных от передатчика для управления сервоприводом мы просто используем библиотеку Arduino Servo и используем значения от 0 до 180 градусов. Для управления бесщеточным двигателем с помощью ESC мы снова можем использовать сервобиблиотеку для генерации сигнала ШИМ частотой 50 Гц, используемого для управления ESC. Изменяя рабочий цикл от 1000 до 2000 микросекунд, мы контролируем частоту вращения двигателя от нуля до максимума.
Обратите внимание, что на самом деле мы не можем связать стандартную систему RC-приемника с этой системой NRF24L01 2,4 ГГц. Вместо этого нам нужно модифицировать или создать собственный приемник, состоящий из Arduino и модуля NRF24L01. Отсюда мы можем генерировать соответствующие сигналы PWM (ШИМ) или PPM для управления RC-устройством.
Код программы при этом также претерпит некоторые изменения.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
/* DIY Arduino based RC Transmitter Project == Receiver Code - ESC and Servo Control == by Dejan Nedelkovski, www.HowToMechatronics.com Library: TMRh20/RF24, https://github.com/tmrh20/RF24/ */ #include <SPI.h> #include <nRF24L01.h> #include <RF24.h> #include <Servo.h> RF24 radio(10, 9); // nRF24L01 (CE, CSN) const byte address[6] = "00001"; unsigned long lastReceiveTime = 0; unsigned long currentTime = 0; Servo esc; // create servo object to control the ESC Servo servo1; Servo servo2; int escValue, servo1Value, servo2Value; // Max size of this struct is 32 bytes - NRF24L01 buffer limit struct Data_Package { byte j1PotX; byte j1PotY; byte j1Button; byte j2PotX; byte j2PotY; byte j2Button; byte pot1; byte pot2; byte tSwitch1; byte tSwitch2; byte button1; byte button2; byte button3; byte button4; }; Data_Package data; //Create a variable with the above structure void setup() { Serial.begin(9600); radio.begin(); radio.openReadingPipe(0, address); radio.setAutoAck(false); radio.setDataRate(RF24_250KBPS); radio.setPALevel(RF24_PA_LOW); radio.startListening(); // Set the module as receiver resetData(); esc.attach(9); servo1.attach(3); servo2.attach(4); } void loop() { // Check whether we keep receving data, or we have a connection between the two modules currentTime = millis(); if ( currentTime - lastReceiveTime > 1000 ) { // If current time is more then 1 second since we have recived the last data, that means we have lost connection resetData(); // If connection is lost, reset the data. It prevents unwanted behavior, for example if a drone jas a throttle up, if we lose connection it can keep flying away if we dont reset the function } // Check whether there is data to be received if (radio.available()) { radio.read(&data, sizeof(Data_Package)); // Read the whole data and store it into the 'data' structure lastReceiveTime = millis(); // At this moment we have received the data } // Controlling servos servo1Value = map(data.j2PotX, 0, 255, 0, 180); servo2Value = map(data.j2PotY, 0, 255, 0, 180); servo1.write(servo1Value); servo2.write(servo2Value); // Controlling brushless motor with ESC escValue = map(data.pot1, 0, 255, 1000, 2000); // Map the receiving value form 0 to 255 to 0 1000 to 2000, values used for controlling ESCs esc.writeMicroseconds(escValue); // Send the PWM control singal to the ESC } void resetData() { // Reset the values when there is no radio connection - Set initial default values data.j1PotX = 127; data.j1PotY = 127; data.j2PotX = 127; data.j2PotY = 127; data.j1Button = 1; data.j2Button = 1; data.pot1 = 1; data.pot2 = 1; data.tSwitch1 = 1; data.tSwitch2 = 1; data.button1 = 1; data.button2 = 1; data.button3 = 1; data.button4 = 1; } |