Skip to content

第 5 章:控制舵机转动(SG90)

5.1 模块原理:舵机与 PWM 角度控制

舵机(伺服电机)与普通直流电机的区别在于:它不连续旋转,而是根据控制信号转动到指定角度并保持。本车使用的 SG90 舵机控制原理为:信号端接收周期 20ms(频率 50Hz)的 PWM 信号,脉冲宽度在 0.5ms~2.5ms 之间变化时,输出轴对应转到 0°~180° 范围内的相应角度,脉冲宽度与角度近似线性对应(如 1.5ms 对应 90° 中位)。本车 SG90 从俯视角度看为逆时针旋转 180°。

SG90 有三根引线:棕色为 GND,红色为 VCC(5V),橙色为信号线。D3 上的接线为:信号线接 GP15,供电接拓展板 USB(5V)供电输出区(红 + 棕 −,10 公母)。舵机上方安装超声波模块(⑦),改变舵机角度即可改变探测方向。

在 Mind+ 中,舵机控制积木已封装脉宽换算,直接设置目标角度即可。使用前需先在"扩展"中加载舵机(执行器)模块。

SG90 舵机支持 0–180° 逆时针转动。实际编程建议设定 10–170°,预留约 10° 余量,防止过转卡死。

Mind+ 加载舵机模块

SG90 舵机 PWM 角度控制原理

5.2 接线检查

接线出厂时已完成,编程前确认舵机插头无松动即可。舵机供电为 5V,信号电平由 Pico 输出,两者已在拓展板上完成匹配。

5.3 代码实现:三个角度定位

目标:舵机依次转到 0° → 90° → 180° → 90°,每个角度停留 1 秒,循环执行。观察舵机转向与角度值的对应关系。

运行以下代码,如果发现舵机设置为90°时,超声波模块两只小眼睛没有真对正前方,请转跳至6.3舵机回正90°解决方法查看操作方法。

舵机三角度定位积木

python
from machine import Pin, PWM
import time

# SG90 舵机:50Hz 固定频率,角度靠"脉宽"控制
servo = PWM(Pin(15), freq=50)

def servo_angle(a):
    # 舵机 0~180° 对应高电平脉宽 0.5ms ~ 2.5ms(周期 20ms)
    # 换算成 duty_u16:0.5/20 × 65535 ≈ 1638,2.5/20 × 65535 ≈ 8191
    # 1638 + (a/180) × 6553 就把角度线性映射到脉宽
    servo.duty_u16(int(1638 + (a / 180) * 6553))

while True:
    servo_angle(10)    # 打 10° 而不打 0°:留余量,防止舵机顶到机械极限卡死
    time.sleep(1)
    servo_angle(90)    # 正前方
    time.sleep(1)
    servo_angle(170)   # 同理,170° 代替 180°
    time.sleep(1)
    servo_angle(90)
    time.sleep(1)
cpp
#include <Servo.h>

Servo servo15;   // GP15 上的 SG90 舵机

void setup() {
  servo15.attach(15);   // Servo 库自动生成 50Hz 舵机信号
}

void loop() {
  // write(角度) 直接给角度,库内部完成脉宽换算
  servo15.write(10);    // 打 10° 而不打 0°:留余量,防止舵机顶到机械极限卡死
  delay(1000);
  servo15.write(90);    // 正前方
  delay(1000);
  servo15.write(170);   // 同理,170° 代替 180°
  delay(1000);
  servo15.write(90);
  delay(1000);
}

注意:舵机从当前角度转到目标角度需要时间(SG90 约 0.1 秒/60°)。程序中每次设置角度后必须保留足够的等待时间(大角度转动至少 0.5~1 秒),否则会出现"指令已执行、舵机未到位"的错位,后续动作将全部跑偏。

5.4 代码实现:带动超声波改变探测方向

目标:舵机带动超声波模块依次停在 45°、90°、135° 三个方向,各停留 1 秒。该动作是第 7 章(避障与扫描跟随)中"扫描"功能的基础。

舵机带动超声波转向积木

python
from machine import Pin, time_pulse_us
import time

# 超声波:Trig 触发脚 GP21(输出),Echo 回响脚 GP20(输入)
trig = Pin(21, Pin.OUT)
echo = Pin(20, Pin.IN)

def distance():
    # 原理:发一束超声波,等它撞到障碍物反射回来
    # 距离 = 声速 × 往返时间 ÷ 2(÷2 因为声波走了来回两趟)
    trig.value(0)
    time.sleep_us(2)
    trig.value(1)          # 拉高 10 微秒 = 发出触发信号
    time.sleep_us(10)
    trig.value(0)
    # time_pulse_us 测量 Echo 脚高电平持续的微秒数,超时 30ms 返回 -1
    # 0.0343 = 声速 343m/s 换算成 cm/μs
    return time_pulse_us(echo, 1, 30000) * 0.0343 / 2

