🧠 平滑舵機控制系統

一、項目概述

本項目基於 Raspberry Pi + PCA9685 + 舵機 實現了一個平滑的舵機控制系統。
通過 adafruit_servokit.ServoKit 控制 PWM 信號輸出,使舵機在指定角度範圍內進行平滑往返運動。
系統支持速度調節、緩動曲線(Easing Function)和平滑控制,可用於機械臂、雲台、舵機測試平台等應用場景。

二、系統組成與依賴

1. 硬件組成

  • Raspberry Pi 4 / 5
  • PCA9685 16通道舵機控制模塊
  • 舵機(如 SG90、MG996R 等)
  • 外部電源(推薦 5V 2A 或更高)

2. 軟件依賴

庫名

説明

安裝命令

adafruit-circuitpython-servokit

控制 PCA9685 輸出舵機信號

pip install adafruit-circuitpython-servokit

mathtime

Python 標準庫

無需安裝

三、核心類與方法

1. 類:SmoothServoController

該類封裝了 PCA9685 通道的初始化與舵機運動控制。

初始化方法:

def __init__(self, channel=0, pulse_min=1000, pulse_max=2000)

參數:

  • channel: 使用的 PCA9685 通道號 (0–15)
  • pulse_min: 最小 PWM 脈寬(μs)
  • pulse_max: 最大 PWM 脈寬(μs)

功能説明:

  • 初始化 ServoKit 實例;
  • 設置舵機脈寬範圍;
  • 默認將舵機角度設為 90°;
  • 打印初始化狀態。

2. 平滑往返運動函數

def smooth_pingpong(
    self,
    angle_range=(0, 180),
    speed=1.0,
    easing="ease_in_out",
    duration=None,
    cycles=None
)

功能:
讓舵機在指定角度範圍內往返運動,並使用緩動算法控制加減速,使運動更加自然。

參數説明:

參數

類型

默認值

説明

angle_range

tuple

(0, 180)

運動角度範圍

speed

float

1.0

運動速度(0.1-2.0)

easing

str

"ease_in_out"

緩動函數類型

duration

float

None

運行時間(秒),None 表示無限循環

cycles

int

None

循環次數,None 表示無限循環

實現邏輯:

  1. 解析角度範圍;
  2. 檢查輸入參數是否合法;
  3. 啓動平滑運動;
  4. 通過正弦曲線函數實現“緩動效果”;
  5. 當達到最大角度後,自動反向回到最小角度,形成循環;
  6. 支持設定運行時間或循環次數。

四、緩動算法説明(Easing Function)

舵機運動採用正弦函數實現緩入緩出(ease-in-out)效果:

基於樹莓派的舵機控制原理-_#自然語言處理

其中:

  • ttt 為歸一化時間(0 → 1)
  • 該函數在開始和結束時速度較慢,中間加速,實現自然的平滑過渡。

五、系統工作原理

  1. Raspberry Pi 通過 I²C 與 PCA9685 通信;
  2. ServoKit 庫控制 PCA9685 輸出 PWM;
  3. PWM 信號控制舵機轉動角度;
  4. 使用 smooth_pingpong 函數循環輸出角度;
  5. 通過 time.sleep() 控制更新頻率,模擬連續運動。

六、運行方法

1、硬件組成

  • Raspberry Pi 4 / 5
  • PCA9685 16通道舵機控制模塊
  • 舵機(如 SG90、MG996R 等)
  • 外部電源(推薦 5V 2A 或更高)

2、安裝依賴

sudo pip3 install adafruit-circuitpython-servokit

基於樹莓派的舵機控制原理-_#自然語言處理_02

def _smooth_move(self, start_angle, end_angle, speed, easing_type, direction):
    steps = max(20, int(50 * speed))  # 動態計算步數
    total_time = 1.0 / speed          # 速度控制總時間
    
    for step in range(steps + 1):
        progress = step / steps  # 線性進度 0.0 → 1.0
        eased_progress = self._apply_easing(progress, easing_type)  # 應用緩動
        current_angle = start_angle + (end_angle - start_angle) * eased_progress
        self._set_angle_smooth(current_angle, total_time / steps)

3、動函數對比

緩動類型

數學公式

運動特點

適用場景

linear

