基于 3D 打印机改造的低成本自动化液体处理系统

嵌入式系统实验自动化运动控制
Main project image

本毕设项目将一台退役的 Raise3D 3D 打印机改造成自动化液体处理平台,围绕 XYZU 四轴运动、注射器式移液机构、Marlin 固件改造、Python 上位机和自然语言协议生成,完成了一个可运行的低成本实验自动化原型。

GitHub 仓库

最低材料成本

约 £82

不含被改造的 Raise3D 打印机本体

运动系统

XYZU 四轴

保留打印机 XYZ,并新增注射器 U 轴

重复移液误差

约 2.2%

重复液体转移实验中的累计相对误差

Deck 结构

6-slot SBS

兼容标准 labware 和 96 孔板工作流

控制链路

Marlin + G-code

SKR Mini E3 V3.0 + 修改后的固件配置

上位机

Python + JSON

GUI、协议抽象和自然语言接口

项目概览

本毕设项目面向实验室重复液体处理场景。许多移液任务本身并不复杂,但一旦进入批量分配、多孔板操作或重复流程,手工操作会占用大量时间,一致性也难以保证。商用自动化平台已经比较成熟,但价格通常较高,对教学、小型研究和原型验证场景来说门槛并不低。

因此项目路线不是从零设计一台全新的实验自动化设备,而是复用一台已经退役的消费级 3D 打印机,保留其现成的运动平台和控制基础,再围绕“自动液体处理”任务补齐机械、固件和软件部分。

最终系统将一台 Raise3D Pro2 Plus 改造成低成本自动化液体处理平台。原有 XYZ 平台被保留下来,同时新增一个用于推动注射器活塞的 U 轴,让整个平台变成 XYZU 四轴系统。上位机部分使用 Python 实现控制与协议执行逻辑,并进一步接入自然语言转协议接口,让高层实验描述可以转换成结构化 JSON 协议,再下发到底层执行。

如果只看结果,这个项目做成的不是一台现成商品,而是一个完整的研究原型:它已经能够在标准 labware 上完成自动液体分配任务,也把机械改造、运动控制、上位机软件和高层协议真正串成了一个闭环。

系统总体示意图。它把平台、控制板、步进电机和主机 PC 串在了一张图里,更接近整套系统真正的工作方式。
系统总体示意图,展示 XYZU 平台、主机 PC、控制板和步进电机之间的关系

系统总体示意图。它把平台、控制板、步进电机和主机 PC 串在了一张图里,更接近整套系统真正的工作方式。

问题定义

项目关注的核心问题不是“自动移液能不能做”,而是一个更现实的问题:实验室自动化的成本门槛是否一定要这么高?

在很多实验流程里,液体处理是最常见也最重复的动作之一。人工移液具备灵活性,但一旦面对多孔板、重复操作或者批量分配场景,就很容易遇到几个实际问题:

项目最终选择从“再利用现有硬件”这个方向切入。和从零做一套运动平台相比,消费级 3D 打印机本身已经具备几项天然优势:笛卡尔坐标运动结构、成熟的步进驱动体系、可编程的 G-code 控制链路,以及较低的获取成本。

这个项目的重点,不只是做一个能吸液的机械装置,而是验证一条完整思路:一台原本用于打印的机器,能不能被重新组织成一个真正可用的自动化液体处理系统。

系统整体架构

整个系统可以理解成四层结构,从下往上分别是运动平台层、机械执行层、控制执行层,以及上位机和高层协议层。

底层是一台被改造后的 Raise3D Pro2 Plus。原有 X、Y、Z 三轴负责空间定位,新加入的 U 轴负责推动注射器活塞,实现体积控制。机械层围绕这个运动平台补齐了注射器式移液机构、枪头适配结构以及兼容标准 labware 的 deck。控制层使用 SKR Mini E3 V3.0 控制板,并基于修改后的 Marlin 固件接收 G-code 指令、驱动 XYZU 四轴执行。上位机层使用 Python 实现 GUI、串口控制、JSON 协议解析和动作抽象,并接入基于云端 API 的自然语言协议生成能力。

这种层次划分使各层职责保持清晰:机械决定“怎么动”,固件负责“如何执行”,上位机负责“如何组织动作”,协议层负责“用户到底想让它做什么”。

更简化的 XYZU 平台示意。它只强调平台本体和四个运动自由度,适合说明这台机器是怎么从打印平台变成液体处理平台的。
XYZU 四轴平台示意图

更简化的 XYZU 平台示意。它只强调平台本体和四个运动自由度,适合说明这台机器是怎么从打印平台变成液体处理平台的。

控制链路示意图。自然语言输入、GUI、JSON 协议、G-code 和底层步进执行被拆成了清晰的几层,而不是直接把高层描述硬连到底层电机。
系统控制架构图,展示自然语言、GUI、控制板和 XYZU 电机之间的链路

控制链路示意图。自然语言输入、GUI、JSON 协议、G-code 和底层步进执行被拆成了清晰的几层,而不是直接把高层描述硬连到底层电机。

改造前的 Raise3D Pro2 Plus。它提供了项目后续复用的 XYZ 运动平台、导轨和整体结构,是整套液体处理系统的原始硬件基础。
Raise3D Pro2 Plus 原始商品图

改造前的 Raise3D Pro2 Plus。它提供了项目后续复用的 XYZ 运动平台、导轨和整体结构,是整套液体处理系统的原始硬件基础。

关键工程实现

1. 从打印平台到液体处理平台

项目的第一步不是写代码,而是先确认这台打印机值不值得改。虽然这台 Raise3D Pro2 Plus 原本的挤出部分已经不能继续承担正常打印任务,但它的运动机构、导轨、皮带、丝杆和整体刚性依然可用。对这个项目来说,这比“还能不能打印”更重要。

保留这些现成结构的意义很直接:项目不需要从零搭建新的 XYZ 平台,而是可以把主要工作集中在“如何让它完成液体处理任务”上。这让整个项目从一开始就带着低成本约束,而不是先做一个理想系统,再回头解释为什么它很贵。

2. 新增注射器 U 轴,实现真正的移液动作

为了让系统不仅能定位,还能实际处理液体,项目新增了一个基于注射器的 U 轴机构。它本质上是一个把步进电机旋转运动转换成活塞线性位移的丝杆推进结构,用于控制吸液和分液。

这个部分真正的难点不在原理,而在约束:它既要能稳定推动注射器活塞,又要和原有平台机械兼容,还得考虑枪头连接、打印件装配和后续调试的便利性。最终版本采用的是比较直接但可验证的方案:独立步进电机 + 丝杆驱动 + 注射器固定与导向结构。

U 轴机构 CAD 图。核心目标是把旋转运动稳定地转换为注射器活塞的线性位移。
注射器 U 轴机构 CAD 示意图

U 轴机构 CAD 图。核心目标是把旋转运动稳定地转换为注射器活塞的线性位移。

U 轴机构实物。第一版重点先放在动作链路跑通和结构可装配性上。
安装在平台上的注射器 U 轴实物照片

U 轴机构实物。第一版重点先放在动作链路跑通和结构可装配性上。

3. 设计兼容标准 labware 的 deck

有了运动系统和移液机构之后,还需要把实验耗材放到一个稳定、可重复访问的坐标体系里。这一步最终落到了 deck 设计上。

deck 没有采用临时容器摆放方式,而是做成兼容 SBS 标准 labware 的结构。这样 96 孔板、枪头盒、试剂槽和废液位都可以用统一规则定义。后续软件层抽象 slot、well 和协议时,不需要再面向“某一个临时位置”,而是面向一个更接近真实实验流程的坐标体系。

空载状态下的 deck。第一版采用 6 个 SBS-compatible 槽位,目的是先建立统一的 labware 坐标体系。
6 槽位 SBS 兼容 deck 实物照片

空载状态下的 deck。第一版采用 6 个 SBS-compatible 槽位,目的是先建立统一的 labware 坐标体系。

加入 labware 后的 deck。可以更直观看到 tip rack、96 孔板、烧杯和原液瓶在平台上的对应位置。
放入 tip rack、96 孔板、烧杯和试剂瓶后的 deck

加入 labware 后的 deck。可以更直观看到 tip rack、96 孔板、烧杯和原液瓶在平台上的对应位置。

这一层在代码里也被单独抽象成了 DeckLabwareInstance。deck 负责把实验平台的 slot 坐标转换成 machine 坐标,而 labware 则把 A1B3 这种孔位映射到具体的 XY 位置。这样,控制逻辑就不需要把坐标常数散落在各个函数里,而是统一通过对象层去取位置。

DECK_LAYOUT = {
    "origin_machine": {"x": -8, "y": -5},
    "slots": {
        "1": {"x": 90.9, "y": 70.0},
        "2": {"x": 242.1, "y": 70.0},
        "3": {"x": 90.9, "y": 170.0},
        "4": {"x": 242.1, "y": 170.0},
        "5": {"x": 90.9, "y": 270.0},
        "6": {"x": 242.1, "y": 270.0},
    },
}

x_src, y_src = src_plate.well_position_machine(src_well)

这一层抽象看起来很基础,但它直接决定了后面 GUI、协议和高层自动化是不是能站得住。如果没有统一坐标体系,协议层最后就会退化成“硬编码几个位置点”的脚本,而不是一个可以继续扩展的平台。

4. 修改固件和控制板配置,把平台真正变成 XYZU 系统

机械部分改完之后,下一步是控制层。系统使用 SKR Mini E3 V3.0 控制板,并基于 Marlin 做了适配这套系统的修改。这里最关键的不是“换板子”本身,而是把一套原本面向 3D 打印的控制逻辑,重新组织成适合非打印场景的四轴执行系统。

更细的固件修改过程,包括 E0 复用成 U 轴、TMC2209 配置、配套界面设置和编译刷写流程,单独整理在下面这篇记录中。

Marlin 固件博客封面

为实验移液自动化修改 Marlin 固件

配置上保留了打印机的运动框架,但去掉挤出相关功能,并把第四轴作为 U 轴使用。项目代码里的 Marlin 配置能直接看出这件事:控制板是 BOARD_BTT_SKR_MINI_E3_V3_0,串口走 115200EXTRUDERS 被设成 0,四轴步进参数则改成了 X/Y/Z/U = { 80, 80, 800, 4000 }。配合 DEFAULT_MAX_FEEDRATEDEFAULT_MAX_ACCELERATION,平台就不再是默认的打印机运动模型,而是一套更适合液体处理的执行系统。

#define MOTHERBOARD BOARD_BTT_SKR_MINI_E3_V3_0
#define BAUDRATE 115200
#define EXTRUDERS 0

#define DEFAULT_AXIS_STEPS_PER_UNIT { 80, 80, 800, 4000 }
#define DEFAULT_MAX_FEEDRATE       { 500, 500, 500, 500 }
#define DEFAULT_MAX_ACCELERATION   { 500, 500, 100, 100 }

#define X_BED_SIZE 305
#define Y_BED_SIZE 305
#define Z_MAX_POS 605

这部分改完之后,底层控制接口仍然保留 G-code。原因很现实:G-code 本身已经是一个足够成熟的底层运动语言,它非常适合作为机械执行层和上位机之间的边界。这样,上层不用关心具体电机如何脉冲输出,只需要决定什么时候发 G28、什么时候发 G1、什么时候停顿和等待即可。

控制硬件使用 SKR Mini E3 V3.0。它负责接收 G-code 并驱动 XYZU 四轴执行。
SKR Mini E3 V3.0 控制板图片

控制硬件使用 SKR Mini E3 V3.0。它负责接收 G-code 并驱动 XYZU 四轴执行。

平台局部实物。机械改造和控制配置最终要在同一个坐标体系里稳定配合。
改造后的打印机运动平台局部照片

平台局部实物。机械改造和控制配置最终要在同一个坐标体系里稳定配合。

5. 用 Python 建立上位机控制和协议层

固件层解决的是“怎么执行动作”,上位机解决的是“怎么把实验流程组织成动作”。上位机使用 Python 实现串口通信、动作封装、GUI 交互以及 JSON 协议解析逻辑。

GUI 这一层做的不是简单按钮联动,而是把常用动作拆成了几类比较明确的协议步骤:home_xyzpick_tipdrop_tiptransferaspiratedispensedwell。这意味着平台不只是“点一下按钮动一下”,而是能把一个实验流程保存成结构化步骤,再统一执行。

values=[
    "home_xyz",
    "pick_tip",
    "drop_tip",
    "transfer",
    "aspirate",
    "dispense",
    "dwell",
]
Python GUI。上位机负责串口控制、协议执行和动作组织,而不是直接让用户面对底层控制指令。
Python 上位机 GUI 截图

Python GUI。上位机负责串口控制、协议执行和动作组织,而不是直接让用户面对底层控制指令。

从协议角度看,项目选择 JSON,而不是直接在 GUI 里把每一步都写死。原因很直接:JSON 在这个场景里足够简单,既适合存档,也适合后续接自然语言生成。

[
  {"type": "home_xyz", "params": {}},
  {"type": "pick_tip", "params": {"slot": "1", "well": "A1"}},
  {
    "type": "transfer",
    "params": {
      "src_slot": "4",
      "src_well": "A1",
      "dst_slot": "4",
      "dst_well": "B3",
      "volume_ul": 50
    }
  },
  {"type": "drop_tip", "params": {"slot": "2", "edge": "left"}}
]

这条 transfer 协议里,src_slotdst_slot 表示 deck 上的槽位编号,src_welldst_well 表示对应 labware 内部的孔位编号,volume_ul 表示本次转移体积。也就是说,协议层并不直接写 X/Y/Z/U 坐标,而是先用实验人员更容易理解的槽位、孔位和体积来描述任务。

6-slot deck 与 protocol 字段的对应关系。`src_slot: 4` 或 `dst_slot: 4` 指向的是 deck 上的第 4 个槽位,而不是一组底层电机坐标。
6 个 deck 槽位与 JSON protocol 中 slot 字段的对应关系

6-slot deck 与 protocol 字段的对应关系。`src_slot: 4` 或 `dst_slot: 4` 指向的是 deck 上的第 4 个槽位,而不是一组底层电机坐标。

示例孔板内部的 well 坐标。`A1`、`B3` 这类字段先在 labware 坐标系里定位,再叠加到对应 slot 的机器坐标上。
示例孔板孔位与 JSON protocol 中 well 字段的对应关系

示例孔板内部的 well 坐标。`A1`、`B3` 这类字段先在 labware 坐标系里定位,再叠加到对应 slot 的机器坐标上。

执行层会遍历协议步骤,根据不同的 type 调用对应的高层函数。这个结构把“实验意图”和“底层运动细节”分开了。

for idx, step in enumerate(self.protocol_steps, start=1):
    self.log(f"Step {idx}: {step}")
    self._run_single_step(step)

这段执行逻辑可以概括为:

load protocol
for each step in protocol:
    if step.type == home_xyz:
        home XYZ
    elif step.type == pick_tip:
        move to tip rack and mount tip
    elif step.type == transfer:
        aspirate from source -> move -> dispense to destination
    elif step.type == drop_tip:
        move to waste and scrape tip off
    elif step.type == dwell:
        wait for a fixed duration

6. 从协议到实际液体动作

真正把软件层和机械层接起来的,是 robot.py 这一层高层动作封装。比如 transfer_volume() 并不是单条命令,而是一整段连续动作:先把 U 轴回到基准,再移动到源孔位、下探、吸液,之后抬起、移动到目标孔位、下探、排液,最后回到安全高度。

这一点很重要,因为它意味着上层协议不需要知道每个 G-code 细节,只需要表达“从哪里取多少液体,送到哪里去”。底层动作链则由控制代码统一展开。

transfer 为例,协议解析后会先完成位置解析:slot 决定 labware 放在 deck 的哪个基准位置,well 决定目标孔位相对这个 labware 原点的偏移。两者叠加后,得到机器坐标系下的 x/y 位置。

params = step["params"]

src_plate = deck.get_labware(params["src_slot"])
dst_plate = deck.get_labware(params["dst_slot"])

x_src, y_src = src_plate.well_position_machine(params["src_well"])
x_dst, y_dst = dst_plate.well_position_machine(params["dst_well"])

robot.transfer_volume(
    x_src=x_src,
    y_src=y_src,
    x_dst=x_dst,
    y_dst=y_dst,
    volume_ul=params["volume_ul"],
)

到这一步,src_slot: 4, src_well: A1 已经不再只是字符串,而是被解析成了源孔位的机器坐标;dst_slot: 4, dst_well: B3 同样会被解析成目标孔位的机器坐标。volume_ul 则会换算成 U 轴注射器需要移动的距离。

# aspirate
self.move_to(u=u_base, feedrate=feed_u)
self.move_to(z=z_safe_src, feedrate=feed_z_up)
self.move_to(x=x_src, y=y_src, feedrate=feed_xy)
self.move_to(z=z_asp, feedrate=feed_z_down)
self.move_to(u=u_asp, feedrate=feed_u)
self.move_to(z=z_safe_src, feedrate=feed_z_up)

# dispense
self.move_to(z=z_safe_dst, feedrate=feed_z_up)
self.move_to(x=x_dst, y=y_dst, feedrate=feed_xy)
self.move_to(z=z_disp, feedrate=feed_z_down)
self.move_to(u=u_base, feedrate=feed_u)
self.move_to(z=z_safe_dst, feedrate=feed_z_up)

再往下一层,move_to() 会把函数参数格式化成 Marlin 能识别的 G-code。上层动作代码只需要传入目标坐标和速度,底层串口层负责生成并发送具体指令。

def move_to(self, x=None, y=None, z=None, u=None, feedrate=None):
    parts = ["G1"]
    if x is not None:
        parts.append(f"X{x:.2f}")
    if y is not None:
        parts.append(f"Y{y:.2f}")
    if z is not None:
        parts.append(f"Z{z:.2f}")
    if u is not None:
        parts.append(f"U{u:.3f}")
    if feedrate is not None:
        parts.append(f"F{feedrate}")

    self.send_gcode(" ".join(parts))

同一条 transfer protocol 最终会展开成一组连续的 G-code 指令。下面用一组示例指令表示从 slot 4 / A1 吸取 50 µL,再移动到 slot 4 / B3 分液的过程。其中 U 轴数值表示注射器轴位置,实际值由体积标定关系换算得到。

; move to source well
G1 X168.12 Y94.76 F12000
G1 Z52.0 F3000
G1 Z18.0 F1200

; aspirate liquid
G1 U8.000 F600

; retract
G1 Z52.0 F3000

; move to destination well
G1 X181.59 Y107.23 F12000
G1 Z18.0 F1200

; dispense liquid
G1 U5.000 F600

; finish
G1 Z52.0 F3000

这段逻辑看起来不像复杂算法,但它是整个平台最核心的“动作语义层”。机械、固件、deck 坐标和协议抽象,最后都要落到这里才能变成真正可执行的任务。

7. 加入自然语言到协议的转换接口

自然语言到协议的转换是项目中进一步扩展的一层。在基础自动化流程跑通之后,系统尝试把自然语言实验描述接入控制链路。用户不再只是在 GUI 里手动定义每一步动作,而是可以先给出一个更接近实验意图的描述,再由云端 API 和大语言模型生成结构化协议。

在 GUI 代码里,这一层通过一个单独的 GPT Helper 标签页接入。前端把文本描述发给云端接口,接口返回协议结果,再交由上位机展示和执行。模型不直接控制电机,而是保留一层中间协议:用户输入自然语言,模型生成 JSON 协议,上位机解释协议,固件执行底层动作。这样虽然多了一层转换,但系统更可验证,也更容易调试。

resp = requests.post(
    GPT_API_URL,
    json={"description": text},
    timeout=300,
)

答辩展示中保留了一个典型的两轮对话例子。第一轮只给出任务目标:将 A 和 B 两种溶液按 1:1 / 1:2 / 1:4 / 1:8 的比例混合,每孔总体积为 180 µL。这时信息仍然不足,因为 A 液和 B 液分别放在哪个槽位、目标孔板放在哪个槽位、要用哪些孔位,以及是否每次都换 tip,都还没有说明。系统没有直接编造一套动作,而是先指出缺失信息,同时给出一个带 MISSING 占位符的协议草稿。

第一轮对话:用户只给了配比和总体积,系统先指出缺失的槽位和目标孔信息,并返回一个带占位符的协议草稿。
第一轮 GPT Helper 对话截图,系统指出缺失信息并返回带占位符的协议草稿

第一轮对话:用户只给了配比和总体积,系统先指出缺失的槽位和目标孔信息,并返回一个带占位符的协议草稿。

第二轮补齐缺失条件:Aslot 4Bslot 6、目标孔板在 slot 2,并且只用 A1-A4 这四个孔。到这一步,系统可以把高层描述收敛成一组明确的 JSON 协议步骤,包括 pick_tip、从不同原液位吸液、在对应孔位分配以及最终 drop_tip。这个例子说明,对话接口不是“生成一段看起来像答案的文字”,而是能够收敛到可执行协议。

第二轮对话:用户补全原液槽位和目标孔位后,系统返回可以直接执行的完整 JSON 协议。
第二轮 GPT Helper 对话截图,补全实验条件后返回完整 JSON 协议

第二轮对话:用户补全原液槽位和目标孔位后,系统返回可以直接执行的完整 JSON 协议。

实验结果

最终这个项目并不只是“能动起来”,而是完成了一个比较完整的研究原型验证。关键结果包括:

其中,U 轴定位误差测试和重复移液实验最能说明系统当前的边界。U 轴本身仍然受到丝杆间隙、机构刚性和 backlash 的影响,但测试结果说明它的误差是有界、可重复的。对于一个原型系统来说,这一点比“单次绝对精度漂亮”更重要,因为它意味着后续还有继续补偿和优化的空间。

U 轴定位误差测试。误差范围大致落在 -0.35 mm 到 +0.65 mm 之间,说明机构仍有 backlash 和刚性优化空间。
U 轴定位误差测试结果

U 轴定位误差测试。误差范围大致落在 -0.35 mm 到 +0.65 mm 之间,说明机构仍有 backlash 和刚性优化空间。

单次液体分配质量测试。不同目标体积下,实测值整体仍跟随理想趋势。
单次液体分配质量测量结果

单次液体分配质量测试。不同目标体积下,实测值整体仍跟随理想趋势。

重复液体转移实验中的累计误差。当前结果对应约 2.2% 的累计相对误差。
重复液体转移累计误差结果

重复液体转移实验中的累计误差。当前结果对应约 2.2% 的累计相对误差。

项目价值

如果只看单个模块,这个项目里的很多部分都不是最复杂的设计:机械机构本身不算特别复杂,固件也不是从零写起,上位机逻辑也不是重量级软件工程。真正的难点一直是系统整合

项目价值在于,它不是停留在概念图或者单点功能验证,而是把机械改造、控制执行、上位机抽象和高层协议真正连成了一个完整系统。它回答的问题也不是“能不能做出一个移液动作”,而是“消费级硬件能不能被重新组织成一个真正可用的实验自动化入口平台”。

从工程实践角度看,这个项目的重点不是单点功能,而是系统如何从原理、结构、控制到验证逐步落地。

演示视频

最后这段视频对应的是一个完整的色素混合任务演示:先输入协议,再由平台按步骤执行移液和分配动作。对这个项目来说,它的意义不是展示“某一个动作能动”,而是说明从协议到底层执行这条链路确实已经跑通了。

色素混合任务演示视频,来源于 Zenodo 公开记录。可以直接在页面内播放,也可以打开原始视频文件查看完整版本。

局限与下一步

这个项目当前更适合定义为“已经完成核心功能验证的研究原型”,而不是成熟仪器。它当前最明确的局限包括:

后续可以继续推进的方向包括:

  1. 优化 U 轴机构刚性和传动精度
  2. 加入更稳定的闭环或半闭环体积控制
  3. 扩展成多通道移液头
  4. 加入视觉检测和自动校准
  5. 继续完善协议层和 GUI 的实验表达能力

从结果上看,这个项目已经证明了方向是可行的。下一步要做的,不是重新定义问题,而是把这个原型继续推向一个更可靠的平台。

总结

这个毕设项目的重点不只是做出一台能够执行液体处理动作的机器,而是完成了一次从机械改造、固件控制、上位机软件到自动化流程整合的系统工程实践。

项目结果表明:实验室自动化的高成本并不完全来自技术本身,很多时候也来自既有系统形态和实现路径。换一个工程视角,利用消费级硬件构建更低成本、更可访问的自动化平台,是一条可行路线。