def scan3():
    # 对应 Mind+ 自定义积木「舵机往复运动带动超声波改变探测方向」
    # 俯视逆时针数角度:45° = 右侧,90° = 正前方,135° = 左侧
    # 每到一个位置先等 1 秒让舵机停稳再测,否则读到的是转动途中的数据
    servo_angle(45)
    time.sleep(1)
    print("右侧 45° 距离:", distance(), "厘米")
    servo_angle(90)
    time.sleep(1)
    print("前方 90° 距离:", distance(), "厘米")
    servo_angle(135)
    time.sleep(1)
    print("左侧 135° 距离:", distance(), "厘米")
    # 回程:135° → 90°,顺便复测一次正前方
    # (截图第 4 个舵机积木是 135° 的笔误,应为 90°,代码按 90° 写)
    servo_angle(90)
    time.sleep(1)
    print("前方 90° 距离:", distance(), "厘米")

# 主程序里循环调用 scan3() 即可,例如:
# while True:
#     scan3()
cpp
// 超声波:Trig 触发脚 GP21(输出),Echo 回响脚 GP20(输入)
const int TRIG = 21;
const int ECHO = 20;

float distance() {
  // 原理:发一束超声波,等它撞到障碍物反射回来
  // 距离 = 声速 × 往返时间 ÷ 2(÷2 因为声波走了来回两趟)
  digitalWrite(TRIG, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG, HIGH);   // 拉高 10 微秒 = 发出触发信号
  delayMicroseconds(10);
  digitalWrite(TRIG, LOW);
  // pulseIn 测量 Echo 脚高电平持续的微秒数,超时 30000μs 返回 0
  // 0.0343 = 声速 343m/s 换算成 cm/μs
  return pulseIn(ECHO, HIGH, 30000) * 0.0343 / 2;
}

void scan3() {
  // 对应 Mind+ 自定义积木「舵机往复运动带动超声波改变探测方向」
  // 俯视逆时针数角度:45° = 右侧,90° = 正前方,135° = 左侧
  // 每到一个位置先等 1 秒让舵机停稳再测
  servo15.write(45);
  delay(1000);
  Serial.print("右侧 45° 距离:"); Serial.print(distance()); Serial.println(" 厘米");
  servo15.write(90);
  delay(1000);
  Serial.print("前方 90° 距离:"); Serial.print(distance()); Serial.println(" 厘米");
  servo15.write(135);
  delay(1000);
  Serial.print("左侧 135° 距离:"); Serial.print(distance()); Serial.println(" 厘米");
  // 回程:135° → 90°,顺便复测一次正前方
  // (截图第 4 个舵机积木是 135° 的笔误,应为 90°,代码按 90° 写)
  servo15.write(90);
  delay(1000);
  Serial.print("前方 90° 距离:"); Serial.print(distance()); Serial.println(" 厘米");
}

// 主程序里循环调用 scan3() 即可,例如:
// void loop() {
//   scan3();
// }

不推崇暴力堆叠的屎山代码。上述逻辑可用循环优化:每0.1秒旋转1°,实现精度更高的180°旋转。

5.5 代码实现:舵机带动超声波实现雷达效果

雷达效果积木

python
def radar():
    # 对应 Mind+ 自定义积木「舵机往复运动带动超声波改变探测方向实现雷达效果」
    k = 1                  # 步长方向:+1 角度越来越大,-1 角度越来越小
    starting_Angle = 10    # 当前角度,从 10° 出发
    servo_angle(starting_Angle)
    time.sleep(2)          # 先停 2 秒,等舵机转到起始位再开始扫描
    while True:
        # 关键一步:角度每轮走一步(k 是 +1 就往前,是 -1 就往回)
        starting_Angle = starting_Angle + k
        servo_angle(starting_Angle)
        time.sleep(0.1)    # 每步停 0.1 秒,扫描更平滑
        if starting_Angle >= 170:
            k = -1         # 扫到 170° 掉头:步长变负,角度开始往回走
        if starting_Angle <= 10:
            k = 1          # 回到 10° 再掉头:步长变正,重新向前
    # 两个"如果"配合,舵机就在 10° ↔ 170° 之间往返摆头,这就是雷达效果
cpp
// 对应 Mind+ 自定义积木「舵机往复运动带动超声波改变探测方向实现雷达效果」
int k = 1;                  // 步长方向:+1 角度越来越大,-1 角度越来越小
int starting_Angle = 10;    // 当前角度,从 10° 出发

void setup() {
  servo15.attach(15);
  servo15.write(starting_Angle);
  delay(2000);              // 先停 2 秒,等舵机转到起始位再开始扫描
}

void loop() {
  // 关键一步:角度每轮走一步(k 是 +1 就往前,是 -1 就往回)
  starting_Angle = starting_Angle + k;
  servo15.write(starting_Angle);
  delay(100);               // 每步停 0.1 秒,扫描更平滑
  if (starting_Angle >= 170) {
    k = -1;                 // 扫到 170° 掉头:步长变负,角度开始往回走
  }
  if (starting_Angle <= 10) {
    k = 1;                  // 回到 10° 再掉头:步长变正,重新向前
  }
  // 两个"如果"配合,舵机就在 10° ↔ 170° 之间往返摆头,这就是雷达效果
}

重要的事情说三遍:SG90舵机沿逆时针方向可以实现0-180角度转动,实际编程中,应当留有余量10左右余量,过转会卡死,推荐程序设定10-170°,留有余量。

不积跬步,无以至千里;万里之行,始于当下。