零知IDE——基于零知标准板驱动PAJ7620U2手势控制L9110风扇模块和SG90舵机系统

电子说

1.4w人已加入

描述

​ ✔零知开源(零知IDE)是一个专为电子初学者/电子兴趣爱好者设计的开源软硬件平台,在硬件上提供超高性价比STM32系列开发板、物联网控制板。取消了Bootloader程序烧录,让开发重心从 “配置环境” 转移到 “创意实现”,极大降低了技术门槛。零知IDE编程软件,内置上千个覆盖多场景的示例代码,支持项目源码一键下载,项目文章在线浏览。零知开源(零知IDE)平台通过软硬件协同创新,让你的创意快速转化为实物,来动手试试吧!

✔访问零知实验室,获取更多实战项目和教程资源吧!

www.lingzhilab.com

项目概述

       本项目使用零知标准板(主控芯片:STM32F103RBT6)作为核心控制器,结合PAJ7620U2手势传感器实现对L9110风扇模块和SG90舵机的智能控制。系统通过识别9种不同的手势动作(上下、左右、顺时针/逆时针、挥手、前推、后拉)分别控制风扇的启停、正反转、调速以及舵机的精确角度定位,实现了无接触式智能交互体验

项目难点及解决方案

        问题描述:零知标准板的analogWrite()函数导致系统卡死

解决方案:放弃analogWrite()函数,手动配置STM32硬件定时器,直接操作定时器寄存器

一、系统接线部分

1.1 硬件清单

名称 型号/参数 数量 说明
主控板 零知标准板 (STM32F103RBT6) 1 核心控制器
扩展板 零知标准板-扩展板 1 传感器扩展板
手势传感器 PAJ7620U2 1 I2C接口,识别9种手势
风扇驱动模块 L9110 / L9110S 1 双路H桥,控制电机正反转
舵机 SG90 (180度) 1 控制风向摆动
杜邦线 公对母/公对公 若干 连接线

1.2 接线方案表

        注意:请严格按照以下代码定义的引脚进行连接,否则程序无法正常工作。

模块 引脚名称 连接到零知标准板 (STM32) 功能说明
PAJ7620U2 VIN 3.3V 通常是3.3V逻辑电平
GND GND 地线
SCL SCL (或对应I2C SCL) I2C 时钟线
SDA SDA (或对应I2C SDA) I2C 数据线
SG90 舵机 信号线 (橙) 12 PWM控制信号
VCC (红) 3.3V(直插拓展板) 电源正
GND (棕) GND 电源地
L9110 风扇 INA 9(PB7) 电机控制脚A
INB 5(PB6) 电机控制脚B
VCC 5V (建议外接) 电源正
GND GND 电源地

PS:本项目采用扩展板直插零知标准板,请注意I2C接口线序,与开发板定义的内容不一致,需要将外接的带锁扣端子转杜邦线调整为VIN、GND、SCL和SDA;舵机直插D12 PWM接口,舵机黄色信号线靠近''D12''丝印一端

 1.3 具体接线图

L9110

请注意:如果风扇使用外部电源,务必将外部电源的负极(-)连接到零知标准板的 GND,否则控制信号无法形成回路

1.4 接线实物图

L9110

二、安装与使用部分

2.1 开源平台-输入"PAJ7620U2"并搜索-代码下载自动打开

L9110

2.2 连接-验证-上传

L9110

2.3 调试-串口监视器

L9110

三、代码讲解部分

        本项目的代码结构清晰,采用了模块化设计,代码从初始化、手势处理逻辑和硬件控制部分展开

3.1 软件I2C配置

 

//1. I2C写寄存器
uint8_t RevEng_PAJ7620::writeRegister(uint8_t i2cAddress, uint8_t dataByte) {
  wireHandle- >beginTransmission(PAJ7620_I2C_BUS_ADDR);  // 0x73
  wireHandle- >write(i2cAddress);   // 寄存器地址
  wireHandle- >write(dataByte);     // 数据
  return wireHandle- >endTransmission();
}

//2. I2C读寄存器
uint8_t RevEng_PAJ7620::readRegister(uint8_t i2cAddress, uint8_t byteCount, uint8_t data[]) {
  wireHandle- >beginTransmission(PAJ7620_I2C_BUS_ADDR);
  wireHandle- >write(i2cAddress);
  uint8_t result = wireHandle- >endTransmission();
  if (result) return result;  // 通信错误
  
  wireHandle- >requestFrom((int)PAJ7620_I2C_BUS_ADDR, (int)byteCount);
  while (wireHandle- >available()) {
    *data = wireHandle- >read();
    data++;
  }
  return 0;
}

 

        软件I2C通过GPIO模拟I2C时序,虽然速度略慢,但稳定性更高

3.2 平滑移动算法

 

// 平滑移动舵机(防止舵机快速转动时抖动或损坏)
void moveServoSmoothly() {
  int step = 2;  // 默认每次移动2度

  // 根据目标位置调整移动步长
  // 如果是移动到0度或180度(左右手势),使用较大步长实现快速响应
  if (targetServoPos == 0 || targetServoPos == 180) {
    step = 5;  // 大步长,快速移动
  }

  // 根据当前位置和目标位置的关系,逐步移动
  if (currentServoPos < targetServoPos) {
    // 当前位置小于目标位置,向右转
    currentServoPos = min(currentServoPos + step, targetServoPos);
  } 
  else if (currentServoPos > targetServoPos) {
    // 当前位置大于目标位置,向左转
    currentServoPos = max(currentServoPos - step, targetServoPos);
  }

  myServo.write(currentServoPos);  // 写入新位置
  delay(15);  // 给舵机一点时间响应
}

 

        一种非阻塞式的控制思路,利用 loop() 的快速刷新特性实现了类似PID控制的缓启缓停效果

3.3 风扇控制算法

 

void controlFan(int speed, int direction) {
  // 限制速度在有效范围 [0, 255]
  speed = constrain(speed, 0, FAN_MAX_SPEED);
  
  if (direction == 1) {
    // 反转: IA=0, IB=PWM
    setPWM(FAN_IA_PIN, 0);
    setPWM(FAN_IB_PIN, speed);
    fanDirection = 1;
  } 
  else if (direction == -1) {
    // 正转: IA=PWM, IB=0
    setPWM(FAN_IA_PIN, speed);
    setPWM(FAN_IB_PIN, 0);
    fanDirection = -1;
  } 
  else {
    // 停止: IA=0, IB=0
    setPWM(FAN_IA_PIN, 0);
    setPWM(FAN_IB_PIN, 0);
    fanDirection = 0;
  }
  
  fanSpeed = speed;
}

 

H桥驱动原理

L9110内部包含一个H桥电路,通过控制4个开关管实现电机正反转

IA=HIGH, IB=LOW  →  正转        IA=LOW,  IB=HIGH →  反转
IA=LOW,  IB=LOW  →  停止        IA=HIGH, IB=HIGH →  刹车(不常用)

3.4 PWM定时器手动配置

定时器工作原理

PWM频率 = 时钟频率 / (预分频系数 × 重装载值)
         = 72MHz / (1 × 65535) ≈ 1098Hz

占空比 = 比较值 / 重装载值 × 100%

 

// ============ PWM初始化 ============
void initPWMTimer() {
  Serial.println("[PWM] 初始化定时器...");
  
  // 配置引脚为PWM模式
  pinMode(FAN_IA_PIN, PWM);  // 引脚9 (PB7)
  pinMode(FAN_IB_PIN, PWM);  // 引脚5 (PB6)
  
  // 暂停定时器进行配置
  Timer4.pause();
  
  // 设置PWM参数
  Timer4.setPrescaleFactor(1);     // 预分频=1(不分频)
  Timer4.setOverflow(65535);       // ARR=65535(16位最大)
  
  // 初始化比较值(占空比0%)
  Timer4.setCompare(TIMER_CH1, 0);  // CCR1=0 (引脚5)
  Timer4.setCompare(TIMER_CH2, 0);  // CCR2=0 (引脚9)
  
  // 刷新寄存器并启动定时器
  Timer4.refresh();
  Timer4.resume();
  
  Serial.println("[PWM] 定时器初始化完成");
}

// ============ PWM占空比设置 ============
void setPWM(int pin, uint8_t dutyCycle) {
  // 将0-255映射到0-65535
  uint16_t compareValue = (uint32_t)dutyCycle * 65535 / 255;
  
  if (pin == FAN_IA_PIN) {
    Timer4.setCompare(TIMER_CH2, compareValue);  // 引脚9
  } else if (pin == FAN_IB_PIN) {
    Timer4.setCompare(TIMER_CH1, compareValue);  // 引脚5
  }
}

 

 参数说明

参数 含义 取值范围 本项目设置
预分频系数 时钟分频倍数 1-65536 1(不分频)
重装载值(ARR) 计数器最大值 1-65535 65535(最大分辨率)
比较值(CCR) 高电平持续计数 0-ARR 0-65535
占空比 CCR/ARR 0%-100% 用户输入0-255映射

3.5 完整代码

 

/**************************************************************************************
 * 文件: /Gesture_Control_Servo_Fan/Gesture_Control_Servo_Fan.ino
 * 作者:零知实验室(深圳市在芯间科技有限公司)
 * -^^- 零知实验室,让电子制作变得更简单! -^^-
 * 时间: 2025-12-30
 * 说明:零知标准板(STM32F103RBT6) + PAJ7620U2 + L9110 手势控制系统
 * 功能:手势控制舵机(12号引脚)和风扇(5,9号引脚)
 *       向上-风扇正转,向下-舵机90°,向左-舵机0°,向右-舵机180°
 *       顺时针-风扇正转,逆时针-风扇反转,挥手-风扇停止
 *       向前-风扇加速,向后-风扇减速
 ***************************************************************************************/

#include < Wire.h >
#include < Servo.h >
#include "RevEng_PAJ7620.h"

// 对象创建
RevEng_PAJ7620 sensor;
Servo myServo;

// 引脚定义
const int SERVO_PIN = 12;

// 风扇引脚 - PB6(引脚5)和PB7(引脚9)对应Timer4
const int FAN_IB_PIN = 5;     // PB6 - TIM4_CH1
const int FAN_IA_PIN = 9;     // PB7 - TIM4_CH2

// 系统参数
#define FAN_MIN_SPEED 80      // 风扇最低启动速度
#define FAN_MAX_SPEED 255      // 风扇最大速度
#define SPEED_STEP 175          // 每次调速的步长

// 状态变量
int currentServoPos = 90;      // 舵机当前位置(角度)
int targetServoPos = 90;       // 舵机目标位置(角度)
int fanSpeed = 0;              // 当前风扇速度(0-255)
int fanDirection = 0;          // 风扇方向:0=停止,1=正转,-1=反转

// 手势检测冷却时间,防止重复触发
unsigned long lastGestureTime = 0;
const unsigned long GESTURE_COOLDOWN = 500;  // 毫秒

bool systemReady = false;      // 系统是否就绪

// 系统状态标志
// ==================== 初始化函数 ====================
void setup() {
  // 第1步:初始化串口通信
  initSerial();
  
  // 第2步:初始化I2C总线(PAJ7620传感器需要)
  initI2C();
  
  // 第3步:初始化舵机
  initServo();
  
  // 第4步:初始化风扇控制引脚
  initFan();
  
  // 第5步:初始化手势传感器
  initGestureSensor();
  
  // 第6步:显示功能说明
  printFunctionMenu();
  
  // 第7步:系统就绪提示
  systemStartupComplete();
  
  systemReady = true;  // 标记系统已就绪
}

// ==================== 主循环函数 ====================
void loop() {
  unsigned long currentTime = millis();

  // 检测手势(带冷却时间,避免同一个手势重复触发)
  if (currentTime - lastGestureTime > GESTURE_COOLDOWN) {
    Gesture gesture = sensor.readGesture();  // 读取当前手势

    // 如果检测到有效手势,则处理
    if (gesture != GES_NONE) {
      lastGestureTime = currentTime;  // 更新最后手势时间
      handleGesture(gesture);         // 调用手势处理函数
    }
  }

  // 平滑移动舵机到目标位置(每次循环移动一小步)
  if (currentServoPos != targetServoPos) {
    moveServoSmoothly();
  }

  delay(50);  // 主循环延迟,不要太短以免CPU负担过重
}

// ==================== PWM相关函数 ====================

// 设置PWM占空比
void setPWM(int pin, uint8_t dutyCycle) {
  // 计算比较值:dutyCycle / 255 * overflow
  uint16_t compareValue = (uint32_t)dutyCycle * 65535 / 255;
  
  if (pin == FAN_IA_PIN) {
    // 引脚9 (PB7) 使用 Timer4 Channel2
    Timer4.setCompare(TIMER_CH2, compareValue);
  } 
  else if (pin == FAN_IB_PIN) {
    // 引脚5 (PB6) 使用 Timer4 Channel1
    Timer4.setCompare(TIMER_CH1, compareValue);
  }
}

// 初始化PWM定时器
void initPWMTimer() {
  Serial.println("[PWM] 初始化定时器...");
  
  // 配置引脚为PWM模式
  pinMode(FAN_IA_PIN, PWM);  // 引脚9 (PB7)
  pinMode(FAN_IB_PIN, PWM);  // 引脚5 (PB6)
  
  // 暂停定时器4进行配置
  Timer4.pause();
  
  // 设置PWM参数
  // 72MHz / 1 / 65535 ≈ 1098Hz
  Timer4.setPrescaleFactor(1);     // 不分频
  Timer4.setOverflow(65535);       // 16位最大分辨率
  
  // 初始化占空比为0(风扇停止)
  Timer4.setCompare(TIMER_CH1, 0);  // 引脚5 (FAN_IB_PIN)
  Timer4.setCompare(TIMER_CH2, 0);  // 引脚9 (FAN_IA_PIN)
  
  // 刷新并启动定时器
  Timer4.refresh();
  Timer4.resume();
  
  Serial.println("[PWM] 定时器初始化完成 (引脚5=PB6/CH1, 引脚9=PB7/CH2)");
}

// ==================== 初始化函数详细实现 ====================

// 初始化串口通信
void initSerial() {
  Serial.begin(115200);
  delay(300);  // 等待串口稳定
  
  Serial.println("n╔═══════════════════╗");
  Serial.println("║    零知实验室 - 手势控制系统 V2.0    ║");
  Serial.println("╚═══════════════════╝n");
  Serial.println("【系统初始化开始】n");
}

// 初始化I2C总线
void initI2C() {
  Serial.print("[1/5] I2C总线初始化...");
  Wire.begin();
  delay(100);
  Serial.println(" ✓");
}

// 初始化舵机
void initServo() {
  Serial.print("[2/5] 舵机初始化...");
  
  myServo.attach(SERVO_PIN);
  myServo.write(currentServoPos);  // 设置初始位置90度
  delay(500);  // 等待舵机转到初始位置
  
  Serial.print(" ✓ (初始位置: ");
  Serial.print(currentServoPos);
  Serial.println("°)");
}

// 初始化风扇控制引脚
void initFan() {
  Serial.print("[3/5] 风扇模块初始化...");
  
  // 先初始化PWM定时器
  initPWMTimer();

  pinMode(LED_BUILTIN, OUTPUT);
  
  // 确保风扇初始状态为停止
  controlFan(0, 0);
  
  delay(200);
  Serial.println(" ✓ (状态: 停止)");
}

// 初始化PAJ7620手势传感器
void initGestureSensor() {
  Serial.println("[4/5] PAJ7620手势传感器初始化...");
  
  bool sensorInitialized = false;
  
  // 尝试5次初始化
  for (int attempt = 1; attempt <= 5; attempt++) {
    Serial.print("      尝试 ");
    Serial.print(attempt);
    Serial.print("/5...");
    
    if (sensor.begin()) {
      sensorInitialized = true;
      Serial.println(" ✓ 成功!");
      break;
    }
    
    Serial.println(" ✗ 失败");
    delay(500);
  }

  // 如果初始化失败,进入错误处理
  if (!sensorInitialized) {
    handleSensorInitError();
  }
}

// 传感器初始化失败处理
void handleSensorInitError() {
  Serial.println("n╔════════════════════╗");
  Serial.println("║         ❌ PAJ7620初始化失败!         ║");
  Serial.println("╚════════════════════╝");
  Serial.println("n【故障排查清单】");
  Serial.println("  □ 1. 传感器VCC是否接3.3V(不能接5V!)");
  Serial.println("  □ 2. GND是否正确接地");
  Serial.println("  □ 3. SDA和SCL引脚是否正确连接");
  Serial.println("  □ 4. 杜邦线接触是否良好");
  Serial.println("  □ 5. 传感器与开发板距离不要太远");
  Serial.println("  □ 6. 检查传感器是否损坏(闻是否有烧焦味)");
  Serial.println("n系统已停止运行,请修复后重新上电。n");
  
  // LED快速闪烁表示错误状态
  while (1) {
    digitalWrite(LED_BUILTIN, HIGH);
    delay(50);
    digitalWrite(LED_BUILTIN, LOW);
    delay(50);
  }
}

// 显示功能菜单
void printFunctionMenu() {
  Serial.println("[5/5] 系统配置加载... ✓n");
  
  Serial.println("n========================================");
  Serial.println("           手势功能说明");
  Serial.println("========================================");
  Serial.println("  向上   ↑  : 风扇正转启动");
  Serial.println("  向下   ↓  : 系统复位(舵机90°)");
  Serial.println("  向左   ←  : 舵机转到0°");
  Serial.println("  向右   →  : 舵机转到180°");
  Serial.println("  顺时针 ↻  : 风扇正转");
  Serial.println("  逆时针 ↺  : 风扇反转");
  Serial.println("  挥手   ✋ : 风扇停止");
  Serial.println("  向前   ⇨  : 风扇加速");
  Serial.println("  向后   ⇦  : 风扇减速");
  Serial.println("========================================n");
}

// 系统启动完成提示
void systemStartupComplete() {
  Serial.println("【系统初始化完成】n");
  
  // LED闪烁3次表示系统就绪
  for (int i = 0; i < 3; i++) {
    digitalWrite(LED_BUILTIN, HIGH);
    delay(150);
    digitalWrite(LED_BUILTIN, LOW);
    delay(150);
  }
  
  Serial.println("✓ 系统就绪,等待手势输入...n");
  Serial.println("═══════════════════════════════════════════n");
}

// ==================== 风扇控制函数 ====================
// 控制风扇的转速和方向
// speed: 速度值(0-255)
// direction: 方向(1=反转,-1=正转,0=停止)
void controlFan(int speed, int direction) {
  // 限制速度在有效范围内
  speed = constrain(speed, 0, FAN_MAX_SPEED);
  
  if (direction == 1) {
    // 反转:IA(引脚9)=0, IB(引脚5)=PWM
    setPWM(FAN_IA_PIN, 0);
    setPWM(FAN_IB_PIN, speed);
    fanDirection = 1;
  } 
  else if (direction == -1) {
    // 正转:IA(引脚9)=PWM, IB(引脚5)=0
    setPWM(FAN_IA_PIN, speed);
    setPWM(FAN_IB_PIN, 0);
    fanDirection = -1;
  } 
  else {
    // 停止:两个引脚都输出0
    setPWM(FAN_IA_PIN, 0);
    setPWM(FAN_IB_PIN, 0);
    fanDirection = 0;
  }
  
  fanSpeed = speed;
}

// 停止风扇
void stopFan() {
  controlFan(0, 0);
  Serial.println("→ 风扇: 已停止");
}

// 风扇正转
void fanForward() {
  // 如果当前是停止状态,使用最小速度启动
  if (fanDirection == 0) {
    fanSpeed = FAN_MIN_SPEED;
  }
  controlFan(fanSpeed, -1);
  Serial.print("→ 风扇: 正转 | 速度: ");
  Serial.println(fanSpeed);
}

// 风扇反转
void fanReverse() {
  // 如果当前是停止状态,使用最小速度启动
  if (fanDirection == 0) {
    fanSpeed = FAN_MIN_SPEED;
  }
  controlFan(fanSpeed, 1);
  Serial.print("→ 风扇: 反转 | 速度: ");
  Serial.println(fanSpeed);
}

// 风扇加速
void fanSpeedUp() {
  // 只有在风扇运行时才能加速
  if (fanDirection != 0) {
    // 增加速度,但不超过最大值
    fanSpeed = constrain(fanSpeed + SPEED_STEP, FAN_MIN_SPEED, FAN_MAX_SPEED);
    controlFan(fanSpeed, fanDirection);  // 应用新速度
    Serial.print("→ 风扇: 加速至 ");
    Serial.println(fanSpeed);
  } else {
    Serial.println("⚠ 提示: 请先启动风扇(向上或顺时针手势)");
  }
}

// 风扇减速
void fanSpeedDown() {
  // 只有在风扇运行时才能减速
  if (fanDirection != 0) {
    // 降低速度,但不低于最小值
    fanSpeed = constrain(fanSpeed - SPEED_STEP, FAN_MIN_SPEED, FAN_MAX_SPEED);
    controlFan(fanSpeed, fanDirection);  // 应用新速度
    Serial.print("→ 风扇: 减速至 ");
    Serial.println(fanSpeed);
  } else {
    Serial.println("⚠ 提示: 请先启动风扇(向上或顺时针手势)");
  }
}

// ==================== 舵机控制函数 ====================
// 平滑移动舵机(防止舵机快速转动时抖动或损坏)
void moveServoSmoothly() {
  int step = 2;  // 默认每次移动2度

  // 根据目标位置调整移动步长
  // 如果是移动到0度或180度(左右手势),使用较大步长实现快速响应
  if (targetServoPos == 0 || targetServoPos == 180) {
    step = 5;  // 大步长,快速移动
  }

  // 根据当前位置和目标位置的关系,逐步移动
  if (currentServoPos < targetServoPos) {
    // 当前位置小于目标位置,向右转
    currentServoPos = min(currentServoPos + step, targetServoPos);
  } 
  else if (currentServoPos > targetServoPos) {
    // 当前位置大于目标位置,向左转
    currentServoPos = max(currentServoPos - step, targetServoPos);
  }

  myServo.write(currentServoPos);  // 写入新位置
  delay(15);  // 给舵机一点时间响应
}

// ==================== 手势处理函数 ====================
void handleGesture(Gesture gesture) {
  Serial.print("✋ 检测到手势: ");

  // 根据不同的手势类型执行相应动作
  switch (gesture) {
    case GES_UP:
      // 向上手势:启动风扇正转
      Serial.println("向上 ↑");
      fanForward();
      break;

    case GES_DOWN:
      // 向下手势:系统复位(舵机回中间,风扇停止)
      Serial.println("向下 ↓");
      Serial.println("→ 执行系统复位");
      targetServoPos = 90;  // 舵机回到90度中间位置
      // stopFan();            // 风扇停止
      break;

    case GES_LEFT:
      // 向左手势:舵机转到0度(最左侧)
      Serial.println("向左 ←");
      targetServoPos = 0;
      Serial.println("→ 舵机: 转向0°");
      break;

    case GES_RIGHT:
      // 向右手势:舵机转到180度(最右侧)
      Serial.println("向右 →");
      targetServoPos = 180;
      Serial.println("→ 舵机: 转向180°");
      break;

    case GES_CLOCKWISE:
      // 顺时针旋转手势:风扇正转
      Serial.println("顺时针 ↻");
      fanForward();
      break;

    case GES_ANTICLOCKWISE:
      // 逆时针旋转手势:风扇反转
      Serial.println("逆时针 ↺");
      fanReverse();
      break;

    case GES_WAVE:
      // 挥手手势:停止风扇
      Serial.println("挥手 ✋");
      stopFan();
      break;

    case GES_FORWARD:
      // 向前推手势:风扇加速
      Serial.println("向前 ⇨");
      fanSpeedUp();
      break;

    case GES_BACKWARD:
      // 向后拉手势:风扇减速
      Serial.println("向后 ⇦");
      fanSpeedDown();
      break;

    default:
      // 未识别的手势
      Serial.println("未识别的手势");
      return;
  }

  // 显示当前系统状态
  displayStatus();
  Serial.println("---");
}

// ==================== 状态显示函数 ====================
void displayStatus() {
  Serial.print("


审核编辑 黄宇

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分