STM32F103C8T6实战:用PAJ7620手势传感器DIY智能灯控(附完整代码)

不知道你有没有过这样的体验:晚上躺在床上看书,困意袭来时却要挣扎着起身去关灯;或者想在客厅营造不同的氛围,却要反复起身去按墙壁开关。传统的物理开关在智能家居时代显得有些笨拙,而市面上的智能灯控方案要么价格昂贵,要么安装复杂。今天,我想分享一个自己动手就能实现的解决方案——用一块十几块钱的STM32F103C8T6开发板,搭配PAJ7620手势传感器,打造一套成本极低、体验却非常酷炫的智能灯光控制系统。

这个项目的核心思路很简单:通过手势传感器识别你的手部动作,控制WS2812B RGB灯带的开关、亮度、颜色和特效。向上挥手开灯,向下挥手关灯,左右移动调节亮度,顺时针旋转切换颜色模式……整个过程无需任何物理接触,就像在空气中“隔空操作”一样。我最初做这个是为了改造书桌台灯,后来发现这套方案同样适用于床头灯、橱柜灯带甚至客厅的氛围照明。对于嵌入式爱好者和创客来说,这不仅仅是一个功能实现,更是一次深入理解I2C通信、传感器数据处理和PWM/DMA驱动LED的绝佳实践。

1. 硬件选型与成本控制:为什么是STM32F103C8T6和PAJ7620?

在开始动手之前,我们先聊聊硬件选择。市面上主控芯片和传感器种类繁多,为什么偏偏选中了这两款?答案很简单:在满足功能需求的前提下,极致性价比。

STM32F103C8T6,江湖人称“蓝色药丸”,是意法半导体Cortex-M3内核的经典款。它拥有72MHz的主频、64KB Flash和20KB RAM,对于处理手势识别和LED驱动绰绰有余。更重要的是,它的价格常年稳定在十元人民币左右,开发板更是遍地开花,资料极其丰富。相比动辄几十上百元的其他平台,它无疑是个人DIY项目的首选。

PAJ7620U2手势识别传感器,则是原相科技推出的一款集成度很高的芯片。它内部集成了红外LED、光学传感器阵列和手势识别算法,能直接输出识别结果,省去了我们自己做图像处理和算法识别的麻烦。它支持9种基本手势(上、下、左、右、前、后、顺时针、逆时针、挥动),以及接近检测功能。通过I2C接口与主控通信,接线简单,通常三十元左右就能买到模块。

至于WS2812B灯带,这是创客圈的“明星产品”了。每个灯珠都集成了驱动芯片,只需要一根数据线就能实现全彩控制,极大地简化了布线。我们可以根据实际需要裁剪长度,非常灵活。

为了让你的采购清单更清晰,我把核心物料和大致成本列在下面:

组件型号/规格数量参考单价(元)说明
主控开发板STM32F103C8T6最小系统板110-15核心控制器,建议选择带USB转串口的版本方便调试
手势传感器PAJ7620U2模块125-35注意选择带电平转换的3.3V/5V兼容模块
RGB灯带WS2812B (60珠/米)0.5米15-20可按需裁剪,建议先买30颗的段进行测试
电源5V/2A直流电源适配器110-15单独给灯带供电,避免开发板USB供电不足
连接线杜邦线(母对母)若干2-5用于模块间连接

总成本可以轻松控制在70元以内。这个价格,可能还买不到一个成品的智能灯泡,但我们得到的是一个完全可定制、可扩展的智能灯光控制核心。你完全可以在此基础上增加蓝牙/Wi-Fi模块实现手机控制,或者接入语音识别模块,打造属于你自己的全屋智能中枢。

提示:购买WS2812B灯带时,注意区分信号输入(DIN)和输出(DOUT)端。裁剪后,需要焊接导线才能继续使用,建议准备一个烙铁。

2. 系统搭建与硬件连接:从零开始的接线指南

硬件到手后,第一步就是正确连接。整个系统的接线逻辑非常清晰:STM32作为大脑,通过I2C总线读取PAJ7620的数据,再通过一个GPIO口以特定的时序协议控制WS2812B灯带。电源方面,我强烈建议将传感器和灯带的供电分开处理,这是保证系统稳定运行的关键。

STM32F103C8T6与PAJ7620U2的连接

PAJ7620模块通常有6个引脚:VCC、GND、SDA、SCL、INT(中断)和ADD(地址选择)。对于基础应用,我们只需要前4个。STM32F103C8T6有硬件I2C接口,对应的引脚是:

  • PB6 -> I2C1_SCL
  • PB7 -> I2C1_SDA

你也可以使用任意两个GPIO口来模拟I2C时序(软件I2C),这在引脚资源紧张时很灵活。这里我们以硬件I2C为例进行连接:

STM32F103C8T6引脚PAJ7620U2模块引脚功能说明
3.3VVCC传感器供电(务必接3.3V,接5V可能损坏!)
GNDGND共地
PB7SDAI2C数据线
PB6SCLI2C时钟线

STM32F103C8T6与WS2812B灯带的连接

WS2812B的控制看似简单,实则对时序要求极其苛刻。每个灯珠需要24位数据(8位绿 + 8位红 + 8位蓝),每一位用高低电平的持续时间来区分0和1。STM32的普通GPIO翻转速度很难满足其纳秒级的要求,因此我们采用一种取巧但非常高效的方法:用SPI的MOSI线配合DMA来模拟数据时序。

具体来说,我们将WS2812B的“0”和“1”码元,转换成SPI时钟周期内特定格式的字节流发送出去。例如,设置SPI时钟为一定频率,使得每个bit的传输时间刚好是WS2812B要求的一位时间(如1.25us)的几分之一。然后,我们用3个SPI数据位来代表WS2812B的1位数据。这种方法利用了硬件SPI和DMA,不占用CPU资源,时序精准。

我们选择SPI1的MOSI引脚(PA7)来连接灯带的数据输入(DIN)。

STM32F103C8T6引脚WS2812B灯带功能说明
PA7DINSPI1_MOSI,用于发送灯带控制数据
5V5V重要:必须外接5V/2A以上电源单独供电
GNDGND电源地,务必与STM32开发板共地

注意:WS2812B灯带工作电流很大,每颗LED全亮白色时约60mA。30颗灯珠就需要近2A的电流。切勿使用STM32开发板上的5V引脚或USB口为整条灯带供电,否则极易导致开发板损坏或USB端口保护。务必使用独立的5V电源适配器为灯带供电,同时将灯带的GND与开发板的GND连接在一起,确保信号地一致。

连接好之后,你的系统应该是这个样子:STM32开发板通过USB线连接电脑用于供电和程序下载,PAJ7620模块通过四根杜邦线连接到STM32,WS2812B灯带的数据线接STM32的PA7,电源正负极接外部5V电源。检查无误后,就可以进入激动人心的软件部分了。

3. 软件架构与核心代码解析:如何让手势“指挥”灯光

软件部分是整个项目的灵魂,我们需要完成三件核心任务:初始化并读取PAJ7620传感器、设计手势识别逻辑、驱动WS2812B灯带显示。代码量不大,但每一部分都很有讲究。我会用最直观的方式,带你理解关键代码段。

3.1 PAJ7620的初始化与数据读取

PAJ7620通过I2C接口通信,地址默认为0x73。初始化过程主要是配置一些内部寄存器,使其进入手势识别模式。网上有很多现成的驱动库,但理解其原理很重要。下面是一个简化的初始化流程:

  1. 发送唤醒序列:向0xEF寄存器写入0x01,使传感器退出睡眠模式。
  2. 选择寄存器组:PAJ7620的寄存器分为BANK0和BANK1。手势识别相关的配置主要在BANK1。通过向0xEF写入0x01切换到BANK1。
  3. 配置手势识别:设置一系列寄存器,例如使能9种手势的中断输出。关键寄存器是0x41~0x42,对应手势中断使能位。
  4. 切换回BANK0并启动:向0xEF写入0x00切回BANK0,然后向0x72寄存器写入0x01启动手势检测引擎。

初始化成功后,我们就可以在一个循环里,不断读取手势状态寄存器(0x43, 0x44)来判断当前手势。下面是一段核心的读取函数示例:

// 读取手势识别结果
uint8_t PAJ7620_Read_Gesture(void) {
    uint8_t data[2] = {0};
    uint16_t gesture_data = 0;

    // 读取手势中断标志寄存器
    if(I2C_ReadBytes(PAJ7620_ADDR, PAJ_REG_GES_RESULT_1, 2, data) == HAL_OK) {
        gesture_data = (data[1] << 8) | data[0];

        if(gesture_data) {
            switch(gesture_data) {
                case GES_RIGHT_FLAG:
                    return GESTURE_RIGHT;
                case GES_LEFT_FLAG:
                    return GESTURE_LEFT;
                case GES_UP_FLAG:
                    return GESTURE_UP;
                case GES_DOWN_FLAG:
                    return GESTURE_DOWN;
                case GES_FORWARD_FLAG:
                    return GESTURE_FORWARD;
                case GES_BACKWARD_FLAG:
                    return GESTURE_BACKWARD;
                case GES_CLOCKWISE_FLAG:
                    return GESTURE_CLOCKWISE;
                case GES_COUNT_CLOCKWISE_FLAG:
                    return GESTURE_ANTICLOCKWISE;
                case GES_WAVE_FLAG:
                    return GESTURE_WAVE;
                default:
                    return GESTURE_NONE;
            }
        }
    }
    return GESTURE_NONE;
}

3.2 手势映射与状态机设计

直接使用传感器输出的9种手势有时过于“灵敏”或“死板”。例如,你可能希望“向上挥手”是开灯,“向下挥手”是关灯,而“左右挥手”是调节亮度。这就需要我们设计一个简单的状态机来管理系统的不同模式(如关闭、常亮、呼吸、彩虹等),并将手势映射到对应的模式切换或参数调整函数。

一个更高级的玩法是利用传感器的“光标模式”。在这个模式下,传感器不再输出离散的手势类型,而是持续输出手部在X-Y平面上的坐标。我们可以用这个坐标来控制灯带的光点位置和亮度,实现手势跟踪效果——手向左移动,光点就向左移动;手抬高,整体亮度就增加。这比简单的开关控制要酷炫得多。

// 简易系统状态机示例
typedef enum {
    SYS_OFF,
    SYS_ON_SOLID,
    SYS_ON_BREATHING,
    SYS_ON_RAINBOW
} SystemState_t;

SystemState_t sysState = SYS_OFF;

void Handle_Gesture(uint8_t gesture) {
    switch(sysState) {
        case SYS_OFF:
            if(gesture == GESTURE_UP) {
                sysState = SYS_ON_SOLID;
                LED_TurnOn();
            }
            break;
        case SYS_ON_SOLID:
            if(gesture == GESTURE_DOWN) {
                sysState = SYS_OFF;
                LED_TurnOff();
            } else if(gesture == GESTURE_CLOCKWISE) {
                sysState = SYS_ON_BREATHING;
            }
            break;
        // ... 其他状态处理
        default:
            break;
    }
}

3.3 WS2812B的SPI+DMA驱动技巧

如前所述,驱动WS2812B的精髓在于时序。使用SPI模拟的代码如下。我们首先需要根据WS2812B的时序要求,计算SPI的时钟分频。假设MCU主频72MHz,WS2812B的0码高电平约0.4us,1码高电平约0.8us,复位码低电平需大于50us。

我们可以设定SPI波特率,使得每个SPI bit的时长在0.4us左右。然后用3个SPI bit来编码WS2812B的1个数据位:对于WS2812B的‘0’,我们发送‘100’(SPI数据);对于‘1’,则发送‘110’。这样,通过DMA发送一长串由‘100’和‘110’组成的字节数组,就能精确控制整条灯带。

// WS2812B SPI DMA 发送函数核心逻辑
#define WS2812_SPI_HANDLE   hspi1
#define NUM_LEDS 30

// 码元定义:根据你的SPI时钟计算得出
#define CODE_FOR_0 0x92  // 对应二进制 10010010,实际需要根据时序计算
#define CODE_FOR_1 0xDB  // 对应二进制 11011011

