LSM303 не может компенсировать наклон

esp8266 sensors magnetometer calibration

Я работаю над компасом с компенсацией наклона с LSM303DLHC. Мне удается откалибровать магнитную и ускорительную часть. Мне удалось рассчитать тангаж и крен. Но когда дело доходит до расчета компенсации наклона компаса, что-то идет не так. Мои значения на последовательном мониторе просто не имеют смысла ... Если я вычисляю направление компаса из калиброванных значений магнето, он работает с использованием оси Y в качестве магнитного вектора. Как бы я мог изменить это на ось X?

   LSM303DLHC including accelerometer to correct for position

*/
#include <Wire.h>


const int LSM303_ADDR = 0x19;
const int LSM303_ADDR_MAG = 0x1E;

// адреса регистров управления
const int CTRL_REG1_ADDR = 0x20;
const int CTRL_REG2_ADDR = 0x21;
const int CTRL_REG3_ADDR = 0x22;
const int CTRL_REG4_ADDR = 0x23;
const int CTRL_REG5_ADDR = 0x24;
const int CTRL_REG6_ADDR = 0x25;

const int CRA_REG_M_ADDR = 0x00;
const int CRB_REG_M_ADDR = 0x01;
const int MR_REG_M_ADDR = 0x02;

//регистр данных адресует сначала младший бит для акселератора, первый старший бит для магнето
const int Accelero_First_data_addr = 0x28;
const int Magneto_First_data_addr = 0x03;
const int Temp_out_data_addr = 0x31;// первые 12 бит MSB


int CTRL_REG1_A_value = 0x47; // 50Hz Low Power отключен, все оси включены
int CTRL_REG2_A_value = 0x00; //
int CTRL_REG3_A_value = 0x00; //нет прерываний или водяных знаков
int CTRL_REG4_A_value = 0x00; // Полная шкала 2G, высокое разрешение, режим SPI не выбран
int CTRL_REG5_A_value = 0x00; // ничего не включено
int CTRL_REG6_A_value = 0x00; // ничего не включено

int CRA_REG_M_value = 0x14; // Датчик температуры включен, скорость передачи данных 15 Гц
int CRB_REG_M_value = 0x20; // 2G — это данные 1100–980 G/LSB

int MR_REG_M_value = 0x00;

int16_t MagRaw_X_axis_value, MagRaw_Y_axis_value, MagRaw_Z_axis_value;
int16_t AccRaw_X_axis_value, AccRaw_Y_axis_value, AccRaw_Z_axis_value;

float pitch, roll;
float Xm_calibrated, Ym_calibrated, Zm_calibrated;
float Xm_norm, Ym_norm, Zm_norm, Xa_norm, Ya_norm, Za_norm;
float norm_a, norm_m;

void setup() {// поместите сюда код установки для однократного запуска:
  Serial.begin(9600);
  Wire.begin();
  setupLSM303();
  Serial.println("Setup complete..");
}

void loop() {// поместите сюда свой основной код для повторного запуска:
  MagnetoDataRead();
  AcceleroDataRead();

}



void setupLSM303() {
  Wire.beginTransmission(LSM303_ADDR);
  Wire.write(CTRL_REG1_ADDR);
  Wire.write(CTRL_REG1_A_value);
  Wire.endTransmission();
  Wire.beginTransmission(LSM303_ADDR);
  Wire.write(CTRL_REG2_ADDR);
  Wire.write(CTRL_REG2_A_value);
  Wire.endTransmission();
  Wire.beginTransmission(LSM303_ADDR);
  Wire.write(CTRL_REG3_ADDR);
  Wire.write(CTRL_REG3_A_value);
  Wire.endTransmission();
  Wire.beginTransmission(LSM303_ADDR);
  Wire.write(CTRL_REG4_ADDR);
  Wire.write(CTRL_REG4_A_value);
  Wire.endTransmission();
  Wire.beginTransmission(LSM303_ADDR);
  Wire.write(CTRL_REG5_ADDR);
  Wire.write(CTRL_REG5_A_value);
  Wire.endTransmission();
  Wire.beginTransmission(LSM303_ADDR);
  Wire.write(CTRL_REG6_ADDR);
  Wire.write(CTRL_REG6_A_value);
  Wire.endTransmission();
  //настройка магниторегистра
  Wire.beginTransmission(LSM303_ADDR_MAG);
  Wire.write(CRA_REG_M_ADDR);
  Wire.write(CRA_REG_M_value);
  Wire.endTransmission();
  Wire.beginTransmission(LSM303_ADDR_MAG);
  Wire.write(CRB_REG_M_ADDR);
  Wire.write(CRB_REG_M_value);
  Wire.endTransmission();
  Wire.beginTransmission(LSM303_ADDR_MAG);
  Wire.write(MR_REG_M_ADDR);
  Wire.write(MR_REG_M_value);
  Wire.endTransmission();
}


