Marlin 二进制文件传输协议(BFT)实战指南:从 M28 B1 到 SD 卡高速写入
【免费下载链接】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 固件仓库中的协议规范文档与源码,完整讲解通过串口向 SD 卡进行二进制文件传输的 BFT(Binary File Transfer)协议。文中将带你掌握协议的数据包格式、Fletchers 校验和算法、连接控制与传输控制两类报文,以及从M28 B1进入二进制模式到最终关闭连接的标准交互流程,并深入binary_stream.cpp、M28_M29.cpp等源码印证每个协议细节的实现方式。
协议概览与启用前提
Marlin 通过串口将二进制数据直接写入内部存储(SD 卡)的能力,由编译期选项BINARY_FILE_TRANSFER控制。该选项默认关闭,需要在 Marlin/Configuration_adv.h 中取消注释:
// Add an optimized binary file transfer mode, initiated with 'M28 B1' //#define BINARY_FILE_TRANSFER #if ENABLED(BINARY_FILE_TRANSFER) // Include extra facilities (e.g., 'M20 F') supporting firmware upload via BINARY_FILE_TRANSFER //#define CUSTOM_FIRMWARE_UPLOAD #endif启用后,配合CUSTOM_FIRMWARE_UPLOAD还可支持固件上传等扩展用途。在 ini/features.ini 中可以确认该功能关联的编译单元:
BINARY_FILE_TRANSFER = build_src_filter=+<src/feature/binary_stream.cpp> +<src/libs/heatshrink>即启用该选项后,固件会额外编译 Marlin/src/feature/binary_stream.cpp(BFT 协议状态机与文件传输实现)以及Marlin/src/libs/heatshrink(heatshrink 解压器,用于可选的压缩传输)。在真实构建环境中,buildroot/tests/mks_robin_nano_v1v2_maple/config-04.ini即为一个同时启用 BFT 的测试配置示例。
一旦固件以该选项编译,主机端(PC 软件等)向打印机串口发送 ASCII 命令M28 B1即可进入二进制传输模式。这一入口定义在 Marlin/src/gcode/sd/M28_M29.cpp 中:M28解析到以B开头且后跟数字的参数时,若数字大于 0 则置位card.flag.binary_mode,并回显Switching to Binary Protocol;同时记录当前命令所在串口作为transfer_port_index,后续二进制数据将从该端口接收。
进入二进制模式后,Marlin 的串口命令循环不再按 ASCII 行解析,而是直接把串口数据喂给BinaryStream::receive()状态机,这一点可以从 Marlin/src/gcode/queue.cpp 的实现得到印证:当card.flag.binary_mode为真时,直接调用binaryStream[card.transfer_port_index.index].receive(...)并以serial_line_buffer作为接收缓冲。
字节序(Endianness)
协议中所有多字节数据结构均采用little-endian(小端序)。构造数据包时,低位字节必须先行发送。每个数据包以 16 位起始标记(Start Token)0xB5AD开头,因此线上实际先发送0xAD,再发送0xB5。
一个只有头部、没有载荷的 Connection SYNC 数据包在字节流中的排列如下:
S S P P P H t y r a a e a n o c y a r c t k l d t o e o e c t a r o d l t C y l S p e e n ---- -- - - ---- ---- ADB5 00 0 1 0000 0103即:AD B5(Start Token)→00(Sync Number)→0(协议 ID 高位 4bit)与1(包类型低位 4bit)合成meta字节 →00 00(Payload Length)→01 03(Header Checksum)。在源码中,头部的解析对应 Marlin/src/feature/binary_stream.h 中定义的Header联合体,其中meta字节的高 4 位是protocol()、低 4 位是type()。
数据包头部(Packet Header)
头部固定为 8 字节,布局如下:
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-------------------------------+---------------+-------+-------+ | Start Token (0xB5AD) | Sync Number | Prot- | Pack- | | | | ocol | et | | | | ID | Type | +-------------------------------+---------------+-------+-------+ | Payload Length | Header Checksum | +-------------------------------+-------------------------------+各字段说明:
| 字段 | 宽度 | 说明 |
|---|---|---|
| Start Token | 16 bits | 每个数据包必须以 16 位值0xB5AD开始。 |
| Sync Number | 8 bits | 同步值。从同步(SYNC)之后的每个数据包都应将其递增 1。 |
| Protocol ID | 4 bits | 协议 ID。0为 Connection Control(连接控制),1为 Transfer(文件传输)。详见下文。 |
| Packet Type | 4 bits | 数据包类型 ID。取值取决于 Protocol ID,见下文各节。 |
| Payload Length | 16 bits | 载荷数据长度。若该值大于 0,则头部之后紧跟数据包载荷。 |
| Header Checksum | 16 bits | 头部数据(不含 Start Token)的 16 位 Fletchers 校验和。 |
数据包载荷(Packet Payload)
当头部中的 Payload Length 字段非零时,头部之后应跟随后续载荷:
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-------------------------------+-------------------------------+ | Payload Data ... | +-------------------------------+-------------------------------+ | ... | Packet Checksum | +-------------------------------+-------------------------------+| 字段 | 宽度 | 说明 |
|---|---|---|
| Payload Data | Payload Length 字节 | 载荷数据。长度不应超过 Connection SYNC 操作报告的缓冲长度。 |
| Packet Checksum | 16 bits | 头部与载荷的 16 位 Fletchers 校验和,包含 Header Checksum,但不含 Start Token。 |
从源码看,接收端的状态机PACKET_HEADER → PACKET_DATA → PACKET_FOOTER → PACKET_PROCESS会严格校验头部校验和与整包校验和;任何一步失败都会进入PACKET_RESEND状态,向主机发送rs重传请求(Marlin/src/feature/binary_stream.h)。
Fletchers 校验和
无论头部校验和还是整包校验和,都使用 16 位 Fletchers 校验和算法,且两种情况都不包含数据包前两个字节(即 Start Token)。一个简单实现示例:
uint16_t cs = 0; for (size_t i = 2; i<packet.size(); i++) { uint8_t cslow = (((cs & 0xFF) + packet[i]) % 255); cs = ((((cs >> 8) + cslow) % 255) << 8) | cslow; }在固件侧,BinaryStream::checksum()实现了完全相同的迭代公式:
// fletchers 16 checksum uint32_t checksum(uint32_t cs, uint8_t value) { uint16_t cs_low = (((cs & 0xFF) + value) % 255); return ((((cs >> 8) + cs_low) % 255) << 8) | cs_low; }(Marlin/src/feature/binary_stream.h)。接收时头部校验和会在读到第 6 字节时被暂存(因为头部校验和字段本身不能参与自己的计算),随后与头部中的校验和字段比对(Marlin/src/feature/binary_stream.h)。
通用响应(General Responses)
ok
除 SYNC 数据包外,所有数据包都会收到一条ok<SYNC>消息作为确认。该确认仅表示客户端已收到数据包且头部格式良好;并不表示操作成功——在客户端还会发送详细响应消息的情况下,ok不代表业务成功。当前实现中最值得注意的是:当主机发送多个相同 Sync Number 的数据包时,客户端仍会回复ok,但不会发送正确的业务响应或任何错误信息。
注意:ok确认会在任何数据包类型专属输出之前发送。SYNC值应与最后发送数据包的 Sync Number 一致,下一个数据包应使用该值 + 1 的 Sync Number。
示例:
ok1从源码看,ok是在PACKET_PROCESS状态处理业务前先输出的(Marlin/src/feature/binary_stream.h):
case StreamState::PACKET_PROCESS: sync++; packet_retries = 0; bytes_received += packet.header.size; SERIAL_ECHOLNPGM("ok", packet.header.sync); // transmit valid packet received dispatch();rs
当数据包乱序到达(Sync Number 不是上一个 Sync Number + 1)时,客户端会发送一条rs<SYNC>消息,其中携带最后接收到的 Sync Number,提示主机据此重传。
示例:
rs1源码中的触发场景包括:头部校验和损坏、载荷校验和损坏、数据流超时等,此时状态机进入PACKET_RESEND并输出rs(Marlin/src/feature/binary_stream.h)。另外,若主机的 Sync Number 恰好是上一个已确认包的 Sync(即sync - 1),客户端会判定此前的ok应答可能丢失,于是补发ok并丢弃重复载荷(Marlin/src/feature/binary_stream.h)。
连接控制(Connection Control,Protocol ID 0)
Protocol ID 0 的数据包负责控制二进制连接本身,只有 2 种类型:
| Packet Type | 名称 | 说明 |
|---|---|---|
| 1 | SYNC | 同步主机与客户端并获取连接信息。 |
| 2 | CLOSE | 关闭二进制连接并切回 ASCII 模式。 |
SYNC 数据包
SYNC 数据包应为主机在启用二进制模式后发送的第一个数据包。成功后,客户端会发送 sync 响应。
注意:这是唯一一个不会被ok响应的数据包。
返回的 sync 响应格式:
ss<SYNC>,<BUFFER_SIZE>,<VERSION_MAJOR>.<VERSION_MINOR>.<VERSION_PATCH>| 值 | 说明 |
|---|---|
| SYNC | 当前 Sync Number,应作为下一个数据包的 Sync Number,并在其后每个数据包上递增 1。 |
| BUFFER_SIZE | 客户端缓冲区大小。数据包 Payload Length 不得超过该值。 |
| VERSION_MAJOR | 客户端 Marlin BFT 协议主版本号,例如0。 |
| VERSION_MINOR | 客户端 Marlin BFT 协议次版本号,例如1。 |
| VERSION_PATCH | 客户端 Marlin BFT 协议补丁版本号,例如0。 |
示例响应:
ss0,96,0.1.0源码中,SYNC 数据包被特殊处理:它在头部校验通过后立即响应,且不经过 Sync Number 校验(Marlin/src/feature/binary_stream.h):
// The SYNC control packet is a special case in that it doesn't require the stream sync to be correct if (static_cast<Protocol>(packet.header.protocol()) == Protocol::CONTROL && static_cast<ProtocolControl>(packet.header.type()) == ProtocolControl::SYNC) { SERIAL_ECHOLN(F("ss"), sync, C(','), buffer_size, C(','), version_major, C('.'), version_minor, C('.'), version_patch); stream_state = StreamState::PACKET_RESET; break; }示例响应中的96即为buffer_size(对应 Marlin/src/feature/binary_stream.h 附近的buffer_size常量),实际可用的接收缓冲为serial_line_buffer,其大小由MAX_CMD_SIZE决定(见 Marlin/src/gcode/queue.cpp 注释)。
CLOSE 数据包
CLOSE 数据包应为主机发送的最后一个数据包。成功后,客户端切回 ASCII 模式。源码中的实现非常直接——将card.flag.binary_mode置为 false(Marlin/src/feature/binary_stream.h):
case ProtocolControl::CLOSE: // revert back to ASCII mode card.flag.binary_mode = false; break;传输控制(Transfer Control,Protocol ID 1)
Protocol ID 1 的数据包控制连接上的文件传输:
| Packet Type | 名称 | 说明 |
|---|---|---|
| 0 | QUERY | 查询客户端协议细节与压缩参数。 |
| 1 | OPEN | 打开文件用于写入并开始接收待传输数据。 |
| 2 | CLOSE | 结束写入并关闭当前文件。 |
| 3 | WRITE | 向已打开文件写入数据。 |
| 4 | ABORT | 中止文件传输。 |
这些类型的解析与分发由SDFileTransferProtocol::process()完成(Marlin/src/feature/binary_stream.h),各分支与上表一一对应。
QUERY 数据包
QUERY 数据包应为主机在 SYNC 数据包之后发送的第二个数据包。成功后,除ok<sync>确认外,还会返回 query 响应。
返回的 query 响应格式:
PFT:version:<VERSION_MAJOR>.<VERSION_MINOR>.<VERSION_PATCH>:compression:<COMPRESSION_ALGO>(,<COMPRESSION_PARAMS>)| 值 | 说明 |
|---|---|
| VERSION_MAJOR | 客户端 Marlin BFT 协议主版本号,例如0。 |
| VERSION_MINOR | 客户端 Marlin BFT 协议次版本号,例如1。 |
| VERSION_PATCH | 客户端 Marlin BFT 协议补丁版本号,例如0。 |
| COMPRESSION_ALGO | 压缩算法。当前为heatshrink或none。 |
| COMPRESSION_PARAMS | 压缩参数,以逗号分隔。当前若算法为 heatshrink,则为窗口大小(window size)与前瞻大小(lookahead size)。 |
示例响应:
PFT:version:0.1.0:compression:heatshrink,8,4其中heatshrink,8,4的8与4对应 Marlin/src/libs/heatshrink/heatshrink_config.h 中的编译期常量:
#define HEATSHRINK_STATIC_WINDOW_BITS 8 #define HEATSHRINK_STATIC_LOOKAHEAD_BITS 4客户端实现(Marlin/src/feature/binary_stream.h)会按是否启用BINARY_STREAM_COMPRESSION分别输出 heatshrink 参数或none:
case FileTransfer::QUERY: SERIAL_ECHO(F("PFT:version:"), version_major, C('.'), version_minor, C('.'), version_patch); #if ENABLED(BINARY_STREAM_COMPRESSION) SERIAL_ECHOLN(F(":compression:heatshrink,"), HEATSHRINK_STATIC_WINDOW_BITS, C(','), HEATSHRINK_STATIC_LOOKAHEAD_BITS); #else SERIAL_ECHOLNPGM(":compression:none"); #endif break;BINARY_STREAM_COMPRESSION在 Marlin/src/feature/binary_stream.h 中定义,并随BINARY_FILE_TRANSFER一同启用 heatshrink 解码器,同时为 DMA 传输准备了 512 字节、按sizeof(size_t)对齐的解码缓冲区。
OPEN 数据包
打开一个文件用于写入。文件名与其他选项在数据包载荷中指定。若固件编译时支持长文件名,则文件名可以是长文件名;但整个数据包载荷长度不得超过 SYNC 数据包返回的缓冲长度。文件名值必须包含空终止符(NULL 字节)。
载荷布局:
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +---------------+---------------+-------------------------------+ | Dummy | Compression | Filename | +---------------------------------------------------------------| | ... | +-------------------------------+-------------------------------+ | ... | NULL (0x0) | Packet Checksum | +-------------------------------+-------------------------------+| 字段 | 宽度 | 说明 |
|---|---|---|
| Dummy | 8 bits | 布尔值,指示本次文件传输是否真正执行。若为1,客户端将假装文件已打开并接受数据传输,但实际上不写入任何数据。 |
| Compression | 8 bits | 布尔值,指示待传输数据是否使用 QUERY 数据包返回的算法与参数进行压缩。 |
| Filename | ... | 包含空终止符字节的文件名。 |
| Packet Checksum | 16 bits | 头部与载荷的 16 位 Fletchers 校验和,包含 Header Checksum,但不含 Start Token。 |
响应:
| 响应 | 说明 |
|---|---|
PFT:success | 文件已打开,可开始写入。 |
PFT:fail | 客户端无法打开文件。 |
PFT:busy | 文件已经处于打开状态。 |
源码中 OPEN 的解析对应Packet::Open结构(Marlin/src/feature/binary_stream.h):Dummy与Compression各占 1 字节(各取低 1 位判断布尔值),其后为以\0结尾的可变长文件名;validate()校验载荷长度并确保末尾是\0。业务逻辑上,若已有传输在进行则回PFT:busy;校验或打开失败则回PFT:fail(Marlin/src/feature/binary_stream.h)。真正打开文件时调用card.openFileWrite(filename)(Marlin/src/feature/binary_stream.h)。
CLOSE 数据包
关闭当前打开的文件。
响应:
| 响应 | 说明 |
|---|---|
PFT:success | 缓冲已刷新且文件已关闭。 |
PFT:ioerror | 客户端存储设备故障。 |
PFT:invalid | 没有已打开的文件。 |
实现上,file_close()会先刷新压缩解码缓冲中残留的数据(若启用压缩),再调用card.closefile()与card.release()(Marlin/src/feature/binary_stream.h)。
WRITE 数据包
向当前打开的文件写入载荷数据。若文件以 Compression 为 1 打开,则数据会先解压再写入。载荷长度不得超过 SYNC 数据包返回的缓冲大小。
响应:成功后返回ok<SYNC>响应。出错时,ok<SYNC>响应后还会跟一条错误响应:
| 响应 | 说明 |
|---|---|
PFT:ioerror | 客户端存储设备故障。 |
PFT:invalid | 没有已打开的文件。 |
从源码看,WRITE 分支在没有打开文件时立即回PFT:invalid;写入失败(如card.write()返回负值)时回PFT:ioerror(Marlin/src/feature/binary_stream.h)。启用压缩时,file_write()将数据送入 heatshrink 解码器,并在解码缓冲攒满 512 字节后一次性写入 SD 卡,兼顾吞吐与 DMA 对齐要求(Marlin/src/feature/binary_stream.h)。
ABORT 数据包
关闭当前打开的文件并将其删除。
响应:
| 响应 | 说明 |
|---|---|
PFT:success | 传输已中止,文件已删除。 |
实现上,transfer_abort()依次调用card.closefile()、card.removeFile(card.filename)、card.release(),并重置传输状态(Marlin/src/feature/binary_stream.h)。此外,固件还实现了传输看门狗:若传输过程中途中断且超过超时时间(默认 10 秒,见timeout = 10000),SDFileTransferProtocol::idle()会自动执行中止逻辑,避免 SD 卡上残留处于打开状态的文件(Marlin/src/feature/binary_stream.h)。
典型使用流程
完整的二进制文件传输流程共 8 步:
- 发送 ASCII 命令
M28 B1进入 Binary Transfer 模式。 - 发送 Connection SYNC 数据包,记录 Sync Number 与 Buffer Size。
- 发送 Transfer QUERY 数据包,使用上一步得到的 Sync Number;记录压缩算法与参数。
- 发送 Transfer OPEN 数据包,使用「最后 Sync Number + 1」,携带文件名与压缩选项。若出错,发送 Connection CLOSE 数据包并中止。
- 发送 Transfer WRITE 数据包,使用「最后 Sync Number + 1」携带文件数据。载荷长度不得超过第 2 步报告的 Buffer Size。出错时,先发送 Transfer ABORT 数据包,再发送 Connection CLOSE 数据包,然后中止传输。
- 发送 Transfer CLOSE 数据包,使用「最后 Sync Number + 1」。
- 发送 Connection CLOSE 数据包,使用「最后 Sync Number + 1」。
- 客户端此时已回到 ASCII 模式,传输完成。
一个基于示例响应的简化报文序列示意(SYNC=0 起步,每包递增):
| 步骤 | 数据包 | 协议/类型 | Sync | 载荷要点 | 期望响应 |
|---|---|---|---|---|---|
| 1 | 无(ASCII) | — | — | M28 B1 | Switching to Binary Protocol |
| 2 | SYNC | 0/1 | 0 | 无 | ss0,96,0.1.0 |
| 3 | QUERY | 1/0 | 1 | 无 | ok1+PFT:version:0.1.0:compression:heatshrink,8,4 |
| 4 | OPEN | 1/1 | 2 | Dummy=0, Compression=1, 文件名+\0 | ok2+PFT:success |
| 5 | WRITE × N | 1/3 | 3,4,5,… | 压缩后文件数据(≤ Buffer Size) | ok3、ok4、… |
| 6 | CLOSE | 1/2 | 6 | 无 | ok6+PFT:success |
| 7 | CLOSE | 0/2 | 7 | 无 | ok7,随后切回 ASCII |
与主机工具对接的注意事项
- 缓冲上限:每个 WRITE 包的载荷长度上限由 SYNC 响应中的
BUFFER_SIZE决定(示例中为 96 字节,实际以固件serial_line_buffer/MAX_CMD_SIZE为准)。发送端应按此值切分文件数据,超出会导致接收端报Datastream packet data buffer overrun(Marlin/src/feature/binary_stream.h)。 - 乱序与重传:任何
rs<SYNC>响应都意味着主机需要从SYNC指示的位置重新发送;头部/载荷校验失败、数据流超时(packet_max_wait = 500ms,见 Marlin/src/feature/binary_stream.h)都会触发重传。 - 压缩可选:QUERY 响应的
heatshrink,8,4表明压缩窗口为 8 位、前瞻为 4 位。若主机端实现压缩,须使用与固件一致的参数,且在 OPEN 时置 Compression=1;否则以未压缩明文传输,置 Compression=0。 - Dummy 传输:OPEN 中 Dummy=1 可用于传输演练/压力测试——客户端照常应答并接收数据,但不会写 SD 卡(Marlin/src/feature/binary_stream.h)。
- 能力查询:主机可通过
M115响应中的BINARY_FILE_TRANSFER能力标识(Marlin/src/gcode/host/M115.cpp)判断固件是否启用本协议,再决定是否发起M28 B1。
【免费下载链接】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),仅供参考