跳转至

协议

Klipper 消息协议用于 Klipper 主机软件和 Klipper 微控制器软件之间的低层通信。在上层看,该协议可以被认为是一系列的命令和响应字符串,它们被压缩、传输,然后在接收方被处理。以下是一组未经压缩的可读的命令示例:

set_digital_out pin=PA3 value=1
set_digital_out pin=PA7 value=1
schedule_digital_out oid=8 clock=4000000 value=0
queue_step oid=7 interval=7458 count=10 add=331
queue_step oid=7 interval=11717 count=4 add=1281

有关可用命令的信息,请参阅 mcu 命令文档。有关如何将 G-Code 文件转换为其相应的可读的微控制器命令信息,请参阅调试文档。

本页提供了Klipper消息传递协议本身的高层描述。它描述了消息是如何被声明、以二进制格式编码("压缩 "方案)和传输的。

该协议的目标是在主机和微控制器之间建立一个无错误的通信通道,对微控制器来说是低延迟、低带宽和低复杂度的。

微控制器接口

Klipper传输协议可以被认为是微控制器和主机之间的一个RPC机制。微控制器软件声明了主机可以调用的命令,以及它可以产生的响应信息。主机使用这些信息来命令微控制器执行动作并解释结果。

宣布命令

微控制器软件通过使用C代码中的DECL_COMMAND()宏来声明一个 “命令”。例如:

DECL_COMMAND(command_update_digital_out, "update_digital_out oid=%c value=%c");

以上声明了一个名为 "update_digital_out "的命令。这允许主机 “invoke”这个命令,这将使得command_update_digital_out()C函数在微控制器中被执行。上述内容还表明,该命令需要两个整数参数。当command_update_digital_out()C代码被执行时,它将被传递一个包含这两个整数的数组--第一个对应于 "oid",第二个对应于 "value"。

一般来说,参数是用printf()风格的语法描述的(例如,"%u")。这种格式化直接对应于人类可读的命令(例如,"update_digital_out oid=7 value=1")。在上面的例子中,"value="是一个参数名称,"%c "表示该参数是一个整数。在内部,参数名只作为记录使用。在这个例子中,"%c "也是作为记录使用的,表示预期的整数是1字节宽度(声明的整数宽度不影响编解码)。

微控制器固件构建的时候包含所有用DECL_COMMAND()声明的命令,确定其参数,并使得它们可以被调用。

声明响应

当从微控制器向主机发送信息时会产生一个 "响应"。这些都是使用sendf()C语言宏来声明和发送的。例如:

sendf("status clock=%u status=%c", sched_read_time(), sched_is_shutdown());

以上传输了一个 "状态 "响应消息,其中包含两个整数参数("时钟 "和 "状态")。微控制器的构建会自动找到所有sendf()的调用,并为其生成编码器。sendf()函数的第一个参数描述了响应,它的格式与命令声明相同。

主机可以为每个响应注册一个回调函数。因此,实际上,命令允许主机在微控制器上调用C函数,响应允许微控制器软件在主机上调用代码。

sendf()宏只能从命令或任务处理程序中调用,而不能从中断或定时器中调用。代码不需要在收到命令的响应中发出sendf(),代码也不限制sendf()的调用次数,任务处理程序在在任何时候都可以调用sendf()。

输出响应

为了简化调试,也有一个output()C函数。例如:

output("The value of %u is %s with size %u.", x, buf, buf_len);

output()函数的用法与printf()相似–它的目的是生成和格式化任意的信息供人阅读。

声明枚举

枚举允许主机代码对微控制器作为整数处理的参数使用字符串标识。它们在微控制器代码中被声明-例如:

DECL_ENUMERATION("spi_bus", "spi", 0);

DECL_ENUMERATION_RANGE("pin", "PC0", 16, 8);

在第一个例子中,DECL_ENUMERATION()宏定义了一个枚举量可以用于任意的命令/响应消息,这个枚举量的参数名称为 "spi_bus "或参数名称后缀为"_spi_bus "。对于此参数,字符串 "spi "是一个有效的值,它将以一个零的整数值被传递。

也可以声明一个枚举范围。在第二个例子中,"pin "参数(或任何后缀为"_pin "的参数)将接受PC0、PC1、PC2、...、PC7作为有效值。字符串将被转化成整数16、17、18...23进行传输。

声明常量

常量也可以被导出。例子如下:

DECL_CONSTANT("SERIAL_BAUD", 250000);

可以从微控制器向主机输出一个名为 "SERIAL_BAUD ",值为250000的常数。也可以将字符串声明成常量-例如:

DECL_CONSTANT_STR("MCU", "pru");

底层消息编码

为了实现上述RPC机制,每个命令和响应都被编码成二进制格式进行传输。本节介绍这个传输系统。

消息块

所有从主机到微控制器以及从微控制器到主机的数据都包含在 "消息块 "中。一个消息块有一个两字节的头和一个三字节的尾。信息块的格式如下:

<1 byte length><1 byte sequence><n-byte content><2 byte crc><1 byte sync>

长度字节指示消息块中的字节数,包括头和尾的字节(因此消息的最短长度为5字节)。目前最大的消息块长度为64字节。序号字节的低4位是序列号,高4位总是0x10(高4位保留给未来用)。内容字节包含任意数据,其格式在下一节中描述。crc字节包含消息块的16位CCITTCRC,CRC计算包括头字节但不包括尾字节。同步字节为0x7e。

消息块的格式是受HDLC消息帧的启发。与HDLC一样,消息块在开始时可以选择包含一个额外的同步字符。与HDLC不同的是,同步字符并不专属于框架,也可以出现在消息块内容中(不需要转义)。

消息块内容