void MagnetoDataRead() {
  Wire.beginTransmission(LSM303_ADDR_MAG );
  Wire.write(Magneto_First_data_addr);
  Wire.endTransmission();

  Wire.requestFrom(LSM303_ADDR_MAG , 6);
  if (Wire.available() <= 6) {
    MagRaw_X_axis_value = (Wire.read() << 8) | (Wire.read());
    MagRaw_Y_axis_value = (Wire.read() << 8) | (Wire.read());
    MagRaw_Z_axis_value = (Wire.read() << 8) | (Wire.read());

    float X_mag_cal = MagRaw_X_axis_value / 1100.0;
    float Y_mag_cal = MagRaw_Y_axis_value / 1100.0;
    float Z_mag_cal = MagRaw_Z_axis_value / 980.0;

    //вводим коэффициенты калибровки
    float Xm_offset_corrected = MagRaw_X_axis_value - 0.041423;
    float Ym_offset_corrected = MagRaw_Y_axis_value + 0.022448;
    float Zm_offset_corrected = MagRaw_Z_axis_value + 0.078354;

    Xm_calibrated = 94.622065 * Xm_offset_corrected + 0.831029 * Ym_offset_corrected + 1.647934 * Zm_offset_corrected;
    Ym_calibrated = 0.831029 * Xm_offset_corrected + 93.462985 * Ym_offset_corrected + 3.416885 * Zm_offset_corrected;
    Zm_calibrated = 1.647934 * Xm_offset_corrected + 3.416885 * Ym_offset_corrected + 76.477691 * Zm_offset_corrected;

    //формула нормализации
    norm_m = sqrt(sq(Xm_calibrated) + sq(Ym_calibrated) + sq(Zm_calibrated));
    Xm_norm = Xm_calibrated / norm_m;
    Ym_norm = Ym_calibrated / norm_m;
    Zm_norm = Zm_calibrated / norm_m;

    //заголовок с плавающей запятой = ((atan2(Ym_calibrated, Xm_calibrated)) * 180 ) / PI;
    //если (Заголовок < 0) {
    // Заголовок = 360 + Заголовок;
    //}
    /*
        Serial.print(X_mag_cal, 10);
        Serial.print("\t");
        Serial.print(Y_mag_cal, 10);
        Serial.print("\t");
        Serial.print(Z_mag_cal, 10);
        Serial.println("\t");
    */

    delay(50);

  }
}