progress

勻速運動

機械運動

ease_in_out

progress²(3-2progress)

平滑啓停

自然運動

ease_in

progress²

緩慢啓動

加速出場

ease_out

1-(1-progress)²

緩慢停止

減速入場

sine

-(cos(πprogress)-1)/2

正弦曲線

波浪運動

circular

1-√(1-progress²)

圓形軌跡

特殊效果

4、全部代碼

#!/usr/bin/env python3
"""
平滑舵機控制器-lianying
功能:實現舵機的平滑運動控制,支持多種緩動效果
"""

import time
import math
from adafruit_servokit import ServoKit

class SmoothServoController:
    """
    平滑舵機控制器類
    提供舵機的平滑運動控制,支持多種緩動效果和運動參數調節
    """
    
    def __init__(self, channel=0, pulse_min=1000, pulse_max=2000):
        """
        初始化平滑舵機控制器
        
        Args:
            channel: PCA9685通道號 (0-15)
            pulse_min: 最小脈衝寬度(微秒),默認1000
            pulse_max: 最大脈衝寬度(微秒),默認2000
        """
        self.channel = channel
        
        # 初始化PCA9685舵機控制板
        self.kit = ServoKit(channels=16)
        # 設置舵機脈衝寬度範圍
        self.kit.servo[channel].set_pulse_width_range(pulse_min, pulse_max)
        
        # 初始化狀態變量
        self.current_angle = 90  # 默認中間位置
        self.kit.servo[channel].angle = self.current_angle
        self.is_running = False  # 運行狀態標誌
        
        print(f"✅ 平滑舵機控制器初始化完成")
        print(f"   - 通道: {channel}")
        print(f"   - 脈衝範圍: {pulse_min}-{pulse_max}μs")
        print(f"   - 初始角度: {self.current_angle}°")
    
    def smooth_pingpong(self, angle_range=(0, 180), speed=1.0, 
                       easing="ease_in_out", duration=None, cycles=None):
        """
        平滑往返運動 - 支持角度範圍入參
        
        Args:
            angle_range: 角度範圍元組 (min_angle, max_angle),默認(0, 180)
            speed: 運動速度係數 (0.1-2.0),默認1.0
            easing: 緩動函數類型,默認"ease_in_out"
            duration: 總運行時間(秒),None表示無限循環
            cycles: 運行循環次數,None表示無限循環
        """
        # 解析角度範圍
        min_angle, max_angle = self._parse_angle_range(angle_range)
        
        # 驗證參數合法性
        self._validate_parameters(min_angle, max_angle, speed)
        
        # 設置運行狀態
        self.is_running = True
        start_time = time.time()
        cycle_count = 0
        
        # 顯示運動參數
        print(f"🚀 開始平滑往返運動")
        print(f"  角度範圍: {min_angle}° - {max_angle}°")
        print(f"  運動速度: {speed}")
        print(f"  緩動類型: {easing}")
        if duration:
            print(f"  持續時間: {duration}秒")
        if cycles:
            print(f"  循環次數: {cycles}次")
        print("  按 Ctrl+C 停止運動")
        print("-" * 50)
        
        try:
            while self.is_running:
                # 檢查循環次數限制
                if cycles and cycle_count >= cycles:
                    print(f"✅ 達到指定循環次數 {cycles}次,停止運動")
                    break
                
                # 正向運動 (min_angle -> max_angle)
                self._smooth_move(min_angle, max_angle, speed, easing, "正向")
                
                # 反向運動 (max_angle -> min_angle)
                self._smooth_move(max_angle, min_angle, speed, easing, "反向")
                
                cycle_count += 1
                
                # 顯示進度信息
                if duration or cycles:
                    self._display_progress(cycle_count, start_time, duration, cycles)
                
                # 檢查是否達到指定持續時間
                if duration and (time.time() - start_time) >= duration:
                    print(f"✅ 達到指定持續時間 {duration}秒,停止運動")
                    break
                    
        except KeyboardInterrupt:
            print("\n🛑 用户中斷運動")
        except Exception as e:
            print(f"❌ 運動錯誤: {e}")
        finally:
            self.is_running = False
            # 回到安全位置(範圍中間值)
            safe_angle = (min_angle + max_angle) / 2
            self._set_angle_smooth(safe_angle, 0.5)
            print(f"🎯 運動結束,共完成 {cycle_count} 個循環")
            print(f"📌 舵機已回到安全位置: {safe_angle}°")
    
    def _parse_angle_range(self, angle_range):
        """
        解析角度範圍參數
        
        Args:
            angle_range: 角度範圍參數
            
        Returns:
            tuple: (min_angle, max_angle)
            
        Raises:
            ValueError: 參數格式錯誤時拋出
        """
        if isinstance(angle_range, (list, tuple)) and len(angle_range) == 2:
            min_angle, max_angle = angle_range
        else:
            raise ValueError("angle_range 必須是包含兩個元素的元組或列表 (min_angle, max_angle)")
        
        # 確保min_angle <= max_angle
        if min_angle > max_angle:
            min_angle, max_angle = max_angle, min_angle
            print(f"⚠️  角度範圍已自動校正: ({min_angle}, {max_angle})")
        
        return min_angle, max_angle
    
    def _validate_parameters(self, min_angle, max_angle, speed):
        """
        驗證參數有效性
        
        Args:
            min_angle: 最小角度
            max_angle: 最大角度
            speed: 速度係數
            
        Raises:
            ValueError: 參數無效時拋出
        """
        if not (0 <= min_angle <= 180 and 0 <= max_angle <= 180):
            raise ValueError("角度必須在 0-180 度範圍內")
        
        if min_angle == max_angle:
            raise ValueError("最小角度和最大角度不能相同")
        
        if not (0.1 <= speed <= 2.0):
            raise ValueError("速度係數必須在 0.1-2.0 範圍內")
    
    def _smooth_move(self, start_angle, end_angle, speed, easing_type, direction):
        """
        執行單次平滑移動
        
        Args:
            start_angle: 起始角度
            end_angle: 結束角度
            speed: 速度係數
            easing_type: 緩動類型
            direction: 運動方向描述
        """
        # 根據速度計算步數和總時間
        steps = max(20, int(50 * speed))  # 根據速度調整步數
        total_time = 1.0 / speed  # 總時間隨速度變化
        
        print(f"🔁 {direction}運動: {start_angle}° → {end_angle}°")
        
        for step in range(steps + 1):
            if not self.is_running:
                break
                
            # 計算當前進度 (0.0 - 1.0)
            progress = step / steps
            
            # 應用緩動函數
            eased_progress = self._apply_easing(progress, easing_type)
            
            # 計算當前角度(線性插值)
            current_angle = start_angle + (end_angle - start_angle) * eased_progress
            
            # 設置角度並等待
            self._set_angle_smooth(current_angle, total_time / steps)
    
    def _apply_easing(self, progress, easing_type):
        """
        應用不同的緩動函數
        
        Args:
            progress: 線性進度 (0.0-1.0)
            easing_type: 緩動類型
            
        Returns:
            float: 緩動後的進度值
        """
        easing_type = easing_type.lower()
        
        if easing_type == "linear":
            # 線性緩動:勻速運動
            return progress
            
        elif easing_type in ["ease_in_out", "easeinout"]:
            # 平滑的緩入緩出:開始和結束慢,中間快
            return progress * progress * (3 - 2 * progress)
            
        elif easing_type in ["ease_out", "easeout"]:
            # 緩出函數:開始快,結束慢
            return 1 - (1 - progress) * (1 - progress)
            
        elif easing_type in ["ease_in", "easein"]:
            # 緩入函數:開始慢,結束快
            return progress * progress
            
        elif easing_type == "quadratic":
            # 二次方緩動
            if progress < 0.5:
                return 2 * progress * progress
            else:
                return 1 - (-2 * progress + 2) * (-2 * progress + 2) / 2
            
        elif easing_type == "sine":
            # 正弦緩動:自然的波浪運動
            return -(math.cos(math.pi * progress) - 1) / 2
            
        elif easing_type == "circular":
            # 圓形緩動:圓形軌跡運動
            return 1 - math.sqrt(1 - math.pow(progress, 2))
            
        else:
            print(f"⚠️  未知的緩動類型 '{easing_type}',使用默認線性緩動")
            return progress
    
    def _set_angle_smooth(self, angle, step_duration):
        """
        平滑設置角度
        
        Args:
            angle: 目標角度
            step_duration: 步進持續時間
        """
        self.kit.servo[self.channel].angle = angle
        self.current_angle = angle
        time.sleep(step_duration)
    
    def _display_progress(self, cycle_count, start_time, duration, cycles):
        """
        顯示進度信息
        
        Args:
            cycle_count: 當前循環次數
            start_time: 開始時間
            duration: 總持續時間
            cycles: 總循環次數
        """
        elapsed = time.time() - start_time
        
        if duration:
            progress_percent = min(100, (elapsed / duration) * 100)
            print(f"📊 進度: {progress_percent:.1f}% ({elapsed:.1f}s/{duration}s) | 循環: {cycle_count}", end='\r')
        elif cycles:
            progress_percent = min(100, (cycle_count / cycles) * 100)
            print(f"📊 進度: {progress_percent:.1f}% ({cycle_count}/{cycles}循環) | 時間: {elapsed:.1f}s", end='\r')
    
    def stop(self):
        """停止運動"""
        self.is_running = False
        print("⏹️  停止運動指令已發送")

    def get_current_angle(self):
        """獲取當前角度"""
        return self.current_angle

    def set_angle(self, angle):
        """
        直接設置角度(無平滑效果)
        
        Args:
            angle: 目標角度 (0-180)
        """
        if 0 <= angle <= 180:
            self.kit.servo[self.channel].angle = angle
            self.current_angle = angle
            print(f"📌 舵機角度設置為: {angle}°")
        else:
            print("❌ 角度必須在0-180範圍內")


# 使用示例和演示函數
def basic_demo():
    """
    基礎演示函數
    展示控制器的基本功能
    """
    print("=" * 60)
    print("🤖 平滑舵機控制器 - 基礎演示")
    print("=" * 60)
    
    # 創建控制器實例
    controller = SmoothServoController(channel=0)
    
    try:
        print("\n1. 小範圍平滑運動演示")
        controller.smooth_pingpong(
            angle_range=(60, 120),  # 60-120度範圍
            speed=0.8,              # 中等速度
            easing="ease_in_out",   # 平滑緩動
            cycles=2                # 運行2個循環
        )
        
        print("\n2. 快速往返運動演示")
        controller.smooth_pingpong(
            angle_range=(30, 150),  # 30-150度範圍
            speed=1.5,              # 快速
            easing="linear",        # 線性運動
            cycles=3                # 運行3個循環
        )
        
    except KeyboardInterrupt:
        print("\n演示被用户中斷")
    except Exception as e:
        print(f"演示錯誤: {e}")
    finally:
        controller.stop()


def easing_comparison_demo():
    """
    緩動函數對比演示
    展示不同緩動函數的運動效果
    """
    print("\n" + "=" * 60)
    print("📊 緩動函數對比演示")
    print("=" * 60)
    
    controller = SmoothServoController(channel=0)
    
    # 緩動函數列表
    easing_types = ["linear", "ease_in", "ease_out", "ease_in_out", "sine"]
    
    for easing in easing_types:
        try:
            print(f"\n🎯 測試緩動函數: {easing}")
            controller.smooth_pingpong(
                angle_range=(45, 135),
                speed=1.0,
                easing=easing,
                cycles=1
            )
            time.sleep(1)  # 演示間隔
            
        except KeyboardInterrupt:
            print("\n對比演示被中斷")
            break
        except Exception as e:
            print(f"緩動測試錯誤: {e}")
    
    controller.stop()


