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

С начала я изучил основы, а именно - посмотрел что там придумали китайцы, а придумали они "полётные контроллеры", в основу которых входит микроконтроллер (в основном STM32), гироскоп, барометр и т.д. В принципе, подумал я, всё выглядит довольно просто, значит, можно повторить.

Итак, мой путь начался с выбора начинки нашего "полётника". Я взял за основу микроконтроллер STM32F103C8T6, расположенный на распаянной плате (blue pil). В периферию: микросхему MPU6050 (3 осевой гироскоп и акселерометр) разведенную на плате под кодовым названием (GY-521), BMP280 (датчик давления), HMC5883L (3-осевой цифровой компас) распаянный на плате (модуль GY-273). Для передачи и приёма я использую MRF49XA (трансивер). В последствии всё будет выпаяно и припаяно по месту назначению, а пока ограничимся макетной платой.

И так начнём, для работы с камнем я буду использовать STM32CubeMX (библиотека HAL), а для редактирования прослойки будем юзать STM32CubeIDE. Почему именно эти проги, во-первых, они официальные с поддержкой STM, во-вторых, имеют привлекательный и понятный интерфейс, а как же большое обилие примеров для изучения. Для дебагинга я использую USART, но в иделае надо бы юзать ST LINK (поэтому не экономим и берём вместе с blue pil-ом).

Приступим-с! Открываем STM32CubeMX и выбираем наш МК. Начинаем настраивать его, а именно, для начала включим внешнее тактирование (ведь чем больше частота - тем быстрее работает МК).

На плате blue pil кварц уже припаян к нужным ножкам, его надо только подключить.
На плате blue pil кварц уже припаян к нужным ножкам, его надо только подключить.

Переходи во вкладку Clock Configuration, вписываем значение нашего кварца, в моём случае это 8. После ищем клеточку с надписью HCLK и вписываем 72 (72 MHz - максимальная частота при кварце на 8 MHz). Программа сама подстроит оставшиеся настройки.

Достаточно удобно.
Достаточно удобно.

Так как я буду использовать интерфейс I2C для общения, то нам надо его включить. Как вы видите, я использую Fast Mode, но в принципе его можно и не использовать. (влияет только на скорость общения)

клацк  - клацк
клацк - клацк

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

  • HAL_StatusTypeDef HAL_I2C_Master_Transmit_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size)

  • HAL_StatusTypeDef HAL_I2C_Master_Receive_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size)

Либо с помощью DMA(Direct Memory Access) - прямой доступ к памяти. Прикол этой штуки в том, чтобы распараллелить программу и облегчить жизнь CPU, то есть DMA общается по I2C, а основной процесс обрабатывает полученные данные.

И как вы поняли, я выбрал 2 способ. И так приступим к настройке. Так как я буду только считывать данные, то будем юзать RX. Все настройке на картинке.

Так же не забываем включить прерывания, чтобы знать, когда DMA сделает своё дело

клацк - клацк
клацк - клацк

Включаю USART для дебага.

Не обязательно
Не обязательно

И так пока остановимся, ведь мне надо пояснить некоторые детали, о которых я должен был ещё сказать в начале статьи, а именно то, что сейчас мы подготавливаем наш МК для работы с MPU6050. Поэтому мы и используем I2С, однако это не все плюшки микросхемы, прошу вас обратить внимание на вывод INT. Он нужен для того, чтобы подавать сигналы мастеру, к примеру, о готовности данных. Удобно! Я тоже так думаю, поэтому нам надо настроить наш МК, чтобы он захватывал эти сигналы, для этого настроим таймер.

not клацк - клацк
not клацк - клацк

Активируем TIM1, ибо он самый мясистый.

Теперь пробежимся по пунктам, и первым идёт режим управления таймером Master/Slave.

Суть заключается в том, чтобы при возникновении тех или иных событий таймер мог посылать различные триггеры (сигналы) другим таймерам — режим Master.

А таймер, получающий сигнал от другого таймера, является подчинённым — режим Slave.

Следовательно, Slave Mode - этот пункт, указывающий, что должен делать таймер, находясь в подчинённом режиме, в нашем случае, таймер будет подчинён сам себе, то есть, совершая захват сигнала, он будет генерировать тригер, и на основании этого сигнала, обнуляет счётчик (Reset Mode — означает, что при поступлении сигнала таймер должен обнулить счётчик) - это нам и нужно. Есть также и другие функции, если кому нужно:

  • Gated Mode — таймер работает пока есть сигнал высокого уровня, и останавливается, когда поступает сигнал низкого уровня.

  • Trigger Mode — счётчик запускается пока есть сигнал высокого уровня, НО не сбрасывается.

  • External Clock Mode 1 — таймер будет триггерится как на внешний сигнал, так и на внутренний.

Trigger Source — а этот пункт указывает что будет служить триггерным сигналом для таймера в моём случае TI1FP1, а так смотрим на картинку.

Идём дальше, и тут стоит заметить, что у каждого таймера есть четыре независимых канала, которые могут подключаться к физическим пинам микроконтроллера, а могут и не подключаться, работая как внутренние входы/выходы. Поэтому при настройке двух каналов (direct и indirect) активировался только один вход (РА8). Зачем мы это сделали? Чтобы первый канал ловил передний фронт, а второй — задний, тем самым мы и измерим длину импульса.

Чтобы не марать ручки, делаем так, чтобы измерения сигнала происходили аппаратно. Для этого, настраиваем DMA, не забывая включить циклический режим.

клацк - клацк
клацк - клацк

Осталось дело за малым, смотрим на картинку. Тут особо менять ничего не надо, но вы проверьте чтобы всё сходилось, а я лишь остановлюсь на одном пункте: Prescaler - предделитель частоты таймера (частоты поступающей с шины APB2). И тут важно отметить! Поскольку счётчик начинает отсчёт с нуля, то предделитель должен быть на 1 меньше. В нашем случае 72мГц / 71 = 1000000 тиков в секунду.

важно
важно

И так с настройкой камня покончено, перейдем к самой микросхеме (MPU6050). И первым делом почитаем даташит и карту регистров. Пойдём по порядку, сначала у нас идёт SELF_TEST, мы его оперативно скипаем ибо мы и сами сможем вычислить среднее значение погрешности. Далее у нас идёт несколько пунктов (SMPLRT_DIV, CONFIG, GYRO_CONFIG, ACCEL_CONFIG), которые нам понадобится для правильной работы датчика. Реализуем это в программе для этого создадим функцию инициализации. В ней мы с помощью стандартной функции общения по I2C (HAL_I2C_Mem_Write()) будем устанавливать начальные параметры работы.

void InitMPU6050(void) {     

        uint8_t data;
        data = 0;
        HAL_I2C_Mem_Write(&hi2c1, MPU6050_Address, PWR_MGMT_1_REG, 1, &data, 1, time);

        data = 0x07;
        HAL_I2C_Mem_Write(&hi2c1, MPU6050_Address, SMPLRT_DIV_REG, 1, &data, 1, time);
 
        data = 0x18;
        HAL_I2C_Mem_Write(&hi2c1, MPU6050_Address, ACCEL_CONFIG_REG, 1, &data, 1, time);

        data = 0x18;
        HAL_I2C_Mem_Write(&hi2c1, MPU6050_Address, GYRO_CONFIG_REG, 1, &data, 1, time);

        data = 0x1;
        HAL_I2C_Mem_Write(&hi2c1, MPU6050_Address, INT_ENABLE_REG, 1, &data, 1, time);

}
  • PWR_MGMT_1_REG - регистр отвечающий за питание микросхемы (0x6B).

  • SMPLRT_DIV_REG - регистр устанавливает частоту работы по формуле Sample Rate = Gyroscope Output Rate(8kHz) / (1 + SMPLRT_DIV) отправляя значение 7, в итоге получаем частоту работы = 1kHz (0x19).

  • GYRO_CONFIG_REG - регистр настраивающий гироскоп, в моём случае получаем 0x18 = 11000 , что соответствует ±2000°/s(0x1B).

  • ACCEL_CONFIG_REG - регистр настраивающий акселерометр, в моём случае получаем 0x18 = 11000 , что соответствует ±16g(0x1C).

  • INT_ENABLE_REG - регистр разрешающий подавать импульсы на вывод INT, в нашем случае по обновлению данных измерений (0x38).

Теперь научимся захватывать наши импульсы, с помощью колбека (он срабатывает при прерывании от DMA) HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim){} после мы проверяем тот ли таймер захватил сигнал, если тот то действуем.

uint32_t zntime;

/* USER CODE BEGIN 2 */
  HAL_TIM_IC_Start_DMA(&htim1, TIM_CHANNEL_2, (uint32_t*)&zntime, 1);//включаем таймер     
/* USER CODE END 2 */

/* USER CODE BEGIN 4 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) 
{
        if(htim->Instance == TIM1)
        {
           HAL_I2C_Mem_Read_DMA(&hi2c1, MPU6050_Address, ACCEL_XOUT_H_REG, 1, data, 14);
        }
}
/* USER CODE END 4 */

Далее будем считывать готовые данные с помощью DMA, для этого нам понадобятся регистры с 3B (ACCEL_XOUT_H_REG) по 48, то есть за раз нам надо считать 14 регистров. Чтение будет выполняться с помощью функции HAL_I2C_Mem_Read_DMA() и с помощью прерывания по окончанию передачи по I2C void HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef *hi2c)

/* USER CODE BEGIN 4 */
void HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef *hi2c)
{
	  MPU6050Read();
}
/* USER CODE END 4 */

Также стоит отметить, что данные будут состоять из двух 8 битных частей, которые надо будет склеить в 16 битное число, а так же для наглядности высчитаем pitch.

pitch - тангаж
pitch - тангаж
typedef struct
{

    int16_t AccelX;
    int16_t AccelY;
    int16_t AccelZ;
    double aX;
    double aY;
    double aZ;

    int16_t GyroX;
    int16_t GyroY;
    int16_t GyroZ;
    double gX;
    double gY;
    double gZ;

    int16_t temp;
    int Temperature;

} MPU6050znach;// можно с помощью переменных или как тут


MPU6050znach z;


void MPU6050Read(void)
{
  ///////////////////////////склейка/////////////////
        z.AccelX = (int16_t)(data[0] << 8 | data[1]);
        z.AccelY = (int16_t)(data[2] << 8 | data[3]);
        z.AccelZ = (int16_t)(data[4] << 8 | data[5]);
        z.temp = (int16_t)(data[6] << 8 | data[7]);
        z.GyroX = (int16_t)(data[8] << 8 | data[9]);
        z.GyroY = (int16_t)(data[10] << 8 | data[11]);
        z.GyroZ = (int16_t)(data[12] << 8 | data[13]);

/////////////////////////////обработка////////////////////
        z.aX = z.AccelX / 2048.0;
        z.aY = z.AccelY / 2048.0;
        z.aZ = z.AccelZ / 2048.0;
        z.Temperature = (int)((int16_t)z.temp / (float)340.0 + (float)36.53);
        z.gX = z.GyroX / 131.0;
        z.gY = z.GyroY / 131.0;
        z.gZ = z.GyroZ / 131.0;

 /////////////////////////вычисление////////////////////
        int pitch;
        int pitch_sqrt = sqrt(z.aY * z.aY +  z.aZ * z.aZ);
        pitch = atan2(-z.aX, pitch_sqrt) * RAD_TO_DEG;

  /////////////////////////вывод/////////////////
         snprintf(msg, sizeof(msg), "%d", pitch);
           HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY);
           HAL_UART_Transmit(&huart1, (uint8_t*)"\r\n", strlen("\r\n"), HAL_MAX_DELAY);
           HAL_UART_Transmit(&huart1, (uint8_t*)"\r\n", strlen("\r\n"), HAL_MAX_DELAY);
}