void AcceleroDataRead() {
  Wire.beginTransmission(LSM303_ADDR);
  Wire.write(Accelero_First_data_addr | 0x80);
  Wire.endTransmission();

  Wire.requestFrom(LSM303_ADDR, 6);
  if (Wire.available() <= 6) {
    AccRaw_X_axis_value = (Wire.read()) | (Wire.read() << 8);
    AccRaw_Y_axis_value = (Wire.read()) | (Wire.read() << 8);
    AccRaw_Z_axis_value = (Wire.read()) | (Wire.read() << 8);

    float X_Acc =  (AccRaw_X_axis_value >> 4);
    float Y_Acc =  (AccRaw_Y_axis_value >> 4);
    float Z_Acc =  (AccRaw_Z_axis_value >> 4);
//калибровать смещение
    float X_acc_offset_corrected = X_Acc + 6.676443;
    float Y_acc_offset_corrected = Y_Acc - 6.622381;
    float Z_acc_offset_corrected = Z_Acc + 55.408804;
//калибровать
    float X_acc_calibrated = 0.977273 * X_acc_offset_corrected - 0.003458 * Y_acc_offset_corrected + 0.012639 * Z_acc_offset_corrected;
    float Y_acc_calibrated = -0.003458 * X_acc_offset_corrected + 0.979674 * Y_acc_offset_corrected - 0.001130 * Z_acc_offset_corrected;
    float Z_acc_calibrated = 0.012639 * X_acc_offset_corrected - 0.001130 * Y_acc_offset_corrected + 0.897083 * Z_acc_offset_corrected;
// нормализовать
    norm_a = sqrt(sq(X_acc_calibrated) + sq(Y_acc_calibrated) + sq(Z_acc_calibrated));
    Xa_norm = X_acc_calibrated / norm_a;
    Ya_norm = Y_acc_calibrated / norm_a;
    Za_norm = Z_acc_calibrated / norm_a;

// вычисляем тангаж и крен
    pitch = asin(-Xa_norm) * 180 / PI;
    roll = (atan2(Ya_norm, Za_norm) * 180) / PI;
//рассчитываем векторы для компенсации наклона
    float Xh = Xm_norm * cos(pitch * PI / 180) + Zm_norm * sin(pitch * PI / 180);
    float Yh = Xm_norm * sin(roll * PI / 180) * sin(pitch * PI / 180) + Ym_norm * cos(roll * PI / 180) - Zm_norm * sin(roll * PI / 180) * cos(pitch * PI / 180);
// вычислить курс с компенсацией наклона
    float Heading = ((atan2(Yh, Xh)) * 180.0 ) / PI;
    if (Heading < 0) {
      Heading = 360 + Heading;
    }

    Serial.print("Pitch \t");
    Serial.print(pitch);
    Serial.print("\t roll \t");
    Serial.print(roll);
    Serial.print("\t");
    Serial.println(Heading);

    delay(50);
  }
}

Я застрял на этом уже несколько дней. Любая помощь приветствуется!

, 👍-1

Обсуждение

Вы разрабатываете оценочный комплект STMicro? Если это так, вы можете использовать библиотеки STMicro MotionMC и MotionEC (внимание, без исходного кода!) для реализации наклонного компаса. Плохо, это форум Arduino - конечно, вы не используете комплект разработки STMicro!, @st2000

Кроме того, вы хотите наклонить свой компас сверх нормы? В смысле больше, чем, скажем, 30 градусов? Если вы хотите, чтобы ваш компас находился под углом 90 градусов относительно местной поверхности Земли, вам придется сделать больше, чем просто реализовать наклон компаса., @st2000

Привет! Спасибо за комментарий! Я разрабатываю NodeMcu с Arduino IDE. Мой компас не будет наклонен дальше таких углов. Формулы, используемые в моем коде, взяты из примечаний к приложению lsm303., @sanrays10

Хм, в другом чипе STMicro (lsm9ds1) один из векторов акк находится в обратном направлении WRT магнита. Я не думаю, что это проблема с LSM303. Могут ли у вас возникнуть проблемы с преобразованием в/из градусов/радианов? Это может сбивать с толку в зависимости от того, откуда вы получаете свою триггерную функцию. Кроме того (просто любопытно), вы держите все в углах Эйлера или используете кватернионы?, @st2000

Я где-то читал, что для dlhc магнитное поле Земли выровнено по оси Y. Позвольте мне еще раз проверить источник. Хотя это лишь частично проблема, которая у меня есть., @sanrays10

Не должно быть. В разделе 1.2 на рис. 2 спецификаций LSM303DLHC показаны lms303 acc X+ и mag X+ (а также acc Y+ и mag Y+) в одном направлении. Обратите внимание, что не все микросхемы STMicro настроены таким образом. Будьте осторожны, чтобы не использовать пример кода неправильного чипа!, @st2000

Извините, я действительно ошибся. Он выровнен по оси X. Я должен направить ось Y на землю, чтобы получить достоверные показания. Что в моем случае означает установку печатной платы вертикально. Хотя моя формула для заголовка взята из примечаний к применению..., @sanrays10

Это звучит неправильно. Все примеры компасов STMicro, которые я видел, имеют оси X и Y, параллельные поверхности земли, и Z, перпендикулярные поверхности земли. (Примечание: не подносите постоянные магниты близко к магнитометру. Я так и сделал, и мне пришлось выкопать старый инструмент для размагничивания, чтобы магнитометр снова заработал.), @st2000

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