def interactive_control():
    """
    交互式控制模式
    用户可以通過命令行實時控制舵機
    """
    print("\n" + "=" * 60)
    print("🎮 交互式舵機控制模式")
    print("=" * 60)
    
    controller = SmoothServoController(channel=0)
    
    while True:
        print("\n請選擇操作:")
        print("1. 啓動平滑往返運動")
        print("2. 直接設置角度")
        print("3. 顯示當前角度")
        print("4. 停止運動")
        print("5. 退出程序")
        
        choice = input("請輸入選擇 (1-5): ").strip()
        
        if choice == "1":
            try:
                print("\n🚀 設置運動參數:")
                min_angle = float(input("最小角度 (0-180) [默認0]: ") or "0")
                max_angle = float(input("最大角度 (0-180) [默認180]: ") or "180")
                speed = float(input("速度 (0.1-2.0) [默認1.0]: ") or "1.0")
                easing = input("緩動類型 (linear/ease_in/ease_out/ease_in_out/sine) [默認ease_in_out]: ") or "ease_in_out"
                cycles = input("循環次數 [默認無限]: ")
                cycles = int(cycles) if cycles else None
                
                controller.smooth_pingpong(
                    angle_range=(min_angle, max_angle),
                    speed=speed,
                    easing=easing,
                    cycles=cycles
                )
                
            except ValueError as e:
                print(f"❌ 參數錯誤: {e}")
            except Exception as e:
                print(f"❌ 錯誤: {e}")
                
        elif choice == "2":
            try:
                angle = float(input("請輸入角度 (0-180): "))
                controller.set_angle(angle)
            except ValueError:
                print("❌ 請輸入有效的角度數值")
                
        elif choice == "3":
            print(f"📊 當前角度: {controller.get_current_angle()}°")
            
        elif choice == "4":
            controller.stop()
            
        elif choice == "5":
            controller.stop()
            print("👋 再見!")
            break
            
        else:
            print("❌ 無效選擇,請重新輸入")


