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

[活动] [ESP8266/ESP32]基于Firebeetle 2 ESP32-C5的五轴机械臂控制系...

[复制链接]
## 一、回顾与目标

在第一篇教程中,我们完成了:
- ✅ 硬件连接与接线
-Arduino环境配置
- ✅ 基础舵机控制代码
- ✅ NRF24L01无线接收

本教程(第二部分)将实现更高级的功能:
1. **发射端遥控器代码**: 完整的控制端程序
2. **平滑运动算法**: 插值实现舵机平滑过渡
3. **动作序列录制与回放**: 记忆机械臂动作
4. **限位保护与速度控制**: 增强系统安全性
5. **优化技巧**: 性能提升与调试方法

## 二、发射端(遥控器)完整代码

### (一) 硬件需求

**发射端硬件清单**:
| 序号 | 名称 | 数量 | 用途 |
|------|------|------|------|
| 1 | Firebeetle 2 ESP32-C5 | 1 | 发射端主控 |
| 2 | NRF24L01无线模块 | 1 | 2.4GHz发射 |
| 3 | 双轴摇杆模块 | 2 | 控制4个舵机 |
| 4 | 扭子开关 | 1 | 控制夹爪 |
| 5 | 按钮开关 | 1 | 功能切换(可选) |

### (二) 发射端接线

| 模块/传感器 | ESP32-C5引脚 | 说明 |
|------------|-------------|------|
| **NRF24L01** | | |
| VCC | 3.3V | 电源 |
| GND | GND | 地线 |
| CE | GPIO9 | 使能 |
| CSN | GPIO10 | 片选 |
| SCK | GPIO6 | 时钟 |
| MOSI | GPIO7 | 数据输出 |
| MISO | GPIO2 | 数据输入 |
| **摇杆1(左)** | | |
| VRx | GPIO3 (ADC) | X轴(控制舵机1) |
| VRy | GPIO4 (ADC) | Y轴(控制舵机2) |
| VCC | 3.3V | 电源 |
| GND | GND | 地线 |
| **摇杆2(右)** | | |
| VRx | GPIO5 (ADC) | X轴(控制舵机3) |
| VRy | GPIO11 (ADC) | Y轴(控制舵机4) |
| VCC | 3.3V | 电源 |
| GND | GND | 地线 |
| **扭子开关** | | |
| 信号端 | GPIO15 | 控制舵机5(夹爪) |
| 公共端 | GND | 地线 |

### (三) 发射端完整代码

