loading

HarmonyOS开发:磁力计与电子罗盘实现

  • Home
  • Blog
  • HarmonyOS开发:磁力计与电子罗盘实现

HarmonyOS开发:磁力计与电子罗盘实现

HarmonyOS开发:磁力计与电子罗盘实现

核心要点:本文深入解析磁力计的物理原理与霍尔效应,详解地磁场数据获取、硬磁/软磁校准算法、电子罗盘航向角计算、九轴传感器融合(加速度计+陀螺仪+磁力计),以及地图导航中的实际应用。

一、背景与动机

磁力计是移动设备中实现电子罗盘和航向感知的关键传感器。在GPS信号不可用的室内环境、城市峡谷或隧道中,磁力计配合加速度计可以确定设备的朝向,为导航、地图旋转、AR定位等场景提供基础能力。

然而,磁力计开发面临独特的挑战:

环境干扰:室内金属结构、电子设备、扬声器等都会产生磁场干扰

硬磁偏移:设备本身的永磁体(如扬声器磁铁)产生恒定偏移

软磁畸变:设备附近的铁磁性材料扭曲地磁场

校准复杂:需要用户配合进行"8字形"校准动作

倾斜补偿:设备倾斜时需要加速度计辅助计算航向角

本文将从磁力计原理出发,系统讲解磁场校准、航向角计算与九轴融合算法。

二、核心原理

2.1 磁力计物理原理

移动设备中的MEMS磁力计主要基于霍尔效应(Hall Effect)或各向异性磁阻效应(AMR)工作:

flowchart TB

subgraph Hall["霍尔效应原理"]

A["电流I沿导体流动"] --> B["垂直磁场B作用"]

B --> C["洛伦兹力偏转载流子"]

C --> D["横向电势差VH产生"]

