Bluetooth · разбор

Bluetooth и Arduino: модуль HC-05 и управление с телефона

Нажали кнопку в телефоне — машинка поехала. Разбираем, что на самом деле делает модуль HC-05, почему с iPhone он не заработает никогда, куда идут четыре провода и зачем на одном из них резисторы. Схемы на странице живые: их можно щёлкать и двигать. Все числа — из даташитов, исходников среды и карточек наших модулей.

Короткий ответ для нетерпеливых. С телефоном на Android модуль HC-05 работает прекрасно. С iPhone — не работает вовсе, и это не лечится ни приложением, ни настройками: в iOS просто нет того профиля Bluetooth, на котором живёт этот модуль. Владельцу iPhone нужен модуль BLE. Подробности — во втором и третьем разделах.

Что такое Bluetooth-модуль и чего он не делает

Проще всего представлять HC-05 как удлинитель для двух проводов. У платы Arduino есть последовательный порт — две линии, по которым она отправляет и принимает байты. Обычно на другом конце этих линий компьютер. Модуль забирает эти же байты и переносит их по воздуху: что плата отправила — то на телефоне появилось, что телефон прислал — то плата прочитала. Больше он не делает ничего.

Из этого следуют три вещи, которые экономят вечер новичку. Первая: никакой особой библиотеки не нужно — работа с модулем это тот же код, что и вывод в монитор порта. Вторая: модуль не даёт плате интернета — Bluetooth и Wi-Fi это разные вещи, и через HC-05 ни сайт открыть, ни данные в облако отправить нельзя. Третья: залить скетч «по воздуху» через него тоже не получится — загрузчику платы нужен сигнал сброса, а этого сигнала в Bluetooth-канале нет.

Последнее стоит пояснить, потому что вопрос задают часто. Когда вы жмёте «Загрузить» при обычном USB-кабеле, плату перезапускает не среда, а переходник: он дёргает линию сброса, и загрузчик успевает поймать программу в свою секундную форточку. По воздуху этой линии нет — модуль переносит только поток данных. Умельцы приспосабливают под сброс вывод STATE с конденсатором, и это иногда работает, но загрузка срывается от любой помехи в эфире: у загрузчика Optiboot это известная и описанная в его багтрекере беда. Так что честный ответ новичку — нет, скетч заливаем кабелем.

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

Главный вопрос: с кем вы собираетесь связываться

Половина неудач случается ещё в магазине. Человек берёт HC-05, потому что он самый дешёвый и самый описанный, а потом выясняется, что телефон у него неподходящий или поделка должна месяц жить от батарейки. Выбор модуля определяется двумя вопросами: кто на другом конце и зачем.

Что брать: зависит от того, с кем связываемся и зачемAndroidесть профильпоследовательногопортаБерите HC-05Управлять платой и видеть показанияМодуль виден в списке Bluetooth, сопрягается по коду1234, а дальше любое приложение-терминал шлёт в негобуквы и цифры.Почему: Android умеет классический профильпоследовательного порта — тот самый, который и есть уHC-05.
Схема живая: выберите, с чем связываете плату и зачем. Ответ для iPhone не меняется ни от приложения, ни от настроек модуля.
МодульКакой BluetoothAndroidiPhoneМожет быть ведущимЗачем берут
HC-05Bluetooth 2.0, классическийДаНетДаУправление с Android и связь двух плат между собой
HC-06Bluetooth 2.0, классическийДаНетНетТо же самое, но только в роли ведомого
BLE-модуль (HM-10 и родня)Bluetooth 4.0, экономичныйДаДаДаЕдинственный путь, если на руках iPhone или питание от батарейки
ESP32 (радио внутри платы)Оба вида сразуДаТолько со скетчем на BLEДаКогда не хочется отдельного модуля и нужен Wi-Fi заодно
nRF24L01+Не Bluetooth вовсеНетНетСвязь двух плат на большом расстоянии, телефон не участвует

Строка про ведущего важнее, чем кажется. Ведомый модуль умеет только ждать, пока к нему подключатся. Ведущий сам ищет напарника и устанавливает связь — поэтому парой модулей HC-05 можно соединить две платы Arduino без всякого телефона, а парой HC-06 нельзя: они оба будут вечно ждать друг друга.

Почему iPhone не увидит HC-05

Это самый частый вопрос по теме и самый неприятный ответ. Дело не в модуле, не в приложении и не в кривой прошивке. Bluetooth — это не один протокол, а набор «профилей»: наушники, гарнитура, передача файлов, клавиатура, последовательный порт. Устройство и телефон должны поддерживать один и тот же профиль, иначе им не о чем разговаривать.

HC-05 умеет ровно один профиль — последовательный порт. Apple публикует список профилей, которые поддерживает iOS: их меньше десятка — гарнитура, наушники, клавиатура, передача контактов и подобное, — и последовательного порта среди них нет. Стороннему приложению система открывает только Bluetooth Low Energy, то есть новое, экономичное поколение Bluetooth. Классический последовательный канал доступен только аксессуарам, прошедшим фирменную программу сертификации Apple, — а туда принимают компании, и аксессуару нужна отдельная микросхема аутентификации на плате.

Поэтому «поискать подходящее приложение в магазине» бессмысленно: приложения работают на том, что им даёт система, а система такого канала не даёт. Ни новое имя модуля, ни смена кода, ни другая прошивка ничего не изменят.

Что делать владельцу iPhone. Брать модуль BLE — например, HM-10 и его родню. Он делает ту же работу: с точки зрения скетча это по-прежнему обычный последовательный порт. Второй путь — плата ESP32, но с оговоркой: у неё есть оба вида Bluetooth, и iPhone увидит её только если писать скетч именно на BLE. Популярная библиотека BluetoothSerial у ESP32 — это классический профиль, тот же самый, который iPhone не поддерживает. И ещё: у новых разновидностей ESP32-S3 и ESP32-C3 классического Bluetooth нет вообще.

Что на плате модуля

Ходовая плата HC-05 — это прямоугольник с синей платкой самого радиомодуля, гребёнкой из шести выводов, маленькой кнопкой и светодиодом. Радиомодуль работает от 3,3 вольта, но на плате-носителе стоит собственный стабилизатор, поэтому питание берут с пятивольтового вывода Arduino.

ВыводЧто этоКуда идёт
STATEСвязь установлена: ноль — нет, единица — естьЛюбой свободный вывод, можно не подключать
RXDПриём модуля: сюда говорит платаВывод передачи платы, только через делитель
TXDПередача модуля: отсюда плата слушаетВывод приёма платы, напрямую
GNDОбщий проводGND
VCCПитание: на плате свой стабилизатор 3,3 В5V
EN (KEY)Режим настройки AT-командами3,3 В или кнопка на самой плате

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

Светодиод на плате — самый честный диагностический прибор в этой теме. По его ритму видно, что происходит внутри, ещё до того, как вы напишете первую строчку кода.

Светодиод модуля: что он говоритBC417модулькнопкасветодиод — справа на платеЖдёт телефонБыстрая частая дробь, несколько вспышек в секундуМодуль включён и здоров, но никто к нему неподключился.Найдите его в списке Bluetooth на телефоне и введитекод. Это нормальное состояние после включения.
Ритм мигания у разных прошивок немного отличается, но картина всегда одна из этих четырёх. Если анимация у вас отключена, светодиод показан горящим.

Подключение: четыре провода, и один — через резисторы

Проводов ровно четыре: питание, земля и две линии данных, перекрещённые между собой. Передача модуля идёт на приём платы, приём модуля — на передачу платы. Земля обязательна: без общего провода два устройства не договорятся о том, что считать нулём.

Единственная тонкость — направление «плата говорит модулю». Arduino выдаёт пять вольт, а вход модуля рассчитан на 1,8–3,6 вольта, и предельным значением в даташите названы 3,7. Пять вольт превышают этот предел почти на полтора, и вход постепенно выходит из строя — обычно тихо, без дыма: модуль просто перестаёт принимать. Лечится двумя резисторами: 1 кОм последовательно и 2 кОм на землю превращают пять вольт в 3,33. В обратную сторону делитель не нужен — пятивольтовая плата считает единицей всё, что выше трёх вольт.

Четыре провода, и один из них — через делительArduino Unoлогика 5 ВHC-05логика 3,3 В5VVCCна плате модуля свой стабилизаторGNDGNDобщий провод обязателенвывод 10TXDнапрямую, делитель не нуженвывод 11RXDтолько через делитель 1 кОм / 2 кОмДелитель на линии «вывод 11 → RXD модуля»1 кОм2 кОм на землю5 В превращаются в 3,33 ВВыводы 0 и 1 заняты связью скомпьютером: модуль на них невешаем, иначе не зальётся скетч.программный порт SoftwareSerial(10, 11)
Переключите плату — схема перестроится. Делитель стоит только на одной линии: там, где пятивольтовая плата говорит трёхвольтовому модулю. Исключение — Trema-модуль: его производитель разрешает подавать на RX напряжение питания, и при питании 5 В резисторы не нужны.

Из этого правила есть одно задокументированное исключение — наш Trema-модуль. В его характеристиках прямо написано, что напряжение на выводах TX и RX не должно превышать напряжение питания модуля, а питание допускается 5 вольт. То есть если вы питаете его пятью вольтами, делитель не нужен. У обычной синей платы такой гарантии в карточке нет, а даташит самой микросхемы говорит про 3,6 вольта — поэтому для неё делитель остаётся обязательным.

Две ловушки при первом подключении. Первая: не вешайте модуль на выводы 0 и 1 у Uno и Nano — по этой же линии в плату заливается программа, и скетч перестанет загружаться. Разбор именно этой ошибки у нас на отдельной странице. Вторая: не питайте модуль от вывода 3.3V. В момент поиска телефона он просит 30–40 миллиампер, а этот вывод рассчитан всего на 50 — модуль будет перезагружаться в самый неподходящий момент.

Первая проверка: мост в монитор порта

Прежде чем писать что-то умное, полезно убедиться, что байты вообще ходят. Этот скетч соединяет монитор порта компьютера с модулем: что напечатали — улетело в воздух, что прилетело — появилось на экране. Он же понадобится позже, при настройке.

// Мост «монитор порта - Bluetooth-модуль».
// Что напечатали в мониторе - улетело в модуль, что пришло по воздуху -
// появилось в мониторе. С этого скетча начинают проверку связи.
// Схема (Arduino Uno): TXD модуля -> вывод 10, RXD модуля -> делитель -> 11,
// VCC модуля -> 5V, GND -> GND.

#include <SoftwareSerial.h>

SoftwareSerial bt(10, 11);     // 10 - приём (RX), 11 - передача (TX)

// Рабочая скорость модуля. В карточках наших модулей указано 38400.
// У модулей из других магазинов часто 9600: если в мониторе идёт мусор -
// поменяйте число здесь и попробуйте 9600, потом 57600.
const long BT_BAUD = 38400;

void setup() {
  Serial.begin(9600);          // окно монитора порта
  bt.begin(BT_BAUD);
  Serial.println("Мост открыт. Печатайте - уйдёт в модуль.");
}

void loop() {
  if (bt.available()) {        // пришло по воздуху - показываем в мониторе
    Serial.write(bt.read());
  }
  if (Serial.available()) {    // напечатали в мониторе - отправляем в модуль
    bt.write(Serial.read());
  }
}

Обратите внимание на строку со скоростью. Модули приезжают с разными заводскими настройками: у массовых плат это обычно 9600, у Trema-модуля в карточке указано 38400, попадаются экземпляры и на 57600. Если в мониторе вместо букв идёт мусор — скорость не та, переберите эти три значения. Точный ответ даст сам модуль, но чуть позже, в разделе про настройку.

Сопряжение с телефоном шаг за шагом

Здесь спотыкается больше людей, чем на пайке. Причина в том, что «сопряжено» и «подключено» — разные состояния, а телефон показывает только первое.

Шаг 1 из 7
От включения до первого байтаТелефонТелефон ещё ничего незнаетМодуль HC-05Светодиод часто мигает:модуль ждётПлата ArduinoПлата уже работает ишлёт байты в пустотувоздухпроводаПитание поданоТелефон: Телефон ещё ничего не знает. Плата: Плата уже работает и шлёт байты в пусто
Пройдите по шагам кнопками. Четвёртый шаг — тот самый, на котором застревает большинство: телефон уже пишет «сопряжено», а плата ещё ничего не получает.

На практике порядок такой. Включаете плату — светодиод модуля часто мигает. В настройках Bluetooth телефона появляется устройство с именем HC-05 или H-C-2010-06-01. Телефон просит код: сначала пробуйте 1234, если не подошёл — 0000. Телефон пишет «сопряжено» — и на этом системная часть заканчивается. Дальше нужно открыть приложение-терминал, выбрать в нём это устройство и нажать «подключиться». Только теперь байты пойдут, светодиод перейдёт на две вспышки с паузой, а вывод STATE поднимется в единицу.

Пара мелочей, которые экономят нервы. Поиск устройств у телефона занимает около десяти секунд, а не мгновение, — список появляется не сразу. Если модуль в списке есть, но приложение его не показывает, почти всегда дело в разрешениях: начиная с двенадцатой версии Android приложению нужно отдельное разрешение «Устройства поблизости», а на версиях постарше — включённая геолокация. И ещё: модуль подключается только к одному устройству за раз, так что второй телефон в семье придётся отсоединить.

Приложений-терминалов для Android много, и все они делают одно и то же: показывают текст, который прислала плата, и отправляют то, что вы напечатали. Выбирать стоит по трём признакам: свежая дата обновления (иначе приложение может просто не встать на новый телефон), настройка окончания строки — она понадобится, когда команды станут длиннее одного символа, — и честный раздел о данных в магазине приложений. Всем трём условиям отвечает, например, Serial Bluetooth Terminal: он бесплатный, не собирает данные и умеет работать и с классическими модулями, и с BLE. Есть и приложения с готовыми пультами вместо текстового окна — с джойстиком и кнопками для машинки; правда, чаще всего они показывают рекламу.

Первая команда: светодиод с телефона

Самый простой протокол — одна цифра, одно действие. Отправили единицу — загорелось, отправили ноль — погасло. Плата отвечает словами, чтобы на экране телефона было видно, что команда дошла.

// Первое управление с телефона: одна цифра - одно действие.
// Отправьте с телефона 1 - светодиод загорится, 0 - погаснет.
// Модуль отвечает словами, чтобы на телефоне было видно, что команда дошла.

#include <SoftwareSerial.h>

SoftwareSerial bt(10, 11);
const long BT_BAUD = 38400;
const uint8_t PIN_LED = 13;    // светодиод на самой плате

void setup() {
  pinMode(PIN_LED, OUTPUT);
  bt.begin(BT_BAUD);
  bt.println("Плата на связи. 1 - включить, 0 - выключить.");
}

void loop() {
  if (!bt.available()) {
    return;
  }

  char command = bt.read();
  if (command == '1') {
    digitalWrite(PIN_LED, HIGH);
    bt.println("свет включён");
  } else if (command == '0') {
    digitalWrite(PIN_LED, LOW);
    bt.println("свет выключен");
  }
  // Всё остальное - в том числе перевод строки от телефона - пропускаем молча.
}

Если всё собрано верно, это работает с первого раза. Если нет — почти наверняка дело в одной из трёх вещей: не та скорость, не перекрещены провода данных или приложение не подключилось к устройству, а только сопряглось.

Команды длиннее одного символа

Как только вы захотите передавать не «включи», а «поставь яркость 120», простая схема ломается. Байты приходят по одному, а главный цикл программы успевает прокрутиться сотни раз между ними — поэтому проверка «а не букву ли L мы получили» срабатывает на первом символе, а число уходит в никуда.

Пришло байтов: 0 из 5
Телефон отправил L120 и перевод строкиL120\nбайты летят по одному, а loop() крутится в сотни раз чащеchar c = bt.read();if (c == 'L') { ... } — сравнили один символ и забыли про остальныеЧисло 120 состоит из трёх отдельных байтов, и каждый приходит сам по себе.Пока ничего не пришло
Нажимайте «Следующий байт» и сравните два способа. Первый — самая частая ошибка новичка: команда из нескольких символов рассыпается по дороге.

Правильный приём — копить символы в буфер, пока не придёт конец строки, и только потом разбирать команду целиком. В приложении-терминале для этого включают отправку перевода строки. Заодно стоит выбрасывать огрызок, если связь оборвалась на середине: иначе следующая команда склеится с ним в бессмыслицу.

// Команды из нескольких символов: буква и число.
// L120 - яркость 120, L0 - погасить, S - спросить состояние.
// Главное правило: по одному байту команду разбирать нельзя, её собирают
// в буфер до перевода строки. В приложении-терминале включите отправку \n.

#include <SoftwareSerial.h>

SoftwareSerial bt(10, 11);
const long BT_BAUD = 38400;
const uint8_t PIN_LED = 9;     // светодиод через резистор на ШИМ-выводе

char line[24];                 // буфер одной команды
uint8_t filled = 0;            // сколько символов уже собрано
unsigned long lastByte = 0;    // когда пришёл последний символ
int brightness = 0;            // текущая яркость, 0...255

void setup() {
  pinMode(PIN_LED, OUTPUT);
  analogWrite(PIN_LED, brightness);
  bt.begin(BT_BAUD);
  bt.println("Готов. L120 - яркость, L0 - погасить, S - состояние.");
}

void loop() {
  while (bt.available()) {
    char symbol = bt.read();
    lastByte = millis();

    if (symbol == '\r') {
      continue;                // возврат каретки не нужен
    }
    if (symbol == '\n') {     // строка кончилась - команда собрана
      line[filled] = '\0';
      if (filled > 0) {
        run(line);
      }
      filled = 0;
      continue;
    }
    if (filled < sizeof(line) - 1) {
      line[filled] = symbol;
      filled++;
    } else {
      filled = 0;              // слишком длинная строка - выбрасываем целиком
      bt.println("команда слишком длинная");
    }
  }

  // Связь пропала на середине строки - выбрасываем огрызок, иначе
  // следующая команда склеится с ним в одну бессмыслицу.
  if (filled > 0 && millis() - lastByte > 200) {
    filled = 0;
  }
}

void run(const char *command) {
  if (command[0] == 'L' || command[0] == 'l') {
    brightness = constrain(atoi(command + 1), 0, 255);
    analogWrite(PIN_LED, brightness);
    bt.print("яркость ");
    bt.println(brightness);
  } else if (command[0] == 'S' || command[0] == 's') {
    bt.print("сейчас яркость ");
    bt.println(brightness);
  } else {
    bt.print("не понял: ");
    bt.println(command);
  }
}

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

Плата отвечает: показания на экран телефона

Связь двусторонняя, и обычно хочется не только командовать, но и видеть, что происходит. Здесь появляется вопрос меры: канал не резиновый. На скорости 9600 через него проходит 960 байт в секунду, и если сыпать показания двадцать раз в секунду, плата будет занята болтовнёй, а команды начнут опаздывать.

Сколько эфира съедает телеметрия на скорости 9600Строка «A0=512» с переводом строки — это 12 байт, или 12.5 мс эфираОтправляем 2 раз в секунду2.5 %Свободное время канала уходит на команды от телефона в обратную сторонуКанал почти свободен: команды с телефона приходят без задержки
Подвигайте ползунок. Медленные величины — раз в полсекунды; «живые» показания вроде расстояния — раз в 200 мс; чаще двадцати раз в секунду смысла нет.