```cpp
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>

// ===== 引脚定义 =====
#define CE_PIN   9
#define CSN_PIN  10

// 摇杆ADC引脚
#define JOYSTICK1_X  3   // 左摇杆X(底座)
#define JOYSTICK1_Y  4   // 左摇杆Y(大臂)
#define JOYSTICK2_X  5   // 右摇杆X(小臂)
#define JOYSTICK2_Y  11  // 右摇杆Y(手腕)

// 开关引脚
#define GRIPPER_SWITCH 15  // 夹爪开关

// ===== 全局对象 =====
const uint64_t pipeOut = 0x123456789;  // 与接收端一致
RF24 radio(CE_PIN, CSN_PIN);

const int deadzone = 5;  // 摇杆死区
const int maxRetries = 3;  // 最大重试次数
const int retryDelay = 10;  // 重试延时(ms)

// ===== 数据结构 =====
struct Signal {
  int ele;   // 底座旋转(-512~512)
  int ail;   // 大臂抬升(-512~512)
  int thr;   // 小臂伸缩(-512~512)
  int rud;   // 手腕旋转(-512~512)
  byte aux1; // 夹爪开合(-512/512)
  byte aux2; // 预留
};
Signal data;

// ===== 辅助函数 =====
void ResetData() {
  data.ele = 0;
  data.ail = 0;
  data.thr = 0;
  data.rud = 0;
  data.aux1 = -512;  // 夹爪默认打开
  data.aux2 = 0;
}

// 死区处理函数
int applyDeadzone(int value, int deadzone, int center) {
  if (abs(value - center) <= deadzone) {
    return center;
  }
  return value;
}

// 带重试的发送函数
bool sendData(const void* data, size_t dataSize) {
  for (int retryCount = 0; retryCount < maxRetries; retryCount++) {
    if (radio.write(data, dataSize)) {
      if (retryCount > 0) {
        Serial.print("发送成功(重试次数:");
        Serial.print(retryCount);
        Serial.println(")");
      }
      return true;
    }
    delay(retryDelay);
  }
  Serial.println("发送失败!");
  return false;
}

// ===== 初始化 =====
void setup() {
  Serial.begin(115200);
  Serial.println("机械臂发射端初始化...");

  // NRF24L01配置
  if (!radio.begin()) {
    Serial.println("NRF24L01初始化失败!");
    while (1);
  }
  
  radio.openWritingPipe(pipeOut);
  radio.stopListening();  // 发射模式
  radio.setDataRate(RF24_250KBPS);  // 低速率高可靠
  radio.setPALevel(RF24_PA_HIGH);   // 高功率
  radio.setRetries(5, 10);          // 硬件重试
  
  Serial.println("NRF24L01初始化成功!");

  // 初始化数据
  ResetData();

  // 配置ADC分辨率(10位:0-1023)
  analogReadResolution(10);

  // 配置开关引脚
  pinMode(GRIPPER_SWITCH, INPUT_PULLUP);

  Serial.println("系统就绪!");
}

// ===== 主循环 =====
void loop() {
  // 1. 读取摇杆模拟值并映射到-512~512
  int raw_ele = map(analogRead(JOYSTICK1_X), 0, 1023, -512, 512);
  int raw_ail = map(analogRead(JOYSTICK1_Y), 0, 1023, -512, 512);
  int raw_thr = map(analogRead(JOYSTICK2_X), 0, 1023, -512, 512);
  int raw_rud = map(analogRead(JOYSTICK2_Y), 0, 1023, -512, 512);

  // 2. 应用死区处理
  data.ele = applyDeadzone(raw_ele, deadzone, 0);
  data.ail = applyDeadzone(raw_ail, deadzone, 0);
  data.thr = applyDeadzone(raw_thr, deadzone, 0);
  data.rud = applyDeadzone(raw_rud, deadzone, 0);

  // 3. 读取夹爪开关(LOW=闭合, HIGH=打开)
  data.aux1 = digitalRead(GRIPPER_SWITCH) ? -512 : 512;

  // 4. 发送数据
  sendData(&data, sizeof(Signal));

  // 5. 调试输出
  Serial.print("发送数据 -> ");
  Serial.print("底座:"); Serial.print(data.ele);
  Serial.print(" 大臂:"); Serial.print(data.ail);
  Serial.print(" 小臂:"); Serial.print(data.thr);
  Serial.print(" 手腕:"); Serial.print(data.rud);
  Serial.print(" 夹爪:"); Serial.println(data.aux1);

  delay(20);  // 50Hz发送频率
}
```

### (四) 代码说明

**关键设计要点**:
1. **摇杆校准**: `map(analogRead(), 0, 1023, -512, 512)`根据实际情况调整范围
2. **死区过滤**: 摇杆中位附近5个单位的抖动被过滤
3. **开关映射**: `digitalRead()`结果直接映射为-512/512
4. **重试机制**: 3次重试确保数据可靠传输

## 三、平滑运动算法

### (一) 问题分析

基础代码中舵机直接跳转到目标角度,导致:
- ❌ 动作生硬,不自然
- ❌ 瞬时电流大,可能导致电源不稳
- ❌ 机械冲击,损伤舵机齿轮

**解决方案**: 使用线性插值,让舵机以固定步长平滑移动。

