news 2026/9/13 12:31:35

Marlin LINUX HAL 深度指南:无 MCU 的原生主机构建、仿真运行与调试实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Marlin LINUX HAL 深度指南:无 MCU 的原生主机构建、仿真运行与调试实践

Marlin LINUX HAL 深度指南:无 MCU 的原生主机构建、仿真运行与调试实践

【免费下载链接】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 固件不仅面向 AVR/STM32 等真实主控板,还在仓库中提供了一套无硬件依赖的 LINUX HALMarlin/src/HAL/LINUX/),让整个固件以普通 C++ 控制台程序的形式在宿主机 CPU 上直接编译、运行与测试。本文以该目录下的维护者工作笔记(AGENTS.md)为核心骨架,结合main.cppGpioeepromserial.hini/native.ini等源码与构建配置,系统讲解 LINUX HAL 的构建测试流程、原生平台集成方式、十个"源码里看不出来"的关键实现细节,以及编写该 HAL 代码时必须遵守的约定。读完本文,你将能够独立完成 LINUX HAL 的编译验证、理解其仿真机制,并能在修改 HAL 代码时避免踩坑。

一、LINUX HAL 是什么:Marlin 的"无板子"运行形态

LINUX HAL 是 Marlin 中一个特殊的硬件抽象层:它不是为某块真实主控板服务,而是把 Marlin 整体编译成一个运行在宿主机上的普通 C++ 程序。与嵌入式构建相比,它有如下本质区别:

  • 没有 MCU,没有 Arduino 框架。整个构建使用宿主机的 C++ 编译器(-std=gnu++17),链接librt/libpthread
  • 程序入口是main(),而不是 Arduino 的setup()/loop()封装;
  • HAL 选择由宏__PLAT_LINUX__决定(HAL/platforms.h 将__PLAT_LINUX__映射到src/HAL/LINUX/),它没有ARDUINO_ARCH_*定义;
  • 用途是开发调试与自动化测试:在没有硬件的情况下跑通固件逻辑、跑 Unity 单元测试,而不是驱动真实电机和传感器。

需要特别区分的是:ini/native.ini中的simulator_linux_*环境构建的是另一个HAL——带 SDL2/GLM 图形界面的 NATIVE_SIM HAL。LINUX HAL 是无头(headless)控制台构建,NATIVE_SIM 是 GUI 模拟器,两者不要混淆。

二、构建与测试闭环:mftest 与两个关键构建环境

LINUX HAL 的验证入口是仓库自带的mftest脚本。它有一个重要的职责:按测试目标重新生成Marlin/Configuration.h,这是裸pio run -e无法可靠做到的事情。因此维护者约定统一用mftest验证:

cd "$(git rev-parse --show-toplevel)" buildroot/bin/mftest -t linux_native -n1 -y # host-native build, EEPROM enabled buildroot/bin/mftest -t linux_native_test -n1 -y # build + Unity unit tests

其中-t指定目标,-n1限制并行任务数,-y自动确认。mftest内部对以lin开头的目标会选用linux_native环境(见 buildroot/bin/mftest 中的lin*) TESTENV='linux_native'分支)。

2.1linux_native:主冒烟目标

该环境定义在 ini/native.ini:

[env:linux_native] platform = native framework = build_flags = ${common.build_flags} -D__PLAT_LINUX__ -std=gnu++17 -ggdb -g -lrt -lpthread -D__MARLIN_FIRMWARE__ -Wno-expansion-to-defined build_src_flags = -Wall -IMarlin/src/HAL/LINUX/include build_unflags = -Wall -fsingle-precision-constant lib_ldf_mode = off build_src_filter = ${common.default_src_filter} +<src/HAL/LINUX>

要点解读:

  • platform = nativeframework =(空):不用任何 Arduino 核心与工具链 SDK,唯一的"平台"就是宿主机编译器;
  • -D__PLAT_LINUX__:让#ifdef __PLAT_LINUX__保护的所有 HAL 源码参与编译;
  • build_src_flags里的-IMarlin/src/HAL/LINUX/include:确保本地定制的Arduino.hserial.hpinmapping.h优先于任何系统头文件被找到;
  • -lrt -lpthread:计时器/时钟与多线程所需链接库;
  • build_src_filter在公共过滤规则上追加+<src/HAL/LINUX>,其余 Marlin 代码正常编译。

它对应的测试配置是 buildroot/tests/linux_native/config-01.ini:

[config:base] motherboard = BOARD_SIMULATED temp_sensor_bed = 1 pidtempbed = on eeprom_settings = on baud_rate_gcode = on

可以看到:主板选BOARD_SIMULATED、开启 EEPROM 设置(对应前文命令注释中的 "EEPROM enabled")。在提交任何 LINUX HAL 修改前,此目标必须保持绿色。

2.2linux_native_test:构建 + Unity 单元测试

[env:linux_native_test] extends = env:linux_native extra_scripts = ${common.extra_scripts} post:buildroot/share/PlatformIO/scripts/collect-code-tests.py build_src_filter = ${env:linux_native.build_src_filter} +<tests> lib_deps = throwtheswitch/Unity@^2.6.0 test_build_src = true build_flags = ${env:linux_native.build_flags} -Werror -DNO_USER_FEEDBACK_WARNING

该环境在linux_native基础上叠加了 PlatformIO 的 Unity 测试框架(+<tests>源码、-Werror严格告警),并通过collect-code-tests.py在构建后动态收集测试目标。仓库内的单元测试包括 tests/core/test_macros.cpp、tests/core/test_types.cpp、tests/feature/test_runout.cpp、tests/gcode/test_gcode.cpp 等。当你改动到有单元测试覆盖的 HAL 代码时,必须跑这个环境。

2.3 强制重建的正确姿势

维护笔记明确警告:不要用rm -rf .pio/build/...强制重建——跨 profile 写保护会拦截该操作,而且本身不安全。正确做法是:

pio run -e <env> -t clean # 只清除构建产物

或者干脆让mftest自动重建。

三、原生平台集成:一个普通 C++ 程序的运行模型

因为 LINUX HAL 是纯 C++ 程序,它的运行时模型与嵌入式完全不同,核心代码集中在 main.cpp(整体包在#ifdef __PLAT_LINUX__#ifndef UNIT_TEST中)。

3.1 三个线程:串口读写与仿真循环

main()依次启动:

int main() { std::thread write_serial (write_serial_thread); // TX 环形缓冲 → stdout std::thread read_serial (read_serial_thread); // stdin → RX 环形缓冲 ... Clock::setFrequency(F_CPU); Clock::setTimeMultiplier(1.0); // some testing at 10x HAL_timer_init(); std::thread simulation (simulation_loop); // 虚拟"硬件"反馈循环 DELAY_US(10000); setup(); for (;;) { loop(); std::this_thread::yield(); } }
  • write_serial_thread:把usb_serial.transmit_buffer中待发送的字节逐个fputcstdout,然后yield()
  • read_serial_thread:用fgetsstdin读取输入(上限 254 字节,配合usb_serial.receive_buffer.free()),逐字节写入 RX 环形缓冲;
  • simulation_loop:构造Heater(热端/热床)与LinearAxis(X/Y/Z/E 轴)对象,周期性调用其update(),在软件层面更新引脚状态——这是 LINUX HAL 唯一的"硬件"反馈回路。

因此,G-code 必须通过进程的stdin/stdout管道输入或交互输入,不存在 UART/USB 设备。

3.2 仿真时钟与计时

Clock::setFrequency(F_CPU)将仿真时钟频率设为F_CPU(HAL.h 中定义为100000000UL,即 100 MHz)。HAL_timer_init()初始化定时器,各线程用std::this_thread::yield()让出 CPU。Clock::setTimeMultiplier(1.0)还可以把时间乘数调成 10x 来加速测试。

3.3 缺失系统函数的兜底

Linux 上如果宿主系统没有strlcpy(macOS 通过HAS_LIBBSD提供),HAL 会退回本地实现MarlinHAL::_strlcpy(定义在 HAL.cpp,HAL.h 中以#define strlcpy hal._strlcpy重定向),无需依赖系统库。

四、十个"源码里看不出来"的关键实现细节

这是 AGENTS.md 的核心价值所在。以下每条都对应真实源码,改动 HAL 代码前务必了解。

4.1 没有真实引脚——GPIO 是内存软件数组

Gpio::pin_count = 255(hardware/Gpio.h),引脚只是内存中pin_map[]数组的索引,而非物理线。pin_data结构体记录dirmodevalue和可选的Peripheral* cb回调。Gpio::set/get/clear/setMode/setDir等静态方法在操作数组的同时,会生成GpioEvent(含时间戳Clock::nanos())通知回调或日志器。

fastio.h 中的所有宏都委托给Gpio::方法:

#define SET_DIR_INPUT(IO) Gpio::setDir(IO, 1) #define SET_DIR_OUTPUT(IO) Gpio::setDir(IO, 0) #define WRITE_PIN_SET(IO) Gpio::set(IO) #define WRITE_PIN_CLR(IO) Gpio::clear(IO) #define READ_PIN(IO) Gpio::get(IO) #define WRITE_PIN(IO,V) Gpio::set(IO, V)

而 include/pinmapping.h 中PIN_EXISTS相关的GET_PIN_MAP_INDEX是恒等传递(return pin),PWM_PININTERRUPT_PIN永远返回false不要在这里添加任何硬件相关的引脚逻辑。

4.2 串口是虚拟的,走 stdio

HalSerial(include/serial.h)是一对RingBuffer<uint8_t, 128>(模板环形缓冲,容量 128 字节,需为 2 的幂)。关键行为:

  • host_connected构造时初始化为true
  • write()在 TX 环形缓冲满时忙等(while (!transmit_buffer.free()););
  • flushTX()会忙等 TX 缓冲清空;
  • MYSERIAL1usb_serial(HAL.cpp 中MSerialT usb_serial(...)),由typedef Serial1Class<HalSerial> MSerialT包装成 Marlin 串口体系。

所以调试时,把 G-code 文件管道进 stdin、从 stdout 读输出即可。

4.3 EEPROM 就是一个磁盘文件

eeprom.cpp(仅在EEPROM_SETTINGS开启时编译)把 EEPROM 模拟为当前工作目录下的eeprom.dat

  • 容量MARLIN_EEPROM_SIZE = 0x1000(4 KB),未定义时默认此值;
  • access_start()fopen("rb")读文件;文件不足 4 KB 时,剩余部分以0xFF填充(EEPROM 擦除值);
  • write_data/read_data直接操作内存缓冲buffer[]并累计crc16
  • 真正落盘发生在access_finish(),它用fopen("wb")把整个缓冲写回文件。

这意味着:设置会在多次运行间通过eeprom.dat持久化;工作目录里残留的旧eeprom.dat可能掩盖你的配置改动,需要全新存储时直接删掉该文件(这是文档明确建议的操作,注意不要用rm -rf .pio之类的方式清理构建)。

4.4 没有真实 ADC / PID 反馈

MarlinHAL::adc_value()(HAL.cpp)从模拟 GPIO 位值合成 10 位读数:

uint16_t MarlinHAL::adc_value() { const pin_t pin = analogInputToDigitalPin(active_ch); if (!isValidPin(pin)) return 0; return uint16_t((Gpio::get(pin) >> 2) & 0x3FF); // return 10bit value as Marlin expects }

adc_start(ch)只记录active_chadc_ready()恒为true。真正的"温度"来自 hardware/Heater.h 的Heater对象:它们挂在HEATER_0_PIN/TEMP_0_PINHEATER_BED_PIN/TEMP_BED_PIN等引脚上,由simulation_loop每轮update()模拟升温/降温。

4.5UNIT_TEST下不编译main()

main.cpp 整体被#ifndef UNIT_TEST包裹。linux_native_test环境把 HAL 编译成库供 Unity 测试使用,不链接main()——所以测试构建"成功"并不代表控制台入口存在。不要写只从main()触发的 HAL 运行时代码,并指望它在测试环境下执行。

4.6 线程 +yield()模拟时序,而非中断

LINUX HAL 没有真实 ISR:

  • CRITICAL_SECTION_START/END为空宏(HAL.h);
  • isr_on/isr_off是空操作,isr_state()true
  • DELAY_CYCLES(x)展开为Clock::delayCycles(x)
  • 步进/仿真计时由Clockmain()里设为F_CPU)和HAL_timer_init()驱动,配合各线程std::this_thread::yield()

因此实时行为是近似的、受宿主机负载影响,不适合做严格时序验证。

4.7freeMemory()恒返回 0

不同于嵌入式 HAL 报告剩余 SRAM,LINUX HAL 的freeMemory()直接返回 0(HAL.cpp),MarlinHAL::freeMemory()只是其包装。任何依赖返回值判断堆内存余量的逻辑在此无效。

4.8 模拟输入固定为 16 路

include/pinmapping.h 硬编码:

constexpr uint8_t NUM_ANALOG_INPUTS = 16; constexpr uint8_t analog_offset = NUM_DIGITAL_PINS - NUM_ANALOG_INPUTS; // 239

模拟索引 0..15 映射到 255 路数字空间的顶部(239..254)。analogInputToDigitalPin/digitalPinToAnalogIndex是 constexpr 算术,不是按板子的查表。

4.9 SD/媒体基于宿主文件系统

没有 SD 卡控制器。虽然 spi_pins.h 仍定义默认SD_SCK_PIN/SD_MISO_PIN/SD_MOSI_PIN(50/51/52)和SOFTWARE_SPI回退,但实际媒体操作作用于宿主机文件系统。把 SD 操作当作宿主环境里的普通文件 I/O 即可。

4.10 可选的 GPIO/位置 CSV 日志

main.cpp 中默认注释掉的//#define GPIO_LOGGING开启后:

  • IOLoggerCSV logger("all_gpio_log.csv")通过Gpio::attachLogger(&logger)订阅全部引脚事件(GpioEvent含时间戳、引脚号、事件类型);
  • 另写axis_position_log.csv记录 X/Y/Z 轴位置变化(仅在坐标变化时追加一行并flush)。

这是仅用于调试的手段,会带来文件系统副作用;除非在追踪引脚/轴活动,否则保持关闭。

五、编码约定与边界

  • 守卫宏:所有 HAL 源码用#ifdef __PLAT_LINUX__保护(而不是ARDUINO_ARCH_*),每个.cpp都套用许可/骨架头。
  • 目录边界:改动应留在Marlin/src/HAL/LINUX/及其hardware/include/inc/u8g/子目录。这里没有板级引脚文件——BOARD_SIMULATED是唯一主板,引脚映射就是固定的软件数组。
  • 串口对象:没有 ArduinoSerial对象,MYSERIAL1HalSerial支撑的usb_serial(HAL.h、HAL.cpp)。
  • 提交前验证:改动 HAL 代码后,跑linux_native(涉及单元测试则加跑linux_native_test)确认绿色再提交。

六、相关资源速查

  • Marlin/src/HAL/NATIVE_SIM/:孪生的 GUI 模拟器 HAL(SDL2/GLM),通过simulator_linux_*环境构建,与本文的 LINUX HAL 是不同目标;
  • Marlin/src/HAL/shared/:本 HAL 复用的共享 API(eeprom_api、SPI 辅助、Marduino.h);
  • ini/native.ini:[env:linux_native]/[env:linux_native_test]定义(另有 macOS/Windows 模拟器环境示例);
  • buildroot/tests/linux_native/config-01.ini:BOARD_SIMULATED+ EEPROM 的测试配置;
  • 单元测试入口:tests/gcode/test_gcode.cpp、tests/feature/test_runout.cpp、tests/core/test_macros.cpp 等。

结语

LINUX HAL 是 Marlin 仓库中一个精巧的"软件定义硬件"层:用 255 路内存引脚、stdio 虚拟串口、文件化 EEPROM 和线程化仿真循环,把一整台 3D 打印机固件搬到了宿主机上。理解它的构建入口(mftest双目标)、运行模型(main()+ 三线程)和十个非显而易见的实现细节,是安全修改该 HAL、为其补充单元测试的前提。下次动手前,请记住两条铁律:验证走mftest -t linux_native(_test),重建走pio run -t clean

【免费下载链接】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 12:30:58

Java官网下载到安装配置全指南:JDK版本选择与环境变量详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 12:30:07

nRF52 低功耗按键唤醒实战:关闭 RTC1 中断 + GPIOTE SENSE 检测方案

摘要&#xff1a;本文基于 nRF52 芯片&#xff0c;实现低功耗按键唤醒方案。核心思路是进入睡眠前关闭 RTC1 中断并断电&#xff0c;配置 GPIOTE SENSE 检测按键&#xff0c;唤醒后恢复 RTC1 并加入软件消抖&#xff0c;避免误触发。下面是低功耗唤醒的完整流程&#xff1a; #m…

作者头像 李华
网站建设 2026/9/13 12:25:46

Delphi 10.3 集成 CEF4Delphi:初始化、多标签与JS交互实战

简介&#xff1a;一份面向Delphi开发者的Chromium内核嵌入方案示例&#xff0c;作者在Rad Studio 10.3 Rio下实测通过。集成CEF4Delphi、DcefBrowser与TChromeTabs组件&#xff0c;解决在原生Windows程序中加载谷歌浏览器内核、管理多标签页&#xff0c;以及JS与网页元素交互控…

作者头像 李华
网站建设 2026/9/13 12:24:28

RISC-V车规开发链路:IAR编译器、芯来IP与MachineWare仿真协同落地

1. 这不是一次普通的技术合作&#xff1a;RISC-V车规级开发链路的“断点缝合”你有没有遇到过这样的场景&#xff1a;团队刚用芯来科技的玄铁RISC-V内核芯片跑通了基础BSP&#xff0c;结果在做ASIL-B功能安全认证时卡在了编译器环节——IAR Embedded Workbench报出一连串未定义…

作者头像 李华
网站建设 2026/9/13 12:22:33

STM32 GPIO外部中断深度解析:从硬件映射到HAL回调全链路

1. 为什么GPIO外部中断不是“配个引脚就能用”的功能&#xff1f;在STM32开发中&#xff0c;GPIO外部中断&#xff08;EXTI&#xff09;是高频使用、却高频出错的功能模块。我带过三届嵌入式实训班&#xff0c;每届都有超过60%的学员在第一个带按键中断的项目里卡住——不是不会…

作者头像 李华