D --> E["VH ∝ B × I
测量磁场强度"]

end

subgraph AMR["AMR效应原理"]

F["铁磁薄膜电阻"] --> G["外磁场改变磁化方向"]

G --> H["电阻值变化ΔR"]

H --> I["惠斯通电桥检测"]

I --> J["输出与磁场成正比"]

end

subgraph Earth["地磁场特征"]

K["磁场强度: 25~65 μT"]

L["磁倾角: 随纬度变化"]

M["水平分量: 指向磁北"]

end

Hall --- Earth

AMR --- Earth

classDef hallStyle fill:#4A90D9,stroke:#2C5F8A,color:#fff,font-weight:bold

classDef amrStyle fill:#9B59B6,stroke:#7D3C98,color:#fff,font-weight:bold

classDef earthStyle fill:#50C878,stroke:#2E8B57,color:#fff,font-weight:bold

class A,B,C,D,E hallStyle

class F,G,H,I,J amrStyle

class K,L,M earthStyle

地磁场关键参数:

参数

说明

典型值

总磁场强度

地球磁场的总矢量模

25~65 μT

水平分量

地磁场在水平面的投影

15~40 μT

磁倾角

地磁场与水平面的夹角

-60°~+60°(随纬度)

磁偏角

磁北与真北的夹角

随地理位置变化

2.2 磁场干扰模型

磁力计测量值 = 地磁场 + 硬磁偏移 + 软磁畸变 + 随机噪声

flowchart LR

A[磁力计测量值] --> B["地磁场
(目标信号)"]

A --> C["硬磁偏移
(恒定偏移)"]

A --> D["软磁畸变
(方向依赖)"]

A --> E["随机噪声
(高频抖动)"]

B --> F["✅ 需要提取"]

C --> G["❌ 需要消除"]

D --> H["❌ 需要消除"]

E --> I["❌ 需要消除"]

classDef signalStyle fill:#50C878,stroke:#2E8B57,color:#fff,font-weight:bold

classDef noiseStyle fill:#FF6B6B,stroke:#CC5555,color:#fff,font-weight:bold

classDef targetStyle fill:#4A90D9,stroke:#2C5F8A,color:#fff,font-weight:bold

class A targetStyle

class B,F signalStyle

class C,D,E,G,H,I noiseStyle

干扰类型详解:

干扰类型

物理来源

数学模型

消除方法

硬磁偏移

设备内永磁体(扬声器、振动马达)

恒定矢量偏移

椭圆拟合求中心

软磁畸变

设备附近铁磁性材料

椭球→球体变换

椭球参数估计

随机噪声

电路噪声、量化误差

高斯白噪声

低通滤波

2.3 航向角计算原理

航向角(Heading/Yaw)是设备Y轴(前方)与磁北方向的夹角。计算步骤:

获取磁力计水平分量:需要加速度计辅助进行倾斜补偿

计算磁北方向:arctan2(magY, magX)

加上磁偏角:航向角 = 磁航向 + 磁偏角 = 真航向

三、代码实战

3.1 磁力计数据获取与磁场可视化

// magnetometer_visual.ets

// 功能:磁力计数据获取与磁场强度可视化

import sensor from '@ohos.sensor';

import { BusinessError } from '@ohos.base';

interface MagSample {

x: number; // X轴磁场 (μT)

y: number; // Y轴磁场 (μT)

z: number; // Z轴磁场 (μT)

magnitude: number; // 总磁场强度

timestamp: number;

}

@Entry

@Component

struct MagnetometerVisualPage {

@State currentMag: MagSample = { x: 0, y: 0, z: 0, magnitude: 0, timestamp: 0 };

@State isRunning: boolean = false;

@State calibSamples: MagSample[] = []; // 校准采样

@State isCalibrating: boolean = false;

@State calibProgress: number = 0;

@State hardIronOffset = { x: 0, y: 0, z: 0 }; // 硬磁偏移

private magCallback: (data: sensor.MagnetometerResponse) => void = () => {};

private maxCalibSamples: number = 200;

private expectedMagStrength: number = 45; // 预期地磁场强度(μT),根据地区调整

aboutToDisappear() {

this.stopMagnetometer();

}

// 功能:启动磁力计订阅

startMagnetometer() {

try {

this.magCallback = (data: sensor.MagnetometerResponse) => {

const magnitude = Math.sqrt(data.x ** 2 + data.y ** 2 + data.z ** 2);

const sample: MagSample = {

x: parseFloat(data.x.toFixed(2)),

y: parseFloat(data.y.toFixed(2)),

z: parseFloat(data.z.toFixed(2)),

magnitude: parseFloat(magnitude.toFixed(2)),

timestamp: data.timestamp

};

this.currentMag = sample;

// 校准模式下收集数据

if (this.isCalibrating) {

this.calibSamples.push(sample);

this.calibProgress = Math.min(100, (this.calibSamples.length / this.maxCalibSamples) * 100);

if (this.calibSamples.length >= this.maxCalibSamples) {

this.performCalibration();

this.isCalibrating = false;

}

}

};

sensor.on(sensor.SensorType.MAGNETOMETER, this.magCallback, {

interval: 'ui' // 16Hz,磁力计不需要太高频率

});

this.isRunning = true;

} catch (error) {

const err = error as BusinessError;

console.error(`[MagVisual] 启动失败: ${err.code} - ${err.message}`);

}

}

// 功能:停止磁力计订阅

stopMagnetometer() {

if (!this.isRunning) return;

try {

sensor.off(sensor.SensorType.MAGNETOMETER, this.magCallback);

this.isRunning = false;

} catch (error) {

console.error(`[MagVisual] 停止失败: ${JSON.stringify(error)}`);

}

}

// 功能:开始校准(用户需画8字形)

startCalibration() {

this.calibSamples = [];

this.isCalibrating = true;

this.calibProgress = 0;

}

// 功能:执行硬磁校准

performCalibration() {

if (this.calibSamples.length < 50) {

console.warn('[MagVisual] 校准数据不足');

return;

}

// 简单硬磁校准:取各轴最大最小值的中心

let minX = Infinity, maxX = -Infinity;

let minY = Infinity, maxY = -Infinity;

let minZ = Infinity, maxZ = -Infinity;

for (const s of this.calibSamples) {

minX = Math.min(minX, s.x); maxX = Math.max(maxX, s.x);

minY = Math.min(minY, s.y); maxY = Math.max(maxY, s.y);

minZ = Math.min(minZ, s.z); maxZ = Math.max(maxZ, s.z);

}

this.hardIronOffset = {

x: (minX + maxX) / 2,

y: (minY + maxY) / 2,

z: (minZ + maxZ) / 2

};

console.info(`[MagVisual] 硬磁校准完成: offset=(${this.hardIronOffset.x.toFixed(1)}, ` +

`${this.hardIronOffset.y.toFixed(1)}, ${this.hardIronOffset.z.toFixed(1)})`);

}

// 功能:获取校准后的磁场数据

getCalibratedMag(): { x: number; y: number; z: number } {

return {

x: this.currentMag.x - this.hardIronOffset.x,

y: this.currentMag.y - this.hardIronOffset.y,

z: this.currentMag.z - this.hardIronOffset.z

};

}

build() {

Scroll() {

Column() {

Text('磁力计监测与校准')

.fontSize(24)

.fontWeight(FontWeight.Bold)

.margin({ bottom: 16 })

// 原始磁场数据

this.RawMagCard()

// 校准后数据

this.CalibratedMagCard()

// 硬磁偏移

this.HardIronCard()

// 校准进度

if (this.isCalibrating) {

Progress({ value: this.calibProgress, total: 100 })

.width('100%')

.color('#4A90D9')

.margin({ bottom: 12 })

Text('请缓慢画8字形旋转设备...')

.fontSize(14)

.fontColor('#FFD93D')

.margin({ bottom: 12 })

}

// 控制按钮

Row() {

Button(this.isRunning ? '停止' : '启动')

.backgroundColor(this.isRunning ? '#FF6B6B' : '#50C878')

.fontColor('#ffffff')

.onClick(() => {

this.isRunning ? this.stopMagnetometer() : this.startMagnetometer();

})

.width('30%')

Button('开始校准')

.backgroundColor('#4A90D9')

.fontColor('#ffffff')

.onClick(() => this.startCalibration())

.width('30%')

.margin({ left: 8 })

.enabled(this.isRunning && !this.isCalibrating)

}

.width('100%')

.justifyContent(FlexAlign.Center)

}

.width('100%')

.padding(20)

}

.width('100%')

.height('100%')

.backgroundColor('#0d0d1a')

}

@Builder

RawMagCard() {

Column() {

Text('原始磁场数据 (μT)')

.fontSize(16)

.fontWeight(FontWeight.Medium)

.margin({ bottom: 12 })

Row() {

this.AxisDisplay('Bx', this.currentMag.x, '#FF6B6B')

this.AxisDisplay('By', this.currentMag.y, '#4ECDC4')

this.AxisDisplay('Bz', this.currentMag.z, '#45B7D1')

}

.width('100%')

.justifyContent(FlexAlign.SpaceAround)

Text(`|B| = ${this.currentMag.magnitude} μT`)

.fontSize(16)

.fontColor('#FFD93D')

.margin({ top: 8 })

}

.width('100%')

.padding(20)

.borderRadius(16)

.backgroundColor('#1a1a2e')

.margin({ bottom: 12 })

}

@Builder

CalibratedMagCard() {

Column() {

Text('校准后磁场 (μT)')

.fontSize(16)

.fontWeight(FontWeight.Medium)

.margin({ bottom: 12 })

const cal = this.getCalibratedMag();

Row() {

this.AxisDisplay('Bx\'', cal.x, '#FF6B6B')

this.AxisDisplay('By\'', cal.y, '#4ECDC4')

this.AxisDisplay('Bz\'', cal.z, '#45B7D1')

}

.width('100%')

.justifyContent(FlexAlign.SpaceAround)

const calMag = Math.sqrt(cal.x ** 2 + cal.y ** 2 + cal.z ** 2);

Text(`|B'| = ${calMag.toFixed(1)} μT`)

.fontSize(16)

.fontColor('#50C878')

.margin({ top: 8 })

}

.width('100%')

.padding(20)

.borderRadius(16)

.backgroundColor('#1a1a2e')

.margin({ bottom: 12 })

}

@Builder

HardIronCard() {

Column() {

Text('硬磁偏移')

.fontSize(14)

.fontWeight(FontWeight.Medium)

.margin({ bottom: 8 })

Row() {

Text(`X: ${this.hardIronOffset.x.toFixed(1)}`)

.fontSize(13).fontColor('#FF6B6B').layoutWeight(1)

Text(`Y: ${this.hardIronOffset.y.toFixed(1)}`)

.fontSize(13).fontColor('#4ECDC4').layoutWeight(1)

Text(`Z: ${this.hardIronOffset.z.toFixed(1)}`)

.fontSize(13).fontColor('#45B7D1').layoutWeight(1)

}

.width('100%')

}

.width('100%')

.padding(16)

.borderRadius(12)

.backgroundColor('#1a1a2e')

.margin({ bottom: 12 })

}

@Builder

AxisDisplay(label: string, value: number, color: string) {

Column() {

Text(label)

.fontSize(14)

.fontColor(color)

Text(`${value}`)

.fontSize(20)

.fontWeight(FontWeight.Bold)

.fontColor('#ffffff')

.margin({ top: 4 })

Text('μT')

.fontSize(11)

.fontColor('#888888')

}

.alignItems(HorizontalAlign.Center)

}

}

3.2 电子罗盘航向角计算

// compass_heading.ets

// 功能:电子罗盘航向角计算,含倾斜补偿

import sensor from '@ohos.sensor';

// 航向角计算引擎

export class CompassEngine {

// 磁力计数据

private magX: number = 0;

private magY: number = 0;

private magZ: number = 0;

// 加速度计数据(用于倾斜补偿)

private accelX: number = 0;

private accelY: number = 0;

private accelZ: number = 0;

// 硬磁校准偏移

private hardIronX: number = 0;

private hardIronY: number = 0;

private hardIronZ: number = 0;

// 磁偏角(度),根据地理位置设置

private declination: number = 0;

// 平滑滤波

private smoothHeading: number = 0;

private smoothingFactor: number = 0.7;

private isRunning: boolean = false;

private magCallback: (data: sensor.MagnetometerResponse) => void = () => {};

private accelCallback: (data: sensor.AccelerometerResponse) => void = () => {};

// 功能:设置磁偏角

setDeclination(degrees: number) {

this.declination = degrees;

}

// 功能:设置硬磁偏移

setHardIronOffset(x: number, y: number, z: number) {

this.hardIronX = x;

this.hardIronY = y;

this.hardIronZ = z;

}

// 功能:启动电子罗盘

start(onHeadingUpdate?: (heading: number, magneticHeading: number) => void) {

if (this.isRunning) return;

// 订阅磁力计

this.magCallback = (data: sensor.MagnetometerResponse) => {

// 减去硬磁偏移

this.magX = data.x - this.hardIronX;

this.magY = data.y - this.hardIronY;

this.magZ = data.z - this.hardIronZ;

this.computeHeading(onHeadingUpdate);

};

// 订阅加速度计(倾斜补偿)

this.accelCallback = (data: sensor.AccelerometerResponse) => {

this.accelX = data.x;

this.accelY = data.y;

this.accelZ = data.z;

};

try {

sensor.on(sensor.SensorType.MAGNETOMETER, this.magCallback, { interval: 'ui' });

sensor.on(sensor.SensorType.ACCELEROMETER, this.accelCallback, { interval: 'ui' });

this.isRunning = true;

} catch (error) {

console.error(`[Compass] 启动失败: ${JSON.stringify(error)}`);

}

}

// 功能:停止电子罗盘

stop() {

if (!this.isRunning) return;

try {

sensor.off(sensor.SensorType.MAGNETOMETER, this.magCallback);

sensor.off(sensor.SensorType.ACCELEROMETER, this.accelCallback);

this.isRunning = false;

} catch (error) {

console.error(`[Compass] 停止失败: ${JSON.stringify(error)}`);

}

}

// 功能:计算航向角(含倾斜补偿)

private computeHeading(

onHeadingUpdate?: (heading: number, magneticHeading: number) => void

) {

// 步骤1:归一化加速度计数据

const accelMag = Math.sqrt(this.accelX ** 2 + this.accelY ** 2 + this.accelZ ** 2);

if (accelMag < 0.1) return; // 异常数据保护

const normAx = this.accelX / accelMag;

const normAy = this.accelY / accelMag;

const normAz = this.accelZ / accelMag;

// 步骤2:倾斜补偿 - 将磁力计数据投影到水平面

// 使用加速度计确定设备倾斜角度,补偿磁力计的倾斜影响

// 参考公式:https://www.st.com/resource/en/application_note/dm00268500.pdf

// 计算倾斜补偿后的水平磁场分量

// East分量(指向东方的磁场分量)

const eastX = this.magZ * normAy - this.magY * normAz;

const eastY = this.magX * normAz - this.magZ * normAx;

const eastZ = this.magY * normAx - this.magX * normAy;

// North分量(指向北方的磁场分量)

const northX = this.magX * eastY - this.magY * eastX;

// 简化计算:直接使用倾斜补偿后的水平分量

const compMagX = this.magX * normAz - this.magZ * normAx;

const compMagY = this.magY * normAz - this.magZ * normAy;

// 步骤3:计算磁航向角

let magneticHeading = Math.atan2(compMagX, compMagY) * (180 / Math.PI);

// 转换为0~360°范围

if (magneticHeading < 0) {

magneticHeading += 360;

}

// 步骤4:平滑滤波(处理角度环绕问题)

this.smoothHeading = this.angleFilter(this.smoothHeading, magneticHeading);

// 步骤5:加上磁偏角得到真航向

let trueHeading = this.smoothHeading + this.declination;

if (trueHeading < 0) trueHeading += 360;

if (trueHeading >= 360) trueHeading -= 360;

onHeadingUpdate?.(trueHeading, this.smoothHeading);

}

// 功能:角度平滑滤波(处理0°/360°环绕)

private angleFilter(previous: number, current: number): number {

let diff = current - previous;

// 处理角度环绕

if (diff > 180) diff -= 360;

if (diff < -180) diff += 360;

return previous + diff * (1 - this.smoothingFactor);

}

// 功能:获取方向名称

static getDirectionName(heading: number): string {

if (heading >= 337.5 || heading < 22.5) return '北';

if (heading >= 22.5 && heading < 67.5) return '东北';

if (heading >= 67.5 && heading < 112.5) return '东';

if (heading >= 112.5 && heading < 157.5) return '东南';

if (heading >= 157.5 && heading < 202.5) return '南';

if (heading >= 202.5 && heading < 247.5) return '西南';

if (heading >= 247.5 && heading < 292.5) return '西';

if (heading >= 292.5 && heading < 337.5) return '西北';

return '未知';

}

}

3.3 电子罗盘UI组件

// compass_ui.ets

// 功能:电子罗盘可视化UI组件

@Entry

@Component

struct CompassUIPage {

@State heading: number = 0;

@State magneticHeading: number = 0;

@State directionName: string = '北';

@State isRunning: boolean = false;

private compassEngine: CompassEngine = new CompassEngine();

aboutToDisappear() {

this.compassEngine.stop();

}

// 功能:启动罗盘

startCompass() {

// 设置磁偏角(示例:北京地区约-6°)

this.compassEngine.setDeclination(-6);

this.compassEngine.start((trueHeading: number, magHeading: number) => {

this.heading = parseFloat(trueHeading.toFixed(1));

this.magneticHeading = parseFloat(magHeading.toFixed(1));

this.directionName = CompassEngine.getDirectionName(trueHeading);

});

this.isRunning = true;

}

// 功能:停止罗盘

stopCompass() {

this.compassEngine.stop();

this.isRunning = false;

}

build() {

Column() {

Text('电子罗盘')

.fontSize(24)

.fontWeight(FontWeight.Bold)

.margin({ bottom: 20 })

// 罗盘表盘

this.CompassDial()

// 航向信息

this.HeadingInfo()

// 控制按钮

Button(this.isRunning ? '停止罗盘' : '启动罗盘')

.backgroundColor(this.isRunning ? '#FF6B6B' : '#50C878')

.fontColor('#ffffff')

.width('60%')

.onClick(() => {

this.isRunning ? this.stopCompass() : this.startCompass();

})

.margin({ top: 20 })

}

.width('100%')

.height('100%')

.padding(20)

.backgroundColor('#0d0d1a')

.alignItems(HorizontalAlign.Center)

}

@Builder

CompassDial() {

Stack() {

// 外圈

Circle()

.width(280)

.height(280)

.fill('#1a1a2e')

.stroke('#333355')

.strokeWidth(2)

// 方向刻度环(旋转)

Column() {

// 北方标记

Text('N')

.fontSize(24)

.fontWeight(FontWeight.Bold)

.fontColor('#FF6B6B')

}

.width(280)

.height(280)

.rotate({ angle: -this.heading }) // 反向旋转,模拟罗盘转动

.justifyContent(FlexAlign.Start)

.padding({ top: 8 })

.alignItems(HorizontalAlign.Center)

// 固定指针(始终朝上)

Column() {

// 指针三角形

Polygon()

.points([[0, -20], [-8, 0], [8, 0]])

.fill('#FF6B6B')

}

.width(8)

.height(20)

.position({ x: '50%', y: '30%' })

.translate({ x: -4 })

// 中心点

Circle()

.width(12)

.height(12)

.fill('#50C878')

// 方位标注

this.DirectionLabels()

}

.width(300)

.height(300)

.margin({ bottom: 20 })

}

@Builder

DirectionLabels() {

// 四个主方向标注(随罗盘旋转)

Stack() {

Text('N').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FF6B6B')

.position({ x: '48%', y: '5%' })

Text('S').fontSize(14).fontColor('#888888')

.position({ x: '48%', y: '88%' })

Text('E').fontSize(14).fontColor('#888888')

.position({ x: '88%', y: '46%' })

Text('W').fontSize(14).fontColor('#888888')

.position({ x: '5%', y: '46%' })

}

.width(280)

.height(280)

.rotate({ angle: -this.heading })

}

@Builder

HeadingInfo() {

Column() {

Text(`${this.heading.toFixed(1)}°`)

.fontSize(48)

.fontWeight(FontWeight.Bold)

.fontColor('#ffffff')

Text(this.directionName)

.fontSize(24)

.fontColor('#4ECDC4')

.margin({ top: 4 })

Text(`磁航向: ${this.magneticHeading.toFixed(1)}°`)

.fontSize(14)

.fontColor('#888888')

.margin({ top: 8 })

}

.alignItems(HorizontalAlign.Center)

}

}

3.4 九轴传感器融合(简化版Madgwick算法)

// sensor_fusion_9dof.ets

// 功能:九轴传感器融合 - 加速度计+陀螺仪+磁力计

import sensor from '@ohos.sensor';

// 简化版Madgwick梯度下降融合算法

export class MadgwickFilter {

private beta: number; // 滤波器增益

private q: number[]; // 四元数 [w, x, y, z]

private sampleFreq: number; // 采样频率(Hz)

constructor(beta: number = 0.041, sampleFreq: number = 50) {

this.beta = beta;

this.sampleFreq = sampleFreq;

this.q = [1, 0, 0, 0]; // 初始化为单位四元数

}

// 功能:九轴融合更新

// gx,gy,gz: 陀螺仪角速度(rad/s)

// ax,ay,az: 加速度计(m/s²)

// mx,my,mz: 磁力计(μT)

update(gx: number, gy: number, gz: number,

ax: number, ay: number, az: number,

mx: number, my: number, mz: number): number[] {

const q0 = this.q[0], q1 = this.q[1], q2 = this.q[2], q3 = this.q[3];

// 归一化加速度计

const accelNorm = Math.sqrt(ax * ax + ay * ay + az * az);

if (accelNorm === 0) return this.q;

const nax = ax / accelNorm, nay = ay / accelNorm, naz = az / accelNorm;

// 归一化磁力计

const magNorm = Math.sqrt(mx * mx + my * my + mz * mz);

if (magNorm === 0) return this.q;

const nmx = mx / magNorm, nmy = my / magNorm, nmz = mz / magNorm;

// 参考方向:地球磁场在旋转后的设备坐标系中的表示

const hx = 2 * nmx * (0.5 - q2 * q2 - q3 * q3) + 2 * nmy * (q1 * q3 - q0 * q2) +

2 * nmz * (q1 * q2 + q0 * q3);

const hy = 2 * nmx * (q1 * q3 + q0 * q2) + 2 * nmy * (0.5 - q1 * q1 - q3 * q3) +

2 * nmz * (q2 * q3 - q0 * q1);

const hz = 2 * nmx * (q1 * q2 - q0 * q3) + 2 * nmy * (q2 * q3 + q0 * q1) +

2 * nmz * (0.5 - q1 * q1 - q2 * q2);

// 水平磁场分量(参考北方向)

const bx = Math.sqrt(hx * hx + hy * hy);

const bz = hz;

// 梯度下降算法的修正向量

const f0 = 2 * (q1 * q3 - q0 * q2) - nax;

const f1 = 2 * (q0 * q1 + q2 * q3) - nay;

const f2 = 2 * (0.5 - q1 * q1 - q2 * q2) - naz;

const f3 = 2 * bx * (0.5 - q2 * q2 - q3 * q3) + 2 * bz * (q1 * q3 - q0 * q2) - nmx;

const f4 = 2 * bx * (q1 * q2 - q0 * q3) + 2 * bz * (q0 * q1 + q2 * q3) - nmy;

const f5 = 2 * bx * (q0 * q2 + q1 * q3) + 2 * bz * (0.5 - q1 * q1 - q2 * q2) - nmz;

// Jacobian矩阵转置 * f

const J0 = -2 * q2 * f0 + 2 * q1 * f1 - 4 * q0 * f2 +

-2 * bz * q2 * f3 + (-2 * bx * q3 + 2 * bz * q1) * f4 +

(2 * bx * q2) * f5;

const J1 = 2 * q3 * f0 + 2 * q0 * f1 - 4 * q1 * f2 +

(-2 * bx * q3 + 2 * bz * q1) * f3 +

(2 * bx * q2 + 2 * bz * q0) * f4 +

(2 * bx * q3 - 4 * q1 * bz) * f5;

const J2 = -2 * q0 * f0 + 2 * q3 * f1 - 4 * q2 * f2 +

(2 * bx * q2 + 2 * bz * q0) * f3 +

(2 * bx * q3 - 4 * q2 * bz + 2 * bz * q1) * f4 +

(-2 * bx * q0 - 4 * q2 * bx) * f5;

const J3 = 2 * q1 * f0 + 2 * q2 * f1 - 4 * q3 * f2 +

(2 * bx * q3 - 4 * q3 * bz + 2 * bz * q2) * f3 +

(-2 * bx * q0 - 4 * q3 * bx) * f4 +

(2 * bx * q1) * f5;

// 归一化梯度

const gradNorm = Math.sqrt(J0 * J0 + J1 * J1 + J2 * J2 + J3 * J3);

const step = this.beta / (gradNorm + 1e-10);

// 陀螺仪积分 + 梯度修正

const dt = 1 / this.sampleFreq;

this.q[0] += (0.5 * (-q1 * gx - q2 * gy - q3 * gz) - step * J0) * dt;

this.q[1] += (0.5 * (q0 * gx + q2 * gz - q3 * gy) - step * J1) * dt;

this.q[2] += (0.5 * (q0 * gy - q1 * gz + q3 * gx) - step * J2) * dt;

this.q[3] += (0.5 * (q0 * gz + q1 * gy - q2 * gx) - step * J3) * dt;

// 归一化四元数

const qNorm = Math.sqrt(this.q[0] ** 2 + this.q[1] ** 2 +

this.q[2] ** 2 + this.q[3] ** 2);

this.q[0] /= qNorm; this.q[1] /= qNorm;

this.q[2] /= qNorm; this.q[3] /= qNorm;

return this.q;

}

// 功能:获取欧拉角(度)

getEulerDegrees(): { roll: number; pitch: number; yaw: number } {

const [w, x, y, z] = this.q;

const sinrCosp = 2 * (w * x + y * z);

const cosrCosp = 1 - 2 * (x * x + y * y);

const roll = Math.atan2(sinrCosp, cosrCosp) * (180 / Math.PI);

const sinp = 2 * (w * y - z * x);

const pitch = (Math.abs(sinp) >= 1 ?

Math.sign(sinp) * 90 : Math.asin(sinp) * (180 / Math.PI));

const sinyCosp = 2 * (w * z + x * y);

const cosyCosp = 1 - 2 * (y * y + z * z);

const yaw = Math.atan2(sinyCosp, cosyCosp) * (180 / Math.PI);

return { roll, pitch, yaw };

}

// 功能:重置

reset() {

this.q = [1, 0, 0, 0];

}

}

四、踩坑与注意事项

4.1 磁力计校准最佳实践

flowchart TD

A[磁力计校准流程] --> B[步骤1: 环境检查
远离金属和电子设备]

B --> C[步骤2: 采集数据
缓慢画8字形旋转设备]

C --> D[步骤3: 硬磁校准
椭圆中心偏移计算]

D --> E[步骤4: 软磁校准
椭球→球体变换]

E --> F[步骤5: 验证
校准后磁场强度≈预期值]

classDef stepStyle fill:#4A90D9,stroke:#2C5F8A,color:#fff,font-weight:bold

classDef checkStyle fill:#50C878,stroke:#2E8B57,color:#fff,font-weight:bold

class A stepStyle

class B,C,D,E checkStyle

class F stepStyle

4.2 常见磁场干扰源

干扰源

影响程度

影响范围

建议

桌面金属支架

<1m

远离使用

电脑扬声器

<0.5m

关闭或远离

室内钢筋结构

整个房间

无法避免,需校准

蓝牙耳机

<0.2m

取下耳机

手机壳磁吸

贴近

移除磁吸壳

4.3 航向角计算注意事项

倾斜补偿必须:设备倾斜时直接用arctan2(magX, magY)计算航向误差很大

角度环绕处理:0°和360°是同一个方向,平滑滤波时必须处理跳变

磁偏角修正:磁北≠真北,需根据地理位置添加磁偏角

异常值过滤:磁场强度偏离正常范围(25~65μT)时,数据不可靠

室内限制:室内磁场干扰严重,电子罗盘精度显著下降

4.4 九轴融合算法选择

算法

精度

计算量

参数调节

适用场景

互补滤波

1个(α)

简单罗盘

Madgwick

1个(β)

通用姿态估计

Mahony

2个(Kp,Ki)

无人机飞控

EKF

最高

多个

精密导航

五、HarmonyOS 6适配

5.1 未校准磁力计

HarmonyOS 6提供了MAGNETOMETER_UNCALIBRATED传感器,同时输出原始数据和校准偏移:

// harmonyos6_uncalibrated_mag.ets

import sensor from '@ohos.sensor';

function subscribeUncalibratedMagnetometer() {

try {

sensor.on(sensor.SensorType.MAGNETOMETER_UNCALIBRATED,

(data: sensor.MagnetometerUncalibratedResponse) => {

// 原始磁场数据(含硬磁偏移)

console.info(`[HOS6] 原始: x=${data.x.toFixed(1)}, y=${data.y.toFixed(1)}, z=${data.z.toFixed(1)}`);

// 硬磁偏移值(系统自动估算)

console.info(`[HOS6] 偏移: biasX=${data.biasX.toFixed(1)}, ` +

`biasY=${data.biasY.toFixed(1)}, biasZ=${data.biasZ.toFixed(1)}`);

// 校准后数据 = 原始 - 偏移

const calX = data.x - data.biasX;

const calY = data.y - data.biasY;

const calZ = data.z - data.biasZ;

console.info(`[HOS6] 校准: x=${calX.toFixed(1)}, y=${calY.toFixed(1)}, z=${calZ.toFixed(1)}`);

},

{ interval: 'ui' }

);

} catch (error) {

console.error(`[HOS6] 未校准磁力计不可用: ${JSON.stringify(error)}`);

}

}

5.2 系统方向传感器

HarmonyOS 6的ORIENTATION传感器内部已完成九轴融合,可直接获取航向角:

// harmonyos6_system_orientation.ets

import sensor from '@ohos.sensor';

function useSystemOrientation() {

try {

sensor.on(sensor.SensorType.ORIENTATION, (data: sensor.OrientationResponse) => {

// data.alpha: 航向角(Yaw),0~360°,北=0

// data.beta: 俯仰角(Pitch),-180~180°

// data.gamma: 横滚角(Roll),-90~90°

const direction = CompassEngine.getDirectionName(data.alpha);

console.info(`[HOS6] 航向: ${data.alpha.toFixed(1)}° (${direction}), ` +

`俯仰: ${data.beta.toFixed(1)}°, 横滚: ${data.gamma.toFixed(1)}°`);

}, { interval: 'ui' });

} catch (error) {

console.error(`[HOS6] 方向传感器不可用: ${JSON.stringify(error)}`);

}

}

class CompassEngine {

static getDirectionName(heading: number): string {

if (heading >= 337.5 || heading < 22.5) return '北';

if (heading >= 22.5 && heading < 67.5) return '东北';

if (heading >= 67.5 && heading < 112.5) return '东';

if (heading >= 112.5 && heading < 157.5) return '东南';

if (heading >= 157.5 && heading < 202.5) return '南';

if (heading >= 202.5 && heading < 247.5) return '西南';

if (heading >= 247.5 && heading < 292.5) return '西';

if (heading >= 292.5 && heading < 337.5) return '西北';

return '未知';

}

}

六、总结

本文从磁力计的物理原理出发,系统讲解了电子罗盘开发的关键技术:

flowchart TB

A[电子罗盘开发路径] --> B[磁力计原理
霍尔效应/AMR]

B --> C[磁场校准
硬磁+软磁]

C --> D[航向角计算
倾斜补偿+arctan2]

D --> E[传感器融合
九轴Madgwick]

E --> F{应用方向}

F --> G[地图导航
方向指示]

F --> H[AR定位
世界坐标对齐]

F --> I[运动追踪
完整6DoF姿态]

classDef rootStyle fill:#4A90D9,stroke:#2C5F8A,color:#fff,font-weight:bold

classDef stepStyle fill:#50C878,stroke:#2E8B57,color:#fff,font-weight:bold

classDef branchStyle fill:#FF8C42,stroke:#CC6B30,color:#fff,font-weight:bold

classDef leafStyle fill:#9B59B6,stroke:#7D3C98,color:#fff,font-weight:bold

class A rootStyle

class B,C,D,E stepStyle

class F branchStyle

class G,H,I leafStyle

核心要点回顾:

磁场干扰:磁力计受环境干扰严重,硬磁/软磁校准是基础前提

倾斜补偿:设备倾斜时必须用加速度计补偿,否则航向角严重失真

角度环绕:0°/360°跳变处理是航向角平滑滤波的关键

九轴融合:加速度计+陀螺仪+磁力计三传感器融合,获得完整6DoF姿态

HOS6优化:优先使用MAGNETOMETER_UNCALIBRATED和ORIENTATION系统传感器

下一篇将深入传感器订阅模式与数据流优化,讲解如何高效、低功耗地使用传感器数据。