---
# System prepended metadata

title: 萬向輪底盤系統 - 學習歷程檔案

---

# 萬向輪底盤程式工程筆記

## 前言

在升上高三後，我持續利用課餘時間參與並協助FRC機器人競賽的程式組開發工作。而今年的自動程式，我選擇讓學弟妹撰寫，並提供必要的指導與建議，以培養他們的技術能力。

高三成員團體照:
![HIGH_SCHOOL_TEAM_PHOTO](https://hackmd.io/_uploads/SJE8mLS0ye.jpg)

## 1. 萬象輪底盤技術解析

### 1.1 萬向輪（Swerve）概念介紹

Swerve 是一種先進的底盤運行模式，其核心特點是每個輪子都能獨立控制其速度與方向。若採用四輪驅動的 Swerve 設計，其組成為四個獨立的驅動單元（模組），每個模組均配備：

![REV-21-3005-Swerve-Hero-FINAL__82364](https://hackmd.io/_uploads/rJ4_lDr0yl.png)


- **Drive Motor**: 控制輪子的轉速（前進後退）
- **Turning Motor**: 控制輪子的旋轉角度（方向）
- **編碼器系統**:
  - **Drive Encoder**: 監測輪子的轉速
  - **Turning Encoder**: 監測輪子的角度
  - **CANcoder**: 提供絕對角度測量

透過這種設計，Swerve 底盤可以實現向任意方向移動，同時能夠獨立控制底盤的旋轉角度，提供極高的機動性與靈活度。


## 2. 技術基礎：深入理解 Swerve 模組架構

### 2.1 核心組件解析

#### 2.1.1 驅動系統
- **驅動馬達 (Drive Motor)**：NEO 無刷馬達，負責控制輪子的轉速
- **轉向馬達 (Turning Motor)**：NEO 無刷馬達，負責控制輪子的方向
- **CANcoder**：專用絕對角度編碼器，提供精確的角度測量

![Swerve底盤示意圖](https://hackmd.io/_uploads/HkagE8HCyg.jpg)

#### 2.1.2 控制系統
- **SparkMax 控制器**：用於控制 NEO 無刷馬達
- **PID 控制器**：確保精確角度控制與平滑轉向

### 2.2 程式架構設計

#### 2.2.1 物件導向結構
```java
public class SwerveMod {
    // 馬達宣告
    private final CANSparkMax driving;  // 控制輪子前進後退
    private final CANSparkMax turning;  // 控制輪子轉向
    
    // 編碼器宣告
    private final RelativeEncoder drivingEncoder;  // 測量驅動馬達轉速和距離
    private final RelativeEncoder turningEncoder;  // 測量轉向馬達轉速
    private final CANCoder cancoder;               // 提供轉向絕對角度
    
    // PID控制器宣告，用於精確控制轉向角度
    private final PIDController turningPidController = new PIDController(0.0113, 0.0115, 0);
    
    // 物理參數
    private final double wheelDiameter = 0.1016;  // 輪子直徑（公尺）
    private final double gearRatio = 6.12;        // 齒輪比
    
    // 建構子與方法（詳見後續內容）
}
```

#### 2.2.2 建構子實作
```java
public SwerveMod(int drivingID, int turningID, int cancoderID, Rotation2d offset) {
    // 初始化馬達
    driving = new CANSparkMax(drivingID, MotorType.kBrushless);
    turning = new CANSparkMax(turningID, MotorType.kBrushless);
    
    // 初始化編碼器
    drivingEncoder = driving.getEncoder();
    turningEncoder = turning.getEncoder();
    cancoder = new CANCoder(cancoderID);
    
    // 配置硬體參數
    configCANcoder(offset);
    configSparkMax();
    
    // 設定PID為連續輸入，處理角度週期性(-180° 到 180°)
    turningPidController.enableContinuousInput(-180, 180);
}
```

### 2.3 核心配置方法

#### 2.3.1 CANcoder 配置
```java
private void configCANcoder(Rotation2d offset) {
    CANcoderConfiguration config = new CANcoderConfiguration();
    config.sensorDirection = true;  // 設定感測器方向為逆時針為正向
    config.magnetOffsetDeg = offset.getDegrees();  // 設定磁性偏移角度
    config.sensorTimeBase = SensorTimeBase.PerSecond;
    cancoder.configAllSettings(config);
}
```

#### 2.3.2 SparkMax 配置
```java
private void configSparkMax() {
    // 重置馬達控制器配置
    driving.restoreFactoryDefaults();
    turning.restoreFactoryDefaults();
    
    // 設定驅動馬達編碼器轉換因子（從旋轉數到移動距離）
    drivingEncoder.setPositionConversionFactor(
        wheelDiameter * Math.PI / gearRatio
    );
    drivingEncoder.setVelocityConversionFactor(
        wheelDiameter * Math.PI / (60.0 * gearRatio)
    );
    
    // 設定轉向馬達編碼器轉換因子（從旋轉數到角度）
    turningEncoder.setPositionConversionFactor(360.0 / 12.8);
    
    // 設定馬達控制器參數
    driving.setIdleMode(IdleMode.kBrake);
    turning.setIdleMode(IdleMode.kBrake);
    driving.setSmartCurrentLimit(40);
    turning.setSmartCurrentLimit(20);
    
    // 將設定燒入控制器永久儲存
    driving.burnFlash();
    turning.burnFlash();
}
```



## 3. 系統整合：底盤控制架構

### 3.1 SwerveChassis 類設計

```java
public class SwerveChassis extends SubsystemBase {
    // 四個 Swerve 模組的定義
    private final SwerveMod frontLeft, frontRight, backLeft, backRight;
    
    // 陀螺儀，用於場地中心控制
    private final AHRS gyro;
    
    // 模組間距參數（單位：米）
    private final double L = 0.487;  // 對角線距離
    private final double trackWidth = 0.37;  // 寬度
    private final double wheelBase = 0.315;  // 長度
    
    // 速度限制
    private final double maxSpeed = 4.935;  // 最大平移速度 (m/s)
    private final double maxOmega = 10.13;  // 最大旋轉速度 (rad/s)
    
    public SwerveChassis() {
        // 初始化陀螺儀
        gyro = new AHRS(SPI.Port.kMXP);
        resetGyro();
        
        // 初始化四個 Swerve 模組
        frontLeft = new SwerveMod(1, 2, 9, Rotation2d.fromDegrees(91.23));
        frontRight = new SwerveMod(3, 4, 10, Rotation2d.fromDegrees(10.10));
        backLeft = new SwerveMod(5, 6, 11, Rotation2d.fromDegrees(15.73));
        backRight = new SwerveMod(7, 8, 12, Rotation2d.fromDegrees(69.08));
    }
    
    // 重設陀螺儀方法
    public void resetGyro() {
        gyro.reset();
    }
    
    // 主要驅動方法
    public void drive(Translation2d targetVelocity, double targetOmega, boolean fieldRelative) {
        // 限制最大速度
        targetVelocity = limitVelocity(targetVelocity);
        targetOmega = Math.min(Math.max(targetOmega, -maxOmega), maxOmega);
        
        // 場地相對坐標轉換
        if (fieldRelative) {
            targetVelocity = targetVelocity.rotateBy(
                Rotation2d.fromDegrees(-gyro.getAngle())
            );
        }
        
        // 計算每個模組的速度和角度
        SwerveModuleState[] states = new SwerveModuleState[4];
        
        // 計算各點相對於中心的位置
        Translation2d fl = new Translation2d(wheelBase/2, trackWidth/2);
        Translation2d fr = new Translation2d(wheelBase/2, -trackWidth/2);
        Translation2d bl = new Translation2d(-wheelBase/2, trackWidth/2);
        Translation2d br = new Translation2d(-wheelBase/2, -trackWidth/2);
        
        // 計算各點的速度向量
        Translation2d flVel = targetVelocity.plus(new Translation2d(0, targetOmega).times(fl.getNorm()));
        Translation2d frVel = targetVelocity.plus(new Translation2d(0, targetOmega).times(fr.getNorm()));
        Translation2d blVel = targetVelocity.plus(new Translation2d(0, targetOmega).times(bl.getNorm()));
        Translation2d brVel = targetVelocity.plus(new Translation2d(0, targetOmega).times(br.getNorm()));
        
        // 設置每個模組的目標狀態
        frontLeft.setDesiredState(new SwerveModuleState(flVel.getNorm(), flVel.getAngle()));
        frontRight.setDesiredState(new SwerveModuleState(frVel.getNorm(), frVel.getAngle()));
        backLeft.setDesiredState(new SwerveModuleState(blVel.getNorm(), blVel.getAngle()));
        backRight.setDesiredState(new SwerveModuleState(brVel.getNorm(), brVel.getAngle()));
    }
    
    // 速度限制方法
    private Translation2d limitVelocity(Translation2d velocity) {
        double magnitude = velocity.getNorm();
        if (magnitude > maxSpeed) {
            return velocity.times(maxSpeed / magnitude);
        }
        return velocity;
    }
}
```

### 3.2 速度參數計算方法詳解

#### 3.2.1 最大平移速度計算
最大平移速度(maxSpeed)是根據馬達規格、齒輪比和輪徑計算得出：

1. **將馬達轉速從 RPM 轉為每秒轉數**：
```
馬達每秒轉速 = freespeed / 60
= 5676 / 60
= 94.6 轉/秒
```

2. **考慮減速比，計算輪子每秒轉速**：
```
輪子每秒轉速 = 馬達每秒轉速 / kDriveMotorGearRatio
= 94.6 / 6.12
≈ 15.46 轉/秒
```

3. **計算輪子每秒行駛距離（maxspeed）**：
- 輪子周長：
```
kWheelDiameterMeters * Math.PI
= 0.1016 * 3.14159
≈ 0.3192 米/轉
```

- 最大速度：
```
輪子每秒轉速 * 周長
= 15.46 * 0.3192
≈ 4.935 米/秒
```

#### 3.2.2 最大旋轉速度計算
最大旋轉速度(maxOmega)是根據最大平移速度和轉動半徑計算得出：

1. **計算轉動半徑 L**：
```
L = √(wheelBase² + trackWidth²)
= √(0.315² + 0.37²)
≈ √(0.0992 + 0.1369)
≈ √0.2361
≈ 0.487 米
```

2. **計算最大旋轉速度**：
```
maxOmega = maxSpeed / L
= 4.935 / 0.487
≈ 10.13 弧度/秒
```

### 3.3 RobotContainer 整合

```java
public class RobotContainer {
    // 底盤系統初始化
    private final SwerveChassis chassis = new SwerveChassis();
    
    // 控制器宣告
    private final CommandXboxController controller = new CommandXboxController(0);
    private final CommandJoystick joystick = new CommandJoystick(1);
    
    // 速度參數
    private final double maxSpeed = 4.935; // 米/秒
    private final double maxOmega = 10.13; // 弧度/秒
    
    public RobotContainer() {
        // 底盤預設命令：使用RunCommand持續執行chassis.drive
        chassis.setDefaultCommand(
            new RunCommand(
                () -> chassis.drive(
                    // 從左搖桿獲取平移指令(X、Y軸)
                    new Translation2d(
                        -controller.getLeftY() * maxSpeed,
                        -controller.getLeftX() * maxSpeed
                    ),
                    // 從右搖桿獲取旋轉指令(X軸)
                    -controller.getRightX() * maxOmega,
                    true  // 使用場地視角控制
                ),
                chassis
            )
        );
        
        // 配置按鈕綁定
        configureBindings();
    }
    
    private void configureBindings() {
        // 重設陀螺儀按鈕
        controller.start().onTrue(new InstantCommand(() -> chassis.resetGyro()));
        
        // 其他按鈕綁定...
    }
}
```
## 4. 技術挑戰與解決方案

### 4.1 校正偏移量挑戰

#### 4.1.1 問題分析
在使用 CANcoder 進行絕對角度測量時，我們面臨了各模組間偏移量校正需要反覆調整的問題。這主要由以下因素導致：
- 機械安裝誤差
- 外部環境干擾
- 初始校準偏差
- 使用過程中的累積誤差

#### 4.1.2 解決方案
1. **精確測量初始位置**：
   - 使用精密量具測量每個模組的初始角度
   - 建立標準化的校準流程

2. **軟體補償**：
   - 在 `configCANcoder` 方法中應用偏移量補償
   - 根據測試數據動態調整偏移量

3. **機械優化**：
   - 加強 CANcoder 安裝結構，減少晃動
   - 定期檢查和維護機械連接處


### 4.2 PID 參數調整

#### 4.2.1 問題分析
精確控制 Swerve 模組的角度需要適當的 PID 參數，但調整過程中我們遇到了以下挑戰：
- 過度補償導致系統振盪
- 不足的補償導致響應緩慢
- 負載變化導致的參數失效

#### 4.2.2 解決方案：系統性 PID 調參方法
我們採用了以下科學步驟進行 PID 參數調整：

1. **初始設置**：
   - 清零所有參數 (Kp=0, Ki=0, Kd=0)
   - 從最簡單的 P 控制開始

2. **調節 Kp（比例項）**：
   - 小步漸進增加 Kp 值
   - 目標：系統能大致接近設定值，但允許存在穩態誤差
   - 觀察點：速度

3. **調節 Ki（積分項）**：
   - 在 Kp 基礎上，緩慢增加 Ki
   - 目標：減少或消除穩態誤差
   - 觀察點：系統穩定性和積分飽和問題

4. **調節 Kd（微分項）**：
   - 謹慎增加 Kd 值
   - 目標：減少超調和振盪
   - 觀察點：噪聲敏感度和響應平滑度

5. **整體優化**：
   - 在實際運行環境中測試參數
   - 根據不同運行狀態微調參數
  

最終確定的 PID 參數是經過大量測試後得出的最佳值：Kp=0.0113, Ki=0.0115, Kd=0。
## 5. 進階子系統：電梯控制機構

電梯系統用於將機器人的抓取機構提升到不同高度以完成得分任務。

![電梯與機械臂](https://hackmd.io/_uploads/rk-rKUHCyl.jpg)

### 5.1 電梯子系統設計

```java
public class Elevator extends SubsystemBase {
    // 馬達與控制器宣告
    private final CANSparkMax elevatorMotor;
    private final SparkMaxPIDController pidController;
    private final RelativeEncoder encoder;
    private final DigitalInput limitSwitch;  // 極限開關防止電梯過度下降
    
    // 高度轉換參數
    private final double gear_diameter = 1.45 * 0.0254;  // 齒輪直徑，單位轉為公尺
    private final double gear_circle = gear_diameter * Math.PI;  // 齒輪周長
    
    public Elevator() {
        // 初始化馬達和控制器
        elevatorMotor = new CANSparkMax(13, MotorType.kBrushless);
        pidController = elevatorMotor.getPIDController();
        encoder = elevatorMotor.getEncoder();
        limitSwitch = new DigitalInput(0);
        
        // 配置馬達控制器
        configSparkMax();
    }
    
    // 配置SparkMax控制器
    private void configSparkMax() {
        elevatorMotor.restoreFactoryDefaults();
        encoder.setPosition(0); // 設定初始位置
        elevatorMotor.setIdleMode(IdleMode.kBrake); // 設定煞車模式
        elevatorMotor.setSmartCurrentLimit(40); // 限制電流
        
        // 設定PID參數
        pidController.setP(0.15);
        pidController.setI(0.0);
        pidController.setD(0.1);
        
        elevatorMotor.burnFlash(); // 將設定寫入控制器
    }
    
    // 設定馬達功率方法
    public void setPower(double p) {
        // 安全機制：碰到底部極限開關且要往下移動時停止
        if (limitSwitch.get() && p < 0) {
            elevatorMotor.set(0);
            encoder.setPosition(0);  // 設定碰到極限開關位置為初始位置
        } else {
            elevatorMotor.set(p);
        }
    }
    
    // 停止電梯
    public void stop() {
        elevatorMotor.set(0);
    }
    
    // 獲取當前高度
    public double getPosition() {
        return encoder.getPosition() / 4 * gear_circle;  // 將馬達旋轉數轉換為高度
    }
    
    // 移動到指定高度方法
    public void moveToPosition(double position) {
        // 根據齒比轉換高度為馬達轉數
        double setpoint = position / gear_circle * 4;  // 4為齒比
        pidController.setReference(setpoint, CANSparkMax.ControlType.kPosition);
    }
    
    @Override
    public void periodic() {
        // 定期檢查極限開關
        if (limitSwitch.get()) {
            encoder.setPosition(0);  // 重設位置
        }
    }
}
```

### 5.2 電梯命令設計

```java
public class ElevatorMoveToPositionCmd extends CommandBase {
    private final Elevator elevator;
    private final double targetPosition;
    
    public ElevatorMoveToPositionCmd(Elevator elevator, double targetPosition) {
        this.elevator = elevator;
        this.targetPosition = targetPosition;
        addRequirements(elevator);
    }
    
    @Override
    public void initialize() {
        // 命令初始化時無需操作
    }
    
    @Override
    public void execute() {
        elevator.moveToPosition(targetPosition);
    }
    
    @Override
    public boolean isFinished() {
        return Math.abs(elevator.getPosition() - targetPosition) < 0.05;  // 容差5cm
    }
    
    @Override
    public void end(boolean interrupted) {
        elevator.stop();  // 命令結束時停止電梯
    }
}
```

### 5.3 電梯按鈕綁定

```java
// 在RobotContainer的configureBindings方法中
private void configureBindings() {
    // 電梯控制按鈕
    joystick.x().onTrue(new ElevatorMoveToPositionCmd(elevator, 0.15));  // X鍵將電梯移動到0.15米高度
    joystick.y().onTrue(new ElevatorMoveToPositionCmd(elevator, 0.475));  // Y鍵將電梯移動到0.475米高度
    joystick.b().onTrue(new ElevatorMoveToPositionCmd(elevator, 0.35));  // B鍵將電梯移動到0.35米高度
}
```

## 6. 高級機械控制：機械臂系統

機械臂用於控制機器人的抓取角度，可以提前抓取道具，抬高後放進得分區。
![LINE_ALBUM_build season Record_250410_6](https://hackmd.io/_uploads/rkcmGDSCJl.jpg)


### 6.1 機械臂子系統設計

```java
public class Arm extends SubsystemBase {
    // 馬達與控制器宣告
    private final CANSparkMax armMotor;
    private final SparkMaxPIDController pidController;
    private final RelativeEncoder encoder;
    
    // 角度計算與前饋控制
    private final double angle_per_rotation = 45.0 / 180.0;  // 馬達轉到45°所需的rotation
    private final ArmFeedforward feedforward = new ArmFeedforward(0, 0.05, 0);  // 避免手臂因重力下滑
    
    public Arm() {
        // 初始化馬達和控制器
        armMotor = new CANSparkMax(14, MotorType.kBrushless);
        pidController = armMotor.getPIDController();
        encoder = armMotor.getEncoder();
        
        // 配置馬達控制器
        configSparkMax();
    }
    
    // 配置SparkMax控制器
    private void configSparkMax() {
        armMotor.restoreFactoryDefaults();
        encoder.setPosition(0);
        armMotor.setIdleMode(IdleMode.kBrake);
        armMotor.setSmartCurrentLimit(20);
        
        // 設定PID參數
        pidController.setP(0.1);
        pidController.setI(0.0001);
        pidController.setD(0.01);
        
        armMotor.burnFlash();
    }
    
    // 設定馬達功率
    public void setPower(double p) {
        armMotor.set(p);
    }
    
    // 停止機械臂
    public void stop() {
        armMotor.set(0);
    }
    
    // 獲取當前角度
    public double getAngle() {
        return encoder.getPosition() * angle_per_rotation;
    }
    
    // 移動到指定角度方法
    public void moveToAngle(double targetAngle) {
        double targetRotation = targetAngle / angle_per_rotation;
        double ff = feedforward.calculate(Math.toRadians(targetAngle), 0);
        
        pidController.setReference(targetRotation, CANSparkMax.ControlType.kPosition, 0, ff);
    }
    
    @Override
    public void periodic() {
        // 可以添加診斷資訊輸出
    }
}
```

### 6.2 機械臂命令設計

```java
public class ArmMoveToAngleCmd extends CommandBase {
    private final Arm arm;
    private final double targetAngle;
    
    public ArmMoveToAngleCmd(Arm arm, double targetAngle) {
        this.arm = arm;
        this.targetAngle = targetAngle;
        addRequirements(arm);
    }
    
    @Override
    public void initialize() {
        // 命令初始化時無需操作
    }
    
    @Override
    public void execute() {
        arm.moveToAngle(targetAngle);
    }
    
    @Override
    public boolean isFinished() {
        return Math.abs(arm.getAngle() - targetAngle) < 3.0;  // 容差3度
    }
    
    @Override
    public void end(boolean interrupted) {
        // 命令結束時無需特別停止，因為已達到目標角度
    }
}
```

### 6.3 機械臂按鈕綁定

```java
// 在RobotContainer的configureBindings方法中
private void configureBindings() {
    // 機械臂控制按鈕
    joystick.leftBumper().onTrue(new ArmMoveToAngleCmd(arm, 0));  // 左肩鈕使機械臂移動到0度
    joystick.rightBumper().onTrue(new ArmMoveToAngleCmd(arm, 45));  // 右肩鈕使機械臂移動到45度
}
```


## 7. 抓取與Roller機構

### 7.1 Roller系統設計

Roller系統負責吸入或排出道具，並將它們傳遞給抓取機構。

![Roller示意圖](https://hackmd.io/_uploads/HkOD_IrCkg.jpg)

```java
public class Roller extends SubsystemBase {
    // 馬達宣告
    private final CANSparkMax rollerMotor;
    
    public Roller() {
        rollerMotor = new CANSparkMax(15, MotorType.kBrushless);
        configSparkMax();
    }
    
    // 配置SparkMax控制器
    private void configSparkMax() {
        rollerMotor.restoreFactoryDefaults();
        rollerMotor.setIdleMode(IdleMode.kCoast);  // 設定滑行模式
        rollerMotor.setSmartCurrentLimit(30);
        rollerMotor.burnFlash();
    }
    
    // 控制滾輪速度方法
    public void runRoller(double speed) {
        rollerMotor.set(speed);  // 正值(1.0)表示吸入，負值(-1.0)表示排出
    }
    
    // 停止方法
    public void stop() {
        rollerMotor.set(0);
    }
}
```

### 7.2 Roller命令設計

```java
// 吸入命令
public class CoralInCmd extends CommandBase {
    private final Roller roller;
    
    public CoralInCmd(Roller roller) {
        this.roller = roller;
        addRequirements(roller);
    }
    
    @Override
    public void execute() {
        roller.runRoller(1.0);  // 以100%速度吸入
    }
    
    @Override
    public void end(boolean interrupted) {
        roller.stop();  // 停止馬達
    }
}

// 排出命令
public class CoralOutCmd extends CommandBase {
    private final Roller roller;
    
    public CoralOutCmd(Roller roller) {
        this.roller = roller;
        addRequirements(roller);
    }
    
    @Override
    public void execute() {
        roller.runRoller(-1.0);  // 以100%速度排出
    }
    
    @Override
    public void end(boolean interrupted) {
        roller.stop();  // 停止馬達
    }
}
```

## 8. 抓取機構(Grab)設計

抓取機構用於夾住道具，配合電梯和機械臂完成得分動作。

![抓取機構示意圖](https://hackmd.io/_uploads/SJ0J_IrAyx.jpg)

### 8.1 Grab子系統設計

```java
public class Grab extends SubsystemBase {
    // 馬達宣告
    private final CANSparkMax grabMotor;
    
    public Grab() {
        grabMotor = new CANSparkMax(16, MotorType.kBrushless);
        configSparkMax();
    }
    
    // 配置SparkMax控制器
    private void configSparkMax() {
        grabMotor.restoreFactoryDefaults();
        grabMotor.setIdleMode(IdleMode.kBrake);  // 設定煞車模式
        grabMotor.setSmartCurrentLimit(20);
        grabMotor.burnFlash();
    }
    
    // 抓取方法
    public void intake() {
        grabMotor.set(0.5);  // 以50%速度抓取
    }
    
    // 釋放方法
    public void outtake() {
        grabMotor.set(-0.5);  // 以50%速度釋放
    }
    
    // 停止方法
    public void stop() {
        grabMotor.set(0);
    }
}
```

### 8.2 Grab命令設計

```java
// 抓取命令
public class GrabIntakeCmd extends CommandBase {
    private final Grab grab;
    
    public GrabIntakeCmd(Grab grab) {
        this.grab = grab;
        addRequirements(grab);
    }
    
    @Override
    public void execute() {
        grab.intake();  // 執行抓取
    }
    
    @Override
    public void end(boolean interrupted) {
        grab.stop();  // 停止馬達
    }
}

// 釋放命令
public class GrabOuttakeCmd extends CommandBase {
    private final Grab grab;
    
    public GrabOuttakeCmd(Grab grab) {
        this.grab = grab;
        addRequirements(grab);
    }
    
    @Override
    public void execute() {
        grab.outtake();  // 執行釋放
    }
    
    @Override
    public void end(boolean interrupted) {
        grab.stop();  // 停止馬達
    }
}
```

## 9. Yati系統設計（Roller抬升機構）

Yati系統用於控制Roller的抬升和放下，以便於道具的收取和傳遞。

### 9.1 Yati子系統設計

```java
public class Yati extends SubsystemBase {
    // 馬達宣告
    private final CANSparkMax yatiMotor;
    private final RelativeEncoder encoder;
    
    public Yati() {
        yatiMotor = new CANSparkMax(17, MotorType.kBrushless);
        encoder = yatiMotor.getEncoder();
        configSparkMax();
    }
    
    // 配置SparkMax控制器
    private void configSparkMax() {
        yatiMotor.restoreFactoryDefaults();
        encoder.setPosition(0);
        yatiMotor.setIdleMode(IdleMode.kBrake);
        yatiMotor.setSmartCurrentLimit(20);
        yatiMotor.burnFlash();
    }
    
    // 控制Yati移動方法
    public void runYati(double speed) {
        yatiMotor.set(speed);
    }
    
    // 獲取當前位置
    public double intakePos() {
        return encoder.getPosition();
    }
    
    // 停止方法
    public void stop() {
        yatiMotor.set(0);
    }
}
```

### 9.2 Yati命令設計

```java
// 抬升命令
public class YatiUpCmd extends CommandBase {
    private final Yati yati;
    
    public YatiUpCmd(Yati yati) {
        this.yati = yati;
        addRequirements(yati);
    }
    
    @Override
    public void execute() {
        yati.runYati(0.3);  // 以30%速度抬升
    }
    
    @Override
    public void end(boolean interrupted) {
        yati.stop();  // 停止馬達
    }
}

// 放下命令
public class YatiDownCmd extends CommandBase {
    private final Yati yati;
    
    public YatiDownCmd(Yati yati) {
        this.yati = yati;
        addRequirements(yati);
    }
    
    @Override
    public void execute() {
        yati.runYati(-0.3);  // 以30%速度放下
    }
    
    @Override
    public void end(boolean interrupted) {
        yati.stop();  // 停止馬達
    }
}
```

## 10. 控制器整合與操作介面設計

### 10.1 RobotContainer中的系統初始化

```java
public class RobotContainer {
    // 底盤系統初始化
    private final SwerveChassis chassis = new SwerveChassis();
    
    // 電梯系統初始化
    private final Elevator elevator = new Elevator();
    
    // 機械臂系統初始化
    private final Arm arm = new Arm();
    
    // Roller系統初始化
    private final Roller roller = new Roller();
    
    // 抓取系統初始化
    private final Grab grab = new Grab();
    
    // Yati系統初始化
    private final Yati yati = new Yati();
    
    // 控制器宣告
    private final CommandXboxController controller = new CommandXboxController(0);
    private final CommandJoystick joystick = new CommandJoystick(1);
    
    // 速度參數
    private final double maxSpeed = 4.935; // 米/秒
    private final double maxOmega = 10.13; // 弧度/秒
    
    public RobotContainer() {
        // 底盤預設命令設置
        configureDefaultCommands();
        
        // 配置按鈕綁定
        configureBindings();
    }
    
    private void configureDefaultCommands() {
        // 底盤預設命令
        chassis.setDefaultCommand(
            new RunCommand(
                () -> chassis.drive(
                    // 從左搖桿獲取平移指令(X、Y軸)
                    new Translation2d(
                        -controller.getLeftY() * maxSpeed,
                        -controller.getLeftX() * maxSpeed
                    ),
                    // 從右搖桿獲取旋轉指令(X軸)
                    -controller.getRightX() * maxOmega,
                    true  // 使用場地視角控制
                ),
                chassis
            )
        );
    }
    
    // 其他配置方法...
}
```

### 10.2 按鈕綁定與命令觸發

```java
private void configureBindings() {
    // 底盤控制
    controller.start().onTrue(new InstantCommand(() -> chassis.resetGyro()));
    
    // 電梯控制按鈕
    joystick.x().onTrue(new ElevatorMoveToPositionCmd(elevator, 0.15));  // X鍵將電梯移動到0.15米高度
    joystick.y().onTrue(new ElevatorMoveToPositionCmd(elevator, 0.475));  // Y鍵將電梯移動到0.475米高度
    joystick.b().onTrue(new ElevatorMoveToPositionCmd(elevator, 0.35));  // B鍵將電梯移動到0.35米高度
    
    // 機械臂控制
    joystick.leftBumper().onTrue(new ArmMoveToAngleCmd(arm, 0));  // 左肩鈕使機械臂移動到0度
    joystick.rightBumper().onTrue(new ArmMoveToAngleCmd(arm, 45));  // 右肩鈕使機械臂移動到45度
    
    // Roller控制按鈕
    controller.x().whileTrue(new CoralInCmd(roller));  // X鍵按住時持續吸入
    controller.y().whileTrue(new CoralOutCmd(roller));  // Y鍵按住時持續排出
    
    // 抓取機構控制
    joystick.leftTrigger().whileTrue(new GrabIntakeCmd(grab));  // 左扳機按住時持續抓取
    joystick.rightTrigger().whileTrue(new GrabOuttakeCmd(grab));  // 右扳機按住時持續釋放
    
    // Yati控制
    controller.a().whileTrue(new YatiUpCmd(yati));  // A鍵按住時Roller抬升
    controller.b().whileTrue(new YatiDownCmd(yati));  // B鍵按住時Roller放下
}
```

## 11. 綜合測試與優化

### 11.1 底盤調試流程

在完成底盤程式撰寫後，我們進行了一系列的測試和調整：

1. **模組零點校準**：
   - 使用特製的校準器(如鋁管)將所有輪子對齊到相同方向
   - 記錄每個模組的初始絕對角度
   - 根據測量結果計算並設定CANcoder的數值

2. **PID參數調整**：
   - 在實際運行環境中測試不同的PID參數組合
   - 收集數據分析轉向精度和穩定性
   - 最終確定Kp=0.0113, Ki=0.0115, Kd=0作為最佳參數

3. **場地測試**：
   - 驗證陀螺儀在連續運行中的漂移情況
   - 根據測試結果調整代碼以提高精度

### 11.2 整合系統測試

在各子系統獨立測試完成後，我們進行了完整機器人的整合測試：

1. **協同動作測試**：
   - 測試電梯和機械臂的協同運動
   - 驗證抓取機構與Roller的配合效果
   - 在不同速度下測試底盤與上層機構的互動

2. **操作穩定性測試**：
   - 模擬比賽的操作測試
   - 監控各馬達的溫度和電流消耗
   - 分析可能的故障點和系統瓶頸

3. **極限條件測試**：
   - 測試最大負載下的系統反應(如速度最大化)
   - 驗證極限位置和角度的安全措施
  

## 12. 學習心得與反思

### 12.1 技術能力的提升

在這次專案中，我深入學習了機器人底盤系統的設計與實作，從單個Swerve模組的控制到整體底盤的運動協調。透過實際編程和調試，我掌握了PID控制器的參數調整技巧，以及如何處理機械系統中的誤差和干擾。

萬向輪底盤的設計與控制不僅要求紮實的編程基礎，還需要對機械原理有充分理解。在程式撰寫過程中，我學會了如何將數學模型轉化為實際的控制邏輯，這種轉化能力對於未來的工程項目至關重要。

### 12.2 問題解決經驗

面對校正偏移量和PID參數調整的挑戰，我學會了精確分析問題，並進行實驗和優化。這種反覆測試和微調的過程，培養了我的耐心和精細能力，也讓我理解到理論與實際應用之間的差距。

特別是在解決馬達振盪問題時，我通過逐步調整PID參數並分析系統響應，最終找到了穩定系統的最佳參數組合。這種問題解決的方法論不僅適用於機器人控制，也適用於其他工程領域。

### 12.3 整合系統的複雜性認知

整合多個子系統（底盤、電梯、機械臂、抓取機構）的過程中，我意識到系統設計的複雜性和各組件間的相互影響。這促使我學習如何設計模組化的程式架構，使各子系統既能獨立運作，又能協同工作。

在設計RobotContainer時，我體會到良好的系統架構對於後期開發和維護的重要性。合理的結構設計、清晰的變數定義以及完善的命令系統，都是確保複雜機器人程式順利運行的關鍵。

### 12.4 工程實踐的寶貴經驗

從程式碼撰寫到硬體調整，再到系統整合測試，每一步都是寶貴的工程實踐經驗。這些經驗不僅提升了我的技術能力，也讓我更深入理解機器人系統的工作原理和設計思路，為未來參與更複雜的專案奠定了堅實基礎。

同時，這次專案也讓我認識到團隊協作的重要性。與機械組、電子組的緊密合作，確保了軟體設計能夠充分考慮硬體限制，最終打造出一個高度整合的機器人系統。

