一 本章简介
本章介绍如何使用 MicroPython 的 PWM 功能控制舵机(Servo)。舵机是一种位置伺服驱动器,广泛应用于玩具机器人关节、云台、机械臂、遥控模型等场景。通过改变 PWM 信号的脉冲宽度,可以精确控制舵机转动到指定角度。
筑基学习板提供了 2 路舵机接口,复用了电机 2 的 PWM 引脚(PB14/PB15),通过拨码开关 BIT5 切换。
IMPORTANT
舵机接口与直流电机 2 共用 PB14/PB15 引脚,通过拨码开关 BIT5 切换:
- BIT5 = OFF(默认,向下)→ 舵机接口
- BIT5 = ON(向上)→ 直流电机 2
使用舵机时,BIT5 必须保持在 OFF 位置(默认状态)。
1.1 学习目标
| 序号 | 学习目标 | 重要程度 |
|---|---|---|
| 1 | 了解舵机的工作原理(PWM 脉宽与角度的关系) | ⭐⭐⭐⭐⭐ |
| 2 | 掌握使用 TIM12 输出 50Hz PWM 控制舵机的方法 | ⭐⭐⭐⭐⭐ |
| 3 | 理解筑基学习板舵机电路的 PWM 反向特性 | ⭐⭐⭐⭐⭐ |
| 4 | 能够实现舵机角度控制和扫描运动 | ⭐⭐⭐⭐ |
1.2 重点提示
- 舵机接口使用 TIM12_CH1(PB14) 和 TIM12_CH2(PB15),可同时控制 2 个舵机。
- 拨码开关 BIT5 必须在 OFF 位置(默认状态),否则 PWM 信号会被路由到直流电机 2,舵机不会动作。
- 筑基学习板的舵机接口做了 3.3V→5V 电平转换(通过三极管升压),这会导致 PWM 信号反向——MCU 输出高电平时,舵机端实际收到低电平,反之亦然。代码中需要对占空比做取反处理。
- 标准舵机的控制信号为 50Hz PWM(周期 20ms),脉冲宽度 0.5ms~2.5ms 对应 0°~180°。
1.3 基础概念与术语
- 舵机(Servo):一种内置电机、减速齿轮和控制电路的位置伺服装置,通过 PWM 信号控制输出轴转动到指定角度并保持。
- 脉冲宽度(Pulse Width):PWM 信号中高电平持续的时间。标准舵机用 0.5ms~2.5ms 的脉宽对应 0°~180° 的角度。
- PWM 反向:由于筑基学习板舵机电路使用三极管做电平转换,三极管的开关特性会将信号反相。MCU 输出的 PWM 到达舵机时,高低电平互换。
二 硬件说明
2.1 舵机工作原理
标准 180° 舵机接收 50Hz 的 PWM 信号(周期 20ms),根据脉冲宽度决定转动角度:
| 脉冲宽度 | 对应角度 | 占空比(20ms 周期) |
|---|---|---|
| 0.5ms | 0° | 2.5% |
| 1.0ms | 45° | 5.0% |
| 1.5ms | 90°(中位) | 7.5% |
| 2.0ms | 135° | 10.0% |
| 2.5ms | 180° | 12.5% |
2.2 筑基学习板舵机电路特性

筑基学习板的舵机接口有两个重要特性:
1️⃣ 5V 电平输出
我们假定驱动的舵机为5V电平的,默认舵机信号线需要 5V 电平,而天空星 GPIO 输出 3.3V。筑基学习板通过 MOS 管将 3.3V 信号转换为 5V 信号输出给舵机。
2️⃣ PWM 信号反向
由于电平转换电路的特性,信号会被反相:
- MCU 输出高电平 → 舵机端收到低电平
- MCU 输出低电平 → 舵机端收到高电平
这意味着我们在代码中设置的占空比,到达舵机时是取反的。例如,要让舵机收到 7.5% 的占空比(90°),MCU 需要输出 100% - 7.5% = 92.5% 的占空比。
WARNING
PWM 反向是筑基学习板舵机电路的硬件特性,不是软件 bug。如果你直接按标准舵机的占空比设置,舵机会转到错误的角度或者完全不动。代码中必须做取反处理。
2.3 舵机资源汇总