Второй момент — время. Задержки в главном цикле недопустимы: приёмный буфер всего 64 байта, и всё, что пришло сверх него, теряется молча. Поэтому телеметрию отправляют по часам, а не через delay().

// Плата сама рассказывает телефону, что у неё происходит.
// Раз в полсекунды уходит значение с датчика - и при этом плата
// продолжает слушать команды: никаких delay() в цикле нет.
// Вывод STATE модуля показывает, есть ли связь: пока никто не подключился,
// говорить в пустоту незачем.

#include <SoftwareSerial.h>

SoftwareSerial bt(10, 11);
const long BT_BAUD = 38400;

const uint8_t PIN_STATE = 8;   // вывод STATE модуля
const uint8_t PIN_SENSOR = A0; // датчик или потенциометр
const unsigned long PERIOD = 500;   // как часто отправлять, мс

unsigned long lastSent = 0;
bool wasLinked = false;

void setup() {
  pinMode(PIN_STATE, INPUT);
  bt.begin(BT_BAUD);
}

void loop() {
  bool linked = digitalRead(PIN_STATE) == HIGH;

  if (linked && !wasLinked) {
    bt.println("телефон подключился");
  }
  wasLinked = linked;

  if (linked && millis() - lastSent >= PERIOD) {
    lastSent = millis();
    bt.print("A0=");
    bt.println(analogRead(PIN_SENSOR));
  }

  while (bt.available()) {     // команды продолжают приниматься между отправками
    char symbol = bt.read();
    if (symbol == 'P' || symbol == 'p') {
      bt.print("сейчас A0=");
      bt.println(analogRead(PIN_SENSOR));
    }
  }
}

Вывод STATE здесь работает выключателем: пока телефон не подключился, говорить не с кем. Заодно это защита для подвижных поделок — по пропаже связи можно остановить моторы.

Этот вывод есть не у всех модулей: на синей плате он выведен на гребёнку, а у Trema-модуля вместо него светодиод «соединён» и вывод K для настройки. Если STATE нет, связь определяют по молчанию: пусть телефон раз в полсекунды присылает любой символ, и если его не было дольше полутора секунд — считаем, что связь потеряна.

Настройка модуля: режим AT-команд

У модуля есть второй режим, в котором он слушает не телефон, а вас: в нём меняют имя, код сопряжения, скорость и роль. Вход туда сделан нарочно неудобным, чтобы модуль не свалился в него случайно: нужно подать высокий уровень на вывод EN до подачи питания и держать. На большинстве плат для этого есть кнопка — её зажимают, включают питание и не отпускают. У Trema-модуля кнопки нет: там эту роль играет вывод K, которым управляет сам скетч, — третьей руки не понадобится.

В этом режиме порт открывается на скорости 38400 независимо от того, что настроено для передачи данных. Именно отсюда растёт путаница «а какая же у HC-05 скорость»: скоростей две, у разных режимов.

// Разговор с модулем на его языке: режим AT-команд.
// Модуль должен быть ВКЛЮЧЁН в этот режим до подачи питания:
// удерживайте кнопку на плате модуля, пока подаёте на него питание,
// либо держите вывод EN (KEY) в HIGH. Светодиод начнёт мигать редко.
// В мониторе порта выберите окончание строки «NL и CR» - без него
// HC-05 не ответит ни на одну команду.

#include <SoftwareSerial.h>

SoftwareSerial bt(10, 11);     // 10 - приём (RX), 11 - передача (TX)

// В режиме AT-команд у HC-05 своя скорость, и она НЕ равна рабочей.
const long AT_BAUD = 38400;

void setup() {
  Serial.begin(9600);
  bt.begin(AT_BAUD);
  Serial.println("Режим настройки. Проверка: наберите AT и нажмите Enter.");
  Serial.println("Ответ OK означает, что модуль слышит команды.");
}

void loop() {
  if (bt.available()) {
    Serial.write(bt.read());
  }
  if (Serial.available()) {
    bt.write(Serial.read());
  }
}

Три правила, без которых модуль будет молчать. В мониторе порта выберите окончание строки «Both NL & CR» — без него команда не считается законченной, и ответа не будет вовсе. Пишите команды заглавными буквами: набор регистрозависим. И держите кнопку нажатой всё время — если отпустить, часть команд перестанет отвечать, и это не поломка, а особенность прошивки.

КомандаЧто делаетОтвет модуля
ATПроверка связи: модуль вообще слышит?OK
AT+VERSION?Какая прошивка внутри+VERSION:2.0-20100601
AT+UART?Узнать рабочую скорость модуля+UART:9600,0,0
AT+UART=9600,0,0Задать рабочую скоростьOK
AT+NAME=RobotСвоё имя в списке телефона, до 32 символовOK
AT+PSWD?Какой код сопряжения задан+PSWD:1234
AT+PSWD=4321Сменить код сопряженияOK
AT+ROLE=1Стать ведущим, чтобы самому искать второй модульOK
AT+RMAADЗабыть все сопряжённые устройстваOK
AT+ORGLЗаводские настройки — и новое имя H-C-2010-06-01OK

Две команды заслуживают предупреждения. Смена скорости меняет её только для режима данных — режим настройки всё равно останется на 38400, поэтому «потерять» модуль нельзя, что бы вы ни поставили. А сброс к заводским настройкам заодно переименовывает модуль: в списке телефона он появится как H-C-2010-06-01, и старую пару придётся удалить.

Машинка, которой рулят с телефона

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

// Машинка на драйвере L298N, которой рулят с телефона.
// Команды: F - вперёд, B - назад, L - влево, R - вправо, S - стоп,
// V80 - скорость 80 процентов. Разбор строк - как в скетче с командами.
// У робота выводы 9 и 10 заняты драйвером, поэтому модуль переехал на 11 и 12.

#include <SoftwareSerial.h>

SoftwareSerial bt(11, 12);     // 11 - приём (RX), 12 - передача (TX)
const long BT_BAUD = 38400;

const uint8_t L_IN1 = 8;
const uint8_t L_IN2 = 7;
const uint8_t L_EN = 9;        // ШИМ

const uint8_t R_IN1 = 4;
const uint8_t R_IN2 = 2;
const uint8_t R_EN = 10;       // ШИМ

const int MIN_PWM = 60;        // ниже этого моторы только гудят
const unsigned long TIMEOUT = 1000;  // молчит дольше секунды - тормозим

char line[16];
uint8_t filled = 0;
unsigned long lastByte = 0;
int speedPercent = 70;
unsigned long lastCommand = 0;

void setup() {
  const uint8_t pins[] = {L_IN1, L_IN2, L_EN, R_IN1, R_IN2, R_EN};
  for (uint8_t i = 0; i < sizeof(pins); i++) {
    pinMode(pins[i], OUTPUT);
  }
  drive(0, 0);
  bt.begin(BT_BAUD);
  bt.println("Машинка готова. F B L R S, скорость V80.");
}

void loop() {
  while (bt.available()) {
    char symbol = bt.read();
    lastByte = millis();
    if (symbol == '\r') {
      continue;
    }
    if (symbol == '\n') {
      line[filled] = '\0';
      if (filled > 0) {
        run(line);
        lastCommand = millis();
      }
      filled = 0;
      continue;
    }
    if (filled < sizeof(line) - 1) {
      line[filled] = symbol;
      filled++;
    } else {
      filled = 0;
    }
  }

  if (filled > 0 && millis() - lastByte > 200) {
    filled = 0;              // огрызок строки не должен ждать следующей команды
  }

  // Связь оборвалась на ходу - машинка не должна уехать в стену.
  if (lastCommand > 0 && millis() - lastCommand > TIMEOUT) {
    drive(0, 0);
  }
}

void run(const char *command) {
  int power = speedPercent;

  switch (command[0]) {
    case 'F': case 'f': drive(power, power); break;
    case 'B': case 'b': drive(-power, -power); break;
    case 'L': case 'l': drive(-power, power); break;
    case 'R': case 'r': drive(power, -power); break;
    case 'S': case 's': drive(0, 0); break;
    case 'V': case 'v':
      speedPercent = constrain(atoi(command + 1), 0, 100);
      bt.print("скорость ");
      bt.println(speedPercent);
      break;
    default:
      bt.println("не понял команду");
      break;
  }
}

void wheel(uint8_t in1, uint8_t in2, uint8_t en, int percent) {
  percent = constrain(percent, -100, 100);
  digitalWrite(in1, percent >= 0 ? HIGH : LOW);
  digitalWrite(in2, percent >= 0 ? LOW : HIGH);

  int power = abs(percent);
  if (power == 0) {
    analogWrite(en, 0);
  } else {
    analogWrite(en, map(power, 1, 100, MIN_PWM, 255));
  }
}

void drive(int left, int right) {
  wheel(L_IN1, L_IN2, L_EN, left);
  wheel(R_IN1, R_IN2, R_EN, right);
}

Обратите внимание на выводы: модуль здесь переехал на другую пару, потому что привычные заняты драйвером. Это обычная история в реальном проекте — выводы кончаются раньше, чем идеи.

Когда не работает

Ниже — то, с чем сталкиваются чаще всего, в порядке от самого частого. Почти всё диагностируется без приборов: хватает светодиода на модуле и монитора порта.

