ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Marlin 固件拆解:一条 G1 命令从串口到定时器的完整旅程,5 层讲清楚

Marlin 固件拆解:一条 G1 命令从串口到定时器的完整旅程,5 层讲清楚 Marlin 固件拆解一条 G1 命令从串口到定时器的完整旅程5 层讲清楚【免费下载链接】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切片软件导出一份几万行的 G 代码文件你盯着首层打印成型热端温度抖一下又稳回来机器没有出任何幺蛾子。这背后Marlin 固件在一颗几兆主频的单片机上把这些行字符一个一个吞进去、排好队、算好速度再吐成步进脉冲——全程没有操作系统就是一个主循环加几个中断。这条链路值得拆开看。我们跟着一行G1 X100 Y50 F6000走一遍它先过命令队列再被速度规划器铺成曲线最后由定时器中断一格一格兑现成脉冲而加热、屏幕、温度自动上报这些后台活全都挤在主循环的缝隙里。下面由外向内一层一层剥。一行指令落进串口先过关行号和校验和串口送来的是一串没有边界的字节Marlin 不急着执行先把它攒成完整的一行。这个工作发生在 queue.cpp 里的get_serial_commands()一个按字符跑的输入状态机处理分号行尾注释、括号内联注释、引号转义直到读到换行才把这一行收进固定长度的环形缓冲就是个首尾相接的定长队列写指针和读指针绕圈走。但如果这一行带了主机协议要求的 N 行号后面还有两个硬检查行号必须等于上一条加一尾部*后面的校验和必须等于前面所有字符的异或。源码里这段逻辑不长char *apos strrchr(command, *); if (apos) { uint8_t checksum 0, count uint8_t(apos - command); while (count) checksum ^ command[--count]; if (strtol(apos 1, nullptr, 10) ! checksum) { gcode_line_error(F(STR_ERR_CHECKSUM_MISMATCH), p); break; } } else { gcode_line_error(F(STR_ERR_NO_CHECKSUM), p); break; }这段在干嘛一行命令进队列之前先把它的完整性验一遍验不过就地打回。打回的方式是gcode_line_error()里那句经典的Resend: n——清空接收缓冲让主机把第 n 行重发一遍。说白了Marlin 承认串口会丢字节所以宁可重传也不赌。行号对不上时还有个约定俗成的解法主机发 M110 重置行号计数器。这里有个坑很多人没注意接收阶段就有几条命令是插队处理的。M112kill 停机这类字在字节流里一出现就直接执行不等自己在环形缓冲里排队——紧急停机命令要是排队那它排队的每一毫秒都是事故。进队之后固件回一个ok给主机开了 ADVANCED_OK 的话回包里还捎带着规划器剩余空位 P 和队列剩余空位 B。主机的流控就是靠这两个数字决定要不要发下一行整个打印过程由此变成你追我赶但谁也不越界的握手。字节流怎么变成一条命令状态机只有几个状态正常接收、行尾注释、内联注释、引号串、转义字符。每来一个字符推一次状态越界就丢弃剩余部分。没有动态内存分配一行命令最多 MAX_CMD_SIZE 字节全部走静态数组——这是 8 位机和 32 位机通吃的关键。丢字节了怎么办就是前面那套行号断档或校验和不对回Resend主机重发。配合ok里带出的 P/B 余量主机端Cura、PrusaSlicer 的发送层都能据此调节发送节奏。第一个脉冲还没发出速度规划器已经算好了整条曲线队列每转一圈advance()从队头取出一条命令交给解析器解析器把参数拆开移动类命令最终在 planner.h 里变成一个 block。注意此刻电机纹丝不动只是入队了一个移动段。每个移动段长这样节选float nominal_speed, // 本段公称速度 (mm/s) entry_speed_sqr, // 上一交接点实际入口速度平方存 (mm/s)^2 min_entry_speed_sqr, // 该交接点允许的最小入口速度 max_entry_speed_sqr, // 该交接点允许的最大入口速度 millimeters, // 本段总行程 (mm) acceleration; // 加速度 (mm/s^2) uint32_t step_event_count; // 走完本段需要的总脉冲事件数 uint32_t accelerate_before, // 从第几个脉冲事件开始进入匀速 decelerate_start; // 从第几个脉冲事件开始减速这段结构体在干嘛把一段 G1 移动压扁成几个数规划的全部结果都落在里面。速度规划用的是梯形规划——把先加速、匀速、再减速算成一段三段直线拼出的速度曲线这套算法是从 grbl 那里继承来的planner.h 文件头注释写得很明白。每个相邻移动段的交界处junction都有一个允许速度范围规划器要解一道约束本段入口速度受本段出口减速约束和上一段加速约束双向夹击所以它先正向扫一遍再反向扫一遍直到所有交接点自洽。转角太急的交接点会额外受 junction_deviation拐角偏差单位是毫米级的路径偏离量压制转急弯必须降速不然轨迹直接划出轮廓。规划完的产物就是那两个索引第 0 个脉冲事件起按加速度爬升decelerate_start起开始收速度中间全是公称速度。规划器把整条路线铺完后面的执行层只负责照单跑腿。另外绕不开的一点规划器缓冲也是定长的。它满了ok里的 P 值就归零主机停发。串口缓冲和规划缓冲这两级定长队列是整条流水线的背压源头。一个移动段怎么装进 block字段就是上面那堆速度、距离、步数、两个转折点索引。存速度的平方而不是速度本身是因为约束方程里全是二次项少做几次开方。两个脉冲索引定生死accelerate_before和decelerate_start把什么时候匀速、什么时候该收钉死在脉冲序号上。执行层不思考它只是数数。运动的最后一微米由定时器中断数出来到了执行层毫米已经没意义了只有脉冲数。stepper.cpp 的中断服务程序被一个硬件定时器按固定频率反复唤醒每响一次给本 block 每个轴的剩余步数各减一次哪个轴先减到零就发出那个轴的 STEP 脉冲并置好方向位——多条轴同时插补就是这么来的本质上是 grbl 一脉的计数器法脉冲节拍里还嵌着 Bresenham 步进一种决定每个脉冲该迈哪个轴的整数直线插值算法的思路。当前 block 所有轴的步数都走完就加载下一个 block把新的accelerate_before/decelerate_start搬进工作寄存器。在 AVR 这种没有硬件乘法器的老芯片上脉冲间的速度换算走的是speed_lookuptable.h里的定点查找表——stepper.h 顶部那个#ifdef __AVR__的 include 就是为此准备的。乘法贵查表便宜嵌入式老手法。顺带一提stepper.h 里的轴位定义能开满 9 个运动轴X/Y/Z 外加 I/J/K/U/V/W挤出机轴最多 8 个所以这台引擎本身是能支撑多轴联动和 8 挤出机的缺的只是主板引脚。每次中断进来干了什么读剩余步数、判断哪个轴到点、拉 STEP、方向已提前设好、block 走完就换。全程几十条指令级别不允许任何阻塞——所以规划慢可以算几百个周期和执行快必须按时分家是整条流水线最重要的一个分工。同一份代码为什么能编译出三十多种芯片的固件从 AVR 到 STM32、ESP32、RP2040Marlin 兼容的 MCU 有三十多种。差异被封在 src/HAL/ 目录里每颗芯片一个子目录各自实现 fastio直接操作寄存器读写引脚、定时器初始化、SPI 这些底层动作。规划器、队列、温度管理这些业务代码从来不碰寄存器它们只调 HAL 暴露的接口。换芯片等于换一套插座上层一行不改。代价是 HAL 目录本身很厚——每个平台的定时器编号、中断向量、引脚映射都得各写各的这部分代码量基本就是平台数量乘以一套底层驱动。运动不停加热也没停主循环怎么干杂活上面讲的都是被命令驱动的活。但一台打印机的多数时间CPU 在干没人指挥的活盯温度、刷屏幕、回温度查询。MarlinCore.cpp 的主循环短得有点朴素void loop() { do { marlin.idle(); // 加热、UI、自动上报…… if (card.flag.abort_sd_printing) abortSDPrinting(); queue.advance(); // 取一条队列命令执行 endstops.event_handler(); // …断电延时、LVGL 刷新等 } while (true); }这个循环在干嘛所有事情都是轮询式的一轮之内该干嘛干嘛没有任何时间片切换也没有任务优先级——它是协作式调度。idle()就是那个大杂烩任务列表void Marlin::idle(const bool no_stepper_sleep) { manage_inactivity(no_stepper_sleep); // 看门狗喂狗、步进休眠等 thermalManager.task(); // 加热每轮必做 ... hal.idletask(); ... ui.update(); // 读按键、刷新屏幕 }注意顺序thermalManager.task()在每次 idle 里无条件跑一轮。temperature.cpp 负责读温度 ADC、做平滑、跑 PID 算出软 PWM 占空比。安全逻辑也在这条链路上目标温度开着、加热输出着温度却长时间不升或者升温速率低于阈值热漂移thermal runaway判定就触发kill()——所有加热器断电、电机停、机器锁死等复位。热敏电阻断了读出来是冰点会先撞 maxtemp/mintemp 范围检查传感器虚焊在冷端走的是 runaway 那条路。两条都堵上加热丝空烧才算真正防住。manage_inactivity()里还有看门狗watchdog硬件定时器代码不按时喂它就复位芯片和步进休眠长时间不动的轴自动断电防止电机堵转发热。加热放在 idle 里热漂移判定的是不升PID 的每一项都不复杂复杂的是判定逻辑它比的不是温度对不对而是温度该动的时候动不动。这也是为什么固件里热漂移报错比超温报错更常见——多数故障是传感器链路的问题不是加热丝的问题。刷屏和温度上报也挤在同一个主循环ui.update()读编码器按键并刷新 LCD自动上报M155 温度、M154 位置这类是一组带节拍的 tick到点了往串口塞一行数据。所以打印时你每 2 秒收到一条ok T:xx /xx B:xx /xx不是谁在专门发而是主循环转到了那一格。上面这张是 Marlin 固件在 TFT 屏上的启动界面上电自检和版本号都在这一步给出也是判断固件有没有正常跑起来的第一个依据。要动 Marlin 源码先看这几处如果你读到这里想上手按这个顺序看少走弯路命令生命周期的主干loop()在 MarlinCore.cpp 底部advance()在 gcode/queue.cpp一出一进先把这条线走通再谈别的。速度算法的入口planner.cpp 里梯形计算那一段输入是 junction_deviation 和加速度配置输出是上面那两个转折点索引。想改运动手感变量在这里。加一条 G 代码gcode.cpp 里是一张命令到函数的表表本身就是全局地图查表比读代码快。支持一块新主板别碰业务代码src/HAL/ 下照着一个现有平台抄目录结构实现 fastio 和定时器上层一行不改。所有行为开关Configuration.h 和 Configuration_adv.h 是入口但真正生效的判断散在各文件的 ENABLED 宏里改之前先全局搜一遍宏名确认它只影响你以为影响的地方。这条从串口字节到脉冲计数器的链路就是 Marlin 固件的全部骨架。它老、它直接、它把每一个工程权衡都摊在台面上——这正是三十多种芯片、几百家整机厂还在用它的原因。【免费下载链接】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),仅供参考
返回列表