### (二) 平滑运动代码

**接收端代码**中添加以下改进:

```cpp
// ===== 全局变量(添加到代码顶部) =====
int currentAngle[5] = {90, 90, 90, 90, 90};  // 当前角度
int targetAngle[5] = {90, 90, 90, 90, 90};   // 目标角度
const int smoothStep = 2;  // 平滑步长(角度/次)

// ===== 修改controlServos函数 =====
void controlServos() {
  // 计算目标角度
  targetAngle[0] = constrain(mapToServoAngle(receivedData.ele), 0, 180);
  targetAngle[1] = constrain(mapToServoAngle(receivedData.ail), 0, 180);
  targetAngle[2] = constrain(mapToServoAngle(receivedData.thr), 0, 180);
  targetAngle[3] = constrain(mapToServoAngle(receivedData.rud), 0, 180);
  targetAngle[4] = constrain(mapAuxToServo(receivedData.aux1), 0, 180);
  
  // 平滑插值移动
  for (int i = 0; i < 5; i++) {
    if (currentAngle < targetAngle) {
      currentAngle += min(smoothStep, targetAngle - currentAngle);
    } else if (currentAngle > targetAngle) {
      currentAngle -= min(smoothStep, currentAngle - targetAngle);
    }
  }
  
  // 写入舵机
  servo1.write(currentAngle[0]);
  servo2.write(currentAngle[1]);
  servo3.write(currentAngle[2]);
  servo4.write(currentAngle[3]);
  servo5.write(currentAngle[4]);
  
  // 调试输出
  Serial.print("当前角度 -> ");
  for (int i = 0; i < 5; i++) {
    Serial.print(i+1); Serial.print(":"); Serial.print(currentAngle);
    Serial.print(" ");
  }
  Serial.println();
}
```

**参数调整**:
- `smoothStep = 1`: 非常缓慢,适合精细操作
- `smoothStep = 2`: 平衡速度与平滑度(推荐)
- `smoothStep = 5`: 快速响应,牺牲平滑度

## 四、动作序列录制与回放

### (一) 功能设计

实现类似"示教"的功能:
1. **录制模式**: 记录机械臂的位置序列
2. **回放模式**: 自动重复录制的动作
3. **存储**: 保存多组动作到EEPROM

### (二) 录制回放代码

**接收端代码**中添加:

