Разработка hexapod с нуля (часть 8) — улучшенная математика передвижения
Всем привет! В результате перехода на удаленную работу у меня появилось больше свободного времени на разработку гексапода (+2 часа в день за счет экономии на дороге). Я наконец-то смог сделать универсальный алгоритм для построения траектории движения в реальном времени. Новая математика позволила реализовать базовые движения путем изменения всего двух параметров. Это очередной шаг к внедрению «автопилота». В этой статье я постараюсь подробно рассказать о новой математике и как это вообще работает. Будет много картинок и gif.
Этапы разработки:
Часть 1 — проектирование
Часть 2 — сборка
Часть 3 — кинематика
Часть 4 — математика траекторий и последовательности
Часть 5 — электроника
Часть 6 — переход на 3D печать
Часть 7 — новый корпус, прикладное ПО и протоколы общения
Часть 8 — улучшенная математика передвижения
До этого момента все базовые движения (вперед, назад, поворот) задавались в виде «перемести ногу из текущей позиции в точку (X, Y, Z), используя линейное/синусоидальное/какое-нибудь перемещение». Это работает достаточно хорошо и надежно, но сильно ограничивает в движении. Например, чтобы реализовать движение по дуге с радиусом R1 нужно заранее рассчитать координаты начала и окончания движения для каждой конечности, добавить дополнительную кнопку в приложение, чтобы можно было выбрать это движение. Соответственно для добавления движения по дуге с другим радиусом R2, нужно опять рассчитать координаты и добавить еще одну кнопку. Крайне неудобно.
Сейчас для реализации базовых движений используется один математический блок, который можно адаптировать под различные ситуации двумя параметрами. Главным бонусом новой математики стала поддержка движения по дуге с изменением её радиуса прямо во время движения!
Для начала стоит объяснить на пальцах как это работает. Что будет, если выбрать на окружности окно фиксированного размера и начать увеличивать её радиус? Вот что:
Всё просто правда? Изменяя радиус окружности мы можем получить различные траектории движения и с некоторой точность прямую линию. На этом можно было и остановиться, но не все так радужно. Нельзя просто взять и реализовать это на основе одного только радиуса — есть нюансы.
Начальные положения конечностей гексапода могут находиться на разных окружностях, соответственно параметры уравнения уже будут другими. В моем случае конечности располагаются следующим образом (примерно):
Значение расстояния, которое должна пройти конечность зависит от радиуса окружности, на которой она находится. Это заставляет переходить к индивидуальному расчету траектории для каждой конечности. Для решения этой проблемы необходимо найти окружность с максимальным радиусом, рассчитать начальный и конечный углы дуги. Дальше относительно полученной дуги находятся дуги для других конечностей. Я сделал анимацию работы данного куска алгоритма:
Есть анимация для наглядности работы всей этой магии (как же я рад что в наше время есть Excel). В начале изменяется значение расстояния, которое должен пройти гексапод за цикл (аналогично скорости), затем изменяется значение кривизны траектории (аналогично повороту руля).
В целом в этом и заключается идея новой математики, надеюсь получилось доступно объяснить. Теперь можно разобрать алгоритм подробнее и попробовать посчитать все руками на практике.
Входные параметры
Переменными входными параметрами являются расстояние (distance) и кривизна траектории движения (curvature). Значение кривизны траектории должно находится в диапазонах [-1.999; -0.001] и [0.001; 1.999], в то время как максимальное значение расстояния зависит от физических характеристик гексапода. В моем случае максимальное расстояние за цикл равно 110 мм, при больших значениях конечности начинают упираться в друг друга. Для примера расчета возьмем значения curvature = 1.5 и distance = 20.
Так же для работы алгоритма необходимо знать начальные положения конечностей. Это точки, в которых находятся конечности, когда гексапод встал на ноги. Для примера будем использовать следующие точки (начало координат каждой конечности находится в месте крепления COXA):
Note: Я работаю в плоскости XZ и на координату Y можно не обращать внимания. Ознакомиться с системой координат гексапода можно в третьей части цикла
В итоге мы имеем следующее:
Формулы и расчеты
Начнем с расчета координат центра движения гексапода в зависимости от значения кривизны и расстояния:
В итоге мы получили точку [R; 0] и траекторию движения тела гексапода. Относительно нее будут рассчитываться траектории для каждой конечности.
Далее необходимо вычислить радиусы траекторий для каждой конечности относительно центра движения (точки [R; 0]) c учетом начального положения конечности [x0; z0]. Более понятным языком — найти длину вектора, проведенного из точки [R; 0] в точку [x0; z0]:
Картинка для наглядности. Синим показаны нужные вектора.
Находим из полученных значений максимальное:
Дальше нужно найти угол для каждого вектора, которые мы пытали в предыдущем шаге.
Теперь находим угол дуги на самой большой окружности радиуса R_maх (самой удаленной от центра движения траектории), длина которой должна быть равна значению distance. Этот угол определяет начальные и конечные углы других дуг, по которым будут двигаться конечности. Думаю картинка ниже поможет понять это.
Угол вычисляется следующим образом:
Дальше используя этот угол мы можем вычислить начальные и конечные углы дуг для других конечностей. Как то так это должно получиться:
Небольшое отступление. Для реализации этого алгоритма необходимо ввести понятие времени, значение которого лежит в диапазоне [0; 1]. Так же необходимо, чтобы каждая конечность при значении времени 0.5 возвращалась в свою начальную точку. Данное правило является проверкой корректности расчетов — все окружности должны проходить через начальные точки каждой конечности.
Дальше начинается вычисление точек по полученным параметрам дуг, используя следующие формулы:
static bool process_advanced_trajectory(float motion_time) {
// Check curvature value
float curvature = (float)g_current_trajectory_config.curvature / 1000.0f;
if (g_current_trajectory_config.curvature == 0) curvature = +0.001f;
if (g_current_trajectory_config.curvature > 1999) curvature = +1.999f;
if (g_current_trajectory_config.curvature < -1999) curvature = -1.999f;
//
// Calculate XZ
//
float distance = (float)g_current_trajectory_config.distance;
// Calculation radius of curvature
float curvature_radius = tanf((2.0f - curvature) * M_PI / 4.0f) * distance;
// Common calculations
float trajectory_radius[SUPPORT_LIMBS_COUNT] = {0};
float start_angle_rad[SUPPORT_LIMBS_COUNT] = {0};
float max_trajectory_radius = 0;
for (uint32_t i = 0; i < SUPPORT_LIMBS_COUNT; ++i) {
float x0 = g_motion_config.start_positions[i].x;
float z0 = g_motion_config.start_positions[i].z;
// Calculation trajectory radius
trajectory_radius[i] = sqrtf((curvature_radius - x0) * (curvature_radius - x0) + z0 * z0);
// Search max trajectory radius
if (trajectory_radius[i] > max_trajectory_radius) {
max_trajectory_radius = trajectory_radius[i];
}
// Calculation limb start angle
start_angle_rad[i] = atan2f(z0, -(curvature_radius - x0));
}
if (max_trajectory_radius == 0) {
return false; // Avoid division by zero
}
// Calculation max angle of arc
int32_t curvature_radius_sign = (curvature_radius >= 0) ? 1 : -1;
float max_arc_angle = curvature_radius_sign * distance / max_trajectory_radius;
// Calculation points by time
for (uint32_t i = 0; i < SUPPORT_LIMBS_COUNT; ++i) {
// Inversion motion time if need
float relative_motion_time = motion_time;
if (g_motion_config.time_directions[i] == TIME_DIR_REVERSE) {
relative_motion_time = 1.0f - relative_motion_time;
}
// Calculation arc angle for current time
float arc_angle_rad = (relative_motion_time - 0.5f) * max_arc_angle + start_angle_rad[i];
// Calculation XZ points by time
g_limbs_list[i].position.x = curvature_radius + trajectory_radius[i] * cosf(arc_angle_rad);
g_limbs_list[i].position.z = trajectory_radius[i] * sinf(arc_angle_rad);
// Calculation Y points by time
if (g_motion_config.trajectories[i] == TRAJECTORY_XZ_ADV_Y_CONST) {
g_limbs_list[i].position.y = g_motion_config.start_positions[i].y;
}
else if (g_motion_config.trajectories[i] == TRAJECTORY_XZ_ADV_Y_SINUS) {
g_limbs_list[i].position.y = g_motion_config.start_positions[i].y;
g_limbs_list[i].position.y += LIMB_STEP_HEIGHT * sinf(relative_motion_time * M_PI);
}
}
return true;
}
Тут я решил показать еще и расчет Y координаты (Calculation Y points by time). Расчет зависит от выбранной траектории, которая задана в коде жестко и необходима для реализации движения конечности по земле и по воздуху.
Так же имеется кусок для реализации обратного направления движения (Inversion motion time if need). Он нужен, чтобы во время движения трех конечностей по земле, другие три конечности двигались в обратном направлении по воздуху.