树莓派驱动步进电机

本文依照记忆,于 2020 年末写成原稿;约一年后,参照代码,重新整理。年代久远,未经测试,仅供参考。

在参加 2020 年的 RoboGame 比赛时,我用树莓派控制步进电机,效果不错。关于这方面,网上没有太详细的资料,所以记录一下我的经验。代码不多,还没有整理: RoboGame2020

硬件与软件的选择

树莓派

咳咳,当然是因为我不会别的(手动狗头)

我们小组避开了竞争激烈的竞技组(听名字就感觉很卷),报名了比较划水的展示组。展示组的内容对实时性要求不高,只要完成动作就行,因此树莓派的软件 PWM 就能满足精度需要,否则需要外接时钟。花的是项目经费,所以毫不吝惜,买了个 8 GB 内存的 4B (当时还没有涨价)。另外,树莓派的生态也不错,外接触摸屏、键鼠,直接在上面跑 VSCode ,写 Python 和 网页完全不成问题。

RPi.GPIO

网上很多教程都是关于 RPi.GPIO 的,功能比较丰富;官方教程中提到了 gpiozero 库,对按钮、LED 灯之类有不错的支持,但是难以实现 PWM 一类的高级功能.所以就选择了 RPi.GPIO。虽然文档中有包括 PWM 在内的若干示例,但并不是很详细,网上相关的代码也大多很简单。

引脚编号相关问题

RPi.GPIO 支持两种引脚编号方式:BOARD 与 BCM。BOARD 根据引脚的位置编号,不会随型号升级而改变,比较推荐。

(然而我为什么会匪夷所思地用了 BCM 编码呢?)因为我从网上买了一个带风扇的外壳,外壳延长了引脚,还在上面按照 BCM 编码进行了标注,找起来特别容易。所以要问卖散热外壳的淘宝商家啦~

树莓派 GPIO 引脚编号图示

另外,一定要注意引脚编号是否合法。一次写错了引脚编号,启动程序后,不仅电机不转,WiFi 竟然也受到了影响,直接断网了,吓我一跳,我当时还以为树莓派坏了。

PWM 相关问题

PWM 模块用起来有许多坑,记录如下:

基本用法

如果只想在脚本中用一下 PWM 功能,并无大碍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import RPi.GPIO as GPIO
from time import sleep

GPIO.setmode(GPIO.BOARD)

port = 8
duty_cycle = 50
freq = 50
working_time = 3

GPIO.setup(port, GPIO.OUT)
p = GPIO.PWM(port, freq)

p.start(duty_cycle)
sleep(working_time)
p.stop()

GPIO.cleanup()

上述代码在 8 号端口上,产生一个频率 、占空比 、持续 秒的 PWM 信号。注意占空比的范围是

一般步进电机有配套的控制板,供电单独连接,树莓派只用给出控制信号。一般控制信号要求输入电压为 5 V,但是树莓派的输出只有 3.3 V,需要进行升压。对于步进电机来说,输出信号占空比似乎不是很重要,只要频率对就行;但如果是那种依据占空比调整角度的舵机,则需要控制占空比在舵机输入信号的有效范围内。

好了,电机已经转起来了,是不是很简单呢?如果你想要控制电机继续进行一系列复杂的操作,那么,坑就来了……

多次调用

你可能会想,这有什么难的,直接把上面这段代码封装成函数,每次调用一下,不就成了?于是你迫不及待地写了这样的代码:

1
2
3
4
5
6
7
def step_motor(port, duty_cycle, freq, working_time):
GPIO.setup(port, GPIO.OUT)
p = GPIO.PWM(port, freq)

p.start(duty_cycle)
sleep(working_time)
p.stop()

然后兴高采烈地调用它。比如下面这个例子,转两秒,歇两秒,再转四秒:

1
2
3
step_motor(8, 5, 50, 2)
sleep(2)
step_motor(8, 5, 50, 4)

Bingo~ 报错啦!恭喜你遇到了第一个坑!

在同一个端口上,不能多次调用 GPIO.PWM,否则代码就会报错。比如这样是不行的:

1
2
p = GPIO.PWM(8, 40)
q = GPIO.PWM(8, 40)

解决方法是:记住第一次调用的时候返回的 PWM 对象。如果要修改频率或者占空比,可以分别使用 p.ChangeFrequency(freq)p.ChangeDutyCycle(dc)

频率设置

有了上面的经验,我们就可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
p = GPIO.PWM(8, 40)

p.ChangeFrequency(60)

p.start(50)
sleep(3)
p.stop()

sleep(5)

p.start(50)
sleep(3)
p.stop()

这段代码的意思非常浅显易懂,是以 的频率转两次。

程序一运行,结果却令人大跌眼镜:电机第二次转动时,频率竟然不是

所以,每次在 p.stop() 之后,不管前后两次的频率是否一样,一定要重新设置一次频率

转动指定角度

如果用时间来控制步进电机的运动,无法得到精确的角度,我们需要控制脉冲信号的个数。步进电机在收到每个脉冲信号之后,都会旋转相应的角度(也就是所谓步进角)。如果我们发出固定数量的脉冲信号,步进电机转动的角度就大致是精准的。

我们用旋转的角度除以步进角,得到周期数:

1
2
3
4
5
for _ in range(cycle):
GPIO.output(port, GPIO.LOW)
sleep(half_cycle_time)
GPIO.output(port, GPIO.HIGH)
sleep(half_cycle_time)

后记

搭好了博客,终于把这篇整理出来了。不禁回忆起 2020 年的那场 RoboGame,和三位小伙伴在那间小屋里的快乐时光。

  1. 树莓派 4B 发热较大,买了有风扇的保护壳,通电后非常炫酷。
  2. 尝试连线,写程序控制电机。本文中大部分结论都是在这个阶段得出的。
  3. 队友制作的电路。松散的连线方便调试,也埋下了隐患。