每个从主机发送至微控制器的消息块,其内容都包含一系列零个或多个消息命令。每条命令以可变长度数值(VLQ)编码的整数command-id开始,后面是这个命令的零或多个VLQ参数。

例如,以下四个命令可以被放在一个消息块中:

update_digital_out oid=6 value=1
update_digital_out oid=5 value=0
get_config
get_clock

并编码为下面八个VLQ整数:

<id_update_digital_out><6><1><id_update_digital_out><5><0><id_get_config><id_get_clock>

为了对信息内容进行编解码,主机和微控制器都必须确保使用的命令的ID和每个命令对应的参数数量一致。因此,在上面的例子中,主机和微控制器都知道 "id_update_digital_out "后面总是跟着两个参数,而 "id_get_config "和 "id_get_clock "没有参数。主机和微控制器共享一个 "数据字典",这个数据字典将命令描述(例如,"update_digital_out oid=%c value=%c")映射到它们的整数命令ID。当处理数据时,解析器将知道一个给定的命令ID后预期有特定数量的VLQ编码参数。

从微控制器发送到主机的块的消息内容遵循相同的格式。这些消息中的标识符是 "响应ID",但它们的作用相同,并遵循相同的编码规则。事实上,从微控制器发送到主机的消息块在消息块内容中只包含一个响应。

可变长度数值

关于VLQ编码的整数的一般格式的更多信息,请参见wikipedia article。Klipper使用支持正负整数的编码方案。接近零的整数使用较少的字节来编码,正整数的编码通常比负整数的使用字节更少。下表显示了每个整数的编码所需的字节数:

整数 编码长度
-32 .. 95 1
-4096 .. 12287 2
-524288 .. 1572863 3
-67108864 .. 201326591 4
-2147483648 .. 4294967295 5

可变长度字符串

作为上述编码规则的例外,如果一个命令或响应的参数是一个动态字符串,那么该参数不会被编码为一个简单的VLQ整数。相反,它的编码方式是将长度作为一个VLQ编码的整数并跟着内容本身来传输:

<VLQ encoded length><n-byte contents>

在数据字典中的命令描述使主机和微控制器都知道哪些命令参数使用简单的VLQ编码,哪些参数使用字符串编码。

数据字典

为了在微控制器和主机之间建立有意义的通信,双方必须商定一个 "数据字典"。这个数据字典包含了命令和响应的整数标识符及它们的描述。

微控制器构建使用DECL_COMMAND()和sendf()宏的内容来生成数据字典。构建会自动为每个命令和响应分配唯一的标识符。这个系统允许主机和微控制器代码既使用人类可读的描述性名称又占用最小的传输带宽。

当主机第一次连接到微控制器时,会查询数据字典。一旦主机从微控制器中下载了数据字典,它就使用该数据字典对所有命令进行编码,并解析来自微控制器的所有响应。因此,主机必须能处理动态的数据字典。然而,为了保持微控制器软件的简单性,微控制器总是使用静态(编译的)数据字典。

数据字典是通过向微控制器发送 "identify"命令来查询的。微控制器将用一个 "identify_response "消息来回应每条识别命令。由于在获取数据字典之前需要这两条命令,它们的整数id和参数类型在微控制器和主机中都是硬编码的。"identify_response "的响应id是0,"identify"的命令id是1。除了有硬编码的id外,识别命令及其响应的声明和传输方式与其他命令和响应相同。除此之外没有其他命令或响应是硬编码的。

传输的数据字典本身的格式是一个zlib压缩的JSON字符串。微控制器的构建过程会生成该字符串,对其进行压缩,并将其存储在微控制器闪存的text部分。数据字典可以比最大的消息块大得多--主机通过发送多个识别命令来分块下载数据字典。一旦获得所有的数据块,主机将把这些数据块组合起来,解压缩数据并解析内容。

除了通信协议的信息外,数据字典还包含软件版本、枚举值(由DECL_ENUMERATION定义)和常量(由DECL_CONSTANT定义)。

消息流

从主机到微控制器发送的信息命令应该是无差错的。微控制器将检查每个信息块中的CRC和顺序号,以确保命令的准确性和顺序性。微控制器总是按顺序处理信息块--如果它收到一个不按顺序的信息块,它将丢弃它和其他不按顺序的信息块,直到它收到具有正确顺序号的信息块。

低层的主机代码为发送到微控制器的丢失和损坏的消息块实现了一个自动重传系统。为此,微控制器在每次成功接收的消息块之后都会发送一个 "ack message block"。主机在发送每个块后会启动超时等待,如果超时后没有收到相应的 "ack",它将重新发送。此外,如果微控制器检测到一个损坏或顺序错误的块,它可以发送一个 "nak message block"实现快速重传。

“ack "是一个内容为空的消息块(即一个5字节的消息块),其顺序号大于最后收到的主机消息顺序号。一个 "nak "是一个内容为空的消息块,其顺序号小于最后收到的主机消息顺序号。

协议实现了 "窗口 "传输系统,因此主机可以在同一时间允许多未完成的消息块在发送。(这是对一个消息块中可以包含多个命令的补充)。通过这种方式,即使在传输延迟的情况下也能最大化地利用带宽。超时、重传、窗口化和响应机制是受到TCP中类似机制的启发。

另一方向,从微控制器发送到主机的信息块被设计成无差错,但并不保证传输。(响应不会出错,但可能会丢失。)这样做是为了保持微控制器的实现简单。响应没有自动重传系统--高层代码应该能够处理偶尔丢失的响应(通常通过重新请求内容或设置响应传输的循环调度)。发送给主机的信息块中的顺序号字段总是比从主机收到的信息块的最后接收顺序号大1。顺序号并不用于跟踪响应消息块的顺序。

回到页面顶部