总览
OverviewBMM150 是 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手写驱动,不依赖图形库
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也能正常供电"这件事,不代表SCL/SDA信号线可以被外部拉到5V。3.3V逻辑的主控(比如本资料用的ESP32)接I2C时,SCL/SDA电平应该保持在3.3V逻辑范围内,不要因为VCC接了5V就误以为信号线也能承受5V。
这是磁力计的通病,不是这个模块特有的问题:模块附近如果有电机、扬声器、继电器、大电流导线、磁铁,读数会明显偏移,测试/使用时尽量远离这些干扰源。真要做成精确指向的电子罗盘,还需要做硬铁/软铁校准(记录传感器在各个方向上的读数,算出偏移量再修正)——本资料这个UI目前只是直接显示getCompassDegree()的原始结果,没有做校准,测出来的角度仅供参考。
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上的IO22/IO27是通用GPIO,本资料当I2C(SCL/SDA)用;同一块板子在别的项目里(比如HC-SR04测距)会把这两个脚直接当数字GPIO用来读TRIG/ECHO——两种用法互斥,同一时间只能接一种模块,换项目前记得先拔掉旧模块。
Windows下这块板子的串口号(COMx)会随着USB口/插拔顺序变化,烧录前建议先用 Get-CimInstance Win32_PnPEntity | Where-Object { $_.Name -match "COM\d+" } 确认一下当前口号,再改 platformio.ini 里的 upload_port,不要直接沿用上一次记录的口号。
接线
✅ 已实物验证只用到4根线,BMM150模块接到CYD的CN1扩展口上。
BMM150 模块ESP32 CYD · CN1
接好线之后先跑一遍I2C地址扫描工具(代码见 ESP32 2.8黄屏\AI开发记录\src\main.ino),屏幕上会直接显示扫到的7位地址。本模块实测就是0x13,下方有实拍截图为证;如果你的模块CSB/SDO接法不同,扫出来的地址可能是0x10~0x12中的一个,代码里改一下 I2C_ADDRESS_4 对应的常量就行。
UI设计 · 实拍对比动画
同一套设计,两种呈现方式UI是圆形刻度盘 + 三角箭头指针的仪表盘风格:每30°一个刻度,N/E/S/W四个方向刻度更长更亮,指针红色箭头指向当前朝向、白色尾部指向反方向,下面两张卡片分别显示航向角度数字和XYZ三轴磁场原始读数。这里放两条视频对比:一条是真实设备实拍,一条是照着同一套配色/布局用代码逐帧渲染出来的动画版。
手机/摄像头拍屏幕天然会有摩尔纹和轻微闪烁——这是摄像头快门频率和屏幕刷新频率没对齐导致的光学现象,跟屏幕或代码本身没关系,几乎所有拍屏幕的视频都会有。为了让UI设计本身(配色、圆盘、指针、卡片布局)能被看得干净清楚,额外用Python逐帧画了一版动画演示,跟实拍放在一起对比看。
完整代码
PlatformIO · Arduino (ESP32) ✅ 已实物验证用 PlatformIO + Arduino核心,DFRobot_BMM150库的源码直接拷进了工程的 lib/ 目录,编译不用联网装库。
下面这份代码就是工程里实际跑的那份,文件在 06_AI开发记录\src\main.ino(同目录 platformio.ini 是编译配置,lib\DFRobot_BMM150\ 是拷进来的库源码),用 VS Code 打开 06_AI开发记录 这个文件夹,PlatformIO 会自动识别。
[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
// 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); }
常见问题排查
全部是实际踩过/查证过的坑- 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两条工具链共同的限制,没有绕过办法,只能换目录。
实拍图与演示
汇总第4、5节已嵌入的素材
参考资料来源
官方数据手册 / 库源码第2节的芯片规格参数来自以下公开资料,感兴趣可以查看原文获取更完整的电气特性表:
- Bosch BMM150 官方数据手册 — Bosch Sensortec 官方PDF
- DFRobot_BMM150 Arduino库源码 — GitHub,本资料第6节代码里的库就是拷贝自这里
- 本模块配套的旧版技术资料页 — 产品页面,讲了通用的Arduino/ESP32/树莓派接线和SPI调试方法;本资料是在此基础上,针对"接到CYD黄屏板 + 指南针UI"这个具体用法重新整理的一份