def educational_examples():
    """
    教學示例集合
    展示各種使用場景和參數組合
    """
    print("\n" + "=" * 60)
    print("🎓 教學示例集合")
    print("=" * 60)
    
    examples = [
        {
            "name": "默認參數運動",
            "params": {"angle_range": (0, 180), "speed": 1.0, "easing": "ease_in_out"}
        },
        {
            "name": "小範圍精密運動", 
            "params": {"angle_range": (80, 100), "speed": 0.5, "easing": "ease_in_out"}
        },
        {
            "name": "快速掃描運動",
            "params": {"angle_range": (20, 160), "speed": 2.0, "easing": "linear"}
        },
        {
            "name": "正弦波浪運動",
            "params": {"angle_range": (30, 150), "speed": 0.7, "easing": "sine"}
        },
    ]
    
    controller = SmoothServoController(channel=0)
    
    for i, example in enumerate(examples, 1):
        print(f"\n{i}. {example['name']}")
        print(f"   參數: {example['params']}")
        
        try:
            controller.smooth_pingpong(
                cycles=1,
                **example['params']
            )
            time.sleep(1)
            
        except KeyboardInterrupt:
            print("\n示例演示被中斷")
            break
        except Exception as e:
            print(f"示例錯誤: {e}")
    
    controller.stop()


# 主程序入口
if __name__ == "__main__":
    print("🚀 平滑舵機控制器啓動")
    
    try:
        while True:
            print("\n" + "=" * 60)
            print("🏠 主菜單")
            print("=" * 60)
            print("1. 基礎演示")
            print("2. 緩動函數對比")
            print("3. 交互式控制")
            print("4. 教學示例")
            print("5. 退出程序")
            
            choice = input("請選擇功能 (1-5): ").strip()
            
            if choice == "1":
                basic_demo()
            elif choice == "2":
                easing_comparison_demo()
            elif choice == "3":
                interactive_control()
            elif choice == "4":
                educational_examples()
            elif choice == "5":
                print("👋 感謝使用平滑舵機控制器!")
                break
            else:
                print("❌ 無效選擇,請重新輸入")
                
    except KeyboardInterrupt:
        print("\n\n👋 程序被用户中斷,再見!")
    except Exception as e:
        print(f"\n❌ 程序錯誤: {e}")

七、調試與優化建議

  • ⚙️ 如果舵機抖動或角度異常,檢查電源供電是否穩定;
  • 🔄 如果希望快速運動,可增大 speed
  • 🕒 若需自動停止,可設置 duration
  • 🔧 如需更高精度控制,可在 PCA9685 初始化時設置頻率(默認 50Hz)。

八、擴展方向

  • 支持多通道同步控制;
  • 增加多種緩動函數(ease-in、ease-out、bounce 等);
  • 支持實時控制(例如通過 MQTT 或 WebSocket 動態調整角度);
  • 加入舵機狀態反饋(角度、速度、電流檢測)。