Что видноВ чём делоЧто сделать
Скетч перестал заливаться, среда пишет, что программатор не отвечаетМодуль сидит на выводах 0 и 1 — на той же линии, по которой заливается программаСнять два провода на время загрузки или сразу перенести модуль на выводы 10 и 11
В мониторе порта вместо текста непонятные знакиСкорость в скетче не совпала со скоростью модуляПеребрать 9600, 38400, 57600 — или войти в режим настройки и спросить AT+UART?
Модуля нет в списке Bluetooth на телефонеНет питания, либо телефон — iPhone, который такие модули не показывает вовсеПроверить, мигает ли светодиод. Если мигает, а это iPhone — нужен модуль BLE
Телефон пишет «сопряжено», плата молчитСопряжение — это только обмен ключами, канал открывает приложениеПодключиться приложением-терминалом, а не строчкой в настройках телефона
Приложение на новом Android пишет, что не может подключитьсяС двенадцатой версии Android нужны отдельные разрешения на устройства поблизостиВыдать приложению разрешение «Устройства поблизости» и включить геолокацию
Данные идут только в одну сторонуПровода приёма и передачи не перекрещены или нет общего провода землиTXD модуля идёт на приём платы, RXD модуля — на передачу платы, GND обязателен
После прямого подключения к пятивольтовому выводу модуль перестал приниматьВход модуля рассчитан на 3,6 вольта, предел по даташиту — 3,7Ставить делитель 1 кОм / 2 кОм. Если вход уже испорчен, модуль меняют
Модуль отваливается или перезагружается в момент сопряженияПитание взято с вывода 3.3V, а модуль в этот момент просит 30–40 мАПитать только от вывода 5V: на плате модуля стоит собственный стабилизатор
В режиме настройки модуль не отвечает ни на одну командуВ мониторе не выбрано окончание строки, не та скорость или команда набрана строчными буквамиВыбрать «Both NL & CR», открыть порт на 38400 и писать команды заглавными
Сменил скорость командой — и потерял модульКоманда меняет скорость только режима данныхРежим настройки всё равно открывается на 38400: войти туда и вернуть AT+UART=9600,0,0
После сброса к заводским модуль пропал из спискаСброс переименовывает модуль в H-C-2010-06-01Искать новое имя, старую пару в телефоне удалить и сопрячься заново
Команды приходят с опозданием или теряютсяВ цикле стоит задержка: приёмный буфер всего 64 байта и переполняется молчаУбрать delay() из главного цикла и считать время через millis()

Честно про защиту

Код сопряжения из четырёх цифр — это не пароль от вашего проекта, а формальность двадцатилетней давности. Он защищает ровно от случайного подключения соседа, и ничего больше. Кто сопрягся с модулем — тот и командует платой: никакой проверки «а ты имеешь право» в самом Bluetooth-канале нет.

Практический вывод простой. Управлять с телефона светом в комнате, машинкой, поливалкой для цветов — нормально. Делать на HC-05 замок входной двери или что-то, где ошибка стоит дорого, — не стоит. Если очень хочется, защиту делают не в модуле, а в своей программе: свой код в команде, подтверждение действия, ограничение по времени. И обязательно поменяйте имя и код сопряжения — заводские знают все.

И ещё одно, про энергию: модуль не умеет спать. В паре он потребляет около 8 миллиампер круглосуточно, независимо от того, передаётся что-нибудь или нет. Для поделки, которая должна месяц жить от батарейки, нужен BLE — он для того и придуман.

Что купить

Всё проверено 24 августа 2026 года: каждая карточка открыта отдельно, цена и артикул взяты с самой страницы товара. Того, чего нет в наличии, в подборке нет.

HC-05 Bluetooth для Arduino

Та самая синяя плата, о которой вся страница: шесть выводов и кнопка для входа в режим настройки. В карточке указано питание 5 В, ток до 40 мА при сопряжении и до 8 мА в работе, дальность 10 метров, скорость по умолчанию 38 400.

531 ₽
Открыть карточку
арт. 398 · цена и наличие на 24.08.2026

Bluetooth HC-05, Trema-модуль

Тот же радиомодуль, но на плате Trema: пять подписанных выводов RX, G, V, K, TX, четыре светодиода состояния и провода в комплекте. Кнопки нет — в режим настройки модуль переводит вывод K, то есть сам скетч. И главное для схемы: в характеристиках прямо сказано, что напряжение на выводах TX и RX не должно превышать напряжение питания, а питание допускается 5 В.

792 ₽
Открыть карточку
арт. 794 · цена и наличие на 24.08.2026

Набор резисторов, 13 номиналов

Для делителя нужны два резистора. В наборе есть и 1 кОм, и 2,2 кОм — второго номинала достаточно: получится 3,44 вольта вместо 3,33, и это по-прежнему в пределах нормы для входа модуля. Отдельно резисторы 1 кОм тоже продаются.

230 ₽
Открыть карточку
арт. 1863 · цена и наличие на 24.08.2026

Провода «папа-мама», 40 штук

Именно этими проводами модуль со штырьками сажают на гнёзда платы: с одной стороны штырёк, с другой гнездо. Длина 20 см, десять цветов — путаницы будет меньше.

161 ₽
Открыть карточку
арт. 361 · цена и наличие на 24.08.2026

Макетная плата на 830 точек

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

160 ₽
Открыть карточку
арт. 169 · цена и наличие на 24.08.2026

Контроллер Piranha ESP32

Ответ для владельца iPhone. Специализированный BLE-модуль HM-10 у нас сейчас закончился, а вот эта плата в наличии, и BLE у неё внутри: внешний модуль не нужен вовсе. Важно помнить, что скетч для iPhone нужно писать именно на BLE, а не на классическом последовательном профиле.

690 ₽
Открыть карточку
арт. 1453 · цена и наличие на 24.08.2026

Драйвер моторов L298N

Вторая половина машинки из предпоследнего раздела: два канала по 2 ампера, питание силовой части от 5 до 35 вольт. Разбор самого драйвера — на странице про моторы.

396 ₽
Открыть карточку
арт. 60 · цена и наличие на 24.08.2026

Робоплатформа на двух колёсах

Готовое шасси в сборе: платформа, два мотор-редуктора, колёса, поворотное колесо, отсек для батареек и выключатель. Остаётся поставить плату, драйвер и Bluetooth-модуль. Батарейки покупают отдельно.

1 190 ₽
Открыть карточку
арт. 1833 · цена и наличие на 24.08.2026

Дальше по теме

Частые вопросы

Почему iPhone не видит модуль HC-05?

Потому что в iOS нет профиля последовательного порта — того самого, на котором работает HC-05. Apple публикует список профилей Bluetooth, которые поддерживает iPhone: их меньше десятка — гарнитура, наушники, клавиатура и подобное, — и последовательного порта среди них нет. Стороннему приложению система открывает только Bluetooth Low Energy, а классический последовательный канал доступен лишь аксессуарам, прошедшим сертификацию Apple, — с отдельной микросхемой на плате. Поэтому «найти правильное приложение» невозможно в принципе: ни смена имени, ни PIN-кода, ни другая прошивка модуля ничего не изменят. Для iPhone нужен модуль BLE или плата ESP32, но именно со скетчем на BLE.

Какая скорость у HC-05: 9600 или 38400?

Обе, и это разные скорости для разных режимов — их постоянно путают. Режим передачи данных живёт на своей скорости: у массовых модулей это чаще всего 9600, у Trema-модуля в карточке указано 38400, встречаются экземпляры и на 57600. Режим настройки AT-командами при входе кнопкой до подачи питания всегда открывается на 38400, и эту скорость поменять нельзя. Точный ответ про свой модуль даёт он сам: в режиме настройки спросите AT+UART?

Можно ли соединять Arduino и HC-05 напрямую, без резисторов?

В одну сторону да, в другую нет. Провод от модуля к плате идёт напрямую: пятивольтовая плата уверенно считает 3,3 вольта единицей, её порог — 3 вольта. А вот от платы к модулю только через делитель из резисторов 1 и 2 килоома: он превращает 5 вольт в 3,33. Вход модуля рассчитан на 1,8–3,6 вольта, а предельным значением в даташите названы 3,7. Питание — отдельная история: на вывод VCC подают именно 5 вольт, потому что на плате-носителе стоит свой стабилизатор.

Телефон пишет «сопряжено», а данные не идут. Что не так?

Сопряжение и подключение — разные вещи. Сопряжение это только обмен ключами, канал данных оно не открывает; поток байтов появляется тогда, когда приложение само откроет последовательный профиль. Поэтому проверять связь надо приложением-терминалом, а не строчкой в системных настройках телефона. Если терминал подключился, а плата всё равно молчит, ищите дальше: не совпали скорости, не перекрещены RX и TX или нет общего провода земли.

Нужна ли библиотека для работы с HC-05?

Нет. Для платы модуль выглядит обычным последовательным портом, поэтому весь код — это Serial или SoftwareSerial, которые уже есть в среде. Библиотеки для HC-05 существуют, но они помогают настраивать модуль AT-командами, а не передавать данные. Для проекта «телефон управляет платой» ни одна из них не нужна.

Чем HC-05 отличается от HC-06 и от BLE-модуля?

HC-05 умеет быть и ведомым, и ведущим, поэтому парой таких модулей связывают две платы между собой. У HC-06 роль не переключается: он всегда ведомый, команд у него меньше, а синтаксис другой. Внешне их проще всего различить по плате: у HC-05 шесть выводов, кнопка и вывод состояния, у HC-06 — четыре вывода и никакой кнопки. BLE-модуль вроде HM-10 — это уже другое поколение Bluetooth: он экономичнее и работает с iPhone, но с HC-05 напрямую не разговаривает.

Почему не заливается скетч, когда подключён Bluetooth-модуль?

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

Что для этого нужно в среде

Ничего особенного: все шесть скетчей с этой страницы собираются в Trema IDE без единой скачанной библиотеки — программный порт входит в саму среду. Пригодятся две привычные вещи: монитор порта с выбором окончания строки (он нужен для режима настройки модуля) и подсказки по ошибкам сборки на русском.

Что почитать рядом: как устроен сам последовательный порт — UART в Arduino. Что делать, если скетч не заливается — Ошибка загрузки. Куда подключать остальные модули — Модули для Arduino. Идеи для проектов — Проекты на Arduino.