第23章:舵机与电机控制

本章介绍舵机、直流电机、130电机的驱动原理与控制方法。

本章的三类执行器在控制方式上差别很大:舵机收脉宽、直流电机收方向和占空比、风扇模块收使能信号。它们共同的前提是电源——大电流负载不能从 Pico 的 3V3 或 VBUS 直接取电,否则电压跌落会让板子复位。接线之前先把供电方案定好,再谈代码。


23.1 舵机控制

23.1.1 舵机工作原理

舵机内部包含:

  • 直流电机
  • 减速齿轮组
  • 位置传感器
  • 控制电路

通过 PWM 信号控制角度。

23.1.2 PWM 信号规范

舵机不是普通的直流电机,它内部有减速齿轮、位置反馈和控制电路,接收的是角度指令而不是电压。控制信号是周期 20ms(50Hz)的 PWM,高电平宽度决定目标角度:0.5ms 对应一端,2.5ms 对应另一端,中间大致线性。这个范围只是常见标准,不同型号的舵机实际行程有偏差,有的只接受 0.4-2.6ms,有的把 500-2500 微秒映射到 180°。用满量程之前建议先小幅测试,找到不顶机械限位的两侧边界,再写进程序。

周期: 20ms (50Hz)
脉宽: 0.5ms (0°) ~ 2.5ms (180°)

RP2040 的 PWM 外设给出的是 16 位占空比,duty_u16 的取值 0-65535 对应一个完整周期。50Hz 时周期是 20ms,每个计数约 0.305 微秒,所以 0.5ms 对应 65535×0.5/20≈1638,2.5ms 对应 65535×2.5/20≈8192——代码里两个常数就是这么来的。弄不清这个换算,改频率或改量程时就不知道该填多少。

23.1.3 舵机驱动类

'''
舵机驱动类
'''
from machine import Pin, PWM

class Servo:
    def __init__(self, pin):
        self.pwm = PWM(Pin(pin))
        self.pwm.freq(50)
        self.min_duty = 1638   # 0.5ms
        self.max_duty = 8192   # 2.5ms
        self.current_angle = 90
    
    def set_angle(self, angle):
        """设置角度 (0-180)"""
        angle = max(0, min(180, angle))
        duty = int(self.min_duty + (self.max_duty - self.min_duty) * angle / 180)
        self.pwm.duty_u16(duty)
        self.current_angle = angle
    
    def smooth_move(self, target, step=1, delay_ms=20):
        """平滑移动"""
        import time
        
        while self.current_angle != target:
            if self.current_angle < target:
                self.current_angle = min(self.current_angle + step, target)
            else:
                self.current_angle = max(self.current_angle - step, target)
            self.set_angle(self.current_angle)
            time.sleep_ms(delay_ms)
    
    def release(self):
        """释放舵机"""
        self.pwm.duty_u16(0)

# 使用
servo = Servo(16)
servo.set_angle(0)
time.sleep(1)
servo.smooth_move(180)

这个类把换算封装在 set_angle 里,对外只暴露角度。set_angle 先用 max/min 把输入夹到 0-180,避免越界脉宽把舵机顶到限位;然后按线性关系把角度映射成占空比,写入 PWM 并记录当前角度,供 smooth_move 使用。 smooth_move 从当前角度逐度靠近目标,每步等 20ms:从 0 到 180 需要 180 步,约 3.6 秒,这个速度对多数小舵机合适。步长和延迟是两个可调参数,减小它们动作更快,但太小会让舵机来不及响应,表现为抖动或电流声。release 把占空比清零,舵机失去保持力矩,好处是不耗电、不发热,代价是外力可以把轴转动;机械臂这类需要保持位置的场合不要随便调用。


23.2 直流电机控制

23.2.1 驱动原理

GPIO 不能直接驱动直流电机,原因有三个:引脚输出电流只有毫安级,电机启动电流可达安培级;电机是感性负载,断电瞬间的反电动势会击穿引脚;正反转需要改变电流方向,单端输出做不到。所以中间必须加驱动芯片,常见的是 L298N、L9110、TB6612 这类 H 桥。上图的接法把两个 GPIO 分别接到 H 桥的两个输入,芯片输出接电机两端:一个高一个低决定转向,两个都低是自由停车,两个都高是短接绕组刹车。

