Vai al contenuto

Panoramica del codice

Questo documento descrive il formato generale del codice e il flusso principale del codice di Klipper.

Directory Layout

La directory src/ contiene il sorgente C per il codice del microcontrollore. src/atsam/, src/atsamd/, src/avr/, src/linux/, src/lpc176x/, src/ Le directory pru/ e src/stm32/ contengono il codice del microcontrollore specifico dell'architettura. src/simulator/ contiene stub di codice che consentono la compilazione di test del microcontrollore su altre architetture. La directory src/generic/ contiene codice di supporto che può essere utile in diverse architetture. La build fa in modo che le include di "board/somefile.h" guardino prima nella directory dell'architettura corrente (ad esempio, src/avr/somefile.h) e poi nella directory generica (ad esempio, src/generic/somefile.h).

La directory klippy/ contiene il software host. La maggior parte del software host è scritto in Python, tuttavia la directory klippy/chelper/ contiene alcuni helper del codice C. La directory klippy/kinematics/ contiene il codice della cinematica del robot. La directory klippy/extras/ contiene i "moduli" estensibili del codice host.

La directory lib/ contiene il codice della libreria di terze parti esterne necessarie per creare alcune destinazioni.

La directory config/ contiene file di configurazione della stampante di esempio.

La directory scripts/ contiene script di build-time utili per compilare il codice del microcontrollore.

La directory test/ contiene test case automatizzati.

Durante la compilazione, la build potrebbe creare una directory out/. Questa contiene oggetti temporanei di compilazione. L'oggetto microcontrollore finale che viene creato è out/klipper.elf.hex su AVR e out/klipper.bin su ARM.

Flusso del codice del microcontrollore

L'esecuzione del codice del microcontrollore inizia nel codice specifico dell'architettura (ad esempio, src/avr/main.c) che alla fine chiama sched_main() che si trova in src/sched.c. Il codice sched_main() inizia eseguendo tutte le funzioni che sono state contrassegnate con la macro DECL_INIT(). Quindi continua a eseguire ripetutamente tutte le funzioni contrassegnate con la macro DECL_TASK().

Una delle principali funzioni dell'attività è command_dispatch() che si trova in src/command.c. Questa funzione viene richiamata dal codice input/output specifico della scheda (es. src/avr/serial.c, src/generic/serial_irq.c) ed esegue le funzioni di comando associate ai comandi trovati nel flusso di input. Le funzioni di comando vengono dichiarate utilizzando la macro DECL_COMMAND() (consultare il documento protocol per ulteriori informazioni).

Le funzioni task, init e comando vengono sempre eseguite con gli interrupt abilitati (tuttavia, possono disabilitare temporaneamente gli interrupt se necessario). Queste funzioni dovrebbero evitare lunghe pause, ritardi o eseguire lavori che durano un tempo significativo. (Lunghi ritardi in queste funzioni di "attività" provocano un jitter di pianificazione per altre "attività" - ritardi superiori a 100us possono diventare evidenti, ritardi superiori a 500us possono causare ritrasmissioni dei comandi, ritardi superiori a 100 ms possono causare riavvii del watchdog.) Queste funzioni pianificano il lavoro a orari specifici programmando i timer.

Le funzioni timer vengono pianificate chiamando sched_add_timer() (che si trova in src/sched.c). Il codice di pianificazione farà in modo che la funzione data venga chiamata all'ora richiesta. Gli interrupt timer sono inizialmente gestiti in un gestore di interrupt specifico dell'architettura (ad esempio, src/avr/timer.c) che chiama sched_timer_dispatch() situato in src/sched.c. L'interruzione del timer porta all'esecuzione delle funzioni del timer di pianificazione. Le funzioni timer vengono eseguite sempre con gli interrupt disabilitati. Le funzioni del timer dovrebbero sempre completarsi entro pochi microsecondi. Al completamento dell'evento timer, la funzione può scegliere di riprogrammare se stessa.

Nel caso in cui venga rilevato un errore, il codice può invocare shutdown() (una macro che chiama sched_shutdown() situata in src/sched.c). Il richiamo di shutdown() provoca l'esecuzione di tutte le funzioni contrassegnate con la macro DECL_SHUTDOWN(). Le funzioni di spegnimento vengono eseguite sempre con gli interrupt disabilitati.

