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

Алгоритм работы был малопонятным, потому что основывался на фантазировании и подгоне. Поэтому оценить его надёжность, масштабировать или восстановить для другого датчика было бы сложно.

Затем в универе показали линейную алгебру и MATLAB. Мне открылись новые возможности, и создать прототип программы удалось очень быстро.
К тому же у меня есть тяга к относительно научному познанию, и через такую работу я могу потренироваться. Текст же поможет структурировать мысли и сохранить математические выкладки.

Описание

Я в процессе разработки электронного девайса с набором датчиков, умеющих измерять ускорения, угловые скорости, магнитные поля, и давление ( 10 степеней свободы). Он обрабатывает эти данные и выдаёт набор чисел, которые отправляются на компьютер по USB. Там их считывает программа, написанная на матлабе и выдаёт картинку из заголовка.
При запуске программа определяет свой наклон при помощи акселерометра, азимут - по магнитометру, затем хитрым образом суммирует повороты по осям, компенсирует дрейф гироскопов и получает набор векторов XYZ.

Цели и задачи

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

  • трёхмерный базис, векторы которого сонаправлены с датчиком (B_{(3\times3)})

  • скорость относительно земли (\overrightarrow{v})

  • ускорение относительно земли (\overrightarrow{a}) и относительно датчика (\overrightarrow{a'})

  • азимут (\theta)

  • высота над уровнем моря или хотя бы над местом старта (alt)

  • время между измерениями акселерометра и гироскопа (\tau), магнитометра (\tau_m) и барометра (\tau_{alt})

  • угловые скорости (\varphi_x,\ \varphi_y,\ \varphi_z, то есть \overrightarrow{\varphi})

  • крен (\theta_r-roll) и тангаж (\theta_p-pitch) для удобоваримости

Затем подготовлю шаблон для настройки микроконтроллера (МК) и сделаю библиотеку для работы с конкретной моделью датчика.

Электронная начинка

Для проверки алгоритмов подготовил элетронную схему с датчиками и микроконтроллером. Главное устройство тут - это модуль 10 DOF IMU sensor (B) от Waveshare, включающий в себя

  • 3-x осевой акселерометр (до \pm156\ \text{м/с}^2)

  • 3-х осевой гироскоп (до \pm2000\ ^{\circ}/\text{с})

  • 3-х осевой магнитометр (до \pm4800 мкТл)

  • барометр (300\sim1100\ \text{гПа}, т.е +9000\sim-500 м)

  • термометр (даже)

Считыванием и обработкой данных занимается МК STM32F103C8T6, в народе Blue Pill. Работает на 72 МГц и не имеет ускорителя вычислений с плавающей точкой (FPU), поэтому считает относительно медленно. Он собирает по I2C данные с акселерометра и гироскопа каждые \tau=4500 мкc, а магнитометра - \tau_m=15000 мкс. С барометром я пока не работал. Результаты вычислений отправляются на ПК через USB-UART конвертер, где их ловит матлаб и выводит их на экран в виде векторов и индикаторов перегрузки.

Алгоритм работы и программная реализация

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

  • ускорение (\overrightarrow{a'})

  • давление (P)

  • угловые скорости \overrightarrow{\varphi'}

  • магнитная индукция \overrightarrow{B'}

Также измеряются \tau,\ \tau_m\,\ \tau_{alt}.

На акселерометр всегда (если он не падает) действует сила тяжести, которую он также регистрирует своими внутренними грузиками - \overrightarrow{g'}. По-хорошему он должен иметь значение \overrightarrow{g'}=(0, 0, -9.8), но это когда датчик правильно понимает совою ориентацию. Поэтому оригинальную силу тяжести назову отдельно - \overrightarrow{g}.

g?

Тут я уточню, что понимаю, что сила обозначается буквой\overrightarrow{F}, а g. - это ускорение свободного падения. Но МЭМС устроен так, что вычисляет ускорение на основе смещения груза, пропорционального\overrightarrow{F} = m\cdot \overrightarrow{a}. Следовательно, если датчик покоится в гравитационном поле, то на его грузик действует сила тяжести, которая неразличима для встроенного микроконтроллера, и возвращается в виде ускорения.
Из двух путаниц выберу меньшую и буду называть силу тяжести ускорением g (чтобы складывать два ускорения, а не силу с ускорением).

Частым термином будет базис B_{(3\times3)}(поправьте, если правильно называть иначе). Под ним я подразумеваю три ортонормированных вектора датчика, как-то ориентированных в пространстве. Например, \overrightarrow{z'} направлен вверх, \overrightarrow{x'} - на север. Получится единичная матрица матрица. Если же \overrightarrow{x'} смотрит на запад, получится так:

B_{(3\times3)}=\begin{matrix} \begin{matrix} x'& y'& z' \end{matrix} \\ \begin{pmatrix}0& 1& 0\\  -1& 0& 0\\  0& 0& 1\end{pmatrix} \end{matrix} \begin{matrix} векторы\ датчика \\проекция\ на\ север\\ проекция\ на\ восток \\проекция\ на\ вертикаль \end{matrix}

Также часто будут использоваться матрицы поворота:
вокруг \overrightarrow{z}:

\mathrm{rotz}(\phi)=\begin{pmatrix}  \cos(\phi)& \sin(\phi)& 0\\  -\sin(\phi)& \cos(\phi)& 0\\  0& 0& 1\end{pmatrix}

вокруг \overrightarrow{x}:

\mathrm{rotx}(\phi)=\begin{pmatrix}  1& 0& 0\\  0& \cos(\phi)& \sin(\phi)\\  0& -\sin(\phi)& \cos(\phi)\end{pmatrix}

вокруг \overrightarrow{y}:

\mathrm{roty}(\phi)=\begin{pmatrix}  \cos(\phi)& 0& -\sin(\phi)\\  0& 1 & 0\\  \sin(\phi)& 0& \cos(\phi)\end{pmatrix}

вокруг произвольного \overrightarrow{l}:

\mathrm{rotl}(\phi, \overrightarrow{l})=\begin{pmatrix}    c+(1-c)l_x^2& (1-c)l_xl_y-sl_z& (1-c)l_zl_x+sl_y\\    (1-c)l_xl_y+sl_z& c+(1-c)l_y^2& (1-c)l_zl_y-sl_x\\    (1-c)l_xl_z-sl_y& (1-c)l_yl_z+sl_x& c+(1-c)l_z^2\end{pmatrix}

где

c=\cos(\phi),\ s = \sin(\phi), \overrightarrow{l}=(l_x, l_y, l_z),\ |\overrightarrow{l}|=1

Математика

Тут начинается самое интересное. Преимущественно разработчиков, например летательный аппаратов, интересуют углы крена \theta_r и тангажа \theta_p. Их можно вычислять простым способом.
На акселерометр действует \overrightarrow{g}, который как-то проецируется на a_x, a_y, a_z, получается \overrightarrow{a'}.

\theta_p=\arcsin \big(\frac{a_x}{|\overrightarrow{a'}|}\big),\ \theta_r=-\arcsin \big(\frac{a_y}{|\overrightarrow{a'}|}\big).

Но возникает проблема. При поступательном ускорении оно складывается с силой тяжести:

\overrightarrow{a'}=\overrightarrow{a}+\overrightarrow{g},

и \theta_r, \theta_p меняются. В таком виде формулу использовать нельзя. Отсюда появляется потребность использовать гироскоп для вычисления поворотов. Однако надо задать начальное положение, которое можно получить написанным верхнему способом, оставив устройство смирно полежать несколько секунд.

Тут стоит отметить, что гироскоп микроэлектромеханический и гироскоп механический - работают по-разному. Второй - это маховик с осью на нескольких шарнирах, который старается сохранить свою ориентацию (или как-то так, надо разобраться). МЭМС гироскоп же возвращает какую-то среднюю скорость вращения вокруг оси за период измерения. Поэтому не получится узнать текущие наклоны, в любой момент обратившись к нему. Надо регулярно вычислять повороты и как-то суммировать их.

\overrightarrow{\phi}=\overrightarrow{\varphi}\cdot\tau=(\phi_x,\ \phi_y,\ \phi_z)

Но мне не нравится представление ориентации в виде трёх (+рысканье) углов, потому что оно понятно только оператору РУ самолётика. Да и сложно представить, что происходит с азимутом, когда нос смотрит вверх, какой угол равен 180^\circ при полёте брюхом кверху - тангаж или крен. И почему. А компьютеру это не объяснить и подавно. Надо бы найти универсальное решение: опуститься к естественному представлению тел в нашем трёхмерном пространстве и обратиться к линейной алгебре.

Так вот. Надо как-то складывать повороты, которые (по крайней мере, в моём представлении) некоммутативные. Однако для малых углов этим можно пренебречь. Поворачиваю я так:

B_{(3\times3)k+1} = B_{(3\times3)k}\cdot \big(\mathrm{rotx}(\tau_k\cdot\varphi_{xk})\cdot \mathrm{roty}(\tau_k\cdot\varphi_{yk})\cdot \mathrm{rotz}(\tau_k\cdot\varphi_{zk})\big)

Оказалось, что если перемножать в таком порядке rotx(\phi_{x}) \cdot B_{(3\times3)}, то базис повернётся вокруг абсолютного вектора \overrightarrow{z}. А если наоборот, то вокруг своего \overrightarrow{z'}.

Все эти повороты начинаются с изначального положения, которое обязательно не вертикально и обязательно не на север. Надо это положение восстановить, и я воспользуюсь испробованным методом.

При включении МК делает пару сотен опросов акселерометра, усредняет показания, получает \overrightarrow{a'}, который должен равняться \overrightarrow{g}. Но выходит

\overrightarrow{g} \neq B_{(3\times3)} \cdot \overrightarrow{a'}.

Третье - это данность, первое даже не обсуждается. Неверным остаётся второй элемент. Надо его исправить.

Датчик какой-то стороной наклонён к земле, значит надо наклонить его обратно, то есть повернуть вокруг какой-то оси на угол отклонения. К тому же ось должна проходить через 0 и быть нормальной к обоим векторам.
Ось вот такая:

\overrightarrow{l}=\frac{\overrightarrow{a'}\times\overrightarrow{g}}{|\overrightarrow{a'}\times\overrightarrow{g}|}

Такой угол:

\phi=\arccos \bigg(\frac{\overrightarrow{a'}\cdot\overrightarrow{g}}{|\overrightarrow{a}|\cdot|\overrightarrow{g}|}\bigg)

Такой поворот:

B_{(3\times3)} = \mathrm{rotl}(\phi, \overrightarrow{l}) \cdot B_{(3\times3)}

Дабы уменьшить погрешности в будущем, МК перезапоминает силу тяжести:

\overrightarrow{g} = (0,\ 0,\ - |\overrightarrow{a'}|)
Про азимут

Аналогичным образом настраивается ориентация по азимуту, только в роли \overrightarrow{g} выступает \overrightarrow{N} = (1, 0, 0), а \overrightarrow{a'} - это \overrightarrow{B} = B_{(3\times3)} \cdot \overrightarrow{B'} с обнулённой вертикальной составляющей. А вращается вокруг \overrightarrow{z}.
Впрочем я немного вру, т.к. магнитометр приходится калибровать, а я давно не делал это. Поэтому временно не использую компас, и если с ним возникнет путаница, не обращайте, пожалуйста, внимание.

Чтобы получить оригинальное ускорение, надо вычесть силу тяжести:

\overrightarrow{a} = \overrightarrow{a'} + \overrightarrow{g}

Теперь можно вращать датчик и любоваться его отображением в матлабе. Но вот ещё одна проблема. Гироскопы сложно откалибровать, и они очень шумные. К тому же от резких поворотов, у них случаются перегрузы, и все скорости выше порога обрезаются. От этого накапливаются ошибки, и вектора постепенно уплывают. Поэтому необходимо периодически корректировать ориентацию. Делается это описанным выше способом, но чтобы было плавнее, базис доворачивается не на весь угол за раз, а на какую-то подобранную долю угла (в моём случае - 0.02), а функция выполняется при 9.6<|\overrightarrow{a'}|<10.

На этом математика в программе заканчивается. Сюда не вошли вычисление скорости и высоты, т.к. над этим ещё предстоит работать.

Алгоритму есть куда улучшаться, но это прототип, и остались нерешённые проблемы, связанные с точностью вычислений и подготовкой устройства к работе.

Прототип

Изначально все чудеса происходили в матлабе. Мобильное приложение собирало показания с датчиков на телефоне в массив, сохраняло в облачный файл, который затем открывался в матлабовском скрипте. В нём я прототипировал алгоритм, что было очень удобно. Произошёл прув оф консепт.

Следующим шагом стало наблюдение в реальном времени. МК общался с датчиком и передавал на ПК сырые данные, которые обрабатывались в матлабовском скрипте. Очень удобно было проводить там калибровку (расскажу позже). Но в радиоуправляемую подводную лодочку ноут не положишь, а к микрокомпьютерам доверия нет, тк даже гоношный ноут напрягается от такой нагрузки. Поэтому стал переносить вычисления на маленький МК .

Программа для ПК

Приложение принимает 10 байт, 9 из которых - это координаты трёх векторов, помноженные на 100 (векторы в длину меньше 1, а передавать single precision вчетверо дольше), а последний хранит 2 бита для индикаторов перегруза датчиков.

Пока что не делал проверку целостности и двустороннее общение, т.к. это демонстрационная программа. Но когда буду писать утилиту для настройки, обязательно займусь этим.

Программа для МК

Реализация

Не буду подробно рассказывать, как я инициализировал периферию и как осуществляется общение, это можно посмотреть на гите.

Алгоритм можно было перенести построчно. Но готовых функций работы с матрицами у меня не было. Первая мысль - воспользоваться С компайлером и получить готовый С код. Но это тоже оказалось муторно, поэтому я решил написать свои матлаб-лайк матричные функции.

Мне были необходимы:

  • переменная, хранящая произвольный набор float чисел и информацию об их порядке

  • умножение на число

  • сумма двух матриц

  • умножение двух матриц

  • транспонирование

  • определитель

  • векторное произведение

  • скалярное произведение

  • нормирование вектора

  • модуль вектора

  • чтение элемента m_{ij}

  • запись элемента

  • создание матрицы m \times n

  • создание единичной матрицы n\times n

  • вычисление угла между векторами

  • матрицы поворота

Матлабовского должно быть вот что:

  • матрица выглядит как одна переменная

  • функции проверяют возможность выполнения операции

  • результат возвращается в виде числа, либо записывается в переменную, указанную в аргументах

  • переменные для результатов не приходится готовить вручную, а функция сама распоряжается памятью для сохранения результата, записывает её параметры матрицы

Матрица у меня - это структура:

struct matrix {
    float *arr;
    uint8_t rows;
    uint8_t cols;
    uint8_t size; // rows*cols чтобы не вычислять каждый раз
};

Типичная функция выглядит так:

int matrixSum(matrix* input1, matrix* input2, matrix* output) {
    if (input1->cols != input2->cols || input1->rows != input2->rows)
        return 1; // проверяю, что размеры совпадают
    if (output->cols != input2->cols || output->rows != input2->rows) { // если размер выходной матрицы не соответствует
        float* tmp = (float*) malloc(sizeof(float) * input1->size); // то создаю новую
        for (int i = 0; i < input1->size; i++) {
            tmp[i] = input1->arr[i] + input2->arr[i];
        }
        matrixRenewArray(output, input1->rows, input1->cols, tmp);
    }
    else {
        for (int i = 0; i < input1->cols * input1->rows; i++) { // или же сразу пишу в выходную
            output->arr[i] = input1->arr[i] + input2->arr[i]; 
        }
    }
    return 0; // всё ок
}

Таким образом, можно на ходу перезаписывать матрицы, не заморачиваясь их размерами.
Я прототипировал в Visual studio на C++. Но у МК память ограничена, а избегать её переполнения я ещё не научился. Поэтому сделал всем матрицам массив по 9 элементов. На удобстве это почти не сказалось, зато могу при компиляции прикинуть количество занятой RAM.

Что касается производительности. Общение с датчиками почти не занимает процессорное время благодаря DMA и прерываниям. В основном цикле нет делэев а есть отсчёт времени между операциями и статусы выполнения. Самым долгим делом пока являются описанные вычисления - до 900 мкс, что при периоде опроса в 4500 мкс оставляет много времени на другие задачи. К тому же речь идёт про один из самых простых МК этой серии. При использовании FPU, вычисления выполняются вдвое быстрее.

Проблемы

Перечислю недостатки, которые я ожидаю.

Корявые вычисления с плавающей точкой

Пока что считаю, что существенный вклад в накопление ошибки вносит интерпретация вещественных чисел компьютером. Значения тригонометрических функций иррациональные, а они в свою очередь коряво преобразуются в двоичное число. Это приводит к тому, что базисы перестают быть ортонормированными. Кажется, это не очень существенная проблема, но (как мне кажется) в корне не решаемая. Однако я могу периодически фиксировать эту ошибку и исправлять базис, теряя при этом частичку информации.

Частота измерений датчика и интерполяция

Надо решить, зачем и как интерполировать получаемые данные. Я пока что не проводил подробных анализов, но предполагаю, что период измерений достаточно большой, чтобы значения между точками сильно различались. Поэтому использовать формулу \phi_{k}=\tau_k\cdot\varphi_{k} -- неудачное решение. Для повышения точности можно хотя бы усреднять соседние измерения, то ест вот так: \phi_{k}=\tau_k\cdot0.5(\varphi_{k}+\varphi_{k-1}) (метод трапеций).
Попробую прикинуть матмодель и найти компромисс между частотой измерений, скоростью вычислений и качеством результата.

Калибровка

Эти микросхемы несовершенные. Датчики могут иметь различную чувствительность, и вообще располагаться не перпендикулярно. А таких датчиков 9. А производитель, видимо, не проводит выходные испытания, и не поставляет покупателю информацию для компенсации. Поэтому придётся придумывать процесс самостоятельно. Поделюсь новостями сразу как появятся результаты.


Планы на будущее

В самом конце обращу внимание на название - я хочу сделать инерциальный трекер. То есть, устройство, которое может отслеживать своё перемещение. Это чуть интереснее, чем просто электронный гироскоп. Я конечно же не ожидаю от него точности, как от GPS, достаточно трёхмерной траектории за последние несколько секунд. Набор таких можно, например, прикрепить к одежде и использовать для передачи движения аватару в метавселенной использовать для стабилизации балансирующего робота.

Комментарии (17)


  1. a9d
    06.09.2023 12:37
    +1

    У всех таких систем проблема с тем, что ошибка накапливается. Без решения этой проблемы у тебя треккер не получится сделать.


    1. he_projectile Автор
      06.09.2023 12:37

      Понимаю
      Я уточнил, что хотел бы отслеживать траекторию за несколько секунд. Например, между пакетами от жипиэс модуля.
      Или же прикрепить датчики к частям тела, нарисовать кинематическую модель человека и определять взаимные расроложения членов. Даже видел рекламу чего-то подобного. В узком смысле это тоже будет трекер, например, руки относительно туловища.


      1. NutsUnderline
        06.09.2023 12:37

        Например, между пакетами от жипиэс модуля

        Это dead readkon есть в GPS модулях UBlox и еще не помню какой фирмы (LC29 вроде модули)

         прикрепить датчики к частям тела, нарисовать кинематическую модель человека и определять взаимные расроложения членов

        есть вот такое https://github.com/SlimeVR/SlimeVR-Tracker-ESP оно вроде немножко по другому но близко


  1. Kotofay
    06.09.2023 12:37

    Был отличный открытый проект mahowii, для того чтобы разобраться самое то.

    https://github.com/mahowik/mahowii


    1. he_projectile Автор
      06.09.2023 12:37

      Спасибо
      После первых самостоятельных результатов можно и чужие работы поизучать


  1. da-nie
    06.09.2023 12:37
    +3

    Вам надо просто комплементарный фильтр взять. А лучше фильтр Мэджвика. Он работает в кватернионах.


    Вот фильтр Мэджвика. Где-то я его брал когда-то. Не помню уже, где.
    /*
     * Madgwick AHRS Arduino library
     * Math author: Sebastian Madgwick
     * Written by Oleg Evsegneev for RobotClass.  
     */
    extern volatile float beta;
    extern volatile float q0, q1, q2, q3;
    
    void MadgwickAHRSupdate(float tdelta, float gx, float gy, float gz, float ax, float ay, float az, float mx, float my, float mz);
    void MadgwickAHRSupdateIMU(float tdelta, float gx, float gy, float gz, float ax, float ay, float az);
    float invSqrt(float x);
    void quat2Euler( float q[4], float e[3] );

    /*
     * Madgwick AHRS Arduino library
     * Math author: Sebastian Madgwick
     * Written by Oleg Evsegneev for RobotClass.  
     */
    #include "madgwickahrs.h"
    #include <math.h>
    
    #define betaDef    0.2f    // 2 * proportional gain
    
    volatile float beta = betaDef; // 2 * proportional gain (Kp)
    volatile float q0 = 1.0f, q1 = 0.0f, q2 = 0.0f, q3 = 0.0f;  // quaternion of sensor frame relative to auxiliary frame
    
    void MadgwickAHRSupdate(float tdelta, float gx, float gy, float gz, float ax, float ay, float az, float mx, float my, float mz) {
      float recipNorm;
      float s0, s1, s2, s3;
      float qDot1, qDot2, qDot3, qDot4;
      float hx, hy;
      float _2q0mx, _2q0my, _2q0mz, _2q1mx, _2bx, _2bz, _4bx, _4bz, _2q0, _2q1, _2q2, _2q3, _2q0q2, _2q2q3, q0q0, q0q1, q0q2, q0q3, q1q1, q1q2, q1q3, q2q2, q2q3, q3q3;
      float _8bx, _8bz;
    
      // Use IMU algorithm if magnetometer measurement invalid (avoids NaN in magnetometer normalisation)
      if ((mx == 0.0f) && (my == 0.0f) && (mz == 0.0f)) {
        MadgwickAHRSupdateIMU(tdelta, gx, gy, gz, ax, ay, az);
        return;
      }
    
      // Rate of change of quaternion from gyroscope
      qDot1 = 0.5f * (-q1 * gx - q2 * gy - q3 * gz);
      qDot2 = 0.5f * (q0 * gx + q2 * gz - q3 * gy);
      qDot3 = 0.5f * (q0 * gy - q1 * gz + q3 * gx);
      qDot4 = 0.5f * (q0 * gz + q1 * gy - q2 * gx);
    
      // Compute feedback only if accelerometer measurement valid (avoids NaN in accelerometer normalisation)
      if (!((ax == 0.0f) && (ay == 0.0f) && (az == 0.0f))) {
    
        // Normalise accelerometer measurement
        recipNorm = invSqrt(ax * ax + ay * ay + az * az);
        ax *= recipNorm;
        ay *= recipNorm;
        az *= recipNorm;
    
        // Normalise magnetometer measurement
        recipNorm = invSqrt(mx * mx + my * my + mz * mz);
        mx *= recipNorm;
        my *= recipNorm;
        mz *= recipNorm;
    
        // Auxiliary variables to avoid repeated arithmetic
        _2q0mx = 2.0f * q0 * mx;
        _2q0my = 2.0f * q0 * my;
        _2q0mz = 2.0f * q0 * mz;
        _2q1mx = 2.0f * q1 * mx;
        _2q0 = 2.0f * q0;
        _2q1 = 2.0f * q1;
        _2q2 = 2.0f * q2;
        _2q3 = 2.0f * q3;
        _2q0q2 = 2.0f * q0 * q2;
        _2q2q3 = 2.0f * q2 * q3;
        q0q0 = q0 * q0;
        q0q1 = q0 * q1;
        q0q2 = q0 * q2;
        q0q3 = q0 * q3;
        q1q1 = q1 * q1;
        q1q2 = q1 * q2;
        q1q3 = q1 * q3;
        q2q2 = q2 * q2;
        q2q3 = q2 * q3;
        q3q3 = q3 * q3;
    
        // Reference direction of Earth's magnetic field
        hx = mx * q0q0 - _2q0my * q3 + _2q0mz * q2 + mx * q1q1 + _2q1 * my * q2 + _2q1 * mz * q3 - mx * q2q2 - mx * q3q3;
        hy = _2q0mx * q3 + my * q0q0 - _2q0mz * q1 + _2q1mx * q2 - my * q1q1 + my * q2q2 + _2q2 * mz * q3 - my * q3q3;
        _2bx = sqrt(hx * hx + hy * hy);
        _2bz = -_2q0mx * q2 + _2q0my * q1 + mz * q0q0 + _2q1mx * q3 - mz * q1q1 + _2q2 * my * q3 - mz * q2q2 + mz * q3q3;
        _4bx = 2.0f * _2bx;
        _4bz = 2.0f * _2bz;
        _8bx = 2.0f * _4bx;
        _8bz = 2.0f * _4bz;
    
        // Gradient decent algorithm corrective step
        s0 = -_2q2 * (2.0f * (q1q3 - q0q2) - ax) + _2q1 * (2.0f * (q0q1 + q2q3) - ay) - _4bz * q2 * (_4bx * (0.5 - q2q2 - q3q3) + _4bz * (q1q3 - q0q2) - mx) + (-_4bx * q3 + _4bz * q1) * (_4bx * (q1q2 - q0q3) + _4bz * (q0q1 + q2q3) - my) + _4bx * q2 * (_4bx * (q0q2 + q1q3) + _4bz * (0.5 - q1q1 - q2q2) - mz);
        s1 = _2q3 * (2.0f * (q1q3 - q0q2) - ax) + _2q0 * (2.0f * (q0q1 + q2q3) - ay) - 4.0f * q1 * (2.0f * (0.5 - q1q1 - q2q2) - az) + _4bz * q3 * (_4bx * (0.5 - q2q2 - q3q3) + _4bz * (q1q3 - q0q2) - mx) + (_4bx * q2 + _4bz * q0) * (_4bx * (q1q2 - q0q3) + _4bz * (q0q1 + q2q3) - my) + (_4bx * q3 - _8bz * q1) * (_4bx * (q0q2 + q1q3) + _4bz * (0.5 - q1q1 - q2q2) - mz);
        s2 = -_2q0 * (2.0f * (q1q3 - q0q2) - ax) + _2q3 * (2.0f * (q0q1 + q2q3) - ay) + (-4.0f * q2) * (2.0f * (0.5 - q1q1 - q2q2) - az) + (-_8bx * q2 - _4bz * q0) * (_4bx * (0.5 - q2q2 - q3q3) + _4bz * (q1q3 - q0q2) - mx) + (_4bx * q1 + _4bz * q3) * (_4bx * (q1q2 - q0q3) + _4bz * (q0q1 + q2q3) - my) + (_4bx * q0 - _8bz * q2) * (_4bx * (q0q2 + q1q3) + _4bz * (0.5 - q1q1 - q2q2) - mz);
        s3 = _2q1 * (2.0f * (q1q3 - q0q2) - ax) + _2q2 * (2.0f * (q0q1 + q2q3) - ay) + (-_8bx * q3 + _4bz * q1) * (_4bx * (0.5 - q2q2 - q3q3) + _4bz * (q1q3 - q0q2) - mx) + (-_4bx * q0 + _4bz * q2) * (_4bx * (q1q2 - q0q3) + _4bz * (q0q1 + q2q3) - my) + (_4bx * q1) * (_4bx * (q0q2 + q1q3) + _4bz * (0.5 - q1q1 - q2q2) - mz);
        recipNorm = invSqrt(s0 * s0 + s1 * s1 + s2 * s2 + s3 * s3);
    
        // normalise step magnitude
        s0 *= recipNorm;
        s1 *= recipNorm;
        s2 *= recipNorm;
        s3 *= recipNorm;
    
        // Apply feedback step
        qDot1 -= beta * s0;
        qDot2 -= beta * s1;
        qDot3 -= beta * s2;
        qDot4 -= beta * s3;
      }
    
      // Integrate rate of change of quaternion to yield quaternion
      q0 += qDot1 * tdelta;
      q1 += qDot2 * tdelta;
      q2 += qDot3 * tdelta;
      q3 += qDot4 * tdelta;
    
      // Normalise quaternion
      recipNorm = invSqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3);
      q0 *= recipNorm;
      q1 *= recipNorm;
      q2 *= recipNorm;
      q3 *= recipNorm;
    }
    
    void MadgwickAHRSupdateIMU(float tdelta, float gx, float gy, float gz, float ax, float ay, float az) {
      float recipNorm;
      float s0, s1, s2, s3;
      float qDot1, qDot2, qDot3, qDot4;
      float _2q0, _2q1, _2q2, _2q3, _4q0, _4q1, _4q2 , _8q1, _8q2, q0q0, q1q1, q2q2, q3q3;
    
      // Rate of change of quaternion from gyroscope
      qDot1 = 0.5f * (-q1 * gx - q2 * gy - q3 * gz);
      qDot2 = 0.5f * (q0 * gx + q2 * gz - q3 * gy);
      qDot3 = 0.5f * (q0 * gy - q1 * gz + q3 * gx);
      qDot4 = 0.5f * (q0 * gz + q1 * gy - q2 * gx);
    
      // Compute feedback only if accelerometer measurement valid (avoids NaN in accelerometer normalisation)
      if (!((ax == 0.0f) && (ay == 0.0f) && (az == 0.0f))) {
        // Normalise accelerometer measurement
        recipNorm = invSqrt(ax * ax + ay * ay + az * az);
        ax *= recipNorm;
        ay *= recipNorm;
        az *= recipNorm;
    
        // Auxiliary variables to avoid repeated arithmetic
        _2q0 = 2.0f * q0;
        _2q1 = 2.0f * q1;
        _2q2 = 2.0f * q2;
        _2q3 = 2.0f * q3;
        _4q0 = 4.0f * q0;
        _4q1 = 4.0f * q1;
        _4q2 = 4.0f * q2;
        _8q1 = 8.0f * q1;
        _8q2 = 8.0f * q2;
        q0q0 = q0 * q0;
        q1q1 = q1 * q1;
        q2q2 = q2 * q2;
        q3q3 = q3 * q3;
    
        // Gradient decent algorithm corrective step
        s0 = _4q0 * q2q2 + _2q2 * ax + _4q0 * q1q1 - _2q1 * ay;
        s1 = _4q1 * q3q3 - _2q3 * ax + 4.0f * q0q0 * q1 - _2q0 * ay - _4q1 + _8q1 * q1q1 + _8q1 * q2q2 + _4q1 * az;
        s2 = 4.0f * q0q0 * q2 + _2q0 * ax + _4q2 * q3q3 - _2q3 * ay - _4q2 + _8q2 * q1q1 + _8q2 * q2q2 + _4q2 * az;
        s3 = 4.0f * q1q1 * q3 - _2q1 * ax + 4.0f * q2q2 * q3 - _2q2 * ay;
        recipNorm = invSqrt(s0 * s0 + s1 * s1 + s2 * s2 + s3 * s3); // normalise step magnitude
        s0 *= recipNorm;
        s1 *= recipNorm;
        s2 *= recipNorm;
        s3 *= recipNorm;
        // Apply feedback step
        qDot1 -= beta * s0;
        qDot2 -= beta * s1;
        qDot3 -= beta * s2;
        qDot4 -= beta * s3;
      }
    
      // Integrate rate of change of quaternion to yield quaternion
      q0 += qDot1 * tdelta;
      q1 += qDot2 * tdelta;
      q2 += qDot3 * tdelta;
      q3 += qDot4 * tdelta;
    
      // Normalise quaternion
      recipNorm = invSqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3);
      q0 *= recipNorm;
      q1 *= recipNorm;
      q2 *= recipNorm;
      q3 *= recipNorm;
    }
    
    float invSqrt(float x) {
      float halfx = 0.5f * x;
      float y = x;
      long i = *(long*)&y;
      i = 0x5f3759df - (i >> 1);
      y = *(float*)&i;
      y = y * (1.5f - (halfx * y * y));
      y = y * (1.5f - (halfx * y * y));
      return y;
    }
    
    void quat2Euler( float q[4], float e[3] ) {
      double sqx = q[1] * q[1];
      double sqy = q[2] * q[2];
      double sqz = q[3] * q[3];
      e[0] = atan2f(2.f * (q[2] * q[3] + q[1] * q[0]), 1 - 2.f * (sqx + sqy)); // -sqx - sqy + sqz + sqw);
      e[1] = asinf(-2.f * (q[1] * q[3] - q[2] * q[0]));
      e[2] = atan2f(2.f * (q[1] * q[2] + q[3] * q[0]), 1 - 2.f * (sqy + sqz)); //sqx - sqy - sqz + sqw);
    }

    Тут две функции — с магнитометром и без.


    Вот использование без магнитометра.
    dt- период обработки в секундах, обратно частоте вызова фильтра.
    gx,gy,gz — гироскопы
    ax,ay,az — акселерометры


    float imu[3];
    float quat[4];
    MadgwickAHRSupdateIMU(dt,GRAD_TO_RAD(gx),GRAD_TO_RAD(gy),GRAD_TO_RAD(gz),ax,ay,az);
    quat[0]=q0;
    quat[1]=q1;
    quat[2]=q2;
    quat[3]=q3;
    quat2Euler(&quat[0],&imu[0]);


    float ma1=RAD_TO_GRAD(imu[0]);//крен
    float ma2=RAD_TO_GRAD(imu[1]);//тангаж
    float ma3=RAD_TO_GRAD(imu[2]);//рысканье


    Эту же задачу решают и фильтром Калмана, но тут у меня нет готового решения.
    Ну и ещё есть задача управления двигателями аппарата — тут уже регуляторы (ПИД, наверное) потребуются.


    Имейте в виду, все гироскопы и акселерометры должны быть привязаны друг к другу, отмасштабированы и без смещений. Для привязки можно использовать матрицы привязки (строятся по трём углам вращения). Но вот процедура калибровки этих углов дома может оказаться сложной.


  1. pistoletov
    06.09.2023 12:37
    +1

    Очень интересная идея. Подобная штука есть в устройстве woo для трекинга прыжков кайтсерферов. Вот насколько она точно работает проверить не довелось. Был еще стартап shadowbox для подобного но он так и не был реализован вроде.


  1. da-nie
    06.09.2023 12:37

    Кстати, качество ваших гироскопов и акселерометров вам покажет девиация Аллана (обычно, график есть в даташите). Выбирайте датчики с лучшими характеристиками! Вот тут описано.



    1. utya
      06.09.2023 12:37

      В даташитах Аллан был только в датчиках стоимость от 500 евро. Все что до 20 баксов одна маленькая строка, аля шум. Алана можно самому посчитать. Собрать лог на 2-4 чтобы наверняка. И любой скрипт гуглиться легко, matlab allan mems


      1. da-nie
        06.09.2023 12:37

        Не знаю, сколько сейчас стоят CRM200/300 и adxl355, но год назад стоили меньше 500 евро. Но и не 20$ точно. С дешёвыми датчиками ничего хорошего не выйдет.


        Алана можно самому посчитать.

        Дома — только для довольно грубых датчиков. Для прецизионных вам потребуется хорошая развязка от колебаний и температурных деформаций. Иначе получатся левые цифры.


  1. lazy-fox
    06.09.2023 12:37

    Кватернионы разве не решают задачу в теории? На практике в авиации и судоходстве давно используют инерционную навигацию.


    1. he_projectile Автор
      06.09.2023 12:37

      Решают

      Но на первом курсе их не преподают, а руки уже чесались.


      1. da-nie
        06.09.2023 12:37

        А специальность-то какая? А то и на остальных курсах их тоже преподавать не будут.


        1. he_projectile Автор
          06.09.2023 12:37

          Информатика и вычислительная техника

          Преподавать не будут, верно. Значит, сам разберусь


  1. utya
    06.09.2023 12:37
    +1

    О сново инерциалка на хабре. И Яндекс свою приблуду выпустил и Ваша статья. Фильтр мэджвика, маст хэв. Книга Матвеева маст хэв и распопава чтобы понять како гэ мэмс. Ну там уже фильтры Калмана, слабо-связные и т.д -:)


  1. TDen
    06.09.2023 12:37

    Главная проблема в том что акселерометры в мемс датчиках имеют довольно большую погрешность. Пусть она много меньше процента, но это вторая производная от положения все-таки. Ошибка акселерометра в 0.1% даст через минуту ошибку по положению больше 30 метров. Так что скорости измерять через такие датчики весьма проблематично. А вот для вычисления ориентации вполне годится.


    1. da-nie
      06.09.2023 12:37

      Там, строго говоря, нужен не акселерометр, а гироинтегратор.