直流电机需要驱动电路(如 L298N、L9110):

GPIO1 → 驱动芯片 → 电机+
GPIO2 → 驱动芯片 → 电机-

正转: GPIO1=高, GPIO2=低
反转: GPIO1=低, GPIO2=高
停止: GPIO1=低, GPIO2=低

23.2.2 PWM 调速

PWM 调速的本质是快速通断,用占空比等效出不同的平均电压。频率选 1kHz,避开了人耳最敏感的中频段,又不会让 H 桥来不及开关。占空比用 16 位表示:speed 是百分比,乘 65535 再除 100 得到计数值。代码里 forward 时 pwm1 输出占空比、pwm2 输出 0,backward 反过来,stop 两个都置 0。刹车函数把两个都置 65535,等于让电机两端同电位,绕组被短接,动能转成制动电流,停得快但电流大;自由停车则靠惯性滑行,适合不需要精确定位的场合。

'''
直流电机 PWM 调速
'''
from machine import Pin, PWM

class DCMotor:
    def __init__(self, pin1, pin2):
        self.pwm1 = PWM(Pin(pin1))
        self.pwm2 = PWM(Pin(pin2))
        self.pwm1.freq(1000)
        self.pwm2.freq(1000)
    
    def forward(self, speed=100):
        """正转"""
        duty = int(speed / 100 * 65535)
        self.pwm1.duty_u16(duty)
        self.pwm2.duty_u16(0)
    
    def backward(self, speed=100):
        """反转"""
        duty = int(speed / 100 * 65535)
        self.pwm1.duty_u16(0)
        self.pwm2.duty_u16(duty)
    
    def stop(self):
        """停止"""
        self.pwm1.duty_u16(0)
        self.pwm2.duty_u16(0)
    
    def brake(self):
        """刹车"""
        self.pwm1.duty_u16(65535)
        self.pwm2.duty_u16(65535)

# 使用
motor = DCMotor(14, 15)
motor.forward(50)  # 50% 速度正转
time.sleep(2)
motor.backward(30)  # 30% 速度反转
time.sleep(2)
motor.stop()

这段实现有一个可以改进的地方:PWM 加在两个方向引脚上,靠通断来调速,低速时 H 桥频繁开关,效率和噪音都不理想。更常见的做法是把其中一个引脚设成固定电平定方向,另一个引脚接 H 桥的使能端,用 PWM 调使能。另外,代码假设电机两端不会同时为高,两路初始化后不会出现直通,这一点必须在驱动芯片允许的范围内确认。


23.3 130 电机风扇模块

23.3.1 模块介绍

130 电机风扇模块包含:

  • 130 微型直流电机
  • 风扇叶片
  • 驱动电路

23.3.2 控制代码

130 电机风扇模块把微型直流电机、扇叶和驱动电路做在一起,对外只留电源与信号线。信号线接的是模块上的使能/控制端,不是电机绕组,所以可以直接用 GPIO 的 PWM 控制转速。模块的电源要按电机额定电压单独供,Pico 只负责给信号。

'''
实验:130 风扇电机控制
'''
from machine import Pin, PWM

fan = PWM(Pin(14))
fan.freq(1000)

def set_fan_speed(percent):
    """设置风扇速度 (0-100%)"""
    duty = int(percent / 100 * 65535)
    fan.duty_u16(duty)

# 风速等级
speeds = [0, 30, 60, 100]

for speed in speeds:
    print(f"风速: {speed}%")
    set_fan_speed(speed)
    time.sleep(3)

这段代码把百分比换算成 16 位占空比,然后用列表依次试 0、30、60、100 四档,每档停 3 秒。实际使用有两点经验:一是风扇启动需要克服静摩擦,占空比太低时可能只发出嗡嗡声而不转,堵转电流反而更大,通常要给 20%-30% 的起步占空比;二是 1kHz 的 PWM 对风扇可能产生可听啸叫,能试的频率范围取决于模块驱动电路,宁可用 20kHz 以上,也要先确认模块支持。

23.3.3 温控风扇

'''
实验:温控风扇
'''
from machine import ADC

temp_sensor = ADC(4)

def read_temperature():
    raw = temp_sensor.read_u16()
    voltage = raw * 3.3 / 65535
    return 27 - (voltage - 0.706) / 0.001721

while True:
    temp = read_temperature()
    
    # 温度-速度映射
    if temp < 30:
        speed = 0
    elif temp < 35:
        speed = 30
    elif temp < 40:
        speed = 60
    else:
        speed = 100
    
    set_fan_speed(speed)
    print(f"温度: {temp:.1f}°C  风速: {speed}%")
    time.sleep(2)

温度来源是 RP2040 片内温度传感器,接在 ADC 通道 4 上,不是外部引脚。read_u16 返回 0-65535,按 3.3V 参考折算成电压,再套数据手册给的经验公式:27°C 时输出约 0.706V,温度每升高 1°C 电压下降约 1.721mV,所以 T = 27 − (V − 0.706) / 0.001721。要注意这个值是芯片结温,比环境温度高一些,而且受板子发热影响,用它做风扇控制时最好实测标定一个偏移量。另外,分段阈值在边界附近会反复切换,比如温度正好在 35°C 上下波动时风扇会频繁启停,加一点迟滞(35°C 开、33°C 关)就能解决。


23.4 多舵机协调

23.4.1 机械臂控制

多个舵机一起用时,最容易被忽略的是电源和机械限位。四个小舵机同时动作,峰值电流可以到 2-3A,远超 Pico 板载稳压器的能力,必须用独立的 5V 电源,并且与 Pico 共地——只连信号不连地,舵机会乱抖。机械限位则要在软件里留余量:程序里的 0-180 是舵机脉宽范围,不等于机械臂实际能到的角度,装上连杆后先手动找两侧极限,再把软件范围收窄十几度,避免每次都顶到限位。

'''
实验:简易机械臂
'''
class RobotArm:
    def __init__(self, base_pin, shoulder_pin, elbow_pin, gripper_pin):
        self.base = Servo(base_pin)
        self.shoulder = Servo(shoulder_pin)
        self.elbow = Servo(elbow_pin)
        self.gripper = Servo(gripper_pin)
        self.home()
    
    def home(self):
        """归位"""
        self.base.set_angle(90)
        self.shoulder.set_angle(90)
        self.elbow.set_angle(90)
        self.gripper.set_angle(90)
    
    def grab(self):
        """抓取"""
        self.gripper.smooth_move(30)
    
    def release(self):
        """释放"""
        self.gripper.smooth_move(90)
    
    def pick_and_place(self, from_pos, to_pos):
        """拾取放置"""
        # 移动到拾取位置
        self.base.smooth_move(from_pos[0])
        self.shoulder.smooth_move(from_pos[1])
        self.elbow.smooth_move(from_pos[2])
        
        # 抓取
        self.grab()
        time.sleep(0.5)
        
        # 移动到放置位置
        self.shoulder.smooth_move(90)  # 抬起
        self.base.smooth_move(to_pos[0])
        self.shoulder.smooth_move(to_pos[1])
        self.elbow.smooth_move(to_pos[2])
        
        # 释放
        self.release()

# 使用
arm = RobotArm(16, 17, 18, 19)
arm.pick_and_place((45, 60, 120), (135, 60, 120))

这个类把四个舵机包装成一组关节。home 把所有关节回到 90° 中位,grab 与 release 通过小角度变化控制夹爪,pick_and_place 按「基座—肩—肘」的顺序靠近目标,抓起后先抬肩再转基座,最后在放置点展开。动作顺序有实际意义:如果先转基座再抬肩,末端会沿着弧线扫过工作区,容易碰到旁边的物体。代码里两处肩关节都给了 90°,相当于每次返回同一个安全高度,真实应用应该把中间位单独定义成常量,便于调整。

23.5 本章小结

本章介绍了执行器控制:

  1. 舵机控制:
    • PWM 信号控制角度
    • 平滑移动实现
  2. 直流电机:
    • H 桥驱动原理
    • PWM 调速
  3. 应用案例:
    • 温控风扇
    • 机械臂控制

下一章将学习综合项目实践。