✔零知开源(零知IDE)是一个专为电子初学者/电子兴趣爱好者设计的开源软硬件平台,在硬件上提供超高性价比STM32系列开发板、物联网控制板。取消了Bootloader程序烧录,让开发重心从 "配置环境" 转移到 "创意实现",极大降低了技术门槛。零知IDE编程软件,内置上千个覆盖多场景的示例代码,支持项目源码一键下载,项目文章在线浏览。零知开源(零知IDE)平台通过软硬件协同创新,让你的创意快速转化为实物,来动手试试吧!
✔访问零知实验室,获取更多实战项目和教程资源吧!
目录
[1.1 硬件清单](#1.1 硬件清单)
[1.2 接线方案表](#1.2 接线方案表)
[1.3 具体接线图](#1.3 具体接线图)
[1.4 接线实物图](#1.4 接线实物图)
[3.1 核心映射函数](#3.1 核心映射函数)
[3.2 手势处理机制](#3.2 手势处理机制)
[3.3 动画效果实现](#3.3 动画效果实现)
[3.4 软件 I2C 通信配置](#3.4 软件 I2C 通信配置)
[3.5 完整代码](#3.5 完整代码)
[4.1 系统操作](#4.1 系统操作)
[4.2 手势操作演示](#4.2 手势操作演示)
[4.3 视频演示](#4.3 视频演示)
[五、PAJ7620U2 手势传感器知识点讲解](#五、PAJ7620U2 手势传感器知识点讲解)
[5.1 I2C通信原理](#5.1 I2C通信原理)
[5.2 寄存器系统架构](#5.2 寄存器系统架构)
[Q1:WS2812 矩阵灯光乱闪,动画显示错位](#Q1:WS2812 矩阵灯光乱闪,动画显示错位)
想象一下,只需在空中轻轻一挥,就能控制一个绚丽的RGB立方体变换出不同图案------本文将手把手教你如何使用零知标准板,结合PAJ7620U2手势传感器和192个WS2812 LED,实现这个酷炫的交互系统。
项目概述
本项目基于零知标准板(主控STM32F103RBT6) 、PAJ7620U2手势识别传感器 以及三块8x8 WS2812 RGB矩阵(共192个LED),打造了一个高度智能化的手势交互立方体系统。通过精确的手势识别技术,用户可以在空中做出9种不同手势(向左、向右、向上、向下、向前、向后、顺时针旋转、逆时针旋转、挥手),系统实时响应
项目难点及解决方案
问题描述:物理 LED 矩阵的灯珠索引值(范围 0-191),与各 RGB 矩阵模块对应的逻辑坐标(face, x, y)存在映射不匹配
**解决方案:**设计getIndex()核心映射函数,统一逻辑坐标到物理 LED 索引的映射
一、系统接线部分
1.1 硬件清单
| 组件名称 | 规格参数 | 数量 | 备注 |
|---|---|---|---|
| 零知标准板 | STM32F103RBT6主控 | 1 | 核心控制器 |
| 扩展板 | 零知标准板-扩展板 | 1 | 传感器扩展板 |
| PAJ7620U2手势传感器 | I²C接口,支持9种手势 | 1 | 手势输入设备 |
| WS2812 RGB矩阵 | 8x8,共64个LED/块 | 3 | 显示设备,共192个LED |
| 电源模块 | 5V/3A输出 | 1 | 为LED矩阵提供充足电流 |
| 连接线 | 杜邦线 | 若干 | 信号与电源连接 |
1.2 接线方案表
本项目使用软件I²C与PAJ7620U2通信,并使用单个GPIO引脚控制WS2812灯带。具体接线如下
| 零知标准板引脚 | 目标设备 | 连接说明 | 代码对应 |
|---|---|---|---|
| A5 | PAJ7620U2 SCL | I²C时钟线 | SoftWire库配置 |
| A4 | PAJ7620U2 SDA | I²C数据线 | SoftWire库配置 |
| 11 | WS2812矩阵 IN | 数据输入线 | WS2812B库配置 |
| 3.3V | PAJ7620U2 VIN | 电源正极 | 模块工作电压 |
| 5V | WS2812矩阵 V+ | 电源正极 | LED工作电压 |
| GND | 所有设备GND | 电源地线 | 共地连接 |
PAJ7620U2手势模块采用扩展板直插,主控、传感器必须共地,WS2812矩阵输入信号为SPI通信的MOSI引脚
1.3 具体接线图
注意:级联 WS2812 矩阵时,按照 "DIN→DOUT" 顺序,最后一块矩阵的DOUT悬空;将RGB矩阵的V+和V-连接,单独使用外部电源+5V 供电
1.4 接线实物图

手势传感器距离手掌建议保持5-15cm,角度垂直;单独测试一块LED矩阵,确认数据引脚和亮度正常
二、安装与使用部分
2.1 开源平台-输入"WS2812 RGB立方体矩阵"并搜索-代码下载自动打开

2.2 连接-验证-上传

2.3 调试-串口监视器

三、代码讲解部分
代码整体结构
3.1 核心映射函数
整个项目的几何核心,负责将三维逻辑坐标转换为物理LED索引
cpp
int getIndex(int face, int x, int y) {
if (x < 0 || x > 7 || y < 0 || y > 7) return -1;
int pixelIndex = 0;
// 面1 (LED 0-63):从左上角开始,蛇形排列
if (face == 1) {
if (y % 2 == 0) { // 偶数行:从右向左
pixelIndex = y * 8 + (7 - x);
} else { // 奇数行:从左向右
pixelIndex = y * 8 + x;
}
return pixelIndex;
}
// 面2 (LED 64-127):旋转180度后的蛇形排列
else if (face == 2) {
int base = 64;
int physRow = 7 - x; // X轴翻转
int physCol = 7 - y; // Y轴翻转
if (physRow % 2 == 0) {
pixelIndex = base + (physRow * 8) + physCol;
} else {
pixelIndex = base + (physRow * 8) + (7 - physCol);
}
return pixelIndex;
}
// 面3 (LED 128-191):标准蛇形排列
else if (face == 3) {
int base = 128;
int physRow = x;
int physCol = y;
if (physRow % 2 == 0) {
pixelIndex = base + (physRow * 8) + physCol;
} else {
pixelIndex = base + (physRow * 8) + (7 - physCol);
}
return pixelIndex;
}
return -1; // 无效坐标
}
3.2 手势处理机制
系统通过状态机模式GestureState管理手势识别与动画更新:
cpp
void loop() {
// 1. 读取手势传感器
Gesture sensorGesture = gestureSensor.readGesture();
GestureState newGestureState = sensorGestureToState(sensorGesture);
// 2. 手势防抖处理
unsigned long currentTime = millis();
bool canChangeGesture = (currentTime - lastGestureChangeTime) >= GESTURE_DEBOUNCE_TIME;
// 3. 手势状态切换
if (newGestureState != GESTURE_NONE &&
newGestureState != currentGesture &&
canChangeGesture) {
handleGestureChange(newGestureState);
}
// 4. 更新当前手势的动画效果
switch (currentGesture) {
case GESTURE_LEFT:
updateLeftEffect();
break;
// ... 其他手势处理
}
delay(10); // 主循环延迟,防止CPU过载
}
调用handleGestureChange函数初始化新动画,根据当前手势调用对应的动画更新函数
3.3 动画效果实现
cpp
// 初始化向上手势状态
void initUpGesture() {
if (arrowMode) {
upPos = -4; // 箭头模式:箭头从底部开始
} else {
upPhase = 0; // 原始模式:火焰效果初始相位
upFrame = 0;
}
upLastUpdate = millis(); // 记录最后更新时间
}
// 更新向上手势动画
void updateUpEffect() {
unsigned long currentTime = millis();
if (currentTime - upLastUpdate < 40) return; // 控制帧率(每40ms更新一帧)
upLastUpdate = currentTime;
strip.clear(); // 清空上一帧画面
if (arrowMode) {
// 箭头模式:红色箭头从面2底部流向面1顶部
// 面2部分
for(int x=0; x<8; x++) {
for(int y=0; y<8; y++) {
int globalY = y;
if (shouldLightVerticalArrowPixel(x, globalY, upPos, true)) {
int distY = upPos - globalY;
int intensity = 255 - distY * 50; // 箭头渐变亮度
if (intensity < 100) intensity = 100;
setPixel(2, x, y, intensity, 0, 0); // 红色箭头
}
}
}
// 面1部分
for(int x=0; x<8; x++) {
for(int y=0; y<8; y++) {
int globalY = (7 - y) + 8; // 面1y坐标反转,实现从下往上流动
if (shouldLightVerticalArrowPixel(x, globalY, upPos, true)) {
int distY = upPos - globalY;
int intensity = 255 - distY * 50;
if (intensity < 100) intensity = 100;
setPixel(1, x, y, intensity, 0, 0);
}
}
}
upPos++; // 箭头位置前进
if (upPos >= 20) upPos = -4; // 循环流动
} else {
// 原始模式:火焰升腾效果
for(int f_idx=0; f_idx<3; f_idx++) {
int actualFace = f_idx + 1;
for(int x=0; x<8; x++) {
drops[f_idx][x]++; // 火焰位置上移
int head = drops[f_idx][x];
// 绘制火焰的渐变效果(头部亮,尾部暗)
for(int k=0; k<3; k++) {
int drawY = head - k;
if(drawY >= 0 && drawY < 8) {
uint8_t bri = (k==0) ? 200 : ((k==1)?100:40);
setPixel(actualFace, x, drawY, bri, bri/4, 0); // 火焰色(红橙)
}
}
// 火焰到顶后重置
if(head > 12) {
drops[f_idx][x] = random(-6, 0);
dropSpeeds[f_idx][x] = random(1, 3);
}
}
}
}
strip.show(); // 刷新显示
}
箭头模式 / 原始模式分别处理,保证同一手势在不同模式下的差异化效果
3.4 软件 I2C 通信配置
SoftWire库可自定义 SDA/SCL 引脚,适配零知标准板的引脚布局
cpp
void SoftWire::i2c_start() {
set_sda(LOW);
set_scl(LOW);
}
void SoftWire::i2c_stop() {
set_sda(LOW);
set_scl(HIGH);
set_sda(HIGH);
}
void SoftWire::i2c_repeated_start() {
set_sda(HIGH);
set_scl(HIGH);
set_sda(LOW);
}
bool SoftWire::i2c_get_ack() {
set_scl(LOW);
set_sda(HIGH);
set_scl(HIGH);
bool ret = !digitalRead(this->sda_pin);
set_scl(LOW);
return ret;
}
void SoftWire::i2c_send_ack() {
set_sda(LOW);
set_scl(HIGH);
set_scl(LOW);
}
void SoftWire::i2c_send_nack() {
set_sda(HIGH);
set_scl(HIGH);
set_scl(LOW);
}
3.5 完整代码
cpp
/**************************************************************************************
* 文件: /GestureRGB_Cube_Control/GestureRGB_Cube_Control.ino
* 作者:零知实验室(深圳市在芯间科技有限公司)
* -^^- 零知实验室,让电子制作变得更简单! -^^-
* 时间: 2026-1-6
* 功能:基于PAJ7620手势识别传感器控制三面8x8 WS2812 RGB灯阵列(共192个LED),支持9种手势识别,每种手势对应一种灯光效果。
* 手势效果:
* - 向左: 青色箭头从右向左 / 向右: 橙色箭头从左向右 / 向上: 红色火焰向上升腾 / 向下: 蓝色雨滴向下落下 / 向前: 切换到箭头模式(上下左右都是箭头指示)
* - 向后: 切换回原始模式(火焰和雨滴) / 顺时针: 彩虹旋转 / 逆时针: 雷达扫描 / 挥手: 关闭所有灯光
***************************************************************************************/
#include <SoftWire.h>
#include "RevEng_PAJ7620.h"
#include "WS2812B.h"
// --- 全局配置 ---
#define NUM_LEDS 192
#define DEFAULT_BRIGHTNESS 60
#define MAX_BRIGHTNESS 200
#define GESTURE_DEBOUNCE_TIME 800 // 手势识别防抖时间(毫秒)
WS2812B strip(NUM_LEDS);
RevEng_PAJ7620 gestureSensor;
uint8_t currentBrightness = DEFAULT_BRIGHTNESS;
// 手势状态枚举
enum GestureState {
GESTURE_NONE,
GESTURE_LEFT,
GESTURE_RIGHT,
GESTURE_UP,
GESTURE_DOWN,
GESTURE_FORWARD,
GESTURE_BACKWARD,
GESTURE_CLOCKWISE,
GESTURE_ANTICLOCKWISE,
GESTURE_WAVE
};
GestureState currentGesture = GESTURE_NONE;
GestureState previousGesture = GESTURE_NONE;
unsigned long gestureStartTime = 0;
unsigned long lastGestureChangeTime = 0; // 上次手势切换时间,用于防抖
// 模式切换:false=原始模式(火焰/雨滴), true=箭头模式
bool arrowMode = false;
// 各手势的动画状态变量
int leftPos = 0;
unsigned long leftLastUpdate = 0;
int rightPos = 0;
unsigned long rightLastUpdate = 0;
int upPos = 0;
unsigned long upLastUpdate = 0;
int downPos = 0;
unsigned long downLastUpdate = 0;
// 原始向上手势状态(火焰升腾)
int upPhase = 0;
int upFrame = 0;
// 原始向下手势状态(雨滴)
int drops[3][8];
int dropSpeeds[3][8];
int clockwiseRotation = 0;
unsigned long clockwiseLastUpdate = 0;
int anticlockwiseScan = 0;
unsigned long anticlockwiseLastUpdate = 0;
unsigned long waveStartTime = 0;
/**
* 核心映射函数:将逻辑坐标 (face, x, y) 映射到物理LED索引
* @param face 面编号 (1, 2, 3)
* @param x X坐标 (0-7)
* @param y Y坐标 (0-7)
* @return 物理LED索引 (0-191),无效坐标返回-1
*/
int getIndex(int face, int x, int y) {
if (x < 0 || x > 7 || y < 0 || y > 7) return -1;
int pixelIndex = 0;
// 面1 (LED 0-63)
if (face == 1) {
if (y % 2 == 0) {
pixelIndex = y * 8 + (7 - x);
} else {
pixelIndex = y * 8 + x;
}
return pixelIndex;
}
// 面2 (LED 64-127)
else if (face == 2) {
int base = 64;
int physRow = 7 - x;
int physCol = 7 - y;
if (physRow % 2 == 0) {
pixelIndex = base + (physRow * 8) + physCol;
} else {
pixelIndex = base + (physRow * 8) + (7 - physCol);
}
return pixelIndex;
}
// 面3 (LED 128-191)
else if (face == 3) {
int base = 128;
int physRow = x;
int physCol = y;
if (physRow % 2 == 0) {
pixelIndex = base + (physRow * 8) + physCol;
} else {
pixelIndex = base + (physRow * 8) + (7 - physCol);
}
return pixelIndex;
}
return -1;
}
/**
* 设置单个像素颜色
* @param f 面编号
* @param x X坐标
* @param y Y坐标
* @param r 红色值 (0-255)
* @param g 绿色值 (0-255)
* @param b 蓝色值 (0-255)
*/
void setPixel(int f, int x, int y, uint8_t r, uint8_t g, uint8_t b) {
int idx = getIndex(f, x, y);
if (idx >= 0 && idx < NUM_LEDS) {
strip.setPixelColor(idx, r, g, b);
}
}
/**
* HSV转RGB颜色
* @param h 色相 (0-255)
* @param s 饱和度 (0-255)
* @param v 亮度 (0-255)
* @return RGB颜色值
*/
uint32_t HSVtoRGB(uint8_t h, uint8_t s, uint8_t v) {
uint8_t r, g, b;
uint8_t region, remainder, p, q, t;
if (s == 0) {
return strip.Color(v, v, v);
}
region = h / 43;
remainder = (h - (region * 43)) * 6;
p = (v * (255 - s)) >> 8;
q = (v * (255 - ((s * remainder) >> 8))) >> 8;
t = (v * (255 - ((s * (255 - remainder)) >> 8))) >> 8;
switch (region) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
default: r = v; g = p; b = q; break;
}
return strip.Color(r, g, b);
}
// 清除所有LED
void clearAll() {
strip.clear();
}
// 设置全局亮度
void setGlobalBrightness(uint8_t b) {
currentBrightness = b;
strip.setBrightness(b);
}
/**
* 水平箭头形状判断(从左向右)
* @param x 当前X坐标
* @param y 当前Y坐标
* @param arrowTipX 箭头尖端X坐标
* @return 是否应该点亮该像素
*/
bool shouldLightArrowPixel(int x, int y, int arrowTipX) {
int distX = arrowTipX - x;
if (distX >= 0 && distX < 7) {
// 箭头主干(中间两行)
if (y == 3 || y == 4) {
return true;
}
// 箭头尖端
if (distX == 0) {
return false;
} else if (distX == 1) {
return (y == 2 || y == 5);
} else if (distX == 2) {
return (y == 1 || y == 6);
} else if (distX == 4) {
return false;
}
}
return false;
}
/**
* 水平箭头形状判断(从右向左,镜像版本)
* @param x 当前X坐标
* @param y 当前Y坐标
* @param arrowTipX 箭头尖端X坐标
* @return 是否应该点亮该像素
*/
bool shouldLightArrowPixelMirror(int x, int y, int arrowTipX) {
int distX = x - arrowTipX;
if (distX >= 0 && distX < 7) {
// 箭头主干(中间两行)
if (y == 3 || y == 4) {
return true;
}
// 箭头尖端
if (distX == 0) {
return false;
} else if (distX == 1) {
return (y == 2 || y == 5);
} else if (distX == 2) {
return (y == 1 || y == 6);
} else if (distX == 4) {
return false;
}
}
return false;
}
/**
* 垂直箭头形状判断
* @param x 当前X坐标
* @param y 当前Y坐标
* @param arrowTipY 箭头尖端Y坐标
* @param pointingUp true表示向上箭头,false表示向下箭头
* @return 是否应该点亮该像素
*/
bool shouldLightVerticalArrowPixel(int x, int y, int arrowTipY, bool pointingUp) {
int distY = pointingUp ? (arrowTipY - y) : (y - arrowTipY);
if (distY >= 0 && distY < 7) {
// 箭头主干(中间两列)
if (x == 3 || x == 4) {
return true;
}
// 箭头尖端
if (distY == 0) {
return false;
} else if (distY == 1) {
return (x == 2 || x == 5);
} else if (distY == 2) {
return (x == 1 || x == 6);
} else if (distY == 4) {
return false;
}
}
return false;
}
// --- 手势初始化函数 ---
// 初始化向左手势
void initLeftGesture() {
leftPos = arrowMode ? 18 : 18;
leftLastUpdate = millis();
}
// 初始化向右手势
void initRightGesture() {
rightPos = -4;
rightLastUpdate = millis();
}
// 初始化向上手势
void initUpGesture() {
if (arrowMode) {
upPos = -4; // 箭头模式从底部开始
} else {
upPhase = 0;
upFrame = 0;
}
upLastUpdate = millis();
}
// 初始化向下手势
void initDownGesture() {
if (arrowMode) {
downPos = 24; // 箭头模式从顶部开始
} else {
// 原始模式:初始化雨滴位置和速度
for(int f=0; f<3; f++) {
for(int x=0; x<8; x++) {
drops[f][x] = random(0, 16);
dropSpeeds[f][x] = random(1, 3);
}
}
}
downLastUpdate = millis();
}
// 初始化向前手势(切换到箭头模式)
void initForwardGesture() {
arrowMode = true;
Serial.println("[模式切换] 已切换到箭头模式(上下左右为箭头指示)");
// 重新初始化当前手势效果
switch(currentGesture) {
case GESTURE_LEFT: initLeftGesture(); break;
case GESTURE_RIGHT: initRightGesture(); break;
case GESTURE_UP: initUpGesture(); break;
case GESTURE_DOWN: initDownGesture(); break;
}
}
// 初始化向后手势(切换回原始模式)
void initBackwardGesture() {
arrowMode = false;
Serial.println("[模式切换] 已切换回原始模式(上下为火焰/雨滴效果)");
// 重新初始化当前手势效果
switch(currentGesture) {
case GESTURE_LEFT: initLeftGesture(); break;
case GESTURE_RIGHT: initRightGesture(); break;
case GESTURE_UP: initUpGesture(); break;
case GESTURE_DOWN: initDownGesture(); break;
}
}
// 初始化顺时针手势
void initClockwiseGesture() {
clockwiseRotation = 0;
clockwiseLastUpdate = millis();
}
// 初始化逆时针手势
void initAnticlockwiseGesture() {
anticlockwiseScan = 0;
anticlockwiseLastUpdate = millis();
}
// 初始化挥手手势(关闭所有灯光)
void initWaveGesture() {
waveStartTime = millis();
clearAll();
strip.show();
}
// --- 向左手势效果 ---
void updateLeftEffect() {
unsigned long currentTime = millis();
if (currentTime - leftLastUpdate < 30) return;
leftLastUpdate = currentTime;
strip.clear();
if (arrowMode) {
// 箭头模式:青色箭头从右向左
for(int f=2; f<=3; f++) {
for(int x=0; x<8; x++) {
int globalX = (f==3) ? x : (x+8); // 面3: 0-7, 面2: 8-15
int arrowTipX = leftPos;
for(int y=0; y<8; y++) {
if (shouldLightArrowPixelMirror(globalX, y, arrowTipX)) {
int distX = globalX - arrowTipX;
int intensity = 255 - distX * 50;
if (intensity < 100) intensity = 100;
setPixel(f, x, y, 0, intensity, intensity); // 青色
}
}
}
}
leftPos--;
if (leftPos < -4) {
leftPos = 18;
}
} else {
// 原始模式:能量波从右向左
for(int w = 0; w < 3; w++) {
int currentX = leftPos + w;
int f = 0, realX = 0;
if(currentX >= 8 && currentX < 16) {
f = 2; realX = currentX - 8;
} else if (currentX >= 0 && currentX < 8) {
f = 3; realX = currentX;
}
if (f != 0) {
for(int y = 0; y < 8; y++) {
uint8_t bri = (w==1) ? 255 : 100;
setPixel(f, realX, y, 0, bri, bri);
}
}
}
leftPos--;
if (leftPos < -2) {
leftPos = 18;
}
}
strip.show();
}
// --- 向右手势效果 ---
void updateRightEffect() {
unsigned long currentTime = millis();
if (currentTime - rightLastUpdate < 30) return;
rightLastUpdate = currentTime;
strip.clear();
// 橙色箭头从左向右
for(int f=2; f<=3; f++) {
for(int x=0; x<8; x++) {
int globalX = (f==3) ? x : (x+8);
int arrowTipX = rightPos;
for(int y=0; y<8; y++) {
if (shouldLightArrowPixel(globalX, y, arrowTipX)) {
int distX = arrowTipX - globalX;
int intensity = 255 - distX * 50;
if (intensity < 100) intensity = 100;
setPixel(f, x, y, 255, intensity, 0); // 橙色
}
}
}
}
rightPos++;
if (rightPos >= 20) {
rightPos = -4;
}
strip.show();
}
// --- 向上手势效果 ---
void updateUpEffect() {
unsigned long currentTime = millis();
if (currentTime - upLastUpdate < 40) return;
upLastUpdate = currentTime;
strip.clear();
if (arrowMode) {
// 箭头模式:面1显示红色箭头 + 面2显示火焰效果
// 面1:红色箭头从下往上
for(int x=0; x<8; x++) {
for(int y=0; y<8; y++) {
int globalY = (7 - y) + 8; // 将y=0-7映射为globalY=15-8(从下往上)
if (shouldLightVerticalArrowPixel(x, globalY, upPos, true)) {
int distY = upPos - globalY;
int intensity = 255 - distY * 50;
if (intensity < 100) intensity = 100;
setPixel(1, x, y, intensity, intensity/4, 0);
}
}
}
// 面2:火焰效果
for(int x=0; x<8; x++) {
drops[1][x]++;
int head = drops[1][x];
for(int k=0; k<3; k++) {
int drawY = head - k;
if(drawY >= 0 && drawY < 8) {
uint8_t bri = (k==0) ? 200 : ((k==1)?100:40);
setPixel(2, x, drawY, bri, bri/4, 0); // 红色火焰
setPixel(3, x, drawY, bri, bri/4, 0);
}
}
if(head > 12) {
drops[1][x] = random(-6, 0);
dropSpeeds[1][x] = random(1, 3);
}
}
upPos++;
if (upPos >= 20) {
upPos = -4;
}
} else {
// 原始模式:火焰升腾效果
for(int f_idx=0; f_idx<3; f_idx++) {
int actualFace = f_idx + 1;
for(int x=0; x<8; x++) {
drops[f_idx][x]++;
int head = drops[f_idx][x];
for(int k=0; k<3; k++) {
int drawY = head - k;
if(drawY >= 0 && drawY < 8) {
uint8_t bri = (k==0) ? 200 : ((k==1)?100:40);
setPixel(actualFace, x, drawY, bri, bri/4, 0); // 红色火焰
}
}
if(head > 12) {
drops[f_idx][x] = random(-6, 0);
dropSpeeds[f_idx][x] = random(1, 3);
}
}
}
}
strip.show();
}
// --- 向下手势效果 ---
void updateDownEffect() {
unsigned long currentTime = millis();
if (currentTime - downLastUpdate < 50) return;
downLastUpdate = currentTime;
strip.clear();
if (arrowMode) {
// 箭头模式:面1显示蓝色箭头 + 面2显示雨滴效果
// 面1:蓝色箭头从上往下
for(int x=0; x<8; x++) {
for(int y=0; y<8; y++) {
int globalY = 7 - y; // 将y=0-7映射为globalY=7-0
if (shouldLightVerticalArrowPixel(x, globalY, downPos, false)) {
int distY = globalY - downPos;
int intensity = 255 - distY * 50;
if (intensity < 100) intensity = 100;
setPixel(1, x, y, 0, intensity, intensity/2);
}
}
}
// 面2:下落效果
for(int x=0; x<8; x++) {
drops[1][x]--;
int head = drops[1][x];
for(int k=0; k<3; k++) {
int drawY = head + k;
if(drawY >= 0 && drawY < 8) {
uint8_t bri = (k==0) ? 200 : ((k==1)?100:40);
setPixel(2, x, drawY, 0, bri, bri/2);
setPixel(3, x, drawY, 0, bri, bri/2);
}
}
if(head < -5) {
drops[1][x] = random(8, 14);
dropSpeeds[1][x] = random(1, 3);
}
}
downPos--;
if (downPos < -4) {
downPos = 24;
}
} else {
// 原始模式:雨滴效果
for(int f_idx=0; f_idx<3; f_idx++) {
int actualFace = f_idx + 1;
for(int x=0; x<8; x++) {
drops[f_idx][x]--;
int head = drops[f_idx][x];
for(int k=0; k<3; k++) {
int drawY = head + k;
if(drawY >= 0 && drawY < 8) {
uint8_t bri = (k==0) ? 200 : ((k==1)?100:40);
setPixel(actualFace, x, drawY, 0, bri/2, bri);
}
}
if(head < -5) {
drops[f_idx][x] = random(8, 14);
dropSpeeds[f_idx][x] = random(1, 3);
}
}
}
}
strip.show();
}
// 顺时针彩虹旋转效果
void updateClockwiseEffect() {
unsigned long currentTime = millis();
if (currentTime - clockwiseLastUpdate < 50) return;
clockwiseLastUpdate = currentTime;
strip.clear();
for(int i=0; i<NUM_LEDS; i++) {
if((i+clockwiseRotation)%5 == 0) {
strip.setPixelColor(i, Wheel((i+clockwiseRotation*5)&255));
}
}
clockwiseRotation++;
if (clockwiseRotation >= 20) clockwiseRotation = 0;
strip.show();
}
// 逆时针雷达扫描效果
void updateAnticlockwiseEffect() {
unsigned long currentTime = millis();
if (currentTime - anticlockwiseLastUpdate < 80) return;
anticlockwiseLastUpdate = currentTime;
strip.clear();
for(int i=0; i<NUM_LEDS; i++) {
if((i+anticlockwiseScan)%8 == 0) {
strip.setPixelColor(i, 0, 255, 0);
}
}
anticlockwiseScan--;
if (anticlockwiseScan < 0) anticlockwiseScan = 20;
strip.show();
}
// 挥手关闭所有灯光
void updateWaveEffect() {
clearAll();
strip.show();
}
// 彩虹色轮函数
uint32_t Wheel(byte WheelPos) {
WheelPos = 255 - WheelPos;
if(WheelPos < 85) {
return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3);
}
if(WheelPos < 170) {
WheelPos -= 85;
return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3);
}
WheelPos -= 170;
return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}
/**
* 处理手势切换
* @param newGesture 新手势状态
*/
void handleGestureChange(GestureState newGesture) {
if (newGesture == currentGesture) return;
previousGesture = currentGesture;
currentGesture = newGesture;
gestureStartTime = millis();
lastGestureChangeTime = millis(); // 记录手势切换时间
clearAll();
strip.show();
// 根据新手势初始化相应效果
switch (currentGesture) {
case GESTURE_LEFT:
initLeftGesture();
break;
case GESTURE_RIGHT:
initRightGesture();
break;
case GESTURE_UP:
initUpGesture();
break;
case GESTURE_DOWN:
initDownGesture();
break;
case GESTURE_FORWARD:
initForwardGesture();
break;
case GESTURE_BACKWARD:
initBackwardGesture();
break;
case GESTURE_CLOCKWISE:
initClockwiseGesture();
break;
case GESTURE_ANTICLOCKWISE:
initAnticlockwiseGesture();
break;
case GESTURE_WAVE:
initWaveGesture();
break;
case GESTURE_NONE:
setGlobalBrightness(DEFAULT_BRIGHTNESS);
break;
}
}
/**
* 传感器手势转换为状态枚举
* @param gesture 传感器识别的手势
* @return 对应的手势状态
*/
GestureState sensorGestureToState(Gesture gesture) {
switch (gesture) {
case GES_LEFT: return GESTURE_LEFT;
case GES_RIGHT: return GESTURE_RIGHT;
case GES_UP: return GESTURE_UP;
case GES_DOWN: return GESTURE_DOWN;
case GES_FORWARD: return GESTURE_FORWARD;
case GES_BACKWARD: return GESTURE_BACKWARD;
case GES_CLOCKWISE: return GESTURE_CLOCKWISE;
case GES_ANTICLOCKWISE: return GESTURE_ANTICLOCKWISE;
case GES_WAVE: return GESTURE_WAVE;
default: return GESTURE_NONE;
}
}
// 启动动画:彩虹呼吸效果
void bootAnimation() {
for(int cycle = 0; cycle < 2; cycle++) {
// 渐亮
for(int b = 5; b <= 150; b += 3) {
for(int i = 0; i < NUM_LEDS; i++) {
uint32_t c = HSVtoRGB((i * 2 + cycle * 50) % 255, 255, b);
strip.setPixelColor(i, c);
}
strip.show();
delay(10);
}
delay(100);
// 渐暗
for(int b = 150; b >= 5; b -= 3) {
for(int i = 0; i < NUM_LEDS; i++) {
uint32_t c = HSVtoRGB((i * 2 + cycle * 50) % 255, 255, b);
strip.setPixelColor(i, c);
}
strip.show();
delay(10);
}
}
strip.clear();
strip.show();
}
// 错误指示:四个角闪烁红灯
void showErrorIndicator() {
for(int blinkCount = 0; blinkCount < 10; blinkCount++) {
strip.clear();
// 面1四个角
strip.setPixelColor(0, 255, 0, 0);
strip.setPixelColor(7, 255, 0, 0);
strip.setPixelColor(56, 255, 0, 0);
strip.setPixelColor(63, 255, 0, 0);
// 面2四个角
strip.setPixelColor(64, 255, 0, 0);
strip.setPixelColor(71, 255, 0, 0);
strip.setPixelColor(120, 255, 0, 0);
strip.setPixelColor(127, 255, 0, 0);
// 面3四个角
strip.setPixelColor(128, 255, 0, 0);
strip.setPixelColor(135, 255, 0, 0);
strip.setPixelColor(184, 255, 0, 0);
strip.setPixelColor(191, 255, 0, 0);
strip.show();
delay(500);
strip.clear();
strip.show();
delay(500);
}
// 保持一个角点亮表示错误状态
strip.setPixelColor(0, 255, 0, 0);
strip.setPixelColor(7, 255, 0, 0);
strip.setPixelColor(56, 255, 0, 0);
strip.setPixelColor(63, 255, 0, 0);
strip.show();
}
// 初始化设置
void setup() {
Serial.begin(115200); // 串口初始化
// 初始化LED灯带
strip.begin();
strip.setBrightness(DEFAULT_BRIGHTNESS);
strip.clear();
strip.show();
// 初始化随机数种子
randomSeed(analogRead(0));
// 初始化手势传感器
if (!gestureSensor.begin()) {
Serial.println("错误:手势传感器初始化失败!");
showErrorIndicator(); // 传感器初始化失败,显示错误
while(true); // 停止运行
} else {
Serial.println("系统初始化成功!");
bootAnimation(); // 传感器初始化成功,播放启动动画
Serial.println("手势传感器和LED灯带已就绪。");
}
}
// 主循环
void loop() {
// 读取手势传感器
Gesture sensorGesture = gestureSensor.readGesture();
GestureState newGestureState = sensorGestureToState(sensorGesture);
// 手势防抖:在防抖时间内不响应新手势
unsigned long currentTime = millis();
bool canChangeGesture = (currentTime - lastGestureChangeTime) >= GESTURE_DEBOUNCE_TIME;
// 如果检测到新手势且不同于当前手势,并且已过防抖时间
if (newGestureState != GESTURE_NONE &&
newGestureState != currentGesture &&
canChangeGesture) {
handleGestureChange(newGestureState);
}
// 更新当前手势的动画效果
switch (currentGesture) {
case GESTURE_LEFT:
updateLeftEffect();
break;
case GESTURE_RIGHT:
updateRightEffect();
break;
case GESTURE_UP:
updateUpEffect();
break;
case GESTURE_DOWN:
updateDownEffect();
break;
case GESTURE_CLOCKWISE:
updateClockwiseEffect();
break;
case GESTURE_ANTICLOCKWISE:
updateAnticlockwiseEffect();
break;
case GESTURE_WAVE:
updateWaveEffect();
break;
case GESTURE_NONE:
break;
}
delay(10);
}
/******************************************************************************
* 深圳市在芯间科技有限公司
* 淘宝店铺:在芯间科技零知板
* 店铺网址:https://shop533070398.taobao.com
* 版权说明:
* 1.本代码的版权归【深圳市在芯间科技有限公司】所有,仅限个人非商业性学习使用。
* 2.严禁将本代码或其衍生版本用于任何商业用途(包括但不限于产品开发、付费服务、企业内部使用等)。
* 3.任何商业用途均需事先获得【深圳市在芯间科技有限公司】的书面授权,未经授权的商业使用行为将被视为侵权。
******************************************************************************/
系统流程图

四、操作流程
4.1 系统操作
上电后,3 块矩阵先播放彩虹呼吸启动动画,随后进入待机状态,串口输出初始化日志;动画结束后,LED全部熄灭,等待手势输入

4.2 手势操作演示
向 PAJ7620U2 传感器做出对应手势(如向上挥手),矩阵立即显示对应动画;

向前手势切换为 "箭头模式",上下手势显示箭头;向后手势切换回 "原始模式",同时串口打印切换后的信息
挥手手势关闭所有灯光,再次做出其他手势恢复显示。
4.3 视频演示
零知标准板手势控制RGB立方体全功能演示
视频展示了基于零知标准板的智能手势控制RGB立方体系统的完整功能。系统启动与初始化过程、9种手势的识别与响应(向左、向右、向上、向下、向前、向后、顺时针、逆时针、挥手)、两种显示模式的切换
五、PAJ7620U2 手势传感器知识点讲解
PAJ7620U2是一款高度集成的光学手势识别传感器,其核心工作原理基于红外成像与模式识别
工作原理
传感器内部集成了一个红外发射LED(IR LED)和一个光电二极管阵列。

内置940nm红外LED,可在完全黑暗环境中工作;内置光学带通滤波器,有效滤除环境光干扰;手势识别算法直接在传感器硬件中实现,减轻主控负担;支持正常模式(60-600°/s)和快速模式(60-1200°/s)
技术参数
| 参数 | 数值 | 说明 |
|---|---|---|
| 工作电压 | 2.8V-3.6V | 使用3.3V供电 |
| I2C频率 | 100kHz/400kHz | 支持标准/快速模式 |
| 检测距离 | 5-15cm | 最佳识别距离 |
| 视角范围 | 60° | 有效识别角度 |
5.1 I2C通信原理
PAJ7620U2 采用 I2C 总线与主控通信,I2C 地址默认为 0x73,可通过硬件引脚修改为 0x72、通信速率支持 100Kbps(标准模式)/400Kbps(快速模式)

主控板通过 I2C 总线周期性读取 GESTURE_VALID_FLAG 寄存器,当检测到标志位为 1 时,读取 GESTURE_DATA 寄存器的手势编码,转换为对应的手势指令
5.2 寄存器系统架构
PAJ7620U2采用Bank寄存器架构,分为Bank 0和Bank 1两个区域:

访问寄存器 0xEF 并写入 0x00 进入 Bank0(常用手势数据都在这里);访问寄存器 0xEF 并写入 0x01 进入 Bank1(用于高级配置)。
本项目使用到的核心寄存器:手势使能寄存器 和手势结果寄存器

函数底层已经帮我们封装好了对
0x43寄存器的读取,并将其映射为GES_UP、GES_DOWN等枚举值
核心寄存器
| 寄存器地址 | 寄存器名称 | 功能说明 |
|---|---|---|
| 0x00 | MODE_SELECT | 模式选择寄存器,用于配置传感器工作模式,本项目设置为手势识别模式(0x01) |
| 0x01-0x03 | INIT_CONFIG | 初始化配置寄存器,用于设置传感器的采样率、增益等参数,确保手势识别精度 |
| 0x43-0x44 | GESTURE_DATA | 手势数据寄存器,存储识别到的手势类型编码,主机通过读取该寄存器获取手势信息 |
| 0x45 | GESTURE_VALID_FLAG | 手势有效标志寄存器,当检测到有效手势时,该寄存器置 1,主机可通过该标志判断是否有手势输入 |
| 0x60-0x6F | GESTURE_CONFIG | 手势配置寄存器,用于使能 / 禁用特定手势类型(如上下、左右、旋转等),本项目使能全部 9 种手势 |
六、常见问题解答(FAQ)
Q1:WS2812 矩阵灯光乱闪,动画显示错位
A:请排查: 坐标映射函数
getIndex()的面 2 / 面 3 转换逻辑错误、DIN/DOUT矩阵级联顺序错误、WS2812 数据引脚接触不良、供电不足导致信号不稳定
Q2:动画卡顿,帧率低
*A:每帧更新间隔过短,亮度设置过高,数据传输量过大,CPU 处理不过来:*增大动画更新间隔,降低DEFAULT_BRIGHTNESS,简化动画绘制逻辑,减少嵌套循环
项目资源整合
PAJ7620U2 数据手册: PAJ7620U2 Datasheet
PAJ7620U2 库文件: RevEng_PAJ7620