尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32手势控制小车:从Paj7620U2传感器到PWM电机驱动完整实现

STM32手势控制小车:从Paj7620U2传感器到PWM电机驱动完整实现 简介基于STM32F103C8T6单片机设计的手势控制小车项目面向嵌入式初学者和电子设计竞赛备赛者用一套完整方案演示了在2.4G无线串口下手势采集端将陀螺仪姿态数据实时发送至小车端小车端解析后执行前进、后退、转向等指令的全过程。整个工程包含手势端与小车端两套独立代码各模块划分清楚注释详细适合边读代码边对照开发板验证。资源压缩包共包含四百九十四个文件以C语言源码、头文件、编译生成的中间文件和可执行文件为主同时提供Keil工程配置、链接脚本以及一键清理脚本等辅助内容压缩包整体大小为十七点一八兆字节目录结构清晰使用Keil打开即可浏览源码和生成烧录文件。该项目已被一千二百一十四人学习通过参考工程可弄清楚无线通信协议如何设计、指令如何打包与解析并在此基础上加入蓝牙、语音或视觉模块改造成更具个人特色的智能小车实用性很强。1. 手势控制小车真正的难点不在电机而在“判手势”用 STM32 做手势控制小车第一反应是高大上的人工智能但实际落地后会发现这个项目的核心矛盾根本不是算法而是“手势信号从哪来、怎么变成可靠的电机指令”。市面上绝大多数方案走的是两条路一条是 Paj7620U2 这类专用手势识别传感器靠红外反射判断手势方向响应快、代码量小另一条是 MPU6050 加速度计加角度解算通过手腕姿态推算出意图成本低但需要自己写滤波和判据。两条路都能跑但调试体验天差地别。这个标题真正在讲的事是用 C 语言在 STM32 上把传感器数据读进来通过状态机或简单阈值逻辑把连续的手势流翻译成离散的动作指令再通过 PWM 控制电机完成前进、后退、转向。它适合正在做课设、电赛或毕业设计的嵌入式初学者也适合想把手势交互从手机 App 挪到裸机环境里验证的工程师。动手前先建立一个认知手势识别在 MCU 上不是做深度学习推理而是做特征判据先把这个想清楚后面每一步都顺。2. 硬件选型与数据链路先把手势变成寄存器里的数2.1 Paj7620U2 还是 MPU6050两条路线的取舍手势传感器选型直接决定代码结构。Paj7620U2 内部集成了红外 LED 和光电二极管阵列通过检测反射光的相位差来识别 9 种手势包括左右、上下、前后、顺时针、逆时针和挥手。它的优势是识别结果已经由芯片内部的 DSP 处理好了MCU 只需要通过 I2C 读取寄存器值一个0x43就能判断当前手势。代价是误判率在强光下会上升而且有效距离只有 10 到 15 厘米。MPU6050 路线的逻辑不一样——它靠加速度计和陀螺仪融合出的角度变化来判断手势。比如快速向左挥动时Y 轴加速度会出现一个明显的负脉冲通过检测脉冲方向和宽度就能区分左挥和右挥。这条路线的优势是没有距离限制、不怕环境光适合做“戴在手上的控制端”但算法上要做滑动窗口滤波、阈值比较和去抖动代码量是 Paj7620U2 的三到五倍。选型建议很直接如果小车的控制端是固定在车头前方、用户用手在传感器上方比划Paj7620U2 是性价比最高的选择如果要做成“手套控制器”或者遥控器形式MPU6050 更合适。本项目以 Paj7620U2 为主线因为它能让你把精力集中在状态机和电机控制上这正是标题里“控制小车”的核心。2.2 最小硬件清单与接线表除了主控 STM32F103C8T6 和传感器还需要两个 DRV8833 或 L298N 电机驱动模块、两个带减速箱的 TT 电机、一个 7.4V 锂电池和一块 AMS1117-3.3 稳压板。ST-Link V2 用于下载调试。I2C 上拉电阻不能省Paj7620U2 模块一般自带 4.7K 上拉但如果用杜邦线延长超过 20 厘米建议在 SCL 和 SDA 上各并联一个 10K 电阻到 3.3V否则高速通信时波形会变形。接线表如下Paj7620U2 引脚STM32F103C8T6 引脚说明VCC3.3V传感器供电务必用 3.3V 而不是 5VGNDGND共地SCLPB6I2C1 时钟线SDAPB7I2C1 数据线INT不接中断引脚可悬空用轮询即可电机驱动的 IN1/IN2 接 PA0/PA1 控制左电机IN3/IN4 接 PA2/PA3 控制右电机PWM 引脚用 PA6 和 PA7 —— 这两个引脚刚好是 TIM3 的通道 1 和通道 2可以同时输出两路 PWM。如果你用的是 L298N记得把驱动板的 5V 输出接到 STM32 的 5V 引脚给板子供电前提是 STM32 板载稳压但传感器必须吃 3.3V不能直接并到 5V 上否则大概率烧芯片。2.3 用 CubuMX 生成 I2C 和 PWM 工程骨架工程初始化用 STM32CubeMX 完成比手写寄存器快得多。新建工程选择 STM32F103C8Tx在 System Core 里把 PB6 和 PB7 配为 I2C1参数全部默认即可再配置 TIM3把 Channel1 和 Channel2 设为 PWM Generation CHxPrescaler 设为 71Counter Period 设为 999这样 PWM 频率就是 72MHz / (72 * 1000) 1000Hz对直流电机来说足够平滑。生成代码后在main.c的USER CODE BEGIN 2段加上传感器初始化和电机 GPIO 初始化。注意 PB6/PB7 在部分蓝板兼容板上已经被板载 LED 占用如果发现 I2C 通信不稳定先确认板子型号或者换到 PB8/PB9 复用为 I2C1 的备选引脚。这个坑在国产兼容板上出现概率极高焊接排针前先用万用表量一下 PB6 是否被下拉电阻拉低。3. 手势识别核心Paj7620U2 寄存器配置与手势映射3.1 初始化序列的两个关键寄存器Paj7620U2 的初始化不是简单写一个0x00就完事的它有一整套银行切换流程。上电后先延时 1 毫秒然后往 Bank0 的寄存器 0xEF 写 0x00进入正常模式再往 0x00 写 0x00、0x01 写 0x00复位传感器。接下来进入 Bank1在 0xEF 写 0x01然后配置 0x00 为 0x07开启所有手势识别、0x01 为 0x01开启人脸检测、0x02 为 0x00、0x03 为 0x00最后跳回 Bank0 并开启轮询模式。初始化代码的核心部分是这样的uint8_t paj7620_init(void) { uint8_t val 0x00; // 进入 Bank0复位芯片 paj7620_write_reg(0xEF, 0x00); paj7620_write_reg(0x00, 0x00); delay_ms(1); // 切换到 Bank1配置手势检测使能 paj7620_write_reg(0xEF, 0x01); paj7620_write_reg(0x00, 0x07); // 使能左右、上下、前后手势 paj7620_write_reg(0x01, 0x01); // 使能顺时针/逆时针 paj7620_write_reg(0x02, 0x00); paj7620_write_reg(0x03, 0x00); // 跳回 Bank0进入工作模式 paj7620_write_reg(0xEF, 0x00); paj7620_write_reg(0x00, 0x00); return paj7620_read_reg(0x00); // 读回校验应为 0x20 }初始化最后一步读回 0x00 寄存器正常值应该是 0x20。如果读到 0xFF 或者 0x00说明 I2C 地址不对或总线被拉死。Paj7620U2 的 I2C 地址是 0x737 位地址在写驱动时不要像操作 AT24C02 那样左移一位STM32 的 HAL 库HAL_I2C_Mem_Write函数会自动处理地址移位直接传 0x73 即可。很多人在这里把地址写成了 0xE6这是 8 位地址写法放到 HAL 里就会一直超时。3.2 手势映射表如何把手势值翻译成控制指令传感器识别到手势后GestureResult 寄存器有固定的映射关系。0x43 是向上、0x42 是向下、0x44 是向左、0x41 是向右、0x45 是向前、0x46 是向后、0x47 是顺时针、0x48 是逆时针、0x49 是挥手。写控制逻辑时不要直接用魔法数字用宏定义或枚举把它语义化否则后续想加手势会让代码变成天书。typedef enum { GESTURE_NONE 0x00, GESTURE_UP 0x43, GESTURE_DOWN 0x42, GESTURE_LEFT 0x44, GESTURE_RIGHT 0x41, GESTURE_FORWARD 0x45, GESTURE_BACKWARD 0x46, GESTURE_CLOCKWISE 0x47, GESTURE_COUNTERCLOCKWISE 0x48, GESTURE_WAVE 0x49 } gesture_t;逻辑说明gesture_t是一个枚举类型把寄存器里的原始数值映射成有意义的动作名称。这样在主循环里写if (gesture GESTURE_FORWARD)就比写if (gesture 0x45)清晰得多而且编译后生成的机器码完全相同不会多占一个字节的 Flash。这个枚举直接替代#define在 C 语言里是更安全的做法因为枚举有类型检查而宏没有。3.3 轮询式读取的手势状态机主循环里最简单的读取方式就是轮询每次循环读一次 GestureResult 寄存器读到有效手势就执行对应动作。但直接这么做会有问题——传感器在识别到一个手势后只要手还停在感应区寄存器会反复输出同一个值导致小车先是猛冲然后原地抽搐。解决办法是引入一个“已处理手势”的缓存变量只有当前读取到的手势和上一次不同时才执行动作。状态机的实现结构如下gesture_t last_gesture GESTURE_NONE; while (1) { uint8_t raw paj7620_read_gesture(); if (raw 0x00) { last_gesture GESTURE_NONE; continue; // 没有手势跳过本次处理 } gesture_t g (gesture_t)raw; if (g last_gesture) { continue; // 手势没变不重复执行 } last_gesture g; switch (g) { case GESTURE_FORWARD: motor_forward(60); break; case GESTURE_BACKWARD: motor_backward(50); break; case GESTURE_LEFT: motor_turn_left(45); break; case GESTURE_RIGHT: motor_turn_right(45); break; case GESTURE_NONE: motor_stop(); break; default: motor_stop(); break; } delay_ms(100); // 消抖避免快速切换时的毛刺 }参数说明motor_forward(60)里的 60 是 PWM 占空比百分比TIM3 的计数值范围是 0 到 999所以底层会换算成 CCR 值为 600。左转时两个电机的 PWM 不一定相同motor_turn_left(45)通常实现为左电机 30、右电机 45这样小车是原地旋转还是绕弧线转弯可以通过差值控制。delay_ms(100)的作用是给手势一个“冷却时间”防止手在边界抖动时触发两次动作。实际测试下来 100 毫秒是甜点值太短会连击太长会感觉小车反应迟钝。4. 电机控制与 PWM 调参把动作指令变成平滑运动4.1 用 TIM3 双通道输出 PWM正反转用 GPIO 高低电平切换直流减速电机的控制需要两路信号一路 PWM 控制速度一路方向电平控制正反转。DRV8833 的输入逻辑是 IN1 和 IN2 配合IN1 高、IN2 低时电机正转反过来就是反转两个都为低时刹车两个都为高时滑行。所以 STM32 的 PA0/PA1控制左电机和 PA2/PA3控制右电机需要根据动作实时改变电平而 PWM 信号只接到 IN1IN2 用普通 GPIO 切换方向——不过更标准的做法是两个引脚都接 PWM一个给正转占空比、一个给反转占空比这样换向时过渡更顺滑不会出现瞬间反接的冲击电流。电机驱动的初始化代码void motor_init(void) { // PA0~PA3 为方向控制引脚初始化为推挽输出 GPIO_InitTypeDef gpio {0}; gpio.Pin GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3; gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, gpio); // PA6 和 PA7 复用为 TIM3 PWM 输出 gpio.Pin GPIO_PIN_6 | GPIO_PIN_7; gpio.Mode GPIO_MODE_AF_PP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, gpio); // 启动 TIM3 的通道1和通道2 HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_2); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 0); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, 0); }逻辑说明HAL_GPIO_Init第一次配置 PA0 到 PA3 是普通推挽输出用来输出方向电平第二次复用 PA6/PA7 为定时器输出这时引脚的控制权从 GPIO 模块移交给 TIM3写 GPIO 的高低电平不再生效。__HAL_TIM_SET_COMPARE这个宏直接把 CCR 寄存器设置成指定值CCR 一旦改变占空比立即生效、不需要重新启动通道。初始化为 0 是防止上电瞬间电机猛转这是安全性考量——第一批板子的教训是上电时小车直接冲出桌面。4.2 直行修正两个电机转速不一致的补偿策略TT 电机的个体差异、轮胎摩擦力不一致、电池电压波动都会导致左右电机设定相同占空比时小车跑偏。调节策略是“先硬编码偏置再加闭环”。硬编码的意思是在程序里定义一个MOTOR_LEFT_BIAS和MOTOR_RIGHT_BIAS初始都设为 100百分比然后把电机速度按比例缩放。void motor_set_speed(uint8_t left, uint8_t right) { uint8_t real_left left * LEFT_BIAS / 100; uint8_t real_right right * RIGHT_BIAS / 100; __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, real_left * 999 / 100); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, real_right * 999 / 100); }参数说明LEFT_BIAS是左电机的补偿系数如果实测发现同样占空比下左电机比右电机慢就把LEFT_BIAS调到 105RIGHT_BIAS保持 100这样左电机的实际 PWM 会高 5%。调整方法在空旷地面让小车以 50% 占空比直行两米量出偏移方向如果向左偏就调大右电机偏置向右偏就调大左电机偏置每次调整步进 2%直行三次验证。这个偏置值在当前电池电量下有效电池从 8.4V 掉到 7.2V 时电机的转速特性会变化偏置可能需要微调——更彻底的方案是加编码器做闭环但课设和大部分自制项目用硬编码就够用了。4.3 转弯时间控制开环转弯的致命缺陷与补偿没有编码器时转弯只能靠“固定时间 固定占空比”实现。比如左转 90 度让左电机反转、右电机正转以 40% 占空比持续 300 毫秒。但这样做的问题是电池电压高时速度快300 毫秒可能转了 120 度电池电压低时可能只转了 60 度。补偿办法是做个简单的电压映射——用 STM32 的 ADC 采电池电压低于阈值时把转弯时间按比例延长。uint16_t battery_voltage_mv(void) { // 假设分压电阻把 8.4V 降到 3.3V 以内量程 0~4096 对应 0~3.3V HAL_ADC_Start(hadc1); uint32_t raw HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); return raw * 3300 / 4096 * 3; // 乘 3 是分压比 } void turn_left_fixed(void) { uint16_t vbat battery_voltage_mv(); uint16_t turn_time 300 (8400 - vbat) / 10; motor_turn_left(40); HAL_Delay(turn_time); motor_stop(); }逻辑说明turn_time的基准值 300 毫秒是 8.4V 满电量下的实测值(8400 - vbat) / 10算出电压每降低 10 毫伏就补偿 1 毫秒转弯时间。这个线性补偿在 7.2V 到 8.4V 区间内精度还不错是因为电机的转速和电压基本成正比。ADC 的raw * 3300 / 4096是把 12 位 ADC 原始值换算成毫伏乘 3 是修正分压电阻的比例——具体系数由电阻分压网络决定不要直接抄。5. 数据滤波与防误触让抬手、划动和收手都能被正确识别5.1 为什么 Paj7620U2 在强光和深色背景下会误报红外手势传感器的物理原理决定了它的行为发射红外光、接收反射光。当环境阳光强烈时太阳光里的红外波段会直接照射到传感器的接收管造成本底噪声抬高当手掌颜色很深时反射率下降接收信号衰减原本设计的阈值就不再成立。表现就是手没动但识别出挥手或者手势切换不灵敏。解决思路不是调传感器内部阈值极难调好且增益改起来麻烦而是在软件侧加一道防线连续读取 N 次手势如果 N 次结果一致才认为有效。由于误报通常是瞬时的真正的手势会持续 200 毫秒以上这种多数表决滤波能过滤掉大部分毛刺。#define FILTER_SAMPLES 5 gesture_t read_gesture_filtered(void) { uint8_t count[9] {0}; // 对应 9 种有效手势 for (int i 0; i FILTER_SAMPLES; i) { uint8_t val paj7620_read_gesture(); if (val 0x41 val 0x49) { count[val - 0x41]; } delay_ms(10); } for (int i 0; i 9; i) { if (count[i] 3) { // 至少 3/5 一致才算有效 return (gesture_t)(0x41 i); } } return GESTURE_NONE; }参数说明FILTER_SAMPLES设为 5每次读取间隔 10 毫秒总耗时约 50 毫秒。对普通手势操作来说 50 毫秒延迟几乎感觉不到但能消除大部分瞬时干扰。count[val - 0x41]是把寄存器值 0x41 到 0x49 映射到数组下标 0 到 8此时按“至少 3 次一致”判定有效相当于三分之一冗余。delay_ms(10)的使用理由是传感器内部算法本身需要一些处理时间连续高速读取会出现读到上一次缓存的情况加 10 毫秒让传感器完成内部刷新。5.2 手势持续时间判定——区分“快挥”和“停留”如果用户把手放在传感器上方不动Paj7620U2 会持续输出当前手势。对小车控制来说这不是问题因为前面的去重逻辑已经保证了“同一手势只执行一次”。但有一种情况比较讨厌用户想连续执行两次前进手势时第一次前进后手没来得及移开第二次前进就丢失了。常见做法是加入一个 300 毫秒的屏蔽窗口。第一次识别到手势后记录当前系统时钟300 毫秒内忽略所有新的手势输入除了停止动作之后才重新接受。这样用户有足够时间把手移开再做下一个手势不会因为过快的连续挥手导致误判。实现用HAL_GetTick()记录时间戳这是 HAL 库自带的毫秒计数器比自己维护一个变量可靠得多。5.3 出界保护手移出感应区后自动停车所有的手势控制系统都要考虑一个问题当用户把手移开时传感器输出会从某个手势变成 0x00无手势。此时如果小车还在执行上一次的转向动作就会一直转下去。处理方法是在主循环里读到 0x00 时调用motor_stop()而不是只把last_gesture置空。完整的防误触逻辑应该是读到 0x00 立即停车但保留当前手势状态下次读到相同手势时因为已经停车可以再执行一次。这样用户可以通过“抬手再放下”让同一个动作反复生效形成类似按键连发但每次都有物理离开的动作保护。这种设计比“手势持续执行”更安全——手离开意味着注意力离开此时车停下来是最稳妥的状态。6. 验证与进阶用串口日志和示波器看穿整个控制链路最后落地一个具体的验证技巧在main主循环里把手势原始值、滤波后的手势值和 PWM 占空比通过串口打印出来。SPL 的说法就是“先看数据再信手感”。接线时把 STM32 的 TXPA9接 USB 转 TTL 的 RX波特率设 1152008N1然后在每个关键分支里加一行printf但注意printf会阻塞主循环打印信息控制在每 200 毫秒一条以内。验证步骤是先用手势触发动作观察串口数据是否符合预期Paj7620U2 读到向前手势串口打印RAW:45, FILT:45, L:60, R:60同时测量 PA6 和 PA7 的波形应该是一个 1kHz、占空比 60% 的方波。如果原始值一直在变而滤波结果稳定说明滤波逻辑正常如果原始值和滤波结果都不稳定用逻辑分析仪抓 I2C 总线看有没有 ACK 错误——大概率是电源问题退而求其次是检查 I2C 上拉电阻。进阶方向有两个。第一个是把 Paj7620U2 换成 MPU6050用 DMP 库直接读取四元数通过四元数转欧拉角之后根据俯仰角变化判断手势抗光干扰能力强很多适合做户外遥控车。第二个是在状态机里加入手势组合比如先向左挥再向前挥实现“先转向再直行”的复合动作存储上一个有效手势即可不需要额外内存开销。这两个方向都不离开标题的核心逻辑传感器给数据STM32 判状态电机出动作。你在扩展时也记住这个闭环就不会跑偏。本文还有配套的精品资源点击获取
返回列表