MicroPython/RP2350ZERO/utils/servo.py

243 lines
9.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import utime
from machine import Pin, PWM
class ServoMotor:
"""微型伺服电机"""
def __init__(self, pin, frequency=50, min_angle=0, max_angle=180, min_pulsewidth=500, max_pulsewidth=2500, deadband_pulsewidth=8, duration_per_60deg=0.13):
"""
:param pin: 绑定引脚
:param frequency: PWM信号输出频率单位为赫兹
:param min_angle: 最小转动角度,单位为度
:param max_angle: 最大转动角度,单位为度
:param min_pulsewidth: 最小转动角度对应的脉宽,单位为微秒
:param max_pulsewidth: 最大转动角度对应的脉宽,单位为微秒
:param deadband_pulsewidth: 死区对应的脉宽,单位为微秒
:param duration_per_60deg: 转动六十度对应的时长,单位为秒/六十度
"""
print()
print(f"正在初始化ServoMotor绑定引脚{pin:2d}...", end="")
# 初始化最小转动角度和最大转动角度,单位为度
self.min_angle, self.max_angle = min_angle, max_angle
# 初始化最小转动角度对应的脉宽和最大转动角度对应的脉宽,单位为纳秒
self.min_pulsewidth, self.max_pulsewidth = min_pulsewidth * 1000, max_pulsewidth * 1000
# 初始化死区对应的脉宽,单位为纳秒
self.deadband_pulsewidth = deadband_pulsewidth * 1000
# 初始化单位角度对应的脉宽,单位为纳秒/度
self.pulsewidth_per_degree = (self.max_pulsewidth - self.min_pulsewidth) / (self.max_angle - self.min_angle)
# 初始化单位时长对应的脉宽,单位为纳秒/毫秒
self.pulsewidth_per_millisecond = self.pulsewidth_per_degree * 60 / duration_per_60deg / 1000
# 初始化目标角度对应的脉宽,单位为纳秒
self.target_pulsewidth = self.min_pulsewidth
# 初始化开始角度对应的脉宽和当前角度对应的脉宽,单位为纳秒
self.start_pulsewidth = self.current_pulsewidth = self.min_pulsewidth
# 初始化转动脉宽差,单位为纳秒
self.pulsewidth_difference = 0
# 初始化转动时长,单位为毫秒
self.duration = 1000
# 初始化转动步骤间延迟
self.delay = int(round(1000 / frequency)) + 10
# 初始化开始角度对应的时间戳和当前角度对应的时间戳,单位为毫秒
self.start_time = self.current_time = utime.ticks_ms()
# 初始化转动步骤迭代器
self.iterator = None
try:
self.pwm = PWM(Pin(pin, Pin.OUT)) # 初始化PWM
self.pwm.freq(frequency) # 设置频率
except Exception as exception:
raise RuntimeError(f"初始化发生异常:{str(exception)}")
# 打开PWM信号输出最小转动角度对应的脉宽
self.pwm.duty_ns(self.min_pulsewidth)
utime.sleep_ms(self.duration)
print("已完成")
def rotate(self, target_angle, duration=1000):
"""
将设置目标角度和执行转动步骤迭代器封装为转动接口
"""
print(f"{self._pulsewidth_to_angle(self.current_pulsewidth):3d}转动至{target_angle:3d},预期转动{duration:4d}毫秒...", end="")
if self._set_target_angle(target_angle, duration):
while self._execute():
pass
print(f"已完成,实际转动{utime.ticks_diff(self.current_time, self.start_time):4d}毫秒")
return True
print("设置目标角度失败:微型伺服电机正在转动")
return False
def _set_target_angle(self, target_angle, duration):
"""
设置目标角度
:param target_angle: 目标角度,单位为度
:param duration: 转动时长,单位为毫秒
"""
if not isinstance(target_angle, int):
raise TypeError("target_angle数据类型应为整数")
if not isinstance(duration, int):
raise TypeError("duration数据类型应为整数")
# 若正在转动则返回失败
if self._rotating:
return False
# 更新目标角度对应的脉宽
self.target_pulsewidth = self._angle_to_pulsewidth(target_angle)
# 更新开始角度对应的脉宽
self.start_pulsewidth = self.current_pulsewidth
# 更新转动脉宽差
self.pulsewidth_difference = self.target_pulsewidth - self.start_pulsewidth
# 若转动脉宽差的绝对值小于死区对应的脉宽则返回成功
if abs(self.pulsewidth_difference) < self.deadband_pulsewidth:
return True
# 更新转动时长
self.duration = max(
self._estimate_min_duration(self.pulsewidth_difference),
duration
)
# 更新当前角度对应的时间戳
self.current_time = utime.ticks_ms()
# 更新开始角度对应的时间戳
self.start_time = self.current_time
# 更新转动步骤迭代器
self.iterator = self._generate_iterator()
return True
@property
def _rotating(self):
"""是否正在转动"""
return self.iterator is not None
def _generate_iterator(self):
"""创建转动迭代器"""
while True:
# 当前角度对应的时间戳
current_time = utime.ticks_ms()
# 当前角度对应的进程
process = max(
0,
min(
1,
utime.ticks_diff(current_time, self.start_time) / self.duration
)
)
# 若当前角度对应的时间戳和上一次当前角度对应的时间戳大于等于转动步骤间延迟则继续,否则跳过
if utime.ticks_diff(current_time, self.current_time) >= self.delay:
# 当前角度对应的脉宽
current_pulsewidth = max(
self.min_pulsewidth,
min(
self.max_pulsewidth,
int(round(self.start_pulsewidth + self.pulsewidth_difference * self._easing(process)))
)
)
# 若转动脉宽差的绝对值大于等于死区对应的脉宽则继续
if abs(current_pulsewidth - self.current_pulsewidth) >= self.deadband_pulsewidth:
# 打开PWM信号输出当前角度对应的脉宽
self.pwm.duty_ns(current_pulsewidth)
# 更新当前角度对应的脉宽
self.current_pulsewidth = current_pulsewidth
# 更新当前角度对应的时间戳
self.current_time = current_time
# 若当前角度对应的进程大于等于1则继续
if process >= 1:
# 打开PWM信号输出当前角度对应的脉宽
self.pwm.duty_ns(self.target_pulsewidth)
# 更新当前角度对应的脉宽
self.current_pulsewidth = self.target_pulsewidth
# 更新当前角度对应的时间戳
self.current_time = utime.ticks_ms()
yield
break
yield
@staticmethod
def _easing(process):
"""
缓动函数:根据当前角度对应的进程计算当前角度
:param process: 当前角度对应的进程
"""
return 3 * process ** 2 - 2 * process ** 3
def _execute(self):
"""执行转动步骤迭代器"""
if self._rotating:
try:
next(self.iterator)
return True
except StopIteration:
self._stop()
return False
def _stop(self):
"""停止转动"""
self.iterator = None
def _estimate_min_duration(self, pulsewidth_difference):
"""
估计最小转动时长
:param pulsewidth_difference: 转动脉宽差,单位为纳秒
"""
min_duration = int(round(abs(pulsewidth_difference) / self.pulsewidth_per_millisecond))
return min_duration
def _angle_to_pulsewidth(self, angle):
"""
将角度转为角度对应的脉宽
:param angle: 角度,单位为度
"""
pulsewidth = max(
self.min_pulsewidth,
min(
self.max_pulsewidth,
int(round(self.min_pulsewidth + (angle - self.min_angle) * self.pulsewidth_per_degree))
)
)
return pulsewidth
def _pulsewidth_to_angle(self, pulsewidth):
"""
将角度对应的脉宽转为角度
:param pulsewidth: 角度对应的脉宽,单位为纳秒
"""
angle = max(
self.min_angle,
min(
self.max_angle,
int(round(self.min_angle + (pulsewidth - self.min_pulsewidth) / self.pulsewidth_per_degree))
)
)
return angle
class SG90(ServoMotor):
"""SG90适用于SG90和MG90S等舵机"""
def __init__(self, pin):
super().__init__(
pin=pin,
)
"""
使用示例
引脚0→舵机信号引脚
import utime
from utils.servo import SG90
servo = SG90(pin=0)
servo.rotate(target_angle=180, duration=1000)
"""