Gran parte delle funzionalità del microcontrollore implica il lavoro con pin di input/output per uso generico (GPIO). Per astrarre il codice specifico dell'architettura di basso livello dal codice dell'attività di alto livello, tutti gli eventi GPIO sono implementati in wrapper specifici dell'architettura (ad esempio, src/avr/gpio.c). Il codice è compilato con l'ottimizzazione "-flto -fwhole-program" di gcc che fa un ottimo lavoro di inlining delle funzioni tra le unità di compilazione, quindi la maggior parte di queste minuscole funzioni gpio sono integrate nei loro chiamanti e non ci sono costi di runtime per l'utilizzo loro.

Panoramica del codice Klippy

Il codice host (Klippy) deve essere eseguito su un computer a basso costo (come un Raspberry Pi) abbinato al microcontrollore. Il codice è scritto principalmente in Python, tuttavia utilizza CFFI per implementare alcune funzionalità nel codice C.

L'esecuzione iniziale comincia in klippy/klippy.py. Questo legge gli argomenti della riga di comando, apre il file di configurazione della stampante, crea un'istanza degli oggetti principali della stampante e avvia la connessione seriale. L'esecuzione principale dei comandi G-code è nel metodo process_commands() in klippy/gcode.py. Questo codice traduce i comandi del codice G in chiamate a oggetti stampante, che spesso traducono le azioni in comandi da eseguire sul microcontrollore (come dichiarato tramite la macro DECL_COMMAND nel codice del microcontrollore).

Ci sono quattro thread nel codice host di Klippy. Il thread principale gestisce i comandi gcode in arrivo. Un secondo thread (che risiede interamente nel codice C klippy/chelper/serialqueue.c) gestisce l'IO di basso livello con la porta seriale. Il terzo thread viene utilizzato per elaborare i messaggi di risposta dal microcontrollore nel codice Python (vedi klippy/serialhdl.py). Il quarto thread scrive i messaggi di debug nel log (vedi klippy/queuelogger.py) in modo che gli altri thread non blocchino mai le scritture del log.

Flusso di codice di un comando di spostamento

Un tipico movimento della stampante inizia quando viene inviato un comando "G1" all'host Klippy e si completa quando vengono prodotti i corrispondenti impulsi di passo sul microcontrollore. Questa sezione delinea il flusso di codice di un tipico comando di spostamento. Il documento cinematica fornisce ulteriori informazioni sulla meccanica dei movimenti.

  • L'elaborazione di un comando di spostamento inizia in gcode.py. L'obiettivo di gcode.py è tradurre il G-code in chiamate interne. Un comando G1 invocherà cmd_G1() in klippy/extras/gcode_move.py. Il codice gcode_move.py gestisce le modifiche all'origine (ad esempio, G92), le modifiche alle posizioni relative rispetto a quelle assolute (ad esempio, G90) e le modifiche alle unità (ad esempio, F6000=100mm/s). Il percorso del codice per una mossa è: _process_data() -> _process_commands() -> cmd_G1(). Infine viene invocata la classe ToolHead per eseguire la richiesta effettiva: cmd_G1() -> ToolHead.move()
  • La classe ToolHead (in toolhead.py) gestisce "look-ahead" e tiene traccia dei tempi delle azioni di stampa. Il percorso di codice principale per una mossa è: ToolHead.move() -> MoveQueue.add_move() -> MoveQueue.flush() -> Move.set_junction() -> ToolHead._process_moves().
    • ToolHead.move() crea un oggetto Move() con i parametri del movimento (in spazio cartesiano e in unità di secondi e millimetri).
    • Alla classe cinematica viene data l'opportunità di controllare ogni movimento (ToolHead.move() -> kin.check_move()). Le classi cinematiche si trovano nella directory klippy/cinematica/. Il codice check_move() può generare un errore se il movimento non è valida. Se check_move() viene completato correttamente, la cinematica sottostante deve essere in grado di gestire lo spostamento.
    • MoveQueue.add_move() posiziona l'oggetto di spostamento nella coda "look-ahead".
    • MoveQueue.flush() determina le velocità di inizio e fine di ogni movimento.
    • Move.set_junction() implementa il "generatore di trapezi" per il movimento. Il "generatore trapezoidale" suddivide ogni movimento in tre parti: una fase di accelerazione costante, seguita da una fase di velocità costante, seguita da una fase di decelerazione costante. Ogni mossa contiene queste tre fasi in questo ordine, ma alcune fasi possono avere durata zero.
    • Quando viene chiamato ToolHead._process_moves(), tutto ciò che riguarda lo spostamento è noto: la sua posizione iniziale, la sua posizione finale, la sua accelerazione, la sua velocità di inizio/crociera/finale e la distanza percorsa durante l'accelerazione/crociera/decelerazione. Tutte le informazioni sono memorizzate nella classe Move() e sono nello spazio cartesiano in unità di millimetri e secondi.
  • Klipper utilizza un risolutore iterativo per generare i tempi di passaggio per ogni stepper. Per motivi di efficienza, i tempi di impulso stepper sono generati in codice C. I movimenti vengono prima posizionati su una "coda di movimento trapezoidale": ToolHead._process_moves() -> trapq_append() (in klippy/chelper/trapq.c). Vengono quindi generati i tempi di passaggio: ToolHead._process_moves() -> ToolHead._update_move_time() -> MCU_Stepper.generate_steps() -> itersolve_generate_steps() -> itersolve_gen_steps_range() (in klippy/chelper/itersolve.c). L'obiettivo del risolutore iterativo è trovare i tempi di passaggio data una funzione che calcola una posizione passo-passo da un tempo. Questo viene fatto "indovinando" ripetutamente varie volte fino a quando la formula della posizione dello stepper non restituisce la posizione desiderata del passaggio successivo sullo stepper. Il feedback prodotto da ciascuna ipotesi viene utilizzato per migliorare le ipotesi future in modo che il processo converga rapidamente al tempo desiderato. Le formule della posizione dello stepper cinematico si trovano nella directory klippy/chelper/ (ad es. kin_cart.c, kin_corexy.c, kin_delta.c, kin_extruder.c).
  • Si noti che l'estrusore è gestito nella propria classe cinematica: ToolHead._process_moves() -> PrinterExtruder.move(). Poiché la classe Move() specifica l'esatto tempo di movimento e poiché gli impulsi di passo vengono inviati al microcontrollore con una tempistica specifica, i movimenti passo-passo prodotti dalla classe estrusore saranno sincronizzati con il movimento della testa anche se il codice viene mantenuto separato.
  • Dopo che il risolutore iterativo ha calcolato i tempi di passaggio, questi vengono aggiunti a un array: itersolve_gen_steps_range() -> stepcompress_append() (in klippy/chelper/stepcompress.c). L'array (struct stepcompress.queue) memorizza i corrispondenti tempi del contatore dell'orologio del microcontrollore per ogni passaggio. Qui il valore del "contatore orologio del microcontrollore" corrisponde direttamente al contatore hardware del microcontrollore - è relativo a quando il microcontrollore è stato acceso l'ultima volta.
  • Il prossimo passo importante è comprimere i passaggi: stepcompress_flush() -> compress_bisect_add() (in klippy/chelper/stepcompress.c). Questo codice genera e codifica una serie di comandi "queue_step" del microcontrollore che corrispondono all'elenco dei tempi di stepper compilati nella fase precedente. Questi comandi "queue_step" vengono quindi accodati, assegnati a priorità e inviati al microcontrollore (tramite stepcompress.c:steppersync e serialqueue.c:serialqueue).
  • L'elaborazione dei comandi queue_step sul microcontrollore inizia in src/command.c che analizza il comando e chiama command_queue_step(). Il codice command_queue_step() (in src/stepper.c) aggiunge semplicemente i parametri di ogni comando queue_step a una coda per stepper. In condizioni normali, il comando queue_step viene analizzato e messo in coda almeno 100 ms prima dell'ora del suo primo passaggio. Infine, la generazione degli eventi stepper viene eseguita in stepper_event(). Viene chiamato dall'interruzione del timer hardware all'ora pianificata del primo passaggio. Il codice stepper_event() genera un impulso di passaggio e quindi si riprogramma per essere eseguito al momento dell'impulso di passaggio successivo per i parametri queue_step specificati. I parametri per ogni comando queue_step sono "interval", "count" e "add". Ad alto livello, stepper_event() esegue quanto segue, 'count' volte: do_step(); next_wake_time = last_wake_time + intervallo; intervallo += aggiungi;

Quanto sopra può sembrare un sacco di complessità per eseguire un movimento. Tuttavia, le uniche parti veramente interessanti sono nelle classi ToolHead e cinematica. È questa parte del codice che specifica i movimenti e le loro tempistiche. Le restanti parti dell'elaborazione sono per lo più solo comunicazioni e collegamenti.

Aggiunta di un modulo host

Il codice host Klippy ha una capacità di caricamento dinamico dei moduli. Se nel file di configurazione della stampante viene trovata una sezione di configurazione denominata "[my_module]", il software tenterà automaticamente di caricare il modulo python klippy/extras/my_module.py . Questo sistema di moduli è il metodo preferito per aggiungere nuove funzionalità a Klipper.

Il modo più semplice per aggiungere un nuovo modulo è utilizzare un modulo esistente come riferimento - vedere klippy/extras/servo.py come esempio.

Possono essere utili anche:

  • L'esecuzione del modulo inizia nella funzione load_config() a livello di modulo (per le sezioni di configurazione del modulo [my_module]) o in load_config_prefix() (per le sezioni di configurazione del modulo [my_module my_name]). A questa funzione viene passato un oggetto "config" e deve restituire un nuovo "oggetto stampante" associato alla sezione di configurazione specificata.
  • Durante il processo di creazione di un'istanza di un nuovo oggetto stampante, l'oggetto di configurazione può essere utilizzato per leggere i parametri dalla sezione di configurazione specificata. Questo viene fatto usando i metodi config.get(), config.getfloat(), config.getint(), ecc. Assicurati di leggere tutti i valori dalla configurazione durante la costruzione dell'oggetto stampante: se l'utente specifica un parametro di configurazione che non viene letto durante questa fase, si presumerà che si tratti di un errore di battitura nella configurazione e verrà generato un errore.
  • Usa il metodo config.get_printer() per ottenere un riferimento alla classe principale "printer". Questa classe "stampante" memorizza i riferimenti a tutti gli "oggetti stampante" di cui è stata creata un'istanza. Usa il metodo printer.lookup_object() per trovare riferimenti ad altri oggetti stampante. Quasi tutte le funzionalità (anche i moduli cinematici principali) sono incapsulate in uno di questi oggetti stampante. Si noti, tuttavia, che quando viene istanziato un nuovo modulo, non saranno stati istanziati tutti gli altri oggetti stampante. I moduli "gcode" e "pins" saranno sempre disponibili, ma per altri moduli è una buona idea rinviare la ricerca.
  • Registra i gestori di eventi usando il metodo printer.register_event_handler() se il codice deve essere chiamato durante gli "events" generati da altri oggetti stampante. Ogni nome di evento è una stringa e, per convenzione, è il nome del modulo sorgente principale che genera l'evento insieme a un nome breve per l'azione che si sta verificando (ad esempio, "klippy:connect"). I parametri passati a ciascun gestore di eventi sono specifici dell'evento dato (così come la gestione delle eccezioni e il contesto di esecuzione). Due eventi di avvio comuni sono:
    • klippy:connect - Questo evento viene generato dopo che tutti gli oggetti stampante sono stati istanziati. Viene comunemente utilizzato per cercare altri oggetti stampante, verificare le impostazioni di configurazione ed eseguire un "handshake" iniziale con l'hardware della stampante.
    • klippy:ready - Questo evento viene generato dopo che tutti i gestori di connessione sono stati completati correttamente. Indica che la stampante sta passando a uno stato pronto per gestire le normali operazioni. Non generare un errore in questo callback.
  • Se c'è un errore nella configurazione dell'utente, assicurati di sollevarlo durante le fasi load_config() o "connect event". Utilizzare raise config.error("my error") o raise printer.config_error ("my error") per segnalare l'errore.
  • Utilizzare il modulo "pin" per configurare un pin su un microcontrollore. Questo è in genere fatto con qualcosa di simile a printer.lookup_object("pins").setup_pin("pwm", config.get("my_pin")). L'oggetto restituito può quindi essere comandato in fase di esecuzione.
  • Se l'oggetto stampante definisce un metodo get_status(), il modulo può esportare informazioni sullo stato tramite macro e tramite Server API. Il metodo get_status() deve restituire un dizionario Python con chiavi che sono stringhe e valori che sono interi, float, stringhe, elenchi, dizionari, True, False o None. È possibile utilizzare anche tuple (e tuple con nome) (appaiono come elenchi quando si accede tramite il server API). Gli elenchi e i dizionari esportati devono essere trattati come "immutabili" - se il loro contenuto cambia, è necessario restituire un nuovo oggetto da get_status(), altrimenti il server API non rileverà tali modifiche.
  • Se il modulo necessita dell'accesso alla temporizzazione del sistema o a descrittori di file esterni, utilizzare printer.get_reactor() per ottenere l'accesso alla classe globale "event reactor". Questa classe reattore consente di programmare i timer, attendere l'input sui descrittori di file e di "sopprimere" il codice host.
  • Non utilizzare variabili globali. Tutto lo stato dovrebbe essere memorizzato nell'oggetto stampante restituito dalla funzione load_config(). Questo è importante, altrimenti il comando RESTART potrebbe non funzionare come previsto. Inoltre, per ragioni simili, se vengono aperti file (o socket) esterni, assicurati di registrare un gestore di eventi "klippy:disconnect" e chiuderli da quella richiamata.
  • Evitare di accedere alle variabili dei membri interni (o di chiamare metodi che iniziano con un trattino basso) di altri oggetti stampante. L'osservanza di questa convenzione semplifica la gestione delle modifiche future.
  • Si consiglia di assegnare un valore a tutte le variabili membro nel costruttore Python delle classi Python. (E quindi evita di utilizzare la capacità di Python di creare dinamicamente nuove variabili membro.)
  • Se una variabile Python deve memorizzare un valore in virgola mobile, si consiglia di assegnare e manipolare sempre quella variabile con costanti in virgola mobile (e non utilizzare mai costanti intere). Ad esempio, preferisci self.speed = 1. su self.speed = 1 e preferisci self.speed = 2. * x su self.speed = 2 * x. L'uso coerente di valori in virgola mobile può evitare difficoltà di debug nelle conversioni di tipo Python.
  • Se invii il modulo per l'inclusione nel codice Klipper principale, assicurati di inserire un avviso di copyright nella parte superiore del modulo. Vedere i moduli esistenti per il formato preferito.