uint8_t spi_buffer[NUM_LEDS * 24 * 3 / 8 + 1]; // 每个LED 24bit,每bit用3个SPI位表示

void WS2812_Send_DMA(uint32_t *led_color_buf) {
    uint32_t index = 0;
    for(int i = 0; i < NUM_LEDS; i++) {
        uint32_t color = led_color_buf[i]; // 格式通常为 0x00GGRRBB
        // 注意WS2812B的数据顺序是GRB
        uint8_t g = (color >> 16) & 0xFF;
        uint8_t r = (color >> 8) & 0xFF;
        uint8_t b = color & 0xFF;

        // 将每个颜色字节的每一位转换为SPI码元
        for(int bit = 7; bit >= 0; bit--) {
            spi_buffer[index++] = ((g >> bit) & 0x01) ? CODE_FOR_1 : CODE_FOR_0;
        }
        for(int bit = 7; bit >= 0; bit--) {
            spi_buffer[index++] = ((r >> bit) & 0x01) ? CODE_FOR_1 : CODE_FOR_0;
        }
        for(int bit = 7; bit >= 0; bit--) {
            spi_buffer[index++] = ((b >> bit) & 0x01) ? CODE_FOR_1 : CODE_FOR_0;
        }
    }
    // 发送复位码(低电平一段时间),可以通过在数组末尾添加全0并延迟实现
    spi_buffer[index] = 0x00;

    HAL_SPI_Transmit_DMA(&WS2812_SPI_HANDLE, spi_buffer, index + 1);
}

这段代码将每个LED的GRB颜色值,按位转换成对应的SPI码元字节,存入缓冲区,然后通过DMA一次性发送出去。CPU在此期间可以完全解放出来处理传感器数据,实现了高效的并行处理。

4. 功能实现与效果优化:从基础开关到炫酷交互

有了基础的驱动和框架,我们就可以尽情发挥创意,设计各种灯光效果和交互逻辑了。这一部分,我将分享几个经过实战检验的效果实现和调试技巧。

4.1 基础手势控制映射

一个直观的映射方案如下,你可以根据自己的习惯调整:

  • 向上挥手: 系统开机,播放一个华丽的启动动画(如从中心向两侧扩散的流光)。
  • 向下挥手: 系统关机,所有灯珠亮度渐暗至熄灭。
  • 向左/向右挥手: 降低/提高全局亮度。
  • 顺时针旋转: 切换灯光模式(例如:常亮白光 -> 彩虹渐变 -> 呼吸效果 -> 色彩循环)。
  • 逆时针旋转: 反向切换灯光模式,或调节模式内的参数(如呼吸速度)。
  • 向前/向后: 增加/减少RGB某一颜色分量,实现调色。
  • 挥动: 可设置为特殊效果触发,如流星雨、警报闪烁等。

4.2 手势跟踪与亮度随动

启用PAJ7620的光标模式后,我们可以获取手部的X、Y坐标。利用这两个值,可以创造出更细腻的交互:

  • X坐标映射位置: 将X坐标映射到灯带LED的索引上。手从左移到右,一个高亮的光点就从灯带左端滑到右端。
  • Y坐标映射亮度: 将Y坐标映射到全局亮度(0-255)。手抬高,灯变亮;手放低,灯变暗。
  • 添加拖尾效果: 记录光点的移动轨迹,让经过的LED亮度缓慢衰减,形成一道美丽的彩虹拖尾。这只需要一个数组来记录每个LED的“热度”,每帧进行衰减即可。
// 手势跟踪与拖尾效果示例代码
float trail[NUM_LEDS] = {0}; // 拖尾强度数组
float decay = 0.9; // 衰减系数

void Update_Trail_Effect(int hand_pos_x) {
    // 1. 所有拖尾强度衰减
    for(int i=0; i<NUM_LEDS; i++) {
        trail[i] *= decay;
        if(trail[i] < 0.05) trail[i] = 0;
    }

    // 2. 将手部X坐标映射为LED索引
    int led_index = map(hand_pos_x, SENSOR_X_MIN, SENSOR_X_MAX, 0, NUM_LEDS-1);
    led_index = constrain(led_index, 0, NUM_LEDS-1);

    // 3. 当前位置设为最高亮度
    trail[led_index] = 1.0;

    // 4. 根据trail数组设置每个LED的颜色和亮度
    for(int i=0; i<NUM_LEDS; i++) {
        if(trail[i] > 0) {
            // 计算彩虹色,并根据trail强度调整亮度
            uint32_t color = Wheel((i * 256 / NUM_LEDS) % 256);
            uint8_t r = (uint8_t)(((color >> 16) & 0xFF) * trail[i]);
            uint8_t g = (uint8_t)(((color >> 8) & 0xFF) * trail[i]);
            uint8_t b = (uint8_t)((color & 0xFF) * trail[i]);
            strip_SetPixelColor(i, r, g, b);
        } else {
            strip_SetPixelColor(i, 0, 0, 0);
        }
    }
    strip_Show();
}

4.3 灵敏度调试与抗干扰

在实际使用中,你可能会遇到手势误触发或不触发的问题。这通常需要通过调整代码中的阈值参数来解决。不要迷信数据手册的推荐值,根据你的安装环境和使用习惯进行实地调试才是王道。

  • 手势识别不灵敏: 检查传感器安装是否平整,手部是否在有效的检测距离(5-15cm)和角度内。在代码中,可以尝试降低手势判定的速度和位移阈值。
  • 容易误触发: 可能是环境光干扰,或者传感器附近有物体晃动。可以尝试增加手势判定的“冷却时间”(即两次手势间的最小间隔),提高速度和位移阈值。也可以软件上做一个简单的滤波,比如连续两次读到相同手势才确认。
// 手势检测逻辑中的可调参数
#define GESTURE_COOLDOWN_MS   800   // 手势冷却时间,防止连发
#define GESTURE_MOVE_THRESHOLD 1000 // 手势移动幅度阈值(原始坐标差)
#define GESTURE_SPEED_THRESHOLD 600 // 手势移动速度阈值(单位/秒)

// 如果总是误触发,尝试增大这些值
// GESTURE_MOVE_THRESHOLD = 1200;
// GESTURE_SPEED_THRESHOLD = 800;
// GESTURE_COOLDOWN_MS = 1000;

// 如果很难触发,尝试减小这些值
// GESTURE_MOVE_THRESHOLD = 800;
// GESTURE_SPEED_THRESHOLD = 400;

4.4 低功耗与待机模式

如果你希望这个系统能够长时间电池供电(比如做成一个便携的氛围灯),那么低功耗设计就很重要。STM32F103本身支持多种低功耗模式。一个简单的策略是:当检测到手部离开传感器区域超过一定时间(比如30秒),系统就进入待机模式。在待机模式下,可以关闭大部分外设,只保留一个外部中断唤醒功能(PAJ7620的INT引脚可以连接到STM32的外部中断引脚)。当传感器再次检测到手部接近时,产生中断唤醒MCU。

// 简易待机逻辑
uint32_t last_activity_time = 0;
#define IDLE_TIMEOUT_MS 30000

void main_loop() {
    if(hand_present) {
        last_activity_time = HAL_GetTick();
        // ... 正常交互逻辑
    } else {
        if(HAL_GetTick() - last_activity_time > IDLE_TIMEOUT_MS) {
            Enter_Standby_Mode();
        } else {
            // 播放待机动画(如缓慢呼吸)
            Play_Idle_Animation();
        }
    }
}

void Enter_Standby_Mode() {
    // 关闭LED灯带电源(可通过MOSFET控制)
    // 配置PAJ7620的INT引脚为唤醒源
    // 设置STM32进入STOP模式
    HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
    // 被唤醒后,系统会从这里继续执行,需要重新初始化外设
    SystemInit();
    // ... 重新初始化传感器、LED等
}

通过以上四个部分的组合,你已经可以打造出一个反应灵敏、效果炫酷、且可根据自己喜好深度定制的智能手势灯控系统。从简单的开关到复杂的光影跟随,所有的交互逻辑都掌握在你自己的代码中。这种从无到有、看着自己的手势真正“控制”光线的成就感,是购买任何成品都无法替代的。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