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, потому что он самый дешёвый и самый описанный, а потом выясняется, что телефон у него неподходящий или поделка должна месяц жить от батарейки. Выбор модуля определяется двумя вопросами: кто на другом конце и зачем.
| Модуль | Какой Bluetooth | Android | iPhone | Может быть ведущим | Зачем берут |
|---|---|---|---|---|---|
| HC-05 | Bluetooth 2.0, классический | Да | Нет | Да | Управление с Android и связь двух плат между собой |
| HC-06 | Bluetooth 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. Для управления с телефона он тоже годится, но роль ведущего у него не переключается, а команды настройки пишутся иначе.
Светодиод на плате — самый честный диагностический прибор в этой теме. По его ритму видно, что происходит внутри, ещё до того, как вы напишете первую строчку кода.
Подключение: четыре провода, и один — через резисторы
Проводов ровно четыре: питание, земля и две линии данных, перекрещённые между собой. Передача модуля идёт на приём платы, приём модуля — на передачу платы. Земля обязательна: без общего провода два устройства не договорятся о том, что считать нулём.
Единственная тонкость — направление «плата говорит модулю». Arduino выдаёт пять вольт, а вход модуля рассчитан на 1,8–3,6 вольта, и предельным значением в даташите названы 3,7. Пять вольт превышают этот предел почти на полтора, и вход постепенно выходит из строя — обычно тихо, без дыма: модуль просто перестаёт принимать. Лечится двумя резисторами: 1 кОм последовательно и 2 кОм на землю превращают пять вольт в 3,33. В обратную сторону делитель не нужен — пятивольтовая плата считает единицей всё, что выше трёх вольт.
Из этого правила есть одно задокументированное исключение — наш 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. Если в мониторе вместо букв идёт мусор — скорость не та, переберите эти три значения. Точный ответ даст сам модуль, но чуть позже, в разделе про настройку.
Сопряжение с телефоном шаг за шагом
Здесь спотыкается больше людей, чем на пайке. Причина в том, что «сопряжено» и «подключено» — разные состояния, а телефон показывает только первое.
На практике порядок такой. Включаете плату — светодиод модуля часто мигает. В настройках 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 мы получили» срабатывает на первом символе, а число уходит в никуда.
Правильный приём — копить символы в буфер, пока не придёт конец строки, и только потом разбирать команду целиком. В приложении-терминале для этого включают отправку перевода строки. Заодно стоит выбрасывать огрызок, если связь оборвалась на середине: иначе следующая команда склеится с ним в бессмыслицу.
// Команды из нескольких символов: буква и число.
// 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 байт в секунду, и если сыпать показания двадцать раз в секунду, плата будет занята болтовнёй, а команды начнут опаздывать.
Второй момент — время. Задержки в главном цикле недопустимы: приёмный буфер всего 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-01 | OK |
Две команды заслуживают предупреждения. Смена скорости меняет её только для режима данных — режим настройки всё равно останется на 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.
Bluetooth HC-05, Trema-модуль
Тот же радиомодуль, но на плате Trema: пять подписанных выводов RX, G, V, K, TX, четыре светодиода состояния и провода в комплекте. Кнопки нет — в режим настройки модуль переводит вывод K, то есть сам скетч. И главное для схемы: в характеристиках прямо сказано, что напряжение на выводах TX и RX не должно превышать напряжение питания, а питание допускается 5 В.
Набор резисторов, 13 номиналов
Для делителя нужны два резистора. В наборе есть и 1 кОм, и 2,2 кОм — второго номинала достаточно: получится 3,44 вольта вместо 3,33, и это по-прежнему в пределах нормы для входа модуля. Отдельно резисторы 1 кОм тоже продаются.
Провода «папа-мама», 40 штук
Именно этими проводами модуль со штырьками сажают на гнёзда платы: с одной стороны штырёк, с другой гнездо. Длина 20 см, десять цветов — путаницы будет меньше.
Макетная плата на 830 точек
Делитель из двух резисторов удобно собрать на макетной плате, не паяя. Пригодится и для всего остального: 630 монтажных точек плюс две шины питания.
Контроллер Piranha ESP32
Ответ для владельца iPhone. Специализированный BLE-модуль HM-10 у нас сейчас закончился, а вот эта плата в наличии, и BLE у неё внутри: внешний модуль не нужен вовсе. Важно помнить, что скетч для iPhone нужно писать именно на BLE, а не на классическом последовательном профиле.
Драйвер моторов L298N
Вторая половина машинки из предпоследнего раздела: два канала по 2 ампера, питание силовой части от 5 до 35 вольт. Разбор самого драйвера — на странице про моторы.
Робоплатформа на двух колёсах
Готовое шасси в сборе: платформа, два мотор-редуктора, колёса, поворотное колесо, отсек для батареек и выключатель. Остаётся поставить плату, драйвер и Bluetooth-модуль. Батарейки покупают отдельно.
Дальше по теме
- Последовательный порт и SoftwareSerial — что происходит в проводе, который заменяет Bluetooth.
- Скетч не загружается — разбор ошибки, которая появляется, если модуль занял выводы 0 и 1.
- Мотор и драйвер L298N — вторая половина проекта «машинка с телефона».
- Плата ESP32 — когда радио уже внутри платы и внешний модуль не нужен.
- Все модули к Arduino — обзорная таблица с библиотеками и разборами.
Частые вопросы
Почему 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.