Aggiunta di una nuova cinematica

Questa sezione fornisce alcuni suggerimenti sull'aggiunta del supporto a Klipper per ulteriori tipi di cinematica della stampante. Questo tipo di attività richiede un'ottima comprensione delle formule matematiche per la cinematica di destinazione. Richiede anche capacità di sviluppo software, sebbene sia necessario solo aggiornare il software host.

Passi utili:

  1. Inizia studiando la sezione "flusso di codice di un movimento" e il documento di cinematica.
  2. Esaminare le classi cinematiche esistenti nella directory klippy/kinematics/. Le classi cinematiche hanno il compito di convertire una mossa in coordinate cartesiane nel movimento su ogni stepper. Si dovrebbe essere in grado di copiare uno di questi file come punto di partenza.
  3. Implementa in C le funzioni di posizione cinematica dello stepper per ogni stepper se non sono già disponibili (vedi kin_cart.c, kin_corexy.c e kin_delta.c in klippy/chelper/). La funzione dovrebbe chiamare move_get_coord() per convertire un dato tempo di spostamento (in secondi) in una coordinata cartesiana (in millimetri), e quindi calcolare la posizione dello stepper desiderata (in millimetri) da quella coordinata cartesiana.
  4. Implementa il metodo calc_position() nella nuova classe cinematica. Questo metodo calcola la posizione della testa di stampa in coordinate cartesiane dalla posizione di ogni stepper. Non è necessario che sia efficiente poiché in genere viene chiamato solo durante le operazioni di homing e probing.
  5. Altri metodi. Implementa i metodi check_move(), get_status(), get_steppers(), home() e set_position(). Queste funzioni sono in genere utilizzate per fornire verifiche cinematiche specifiche. Tuttavia, all'inizio dello sviluppo è possibile utilizzare il codice boilerplate qui.
  6. Implementare casi di prova. Crea un file g-code con una serie di movimenti che possono testare casi importanti per la cinematica data. Segui la documentazione di debug per convertire questo file di codice G in comandi del microcontrollore. Questo è utile per esercitare corner case e per verificare la presenza di regressioni.

Porting su un nuovo microcontrollore

Questa sezione fornisce alcuni suggerimenti sul porting del codice del microcontrollore di Klipper su una nuova architettura. Questo tipo di attività richiede una buona conoscenza dello sviluppo embedded e un accesso diretto al microcontrollore di destinazione.

Passi utili:

  1. Inizia identificando eventuali librerie di terze parti che verranno utilizzate durante il trasferimento. Esempi comuni includono wrapper "CMSIS" e librerie "HAL" del produttore. Tutto il codice di terze parti deve essere compatibile con GNU GPLv3. Il codice di terze parti dovrebbe essere salvato nella directory lib/ di Klipper. Aggiorna il file lib/README con informazioni su dove e quando è stata ottenuta la libreria. È preferibile copiare il codice nel repository di Klipper senza modifiche, ma se sono necessarie modifiche, tali modifiche dovrebbero essere elencate esplicitamente nel file lib/README.
  2. Crea una nuova sottodirectory di architettura nella directory src/ e aggiungi il supporto iniziale di Kconfig e Makefile. Utilizzare le architetture esistenti come guida. src/simulator fornisce un esempio di base di un punto di partenza minimo.
  3. Il primo compito di programmazione è portare il supporto di comunicazione alla scheda di destinazione. Questo è il passo più difficile in un nuovo porting. Una volta che la comunicazione di base funziona, i passaggi rimanenti tendono a essere molto più semplici. È tipico utilizzare un dispositivo seriale di tipo UART durante lo sviluppo iniziale poiché questi tipi di dispositivi hardware sono generalmente più facili da abilitare e controllare. Durante questa fase, fai un uso generoso del codice di supporto dalla directory src/generic/ (controlla come src/simulator/Makefile include il codice C generico nella build). È inoltre necessario definire timer_read_time() (che restituisce l'orologio di sistema corrente) in questa fase, ma non è necessario supportare completamente la gestione di timer irq.
  4. Acquisisci familiarità con lo strumento console.py (come descritto nel documento di debug) e verifica la connettività al microcontrollore con esso. Questo strumento traduce il protocollo di comunicazione del microcontrollore di basso livello in un formato leggibile dall'uomo.
  5. Aggiungi il supporto per l'invio del timer da interrupt hardware. Vedere Klipper commit 970831ee come esempio dei passaggi 1-5 eseguiti per l'architettura LPC176x.
  6. Visualizza il supporto di input e output GPIO di base. Vedi Klipper commit c78b9076 come esempio di questo.
  7. Riporta periferiche aggiuntive, ad esempio consulta il commit di Klipper 65613aed, c812a40a, e c381d03a.
  8. Crea un file di configurazione di Klipper di esempio nella directory config/. Testare il microcontrollore con il programma principale klippy.py.
  9. Prendi in considerazione l'aggiunta di build test case nella directory test/.

Ulteriori suggerimenti per la programmazione:

  1. Evitare di utilizzare "C bitfields" per accedere ai registri IO; preferire operazioni di lettura e scrittura dirette di numeri interi a 32 bit, 16 bit o 8 bit. Le specifiche del linguaggio C non specificano chiaramente come il compilatore deve implementare campi di bit C (ad esempio, endianness e layout di bit), ed è difficile determinare quali operazioni di I/O si verificheranno su un campo di bit C letto o scritto.
  2. Preferibilmente scrivere valori espliciti nei registri IO invece di usare operazioni di lettura-modifica-scrittura. Cioè, se si aggiorna un campo in un registro IO in cui gli altri campi hanno valori noti, è preferibile scrivere in modo esplicito il contenuto completo del registro. Le scritture esplicite producono codice più piccolo, più veloce e più facile da eseguire il debug.

Sistemi di coordinate

Internamente, Klipper tiene traccia principalmente della posizione della testa di stampa in coordinate cartesiane relative al sistema di coordinate specificato nel file di configurazione. Cioè, la maggior parte del codice Klipper non subirà mai un cambiamento nei sistemi di coordinate. Se l'utente fa una richiesta per cambiare l'origine (ad esempio, un comando G92), allora quell'effetto si ottiene traducendo i comandi futuri nel sistema di coordinate primario.

Tuttavia, in alcuni casi è utile ottenere la posizione della testa di stampa in qualche altro sistema di coordinate e Klipper ha diversi strumenti per facilitarlo. Questo può essere visto eseguendo il comando GET_POSITION. Per esempio:

Send: GET_POSITION
Recv: // mcu: stepper_a:-2060 stepper_b:-1169 stepper_c:-1613
Recv: // stepper: stepper_a:457.254159 stepper_b:466.085669 stepper_c:465.382132
Recv: // kinematic: X:8.339144 Y:-3.131558 Z:233.347121
Recv: // toolhead: X:8.338078 Y:-3.123175 Z:233.347878 E:0.000000
Recv: // gcode: X:8.338078 Y:-3.123175 Z:233.347878 E:0.000000
Recv: // gcode base: X:0.000000 Y:0.000000 Z:0.000000 E:0.000000
Recv: // gcode homing: X:0.000000 Y:0.000000 Z:0.000000

La posizione "mcu" (stepper.get_mcu_position() nel codice) è il numero totale di passaggi che il microcontrollore ha emesso in direzione positiva meno il numero di passaggi emessi in direzione negativa dall'ultimo microcontrollore Ripristina. Se il robot è in movimento quando viene emessa la query, il valore riportato include le mosse memorizzate nel buffer del microcontrollore, ma non include le mosse nella coda di previsione.

La posizione "stepper" (stepper.get_commanded_position()) è la posizione del dato stepper come tracciato dal codice della cinematica. Questo corrisponde generalmente alla posizione (in mm) del carrello lungo il suo binario, rispetto a position_endstop specificato nel file di configurazione. (Alcune cinematiche tracciano le posizioni dello stepper in radianti anziché in millimetri.) Se il robot è in movimento quando viene emessa la query, il valore riportato include i movimenti memorizzati nel buffer del microcontrollore, ma non include i movimenti sulla coda di previsione. Si possono usare le chiamate toolhead.flush_step_generation() o toolhead.wait_moves() per svuotare completamente il codice look-ahead e generazione di passaggi.

La posizione "cinematica" (kin.calc_position()) è la posizione cartesiana della testa di stampa come derivata dalle posizioni "stepper" ed è relativa al sistema di coordinate specificato nel file di configurazione. Questo può differire dalla posizione cartesiana richiesta a causa della granularità dei motori passo-passo. Se il robot è in movimento quando vengono prese le posizioni "stepper", il valore riportato include i movimenti memorizzati nel buffer del microcontrollore, ma non include i movimenti sulla coda di previsione. Si possono usare le chiamate toolhead.flush_step_generation() o toolhead.wait_moves() per svuotare completamente il codice look-ahead e generazione di passaggi.

La posizione della "testa di stampa" (toolhead.get_position()) è l'ultima posizione richiesta della testa di stampa in coordinate cartesiane rispetto al sistema di coordinate specificato nel file di configurazione. Se il robot è in movimento quando viene emessa la richiesta, il valore riportato include tutti i movimenti richiesti (anche quelli nei buffer in attesa di essere inviati ai driver del motore passo-passo).

La posizione "gcode" è l'ultima posizione richiesta da un comando G1 (o G0) in coordinate cartesiane relative al sistema di coordinate specificato nel file di configurazione. Questo può differire dalla posizione "toolhead" se è attiva una trasformazione del g-code (ad es. bed_mesh, bed_tilt, skew_correction). Questo può differire dalle coordinate effettive specificate nell'ultimo comando G1 se l'origine del g-code è stata modificata (ad esempio, G92, SET_GCODE_OFFSET, M221). Il comando M114 (gcode_move.get_status()['gcode_position']) riporterà l'ultima posizione del g-code rispetto al sistema di coordinate del g-code corrente.

La "gcode base" è la posizione dell'origine del codice g in coordinate cartesiane rispetto al sistema di coordinate specificato nel file di configurazione. Comandi come G92, SET_GCODE_OFFSET e M221 alterano questo valore.

Il "gcode homing" è la posizione da usare per l'origine del g-code (in coordinate cartesiane relative al sistema di coordinate specificato nel file di configurazione) dopo un comando home G28. Il comando SET_GCODE_OFFSET può alterare questo valore.

Time

Fondamentale per il funzionamento di Klipper è la gestione di orologi, orari e timestamp. Klipper esegue azioni sulla stampante programmando eventi che si verificheranno nel prossimo futuro. Ad esempio, per accendere una ventola, il codice potrebbe programmare una modifica a un pin GPIO in 100 ms. È raro che il codice tenti di eseguire un'azione istantanea. Pertanto, la gestione del tempo all'interno di Klipper è fondamentale per il corretto funzionamento.

Esistono tre tipi di tempi tracciati internamente nel software host di Klipper:

  • Ora di sistema. L'ora del sistema utilizza l'orologio del sistema: è un numero in virgola mobile memorizzato come secondi ed è (generalmente) relativo all'ultimo avvio del computer host. I tempi di sistema hanno un uso limitato nel software: vengono utilizzati principalmente durante l'interazione con il sistema operativo. All'interno del codice host, gli orari di sistema sono spesso archiviati in variabili denominate eventtime o curtime.
  • Tempo di stampa. Il tempo di stampa è sincronizzato con l'orologio principale del microcontrollore (il microcontrollore definito nella sezione di configurazione "[mcu]"). È un numero in virgola mobile memorizzato come secondi ed è relativo all'ultimo riavvio dell'mcu principale. È possibile convertire da un "tempo di stampa" all'orologio hardware del microcontrollore principale moltiplicando il tempo di stampa per la frequenza di frequenza configurata staticamente dell'mcu. Il codice host di alto livello utilizza i tempi di stampa per calcolare quasi tutte le azioni fisiche (ad es. movimento della testa, modifiche del riscaldatore, ecc.). All'interno del codice host, i tempi di stampa sono generalmente memorizzati in variabili denominate print_time o move_time.
  • Orologio MCU. Questo è il contatore dell'orologio hardware su ogni microcontrollore. Viene memorizzato come numero intero e la sua velocità di aggiornamento è relativa alla frequenza del microcontrollore specificato. Il software host traduce i suoi tempi interni in orologi prima della trasmissione all'mcu. Il codice mcu tiene traccia del tempo solo in tick dell'orologio. All'interno del codice host, i valori di clock vengono tracciati come interi a 64 bit, mentre il codice mcu utilizza interi a 32 bit. All'interno del codice host, gli orologi sono generalmente memorizzati in variabili con nomi contenenti clock o tick.

La conversione tra i diversi formati dell'ora è implementata principalmente nel codice klippy/clocksync.py.

Alcune cose da tenere presenti durante la revisione del codice:

  • Orologi a 32 bit e 64 bit: per ridurre la larghezza di banda e migliorare l'efficienza del microcontrollore, gli orologi sul microcontrollore vengono tracciati come numeri interi a 32 bit. Quando si confrontano due orologi nel codice mcu, la funzione timer_is_before() deve essere sempre utilizzata per garantire che i rollover interi siano gestiti correttamente. Il software host converte gli orologi a 32 bit in orologi a 64 bit aggiungendo i bit di ordine superiore dall'ultimo timestamp mcu che ha ricevuto - nessun messaggio dall'mcu è mai più di 2^31 tick di clock in futuro o nel passato, quindi questa conversione non è mai ambigua . L'host converte da clock a 64 bit a clock a 32 bit semplicemente troncando i bit di ordine superiore. Per garantire che non vi siano ambiguità in questa conversione, il codice klippy/chelper/serialqueue.c memorizza i messaggi nel buffer finché non si trovano entro 2^31 tick di clock dall'ora target.
  • Microcontrollori multipli: il software host supporta l'utilizzo di più microcontrollori su una singola stampante. In questo caso, il "clock MCU" di ogni microcontrollore viene tracciato separatamente. Il codice clocksync.py gestisce la deriva dell'orologio tra i microcontrollori modificando il modo in cui converte da "tempo di stampa" a "orologio MCU". Sul mcus secondario, la frequenza mcu utilizzata in questa conversione viene regolarmente aggiornata per tenere conto della deriva misurata.
Torna su