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 HAL(Marlin/src/HAL/LINUX/),让整个固件以普通 C++ 控制台程序的形式在宿主机 CPU 上直接编译、运行与测试。本文以该目录下的维护者工作笔记(AGENTS.md)为核心骨架,结合main.cpp、Gpio、eeprom、serial.h、ini/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 = native、framework =(空):不用任何 Arduino 核心与工具链 SDK,唯一的"平台"就是宿主机编译器;-D__PLAT_LINUX__:让#ifdef __PLAT_LINUX__保护的所有 HAL 源码参与编译;build_src_flags里的-IMarlin/src/HAL/LINUX/include:确保本地定制的Arduino.h、serial.h、pinmapping.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中待发送的字节逐个fputc到stdout,然后yield();read_serial_thread:用fgets从stdin读取输入(上限 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结构体记录dir、mode、value和可选的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_PIN和INTERRUPT_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 缓冲清空;MYSERIAL1即usb_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_ch,adc_ready()恒为true。真正的"温度"来自 hardware/Heater.h 的Heater对象:它们挂在HEATER_0_PIN/TEMP_0_PIN、HEATER_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);- 步进/仿真计时由
Clock(main()里设为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是唯一主板,引脚映射就是固定的软件数组。 - 串口对象:没有 Arduino
Serial对象,MYSERIAL1是HalSerial支撑的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),仅供参考