Думаю, многие помнят одно из недавних наших творений — клешню из ПКЛ (поликапролактона), и кто-то, возможно, даже читал о Bluetooth-модулях HC-05. Так вот, мы решили сделать клешню управляемой по Bluetooth с Arduino.
Я использовал предельно простой метод — использовать HC-05 как радиоудлинитель UART, и гонять по нему данные. Для начала, нужно было определиться с тем, какие данные нужно передавать с… эээ… «джойстика»? Короче, вот этой штуки:

Решил переложить обработку данных на саму клешню, а с «джойстика» клешне слать положения потенциометров. Для начала нужно было наладить связь. Можно было пойти сложным путём — при старте Arduino переводить HC-05 в режим AT-команд и запускать подключение, но есть способ проще: использовать функцию автоподключения. В даташите написано, что нужно сначала сделать AT+CMODE=1 (подключаться к любому устройству), потом подключиться к нужному устройству и сделать AT+CMODE=0 — и тогда модуль будет подключаться к нужному устройству автоматически. Брехня чистой воды! В очередной раз убедился, что документацию писали несколько разных китайцев, необязательно даже разработчиков модуля. На деле, как я и думал, нужно выполнить AT+BIND=<адрес устройства> — привязать модуль к устройству, а потом сделать AT+CMODE=0, чтобы модуль автоматически подключался к нему. Как говорится, доверяй, но проверяй (:
После чтения китайшита я подключил модули к ардуинам у «джойстика» (Модуль A) и клешни (Модуль B) и проделал с модулями следующие манипуляции:
- Подключил к обоим модулям питание и перевёл их в AT-режим.
- Подключился к модулям через терминалку.
- Сбросил у обоих настройки на дефолтные командой AT+ORGL.
- Перевёл модуль A в режим master (инициирует подключение) и дал ему имя:
AT+ROLE=1 ; режим master AT+NAME=joystick
- То же проделал с модулем B, только с режимом slave (ждёт подключения):
AT+ROLE=0 ; режим slave AT+NAME=manipulator
- Узнал адреса модулей:
A:AT+ADDR? +ADDR:11:4:290255 OK
B:
AT+ADDR? +ADDR:11:4:291093 OK
- Наказал модулю B не принимать незваных гостей:
AT+RMAAD ; очистить список авторизованных устройств AT+BIND=11,4,290255 ; привязать модуль B к модулю A AT+CMODE=0 ; в режиме slave принимать подключение только от модуля A
- Модулю A наказал подключаться только к модулю B:
AT+RMAAD AT+BIND=11,4,291093 ; в режиме master подключаться только к модулю B AT+CMODE=0
Всё, теперь модуль A при включении будет искать модуль B и автоматически подключаться к нему (и переподключаться при обрыве связи). Теперь нужно написать программы для отсылки показаний потенциометров клешне и для приёма и обработки этих показаний. Начнём с отправки.
Быстродействие сервоприводов ограничено, так что вполне достаточно будет передавать данные о положении потенциометров 100 раз в секунду. Только нужно определиться, как их передавать. Дело в том, что если просто слать номер потенциометра и считанное значение, приёмник не сможет узнать, где в потоке принятых байт начинается номер, а где — значение.
Вот тут нам пригодится такая замечательная вещь, как CRC. Если коротко, то это контрольная сумма данных, имеющая фиксированную длину. Если сравнить контрольные суммы двух наборов данных, то можно с большой вероятностью говорить об идентичности этих наборов данных. В данном случае я выбрал CRC-8, 8-битный алгоритм CRC, который для данных любой длины генерирует контрольную сумму длиной 8 бит, или 1 байт. Вот её-то мы и будем посылать после наших данных — тогда принимающая сторона сможет вычислять текущее значение CRC для принятых данных и сравнивать с последним принятым байтом: если значения равны, значит принятые ранее байты содержат правильные данные. Сам алгоритм прост, как устройство пуговицы — он побитово сканирует входные данные и применяет к их старшему биту операцию «исключающее ИЛИ» с участием специального двоичного многочлена (CRC polynomial), в котором вся соль алгоритма, и после каждой такой операции сдвигает результат влево. Звучит страшно? Двоичный многочлен — это всего-то 8-битное число, 1 байт. Да и нам его придумывать не нужно, за нас уже всё придумали программисты-математики. Вот функция для вычисления CRC-8 для одного байта данных:
uint8_t crc8(uint8_t crc, uint8_t data, uint8_t polynomial)
{
crc ^= data;
for (int i = 0; i < 8; ++i)
crc = (crc << 1) ^ ((crc & 0x80) ? polynomial : 0);
return crc;
}
Если обрабатываем первый байт данных, то передаём этой функции нулевой crc, иначе - результат применения функции к предыдущему байту данных. Полином я выбрал рекомендованный институтом CCITT - 0x07, после чего написал вспомогательную функцию:
uint8_t crc8_ccitt(uint8_t crc, uint8_t data)
{
return crc8(crc, data, 0x07);
}
Теперь вычисление CRC-8 для блока данных выглядит так:
uint8_t crc = crc8_ccitt(0, byte1); crc = crc8_ccitt(crc, byte2); crc = crc8_ccitt(crc, byte3); ... crc = crc8_ccitt(crc, byteN);
Отсылка данных потенциометра происходит в следующем порядке побайтно:
- Номер потенциометра (их по-любому меньше 256)
- Младший байт значения, считанного с потенциометра: analogRead() возвращает 10-битное значение (0..1023), которое приходится разбивать на два куска.
- Старший байт значения.
- CRC-8 от предыдущих трёх байтов.
Такой формат данных очень удобен для принимающей стороны, так как каждая порция данных имеет один и тот же фиксированный размер - 4 байта, а значит можно завести один 4-байтный буфер и скидывать в него поступающие байты:
Вот так и происходит приём данных: сдвигаем принятые байты к началу буфера, записываем в конец буфера считанный байт и проверяем, является ли он результатом вычисления CRC-8 от предыдущих трёх байт. Если да, то забираем из буфера данные и обрабатываем. Конечно, мой метод отличается от классических, когда CRC служит только для проверки целостности данных, а разбиение на пакеты происходит с использованием комбинации символов, которая гарантированно не может встретиться в правильных данных, или предварительной отправки длины пакета. Но в данном случае проще было сделать так.
После всех этих размышлений родился следующий код для "джойстика":
/* Для удобной работы с потенциометром нарисовал такую структурку */
struct PotData
{
uint8_t pin; // номер аналогового пина
int value; // последнее считанное значение
bool changed; // признак того, что считанное значение изменилось
void read()
{
int old_value = value;
value = analogRead(pin);
/* Попытка отбросить часть случайных флуктуаций на аналоговом входе */
changed = abs(value - old_value) > 2;
}
void sendOverSerial()
{
/* Считаем CRC для отправляемых данных */
uint8_t crc = crc8_ccitt(0, pin);
uint8_t value_low = lowByte((uint16_t)value),
value_high = highByte((uint16_t)value);
crc = crc8_ccitt(crc, value_low);
crc = crc8_ccitt(crc, value_high);
/* Шлём данные и CRC */
Serial.write(pin);
Serial.write(value_low);
Serial.write(value_high);
Serial.write(crc);
}
};
enum { POTS_AMOUNT = 6 };
PotData pot_data[POTS_AMOUNT] =
{
{ 0, 0, false },
{ 1, 0, false },
{ 2, 0, false },
{ 3, 0, false },
{ 4, 0, false },
{ 5, 0, false },
};
void setup()
{
Serial.begin(38400);
}
void loop()
{
/* 100 раз в секунду читаем и шлём значения с аналоговых входов */
for (int i = 0; i < POTS_AMOUNT; ++i)
{
pot_data[i].read();
pot_data[i].sendOverSerial();
}
delay(10);
}
uint8_t crc8_ccitt(uint8_t crc, uint8_t data)
{
return crc8(crc, data, 0x07);
}
uint8_t crc8(uint8_t crc, uint8_t data, uint8_t polynomial)
{
crc ^= data;
for (int i = 0; i < 8; ++i)
crc = (crc << 1) ^ ((crc & 0x80) ? polynomial : 0);
return crc;
}
Код для клешни будет пообъёмнее, ведь нужно данные не только принять, но и обработать соответствующим образом. Впрочем, обработка тут почти не отличается от той, что была в первой статье о клешне, объём добавляет только приём данных по Bluetooth:
#include <Servo.h>
struct Joint
{
uint8_t servo_pin;
int pot_min, pot_max;
int servo_min, servo_max;
Servo servo;
};
Joint joints[] =
{
/* захват */
{ 2,
10, 600,
0, 180,
Servo()
},
/* поворот кисти */
{ 4,
0, 1024,
0, 180,
Servo()
},
/* наклон кисти */
{ 3,
0, 550,
180, 0,
Servo()
},
/* локоть */
{ 5,
350, 950,
180, 0,
Servo()
},
/* поворот */
{ 7,
0, 1024,
180, 0,
Servo()
},
/* плечо */
{ 6,
0, 1024,
180, 0,
Servo()
},
};
enum { JOINTS_AMOUNT = sizeof(joints) / sizeof(joints[0]) };
void setup()
{
Serial.begin(38400);
for (int i = 0; i < JOINTS_AMOUNT; ++i)
{
Joint &joint = joints[i];
joint.servo.attach(joint.servo_pin);
}
pinMode(13, OUTPUT); // будем мигать светодиодом L при приёме данных
}
void loop()
{
/* Вот и наш буфер для приёма данных размером в 4 байта */
static uint8_t packet[4];
while (Serial.available() > 0)
{
/* Функция memmove() копирует содержимое одного блока памяти
* в другой, при этом позволяя указывать перекрывающиеся блоки,
* а нам именно это и нужно. Кстати, функция memcpy() работает
* аналогично, но для перекрывающихся блоков работает неправильно,
* и это не баг, а фича для ускорения работы.
*/
memmove(packet, packet + 1, sizeof(packet) - 1);
/* Данные сдвинули - принимаем свежие, в конец буфера */
packet[3] = Serial.read();
/* Вычисляем CRC для первых трёх байт буфера */
uint8_t crc = crc8_ccitt_block(packet, sizeof(packet) - 1);
/* Если последний принятый байт - CRC от предыдущих трёх,
* значит, мы приняли корректные данные потенциометра.
*/
if (crc == packet[3])
{
/* Мигнём L-кой, сигнализируя успешный приём данных */
digitalWrite(13, !digitalRead(13));
/* Выдёргиваем из буфера номер потенциометра
* и считанное с него значение.
*/
uint8_t pot_index = packet[0];
int pot_angle = packet[1] | packet[2] << 8;
/* В обработке всё то же самое, ничего нового */
if (0 <= pot_index && pot_index < JOINTS_AMOUNT)
{
Joint &joint = joints[pot_index];
int servo_angle = map(pot_angle, joint.pot_min, joint.pot_max,
joint.servo_min, joint.servo_max);
bool inverse = joint.servo_min > joint.servo_max;
servo_angle = constrain(servo_angle,
(inverse ? joint.servo_max : joint.servo_min),
(inverse ? joint.servo_min : joint.servo_max));
joint.servo.write(servo_angle);
}
}
}
}
/* Вспомогательная функция вычисления CRC для массива байтов */
uint8_t crc8_ccitt_block(const uint8_t *data, size_t length)
{
uint8_t crc = 0;
for (size_t i = 0; i < length; ++i)
crc = crc8_ccitt(crc, data[i]);
return crc;
}
uint8_t crc8_ccitt(uint8_t crc, uint8_t data)
{
return crc8(crc, data, 0x07);
}
uint8_t crc8(uint8_t crc, uint8_t data, uint8_t polynomial)
{
crc ^= data;
for (int i = 0; i < 8; ++i)
crc = (crc << 1) ^ ((crc & 0x80) ? polynomial : 0);
return crc;
}
Тут я кое-что недоглядел: управление реализовано так, что для калибровки приходится менять принимающую программу, в которой заложены допустимые пределы углов поворота сервомашинок, и прошивать контроллер, к которому подключена рука. Для удобства тестирования следовало бы прописать все настройки в программе джойстика, а манипулятор принимал бы только непосредственно углы серв.
Ну, а теперь демонстрация прототипа дистанционной руки-убийцы:


19 комментариев на «“Теперь клешня и по Bluetooth”»
здорово но дороговато…
Ха дорговато, иди найди дешевле!
HC-05 на сегодня одни из самы дешовых БТ модулей.
Что то видео не показывает 🙁
Проверил… показывает.
Круть! Только вы уже недели как две переехали, а на видео обстановочка старая)))
Ну дык, снято было давно, а выложено только сейчас (:
Для тех у кого с китайским проблема можно посмотреть вариант даташита на HC-05)))
Может я что-то не понимаю, но период следования фазозадающих импульсов у сервоприводов — 20мс (50Гц) зачем слать 100 раз в секунду??
Кстати для тех у кого с китайским проблема можно посмотреть вариант даташита на HC-05)))
Эээ, так даташит, указанный в статье — на английском. А «китайшит» — это такой тонкий намёк на его происхождение и качество.
Что касательно 100 раз в секунду — так это я с потолка цифру взял, можно было бы и 50 раз в секунду, просто я высокий, и до потолка мне дотянуться не трудно. Всё вы правильно понимаете (:
Доброго времени суток всем! у меня такой вопрос, насколько он тяжелый? а то для некоторых моделей это может быть очень принципиально. ответтье кто знает, кто использовал этот материал. и что лучше, это или просто сделать каркас из перфариованной ленты?
Вес ПКЛ в нашей клешне — около 0.6 кг, можете оценить его плотность. Для моделей с особыми требованиями по весу его лучше не применять, т.к. он гибкий, когда тонкий, но тяжёлый, когда толстый.
Я вот не понимаю для чего нужно использовать функцию memmove()? В чем выигрыш, что есть не использовать данную функция?
Ну, если комментария в коде …
Я знаю что эта за функция я просто не понял для чего она. То что она копирует один блок в другой понятно, но что это дает?
Ладно, не в этом дело. Я не работаю с Ардуино, я не знаю ее библиотеку serialport, но когда я использую переменную типа uint8_t для буфера принятых значении то они потом не распознаются в программе. Только когда я пишу char все работает. Странно как твой код работает с переменными uint8_t просто интересно)) я знаю что это тоже самое что и char только через typedef переделана)
«Я знаю что эта за функция я просто не понял для чего она. То что она копирует один блок в другой понятно, но что это дает?»
Можешь не отвечать, извини. В данном случае понял)
Привет. Занялся интересным проектом по замене старой аналоговой приборной панели в машине на новую цифровую работающую по кан шине. Двигатель управляется эбу vems(vems.hu), передает и принимает данные по rs232, есть описание протокола вывода основных данных для подключения всяких доп показометров. Пакет данных имеет длинну 5 байт, Первый байт номер канала данных, второй всегда a3(hex), третий и четвертый вывод данных, пятый контольная сумма. Пытался использовать код от манипулятора по блютуз, но что то не получается из пакета выдергивать определенный байт, если вписать в Serial.print(packet[5]); то выводятся байты по очереди, если вписать от[1-4] то в мониторе ардуино принимает 0, а в другом терминале принимает значение (30). Буфер до пяти байт расширил, подсчет crc до четырех байт расширил, но есть подозрение что косяк в том, что эбу шлет данные в hex и uint8_t для этих целей не совсем подходит, а перевести на uint16 с работающим подсчетом crc у меня не получается. Вот инструкция к протоколу:
Если есть возможность, то помогите пожалуйста 🙂
Ужос)) Тема прикольная, жаль что китайский модуль такой геморойный и непредсказуемый во всех отношениях))) НО мне кажется с CRC ты явно перемудрил)) влолне можно использовать более примитивный алгоритм, например так:
это не намного хуже чем CRC8 зато на сколько проще, а оптимизация для программ для ардуино, особенно в больших проектах весьма актуальна)
Одна из целей статьи — рассказать о CRC (:
Штука очень полезная, но почти не известная начинающим. Что до оптимизации — ATmega168 не имеет аппаратного деления, и если делить не на степени двойки (этот случай компилятор может оптимизировать в сдвиг вправо на эту степень), то компилятор применит программное деление, которое жутко медленное, к тому же делить придётся 16-разрядное число на 8-разрядное. Так что вряд ли тут будет выигрыш, т.к. выражение
компилятор оптимизирует до примерно такого кода:
Итого имеем 7 тактов на тело цикла, которое выполняется 8 раз — имеем 56 тактов + накладные расходы на цикл около 8 * 3 = 24 такта и 2 такта на crc ^= data и return crc. Итого грубо 90 тактов, выполняемых 3 раза (для трёх байтов данных) = 270 тактов или 17 микросекунд. Размер кода функции не превышает 30 байт.
Это очень дорого — 30 байт кода и 17 мкс времени? (:
Ой, в последней строке листнига коммент должен быть таким: