HMC5883L (QMC5883L) — это цифровой трёхосевой магнитометр, разработанный компанией Honeywell (или его аналог QMC5883L от QST Corporation, Китай), который измеряет магнитное поле Земли по трём взаимно перпендикулярным осям (X, Y и Z) и используется как компас для определения направления движения. Такой датчик незаменим в системах навигации, робототехнике и мобильных устройствах, где требуется точное отслеживание ориентации в пространстве и компенсация магнитных помех.
В рамках данной статьи мы подробно рассмотрим устройство и принцип работы сенсора (конкретно модели QMC5883L), а также соберём в качестве примера компас на базе ESP32 с индикацией на TFT-дисплее (ST7735 или ST7789), который будет показывать направление в реальном времени с элементами калибровки и визуализацией стрелки компаса.

Содержание
- Отличие между HMC5883L и QMC5883L
- Устройство и принцип работы
- Параметры и характеристики
- QMC5883L в составе модуля GY-273 или HW-127
- Схема подключения
- Программный код (скетч)
- Эксперимент
- Алгоритм работы
- Заключение
Отличие между HMC5883L и QMC5883L
В процессе поиска магнитометра в Интернет-магазинах чаще всего попадается вот такой модуль:

На нём чётко написана модель микросхемы и вариант реализации платы (HW-127, иногда бывает GY-273). Логика подсказывает, что для программирования в Arduino IDE нужно искать библиотеку с соответствующим названием. Но с высокой вероятностью данные с датчика не будет считываться, как это произошло и у меня. А всё потому, что на плате была установлена вовсе не микросхема HMC, а QMC.
На сегодняшний день существует два однотипных датчика: HMC5883L (Honeywell, США) и QMC5883L (QST Corporation, Китай). Американский чип был разработан примерно в 2010 году и производился до 2014 года. А вот китайский аналог бал разработан предположительно к 2015 году как более дешёвая и несколько усовершенствованная версия американского оригинала, при этом производится до сих пор. И, скорее всего, практически все модули магнитометров, которые сейчас можно купить — это именно китайский сенсор. Но платы продолжают маркировать как HMC, потому как сенсор из Поднебесной позиционируется как клон американского чипа.
На самом деле между указанными преобразователями есть несколько важных отличий, на которые необходимо обратить внимание, поскольку даже с точки зрения программного кода сенсоры не являются полностью взаимозаменяемыми.
Для наглядности приведу фотографию из Интернета, где показывается разница в маркировке чипов:

На фотографии выше видно, что оригинального американского чипа маркировка начинает с «L883». Китайский чип имеет обозначение «DA 5883» (или как в моём случае — DB).
Ниже в таблице будут приведены базовые отличия между сенсорами:

Для более требовательных приложений рекомендуется рассмотреть следующие аналоги:
— MPU-9250: магнитометр AK8963 + акселерометр/гироскоп (9 осей для полной навигации);
— LIS3MDL: более современный и точный магнитометр с I2C/SPI;
— BMX055: 9 осей, низкое энергопотребление с интегрированным калибровочным алгоритмом.
Устройство и принцип работы
Работа интегрального магнитометра основана на анизотропной магниторезистивной (Anisotropic Magnetoresistive — AMR) лицензированной технологии Honeywell:
— анизотропная означает «неравномерная»: технология реагирует на магнитное поле по-разному в разных направлениях;
— магниторезистивная подразумевает наличие сопротивлений, реагирующих на поле. Внутри чипа есть тонкие металлические полоски (чувствительные элементы), которые изменяют своё электрическое сопротивление при воздействии поля;
— лицензированная технология Honeywell включает в себя специальный запатентованный способ изготовления чувствительных элементов. Датчики на базе этих элементов чувствительны только к полю вдоль своей оси (не путаются с полем под углом), а также обладают высокой линейностью измерений с минимальными искажениями.
Принцип работы преобразователя основан на магниторезистивном эффекте, объяснить который можно на простом примере. Представьте, что есть тонкий металлический проводник, как тонкая проволочка из специального сплава, например, пермаллой (смесь железа и никеля). Когда к этому проводнику приближается постоянный магнит, электрический ток в проводнике начинает реагировать на поле в виде изменения электрического сопротивления проводника. Это происходит потому, что в металле электроны (которые являются основными носителями заряда, и их упорядоченное движение под воздействием электрического поля называется электрическим током) под воздействием магнитного поля отклоняются от прямого пути, что затрудняет их движение и увеличивает сопротивление. Таким образом, изменение электрического сопротивления пропорционально изменению воздействующего извне поля.
Чтобы регистрировать это внешнее воздействие, в микросхеме используется четыре тонкие полоски магниторезистивного материала, соединённые в измерительный мост Уитстона (Wheatstone Bridge), о котором несколько подробнее описано в статье про схемы включения терморезисторов.
Измерительный мост Уитстона — это электрическая схема из резисторов, где выходное напряжение равно нулю, когда все плечи находятся в балансе. В тот момент, когда поле влияет только на одно плечо (одну полоску), баланс нарушается, и на выходе схемы напряжение отклоняется от ноля. Эта величина пропорциональна силе искомого магнитного поля.
Чувствительные элементы сенсоров изготавливаются из аморфного ферромагнитного сплава (железо-никель) с анизотропией (по одной оси сопротивление сильнее зависит от поля). Это делает его чувствительным именно к направлению поля.
При этом в сенсоре сразу три моста Уитстона, чтобы обеспечить регистрацию изменения поля сразу по трём осям. Дело в том, что в Мире магнитное поле имеет направление (вектор), и поэтому для полного описания нужны измерения вдоль трёх перпендикулярных осей: X (вперёд-назад), Y (влево-вправо), Z (вверх-вниз). Соответственно, в сенсоре имеется три независимых моста для каждой оси. Благодаря этому предоставляется возможность измерять магнитное поле от сотен нТл (нанотесла) до тысяч Гс (Гаусс), что подходит для земного магнетизма (около 25–65 мкТл в зависимости от местности).
Тесла (Тл) — основная единица измерения магнитной индукции (силы магнитного поля) в Международной системе единиц (СИ). Названа в честь Николы Тесла.
Измеряет силу воздействия поля на заряженные частицы. Например, магнитное поле Земли около 25…65 мкТл (микротесла), в зависимости от широты и долготы.
Гаусс (Гс) — устаревшая единица измерения в системе СГС, названа в честь Карла Гаусса. Поле Земли примерно 0,25…0,65 Гс.
1 Тл = 10 000 Гс или 1 Гс = 10⁻⁴ Тл
Рассматриваемая модель китайского магнитометра QMC представляет собой интегральную микросхему в корпусе LGA-16 (Land Grid Array). Ниже представлена общая структура чипа:

Сенсор состоит из следующих блоков:
— мост на основе анизотропного магнитосопротивления (AMR Bridge), который преобразует магнитное поле в электрический сигнал;
— мультиплексор (MUX), который по очереди подключается к одному из трёх чувствительных элементов, соответствующих своей оси X/Y/Z;
— программируемый усилитель сигнала (PGA);
— 16-разрядный аналогово-цифровой преобразователь (ADC), преобразующий аналоговый сигнал в цифровой;
— блок обработки сигнала (Signal Conditioning), который производит фильтрацию и корректировку оцифрованного сигнала, на основании чего выполняется внутренняя калибровка;
— регистр (Register) хранения данных и настроек микросхемы;
— схема сброса внутренних регистров в исходное состояние при подачи питания (POR);
— внутренний источник опорного напряжения (Reference) для работы АЦП;
— драйвер установки/сброса (SET/RST Driver ), управляющий состоянием микросхемы;
— температурный датчик (Temp. Sensor) для внутренней компенсации чувствительности и смещения;
— генератор тактовых импульсов (CLK Gen.) для обеспечения синхронизации микросхемы преобразователя и внешнего микроконтроллера;
— контроллер I2C-интерфейса для связи с внешним микроконтроллером;
— энергонезависимая память (NVM), в которой хранятся калибровочные данные и настройки.
Параметры и характеристики магнитометра QMC5885L

QMC5883L в составе платы (модуля) GY-271 / HW-127
Для экспериментов часто применяют популярную отладочную плату HW-127 (также часто обозначается как GY-271), на которой распаяна микросхема со всей необходимой обвязкой:

Данная плата содержит всю необходимую обвязку для функционирования с микросхемой сенсора, включая понижающий (с низким падением напряжения) линейный стабилизатор напряжения на 3,3 В. Однако, для полноценной работы модуля можно подавать на вход VCC и напряжение +3,3 В.
Ниже представлены таблица распиновки и принципиальная схема платы модуля HW-127:


В документации к микросхеме представлено изображение, которое иллюстрирует как соотносятся направления осей и корпус самой микросхемы (стрелка указывает направление магнитного поля):

Для корректного использования в приложениях компаса (определения направления Севера) важно знать ориентацию осей относительно корпуса микросхемы.
Ориентация осей:
— X направлена сверху вних, как читается надписи на корпусе чипа (первый пин расположен в верхнем левом углу корпуса, обычно отмечен выемкой или точкой). Другими словами: вниз, если смотреть на корпус сверху;
— Y перпендикулярна оси X. Если смотреть на корпус сверху, то направление оси Y направлено направо относительно первого пина;
— Z направлена перпендикулярно плоскости корпуса вверх (если смотреть на корпус сверху, то стрелка направлена прямо в лицо зрителю).
Чтобы использовать сенсор в качестве компаса (определения азимута, включая северное направление) микросхему необходимо расположить параллельно плоскости Земли (плоскости горизонта), чтобы ось Z была направлена вертикально вверх (перпендикулярно Земле). Другими словами: корпус чипа лежит плоско на плате (плата ориентирована горизонтально). Оси X и Y будут в горизонтальной плоскости, ось Z — вертикально вверх. Это обеспечивает измерение горизонтальной компоненты поля, которая соответствует направлению земного магнетизма.
Северное направление рассчитывается на основе данных по X и Y (Z используется для учёта наклона, если датчик комбинируется с акселерометром). Азимут (угол до Севера) вычисляется по формуле:
θ = θ_raw – (угол деклинации) + (угол поворота оси),
где θ_raw (в градусах) = atan2(Y, X) × (180/π) — это азимут по сырым данным, (180/π) нужно для перевода в градусы;
угол деклинации (в градусах) — это угол между магнитным Севером (на который указывает компас) и географическим Севером (условной точкой). Он зависит от местоположения наблюдателя на Земле;
угол поворота оси (в градусах) — это корректировка, если X не совпадает с направлением «вперёд» на устройстве, в котором используется магнитометр.
Если ось X направлена точно на Север, данные по X будут максимальными, Y — близкими к нулю.
Схема подключения QMC5883L к ESP32
Для построения компаса воспользуемся отладочной платой NodeMCU-32S (38 pin), которая построена на базе модуля ESP-WROOM-32. Для отображения стрелки курсового указателя применим TFT-дисплей на базе контроллера ST7789, диагональ экрана 1,9 дюйма, а разрешение — 170 × 320:

Ниже представлена таблица подключений сенсора и дисплея к отладочной плате.

Схема подключения и фотография собранного макета:


Программный код (скетч)
Проект создавался в среде программирования PlatformIO (расширение для редактора кода Visual Studio Code). Для взаимодействия с TFT-дисплеем воспользуемся библиотекой tft_eSPI, поскольку она, в отличие от Adafruit_ST7735_and_ST7789_Library, позволяет быстро перерисовывать изображение на экране, что практически незаметно для глаз.
О том, как сформировать проект в среде PlatformIO и настроить библиотеку tft_eSPI для корректного функционирования дисплея, можно подробно ознакомиться в статье про двухосевой джойстик KY-023.
Дополнительно укажу конкретные строчки, которые нужно проверить в конфигурационному файле User_Setup, при настройке дисплея:
— выбор драйвера

— разрешение экрана

— пины подключения к отладочной плате

— шрифты (этот блок обычно не меняют, оставляем по умолчанию

— тактовая частота (также оставляем по умолчанию)

Для взаимодействия с магнитометром мы создадим свои функции, поскольку работа с популярной библиотекой Adafruit_HMC5883L_Unified возникали проблемы (китайский чип QMC несколько отличается от американского HMC с точки зрения организации регистров).
Калибровка
Магнитометр измеряет параметры магнитного поля Земли (по трём осям X/Y/Z), что позволяет определять направление компаса. Однако сырые данные преобразователя подвержены искажениям и помехам, которые появляются из-за внешних факторов:
— рядом с сенсором были металлы (железо, сталь), которые «притягиваются» к полю и искажают его;
— постоянные магниты или намагниченные материалы поблизости (например, аккумулятор питания, батарея смартфона или металлический корпус устройства) добавляют постоянный сдвиг к данным;
— электромагнитные поля от проводов, моторов или даже солнечная активность могут добавлять шум.
Калибровка устраняет эти эффекты путём сбора значений по осям X и Y во время вращения датчика по оси Z и последующей нормализации данных. Это обеспечивает точное вычисление азимута, минимизируя ошибки и повышая надёжность показаний.
Перед тем, как запустить компас, необходимо произвести предварительную калибровку конкретного используемого экземпляра сенсора. Ниже представлен подробно прокомментированный код, который представляет собой программу для определения калибровочных коэффициентов, которым потом мы будем использовать.
// Код для калибровки магнитометра
#include <Arduino.h> // Подключаем библиотеку Arduino для базовых функций (например, Serial и delay)
#include <Wire.h> // Подключаем библиотеку Wire для работы с I2C (для общения с магнитометром)
// Определяем константы для адреса и регистров QMC5883L (магнитометра)
// Адрес устройства на шине I2C (стандартный для QMC5883L)
#define QMC5883L_ADDR 0x0D
// Регистр, из которого читаем данные (X, Y, Z)
#define QMC5883L_REG_DATA 0x00
// Регистры управления: CTRL1 - режим работы, CTRL2 - сброс, CTRL3 - режим измерений
#define QMC5883L_REG_CTRL1 0x09
#define QMC5883L_REG_CTRL2 0x0A
#define QMC5883L_REG_CTRL3 0x0B
// Определяем пины для I2C на ESP32 (стандартные пины для ESP32: SDA на 21, SCL на 22)
const int I2C_SDA = 21;
const int I2C_SCL = 22;
// Структура для хранения данных магнитометра: значения X, Y, Z и флаг успеха чтения
struct MagnetometerData {
int16_t x, y, z; // 16-битные целые числа для магнитных полей по осям (в единицах датчика)
bool success; // true, если чтение прошло успешно; false, если ошибка
};
// Глобальные переменные для калибровки: минимальные и максимальные значения по осям X и Y
// Инициализируем minX очень большим числом (32767 - максимум для int16_t), maxX - минимальным (-32768)
// Аналогично для Y. Это позволит находить реальные минимумы и максимумы во время калибровки.
int16_t minX = 32767, maxX = -32768;
int16_t minY = 32767, maxY = -32768;
// Вспомогательная функция для нахождения минимума из двух чисел
// Возвращает меньшее из a и b
int16_t min_val(int16_t a, int16_t b) {
return (a < b) ? a : b; // Если a меньше b, вернуть a; иначе b
}
// Вспомогательная функция для нахождения максимума из двух чисел
// Возвращает большее из a и b (исправлено: в оригинале была ошибка (a > a), теперь (a > b))
int16_t max_val(int16_t a, int16_t b) {
return (a > b) ? a : b; // Если a больше b, вернуть a; иначе b
}
// Функция инициализации QMC5883L
// Настраивает датчик для работы: устанавливает режим непрерывных измерений, сбрасывает и включает датчик
// Возвращает true, если инициализация успешна; false, если устройство не найдено или ошибка связи
bool initializeQMC5883L() {
// Проверяем, есть ли устройство по адресу QMC5883L_ADDR на шине I2C
// beginTransmission начинает передачу, endTransmission завершает и возвращает 0, если успех
Wire.beginTransmission(QMC5883L_ADDR);
if (Wire.endTransmission() != 0) return false; // Если не 0, устройство не отвечает - ошибка
// Устанавливаем CTRL1: значение 0x1D включает непрерывный режим измерений с частотой 200 Гц и разрешением 2 байта
Wire.beginTransmission(QMC5883L_ADDR);
Wire.write(QMC5883L_REG_CTRL1); // Указываем регистр CTRL1
Wire.write(0x1D); // Пишем значение для активации
if (Wire.endTransmission() != 0) return false; // Проверяем успех
delay(10); // Небольшая пауза для стабилизации
// Устанавливаем CTRL2: 0x00 - сброс и нормальный режим (без soft reset)
Wire.beginTransmission(QMC5883L_ADDR);
Wire.write(QMC5883L_REG_CTRL2);
Wire.write(0x00);
if (Wire.endTransmission() != 0) return false;
delay(10);
// Устанавливаем CTRL3: 0x01 - включает датчик в режим измерений
Wire.beginTransmission(QMC5883L_ADDR);
Wire.write(QMC5883L_REG_CTRL3);
Wire.write(0x01);
if (Wire.endTransmission() != 0) return false;
delay(10);
return true; // Все настройки прошли успешно
}
// Функция чтения данных с магнитометра
// Читает сырые значения X, Y, Z из датчика
// Возвращает структуру MagnetometerData с данными или с success=false при ошибке
MagnetometerData readMagnetometer() {
MagnetometerData data = {0, 0, 0, false}; // Инициализируем структуру с нулями и false
// Начинаем чтение: указываем регистр данных
Wire.beginTransmission(QMC5883L_ADDR);
Wire.write(QMC5883L_REG_DATA); // Регистр, с которого начнём читать
if (Wire.endTransmission() != 0) return data; // Если ошибка, возвращаем пустые данные
delay(10); // Ждём, пока датчик подготовит данные (необязательно, но для надёжности)
// Запрашиваем 6 байт данных (2 байта на каждую ось: X_low, X_high, Y_low, Y_high, Z_low, Z_high)
if (Wire.requestFrom(QMC5883L_ADDR, 6) == 6) { // Если получили ровно 6 байт
uint8_t xLow = Wire.read(); // Младший байт X
uint8_t xHigh = Wire.read(); // Старший байт X
uint8_t yLow = Wire.read(); // Младший байт Y
uint8_t yHigh = Wire.read(); // Старший байт Y
uint8_t zLow = Wire.read(); // Младший байт Z
uint8_t zHigh = Wire.read(); // Старший байт Z
// Собираем 16-битные значения: старший байт сдвигаем влево на 8 бит и объединяем с младшим
data.x = (int16_t)(xHigh << 8 | xLow);
data.y = (int16_t)(yHigh << 8 | yLow);
data.z = (int16_t)(zHigh << 8 | zLow);
data.success = true; // Устанавливаем флаг успеха
}
return data; // Возвращаем структуру с данными
}
// Функция калибровки магнитометра
// Сбор данных в течение 15 секунд, пока пользователь вращает устройство, чтобы найти диапазоны X и Y
// Выводит прогресс в Serial, а в конце - найденные минимумы и максимумы
void calibrateMagnetometer() {
Serial.println("Калибровка магнитометра..."); // Сообщение о начале калибровки
Serial.println("Вращайте устройство в обе стороны в течение 10-15 секунд"); // Инструкция для пользователя
int samples = 0; // Счётчик собранных образцов данных
unsigned long startTime = millis(); // Время начала калибровки (в миллисекундах)
// Цикл сбора данных на 15 секунд (15000 мс)
while (millis() - startTime < 15000) {
MagnetometerData data = readMagnetometer(); // Читаем данные с датчика
if (data.success) { // Если чтение успешно
// Обновляем минимумы и максимумы для X и Y (Z игнорируем, так как для компаса нужны только X и Y)
minX = min_val(minX, data.x); // minX = меньшему из текущего minX и нового data.x
maxX = max_val(maxX, data.x); // maxX = большему из текущего maxX и нового data.x
minY = min_val(minY, data.y);
maxY = max_val(maxY, data.y);
samples++; // Увеличиваем счётчик образцов
// Каждые 10 образцов выводим прогресс (чтобы не засорять Serial)
if (samples % 10 == 0) {
int progress = ((millis() - startTime) * 100) / 15000; // Процент завершения (от 0 до 100)
Serial.printf("Прогресс: %d%%\n", progress); // Вывод прогресса и количества образцов
}
}
delay(100); // Пауза 100 мс между чтениями (чтобы не перегружать I2C и дать время на вращение)
}
// После завершения выводим результаты калибровки
Serial.printf("minX: [%d]\n", minX); // Диапазон для X: минимум
Serial.printf("maxX: [%d]\n", maxX); // Диапазон для X: максимум
Serial.printf("minY: [%d]\n", minY); // Диапазон для Y: минимум
Serial.printf("maxY: [%d]\n", maxY); // Диапазон для Y: максимум
Serial.println("Скопируйте эти значения в Ваш основной код для компаса"); // Инструкция: скопировать эти значения в основной код компаса
}
// Функция setup: выполняется один раз при запуске ESP32
void setup() {
Serial.begin(115200); // Инициализируем Serial для вывода сообщений на скорость 115200 бод
Serial.println("Калибровка магнитометра QMC5883L"); // Приветственное сообщение
// Инициализируем I2C с указанными пинами и частотой 100 кГц (стандартная для большинства датчиков)
Wire.begin(I2C_SDA, I2C_SCL);
Wire.setClock(100000);
delay(1000); // Ждём 1 секунду для стабилизации I2C
// Пытаемся инициализировать QMC5883L
if (initializeQMC5883L()) {
Serial.println("Инициализация QMC5883L прошла успешно. Старт калибровки..."); // Успех: начинаем калибровку
calibrateMagnetometer(); // Запускаем функцию калибровки
} else {
Serial.println("Ошибка: QMC5883L не найден. Проверьте подключение."); // Ошибка: проверь подключение
}
}
// Функция loop: выполняется бесконечно после setup (здесь ничего не делаем, так как калибровка однократная)
void loop() {
delay(1000); // Пауза 1 секунда (чтобы процессор не нагружался зря)
}
Скомпилируйте код и прошейте отладочную плату. После перезагрузки начнётся процесс калибровки: необходимо в течение примерно 10-15 секунд вращать (по оси Z) устройство в разные стороны. То есть макетная плата лежит на столе, и крутим по очереди по часовой стрелке несколько раз, а потом обратно столько же. В результате в мониторе порта будут отображены результаты калибровки с необходимыми нам коэффициентами:
Калибровка магнитометра QMC5883L
Инициализация QMC5883L прошла успешно. Старт калибровки...
Калибровка магнитометра...
Вращайте устройство в обе стороны в течение 10-15 секунд
Прогресс: 6%
Прогресс: 14%
Прогресс: 21%
Прогресс: 28%
Прогресс: 36%
Прогресс: 43%
Прогресс: 51%
Прогресс: 58%
Прогресс: 65%
Прогресс: 73%
Прогресс: 80%
Прогресс: 88%
Прогресс: 95%
minX: [-266]
maxX: [1227]
minY: [1787]
maxY: [3088]
Скопируйте эти значения в Ваш основной код для компаса
Нас интересуют значения minx | maxX | minY | maxY. Их нужно запомнить, чтобы добавить в основной код.
Основной код работы компаса
Теперь внесём определённые калибровочные коэффициенты в основную программу (в начале кода, где объявляются переменные):

Однако настоятельно рекомендуется производить калибровку сенсора при каждом включении устройства, поскольку при переносе устройства в другое место факторы внешнего воздействия меняются. Соответственно, в коде предусмотрена область (в составе функции calibrateMagnetometer), которую следует раскомментировать, после чего калибровочные коэффициенты будут каждый раз перезаписываться.
Также нужно отметить, что в коде добавлено магнитное склонение — угол отклонения магнитного севера (куда показывает компас) от истинного географического севера. Для примера указано значение склонения (деклинации) 10 для Москвы, поскольку север отклонён примерно на 10 градусов к востоку. Без этой корректировки компас будет «врать» на те самые 10 градусов.

Ниже представлен очень подробно прокомментированный полный листинг кода:
#include <Arduino.h> // Базовые функции Arduino (Serial, delay и т.п.)
#include <TFT_eSPI.h> // Библиотека для работы с TFT-дисплеем на ESP32
#include <Wire.h> // Библиотека для работы с I2C (магнитометр QMC5883L)
// Адрес и регистры QMC5883L (магнитометра)
#define QMC5883L_ADDR 0x0D
#define QMC5883L_REG_DATA 0x00
#define QMC5883L_REG_CTRL1 0x09
#define QMC5883L_REG_CTRL2 0x0A
#define QMC5883L_REG_CTRL3 0x0B
// Размеры дисплея (ширина и высота в пикселях)
#define TFT_WIDTH 320
#define TFT_HEIGHT 170
// Центр экрана по X и Y (для удобства позиционирования)
#define CENTER_X (TFT_WIDTH / 2)
#define CENTER_Y (TFT_HEIGHT / 2)
// Радиус круга компаса (от центра)
#define CIRCLE_RADIUS 80
// Создаём объект для работы с TFT-дисплеем
TFT_eSPI tft = TFT_eSPI();
// Структура для хранения данных магнитометра (X, Y, Z и флаг успеха чтения)
struct MagnetometerData {
int16_t x, y, z;
bool success;
};
// Флаг, найден ли магнитометр (для проверки перед чтением)
bool magnetometerFound = false;
// Магнитное склонение для Москвы (в градусах)
// Используется для корректировки направления с учётом географических особенностей
float declination = 10.0;
// Коэффициенты калибровки (минимумы и максимумы по осям X и Y), которые необходимы для нормализации данных магнитометра
int16_t minX = 0, maxX = 0, minY = 0, maxY = 0; // объявляем переменные, которые используюся в коде
// Значения калибровочных коэффициентов, полученые после отдельной калибровки
int16_t koeffi_min_x = -276;
int16_t koeffi_max_x = 1238;
int16_t koeffi_min_y = 1767;
int16_t koeffi_max_y = 3076;
// Пины I2C на ESP32 (SDA и SCL)
const int I2C_SDA = 21;
const int I2C_SCL = 22;
// Вспомогательные функции для нахождения минимума и максимума из двух чисел
int16_t min_val(int16_t a, int16_t b) {
return (a < b) ? a : b;
}
int16_t max_val(int16_t a, int16_t b) {
return (a > b) ? a : b;
}
// Функция инициализации магнитометра QMC5883L
bool initializeQMC5883L() {
// Проверяем, отвечает ли устройство по I2C адресу
Wire.beginTransmission(QMC5883L_ADDR);
if (Wire.endTransmission() != 0) return false; // Если ошибка — возвращаем false
// Настраиваем регистр CTRL1: непрерывный режим, частота 200 Гц, диапазон ±8G, OSR=512
Wire.beginTransmission(QMC5883L_ADDR);
Wire.write(QMC5883L_REG_CTRL1);
Wire.write(0x1D);
if (Wire.endTransmission() != 0) return false;
delay(10);
// Настраиваем регистр CTRL2: сброс и нормальный режим (0x00)
Wire.beginTransmission(QMC5883L_ADDR);
Wire.write(QMC5883L_REG_CTRL2);
Wire.write(0x00);
if (Wire.endTransmission() != 0) return false;
delay(10);
// Настраиваем регистр CTRL3: включаем Set/Reset период (0x01)
Wire.beginTransmission(QMC5883L_ADDR);
Wire.write(QMC5883L_REG_CTRL3);
Wire.write(0x01);
if (Wire.endTransmission() != 0) return false;
delay(10);
return true; // Инициализация прошла успешно
}
// Функция чтения данных с магнитометра
MagnetometerData readMagnetometer() {
MagnetometerData data = {0, 0, 0, false}; // Инициализируем структуру с нулями и флагом false
// Указываем регистр данных (начинаем чтение с 0x00)
Wire.beginTransmission(QMC5883L_ADDR);
Wire.write(QMC5883L_REG_DATA);
if (Wire.endTransmission() != 0) return data; // Ошибка — возвращаем пустые данные
delay(10); // Ждём, чтобы данные были готовы
// Запрашиваем 6 байт (по 2 байта на каждую ось: X, Y, Z)
if (Wire.requestFrom(QMC5883L_ADDR, 6) == 6) {
// Читаем младшие и старшие байты для каждой оси
uint8_t xLow = Wire.read();
uint8_t xHigh = Wire.read();
uint8_t yLow = Wire.read();
uint8_t yHigh = Wire.read();
uint8_t zLow = Wire.read();
uint8_t zHigh = Wire.read();
// Собираем 16-битные значения из двух байт (старший байт сдвигаем влево)
data.x = (int16_t)(xHigh << 8 | xLow);
data.y = (int16_t)(yHigh << 8 | yLow);
data.z = (int16_t)(zHigh << 8 | zLow);
data.success = true; // Флаг успешного чтения
}
return data; // Возвращаем считанные данные
}
// Функция калибровки магнитометра
// Здесь жестко прописаны калибровочные коэффициенты, полученные при отдельной калибровке
// При желании можно расскоментировать часть кода, который позволит производить новую калибровку при каждом включении устройства
void calibrateMagnetometer() {
////////////////////////////////////////////////////////////////////////////-- функция калибровки
/*
Serial.println("Калибровка магнитометра...");
Serial.println("Вращайте устройство в обе стороны в течение 15 секунд");
tft.fillScreen(TFT_BLACK);
tft.drawString("Calibrating...", CENTER_X, CENTER_Y - 20, 2);
tft.drawString("Rotate device", CENTER_X, CENTER_Y, 2);
tft.drawString("in all directions", CENTER_X, CENTER_Y + 20, 2);
int samples = 0;
unsigned long startTime = millis();
// Начальные значения
koeffi_min_x = koeffi_min_y = 32767;
koeffi_max_x = koeffi_max_y = -32768;
while (millis() - startTime < 15000) {
MagnetometerData data = readMagnetometer();
if (data.success) {
// Используем наши функции min_val и max_val
koeffi_min_x = min_val(koeffi_min_x, data.x);
koeffi_max_x = max_val(koeffi_max_x, data.x);
koeffi_min_y = min_val(koeffi_min_y, data.y);
koeffi_max_y = max_val(koeffi_max_y, data.y);
samples++;
// Прогресс
if (samples % 10 == 0) {
int progress = ((millis() - startTime) * 100) / 15000;
tft.fillRect(CENTER_X - 60, CENTER_Y + 40, 120, 20, TFT_BLACK);
tft.drawString("Progress: " + String(progress) + "%", CENTER_X, CENTER_Y + 40, 2);
}
}
delay(100);
}
Serial.printf("minX: [%d]\n", koeffi_min_x); // Диапазон для X: минимум
Serial.printf("maxX: [%d]\n", koeffi_max_x); // Диапазон для X: максимум
Serial.printf("minY: [%d]\n", koeffi_min_y); // Диапазон для Y: минимум
Serial.printf("maxY: [%d]\n", koeffi_max_y); // Диапазон для Y: максимум
*/
///////////////////////////////////////////////////////////////////////-- функция калибровки
// калибровочные коэффициента, полученные при отдельной калибровке
// если расскоментировать часть функции выше для калибровки при каждом включении устройства, то переменные ниже будут перезаписываться
minX = koeffi_min_x;
maxX = koeffi_max_x;
minY = koeffi_min_y;
maxY = koeffi_max_y;
// Очищаем экран перед началом работы
tft.fillScreen(TFT_BLACK);
}
// Функция вычисления текущего направления (heading) в градусах
float getHeading() {
MagnetometerData data = readMagnetometer(); // Считываем данные с магнитометра
if (!data.success) return -1; // Если ошибка — возвращаем -1
// Нормализация данных по X и Y в диапазон [-1, 1]
// Формула: сначала сдвигаем в ноль, потом масштабируем в [-1;1]
float x_norm = 2.0 * (data.x - minX) / (maxX - minX) - 1.0;
float y_norm = 2.0 * (data.y - minY) / (maxY - minY) - 1.0;
// Вычисляем угол направления с помощью atan2 (возвращает значение в радианах от -PI до PI)
float heading = atan2(y_norm, x_norm);
// Добавляем магнитное склонение (переводим градусы в радианы)
heading += declination * PI / 180.0;
// Нормализуем угол в диапазон [0, 2*PI]
if (heading < 0) heading += 2 * PI;
if (heading > 2 * PI) heading -= 2 * PI;
// Переводим радианы в градусы и возвращаем
return heading * 180.0 / PI;
}
// Функция отрисовки базового интерфейса компаса (круг и буквы сторон света)
void drawCompassBase() {
tft.fillScreen(TFT_BLACK); // Очищаем экран
// Рисуем два круга для эффекта толщины линии
tft.drawCircle(CENTER_X, CENTER_Y, CIRCLE_RADIUS, TFT_WHITE);
tft.drawCircle(CENTER_X, CENTER_Y, CIRCLE_RADIUS - 1, TFT_WHITE);
// Настраиваем цвет и размер текста для подписей
tft.setTextColor(TFT_BLUE, TFT_BLACK);
tft.setTextSize(1);
tft.setTextDatum(MC_DATUM); // Центрирование текста по месту рисования
// Подписываем стороны света вокруг круга
tft.drawString("N", CENTER_X, CENTER_Y - 65, 2); // Север
tft.drawString("S", CENTER_X, CENTER_Y + 65, 2); // Юг
tft.drawString("W", CENTER_X - 65, CENTER_Y, 2); // Запад
tft.drawString("E", CENTER_X + 65, CENTER_Y, 2); // Восток
// Рисуем красную точку в центре компаса
tft.fillCircle(CENTER_X, CENTER_Y, 3, TFT_RED);
}
// Функция рисования стрелки компаса, указывающей на направление heading (в градусах)
void drawCompassArrow(float heading, uint16_t color) {
// Преобразуем угол так, чтобы 0° был направлен вверх (север)
float angle = (heading - 90) * PI / 180.0;
// Координаты кончика стрелки (на радиусе круга минус небольшой отступ)
int tipX = CENTER_X + (CIRCLE_RADIUS - 25) * cos(angle);
int tipY = CENTER_Y + (CIRCLE_RADIUS - 25) * sin(angle);
// Координаты основания стрелки по бокам (ширина стрелки 20 пикселей)
int baseX1 = CENTER_X + 10 * cos(angle + PI/2);
int baseY1 = CENTER_Y + 10 * sin(angle + PI/2);
int baseX2 = CENTER_X + 10 * cos(angle - PI/2);
int baseY2 = CENTER_Y + 10 * sin(angle - PI/2);
// Рисуем заполненный треугольник — стрелку компаса
tft.fillTriangle(tipX, tipY, baseX1, baseY1, baseX2, baseY2, color);
// Рисуем линию от центра к кончику стрелки (для акцента)
tft.drawLine(CENTER_X, CENTER_Y, tipX, tipY, color);
}
// Функция получения текстового обозначения направления по углу heading (в градусах)
String getDirection(float heading) {
// Массив с 16 направлениями (каждое по 22.5°)
String directions[] = {"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
"S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"};
// Вычисляем индекс направления с округлением (плюс 11.25 для центрирования секторов)
int index = (int)((heading + 11.25) / 22.5) % 16;
return directions[index];
}
// Функция обновления экрана компаса с новым значением heading
void updateCompassDisplay(float heading) {
static float lastHeading = -1; // Запоминаем последний угол стрелки
// Если стрелка уже была, стираем её предыдущую позицию (рисуем чёрным)
if (lastHeading != -1) {
drawCompassArrow(lastHeading, TFT_BLACK);
}
// Рисуем новую стрелку красным цветом
drawCompassArrow(heading, TFT_RED);
// Очищаем область с цифровым значением угла
tft.fillRect(CENTER_X - 160, CENTER_Y + 45, 90, 40, TFT_BLACK);
// Выводим число градусов (с точностью до 1 знака после запятой)
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setTextSize(2);
tft.drawString(String(heading, 1) + "°", CENTER_X - 100, CENTER_Y + 55, 2);
// Выводим текстовое направление (N, NE, E и т.п.)
String direction = getDirection(heading);
tft.setTextColor(TFT_CYAN, TFT_BLACK);
tft.fillRect(CENTER_X + 80, CENTER_Y + 45, 100, 40, TFT_BLACK);
tft.drawString(direction, CENTER_X + 110, CENTER_Y + 55, 2);
// Запоминаем текущий угол для следующего обновления
lastHeading = heading;
}
// Функция отображения ошибки на экране и в Serial
void displayError(const char* message) {
tft.fillScreen(TFT_BLACK); // Очищаем экран
tft.setTextColor(TFT_RED, TFT_BLACK);
tft.setTextSize(2);
tft.setTextDatum(MC_DATUM); // Центрируем текст
tft.drawString(message, CENTER_X, CENTER_Y, 2); // Выводим сообщение
Serial.println(message); // Печатаем в Serial для отладки
}
// Функция setup — выполняется один раз при старте
void setup() {
Serial.begin(115200); // Запускаем Serial для отладки
Serial.println("Компас QMC5883L");
// Инициализируем дисплей
tft.init();
tft.setRotation(3); // Поворот экрана (ориентация)
tft.fillScreen(TFT_BLACK);
// Инициализируем I2C с указанными пинами
Wire.begin(I2C_SDA, I2C_SCL);
Wire.setClock(100000); // Частота I2C 100 кГц
delay(1000); // Ждём стабилизации
// Выводим сообщение об инициализации
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.setTextDatum(MC_DATUM);
tft.drawString("Initializing...", CENTER_X, CENTER_Y, 2);
// Пытаемся инициализировать магнитометр
if (initializeQMC5883L()) {
magnetometerFound = true;
Serial.println("Магнитометр QMC5883L инициализирован успешно");
calibrateMagnetometer(); // Устанавливаем калибровочные коэффициенты
// Отрисовываем базовый интерфейс компаса
tft.fillScreen(TFT_BLACK);
drawCompassBase();
Serial.println("Компас готов!");
} else {
// Если инициализация не удалась — показываем ошибку
displayError("Sensor init failed");
}
}
// Главный цикл программы — выполняется бесконечно
void loop() {
if (!magnetometerFound) {
delay(1000); // Если датчик не найден, ждём и не делаем ничего
return;
}
float heading = getHeading(); // Получаем текущее направление
if (heading >= 0) {
updateCompassDisplay(heading); // Обновляем экран компаса
} else {
Serial.println("Ошибка чтения данных"); // Ошибка чтения данных
}
delay(200); // Пауза 200 мс между обновлениями (5 раз в секунду)
}
Эксперимент
Итак, кладём устройство на горизонтальную поверхность (стол) и подаём питание на контроллер. Если Вы решили превентивно выполнять калибровку при каждом включении питания, то на дисплее будет отображаться надпись «Calibrating… Rotate device in all directions». Она гласит, что начался процесс калибровки и нужно вращать в разные стороны устройство (крутить по часовой стрелке и в обратную сторону). При этом на экране будет отображаться прогресс в процентах (общее время примерно 10-15 секунд).
После калибровки девайс готов определять курс. Важно, чтобы на расстоянии менее 3 сантиметром от датчика не было аккумуляторной батареи (если предполагается использовать автономное питание), иначе показания будут сильно отклоняться от истины. В качестве образцового компаса воспользуемся соответствующим приложением на смартфоне:
— курс на север (0 градусов)

— курс на северо-восток (60 градусов)

На TFT-дисплее отображается круг с надписями сторон света. Круг стоит неподвижно, вращается стрелка, указывая курс.
Слева от круга представлены показания азимута (угол, отсчитываемый от севера по часовой стрелке) в градусах. Справа — направление стороны света (румб).
В зависимости от качества калибровки, показания могут в той или иной мере отличаться от истинного значения. Учитывая, что наш сенсор в принципе имеет точность примерно ±2 градуса, можно сделать вывод, что для базовых задач навигации разработанное устройство вполне пригодно.
Алгоритм работы
Общий алгоритм исполнения программы следующий:
1) Запуск и настройка
При подачи питания на ESP32 производится инициализация дисплея по SPI и датчика через I2C. Магнитометр настраивается в непрерывный режим: измерение с частотой 200 Гц, предел ±8 Гс, а также выборка из 512 измерений.
2) Калибровка
Чтобы компенсировать искажения (от металлических предметов вокруг, устройств с мощным электромагнитным излучением, проводов с током или дефектов датчика), применяют коэффициенты min/max по осям X/Y. В программе они могут быть либо фиксированы (определены предварительно отдельным процессом), либо определяться при каждом сбросе питания устройства.
3) Чтение данных
Каждые 200 мс (в главном цикле) микроконтроллер отправляет запрос датчику на получение сырых данных. Магнитометр отвечает 6 байтами (по 2 на ось X, Y, Z). Их собирают в 16-битные числа: X — сила поля влево-вправо, Y — вперёд-назад, Z — вверх-вниз. Если чтение успешное, данные готовы для обработки.
4) Обработка данных
Сырые X/Y нормализуют — переводят в относительные единицы от -1 до 1, чтобы центром был 0. Например, x_norm = 2*(X – minX)/(maxX – minX) – 1. Затем используют функцию atan2(Y_nom, X_norm) — она вычисляет угол между вектором (Y, X) и осью X (в радианах, от -180° до 180°). Это дает предварительное направление на магнитный север.
5) Коррекция направления
Добавляют склонение (для Москвы 10 градусов) — разницу между магнитным и истинным севером. Переводят в градусы, нормализуют угол в 0–360 градусов (если меньше ноля, то прибавляют 360, если больше 360 — вычитают). Теперь это точный угол (heading) на истинный север, в градусах.
Заключение
В данной статье мы подробно рассмотрели устройство, принцип работы и ключевые особенности цифровых микросхем трёхосевых магнитометров HMC5883L и QMC5883L, включая их различия в характеристиках, а также познакомились с применением магниторезистивного эффекта для измерения магнитного поля Земли. Эти датчики открывают широкие возможности для создания систем навигации и ориентации: от простых компасов до комплексных систем в робототехнике и мобильных устройствах, позволяя точно отслеживать направление и компенсировать внешние помехи.
В качестве примера мы на базе отладочной платы ESP32 собрали функциональный цифровой компас с использованием модуля QMC5883L и TFT-дисплея ST7789, включая процесс калибровки. Подробно описанный алгоритм работы, от инициализации сенсора до коррекции угла с учётом склонения, можно легко адаптировать для других задач, таких как интеграция с GPS или акселерометрами. Это делает рассматриваемый сенсор отличным выбором для начинающих и опытных разработчиков, стремящихся разрабатывать надёжные и интерактивные устройства для ориентирования и контроля движения.