U1205 · Bosch BMM150 · 三轴地磁传感器

U1205 BMM150 三轴地磁传感器
+ 指南针UI 完整技术资料

这块BMM150地磁模块接到 ESP32-2432S028R(2.8"黄屏/CYD)上,做成一个带彩色刻度盘、三角箭头指针的指南针UI。这份资料整理了芯片规格、实际接线、完整代码、常见问题,以及两条对比视频——一条实拍、一条电脑渲染的UI设计效果。2026-07-23:首次接入,I2C扫描确认地址0x13,UI从纯文字读数迭代成了仪表盘风格。

1

总览

Overview

BMM150 是 Bosch 出的三轴数字地磁传感器,常见于做电子指南针/罗盘应用。这次把它接到 ESP32-2432S028R(俗称"CYD",板载2.8寸TFT屏)上,屏幕上实时画一个圆形刻度盘+箭头指针,跟传统指南针一样直观,下面还带一张XYZ三轴磁场原始读数的卡片。

BMM150

  • Bosch 三轴数字地磁传感器
  • I2C / SPI 双接口可选
  • I2C地址范围 0x10~0x13
  • 本模块实测地址:0x13
  • 板载LDO,支持3.3V/5V供电

ESP32-2432S028R(CYD)

  • ESP32-D0WD-V3 双核
  • 板载2.8" ST7789 TFT,240×320
  • CN1扩展口:GND/IO22/IO27/3V3
  • 本项目把CN1当I2C用(SCL/SDA)
  • USB芯片CH340,PlatformIO直接烧录

指南针UI

  • 彩色刻度圆盘,每30°一个刻度
  • 红/白双色箭头指针,指向实时朝向
  • 航向角度数字卡片(橙色)
  • XYZ三轴磁场原始读数卡片(绿色)
  • 位带SPI手写驱动,不依赖图形库
2

BMM150 详解

三轴数字地磁传感器

BMM150 是 Bosch Sensortec 出的低功耗三轴数字地磁传感器,专门为电子指南针类应用设计,内部包含霍尔效应传感元件和信号调理电路,直接通过总线读出已经换算好的磁场强度数值(单位微特斯拉 µT),不需要自己处理模拟信号。

项目参数
测量轴数三轴(X/Y/Z),单位 µT(微特斯拉)
总线接口I2C 或 SPI,由模块上 PS 引脚电平选择(本资料用I2C)
I2C地址0x10 / 0x11 / 0x12 / 0x13 四选一,由 CSB/SDO 两个引脚的高低电平组合决定;本模块实测应答地址是 0x13(对应DFRobot库里的 I2C_ADDRESS_4,CSB/SDO都是高电平,也是最常见的默认接法)
SPI芯片ID走SPI模式时读到的芯片ID寄存器预期值是 0x32,用来验证接线是否正常
供电芯片本身工作电压较低,多数成品模块板载LDO稳压,支持3.3V或5V输入供电
典型工作电流低功耗模式下电流很小(µA~mA级),具体看官方数据手册的功耗模式表

⚠ LDO 供电 ≠ 信号线可以接5V

模块板载的LDO解决的是"给5V也能正常供电"这件事,不代表SCL/SDA信号线可以被外部拉到5V。3.3V逻辑的主控(比如本资料用的ESP32)接I2C时,SCL/SDA电平应该保持在3.3V逻辑范围内,不要因为VCC接了5V就误以为信号线也能承受5V。


磁场环境干扰 & 校准

这是磁力计的通病,不是这个模块特有的问题:模块附近如果有电机、扬声器、继电器、大电流导线、磁铁,读数会明显偏移,测试/使用时尽量远离这些干扰源。真要做成精确指向的电子罗盘,还需要做硬铁/软铁校准(记录传感器在各个方向上的读数,算出偏移量再修正)——本资料这个UI目前只是直接显示getCompassDegree()的原始结果,没有做校准,测出来的角度仅供参考。

3

ESP32 CYD 详解

ESP32-2432S028R · 2.8" 黄屏

圈内俗称"CYD"(Cheap Yellow Display),主控是ESP32-D0WD-V3双核芯片,板载一块2.8寸ST7789驱动的TFT彩屏(240×320,SPI接口),还带一个CN1四针扩展口,本资料没用板子自带的电阻触摸屏,只用了屏幕显示 + CN1接I2C模块这两个功能。

项目参数
主控ESP32-D0WD-V3,双核,自带Wi-Fi/蓝牙(本项目未使用)
屏幕2.8" ST7789,240×320,SPI接口,位带(bit-bang)手写驱动
CN1扩展口GND / IO22 / IO27 / 3V3 四针,本资料把 IO22 当SCL、IO27 当SDA 用
USB芯片CH340,Windows下识别为USB-SERIAL CH340
烧录方式PlatformIO命令行 pio run --target upload,或Arduino IDE

CN1 引脚复用提醒

CN1上的IO22/IO27是通用GPIO,本资料当I2C(SCL/SDA)用;同一块板子在别的项目里(比如HC-SR04测距)会把这两个脚直接当数字GPIO用来读TRIG/ECHO——两种用法互斥,同一时间只能接一种模块,换项目前记得先拔掉旧模块。


⚠ 同一块板子换USB口,COM口号会变

Windows下这块板子的串口号(COMx)会随着USB口/插拔顺序变化,烧录前建议先用 Get-CimInstance Win32_PnPEntity | Where-Object { $_.Name -match "COM\d+" } 确认一下当前口号,再改 platformio.ini 里的 upload_port,不要直接沿用上一次记录的口号。

4

接线

✅ 已实物验证

只用到4根线,BMM150模块接到CYD的CN1扩展口上。

BMM150 模块ESP32 CYD · CN1

VCC3V3
GNDGND
SCLIO22
SDAIO27
模块走I2C模式,PS/CSB这类SPI相关引脚本资料没有用到,悬空即可

怎么确认地址是不是0x13

接好线之后先跑一遍I2C地址扫描工具(代码见 ESP32 2.8黄屏\AI开发记录\src\main.ino),屏幕上会直接显示扫到的7位地址。本模块实测就是0x13,下方有实拍截图为证;如果你的模块CSB/SDO接法不同,扫出来的地址可能是0x10~0x12中的一个,代码里改一下 I2C_ADDRESS_4 对应的常量就行。


I2C地址扫描结果显示0x13
实拍 · I2C扫描工具确认BMM150模块应答地址是0x13
5

UI设计 · 实拍对比动画

同一套设计,两种呈现方式

UI是圆形刻度盘 + 三角箭头指针的仪表盘风格:每30°一个刻度,N/E/S/W四个方向刻度更长更亮,指针红色箭头指向当前朝向、白色尾部指向反方向,下面两张卡片分别显示航向角度数字和XYZ三轴磁场原始读数。这里放两条视频对比:一条是真实设备实拍,一条是照着同一套配色/布局用代码逐帧渲染出来的动画版。

为什么放两条视频

手机/摄像头拍屏幕天然会有摩尔纹和轻微闪烁——这是摄像头快门频率和屏幕刷新频率没对齐导致的光学现象,跟屏幕或代码本身没关系,几乎所有拍屏幕的视频都会有。为了让UI设计本身(配色、圆盘、指针、卡片布局)能被看得干净清楚,额外用Python逐帧画了一版动画演示,跟实拍放在一起对比看。


实拍(优化版)· 真实设备画面,做了轻度调色/锐化,原始内容没有删减

动画渲染版 · 电脑逐帧画出来的UI设计效果演示,不是真实设备画面
6

完整代码

PlatformIO · Arduino (ESP32) ✅ 已实物验证

用 PlatformIO + Arduino核心,DFRobot_BMM150库的源码直接拷进了工程的 lib/ 目录,编译不用联网装库。

代码在本机的位置

下面这份代码就是工程里实际跑的那份,文件在 06_AI开发记录\src\main.ino(同目录 platformio.ini 是编译配置,lib\DFRobot_BMM150\ 是拷进来的库源码),用 VS Code 打开 06_AI开发记录 这个文件夹,PlatformIO 会自动识别。


platformio.ini
[env:esp32dev]
platform = espressif32@6.13.0
board = esp32dev
framework = arduino
; Arduino-ESP32 核心3.x(ESP-IDF5)在这台机器上链接报错:
;   undefined reference to `esp_bt_controller_mem_release'
; 是工具链BT库版本不匹配的已知问题,跟代码无关。固定用2.0.17这个稳定核心版本绕开。
; (跟 ESP32 2.8黄屏\AI开发记录\platformio.ini 里踩过的坑是同一个,写法照抄。)
platform_packages =
    framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32/releases/download/2.0.17/esp32-2.0.17.zip
upload_port = COM20
monitor_port = COM20
monitor_speed = 115200

src/main.ino
// BMM150 三轴地磁传感器 · ESP32-2432S028 (2.8" 黄屏/CYD) · 板载TFT指南针UI
// I2C: SCL=IO22, SDA=IO27 (CN1接口,跟HC-SR04/I2C扫描工程共用同一组引脚)
// 2026-07-23 用I2C扫描工具确认过,这块BMM150模块的地址是0x13
//   (对应DFRobot库里的I2C_ADDRESS_4,CS/SDO都接高电平的默认地址)。
// 屏幕驱动(位带SPI ST7789 + 5x7点阵字库)照抄HC-SR04项目里已验证过的
// hcsr04_esp32_cyd_tft.ino,没有改动驱动部分;在此基础上加了画圆/画线/填充圆
// 几个绘图函数,用来画指南针表盘和箭头指针。
// 传感器读数用的是 DFRobot_BMM150 库(已拷贝到本工程 lib/ 目录下,不依赖联网下载)。
#include <Wire.h>
#include <math.h>
#include "soc/gpio_struct.h"
#include "DFRobot_BMM150.h"

#define I2C_SCL_PIN 22
#define I2C_SDA_PIN 27

#define LCD_W 240
#define LCD_H 320

// ESP32-2432S028 / CYD 2.8 inch ST7789 internal LCD pins.
#define PIN_SCK  14
#define PIN_SDA  13
#define PIN_CS   15
#define PIN_DC   2
#define PIN_BLK  21

#define SCK_MASK (1UL << PIN_SCK)
#define SDA_MASK (1UL << PIN_SDA)
#define CS_MASK  (1UL << PIN_CS)
#define DC_MASK  (1UL << PIN_DC)
#define BLK_MASK (1UL << PIN_BLK)

#define FAST_HIGH(mask) (GPIO.out_w1ts = (mask))
#define FAST_LOW(mask)  (GPIO.out_w1tc = (mask))

#define SCK_0 FAST_LOW(SCK_MASK)
#define SCK_1 FAST_HIGH(SCK_MASK)
#define SDA_0 FAST_LOW(SDA_MASK)
#define SDA_1 FAST_HIGH(SDA_MASK)
#define CS_0  FAST_LOW(CS_MASK)
#define CS_1  FAST_HIGH(CS_MASK)
#define DC_0  FAST_LOW(DC_MASK)
#define DC_1  FAST_HIGH(DC_MASK)
#define BLK_1 FAST_HIGH(BLK_MASK)

// 16位 RGB565 颜色,配色比单纯黑白/红绿更接近成品UI的感觉
#define BLACK    0x0000
#define WHITE    0xFFFF
#define RED      0xF800
#define GREEN    0x07E0
#define BLUE     0x001F
#define CYAN     0x07FF
#define YELLOW   0xFFE0
#define GREY     0x4208
#define DKGREY   0x2104
#define ORANGE   0xFD20
#define NAVY     0x000C
#define SKYBLUE  0x2D7C
#define PANEL_BG 0x18E3   // 深蓝灰,卡片底色
#define RING_BG  0x0841   // 表盘底色,比纯黑亮一点点,有层次感

static const uint8_t FONT[][6] = {
  {' ', 0x00, 0x00, 0x00, 0x00, 0x00},
  {':', 0x00, 0x36, 0x36, 0x00, 0x00},
  {'.', 0x00, 0x60, 0x60, 0x00, 0x00},
  {'-', 0x08, 0x08, 0x08, 0x08, 0x08},
  {'0', 0x3E, 0x51, 0x49, 0x45, 0x3E},
  {'1', 0x00, 0x42, 0x7F, 0x40, 0x00},
  {'2', 0x42, 0x61, 0x51, 0x49, 0x46},
  {'3', 0x21, 0x41, 0x45, 0x4B, 0x31},
  {'4', 0x18, 0x14, 0x12, 0x7F, 0x10},
  {'5', 0x27, 0x45, 0x45, 0x45, 0x39},
  {'6', 0x3C, 0x4A, 0x49, 0x49, 0x30},
  {'7', 0x01, 0x71, 0x09, 0x05, 0x03},
  {'8', 0x36, 0x49, 0x49, 0x49, 0x36},
  {'9', 0x06, 0x49, 0x49, 0x29, 0x1E},
  {'A', 0x7E, 0x11, 0x11, 0x11, 0x7E},
  {'B', 0x7F, 0x49, 0x49, 0x49, 0x36},
  {'C', 0x3E, 0x41, 0x41, 0x41, 0x22},
  {'D', 0x7F, 0x41, 0x41, 0x22, 0x1C},
  {'E', 0x7F, 0x49, 0x49, 0x49, 0x41},
  {'F', 0x7F, 0x09, 0x09, 0x09, 0x01},
  {'G', 0x3E, 0x41, 0x49, 0x49, 0x7A},
  {'H', 0x7F, 0x08, 0x08, 0x08, 0x7F},
  {'I', 0x00, 0x41, 0x7F, 0x41, 0x00},
  {'K', 0x7F, 0x08, 0x14, 0x22, 0x41},
  {'L', 0x7F, 0x40, 0x40, 0x40, 0x40},
  {'M', 0x7F, 0x02, 0x0C, 0x02, 0x7F},
  {'N', 0x7F, 0x04, 0x08, 0x10, 0x7F},
  {'O', 0x3E, 0x41, 0x41, 0x41, 0x3E},
  {'P', 0x7F, 0x09, 0x09, 0x09, 0x06},
  {'R', 0x7F, 0x09, 0x19, 0x29, 0x46},
  {'S', 0x46, 0x49, 0x49, 0x49, 0x31},
  {'T', 0x01, 0x01, 0x7F, 0x01, 0x01},
  {'U', 0x3F, 0x40, 0x40, 0x40, 0x3F},
  {'V', 0x1F, 0x20, 0x40, 0x20, 0x1F},
  {'W', 0x3F, 0x40, 0x38, 0x40, 0x3F},
  {'X', 0x63, 0x14, 0x08, 0x14, 0x63},
  {'x', 0x44, 0x28, 0x10, 0x28, 0x44},
  {'Y', 0x07, 0x08, 0x70, 0x08, 0x07},
  {'Z', 0x61, 0x51, 0x49, 0x45, 0x43},
};

static void spiSendByte(uint8_t value) {
  for (uint8_t i = 0; i < 8; i++) {
    SCK_0;
    if (value & 0x80) { SDA_1; } else { SDA_0; }
    value <<= 1;
    SCK_1;
    SCK_0;
  }
}

static void sendCmd(uint8_t value) { DC_0; spiSendByte(value); }
static void sendData(uint8_t value) { DC_1; spiSendByte(value); }

static void setWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
  sendCmd(0x2A);
  sendData(x0 >> 8); sendData(x0 & 0xFF); sendData(x1 >> 8); sendData(x1 & 0xFF);
  sendCmd(0x2B);
  sendData(y0 >> 8); sendData(y0 & 0xFF); sendData(y1 >> 8); sendData(y1 & 0xFF);
  sendCmd(0x2C);
}

static void fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) {
  if (w <= 0 || h <= 0) return;
  if (x < 0) { w += x; x = 0; }
  if (y < 0) { h += y; y = 0; }
  if (x >= LCD_W || y >= LCD_H || w <= 0 || h <= 0) return;
  if (x + w > LCD_W) w = LCD_W - x;
  if (y + h > LCD_H) h = LCD_H - y;
  setWindow(x, y, x + w - 1, y + h - 1);
  for (uint32_t i = 0; i < (uint32_t)w * h; i++) {
    sendData(color >> 8);
    sendData(color & 0xFF);
  }
}

static void fillScreen(uint16_t color) {
  setWindow(0, 0, LCD_W - 1, LCD_H - 1);
  for (uint32_t i = 0; i < (uint32_t)LCD_W * LCD_H; i++) {
    sendData(color >> 8);
    sendData(color & 0xFF);
  }
}

static const uint8_t *glyphFor(char c) {
  for (uint8_t i = 0; i < sizeof(FONT) / sizeof(FONT[0]); i++) {
    if (FONT[i][0] == c) return &FONT[i][1];
  }
  return &FONT[0][1];
}

static void drawCharBg(uint16_t x, uint16_t y, char c, uint16_t color, uint16_t bg, uint8_t scale) {
  const uint8_t *g = glyphFor(c);
  uint16_t w = 6 * scale;
  uint16_t h = 8 * scale;
  setWindow(x, y, x + w - 1, y + h - 1);
  for (uint8_t py = 0; py < h; py++) {
    uint8_t row = py / scale;
    for (uint8_t px = 0; px < w; px++) {
      uint8_t col = px / scale;
      bool on = false;
      if (col < 5 && row < 7) on = g[col] & (1 << row);
      uint16_t out = on ? color : bg;
      sendData(out >> 8);
      sendData(out & 0xFF);
    }
  }
}

static void drawTextBg(uint16_t x, uint16_t y, const char *text, uint16_t color, uint16_t bg, uint8_t scale) {
  while (*text) {
    drawCharBg(x, y, *text++, color, bg, scale);
    x += 6 * scale;
  }
}

static void drawText(uint16_t x, uint16_t y, const char *text, uint16_t color, uint8_t scale) {
  drawTextBg(x, y, text, color, BLACK, scale);
}

static uint16_t textWidth(const char *text, uint8_t scale) {
  return strlen(text) * 6 * scale;
}

// 居中画一行字,center x 是文字整体的水平中心点
static void drawTextCentered(uint16_t centerX, uint16_t y, const char *text, uint16_t color, uint16_t bg, uint8_t scale) {
  uint16_t w = textWidth(text, scale);
  uint16_t x = (centerX > w / 2) ? centerX - w / 2 : 0;
  drawTextBg(x, y, text, color, bg, scale);
}

// ---------------- 基础图形:点/线/圆/填充圆 ----------------

static void setPixel(int16_t x, int16_t y, uint16_t color) {
  if (x < 0 || y < 0 || x >= LCD_W || y >= LCD_H) return;
  fillRect(x, y, 1, 1, color);
}

static void drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color) {
  int16_t dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
  int16_t dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
  int16_t err = dx + dy, e2;
  for (;;) {
    setPixel(x0, y0, color);
    if (x0 == x1 && y0 == y1) break;
    e2 = 2 * err;
    if (e2 >= dy) { err += dy; x0 += sx; }
    if (e2 <= dx) { err += dx; y0 += sy; }
  }
}

// 稍微加粗的线:主线 + 垂直方向各偏移1px,指针/刻度看起来不会细得像发丝
static void drawThickLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color) {
  drawLine(x0, y0, x1, y1, color);
  float dx = x1 - x0, dy = y1 - y0;
  float len = sqrtf(dx * dx + dy * dy);
  if (len < 1) return;
  int16_t ox = (int16_t)roundf(-dy / len);
  int16_t oy = (int16_t)roundf(dx / len);
  drawLine(x0 + ox, y0 + oy, x1 + ox, y1 + oy, color);
  drawLine(x0 - ox, y0 - oy, x1 - ox, y1 - oy, color);
}

static void drawCircle(int16_t cx, int16_t cy, int16_t r, uint16_t color) {
  int16_t x = r, y = 0, err = 0;
  while (x >= y) {
    setPixel(cx + x, cy + y, color); setPixel(cx + y, cy + x, color);
    setPixel(cx - y, cy + x, color); setPixel(cx - x, cy + y, color);
    setPixel(cx - x, cy - y, color); setPixel(cx - y, cy - x, color);
    setPixel(cx + y, cy - x, color); setPixel(cx + x, cy - y, color);
    y++;
    if (err <= 0) { err += 2 * y + 1; }
    if (err > 0) { x--; err -= 2 * x + 1; }
  }
}

static void fillCircle(int16_t cx, int16_t cy, int16_t r, uint16_t color) {
  for (int16_t dy = -r; dy <= r; dy++) {
    int16_t dx = (int16_t)sqrtf((float)(r * r - dy * dy));
    fillRect(cx - dx, cy + dy, 2 * dx + 1, 1, color);
  }
}

// 圆角矩形卡片(角是简单裁掉的斜角,不是真正圆弧,够用且不慢)
static void fillRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r, uint16_t color) {
  fillRect(x + r, y, w - 2 * r, h, color);
  fillRect(x, y + r, r, h - 2 * r, color);
  fillRect(x + w - r, y + r, r, h - 2 * r, color);
  for (int16_t dy = 0; dy <= r; dy++) {
    int16_t dx = (int16_t)sqrtf((float)(r * r - dy * dy));
    fillRect(x + r - dx, y + r - dy, dx, 1, color);
    fillRect(x + w - r, y + r - dy, dx, 1, color);
    fillRect(x + r - dx, y + h - r + dy - 1, dx, 1, color);
    fillRect(x + w - r, y + h - r + dy - 1, dx, 1, color);
  }
}

// 实心三角形(用于指针箭头),扫描线填充
static void fillTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint16_t color) {
  if (y0 > y1) { int16_t t; t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; }
  if (y1 > y2) { int16_t t; t = y1; y1 = y2; y2 = t; t = x1; x1 = x2; x2 = t; }
  if (y0 > y1) { int16_t t; t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; }
  if (y0 == y2) return;
  for (int16_t y = y0; y <= y2; y++) {
    bool secondHalf = y > y1 || y1 == y0;
    int16_t xa = x0 + (int32_t)(x2 - x0) * (y - y0) / (y2 - y0);
    int16_t xb;
    if (secondHalf) {
      xb = (y1 == y2) ? x2 : x1 + (int32_t)(x2 - x1) * (y - y1) / (y2 - y1);
    } else {
      xb = (y0 == y1) ? x1 : x0 + (int32_t)(x1 - x0) * (y - y0) / (y1 - y0);
    }
    if (xa > xb) { int16_t t = xa; xa = xb; xb = t; }
    fillRect(xa, y, xb - xa + 1, 1, color);
  }
}

static void tftInit() {
  pinMode(PIN_SCK, OUTPUT);
  pinMode(PIN_SDA, OUTPUT);
  pinMode(PIN_CS, OUTPUT);
  pinMode(PIN_DC, OUTPUT);
  pinMode(PIN_BLK, OUTPUT);

  SCK_1; CS_1; BLK_1;
  delay(100);
  CS_0;

  sendCmd(0x11);
  delay(120);
  sendCmd(0x3A); sendData(0x05);
  sendCmd(0xC5); sendData(0x1A);
  sendCmd(0x36); sendData(0x00);
  sendCmd(0xB2); sendData(0x05); sendData(0x05); sendData(0x00); sendData(0x33); sendData(0x33);
  sendCmd(0xB7); sendData(0x05);
  sendCmd(0xBB); sendData(0x3F);
  sendCmd(0xC0); sendData(0x2C);
  sendCmd(0xC2); sendData(0x01);
  sendCmd(0xC3); sendData(0x0F);
  sendCmd(0xC4); sendData(0x20);
  sendCmd(0xC6); sendData(0x01);
  sendCmd(0xD0); sendData(0xA4); sendData(0xA1);
  sendCmd(0xE8); sendData(0x03);
  sendCmd(0xE9); sendData(0x09); sendData(0x09); sendData(0x08);

  sendCmd(0xE0);
  sendData(0xD0); sendData(0x05); sendData(0x09); sendData(0x09);
  sendData(0x08); sendData(0x14); sendData(0x28); sendData(0x33);
  sendData(0x3F); sendData(0x07); sendData(0x13); sendData(0x14);
  sendData(0x28); sendData(0x30);

  sendCmd(0xE1);
  sendData(0xD0); sendData(0x05); sendData(0x09); sendData(0x09);
  sendData(0x08); sendData(0x03); sendData(0x24); sendData(0x32);
  sendData(0x32); sendData(0x3B); sendData(0x14); sendData(0x13);
  sendData(0x28); sendData(0x2F);

  sendCmd(0x20); // this 2.8" ST7789 panel needs INVOFF, not INVON
  sendCmd(0x29);
  CS_0;
}

// ---------------- 指南针 UI 布局 ----------------
// 表盘圆心/半径,往下留够两张数据卡片(航向角 + XYZ磁场)的空间
#define CX        120
#define CY        124
#define R_OUTER   54
#define R_NEEDLE  (R_OUTER - 16)   // 指针红色箭头长度,必须小于下面的清屏半径
#define R_TAIL    18               // 指针白色尾部,比箭头短,看起来才像指南针
#define R_CLEAR   (R_OUTER - 14)   // 每帧只清空这个半径内的区域来擦掉旧指针,刻度圈不用重画

DFRobot_BMM150_I2C bmm150(&Wire, I2C_ADDRESS_4);

static void drawDial() {
  // 表盘底色 + 双层外圈,比单线圆更有"仪表"质感
  fillCircle(CX, CY, R_OUTER + 3, DKGREY);
  fillCircle(CX, CY, R_OUTER, RING_BG);
  drawCircle(CX, CY, R_OUTER, SKYBLUE);
  drawCircle(CX, CY, R_OUTER + 3, GREY);

  // 每30度一个刻度,逢90度(N/E/S/W)刻度更长更亮
  for (int deg = 0; deg < 360; deg += 30) {
    float rad = deg * PI / 180.0f;
    float s = sinf(rad), c = -cosf(rad);
    bool major = (deg % 90 == 0);
    int16_t rOut = R_OUTER - 2;
    int16_t rIn = major ? (rOut - 10) : (rOut - 5);
    uint16_t tickColor = major ? YELLOW : SKYBLUE;
    drawLine(CX + (int16_t)(s * rIn), CY + (int16_t)(c * rIn),
              CX + (int16_t)(s * rOut), CY + (int16_t)(c * rOut), tickColor);
  }

  drawTextCentered(CX, CY - R_OUTER - 15, "N", RED, BLACK, 2);
  drawTextCentered(CX, CY + R_OUTER + 3, "S", WHITE, BLACK, 2);
  drawTextCentered(CX - R_OUTER - 12, CY - 7, "W", WHITE, BLACK, 2);
  drawTextCentered(CX + R_OUTER + 12, CY - 7, "E", WHITE, BLACK, 2);
}

// 只清空指针占用的内圆区域,表盘外圈/刻度/字母不用每次重画,减少闪烁
static void clearNeedleArea() {
  fillCircle(CX, CY, R_CLEAR, RING_BG);
}

static void drawNeedle(float headingDeg) {
  float rad = headingDeg * PI / 180.0f;
  float s = sinf(rad), c = -cosf(rad);
  int16_t tipX = CX + (int16_t)(s * R_NEEDLE);
  int16_t tipY = CY + (int16_t)(c * R_NEEDLE);
  int16_t tailX = CX - (int16_t)(s * R_TAIL);
  int16_t tailY = CY - (int16_t)(c * R_TAIL);

  // 指针尾部(灰白,指向正南方向)
  drawThickLine(CX, CY, tailX, tailY, WHITE);

  // 指针头部画成三角箭头(红色,指向传感器当前朝向),比直线更像真实指南针
  float perpX = -c, perpY = s;   // 垂直于指针方向的单位向量
  float baseX = CX + s * (R_NEEDLE - 14);
  float baseY = CY + c * (R_NEEDLE - 14);
  int16_t leftX  = (int16_t)(baseX + perpX * 5);
  int16_t leftY  = (int16_t)(baseY + perpY * 5);
  int16_t rightX = (int16_t)(baseX - perpX * 5);
  int16_t rightY = (int16_t)(baseY - perpY * 5);
  fillTriangle(tipX, tipY, leftX, leftY, rightX, rightY, RED);
  drawThickLine(CX, CY, (int16_t)baseX, (int16_t)baseY, RED);

  fillCircle(CX, CY, 4, YELLOW);
  drawCircle(CX, CY, 4, DKGREY);
}

#define CARD1_Y (CY + R_OUTER + 18)
#define CARD1_H 32
#define CARD2_Y (CARD1_Y + CARD1_H + 8)
#define CARD2_H 78

static void drawStaticChrome() {
  fillScreen(BLACK);
  fillRect(0, 0, LCD_W, 34, NAVY);
  drawTextCentered(LCD_W / 2, 9, "BMM150 COMPASS", CYAN, NAVY, 2);
  drawDial();
  fillRoundRect(8, CARD1_Y, LCD_W - 16, CARD1_H, 6, PANEL_BG);
  fillRoundRect(8, CARD2_Y, LCD_W - 16, CARD2_H, 6, PANEL_BG);
  drawText(16, CARD2_Y + 6, "MAG FIELD UT", GREY, 1);
}

static void showStatus(const char *msg, uint16_t color) {
  fillRect(8, CARD1_Y, LCD_W - 16, CARD1_H, PANEL_BG);
  drawTextCentered(LCD_W / 2, CARD1_Y + (CARD1_H - 16) / 2, msg, color, PANEL_BG, 2);
}

static void drawHeadingCard(float headingDeg) {
  fillRoundRect(8, CARD1_Y, LCD_W - 16, CARD1_H, 6, PANEL_BG);
  char buf[16];
  snprintf(buf, sizeof(buf), "%3d DEG", (int)(headingDeg + 0.5f));
  drawTextCentered(LCD_W / 2, CARD1_Y + (CARD1_H - 24) / 2, buf, ORANGE, PANEL_BG, 3);
}

// X/Y/Z 三行竖排显示,负数、超过三位数的读数都留够宽度不会挤在一起
static void drawMagRow(int16_t y, char axis, float value, uint16_t color) {
  char num[10];
  dtostrf(value, 7, 1, num);
  char line[16];
  snprintf(line, sizeof(line), "%c%s", axis, num);
  drawTextBg(14, y, line, color, PANEL_BG, 2);
}

static void drawMagCard(float x, float y, float z) {
  fillRect(10, CARD2_Y + 16, LCD_W - 20, CARD2_H - 18, PANEL_BG);
  drawMagRow(CARD2_Y + 18, 'X', x, GREEN);
  drawMagRow(CARD2_Y + 38, 'Y', y, GREEN);
  drawMagRow(CARD2_Y + 58, 'Z', z, GREEN);
}

void setup() {
  Serial.begin(115200);
  tftInit();
  drawStaticChrome();
  showStatus("CONNECTING", YELLOW);

  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);

  while (bmm150.begin() != 0) {
    showStatus("BMM150 NOT FOUND", RED);
    Serial.println("bmm150 init failed, retrying...");
    delay(1000);
  }

  bmm150.setOperationMode(BMM150_POWERMODE_NORMAL);
  bmm150.setPresetMode(BMM150_PRESETMODE_REGULAR);
  bmm150.setRate(BMM150_DATA_RATE_10HZ);
  bmm150.setMeasurementXYZ();

  showStatus("READY", GREEN);
  delay(400);
}

void loop() {
  sBmm150MagData_t mag = bmm150.getGeomagneticData();
  float heading = bmm150.getCompassDegree();

  clearNeedleArea();
  drawNeedle(heading);
  drawHeadingCard(heading);
  drawMagCard(mag.x, mag.y, mag.z);

  Serial.printf("heading=%.1f  x=%.1f y=%.1f z=%.1f uT\n", heading, mag.x, mag.y, mag.z);
  delay(150);
}
7

常见问题排查

全部是实际踩过/查证过的坑
  • I2C扫描不到设备:先确认VCC/GND有没有接反或者没接实,再确认SCL/SDA接的是IO22/IO27——CYD的CN1排针如果没插到底,插拔几次通常就好。
  • I2C扫到的地址不是0x13:不是坏了,是模块CSB/SDO两个引脚的接法/焊接状态不同,扫到0x10~0x12都正常,代码里把 I2C_ADDRESS_4 换成扫到的实际地址对应的常量即可。
  • 读数一直是0或者跳动很大:先排查附近有没有电机、喇叭、继电器、磁铁、大电流导线,这类磁场干扰源是磁力计最常见的读数异常原因,挪开模块或者远离干扰源试试。
  • 做成真正准确指向的电子罗盘:必须做硬铁/软铁校准,本资料的UI目前没有实现校准,显示的角度是传感器原始换算结果,仅供参考,见第2节说明。
  • 误以为LDO支持5V输入,信号线也能接5V:LDO只负责电源,SCL/SDA信号电平不受LDO影响,3.3V主控还是要按3.3V逻辑对待,见第2节危险提示。
  • 实拍视频屏幕上有一条条纹/摩尔纹:是摄像头拍屏幕的正常光学现象(快门和屏幕刷新频率没对齐),不是硬件或代码问题,见第5节的动画对比版。
  • 同一块CYD板子COM口对不上:换了USB口或者重新插拔,Windows分配的COM口号会变,烧录前用PowerShell命令重新确认一下,见第3节。
  • PlatformIO编译报 undefined reference to 'esp_bt_controller_mem_release':默认拉取的Arduino-ESP32核心3.x(ESP-IDF5)跟本机工具链里的BT库版本不匹配,是已知问题,platformio.ini里已经用platform_packages固定了2.0.17这个稳定核心版本绕开,不用再重新踩一次。
  • Windows下编译/烧录报中文路径相关的错误:PlatformIO/GCC工具链不能处理中文路径,工程文件夹要放在纯英文路径下才能正常编译,这是STM32/ESP32两条工具链共同的限制,没有绕过办法,只能换目录。
8

实拍图与演示

汇总第4、5节已嵌入的素材
I2C地址扫描结果显示0x13
I2C扫描结果,确认BMM150模块地址0x13

实拍(优化版)· 指南针UI真实运行效果

动画渲染版 · UI设计效果演示
9

参考资料来源

官方数据手册 / 库源码

第2节的芯片规格参数来自以下公开资料,感兴趣可以查看原文获取更完整的电气特性表:

  • Bosch BMM150 官方数据手册 — Bosch Sensortec 官方PDF
  • DFRobot_BMM150 Arduino库源码 — GitHub,本资料第6节代码里的库就是拷贝自这里
  • 本模块配套的旧版技术资料页 — 产品页面,讲了通用的Arduino/ESP32/树莓派接线和SPI调试方法;本资料是在此基础上,针对"接到CYD黄屏板 + 指南针UI"这个具体用法重新整理的一份