30浏览
查看: 30|回复: 0

[项目] 智能重力感应交互沙漏

[复制链接]
本帖最后由 云天 于 2026-5-22 15:51 编辑

【项目简介】

传统沙漏靠重力驱动,翻转一次固定时间。本项目用 ESP32-S2 主控 + 16×32 HUB75 LED 矩阵 + LIS2DH 三轴加速度传感器,打造一枚"数字沙漏"。核心特性:
  • 屏幕正放时,沙子静止在上半部
  • 翻转屏幕,沙子开始逐颗下落,约 60 秒 漏完
  • 再次翻转,沙子"倒流"回上半部
  • 每颗沙子从行中间向两侧扩散,模拟真实堆积形态
  • 严格 1 秒 1 颗,时间精准可控
智能重力感应交互沙漏图5

【硬件清单】
智能重力感应交互沙漏图1

I2C LIS2DH 三轴加速度计(https://www.dfrobot.com.cn/goods-1372.html

32x16 RGB LED Matrix - 6mm pitch 点阵屏(https://www.dfrobot.com.cn/goods-1474.html

ESP32-S2-DevKitC-1-N8R2开发板(https://www.dfrobot.com.cn/goods-3075.html

【接线图】
HUB75 LED 矩阵 → ESP32-S2

智能重力感应交互沙漏图2

智能重力感应交互沙漏图4
LIS2DH 传感器 → ESP32-S2
智能重力感应交互沙漏图3

【硬件组装】
智能重力感应交互沙漏图6
接线
智能重力感应交互沙漏图7
两充电宝供电,一个为主控供电,一个为屏幕供电。
智能重力感应交互沙漏图8

智能重力感应交互沙漏图9



【完整代码】
  1. #include <ESP32-HUB75-MatrixPanel-I2S-DMA.h>
  2. #include <Wire.h>
  3. // ==================== 面板配置 ====================
  4. #define R1_PIN 42
  5. #define G1_PIN 41
  6. #define B1_PIN 40
  7. #define R2_PIN 38
  8. #define G2_PIN 39
  9. #define B2_PIN 37
  10. #define A_PIN  45
  11. #define B_PIN  36
  12. #define C_PIN  35
  13. #define D_PIN  -1
  14. #define E_PIN  -1
  15. #define LAT_PIN 34
  16. #define OE_PIN  14
  17. #define CLK_PIN 2
  18. HUB75_I2S_CFG::i2s_pins _pins = {
  19.   R1_PIN, G1_PIN, B1_PIN,
  20.   R2_PIN, G2_PIN, B2_PIN,
  21.   A_PIN, B_PIN, C_PIN, D_PIN, E_PIN,
  22.   LAT_PIN, OE_PIN, CLK_PIN
  23. };
  24. HUB75_I2S_CFG mxconfig(32, 16, 1, _pins);
  25. MatrixPanel_I2S_DMA dma_display(mxconfig);
  26. // ==================== 屏幕参数 ====================
  27. const int SCREEN_W = 16;
  28. const int SCREEN_H = 32;
  29. const int HW = 12;          // 沙漏宽度
  30. const int HH = 26;          // 沙漏高度(24+2,上下各加一行)
  31. const int START_X = (SCREEN_W - HW) / 2;   // = 2
  32. const int START_Y = (SCREEN_H - HH) / 2;   // = 3
  33. const int HALF_H = HH / 2;  // = 13
  34. const uint16_t COLOR_BORDER = 0xFFFF;  // 白
  35. const uint16_t COLOR_SAND   = 0xFFE0;  // 黄
  36. const uint16_t COLOR_BG     = 0x0000;  // 黑
  37. // ==================== LIS2DH I2C ====================
  38. #define LIS2DH_ADDR 0x19
  39. #define SDA_PIN 8
  40. #define SCL_PIN 9
  41. // ==================== 沙漏像素表 ====================
  42. struct Pixel { int8_t x; int8_t y; };
  43. Pixel topPixels[80];
  44. Pixel bottomPixels[80];
  45. int topPixelCount = 0;
  46. int bottomPixelCount = 0;
  47. int maxStep = 0;
  48. // ==================== 状态机 ====================
  49. int currentStep = 0;      // 当前步数(0=上半满, maxStep=漏完)
  50. int targetStep = 0;       // 目标步数
  51. int orientation = 0;      // 0=正放, 1=倒放
  52. // ==================== LIS2DH 底层驱动 ====================
  53. void lis2dhWrite(uint8_t reg, uint8_t val) {
  54.   Wire.beginTransmission(LIS2DH_ADDR);
  55.   Wire.write(reg);
  56.   Wire.write(val);
  57.   Wire.endTransmission();
  58. }
  59. uint8_t lis2dhRead(uint8_t reg) {
  60.   Wire.beginTransmission(LIS2DH_ADDR);
  61.   Wire.write(reg);
  62.   Wire.endTransmission();
  63.   Wire.requestFrom(LIS2DH_ADDR, 1);
  64.   return Wire.read();
  65. }
  66. void initLIS2DH() {
  67.   Wire.begin(SDA_PIN, SCL_PIN);
  68.   lis2dhWrite(0x20, 0x57);  // CTRL_REG1: 100Hz, 正常模式, XYZ使能
  69.   lis2dhWrite(0x23, 0x88);  // CTRL_REG4: BDU=1, HR=1, ±2g
  70. }
  71. void readAccel(int16_t &x, int16_t &y, int16_t &z) {
  72.   Wire.beginTransmission(LIS2DH_ADDR);
  73.   Wire.write(0x28 | 0x80);   // 从 OUT_X_L 自动递增读取
  74.   Wire.endTransmission();
  75.   Wire.requestFrom(LIS2DH_ADDR, 6);
  76.   uint8_t xl = Wire.read(), xh = Wire.read();
  77.   uint8_t yl = Wire.read(), yh = Wire.read();
  78.   uint8_t zl = Wire.read(), zh = Wire.read();
  79.   x = (int16_t)(xl | (xh << 8)) >> 4;
  80.   y = (int16_t)(yl | (yh << 8)) >> 4;
  81.   z = (int16_t)(zl | (zh << 8)) >> 4;
  82. }
  83. // ==================== 沙漏几何计算 ====================
  84. int getOffset(int y) {
  85.   int relY = y - START_Y;
  86.   if (relY < HALF_H) {
  87.     return max(0, (relY - 1) / 2);      // 上半:前2行不缩进
  88.   } else {
  89.     return max(0, (HH - 2 - relY) / 2); // 下半:底部2行不缩进
  90.   }
  91. }
  92. void collectRow(int y, Pixel *buf, int &idx) {
  93.   int off = getOffset(y);
  94.   int left  = START_X + off + 1;
  95.   int right = START_X + HW - 2 - off;
  96.   if (left > right) return;
  97.   int mid = (left + right) / 2;
  98.   for (int d = 0; ; d++) {
  99.     bool added = false;
  100.     int xl = mid - d, xr = mid + d;
  101.     if (d == 0) {
  102.       if (xl >= left && xl <= right) {
  103.         buf[idx++] = {(int8_t)xl, (int8_t)y}; added = true;
  104.       }
  105.     } else {
  106.       if (xl >= left && xl <= right) {
  107.         buf[idx++] = {(int8_t)xl, (int8_t)y}; added = true;
  108.       }
  109.       if (xr >= left && xr <= right && xr != xl) {
  110.         buf[idx++] = {(int8_t)xr, (int8_t)y}; added = true;
  111.       }
  112.     }
  113.     if (!added) break;
  114.   }
  115. }
  116. void initPixels() {
  117.   int idx = 0;
  118.   for (int y = START_Y + 1; y < START_Y + HALF_H; y++)
  119.     collectRow(y, topPixels, idx);
  120.   topPixelCount = idx;
  121.   idx = 0;
  122.   for (int y = START_Y + HH - 2; y >= START_Y + HALF_H; y--)
  123.     collectRow(y, bottomPixels, idx);
  124.   bottomPixelCount = idx;
  125.   maxStep = max(topPixelCount, bottomPixelCount);
  126. }
  127. // ==================== 绘制函数 ====================
  128. void drawFrame() {
  129.   for (int x = START_X; x < START_X + HW; x++) {
  130.     dma_display.drawPixel(x, START_Y, COLOR_BORDER);
  131.     dma_display.drawPixel(x, START_Y + HH - 1, COLOR_BORDER);
  132.   }
  133.   for (int y = START_Y; y < START_Y + HH; y++) {
  134.     int off = getOffset(y);
  135.     dma_display.drawPixel(START_X + off, y, COLOR_BORDER);
  136.     dma_display.drawPixel(START_X + HW - 1 - off, y, COLOR_BORDER);
  137.   }
  138. }
  139. void clearInside() {
  140.   for (int y = START_Y + 1; y < START_Y + HH - 1; y++) {
  141.     int off = getOffset(y);
  142.     int left  = START_X + off + 1;
  143.     int right = START_X + HW - 2 - off;
  144.     if (left <= right)
  145.       for (int x = left; x <= right; x++)
  146.         dma_display.drawPixel(x, y, COLOR_BG);
  147.   }
  148. }
  149. void drawSand(int step, bool reverse) {
  150.   clearInside();
  151.   if (!reverse) {
  152.     int topShow = constrain(topPixelCount - step, 0, topPixelCount);
  153.     for (int i = topPixelCount - topShow; i < topPixelCount; i++)
  154.       dma_display.drawPixel(topPixels[i].x, topPixels[i].y, COLOR_SAND);
  155.     int botShow = constrain(step, 0, bottomPixelCount);
  156.     for (int i = 0; i < botShow; i++)
  157.       dma_display.drawPixel(bottomPixels[i].x, bottomPixels[i].y, COLOR_SAND);
  158.   } else {
  159.     int topShow = constrain(step, 0, topPixelCount);
  160.     for (int i = 0; i < topShow; i++)
  161.       dma_display.drawPixel(topPixels[i].x, topPixels[i].y, COLOR_SAND);
  162.     int botShow = constrain(bottomPixelCount - step, 0, bottomPixelCount);
  163.     for (int i = bottomPixelCount - botShow; i < bottomPixelCount; i++)
  164.       dma_display.drawPixel(bottomPixels[i].x, bottomPixels[i].y, COLOR_SAND);
  165.   }
  166.   drawFrame();
  167. }
  168. // ==================== 主程序 ====================
  169. void setup() {
  170.   Serial.begin(115200);
  171.   initLIS2DH();
  172.   Serial.print("LIS2DH WHO_AM_I: 0x");
  173.   Serial.println(lis2dhRead(0x0F), HEX);
  174.   dma_display.begin();
  175.   dma_display.setBrightness(20);
  176.   dma_display.fillScreen(COLOR_BG);
  177.   dma_display.setRotation(1);
  178.   initPixels();
  179.   Serial.printf("Top=%d Bot=%d Step=%d\n", topPixelCount, bottomPixelCount, maxStep);
  180.   drawSand(0, false);
  181. }
  182. void loop() {
  183.   int16_t ax, ay, az;
  184.   readAccel(ax, ay, az);
  185.   int newOrient = orientation;
  186.   if (az > 700)       newOrient = 0;
  187.   else if (az < -700) newOrient = 1;
  188.   if (newOrient != orientation) {
  189.     orientation = newOrient;
  190.     targetStep = (orientation == 0) ? 0 : maxStep;
  191.   }
  192.   if (currentStep < targetStep) {
  193.     currentStep++;
  194.     drawSand(currentStep, false);
  195.     delay(1000);
  196.   } else if (currentStep > targetStep) {
  197.     currentStep--;
  198.     drawSand(maxStep - currentStep, true);
  199.     delay(1000);
  200.   } else {
  201.     delay(50);
  202.   }
  203. }
复制代码
【功能模块解析】
模块一:LED 矩阵驱动配置
  1. HUB75_I2S_CFG mxconfig(32, 16, 1, _pins);
  2. MatrixPanel_I2S_DMA dma_display(mxconfig);
复制代码
ESP32-HUB75-MatrixPanel-I2S-DMA 库利用 ESP32 的 I2S 外设 + DMA 驱动 HUB75 面板,CPU 只需填充显存,刷新由硬件自动完成,无闪烁。
参数 (32, 16, 1, _pins) 表示:
  • 32 列 × 16 行物理分辨率
  • 1 个面板级联
  • 自定义引脚映射 _pins
竖屏模式:dma_display.setRotation(1) 将坐标系旋转 90°,使 16×32 变为 32×16 的竖屏使用体验。

模块二:沙漏几何引擎
  1. int getOffset(int y) {
  2.   int relY = y - START_Y;
  3.   if (relY < HALF_H) {
  4.     return max(0, (relY - 1) / 2);      // 上半
  5.   } else {
  6.     return max(0, (HH - 2 - relY) / 2); // 下半
  7.   }
  8. }
复制代码
这是沙漏形状的核心算法。offset 表示某一行相对于边框的缩进量。
关键设计:max(0, ...) 确保顶部和底部各有 2 行保持满宽(10 点),之后每 2 行缩进 1 点。单边像素分布:

  1. 行号:  1  2  3  4  5  6  7  8  9  10 11 12 13
  2. 宽度: 10 10  8  8  6  6  4  4  2  2  0  0  0(腰部)
复制代码
总计 60 点,翻一次 60 秒漏完。
模块三:像素排序与逐点动画
  1. void collectRow(int y, Pixel *buf, int &idx) {
  2.   // ... 每行从中间向两边收集像素
  3. }
复制代码
排序规则
  • 上半:y 从 顶部往下,每行内 中间 → 左 → 右
  • 下半:y 从 底部往上,每行内 中间 → 左 → 右
这样 topPixels[0] 是最顶部行的中心点,topPixels[59] 是腰部边缘点。正向动画时显示数组尾部(保留腰部),隐藏头部(顶部先空),实现最顶行中心先漏、向两侧扩散的真实效果。

模块四:正向/逆向镜像渲染
  1. void drawSand(int step, bool reverse) {
  2.   if (!reverse) {
  3.     // 正向:上半显示尾部(腰部保留),下半显示头部(底部堆积)
  4.   } else {
  5.     // 逆向:上半显示头部(顶部填满),下半显示尾部(腰部保留)
  6.   }
  7. }
复制代码
时间反演对称性:逆向不是简单倒放 percent,而是严格镜像——上半从顶部往下填充,下半从腰部往下消失。这样沙子"流回去"和"流下来"的视觉逻辑完全一致。
模块五:LIS2DH 裸机驱动
  1. void initLIS2DH() {
  2.   Wire.begin(SDA_PIN, SCL_PIN);
  3.   lis2dhWrite(0x20, 0x57);  // 100Hz ODR
  4.   lis2dhWrite(0x23, 0x88);  // BDU + 高分辨率
  5. }
复制代码
不依赖外部库,直接用 Wire.h 操作寄存器:
  • 0x20 (CTRL_REG1):100Hz 数据率,XYZ 三轴使能
  • 0x23 (CTRL_REG4):BDU=1 防止高低字节撕裂,HR=1 开启 12bit 高分辨率
读取时 0x28 | 0x80 启用地址自动递增,一次读取 6 字节 XYZ 数据。

模块六:方向检测与状态机
  1. int newOrient = orientation;
  2. if (az > 700)       newOrient = 0;   // 正面朝上
  3. else if (az < -700) newOrient = 1;   // 倒过来
复制代码
迟滞设计:700(约 0.7g)作为阈值,中间区间保持原状态,防止抖动。
状态机
  • orientation 记录当前朝向
  • targetStep 根据朝向设为 0 或 maxStep
  • currentStep 逐秒向目标逼近
翻转后沙子不会瞬间跳变,而是从当前状态平滑继续流动,体验自然。

【效果演示】

1.正放静止
60 颗沙子在上半部,底部空
2.翻转开始漏
最顶行中心第 1 颗消失,底部中心第 1 颗出现
3.漏到一半
上半顶部已空出约 5 行,下半底部堆起约 5 行
4.漏完
上半全空,下半满 60 颗
5.再翻转
沙子从下半"倒流"回上半,严格镜像














您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4 备案 沪公网安备31011502402448

© 2013-2026 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail