news 2026/9/13 17:05:27

Marlin 固件单元测试完全指南:从配置矩阵到 Makefile 一键执行

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Marlin 固件单元测试完全指南:从配置矩阵到 Makefile 一键执行

Marlin 固件单元测试完全指南:从配置矩阵到 Makefile 一键执行

【免费下载链接】MarlinMarlin is a firmware for RepRap 3D printers optimized for both 8 and 32 bit microcontrollers. Marlin supports all common platforms. Many commercial 3D printers come with Marlin installed. Check with your vendor if you need source code for your specific machine.项目地址: https://gitcode.com/GitHub_Trending/ma/Marlin

Marlin 固件在持续演进中引入了两套自动化测试体系,其中**单元测试(Unit Tests)**专门用于在开发机上验证纯逻辑代码的正确性,为重构与功能迭代提供回归护栏。本篇以仓库中 Marlin/tests/README.md 为骨架,结合test目录、Marlin/tests测试源码、顶层 Makefile 与 ini/native.ini 等实际文件,完整讲解 Marlin 单元测试的架构、配置矩阵、测试编写方式与运行方法。读完本文,你将能够在本机或 Docker 中运行全部单元测试,理解"一个配置一个测试二进制"的设计动机,并学会借助MARLIN_TEST宏为 Marlin 的宏、类型与 G-code 解析逻辑编写自己的回归测试。

Marlin 测试体系总览:Build Tests 与 Unit Tests 的分工

根据仓库根目录下的 test/README.md(Unit Tests 的完整说明文档),Marlin 包含两类自动化测试:

  • 构建测试(Build Tests):位于buildroot/tests目录,为数十种真实板卡与配置组合执行完整编译,用于捕获语法错误与代码构建错误
  • 单元测试(Unit Tests):位于test目录,用于捕获实现逻辑错误,即运行时不涉及硬件的功能行为是否正确。

这两者的分工可以从目录结构直接看出:buildroot/tests下按板卡/机型(如mega2560STM32F103RC_bttlinux_native等)存放编译测试用的 INI 配置;而test目录则专注于在 Linux 本机以BOARD_SIMULATED方式运行逻辑测试。Marlin/tests/README.md明确说明:Marlin/tests目录下的测试文件由<root>/test目录构建出的单元测试二进制执行。

单元测试的价值在于:它可以在任意开发者本地机器以及通用的 CI 机器上运行,无需连接真实打印板。相比之下,在目标控制器上直接跑单元测试既不现实也未实现——那需要专门的测试台架,远不如在开发/构建机上测试来得通用。

从源码结构看,Marlin 目前采用的是 PlatformIO 单元测试框架 + Unity 断言库的组合,这从 ini/native.ini 中的lib_deps = throwtheswitch/Unity@^2.6.0可以得到直接印证。

单元测试能做什么、不能做什么

测试边界

单元测试可以验证单个函数或整个特性的逻辑,前提是该逻辑不依赖真实硬件。例如:

  • 可以测试 G-code 命令是否正确解析、是否产生预期的状态变更;
  • 但无法直接测试"G-code 是否触发了限位开关或断料传感器"——除非额外增加一层引脚模拟。

回归护栏的意义

单元测试最典型的价值在编写测试的初期就能发现大量问题,此后则持续为代码库(尤其是核心类与核心类型)提供防回归能力,对重构尤其有用。原文档 FAQ 中的关键结论值得记住:

  • 写单元测试是否很费工夫?是的,对未按可测试性设计的存量代码尤其如此。需要结合实际判断在何处、以什么粒度实施测试。投入虽大,但在防止回归、故障定位上的回报显著。
  • 会不会让重构更难?既有会(直接引用被测代码的测试需要同步修改),也有不会——测试反过来给重构提供了"机制仍然按预期工作"的保障。
  • 如何调试失败的单元测试?没有现成捷径。理论上可以通过 PlatformIO 交互式调试,但配置起来需要一定创造性。好在单元测试通常极小,即使不交互调试,也能很快定位到问题根源附近。

单元测试架构:一个配置对应一个测试二进制

Marlin 的单元测试架构建立在两个关键事实上:

  1. Marlin 只编译配置所要求的代码(条件编译贯穿整个源码,如#if ENABLED(FEATURE))。
  2. 因此,任何配置变化都必须重新生成一个独立的测试二进制

于是架构呈现出清晰的四步流程(源自 test/README.md):

  1. test目录存放一组 INI 配置文件(config.ini),每个文件代表一套用于单元测试的配置选项组合;所有适用的单元测试会为其中每一个配置各跑一遍
  2. Marlin/tests目录存放全部单元测试的 C++ 源码。测试通过 Marlin 宏(ENABLED(feature)TERN(FEATURE, A, B)等)决定注册哪些测试、改变测试行为。
  3. linux_native_test这个 PlatformIO 环境指定了一个脚本,负责收集Marlin/tests下全部测试,并把它们加入 PlatformIO 的测试目标列表。
  4. 测试最终由顶层 Makefile 的unit-test-all-localunit-test-all-local-docker命令构建并执行。

深入测试基础设施:test目录与 Unity 主程序

单元测试注册机制:MARLIN_TEST

test/unit_tests.h 定义了测试注册的核心:一个MarlinTest类和一个MARLIN_TEST(SUITE, NAME)宏。宏会生成一个继承自MarlinTest的类,在静态初始化期把测试实例注册进全局测试链表,测试名格式为SUITE___NAME(三个下划线分隔)。用法形如:

MARLIN_TEST(test_suite_name, test_name) { // Test body }

宏内部自动完成类定义、实例化与TestBody静态函数的声明/定义,__FILE____LINE__会被记录,供 Unity 报告失败位置。

主程序:unit_tests.cpp

test/unit_tests.cpp 为所有单元测试二进制提供唯一的main():它遍历全局注册链表中的每个MarlinTest并通过 Unity 执行,结构如下:

int main(int argc, char **argv) { UNITY_BEGIN(); run_all_marlin_tests(); UNITY_END(); return 0; }

MarlinTest::run()会把file(源文件名)和line交给UnityDefaultTestRun,从而让断言失败信息精确指向源码位置。同时unit_tests.h无条件包含src/inc/MarlinConfig.h,确保所有测试都能看到当前配置宏——这正是"同一份测试源码在不同配置下行为不同"的前提。

测试源码文件

Marlin/tests下的测试按主题组织(对应MARLIN_TEST的 suite 参数):

文件覆盖主题关键验证点
Marlin/tests/core/test_macros.cpp核心宏位操作、几何/数值宏、配置宏、数组与参数展开宏
Marlin/tests/core/test_types.cpp核心类型XYval/XYZval/XYZEvalFlags<N>、字符串与毫秒宏
Marlin/tests/gcode/test_gcode.cppG-code 解析命令号上限、溢出拒绝、参数seen检测
Marlin/tests/feature/test_runout.cpp断料传感器poll_runout_states默认状态位

**宏测试(test_macros.cpp)**是很好的入门样本,它直接验证src/core/macros.h中的行为,例如位操作宏:

MARLIN_TEST(macros_bitwise_8, SBI) { uint8_t n; n = 0x00; SBI(n, 0); TEST_ASSERT_EQUAL(0x01, n); // LSB n = 0x00; SBI(n, 7); TEST_ASSERT_EQUAL(0x80, n); // MSB n = 0x00; SBI(n, 3); TEST_ASSERT_EQUAL(0x08, n); // 中间位 }

配置宏测试(如ENABLED/DISABLED/ANY/ALL/NONE/COUNT_ENABLED/MANY/TERN系列、OPTITEM/OPTARG/OPTCODE)验证了 Marlin 条件编译体系本身的正确性,这是整个固件可配置性的根基。

**类型测试(test_types.cpp)**覆盖src/core/types.h中的XYval/XYZval/XYZEval(bool 转换、reset/setmagnitudesmall/large、四则运算、ABS/ROUNDL/reciprocal)、Flags<N>(1/8/16/32/64/302 位)、AxisFlags/AxisBits,以及 src/core/mstring.h 的MString/SString与 src/core/millis_t.h 的PENDING/ELAPSED(注意测试注释中提及 32 位毫秒计数的环绕语义)。其中small/large对负数语义的测试甚至保留了BUG?注释,体现了测试即文档的特点。

**G-code 解析测试(test_gcode.cpp)**直接驱动src/gcode/parser.h

MARLIN_TEST(gcode, parse_g_code_number_limit) { char max_command[] = "G65535"; parser.parse(max_command); TEST_ASSERT_TRUE(parser.is_command('G', UINT16_MAX)); // 上限合法 char overflow_command[] = "G65536"; parser.parse(overflow_command); TEST_ASSERT_EQUAL('?', parser.command_letter); // 溢出被拒绝 }

parse_g1_xzparse_g1_nxz则验证带 N 行号前缀时,parser.seen('X')/seen('Z')等参数检测仍正确工作。

**断料测试(test_runout.cpp)**演示了宏驱动的条件注册——整个测试体被#if ENABLED(FILAMENT_RUNOUT_SENSOR)包裹,只有启用该特性时才编译;测试断言FilamentSensorBasepoll_runout_states()默认返回"每个挤出器一位"的位掩码(~(~0U << NUM_RUNOUT_SENSORS))。

配置矩阵:test目录下的 INI 文件

test目录共三个配置文件,构成单元测试的配置矩阵。文件命名带数字前缀(001-002-003-),由收集脚本剥去前缀后作为测试目标名。

001-default.ini —— 最小基线配置

test/001-default.ini 刻意保持"除主板外为空",仅声明使用基础配置并强制模拟主板:

[config:base] ini_use_config = base # Unit tests must use BOARD_SIMULATED to run natively in Linux motherboard = BOARD_SIMULATED

关键约定:单元测试必须使用BOARD_SIMULATED才能在 Linux 上原生运行。文件注释还强调:如果测试需要改动配置,应当新增独立配置文件而不是修改本文件。

002-extruders_1_runout.ini —— 单挤出器 + 断料

test/002-extruders_1_runout.ini 针对单挤出器断料场景启用了完整功能链:

[config:base] ini_use_config = base motherboard = BOARD_SIMULATED filament_runout_sensor = on fil_runout_pin = 4 # dummy advanced_pause_feature = on emergency_parser = on nozzle_park_feature = on # Option to support testing parsing with parentheses comments enabled paren_comments = on

注意fil_runout_pin = 4标注为dummy——模拟主板上该引脚仅用于通过健全性检查(Sanity Check),并非真实硬件引脚。额外启用paren_comments是为了覆盖带括号注释的 G-code 解析路径(对应test_gcode.cppprocess_parsed_command等用例的运行环境)。

003-extruders_3_runout.ini —— 三挤出器 + 断料

test/003-extruders_3_runout.ini 将矩阵扩展到三个挤出器、三个断料传感器:

[config:base] ini_use_config = base motherboard = BOARD_SIMULATED extruders = 3 temp_sensor_1 = 1 temp_sensor_2 = 1 num_runout_sensors = 3 filament_runout_sensor = on fil_runout_pin = 4 # dummy fil_runout2_pin = 4 # dummy fil_runout3_pin = 4 # dummy filament_runout_script = "M600 %%c" advanced_pause_feature = on emergency_parser = on nozzle_park_feature = on

其中filament_runout_script = "M600 %%c"演示了 INI 转义——%%最终解析为字面量%,即断料触发时执行的 M600 脚本带挤出器占位符。NUM_RUNOUT_SENSORS = 3会直接影响test_runout.cpp中断料状态位掩码的期望值,这正是"同一测试在不同配置下验证不同行为"的实例。

这组配置与 buildroot/tests/linux_native/config-01.ini 同属 Linux 原生测试阵营,但前者服务于单元测试,后者服务于构建测试,二者在 buildroot/tests 与 test 两个目录中分离存放。

运行单元测试:Makefile 命令

Marlin/tests/README.md明确指向顶层 Makefile。相关的核心目标如下(见 Makefile):

命令作用底层实现
make unit-test-all-local本机运行全部单元测试配置platformio run -t test-marlin -e linux_native_test
make unit-test-all-local-dockerDocker 中运行全部单元测试容器内执行make unit-test-all-local
make unit-test-single-local只跑单个配置的单元测试platformio run -t marlin_$(UNIT_TEST_CONFIG) -e linux_native_test
make unit-test-single-local-dockerDocker 中跑单个配置容器内执行unit-test-single-local

常用变量

  • UNIT_TEST_CONFIG:指定要运行的配置名(不含数字前缀),默认值为default,即对应001-default.ini。例如要跑断料配置可执行:
    make unit-test-single-local UNIT_TEST_CONFIG=extruders_1_runout
  • VERBOSE_PLATFORMIO:置任意值可输出完整的 PlatformIO 日志。
  • GIT_RESET_HARD:供 CI 使用,会重置本地改动(Makefile 中明确警告"这会撤销你所有的改动"),本地调试慎用。

Docker 方式

若本机缺少 PlatformIO 环境,可以先用make setup-local-docker构建marlin-dev镜像(基于 docker/Dockerfile,带用户名/UID/GID 参数保证容器内文件权限一致),然后执行:

make unit-test-all-local-docker

Makefile 会在镜像不存在时自动先构建(unit-test-single-local-dockerunit-test-all-local-docker均包含镜像检查逻辑)。

PlatformIO 层面:linux_native_test环境与测试收集脚本

环境定义

ini/native.ini 中[env:linux_native_test]继承自[env:linux_native],其关键点:

  • 平台为native(纯本机编译,无 Arduino 框架),编译宏包含-D__PLAT_LINUX__-std=gnu++17,源码过滤为+<src/HAL/LINUX>(Linux 硬件抽象层);
  • 通过extra_scriptspost脚本挂载buildroot/share/PlatformIO/scripts/collect-code-tests.py
  • build_src_filter追加+<tests>,把test目录纳入构建;
  • 依赖throwtheswitch/Unity@^2.6.0提供断言框架,并开启-Werror把警告视为错误。

测试收集脚本

buildroot/share/PlatformIO/scripts/collect-code-tests.py 是整个"配置矩阵"机制的实现核心:

  1. 扫描./test/*.ini,用正则^\d+-|\.ini$剥离数字前缀得到配置名(如defaultextruders_1_runoutextruders_3_runout);
  2. 为每个配置注册自定义 PIO 目标marlin_<name>,其动作序列为:
    restore_configs → cp -f <ini> ./Marlin/config.ini → 运行 configuration.py 生成配置 → platformio test -e linux_native_test -f <name> → restore_configs

    把对应 INI 复制为Marlin/config.ini、重新生成配置、再对指定的测试过滤器运行platformio test,跑完恢复原配置;

  3. 额外注册聚合目标test-marlin,依次执行所有marlin_<name>,即 Makefile 中unit-test-all-local所触发的目标。

因此,make unit-test-all-local的实际效果等价于:对test目录下每一个 INI 配置,分别构建一个独立的linux_native_test二进制并执行其中所有注册的测试——这正对应Marlin/tests/README.md所述"收集所有测试文件并编译进多个 PlatformIO 测试二进制"。

常见问题与调试建议

  • 如何判断某个测试是否适用于当前配置?观察测试源码是否被宏包裹。test_runout.cpp#if ENABLED(FILAMENT_RUNOUT_SENSOR)说明它只在启用断料传感器的配置(如extruders_1_runout/extruders_3_runout)中注册;而test_macros.cpptest_types.cpp中的大部分用例无硬件依赖,会在所有配置下执行。
  • 新增一个测试配置矩阵?test目录的命名规范新建NNN-<name>.ini(复用[config:base]并设置motherboard = BOARD_SIMULATED),收集脚本会自动发现并注册对应目标——脚本本身不可修改,但新增配置文件即可生效(仓库为只读,本说明仅供理解机制)。
  • 失败定位:由于每个测试都记录__FILE__/__LINE__,Unity 会直接报告失败的源文件与行号;MARLIN_TEST命名中的 suite 前缀(如macros_bitwise_8typesgcoderunout)也便于按主题筛选。
  • 环境差异tests-all-local(构建测试)在 macOS 上会跳过linux_native目标(见 Makefile 中uname = Darwin的判断),而单元测试目标不在此限制内;此外 Makefile 要求 Python 3(缺失或版本不符会在解析阶段直接报错)。

结语

Marlin/tests/README.md这短短五行的入口出发,可以串起 Marlin 单元测试的完整链路:test目录的 INI 配置矩阵定义"测什么配置",Marlin/tests的 C++ 源码定义"测什么逻辑",collect-code-tests.py 与 linux_native_test 环境负责"如何构建多个二进制",而顶层 Makefile 则是统一入口。理解这条链路后,无论是复现 CI 失败、为某个新功能补回归测试,还是扩展配置覆盖,都有据可循、有命令可用。建议进一步阅读 test/README.md 获取测试哲学的完整阐述,并结合 Marlin/tests/core/test_macros.cpp 与 Marlin/tests/gcode/test_gcode.cpp 等真实用例学习断言写法。

【免费下载链接】MarlinMarlin is a firmware for RepRap 3D printers optimized for both 8 and 32 bit microcontrollers. Marlin supports all common platforms. Many commercial 3D printers come with Marlin installed. Check with your vendor if you need source code for your specific machine.项目地址: https://gitcode.com/GitHub_Trending/ma/Marlin

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 17:05:27

小爱音箱接入 ChatGPT 大模型:MiGPT 部署与使用指南

小爱音箱接入 ChatGPT 大模型&#xff1a;MiGPT 部署与使用指南 【免费下载链接】mi-gpt &#x1f3e0; 将小爱音箱接入 ChatGPT 和豆包&#xff0c;改造成你的专属语音助手。 项目地址: https://gitcode.com/GitHub_Trending/mi/mi-gpt MiGPT 是一个开源项目&#xff0…

作者头像 李华
网站建设 2026/9/13 17:04:00

手工实现KNN与朴素贝叶斯:鸢尾花分类算法全解析

简介&#xff1a;这一项目以鸢尾花数据集为对象&#xff0c;手工实现KNN与朴素贝叶斯两种经典分类算法&#xff0c;适合机器学习初学者对照理论动手实践。压缩包内共5个文件&#xff0c;包含两个Python代码文件、鸢尾花数据csv、结果txt以及README说明&#xff0c;整个资源仅4K…

作者头像 李华
网站建设 2026/9/13 17:03:53

Python人工智能案例包:环境配置与代码实战全流程指南

简介&#xff1a;一套聚焦Python人工智能入门与进阶的经典案例合集&#xff0c;面向希望结合真实数据动手实践的学习者&#xff0c;覆盖数据加载、特征分析、模型训练与结果可视化等典型环节。压缩包内共104个文件&#xff0c;主要包含28个csv数据集、24个py案例脚本与10张jpg效…

作者头像 李华
网站建设 2026/9/13 17:03:40

MATLAB BP神经网络分类实战:从蠓虫到鸢尾花全流程

简介&#xff1a;这是一份面向MATLAB初学者的ANN神经网络入门资料&#xff0c;聚焦BP神经网络在分类问题中的完整实现&#xff0c;适合刚接触神经网络或希望在MATLAB中动手实践分类任务的读者。压缩包内共7个文件&#xff0c;含5个txt数据文件与2个m源程序&#xff0c;结构清晰…

作者头像 李华
网站建设 2026/9/13 17:02:51

LabVIEW UDS上位机从TOOMOSS到ZLG的CAN硬件移植指南

1. 项目概述&#xff1a;为什么一个CAN UDS上位机的移植值得专门写十三篇&#xff1f;“基于周立功的CAN UDS升级上位机-LabVIEW版本&#xff08;十三&#xff09;&#xff1a;从图莫斯到ZLG的移植指南”——这个标题里藏着三个关键信号&#xff1a;CAN总线、UDS诊断协议、LabV…

作者头像 李华