| 参数 | 舵机 1 | 舵机 2 |
|---|---|---|
| 控制引脚 | PB14 | PB15 |
| 定时器通道 | TIM12_CH1 | TIM12_CH2 |
| PWM 频率 | 50Hz | 50Hz |
| 拨码开关 | BIT5 = OFF | BIT5 = OFF |
| 信号电平 | 5V(板载电平转换) | 5V(板载电平转换) |
| PWM 反向 | 是(需取反) | 是(需取反) |
| 供电 | 5V(舵机接口已供电) | 5V(舵机接口已供电) |
市面上购买的舵机,最常见的定义为:中间红线为电源正极 ,棕色线(或黑色)为电源负极 ,橙色线(或黄色/白色)为信号线。如果大家不确定具体定义的话,请询问舵机厂家确认。
2.4 拨码开关 BIT5 设置
CAUTION
使用舵机接口时,拨码开关 BIT5 必须在 OFF 位置(向下,默认状态)。如果 BIT5 被拨到 ON,PB14/PB15 的 PWM 信号会被路由到直流电机 2 的驱动芯片,舵机将无法工作。
| BIT5 状态 | 效果 |
|---|---|
| OFF(默认,向下) | PB14/PB15 连接到舵机接口 |
| ON(向上) | PB14/PB15 连接到直流电机 2 |
三 软件设计
3.1 基础部分
3.1.1 舵机转到指定角度
由于 PWM 反向,我们需要用 100 - duty 来取反。以下代码控制舵机 1(PB14)转到 90°:
# 舵机基本控制(PB14,TIM12_CH1)
from pyb import Pin, Timer
import time
# PB14 -> TIM12_CH1
pin = Pin('PB14', Pin.AF_PP, af=Pin.AF9_TIM12)
tim = Timer(12, freq=50) # 舵机标准频率 50Hz
ch = tim.channel(1, Timer.PWM, pin=pin)
def set_servo_angle(channel, angle):
"""
设置舵机角度(0~180°)
考虑了 PWM 反向:MCU 输出取反后的占空比
"""
# 标准舵机:0.5ms~2.5ms 对应 0°~180°
# 50Hz 周期 = 20ms
# 占空比 = 脉宽 / 周期 * 100%
# 0° -> 0.5ms -> 2.5%
# 180° -> 2.5ms -> 12.5%
duty = 2.5 + (angle / 180.0) * 10.0 # 2.5% ~ 12.5%
# PWM 反向:取反
inverted_duty = 100.0 - duty
channel.pulse_width_percent(inverted_duty)
# 转到 0°
print("舵机转到 0°")
set_servo_angle(ch, 0)
time.sleep(1)
# 转到 90°(中位)
print("舵机转到 90°")
set_servo_angle(ch, 90)
time.sleep(1)
# 转到 180°
print("舵机转到 180°")
set_servo_angle(ch, 180)
time.sleep(1)
# 回到中位
print("舵机回到 90°")
set_servo_angle(ch, 90)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
3.1.2 舵机来回扫描
# 舵机来回扫描(0° ↔ 180°)
from pyb import Pin, Timer
import time
pin = Pin('PB14', Pin.AF_PP, af=Pin.AF9_TIM12)
tim = Timer(12, freq=50)
ch = tim.channel(1, Timer.PWM, pin=pin)
def set_angle(channel, angle):
duty = 2.5 + (angle / 180.0) * 10.0
channel.pulse_width_percent(100.0 - duty) # PWM 反向
print("舵机扫描启动,Ctrl+C 停止")
try:
while True:
# 0° -> 180°
for angle in range(0, 181, 2):
set_angle(ch, angle)
time.sleep_ms(15)
# 180° -> 0°
for angle in range(180, -1, -2):
set_angle(ch, angle)
time.sleep_ms(15)
except KeyboardInterrupt:
set_angle(ch, 90)
print("已停止,舵机回到中位")2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
3.1.3 同时控制两个舵机
# 同时控制两个舵机
from pyb import Pin, Timer
import time
# 舵机 1: PB14 -> TIM12_CH1
# 舵机 2: PB15 -> TIM12_CH2
pin1 = Pin('PB14', Pin.AF_PP, af=Pin.AF9_TIM12)
pin2 = Pin('PB15', Pin.AF_PP, af=Pin.AF9_TIM12)
tim = Timer(12, freq=50) # 两个通道共享同一个定时器和频率
ch1 = tim.channel(1, Timer.PWM, pin=pin1)
ch2 = tim.channel(2, Timer.PWM, pin=pin2)
def set_angle(channel, angle):
duty = 2.5 + (angle / 180.0) * 10.0
channel.pulse_width_percent(100.0 - duty)
# 舵机 1 转到 0°,舵机 2 转到 180°
set_angle(ch1, 0)
set_angle(ch2, 180)
time.sleep(1)
# 两个舵机都转到 90°
set_angle(ch1, 90)
set_angle(ch2, 90)
time.sleep(1)
# 交替运动
print("交替运动,Ctrl+C 停止")
try:
while True:
set_angle(ch1, 45)
set_angle(ch2, 135)
time.sleep_ms(500)
set_angle(ch1, 135)
set_angle(ch2, 45)
time.sleep_ms(500)
except KeyboardInterrupt:
set_angle(ch1, 90)
set_angle(ch2, 90)
print("已停止")2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
3.2 进阶部分
3.2.1 舵机驱动类封装
# 舵机驱动类(适配筑基学习板 PWM 反向)
from pyb import Pin, Timer
import time
class Servo:
"""
舵机驱动类
适配筑基学习板的 PWM 反向电路
"""
def __init__(self, pin_name, tim_id, ch_id, af,
min_us=500, max_us=2500, inverted=True):
"""
pin_name: 引脚名称
tim_id: 定时器编号
ch_id: 通道编号
af: 复用功能编号
min_us: 最小脉宽(微秒),对应 0°
max_us: 最大脉宽(微秒),对应 180°
inverted: 是否 PWM 反向(筑基学习板为 True)
"""
self.pin = Pin(pin_name, Pin.AF_PP, af=af)
self.tim = Timer(tim_id, freq=50)
self.ch = self.tim.channel(ch_id, Timer.PWM, pin=self.pin)
self.min_us = min_us
self.max_us = max_us
self.inverted = inverted
self._angle = 90
self.write(90) # 初始中位
def write(self, angle):
"""设置舵机角度(0~180)"""
angle = max(0, min(180, angle))
self._angle = angle
# 脉宽(微秒)
pulse_us = self.min_us + (angle / 180.0) * (self.max_us - self.min_us)
# 占空比(50Hz 周期 = 20000us)
duty = (pulse_us / 20000.0) * 100.0
if self.inverted:
duty = 100.0 - duty
self.ch.pulse_width_percent(duty)
def read(self):
"""读取当前角度"""
return self._angle
def sweep(self, start=0, end=180, step=2, delay_ms=15):
"""从 start 扫描到 end"""
if start < end:
for a in range(start, end + 1, step):
self.write(a)
time.sleep_ms(delay_ms)
else:
for a in range(start, end - 1, -step):
self.write(a)
time.sleep_ms(delay_ms)
# 使用示例
servo1 = Servo('PB14', 12, 1, Pin.AF9_TIM12)
servo2 = Servo('PB15', 12, 2, Pin.AF9_TIM12)
# 转到指定角度
servo1.write(0)
servo2.write(180)
time.sleep(1)
# 扫描
servo1.sweep(0, 180)
servo1.sweep(180, 0)
# 回到中位
servo1.write(90)
servo2.write(90)
print("完成")2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
3.2.2 按键控制舵机角度
用 PA0 和 PE8 两个按键分别控制舵机左转和右转:
# 按键控制舵机角度
from pyb import Pin, Timer
import pyb
pin = Pin('PB14', Pin.AF_PP, af=Pin.AF9_TIM12)
tim = Timer(12, freq=50)
ch = tim.channel(1, Timer.PWM, pin=pin)
btn_left = Pin('PA0', Pin.IN, Pin.PULL_DOWN) # 左转
btn_right = Pin('PE8', Pin.IN, Pin.PULL_DOWN) # 右转
def set_angle(angle):
duty = 2.5 + (angle / 180.0) * 10.0
ch.pulse_width_percent(100.0 - duty)
angle = 90
set_angle(angle)
print("当前角度: {}°".format(angle))
print("PA0=左转 PE8=右转")
while True:
if btn_left.value() == 1:
angle = max(0, angle - 5)
set_angle(angle)
print("角度: {}°".format(angle))
pyb.delay(150)
if btn_right.value() == 1:
angle = min(180, angle + 5)
set_angle(angle)
print("角度: {}°".format(angle))
pyb.delay(150)
pyb.delay(10)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
四 常见问题
Q: 舵机完全不动?
- 检查拨码开关 BIT5:必须在 OFF 位置(向下,默认状态)。
- 确认舵机已正确插入舵机接口(3P 排针:信号、电源、地)。
- 确认供电正常。舵机接口的 5V 来自底板电源,如果只用 TYPE-C 供电,电流可能不够驱动舵机。建议通过 DC 头来给筑基学习板供电。
Q: 舵机转到了错误的角度?
很可能是没有做 PWM 反向处理。筑基学习板的舵机电路会反转 PWM 信号,代码中必须用 100 - duty 取反。参考 3.1.1 节的 set_servo_angle 函数。
Q: 舵机在某个角度抖动?
- 供电不足是最常见的原因,舵机在负载较大时电流需求会突增,导致电压跌落引起抖动。
- 脉宽超出舵机的有效范围也会导致抖动。不同品牌的舵机有效脉宽范围略有差异,可以微调
min_us和max_us参数。
Q: 可以同时使用舵机和直流电机 2 吗?
不可以。舵机接口和直流电机 2 共用 PB14/PB15 引脚,通过 BIT5 二选一。直流电机 1(PE5/PE6)不受影响,可以同时使用。
五 本节参考文档
- MicroPython pyb.Timer 文档:
- MicroPython pyb.Pin 文档: