Marlin 固件 TEENSY31_32 HAL 深入解析:MK20DX256 平台适配与开发约定
【免费下载链接】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
导读
本文基于 TEENSY31_32 HAL 工作笔记 展开,系统讲解 Marlin 固件在 PJRC Teensy 3.1 / 3.2 板卡(NXP/Freescale MK20DX256,ARM Cortex-M4,72 MHz,256 KB Flash)上的硬件抽象层实现。文中将完整继承原文档的构建测试流程、框架集成方式、源码级"陷阱清单"与开发约定,并深入对应源码文件验证每一步实现,帮助读者掌握:如何为 Teensy 3.1/3.2 构建与回归测试 Marlin、该 HAL 各模块(EEPROM、定时器、SPI、舵机、ADC、快速 IO、串口)的真实实现机理,以及为这一平台贡献代码时必须遵守的边界与约束。
平台概览:TEENSY31_32 HAL 是什么
Marlin/src/HAL/TEENSY31_32/是 Marlin 面向Teensy 3.1 / 3.2两块板卡的硬件抽象层。两者共用同一颗 MK20DX256 芯片:ARM Cortex-M4 内核、72 MHz 主频、256 KB Flash、64 KB RAM,并拥有 2 KB 真正的片上 EEPROM。
与其他一些 HAL 最大的不同点在于:Teensy 的 Arduino 核心(core)并非随仓库 vendored(内置),而是由 PlatformIO 的teensy平台在构建时拉取的上游 Teensyduino/Arduino 核心。这意味着Marlin/src/HAL/TEENSY31_32/只包含 Marlin 与上游核心之间的"胶水"适配代码,改动范围被严格限制在本 HAL 目录与引脚映射文件内。
目录结构如下:
Marlin/src/HAL/TEENSY31_32/ ├── AGENTS.md # 本文所依据的工作笔记 ├── HAL.cpp / HAL.h # 平台核心抽象(复位、看门狗、ADC、串口声明) ├── timers.cpp / timers.h # FTM0/FTM1 定时器(步进电机 + 温度采样) ├── eeprom.cpp # 有线 EEPROM 持久化存储后端 ├── HAL_SPI.cpp # 基于 Teensyduino SPI 库的 SPI 封装 ├── MarlinSPI.h # MarlinSPI 类型别名 ├── Servo.cpp / Servo.h # 舵机库封装 ├── fastio.h # 基于 bitbanding 的快速 GPIO ├── endstop_interrupts.h # 限位开关中断 ├── spi_pins.h / pinsDebug.h └── inc/ # LCD 条件编译与 SanityCheck构建与测试:用 mftest 验证 Teensy 3.1/3.2 固件
为什么用mftest而不是裸pio run
原文档强调,验证 TEENSY31_32 HAL 的标准方式是mftest,因为它会按目标重新生成Marlin/Configuration.h,而直接pio run -e无法可靠地做到这一点。构建命令如下:
cd "$(git rev-parse --show-toplevel)" buildroot/bin/mftest -t teensy31 -n1 -y # BOARD_TEENSY31_32, MK20DX256该命令会以teensy31环境为目标执行完整构建流程,对应平台配置在 ini/teensy.ini 中定义:
[teensy_arm] platform = teensy@~4.12.0 build_src_filter = ${common.default_src_filter} lib_ignore = NativeEthernet [env:teensy31] extends = teensy_arm board = teensy31 build_src_filter = ${teensy_arm.build_src_filter} +<src/HAL/TEENSY31_32>关键点:build_src_filter在通用源文件基础上追加了src/HAL/TEENSY31_32目录,因此新增 HAL 源文件会被自动纳入构建,无需修改 ini 文件。
三套测试配置与通过标准
buildroot/tests/teensy31/下存放三套测试配置,全部指向同一块主板BOARD_TEENSY31_32:
| 配置 | 说明 |
|---|---|
| config-01.ini | 默认配置 |
| config-02.ini | 零限位开关(zero-endstops)配置 |
| config-03.ini | 功能最全(many-features),压力最大 |
其中config-03 是最紧的约束——它以完整功能集构建,用来压测 256 KB Flash 的容量极限。从其内容可见功能密度极高:双温度传感器、16×16 双线性自动调平、断电续打(eeprom_settings)、断料检测(filament_width_sensor)、弧线/贝塞尔插补、babystepping、喷嘴清洁/停靠、主机动作命令等一应俱全。
通过标准:三套配置必须全部构建绿色,才能认为 Teensy 3.1/3.2 相关工作完成。
清理构建产物的正确姿势
原文档特别警告:不要用rm -rf .pio/build/...强制重建——跨 profile 的写入保护会阻止该操作,且本身不安全。正确做法是:
pio run -e teensy31 -t clean # 只删除构建产物或者干脆让mftest自行重建。
框架集成:上游 Teensyduino 核心,而非 vendored
核心来源与安装位置
Teensy Arduino 核心来自上游 Teensyduino 包(framework-arduinoteensy,平台版本teensy@~4.12.0,见 ini/teensy.ini),通常安装于~/.platformio/packages/framework-arduinoteensy*。它不在buildroot/share/PlatformIO/下(AT32 才是例外情况)。
因此不存在"镜像到已安装包"的步骤:Marlin 侧的改动只存在于本 HAL 文件夹,核心直接从 PlatformIO 注册表拉取。
HAL 目录选择由编译器宏驱动
Marlin/src/HAL/platforms.h中的HAL_PATH宏负责按芯片宏选择 HAL 目录:
#elif defined(__MK20DX256__) #define HAL_PATH(PATH, NAME) XSTR(PATH/HAL/TEENSY31_32/NAME)也就是说,选择本 HAL 的守护宏是__MK20DX256__,而不是ARDUINO_ARCH_TEENSY。文件夹内每个.cpp都以#ifdef __MK20DX256__包裹(例如 HAL.cpp、timers.cpp、eeprom.cpp)。因为 Teensy 3.5/3.6 与 4.0/4.1 的 HAL 共享同一套teensy_arm平台与teensy工具链,一旦漏写该守护宏,文件会被编译进错误的 MCU 目标。这是所有新增源文件都必须遵守的第一约定。
在 HAL.h 中还定义了IS_TEENSY_31_32、IS_TEENSY31/IS_TEENSY32等平台宏,供引脚映射等条件编译使用。
源码级"陷阱清单"逐一验证
原文档列出六条"仅看源码不容易发现"的坑,以下结合源码逐一展开验证,这也是本 HAL 开发中最容易踩雷的地方。
陷阱 1:编译器守护宏是__MK20DX256__
如上一节所述,platforms.h 用__MK20DX256__选择 TEENSY31_32 目录。Teensy 3.5/3.6(__MK64FX512__/__MK66FX1M0__)与 4.0/4.1(__IMXRT1062__)各走各的 HAL,共享工具链但共享不了源文件。始终保留#ifdef __MK20DX256__守护是贡献者最需要记住的一条。
陷阱 2:EEPROM 是有线的,不是 SD/Flash 模拟
MK20DX256 芯片自带 2 KB 真实 EEPROM,因此本 HAL 使用有线(wired)EEPROM,直接调用 AVR 风格的<avr/eeprom.h>接口,见 eeprom.cpp:
#if USE_WIRED_EEPROM #include "../shared/eeprom_api.h" #include <avr/eeprom.h> #ifndef MARLIN_EEPROM_SIZE #define MARLIN_EEPROM_SIZE size_t(E2END + 1) #endif size_t PersistentStore::capacity() { return MARLIN_EEPROM_SIZE - eeprom_exclude_size; }写入时通过REAL_EEPROM_ADDR(pos)计算真实地址,并做了两个工程优化:
- 只写变化的字节:
if (v != eeprom_read_byte(p))—— 因为 EEPROM 寿命约 10 万次擦写,避免无谓磨损; - 长写入期间喂狗:
if (++written & 0x7F) delay(2); else safe_delay(2);防止批量写 EEPROM 时触发看门狗复位。
因此不要为此平台添加SDCARD_EEPROM_EMULATION或任何 SPI/I2C EEPROM 后端——芯片上有真实 EEPROM,USE_WIRED_EEPROM已经覆盖。注意MARLIN_EEPROM_SIZE的默认值基于E2END + 1,即 2 KB 的容量。
陷阱 3:定时器硬编码假设 60 MHz 总线时钟
timer.h 中,FTM 定时器的计数率直接由总线时钟推算:
#define FTM0_TIMER_PRESCALE 8 #define FTM1_TIMER_PRESCALE 4 #define FTM0_TIMER_PRESCALE_BITS 0b011 #define FTM1_TIMER_PRESCALE_BITS 0b010 #define FTM0_TIMER_RATE (F_BUS / (FTM0_TIMER_PRESCALE)) // 60MHz / 8 = 7.5MHz #define FTM1_TIMER_RATE (F_BUS / (FTM1_TIMER_PRESCALE)) // 60MHz / 4 = 15MHz而 timers.cpp 的HAL_timer_start()据此计算 FTM 比较值:
case MF_TIMER_STEP: FTM0_MODE = FTM_MODE_WPDIS | FTM_MODE_FTMEN; FTM0_SC = 0x00; FTM0_CNT = 0x0000; FTM0_MOD = 0xFFFF; FTM0_C0V = (FTM0_TIMER_RATE) / frequency; FTM0_SC = (FTM_SC_CLKS(0b1) & FTM_SC_CLKS_MASK) | (FTM_SC_PS(FTM0_TIMER_PRESCALE_BITS) & FTM_SC_PS_MASK); // Bus clock 60MHz / 8关键假设:F_BUS必须为 60 MHz。teensy31板卡 JSON 的F_CPU与之一致;若该值将来改变,步进电机与温度定时器会以错误速率运行,导致运动控制与温控失真。因此修改时钟相关配置必须极为谨慎。
另一个与温度定时器相关的细节:adc_init()中执行NVIC_ENABLE_IRQ(IRQ_FTM1)(见 HAL.cpp),即温度定时器 FTM1 的中断必须在此处使能,共享温度 ISR(ftm1_isr,见 timers.h)才会触发。MF_TIMER_STEP=0、MF_TIMER_TEMP=1,步进与温度各占一个 FTM 通道。
中断使能/禁用在 timers.cpp 中实现,禁用路径特意加入了内存屏障__DSB()/__ISB(),确保中断在 Cortex-M 上被真正禁用。
陷阱 4:SPI 包装的是 TeensyduinoSPI库,不是自研驱动
HAL_SPI.cpp 的所有收发路径最终都落到上游<SPI.h>的SPI.begin()/SPI.transfer():
uint8_t spiRec() { SPI.beginTransaction(spiConfig); const uint8_t returnByte = SPI.transfer(0xFF); SPI.endTransaction(); return returnByte; }速率映射由 Marlin 的标准速率常量给出(见spiInit):
| spiRate | 时钟 |
|---|---|
SPI_FULL_SPEED | 10 MHz |
SPI_HALF_SPEED | 5 MHz |
SPI_QUARTER_SPEED | 2.5 MHz |
SPI_EIGHTH_SPEED | 1.25 MHz |
SPI_SPEED_5 | 625 kHz |
SPI_SPEED_6 | 312.5 kHz |
| 默认 | 4 MHz |
同时 MarlinSPI.h 简单地将MarlinSPI定义为SPIClass的别名:
#include <SPI.h> using MarlinSPI = SPIClass;注意事项:spiSendBlock()中仍残留着 AVR 风格的SPDR/SPSR寄存器操作代码(见 HAL_SPI.cpp),但该路径实际是死代码——活跃路径走SPI.transfer。不要被误导去把它"修复"成寄存器驱动,当前实现才是正确语义。
陷阱 5:舵机move()里的attach(0)是占位符,不是 0 号引脚
Servo.h 中libServo继承官方Servo库并扩展了attach/move。真正的引脚保存在servoPin[MAX_SERVOS]数组中,Servo.cpp 的实现为:
int8_t libServo::attach(const int inPin) { if (servoIndex >= MAX_SERVOS) return -1; if (inPin > 0) servoPin[servoIndex] = inPin; // 真实引脚只在此处记录 return super::attach(servoPin[servoIndex]); } void libServo::move(const int value) { constexpr uint16_t servo_delay[] = SERVO_DELAY; static_assert(COUNT(servo_delay) == NUM_SERVOS, "SERVO_DELAY must be an array NUM_SERVOS long."); if (attach(0) >= 0) { // 0 是占位符:不覆盖 servoPin,仅重挂载已存引脚 write(value); safe_delay(servo_delay[servoIndex]); TERN_(DEACTIVATE_SERVOS_AFTER_MOVE, detach()); } }attach(0)传入 0 时由于inPin > 0不成立,servoPin[servoIndex]保持原值,从而解析到之前记录的真实引脚。不要改成实际引脚号,否则move()每次都会覆盖正确的引脚记录。move()还带有SERVO_DELAY驱动的安全延时,并支持DEACTIVATE_SERVOS_AFTER_MOVE配置在移动后自动detach()。
陷阱 6:ADC 走固定查表pin2sc1a[],不是analogRead
HAL.cpp 的adc_start()使用一张手工构建的查表,把 Marlin 引脚号映射为 MK20DX256 的 ADC0 SC1A 通道号:
void MarlinHAL::adc_start(const pin_t pin) { static const uint8_t pin2sc1a[] = { 5, 14, 8, 9, 13, 12, 6, 7, 15, 4, 0, 19, 3, 31, // 0-13, 视为 A0-A13 5, 14, 8, 9, 13, 12, 6, 7, 15, 4, // 14-23 (A0-A9) 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, // 24-33 0+64, 19+64, 3+64, 31+64, // 34-37 (A10-A13) 26, 22, 23, 27, 29, 30 // 38-43: 温度传感器、VREF_OUT、A14、bandgap、VREFH、VREFL }; ADC0_SC1A = pin2sc1a[pin]; } uint16_t MarlinHAL::adc_value() { return ADC0_RA; }采样流程是adc_start()写通道 →ADC0_RA读结果(adc_value()),由温度中断驱动。而 HAL.h 中的analogInputToDigitalPin宏只覆盖前 12 个 A 引脚((p < 12U) ? (p) + 54U : -1)。因此模拟采样必须走hal.adc_start()/adc_value(),绝不要用analogRead()。ADC 参考电压为 3.3 V、10 位分辨率(HAL_ADC_VREF_MV 3300、HAL_ADC_RESOLUTION 10)。
adc_init()中先analog_init()并等待校准完成,再使能 FTM1 中断(见 HAL.cpp)。
开发约定与代码边界
目录职责划分
- HAL 改动只允许落在
Marlin/src/HAL/TEENSY31_32/; - 板级引脚改动只允许落在
Marlin/src/pins/teensy3/pins_TEENSY31_32.h(例如该文件将 X/Y/Z 限位开关映射到引脚 3/4/5、步进使能共用引脚 2 等,见 pins_TEENSY31_32.h); - 核心与 PlatformIO 集成属于上游——这里没有可编辑的 vendored 框架,不要试图"镜像"或修改已安装的上游核心。
fastio.h:位带(bitbanding)快速 GPIO 与一个已知怪癖
fastio.h 的头部注释写着"Teensy 3.5 and 3.6",但实际是 3.1/3.2 的实现——这是一个历史遗留的注释错误,实现本身通过 Cortex-M 位带操作直接操纵端口寄存器:
#define GPIO_BITBAND_ADDR(reg, bit) (((uint32_t)&(reg) - 0x40000000) * 32 + (bit) * 4 + 0x42000000) #define GPIO_BITBAND(reg, bit) (*(uint32_t *)GPIO_BITBAND_ADDR((reg), (bit))) #define _WRITE(P,V) do{ \ if (V) CORE_PIN ## P ## _PORTSET = CORE_PIN ## P ## _BITMASK; \ else CORE_PIN ## P ## _PORTCLEAR = CORE_PIN ## P ## _BITMASK; \ }while(0)它基于 Teensy 核心提供的CORE_PINxx_PORTSET/PORTCLEAR符号实现原子置位/清零。原文档指出一个已知怪癖:_IS_OUTPUT与_IS_INPUT定义完全相同(都检查方向寄存器的 0 值),即输出方向实际上没有被区分——目前无害,但不要新增依赖输出方向判断的代码。
串口体系:Serial0 别名与多端口转发
HAL.h 定义了完整的串口抽象:
Serial0直接别名核心的Serial;USBSerial包装SerialUSB(ForwardSerial1Class<decltype(SerialUSB)>);- 多端口
DefaultSerialX对象转发到对应的SerialX(ForwardSerial1Class<decltype(Serial##X)>); SERIAL_INDEX_MIN/MAX为 0..3,即支持最多 4 个串口索引。
HAL.cpp 根据SERIAL_PORT/SERIAL_PORT_2/SERIAL_PORT_3/MMU_SERIAL_PORT/LCD_SERIAL_PORT等配置实例化对应端口对象,并与 shared/serial_ports.h 协同完成按SERIAL_PORT选择默认串口的逻辑。
其余模块速览
- 看门狗(HAL.cpp):
USE_WATCHDOG启用时配置 WDOG 超时 4 s 或 8 s(WATCHDOG_DURATION_8S),刷新序列为经典0xA602/0xB480; - 复位源(HAL.cpp):通过
RCM_SRS0区分上电/外部/看门狗复位; - 限位中断(endstop_interrupts.h):所有限位引脚
attachInterrupt(..., CHANGE)汇入单一endstop_ISR(),仅在状态变化时调用endstops.update(),省去温度中断里的轮询开销; - 自由内存(HAL.cpp):基于
__bss_end/__heap_start/__brkval计算堆剩余。
关联资源导航
- 共享 HAL API(
eeprom_api、SPI 辅助):Marlin/src/HAL/shared/ - 板级引脚映射:Marlin/src/pins/teensy3/pins_TEENSY31_32.h
- 构建定义:ini/teensy.ini
- 回归测试配置:buildroot/tests/teensy31/
- 平台选择逻辑:Marlin/src/HAL/platforms.h
小结
TEENSY31_32 HAL 是 Marlin 硬件抽象体系中一个"上游核心 + 薄胶水层"的典型代表:全部源码受__MK20DX256__守护,EEPROM 使用芯片自带 2 KB 有线存储,定时器建立在 60 MHz 总线时钟假设之上,SPI/舵机/ADC 分别包装或绕过上游库的特定行为。本文的六条陷阱清单直接来自维护者的开发笔记,并经源码逐条印证——对任何准备为 Teensy 3.1/3.2 提交代码、排查构建或运行时问题的开发者而言,遵循这些约定(尤其是守护宏、EEPROM 后端选择、attach(0)占位符语义与 ADC 查表路径)都能显著降低踩坑概率。
【免费下载链接】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),仅供参考