跳轉至

協議

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。順序號並不用於跟蹤響應訊息塊的順序。

Back to top