```cpp
#include <Preferences.h>  // ESP32持久化存储

// ===== 全局变量 =====
Preferences preferences;
const int MAX_STEPS = 50;  // 最多录制50步
int recordedAngles[MAX_STEPS][5];  // 录制的角度数据
int recordedSteps = 0;  // 已录制步数
bool recordMode = false;  // 录制模式标志
bool playbackMode = false;  // 回放模式标志

// 按钮引脚(可选:用于切换模式)
#define RECORD_BUTTON 14
#define PLAYBACK_BUTTON 22

// ===== 初始化中添加 =====
void setup() {
  // ...现有代码...
  
  pinMode(RECORD_BUTTON, INPUT_PULLUP);
  pinMode(PLAYBACK_BUTTON, INPUT_PULLUP);
  
  preferences.begin("robot", false);  // 打开命名空间
}

// ===== 录制函数 =====
void recordStep() {
  if (recordedSteps < MAX_STEPS) {
    for (int i = 0; i < 5; i++) {
      recordedAngles[recordedSteps] = currentAngle;
    }
    recordedSteps++;
    Serial.print("录制步骤 ");
    Serial.print(recordedSteps);
    Serial.println("/50");
  } else {
    Serial.println("录制已满!");
    recordMode = false;
  }
}

// ===== 回放函数 =====
void playbackSequence() {
  Serial.println("开始回放...");
  
  for (int step = 0; step < recordedSteps; step++) {
    Serial.print("执行步骤 ");
    Serial.print(step + 1);
    Serial.print("/");
    Serial.println(recordedSteps);
   
    // 设置目标角度
    for (int i = 0; i < 5; i++) {
      targetAngle = recordedAngles[step];
    }
   
    // 平滑移动到目标位置
    bool arrived = false;
    while (!arrived) {
      arrived = true;
      
      for (int i = 0; i < 5; i++) {
        if (currentAngle != targetAngle) {
          arrived = false;
         
          if (currentAngle < targetAngle) {
            currentAngle += min(smoothStep, targetAngle - currentAngle);
          } else {
            currentAngle -= min(smoothStep, currentAngle - targetAngle);
          }
        }
      }
      
      // 写入舵机
      servo1.write(currentAngle[0]);
      servo2.write(currentAngle[1]);
      servo3.write(currentAngle[2]);
      servo4.write(currentAngle[3]);
      servo5.write(currentAngle[4]);
      
      delay(20);
    }
   
    delay(500);  // 每步之间停留0.5秒
  }
  
  Serial.println("回放完成!");
  playbackMode = false;
}

// ===== 保存到EEPROM =====
void saveSequence(int slot) {
  String key = "seq" + String(slot);
  preferences.putInt(key + "_steps", recordedSteps);
  
  for (int i = 0; i < recordedSteps; i++) {
    String stepKey = key + "_" + String(i);
    preferences.putBytes(stepKey.c_str(), recordedAngles, sizeof(recordedAngles));
  }
  
  Serial.print("序列已保存到槽位 ");
  Serial.println(slot);
}

// ===== 从EEPROM加载 =====
void loadSequence(int slot) {
  String key = "seq" + String(slot);
  recordedSteps = preferences.getInt(key + "_steps", 0);
  
  for (int i = 0; i < recordedSteps; i++) {
    String stepKey = key + "_" + String(i);
    preferences.getBytes(stepKey.c_str(), recordedAngles, sizeof(recordedAngles));
  }
  
  Serial.print("从槽位 ");
  Serial.print(slot);
  Serial.print(" 加载了 ");
  Serial.print(recordedSteps);
  Serial.println(" 步动作");
}

// ===== 主循环中添加 =====
void loop() {
  // 检查录制按钮
  if (digitalRead(RECORD_BUTTON) == LOW) {
    delay(50);  // 消抖
    if (digitalRead(RECORD_BUTTON) == LOW) {
      recordMode = !recordMode;
      if (recordMode) {
        recordedSteps = 0;  // 重置
        Serial.println("开始录制!");
      } else {
        Serial.println("停止录制!");
        saveSequence(1);  // 保存到槽位1
      }
      while (digitalRead(RECORD_BUTTON) == LOW);  // 等待释放
    }
  }
  
  // 检查回放按钮
  if (digitalRead(PLAYBACK_BUTTON) == LOW) {
    delay(50);
    if (digitalRead(PLAYBACK_BUTTON) == LOW) {
      playbackMode = true;
      while (digitalRead(PLAYBACK_BUTTON) == LOW);
    }
  }
  
  // 执行模式
  if (playbackMode) {
    playbackSequence();
  } else if (radio.available()) {
    radio.read(&receivedData, sizeof(Signal));
    lastReceiveTime = millis();
    controlServos();
   
    // 录制模式:每隔一段时间记录一次
    if (recordMode) {
      static unsigned long lastRecordTime = 0;
      if (millis() - lastRecordTime > 1000) {  // 每秒记录一次
        recordStep();
        lastRecordTime = millis();
      }
    }
  }
  
  // ...其余代码...
}
```

### (三) 使用方法

1. **开始录制**: 按下录制按钮,LED提示进入录制模式
2. **操作机械臂**: 通过遥控器控制机械臂,系统每秒自动记录一次位置
3. **停止录制**: 再次按下录制按钮,序列自动保存
4. **回放**: 按下回放按钮,机械臂自动执行录制的动作

## 五、限位保护与速度控制

### (一) 软件限位

为不同舵机设置独立的角度限制:

```cpp
// ===== 舵机限位配置 =====
struct ServoLimits {
  int minAngle;
  int maxAngle;
};

ServoLimits servoLimits[5] = {
  {0, 180},    // 舵机1:底座(全范围)
  {30, 150},   // 舵机2:大臂(避免过度后仰)
  {20, 160},   // 舵机3:小臂(避免碰撞)
  {0, 180},    // 舵机4:手腕(全范围)
  {30, 150}    // 舵机5:夹爪(有效范围)
};

// ===== 限位保护函数 =====
int applyLimits(int angle, int servoIndex) {
  return constrain(angle,
                   servoLimits[servoIndex].minAngle,
                   servoLimits[servoIndex].maxAngle);
}

// ===== 在controlServos中应用 =====
void controlServos() {
  targetAngle[0] = applyLimits(mapToServoAngle(receivedData.ele), 0);
  targetAngle[1] = applyLimits(mapToServoAngle(receivedData.ail), 1);
  targetAngle[2] = applyLimits(mapToServoAngle(receivedData.thr), 2);
  targetAngle[3] = applyLimits(mapToServoAngle(receivedData.rud), 3);
  targetAngle[4] = applyLimits(mapAuxToServo(receivedData.aux1), 4);
  
  // ...其余平滑运动代码...
}
```

### (二) 速度分级控制

通过按钮切换速度档位:

```cpp
// ===== 速度档位 =====
enum SpeedMode {
  SLOW = 1,    // 慢速(精细操作)
  NORMAL = 2,  // 正常速度
  FAST = 5     // 快速(粗略移动)
};

SpeedMode currentSpeed = NORMAL;
#define SPEED_BUTTON 23  // 速度切换按钮

// ===== 初始化中添加 =====
pinMode(SPEED_BUTTON, INPUT_PULLUP);

// ===== 主循环中添加 =====
if (digitalRead(SPEED_BUTTON) == LOW) {
  delay(50);
  if (digitalRead(SPEED_BUTTON) == LOW) {
    // 循环切换速度
    if (currentSpeed == SLOW) currentSpeed = NORMAL;
    else if (currentSpeed == NORMAL) currentSpeed = FAST;
    else currentSpeed = SLOW;
   
    Serial.print("速度切换到:");
    Serial.println(currentSpeed);
   
    while (digitalRead(SPEED_BUTTON) == LOW);
  }
}

// ===== 在平滑运动中使用 =====
int effectiveStep = smoothStep * currentSpeed;
if (currentAngle < targetAngle) {
  currentAngle += min(effectiveStep, targetAngle - currentAngle);
}
```

## 六、系统优化技巧

### (一) 电源优化

**问题**: 5个舵机同时转动时电流可能超过3A,导致电压跌落。

**解决方案**:
1. **大容量电容**: 在电源端并联1000uF-2200uF电解电容
2. **分时驱动**: 避免所有舵机同时大角度转动

```cpp
// 分时驱动示例
void controlServosStaggered() {
  static int activeServo = 0;
  
  // 每次只更新一个舵机
  if (currentAngle[activeServo] != targetAngle[activeServo]) {
    if (currentAngle[activeServo] < targetAngle[activeServo]) {
      currentAngle[activeServo]++;
    } else {
      currentAngle[activeServo]--;
    }
   
    switch(activeServo) {
      case 0: servo1.write(currentAngle[0]); break;
      case 1: servo2.write(currentAngle[1]); break;
      case 2: servo3.write(currentAngle[2]); break;
      case 3: servo4.write(currentAngle[3]); break;
      case 4: servo5.write(currentAngle[4]); break;
    }
  }
  
  activeServo = (activeServo + 1) % 5;  // 轮询下一个
}
```

### (二) 通信优化

