方盒水动力灯——用ESP32-C3+LIS2DW12做的"会晃动的液体"
一、项目灵感
一直想做个"看得见液体"的小玩具——不是真的装水,而是用LED点阵模拟一个密封盒子里装着半盒蓝色液体,无论你怎么翻动盒子,液体永远流向物理上的最低处;摇晃它,还能看到水花溅起。
选材的难点在于:真实感。市面上大多数LED水动效果都是"单向倾斜",盒子里是水,但盒子不能翻。而我要的是——
- 盒子竖着放,水在底下 ✓
- 向左倒90°,水完全倒向左侧 ✓
- 倒过来,水"跑"到顶部 ✓
- 用力晃一下,水花散开又消散 ✓
这就需要一个能实时判断重力方向的传感器,和一块高密度、单点可控的LED点阵。


二、硬件选型
| 模块 | 型号 | 作用 |
|---|---|---|
| 主控 | DFRobot Beetle ESP32-C3 (DFR0868) | 硬币大小、带锂电池管理、支持Arduino IDE |
| 传感器 | LIS2DW12 三轴加速度计 (SEN0405) | 超低功耗、±2g量程、16位精度 |
| 显示 | 8×8 RGB LED Matrix (DFR0459) | 64颗WS2812、单点可控 |
| 电源 | 改造充电宝(2×18650) | 5V输出给LED,VIN给C3 |
为什么选这三个模块?
- ESP32-C3:RISC-V单核,自带WiFi/BLE,方便后续扩展;Beetle板载充电管理省掉一颗充电芯片;板载LED和BOOT键方便调试。
- LIS2DW12:相比MPU6050,它超低功耗(50nA)、抗冲击10000g,倾斜检测的灵敏度够用(1.3mg RMS)。
- 8×8点阵:64个像素正好能表达"半盒水的形状",尺寸小到能塞进方盒。

三、电源改造:把充电宝"瘦身"
3.1 为什么改造?
原充电宝6节18650,体积太大,没法塞进与LED灯板同尺寸的外壳。改成2节18650并联(3.7V,约5000mAh),体积缩小到原来的1/3。
3.2 改造步骤
- 拆壳:保留充电宝的升压板(5V输出)和充电口,去掉多余的电池支架。
- 重新焊接:2节18650并联合焊到升压板BAT+/-端。
- 引出5V:从USB-A口的VCC/GND焊盘各引出一根线,焊接在对外开关上。
- 两条供电支路:
- LED支路:开关 → LED灯板5V/GND
- C3支路:另一条USB-A口(或从同口分流) → Beetle VIN引脚
- 共地:LED的GND、C3的GND、充电宝的GND全部相连。
3.3 接线图
【电源正极路径】
电池+ ──> 升压板 ──┬──> 开关 ──> 8×8 LED 点阵 VCC
│
└──> Beetle ESP32-C3 VIN
【公共负极路径(必须共地)】
电池- ──> 升压板 ──┬──> 8×8 LED 点阵 GND
│
└──> Beetle ESP32-C3 GND
【信号路径】
LIS2DW12 ──I2C──> Beetle ESP32-C3
Beetle ESP32-C3 GPIO 8 ──> 8×8 LED 点阵 DIN
3.4 完整系统接线表
| 从 | 到 | 说明 |
|---|---|---|
| 电池+ | 升压板 BAT+ | 3.7V 输入 |
| 电池- | 升压板 BAT- | 共地参考 |
| 升压板 5V | 开关一端 | 输出 5V |
| 开关另一端 | LED点阵 VCC | 控制LED通断 |
| 升压板 5V | Beetle VIN | 5V 给C3 |
| 升压板 GND | LED点阵 GND | 共地 |
| 升压板 GND | Beetle GND | 共地(关键) |
| LIS2DW12 VCC | Beetle 3V3 | 传感器3.3V |
| LIS2DW12 GND | Beetle GND | 传感器地 |
| LIS2DW12 SDA | Beetle GPIO 4 | I2C数据 |
| LIS2DW12 SCL | Beetle GPIO 1 | I2C时钟 |
| LED点阵 DIN | Beetle GPIO 8 | 数据信号 |
3.5 三个要点提醒
-
一定要共地:升压板GND、LED点阵GND、Beetle GND、传感器GND 必须全部连在一起,否则数据信号无法正确识别。
-
共地方案二选一:
- 方案A(推荐):升压板5V分别给LED和C3,GND在升压板输出端汇合
- 方案B:从C3的GND再分出线给传感器——只要最终所有GND在同一节点即可
-
电容:在升压板5V输出端并一个 1000µF电解电容,正极接5V,负极接GND。能吸收LED点阵瞬态电流(64颗灯全亮可达1.15A),防止C3复位或LED闪烁。

四、外壳设计
外壳用3mm亚克力激光切割,外形尺寸和LED灯板一致(约60×60mm),分三层:
| 层 | 厚度 | 功能 |
|---|---|---|
| 前层 | 3mm 透明亚克力 | 透光窗口,把LED像素柔和化 |
| 中层 | 3mm 亚克力 | 容纳LED+传感器+电池,侧边开USB口 |
| 后层 | 3mm 黑色亚克力 | 底板,开开关小孔 |
USB开口对准充电宝的充电口。

五、程序解析(重点)
代码整体分四大模块。我逐一拆解核心逻辑。
5.1 模块一:传感器初始化
acce.softReset();
acce.setRange(DFRobot_LIS2DW12::e2_g); // ±2g
acce.setDataRate(DFRobot_LIS2DW12::eRate_50hz); // 50Hz
acce.setPowerMode(DFRobot_LIS2DW12::eContLowPwrLowNoise2_14bit);
acce.continRefresh(true); // 连续采集
acce.setFilterPath(DFRobot_LIS2DW12::eLPF); // 低通滤波
为什么这样配?
- ±2g:水杯晃动属于低加速度场景,小量程=高分辨率。
- 50Hz:平衡功耗和响应,太快没必要,太慢水花会掉帧。
- 低功耗低噪声模式:噪声小,倾斜判断更稳定。
5.2 模块二:重力方向计算(核心)
这是整个项目的灵魂。普通做法是"倾斜角度 → 水位高低",但这样盒子倒立时水会一直留在底部(错的)。正确做法是用重力在点阵平面上的投影方向来决定"水堆积中心"。
// 归一化加速度
float A = sqrt(ax*ax + ay*ay + az*az);
float gy = -ay / A; // 重力在Y轴(点阵右)的分量
float gz = -az / A; // 重力在Z轴(点阵上)的分量
// 取Y-Z平面上的投影
float projSize = sqrt(gy*gy + gz*gz);
targetDirY = gy / projSize;
targetDirZ = gz / projSize;
关键点:
- 静止时加速度读数 = 比力方向(世界上方),重力方向 = 取反。
- 点阵是平面的,重力垂直于平面时(点阵朝上/朝下)投影为0,此时默认水在下方。
然后做方向平滑,避免抖动导致水乱跳:
gDirY += (targetDirY - gDirY) * DIR_SMOOTH; // DIR_SMOOTH = 0.25
gDirZ += (targetDirZ - gDirZ) * DIR_SMOOTH;
5.3 模块三:每个像素的"投影"判定
这是渲染的关键。水应该出现在哪?—— 重力方向那一侧。
for (int row = 0; row < MATRIX_H; row++) {
for (int col = 0; col < MATRIX_W; col++) {
// 像素在点阵平面上的归一化坐标
float px = (col - 3.5) / 3.5; // -1 左 ~ +1 右
float py = (row - 3.5) / 3.5; // -1 上 ~ +1 下
// 该像素在重力方向上的投影
float proj = px * gDirY + py * (-gDirZ);
proj > 0→ 像素在重力"下游" → 水体proj ≈ 0→ 像素在水面线上proj < 0→ 像素在重力"上游" → 空气
数学直觉:proj 就是该像素到"垂直于重力方向的平面"的带符号距离。你不需要去算"水流到哪了",只需要算"哪些像素在重力方向上"。
5.4 模块四:水花与蛇形布局修正
水花:当加速度跳变超过阈值时,splashIntensity 置为1,然后每帧×0.88 自然衰减。
if (dX > 350 || dY > 350 || dZ > 350) {
splashIntensity = 1.0;
}
splashIntensity *= 0.88;
水花只在水面之上(proj < 0)出现,离水面越远概率越低:
float dist = -proj - SURFACE_WIDTH;
float splashProb = splashIntensity * (1.0 - dist * 2.0);
颜色:用 r/b 比例调出淡蓝色(30%红 + 70%绿 + 100%蓝)。
蛇形布局修正:这是踩过的坑!大多数WS2812矩阵是蛇形接线(奇数行反向)。如果不修正,就会看到"水一半在左、一半在右"的错乱。
physicalCol = (row % 2 == 0) ? col : (MATRIX_W - 1 - col);
六、调试踩坑记录
| 坑 | 现象 | 解决 |
|---|---|---|
| I2C引脚冲突 | GPIO 9是BOOT键,用它会无法烧录 | 改用GPIO 1/4 |
| 方向反了 | 向左倒,水往右流 | 翻转 SENSOR_Y_SIGN |
| 水位不满 | 倾斜90°水只到一侧一半 | 删除 TILT_MAX 限幅 |
| 水位"锯齿" | 一行在左一行在右 | 加蛇形布局修正 |
| 编译报错 | DFRobot_LIS2DW12.h找不到 |
安装库时选对作者(DFRobot) |
| USB不识别 | 无法烧录 | 按住BOOT,点RST,松开BOOT |
七、效果演示
- 竖直放:蓝色水在下方,静静一片
- 向左倒:水"哗"地流到左边,屏幕左半边全亮
- 倒过来:水"跑"到了顶部,像真正密封的液体
- 用力晃:淡蓝色的水花在水面上方散开又消失
- 缓缓回正:水慢慢流回,水面线波动两下才安静
八、总结
这个项目的技术亮点不在硬件,而在用一个小小的向量投影公式,把"重力"和"像素颜色"直接挂钩——不需要模拟流体、不需要状态机,只需要把每个像素和重力方向做个点积,就得到了液体在盒子里的样子。
这就是创客的乐趣:用最少的代码,讲一个最直观的物理故事。



