每日算法 — 使用java实现陀螺:进动角速度计算与姿态稳定控制

引言

陀螺是经典的旋转玩具,高速旋转时能在尖端保持直立而不倒,这一现象涉及刚体转动动力学中的进动与章动。本文用Java实现一个三维陀螺物理模拟系统,核心算法包括欧拉角姿态表示、基于欧拉方程的刚体旋转动力学数值积分,以及用PID控制器实现主动姿态稳定,同时分析角动量守恒条件下的陀螺稳定性判据。

核心算法思路

1. 刚体旋转动力学(欧拉方程)

对于绕固定点旋转的刚体,在随体坐标系(主轴坐标系)中,角速度 ω = (ωx, ωy, ωz) 满足欧拉方程:
– Ix * dωx/dt = (Iy - Iz) * ωy * ωz + τx
– Iy * dωy/dt = (Iz - Ix) * ωz * ωx + τy
– Iz * dωz/dt = (Ix - Iy) * ωx * ωy + τz

其中 Ix, Iy, Iz 为主转动惯量,τ 为外力矩(重力矩)。对于对称陀螺(Ix = Iy ≠ Iz),方程可大幅简化。

2. 四元数姿态更新

为避免欧拉角的万向节锁问题,使用四元数表示姿态。设当前姿态四元数为 q,角速度为 ω,则:
– dq/dt = 0.5 * q ⊗ (0, ωx, ωy, ωz)

每步积分后需归一化四元数以保持单位长度。

3. 重力矩计算

设陀螺质心到支点的距离为 l,质量为 m,则重力矩:
– τ = r × mg

在随体坐标系中,需先将重力向量转换到随体坐标系再计算叉乘。

4. 稳定性判据

对称陀螺绕竖直轴高速旋转时,当自转角速度 ψ̇ 满足:
– ψ̇² > 4 * m * g * l * Ix / Iz²

陀螺的直立旋转是稳定的(Sleeping Top条件)。这一判据可用于设计陀螺的初始参数。

5. PID姿态稳定控制

若陀螺有驱动电机,可通过测量倾角误差,用PID控制器输出控制力矩:
– τ_control = Kp * e + Ki * ∫e dt + Kd * de/dt

其中 e 为当前姿态与目标姿态(直立)的偏差。

完整Java实现

import java.util.*;

/**
 * 三维陀螺物理模拟与姿态稳定控制系统
 * 核心算法:欧拉方程 + 四元数姿态积分 + PID稳定控制
 */
public class GyroscopeSimulator {

    static final double GRAVITY = 9.8;
    static final double DT = 0.001; // 1ms积分步长

    // 三维向量
    static class Vec3 {
        double x, y, z;
        Vec3(double x, double y, double z) { this.x = x; this.y = y; this.z = z; }
        Vec3() { this(0, 0, 0); }

        Vec3 add(Vec3 v) { return new Vec3(x + v.x, y + v.y, z + v.z); }
        Vec3 sub(Vec3 v) { return new Vec3(x - v.x, y - v.y, z - v.z); }
        Vec3 scale(double s) { return new Vec3(x * s, y * s, z * s); }
        Vec3 cross(Vec3 v) {
            return new Vec3(
                y * v.z - z * v.y,
                z * v.x - x * v.z,
                x * v.y - y * v.x
            );
        }
        double dot(Vec3 v) { return x * v.x + y * v.y + z * v.z; }
        double norm() { return Math.sqrt(x*x + y*y + z*z); }
        Vec3 normalize() {
            double n = norm();
            if (n < 1e-10) return new Vec3(0, 0, 1);
            return scale(1.0 / n);
        }
    }

    // 四元数
    static class Quaternion {
        double w, x, y, z;
        Quaternion(double w, double x, double y, double z) {
            this.w = w; this.x = x; this.y = y; this.z = z;
        }
        Quaternion() { this(1, 0, 0, 0); }

        Quaternion multiply(Quaternion q) {
            return new Quaternion(
                w*q.w - x*q.x - y*q.y - z*q.z,
                w*q.x + x*q.w + y*q.z - z*q.y,
                w*q.y - x*q.z + y*q.w + z*q.x,
                w*q.z + x*q.y - y*q.x + z*q.w
            );
        }