**减少数据包大小**:
```cpp
// 使用8位数据代替int(减少50%数据量)
struct CompactSignal {
  byte servo1;  // 0-180直接映射
  byte servo2;
  byte servo3;
  byte servo4;
  byte servo5;
  byte checksum;  // 校验和
};
```

**添加数据校验**:
```cpp
byte calculateChecksum(CompactSignal* sig) {
  return (sig->servo1 + sig->servo2 + sig->servo3 +
          sig->servo4 + sig->servo5) & 0xFF;
}

bool verifyData(CompactSignal* sig) {
  return sig->checksum == calculateChecksum(sig);
}
```

### (三) 调试技巧

**1. 串口可视化**:
```cpp
void printAnglesBar() {
  Serial.println("=== 舵机角度可视化 ===");
  for (int i = 0; i < 5; i++) {
    Serial.print("舵机"); Serial.print(i+1); Serial.print(" [");
   
    int bars = currentAngle / 10;  // 每10度一个字符
    for (int j = 0; j < bars; j++) Serial.print("=");
    for (int j = bars; j < 18; j++) Serial.print(" ");
   
    Serial.print("] "); Serial.print(currentAngle); Serial.println("°");
  }
  Serial.println();
}
```

**2. LED状态指示**:
```cpp
#define STATUS_LED 25

void updateStatusLED() {
  if (millis() - lastReceiveTime < 1000) {
    digitalWrite(STATUS_LED, HIGH);  // 通信正常:常亮
  } else {
    // 通信中断:闪烁
    digitalWrite(STATUS_LED, (millis() / 500) % 2);
  }
}
```

## 七、完整进阶接收端代码

将以上所有功能整合:

```cpp
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <ESP32Servo.h>
#include <Preferences.h>

// ===== 引脚定义 =====
#define CE_PIN   9
#define CSN_PIN  10
#define SERVO1_PIN 12
#define SERVO2_PIN 13
#define SERVO3_PIN 20
#define SERVO4_PIN 21
#define SERVO5_PIN 8
#define RECORD_BUTTON 14
#define PLAYBACK_BUTTON 22
#define SPEED_BUTTON 23
#define STATUS_LED 25

// ===== 全局对象 =====
const uint64_t pipeIn = 0x123456789;
RF24 radio(CE_PIN, CSN_PIN);
Servo servo1, servo2, servo3, servo4, servo5;
Preferences preferences;

// ===== 数据结构 =====
struct Signal {
  int ele, ail, thr, rud;
  byte aux1, aux2;
};
Signal receivedData;

// ===== 状态变量 =====
int currentAngle[5] = {90, 90, 90, 90, 90};
int targetAngle[5] = {90, 90, 90, 90, 90};
const int smoothStep = 2;
unsigned long lastReceiveTime = 0;
const unsigned long timeout = 1000;

// 速度控制
enum SpeedMode { SLOW = 1, NORMAL = 2, FAST = 5 };
SpeedMode currentSpeed = NORMAL;

// 录制回放
const int MAX_STEPS = 50;
int recordedAngles[MAX_STEPS][5];
int recordedSteps = 0;
bool recordMode = false;
bool playbackMode = false;

// 限位配置
struct ServoLimits {
  int minAngle, maxAngle;
};
ServoLimits servoLimits[5] = {
  {0, 180}, {30, 150}, {20, 160}, {0, 180}, {30, 150}
};

// ===== 辅助函数 =====
int mapToServoAngle(int value) {
  return map(value, -512, 512, 0, 180);
}

int mapAuxToServo(byte value) {
  return (value == -512) ? 30 : 150;
}

int applyLimits(int angle, int servoIndex) {
  return constrain(angle,
                   servoLimits[servoIndex].minAngle,
                   servoLimits[servoIndex].maxAngle);
}

void controlServos() {
  // 计算目标角度并应用限位
  targetAngle[0] = applyLimits(mapToServoAngle(receivedData.ele), 0);
  targetAngle[1] = applyLimits(mapToServoAngle(receivedData.ail), 1);
  targetAngle[2] = applyLimits(mapToServoAngle(receivedData.thr), 2);
  targetAngle[3] = applyLimits(mapToServoAngle(receivedData.rud), 3);
  targetAngle[4] = applyLimits(mapAuxToServo(receivedData.aux1), 4);
  
  // 平滑插值(根据速度档位)
  int effectiveStep = smoothStep * currentSpeed;
  for (int i = 0; i < 5; i++) {
    if (currentAngle < targetAngle) {
      currentAngle += min(effectiveStep, targetAngle - currentAngle);
    } else if (currentAngle > targetAngle) {
      currentAngle -= min(effectiveStep, currentAngle - targetAngle);
    }
  }
  
  // 写入舵机
  servo1.write(currentAngle[0]);
  servo2.write(currentAngle[1]);
  servo3.write(currentAngle[2]);
  servo4.write(currentAngle[3]);
  servo5.write(currentAngle[4]);
}

void recordStep() {
  if (recordedSteps < MAX_STEPS) {
    for (int i = 0; i < 5; i++) {
      recordedAngles[recordedSteps] = currentAngle;
    }
    recordedSteps++;
    Serial.print("录制步骤 "); Serial.print(recordedSteps); Serial.println("/50");
  }
}

void playbackSequence() {
  Serial.println("开始回放...");
  for (int step = 0; step < recordedSteps; step++) {
    for (int i = 0; i < 5; i++) {
      targetAngle = recordedAngles[step];
    }
   
    bool arrived = false;
    while (!arrived) {
      arrived = true;
      for (int i = 0; i < 5; i++) {
        if (currentAngle != targetAngle) {
          arrived = false;
          if (currentAngle < targetAngle) currentAngle++;
          else currentAngle--;
        }
      }
      servo1.write(currentAngle[0]);
      servo2.write(currentAngle[1]);
      servo3.write(currentAngle[2]);
      servo4.write(currentAngle[3]);
      servo5.write(currentAngle[4]);
      delay(20);
    }
    delay(500);
  }
  Serial.println("回放完成!");
  playbackMode = false;
}

void updateStatusLED() {
  if (millis() - lastReceiveTime < 1000) {
    digitalWrite(STATUS_LED, HIGH);
  } else {
    digitalWrite(STATUS_LED, (millis() / 500) % 2);
  }
}

// ===== 初始化 =====
void setup() {
  Serial.begin(115200);
  Serial.println("高级机械臂系统初始化...");

  if (!radio.begin()) {
    Serial.println("NRF24L01初始化失败!");
    while (1);
  }
  radio.openReadingPipe(1, pipeIn);
  radio.startListening();
  radio.setDataRate(RF24_250KBPS);
  radio.setPALevel(RF24_PA_HIGH);
  radio.setAutoAck(true);

  servo1.attach(SERVO1_PIN, 500, 2500);
  servo2.attach(SERVO2_PIN, 500, 2500);
  servo3.attach(SERVO3_PIN, 500, 2500);
  servo4.attach(SERVO4_PIN, 500, 2500);
  servo5.attach(SERVO5_PIN, 500, 2500);
  
  for (int i = 0; i < 5; i++) {
    switch(i) {
      case 0: servo1.write(90); break;
      case 1: servo2.write(90); break;
      case 2: servo3.write(90); break;
      case 3: servo4.write(90); break;
      case 4: servo5.write(90); break;
    }
  }

  pinMode(RECORD_BUTTON, INPUT_PULLUP);
  pinMode(PLAYBACK_BUTTON, INPUT_PULLUP);
  pinMode(SPEED_BUTTON, INPUT_PULLUP);
  pinMode(STATUS_LED, OUTPUT);

  preferences.begin("robot", false);

  Serial.println("系统就绪!");
  delay(1000);
}

// ===== 主循环 =====
void loop() {
  // 按钮检测
  static unsigned long lastButtonCheck = 0;
  if (millis() - lastButtonCheck > 100) {
    // 录制按钮
    if (digitalRead(RECORD_BUTTON) == LOW) {
      delay(50);
      if (digitalRead(RECORD_BUTTON) == LOW) {
        recordMode = !recordMode;
        recordedSteps = recordMode ? 0 : recordedSteps;
        Serial.println(recordMode ? "开始录制!" : "停止录制!");
        while (digitalRead(RECORD_BUTTON) == LOW);
      }
    }
   
    // 回放按钮
    if (digitalRead(PLAYBACK_BUTTON) == LOW) {
      delay(50);
      if (digitalRead(PLAYBACK_BUTTON) == LOW) {
        playbackMode = true;
        while (digitalRead(PLAYBACK_BUTTON) == LOW);
      }
    }
   
    // 速度按钮
    if (digitalRead(SPEED_BUTTON) == LOW) {
      delay(50);
      if (digitalRead(SPEED_BUTTON) == LOW) {
        currentSpeed = (currentSpeed == SLOW) ? NORMAL :
                       (currentSpeed == NORMAL) ? FAST : SLOW;
        Serial.print("速度:"); Serial.println(currentSpeed);
        while (digitalRead(SPEED_BUTTON) == LOW);
      }
    }
   
    lastButtonCheck = millis();
  }

  // 回放模式
  if (playbackMode) {
    playbackSequence();
    return;
  }

  // 接收数据
  if (radio.available()) {
    radio.read(&receivedData, sizeof(Signal));
    lastReceiveTime = millis();
    controlServos();
   
    // 录制模式
    if (recordMode) {
      static unsigned long lastRecordTime = 0;
      if (millis() - lastRecordTime > 1000) {
        recordStep();
        lastRecordTime = millis();
      }
    }
  } else {
    // 超时保护
    if (millis() - lastReceiveTime > timeout) {
      for (int i = 0; i < 5; i++) targetAngle = 90;
      targetAngle[4] = 30;  // 夹爪打开
    }
  }

  updateStatusLED();
  delay(20);
}
```

## 八、实战应用案例

### (一) 物品抓取流程

```cpp
void pickAndPlace() {
  // 1. 初始位置
  moveToPosition(90, 90, 90, 90, 30);  // 夹爪打开
  delay(1000);
  
  // 2. 移动到物品上方
  moveToPosition(45, 60, 120, 90, 30);
  delay(1000);
  
  // 3. 下降
  moveToPosition(45, 80, 140, 90, 30);
  delay(1000);
  
  // 4. 夹取
  moveToPosition(45, 80, 140, 90, 150);  // 夹爪闭合
  delay(1000);
  
  // 5. 抬升
  moveToPosition(45, 60, 120, 90, 150);
  delay(1000);
  
  // 6. 旋转到目标位置
  moveToPosition(135, 60, 120, 90, 150);
  delay(1000);
  
  // 7. 释放
  moveToPosition(135, 60, 120, 90, 30);
  delay(1000);
  
  // 8. 复位
  moveToPosition(90, 90, 90, 90, 30);
}

void moveToPosition(int a1, int a2, int a3, int a4, int a5) {
  targetAngle[0] = a1;
  targetAngle[1] = a2;
  targetAngle[2] = a3;
  targetAngle[3] = a4;
  targetAngle[4] = a5;
  
  // 等待到位
  while (currentAngle[0] != targetAngle[0] ||
         currentAngle[1] != targetAngle[1] ||
         currentAngle[2] != targetAngle[2] ||
         currentAngle[3] != targetAngle[3] ||
         currentAngle[4] != targetAngle[4]) {
    controlServos();
    delay(20);
  }
}
```

## 九、视频展示

[机械臂展示视频]()


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

本版积分规则

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

硬件清单

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

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

mail