【工业树莓派 CM0 NANO 单板计算机】步进电机的远程控制与 LabVIEW 数据采集

【工业树莓派 CM0 NANO 单板计算机】步进电机的远程控制与 LabVIEW 数据采集

本文介绍了工业树莓派 CM0 NANO 单板计算机结合 TMC2209 模块实现 42 步进电机驱动,通过 MQTT 通信实现远程控制,进一步设计 LabVIEW 上位机,实现步进电机的自动化连续运行和数据采集,包括硬件连接、环境搭建、OLED 显示、流程图、工程代码、效果演示等。

项目介绍

  • 准备工作:硬件连接、环境搭建、python 库安装;
  • OLED 驱动:获取 RTC 时间、OLED 显示、动图显示等;
  • 步进电机驱动:通过 GPIO 控制 TMC2209 驱动步进电机,OLED 显示旋转状态信息;
  • 远程控制:结合 MQTT 协议和云端服务器,实现远程控制步进电机;
  • 数据采集:设计 LabVIEW 上位机,实现步进电机的连续自动化运行,并模拟采集存储数据。

硬件连接

  • 使用 Type-C 数据线连接电源适配器;
  • 使用杜邦线连接 CM0 NANO 开发板和 0.96 寸 OLED 模块;
  • 使用杜邦线连接 CM0 NANO 和 TMC2209 步进电机驱动模块;
  • 连接步进电机和 TMC2209 模块;
  • 使用工业电源连接 TMC2209 为步进电机供电。

OLED

OLED 模块使用 SSD1306 驱动,将其与 IIC 总线 1 连接。

实物连接如下

接线方式如下

OLED CM0 Nano Note
SCL SCL Serial Clock
SDA SDA Serial Data
GND GND Ground
VCC 3.3V Power

步进电机

步进电机采用 NIDEC 尼得科 8mm 丝杆步进电机,步距角 18 度,详见:PM型步进电机(轴/螺杆型) | NIDEC(尼得科) .

示意图如下

驱动器使用 TMC2209,参数详见:TMC2209SILENTSTEPSTICK .

接线方式如下

TMC2209 CM0 Nano Note
Dir GPIO27 Dirction
STEP GPIO17 Step
EN GPIO22 Enable
GND GND Ground
VCC 3V3 Power

根据步进电机额定电压选择合适的电源,这里使用板载 5V 排针供电。

环境搭建

这里使用 luma.oled 库驱动 OLED 模块。

  • 终端执行下列代码,安装 luma.oled 库
bash 复制代码
pip install luma.oled

详见:luma.oled · PyPI .

MQTT Broker

MQTT 是一种适用于物联网的轻量级协议,MQTT 服务器(MQTT Broker)是其核心组件。

MQTT Broker 是一种中介实体,帮助 MQTT 客户端进行通信。作为中央枢纽,MQTT Broker 能够高效管理设备与应用之间的信息流。具体来说,MQTT Broker 接收客户端发布的消息,根据主题对消息进行过滤,并分发给订阅者。

可使用 EMQX 搭建本地 MQTT 服务器,也可采用开发商提供的云端服务器。

这里使用 KZone喵星球 IoT Studio 作为订阅消息的中转平台。

OLED 驱动

制作一款桌面 RTC 时钟,OLED 屏幕显示数字时钟、日期和动画。

代码

终端执行指令 touch oled_rtc.py 新建文件,并添加如下代码

python 复制代码
from luma.core.interface.serial import i2c
from luma.core.render import canvas
from luma.oled.device import ssd1306
from PIL import Image, ImageFont
import os
import time
import datetime

serial = i2c(port=1, address=0x3C)
device = ssd1306(serial)

# 字体
font_fg = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 22)
font_date = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 20)

# 加载20帧
frame_list = []
for i in range(1, 21):
    img_path = os.path.join("frame", f"{i}.jpg")
    img = Image.open(img_path).convert("1")
    frame_list.append(img)

frame_index = 0

try:
    while True:
        now = datetime.datetime.now()
        date_str = now.strftime("%Y-%m-%d")
        time_str = now.strftime("%H:%M")

        with canvas(device) as draw:
            # 动画
            draw.bitmap((32, 0), frame_list[frame_index], fill=1)
            # 时间
            draw.text((2, 25), time_str, font=font_fg, fill=1)
            # 日期
            draw.text((2, 5), date_str, fill=1)

        frame_index = (frame_index + 1) % 20
        time.sleep(0.1)

except KeyboardInterrupt:
    device.clear()

保存代码。

动画包含的帧画面可自定义,通过 GIF 或视频格式提取帧图片,按照顺序数字命名,保存至 frame 文件夹。

效果

  • 终端执行指令 python rtc_oled.py 运行程序;

  • OLED 显示日期、时间以及太空人动画;

步进电机驱动

使用树莓派 CM0 NANO 板载 40pin 引脚接口,控制 GPIO 输出电平,结合 TMC2209 实现 42 步进电机驱动,并显示旋转状态,包括转速、角度和方向。

代码

终端执行指令 touch stepmotor_oled.py 新建文件,并添加如下代码

python 复制代码
from luma.core.interface.serial import i2c
from luma.core.render import canvas
from luma.oled.device import ssd1306
from PIL import Image, ImageFont
import os
import time
import datetime
import threading
import RPi.GPIO as GPIO

# ---------------------- OLED 初始化 ----------------------
serial = i2c(port=1, address=0x3C)
device = ssd1306(serial)

# 字体
font_main = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 14)
font_small = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 12)

# 加载帧动画
frame_list = []
for i in range(1, 21):
    img = Image.open(os.path.join("frame", f"{i}.jpg")).convert("1")
    frame_list.append(img)
frame_index = 0

# ---------------------- 运行状态变量 ----------------------
current_angle = 180
speed_ms = 0.2
direct_text = "CW"
running = True

# 独立屏幕刷新线程(动画持续运行,不受电机阻塞)
def screen_task():
    global frame_index
    while running:
        now = datetime.datetime.now()
        date_str = now.strftime("%Y-%m-%d")
        time_str = now.strftime("%H:%M:%S")

        with canvas(device) as draw:
            # 太空人动画显示
            draw.bitmap((32, 0), frame_list[frame_index], fill=1)

            # 左侧时间信息
            draw.text((0, 0), date_str, font=font_small, fill=1)
            draw.text((0, 11), time_str, font=font_main, fill=1)

            # 左侧电机运行状态
            info_dir = f"Dir: {direct_text}"
            info_speed = f"V: {speed_ms}ms"
            info_angle = f"Ang: {abs(current_angle)}deg"
            draw.text((2, 24), info_dir, font=font_small, fill=1)
            draw.text((2, 36), info_speed, font=font_small, fill=1)
            draw.text((2, 48), info_angle, font=font_small, fill=1)

        frame_index = (frame_index + 1) % len(frame_list)
        time.sleep(0.1)

# ---------------------- 电机驱动函数 ----------------------
STEP = 17
DIR = 27
EN = 22
PULSE_PER_360DEG = 1600  # 8 细分
PULSE_PER_DEG = PULSE_PER_360DEG / 360

GPIO.setmode(GPIO.BCM)
GPIO.setup(STEP, GPIO.OUT)
GPIO.setup(DIR, GPIO.OUT)
GPIO.setup(EN, GPIO.OUT)
GPIO.output(EN, GPIO.LOW)

def run_angle(degree, speed):
    global direct_text
    pulses = int(abs(degree) * PULSE_PER_DEG)
    if degree > 0:
        GPIO.output(DIR, GPIO.HIGH)
        direct_text = "CW"
    else:
        GPIO.output(DIR, GPIO.LOW)
        direct_text = "CCW"

    delay_s = speed / 1000.0
    # 纯脉冲,无阻塞,转速稳定
    for _ in range(pulses):
        GPIO.output(STEP, GPIO.HIGH)
        time.sleep(delay_s)
        GPIO.output(STEP, GPIO.LOW)
        time.sleep(delay_s)

if __name__ == "__main__":
    # 启动后台屏幕线程
    t = threading.Thread(target=screen_task, daemon=True)
    t.start()

    try:
        while True:
            current_angle = 180
            run_angle(current_angle, speed_ms)
            time.sleep(1)

            current_angle = -180
            run_angle(current_angle, speed_ms)
            time.sleep(1)

    except KeyboardInterrupt:
        running = False
        time.sleep(0.2)
        device.clear()
        GPIO.output(EN, GPIO.HIGH)
        GPIO.cleanup()

保存代码。

效果

  • 终端执行指令 python stepmotor_oled.py 运行程序;

  • OLED 显示日期、时间、太空人动画、步进电机状态,包括方向、速度、角度等;

  • 步进电机正反交替旋转;

远程控制

树莓派 CM0 NANO 连接网络,通过 MQTT 协议实现云端消息传输,进而控制终端步进电机旋转。

流程图

#mermaid-svg-xPSfE4SfDPHet1sd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xPSfE4SfDPHet1sd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xPSfE4SfDPHet1sd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xPSfE4SfDPHet1sd .error-icon{fill:#552222;}#mermaid-svg-xPSfE4SfDPHet1sd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xPSfE4SfDPHet1sd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xPSfE4SfDPHet1sd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xPSfE4SfDPHet1sd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xPSfE4SfDPHet1sd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xPSfE4SfDPHet1sd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xPSfE4SfDPHet1sd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xPSfE4SfDPHet1sd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xPSfE4SfDPHet1sd .marker.cross{stroke:#333333;}#mermaid-svg-xPSfE4SfDPHet1sd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xPSfE4SfDPHet1sd p{margin:0;}#mermaid-svg-xPSfE4SfDPHet1sd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xPSfE4SfDPHet1sd .cluster-label text{fill:#333;}#mermaid-svg-xPSfE4SfDPHet1sd .cluster-label span{color:#333;}#mermaid-svg-xPSfE4SfDPHet1sd .cluster-label span p{background-color:transparent;}#mermaid-svg-xPSfE4SfDPHet1sd .label text,#mermaid-svg-xPSfE4SfDPHet1sd span{fill:#333;color:#333;}#mermaid-svg-xPSfE4SfDPHet1sd .node rect,#mermaid-svg-xPSfE4SfDPHet1sd .node circle,#mermaid-svg-xPSfE4SfDPHet1sd .node ellipse,#mermaid-svg-xPSfE4SfDPHet1sd .node polygon,#mermaid-svg-xPSfE4SfDPHet1sd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xPSfE4SfDPHet1sd .rough-node .label text,#mermaid-svg-xPSfE4SfDPHet1sd .node .label text,#mermaid-svg-xPSfE4SfDPHet1sd .image-shape .label,#mermaid-svg-xPSfE4SfDPHet1sd .icon-shape .label{text-anchor:middle;}#mermaid-svg-xPSfE4SfDPHet1sd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xPSfE4SfDPHet1sd .rough-node .label,#mermaid-svg-xPSfE4SfDPHet1sd .node .label,#mermaid-svg-xPSfE4SfDPHet1sd .image-shape .label,#mermaid-svg-xPSfE4SfDPHet1sd .icon-shape .label{text-align:center;}#mermaid-svg-xPSfE4SfDPHet1sd .node.clickable{cursor:pointer;}#mermaid-svg-xPSfE4SfDPHet1sd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xPSfE4SfDPHet1sd .arrowheadPath{fill:#333333;}#mermaid-svg-xPSfE4SfDPHet1sd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xPSfE4SfDPHet1sd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xPSfE4SfDPHet1sd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xPSfE4SfDPHet1sd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xPSfE4SfDPHet1sd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xPSfE4SfDPHet1sd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xPSfE4SfDPHet1sd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xPSfE4SfDPHet1sd .cluster text{fill:#333;}#mermaid-svg-xPSfE4SfDPHet1sd .cluster span{color:#333;}#mermaid-svg-xPSfE4SfDPHet1sd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xPSfE4SfDPHet1sd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xPSfE4SfDPHet1sd rect.text{fill:none;stroke-width:0;}#mermaid-svg-xPSfE4SfDPHet1sd .icon-shape,#mermaid-svg-xPSfE4SfDPHet1sd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xPSfE4SfDPHet1sd .icon-shape p,#mermaid-svg-xPSfE4SfDPHet1sd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xPSfE4SfDPHet1sd .icon-shape .label rect,#mermaid-svg-xPSfE4SfDPHet1sd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xPSfE4SfDPHet1sd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xPSfE4SfDPHet1sd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xPSfE4SfDPHet1sd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始
初始化
开启OLED线程
连接MQTT服务器
订阅指令话题
等待MQTT消息
Ctrl+C
解析JSON消息
执行转动
发布回执
刷新屏幕

更新数据
息屏
释放扭矩
结束

代码

终端执行指令 touch stepmotor_mqtt.py 新建文件,并添加如下代码

python 复制代码
from luma.core.interface.serial import i2c
from luma.core.render import canvas
from luma.oled.device import ssd1306
from PIL import Image, ImageFont
import os
import time
import datetime
import threading
import json
import RPi.GPIO as GPIO
import paho.mqtt.client as mqtt

# ====================== 全局变量 ======================
current_angle = 0
speed_ms = 0.2
direct_text = "STOP"
running_flag = True
mqtt_client = None

# ====================== OLED 初始化 ======================
serial = i2c(port=1, address=0x3C)
device = ssd1306(serial)

font_main = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 14)
font_small = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 12)

# 加载帧画面
frame_list = []
for i in range(1, 21):
    img = Image.open(os.path.join("frame", f"{i}.jpg")).convert("1")
    frame_list.append(img)
frame_index = 0

def screen_task():
    """OLED后台刷新线程"""
    global frame_index
    while running_flag:
        now = datetime.datetime.now()
        date_str = now.strftime("%Y-%m-%d")
        time_str = now.strftime("%H:%M:%S")

        with canvas(device) as draw:
            # 太空人动画
            draw.bitmap((32, 0), frame_list[frame_index], fill=1)
            # 日期时间
            draw.text((2, 0), date_str, font=font_small, fill=1)
            draw.text((2, 10), time_str, font=font_main, fill=1)
            # 运行状态
            draw.text((2, 24), f"Dir: {direct_text}", font=font_small, fill=1)
            draw.text((2, 36), f"V: {speed_ms}ms", font=font_small, fill=1)
            draw.text((2, 48), f"Ang: {current_angle}deg", font=font_small, fill=1)

        frame_index = (frame_index + 1) % len(frame_list)
        time.sleep(0.07)

# ====================== 电机配置 ======================
STEP = 17
DIR = 27
EN = 22
PULSE_PER_360DEG = 1600  # 8细分
PULSE_PER_DEG = PULSE_PER_360DEG / 360

GPIO.setmode(GPIO.BCM)
GPIO.setup(STEP, GPIO.OUT)
GPIO.setup(DIR, GPIO.OUT)
GPIO.setup(EN, GPIO.OUT)
GPIO.output(EN, GPIO.LOW)

def run_angle(degree, speed):
    global direct_text, current_angle
    pulses = int(abs(degree) * PULSE_PER_DEG)
    current_angle = degree
    if degree > 0:
        GPIO.output(DIR, GPIO.HIGH)
        direct_text = "CW"
    else:
        GPIO.output(DIR, GPIO.LOW)
        direct_text = "CCW"

    delay_s = speed / 1000.0
    for _ in range(pulses):
        GPIO.output(STEP, GPIO.HIGH)
        time.sleep(delay_s)
        GPIO.output(STEP, GPIO.LOW)
        time.sleep(delay_s)
    direct_text = "STOP"