        Quaternion conjugate() { return new Quaternion(w, -x, -y, -z); }

        Quaternion normalize() {
            double n = Math.sqrt(w*w + x*x + y*y + z*z);
            return new Quaternion(w/n, x/n, y/n, z/n);
        }

        // 将向量从随体坐标系旋转到惯性系
        Vec3 rotate(Vec3 v) {
            Quaternion qv = new Quaternion(0, v.x, v.y, v.z);
            Quaternion result = this.multiply(qv).multiply(this.conjugate());
            return new Vec3(result.x, result.y, result.z);
        }

        // 将向量从惯性系旋转到随体坐标系
        Vec3 rotateInverse(Vec3 v) {
            Quaternion qv = new Quaternion(0, v.x, v.y, v.z);
            Quaternion result = this.conjugate().multiply(qv).multiply(this);
            return new Vec3(result.x, result.y, result.z);
        }
    }

    // 陀螺状态
    static class GyroState {
        Quaternion q;       // 姿态四元数(随体->惯性)
        Vec3 omegaBody;     // 随体坐标系角速度
        double Ix, Iy, Iz;  // 主转动惯量
        double mass;        // 质量
        double comLength;   // 质心到支点距离

        GyroState(double Ix, double Iy, double Iz, double mass, double comLength) {
            this.q = new Quaternion();
            this.omegaBody = new Vec3();
            this.Ix = Ix; this.Iy = Iy; this.Iz = Iz;
            this.mass = mass;
            this.comLength = comLength;
        }

        // 角动量(惯性系)
        Vec3 angularMomentum() {
            Vec3 Lbody = new Vec3(Ix * omegaBody.x, Iy * omegaBody.y, Iz * omegaBody.z);
            return q.rotate(Lbody);
        }

        // 动能
        double kineticEnergy() {
            return 0.5 * (Ix * omegaBody.x * omegaBody.x +
                         Iy * omegaBody.y * omegaBody.y +
                         Iz * omegaBody.z * omegaBody.z);
        }

        // 势能(以支点为参考)
        double potentialEnergy() {
            // 质心位置 = q.rotate((0,0,comLength))
            Vec3 comDir = q.rotate(new Vec3(0, 0, 1));
            return mass * GRAVITY * comDir.z * comLength;
        }
    }

    // PID控制器
    static class PIDController {
        double Kp, Ki, Kd;
        double integral = 0;
        double prevError = 0;

        PIDController(double Kp, double Ki, double Kd) {
            this.Kp = Kp; this.Ki = Ki; this.Kd = Kd;
        }

        double update(double error, double dt) {
            integral += error * dt;
            double derivative = (error - prevError) / dt;
            prevError = error;
            return Kp * error + Ki * integral + Kd * derivative;
        }

        void reset() {
            integral = 0;
            prevError = 0;
        }
    }

    private GyroState gyro;
    private PIDController pidX, pidY;
    private boolean activeControl; // 是否启用主动控制
    private List<double[]> history; // 历史记录

    public GyroscopeSimulator(GyroState gyro, boolean activeControl) {
        this.gyro = gyro;
        this.activeControl = activeControl;
        this.pidX = new PIDController(2.0, 0.5, 0.8);
        this.pidY = new PIDController(2.0, 0.5, 0.8);
        this.history = new ArrayList<>();
    }

    /**
     * 计算重力矩(在随体坐标系中)
     */
    private Vec3 computeGravityTorque() {
        // 重力在惯性系中为 (0, 0, -mg)
        Vec3 gravityInertial = new Vec3(0, 0, -gyro.mass * GRAVITY);
        // 转换到随体坐标系
        Vec3 gravityBody = gyro.q.rotateInverse(gravityInertial);
        // 质心在随体坐标系中为 (0, 0, comLength)
        Vec3 comBody = new Vec3(0, 0, gyro.comLength);
        // 力矩 = r × F
        return comBody.cross(gravityBody);
    }

    /**
     * 单步积分:龙格-库塔4阶(RK4)
     */
    public void stepRK4() {
        // 状态向量: [omega_x, omega_y, omega_z, q_w, q_x, q_y, q_z]
        // 为简化,这里使用半隐式方法:先积分角速度,再积分姿态

        Vec3 tau = computeGravityTorque();

        // 主动控制力矩
        if (activeControl) {
            // 计算当前倾角误差(从竖直方向z轴的偏离)
            Vec3 bodyZ = gyro.q.rotate(new Vec3(0, 0, 1));
            // 误差为x和y方向的倾斜分量
            double errorX = bodyZ.x;
            double errorY = bodyZ.y;

            double controlTx = pidX.update(errorX, DT);
            double controlTy = pidY.update(errorY, DT);

            // 控制力矩施加在随体坐标系的x和y轴
            tau = tau.add(new Vec3(controlTx, controlTy, 0));
        }

        // RK4积分角速度(欧拉方程)
        Vec3 omega = gyro.omegaBody;
        Vec3 k1 = eulerEquation(omega, tau);
        Vec3 k2 = eulerEquation(omega.add(k1.scale(DT * 0.5)), tau);
        Vec3 k3 = eulerEquation(omega.add(k2.scale(DT * 0.5)), tau);
        Vec3 k4 = eulerEquation(omega.add(k3.scale(DT)), tau);

        Vec3 deltaOmega = k1.add(k2.scale(2)).add(k3.scale(2)).add(k4).scale(DT / 6.0);
        gyro.omegaBody = omega.add(deltaOmega);

        // 四元数更新(一阶近似,RK4对四元数较复杂,这里用一阶+小步长补偿)
        Vec3 omegaInertial = gyro.q.rotate(gyro.omegaBody);
        Quaternion deltaQ = new Quaternion(
            0,
            0.5 * omegaInertial.x * DT,
            0.5 * omegaInertial.y * DT,
            0.5 * omegaInertial.z * DT
        );
        gyro.q = gyro.q.addQuaternion(deltaQ).normalize();
    }

    private Vec3 eulerEquation(Vec3 omega, Vec3 tau) {
        double dwx = ((gyro.Iy - gyro.Iz) * omega.y * omega.z + tau.x) / gyro.Ix;
        double dwy = ((gyro.Iz - gyro.Ix) * omega.z * omega.x + tau.y) / gyro.Iy;
        double dwz = ((gyro.Ix - gyro.Iy) * omega.x * omega.y + tau.z) / gyro.Iz;
        return new Vec3(dwx, dwy, dwz);
    }

    // 四元数加法辅助
    private Quaternion addQuaternion(Quaternion a, Quaternion b) {
        return new Quaternion(a.w + b.w, a.x + b.x, a.y + b.y, a.z + b.z);
    }

    /**
     * 长时间模拟
     */
    public void simulate(double duration) {
        int steps = (int)(duration / DT);
        for (int i = 0; i < steps; i++) {
            stepRK4();

            // 每100步记录一次
            if (i % 100 == 0) {
                Vec3 comDir = gyro.q.rotate(new Vec3(0, 0, 1));
                double tiltAngle = Math.toDegrees(Math.acos(Math.min(1, Math.abs(comDir.z))));
                history.add(new double[]{
                    i * DT,
                    tiltAngle,
                    gyro.omegaBody.z, // 自转角速度
                    gyro.kineticEnergy()
                });
            }
        }
    }

    /**
     * 稳定性判据检验
     */
    public boolean checkStabilityCriterion() {
        double criticalOmega = Math.sqrt(4 * gyro.mass * GRAVITY * gyro.comLength * gyro.Ix) / gyro.Iz;
        double spinRate = Math.abs(gyro.omegaBody.z);
        System.out.printf("稳定性判据: 当前自转%.2f rad/s, 临界值%.2f rad/s\n", spinRate, criticalOmega);
        return spinRate > criticalOmega;
    }

    public void printResults() {
        System.out.println("=== 陀螺模拟结果 ===");
        System.out.println("记录点数: " + history.size());

        double maxTilt = 0;
        for (double[] h : history) {
            maxTilt = Math.max(maxTilt, h[1]);
        }
        System.out.printf("最大倾斜角: %.2f°\n", maxTilt);

        if (!history.isEmpty()) {
            double[] first = history.get(0);
            double[] last = history.get(history.size() - 1);
            System.out.printf("初始倾斜: %.2f°, 最终倾斜: %.2f°\n", first[1], last[1]);
            System.out.printf("初始自转: %.2f rad/s, 最终自转: %.2f rad/s\n", first[2], last[2]);
        }
    }

    public static void main(String[] args) {
        // 创建对称陀螺:Ix = Iy, Iz为轴向转动惯量
        // 参数参考典型金属陀螺
        double mass = 0.1;       // 100g
        double comLength = 0.03; // 质心距3cm
        double Ix = 1.5e-4;      // kg·m²
        double Iy = 1.5e-4;
        double Iz = 3.0e-5;      // 轴向转动惯量较小

        GyroState gyro = new GyroState(Ix, Iy, Iz, mass, comLength);
        // 初始条件:高速自转 + 微小倾斜
        gyro.omegaBody = new Vec3(0.5, 0, 30); // x方向微扰,z方向高速自转
        // 初始姿态:轻微倾斜5度
        double tiltRad = Math.toRadians(5);
        gyro.q = new Quaternion(
            Math.cos(tiltRad / 2),
            Math.sin(tiltRad / 2), 0, 0
        ).normalize();

        System.out.println("=== 无控制模拟 ===");
        GyroscopeSimulator sim1 = new GyroscopeSimulator(gyro, false);
        sim1.checkStabilityCriterion();
        sim1.simulate(2.0); // 模拟2秒
        sim1.printResults();

        // 重新初始化用于控制模拟
        GyroState gyro2 = new GyroState(Ix, Iy, Iz, mass, comLength);
        gyro2.omegaBody = new Vec3(0.5, 0, 30);
        gyro2.q = new Quaternion(
            Math.cos(tiltRad / 2),
            Math.sin(tiltRad / 2), 0, 0
        ).normalize();

        System.out.println("\n=== PID主动控制模拟 ===");
        GyroscopeSimulator sim2 = new GyroscopeSimulator(gyro2, true);
        sim2.simulate(2.0);
        sim2.printResults();

        // 低速自转测试(应不稳定)
        System.out.println("\n=== 低速自转测试(应倾倒)===");
        GyroState gyro3 = new GyroState(Ix, Iy, Iz, mass, comLength);
        gyro3.omegaBody = new Vec3(0.5, 0, 5); // 低速自转
        gyro3.q = new Quaternion(
            Math.cos(tiltRad / 2),
            Math.sin(tiltRad / 2), 0, 0
        ).normalize();
        GyroscopeSimulator sim3 = new GyroscopeSimulator(gyro3, false);
        sim3.checkStabilityCriterion();
        sim3.simulate(2.0);
        sim3.printResults();
    }
}

算法复杂度分析

步骤 时间复杂度 空间复杂度 说明
单步RK4积分 O(1) O(1) 固定向量运算
模拟N步 O(N) O(N/S) S为记录间隔
PID控制计算 O(1) O(1) 固定公式计算
稳定性判据 O(1) O(1) 直接公式判断

扩展方向

  1. 可视化渲染:用JavaFX或LWJGL实时渲染陀螺三维姿态,绘制进动轨迹
  2. 双陀螺耦合:两个陀螺通过框架连接,形成陀螺仪,模拟导航应用
  3. 摩擦力模型:加入支点处的滚动摩擦和空气阻力,更精确地模拟能量耗散
  4. 数据驱动辨识:用传感器数据(陀螺仪、加速度计)通过扩展卡尔曼滤波实时估计姿态

总结

本文从欧拉方程出发,建立了对称陀螺的旋转动力学模型,使用四元数避免了欧拉角奇异问题,并采用RK4数值积分保证长时间模拟的精度。推导并验证了陀螺直立旋转的稳定性判据,同时展示了PID控制器在主动姿态稳定中的有效性。这一框架不仅适用于游戏模拟,也是理解惯性导航和航天器姿态控制的基础。