Теперь, когда у меня работают магнитометр и акселерометр, кажется, что если я наклоняю компас, мои направления все еще колеблются... хотя я применяю формулы из примечания к приложению...., @sanrays10


2 ответа


Лучший ответ:

0

Кажется, у меня все получилось! Большое спасибо за вашу помощь, я должен вам несколько пива. Для всех, кто следит и интересуется, вот мой код:

   LSM303DLHC including accelerometer to correct for position
  Vector calculation to be implemented
*/
#include <Wire.h>
#include <Math.h>




const int LSM303_ADDR = 0x19;
const int LSM303_ADDR_MAG = 0x1E;

// адреса регистров управления
const int CTRL_REG1_ADDR = 0x20;
const int CTRL_REG2_ADDR = 0x21;
const int CTRL_REG3_ADDR = 0x22;
const int CTRL_REG4_ADDR = 0x23;
const int CTRL_REG5_ADDR = 0x24;
const int CTRL_REG6_ADDR = 0x25;

const int CRA_REG_M_ADDR = 0x00;
const int CRB_REG_M_ADDR = 0x01;
const int MR_REG_M_ADDR = 0x02;

//регистр данных адресует сначала младший бит для акселератора, первый старший бит для магнето
const int Accelero_First_data_addr = 0x28;
const int Magneto_First_data_addr = 0x03;
const int Temp_out_data_addr = 0x31;// первые 12 бит MSB


int CTRL_REG1_A_value = 0x47; // 50Hz Low Power отключен, все оси включены
int CTRL_REG2_A_value = 0x00; //
int CTRL_REG3_A_value = 0x00; //нет прерываний или водяных знаков
int CTRL_REG4_A_value = 0x00; // Полная шкала 2G, высокое разрешение, режим SPI не выбран
int CTRL_REG5_A_value = 0x00; // ничего не включено
int CTRL_REG6_A_value = 0x00; // ничего не включено

int CRA_REG_M_value = 0x14; // Датчик температуры включен, скорость передачи данных 15 Гц
int CRB_REG_M_value = 0x20; // 2G — это данные 1100–980 G/LSB

int MR_REG_M_value = 0x00;

int16_t MagRaw_X_axis_value, MagRaw_Y_axis_value, MagRaw_Z_axis_value;
int16_t AccRaw_X_axis_value, AccRaw_Y_axis_value, AccRaw_Z_axis_value;

float pitch, roll;
float Xm_calibrated, Ym_calibrated, Zm_calibrated;
float Xm_norm, Ym_norm, Zm_norm, Xa_norm, Ya_norm, Za_norm;
float norm_a, norm_m;

void setup() {// поместите сюда код установки для однократного запуска:
  Serial.begin(9600);
  Wire.begin();
  setupLSM303();
  Serial.println("Setup complete..");
}

void loop() {// поместите сюда свой основной код для повторного запуска:
  MagnetoDataRead();
  AcceleroDataRead();

}



void setupLSM303() {
  Wire.beginTransmission(LSM303_ADDR);
  Wire.write(CTRL_REG1_ADDR);
  Wire.write(CTRL_REG1_A_value);
  Wire.endTransmission();
  Wire.beginTransmission(LSM303_ADDR);
  Wire.write(CTRL_REG2_ADDR);
  Wire.write(CTRL_REG2_A_value);
  Wire.endTransmission();
  Wire.beginTransmission(LSM303_ADDR);
  Wire.write(CTRL_REG3_ADDR);
  Wire.write(CTRL_REG3_A_value);
  Wire.endTransmission();
  Wire.beginTransmission(LSM303_ADDR);
  Wire.write(CTRL_REG4_ADDR);
  Wire.write(CTRL_REG4_A_value);
  Wire.endTransmission();
  Wire.beginTransmission(LSM303_ADDR);
  Wire.write(CTRL_REG5_ADDR);
  Wire.write(CTRL_REG5_A_value);
  Wire.endTransmission();
  Wire.beginTransmission(LSM303_ADDR);
  Wire.write(CTRL_REG6_ADDR);
  Wire.write(CTRL_REG6_A_value);
  Wire.endTransmission();
  //настройка магниторегистра
  Wire.beginTransmission(LSM303_ADDR_MAG);
  Wire.write(CRA_REG_M_ADDR);
  Wire.write(CRA_REG_M_value);
  Wire.endTransmission();
  Wire.beginTransmission(LSM303_ADDR_MAG);
  Wire.write(CRB_REG_M_ADDR);
  Wire.write(CRB_REG_M_value);
  Wire.endTransmission();
  Wire.beginTransmission(LSM303_ADDR_MAG);
  Wire.write(MR_REG_M_ADDR);
  Wire.write(MR_REG_M_value);
  Wire.endTransmission();
}


void MagnetoDataRead() {
  Wire.beginTransmission(LSM303_ADDR_MAG );
  Wire.write(Magneto_First_data_addr);
  Wire.endTransmission();

  Wire.requestFrom(LSM303_ADDR_MAG , 6);
  if (Wire.available() <= 6) {
    MagRaw_X_axis_value = (Wire.read() << 8) | (Wire.read());
    MagRaw_Z_axis_value = (Wire.read() << 8) | (Wire.read());
    MagRaw_Y_axis_value = (Wire.read() << 8) | (Wire.read());

    float X_mag_cal = MagRaw_X_axis_value / 1100.0;
    float Y_mag_cal = MagRaw_Y_axis_value / 1100.0;
    float Z_mag_cal = MagRaw_Z_axis_value / 980.0;

    //вводим коэффициенты калибровки
    float Xm_offset_corrected = X_mag_cal - 0.032874;
    float Ym_offset_corrected = Y_mag_cal + 0.047876;
    float Zm_offset_corrected = Z_mag_cal + 0.039649;

    Xm_calibrated = 92.387197 * Xm_offset_corrected - 0.307374 * Ym_offset_corrected + 1.151625 * Zm_offset_corrected;
    Ym_calibrated = -0.307374 * Xm_offset_corrected + 85.002110 * Ym_offset_corrected + 1.169755 * Zm_offset_corrected;
    Zm_calibrated = 1.151625 * Xm_offset_corrected + 1.169755 * Ym_offset_corrected + 80.276037 * Zm_offset_corrected;

    //формула нормализации
    norm_m = sqrt(sq(Xm_calibrated) + sq(Ym_calibrated) + sq(Zm_calibrated));
    Xm_norm = Xm_calibrated / norm_m;
    Ym_norm = Ym_calibrated / norm_m;
    Zm_norm = Zm_calibrated / norm_m;

    /*
        Serial.print(X_mag_cal, 10);
        Serial.print("\t");
        Serial.print(Y_mag_cal, 10);
        Serial.print("\t");
        Serial.print(Z_mag_cal, 10);
        Serial.println("\t");
    */

    //заголовок с плавающей запятой = ((atan2(Ym_norm, Xm_norm)) * 180.0) / PI;
    //если (Заголовок < 0) {
    // Заголовок = 360 + Заголовок;
    //}
    //Serial.println(Заголовок);
    // задержка (50);

  }
}

void AcceleroDataRead() {
  Wire.beginTransmission(LSM303_ADDR);
  Wire.write(Accelero_First_data_addr | 0x80);
  Wire.endTransmission();

  Wire.requestFrom(LSM303_ADDR, 6);
  if (Wire.available() <= 6) {
    AccRaw_X_axis_value = (Wire.read()) | (Wire.read() << 8);
    AccRaw_Y_axis_value = (Wire.read()) | (Wire.read() << 8);
    AccRaw_Z_axis_value = (Wire.read()) | (Wire.read() << 8);

    float X_Acc =  (AccRaw_X_axis_value >> 4);
    float Y_Acc =  (AccRaw_Y_axis_value >> 4);
    float Z_Acc =  (AccRaw_Z_axis_value >> 4);

    //калибровать смещение
    float X_acc_offset_corrected = X_Acc + 6.676443;
    float Y_acc_offset_corrected = Y_Acc - 6.622381;
    float Z_acc_offset_corrected = Z_Acc + 55.408804;

    //калибровать
    float X_acc_calibrated = 0.977273 * X_acc_offset_corrected - 0.003458 * Y_acc_offset_corrected + 0.012639 * Z_acc_offset_corrected;
    float Y_acc_calibrated = -0.003458 * X_acc_offset_corrected + 0.979674 * Y_acc_offset_corrected - 0.001130 * Z_acc_offset_corrected;
    float Z_acc_calibrated = 0.012639 * X_acc_offset_corrected - 0.001130 * Y_acc_offset_corrected + 0.897083 * Z_acc_offset_corrected;
    // нормализовать
    norm_a = sqrt(sq(X_acc_calibrated) + sq(Y_acc_calibrated) + sq(Z_acc_calibrated));
    Xa_norm = X_acc_calibrated / norm_a;
    Ya_norm = Y_acc_calibrated / norm_a;
    Za_norm = Z_acc_calibrated / norm_a;

    // вычисляем тангаж и крен
    /*
      pitch = asin(-Xa_norm) * 180 / PI;
      roll = asin(Ya_norm / cos(pitch * PI / 180)) * 180 / PI;
    */
    roll = atan2(Ya_norm, Za_norm);
    float  Gz2 = Ya_norm * sin(roll) + Za_norm * cos(roll);
    pitch = atan(-Xa_norm / Gz2);
    float By2 = Zm_norm * sin(roll) - Ym_norm * cos(roll);
    float Bz2 = Ym_norm * sin(roll) + Zm_norm * cos(roll);
    float Bx3 = Xm_norm * cos(pitch) + Bz2 * sin(pitch);
    float yaw = (atan2(By2, Bx3)*180)/PI;
    if (yaw < 0) {
      yaw = 360 + yaw;
    }

    float tilt = atan2(sqrt((sq(Xa_norm)+(sq(Ya_norm)))),Za_norm);

    //рассчитываем векторы для компенсации наклона
    float Xh = Xm_norm * cos(pitch) + Zm_norm * sin(pitch);
    float Yh = Xm_norm * sin(roll ) * sin(pitch) + Ym_norm * cos(roll) - Zm_norm * sin(roll) * cos(pitch);

        // расчет курса с компенсацией наклона
        float Heading = ((atan2(Yh, Xh)) * 180.0 ) / PI;
        if (Heading < 0) {
          Heading = 360 + Heading;
        }


    Serial.print("Pitch \t");
    Serial.print(pitch * 180 / PI);
    Serial.print("\t roll \t");
    Serial.print(roll * 180 / PI);
    Serial.print("\t tilt \t");
    Serial.print(tilt*180/PI);
    Serial.print("\t");
    Serial.println(Heading);
    delay(500);
  }
} 
,

0

Нашел...Данные Magneto выходят XZY, а не XYZ...когда я это изменил, все заработало! Извините, RTFMA сразу бы дал мне ответ.

,

Рад слышать это. Но вы разместили ответ 10 часов назад и сделали комментарий, что он все еще не работает 8 часов назад. Что он?, @st2000

Извините за путаницу. Данные датчика для магнето и акселератора работают независимо. Но когда мне приходится комбинировать 2 для компенсации наклона... это не компенсирует..., @sanrays10

Попробуйте изменить знак вашего значения Z-нормаль, прежде чем использовать его в ваших уравнениях Xh и Yh (примерно за 15 строк до конца вашей программы). Если это сработает, я опубликую ответ, объясняющий, почему., @st2000

Я пробовал Za_norm = -Z_acc_calibrated/norm_a; но не повезло. Я также пытался изменить знак в формуле Xh... тоже не повезло..., @sanrays10

облом. Меня тоже интересует расчет тона. Это отличается от бумаги STMicro, на которую я смотрю. Вы используете [этот документ stmicro 2010](https://www.pololu.com/file/0J434/LSM303DLH-compass-app-note.pdf)? Я даже не могу найти его на веб-сайте STMicro. Попробуйте [этот документ stmicro 2018](https://www.st.com/content/ccc/resource/technical/document/design_tip/group0/56/9a/e4/04/4b/6c/44/ef/DM00269987/ files/DM00269987.pdf/jcr:content/translations/en.DM00269987.pdf). Похоже, расчет шага отличается. Вы можете распечатать значения Pitch и Roll для проверки., @st2000

Спасибо, что прислали мне эту ссылку! Это очень полезно. Расчеты по тангажу и крену были распечатаны и оказались правильными. Я попробую реализовать расчеты из документа, но для этого мне нужно время. На данный момент я уже могу сказать вам, что эти формулы также работают. На следующий шаг!, @sanrays10