И тадам получаем угол отклонения, однако значения достаточно сырые, ибо должно было получится где-то -90 градусов.

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

закройте окно - шумно!
закройте окно - шумно!

Для борьбы с этим недугом есть 2 пути решения использовать фильтр (к примеру Кальмана) или DMP (это небольшая програмка вшитая в MPU6050, которая сглаживает значения). И об этом мы поговорим в следующей статье, а так же подключим HMC5883L (3-осевой цифровой компас). На этом всё, до скорого!

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


  1. FGV
    03.11.2021 11:36
    +1

    z.aX = z.AccelX / 16384.0;

    z.aY = z.AccelY / 16384.0;

    z.aZ = z.AccelZ / 14418.0;

    у канала z другой масштабный коэф.?


    1. zzomda Автор
      03.11.2021 12:23

      Да, в даташите табличка есть для всех значений.


      1. FGV
        03.11.2021 13:00

        Да, в даташите табличка есть для всех значений.

        И в каком месте? Для "MPU-6000 and MPU-6050 Product Specification Revision 3.4" у всех каналов аксика коэффициенты одинаковы... по 13-й листу цены мл. разряда в зависимости от AFS_SEL:

        AFS_SEL=0: k=1.0/16384.0 (диапазон +/-2g)

        AFS_SEL=1: k=1.0/8192.0 (диапазон +/-4g)

        AFS_SEL=2: k=1.0/4096.0 (диапазон +/-8g)

        AFS_SEL=3: k=1.0/2048.0 (диапазон +/-16g)


        1. zzomda Автор
          03.11.2021 13:02

          Извиняюсь, перепутал.


  1. sami777
    03.11.2021 12:20

    Когда ж вы наконец то перестанете включать USART для дебага на стм32?


    1. zzomda Автор
      03.11.2021 12:23
      +1

      Что вы можете предложить в замен этому?


      1. Sl-Alex
        03.11.2021 14:18

        Serial Wire Viewer (SWV) вроде как должен нормально работать через SWO pin


        1. zzomda Автор
          03.11.2021 14:20

          Изучу этот вопрос по подробнее.


  1. OvO
    03.11.2021 12:23
    +1

    STM32F103C8T6 трудно найти в дикой природе за разумные деньги, в магазинах водится только STM32x0 серии. На смену MPU6050 пришел mpu9250 со встроенным компасом, а для новых разработок рекомендуется использовать icm20948.


    1. zzomda Автор
      03.11.2021 12:27

      Насчёт STM32 не согласен взял её рублей за 200 на алике, а так в принципе интересное предложение, возьму на заметку.


      1. OvO
        03.11.2021 12:38

        За Bluepill - не сомневаюсь, только не уверен, что там stm32. А вот новый STM32Fx для собственной платы будет стоить 7-15$.


        1. zzomda Автор
          03.11.2021 12:43

          Мне и Bluepill-а с головой хватает.


    1. DanilinS
      03.11.2021 14:22
      +1

      Сейчас вообще очень сложно найти что-то из STM32 за разумные деньги. Все как метлой вымели ...


      1. OvO
        03.11.2021 16:21
        +1

        Сейчас STM32[G,L]0 можно отхватить за ~2$ за штуку, но это без USB. STM32G4 LQFP месяц назад были за ~4$, а сейчас только остатки в *BGA корпусах. Или китайские аналоги.


        1. DanilinS
          03.11.2021 17:11
          +1

          Все, что сейчас можно отхватить на али - очень часто отбраковка производства.

          Заказывал 2 платы на али на STM32G431 у разных продавцов. Первая имела не рабочий DAC, на второй плата чип вообще через ST-Link не виделся.


          1. OvO
            04.11.2021 12:21

            Я говорил про евроамериканские магазины:mouser, arrow, digikey, rs и др. В начале октября забрал десяток STM32G431 в Mouser по 4$ - их с начала года много было по всем магазинам, а сейчас почти нет. STM32[G,L]0 пока в ассортименте от 2$.

            На aliexpress покупать под пайку компоненты - рисковое дело. Привезут дрянь или пустышки и ничего не докажешь. Где-то видел статью по разновидности обмана на aliexpress и отбраковка еще не самый плохой вариант.


  1. FGV
    03.11.2021 14:02

    int pitch;

    int pitch_sqrt = sqrt(z.aY * z.aY + z.aZ * z.aZ);

    pitch = atan2(-z.aX, pitch_sqrt) * RAD_TO_DEG;

    Странно у Вас тангаж считается.

    1) Почему инты? что 89.9 градусов или .99g не может быть?

    2) Крен/тангаж обычно считаются как atan2(Ax,Ay)/atan2(Az,Zy), откуда такая волшебная формула приведенная выше? В чем ее математический/физический смысл? Может чертежик нарисовать с вектором тяжести и как он по осям аксика раскладывается написать?


    1. zzomda Автор
      03.11.2021 14:20


      1. FGV
        04.11.2021 11:56

        Да, запамятовал, действительно так. Но почему типы данных - инты?


        1. zzomda Автор
          05.11.2021 12:57

          Для примера и так неплохо.


    1. zzomda Автор
      03.11.2021 14:31

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


  1. vladimirad
    03.11.2021 22:02

    ВЫчитай те ошибки, в идеали.


  1. Shinsei75
    05.11.2021 12:54

    Про MPU6050_Address и time можно по подробнее?


    1. zzomda Автор
      05.11.2021 12:55

      MPU6050_Address - адресс устройства прописан в даташите.

      time - время ожидания ответа, может быт любым, на ваше усмотрение. У меня 500.


      1. Shinsei75
        05.11.2021 16:14

        Спасибо за ответ.

        Т.е. Вы сделали хедер файл со всеми адресами из даташита


        1. zzomda Автор
          05.11.2021 19:01

          Да!


      1. Shinsei75
        05.11.2021 18:50

        И еще один вопрос. Я наверное глуп, но как data у Вас превратилась в массив, если изначально она объявлена как простая переменная uint8_t?


        1. zzomda Автор
          05.11.2021 19:02

          Одна переменная объявлена внутри функции, то есть она распространяется только в ней. А другая уже объявлена глобально, то есть распространяется на весь код.


  1. Userpc0101
    05.11.2021 23:31

    Почему MPU6050? Этот IMU довольно редко встречается в дронах чаще MPU6000. Обычно для борьбы с шумом в датчике применяется ещё DLPF (ФНЧ). Ну и т.к. у вас летающий аппарат вы уже пробовали делать алгоритм слияния данных с GNSS?


    1. zzomda Автор
      07.11.2021 11:44

      Что было под рукой, как говорится! Насчёт DLPF, интересная тема, обязательно учту


      1. Userpc0101
        07.11.2021 12:50

        Да, вероятно вам придётся бороться с вибрациями на аппарате и если они будут достаточно сильными придётся снижать частоту опроса и увеличивать размер окна фильтра таким образом, что-бы амплитуда колебаний от вибрации полностью поместилась в размерность окна фильтра, тогда даже при сильных вибрациях датчик будет показывать правильный результат, это конечно увеличит задержку и возможно значительно на 20-30мс но это единственный выход в такой ситуации как мне кажется.


        1. FGV
          08.11.2021 06:43

          … даже при сильных вибрациях датчик будет показывать правильный результат…

          Что значит правильный? Без составляющей от вибровоздействия?