# ====================== MQTT 回调 ======================
def on_connect(client, userdata, flags, rc):
    print("MQTT已连接")
    client.subscribe("ljl/stepmotor")

def on_message(client, userdata, msg):
    global speed_ms
    try:
        payload = json.loads(msg.payload.decode())
        angle = payload.get("angle", 0)
        speed_ms = payload.get("speed", 0.2)
        print(f"收到指令: 角度 {angle}, 速度 {speed_ms}ms")

        # 执行旋转
        run_angle(angle, speed_ms)
        # 旋转完成回复OK
        client.publish("ljl/stepmotor", '{"result":"OK"}')
    except Exception as e:
        print("解析错误:", e)

# ====================== 主程序 ======================
if __name__ == "__main__":
    # 启动OLED线程
    t_screen = threading.Thread(target=screen_task, daemon=True)
    t_screen.start()

    # MQTT客户端
    mqtt_client = mqtt.Client()
    mqtt_client.on_connect = on_connect
    mqtt_client.on_message = on_message
    mqtt_client.connect("iot.kittenbot.cn", 1883, 60)

    try:
        mqtt_client.loop_forever()
    except KeyboardInterrupt:
        running_flag = False
        time.sleep(0.2)
        device.clear()
        GPIO.output(EN, GPIO.HIGH)
        GPIO.cleanup()

保存代码。

效果

  • 终端执行指令 python stepmotor_mqtt.py 运行程序;

  • OLED 显示日期、时间、太空人动画、步进电机状态,包括方向、速度、角度等;

  • 终端打印 MQTT 连接状态、指令接收、电机运转等信息;

  • 运行 MQTTX 软件,新建连接,输入 MQTT 服务器地址 iot.kittenbot.cn
  • 新建订阅话题 ljl/stepmotor
  • ljl/stepmotor 为 topic 发送 JSON 指令 {"angle": -95, "speed": 0.2} 旋转目标角度;
  • 步进电机旋转,OLED 显示更新;

LabVIEW 上位机

在工业和科研等实际应用场景,往往需要面对较长时间的步进电机控制和数据采集任务,手动发送消息的方案已无法满足实验需求。

使用 LabVIEW 快速搭建逻辑框架,通过 MQTT 协议定时发送消息,实现步进电机的远程自动化控制、仪器联合调用、数据采集与存储等复杂综合性项目的快速落地。

前面板

前面板设计包括 MQTT 配置、单步测试、连续运行测试、实时演化曲线、数据保存、程序控制等模块。

程序面板

程序面板采用模块化设计,将 MQTT 消息封装,确保连续发送指令准确执行。

运行效果

  • 配置 MQTT 服务器地址,运行程序;

  • 设置主题、步长、目标角度、延时、存储路径等;

  • 点击 START 按钮,开始运行步进电机并采集数据;

LabVIEW 程序运行后,步进电机按照设定的步长和延时连续旋转;

前面板显示采集数据的实时演化曲线;

数据采集

  • 循环采集完成后,MQTT 停止发送消息,数据自动存储至目标路径;

  • 根据前面板设置的存储路径,找到目标文件;

  • 打开存储的数据文件,第一列为角度,第二列为模拟采集的数值;

总结

本文介绍了工业树莓派 CM0 NANO 单板计算机结合 TMC2209 模块实现 42 步进电机驱动,通过 MQTT 通信实现远程控制,进一步设计 LabVIEW 上位机,实现步进电机的自动化连续运行和数据采集,包括硬件连接、环境搭建、OLED 显示、流程图、工程代码、效果演示等,为相关产品在工业物联网领域的快速开发和应用设计提供了参考。