ESP-12F通过IIC读取MPU6050六轴数据与互补滤波姿态解算
简介:面向物联网与嵌入式开发者的 ESP-12F 平台 MPU6050 六轴传感器接入示例,聚焦 ESP8266 Wi-Fi 模块通过 I²C 读取加速度计、陀螺仪数据这一常见场景。压缩包共 605 个文件,约 11.02MB,以 C 源文件与头文件为主体,辅以 Makefile、链接脚本、预编译库、烧录脚本及说明文档,目录覆盖驱动初始化、寄存器读取、配置与构建等环节,便于对照 GPIO14 时钟线、0x68 地址等硬件连接理解通信流程。已有 483 人学习,适合需要完成课程实验、毕业设计或 IoT 原型验证的初中级开发者参考。代码与库文件较完整,可用于梳理 ESP-12F 的 I²C 时序、传感器原始数据获取与基础处理思路,也能作为后续姿态解算、运动追踪和设备联调的起点。
1. esp-12f_mpu6050_demo.rar 拆开看:一条 IIC 链路能卡住多少人
esp-12f_mpu6050_demo.rar 解压出来是一套跑在 ESP-12F 上、通过 IIC 读 MPU6050 六轴数据的例程,核心链路只有一条:ESP8266 用软件 IIC 去读 0x68 这个从机地址上的 14 个数据寄存器。听着简单,卡人的地方却全在代码之外——串口刷出来一行 0,或者 WHO_AM_I 读回 0xFF,很多人第一反应是模块坏了,实际往往是 GPIO0 和 GPIO15 的启动电平没有配对,或者把 5V 电平直接灌进了只吃 3.3V 的引脚。
它适合刚上手 ESP8266、想拿六轴数据做倾角检测、振动记录或者简易姿态 Demo 的人,也适合把它当成 IIC 时序调试的练手板:只有一根 SDA 一根 SCL,出问题时能排除的变量少,反而容易把问题定位清楚。下面按接线、寄存器、换算、滤波、排错的顺序拆。
2. 接线与寄存器地图:先把 IIC 链路跑通再谈姿态解算
硬件这一层是整套 demo 的地基,也是新手失败率最高的环节。ESP-12F 本质是一块带 4MB Flash 的 ESP8266 模组,引脚间距 2mm,直接焊在转接板上才好插面包板。它不像 STM32 那样上电就能跑,需要几个引脚配合成特定的电平组合才能正常启动;MPU6050 那边看起来只要四根线,但对电源纹波和总线电平同样敏感。
2.1 供电与引脚:ESP-12F 的启动条件比 MPU6050 更挑剔
先把 ESP-12F 的启动条件摆清楚,这是很多人第一次接线就失败的原因。
| 引脚 | 启动时要求 | 常见做法 |
|---|---|---|
| VCC | 3.3V,峰值电流要留到 400mA | 独立 LDO 供电,不要从 USB-TTL 的 3.3V 引脚抽取大电流 |
| GND | 共地 | 模组地与传感器地单点汇合 |
| CH_PD (EN) | 高电平 | 10k 上拉到 3.3V |
| GPIO0 | 高电平为运行,低电平为下载 | 10k 上拉,加一个按键到地 |
| GPIO15 | 低电平 | 10k 下拉到地 |
| GPIO2 | 高电平 | 悬空或 10k 上拉 |
| GPIO4 | IIC SDA | 接 MPU6050 SDA,模组板载 4.7k 上拉可省 |
| GPIO5 | IIC SCL | 接 MPU6050 SCL |
MPU6050 一侧,VCC 接 3.3V、GND 接地、SDA/SCL 对应接上,AD0 接地时地址是 0x68,接 3.3V 时是 0x69。INT 引脚在基础 demo 里可以不接,后面做中断触发采样时才用得上。市面上常见的小蓝板自带 3.3V LDO 和 4.7k 上拉电阻,可以直接吃 5V;裸芯片版本的 MPU6050 绝对只能给 3.3V,这一点不要凭印象判断,看一眼板上有没有稳压芯片再说。
另一个容易被忽略的点:WiFi 发射瞬间的电流会冲到 300mA 以上,如果你的 3.3V 来源是一颗只有 100mA 输出的 LDO,现象会非常像 IIC 通信挂了——数据偶尔正常、偶尔全 0,抓不到规律。我一般在模组 VCC 与 GND 之间并一颗 100uF 电解加一颗 0.1uF 瓷片,成本两毛钱,能省掉半天排查。
2.2 四根线接完,用 WHO_AM_I 判定链路是否真的通
不要一上来就写完整的采集程序。先用一个最小程序确认两件事:总线上有没有设备,以及 MPU6050 到底是不是正品。
#include <Wire.h> // ESP-12F 转接板上 GPIO5 是 SCL,GPIO4 是 SDA,按模组丝印接即可 static const uint8_t PIN_SDA = 4; static const uint8_t PIN_SCL = 5; void setup() { Serial.begin(115200); delay(300); // 等 USB-TTL 稳定,省掉这句会丢开头几行日志 Wire.begin(PIN_SDA, PIN_SCL); Wire.setClock(100000); // 先用 100kHz,链路稳了再往上试 Serial.println("i2c scan start"); } void loop() { // 扫全总线,把应答的从机地址打出来 for (uint8_t addr = 1; addr < 127; addr++) { Wire.beginTransmission(addr); if (Wire.endTransmission() == 0) { Serial.printf("device found at 0x%02X\n", addr); } } // 读 WHO_AM_I(0x75),MPU6050 正常返回 0x68 Wire.beginTransmission(0x68); Wire.write(0x75); uint8_t err = Wire.endTransmission(false); // false 表示不发 STOP,接着走重复起始 uint8_t n = Wire.requestFrom((uint8_t)0x68, (uint8_t)1); if (err == 0 && n == 1) { Serial.printf("WHO_AM_I = 0x%02X\n", Wire.read()); } else { Serial.printf("read fail, err=%u, got=%u\n", err, n); } delay(1000); }这段代码有两个关键参数值得说清楚。Wire.endTransmission(false)里的false是重复起始条件,IIC 读寄存器的标准动作是「写寄存器地址 → 不发 STOP → 重新起始 → 读一个字节」,如果这里写成true,某些从机会把总线释放掉,第二次起始就变成了一个新的写事务,读出来自然是 0。另外requestFrom返回的是实际读到的字节数,必须校验它等于 1,否则Wire.read()会返回 -1,强制转成 uint8_t 就是 0xFF,看起来特别像「从机不应答」。
Wire.endTransmission()的返回值是一张诊断表:0 表示成功,1 表示数据太长,2 表示地址阶段收到 NACK,3 表示数据阶段收到 NACK,4 表示其他错误,5 表示超时。看到 2 就往地址和接线方向查,看到 5 就往总线上拉和线长方向查,比盲猜快得多。
如果 WHO_AM_I 读回来是 0x68,链路通了。如果是 0x70 或 0x98,说明手上这颗是 MPU6500 系列的兼容片,寄存器地址大体一致但部分配置位含义不同,DMP 固件更是不能混用。如果是 0x00 或 0xFF,先别怀疑芯片,往下看第 5 章的排查清单。
2.3 寄存器初始化顺序:从复位到 1kHz 采样
MPU6050 上电后默认处于睡眠状态,所有数据寄存器都是 0。不写 PWR_MGMT_1 就直接读,会得到一堆漂亮的 0,很多人就是在这里误判成接线问题。
static void writeReg(uint8_t reg, uint8_t val) { Wire.beginTransmission(0x68); Wire.write(reg); Wire.write(val); Wire.endTransmission(); } void mpuInit() { writeReg(0x6B, 0x80); // DEVICE_RESET,软复位所有寄存器 delay(100); // 复位后必须等,短了后续写入会被吞掉 writeReg(0x6B, 0x01); // 退出睡眠,时钟源选陀螺 X 轴 PLL,比内部 RC 稳 delay(10); writeReg(0x19, 0x04); // 采样分频:1kHz / (1+4) = 200Hz 输出率 writeReg(0x1A, 0x03); // DLPF 低通 44Hz(加速度)/42Hz(陀螺),压掉机架振动 writeReg(0x1B, 0x00); // 陀螺量程 ±250 dps writeReg(0x1C, 0x00); // 加速度量程 ±2g writeReg(0x38, 0x01); // 数据就绪中断使能,INT 悬空也不影响 writeReg(0x6A, 0x00); // 关 FIFO,保持最简单的直读模式 }逐条解释一下取舍。0x6B写0x01而不是0x00,是因为0x00用的是内部 8MHz RC 振荡器做时钟源,温漂大,陀螺零偏会跟着漂;选陀螺 X 轴 PLL 之后稳定性明显好一截,代价是上电后要等陀螺起振,所以中间那个delay(10)不能删。
0x19的采样分频决定了输出率:DLPF 使能时内部采样率固定 1kHz,输出率 = 1000/(1+div)。写 4 得到 200Hz,正好和后面互补滤波的循环节拍对得上。想跑更快就写 0(1kHz),但 ESP8266 的软件 IIC 在 100kHz 速率下读 14 字节大约要 1.5ms,1kHz 是跑不满的。
0x1A的 DLPF 是最容易被忽视又最影响观感的一个参数。装在电机、风扇、车架上的模块,原始数据里的高频毛刺非常多,开到 44Hz 低通之后曲线立刻变干净。如果做的是慢速倾角检测,可以一路开到0x06(5Hz),代价是响应变迟钝,快速翻转时会明显滞后。
0x1B和0x1C的量程选择见下一章,先记住一点:量程改了一定要同步改换算系数,两处不一致是数据「看着像对的其实全错」的经典来源。
3. 原始值到物理量:三组换算系数与 500 次零偏标定
寄存器读出来的是 int16 的原始码值,不是加速度也不是角速度。这一步做错,后面滤波再怎么调都是白费。同时陀螺的零偏是必须标定的,不标定直接积分,静止放置几十秒角度就能漂出好几度。
3.1 量程选择与 LSB 换算:±2g 与 ±250dps 不是随便选的
MPU6050 的 ADC 是 16 位,但有效范围由量程决定,换算关系如下表。
| 寄存器 | 配置值 | 量程 | 灵敏度 (LSB) | 适用场景 |
|---|---|---|---|---|
| ACCEL_CONFIG(0x1C) | 0x00 | ±2g | 16384 LSB/g | 倾角检测、静态姿态,分辨率最高 |
| 同上 | 0x08 | ±4g | 8192 LSB/g | 手持设备、慢速运动 |
| 同上 | 0x10 | ±8g | 4096 LSB/g | 有冲击、跌落检测 |
| 同上 | 0x18 | ±16g | 2048 LSB/g | 碰撞、振动记录 |
| GYRO_CONFIG(0x1B) | 0x00 | ±250 dps | 131 LSB/(°/s) | 慢速姿态,分辨率最高 |
| 同上 | 0x08 | ±500 dps | 65.5 LSB/(°/s) | 一般机器人 |
| 同上 | 0x10 | ±1000 dps | 32.8 LSB/(°/s) | 快速旋转 |
| 同上 | 0x18 | ±2000 dps | 16.4 LSB/(°/s) | 高动态、无人机 |
选型的判断标准是「量程要略大于实际峰值」。做桌面倾角仪用 ±2g 完全够,一旦超过 2g 就会削顶,波形被切成平头,比噪声还难处理;反过来做碰撞记录用 ±2g,几乎必然削顶。陀螺同理,手部动作峰值一般在 300dps 上下,±500dps 是比较稳妥的档。
温度那两字节的换算公式是temp = raw/340.0 + 36.53,虽然精度一般,但用来观察芯片有没有发热、有没有在正常工作很管用。
3.2 零偏标定:静止采样 500 次求均值
陀螺仪的零偏是器件固有的, datasheet 上给的典型值是 ±20dps 以内,实际个体差异可能更大。不扣掉这个偏置,做积分时角度会以肉眼可见的速度单向漂移。
struct ImuRaw { int16_t ax, ay, az, gx, gy, gz, t; }; bool readImu(ImuRaw &r) { Wire.beginTransmission(0x68); Wire.write(0x3B); // 从 ACCEL_XOUT_H 开始连续读 if (Wire.endTransmission(false) != 0) return false; if (Wire.requestFrom((uint8_t)0x68, (uint8_t)14) != 14) return false; uint8_t b[14]; for (uint8_t i = 0; i < 14; i++) b[i] = Wire.read(); r.ax = (int16_t)((b[0] << 8) | b[1]); r.ay = (int16_t)((b[2] << 8) | b[3]); r.az = (int16_t)((b[4] << 8) | b[5]); r.t = (int16_t)((b[6] << 8) | b[7]); r.gx = (int16_t)((b[8] << 8) | b[9]); r.gy = (int16_t)((b[10] << 8) | b[11]); r.gz = (int16_t)((b[12] << 8) | b[13]); return true; } // 静止状态下调用,板子要放平且不要碰 void calibrateGyro(float &bx, float &by, float &bz) { const int N = 500; double sx = 0, sy = 0, sz = 0; int valid = 0; ImuRaw r; for (int i = 0; i < N; i++) { if (readImu(r)) { sx += r.gx; sy += r.gy; sz += r.gz; valid++; } delay(5); // 约 200Hz,与 SMPLRT_DIV 对齐 } const float LSB = 131.0f; // 必须与 GYRO_CONFIG 一致 bx = sx / valid / LSB; by = sy / valid / LSB; bz = sz / valid / LSB; Serial.printf("gyro bias: %.3f %.3f %.3f dps\n", bx, by, bz); }一次读 14 个字节而不是分 7 次读,是因为分开读会引入时间偏差:加速度和角速度不是同一时刻采样的,做姿态融合时会表现为高频抖动。MPU6050 的 0x3B 到 0x48 是连续排列的,一次突发读正好覆盖加速度、温度、陀螺三组数据,这是这颗芯片设计上给出的便利。
valid计数不能省。如果标定过程中有几次读失败,直接拿 N 去除会把偏置算小,表现为「标定了但还是漂」。另外标定必须在静止且水平的状态下做,很多人拿着板子一边晃一边按复位,标出来的偏置是个随机数,这类翻车我见过不止一次。
标定结果不需要每次上电都重算,可以存到 EEPROM 或 RTC 内存里,第 6 章会给具体做法。
3.3 串口与 CSV 输出:让数据能进表格也能进示波器
数据出来之后要能看。ESP8266 的 Arduino 串口绘图器能识别几种固定格式,最简单的是每行只打一个数;但六轴数据需要同时看多路,用 CSV 更方便,可以直接粘进表格画图。
void loop() { ImuRaw r; if (!readImu(r)) { delay(5); return; } const float ACC_LSB = 16384.0f; // ±2g const float GYR_LSB = 131.0f; // ±250 dps float ax = r.ax / ACC_LSB, ay = r.ay / ACC_LSB, az = r.az / ACC_LSB; float gx = r.gx / GYR_LSB - biasX; float gy = r.gy / GYR_LSB - biasY; float gz = r.gz / GYR_LSB - biasZ; float tc = r.t / 340.0f + 36.53f; // 带时间戳的 CSV,Arduino 串口绘图器里把分隔符设成逗号即可多路同屏 Serial.printf("%lu,%.3f,%.3f,%.3f,%.2f,%.2f,%.2f,%.2f\n", millis(), ax, ay, az, gx, gy, gz, tc); delay(5); }delay(5)对应约 200Hz,和采样分频保持一致。如果你把 0x19 改成了别的值,这里的延时也要跟着改,否则要么读到的数据重复、要么丢帧。串口输出本身是有成本的,115200 波特率下一行 60 个字符大约要 5ms,所以想跑到 200Hz 以上就必须把打印降频,比如每 10 帧打一次,或者改成二进制帧。
4. 互补滤波上板:ESP8266 每秒 200 帧的算力账怎么算
拿到干净的六轴数据之后,下一步是把它变成角度。这一步的核心矛盾在于:加速度计静态准、动态噪声大;陀螺仪动态准、静态漂移。两者融合是必须的,问题是选哪种融合方式,以及在这颗 80MHz 的芯片上跑不跑得动。
4.1 为什么不用 DMP:ESP8266 上移植 DMP 的现实成本
MPU6050 内部有个 DMP 单元,能直接输出四元数,听起来很省事。但实际情况是:DMP 需要往芯片里灌一段约 3KB 的固件镜像,官方只提供了针对特定平台的驱动库,移植到 ESP8266 上要处理 IIC 读写时序、内存布局、中断响应一整套东西。更麻烦的是这段固件是黑匣子,出问题时你只能看到四元数不对,没法知道是哪一步错了。
我在一个模拟项目里试过移植,卡了整整两天,最后放弃改用互补滤波,二十分钟就跑起来了。所以这套 demo 的选择很实际:不做 DMP,用互补滤波。理由有三条——代码量不到 20 行,参数只有一个且物理意义明确,出问题时每一步中间量都能打印出来看。对绝大多数倾角、姿态展示类需求,互补滤波的精度完全够用。
如果确实需要高动态下的精确姿态,正确的路线是换一颗自带姿态解算的传感器,或者上更完整的卡尔曼滤波,而不是在 DMP 移植上死磕。
4.2 互补滤波实现与 dt 的取法
互补滤波的思路很直白:陀螺积分得到短时角度,加速度算出绝对倾角,用加权平均把两者揉在一起。
float roll = 0.0f, pitch = 0.0f; uint32_t lastUs = 0; const float ALPHA = 0.98f; // 陀螺权重,越大越信任陀螺 void updateAttitude(float ax, float ay, float az, float gx, float gy, float gz) { uint32_t now = micros(); float dt = (now - lastUs) / 1e6f; lastUs = now; if (dt <= 0.0f || dt > 0.2f) return; // 首帧或长时间阻塞直接丢弃 // 加速度算绝对倾角,注意 atan2 的参数顺序 float accRoll = atan2f(ay, az) * 57.2958f; float accPitch = atan2f(-ax, sqrtf(ay * ay + az * az)) * 57.2958f; roll = ALPHA * (roll + gx * dt) + (1.0f - ALPHA) * accRoll; pitch = ALPHA * (pitch + gy * dt) + (1.0f - ALPHA) * accPitch; }dt必须用micros()而不是millis()。ESP8266 跑着 WiFi 协议栈,主循环随时可能被中断抢占几十毫秒,用毫秒级时间戳算出来的 dt 误差可达 10% 以上,表现为角度偶尔跳一下。用微秒级时间戳再配合dt > 0.2f的丢弃判断,能过滤掉绝大部分卡顿帧。
ALPHA取 0.98 是个经验值,对应时间常数约 0.5 秒。想更信任加速度计就调小到 0.9,角度跟随快但抖动明显;想更平滑就调到 0.99,代价是角度变化滞后。做慢速倾角仪我一般用 0.95,做手持设备用 0.98。
atan2f的参数顺序是最常见的笔误来源。roll 用atan2(ay, az),pitch 用atan2(-ax, sqrt(ay²+az²)),写成atan2(az, ay)得到的是余角,现象是「板子放平显示 90 度」,很容易误判成传感器装反了。另外加速度计的坐标定义要和芯片丝印对齐,MPU6050 的 X 轴指向芯片的某一侧,焊接方向一转,符号全反。
4.3 把数据送出去:UDP 推流与采样节拍
串口只能连一根线,做完姿态解算之后通常要把数据送到上位机。ESP8266 自带 WiFi,用 UDP 推流是最省事的方式——不需要建立连接,丢一两包也不影响趋势观察。
#include <ESP8266WiFi.h> #include <WiFiUdp.h> WiFiUDP udp; IPAddress hostIp(192, 168, 1, 100); // 上位机地址,按实际网段改 const uint16_t HOST_PORT = 9000; void pushSample(float ax, float ay, float az, float gx, float gy, float gz, float rl, float pt) { static uint32_t lastPush = 0; if (millis() - lastPush < 20) return; // 限到 50Hz,UDP 发太快会挤爆发送缓冲 lastPush = millis(); udp.beginPacket(hostIp, HOST_PORT); udp.printf("%lu,%.3f,%.3f,%.3f,%.2f,%.2f,%.2f,%.2f,%.2f\n", millis(), ax, ay, az, gx, gy, gz, rl, pt); udp.endPacket(); }50Hz 的上限是根据实际测出来的:ESP8266 在 STA 模式下单包 UDP 的发送开销大约 1 到 3ms,200Hz 连续发包会把发送缓冲堆满,endPacket()开始返回失败,表现为上位机收到的数据有大量空洞。要做 200Hz 就得改 TCP 或者用二进制打包压缩帧长,文本协议不适合高速场景。
还有一个节拍上的细节:采样和发送不要耦合在同一个节奏里。采样按 200Hz 走,发送按 50Hz 走,中间用一个环形缓冲过渡。否则一旦网络卡一下,采样也跟着停,dt 就乱了,角度会抖。
5. 排查清单:IIC 读不到 0x68 的五种现场与对应改法
下面这五条都是我在实际调试里反复遇到的,按出现频率排序。每一条都给出「现象 → 原因 → 解决」,遇到问题可以直接对号入座。
5.1 现场一:扫描不到任何设备,串口一片空白
现象:IIC 扫描循环跑完,一个device found都没打印。
原因:先看硬件。第一种可能是 SDA/SCL 接反,ESP-12F 的 GPIO4/GPIO5 在转接板上的丝印顺序不一定和代码里的常量对应,接反了扫描不到任何东西但也不会报错;第二种可能是上拉电阻缺失,直接用裸芯片时总线上没有 4.7k 上拉,开漏输出无法拉高,总线一直是低电平。
解决:用万用表量 SDA、SCL 对 3.3V 的电阻,正常在 4.7k 左右;如果量到无穷大就补两颗 4.7k 上拉。然后再确认代码里Wire.begin(4, 5)的两个参数和实际焊接的引脚一致,改一次编译一次,不要凭记忆。
5.2 现场二:扫到了 0x68,但 WHO_AM_I 读回 0xFF
现象:总线上能扫到设备,说明从机在应答地址,但读寄存器时requestFrom返回 0 或者读出 0xFF。
原因:0xFF 是Wire.read()在无数据时返回 -1 强转成 uint8_t 的结果,本质上还是「没读到」。最常见的原因是endTransmission()里传了true,STOP 条件已经把事务结束了,后面的requestFrom变成一次孤立读,从机不知道你要读哪个寄存器。
解决:确认写寄存器地址那一步用的是endTransmission(false),并且requestFrom之后校验返回值等于期望字节数。如果还是不行,把时钟降到 50kHz 试试,长杜邦线在 100kHz 下就可能因为边沿变缓而出错。
5.3 现场三:WHO_AM_I 正常,但六轴数据全是 0
现象:0x75 返回 0x68,链路确认无误,可是 0x3B 起始的 14 个字节读出来全 0。
原因:芯片还在睡眠状态。MPU6050 上电默认 PWR_MGMT_1 的 SLEEP 位是 1,所有传感器停摆,数据寄存器保持 0。另一种可能是复位之后没有等待足够时间,后续的配置写入被吞掉,芯片实际还停在复位默认值上。
解决:确认初始化序列里先写0x6B = 0x80复位、delay(100)、再写0x6B = 0x01唤醒。唤醒之后回读一次0x6B,看 SLEEP 位是不是真的清了,回读这一步能省掉大量猜测。
5.4 现场四:数据大部分正常,每隔几秒跳一次野值
现象:曲线整体平滑,但每隔一段时间出现一个明显偏离的尖峰,或者干脆读到一堆 0。
原因:电源问题。ESP8266 在 WiFi 发射瞬间的电流冲击会把 3.3V 拉低,MPU6050 的欠压检测触发,内部寄存器短暂复位。这种情况在用小电流 LDO 或者从 USB-TTL 取电时特别常见,而且现象完全随机,非常像「玄学」。
解决:模组 VCC 就近并 100uF 电解加 0.1uF 瓷片,供电换成能持续输出 500mA 的 LDO,杜邦线尽量短。如果条件允许,把 WiFi 关掉(WiFi.mode(WIFI_OFF))跑一遍,数据立刻变干净就基本可以确定是电源。
5.5 现场五:角度静止时缓慢单向漂移
现象:板子放在桌上不动,roll 或 pitch 每分钟漂一两度,时间越长越离谱。
原因:陀螺零偏没有标定,或者标定值没有在运行时真正减掉。还有一种隐蔽情况是量程改过但换算系数没改,比如 GYRO_CONFIG 写了 ±500dps 却还用 131 去除,算出来的角速度直接小一半,滤波器的权重就失衡了。
解决:跑一遍 500 次静止采样标定,把三个轴的偏置打印出来看是否在 ±20dps 以内;数值过大说明标定时板子没放稳。然后在updateAttitude之前确认减的是标定值而不是 0。改过量程的话,回头把换算表和代码里的常量对齐,这两处不一致是最难查的错。
6. 深睡采样进阶:GPIO16 接 RST 与 RTC 内存的持久化技巧
做到这一步,采集、换算、滤波、推流都通了,但如果是要做电池供电的长期记录设备,一直跑着 WiFi 功耗在 70mA 上下,一节 18650 撑不过两天。这时候该上深睡了。
ESP8266 的深睡电流可以降到 20uA 量级,唤醒方式是把 GPIO16(模组上的 D0)用一根线接到 RST 引脚,定时器到期后 GPIO16 拉低复位,芯片重新从头跑 setup。这里有个硬件前提:这根线不接,ESP.deepSleep()调用之后芯片就再也醒不过来了,我第一次调的时候不知道这一点,等了十分钟以为程序卡死,重新刷机才发现问题。
// RTC 内存掉电不丢,用来跨睡眠周期累计数据 RTC_DATA_ATTR uint32_t bootCount = 0; RTC_DATA_ATTR float biasX = 0.0f; RTC_DATA_ATTR float biasY = 0.0f; RTC_DATA_ATTR float biasZ = 0.0f; void setup() { Serial.begin(115200); delay(100); bootCount++; Wire.begin(4, 5); Wire.setClock(100000); mpuInit(); if (bootCount == 1) { // 只在首次上电标定,之后从 RTC 内存里取 calibrateGyro(biasX, biasY, biasZ); } ImuRaw r; float sumAx = 0, sumAy = 0, sumAz = 0; int n = 0; for (int i = 0; i < 32; i++) { // 醒着的时候密集采 32 帧 if (readImu(r)) { sumAx += r.ax; sumAy += r.ay; sumAz += r.az; n++; } delay(5); } if (n > 0) { Serial.printf("boot=%lu ax=%.3f ay=%.3f az=%.3f\n", bootCount, sumAx / n / 16384.0f, sumAy / n / 16384.0f, sumAz / n / 16384.0f); } Serial.flush(); // 睡之前把串口发完,否则最后一行会丢 ESP.deepSleep(60e6, WAKE_RF_DEFAULT); // 睡 60 秒,单位微秒 }这段代码有三个技术点值得单独拎出来。第一,RTC_DATA_ATTR修饰的变量存在 RTC 内存里,深睡期间不掉电,但普通全局变量会归零,所以标定值必须放在这里,否则每次醒来都重标一遍,32 帧的采样窗口根本不够用。第二,ESP.deepSleep的第一个参数单位是微秒,60 秒要写60e6,写成60就是睡 60 微秒,等于没睡,这个错误非常隐蔽,因为程序逻辑上完全跑得通。第三个是Serial.flush(),深睡会立刻断掉串口,不加这一句最后一行日志大概率丢失,调试时会误以为程序没跑到那一步。
唤醒源除了定时,还可以用 MPU6050 的 INT 引脚接到 ESP8266 的 RST 上做运动唤醒:配置0x38打开 motion detection 中断,静止时芯片深睡,一旦有运动 INT 拉低复位。这套组合在物流震动记录、设备倾斜报警这类场景里很实用。
功耗上做个对比,方便估算电池寿命:
| 工作模式 | 平均电流 | 1000mAh 电池续航 |
|---|---|---|
| 常开 WiFi + 200Hz 采样 | 约 75mA | 约 13 小时 |
| 关 WiFi + 200Hz 采样 | 约 22mA | 约 45 小时 |
| 深睡 + 每分钟醒 200ms 采样 | 约 0.3mA | 约 130 天 |
最后一行是按 60 秒周期、每次醒着 200ms 估算的,实际还要加上 LDO 的静态电流,选型时注意挑静态电流小于 50uA 的型号,否则深睡省下来的电全被稳压器吃掉了。
还有一个习惯我一直在用:每次改完接线或者换模块,先跑一遍第 2 章那个扫描程序,确认 WHO_AM_I 和地址没问题再烧完整固件。这三十秒看起来很浪费,但它挡掉的问题比我后来花在排查上的时间多得多。从那以后我每次动硬件都强制走一遍这个流程,哪怕只是换了一根杜邦线。希望帮到你。
本文还有配套的精品资源,点击获取